From 40b00ddde1b28c82b34d9770e25df1d48b597317 Mon Sep 17 00:00:00 2001 From: Cyd Date: Wed, 29 Jul 2026 09:00:33 -0500 Subject: [PATCH] The game window remembers where you put it too RP412MFDLAYOUT already kept the exploded view's display panes where they were dragged. The main window is the one people move most, and it was still being placed fresh every launch, so it joins them. MFDSplitView_LoadLayout/SaveLayout become RPWindowLayout_Load/Save, with Register/Forget taking any HWND rather than the module reaching into a pane registry. Same file, same format, one more line in it. What comes back depends on the window, so Register takes it as a flag: display panes position only, as before. A pane's size follows its content and its button banks, so an old size from a different build must not distort it. the game window position and size in the cockpit view. Nothing derives that size - the cockpit fits itself to whatever client area it is given - so a window sized to suit a monitor should come back that way, and half-restoring it would be the strange behaviour. In the exploded view its size IS the render resolution -res asked for, so there only the position returns. Registered after the CockpitShellProc subclass is installed, on purpose: the restore's WM_SIZE then runs LayoutCockpit again and the canvas re-fits the restored client area. -fit does not register at all - it owns the whole monitor, so there is no placement of the player's to keep. CockpitShellProc gained the WM_EXITSIZEMOVE hook the panes already had, so dragging or resizing the shell writes the file immediately rather than waiting for teardown. Two hazards the panes were small enough to get away with and the game window is not: - Save reads rcNormalPosition rather than GetWindowRect. A minimised window reports a nonsense rect and a maximised one reports the screen; since the file is rewritten whole, either would have replaced a good line with a useless one. rcNormalPosition is the restored placement whatever state the window is in. - Load drops any placement that intersects none of the monitors currently plugged in. Restoring the game window onto a display that is no longer there would leave nothing to drag back. Verified by round trip in both views. Cockpit: dragged and resized to 240,120 1000x620, the file took it, a fresh launch in load mode came up exactly there with a 984x581 client - and a screenshot confirms the canvas re-fit it, displays at the corners and the map centred at the bottom, nothing spilling. Exploded: the shell came back at 60,60 still 1280x720 from -res while Map came back at 777,333, which is the size-flag split doing its job. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 5 +- MUNGA_L4/L4MFDVIEW.cpp | 197 ++++++++++++++++++++++++++++++----------- MUNGA_L4/L4MFDVIEW.h | 58 ++++++++---- MUNGA_L4/L4VB16.cpp | 55 ++++++++++-- pack-dist.ps1 | 20 +++-- 5 files changed, 250 insertions(+), 85 deletions(-) diff --git a/.gitignore b/.gitignore index 4360b5d..fff37de 100644 --- a/.gitignore +++ b/.gitignore @@ -40,8 +40,9 @@ ipch/ *.log rpl4.log -# Where the exploded view's panes were last dragged to (RP412MFDLAYOUT). -# Per-machine by nature - it is screen coordinates on somebody's desk. +# Where the game window and the exploded view's panes were last dragged +# to (RP412MFDLAYOUT). Per-machine by nature - it is screen coordinates +# on somebody's desk. mfd_layout.cfg # Build-output static libs that land in lib/ (the two committed dependency diff --git a/MUNGA_L4/L4MFDVIEW.cpp b/MUNGA_L4/L4MFDVIEW.cpp index 03bae49..27ac97a 100644 --- a/MUNGA_L4/L4MFDVIEW.cpp +++ b/MUNGA_L4/L4MFDVIEW.cpp @@ -18,10 +18,16 @@ namespace const char layoutFileName[] = "mfd_layout.cfg"; - // the panes that own a desktop window, so Save can walk them - enum { maxLayoutPanes = 8 }; - MFDSplitView *layoutPanes[maxLayoutPanes]; - int layoutPaneCount = 0; + // every window taking part, so Save can walk them + enum { maxLayoutWindows = 12 }; + struct LayoutWindow + { + void *window; + const char *title; + Logical withSize; + }; + LayoutWindow layoutWindows[maxLayoutWindows]; + int layoutWindowCount = 0; int LayoutMode() { @@ -160,7 +166,7 @@ namespace // than trusting teardown, so a hard kill still leaves the // layout that was on screen. No-op unless the mode is save. // - MFDSplitView_SaveLayout(); + RPWindowLayout_Save(); return 0; case WM_CLOSE: @@ -289,10 +295,11 @@ MFDSplitView::MFDSplitView( ShowWindow((HWND) window, SW_SHOWNOACTIVATE); // only the exploded view's own windows can be dragged, so only - // those have a position worth remembering - if (ownWindow && layoutPaneCount < maxLayoutPanes) + // those have a placement worth remembering. Position only - see + // RPWindowLayout_Register. + if (ownWindow) { - layoutPanes[layoutPaneCount++] = this; + RPWindowLayout_Register(window, paneTitle, False); } } else @@ -306,15 +313,7 @@ MFDSplitView::~MFDSplitView() Check_Pointer(this); // out of the layout registry before the window goes - for (int i = 0; i < layoutPaneCount; ++i) - { - if (layoutPanes[i] == this) - { - layoutPanes[i] = layoutPanes[--layoutPaneCount]; - layoutPanes[layoutPaneCount] = NULL; - break; - } - } + RPWindowLayout_Forget(window); if (window != NULL) { @@ -335,18 +334,61 @@ Logical } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// Sticky placement: restore saved positions over the computed ones. -// -// Called after the exploded view has built and placed every pane, so a -// pane the file does not mention simply keeps where the layout put it. -// Position only - the saved size is read and discarded, because a pane's -// size comes from its content and its button banks, and letting an old -// w,h back in would misshape it after any geometry change. +// Join the saved layout. Registering twice just refreshes the entry, so +// a window re-registering after being rebuilt is harmless. //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void - MFDSplitView_LoadLayout() + RPWindowLayout_Register(void *window, const char *title, Logical with_size) { - if (LayoutMode() == LayoutOff || layoutPaneCount == 0) + if (window == NULL || title == NULL) + { + return; + } + for (int i = 0; i < layoutWindowCount; ++i) + { + if (layoutWindows[i].window == window) + { + layoutWindows[i].title = title; + layoutWindows[i].withSize = with_size; + return; + } + } + if (layoutWindowCount >= maxLayoutWindows) + { + return; + } + layoutWindows[layoutWindowCount].window = window; + layoutWindows[layoutWindowCount].title = title; + layoutWindows[layoutWindowCount].withSize = with_size; + ++layoutWindowCount; +} + +void + RPWindowLayout_Forget(void *window) +{ + for (int i = 0; i < layoutWindowCount; ++i) + { + if (layoutWindows[i].window == window) + { + layoutWindows[i] = layoutWindows[--layoutWindowCount]; + layoutWindows[layoutWindowCount].window = NULL; + layoutWindows[layoutWindowCount].title = NULL; + return; + } + } +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Sticky placement: restore the saved placement over the computed one. +// +// Called once everything has been built and placed, so a window the file +// does not mention simply keeps where it was put. Size comes back only +// for windows registered with it - see the header. +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +void + RPWindowLayout_Load() +{ + if (LayoutMode() == LayoutOff || layoutWindowCount == 0) { return; } @@ -393,33 +435,75 @@ void continue; } - for (int i = 0; i < layoutPaneCount; ++i) + for (int i = 0; i < layoutWindowCount; ++i) { - MFDSplitView *pane = layoutPanes[i]; - if (pane != NULL && pane->Title() != NULL && - !strcmp(pane->Title(), cursor)) + LayoutWindow &entry = layoutWindows[i]; + if (entry.title == NULL || strcmp(entry.title, cursor) != 0) + { + continue; + } + HWND target = (HWND) entry.window; + if (target == NULL || !IsWindow(target)) { - pane->SetPosition(x, y); - ++restored; break; } + + // a saved size of nothing is a corrupt line, not an instruction + Logical size_it = (entry.withSize && w > 0 && h > 0) ? True : False; + + // + // Saved coordinates belong to whatever monitors were plugged + // in that day. If the placement lands on none of today's, drop + // it and keep the computed one - restoring the game window + // somewhere invisible would leave nothing to drag back. + // + RECT landing; + landing.left = x; + landing.top = y; + if (size_it) + { + landing.right = x + w; + landing.bottom = y + h; + } + else + { + RECT current; + if (!GetWindowRect(target, ¤t)) + { + break; + } + landing.right = x + (current.right - current.left); + landing.bottom = y + (current.bottom - current.top); + } + if (MonitorFromRect(&landing, MONITOR_DEFAULTTONULL) == NULL) + { + DEBUG_STREAM << "MFDLayout: " << cursor << " was saved at " + << x << "," << y << " - off every monitor now, ignoring\n" + << std::flush; + break; + } + + SetWindowPos(target, NULL, x, y, w, h, + SWP_NOZORDER | SWP_NOACTIVATE | (size_it ? 0 : SWP_NOSIZE)); + ++restored; + break; } } fclose(file); - DEBUG_STREAM << "MFDLayout: restored " << restored << " pane position(s) from " + DEBUG_STREAM << "MFDLayout: restored " << restored << " window placement(s) from " << layoutFileName << "\n" << std::flush; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// Write where every open pane currently is. The whole file each time - it -// is a handful of lines, and a rewrite leaves nothing half-written for a -// hard kill to catch. +// Write where every registered window currently is. The whole file each +// time - it is a handful of lines, and a rewrite leaves nothing +// half-written for a hard kill to catch. //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void - MFDSplitView_SaveLayout() + RPWindowLayout_Save() { - if (LayoutMode() != LayoutSave || layoutPaneCount == 0) + if (LayoutMode() != LayoutSave || layoutWindowCount == 0) { return; } @@ -432,30 +516,41 @@ void return; } - fputs("# RP412 exploded-view pane placement. RP412MFDLAYOUT=save writes\n" - "# this on each finished drag and on exit; =load restores it.\n" - "# =<x>,<y>,<w>,<h> - the size is written for reference and\n" - "# ignored on load.\n", file); + fputs("# RP412 window placement. RP412MFDLAYOUT=save writes this on each\n" + "# finished drag and on exit; =load restores it.\n" + "# <title>=<x>,<y>,<w>,<h>. The game window gets its size back too;\n" + "# for the display panes the size is reference only, since theirs\n" + "# follows their content.\n", file); int wrote = 0; - for (int i = 0; i < layoutPaneCount; ++i) + for (int i = 0; i < layoutWindowCount; ++i) { - MFDSplitView *pane = layoutPanes[i]; - if (pane == NULL || pane->Title() == NULL) + LayoutWindow &entry = layoutWindows[i]; + if (entry.title == NULL) { continue; } - HWND pane_window = (HWND) pane->Window(); - if (pane_window == NULL || !IsWindow(pane_window)) + HWND entry_window = (HWND) entry.window; + if (entry_window == NULL || !IsWindow(entry_window)) { continue; } - RECT bounds; - if (!GetWindowRect(pane_window, &bounds)) + // + // rcNormalPosition rather than GetWindowRect: a minimised window + // reports a nonsense rect and a maximised one reports the screen, + // and neither is what to come back to. This is the restored + // placement whatever state the window is in, so quitting from + // maximised still records where the window will reappear. + // + WINDOWPLACEMENT placement; + memset(&placement, 0, sizeof(placement)); + placement.length = sizeof(placement); + if (!GetWindowPlacement(entry_window, &placement)) { continue; } - fprintf(file, "%s=%ld,%ld,%ld,%ld\n", pane->Title(), + RECT bounds = placement.rcNormalPosition; + fprintf(file, "%s=%ld,%ld,%ld,%ld\n", entry.title, (long) bounds.left, (long) bounds.top, (long) (bounds.right - bounds.left), (long) (bounds.bottom - bounds.top)); @@ -463,7 +558,7 @@ void } fclose(file); - DEBUG_STREAM << "MFDLayout: saved " << wrote << " pane position(s) to " + DEBUG_STREAM << "MFDLayout: saved " << wrote << " window placement(s) to " << layoutFileName << "\n" << std::flush; } diff --git a/MUNGA_L4/L4MFDVIEW.h b/MUNGA_L4/L4MFDVIEW.h index a349b4f..f605f26 100644 --- a/MUNGA_L4/L4MFDVIEW.h +++ b/MUNGA_L4/L4MFDVIEW.h @@ -167,30 +167,58 @@ protected: }; //######################################################################## -// Sticky window placement for the exploded view (L4MFDSPLIT=2). +// Sticky window placement. // -// Those panes are draggable desktop windows, but the arrangement is -// recomputed on every launch, so dragging one somewhere useful never -// survived the menu-race-menu loop. RP412MFDLAYOUT persists it to -// mfd_layout.cfg beside bindings.txt: +// The game window and, in the exploded view, each display pane are all +// draggable, but their placement is recomputed on every launch - so +// putting a window somewhere useful never survived the menu-race-menu +// loop. RP412MFDLAYOUT remembers it, in mfd_layout.cfg beside +// bindings.txt: // -// off / 0 / unset computed arrangement only, no file (default) -// load / restore restore saved positions at startup, never write +// off / 0 / unset computed placement only, no file (default) +// load / restore restore saved placement at startup, never write // save / adjust restore, then rewrite on each finished drag and // on teardown - the round trip // -// One "<title>=x,y,w,h" line per pane. Position is restored and size -// ignored: a pane's size follows its content and its button banks, so an -// old w,h from a different build must not distort it. +// One "<title>=x,y,w,h" line per window. +// +// Whether the size comes back depends on the window, which is why +// Register takes it as a flag: +// +// display panes position only. A pane's size follows its content and +// its button banks, so an old size from a different +// build must not distort it. +// the game window position, and size in the cockpit view. Nothing +// derives that size - the cockpit fits itself to +// whatever client area it is given - so a window sized +// to suit a monitor should come back that way, and +// half-restoring it would be the strange behaviour. In +// the exploded view its size IS the render resolution +// -res asked for, so there only the position returns. +// +// A placement that lands on none of the monitors currently plugged in is +// ignored rather than applied - restoring the game window off screen +// would leave nothing to drag back. +// +// Lives here because the display panes were the first to need it; the +// game window joined later rather than grow a second copy of the same +// file format. // // Ported from BT411's BT_GLASS_LAYOUT. //######################################################################## -// Apply saved positions over the computed ones. Call after the panes are -// built and placed. +// Take part in the saved layout. title is the key in the file and must +// outlive the window (the callers pass string literals). void - MFDSplitView_LoadLayout(); + RPWindowLayout_Register(void *window, const char *title, Logical with_size); +void + RPWindowLayout_Forget(void *window); -// Write every open pane's position. No-op unless the mode is save. +// Apply the saved placement over the computed one. Call once everything +// has been built and placed. void - MFDSplitView_SaveLayout(); + RPWindowLayout_Load(); + +// Write every registered window's placement. No-op unless mode is save. +void + RPWindowLayout_Save(); diff --git a/MUNGA_L4/L4VB16.cpp b/MUNGA_L4/L4VB16.cpp index 6d75ef6..d34d1a8 100644 --- a/MUNGA_L4/L4VB16.cpp +++ b/MUNGA_L4/L4VB16.cpp @@ -3839,6 +3839,15 @@ static LRESULT CALLBACK cockpit->LayoutCockpit(LOWORD(lParam), HIWORD(lParam)); } } + else if (message == WM_EXITSIZEMOVE) + { + // + // The shell was just dragged or resized. Write the arrangement now + // rather than trusting teardown, so a hard kill still leaves what + // was on screen. No-op unless RP412MFDLAYOUT is save. + // + RPWindowLayout_Save(); + } return CallWindowProcA(gCockpitBaseProc, hwnd, message, wParam, lParam); } @@ -4459,11 +4468,24 @@ SVGA16::SVGA16( splitView[SplitMap]->SetPosition(work.left + map_x, work.top + map_y); // - // ...and then let RP412MFDLAYOUT put back anywhere they were - // dragged to last time. Panes the file does not mention keep the + // The game window joins them, position only: here its size is the + // render resolution -res asked for, so restoring an old one would + // leave a stretched back buffer in a window the wrong size. + // + { + HWND shell = ApplicationManager::GetCurrentManager()->GetHWnd(); + if (shell != NULL) + { + RPWindowLayout_Register(shell, "RPL4", False); + } + } + + // + // ...and then let RP412MFDLAYOUT put everything back where it was + // dragged to last time. Windows the file does not mention keep the // arrangement just computed above. // - MFDSplitView_LoadLayout(); + RPWindowLayout_Load(); } else if (splitViews) { @@ -4675,6 +4697,23 @@ SVGA16::SVGA16( // catch maximise / restore / drag-resize and re-fit gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA( cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc); + + // + // Sticky placement for the shell. Position AND size: nothing + // derives that size here - -res only decides how sharp the + // scene is - so a window sized to suit a monitor should come + // back that way. Registered after the subclass on purpose, so + // the restore's WM_SIZE runs LayoutCockpit again and the + // canvas re-fits the restored client area. + // + // -fit sits it borderless over the whole monitor, so there is + // no placement of the player's to remember. + // + if (!fit_display) + { + RPWindowLayout_Register(cockpit, "RPL4", True); + RPWindowLayout_Load(); + } } else { @@ -4871,12 +4910,12 @@ SVGA16::~SVGA16() //SVGASetSplitterClock(False); // - // Backstop for the sticky exploded-view placement: every finished drag - // has already been written, but a pane moved and then closed straight - // away would otherwise be missed. Must run before the panes go - it - // reads their live window rects. + // Backstop for the sticky placement: every finished drag has already + // been written, but a window moved and then closed straight away would + // otherwise be missed. Must run before the panes go - it reads their + // live window rects, and the game window's. // - MFDSplitView_SaveLayout(); + RPWindowLayout_Save(); for (int view = 0; view < SplitViewCount; ++view) { diff --git a/pack-dist.ps1 b/pack-dist.ps1 index 0895180..fb46cbc 100644 --- a/pack-dist.ps1 +++ b/pack-dist.ps1 @@ -166,16 +166,18 @@ L4PLASMA=SCREEN # 480x640 - decoded exactly as the pod's VDB split them, no downscale). L4MFDSPLIT=1 -# In the exploded view (L4MFDSPLIT=2) each display is a window you can -# drag, but the arrangement is recomputed every launch, so moving one -# somewhere useful never survived the menu-race-menu loop. This remembers -# it, in mfd_layout.cfg beside this file: -# off / 0 / unset computed arrangement only, no file (default) -# load put the panes back where they were saved +# The game window - and in the exploded view (L4MFDSPLIT=2) each display +# window - is placed fresh every launch, so moving one somewhere useful +# never survived the menu-race-menu loop. This remembers where you put +# them, in mfd_layout.cfg beside this file: +# off / 0 / unset computed placement only, no file (default) +# load put the windows back where they were saved # save the same, and re-save on every finished drag -# Only positions are remembered - a pane's size follows its content and -# its button banks, so an old size is never restored over it. Drag the -# displays where you want them once with save, then leave it on load. +# The game window gets its size back too, so you can size the cockpit to +# suit your monitor once and keep it. The display windows get position +# only: their size follows their content and their button banks, so an +# old one is never restored over them. Arrange everything once with +# save, then leave it on load. #RP412MFDLAYOUT=off # Size of the six secondary displays in the glass cockpit, as a