Persist window size, position, state and sort order between sessions

Added functionality to automatically save and restore the main window's position, size, (maximized) state, and sort order for list and gallery views between sessions. Introduced new properties in SettingsManager for window and sort state persistence. Updated MainForm to load and save these settings on startup, close, and sort changes, and added default/fallback behavior. Modified ModernListView to suppress header drawing and interaction during initialization
This commit is contained in:
jp64k
2025-12-31 04:54:11 +01:00
parent 3993e574a8
commit fdb091cee6
3 changed files with 285 additions and 2 deletions

View File

@@ -239,6 +239,9 @@ namespace AndroidSideloader
// Subscribe to click events to unfocus search text box // Subscribe to click events to unfocus search text box
this.Click += UnfocusSearchTextBox; this.Click += UnfocusSearchTextBox;
// Load saved window state
LoadWindowState();
} }
private void CheckCommandLineArguments() private void CheckCommandLineArguments()
@@ -411,8 +414,11 @@ namespace AndroidSideloader
} }
}); });
// Basic UI setup // Basic UI setup - only center if no saved position
CenterToScreen(); if (this.StartPosition != FormStartPosition.Manual)
{
CenterToScreen();
}
gamesListView.View = View.Details; gamesListView.View = View.Details;
gamesListView.FullRowSelect = true; gamesListView.FullRowSelect = true;
gamesListView.GridLines = false; gamesListView.GridLines = false;
@@ -2464,6 +2470,12 @@ namespace AndroidSideloader
loaded = true; loaded = true;
Logger.Log($"initListView total completed in {sw.ElapsedMilliseconds}ms"); Logger.Log($"initListView total completed in {sw.ElapsedMilliseconds}ms");
// Show header now that loading is complete
if (_listViewRenderer != null)
{
_listViewRenderer.SuppressHeader = false;
}
// Now that ListView is fully populated and _allItems is initialized, // Now that ListView is fully populated and _allItems is initialized,
// switch to the user's preferred view // switch to the user's preferred view
this.Invoke(() => this.Invoke(() =>
@@ -4020,6 +4032,9 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
private async void Form1_FormClosing(object sender, FormClosingEventArgs e) private async void Form1_FormClosing(object sender, FormClosingEventArgs e)
{ {
// Save window state before closing
SaveWindowState();
// Cleanup DNS helper (stops proxy) // Cleanup DNS helper (stops proxy)
DnsHelper.Cleanup(); DnsHelper.Cleanup();
@@ -4697,6 +4712,9 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
// Invalidate header to update sort indicators // Invalidate header to update sort indicators
gamesListView.Invalidate(new Rectangle(0, 0, gamesListView.ClientSize.Width, gamesListView.Invalidate(new Rectangle(0, 0, gamesListView.ClientSize.Width,
gamesListView.Font.Height + 8)); gamesListView.Font.Height + 8));
// Save sort state
SaveWindowState();
} }
private void CheckEnter(object sender, System.Windows.Forms.KeyPressEventArgs e) private void CheckEnter(object sender, System.Windows.Forms.KeyPressEventArgs e)
@@ -6153,6 +6171,15 @@ function onYouTubeIframeAPIReady() {
selectedReleaseName = selectedItem.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text; selectedReleaseName = selectedItem.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text;
} }
} }
else if (isGalleryView && _fastGallery != null && _fastGallery._selectedIndex >= 0)
{
// Capture selection from gallery view
var galleryItem = _fastGallery.GetItemAtIndex(_fastGallery._selectedIndex);
if (galleryItem != null && galleryItem.SubItems.Count > 1)
{
selectedReleaseName = galleryItem.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text;
}
}
// Capture current sort state before switching // Capture current sort state before switching
if (isGalleryView && _fastGallery != null) if (isGalleryView && _fastGallery != null)
@@ -6223,6 +6250,22 @@ function onYouTubeIframeAPIReady() {
// Apply shared sort state to list view // Apply shared sort state to list view
ApplySortToListView(); ApplySortToListView();
// Scroll to the previously selected item in list view
if (!string.IsNullOrEmpty(selectedReleaseName))
{
foreach (ListViewItem item in gamesListView.Items)
{
if (item.SubItems.Count > 1 &&
item.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text.Equals(selectedReleaseName, StringComparison.OrdinalIgnoreCase))
{
item.Selected = true;
item.Focused = true;
item.EnsureVisible();
break;
}
}
}
} }
} }
@@ -6336,6 +6379,9 @@ function onYouTubeIframeAPIReady() {
_sharedSortField = _fastGallery.CurrentSortField; _sharedSortField = _fastGallery.CurrentSortField;
_sharedSortDirection = _fastGallery.CurrentSortDirection; _sharedSortDirection = _fastGallery.CurrentSortDirection;
} }
// Save sort state
SaveWindowState();
} }
private void GamesGalleryView_Resize(object sender, EventArgs e) private void GamesGalleryView_Resize(object sender, EventArgs e)
@@ -7710,6 +7756,143 @@ function onYouTubeIframeAPIReady() {
speedLabel.Text = ""; speedLabel.Text = "";
} }
private void SaveWindowState()
{
try
{
// Save maximized state separately
settings.WindowMaximized = this.WindowState == FormWindowState.Maximized;
// Save normal bounds (not maximized bounds)
if (this.WindowState == FormWindowState.Normal)
{
settings.WindowX = this.Location.X;
settings.WindowY = this.Location.Y;
settings.WindowWidth = this.Size.Width;
settings.WindowHeight = this.Size.Height;
}
else if (this.WindowState == FormWindowState.Maximized)
{
settings.WindowX = this.RestoreBounds.X;
settings.WindowY = this.RestoreBounds.Y;
settings.WindowWidth = this.RestoreBounds.Width;
settings.WindowHeight = this.RestoreBounds.Height;
}
// Capture current sort state from active view before saving
if (isGalleryView && _fastGallery != null)
{
_sharedSortField = _fastGallery.CurrentSortField;
_sharedSortDirection = _fastGallery.CurrentSortDirection;
}
else if (!isGalleryView && lvwColumnSorter != null)
{
_sharedSortField = ColumnIndexToSortField(lvwColumnSorter.SortColumn);
SortDirection listDirection = lvwColumnSorter.Order == SortOrder.Ascending
? SortDirection.Ascending
: SortDirection.Descending;
// Flip popularity when capturing from list view
if (_sharedSortField == SortField.Popularity)
{
_sharedSortDirection = listDirection == SortDirection.Ascending
? SortDirection.Descending
: SortDirection.Ascending;
}
else
{
_sharedSortDirection = listDirection;
}
}
// Save sort state
settings.SortColumn = SortFieldToColumnIndex(_sharedSortField);
settings.SortAscending = _sharedSortDirection == SortDirection.Ascending;
settings.Save();
}
catch (Exception ex)
{
Logger.Log($"Failed to save window state: {ex.Message}", LogLevel.WARNING);
}
}
private void LoadWindowState()
{
try
{
// Load window position and size
if (settings.WindowWidth > 0 && settings.WindowHeight > 0)
{
// Validate that the saved position is on a visible screen
Rectangle savedBounds = new Rectangle(
settings.WindowX,
settings.WindowY,
settings.WindowWidth,
settings.WindowHeight);
bool isOnScreen = false;
foreach (Screen screen in Screen.AllScreens)
{
if (screen.WorkingArea.IntersectsWith(savedBounds))
{
isOnScreen = true;
break;
}
}
if (isOnScreen)
{
this.StartPosition = FormStartPosition.Manual;
this.Location = new Point(settings.WindowX, settings.WindowY);
this.Size = new Size(settings.WindowWidth, settings.WindowHeight);
if (settings.WindowMaximized)
{
this.WindowState = FormWindowState.Maximized;
}
}
else
{
// Saved position is off-screen, use defaults
this.StartPosition = FormStartPosition.CenterScreen;
}
}
// Load sort state
_sharedSortField = ColumnIndexToSortField(settings.SortColumn);
_sharedSortDirection = settings.SortAscending ? SortDirection.Ascending : SortDirection.Descending;
// Apply to list view sorter (with popularity flip)
if (settings.SortColumn >= 0 && settings.SortColumn < gamesListView.Columns.Count)
{
lvwColumnSorter.SortColumn = settings.SortColumn;
// For popularity, flip direction for list view
SortDirection effectiveDirection = _sharedSortDirection;
if (_sharedSortField == SortField.Popularity)
{
effectiveDirection = _sharedSortDirection == SortDirection.Ascending
? SortDirection.Descending
: SortDirection.Ascending;
}
lvwColumnSorter.Order = effectiveDirection == SortDirection.Ascending
? SortOrder.Ascending
: SortOrder.Descending;
}
}
catch (Exception ex)
{
Logger.Log($"Failed to load window state: {ex.Message}", LogLevel.WARNING);
this.StartPosition = FormStartPosition.CenterScreen;
lvwColumnSorter.SortColumn = 0;
lvwColumnSorter.Order = SortOrder.Ascending;
_sharedSortField = SortField.Name;
_sharedSortDirection = SortDirection.Ascending;
}
}
} }
public static class ControlExtensions public static class ControlExtensions

View File

@@ -122,9 +122,31 @@ namespace AndroidSideloader
[DllImport("user32.dll")] [DllImport("user32.dll")]
private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern bool InvalidateRect(IntPtr hWnd, IntPtr lpRect, bool bErase);
[DllImport("gdi32.dll")] [DllImport("gdi32.dll")]
private static extern bool DeleteObject(IntPtr hObject); private static extern bool DeleteObject(IntPtr hObject);
private bool _suppressHeader = true;
public bool SuppressHeader
{
get => _suppressHeader;
set
{
if (_suppressHeader == value) return;
_suppressHeader = value;
// Invalidate ListView and header control
_listView.Invalidate();
if (_headerCursor.Handle != IntPtr.Zero)
{
InvalidateRect(_headerCursor.Handle, IntPtr.Zero, true);
}
}
}
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
private struct HDHITTESTINFO private struct HDHITTESTINFO
{ {
@@ -178,6 +200,57 @@ namespace AndroidSideloader
protected override void WndProc(ref Message m) protected override void WndProc(ref Message m)
{ {
const int WM_LBUTTONDOWN = 0x0201;
const int WM_LBUTTONUP = 0x0202;
const int WM_LBUTTONDBLCLK = 0x0203;
const int WM_RBUTTONDOWN = 0x0204;
const int WM_RBUTTONUP = 0x0205;
// Block mouse interaction when header is suppressed, but still handle layout
if (_owner._suppressHeader)
{
// Block mouse clicks
if (m.Msg == WM_LBUTTONDOWN || m.Msg == WM_LBUTTONUP || m.Msg == WM_LBUTTONDBLCLK ||
m.Msg == WM_RBUTTONDOWN || m.Msg == WM_RBUTTONUP)
{
m.Result = IntPtr.Zero;
return;
}
if (m.Msg == WM_SETCURSOR)
{
Cursor.Current = Cursors.Default;
m.Result = (IntPtr)1;
return;
}
// Still handle HDM_LAYOUT to maintain custom header height
if (m.Msg == HDM_LAYOUT)
{
base.WndProc(ref m);
try
{
HDLAYOUT hdl = Marshal.PtrToStructure<HDLAYOUT>(m.LParam);
WINDOWPOS wpos = Marshal.PtrToStructure<WINDOWPOS>(hdl.pwpos);
RECT rc = Marshal.PtrToStructure<RECT>(hdl.prc);
wpos.cy = CUSTOM_HEADER_HEIGHT;
rc.top = CUSTOM_HEADER_HEIGHT;
Marshal.StructureToPtr(wpos, hdl.pwpos, false);
Marshal.StructureToPtr(rc, hdl.prc, false);
}
catch { }
m.Result = (IntPtr)1;
return;
}
base.WndProc(ref m);
return;
}
if (m.Msg == WM_ERASEBKGND) if (m.Msg == WM_ERASEBKGND)
{ {
m.Result = (IntPtr)1; m.Result = (IntPtr)1;
@@ -610,6 +683,14 @@ namespace AndroidSideloader
var g = e.Graphics; var g = e.Graphics;
int listViewWidth = _listView.ClientSize.Width; int listViewWidth = _listView.ClientSize.Width;
// During loading, just fill with background color
if (_suppressHeader)
{
g.FillRectangle(RowNormalBrush, new Rectangle(0, e.Bounds.Y, listViewWidth, e.Bounds.Height));
e.DrawDefault = false;
return;
}
g.FillRectangle(HeaderBgBrush, e.Bounds); g.FillRectangle(HeaderBgBrush, e.Bounds);
if (e.ColumnIndex == _listView.Columns.Count - 1) if (e.ColumnIndex == _listView.Columns.Count - 1)

View File

@@ -137,6 +137,18 @@ namespace AndroidSideloader.Utilities
public string ProxyPort { get; set; } = string.Empty; public string ProxyPort { get; set; } = string.Empty;
public bool TrailersEnabled { get; set; } = true; public bool TrailersEnabled { get; set; } = true;
public bool UseGalleryView { get; set; } = true; public bool UseGalleryView { get; set; } = true;
// Window state persistence
public int WindowX { get; set; } = -1;
public int WindowY { get; set; } = -1;
public int WindowWidth { get; set; } = -1;
public int WindowHeight { get; set; } = -1;
public bool WindowMaximized { get; set; } = false;
// Sort state persistence
public int SortColumn { get; set; } = 0;
public bool SortAscending { get; set; } = true;
private SettingsManager() private SettingsManager()
{ {
Load(); Load();
@@ -261,6 +273,13 @@ namespace AndroidSideloader.Utilities
ProxyPort = string.Empty; ProxyPort = string.Empty;
TrailersEnabled = true; TrailersEnabled = true;
UseGalleryView = true; UseGalleryView = true;
WindowX = -1;
WindowY = -1;
WindowWidth = -1;
WindowHeight = -1;
WindowMaximized = false;
SortColumn = 0;
SortAscending = true;
Save(); Save();
Debug.WriteLine("Default settings created."); Debug.WriteLine("Default settings created.");