diff --git a/lib/src/legacy/window_manager.dart b/lib/src/legacy/window_manager.dart index 73396ab9..9432653a 100644 --- a/lib/src/legacy/window_manager.dart +++ b/lib/src/legacy/window_manager.dart @@ -63,6 +63,10 @@ class WindowManager { final ObserverList _listeners = ObserverList(); + // Move/resize events are frequent; only rebuild the immutable iteration + // snapshot when subscriptions change. + List _listenerSnapshot = const []; + nativeapi.Window? _window; nativeapi.ListenerId? _nativeListenerId; @@ -81,7 +85,7 @@ class WindowManager { } List get listeners { - return List.from(_listeners); + return List.from(_listenerSnapshot); } bool get hasListeners { @@ -90,11 +94,13 @@ class WindowManager { void addListener(WindowListener listener) { _listeners.add(listener); + _listenerSnapshot = List.of(_listeners); _wireNativeEvents(); } void removeListener(WindowListener listener) { _listeners.remove(listener); + _listenerSnapshot = List.of(_listeners); if (_listeners.isEmpty && !_isPreventClose) { _unwireNativeEvents(); } @@ -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 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(); + } } } } diff --git a/test/legacy_dispatch_test.dart b/test/legacy_dispatch_test.dart new file mode 100644 index 00000000..0e3f8a6c --- /dev/null +++ b/test/legacy_dispatch_test.dart @@ -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 = []; + 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 = []; + 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 = []; + 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); + }); +}