#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" "#