Exploded view remembers where you drag its windows

Ported from BT411's BT_GLASS_LAYOUT (29c502d).

The exploded view's 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:

  off / 0 / unset   computed arrangement only, no file (default)
  load / restore    restore saved positions at startup, never write
  save / adjust     restore, then rewrite on each finished drag
                    (WM_EXITSIZEMOVE) and on teardown

One "<title>=x,y,w,h" line per pane. Position is restored and the size
read and discarded: a pane's size follows its content and its button
banks, so letting an old size back in would misshape it after any
geometry change - and this port has changed that geometry twice already.

Load runs after the computed arrangement rather than instead of it, so a
pane the file does not mention simply keeps its computed spot. Only the
exploded view registers: the composited cockpit's panes are chrome-less
children with nothing to drag, so they have no position worth keeping.

RP412 needs no equivalent of BT411's "restored" flag. Its re-snap is
LayoutCockpit on WM_SIZE, which only runs in cockpit mode, so nothing
comes back later to overwrite a hand-placed window.

Verified by round trip: dragged Map to 777,333 in save mode, the file
took all six panes, and a fresh launch in load mode put it physically
back at 777,333. The harness also resized the window while moving it,
which incidentally proved the saved size really is ignored - the pane
came back correctly sized from a cfg that recorded 136x39.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-29 08:28:44 -05:00
co-authored by Claude Fable 5
parent 2a23ec0923
commit 16ce4dfbea
5 changed files with 285 additions and 0 deletions
+208
View File
@@ -10,6 +10,52 @@ namespace
const int buttonGap = 4;
//---------------------------------------------------------------
// Sticky placement for the exploded view. See L4MFDVIEW.h; ported
// from BT411's BT_GLASS_LAYOUT.
//---------------------------------------------------------------
enum { LayoutOff = 0, LayoutLoad, LayoutSave };
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;
int LayoutMode()
{
static int mode = -1;
if (mode < 0)
{
const char *setting = getenv("RP412MFDLAYOUT");
if (setting == NULL || setting[0] == '\0')
{
mode = LayoutOff;
}
else if (!stricmp(setting, "off") || !stricmp(setting, "0"))
{
mode = LayoutOff;
}
else if (!stricmp(setting, "load") || !stricmp(setting, "restore"))
{
mode = LayoutLoad;
}
else // save / adjust / on / 1
{
mode = LayoutSave;
}
if (mode != LayoutOff)
{
DEBUG_STREAM << "MFDLayout: RP412MFDLAYOUT="
<< ((mode == LayoutSave) ? "save" : "load")
<< " (" << layoutFileName << ")\n" << std::flush;
}
}
return mode;
}
//---------------------------------------------------------------
// Button banks run UNDER the glass: a button reaches this far in
// from the display edge, but only the indicator strip clears that
@@ -108,6 +154,15 @@ namespace
}
break;
case WM_EXITSIZEMOVE:
//
// A drag just finished. Write the whole arrangement now rather
// than trusting teardown, so a hard kill still leaves the
// layout that was on screen. No-op unless the mode is save.
//
MFDSplitView_SaveLayout();
return 0;
case WM_CLOSE:
// Part of the cockpit; just hide it.
ShowWindow(hwnd, SW_HIDE);
@@ -148,6 +203,8 @@ MFDSplitView::MFDSplitView(
sourceHeight = source_height;
buttonCount = 0;
pressedIndex = -1;
paneTitle = title;
ownWindow = (parent == NULL) ? True : False;
pixels = new unsigned long[source_width * source_height];
memset(pixels, 0, source_width * source_height * sizeof(unsigned long));
@@ -230,6 +287,13 @@ MFDSplitView::MFDSplitView(
{
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this);
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)
{
layoutPanes[layoutPaneCount++] = this;
}
}
else
{
@@ -241,6 +305,17 @@ 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;
}
}
if (window != NULL)
{
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, 0);
@@ -259,6 +334,139 @@ Logical
return pixels != NULL;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void
MFDSplitView_LoadLayout()
{
if (LayoutMode() == LayoutOff || layoutPaneCount == 0)
{
return;
}
FILE *file = fopen(layoutFileName, "rt");
if (file == NULL)
{
DEBUG_STREAM << "MFDLayout: no " << layoutFileName
<< " yet (using the computed arrangement)\n" << std::flush;
return;
}
int restored = 0;
char line[256];
while (fgets(line, sizeof(line), file) != NULL)
{
char *cursor = line;
while (*cursor == ' ' || *cursor == '\t')
{
++cursor;
}
if (*cursor == '#' || *cursor == '\r' || *cursor == '\n' || *cursor == '\0')
{
continue;
}
// titles carry no '=', so the first one splits the line
char *equals = strchr(cursor, '=');
if (equals == NULL)
{
continue;
}
*equals = '\0';
char *end = equals;
while (end > cursor && (end[-1] == ' ' || end[-1] == '\t'))
{
--end;
}
*end = '\0';
int x = 0, y = 0, w = 0, h = 0;
if (sscanf(equals + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2)
{
continue;
}
for (int i = 0; i < layoutPaneCount; ++i)
{
MFDSplitView *pane = layoutPanes[i];
if (pane != NULL && pane->Title() != NULL &&
!strcmp(pane->Title(), cursor))
{
pane->SetPosition(x, y);
++restored;
break;
}
}
}
fclose(file);
DEBUG_STREAM << "MFDLayout: restored " << restored << " pane position(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.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void
MFDSplitView_SaveLayout()
{
if (LayoutMode() != LayoutSave || layoutPaneCount == 0)
{
return;
}
FILE *file = fopen(layoutFileName, "wt");
if (file == NULL)
{
DEBUG_STREAM << "MFDLayout: could not write " << layoutFileName
<< "\n" << std::flush;
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);
int wrote = 0;
for (int i = 0; i < layoutPaneCount; ++i)
{
MFDSplitView *pane = layoutPanes[i];
if (pane == NULL || pane->Title() == NULL)
{
continue;
}
HWND pane_window = (HWND) pane->Window();
if (pane_window == NULL || !IsWindow(pane_window))
{
continue;
}
RECT bounds;
if (!GetWindowRect(pane_window, &bounds))
{
continue;
}
fprintf(file, "%s=%ld,%ld,%ld,%ld\n", pane->Title(),
(long) bounds.left, (long) bounds.top,
(long) (bounds.right - bounds.left),
(long) (bounds.bottom - bounds.top));
++wrote;
}
fclose(file);
DEBUG_STREAM << "MFDLayout: saved " << wrote << " pane position(s) to "
<< layoutFileName << "\n" << std::flush;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Compute the display rectangle and the button rectangles, growing the
// client area to make room for the strips.