diff --git a/lib/src/legacy/window_manager.dart b/lib/src/legacy/window_manager.dart index 73396ab9..542e6553 100644 --- a/lib/src/legacy/window_manager.dart +++ b/lib/src/legacy/window_manager.dart @@ -4,7 +4,8 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui' show Brightness, Color, Offset, PlatformDispatcher, Rect, Size; -import 'package:flutter/foundation.dart' show ObserverList, VoidCallback; +import 'package:flutter/foundation.dart' + show ObserverList, VoidCallback, visibleForTesting; import 'package:flutter/painting.dart' show Alignment; import 'package:nativeapi/nativeapi.dart' as nativeapi; import 'package:nativeapi_flutter/nativeapi_flutter.dart' @@ -66,6 +67,15 @@ class WindowManager { nativeapi.Window? _window; nativeapi.ListenerId? _nativeListenerId; + /// Addresses [window] instead of the platform's current window, or goes + /// back to the current window with null. For tests of this bridge only. + @visibleForTesting + void debugBindWindow(nativeapi.Window? window) { + _unwireNativeEvents(); + _window = window; + if (_listeners.isNotEmpty || _isPreventClose) _wireNativeEvents(); + } + bool _isPreventClose = false; bool _isDestroying = false; bool _isMaximized = false; @@ -569,11 +579,33 @@ class WindowManager { _target?.isVisibleInTaskbar = !isSkipTaskbar; } + /// Replaces [nativeapi.Application.instance]. For tests of this bridge only. + @visibleForTesting + nativeapi.Application? debugApplicationOverride; + + nativeapi.Application get _application => + debugApplicationOverride ?? nativeapi.Application.instance; + /// Sets progress value in progress bar. Valid range is [0, 1.0]. /// /// @platforms macos,windows Future setProgressBar(double progress) async { - tryNative(() => nativeapi.Application.instance.setProgressBar(progress)); + final window = _target; + tryNative(() { + _designatePrimaryWindow(window); + _application.setProgressBar(progress); + }); + } + + // The taskbar progress and badge belong to the application's primary + // window. Without one, Windows falls back to the first visible titled + // top-level window on the desktop, which need not be this app's. Designate + // the addressed window unless the app chose a primary window itself. + void _designatePrimaryWindow(nativeapi.Window? window) { + if (window == null) return; + final application = _application; + if (application.getPrimaryWindow() != null) return; + application.setPrimaryWindow(window); } /// Sets window/taskbar icon. @@ -620,7 +652,11 @@ class WindowManager { /// /// @platforms macos Future setBadgeLabel([String? label]) async { - tryNative(() => nativeapi.Application.instance.setBadgeLabel(label ?? '')); + final window = _target; + tryNative(() { + _designatePrimaryWindow(window); + _application.setBadgeLabel(label ?? ''); + }); } /// Returns `bool` - Whether the window has a shadow. On Windows, always diff --git a/test/legacy_taskbar_test.dart b/test/legacy_taskbar_test.dart new file mode 100644 index 00000000..d8c93473 --- /dev/null +++ b/test/legacy_taskbar_test.dart @@ -0,0 +1,81 @@ +// The taskbar progress and badge of package:window_manager/legacy.dart. +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter_test/flutter_test.dart'; +import 'package:nativeapi/nativeapi.dart' as nativeapi; +import 'package:window_manager/legacy.dart'; + +// A nativeapi window simulated in Dart; no native window is created. +class _FakeNativeWindow extends nativeapi.Window { + _FakeNativeWindow() : super.borrowed(0); +} + +// Only the primary window and taskbar calls the bridge makes; any other +// member throws. +class _FakeApplication implements nativeapi.Application { + nativeapi.Window? primaryWindow; + final List calls = []; + + @override + nativeapi.Window? getPrimaryWindow() => primaryWindow; + + @override + void setPrimaryWindow(nativeapi.Window? window) { + calls.add('setPrimaryWindow'); + primaryWindow = window; + } + + @override + bool setProgressBar(double progress) { + calls.add('setProgressBar($progress)'); + return true; + } + + @override + bool setBadgeLabel(String label) { + calls.add('setBadgeLabel($label)'); + return true; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + late _FakeApplication application; + + setUp(() { + application = _FakeApplication(); + windowManager.debugApplicationOverride = application; + }); + + tearDown(() { + windowManager.debugApplicationOverride = null; + windowManager.debugBindWindow(null); + }); + + test('progress and badge designate the window as primary', () async { + final window = _FakeNativeWindow(); + windowManager.debugBindWindow(window); + + await windowManager.setProgressBar(0.5); + expect(application.primaryWindow, same(window)); + await windowManager.setBadgeLabel('3'); + expect(application.calls, [ + 'setPrimaryWindow', + 'setProgressBar(0.5)', + 'setBadgeLabel(3)', + ]); + }); + + test('a primary window the app chose is kept', () async { + final chosen = _FakeNativeWindow(); + application.primaryWindow = chosen; + windowManager.debugBindWindow(_FakeNativeWindow()); + + await windowManager.setProgressBar(1); + await windowManager.setBadgeLabel(); + expect(application.primaryWindow, same(chosen)); + expect(application.calls, ['setProgressBar(1.0)', 'setBadgeLabel()']); + }); +}