diff --git a/RP_L4/RPL4FE.cpp b/RP_L4/RPL4FE.cpp index 6c20b78..370ecc8 100644 --- a/RP_L4/RPL4FE.cpp +++ b/RP_L4/RPL4FE.cpp @@ -242,6 +242,13 @@ namespace HWND menuWindow; HWND nameEdit; + + // Every list group is a drop-down. They are owner-drawn so they + // keep the green-on-black panel look instead of arriving in + // system colours, and their labels are painted by PaintMenu. + HWND combo[GroupCount]; + RECT comboLabel[GroupCount]; + HFONT textFont; HFONT titleFont; HBRUSH editBrush; @@ -251,6 +258,9 @@ namespace }; FEState *gFE = NULL; + + // ItemName() reads this to answer with the right track list. + const int *gItemNameSelection = NULL; int gLastMissionSeconds = 0; // carried across races so cycling back to the menu keeps the @@ -613,9 +623,54 @@ namespace // Layout: three columns of lists + the launch button //--------------------------------------------------------------- + //--------------------------------------------------------------- + // The groups that became drop-downs, in the order they are laid + // out. Scenario is deliberately NOT one of them: it decides what + // the others contain, so it stays visible as a pair of buttons. + //--------------------------------------------------------------- + int ComboGroups(const int *selection, int *groups) + { + int n = 0; + groups[n++] = GroupMap; + groups[n++] = GroupTime; + groups[n++] = GroupWeather; + groups[n++] = GroupLength; + groups[n++] = GroupVehicle; + if (IsFootball(selection)) + { + groups[n++] = GroupTeam; + groups[n++] = GroupPosition; + } + else + { + groups[n++] = GroupColor; + groups[n++] = GroupBadge; + } + return n; + } + + // The combo id is its group, so CBN_SELCHANGE says which list moved. + const int kComboIdBase = 100; + + void DestroyCombos(FEState *fe) + { + for (int g = 0; g < GroupCount; ++g) + { + if (fe->combo[g] != NULL) + { + DestroyWindow(fe->combo[g]); + fe->combo[g] = NULL; + } + } + } + + // defined below, next to the catalogs it reads + const char *ItemName(int group, int index); + void LayoutMenu(FEState *fe, int client_w, int client_h) { fe->itemCount = 0; + gItemNameSelection = fe->selection; int row_h = client_h / 36; if (row_h < 18) row_h = 18; @@ -624,111 +679,71 @@ namespace int top = client_h / 7; //--------------------------------------------------------------- - // Fixed columns cannot hold this menu any more. The promoted - // resource file brought eleven vehicles and seven maps, and the - // settings column - scenario, map, time, weather, length - was - // already within 11px of the bottom at 800x600 before any of - // them arrived. So the groups FLOW: they fill a column, then - // start the next, and the layout takes as many columns as the - // content needs. Nothing here knows how many that will be, which - // is the point - adding a map or a vehicle can no longer push - // anything off the screen. + // Every list is a drop-down, so the menu no longer grows with the + // content: eight controls instead of ninety-odd rows. Two columns + // of label-over-box, settings on the left and loadout on the right, + // with the scenario left as visible buttons because it decides what + // the other lists contain. //--------------------------------------------------------------- - int map_count; - ActiveMaps(fe->selection, &map_count); - - struct FlowGroup { int group; int count; }; - FlowGroup flow[8]; - int flow_count = 0; - flow[flow_count].group = GroupScenario; - flow[flow_count++].count = FE_COUNT(kScenarios); - flow[flow_count].group = GroupMap; - flow[flow_count++].count = map_count; - flow[flow_count].group = GroupTime; - flow[flow_count++].count = FE_COUNT(kTimes); - flow[flow_count].group = GroupWeather; - flow[flow_count++].count = FE_COUNT(kWeather); - flow[flow_count].group = GroupLength; - flow[flow_count++].count = FE_COUNT(kLengths); - flow[flow_count].group = GroupVehicle; - flow[flow_count++].count = FE_COUNT(kVehicles); - if (IsFootball(fe->selection)) - { - flow[flow_count].group = GroupTeam; - flow[flow_count++].count = FE_COUNT(kTeams); - flow[flow_count].group = GroupPosition; - flow[flow_count++].count = FE_COUNT(kPositions); - } - else - { - flow[flow_count].group = GroupColor; - flow[flow_count++].count = FE_COUNT(kColors); - flow[flow_count].group = GroupBadge; - flow[flow_count++].count = FE_COUNT(kBadges); - } - - // Every column starts two rows down so the pilot name box has the - // same home on whichever column ends up last, and they all line - // up. The bottom is reserved for LAUNCH and the lobby buttons. - int content_top = top + 2 * row_h; - int bottom = client_h - (RPL4Lobby_Configured() ? 7 : 4) * row_h; - int rows_per_col = (bottom - content_top) / row_h; - if (rows_per_col < 1) rows_per_col = 1; - - // Pass one: how many columns does this content want? A group is - // preceded by a blank row unless it lands at the top of a column, - // and a group longer than a column simply continues into the next. - int columns = 1; - { - int row = 0; - for (int g = 0; g < flow_count; ++g) - { - if (row > 0) ++row; - for (int i = 0; i < flow[g].count; ++i) - { - if (row >= rows_per_col) { ++columns; row = 0; } - ++row; - } - } - } - - int margin = client_w / 20; - int gap = client_w / 50; - int col_w = (client_w - 2 * margin - (columns - 1) * gap) / columns; + int margin = client_w / 14; + int gap = client_w / 20; + int col_w = (client_w - 2 * margin - gap) / 2; if (col_w < 1) col_w = 1; + int col2 = margin + col_w + gap; - // Pass two: the same walk, placing items this time. - int last_col = 0; + int block_h = row_h * 5 / 2; // label, box, breathing room + + // scenario: buttons, top of the left column + int y = top; + fe->comboLabel[GroupScenario].left = margin; + fe->comboLabel[GroupScenario].right = margin + col_w; + fe->comboLabel[GroupScenario].top = y; + fe->comboLabel[GroupScenario].bottom = y + row_h; + y += row_h; + for (int i = 0; i < FE_COUNT(kScenarios); ++i) { - int column = 0, row = 0; - for (int g = 0; g < flow_count; ++g) - { - if (row > 0) ++row; - for (int i = 0; i < flow[g].count; ++i) - { - if (row >= rows_per_col) { ++column; row = 0; } - FEItem *item = &fe->items[fe->itemCount++]; - item->group = flow[g].group; - item->index = i; - item->rect.left = margin + column * (col_w + gap); - item->rect.top = content_top + row * row_h; - item->rect.right = item->rect.left + col_w; - item->rect.bottom = item->rect.top + row_h; - ++row; - } - last_col = column; - } + FEItem *item = &fe->items[fe->itemCount++]; + item->group = GroupScenario; + item->index = i; + item->rect.left = margin + i * (col_w / 2); + item->rect.top = y; + item->rect.right = item->rect.left + col_w / 2 - row_h / 3; + item->rect.bottom = y + row_h; + } + y += row_h + row_h / 2; + + int groups[GroupCount]; + int group_count = ComboGroups(fe->selection, groups); + + // left column takes the mission settings, right the loadout; the + // split is where the vehicle list starts. + int left_y = y; + int right_y = top; + for (int g = 0; g < group_count; ++g) + { + int group = groups[g]; + Logical left = (group == GroupMap || group == GroupTime || + group == GroupWeather || group == GroupLength); + int x = left ? margin : col2; + int *slot = left ? &left_y : &right_y; + + fe->comboLabel[group].left = x; + fe->comboLabel[group].right = x + col_w; + fe->comboLabel[group].top = *slot; + fe->comboLabel[group].bottom = *slot + row_h; + *slot += block_h; } - int loadout_col = margin + last_col * (col_w + gap); + // pilot name goes under the loadout column + int name_y = right_y; // launch button FEItem *launch = &fe->items[fe->itemCount++]; launch->group = GroupLaunch; launch->index = 0; - launch->rect.left = loadout_col; + launch->rect.left = col2; launch->rect.top = client_h - 3 * row_h; - launch->rect.right = loadout_col + col_w; + launch->rect.right = col2 + col_w; launch->rect.bottom = client_h - row_h; // Steam lobby buttons, offered whenever environ.ini asked for Steam. @@ -738,17 +753,17 @@ namespace FEItem *host = &fe->items[fe->itemCount++]; host->group = GroupSteamHost; host->index = 0; - host->rect.left = loadout_col; + host->rect.left = col2; host->rect.top = client_h - 6 * row_h; - host->rect.right = loadout_col + col_w; + host->rect.right = col2 + col_w; host->rect.bottom = client_h - 5 * row_h; FEItem *join = &fe->items[fe->itemCount++]; join->group = GroupSteamJoin; join->index = 0; - join->rect.left = loadout_col; + join->rect.left = col2; join->rect.top = client_h - (9 * row_h) / 2; - join->rect.right = loadout_col + col_w; + join->rect.right = col2 + col_w; join->rect.bottom = client_h - (7 * row_h) / 2; } @@ -769,11 +784,54 @@ namespace quit->rect.right = margin + col_w / 2; quit->rect.bottom = client_h - row_h; - // pilot name edit sits at the top of column 3 + // pilot name, under the loadout column + fe->comboLabel[GroupLaunch].left = col2; // reused: name label + fe->comboLabel[GroupLaunch].right = col2 + col_w; + fe->comboLabel[GroupLaunch].top = name_y; + fe->comboLabel[GroupLaunch].bottom = name_y + row_h; if (fe->nameEdit != NULL) { - MoveWindow(fe->nameEdit, - loadout_col, top - row_h / 4, col_w, row_h, TRUE); + MoveWindow(fe->nameEdit, col2, name_y + row_h, col_w, row_h, TRUE); + } + + //--------------------------------------------------------------- + // The drop-downs themselves. Rebuilt rather than moved, because + // the scenario swaps two of them outright (team/position for + // colour/badge) and reshuffles the track list. + //--------------------------------------------------------------- + DestroyCombos(fe); + for (int g = 0; g < group_count; ++g) + { + int group = groups[g]; + const RECT &label = fe->comboLabel[group]; + int count = GroupSize(group, fe->selection); + + // The height given at creation is the DROPPED height - what the + // closed box shows is the item height - so ask for enough to + // show a dozen rows without a scrollbar where the list is short. + int drop_rows = (count < 12) ? count : 12; + HWND box = CreateWindowExA( + 0, "COMBOBOX", "", + WS_CHILD | WS_VISIBLE | WS_VSCROLL | + CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS, + label.left, label.top + row_h, + label.right - label.left, row_h + drop_rows * row_h, + fe->menuWindow, (HMENU)(INT_PTR)(kComboIdBase + group), + (HINSTANCE) GetWindowLongPtr(fe->menuWindow, GWLP_HINSTANCE), NULL); + if (box == NULL) + { + continue; + } + SendMessageA(box, WM_SETFONT, (WPARAM) fe->textFont, TRUE); + SendMessageA(box, CB_SETITEMHEIGHT, (WPARAM) -1, row_h); + SendMessageA(box, CB_SETITEMHEIGHT, 0, row_h); + for (int i = 0; i < count; ++i) + { + SendMessageA(box, CB_ADDSTRING, 0, (LPARAM) ItemName(group, i)); + } + if (fe->selection[group] >= count) fe->selection[group] = 0; + SendMessageA(box, CB_SETCURSEL, fe->selection[group], 0); + fe->combo[group] = box; } } @@ -796,7 +854,6 @@ namespace } // the map rows need the active scenario's list - const int *gItemNameSelection = NULL; const char *ItemName(int group, int index) { @@ -858,21 +915,31 @@ namespace SelectObject(mem, fe->textFont); - // group headers (drawn above each group's first item) - int previous_group = -1; + // Labels: one above each drop-down, one above the scenario + // buttons, one above the pilot name box. The drop-downs draw + // themselves (WM_DRAWITEM), so all that is left here is their + // captions. + SetTextColor(mem, kGreenBright); + for (int g = 0; g < GroupCount; ++g) + { + Logical labelled = (g == GroupScenario) || (fe->combo[g] != NULL); + if (!labelled && g != GroupLaunch) + { + continue; + } + RECT label = fe->comboLabel[g]; + if (label.right <= label.left) + { + continue; + } + const char *caption = (g == GroupLaunch) ? "PILOT NAME" : GroupTitle(g); + DrawTextA(mem, caption, -1, &label, + DT_LEFT | DT_VCENTER | DT_SINGLELINE); + } + for (int i = 0; i < fe->itemCount; ++i) { const FEItem *item = &fe->items[i]; - if (item->group != previous_group && item->group < GroupLaunch) - { - previous_group = item->group; - RECT header = item->rect; - header.top -= (item->rect.bottom - item->rect.top); - header.bottom = item->rect.top; - SetTextColor(mem, kGreenBright); - DrawTextA(mem, GroupTitle(item->group), -1, &header, - DT_LEFT | DT_VCENTER | DT_SINGLELINE); - } Logical selected = (item->group >= GroupLaunch) || @@ -937,22 +1004,7 @@ namespace DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); } - // pilot-name header - if (fe->nameEdit != NULL) - { - RECT edit_rect; - GetWindowRect(fe->nameEdit, &edit_rect); - POINT corner = { edit_rect.left, edit_rect.top }; - ScreenToClient(fe->menuWindow, &corner); - RECT header; - header.left = corner.x; - header.right = corner.x + 300; - header.bottom = corner.y; - header.top = corner.y - 26; - SetTextColor(mem, kGreenBright); - DrawTextA(mem, "PILOT NAME", -1, &header, - DT_LEFT | DT_VCENTER | DT_SINGLELINE); - } + // (the pilot-name caption is drawn with the other labels above) BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY); @@ -1039,6 +1091,63 @@ namespace } break; + case WM_COMMAND: + if (fe != NULL && HIWORD(wParam) == CBN_SELCHANGE) + { + int group = LOWORD(wParam) - kComboIdBase; + if (group >= 0 && group < GroupCount && fe->combo[group] != NULL) + { + int pick = (int) SendMessageA(fe->combo[group], CB_GETCURSEL, 0, 0); + if (pick >= 0) + { + fe->selection[group] = pick; + } + InvalidateRect(fe->menuWindow, NULL, FALSE); + return 0; + } + } + break; + + // + // The drop-downs are owner-drawn so they read as part of the + // panel rather than as system widgets: green on black, and the + // highlight inverted rather than tinted. + // + case WM_DRAWITEM: + if (fe != NULL) + { + DRAWITEMSTRUCT *di = (DRAWITEMSTRUCT *) lParam; + if (di->CtlType == ODT_COMBOBOX && (int) di->itemID >= 0) + { + Logical hot = + ((di->itemState & (ODS_SELECTED | ODS_COMBOBOXEDIT)) == ODS_SELECTED); + HBRUSH back = CreateSolidBrush(hot ? kGreenDim : kBlack); + FillRect(di->hDC, &di->rcItem, back); + DeleteObject(back); + + char text[128]; + text[0] = '\0'; + SendMessageA(di->hwndItem, CB_GETLBTEXT, di->itemID, (LPARAM) text); + RECT label = di->rcItem; + label.left += 6; + SetBkMode(di->hDC, TRANSPARENT); + SetTextColor(di->hDC, hot ? kBlack : kGreenBright); + DrawTextA(di->hDC, text, -1, &label, + DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); + return TRUE; + } + } + break; + + case WM_CTLCOLORLISTBOX: + if (fe != NULL) + { + SetTextColor((HDC) wParam, kGreenBright); + SetBkColor((HDC) wParam, kBlack); + return (LRESULT) fe->editBrush; + } + break; + case WM_CTLCOLOREDIT: if (fe != NULL) {