Ports BattleTech 4.11's button-protrusion knob to Red Planet, DEFAULT 0 -- nothing changes unless a player asks for it. BT411 shipped this at 10 because its players asked for longer lamps; Red Planet keeps the pod's geometry until someone wants otherwise, so the two games stay tuned separately. Mechanism: the bonus is added to `indicatorStrip` (10) at the two places it is consumed -- strip_h for the MFD strips, strip_w for the map's side columns. Everything else already derives from those two locals (the client size, the display offset, both banks), so the window grows to fit and no other code needs to know. Only the strip moves, so the button keeps its width and its reach behind the glass: a LONGER lamp bar, not a fatter face, and the press target and the picture over it are unchanged. Scope is the red MFD strips and the amber side columns -- the only two ButtonStyles that have glass to hide behind. Value rules match BT411's: unset -> 0; a number is that many pixels; negative clamps to 0, over 200 clamps to 200; a WORD parses to 0, i.e. off. A non-zero value logs 'MFDView: RP412MFDPROTRUDE=N (indicator strip 10 -> N+10)' once. Named for this repo's convention (RP412-prefixed, no underscores), following RP412MFDLAYOUT -- itself the port of BT411's BT_GLASS_LAYOUT. Verified live in dist with the freshly linked rpl4opt.exe: RP412MFDPROTRUDE=25 prints the receipt and the exploded windows lay out around the longer strips; UNSET prints nothing at all, i.e. bonus 0 and geometry byte-identical to before this commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
952 lines
27 KiB
C++
952 lines
27 KiB
C++
#include "mungal4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "l4mfdview.h"
|
|
#include "l4padrio.h"
|
|
|
|
namespace
|
|
{
|
|
const char mfdViewClass[] = "RPMFDView";
|
|
|
|
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";
|
|
|
|
// every window taking part, so Save can walk them
|
|
enum { maxLayoutWindows = 12 };
|
|
struct LayoutWindow
|
|
{
|
|
void *window;
|
|
const char *title;
|
|
Logical withSize;
|
|
Logical bare; // frame stripped, per the file
|
|
};
|
|
LayoutWindow layoutWindows[maxLayoutWindows];
|
|
int layoutWindowCount = 0;
|
|
|
|
//---------------------------------------------------------------
|
|
// Take a window's frame off: no title bar, no border, nothing to
|
|
// drag it by. WS_SYSMENU stays - it draws nothing once the caption
|
|
// is gone, but without one DefWindowProc will not honour Alt+F4,
|
|
// and a bare window with no way to close it is a trap.
|
|
//---------------------------------------------------------------
|
|
void StripWindowFrame(HWND window)
|
|
{
|
|
LONG_PTR style = GetWindowLongPtrA(window, GWL_STYLE);
|
|
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX |
|
|
WS_MAXIMIZEBOX | WS_BORDER | WS_DLGFRAME);
|
|
style |= WS_POPUP;
|
|
SetWindowLongPtrA(window, GWL_STYLE, style);
|
|
}
|
|
|
|
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
|
|
// edge - the rest sits behind the picture, which is what the
|
|
// player actually clicks. Applies to both the MFD strips (depth
|
|
// measured vertically) and the map's side columns (horizontally).
|
|
//---------------------------------------------------------------
|
|
// 240 against the 480 glass: the two banks reach in far enough to
|
|
// meet in the middle bar the strips' own width, so practically the
|
|
// whole display is a press target and neither bank overlaps the
|
|
// other. The clamps below keep that true on a scaled-down cockpit.
|
|
const int buttonDepth = 240;
|
|
const int indicatorStrip = 10;
|
|
|
|
//---------------------------------------------------------------
|
|
// Extra pixels of indicator strip, on top of the 10 above, so a
|
|
// FLASHING lamp is easier to notice on desktop glass - the pod's
|
|
// buttons were physical and backlit and never had that problem.
|
|
// Only the strip moves, so the button keeps its width and reaches
|
|
// the same distance behind the glass: this makes the lamp a
|
|
// LONGER bar, not a fatter face. The client size, the display
|
|
// offset and both banks all derive from strip_h/strip_w below, so
|
|
// the window grows to fit and nothing needs to know about it.
|
|
//
|
|
// DEFAULT 0 - Red Planet keeps the pod's geometry unless a player
|
|
// asks otherwise. (BattleTech 4.11 carries the same knob defaulted
|
|
// to 10, where the players asked for it; the two games are tuned
|
|
// separately on purpose.) Ported alongside RP412MFDLAYOUT, which
|
|
// took the same route from BT411's BT_GLASS_LAYOUT.
|
|
//---------------------------------------------------------------
|
|
const int defaultProtrude = 0;
|
|
|
|
int ProtrudeBonus()
|
|
{
|
|
static int bonus = -1;
|
|
if (bonus < 0)
|
|
{
|
|
const char *setting = getenv("RP412MFDPROTRUDE");
|
|
bonus = (setting != NULL && setting[0] != '\0')
|
|
? atoi(setting) // a word parses to 0, i.e. off
|
|
: defaultProtrude;
|
|
if (bonus < 0) bonus = 0;
|
|
if (bonus > 200) bonus = 200;
|
|
|
|
if (bonus != 0)
|
|
{
|
|
DEBUG_STREAM << "MFDView: RP412MFDPROTRUDE=" << bonus
|
|
<< " (indicator strip " << indicatorStrip << " -> "
|
|
<< (indicatorStrip + bonus) << ")\n" << std::flush;
|
|
}
|
|
}
|
|
return bonus;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// The map paints its own legend beside each side button, and that
|
|
// grid is not the naive height/6: measured off the 640-tall map,
|
|
// the first cell starts 13 rows down and the six cells are 102
|
|
// tall on a 105 pitch (13 + 6*102 + 5*3 = 640). Spacing the
|
|
// buttons evenly from the top instead left every one of them
|
|
// riding high of its label. Scaled to the live glass height.
|
|
//---------------------------------------------------------------
|
|
const int mapLegendSpan = 640;
|
|
const int mapLegendTop = 13;
|
|
const int mapLegendCell = 102;
|
|
const int mapLegendPitch = 105;
|
|
|
|
//---------------------------------------------------------------
|
|
// Button fill colors by lamp brightness (0 off / 1-2 dim / 3
|
|
// bright), red family for MFD strips, amber for the side columns.
|
|
//---------------------------------------------------------------
|
|
COLORREF ButtonFill(int amber, int level)
|
|
{
|
|
if (amber)
|
|
{
|
|
if (level >= 3) return RGB(255, 210, 48);
|
|
if (level >= 1) return RGB(150, 124, 26);
|
|
return RGB(64, 52, 10);
|
|
}
|
|
if (level >= 3) return RGB(255, 72, 44);
|
|
if (level >= 1) return RGB(150, 44, 28);
|
|
return RGB(64, 18, 12);
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Lamp byte -> current brightness level, animating flash modes.
|
|
// Low 2 bits: solid/slow/med/fast; bits 2-3: state 1 brightness;
|
|
// bits 4-5: state 2 brightness (the flash alternate).
|
|
//---------------------------------------------------------------
|
|
int LampLevel(int lamp_state)
|
|
{
|
|
int mode = lamp_state & 0x03;
|
|
int level1 = (lamp_state >> 2) & 0x03;
|
|
int level2 = (lamp_state >> 4) & 0x03;
|
|
|
|
if (mode == 0)
|
|
{
|
|
return level1;
|
|
}
|
|
|
|
static const int half_period[4] = { 0, 500, 250, 125 };
|
|
return ((GetTickCount() / half_period[mode]) & 1) ? level2 : level1;
|
|
}
|
|
|
|
LRESULT CALLBACK
|
|
MFDViewWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
MFDSplitView *view =
|
|
(MFDSplitView *) GetWindowLongPtrA(hwnd, GWLP_USERDATA);
|
|
|
|
switch (message)
|
|
{
|
|
case WM_PAINT:
|
|
if (view != NULL)
|
|
{
|
|
view->Paint();
|
|
return 0;
|
|
}
|
|
// no instance (e.g. the plain viewscreen child): default paint
|
|
break;
|
|
|
|
case WM_LBUTTONDOWN:
|
|
if (view != NULL)
|
|
{
|
|
view->MouseDown((int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam));
|
|
return 0;
|
|
}
|
|
break;
|
|
|
|
case WM_LBUTTONUP:
|
|
case WM_CAPTURECHANGED:
|
|
if (view != NULL)
|
|
{
|
|
view->MouseUp();
|
|
return 0;
|
|
}
|
|
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.
|
|
//
|
|
RPWindowLayout_Save();
|
|
return 0;
|
|
|
|
case WM_CLOSE:
|
|
// Part of the cockpit; just hide it.
|
|
ShowWindow(hwnd, SW_HIDE);
|
|
return 0;
|
|
|
|
case WM_ERASEBKGND:
|
|
if (view != NULL)
|
|
{
|
|
return 1;
|
|
}
|
|
break; // viewscreen child: erase with the class black brush
|
|
}
|
|
return DefWindowProcA(hwnd, message, wParam, lParam);
|
|
}
|
|
}
|
|
|
|
//########################################################################
|
|
//############################ MFDSplitView ##############################
|
|
//########################################################################
|
|
|
|
MFDSplitView::MFDSplitView(
|
|
const char *title,
|
|
int source_width,
|
|
int source_height,
|
|
int display_width,
|
|
int display_height,
|
|
int x,
|
|
int y,
|
|
ButtonStyle button_style,
|
|
int anchorA,
|
|
int anchorB,
|
|
void *parent
|
|
)
|
|
{
|
|
Check_Pointer(this);
|
|
|
|
sourceWidth = source_width;
|
|
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));
|
|
|
|
BITMAPINFOHEADER *header = new BITMAPINFOHEADER;
|
|
memset(header, 0, sizeof(BITMAPINFOHEADER));
|
|
header->biSize = sizeof(BITMAPINFOHEADER);
|
|
header->biWidth = source_width;
|
|
header->biHeight = -source_height; // top-down
|
|
header->biPlanes = 1;
|
|
header->biBitCount = 32;
|
|
header->biCompression = BI_RGB;
|
|
blitHeader = header;
|
|
|
|
buttonStyle = button_style;
|
|
buttonAnchorA = anchorA;
|
|
buttonAnchorB = anchorB;
|
|
|
|
int client_width = display_width;
|
|
int client_height = display_height;
|
|
LayoutButtons(button_style, anchorA, anchorB,
|
|
display_width, display_height, &client_width, &client_height);
|
|
clientWidth = client_width;
|
|
clientHeight = client_height;
|
|
|
|
HINSTANCE instance = GetModuleHandleA(NULL);
|
|
|
|
static Logical class_registered = False;
|
|
if (!class_registered)
|
|
{
|
|
class_registered = True;
|
|
|
|
WNDCLASSA window_class;
|
|
memset(&window_class, 0, sizeof(window_class));
|
|
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
|
window_class.lpfnWndProc = MFDViewWndProc;
|
|
window_class.hInstance = instance;
|
|
window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
|
|
window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
|
|
window_class.lpszClassName = mfdViewClass;
|
|
RegisterClassA(&window_class);
|
|
}
|
|
|
|
DWORD style;
|
|
int window_w, window_h;
|
|
|
|
if (parent != NULL)
|
|
{
|
|
// chrome-less pane inside the cockpit window; clips siblings so
|
|
// overlapping panes and the viewscreen never paint over it
|
|
style = WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS;
|
|
window_w = client_width;
|
|
window_h = client_height;
|
|
}
|
|
else
|
|
{
|
|
style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
|
|
RECT bounds;
|
|
bounds.left = 0;
|
|
bounds.top = 0;
|
|
bounds.right = client_width;
|
|
bounds.bottom = client_height;
|
|
AdjustWindowRect(&bounds, style, FALSE);
|
|
window_w = bounds.right - bounds.left;
|
|
window_h = bounds.bottom - bounds.top;
|
|
}
|
|
|
|
window = CreateWindowExA(
|
|
0,
|
|
mfdViewClass,
|
|
title,
|
|
style,
|
|
x, y,
|
|
window_w,
|
|
window_h,
|
|
(HWND) parent, NULL, instance, NULL
|
|
);
|
|
|
|
if (window != NULL)
|
|
{
|
|
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 placement worth remembering. Position only - see
|
|
// RPWindowLayout_Register.
|
|
if (ownWindow)
|
|
{
|
|
RPWindowLayout_Register(window, paneTitle, False);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DEBUG_STREAM << "MFDSplitView: window creation failed (" << title << ")\n" << std::flush;
|
|
}
|
|
}
|
|
|
|
MFDSplitView::~MFDSplitView()
|
|
{
|
|
Check_Pointer(this);
|
|
|
|
// out of the layout registry before the window goes
|
|
RPWindowLayout_Forget(window);
|
|
|
|
if (window != NULL)
|
|
{
|
|
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, 0);
|
|
DestroyWindow((HWND) window);
|
|
window = NULL;
|
|
}
|
|
delete (BITMAPINFOHEADER *) blitHeader;
|
|
blitHeader = NULL;
|
|
delete [] pixels;
|
|
pixels = NULL;
|
|
}
|
|
|
|
Logical
|
|
MFDSplitView::TestInstance() const
|
|
{
|
|
return pixels != NULL;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Join the saved layout. Registering twice just refreshes the entry, so
|
|
// a window re-registering after being rebuilt is harmless.
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
void
|
|
RPWindowLayout_Register(void *window, const char *title, Logical with_size)
|
|
{
|
|
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;
|
|
layoutWindows[layoutWindowCount].bare = False;
|
|
++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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
//
|
|
// Optional trailing keyword, "x,y,w,h,noframe". Searched for
|
|
// rather than parsed in place so a hand-edited line survives a
|
|
// stray space or a missing comma.
|
|
//
|
|
Logical bare = (strstr(equals + 1, "noframe") != NULL) ? True : False;
|
|
|
|
for (int i = 0; i < layoutWindowCount; ++i)
|
|
{
|
|
LayoutWindow &entry = layoutWindows[i];
|
|
if (entry.title == NULL || strcmp(entry.title, cursor) != 0)
|
|
{
|
|
continue;
|
|
}
|
|
HWND target = (HWND) entry.window;
|
|
if (target == NULL || !IsWindow(target))
|
|
{
|
|
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;
|
|
}
|
|
|
|
//
|
|
// Frame off, if the line asked. A bare window's rect IS its
|
|
// client rect, so whatever client area it has now is the size
|
|
// to hand back - which also makes the round trip stable: once
|
|
// bare, what Save records is already the client.
|
|
//
|
|
entry.bare = bare;
|
|
if (bare)
|
|
{
|
|
if (!size_it)
|
|
{
|
|
RECT client;
|
|
if (GetClientRect(target, &client))
|
|
{
|
|
w = client.right;
|
|
h = client.bottom;
|
|
size_it = True;
|
|
}
|
|
}
|
|
StripWindowFrame(target);
|
|
}
|
|
|
|
SetWindowPos(target, NULL, x, y, w, h,
|
|
SWP_NOZORDER | SWP_NOACTIVATE |
|
|
(size_it ? 0 : SWP_NOSIZE) | (bare ? SWP_FRAMECHANGED : 0));
|
|
++restored;
|
|
break;
|
|
}
|
|
}
|
|
fclose(file);
|
|
|
|
DEBUG_STREAM << "MFDLayout: restored " << restored << " window placement(s) from "
|
|
<< layoutFileName << "\n" << std::flush;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// 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
|
|
RPWindowLayout_Save()
|
|
{
|
|
if (LayoutMode() != LayoutSave || layoutWindowCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FILE *file = fopen(layoutFileName, "wt");
|
|
if (file == NULL)
|
|
{
|
|
DEBUG_STREAM << "MFDLayout: could not write " << layoutFileName
|
|
<< "\n" << std::flush;
|
|
return;
|
|
}
|
|
|
|
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"
|
|
"# Append ,noframe to take that window's title bar and border off.\n"
|
|
"# Put it where you want it first - a bare window has nothing to\n"
|
|
"# drag by, so its line stops changing once the frame is gone.\n", file);
|
|
|
|
int wrote = 0;
|
|
for (int i = 0; i < layoutWindowCount; ++i)
|
|
{
|
|
LayoutWindow &entry = layoutWindows[i];
|
|
if (entry.title == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
HWND entry_window = (HWND) entry.window;
|
|
if (entry_window == NULL || !IsWindow(entry_window))
|
|
{
|
|
continue;
|
|
}
|
|
//
|
|
// 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;
|
|
}
|
|
RECT bounds = placement.rcNormalPosition;
|
|
// the flag is the player's instruction, not something measured off
|
|
// the window, so a rewrite has to carry it back out
|
|
fprintf(file, "%s=%ld,%ld,%ld,%ld%s\n", entry.title,
|
|
(long) bounds.left, (long) bounds.top,
|
|
(long) (bounds.right - bounds.left),
|
|
(long) (bounds.bottom - bounds.top),
|
|
entry.bare ? ",noframe" : "");
|
|
++wrote;
|
|
}
|
|
fclose(file);
|
|
|
|
DEBUG_STREAM << "MFDLayout: saved " << wrote << " window placement(s) to "
|
|
<< layoutFileName << "\n" << std::flush;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Compute the display rectangle and the button rectangles, growing the
|
|
// client area to make room for the strips.
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
void
|
|
MFDSplitView::LayoutButtons(
|
|
ButtonStyle button_style,
|
|
int anchorA,
|
|
int anchorB,
|
|
int display_width,
|
|
int display_height,
|
|
int *client_width,
|
|
int *client_height
|
|
)
|
|
{
|
|
displayX = 0;
|
|
displayY = 0;
|
|
displayW = display_width;
|
|
displayH = display_height;
|
|
|
|
if (button_style == MFDStrips)
|
|
{
|
|
//-----------------------------------------------------------
|
|
// 4 buttons above the glass, 4 below, flush to the pane
|
|
// edges (no border against the viewscreen behind); RIO
|
|
// addresses descend from the anchor, row-major (vRIO
|
|
// CockpitLayout::Mfd).
|
|
//
|
|
// The banks reach BEHIND the display: each button is
|
|
// mfdButtonHeight tall but only mfdIndicatorStrip of it
|
|
// clears the glass edge, so the lamp reads as a thin strip
|
|
// and the picture above/below it is the click target. Paint
|
|
// draws the buttons before the MFD image for that reason.
|
|
//-----------------------------------------------------------
|
|
int strip_h = indicatorStrip + ProtrudeBonus();
|
|
int button_h = buttonDepth;
|
|
|
|
// on a scaled-down cockpit, keep the two banks from meeting
|
|
int limit = (display_height + 2 * strip_h) / 2;
|
|
if (button_h > limit) button_h = limit;
|
|
if (button_h < strip_h) button_h = strip_h;
|
|
|
|
int button_w = (display_width - 3 * buttonGap) / 4;
|
|
|
|
displayY = strip_h;
|
|
*client_height = display_height + 2 * strip_h;
|
|
|
|
for (int i = 0; i < 8; ++i)
|
|
{
|
|
int row = i / 4; // 0 = top bank, 1 = bottom bank
|
|
int col = i % 4;
|
|
|
|
ScreenButton *button = &buttons[buttonCount++];
|
|
button->unit = anchorA - i;
|
|
button->amber = 0;
|
|
button->x = col * (button_w + buttonGap);
|
|
button->y = row
|
|
? (displayY + displayH + strip_h - button_h)
|
|
: 0;
|
|
button->w = (col == 3)
|
|
? (display_width - 3 * (button_w + buttonGap))
|
|
: button_w;
|
|
button->h = button_h;
|
|
}
|
|
}
|
|
else if (button_style == SideColumns)
|
|
{
|
|
//-----------------------------------------------------------
|
|
// 6 buttons down each side of the glass (the pod wires only 6
|
|
// of each column's 8 addresses to buttons; the rest are Tesla
|
|
// relays). Left = anchorA ascending, right = anchorB.
|
|
//
|
|
// Same trick as the MFD banks, turned on its side: each
|
|
// button reaches buttonDepth in behind the map and leaves an
|
|
// indicatorStrip clearing the edge, so the lamp reads as a
|
|
// slim column and the map itself is the click target.
|
|
//-----------------------------------------------------------
|
|
int strip_w = indicatorStrip + ProtrudeBonus();
|
|
int button_w = buttonDepth;
|
|
|
|
// keep the two columns from meeting behind a narrow map
|
|
int limit = (display_width + 2 * strip_w) / 2;
|
|
if (button_w > limit) button_w = limit;
|
|
if (button_w < strip_w) button_w = strip_w;
|
|
|
|
displayX = strip_w;
|
|
*client_width = display_width + 2 * strip_w;
|
|
|
|
for (int i = 0; i < 6; ++i)
|
|
{
|
|
//-------------------------------------------------------
|
|
// Take top and bottom off the legend grid separately and
|
|
// subtract: scaling a height directly would let rounding
|
|
// drift the buttons out of step with the labels.
|
|
//-------------------------------------------------------
|
|
int cell_top = mapLegendTop + i * mapLegendPitch;
|
|
int top = (cell_top * display_height) / mapLegendSpan;
|
|
int bottom =
|
|
((cell_top + mapLegendCell) * display_height) / mapLegendSpan;
|
|
int h = bottom - top;
|
|
if (h < 1) h = 1;
|
|
|
|
ScreenButton *left = &buttons[buttonCount++];
|
|
left->unit = anchorA + i;
|
|
left->amber = 1;
|
|
left->x = 0;
|
|
left->y = top;
|
|
left->w = button_w;
|
|
left->h = h;
|
|
|
|
ScreenButton *right = &buttons[buttonCount++];
|
|
right->unit = anchorB + i;
|
|
right->amber = 1;
|
|
right->x = displayX + displayW + strip_w - button_w;
|
|
right->y = left->y;
|
|
right->w = button_w;
|
|
right->h = h;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MFDSplitView::Paint()
|
|
{
|
|
PAINTSTRUCT ps;
|
|
HDC hdc = BeginPaint((HWND) window, &ps);
|
|
|
|
RECT client;
|
|
GetClientRect((HWND) window, &client);
|
|
|
|
//---------------------------------------------------------------
|
|
// Double-buffered: compose the whole pane off screen and blit it
|
|
// in one operation, otherwise the clear-then-draw sequence
|
|
// flickers at the repaint cadence.
|
|
//---------------------------------------------------------------
|
|
HDC mem = CreateCompatibleDC(hdc);
|
|
HBITMAP surface = CreateCompatibleBitmap(hdc, client.right, client.bottom);
|
|
HBITMAP old_surface = (HBITMAP) SelectObject(mem, surface);
|
|
|
|
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
|
|
|
//---------------------------------------------------------------
|
|
// Buttons first, glass second: the MFD banks extend under the
|
|
// display so the picture is the click target, and the image
|
|
// painted over them leaves only their indicator strips showing.
|
|
//---------------------------------------------------------------
|
|
for (int i = 0; i < buttonCount; ++i)
|
|
{
|
|
const ScreenButton *button = &buttons[i];
|
|
|
|
int level = PadRIO::IsActive()
|
|
? LampLevel(PadRIO::GetLampState(button->unit))
|
|
: 0;
|
|
|
|
RECT rect;
|
|
rect.left = button->x;
|
|
rect.top = button->y;
|
|
rect.right = button->x + button->w;
|
|
rect.bottom = button->y + button->h;
|
|
|
|
HBRUSH fill = CreateSolidBrush(ButtonFill(button->amber, level));
|
|
FillRect(mem, &rect, fill);
|
|
DeleteObject(fill);
|
|
|
|
HBRUSH frame = CreateSolidBrush(
|
|
(i == pressedIndex) ? RGB(255, 255, 255) : RGB(48, 48, 48));
|
|
FrameRect(mem, &rect, frame);
|
|
DeleteObject(frame);
|
|
}
|
|
|
|
// HALFTONE area-averages the downscale (the compact glass shows the
|
|
// full 640x480 canvas at ~half size); COLORONCOLOR dropped every other
|
|
// row/column, which shredded the 1-bit vector strokes and small text.
|
|
// HALFTONE requires the brush origin be set (MSDN).
|
|
SetStretchBltMode(mem, HALFTONE);
|
|
SetBrushOrgEx(mem, 0, 0, NULL);
|
|
StretchDIBits(
|
|
mem,
|
|
displayX, displayY, displayW, displayH,
|
|
0, 0, sourceWidth, sourceHeight,
|
|
pixels, (const BITMAPINFO *) blitHeader,
|
|
DIB_RGB_COLORS, SRCCOPY
|
|
);
|
|
|
|
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
|
|
|
|
SelectObject(mem, old_surface);
|
|
DeleteObject(surface);
|
|
DeleteDC(mem);
|
|
|
|
EndPaint((HWND) window, &ps);
|
|
}
|
|
|
|
void
|
|
MFDSplitView::MouseDown(int x, int y)
|
|
{
|
|
for (int i = 0; i < buttonCount; ++i)
|
|
{
|
|
const ScreenButton *button = &buttons[i];
|
|
if (x >= button->x && x < button->x + button->w &&
|
|
y >= button->y && y < button->y + button->h)
|
|
{
|
|
pressedIndex = i;
|
|
SetCapture((HWND) window);
|
|
PadRIO::SetScreenButton(button->unit, True);
|
|
Repaint();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MFDSplitView::MouseUp()
|
|
{
|
|
if (pressedIndex >= 0)
|
|
{
|
|
PadRIO::SetScreenButton(buttons[pressedIndex].unit, False);
|
|
pressedIndex = -1;
|
|
if (GetCapture() == (HWND) window)
|
|
{
|
|
ReleaseCapture();
|
|
}
|
|
Repaint();
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Paint NOW rather than queueing an invalidation: the game loop pumps
|
|
// one message per frame, and queued WM_PAINTs (lowest priority) starve
|
|
// behind it - panes would freeze on their first frame.
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Off screen, and stay off: the pane keeps its pixels and geometry, it
|
|
// simply stops being shown. Repaint() on a hidden window is harmless.
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
void
|
|
MFDSplitView::Hide()
|
|
{
|
|
Check_Pointer(this);
|
|
if (window != NULL)
|
|
{
|
|
ShowWindow((HWND) window, SW_HIDE);
|
|
}
|
|
}
|
|
|
|
void
|
|
MFDSplitView::SetPosition(int x, int y)
|
|
{
|
|
Check_Pointer(this);
|
|
if (window != NULL)
|
|
{
|
|
SetWindowPos((HWND) window, NULL, x, y, 0, 0,
|
|
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Re-scale to a new glass size: the pixel buffer keeps its native
|
|
// source resolution, so only the destination rectangle, the button
|
|
// geometry and the window size change.
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
void
|
|
MFDSplitView::Resize(int display_width, int display_height)
|
|
{
|
|
Check_Pointer(this);
|
|
|
|
buttonCount = 0;
|
|
pressedIndex = -1;
|
|
|
|
int client_width = display_width;
|
|
int client_height = display_height;
|
|
LayoutButtons(buttonStyle, buttonAnchorA, buttonAnchorB,
|
|
display_width, display_height, &client_width, &client_height);
|
|
clientWidth = client_width;
|
|
clientHeight = client_height;
|
|
|
|
if (window != NULL)
|
|
{
|
|
SetWindowPos((HWND) window, NULL, 0, 0,
|
|
client_width, client_height,
|
|
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
}
|
|
}
|
|
|
|
void
|
|
MFDSplitView::Repaint()
|
|
{
|
|
Check_Pointer(this);
|
|
if (window != NULL)
|
|
{
|
|
RedrawWindow((HWND) window, NULL, NULL,
|
|
RDW_INVALIDATE | RDW_UPDATENOW);
|
|
}
|
|
}
|