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
2 changes: 1 addition & 1 deletion src/App/Display/WindowsGameDisplayService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ private static GameDisplayInfo CreateDisplayInfo(Screen screen, int fallbackNumb
);
}

private static (double Scale, bool IsReliable) GetDpiScale(Rectangle bounds)
internal static (double Scale, bool IsReliable) GetDpiScale(Rectangle bounds)
{
try
{
Expand Down
229 changes: 221 additions & 8 deletions src/App/PageSwitcher.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Windows;
using System.Windows.Automation;
Expand All @@ -8,6 +10,7 @@
using System.Windows.Shell;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Win32;
using RatScanner.Display;
using RatScanner.View;
using ContextMenuStrip = System.Windows.Forms.ContextMenuStrip;
using NotifyIcon = System.Windows.Forms.NotifyIcon;
Expand Down Expand Up @@ -42,6 +45,8 @@ public partial class PageSwitcher : Window

private UserControl? activeControl;
private bool _isMinimalUi;
private bool _isExiting;
private bool _hasPersistedWindowBounds;
private WindowState _restoreWindowState = WindowState.Normal;
private Rect _restoreBounds;

Expand Down Expand Up @@ -80,11 +85,7 @@ public PageSwitcher()
AddJumpList();
AddTrayIcon();

if (RatConfig.LastWindowPositionX != int.MinValue && RatConfig.LastWindowPositionY != int.MinValue)
{
Left = RatConfig.LastWindowPositionX;
Top = RatConfig.LastWindowPositionY;
}
RestoreWindowBounds();
Topmost = RatConfig.AlwaysOnTop;
if (RatConfig.LastWindowMode == RatConfig.WindowMode.Minimal)
ShowMinimalUI();
Expand All @@ -111,6 +112,143 @@ internal void ResetWindowSize()
Height = DefaultHeight;
}

/// <summary>
/// Restores the persisted normal-mode window size and position from the
/// previous session. Saved bounds are validated against the currently
/// attached displays: a monitor layout change since the last run must not
/// resurrect the window on a screen that no longer exists.
/// </summary>
private void RestoreWindowBounds()
{
if (
!TryGetRestorableBounds(
RatConfig.LastWindowPositionX,
RatConfig.LastWindowPositionY,
RatConfig.LastWindowWidth,
RatConfig.LastWindowHeight,
Width,
Height,
GetLogicalWorkingAreas(),
out Rect bounds
)
)
return;

Width = bounds.Width;
Height = bounds.Height;
Left = bounds.Left;
Top = bounds.Top;
}

/// <summary>
/// True when enough of the window (roughly the caption strip) intersects
/// the working area of at least one attached display for the user to grab it.
/// </summary>
internal static bool IsVisibleOnAnyScreen(double left, double top, double width, double height)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
{
return IsVisibleOnAnyScreen(left, top, width, height, GetLogicalWorkingAreas());
}

internal static bool IsVisibleOnAnyScreen(
double left,
double top,
double width,
double height,
IReadOnlyList<LogicalWorkingArea> workingAreas
)
{
// The title bar is the reliable grab strip; require most of it plus a
// minimal slice of the window body to land on some screen.
double grabLeft = left + Math.Min(40, width / 4);
double grabRight = left + width - Math.Min(40, width / 4);
double grabTop = top;
double grabBottom = top + Math.Min(48, height);

foreach (LogicalWorkingArea area in workingAreas)
{
bool intersects =
grabRight > area.Left && grabLeft < area.Right && grabBottom > area.Top && grabTop < area.Bottom;
if (intersects)
return true;
}
return false;
Comment thread
DysektAI marked this conversation as resolved.
}

internal static bool TryGetRestorableBounds(
int savedLeft,
int savedTop,
int savedWidth,
int savedHeight,
double defaultWidth,
double defaultHeight,
IReadOnlyList<LogicalWorkingArea> workingAreas,
out Rect bounds
)
{
bounds = Rect.Empty;
if (savedLeft == int.MinValue || savedTop == int.MinValue)
return false;

bool hasValidSavedSize = savedWidth >= MinimumWidth && savedHeight >= MinimumHeight;
double width = hasValidSavedSize ? savedWidth : defaultWidth;
double height = hasValidSavedSize ? savedHeight : defaultHeight;
if (
!double.IsFinite(width)
|| !double.IsFinite(height)
|| width < MinimumWidth
|| height < MinimumHeight
|| !IsVisibleOnAnyScreen(savedLeft, savedTop, width, height, workingAreas)
)
return false;

bounds = new Rect(savedLeft, savedTop, width, height);
return true;
}

internal static bool TryPhysicalToLogicalWorkingArea(
System.Drawing.Rectangle physicalArea,
double dpiScale,
bool isDpiReliable,
out LogicalWorkingArea workingArea
)
{
workingArea = default;
if (!isDpiReliable || !double.IsFinite(dpiScale) || dpiScale <= 0)
return false;

workingArea = new LogicalWorkingArea(
physicalArea.Left / dpiScale,
physicalArea.Top / dpiScale,
physicalArea.Right / dpiScale,
physicalArea.Bottom / dpiScale
);
return true;
}

private static IReadOnlyList<LogicalWorkingArea> GetLogicalWorkingAreas()
{
System.Windows.Forms.Screen[] screens = System.Windows.Forms.Screen.AllScreens;
List<LogicalWorkingArea> workingAreas = new(screens.Length);
foreach (System.Windows.Forms.Screen screen in screens)
{
(double dpiScale, bool isDpiReliable) = WindowsGameDisplayService.GetDpiScale(screen.Bounds);
// WinForms exposes physical pixels while WPF persists window
// coordinates in device-independent units.
if (
TryPhysicalToLogicalWorkingArea(
screen.WorkingArea,
dpiScale,
isDpiReliable,
out LogicalWorkingArea workingArea
)
)
workingAreas.Add(workingArea);
}
return workingAreas;
}

internal readonly record struct LogicalWorkingArea(double Left, double Top, double Right, double Bottom);

internal void Navigate(UserControl nextControl, object? state = null)
{
if (!(nextControl is ISwitchable))
Expand Down Expand Up @@ -140,6 +278,13 @@ protected override void OnStateChanged(EventArgs e)
base.OnStateChanged(e);
}

protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
if (!e.Cancel)
PersistWindowBoundsOnce();
}

protected override void OnClosed(EventArgs e)
{
SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
Expand Down Expand Up @@ -400,6 +545,14 @@ internal void ShowUI()
Top = minimalTop - chromeMargin;
}
}
else
{
// Entered minimal UI straight from startup (LastWindowMode =
// Minimal), so no in-session restore bounds exist. Fall back
// to the persisted normal-mode bounds.
WindowState = WindowState.Normal;
RestoreWindowBounds();
}

// The offset is only valid for one exit; clear it so a subsequent
// tray-menu exit doesn't use a stale value.
Expand Down Expand Up @@ -513,12 +666,72 @@ private void AnchorNearTopRight(double rightEdge, double topEdge)

internal void ExitApplication()
{
RatConfig.LastWindowPositionX = (int)Left;
RatConfig.LastWindowPositionY = (int)Top;
RatConfig.SaveConfig();
if (_isExiting)
return;

_isExiting = true;
PersistWindowBoundsOnce();
Application.Current.Shutdown();
}

private void PersistWindowBoundsOnce()
{
if (_hasPersistedWindowBounds)
return;

PersistWindowBounds();
RatConfig.SaveConfig();
_hasPersistedWindowBounds = true;
}

/// <summary>
/// Persists the normal-mode window bounds so the next launch restores the
/// user's size and position. When closing from minimal UI or a maximized
/// window, the pre-minimal / pre-maximize restore bounds are what the user
/// actually arranged, so those are saved instead of the live geometry.
/// </summary>
private void PersistWindowBounds()
{
if (
!TryGetPersistableBounds(
_isMinimalUi,
WindowState,
_restoreBounds,
RestoreBounds,
Comment thread
DysektAI marked this conversation as resolved.
new Rect(Left, Top, Width, Height),
out Rect bounds
)
)
return;

RatConfig.LastWindowPositionX = (int)bounds.X;
RatConfig.LastWindowPositionY = (int)bounds.Y;
RatConfig.LastWindowWidth = (int)bounds.Width;
RatConfig.LastWindowHeight = (int)bounds.Height;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

internal static bool TryGetPersistableBounds(
bool isMinimalUi,
WindowState windowState,
Rect minimalRestoreBounds,
Rect stateRestoreBounds,
Rect liveBounds,
out Rect bounds
)
{
bounds =
isMinimalUi ? minimalRestoreBounds
: windowState != WindowState.Normal ? stateRestoreBounds
: liveBounds;
return !bounds.IsEmpty
&& double.IsFinite(bounds.X)
&& double.IsFinite(bounds.Y)
&& double.IsFinite(bounds.Width)
&& double.IsFinite(bounds.Height)
&& bounds.Width >= MinimumWidth
&& bounds.Height >= MinimumHeight;
}

private void OnToggleSidebar(object? sender, RoutedEventArgs e)
{
_appStateService?.ToggleSidebar();
Expand Down
6 changes: 6 additions & 0 deletions src/App/RatConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ internal static bool LogDebug
internal static event Action? SettingsChanged;
internal static int LastWindowPositionX = int.MinValue;
internal static int LastWindowPositionY = int.MinValue;
internal static int LastWindowWidth = 0;
internal static int LastWindowHeight = 0;
internal static WindowMode LastWindowMode = WindowMode.Normal;

internal static float GameScale => RatScannerMain.Instance.RatEyeEngine.Config.ProcessingConfig.Scale;
Expand Down Expand Up @@ -390,6 +392,8 @@ private static void LoadConfig(string configPath, bool showMigrationMessage)

LastWindowPositionX = config.ReadInt(nameof(LastWindowPositionX), LastWindowPositionX);
LastWindowPositionY = config.ReadInt(nameof(LastWindowPositionY), LastWindowPositionY);
LastWindowWidth = config.ReadInt(nameof(LastWindowWidth), LastWindowWidth);
LastWindowHeight = config.ReadInt(nameof(LastWindowHeight), LastWindowHeight);
LastWindowMode = (WindowMode)config.ReadInt(nameof(LastWindowMode), (int)LastWindowMode);

if (GameDisplayPreferencesStore.TryRead(config, ScreenWidth, ScreenHeight, ScreenScale, out var preferences))
Expand Down Expand Up @@ -521,6 +525,8 @@ internal static void SaveConfig(string configPath)
config.WriteInt(nameof(ConfigVersion), ConfigVersion);
config.WriteInt(nameof(LastWindowPositionX), LastWindowPositionX);
config.WriteInt(nameof(LastWindowPositionY), LastWindowPositionY);
config.WriteInt(nameof(LastWindowWidth), LastWindowWidth);
config.WriteInt(nameof(LastWindowHeight), LastWindowHeight);
config.WriteInt(nameof(LastWindowMode), (int)LastWindowMode);

GameDisplayPreferencesStore.Write(config, GetGameDisplayPreferences());
Expand Down
Loading