/* * Portions of this file are copyright Rebirth contributors and licensed as * described in COPYING.txt. * Portions of this file are copyright Parallax Software and licensed * according to the Parallax license below. * See COPYING.txt for license details. THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX SOFTWARE CORPORATION ("PARALLAX"). PARALLAX, IN DISTRIBUTING THE CODE TO END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS IN USING, DISPLAYING, AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE FREE PURPOSES. IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES. THE END-USER UNDERSTANDS AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE. COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION. ALL RIGHTS RESERVED. */ #include #include #include #include "physfsx.h" #include "u_mem.h" #include "maths.h" #include "pstypes.h" #include "gr.h" #include "strutil.h" #include "event.h" #include "window.h" #include "ui.h" #include "mouse.h" #include "key.h" #include "func.h" #include "dxxerror.h" #include "compiler-range_for.h" #include "d_enumerate.h" #include "d_range.h" #include "partial_range.h" #include namespace dcx { namespace { struct ITEM { short x = 0, y = 0, w = 0, h = 0; short Hotkey = -1; int (*user_function)(void) = nullptr; std::unique_ptr Text; std::unique_ptr InactiveText; }; struct MENU { window *wind = nullptr; short x = 0, y = 0, w = 0, h = 0; uint8_t ShowBar = 0; uint8_t CurrentItem = 0; uint16_t NumItems = 0; uint8_t Displayed = 0; uint8_t Active = 0; std::array Item; }; struct menubar_window : window { MENU &menubar; explicit menubar_window(grs_canvas &src, const int x, const int y, const int w, const int h, MENU &menu) : window(src, x, y, w, h), menubar(menu) { } virtual window_event_result event_handler(const d_event &) override; }; struct menu_window : window { MENU &menu; explicit menu_window(grs_canvas &src, const int x, const int y, const int w, const int h, MENU &menu) : window(src, x, y, w, h), menu(menu) { } virtual window_event_result event_handler(const d_event &) override; }; static std::array Menu; static unsigned num_menus; static int state; #define CMENU (Menu[0]->CurrentItem+1) //------------------------- Show a menu item ------------------- static void item_show(const MENU &menu, const int n) { const auto &item = menu.Item[n]; if (!item.Text) return; gr_set_default_canvas(); auto &canvas = *grd_curcanv; // If this is a seperator, then draw it. if (item.Text[0] == '-') { const auto color = CBLACK; gr_urect(canvas, item.x, item.y + item.h / 2, item.x + item.w - 1, item.y + item.h / 2, color); return; } if (menu.CurrentItem == n && menu.ShowBar) { if (&menu != Menu[0]) { const auto color = CBLACK; gr_urect(canvas, item.x + 1, item.y + 1, item.x + menu.w - 2, item.y + item.h - 2, color); } gr_set_fontcolor(canvas, CWHITE, CBLACK); }else { if (&menu != Menu[0]) { const auto color = CGREY; gr_urect(canvas, item.x + 1, item.y + 1, item.x + menu.w - 2, item.y + item.h - 2, color); } gr_set_fontcolor(canvas, CBLACK, CGREY); } if (&menu != Menu[0]) { if (menu.Active) gr_ustring(canvas, *canvas.cv_font, item.x + 1, item.y + 1, item.Text.get()); else gr_ustring(canvas, *canvas.cv_font, item.x + 1, item.y + 1, item.InactiveText.get()); } else { if (menu.Active) gr_ustring(canvas, *canvas.cv_font, item.x, item.y, item.Text.get()); else gr_ustring(canvas, *canvas.cv_font, item.x, item.y, item.InactiveText.get()); } } static void menu_draw(const MENU &menu) { gr_set_default_canvas(); auto &canvas = *grd_curcanv; // Draw the menu background gr_urect(canvas, menu.x, menu.y, menu.x + menu.w - 1, menu.y + menu.h - 1, CGREY); if (&menu != Menu[0]) { gr_ubox(canvas, menu.x, menu.y, menu.x + menu.w - 1, menu.y + menu.h - 1, CBLACK); } // Draw the items for (const auto i : xrange(menu.NumItems)) item_show( menu, i ); } //---------------------------- Show a menu --------------------- static void menu_show(MENU &menu) { menu.wind->set_visible(1); // Mark as displayed. menu.Displayed = 1; } static void menu_other_show() { auto &menu = *Menu[CMENU]; if (!menu.wind) { menu.wind = window_create(grd_curscreen->sc_canvas, menu.x, menu.y, menu.w, menu.h, menu); } menu_show(menu); } //-------------------------- Hide a menu ----------------------- static void menu_hide(MENU &menu) { // Can't hide if it's not already drawn if (!menu.Displayed) return; if (&menu != Menu[0] && menu.wind) menu.wind->set_visible(0); // don't draw or receive events menu.Active = 0; if (&menu == Menu[0] && menu.wind) menu.wind->set_modal(0); // Mark as hidden. menu.Displayed = 0; } //------------------------- Move the menu bar ------------------ static void menu_move_bar_to(MENU &menu, const int number) { const auto old_item = menu.CurrentItem; menu.CurrentItem = number; if (menu.Displayed && number != old_item) { item_show( menu, old_item ); item_show( menu, number ); } } //------------------------ Match keypress to item ------------------ static int menu_match_keypress(MENU &menu, int keypress) { char c; if ((keypress & KEY_CTRLED) || (keypress & KEY_SHIFTED)) return -1; keypress &= 0xFF; c = key_ascii(); for (auto &&[i, item] : enumerate(partial_range(menu.Item, menu.NumItems))) { auto letter = strrchr(item.Text.get(), CC_UNDERLINE); if (letter) { letter++; if (c==tolower(*letter)) return i; } } return -1; } static int menu_is_mouse_on(const ITEM &item) { int x, y, z; mouse_get_pos(&x, &y, &z); if (x >= item.x && x < item.x + item.w && y >= item.y && y <= item.y + item.h) return 1; else return 0; } static int menu_check_mouse_item(const MENU &menu) { for (auto &&[i, item] : enumerate(partial_range(menu.Item, menu.NumItems))) { if (menu_is_mouse_on(item)) { if (item.Text[0] == '-') return -1; else return i; } } return -1; } static void menu_hide_all() { range_for (auto &i, partial_range(Menu, 1u, num_menus)) menu_hide(*i); auto &menubar = *Menu[0]; menubar.ShowBar = 0; menubar.Active = 0; if (menubar.wind) menubar.wind->set_modal(0); menubar_show(); } static int state2_alt_down; static window_event_result do_state_0(const d_event &event) { int keypress = 0; if (event.type == EVENT_KEY_COMMAND) keypress = event_key_get(event); auto &menubar = *Menu[0]; menubar.Active = 0; if (menubar.wind) menubar.wind->set_modal(0); menubar.ShowBar = 0; if ( keypress & KEY_ALTED ) { const auto i = menu_match_keypress(menubar, keypress); if (i > -1 ) { auto &menubar = *Menu[0]; menubar.CurrentItem = i; menubar.Active = 0; if (menubar.wind) menubar.wind->set_modal(0); state = 3; auto &other_menu = *Menu[CMENU]; other_menu.ShowBar = 1; other_menu.Active = 1; menubar.ShowBar = 1; menu_other_show(); menubar_show(); return window_event_result::handled; } } range_for (auto &i, partial_const_range(Menu, num_menus)) for (auto &j : partial_const_range(i->Item, i->NumItems)) { if ( j.Hotkey == keypress ) { if (j.user_function) j.user_function(); return window_event_result::handled; } } if (keypress & KEY_ALTED) //if ( (keypress & 0xFF) == KEY_LALT ) { // Make sure the menubar receives events exclusively state = 1; menubar.Active = 1; // Put the menubar in front - hope this doesn't mess anything up by leaving it there // If it does, will need to remember the previous front window and restore it. // (Personally, I just use either the mouse or 'hotkeys' for menus) window_select(*menubar.wind); menubar.wind->set_modal(1); menubar_show(); return window_event_result::handled; } const auto i = menu_check_mouse_item(menubar); if ( B1_JUST_PRESSED && (i > -1)) { menubar.CurrentItem = i; state = 3; auto &other_menu = *Menu[CMENU]; other_menu.ShowBar = 1; menubar.ShowBar = 1; other_menu.Active = 1; menubar.Active = 0; menubar.wind->set_modal(0); menu_other_show(); menubar_show(); return window_event_result::handled; } return window_event_result::ignored; } static window_event_result do_state_1(const d_event &event) { int keypress = 0; window_event_result rval = window_event_result::ignored; if (event.type == EVENT_KEY_COMMAND) keypress = event_key_get(event); auto &menubar = *Menu[0]; if ((event.type == EVENT_KEY_RELEASE) && !(event_key_get(event) & KEY_ALTED)) { state = 2; state2_alt_down = 0; menubar.ShowBar = 1; menubar.Active = 1; menubar_show(); rval = window_event_result::handled; } if (const auto i = menu_match_keypress(menubar, keypress); i > -1) { menubar.CurrentItem = i; menubar.Active = 0; menubar.wind->set_modal(0); state = 3; auto &other_menu = *Menu[CMENU]; other_menu.ShowBar = 1; other_menu.Active = 1; menubar.ShowBar = 1; menu_other_show(); menubar_show(); rval = window_event_result::handled; } const auto i = menu_check_mouse_item(menubar); if ( (i == -1) && B1_JUST_RELEASED ) { state = 0; menu_hide_all(); return window_event_result::handled; } if ( B1_JUST_PRESSED && (i > -1)) { menubar.CurrentItem = i; state = 3; auto &other_menu = *Menu[CMENU]; other_menu.ShowBar = 1; other_menu.Active = 1; menubar.ShowBar = 1; menubar.Active = 0; menubar.wind->set_modal(0); menu_other_show(); menubar_show(); return window_event_result::handled; } return rval; } static window_event_result do_state_2(const d_event &event) { int i; int keypress = 0; window_event_result rval = window_event_result::ignored; if (event.type == EVENT_KEY_COMMAND) keypress = event_key_get(event); if (keypress & KEY_ALTED) { state2_alt_down = 1; rval = window_event_result::handled; } if ((event.type == EVENT_KEY_RELEASE) && !(event_key_get(event) & KEY_ALTED) && state2_alt_down) { state = 0; menu_hide_all(); rval = window_event_result::handled; } switch( keypress ) { case KEY_ESC: state = 0; menu_hide_all(); return window_event_result::handled; case KEY_LEFT: case KEY_PAD4: { auto &menubar = *Menu[0]; i = menubar.CurrentItem; if (i <= 0) i = menubar.NumItems; menu_move_bar_to(menubar, i - 1); return window_event_result::handled; } case KEY_RIGHT: case KEY_PAD6: { auto &menubar = *Menu[0]; i = menubar.CurrentItem + 1; if (i >= menubar.NumItems) i = 0; menu_move_bar_to(menubar, i); return window_event_result::handled; } case KEY_ENTER: case KEY_PADENTER: case KEY_DOWN: case KEY_PAD2: state = 3; { auto &other_menu = *Menu[CMENU]; other_menu.ShowBar = 1; other_menu.Active = 1; } { auto &menubar = *Menu[0]; menubar.Active = 0; menubar.wind->set_modal(0); } menubar_show(); menu_other_show(); return window_event_result::handled; default: { auto &menubar = *Menu[0]; i = menu_match_keypress(menubar, keypress); if (i > -1 ) { menubar.CurrentItem = i; menubar.Active = 0; menubar.wind->set_modal(0); state = 3; auto &other_menu = *Menu[CMENU]; other_menu.ShowBar = 1; other_menu.Active = 1; menubar.ShowBar = 1; menu_other_show(); menubar_show(); return window_event_result::handled; } i = menu_check_mouse_item(menubar); if ( (i == -1) && B1_JUST_RELEASED ) { state = 0; menu_hide_all(); return window_event_result::handled; } if (i > -1) { menubar.CurrentItem = i; menubar.Active = 0; menubar.wind->set_modal(0); state = 3; auto &other_menu = *Menu[CMENU]; other_menu.ShowBar = 1; other_menu.Active = 1; menubar.ShowBar = 1; menu_other_show(); menubar_show(); return window_event_result::handled; } } } return rval; } window_event_result menu_window::event_handler(const d_event &event) { int keypress = 0; if (state != 3) return window_event_result::ignored; if (event.type == EVENT_KEY_COMMAND) keypress = event_key_get(event); else if (event.type == EVENT_WINDOW_CLOSE) // quitting { state = 0; menu_hide_all(); menu.wind = nullptr; return window_event_result::ignored; } window_event_result rval = window_event_result::ignored; switch( keypress ) { case 0: break; case KEY_ESC: state = 0; menu_hide_all(); rval = window_event_result::handled; break; case KEY_DOWN: case KEY_PAD2: { auto &other_menu = *Menu[CMENU]; auto i = other_menu.CurrentItem; do { i++; if (i >= other_menu.NumItems) i = 0; } while(other_menu.Item[i].Text[0] == '-'); menu_move_bar_to(other_menu, i); rval = window_event_result::handled; break; } case KEY_UP: case KEY_PAD8: { auto &other_menu = *Menu[CMENU]; auto i = other_menu.CurrentItem; do { if (i <= 0) i = other_menu.NumItems; --i; } while(other_menu.Item[i].Text[0] == '-'); menu_move_bar_to(other_menu, i); rval = window_event_result::handled; break; } case KEY_RIGHT: case KEY_PAD6: { auto &other_menu = *Menu[CMENU]; menu_hide(other_menu); auto &menubar = *Menu[0]; auto i = menubar.CurrentItem + 1; if (i >= menubar.NumItems) i = 0; menu_move_bar_to(menubar, i); other_menu.ShowBar = 1; other_menu.Active = 1; menu_other_show(); rval = window_event_result::handled; break; } case KEY_LEFT: case KEY_PAD4: { auto &other_menu = *Menu[CMENU]; menu_hide(other_menu); auto &menubar = *Menu[0]; auto i = menubar.CurrentItem; if (i <= 0) i = menubar.NumItems; --i; menu_move_bar_to(menubar, i); other_menu.ShowBar = 1; other_menu.Active = 1; menu_other_show(); rval = window_event_result::handled; break; } case KEY_ENTER: case KEY_PADENTER: { state = 0; menu_hide_all(); auto &other_menu = *Menu[CMENU]; if (const auto f = other_menu.Item[other_menu.CurrentItem].user_function) f(); rval = window_event_result::handled; break; } default: { auto &other_menu = *Menu[CMENU]; const auto i = menu_match_keypress(other_menu, keypress); if (i > -1 ) { menu_move_bar_to(other_menu, i); state = 0; menu_hide_all(); if (const auto f = other_menu.Item[other_menu.CurrentItem].user_function) f(); rval = window_event_result::handled; break; } break; } } if (event.type == EVENT_MOUSE_MOVED || B1_JUST_RELEASED) { auto &other_menu = *Menu[CMENU]; const auto i = menu_check_mouse_item(other_menu); if (i > -1 ) { if ( B1_JUST_RELEASED ) { menu_move_bar_to(other_menu, i); state = 0; menu_hide_all(); if (const auto f = other_menu.Item[other_menu.CurrentItem].user_function) f(); rval = window_event_result::handled; } else { menu_move_bar_to(other_menu, i); rval = window_event_result::handled; } } else { auto &menubar = *Menu[0]; const auto i = menu_check_mouse_item(menubar); if (i > -1) { if (menubar.CurrentItem != i) { menu_hide(other_menu); menu_move_bar_to(menubar, i); other_menu.ShowBar = 1; other_menu.Active = 1; menu_other_show(); } rval = window_event_result::handled; } if ( B1_JUST_RELEASED ) { state = 0; menu_hide_all(); rval = window_event_result::handled; } } } if (event.type == EVENT_WINDOW_DRAW) { auto &other_menu = *Menu[CMENU]; menu_draw(other_menu); return window_event_result::handled; } return rval; } window_event_result menubar_window::event_handler(const d_event &event) { if (event.type == EVENT_WINDOW_DRAW) { menu_draw(menubar); return window_event_result::handled; } else if (event.type == EVENT_WINDOW_CLOSE) { range_for (const auto pi, partial_range(Menu, 1u, num_menus)) { auto &i = *pi; if (i.wind) { window_close(std::exchange(i.wind, nullptr)); } } menubar.wind = nullptr; } switch (state) { case 0: return do_state_0(event); case 1: return do_state_1(event); case 2: return do_state_2(event); case 3: break; default: state = 0; menu_hide_all(); } return window_event_result::ignored; } static void CommaParse(uint_fast32_t n, char * dest, const PHYSFSX_gets_line_t<200>::line_t &source) { int i = 0, j=0, cn = 0; // Go to the n'th comma while (cn < n ) if (source[i++] == ',' ) cn++; // Read all the whitespace while ( source[i]==' ' || source[i]=='\t' || source[i]==13 || source[i]==10 ) i++; // Read up until the next comma while ( source[i] != ',' ) { dest[j] = source[i++]; j++; } // Null-terminate dest[j++] = 0; } //translate '&' characters to the underline character static void ul_xlate(char *s) { while ((s=strchr(s,'&'))!=NULL) *s = CC_UNDERLINE; } static MENU &menu_allocate(MENU *& p) { if (p) return *p; p = new MENU; return *p; } } int menubar_init(grs_canvas &canvas, const char *const file) { int np; char buf1[200]; char buf2[204]; num_menus = state = 0; // This method should be faster than explicitly setting all the variables (I think) Menu = {}; auto &&[infile, physfserr] = PHYSFSX_openReadBuffered(file); if (!infile) { Warning("Menubar: failed to open \"%s\": %s\n", file, PHYSFS_getErrorByCode(physfserr)); return 0; } PHYSFSX_gets_line_t<200> buffer; while ( PHYSFSX_fgets( buffer, infile) != NULL ) { if ( buffer[0] == ';' ) continue; CommaParse( 0, buf1, buffer ); char *p; const auto mi = strtoul(buf1, &p, 10); if (mi >= Menu.size()) { con_printf(CON_URGENT, "%s:%u: too many menus defined: max=%" DXX_PRI_size_type ", requested=%lu in \"%s\"", __FILE__, __LINE__, Menu.size(), mi, file); break; } CommaParse( 1, buf1, buffer ); const auto ii = strtoul(buf1, &p, 10); auto &menu = menu_allocate(Menu[mi]); if (ii >= menu.Item.size()) { con_printf(CON_URGENT, "%s:%u: too many items defined: max=%" DXX_PRI_size_type ", requested=%lu in \"%s\"", __FILE__, __LINE__, menu.Item.size(), ii, file); break; } auto &item = menu.Item[ii]; CommaParse( 2, buf1, buffer ); ul_xlate(buf1); item.Text = d_strdup(buf1[0] == '-' ? buf1 : (snprintf(buf2, sizeof(buf2), " %.197s ", buf1), buf2)); item.InactiveText = d_strdup(item.Text.get()); for (int i = 0, j = 0;; i++ ) { np = item.Text[i]; if (np != CC_UNDERLINE) item.InactiveText[j++] = np; if (!np) break; } CommaParse( 3, buf1, buffer ); if (buf1[0]=='{' && buf1[1] =='}') item.Hotkey = -1; else { const auto i = DecodeKeyText(buf1); if (i<1) { UserError("Unknown key, %s, in %s\n", buf1, file ); } else { item.Hotkey = i; } } CommaParse( 4, buf1, buffer ); if (buf1[0]) { item.user_function = func_get(buf1, &np); if (!item.user_function) { con_printf(CON_URGENT, "%s:%u: unknown function \"%s\" in \"%s\"", __FILE__, __LINE__, buf1, file); } } item.x = menu.x; item.y = menu.y; int w, h; if (item.Text[0] == '-') { w = 1; h = 3; } else { const auto r = gr_get_string_size(*canvas.cv_font, item.Text.get()); w = r.width + 2; h = r.height + 2; } if (mi == 0) { menu.h = h; item.x = menu.x + menu.w; item.y = menu.y; auto &next_menu = menu_allocate(Menu[ii + 1]); next_menu.x = menu.x + menu.w; next_menu.y = menu.h - 2; item.w = w; item.h = h; menu.w += w; }else { if (w > menu.w) { menu.w = w; range_for (auto &i, partial_range(menu.Item, menu.NumItems)) i.w = w; } item.w = menu.w; item.x = menu.x; item.y = menu.y+menu.h; item.h = h; menu.h += h; } if (ii >= menu.NumItems) { menu.NumItems = ii + 1; } if (mi >= num_menus) num_menus = mi + 1; } return 1; } void menubar_hide() { state = 0; menu_hide_all(); menu_hide(*Menu[0]); } void menubar_show() { auto &menu = *Menu[0]; if (!menu.wind) { menu.wind = window_create(grd_curscreen->sc_canvas, menu.x, menu.y, menu.w, menu.h, menu); menu.wind->set_modal(0); // allow windows behind the menubar to accept events (e.g. the keypad dialogs) } menu_show(menu); } void menubar_close() { auto &menubar = *Menu[0]; if (!menubar.wind) return; window_close(std::exchange(menubar.wind, nullptr)); } }