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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,39 @@ What differs from 0.5.x:
- New example on `package:flutter/widgets.dart` alone; the full one is nativeapi's
[window_example](https://github.com/libnativeapi/nativeapi/tree/main/examples/flutter_window_example).

#### Choosing the window `legacy.dart` addresses

By default every call addresses the window the platform currently considers the app's
own (nativeapi's `WindowManager.getCurrent()`). On Windows the current window is the
active one, which a window kept hidden during startup is not; there the bridge also
takes the application's primary window, else this process's top-level window that hosts
the Flutter view (window class `FLUTTERVIEW`), hidden or not, when exactly one exists.
Until a window is found the bridge looks again on every call: listeners and
`setPreventClose(true)` set up earlier are connected to the first window found, and a
`show()` that found no window is carried out on it. Other calls do nothing until then.

To address a particular window, for example with several Flutter windows in one
process, bind it explicitly. On Windows,
`nativeapi.Window.createWithNativeWindow` wraps an `HWND`, for example one the runner
passes to Dart:

```dart
import 'dart:ffi';

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

void bindMainWindow(int hwnd) {
windowManager.bindWindow(
nativeapi.Window.createWithNativeWindow(Pointer<Void>.fromAddress(hwnd)),
);
}
```

An app on Flutter's multi-window API can bind `controller.nativeWindow` from
`package:window_manager/windowing.dart` instead. Listeners and prevent-close move to the
bound window; `bindWindow(null)` goes back to the current window.

#### Moving to the native API

| 0.5.x (`legacy.dart`) | Native API (`window_manager.dart`) |
Expand Down
106 changes: 106 additions & 0 deletions lib/src/legacy/flutter_host_window_win32.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import 'dart:ffi';
import 'dart:io';

import 'package:ffi/ffi.dart';

const _gwOwner = 4;

typedef _EnumWindowsProcNative = Int32 Function(IntPtr hwnd, IntPtr lParam);

/// The `HWND` of this process's only top-level window that hosts a Flutter
/// view, or 0 when there is none, more than one, or the platform is not
/// Windows.
///
/// nativeapi finds the current window on Windows through `GetActiveWindow()`,
/// which knows nothing of a window that is still hidden. A Flutter runner
/// parents the engine's view (window class `FLUTTERVIEW`) to its top-level
/// window, visible or not; when that identifies a single window of this
/// process, it is the app's window. With several such windows the answer
/// would be a guess, so there is none.
int findSoleFlutterHostWindow() {
if (!Platform.isWindows) return 0;
final api = _Win32.instance;
final self = api.getCurrentProcessId();
return using((arena) {
final viewClass = 'FLUTTERVIEW'.toNativeUtf16(allocator: arena);
final processId = arena<Uint32>();
var found = 0;
// EnumWindows walks a snapshot of the desktop's top-level windows, hidden
// ones included, so windows that move in the z-order meanwhile are seen
// once.
for (final hwnd in _topLevelWindows(api)) {
api.getWindowThreadProcessId(hwnd, processId);
if (processId.value != self) continue;
if (api.getWindow(hwnd, _gwOwner) != 0) continue;
if (api.findWindowEx(hwnd, 0, viewClass, nullptr) == 0) continue;
if (found != 0) return 0;
found = hwnd;
}
return found;
});
}

// Filled by [_collectWindow] during one synchronous EnumWindows call.
List<int>? _collected;

int _collectWindow(int hwnd, int lParam) {
_collected?.add(hwnd);
return 1;
}

List<int> _topLevelWindows(_Win32 api) {
final windows = _collected = <int>[];
try {
api.enumWindows(
Pointer.fromFunction<_EnumWindowsProcNative>(_collectWindow, 0),
0,
);
return windows;
} finally {
_collected = null;
}
}

class _Win32 {
_Win32._() {
final user32 = DynamicLibrary.open('user32.dll');
final kernel32 = DynamicLibrary.open('kernel32.dll');
enumWindows = user32
.lookupFunction<
Int32 Function(
Pointer<NativeFunction<_EnumWindowsProcNative>>,
IntPtr,
),
int Function(Pointer<NativeFunction<_EnumWindowsProcNative>>, int)
>('EnumWindows');
findWindowEx = user32
.lookupFunction<
IntPtr Function(IntPtr, IntPtr, Pointer<Utf16>, Pointer<Utf16>),
int Function(int, int, Pointer<Utf16>, Pointer<Utf16>)
>('FindWindowExW');
getWindowThreadProcessId = user32
.lookupFunction<
Uint32 Function(IntPtr, Pointer<Uint32>),
int Function(int, Pointer<Uint32>)
>('GetWindowThreadProcessId');
getWindow = user32
.lookupFunction<
IntPtr Function(IntPtr, Uint32),
int Function(int, int)
>('GetWindow');
getCurrentProcessId = kernel32
.lookupFunction<Uint32 Function(), int Function()>(
'GetCurrentProcessId',
);
}

static final _Win32 instance = _Win32._();

late final int Function(Pointer<NativeFunction<_EnumWindowsProcNative>>, int)
enumWindows;
late final int Function(int, int, Pointer<Utf16>, Pointer<Utf16>)
findWindowEx;
late final int Function(int, Pointer<Uint32>) getWindowThreadProcessId;
late final int Function(int, int) getWindow;
late final int Function() getCurrentProcessId;
}
142 changes: 131 additions & 11 deletions lib/src/legacy/window_manager.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// ignore_for_file: deprecated_member_use_from_same_package

import 'dart:async';
import 'dart:developer' as developer;
import 'dart:ffi' show Pointer, Void;
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 All @@ -17,6 +20,7 @@ import 'package:nativeapi_flutter/nativeapi_flutter.dart'
RectangleToRect,
SizeToNative;
import 'package:window_manager/src/legacy/calc_window_position.dart';
import 'package:window_manager/src/legacy/flutter_host_window_win32.dart';
import 'package:window_manager/src/legacy/window_listener.dart';
import 'package:window_manager/src/legacy/window_options.dart';
import 'package:window_manager/src/native_guard.dart';
Expand Down Expand Up @@ -48,9 +52,9 @@ enum DockSide { left, right }
/// for code that has not moved to the native API yet.
///
/// Every call goes to the window the platform currently considers the app's
/// own. 0.5.x only ever addressed one window; an app with several of them
/// should use the native API, where each [nativeapi.Window] is addressed
/// directly.
/// own, or to the one passed to [WindowManager.bindWindow]. 0.5.x only ever
/// addressed one window; an app with several of them should use the native
/// API, where each [nativeapi.Window] is addressed directly.
@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 All @@ -72,14 +76,100 @@ class WindowManager {
bool _isMinimized = false;
bool _isFullScreen = false;

/// The window every call below addresses, resolved once the platform has
/// one. Null before the first window exists.
// The addressed window is the one passed to [bindWindow], or the platform's
// current window once it could be resolved; "no window" is never cached.
//
// A show() that arrived while no window could be resolved is carried out on
// the window that is resolved or bound next. Null when none is pending.
bool? _pendingShowInactive;
bool _didWarnUnresolvedShow = false;

/// Replaces how the platform's current window is looked up. For tests of
/// this bridge only; null restores the platform lookup.
@visibleForTesting
nativeapi.Window? Function()? debugCurrentWindowOverride;

/// Makes every call address [window], or with null goes back to the window
/// the platform currently considers the app's own.
///
/// By default the window is looked up with nativeapi's
/// `WindowManager.getCurrent()`, retried by every call until it answers. On
/// Windows that is the active window, so an app that keeps its window hidden
/// during startup may have none to address before it is shown. Binding the
/// window explicitly removes that dependency:
///
/// ```dart
/// // hwnd: the HWND of the app's top-level window, from the runner.
/// windowManager.bindWindow(
/// nativeapi.Window.createWithNativeWindow(Pointer<Void>.fromAddress(hwnd)),
/// );
/// ```
///
/// Listeners and [setPreventClose] move to the new window: native events
/// stop arriving from the previous one. A [show] requested while no window
/// could be found is carried out on [window].
void bindWindow(nativeapi.Window? window) {
_unwireNativeEvents();
_window = null;
if (window != null) {
_adopt(window);
} else if (_wantsNativeEvents) {
// Resolves the current window, if there is one yet, and wires it.
_wireNativeEvents();
}
}

bool get _wantsNativeEvents => _listeners.isNotEmpty || _isPreventClose;

/// The window every call below addresses.
///
/// Null while the platform has no current window for the app. Null is not
/// remembered: the next call looks again, and the first window found gets
/// the listeners and prevent-close that were set up before it existed.
nativeapi.Window? get _target {
return _window ??= tryNative<nativeapi.Window?>(
() => nativeapi.WindowManager.instance.getCurrent(),
final window = _window;
if (window != null) return window;
final resolved = _resolveCurrentWindow();
if (resolved != null) _adopt(resolved);
return resolved;
}

nativeapi.Window? _resolveCurrentWindow() {
final override = debugCurrentWindowOverride;
if (override != null) return override();
return tryNative<nativeapi.Window?>(
() =>
nativeapi.WindowManager.instance.getCurrent() ??
_findHiddenWindowOnWindows(),
);
}

// Windows reports only the active window as current, so a window kept
// hidden until the app is ready would be out of reach until then. Linux and
// macOS already fall back to a hidden window of their own.
nativeapi.Window? _findHiddenWindowOnWindows() {
if (!Platform.isWindows) return null;
// An app that designated its primary window has one even while it is
// hidden.
final primary = nativeapi.Application.instance.getPrimaryWindow();
if (primary != null) return primary;
final hwnd = findSoleFlutterHostWindow();
if (hwnd == 0) return null;
return nativeapi.Window.createWithNativeWindow(
Pointer<Void>.fromAddress(hwnd),
);
}

void _adopt(nativeapi.Window window) {
_window = window;
if (_wantsNativeEvents) _wireNativeEvents();
final inactive = _pendingShowInactive;
if (inactive != null) {
_pendingShowInactive = null;
_showWindow(window, inactive: inactive);
}
}

List<WindowListener> get listeners {
return List<WindowListener>.from(_listeners);
}
Expand Down Expand Up @@ -107,7 +197,7 @@ class WindowManager {

/// Connects listeners registered before the native window was available.
Future<void> ensureInitialized() async {
if (_listeners.isNotEmpty || _isPreventClose) _wireNativeEvents();
if (_wantsNativeEvents) _wireNativeEvents();
}

/// Returns `int` - The ID of the window.
Expand Down Expand Up @@ -251,9 +341,32 @@ class WindowManager {
}

/// Shows and gives focus to the window.
///
/// When no window can be found yet (see [bindWindow]), the request is kept
/// and carried out on the first window that is found or bound; a later
/// [hide] cancels it.
Future<void> show({bool inactive = false}) async {
final window = _target;
if (window == null) return;
if (window == null) {
_pendingShowInactive = inactive;
assert(() {
if (!_didWarnUnresolvedShow) {
_didWarnUnresolvedShow = true;
developer.log(
'show() found no window to show yet; it will be shown once one '
'is found. Call windowManager.bindWindow() to address the window '
'explicitly.',
name: 'window_manager',
);
}
return true;
}());
return;
}
_showWindow(window, inactive: inactive);
}

void _showWindow(nativeapi.Window window, {required bool inactive}) {
if (window.isMinimized) {
window.restore();
}
Expand All @@ -265,7 +378,10 @@ class WindowManager {
}

/// Hides the window.
///
/// Also cancels a [show] that is waiting for a window to be found.
Future<void> hide() async {
_pendingShowInactive = null;
_target?.hide();
}

Expand Down Expand Up @@ -727,10 +843,14 @@ class WindowManager {
// A listener on the window itself, not on nativeapi's WindowManager: close
// requests reach only the window's own listeners, and other windows of the
// app stay out of the 0.5.x events.
//
// Without a window yet this is a no-op; the window found later is wired
// when it is adopted. Safe to call any number of times.
void _wireNativeEvents() {
if (_nativeListenerId != null) return;
final window = _target;
if (window == null) return;
// Adopting a newly found window may have wired it already.
if (window == null || _nativeListenerId != null) return;
// State may have changed while there were no listeners, or before the
// first listener was added. A restored event alone cannot tell us whether
// the window was minimized or maximized.
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ environment:
dependencies:
flutter:
sdk: flutter
ffi: ^2.1.0
nativeapi: ^0.5.5
nativeapi_flutter: ^0.5.5

Expand Down
Loading