/
imperative.dart
244 lines (217 loc) 路 7.34 KB
/
imperative.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
part of 'plugin.dart';
// ignore_for_file: invalid_use_of_protected_member
/// A system tray icon.
///
/// Call [show] to show the icon.
/// And call [dispose] once you don't need it anymore.
///
/// With [setTooltip], [setImage] and [hide] you can
/// change the appearance of the icon.
///
/// If an icons has been disposed,
/// [isActive] will return `false`.
/// And you cannot interact with it anymore.
/// Construct a new one.
///
/// To nuke all existing icons, use [TrayIcon.clearAll]
class TrayIcon {
static final BetrayalPlugin _plugin = BetrayalPlugin.instance;
static final Map<Id, TrayIcon> _allIcons = {};
static final Random _random = Random();
late final Logger _logger;
/// This [Id] is used by the operating system
/// to identify the icon and address it
/// when it has been clicked.
static Id _newId() {
Id temp = 0;
while (_allIcons.containsKey(temp)) {
temp = _random.nextInt(_kMaximumId - _kMinimumId);
}
return temp;
}
/// The id used by Windows to distinguish this icon.
final Id _id;
/// Is this [TrayIcon] currently visible?
bool get isVisible => _isVisible;
bool _isVisible = false;
/// Has this class been disposed?
bool get isActive => _isActive;
bool _isActive = false;
/// This [StackTrace] is created when [dispose] is called.
/// That way it's possible to debug
/// when a [TrayIcon] has been disposed of too early.
late StackTrace _disposedAt;
/// `true` if the icon has been constructed in native code.
/// 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.
///
/// If provided, [id] has to be between `0` and `4096`.
TrayIcon({Id? id}) : _id = id ?? _newId() {
if (_id < 0 || _id >= (_kMaximumId - _kMinimumId)) {
throw ArgumentError.value(id, "id",
"The Id needs to be in between 0 and ${_kMaximumId - _kMinimumId}");
}
_logger = Logger("betrayal.icon.${_id.hex}");
_allIcons[_id] = this;
_isActive = true;
_logger.fine("initialized instance");
// In Debug mode users can hot restart the app.
// [As of right now, there is no way to detect that.](https://github.com/flutter/flutter/issues/10437)
// That means we have to call an init method for cleanup.
// This method is automatically called, when the [BetrayalPlugin._instance]
// is constructed.
// This happens happens the first time [_plugin] is accessed.
if (kDebugMode) _plugin._noop();
}
final __callbacks = <WinEvent, EventCallback>{};
@override
String toString() =>
"TrayIcon(${_id.hex}, active: $_isActive, visible: $_isVisible)";
/// Retrieve the [TrayIcon] managed by a [TrayIconWidget] further up the tree.
static TrayIcon of(BuildContext context) {
final TrayIcon? result =
context.dependOnInheritedWidgetOfExactType<_TrayIconHeritage>()?.icon;
assert(result != null, 'No TrayIcon found in context');
result!._logger.fine("provided by `_TrayIconHeritage`");
return result;
}
/// Disposes all icons and clears up any residual icons.
///
/// Use this method to clean up after a hot restart,
/// if you aren't going to immediately create a new [TrayIcon].
///
/// ```dart
/// void main() {
/// // Hot restarts only happen in debug mode
/// if (kDebugMode) TrayIcon.clearAll();
/// runApp(const MyApp());
/// }
/// ```
static void clearAll() {
for (final TrayIcon icon in _allIcons.values) {
icon._isActive = false;
icon._logger.fine("disposed by `clearAll`");
}
_allIcons.clear();
_plugin.reset();
}
/// Disposes the icon.
///
/// This will permanently remove the icon from the system tray.
/// Resources in native code will be released and this instance
/// will become unusable.
///
/// Calling this method twice is a no-op.
Future<void> dispose() async {
_logger.finer("trying to dispose");
if (!_isActive) return;
_isActive = false;
if (_isReal) {
await _plugin.disposeTray(_id);
}
_allIcons.remove(_id);
_disposedAt = StackTrace.current;
_logger.fine("disposed", null, _disposedAt);
}
final _lock = Lock();
/// Ensures the icon has been constructed in native code.
///
/// This is deferred until usage, because constructors can't be `async`.
Future<void> _ensureIsReal() async {
await _lock.synchronized(() async {
if (!_isReal) {
await _plugin.addTray(_id);
_isReal = true;
_logger.fine("made real (created in native code).");
}
});
}
/// Tests if this instance has already been disposed of.
void _ensureIsActive() {
if (!_isActive) {
throw StateError(
'TrayIcon${_id.hex} is not active anymore.\n\nIt was disposed at:\n$_disposedAt\nCurrent StackTrace:');
}
}
/// Shows the icon in the system tray.
Future<void> show() async {
_ensureIsActive();
if (_isVisible) return;
await _ensureIsReal();
await _plugin.showIcon(_id);
_isVisible = true;
}
/// Hides the icon from the system tray.
///
/// Internally the icon is actually unregistered from the system entirely,
/// however this plugin still keeps track of it to be able to show it again.
Future<void> hide() async {
_ensureIsActive();
if (!_isVisible) return;
if (!_isReal) return;
await _plugin.hideIcon(_id);
_isVisible = false;
}
/// Sets the tooltip text. If [message] is `null`, the tooltip is removed.
Future<void> setTooltip(String? message) async {
_ensureIsActive();
await _ensureIsReal();
if (message != null) {
await _plugin.setTooltip(_id, message);
} else {
await _plugin.removeTooltip(_id);
}
}
/// Sets the image on this icon.
///
/// If multiple arguments are passed, they are resolved in this order:
/// 1. [delegate]
/// 2. [pixels]
/// 3. [path]
/// 4. [asset]
/// 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,
ByteBuffer? pixels,
String? asset,
StockIcon? stockIcon,
WinIcon? winIcon,
}) async {
_ensureIsActive();
await _ensureIsReal();
if (delegate != null) {
} else if (pixels != null) {
delegate = TrayIconImageDelegate.fromBytes(pixels);
} else if (asset != null) {
delegate = TrayIconImageDelegate.fromAsset(asset);
} else if (path != null) {
delegate = TrayIconImageDelegate.fromPath(uri: path);
} else if (stockIcon != null) {
delegate = TrayIconImageDelegate.fromStockIcon(stockIcon);
} else if (winIcon != null) {
delegate = TrayIconImageDelegate.fromWinIcon(winIcon);
} else {
delegate = TrayIconImageDelegate.noImage();
}
await delegate.setIcon(_id, _plugin);
}
}