Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 46 additions & 23 deletions lib/src/legacy/window_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ class WindowManager {
final ObserverList<WindowListener> _listeners =
ObserverList<WindowListener>();

// Move/resize events are frequent; only rebuild the immutable iteration
// snapshot when subscriptions change.
List<WindowListener> _listenerSnapshot = const <WindowListener>[];

nativeapi.Window? _window;
nativeapi.ListenerId? _nativeListenerId;

Expand All @@ -81,7 +85,7 @@ class WindowManager {
}

List<WindowListener> get listeners {
return List<WindowListener>.from(_listeners);
return List<WindowListener>.from(_listenerSnapshot);
}

bool get hasListeners {
Expand All @@ -90,11 +94,13 @@ class WindowManager {

void addListener(WindowListener listener) {
_listeners.add(listener);
_listenerSnapshot = List<WindowListener>.of(_listeners);
_wireNativeEvents();
}

void removeListener(WindowListener listener) {
_listeners.remove(listener);
_listenerSnapshot = List<WindowListener>.of(_listeners);
if (_listeners.isEmpty && !_isPreventClose) {
_unwireNativeEvents();
}
Expand Down Expand Up @@ -808,29 +814,46 @@ class WindowManager {
}

void _emit(String eventName) {
for (final listener in listeners) {
if (!_listeners.contains(listener)) {
continue;
}
if (_listeners.isEmpty) return;
// Existing listeners receive this event. Callbacks may subscribe others,
// which join only on the next event rather than this iteration.
for (final listener in _listenerSnapshot) {
if (!_listeners.contains(listener)) continue;
listener.onWindowEvent(eventName);
final Map<String, void Function()> funcMap = {
kWindowEventClose: listener.onWindowClose,
kWindowEventFocus: listener.onWindowFocus,
kWindowEventBlur: listener.onWindowBlur,
kWindowEventMaximize: listener.onWindowMaximize,
kWindowEventUnmaximize: listener.onWindowUnmaximize,
kWindowEventMinimize: listener.onWindowMinimize,
kWindowEventRestore: listener.onWindowRestore,
kWindowEventResize: listener.onWindowResize,
kWindowEventResized: listener.onWindowResized,
kWindowEventMove: listener.onWindowMove,
kWindowEventMoved: listener.onWindowMoved,
kWindowEventEnterFullScreen: listener.onWindowEnterFullScreen,
kWindowEventLeaveFullScreen: listener.onWindowLeaveFullScreen,
kWindowEventDocked: listener.onWindowDocked,
kWindowEventUndocked: listener.onWindowUndocked,
};
funcMap[eventName]?.call();
// A switch, not a map of tear-offs per listener: move and resize arrive
// many times per second while the user drags the window.
switch (eventName) {
case kWindowEventClose:
listener.onWindowClose();
case kWindowEventFocus:
listener.onWindowFocus();
case kWindowEventBlur:
listener.onWindowBlur();
case kWindowEventMaximize:
listener.onWindowMaximize();
case kWindowEventUnmaximize:
listener.onWindowUnmaximize();
case kWindowEventMinimize:
listener.onWindowMinimize();
case kWindowEventRestore:
listener.onWindowRestore();
case kWindowEventResize:
listener.onWindowResize();
case kWindowEventResized:
listener.onWindowResized();
case kWindowEventMove:
listener.onWindowMove();
case kWindowEventMoved:
listener.onWindowMoved();
case kWindowEventEnterFullScreen:
listener.onWindowEnterFullScreen();
case kWindowEventLeaveFullScreen:
listener.onWindowLeaveFullScreen();
case kWindowEventDocked:
listener.onWindowDocked();
case kWindowEventUndocked:
listener.onWindowUndocked();
}
}
}
}
Expand Down
98 changes: 98 additions & 0 deletions test/legacy_dispatch_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Event delivery of package:window_manager/legacy.dart. With prevent-close on
// and no native window, close() reports onWindowClose, which drives delivery
// without a window.
// ignore_for_file: deprecated_member_use_from_same_package

import 'package:flutter_test/flutter_test.dart';
import 'package:window_manager/legacy.dart';

class _OnClose with WindowListener {
_OnClose(this.action);

final void Function() action;
int closes = 0;

@override
void onWindowEvent(String eventName) {
if (eventName == kWindowEventClose) action();
}

@override
void onWindowClose() => closes++;
}

void main() {
setUp(() => windowManager.setPreventClose(true));
tearDown(() => windowManager.setPreventClose(false));

test('the named callback follows onWindowEvent once per event', () async {
final events = <String>[];
final listener = _OnClose(() => events.add('event'));
windowManager.addListener(listener);
addTearDown(() => windowManager.removeListener(listener));

await windowManager.close();
await windowManager.close();
expect(events, ['event', 'event']);
expect(listener.closes, 2);
});

test(
'removing a listener during delivery does not starve later listeners',
() async {
final events = <String>[];
late final _OnClose removed;
final first = _OnClose(() => windowManager.removeListener(removed));
removed = _OnClose(() => events.add('removed'));
final last = _OnClose(() => events.add('last'));
windowManager.addListener(first);
windowManager.addListener(removed);
windowManager.addListener(last);
addTearDown(() {
windowManager.removeListener(first);
windowManager.removeListener(removed);
windowManager.removeListener(last);
});

await windowManager.close();
expect(events, ['last']);
expect(removed.closes, 0);
},
);

test('a listener added during delivery joins the next event only', () async {
final received = <String>[];
late final _OnClose newlyAdded;
var added = false;
final first = _OnClose(() {
if (!added) {
added = true;
windowManager.addListener(newlyAdded);
}
});
newlyAdded = _OnClose(() => received.add('new'));
final tail = _OnClose(() => received.add('tail'));
windowManager.addListener(first);
windowManager.addListener(tail);
addTearDown(() {
windowManager.removeListener(first);
windowManager.removeListener(tail);
windowManager.removeListener(newlyAdded);
});

await windowManager.close();
expect(received, ['tail']);
await windowManager.close();
expect(received, ['tail', 'tail', 'new']);
});

test('listeners returns a copy', () {
final listener = _OnClose(() {});
windowManager.addListener(listener);
addTearDown(() => windowManager.removeListener(listener));

windowManager.listeners.clear();
expect(windowManager.listeners, [listener]);
expect(windowManager.hasListeners, isTrue);
});
}