Skip to content

Commit

Permalink
feat!: retrieve the correct icon dimensions
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The internal API for [_TrayIconInteraction](https://github.com/benthillerkus/betrayal/blob/b83703b7c56095ee2eca74a7e53436fa773789fd/lib/src/interaction.dart#L19-L20) changed by introducing the new [rawEvent].
  • Loading branch information
benthillerkus committed Apr 14, 2022
1 parent b83703b commit e8fed3b
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 28 deletions.
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@ Widget build(BuildContext context) => MaterialApp(
Please refer to the [example subdirectory](https://github.com/benthillerkus/betrayal/tree/main/example) for more [information](https://github.com/benthillerkus/betrayal/blob/main/example/README.md) and code.

# Development
## TBD before v1

- TODO Find out, communicate and memoize the correct system metrics (icon resolution)

## Style

Expand Down
14 changes: 10 additions & 4 deletions lib/src/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,20 @@ Continuing under the assumption that only the file extension is wrong""");

/// A [TrayIconImageDelegate] that uses an RGBA image buffer.
///
/// The buffer is expected to be in the format of a 32x32 RGBA image.
/// {@template betrayal.image.pixels}
/// The buffer is expected to be in the format of an RGBA image
/// where the size equals [TrayIcon.preferredImageSize].
/// {@endtemplate}
TrayIconImageDelegate.fromBytes(ByteBuffer pixels) {
if (pixels.lengthInBytes != 32 * 32 * 4) {
final size = TrayIcon.preferredImageSize;
int x = size.width.toInt();
int y = size.height.toInt();
if (pixels.lengthInBytes != x * y * 4) {
throw ArgumentError(
"The buffer must be 32x32 pixels and 4 bytes per pixel.");
"The buffer must have $x×$y pixels and 4 bytes per pixel.");
}
setIcon = (id, plugin) =>
plugin.setImageFromPixels(id, 32, 32, pixels.asInt32List());
plugin.setImageFromPixels(id, x, y, pixels.asInt32List());
}

/// A [TrayIconImageDelegate] that uses no image and will remove existing images.
Expand Down
17 changes: 17 additions & 0 deletions lib/src/imperative.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ class TrayIcon {
/// This is deferred until usage, because constructors can't be `async`.
bool _isReal = false;

/// The size an icon image set via [setImage] should be.
static Size get preferredImageSize => BetrayalPlugin.preferredImageSize;

/// The size a large icon, for example for a notification bubble
/// should be.
///
/// For a tray icon, use [preferredImageSize].
static Size get preferredLargeImageSize =>
BetrayalPlugin.preferredLargeImageSize;

/// Creates a new [TrayIcon] that controls a single icon in the system tray.
TrayIcon() : _id = _newId() {
_logger = Logger("betrayal.icon.${_id.hex}");
Expand Down Expand Up @@ -181,6 +191,13 @@ class TrayIcon {
/// 5. [winIcon]
///
/// If no argument is passed, the image is removed.
///
/// {@template betrayal.icon.image_parameters}
/// For [pixels], you should note that
/// {@macro betrayal.image.pixels}
///
/// For more information on the parameters, check out [TrayIconImageDelegate].
/// {@endtemplate}
Future<void> setImage({
TrayIconImageDelegate? delegate,
Uri? path,
Expand Down
27 changes: 22 additions & 5 deletions lib/src/interaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,36 @@ part of 'plugin.dart';

/// A data class to store events fired by native code.
///
/// Interactions / events are identifer by [id] + [event] + [hWnd].
/// Interactions / events are identified by [id] + [event] + [hWnd].
///
/// It might be useful to expose more of these
/// If the user has set their mouse buttons to be inverted,
/// [rawEvent] will be the event that was *actually* fired,
/// and [event] will be the semantically correct one.
///
/// {@template betrayal.expose_more_members_on_interaction}
/// If you need access to more data than the [_TrayIconInteraction.position], please [file an issue](https://github.com/benthillerkus/betrayal/issues/new).
/// {@endtemplate}
@immutable
class _TrayIconInteraction {
final Offset position;
final int hWnd;
final WinEvent event;
final WinEvent rawEvent;
final Id id;

_TrayIconInteraction(this.event, this.position, this.id, this.hWnd);
const _TrayIconInteraction(
{required this.event,
required this.rawEvent,
required this.position,
required this.id,
required this.hWnd});

@override
String toString() {
if (rawEvent != event) {
return "${rawEvent.name} (interpreted as ${event.name}) at $position";
}

return "${event.name} at $position";
}
}
Expand All @@ -25,14 +42,14 @@ typedef _EventCallback = void Function(Offset position)?;
///
/// Relies on the [TrayIcon.__callbacks] map to store the callbacks.
extension InteractionHandling on TrayIcon {
/// {@template interaction_handling.set}
/// {@template betrayal.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.
/// {@macro betrayal.expose_more_members_on_interaction}
///
/// It would be possible to also expose the [hWnd] and [WinEvent] as well (See [_TrayIconInteraction]).
/// {@endtemplate}
Expand Down
83 changes: 76 additions & 7 deletions lib/src/plugin.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/// This library is not part of the public API, but feel free to use it.
/// However, note that trying to interfere with icons managed by a [TrayIcon]´
/// will break stuff.
import 'dart:math';
import 'dart:typed_data';

Expand All @@ -13,8 +12,8 @@ import 'package:logging/logging.dart';

import 'image.dart';
import 'stock_icon.dart';
import 'win_icon.dart';
import 'win_event.dart';
import 'win_icon.dart';

part 'imperative.dart';
part 'interaction.dart';
Expand Down Expand Up @@ -50,6 +49,48 @@ class BetrayalPlugin {
final _logger = Logger('betrayal.plugin');
final _nativeLogger = Logger('betrayal.native');

/// {@template betrayal.preferredImageSize}
/// The small icon size
///
/// See: [docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics#:~:text=same%20as%20SM_CXFRAME.-,SM_CXSMICON,-49)
///
/// Used by [TrayIcon.preferredImageSize].
///
/// Set through [BetrayalPlugin._updateSystemMetrics].
/// {@endtemplate}
static Size _preferredImageSize = const Size(16, 16);

/// {@macro betrayal.preferredLargeImageSize}
static Size get preferredImageSize => _preferredImageSize;

/// {@template betrayal.preferredLargeImageSize}
/// The standard icon size
///
/// See: [docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics#:~:text=bar%2C%20in%20pixels.-,SM_CXICON,-11)
///
/// Used by [TrayIcon.preferredLargeImageSize].
///
/// Set through [BetrayalPlugin._updateSystemMetrics].
/// {@endtemplate}
static Size _preferredLargeImageSize = const Size(32, 32);

/// {@macro betrayal.preferredLargeImageSize}
static Size get preferredLargeImageSize => _preferredImageSize;

/// {@template betrayal.primaryAndSecondarySwapped}
/// If the user has inverted their mouse buttons.
///
/// Windows will automatically normalize inputs, but it still might be
/// interesting what button actually was pressed / be able to prompt
/// the user to press the correct button.
///
/// Set through [BetrayalPlugin._updateSystemMetrics].
/// {@endtemplate}
static bool _primaryAndSecondarySwapped = false;

/// {@macro betrayal.primaryAndSecondarySwapped}
static bool get primaryAndSecondarySwapped => _primaryAndSecondarySwapped;

/// The singleton constructor.
///
/// Once it is invoked, it will try to clear up any icons registered
Expand All @@ -62,7 +103,9 @@ class BetrayalPlugin {
WidgetsFlutterBinding.ensureInitialized();

_channel.setMethodCallHandler(_handleMethod);
_updateSystemMetrics();
reset();

_logger.info('connection initialized');
}

Expand All @@ -81,26 +124,52 @@ class BetrayalPlugin {
final int hWnd = args["hWnd"];
final Offset position =
Offset(args["x"].toDouble(), args["y"].toDouble());
final int event = args["event"];
final int code = args["event"];
final Id id = args["id"];

try {
if (event == WinEvent.mouseFirst.code && id == 0) {
if (code == WinEvent.mouseFirst.code && id == 0) {
final icon = TrayIcon._allIcons[message - 0x0400]!;
icon._logger.info("added to tray at $position");
} else {
final icon = TrayIcon._allIcons[id]!;
icon._handleInteraction(
_TrayIconInteraction(fromCode(event), position, id, hWnd));
final event = fromCode(code);
final eventRaw =
primaryAndSecondarySwapped ? event.inverted : event;
icon._handleInteraction(_TrayIconInteraction(
event: event,
rawEvent: eventRaw,
position: position,
id: id,
hWnd: hWnd));
}
} on Error {
_logger.warning(
"message: 10b$message ${message.hex}, id: ${id.hex}, event: ${event.toRadixString(16)}, position: $position}, hWnd: $hWnd");
"message: 10b$message ${message.hex}, id: ${id.hex}, event: ${code.toRadixString(16)}, position: $position}, hWnd: $hWnd");
}
break;
}
}

/// Updates [preferredImageSize], [preferredLargeImageSize] and [primaryAndSecondarySwapped]
@protected
Future<void> _updateSystemMetrics() async {
final metrics =
await _channel.invokeMapMethod<String, dynamic>('getSystemMetrics');

_preferredImageSize = Size(metrics!["preferredImageSizeX"].toDouble(),
metrics["preferredImageSizeY"].toDouble());

_preferredLargeImageSize = Size(
metrics["preferredLargeImageSizeX"].toDouble(),
metrics["preferredLargeImageSizeY"].toDouble());

_primaryAndSecondarySwapped = metrics["primaryAndSecondarySwapped"] != 0;

_logger.fine(
"preferredImageSize: $preferredImageSize preferredLargeImageSize $preferredLargeImageSize primaryAndSecondarySwapped: $primaryAndSecondarySwapped");
}

/// Asks the plugin to call [_handleMethod] whenever [id] + [event] happens.
///
/// Effectively, this means that events will per default *not* clog up the `MethodChannel`.
Expand Down
2 changes: 2 additions & 0 deletions lib/src/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ class TrayIconWidget extends StatefulWidget {
/// are `null`, the current values are kept. This is to ensure that rebuilds
/// don't interfere with using the [TrayIcon] widget directly through.
/// [TrayIcon.of(BuildContext context)].
///
/// {@macro betrayal.icon.image_parameters}
TrayIconWidget(
{Key? key,
required this.child,
Expand Down
20 changes: 20 additions & 0 deletions lib/src/win_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,24 @@ extension EventCodes on WinEvent {
return 0x0209;
}
}

/// Get the opposite event (left = right, right = left)
WinEvent get inverted {
switch (this) {
case WinEvent.leftButtonUp:
return WinEvent.rightButtonUp;
case WinEvent.leftButtonDown:
return WinEvent.rightButtonDown;
case WinEvent.leftButtonDoubleClick:
return WinEvent.rightButtonDoubleClick;
case WinEvent.rightButtonUp:
return WinEvent.leftButtonUp;
case WinEvent.rightButtonDown:
return WinEvent.leftButtonDown;
case WinEvent.rightButtonDoubleClick:
return WinEvent.leftButtonDoubleClick;
default:
return this;
}
}
}
29 changes: 20 additions & 9 deletions windows/betrayal_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,17 @@ namespace Betrayal
auto x = GET_X_LPARAM(wParam);
auto y = GET_Y_LPARAM(wParam);

try {
auto icon = icons.get(hWnd, id);
if (icon != nullptr && icon->is_subscribed(event))
{
invokeInteraction(hWnd, message, event, id, x, y);
return result;
}
try
{
auto icon = icons.get(hWnd, id);
if (icon != nullptr && icon->is_subscribed(event))
{
invokeInteraction(hWnd, message, event, id, x, y);
return result;
}
}
catch (const std::out_of_range&) {

catch (const std::out_of_range &)
{
}
if (event == WM_CONTEXTMENU)
{
Expand Down Expand Up @@ -115,6 +116,16 @@ namespace Betrayal
{
icons.clear_all();
}
else if (method.compare("getSystemMetrics") == 0)
{
result->Success(flutter::EncodableMap({
{flutter::EncodableValue("preferredImageSizeX"), flutter::EncodableValue(::GetSystemMetrics(SM_CXSMICON))},
{flutter::EncodableValue("preferredImageSizeY"), flutter::EncodableValue(::GetSystemMetrics(SM_CYSMICON))},
{flutter::EncodableValue("preferredLargeImageSizeX"), flutter::EncodableValue(::GetSystemMetrics(SM_CXICON))},
{flutter::EncodableValue("preferredLargeImageSizeY"), flutter::EncodableValue(::GetSystemMetrics(SM_CYICON))},
{flutter::EncodableValue("primaryAndSecondarySwapped"), flutter::EncodableValue(::GetSystemMetrics(SM_SWAPBUTTON))},
}));
}
else if (method.compare("subscribeTo") == 0)
{
WITH_ARGS_HWND_ID
Expand Down

0 comments on commit e8fed3b

Please sign in to comment.