/* * File: menu.cc * Summary: Menus and associated malarkey. * Written by: Darshan Shaligram */ #include "AppHdr.h" #include #include "cio.h" #include "colour.h" #include "coord.h" #include "env.h" #include "menu.h" #include "macro.h" #include "message.h" #include "options.h" #include "player.h" #include "tutorial.h" #ifdef USE_TILE #include "mon-stuff.h" #include "mon-util.h" #include "newgame.h" #include "terrain.h" #include "tiles.h" #include "tiledef-dngn.h" #include "tiledef-main.h" #include "tiledef-player.h" #include "travel.h" #endif MenuDisplay::MenuDisplay(Menu *menu) : m_menu(menu) { m_menu->set_maxpagesize(get_number_of_lines()); } MenuDisplayText::MenuDisplayText(Menu *menu) : MenuDisplay(menu), m_starty(1) { } void MenuDisplayText::draw_stock_item(int index, const MenuEntry *me) { if (crawl_state.doing_prev_cmd_again) return; const int col = m_menu->item_colour(index, me); textattr(col); if (m_menu->get_flags() & MF_ALLOW_FORMATTING) formatted_string::parse_string(me->get_text(), true, NULL, col).display(); else { std::string text = me->get_text(); if ((int) text.length() > get_number_of_cols()) text = text.substr(0, get_number_of_cols()); cprintf("%s", text.c_str()); } } void MenuDisplayText::draw_more() { cgotoxy(1, m_menu->get_y_offset() + m_menu->get_pagesize() - count_linebreaks(m_menu->get_more())); m_menu->get_more().display(); } #ifdef USE_TILE MenuDisplayTile::MenuDisplayTile(Menu *menu) : MenuDisplay(menu) { m_menu->set_maxpagesize(tiles.get_menu()->maxpagesize()); } void MenuDisplayTile::draw_stock_item(int index, const MenuEntry *me) { int colour = m_menu->item_colour(index, me); std::string text = me->get_text(); tiles.get_menu()->set_entry(index, text, colour, me); } void MenuDisplayTile::set_offset(int lines) { tiles.get_menu()->set_offset(lines); m_menu->set_maxpagesize(tiles.get_menu()->maxpagesize()); } void MenuDisplayTile::draw_more() { tiles.get_menu()->set_more(m_menu->get_more()); m_menu->set_maxpagesize(tiles.get_menu()->maxpagesize()); } void MenuDisplayTile::set_num_columns(int columns) { tiles.get_menu()->set_num_columns(columns); m_menu->set_maxpagesize(tiles.get_menu()->maxpagesize()); } #endif Menu::Menu( int _flags, const std::string& tagname, bool text_only ) : f_selitem(NULL), f_drawitem(NULL), f_keyfilter(NULL), action_cycle(CYCLE_NONE), menu_action(ACT_EXAMINE), title(NULL), title2(NULL), flags(_flags), tag(tagname), first_entry(0), y_offset(0), pagesize(0), max_pagesize(0), more("-more-", true), items(), sel(), select_filter(), highlighter(new MenuHighlighter), num(-1), lastch(0), alive(false), last_selected(-1) { #ifdef USE_TILE if (text_only) mdisplay = new MenuDisplayText(this); else mdisplay = new MenuDisplayTile(this); #else mdisplay = new MenuDisplayText(this); #endif mdisplay->set_num_columns(1); set_flags(flags); } Menu::Menu( const formatted_string &fs ) : f_selitem(NULL), f_drawitem(NULL), f_keyfilter(NULL), action_cycle(CYCLE_NONE), menu_action(ACT_EXAMINE), title(NULL), title2(NULL), // This is a text-viewer menu, init flags to be easy on the user. flags(MF_NOSELECT | MF_EASY_EXIT), tag(), first_entry(0), y_offset(0), pagesize(0), max_pagesize(0), more("-more-", true), items(), sel(), select_filter(), highlighter(new MenuHighlighter), num(-1), lastch(0), alive(false), last_selected(-1) { mdisplay = new MenuDisplayText(this); mdisplay->set_num_columns(1); int colour = LIGHTGREY; int last_text_colour = LIGHTGREY; std::string line; for (formatted_string::oplist::const_iterator i = fs.ops.begin(); i != fs.ops.end(); ++i) { const formatted_string::fs_op &op(*i); switch (op.type) { case FSOP_COLOUR: colour = op.x; break; case FSOP_TEXT: { line += op.text; const std::string::size_type nonblankp = op.text.find_first_not_of(" \t\r\n"); const bool nonblank = nonblankp != std::string::npos; const std::string::size_type eolp = op.text.find(EOL); const bool starts_with_eol = nonblank && eolp != std::string::npos && eolp < nonblankp; if (nonblank && !starts_with_eol) last_text_colour = colour; check_add_formatted_line(last_text_colour, colour, line, true); if (nonblank && starts_with_eol) last_text_colour = colour; break; } default: break; } } check_add_formatted_line(last_text_colour, colour, line, false); } void Menu::check_add_formatted_line(int firstcol, int nextcol, std::string &line, bool check_eol) { if (line.empty()) return; if (check_eol && line.find(EOL) == std::string::npos) return; std::vector lines = split_string(EOL, line, false, true); int size = lines.size(); // If we have stuff after EOL, leave that in the line variable and // don't add an entry for it, unless the caller told us not to // check EOL sanity. if (check_eol && !ends_with(line, EOL)) line = lines[--size]; else line.clear(); for (int i = 0, col = firstcol; i < size; ++i, col = nextcol) { std::string &s(lines[i]); trim_string_right(s); MenuEntry *me = new MenuEntry(s); me->colour = col; if (!title) set_title(me); else add_entry(me); } line.clear(); } Menu::~Menu() { for (int i = 0, count = items.size(); i < count; ++i) delete items[i]; delete title; if (title2) delete title2; delete highlighter; delete mdisplay; } void Menu::clear() { for (int i = 0, count = items.size(); i < count; ++i) delete items[i]; items.clear(); } void Menu::set_maxpagesize(int max) { max_pagesize = max; } void Menu::set_flags(int new_flags, bool use_options) { flags = new_flags; if (use_options && Options.easy_exit_menu) flags |= MF_EASY_EXIT; } void Menu::set_more(const formatted_string &fs) { more = fs; } void Menu::set_highlighter( MenuHighlighter *mh ) { if (highlighter != mh) delete highlighter; highlighter = mh; } void Menu::set_title( MenuEntry *e, bool first ) { if (first) { if (title != e) delete title; title = e; title->level = MEL_TITLE; } else { title2 = e; title2->level = MEL_TITLE; } } void Menu::add_entry( MenuEntry *entry ) { entry->tag = tag; items.push_back( entry ); } void Menu::reset() { first_entry = 0; } std::vector Menu::show(bool reuse_selections) { cursor_control cs(false); if (reuse_selections) { get_selected(&sel); } else { deselect_all(false); sel.clear(); } // Reset offset to default. mdisplay->set_offset(1 + !!title); // Lose lines for the title + room for -more- line. #ifdef USE_TILE pagesize = max_pagesize - !!title - 1; #else pagesize = get_number_of_lines() - !!title - 1; if (max_pagesize > 0 && pagesize > max_pagesize) pagesize = max_pagesize; #endif do_menu(); return (sel); } void Menu::do_menu() { draw_menu(); alive = true; while (alive) { #ifndef USE_TILE int keyin = getchm(KMC_MENU, c_getch); #else mouse_control mc(MOUSE_MODE_MORE); int keyin = getch(); #endif if (!process_key( keyin )) return; } } bool Menu::is_set(int flag) const { return (flags & flag) == flag; } int Menu::pre_process(int k) { return (k); } int Menu::post_process(int k) { return (k); } bool Menu::process_key( int keyin ) { if (items.size() == 0) { lastch = keyin; return (false); } else if (action_cycle == CYCLE_TOGGLE && (keyin == '!' || keyin == '?')) { ASSERT(menu_action != ACT_MISC); if (menu_action == ACT_EXECUTE) menu_action = ACT_EXAMINE; else menu_action = ACT_EXECUTE; sel.clear(); update_title(); return (true); } else if (action_cycle == CYCLE_CYCLE && (keyin == '!' || keyin == '?')) { menu_action = (action)((menu_action+1) % ACT_NUM); sel.clear(); update_title(); return (true); } bool nav = false, repaint = false; if (f_keyfilter) keyin = (*f_keyfilter)(keyin); keyin = pre_process(keyin); switch (keyin) { case 0: return (true); case CK_ENTER: return (false); case CK_ESCAPE: case CK_MOUSE_B2: case CK_MOUSE_CMD: sel.clear(); lastch = keyin; return (false); case ' ': case CK_PGDN: case '>': case '\'': case CK_MOUSE_B1: case CK_MOUSE_CLICK: nav = true; repaint = page_down(); if (!repaint && !is_set(MF_EASY_EXIT) && !is_set(MF_NOWRAP)) { repaint = (first_entry != 0); first_entry = 0; } break; case CK_PGUP: case '<': case ';': nav = true; repaint = page_up(); break; case CK_UP: nav = true; repaint = line_up(); break; case CK_DOWN: nav = true; repaint = line_down(); break; case CK_HOME: nav = true; repaint = (first_entry != 0); first_entry = 0; break; case CK_END: { nav = true; const int breakpoint = (items.size() + 1) - pagesize; if (first_entry < breakpoint) { first_entry = breakpoint; repaint = true; } break; } case CONTROL('F'): { if (!(flags & MF_ALLOW_FILTER)) break; char linebuf[80]; cgotoxy(1,1); clear_to_end_of_line(); textcolor(WHITE); cprintf("Select what? (regex) "); textcolor(LIGHTGREY); bool validline = !cancelable_get_line(linebuf, sizeof linebuf, 80); if (validline && linebuf[0]) { text_pattern tpat(linebuf, true); for (unsigned int i = 0; i < items.size(); ++i) { if (items[i]->level == MEL_ITEM && tpat.matches(items[i]->get_text())) { select_index(i); if (flags & MF_SINGLESELECT) { // Return the first item found. get_selected(&sel); return (false); } } } get_selected(&sel); } nav = true; repaint = true; break; } case '.': if (last_selected != -1) { if ((first_entry + pagesize - last_selected) == 1) { page_down(); nav = true; } select_index(last_selected + 1); get_selected(&sel); repaint = true; } break; default: keyin = post_process(keyin); lastch = keyin; // If no selection at all is allowed, exit now. if (!(flags & (MF_SINGLESELECT | MF_MULTISELECT))) return (false); if (!is_set(MF_NO_SELECT_QTY) && isdigit( keyin )) { if (num > 999) num = -1; num = (num == -1)? keyin - '0' : num * 10 + keyin - '0'; } select_items( keyin, num ); get_selected( &sel ); if (sel.size() == 1 && (flags & MF_SINGLESELECT)) return (false); draw_select_count( sel.size() ); if (flags & MF_ANYPRINTABLE && (!isdigit(keyin) || is_set(MF_NO_SELECT_QTY))) { return (false); } break; } if (last_selected != -1 && (items.size() == ((unsigned int) last_selected + 1) || items[last_selected + 1] == NULL || items[last_selected + 1]->level != MEL_ITEM)) { last_selected = -1; } if (!isdigit( keyin )) num = -1; if (nav) { if (repaint) draw_menu(); // Easy exit should not kill the menu if there are selected items. else if (sel.empty() && is_set(MF_EASY_EXIT)) { sel.clear(); return (false); } } return (true); } bool Menu::draw_title_suffix( const std::string &s, bool titlefirst ) { if (crawl_state.doing_prev_cmd_again) return (true); int oldx = wherex(), oldy = wherey(); if (titlefirst) draw_title(); int x = wherex(); if (x > get_number_of_cols() || x < 1) { cgotoxy(oldx, oldy); return (false); } // Note: 1 <= x <= get_number_of_cols(); we have no fear of overflow. unsigned avail_width = get_number_of_cols() - x + 1; std::string towrite = s.length() > avail_width? s.substr(0, avail_width) : s.length() == avail_width? s : s + std::string(avail_width - s.length(), ' '); cprintf("%s", towrite.c_str()); cgotoxy( oldx, oldy ); return (true); } bool Menu::draw_title_suffix( const formatted_string &fs, bool titlefirst ) { if (crawl_state.doing_prev_cmd_again) return (true); int oldx = wherex(), oldy = wherey(); if (titlefirst) draw_title(); int x = wherex(); if (x > get_number_of_cols() || x < 1) { cgotoxy(oldx, oldy); return (false); } // Note: 1 <= x <= get_number_of_cols(); we have no fear of overflow. const unsigned int avail_width = get_number_of_cols() - x + 1; const unsigned int fs_length = fs.length(); if (fs_length > avail_width) { formatted_string fs_trunc = fs.substr(0, avail_width); fs_trunc.display(); } else { fs.display(); if (fs_length < avail_width) { char fmt[20]; sprintf(fmt, "%%%ds", avail_width-fs_length); cprintf(fmt, " "); } } cgotoxy( oldx, oldy ); return (true); } void Menu::draw_select_count(int count, bool force) { if (!force && !is_set(MF_MULTISELECT)) return; if (f_selitem) { draw_title_suffix(f_selitem(&sel)); } else { char buf[100] = ""; if (count) { snprintf(buf, sizeof buf, " (%d item%s) ", count, (count > 1? "s" : "")); } draw_title_suffix(buf); } } std::vector Menu::selected_entries() const { std::vector selection; get_selected(&selection); return (selection); } void Menu::get_selected( std::vector *selected ) const { selected->clear(); for (int i = 0, count = items.size(); i < count; ++i) if (items[i]->selected()) selected->push_back( items[i] ); } void Menu::deselect_all(bool update_view) { for (int i = 0, count = items.size(); i < count; ++i) { if (items[i]->level == MEL_ITEM) { items[i]->select(0); if (update_view) draw_item(i); } } } bool Menu::is_hotkey(int i, int key) { int end = first_entry + pagesize; if (end > static_cast(items.size())) end = items.size(); bool ishotkey = (is_set(MF_SINGLESELECT) ? items[i]->is_primary_hotkey(key) : items[i]->is_hotkey(key)); return !is_set(MF_SELECT_BY_PAGE) ? ishotkey : ishotkey && i >= first_entry && i < end; } void Menu::select_items(int key, int qty) { int x = wherex(), y = wherey(); if (key == ',' || key == '*') select_index( -1, qty ); else if (key == '-') select_index( -1, 0 ); else { int final = items.size(); bool selected = false; // Process all items, in case user hits hotkey for an // item not on the current page. // We have to use some hackery to handle items that share // the same hotkey (as for pickup when there's a stack of // >52 items). If there are duplicate hotkeys, the items // are usually separated by at least a page, so we should // only select the item on the current page. This is why we // use two loops, and check to see if we've matched an item // by its primary hotkey (which is assumed to always be // hotkeys[0]), in which case, we stop selecting further // items. for (int i = first_entry; i < final; ++i) { if (is_hotkey( i, key )) { select_index( i, qty ); if (items[i]->hotkeys[0] == key) { selected = true; break; } } } if (!selected) { for (int i = 0; i < first_entry; ++i) { if (is_hotkey( i, key )) { select_index( i, qty ); if (items[i]->hotkeys[0] == key) { selected = true; break; } } } } } cgotoxy( x, y ); } MonsterMenuEntry::MonsterMenuEntry(const std::string &str, const monsters* mon, int hotkey) : MenuEntry(str, MEL_ITEM, 1, hotkey) { data = (void*)mon; quantity = 1; } FeatureMenuEntry::FeatureMenuEntry(const std::string &str, const coord_def p, int hotkey) : MenuEntry(str, MEL_ITEM, 1, hotkey) { if (in_bounds(p)) feat = grd(p); else feat = DNGN_UNSEEN; pos = p; quantity = 1; } FeatureMenuEntry::FeatureMenuEntry(const std::string &str, const dungeon_feature_type f, int hotkey) : MenuEntry(str, MEL_ITEM, 1, hotkey) { pos.reset(); feat = f; quantity = 1; } #ifdef USE_TILE PlayerMenuEntry::PlayerMenuEntry(const std::string &str) : MenuEntry(str, MEL_ITEM, 1) { quantity = 1; } bool MenuEntry::get_tiles(std::vector& tileset) const { if (!Options.tile_menu_icons || tiles.empty()) return (false); for (unsigned int i = 0; i < tiles.size(); i++) tileset.push_back(tiles[i]); return (true); } void MenuEntry::add_tile(tile_def tile) { tiles.push_back(tile); } bool MonsterMenuEntry::get_tiles(std::vector& tileset) const { if (!Options.tile_menu_icons) return (false); monsters *m = (monsters*)(data); if (!m) return (false); MenuEntry::get_tiles(tileset); const bool fake = m->props.exists("fake"); const coord_def c = m->pos(); int ch = TILE_FLOOR_NORMAL; if (!fake) { ch = tileidx_feature(grd(c), c.x, c.y); if (ch == TILE_FLOOR_NORMAL) ch = env.tile_flv(c).floor; else if (ch == TILE_WALL_NORMAL) ch = env.tile_flv(c).wall; } tileset.push_back(tile_def(ch, TEX_DUNGEON)); if (m->type == MONS_DANCING_WEAPON) { // For fake dancing weapons, just use a generic long sword, since // fake monsters won't have a real item equipped. item_def item; if (fake) { item.base_type = OBJ_WEAPONS; item.sub_type = WPN_LONG_SWORD; item.quantity = 1; } else item = mitm[m->inv[MSLOT_WEAPON]]; tileset.push_back(tile_def(tileidx_item(item), TEX_DEFAULT)); tileset.push_back(tile_def(TILE_ANIMATED_WEAPON, TEX_DEFAULT)); } else if (mons_is_mimic(m->type)) tileset.push_back(tile_def(tileidx_monster_base(m), TEX_DEFAULT)); else tileset.push_back(tile_def(tileidx_monster_base(m), TEX_PLAYER)); // A fake monster might not have it's ghost member set up properly, // and mons_flies() looks at ghost. if (!fake && !mons_flies(m)) { if (ch == TILE_DNGN_LAVA) tileset.push_back(tile_def(TILE_MASK_LAVA, TEX_DEFAULT)); else if (ch == TILE_DNGN_SHALLOW_WATER) tileset.push_back(tile_def(TILE_MASK_SHALLOW_WATER, TEX_DEFAULT)); else if (ch == TILE_DNGN_DEEP_WATER) tileset.push_back(tile_def(TILE_MASK_DEEP_WATER, TEX_DEFAULT)); else if (ch == TILE_DNGN_SHALLOW_WATER_MURKY) tileset.push_back(tile_def(TILE_MASK_SHALLOW_WATER_MURKY, TEX_DEFAULT)); else if (ch == TILE_DNGN_DEEP_WATER_MURKY) tileset.push_back(tile_def(TILE_MASK_DEEP_WATER_MURKY, TEX_DEFAULT)); } if (!monster_descriptor(m->type, MDSC_NOMSG_WOUNDS)) { std::string damage_desc; mon_dam_level_type damage_level; mons_get_damage_level(m, damage_desc, damage_level); switch (damage_level) { case MDAM_DEAD: case MDAM_ALMOST_DEAD: tileset.push_back(tile_def(TILE_MDAM_ALMOST_DEAD, TEX_DEFAULT)); break; case MDAM_SEVERELY_DAMAGED: tileset.push_back(tile_def(TILE_MDAM_SEVERELY_DAMAGED, TEX_DEFAULT)); break; case MDAM_HEAVILY_DAMAGED: tileset.push_back(tile_def(TILE_MDAM_HEAVILY_DAMAGED, TEX_DEFAULT)); break; case MDAM_MODERATELY_DAMAGED: tileset.push_back(tile_def(TILE_MDAM_MODERATELY_DAMAGED, TEX_DEFAULT)); break; case MDAM_LIGHTLY_DAMAGED: tileset.push_back(tile_def(TILE_MDAM_LIGHTLY_DAMAGED, TEX_DEFAULT)); break; case MDAM_OKAY: default: // no flag for okay. break; } } if (m->friendly()) tileset.push_back(tile_def(TILE_HEART, TEX_DEFAULT)); else if (m->neutral()) tileset.push_back(tile_def(TILE_NEUTRAL, TEX_DEFAULT)); else if (mons_looks_stabbable(m)) tileset.push_back(tile_def(TILE_STAB_BRAND, TEX_DEFAULT)); else if (mons_looks_distracted(m)) tileset.push_back(tile_def(TILE_MAY_STAB_BRAND, TEX_DEFAULT)); return (true); } bool FeatureMenuEntry::get_tiles(std::vector& tileset) const { if (!Options.tile_menu_icons) return (false); if (feat == DNGN_UNSEEN) return (false); MenuEntry::get_tiles(tileset); tileset.push_back(tile_def(tileidx_feature(feat, pos.x, pos.y), TEX_DUNGEON)); if (in_bounds(pos) && is_unknown_stair(pos)) tileset.push_back(tile_def(TILE_NEW_STAIR, TEX_DEFAULT)); return (true); } bool PlayerMenuEntry::get_tiles(std::vector& tileset) const { if (!Options.tile_menu_icons) return (false); MenuEntry::get_tiles(tileset); const player_save_info &player = *static_cast( data ); dolls_data equip_doll = player.doll; // FIXME: A lot of code duplication from DungeonRegion::pack_doll(). int p_order[TILEP_PART_MAX] = { TILEP_PART_SHADOW, // 0 TILEP_PART_HALO, TILEP_PART_ENCH, TILEP_PART_DRCWING, TILEP_PART_CLOAK, TILEP_PART_BASE, // 5 TILEP_PART_BOOTS, TILEP_PART_LEG, TILEP_PART_BODY, TILEP_PART_ARM, TILEP_PART_HAND1, // 10 TILEP_PART_HAND2, TILEP_PART_HAIR, TILEP_PART_BEARD, TILEP_PART_HELM, TILEP_PART_DRCHEAD // 15 }; int flags[TILEP_PART_MAX]; tilep_calc_flags(equip_doll.parts, flags); // For skirts, boots go under the leg armour. For pants, they go over. if (equip_doll.parts[TILEP_PART_LEG] < TILEP_LEG_SKIRT_OFS) { p_order[6] = TILEP_PART_BOOTS; p_order[7] = TILEP_PART_LEG; } // Special case bardings from being cut off. bool is_naga = (equip_doll.parts[TILEP_PART_BASE] == TILEP_BASE_NAGA || equip_doll.parts[TILEP_PART_BASE] == TILEP_BASE_NAGA + 1); if (equip_doll.parts[TILEP_PART_BOOTS] >= TILEP_BOOTS_NAGA_BARDING && equip_doll.parts[TILEP_PART_BOOTS] <= TILEP_BOOTS_NAGA_BARDING_RED) { flags[TILEP_PART_BOOTS] = is_naga ? TILEP_FLAG_NORMAL : TILEP_FLAG_HIDE; } bool is_cent = (equip_doll.parts[TILEP_PART_BASE] == TILEP_BASE_CENTAUR || equip_doll.parts[TILEP_PART_BASE] == TILEP_BASE_CENTAUR + 1); if (equip_doll.parts[TILEP_PART_BOOTS] >= TILEP_BOOTS_CENTAUR_BARDING && equip_doll.parts[TILEP_PART_BOOTS] <= TILEP_BOOTS_CENTAUR_BARDING_RED) { flags[TILEP_PART_BOOTS] = is_cent ? TILEP_FLAG_NORMAL : TILEP_FLAG_HIDE; } for (int i = 0; i < TILEP_PART_MAX; ++i) { const int p = p_order[i]; const int idx = equip_doll.parts[p]; if (idx == 0 || idx == TILEP_SHOW_EQUIP || flags[p] == TILEP_FLAG_HIDE) continue; ASSERT(idx >= TILE_MAIN_MAX && idx < TILEP_PLAYER_MAX); int ymax = TILE_Y; if (flags[p] == TILEP_FLAG_CUT_CENTAUR || flags[p] == TILEP_FLAG_CUT_NAGA) { ymax = 18; } tileset.push_back(tile_def(idx, TEX_PLAYER, ymax)); } if (player.held_in_net) tileset.push_back(tile_def(TILEP_TRAP_NET, TEX_PLAYER)); return (true); } #endif bool Menu::is_selectable(int item) const { if (select_filter.empty()) return (true); std::string text = items[item]->get_filter_text(); for (int i = 0, count = select_filter.size(); i < count; ++i) if (select_filter[i].matches(text)) return (true); return (false); } void Menu::select_index( int index, int qty ) { int si = index == -1? first_entry : index; if (index == -1) { if (flags & MF_MULTISELECT) { for (int i = 0, count = items.size(); i < count; ++i) { if (items[i]->level != MEL_ITEM || items[i]->hotkeys.empty()) { continue; } if (is_hotkey(i, items[i]->hotkeys[0]) && is_selectable(i)) { last_selected = i; items[i]->select( qty ); draw_item( i ); } } } } else if (items[si]->level == MEL_SUBTITLE && (flags & MF_MULTISELECT)) { for (int i = si + 1, count = items.size(); i < count; ++i) { if (items[i]->level != MEL_ITEM || items[i]->hotkeys.empty()) { continue; } if (is_hotkey(i, items[i]->hotkeys[0])) { last_selected = i; items[i]->select( qty ); draw_item( i ); } } } else if (items[si]->level == MEL_ITEM && (flags & (MF_SINGLESELECT | MF_MULTISELECT))) { last_selected = si; items[si]->select( qty ); draw_item( si ); } } int Menu::get_entry_index( const MenuEntry *e ) const { int index = 0; for (unsigned int i = first_entry; i < items.size(); i++) { if (items[i] == e) return (index); if (items[i]->quantity != 0) index++; } return -1; } void Menu::draw_menu() { if (crawl_state.doing_prev_cmd_again) return; clrscr(); draw_title(); draw_select_count( sel.size() ); y_offset = 1 + !!title; mdisplay->set_offset(y_offset); int end = first_entry + pagesize; if (end > (int) items.size()) end = items.size(); for (int i = first_entry; i < end; ++i) draw_item( i ); if (end < (int) items.size() || is_set(MF_ALWAYS_SHOW_MORE)) mdisplay->draw_more(); } void Menu::update_title() { int x = wherex(), y = wherey(); draw_title(); cgotoxy(x, y); } int Menu::item_colour(int, const MenuEntry *entry) const { int icol = -1; if (highlighter) icol = highlighter->entry_colour(entry); return (icol == -1? entry->colour : icol); } void Menu::draw_title() { if (title) { cgotoxy(1, 1); write_title(); } } void Menu::write_title() { const bool first = (action_cycle == CYCLE_NONE || menu_action == ACT_EXECUTE); if (!first) ASSERT(title2); textattr( item_colour(-1, first ? title : title2) ); std::string text = (first ? title->get_text() : title2->get_text()); cprintf("%s", text.c_str()); if (flags & MF_SHOW_PAGENUMBERS) { // The total number of pages is well defined, but the current // page a bit less so. To make sense, we hack it so that your // current page is based on the first line you're seeing, *unless* // you're seeing the last item. int numpages = items.empty() ? 1 : ((items.size()-1) / pagesize + 1); int curpage = first_entry / pagesize + 1; if (in_page(items.size() - 1)) curpage = numpages; cprintf(" (page %d of %d)", curpage, numpages); } const int x = wherex(), y = wherey(); cprintf("%-*s", get_number_of_cols() - x, ""); cgotoxy(x, y); } bool Menu::in_page(int index) const { return (index >= first_entry && index < first_entry + pagesize); } void Menu::draw_item( int index ) const { if (!in_page(index) || crawl_state.doing_prev_cmd_again) return; cgotoxy( 1, y_offset + index - first_entry ); draw_index_item(index, items[index]); } void Menu::draw_index_item(int index, const MenuEntry *me) const { if (crawl_state.doing_prev_cmd_again) return; if (f_drawitem) (*f_drawitem)(index, me); else draw_stock_item(index, me); } void Menu::draw_stock_item(int index, const MenuEntry *me) const { mdisplay->draw_stock_item(index, me); } bool Menu::page_down() { int old_first = first_entry; if ((int) items.size() > first_entry + pagesize) { first_entry += pagesize; //if (first_entry + pagesize > (int) items.size()) // first_entry = items.size() - pagesize; if (old_first != first_entry) return (true); } return (false); } bool Menu::page_up() { int old_first = first_entry; if (first_entry > 0) { if ((first_entry -= pagesize) < 0) first_entry = 0; if (old_first != first_entry) return (true); } return (false); } bool Menu::line_down() { if (first_entry + pagesize < (int) items.size()) { ++first_entry; return (true); } return (false); } bool Menu::line_up() { if (first_entry > 0) { --first_entry; return (true); } return (false); } ///////////////////////////////////////////////////////////////// // slider_menu slider_menu::slider_menu(int fl, bool text_only) : Menu(fl, "", text_only), less(), starty(1), endy(get_number_of_lines()), selected(0), need_less(true), need_more(true), oldselect(0), lastkey(0), search() { less.textcolor(DARKGREY); less.cprintf("<---- More"); more.clear(); more.textcolor(DARKGREY); more.cprintf("More ---->"); } void slider_menu::set_search(const std::string &s) { search = s; } void slider_menu::set_limits(int y1, int y2) { starty = y1; endy = y2; } void slider_menu::select_search(const std::string &s) { std::string srch = s; lowercase(srch); for (int i = 0, size = items.size(); i < size; ++i) { std::string text = items[i]->get_text(); lowercase(text); std::string::size_type found = text.find(srch); if (found != std::string::npos && found == text.find_first_not_of(" ")) { move_selection(i); break; } } } int slider_menu::post_process(int key) { const time_t now = time(NULL); if (now - lastkey >= 2) search.clear(); lastkey = now; select_search( search += key ); return (key); } bool slider_menu::process_key(int key) { // Some of this key processing should really be in a user-passed-in function // If we ever need to use slider_menu elsewhere, we should factor it out. if (key == CK_ESCAPE || key == '\t') { oldselect = selected; selected = -1; draw_item(oldselect); sel.clear(); search.clear(); lastch = key; return (false); } if (Menu::is_set(MF_NOWRAP) && selected == 0 && (key == CK_UP || key == CK_PGUP || key == '<' || key == ';')) { oldselect = selected; selected = -1; draw_item(oldselect); search.clear(); return (false); } return Menu::process_key(key); } void slider_menu::adjust_pagesizes(int recurse_depth) { if (first_entry == 1 && selected == 1) first_entry = 0; need_less = !!first_entry; pagesize = endy - starty + 1 - !!title - need_less; const int nitems = items.size(); need_more = first_entry + pagesize < nitems; if (need_more) pagesize--; if (selected != -1 && (selected < first_entry || selected >= first_entry + pagesize) && recurse_depth > 0) { fix_entry(recurse_depth - 1); } calc_y_offset(); } void slider_menu::display() { adjust_pagesizes(); if (selected != -1) oldselect = selected; selected = -1; draw_menu(); } std::vector slider_menu::show() { cursor_control coff(false); sel.clear(); adjust_pagesizes(); if (selected == -1) selected = oldselect; if (!search.empty()) select_search(search); fix_entry(); do_menu(); if (selected >= 0 && selected < (int) items.size()) sel.push_back(items[selected]); return (sel); } const MenuEntry *slider_menu::selected_entry() const { if (selected >= 0 && selected < (int) items.size()) return (items[selected]); return (NULL); } void slider_menu::fill_line() const { const int x = wherex(), maxx = get_number_of_cols(); if (x < maxx) cprintf("%-*s", maxx - x, ""); } void slider_menu::draw_stock_item(int index, const MenuEntry *me) const { Menu::draw_stock_item(index, me); fill_line(); } int slider_menu::item_colour(int index, const MenuEntry *me) const { int colour = Menu::item_colour(index, me); if (index == selected && selected != -1) { #if (defined(TARGET_OS_WINDOWS) && !defined(USE_TILE)) || defined(TARGET_OS_DOS) colour = dos_brand(colour, CHATTR_REVERSE); #elif defined(USE_TILE) colour = (colour == WHITE ? YELLOW : WHITE); #else colour |= COLFLAG_REVERSE; #endif } return (colour); } void slider_menu::show_less() { if (!need_less) return ; if (first_entry > 0) less.display(); else textattr(LIGHTGREY); fill_line(); } void slider_menu::show_more() { if (!need_more) return ; const int end = entry_end(); cgotoxy( 1, y_offset + pagesize ); if (end < (int) items.size() || is_set(MF_ALWAYS_SHOW_MORE)) mdisplay->draw_more(); else textattr(LIGHTGREY); fill_line(); } void slider_menu::calc_y_offset() { y_offset = starty + !!title + need_less; } int slider_menu::entry_end() const { int end = first_entry + pagesize; if (end > (int) items.size()) end = items.size(); return (end); } void slider_menu::draw_menu() { cgotoxy(1, starty); write_title(); calc_y_offset(); int end = entry_end(); // We're using get_number_of_cols() - 1 because we want to avoid line wrap // on DOS (the conio.h functions go batshit if that happens). cgotoxy(1, y_offset - 1); show_less(); mdisplay->set_offset(starty + 1); for (int i = first_entry; i < end; ++i) draw_item( i ); textattr(LIGHTGREY); for (int i = end; i < first_entry + pagesize; ++i) { cgotoxy(1, y_offset + i - first_entry); cprintf("%-*s", get_number_of_cols() - 2, ""); } show_more(); } void slider_menu::select_items(int, int) { // Ignored. } bool slider_menu::is_set(int flag) const { if (flag == MF_EASY_EXIT) return (false); return Menu::is_set(flag); } bool slider_menu::fix_entry(int recurse_depth) { if (selected < 0 || selected >= (int) items.size()) return (false); const int oldfirst = first_entry; if (selected < first_entry) first_entry = selected; else if (selected >= first_entry + pagesize) { first_entry = selected - pagesize + 1; if (first_entry < 0) first_entry = 0; } if (recurse_depth > 0) adjust_pagesizes(recurse_depth - 1); return (first_entry != oldfirst); } void slider_menu::new_selection(int nsel) { if (nsel < 0) { if (!is_set(MF_NOWRAP)) { do nsel += items.size(); while ( nsel < 0 ); } else nsel = 0; } if (nsel >= static_cast(items.size())) { if (!is_set(MF_NOWRAP)) { do { nsel -= items.size(); } while ( nsel >= static_cast(items.size()) ); } else nsel = items.size() - 1; } const int old = selected; selected = nsel; if (old != selected && nsel >= first_entry && nsel < first_entry + pagesize) { draw_item(old); draw_item(selected); } } bool slider_menu::move_selection(int nsel) { new_selection(nsel); return fix_entry(); } bool slider_menu::page_down() { search.clear(); return move_selection( selected + pagesize ); } bool slider_menu::page_up() { search.clear(); return move_selection( selected - pagesize ); } bool slider_menu::line_down() { search.clear(); return move_selection( selected + 1 ); } bool slider_menu::line_up() { search.clear(); return move_selection( selected - 1 ); } ///////////////////////////////////////////////////////////////// // Menu colouring // int menu_colour(const std::string &text, const std::string &prefix, const std::string &tag) { const std::string tmp_text = prefix + text; for (unsigned int i = 0; i < Options.menu_colour_mappings.size(); ++i) { const colour_mapping &cm = Options.menu_colour_mappings[i]; if ((cm.tag.empty() || cm.tag == "any" || cm.tag == tag || cm.tag == "inventory" && tag == "pickup") && cm.pattern.matches(tmp_text) ) { return (cm.colour); } } return (-1); } int MenuHighlighter::entry_colour(const MenuEntry *entry) const { return entry->highlight_colour(); } /////////////////////////////////////////////////////////////////////// // column_composer column_composer::column_composer(int cols, ...) : ncols(cols), pagesize(0), columns() { ASSERT(cols > 0); va_list args; va_start(args, cols); columns.push_back( column(1) ); int lastcol = 1; for (int i = 1; i < cols; ++i) { int nextcol = va_arg(args, int); ASSERT(nextcol > lastcol); lastcol = nextcol; columns.push_back( column(nextcol) ); } va_end(args); } void column_composer::set_pagesize(int ps) { pagesize = ps; } void column_composer::clear() { flines.clear(); } void column_composer::add_formatted(int ncol, const std::string &s, bool add_separator, bool eol_ends_format, bool (*tfilt)(const std::string &), int margin) { ASSERT(ncol >= 0 && ncol < (int) columns.size()); column &col = columns[ncol]; std::vector segs = split_string("\n", s, false, true); std::vector newlines; // Add a blank line if necessary. Blank lines will not // be added at page boundaries. if (add_separator && col.lines && !segs.empty() && (!pagesize || col.lines % pagesize)) { newlines.push_back(formatted_string()); } for (unsigned i = 0, size = segs.size(); i < size; ++i) { newlines.push_back( formatted_string::parse_string( segs[i], eol_ends_format, tfilt)); } strip_blank_lines(newlines); compose_formatted_column( newlines, col.lines, margin == -1? col.margin : margin ); col.lines += newlines.size(); strip_blank_lines(flines); } std::vector column_composer::formatted_lines() const { return (flines); } void column_composer::strip_blank_lines(std::vector &fs) const { for (int i = fs.size() - 1; i >= 0; --i) { if (fs[i].length() == 0) fs.erase( fs.begin() + i ); else break; } } void column_composer::compose_formatted_column( const std::vector &lines, int startline, int margin) { if (flines.size() < startline + lines.size()) flines.resize(startline + lines.size()); for (unsigned i = 0, size = lines.size(); i < size; ++i) { int f = i + startline; if (margin > 1) { int xdelta = margin - flines[f].length() - 1; if (xdelta > 0) flines[f].cprintf("%-*s", xdelta, ""); } flines[f] += lines[i]; } } formatted_scroller::formatted_scroller() : Menu() { set_highlighter(NULL); } formatted_scroller::formatted_scroller(int _flags, const std::string& s) : Menu(_flags) { set_highlighter(NULL); add_text(s); } void formatted_scroller::add_text(const std::string& s, bool new_line) { std::vector parts; formatted_string::parse_string_to_multiple(s, parts); for (unsigned int i = 0; i < parts.size(); ++i) add_item_formatted_string(parts[i]); if (new_line) add_item_formatted_string(formatted_string::parse_string(EOL)); } void formatted_scroller::add_item_formatted_string(const formatted_string& fs, int hotkey) { MenuEntry* me = new MenuEntry; me->data = new formatted_string(fs); if (hotkey) { me->add_hotkey(hotkey); me->quantity = 1; } add_entry(me); } void formatted_scroller::add_item_string(const std::string& s, int hotkey) { MenuEntry* me = new MenuEntry(s); if (hotkey) me->add_hotkey(hotkey); add_entry(me); } void formatted_scroller::draw_index_item(int index, const MenuEntry *me) const { if (me->data == NULL) Menu::draw_index_item(index, me); else static_cast(me->data)->display(); } formatted_scroller::~formatted_scroller() { // Very important: this destructor is called *before* the base // (Menu) class destructor... which is as it should be. for (unsigned i = 0; i < items.size(); ++i) if (items[i]->data != NULL) delete static_cast(items[i]->data); } int linebreak_string( std::string& s, int wrapcol, int maxcol ) { size_t loc = 0; int xpos = 0; int breakcount = 0; while (loc < s.size()) { if (s[loc] == '<') // tag { // << escape if (loc + 1 < s.size() && s[loc+1] == '<') { ++xpos; loc += 2; // Um, we never break on <<. That's a feature. Right. continue; } // skip tag while (loc < s.size() && s[loc] != '>') ++loc; ++loc; } else { // user-forced newline if (s[loc] == '\n') xpos = 0; // soft linebreak else if (s[loc] == ' ' && xpos > wrapcol) { s.replace(loc, 1, "\n"); xpos = 0; ++breakcount; } // hard linebreak else if (xpos > maxcol) { s.insert(loc, "\n"); xpos = 0; ++breakcount; } // bog-standard else ++xpos; ++loc; } } return breakcount; } int linebreak_string2( std::string& s, int maxcol ) { size_t loc = 0; int xpos = 0, spaceloc = 0; int breakcount = 0; while ( loc < s.size() ) { if ( s[loc] == '<' ) // tag { // << escape if ( loc + 1 < s.size() && s[loc+1] == '<' ) { ++xpos; loc += 2; // Um, we never break on <<. That's a feature. Right. continue; } // skip tag while ( loc < s.size() && s[loc] != '>' ) ++loc; ++loc; } else { // user-forced newline, or one we just stuffed in if (s[loc] == '\n') { xpos = 0; spaceloc = 0; ++loc; continue; } // force a wrap? if (xpos >= maxcol) { if (spaceloc) { loc = spaceloc; s.replace(loc, 1, "\n"); } else { s.insert(loc, "\n"); } ++breakcount; // reset pointers when we come around and see the \n continue; } // save possible linebreak location if (s[loc] == ' ' && xpos > 0) { spaceloc = loc; } ++xpos; ++loc; } } return breakcount; } std::string get_linebreak_string(const std::string& s, int maxcol) { std::string r = s; linebreak_string2(r, maxcol); return r; } // Takes a (possibly tagged) string, breaks it into lines and // prints it into the given message channel. void print_formatted_paragraph(std::string &s, msg_channel_type channel) { int maxcol = get_number_of_cols(); if (Options.delay_message_clear) --maxcol; linebreak_string2(s,maxcol); std::string text; size_t loc = 0, oldloc = 0; while ( loc < s.size() ) { if (s[loc] == '\n') { text = s.substr(oldloc, loc-oldloc); formatted_message_history( text, channel ); oldloc = ++loc; } loc++; } formatted_message_history( s.substr(oldloc, loc-oldloc), channel ); } bool formatted_scroller::jump_to( int i ) { if (i == first_entry + 1) return (false); if (i == 0) first_entry = 0; else first_entry = i - 1; return (true); } // Don't scroll past MEL_TITLE entries bool formatted_scroller::page_down() { const int old_first = first_entry; if ((int) items.size() <= first_entry + pagesize) return (false); // If, when scrolling forward, we encounter a MEL_TITLE // somewhere in the newly displayed page, stop scrolling // just before it becomes visible int target; for (target = first_entry; target < first_entry + pagesize; ++target ) { const int offset = target + pagesize; if (offset < (int)items.size() && items[offset]->level == MEL_TITLE) break; } first_entry = target; return (old_first != first_entry); } bool formatted_scroller::page_up() { int old_first = first_entry; // If, when scrolling backward, we encounter a MEL_TITLE // somewhere in the newly displayed page, stop scrolling // just before it becomes visible if (items[first_entry]->level == MEL_TITLE) return (false); for (int i = 0; i < pagesize; ++i) { if (first_entry == 0 || items[first_entry-1]->level == MEL_TITLE) break; --first_entry; } return (old_first != first_entry); } bool formatted_scroller::line_down() { if (first_entry + pagesize < static_cast(items.size()) && items[first_entry + pagesize]->level != MEL_TITLE) { ++first_entry; return (true); } return (false); } bool formatted_scroller::line_up() { if (first_entry > 0 && items[first_entry-1]->level != MEL_TITLE && items[first_entry]->level != MEL_TITLE) { --first_entry; return (true); } return (false); } bool formatted_scroller::jump_to_hotkey( int keyin ) { for (unsigned int i = 0; i < items.size(); ++i) if (items[i]->is_hotkey(keyin)) return jump_to(i); return (false); } bool formatted_scroller::process_key( int keyin ) { lastch = keyin; if (f_keyfilter) keyin = (*f_keyfilter)(keyin); bool repaint = false; // Any key is assumed to be a movement key for now... bool moved = true; switch (keyin) { case 0: return (true); case -1: case CK_ESCAPE: case CK_MOUSE_CMD: return (false); case ' ': case '+': case '=': case CK_PGDN: case '>': case '\'': case CK_MOUSE_B5: case CK_MOUSE_CLICK: repaint = page_down(); break; case '-': case CK_PGUP: case '<': case ';': case CK_MOUSE_B4: repaint = page_up(); break; case CK_UP: repaint = line_up(); break; case CK_DOWN: case CK_ENTER: repaint = line_down(); break; case CK_HOME: repaint = jump_to(0); break; case CK_END: { const int breakpoint = (items.size() + 1) - pagesize; if (first_entry < breakpoint) repaint = jump_to(breakpoint); break; } default: if (is_set(MF_SINGLESELECT)) { select_items( keyin, -1 ); get_selected( &sel ); if (sel.size() >= 1) return (false); } else repaint = jump_to_hotkey(keyin); break; } if (repaint) draw_menu(); else if (moved && is_set(MF_EASY_EXIT)) return (false); return (true); } int ToggleableMenu::pre_process(int key) { if (std::find(toggle_keys.begin(), toggle_keys.end(), key) != toggle_keys.end()) { // Toggle all menu entries for (unsigned int i = 0; i < items.size(); ++i) { ToggleableMenuEntry* const p = dynamic_cast(items[i]); if (p) p->toggle(); } // Toggle title ToggleableMenuEntry* const pt = dynamic_cast(title); if (pt) pt->toggle(); // Redraw draw_menu(); // Don't further process the key return 0; } return key; }