Skip to content

Commit

Permalink
feat: make logging customizable
Browse files Browse the repository at this point in the history
  • Loading branch information
benthillerkus committed Apr 9, 2022
1 parent e39bef1 commit 439909a
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 42 deletions.
15 changes: 15 additions & 0 deletions example/README.md
Expand Up @@ -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).
4 changes: 3 additions & 1 deletion example/widgets_api/README.md
Expand Up @@ -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)`
- …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
4 changes: 3 additions & 1 deletion example/widgets_api/lib/main.dart
Expand Up @@ -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());
}

Expand Down
3 changes: 2 additions & 1 deletion 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';
20 changes: 6 additions & 14 deletions 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.
///
Expand Down Expand Up @@ -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 =
Expand Down
111 changes: 111 additions & 0 deletions 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<LogRecord>? _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);
}
}
40 changes: 30 additions & 10 deletions lib/src/plugin.dart
Expand Up @@ -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.
Expand All @@ -24,41 +34,51 @@ 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
WidgetsFlutterBinding.ensureInitialized();

_channel.setMethodCallHandler(_handleMethod);
reset();
_logger.info('BetrayalPlugin initialized!');
_logger.info('connection initialized');
}

Future<dynamic> _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;
}
}
Expand Down
4 changes: 2 additions & 2 deletions 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.
///
Expand Down Expand Up @@ -120,7 +120,7 @@ class _TrayIconWidgetState extends State<TrayIconWidget> {
@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;
Expand Down
94 changes: 94 additions & 0 deletions 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;
}
}
}

0 comments on commit 439909a

Please sign in to comment.