From c80bff92a6576279ec30d4801e5aa86d495f0eff Mon Sep 17 00:00:00 2001 From: palmoni5 Date: Tue, 10 Nov 2026 12:34:59 +0200 Subject: [PATCH] fix(legacy): show taskbar progress and badge on this app's window setProgressBar and setBadgeLabel go through nativeapi's Application, which targets the application's primary window. On Windows, with no primary window set, it falls back to the first window of WindowManager.getAll(), which enumerates visible titled top-level windows without a process filter, so the progress could land on another application's taskbar button. Both calls now make the addressed window the primary window first when the app has not chosen one. A primary window the app set itself is left alone. Adds @visibleForTesting debugBindWindow and debugApplicationOverride so the bridge can be driven by simulated nativeapi objects in tests. --- lib/src/legacy/window_manager.dart | 42 ++++++++++++++-- test/legacy_taskbar_test.dart | 81 ++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 test/legacy_taskbar_test.dart 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()']); + }); +}