Skip to content
Merged
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
191 changes: 188 additions & 3 deletions src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,15 @@ public MainWindow()
InitializeComponent();
SetupTitleBar();

RestoreGeometry();

KeyDown += Window_KeyDown;
ViewModel.CurrentPageChanged += OnCurrentPageChanged;

Resized += (_, _) => _ = SaveGeometryAsync();
PositionChanged += (_, _) => _ = SaveGeometryAsync();
this.GetObservable(WindowStateProperty).Subscribe(state => { _ = SaveGeometryAsync(); });

_trayService = new TrayService(this);
_trayService.UpdateStatus();
}
Expand Down Expand Up @@ -117,6 +123,7 @@ protected override void OnClosing(WindowClosingEventArgs e)
return;
}

SaveGeometryNow();
AvaloniaAutoUpdater.ReleaseLockForAutoupdate_Window = true;
_trayService?.Dispose();
_trayService = null;
Expand Down Expand Up @@ -266,7 +273,6 @@ private void SetupTitleBar()
{
CreateResizeGrips();
}

}
}

Expand Down Expand Up @@ -441,7 +447,143 @@ static Border MakeGrip(MainWindow window, double width, double height,
};
return grip;
}
}

private async Task SaveGeometryAsync()
{
try
{
int oldWidth = (int)Width;
int oldHeight = (int)Height;
PixelPoint oldPosition = Position;
WindowState oldState = WindowState;
await Task.Delay(100);

if (oldWidth != (int)Width || oldHeight != (int)Height
|| oldPosition != Position || oldState != WindowState)
return;

SaveGeometryNow();
}
catch (Exception ex)
{
Logger.Error(ex);
}
}

private void SaveGeometryNow()
{
try
{
int state = WindowState == WindowState.Maximized ? 1 : 0;
string geometry = $"v2,{Position.X},{Position.Y},{(int)Width},{(int)Height},{state}";
Settings.SetValue(Settings.K.WindowGeometry, geometry);
}
catch (Exception ex)
{
Logger.Error(ex);
}
}

private void RestoreGeometry()
{
string geometry = Settings.GetValue(Settings.K.WindowGeometry);
if (string.IsNullOrEmpty(geometry))
return;

string[] items = geometry.Split(',');
if (items.Length is not (5 or 6))
{
Logger.Warn($"The restored geometry did not have a supported item count (found length was {items.Length})");
return;
}

int x, y, width, height, state;
try
{
if (items.Length == 6 && items[0] == "v2")
{
x = int.Parse(items[1]);
y = int.Parse(items[2]);
width = int.Parse(items[3]);
height = int.Parse(items[4]);
state = int.Parse(items[5]);
}
else
{
x = int.Parse(items[0]);
y = int.Parse(items[1]);
width = int.Parse(items[2]);
height = int.Parse(items[3]);
state = int.Parse(items[4]);
}
}
catch (Exception ex)
{
Logger.Error("Could not parse window geometry integers");
Logger.Error(ex);
return;
}

WindowStartupLocation = WindowStartupLocation.Manual;

if (state == 1)
{
// Mirror WinUI behaviour: don't reapply the saved (maximized) bounds, just
// maximize. The OS / Avalonia picks a sensible un-maximize restore size.
WindowState = WindowState.Maximized;
}
else if (IsRectangleFullyVisible(x, y, width, height))
{
Width = width;
Height = height;
Position = new PixelPoint(x, y);
}
else
{
Logger.Warn("Restored geometry was outside of desktop bounds");
}
}

private bool IsRectangleFullyVisible(int x, int y, int width, int height)
{
// Position is in screen pixels, Width/Height are DIPs. Scale width/height
// by the DPI of the screen that contains the saved position before comparing
// against the union of all monitor bounds (which Avalonia reports in pixels).
var screens = Screens?.All;
if (screens is null || screens.Count == 0)
return true;

int minX = int.MaxValue, minY = int.MaxValue;
int maxX = int.MinValue, maxY = int.MinValue;
double hostScaling = 1.0;
bool foundHost = false;

foreach (var screen in screens)
{
var bounds = screen.Bounds;
if (bounds.X < minX) minX = bounds.X;
if (bounds.Y < minY) minY = bounds.Y;
if (bounds.X + bounds.Width > maxX) maxX = bounds.X + bounds.Width;
if (bounds.Y + bounds.Height > maxY) maxY = bounds.Y + bounds.Height;

if (!foundHost && bounds.Contains(new PixelPoint(x, y)))
{
hostScaling = screen.Scaling;
foundHost = true;
}
}

if (!foundHost)
hostScaling = Screens?.Primary?.Scaling ?? 1.0;

int widthPx = (int)(width * hostScaling);
int heightPx = (int)(height * hostScaling);

if (x + 10 < minX || x + widthPx - 10 > maxX || y + 10 < minY || y + heightPx - 10 > maxY)
return false;

return true;
}

private void MinimizeButton_Click(object? sender, RoutedEventArgs e)
Expand Down Expand Up @@ -505,6 +647,32 @@ private static nint OnWindowsWndProc(nint hWnd, uint msg, nint wParam, nint lPar
mmi.ptMaxSize.Y = mi.rcWork.Bottom - mi.rcWork.Top;
if (mmi.ptMaxTrackSize.X < mmi.ptMaxSize.X) mmi.ptMaxTrackSize.X = mmi.ptMaxSize.X;
if (mmi.ptMaxTrackSize.Y < mmi.ptMaxSize.Y) mmi.ptMaxTrackSize.Y = mmi.ptMaxSize.Y;
// Set ptMinTrackSize to MinWidth/MinHeight in DIPs plus the real
// WS_THICKFRAME inset. Avalonia's own handler would omit the inset for
// BorderOnly (BorderThickness returns 0), letting the outer window shrink
// below the client minimum — Avalonia then grows it back via SetWindowPos
// pinning x, pushing the right edge → the window slides past MinWidth.
if (Instance is { } w)
{
uint dpi = NativeMethods.GetDpiForWindow(hWnd);
if (dpi == 0) dpi = 96;
double scale = dpi / 96.0;
uint style = (uint)NativeMethods.GetWindowLongPtr(hWnd, GWL_STYLE).ToInt64();

var frame = default(NativeMethods.RECT);
int frameW = 0, frameH = 0;
if (NativeMethods.AdjustWindowRectExForDpi(ref frame, style, false, 0, dpi))
{
frameW = (-frame.Left) + frame.Right;
frameH = (-frame.Top) + frame.Bottom;
}

int minX = (int)Math.Ceiling(w.MinWidth * scale) + frameW;
int minY = (int)Math.Ceiling(w.MinHeight * scale) + frameH;
if (mmi.ptMinTrackSize.X < minX) mmi.ptMinTrackSize.X = minX;
if (mmi.ptMinTrackSize.Y < minY) mmi.ptMinTrackSize.Y = minY;
}

Marshal.StructureToPtr(mmi, lParam, false);
handled = true;
return 0;
Expand All @@ -524,6 +692,13 @@ private static class NativeMethods
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)]
public static extern nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);

[DllImport("user32.dll")]
public static extern uint GetDpiForWindow(nint hWnd);

[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool AdjustWindowRectExForDpi(ref RECT lpRect, uint dwStyle, [MarshalAs(UnmanagedType.Bool)] bool bMenu, uint dwExStyle, uint dpi);

[DllImport("user32.dll")]
public static extern nint MonitorFromWindow(nint hwnd, uint dwFlags);

Expand Down Expand Up @@ -579,8 +754,18 @@ private void CloseButton_Click(object? sender, RoutedEventArgs e)

private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;

if (e.ClickCount == 2)
{
WindowState = WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
return;
}

BeginMoveDrag(e);
}

private void SearchBox_KeyDown(object? sender, KeyEventArgs e)
Expand Down
Loading