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
48 changes: 41 additions & 7 deletions lib/src/legacy/window_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -39,6 +40,10 @@ const kWindowEventLeaveFullScreen = 'leave-full-screen';
const kWindowEventDocked = 'docked';
const kWindowEventUndocked = 'undocked';

// Reported by 0.5.x through onWindowEvent only, without a named constant.
const _kWindowEventShow = 'show';
const _kWindowEventHide = 'hide';

@Deprecated(
'The 0.5.x compatible API will be removed in a future release. Use the native API from package:window_manager/window_manager.dart.',
)
Expand Down Expand Up @@ -66,6 +71,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;
Expand Down Expand Up @@ -251,9 +265,13 @@ class WindowManager {
}

/// Shows and gives focus to the window.
///
/// Listeners get `onWindowEvent('show')` when this call makes a hidden
/// window visible.
Future<void> show({bool inactive = false}) async {
final window = _target;
if (window == null) return;
final wasVisible = window.isVisible;
if (window.isMinimized) {
window.restore();
}
Expand All @@ -262,11 +280,23 @@ class WindowManager {
} else {
window.show();
}
// nativeapi has no show/hide event; 0.5.x reported both through
// onWindowEvent, which apps use to pause and resume work.
// Check the result: the platform may refuse or defer the change.
if (!wasVisible && window.isVisible) _emit(_kWindowEventShow);
}

/// Hides the window.
///
/// Listeners get `onWindowEvent('hide')` when this call hides a window that
/// was visible or minimized.
Future<void> hide() async {
_target?.hide();
final window = _target;
if (window == null) return;
// macOS reports a minimized window as not visible.
final wasShown = window.isVisible || window.isMinimized;
window.hide();
if (wasShown && !window.isVisible) _emit(_kWindowEventHide);
}

/// Returns `bool` - Whether the window is visible to the user.
Expand Down Expand Up @@ -770,17 +800,21 @@ class WindowManager {
case nativeapi.WindowBlurredEvent():
_emit(kWindowEventBlur);
case nativeapi.WindowMaximizedEvent():
// A minimized window that was maximized comes back with this event
// alone, as 0.5.x reported it on Windows.
_isMinimized = false;
_isMaximized = true;
_emit(kWindowEventMaximize);
case nativeapi.WindowMinimizedEvent():
_isMinimized = true;
_emit(kWindowEventMinimize);
case nativeapi.WindowRestoredEvent():
if (_isMinimized) {
_isMinimized = false;
_emit(kWindowEventRestore);
} else if (_isMaximized) {
_isMaximized = false;
final wasMinimized = _isMinimized;
final wasMaximized = _isMaximized;
_isMinimized = false;
// Leaving the minimized state may land back in the maximized one.
_isMaximized = _target?.isMaximized ?? false;
if (!wasMinimized && wasMaximized && !_isMaximized) {
_emit(kWindowEventUnmaximize);
} else {
_emit(kWindowEventRestore);
Expand Down
188 changes: 188 additions & 0 deletions test/legacy_visibility_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// ignore_for_file: deprecated_member_use_from_same_package

import 'dart:async';

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

class _Recorder with WindowListener {
final List<String> events = <String>[];

@override
void onWindowEvent(String eventName) => events.add(eventName);
}

// A nativeapi window simulated in Dart; no native window is created.
class _FakeNativeWindow extends nativeapi.Window {
_FakeNativeWindow() : super.borrowed(0);

bool visible = false;
bool minimized = false;
bool maximized = false;
bool refuseVisibilityChanges = false;
FutureOr<void> Function(nativeapi.WindowEvent)? _callback;

bool get isListening => _callback != null;

// As nativeapi on macOS, a minimized window does not count as visible.
@override
bool get isVisible => visible && !minimized;

@override
bool get isMinimized => minimized;

@override
bool get isMaximized => maximized;

@override
bool get isFullScreen => false;

@override
void show() => visible = visible || !refuseVisibilityChanges;

@override
void showInactive() => show();

@override
void hide() => visible = visible && refuseVisibilityChanges;

@override
void restore() => minimized = false;

@override
nativeapi.ListenerId addListener(
FutureOr<void> Function(nativeapi.WindowEvent) listener,
) {
_callback = listener;
return 1;
}

@override
bool removeListener(nativeapi.ListenerId id) {
_callback = null;
return true;
}

Future<void> fire(nativeapi.WindowEvent event) async {
await _callback?.call(event);
}
}

class _Named with WindowListener {
final List<String> calls = <String>[];

@override
void onWindowMaximize() => calls.add('maximize');

@override
void onWindowUnmaximize() => calls.add('unmaximize');

@override
void onWindowRestore() => calls.add('restore');
}

void main() {
late _FakeNativeWindow fake;
late _Recorder listener;

setUp(() {
fake = _FakeNativeWindow();
listener = _Recorder();
windowManager.debugBindWindow(fake);
windowManager.addListener(listener);
});

tearDown(() {
windowManager.removeListener(listener);
windowManager.debugBindWindow(null);
});

group('show and hide', () {
test('are reported only when the visibility changes', () async {
await windowManager.show();
await windowManager.show();
await windowManager.hide();
await windowManager.hide();
await windowManager.show(inactive: true);
expect(listener.events, ['show', 'hide', 'show']);
});

test('a minimized window counts as shown', () async {
fake.visible = true;
fake.minimized = true;
await windowManager.hide();
expect(listener.events, ['hide']);

fake.visible = true;
fake.minimized = true;
await windowManager.show();
expect(fake.minimized, isFalse);
expect(listener.events, ['hide', 'show']);
});

test('are not reported when the platform keeps the visibility', () async {
fake.refuseVisibilityChanges = true;
await windowManager.show();
fake.visible = true;
await windowManager.hide();
expect(listener.events, isEmpty);
});
});

group('restore tracking', () {
test(
'a minimized window that comes back maximized is not minimized',
() async {
fake.minimized = true;
await fake.fire(const nativeapi.WindowMinimizedEvent(windowId: 1));
fake.minimized = false;
fake.maximized = true;
await fake.fire(const nativeapi.WindowMaximizedEvent(windowId: 1));
fake.maximized = false;
await fake.fire(const nativeapi.WindowRestoredEvent(windowId: 1));

expect(listener.events, [
kWindowEventMinimize,
kWindowEventMaximize,
kWindowEventUnmaximize,
]);
},
);

test('restoring a minimized window keeps it maximized', () async {
fake.maximized = true;
await fake.fire(const nativeapi.WindowMaximizedEvent(windowId: 1));
fake.minimized = true;
await fake.fire(const nativeapi.WindowMinimizedEvent(windowId: 1));
fake.minimized = false;
await fake.fire(const nativeapi.WindowRestoredEvent(windowId: 1));
fake.maximized = false;
await fake.fire(const nativeapi.WindowRestoredEvent(windowId: 1));

expect(listener.events, [
kWindowEventMaximize,
kWindowEventMinimize,
kWindowEventRestore,
kWindowEventUnmaximize,
]);
});

test('named callbacks follow the tracked state', () async {
final named = _Named();
windowManager.addListener(named);
addTearDown(() => windowManager.removeListener(named));

fake.maximized = true;
await fake.fire(const nativeapi.WindowMaximizedEvent(windowId: 1));
fake.maximized = false;
await fake.fire(const nativeapi.WindowRestoredEvent(windowId: 1));
fake.minimized = true;
await fake.fire(const nativeapi.WindowMinimizedEvent(windowId: 1));
fake.minimized = false;
await fake.fire(const nativeapi.WindowRestoredEvent(windowId: 1));

expect(named.calls, ['maximize', 'unmaximize', 'restore']);
});
});
}