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) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-29 09:00:33 -05:00
co-authored by Claude Opus 5
parent 16ce4dfbea
commit 40b00ddde1
5 changed files with 250 additions and 85 deletions
+146 -51
View File
@@ -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, &current))
{
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"
"# <title>=<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;
}