From 439909af1ef5862a379d1a77bfe23bf38d5ddbed Mon Sep 17 00:00:00 2001 From: Bent Hillerkus <29630575+benthillerkus@users.noreply.github.com> Date: Sun, 10 Apr 2022 00:47:36 +0200 Subject: [PATCH] feat: make logging customizable --- example/README.md | 15 ++++ example/widgets_api/README.md | 4 +- example/widgets_api/lib/main.dart | 4 +- lib/betrayal.dart | 3 +- lib/src/imperative.dart | 20 ++---- lib/src/logging.dart | 111 ++++++++++++++++++++++++++++++ lib/src/plugin.dart | 40 ++++++++--- lib/src/widgets.dart | 4 +- lib/src/win_event.dart | 94 +++++++++++++++++++++++++ lib/src/win_icon.dart | 7 +- windows/betrayal_plugin.cpp | 13 +--- 11 files changed, 273 insertions(+), 42 deletions(-) create mode 100644 lib/src/logging.dart create mode 100644 lib/src/win_event.dart diff --git a/example/README.md b/example/README.md index 057ad51..88921d3 100644 --- a/example/README.md +++ b/example/README.md @@ -78,3 +78,18 @@ Widget build(BuildContext context) { Check out the [edit_icon](edit_icon/README.md) example for a more complex app that uses the imperative api. If you want to see how *Betrayal* can work with multiple icons and how to generate images at runtime through `Canvas` please look at the [add_many](add_many/lib/main.dart) + +## Is there a way to get rid of the log messages? + +Yeah, you can change the log level, as seen in the [widgets_api](widgets_api/lib/main.dart) example. + +```dart +// sets `hierarchicalLoggingEnabled` +// from the `logging` package to `true` +BetrayalLogConfig.allowIndividualLevels(); + +// disables logging only for *betrayal* +BetrayalLogConfig.level = "OFF"; +``` + +To learn more about logging in *betrayal* have a look at the implementation in [logging.dart](../lib/src/logging.dart). \ No newline at end of file diff --git a/example/widgets_api/README.md b/example/widgets_api/README.md index 8707196..277e5e5 100644 --- a/example/widgets_api/README.md +++ b/example/widgets_api/README.md @@ -3,4 +3,6 @@ This example shows you how… - …to manage an icon just using widgets -- …to update the icon with `setState` and `TrayIcon.of(context)` \ No newline at end of file +- …to update the icon with `setState` and `TrayIcon.of(context)` +- …to make sure that all icons will be removed on hot restart +- …to change the log levels for the plugin diff --git a/example/widgets_api/lib/main.dart b/example/widgets_api/lib/main.dart index 21a9db7..0768a8f 100644 --- a/example/widgets_api/lib/main.dart +++ b/example/widgets_api/lib/main.dart @@ -12,7 +12,9 @@ void main() { // Now the app will immediately need a new [TrayIcon] // and the old one can be removed. if (kDebugMode) TrayIcon.clearAll(); - + // Configure plugin log levels + BetrayalLogConfig.allowIndividualLevels(); + BetrayalLogConfig.level = kDebugMode ? "FINE" : "INFO"; runApp(const MyApp()); } diff --git a/lib/betrayal.dart b/lib/betrayal.dart index 934a6b1..76c95fd 100644 --- a/lib/betrayal.dart +++ b/lib/betrayal.dart @@ -1,3 +1,4 @@ -export 'src/imperative.dart'; export 'src/win_icon.dart'; export 'src/image.dart'; +export 'src/plugin.dart'; +export 'src/logging.dart'; diff --git a/lib/src/imperative.dart b/lib/src/imperative.dart index 006656c..7ecf032 100644 --- a/lib/src/imperative.dart +++ b/lib/src/imperative.dart @@ -1,18 +1,6 @@ -// ignore_for_file: invalid_use_of_protected_member - -import 'dart:async'; -import 'dart:math'; -import 'dart:typed_data'; - -import 'package:betrayal/src/plugin.dart'; -import 'package:betrayal/src/image.dart'; -import 'package:betrayal/src/win_icon.dart'; +part of 'plugin.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter/foundation.dart'; -import 'package:logging/logging.dart'; - -part 'widgets.dart'; +// ignore_for_file: invalid_use_of_protected_member /// A system tray icon. /// @@ -75,6 +63,10 @@ class TrayIcon { if (kDebugMode) _plugin; } + @override + String toString() => + "TrayIcon($_id, hidden: $_isVisible, active: $_isActive)"; + /// Retrieve the [TrayIcon] managed by a [TrayIconWidget] further up the tree. static TrayIcon of(BuildContext context) { final TrayIcon? result = diff --git a/lib/src/logging.dart b/lib/src/logging.dart new file mode 100644 index 0000000..9b1757e --- /dev/null +++ b/lib/src/logging.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:logging/logging.dart'; +import 'dart:developer'; + +/// Manager for logging in the *betrayal* package. +/// +/// All of the functions and fields are static. +/// However, since static stuff is lazy in Dart, +/// [BetrayalPlugin] creates a singleton instance +/// to initialize everything. +class BetrayalLogConfig { + /// Retrieves the singleton instance. + factory BetrayalLogConfig() => _instance; + + static final _instance = BetrayalLogConfig._internal(); + + static final Logger _logger = Logger("betrayal"); + + BetrayalLogConfig._internal() { + onRecord = (LogRecord record) { + log(record.message, + level: record.level.value, + name: record.loggerName, + sequenceNumber: record.sequenceNumber, + time: record.time, + error: record.error, + stackTrace: record.stackTrace, + zone: record.zone); + }; + _logger.info("ready to log!"); + } + + static Level _level = Logger.root.level; + + /// Allows individual loggers to have their own level. + /// + /// In this case, you could use it to make *betrayal* shut up, for example. + /// ```dart + /// BetrayalLogConfig.allowIndividualLevels(); + /// BetrayalLogConfig().setLevel(Level.OFF); + /// ``` + /// + /// This function is implemented as + /// ``` + /// static void allowIndividualLevels() => hierarchicalLoggingEnabled = true; + /// ``` + /// so unfortunately, it's changing a global variable from the `logging` package. + static void allowIndividualLevels() => hierarchicalLoggingEnabled = true; + + /// Use this to change the logging level `betrayal` scope. + /// + /// This is only possible, if `hierarchicalLoggingEnabled == true`. + /// + /// If the global isn't in scope, + /// you can call [BetrayalLogConfig.allowIndividualLevels] instead. + /// + /// Valid input types are [Level], [String] and [num]. + /// + /// | String | num | + /// |-------|----| + /// | `ALL` | 0 | + /// | `FINEST` | 300 | + /// | `FINER` | 400 | + /// | `FINE` | 500 | + /// | `CONFIG` | 700 | + /// | `INFO` | 800 | + /// | `WARNING` | 900 | + /// | `SEVERE` | 1000 | + /// | `SHOUT` | 1200 | + /// | `OFF` | 2000 | + static set level(dynamic level) { + if (level is Level) { + _level = level; + } else if (level is String) { + level = level.toUpperCase(); + _level = Level.LEVELS.firstWhere( + (element) => element.name == level, + orElse: () => + throw ArgumentError.value(level, "level", "not a valid Level!"), + ); + } else if (level is num) { + // This works assuming that the order will not be changed + // from lowest value (ALL) to highest (OFF) in the future. + _level = Level.LEVELS.lastWhere((element) => element.value <= level); + } else { + throw ArgumentError( + "level must be a Level, String or num, not ${level.runtimeType}!"); + } + _logger.level = _level; + _logger.info("logging level changed to ${_level.name}"); + } + + /// The current logging level for the `betrayal` scope. + static Level get level => _level; + + // ignore: prefer_function_declarations_over_variables + static late void Function(LogRecord record) _onRecord; + + static StreamSubscription? _subscription; + + /// Handles log records. + /// + /// Per default they will be send to `developer.log`, + /// which is the default logging facility in Dart. + static set onRecord(void Function(LogRecord record) onRecord) { + _onRecord = onRecord; + _subscription?.cancel(); + _subscription = _logger.onRecord.listen(_onRecord); + } +} diff --git a/lib/src/plugin.dart b/lib/src/plugin.dart index c2d27fa..1ee8637 100644 --- a/lib/src/plugin.dart +++ b/lib/src/plugin.dart @@ -2,12 +2,22 @@ /// However, note that trying to interfere with icons managed by a [TrayIcon]´ /// will break stuff. +import 'dart:math'; import 'dart:typed_data'; +import 'package:betrayal/src/logging.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:logging/logging.dart'; +import 'image.dart'; +import 'win_icon.dart'; +import 'win_event.dart'; + +part 'imperative.dart'; +part 'widgets.dart'; + /// An identifier. /// /// The Windows api also supports UUIDs, but for now ints are fine. @@ -24,18 +34,20 @@ typedef Id = int; class BetrayalPlugin { static final BetrayalPlugin _instance = BetrayalPlugin._internal(); - /// Creates the singleton instance. + /// Retrieves the singleton instance. factory BetrayalPlugin() => _instance; static const MethodChannel _channel = MethodChannel('betrayal'); - final Logger _logger = Logger('betrayal.plugin'); + final _logger = Logger('betrayal.plugin'); + final _nativeLogger = Logger('betrayal.native'); /// The singleton constructor. /// /// Once it is invoked, it will try to clear up any icons registered /// with the plugin. BetrayalPlugin._internal() { + BetrayalLogConfig(); _logger.finer('initializing plugin...'); // This makes sure the plugin can be invoked // before `runApp` is called in main @@ -43,22 +55,30 @@ class BetrayalPlugin { _channel.setMethodCallHandler(_handleMethod); reset(); - _logger.info('BetrayalPlugin initialized!'); + _logger.info('connection initialized'); } Future _handleMethod(MethodCall methodCall) async { switch (methodCall.method) { case "print": - print(methodCall.arguments); + _nativeLogger.info(methodCall.arguments); break; case "logWindowProc": final args = methodCall.arguments; - final message = args["message"]; - final wParam = args["wParam"]; - final lParam = args["lParam"]; - final hWnd = args["hWnd"]; - print( - "message: $message, wParam: $wParam, lParam: $lParam, hWnd: $hWnd"); + final int message = args["message"]; + final int wParam = args["wParam"]; + final int lParam = args["lParam"]; + final int hWnd = args["hWnd"]; + + try { + final action = fromCode(lParam); + final icon = TrayIcon._allIcons[message - 0x400]!; + _logger.fine( + "${action.name} on $icon, wParam: ${wParam.toRadixString(16)}"); + } on Error { + _logger.info( + "message: 10b$message 0x${message.toRadixString(16)}, wParam: ${wParam.toRadixString(16)}, lParam: ${lParam.toRadixString(16)}, hWnd: $hWnd"); + } break; } } diff --git a/lib/src/widgets.dart b/lib/src/widgets.dart index 9368ff8..7af9103 100644 --- a/lib/src/widgets.dart +++ b/lib/src/widgets.dart @@ -1,4 +1,4 @@ -part of 'imperative.dart'; +part of 'plugin.dart'; /// A widget that can be used to add a [TrayIcon] to the system tray. /// @@ -120,7 +120,7 @@ class _TrayIconWidgetState extends State { @override void didUpdateWidget(covariant TrayIconWidget oldWidget) { super.didUpdateWidget(oldWidget); - _logger.info("updating icon…"); + _logger.fine("updating icon…"); if (widget.visible != null && !widget.visible!) { _icon.hide(); return; diff --git a/lib/src/win_event.dart b/lib/src/win_event.dart new file mode 100644 index 0000000..8b83132 --- /dev/null +++ b/lib/src/win_event.dart @@ -0,0 +1,94 @@ +import 'package:flutter/widgets.dart'; + +/// Represents constants in WinUser.h +enum WinEvent { + /// `0x200`, equivalent to `EVENT_SYSTEM_MOVESIZESTART` + mouseMove, + + /// `0x201`, equivalent to `onTapDown` + leftButtonDown, + + /// `0x202`, equivalent to `onTapUp` + leftButtonUp, + + /// `0x203`, equivalent to `onDoubleTap` + leftButtonDoubleClick, + + /// `0x204`, equivalent to `onSecondaryTapDown` + rightButtonDown, + + /// `0x205`, equivalent to `onSecondaryTapUp` + rightButtonUp, + + /// `0x206` + rightButtonDoubleClick, + + /// `0x207`, equivalent to `onTertiaryTapDown` + middleButtonDown, + + /// `0x207`, equivalent to `onTertiaryTapUp` + middleButtonUp, + + /// `0x207` + middleButtonDoubleClick, +} + +/// Returns the [WinEvent] for the given [code]. +@protected +WinEvent fromCode(int code) { + switch (code) { + case 0x200: + return WinEvent.mouseMove; + case 0x201: + return WinEvent.leftButtonDown; + case 0x202: + return WinEvent.leftButtonUp; + case 0x203: + return WinEvent.leftButtonDoubleClick; + case 0x204: + return WinEvent.rightButtonDown; + case 0x205: + return WinEvent.rightButtonUp; + case 0x206: + return WinEvent.rightButtonDoubleClick; + case 0x207: + return WinEvent.middleButtonDown; + case 0x208: + return WinEvent.middleButtonUp; + case 0x209: + return WinEvent.middleButtonDoubleClick; + } + throw ArgumentError.value(code, "code", "Unknown code"); +} + +/// This extension allows adding members to the [WinEvent] enum. +/// +/// In Dart 2.17 and later, this will be possible doable +/// without an extension. +extension EventCodes on WinEvent { + /// The internal constant used by Windows. + int get code { + switch (this) { + case WinEvent.mouseMove: + return 0x200; + case WinEvent.leftButtonDown: + return 0x201; + case WinEvent.leftButtonUp: + return 0x202; + case WinEvent.leftButtonDoubleClick: + return 0x203; + case WinEvent.rightButtonDown: + return 0x204; + case WinEvent.rightButtonUp: + return 0x205; + case WinEvent.rightButtonDoubleClick: + return 0x206; + case WinEvent.middleButtonDown: + return 0x207; + case WinEvent.middleButtonUp: + return 0x208; + case WinEvent.middleButtonDoubleClick: + return 0x209; + } + } +} diff --git a/lib/src/win_icon.dart b/lib/src/win_icon.dart index 0e6a706..ab1b421 100644 --- a/lib/src/win_icon.dart +++ b/lib/src/win_icon.dart @@ -4,20 +4,25 @@ enum WinIcon { /// 📰 An icon that looks similar to a program GUI application, + /// ❌ A red circle with a white cross inside error, + /// ❔ A blue circle with a white question mark inside question, + /// ⚠️ A yellow triangle with a white exclamation mark inside warning, + /// ❕ A blue circle with a white exclamation mark inside information, + /// 🛡 A shield with a yellow and blue square pattern shield, } /// This extension allows adding members to the [WinIcon] enum. -/// +/// /// In Dart 2.17 and later, this will be possible doable /// without an extension. extension IconCodes on WinIcon { diff --git a/windows/betrayal_plugin.cpp b/windows/betrayal_plugin.cpp index 8c80cab..b34f911 100644 --- a/windows/betrayal_plugin.cpp +++ b/windows/betrayal_plugin.cpp @@ -51,18 +51,7 @@ namespace Betrayal } else if (message >= WM_USER && message < WM_APP) { - switch (lParam) - { - case WM_LBUTTONUP: - Print("Left Click!"); - break; - case WM_RBUTTONUP: - Print("Right Click!"); - break; - default: - LogWindowProc(hWnd, message, wParam, lParam); - break; - } + LogWindowProc(hWnd, message, wParam, lParam); } return result;