#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; }; LayoutWindow layoutWindows[maxLayoutWindows]; int layoutWindowCount = 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 // 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; //--------------------------------------------------------------- // 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; ++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; } 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; } 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 << " 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" "# =<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 < 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; 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)); ++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; 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; 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); } }