Playtest: the half-height red buttons did not read well; back to display_height/8 (18..40px). The contiguous amber map columns stay. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
423 lines
10 KiB
C++
423 lines
10 KiB
C++
#include "mungal4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "l4mfdview.h"
|
|
#include "l4padrio.h"
|
|
|
|
namespace
|
|
{
|
|
const char mfdViewClass[] = "RPMFDView";
|
|
|
|
const int buttonGap = 4;
|
|
|
|
//---------------------------------------------------------------
|
|
// 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_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;
|
|
|
|
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;
|
|
|
|
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);
|
|
}
|
|
else
|
|
{
|
|
DEBUG_STREAM << "MFDSplitView: window creation failed (" << title << ")\n" << std::flush;
|
|
}
|
|
}
|
|
|
|
MFDSplitView::~MFDSplitView()
|
|
{
|
|
Check_Pointer(this);
|
|
|
|
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;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// 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; RIO addresses descend
|
|
// from the anchor, row-major (vRIO CockpitLayout::Mfd).
|
|
//-----------------------------------------------------------
|
|
int strip_h = display_height / 8;
|
|
if (strip_h < 18) strip_h = 18;
|
|
if (strip_h > 40) strip_h = 40;
|
|
|
|
int button_w = (display_width - 5 * buttonGap) / 4;
|
|
|
|
displayY = strip_h + 2 * buttonGap;
|
|
*client_height = display_height + 2 * (strip_h + 2 * buttonGap);
|
|
|
|
for (int i = 0; i < 8; ++i)
|
|
{
|
|
int row = i / 4; // 0 = top strip, 1 = bottom strip
|
|
int col = i % 4;
|
|
|
|
ScreenButton *button = &buttons[buttonCount++];
|
|
button->unit = anchorA - i;
|
|
button->amber = 0;
|
|
button->x = buttonGap + col * (button_w + buttonGap);
|
|
button->y = row ? (displayY + displayH + buttonGap) : buttonGap;
|
|
button->w = button_w;
|
|
button->h = strip_h;
|
|
}
|
|
}
|
|
else if (button_style == SideColumns)
|
|
{
|
|
//-----------------------------------------------------------
|
|
// 6 buttons down each side of the glass, stacked contiguous
|
|
// like the pod's strips (the pod wires only 6 of each
|
|
// column's 8 addresses to buttons; the rest are Tesla
|
|
// relays). Left = anchorA ascending, right = anchorB.
|
|
//-----------------------------------------------------------
|
|
int col_w = display_width / 8;
|
|
if (col_w < 20) col_w = 20;
|
|
if (col_w > 40) col_w = 40;
|
|
|
|
int button_h = display_height / 6;
|
|
|
|
displayX = col_w + 2 * buttonGap;
|
|
*client_width = display_width + 2 * (col_w + 2 * buttonGap);
|
|
|
|
for (int i = 0; i < 6; ++i)
|
|
{
|
|
ScreenButton *left = &buttons[buttonCount++];
|
|
left->unit = anchorA + i;
|
|
left->amber = 1;
|
|
left->x = buttonGap;
|
|
left->y = i * button_h;
|
|
left->w = col_w;
|
|
left->h = button_h;
|
|
|
|
ScreenButton *right = &buttons[buttonCount++];
|
|
right->unit = anchorB + i;
|
|
right->amber = 1;
|
|
right->x = displayX + displayW + buttonGap;
|
|
right->y = left->y;
|
|
right->w = col_w;
|
|
right->h = button_h;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MFDSplitView::Paint()
|
|
{
|
|
PAINTSTRUCT ps;
|
|
HDC hdc = BeginPaint((HWND) window, &ps);
|
|
|
|
RECT client;
|
|
GetClientRect((HWND) window, &client);
|
|
FillRect(hdc, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
|
|
|
SetStretchBltMode(hdc, COLORONCOLOR);
|
|
StretchDIBits(
|
|
hdc,
|
|
displayX, displayY, displayW, displayH,
|
|
0, 0, sourceWidth, sourceHeight,
|
|
pixels, (const BITMAPINFO *) blitHeader,
|
|
DIB_RGB_COLORS, SRCCOPY
|
|
);
|
|
|
|
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(hdc, &rect, fill);
|
|
DeleteObject(fill);
|
|
|
|
HBRUSH frame = CreateSolidBrush(
|
|
(i == pressedIndex) ? RGB(255, 255, 255) : RGB(48, 48, 48));
|
|
FrameRect(hdc, &rect, frame);
|
|
DeleteObject(frame);
|
|
}
|
|
|
|
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.
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
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);
|
|
}
|
|
}
|
|
|
|
void
|
|
MFDSplitView::Repaint()
|
|
{
|
|
Check_Pointer(this);
|
|
if (window != NULL)
|
|
{
|
|
RedrawWindow((HWND) window, NULL, NULL,
|
|
RDW_INVALIDATE | RDW_UPDATENOW);
|
|
}
|
|
}
|