Skip to content

Commit

Permalink
feat: introduce support for interactions
Browse files Browse the repository at this point in the history
  • Loading branch information
benthillerkus committed Apr 12, 2022
1 parent 0242703 commit f927a35
Show file tree
Hide file tree
Showing 7 changed files with 326 additions and 85 deletions.
2 changes: 0 additions & 2 deletions README.md
Expand Up @@ -59,9 +59,7 @@ Please refer to the [example subdirectory](https://github.com/benthillerkus/betr
# Development
## TBD before v1

- FIXME Find out all possible errors and repackage / handle them
- TODO Find out, communicate and memoize the correct system metrics (icon resolution)
- TODO Support interaction

## Style

Expand Down
2 changes: 2 additions & 0 deletions lib/src/imperative.dart
Expand Up @@ -63,6 +63,8 @@ class TrayIcon {
if (kDebugMode) _plugin._noop();
}

final __callbacks = <WinEvent, _EventCallback>{};

@override
String toString() =>
"TrayIcon(${_id.hex}, active: $_isActive, visible: $_isVisible)";
Expand Down
154 changes: 154 additions & 0 deletions lib/src/interaction.dart
@@ -0,0 +1,154 @@
part of 'plugin.dart';

/// A data class to store events fired by native code.
///
/// Interactions / events are identifer by [id] + [event] + [hWnd].
///
/// It might be useful to expose more of these
class _TrayIconInteraction {
final Offset position;
final int hWnd;
final WinEvent event;
final Id id;

_TrayIconInteraction(this.event, this.position, this.id, this.hWnd);

@override
String toString() {
return "${event.name} at $position";
}
}

typedef _EventCallback = void Function(Offset position)?;

/// Gives the [TrayIcon] the ability to react to [WinEvent]s.
///
/// Relies on the [TrayIcon.__callbacks] map to store the callbacks.
extension InteractionHandling on TrayIcon {
/// {@template interaction_handling.set}
/// Register this callback to be called, whenever the associated [WinEvent] is fired.
///
/// If `null` is passed, the callback will be removed.
/// This should mean better performance as the native code
/// won't have to call into dart for this even anymore.
///
/// If you need access to more data than the position, please file an issue.
///
/// It would be possible to also expose the [hWnd] and [WinEvent] as well (See [_TrayIconInteraction]).
/// {@endtemplate}
set onTap(_EventCallback onTap) =>
_manageEventSubscription(WinEvent.select, onTap);

/// {@template betrayal.interaction_handling.get}
/// The callback that is currently being run `onTap`.
///
/// If `null`, this [WinEvent] can be skipped.
/// {@endtemplate}
_EventCallback get onTap => _callbacks[WinEvent.select];

/// {@macro betrayal.interaction_handling.set}
set onSecondaryTap(_EventCallback onSecondaryTap) =>
_manageEventSubscription(WinEvent.contextMenu, onSecondaryTap);

/// {@macro betrayal.interaction_handling.get}
_EventCallback get onSecondaryTap => _callbacks[WinEvent.contextMenu];

/// {@macro betrayal.interaction_handling.set}
set onTapDown(_EventCallback onTapDown) =>
_manageEventSubscription(WinEvent.leftButtonDown, onTapDown);

/// {@macro betrayal.interaction_handling.get}
_EventCallback get onTapDown => _callbacks[WinEvent.leftButtonDown];

/// {@macro betrayal.interaction_handling.set}
set onSecondaryTapDown(_EventCallback onSecondaryTapDown) =>
_manageEventSubscription(WinEvent.rightButtonDown, onSecondaryTapDown);

/// {@macro betrayal.interaction_handling.get}
_EventCallback get onSecondaryTapDown => _callbacks[WinEvent.rightButtonDown];

/// {@macro betrayal.interaction_handling.set}
set onTertiaryTapDown(_EventCallback onTertiaryTapDown) =>
_manageEventSubscription(WinEvent.middleButtonDown, onTertiaryTapDown);

/// {@macro betrayal.interaction_handling.get}
_EventCallback get onTertiaryTapDown => _callbacks[WinEvent.middleButtonDown];

/// {@macro betrayal.interaction_handling.set}
set onTapUp(_EventCallback onTapUp) =>
_manageEventSubscription(WinEvent.leftButtonUp, onTapUp);

/// {@macro betrayal.interaction_handling.get}
_EventCallback get onTapUp => _callbacks[WinEvent.leftButtonUp];

/// {@macro betrayal.interaction_handling.set}
set onSecondaryTapUp(_EventCallback onSecondaryTapUp) =>
_manageEventSubscription(WinEvent.rightButtonUp, onSecondaryTapUp);

/// {@macro betrayal.interaction_handling.get}
_EventCallback get onSecondaryTapUp => _callbacks[WinEvent.rightButtonUp];

/// {@macro betrayal.interaction_handling.set}
set onTertiaryTapUp(_EventCallback onTertiaryTapUp) =>
_manageEventSubscription(WinEvent.middleButtonUp, onTertiaryTapUp);

/// {@macro betrayal.interaction_handling.get}
_EventCallback get onTertiaryTapUp => _callbacks[WinEvent.middleButtonUp];

/// {@macro betrayal.interaction_handling.set}
set onDoubleTap(_EventCallback onDoubleTap) =>
_manageEventSubscription(WinEvent.select, onDoubleTap);

/// {@macro betrayal.interaction_handling.get}
_EventCallback get onDoubleTap => _callbacks[WinEvent.select];

/// {@macro betrayal.interaction_handling.set}
set onSecondaryDoubleTap(_EventCallback onSecondaryDoubleTap) =>
_manageEventSubscription(
WinEvent.rightButtonDoubleClick, onSecondaryDoubleTap);

/// {@macro betrayal.interaction_handling.get}
_EventCallback get onSecondaryDoubleTap =>
_callbacks[WinEvent.rightButtonDoubleClick];

/// {@macro betrayal.interaction_handling.set}
set onTertiaryDoubleTap(_EventCallback onTertiaryDoubleTap) =>
_manageEventSubscription(
WinEvent.middleButtonDoubleClick, onSecondaryDoubleTap);

/// {@macro betrayal.interaction_handling.get}
_EventCallback get onTertiaryDoubleTap =>
_callbacks[WinEvent.middleButtonDoubleClick];

/// {@macro betrayal.interaction_handling.set}
set onPointerMove(_EventCallback onPointerMove) =>
_manageEventSubscription(WinEvent.mouseMove, onPointerMove);

/// {@macro betrayal.interaction_handling.get}
_EventCallback get onPointerMove => _callbacks[WinEvent.mouseMove];

set _callbacks(Map<WinEvent, _EventCallback> _callbacks) {
_callbacks.forEach(_manageEventSubscription);
}

Map<WinEvent, _EventCallback> get _callbacks => __callbacks;

void _manageEventSubscription(WinEvent event, _EventCallback callback) {
final oldCallback = __callbacks[event];
if (callback == oldCallback) return;
if (callback != null && oldCallback == null) {
_logger.fine("subscribing to ${event.name}");
TrayIcon._plugin.subscribeTo(_id, event);
} else if (callback == null && oldCallback != null) {
_logger.fine("unsubscribing from ${event.name}");
TrayIcon._plugin.unsubscribeFrom(_id, event);
}
__callbacks[event] = callback;
return;
}

void _handleInteraction(_TrayIconInteraction interaction) {
_logger.finer("received interaction: $interaction");
_callbacks[interaction.event]?.call(interaction.position);
}
}
25 changes: 23 additions & 2 deletions lib/src/plugin.dart
Expand Up @@ -17,6 +17,7 @@ import 'win_icon.dart';
import 'win_event.dart';

part 'imperative.dart';
part 'interaction.dart';
part 'widgets.dart';

/// An identifier.
Expand Down Expand Up @@ -88,9 +89,9 @@ class BetrayalPlugin {
final icon = TrayIcon._allIcons[message - 0x0400]!;
icon._logger.info("added to tray at $position");
} else {
final action = fromCode(event);
final icon = TrayIcon._allIcons[id]!;
icon._logger.fine("${action.name} at $position}");
icon._handleInteraction(
_TrayIconInteraction(fromCode(event), position, id, hWnd));
}
} on Error {
_logger.warning(
Expand All @@ -100,6 +101,26 @@ class BetrayalPlugin {
}
}

/// Asks the plugin to call [_handleMethod] whenever [id] + [event] happens.
///
/// Effectively, this means that events will per default *not* clog up the `MethodChannel`.
@protected
Future<void> subscribeTo(Id id, WinEvent event) async {
await _channel.invokeMethod("subscribeTo", {
"id": id,
"event": event.code,
});
}

/// Notifies the plugin that events that the pattern [id] + [event] can be ignored.
@protected
Future<void> unsubscribeFrom(Id id, WinEvent event) async {
await _channel.invokeMethod("unsubscribeFrom", {
"id": id,
"event": event.code,
});
}

/// Removes and cleans up any icon managed by the plugin.
@protected
Future<void> reset() async {
Expand Down
31 changes: 31 additions & 0 deletions lib/src/widgets.dart
Expand Up @@ -40,6 +40,8 @@ class TrayIconWidget extends StatefulWidget {

late final TrayIconImageDelegate? _delegate;

late final Map<WinEvent, _EventCallback> _callbacks;

/// Manages a [TrayIcon] as a [StatefulWidget].
///
/// The underlying resource will automatically be disposed according to the
Expand All @@ -57,6 +59,18 @@ class TrayIconWidget extends StatefulWidget {
this.visible = true,
this.addToContext = true,
this.tooltip,
_EventCallback onTap,
_EventCallback onSecondaryTap,
_EventCallback onTapDown,
_EventCallback onSecondaryTapDown,
_EventCallback onTertiaryTapDown,
_EventCallback onTapUp,
_EventCallback onSecondaryTapUp,
_EventCallback onTertiaryTapUp,
_EventCallback onDoubleTap,
_EventCallback onSecondaryDoubleTap,
_EventCallback onTertiaryDoubleTap,
_EventCallback onPointerMove,
TrayIconImageDelegate? imageDelegate,
ByteBuffer? imagePixels,
String? imageAsset,
Expand All @@ -79,6 +93,21 @@ class TrayIconWidget extends StatefulWidget {
} else {
_delegate = null;
}

_callbacks = {
WinEvent.select: onTap,
WinEvent.leftButtonDoubleClick: onDoubleTap,
WinEvent.leftButtonDown: onTapDown,
WinEvent.leftButtonUp: onTapUp,
WinEvent.contextMenu: onSecondaryTap,
WinEvent.rightButtonDown: onSecondaryTapDown,
WinEvent.rightButtonUp: onSecondaryTapUp,
WinEvent.rightButtonDoubleClick: onSecondaryDoubleTap,
WinEvent.middleButtonDown: onTertiaryTapDown,
WinEvent.middleButtonUp: onTertiaryTapUp,
WinEvent.middleButtonDoubleClick: onTertiaryDoubleTap,
WinEvent.mouseMove: onPointerMove,
};
}

@override
Expand Down Expand Up @@ -124,6 +153,7 @@ class _TrayIconWidgetState extends State<TrayIconWidget> {
void didUpdateWidget(covariant TrayIconWidget oldWidget) {
super.didUpdateWidget(oldWidget);
_logger.fine("updating icon…");
_icon._callbacks = widget._callbacks;
if (widget.visible != null && !widget.visible!) {
_icon.hide();
return;
Expand Down Expand Up @@ -152,5 +182,6 @@ class _TrayIconWidgetState extends State<TrayIconWidget> {
if (widget._delegate != null) _icon.setImage(delegate: widget._delegate);
if (widget.tooltip != null) _icon.setTooltip(widget.tooltip!);
if (widget.visible ?? false) _icon.show();
_icon._callbacks = widget._callbacks;
}
}

0 comments on commit f927a35

Please sign in to comment.