Skip to content
Merged
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
9 changes: 6 additions & 3 deletions integration_test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ enum WindowSize {
wide(Size(1024, 840));

final Size size;

const WindowSize(this.size);

static WindowSize get standard => windowSizeName.isNotEmpty
Expand All @@ -57,6 +58,7 @@ class TestParameters {
}

Widget? _app;

Future<Widget> getApp() async {
if (_app == null) {
isRunningTest = true; // Enable test mode
Expand Down Expand Up @@ -89,17 +91,17 @@ void testApp(
}
if (isDesktop) {
patrolWidgetTest(name, (widgetTester) async {
final window = WindowManagerHelper.withPreferences(
final windowManagerHelper = WindowManagerHelper.withPreferences(
await SharedPreferences.getInstance(),
);
await widgetTester.pumpWidget(await getApp());

// Set window size
final size = params.windowSize.size;
final rect = Rect.fromLTWH(10, 10, size.width, size.height);
if (await window.getBounds() != rect) {
if (await windowManagerHelper.getBounds() != rect) {
await Future.delayed(const Duration(milliseconds: 200));
await window.setBounds(rect);
await windowManagerHelper.setBounds(rect);
}

await widgetTester.pumpAndSettle();
Expand Down Expand Up @@ -209,6 +211,7 @@ extension DeviceInfoUtils on DeviceInfo {

extension PatrolFinderUtils on PatrolFinder {
T widget<T extends Widget>() => tester.tester.widget<T>(this);

T element<T extends Element>() => tester.tester.element<T>(this);
}

Expand Down
47 changes: 21 additions & 26 deletions lib/desktop/init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import 'package:args/args.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:local_notifier/local_notifier.dart';
import 'package:logging/logging.dart';
Expand Down Expand Up @@ -63,22 +62,21 @@ import 'window_manager_helper/window_manager_helper.dart';

final _log = Logger('desktop.init');

const String _keyLeft = 'DESKTOP_WINDOW_LEFT';
const String _keyTop = 'DESKTOP_WINDOW_TOP';
const String _keyWidth = 'DESKTOP_WINDOW_WIDTH';
const String _keyHeight = 'DESKTOP_WINDOW_HEIGHT';
const String _logLevel = 'log-level';
const String _logFile = 'log-file';
const String _hidden = 'hidden';
const String _shown = 'shown';

void _saveWindowBounds(WindowManagerHelper helper) async {
final bounds = await helper.getBounds();
await helper.sharedPreferences.setDouble(_keyWidth, bounds.width);
await helper.sharedPreferences.setDouble(_keyHeight, bounds.height);
await helper.sharedPreferences.setDouble(_keyLeft, bounds.left);
await helper.sharedPreferences.setDouble(_keyTop, bounds.top);
_log.debug('Saving window bounds: $bounds');
Timer? _saveWindowManagerPropertiesTimer;

void _queueSaveWindowManagerProperties(WindowManagerHelper helper) {
_saveWindowManagerPropertiesTimer?.cancel();
_saveWindowManagerPropertiesTimer = Timer(
const Duration(
milliseconds: 500, // lower values result in invalid display properties
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a timer here? Is its purpose to prevent writing/saving too frequently? Was this an issue in the old code?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly that. The timer prevents racing conditions when persisting the values.

),
() async => await helper.saveWindowManagerProperties(),
);
}

class _ScreenRetrieverListener extends ScreenListener {
Expand All @@ -89,7 +87,7 @@ class _ScreenRetrieverListener extends ScreenListener {
@override
void onScreenEvent(String eventName) async {
_log.debug('Screen event: $eventName');
_saveWindowBounds(_helper);
_queueSaveWindowManagerProperties(_helper);
}
}

Expand All @@ -101,13 +99,13 @@ class _WindowEventListener extends WindowListener {
@override
void onWindowResize() async {
_log.debug('Window event: onWindowResize');
_saveWindowBounds(_helper);
_queueSaveWindowManagerProperties(_helper);
}

@override
void onWindowMoved() async {
_log.debug('Window event: onWindowMoved');
_saveWindowBounds(_helper);
_queueSaveWindowManagerProperties(_helper);
}

@override
Expand Down Expand Up @@ -157,21 +155,12 @@ Future<Widget> initialize(List<String> argv) async {

_log.info('Window hidden on startup: $isHidden');

final bounds = Rect.fromLTWH(
prefs.getDouble(_keyLeft) ?? WindowDefaults.bounds.left,
prefs.getDouble(_keyTop) ?? WindowDefaults.bounds.top,
prefs.getDouble(_keyWidth) ?? WindowDefaults.bounds.width,
prefs.getDouble(_keyHeight) ?? WindowDefaults.bounds.height,
);

_log.debug('Using saved window bounds (or defaults): $bounds');

final windowReady = windowManager
.waitUntilReadyToShow(
const WindowOptions(minimumSize: WindowDefaults.minSize),
)
.then((_) async {
await windowManagerHelper.setBounds(bounds);
await windowManagerHelper.restoreWindowManagerProperties();

if (isHidden) {
await windowManager.setSkipTaskbar(true);
Expand Down Expand Up @@ -346,12 +335,18 @@ void _initLogging(ArgResults args) {
(level) => level.name == levelName.toUpperCase(),
);
Logger.root.level = level;
_log.info('Log level initialized from command line argument');
_log.info(
'Log level ${level.name} initialized from command line argument',
);
} catch (error) {
_log.error('Failed to set log level', error);
}
}

if (file != null) {
_log.info('Logging to ${file.absolute}');
}

_log.info('Logging initialized, outputting to stderr');
}

Expand Down
96 changes: 79 additions & 17 deletions lib/desktop/window_manager_helper/_wm_helper_macos_impl.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Yubico.
* Copyright (C) 2023-2025 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,6 +24,59 @@ import 'defaults.dart';
class WindowManagerHelperMacOs {
static const _keyPosDisplay = 'DESKTOP_WINDOW_POS_DISPLAY';

static Future<void> saveWindowManagerProperties(
SharedPreferences prefs,
) async {
final size = await windowManager.getSize();
final offset = await windowManager.getPosition();
var result = await _findCurrentDisplayAndLocalOffset(offset, size);
if (result != null) {
await prefs.setString(_keyPosDisplay, result.displayName);
}
}

static Future<void> restoreWindowManagerProperties(
SharedPreferences prefs,
Rect bounds,
) async {
await windowManager.setMinimumSize(WindowDefaults.minSize);

final width = bounds.width;
final height = bounds.height;
final posX = bounds.left;
final posY = bounds.top;

final posDisplay = prefs.getString(_keyPosDisplay);

if (posDisplay != null) {
final displays = await screenRetriever.getAllDisplays();
for (var d in displays) {
if (d.name == posDisplay) {
var globalPos = Offset(
10 + d.visiblePosition!.dx,
10 + d.visiblePosition!.dy,
);
if ((posX >= 0) &&
(posX < d.visibleSize!.width) &&
(posY >= 0) &&
(posY < d.visibleSize!.height)) {
// if the local position exists on the display, use it
globalPos = Offset(
posX + d.visiblePosition!.dx,
posY + d.visiblePosition!.dy,
);
}

await windowManager.setBounds(
null,
size: Size(width, height),
position: Offset(globalPos.dx, globalPos.dy),
);
}
}
}
}

static Future<void> setBounds(SharedPreferences prefs, Rect bounds) async {
await windowManager.setMinimumSize(WindowDefaults.minSize);

Expand Down Expand Up @@ -63,18 +116,35 @@ class WindowManagerHelperMacOs {
}
}

static Future<Rect> getBounds(SharedPreferences prefs) async {
static Future<Rect> getBounds() async {
final size = await windowManager.getSize();
final offset = await windowManager.getPosition();
final displays = await screenRetriever.getAllDisplays();
var result = await _findCurrentDisplayAndLocalOffset(offset, size);
if (result != null) {
return Rect.fromLTWH(
result.localOffset.dx,
result.localOffset.dy,
size.width,
size.height,
);
}

return WindowDefaults.bounds;
}

static Future<({String displayName, Offset localOffset})?>
_findCurrentDisplayAndLocalOffset(
Offset windowOffset,
Size windowSize,
) async {
final displays = await screenRetriever.getAllDisplays();
for (var d in displays) {
if (d.visiblePosition != null &&
d.visibleSize != null &&
d.name != null) {
final windowCenter = Offset(
offset.dx + size.width / 2.0,
offset.dy + size.height / 2.0,
windowOffset.dx + windowSize.width / 2.0,
windowOffset.dy + windowSize.height / 2.0,
);
if ((windowCenter.dx >= d.visiblePosition!.dx) &&
(windowCenter.dx <
Expand All @@ -83,21 +153,13 @@ class WindowManagerHelperMacOs {
(windowCenter.dy <
(d.visiblePosition!.dy + d.visibleSize!.height))) {
final localOffset = Offset(
offset.dx - d.visiblePosition!.dx,
offset.dy - d.visiblePosition!.dy,
);
await prefs.setString(_keyPosDisplay, d.name!);

return Rect.fromLTWH(
localOffset.dx,
localOffset.dy,
size.width,
size.height,
windowOffset.dx - d.visiblePosition!.dx,
windowOffset.dy - d.visiblePosition!.dy,
);
return (displayName: d.name!, localOffset: localOffset);
}
}
}

return WindowDefaults.bounds;
return null;
}
}
Loading
Loading