#include "menu.hpp" #include "../font.hpp" #include "../sdl_utils.hpp" #include "../show_dialog.hpp" #include "../util.hpp" #include "../video.hpp" #include namespace { const size_t menu_font_size = 14; const size_t menu_cell_padding = 10; } namespace gui { menu::menu(display& disp, const std::vector& items, bool click_selects, int max_height) : max_height_(max_height), max_items_(-1), item_height_(-1), display_(&disp), x_(0), y_(0), cur_help_(-1,-1), help_string_(-1), buffer_(NULL), selected_(click_selects ? -1:0), click_selects_(click_selects), previous_button_(true), drawn_(false), show_result_(false), height_(-1), width_(-1), first_item_on_screen_(0), uparrow_(disp,"",gui::button::TYPE_PRESS,"uparrow-button"), downarrow_(disp,"",gui::button::TYPE_PRESS,"downarrow-button"), scrollbar_(disp,this), scrollbar_height_(0), double_clicked_(false), num_selects_(true), ignore_next_doubleclick_(false), last_was_doubleclick_(false) { for(std::vector::const_iterator item = items.begin(); item != items.end(); ++item) { items_.push_back(config::quoted_split(*item,',',false)); //make sure there is always at least one item if(items_.back().empty()) items_.back().push_back(" "); //if the first character in an item is an asterisk, //it means this item should be selected by default std::string& first_item = items_.back().front(); if(first_item.empty() == false && first_item[0] == '*') { selected_ = items_.size()-1; first_item.erase(first_item.begin()); } } create_help_strings(); } void menu::create_help_strings() { help_.clear(); for(std::vector >::iterator i = items_.begin(); i != items_.end(); ++i) { help_.resize(help_.size()+1); for(std::vector::iterator j = i->begin(); j != i->end(); ++j) { if(std::find(j->begin(),j->end(),HELP_STRING_SEPERATOR) == j->end()) { help_.back().push_back(""); } else { const std::vector& items = config::split(*j,HELP_STRING_SEPERATOR,0); if(items.size() >= 2) { *j = items.front(); help_.back().push_back(items.back()); } else { help_.back().push_back(""); } } } } } // The scrollbar height depends on the number of visible items versus void menu::set_scrollbar_height() { int buttons_height = downarrow_.height() + uparrow_.height(); float pos_percent = (float)max_items_onscreen()/(float)items_.size(); scrollbar_height_ = (int)(pos_percent * (height()-buttons_height)); int min_height = scrollbar_.get_minimum_grip_height(); if (scrollbar_height_ < min_height) scrollbar_height_ = min_height; if (scrollbar_height_ > height()) { std::cerr << "Strange. For some reason I want my scrollbar" << " to be larger than me!\n\n"; std::cerr << "pos_percent=" << pos_percent << " height()=" << height() << std::endl; scrollbar_height_ = height() - buttons_height; } scrollbar_.set_grip_height(scrollbar_height_); } int menu::height() const { if(height_ == -1) { height_ = 0; for(size_t i = 0; i != items_.size() && i != max_items_onscreen(); ++i) { height_ += get_item_rect(i).h; } } return height_; } int menu::width() const { if(width_ == -1) { const std::vector& widths = column_widths(); width_ = std::accumulate(widths.begin(),widths.end(),0); if(show_scrollbar()) { width_ += scrollbar_.get_max_width(); } } return width_; } int menu::selection() const { return selected_; } void menu::set_loc(int x, int y) { x_ = x; y_ = y; const int w = width(); SDL_Rect portion = {x_,y_,w,height()}; SDL_Surface* const screen = display_->video().getSurface(); buffer_.assign(get_surface_portion(screen, portion)); if(show_scrollbar()) { const int menu_width = width() - scrollbar_.get_max_width(); scrollbar_.enable(true); int scr_width = scrollbar_.get_width(); SDL_Rect scroll_rect = {x_ + menu_width, y_+uparrow_.height(), scr_width, height()-downarrow_.height()-uparrow_.height()}; scrollbar_.set_location(scroll_rect); set_scrollbar_height(); uparrow_.set_location(x_ + menu_width,y_); downarrow_.set_location(x_+ menu_width,scrollbar_.location().y + scrollbar_.location().h); } } void menu::set_width(int w) { width_ = w; set_loc(x_, y_); itemRects_.clear(); } void menu::redraw() { if(x_ == 0 && y_ == 0) { return; } draw(); uparrow_.draw(); downarrow_.draw(); scrollbar_.redraw(); } void menu::change_item(int pos1, int pos2,std::string str) { items_[pos1][pos2] = str; undrawn_items_.insert(pos1); } void menu::erase_item(size_t index) { if(index < items_.size()) { clear_item(items_.size()-1); items_.erase(items_.begin() + index); itemRects_.clear(); if(size_t(selected_) >= items_.size()) { selected_ = int(items_.size()-1); } calculate_position(); drawn_ = false; } } void menu::set_items(const std::vector& items) { items_.clear(); itemRects_.clear(); column_widths_.clear(); undrawn_items_.clear(); height_ = -1; // Force recalculation of the height. width_ = -1; // Force recalculation of the width. max_items_ = -1; // Force recalculation of the max items. // Scrollbar and buttons will be reanabled if they are needed. scrollbar_.enable(false); uparrow_.hide(true); downarrow_.hide(true); first_item_on_screen_ = 0; selected_ = click_selects_ ? -1:0; for (std::vector::const_iterator item = items.begin(); item != items.end(); ++item) { items_.push_back(config::quoted_split(*item,',',false)); //make sure there is always at least one item if(items_.back().empty()) items_.back().push_back(" "); //if the first character in an item is an asterisk, //it means this item should be selected by default std::string& first_item = items_.back().front(); if(first_item.empty() == false && first_item[0] == '*') { selected_ = items_.size()-1; first_item.erase(first_item.begin()); } } set_loc(x_, y_); // Force some more updating. calculate_position(); drawn_ = false; } void menu::set_max_height(const int new_max_height) { max_height_ = new_max_height; } size_t menu::max_items_onscreen() const { if(max_items_ != -1) { return size_t(max_items_); } const size_t max_height = max_height_ == -1 ? (display_->y()*66)/100 : max_height_; std::vector heights; size_t n; for(n = 0; n != items_.size(); ++n) { heights.push_back(get_item_height(n)); } std::sort(heights.begin(),heights.end(),std::greater()); size_t sum = 0; for(n = 0; n != items_.size() && sum < max_height; ++n) { sum += heights[n]; } if(sum > max_height && n > 1) --n; return max_items_ = n; } void menu::calculate_position() { if(click_selects_) return; if(selected_ < first_item_on_screen_) { first_item_on_screen_ = selected_; itemRects_.clear(); drawn_ = false; } if(selected_ >= first_item_on_screen_ + int(max_items_onscreen())) { first_item_on_screen_ = selected_ - (max_items_onscreen() - 1); itemRects_.clear(); drawn_ = false; } } void menu::key_press(SDLKey key) { switch(key) { case SDLK_UP: { if(!click_selects_ && selected_ > 0) { --selected_; calculate_position(); undrawn_items_.insert(selected_); undrawn_items_.insert(selected_+1); } break; } case SDLK_DOWN: { if(!click_selects_ && selected_ < int(items_.size())-1) { ++selected_; calculate_position(); undrawn_items_.insert(selected_); undrawn_items_.insert(selected_-1); } break; } case SDLK_PAGEUP: { if(!click_selects_) { selected_ -= max_items_onscreen(); if(selected_ < 0) selected_ = 0; calculate_position(); drawn_ = false; } break; } case SDLK_PAGEDOWN: { if(!click_selects_) { selected_ += max_items_onscreen(); if(selected_ >= int(items_.size())) selected_ = int(items_.size())-1; calculate_position(); drawn_ = false; } break; } default: break; } if(key >= SDLK_1 && key <= SDLK_9 && num_selects_) { const int pos = key - SDLK_1; if(size_t(pos) < items_.size()) { undrawn_items_.insert(selected_); selected_ = pos; calculate_position(); undrawn_items_.insert(selected_); } } } void menu::handle_event(const SDL_Event& event) { if(event.type == SDL_KEYDOWN) { key_press(event.key.keysym.sym); } else if(event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT || event.type == DOUBLE_CLICK_EVENT) { int x = 0; int y = 0; if(event.type == SDL_MOUSEBUTTONDOWN) { x = event.button.x; y = event.button.y; } else { x = (int)event.user.data1; y = (int)event.user.data2; } const int item = hit(x,y); if(item != -1) { undrawn_items_.insert(selected_); selected_ = item; undrawn_items_.insert(selected_); if(click_selects_) { show_result_ = true; } if(event.type == DOUBLE_CLICK_EVENT) { if (ignore_next_doubleclick_) { ignore_next_doubleclick_ = false; } else { double_clicked_ = true; last_was_doubleclick_ = true; } } else if (last_was_doubleclick_) { // If we have a double click as the next event, it means // this double click was generated from a click that // already has helped in generating a double click. SDL_Event ev; SDL_PeepEvents(&ev, 1, SDL_PEEKEVENT, SDL_EVENTMASK(DOUBLE_CLICK_EVENT)); if (ev.type == DOUBLE_CLICK_EVENT) { ignore_next_doubleclick_ = true; } last_was_doubleclick_ = false; } } } else if(event.type == SDL_MOUSEMOTION && click_selects_) { const int item = hit(event.motion.x,event.motion.y); if(item != selected_) { selected_ = item; drawn_ = false; } } else if(event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_WHEELDOWN) { if(first_item_on_screen_+max_items_onscreen()0) { --first_item_on_screen_; if (selected_>=first_item_on_screen_+int(max_items_onscreen())) selected_=first_item_on_screen_+max_items_onscreen()-1; itemRects_.clear(); drawn_ = false; } } } int menu::process(int x, int y, bool button,bool up_arrow,bool down_arrow, bool page_up, bool page_down, int select_item) { static int last_scroll_position = 0; bool scroll_in_use = false; const int last_top_idx = items_.size() - max_items_onscreen(); int max_scroll_position = scrollbar_.location().h-scrollbar_height_; if(show_scrollbar()) { const bool up = uparrow_.process(x,y,button); if(up && first_item_on_screen_ > 0) { --first_item_on_screen_; itemRects_.clear(); drawn_ = false; } const bool down = downarrow_.process(x,y,button); if(down && first_item_on_screen_ < last_top_idx) { ++first_item_on_screen_; itemRects_.clear(); drawn_ = false; } scrollbar_.process(); int scroll_position = scrollbar_.get_grip_position(); int new_first_item; if (scroll_position != last_scroll_position) { last_scroll_position = scroll_position; if (scroll_position == 0) { new_first_item = 0; } else if (scroll_position > (max_scroll_position)) { new_first_item = last_top_idx; } else { new_first_item = scroll_position * last_top_idx / max_scroll_position; } if (new_first_item != first_item_on_screen_) { scroll_in_use = true; first_item_on_screen_ = new_first_item; itemRects_.clear(); drawn_ = false; } } else { int groove = scrollbar_.groove_clicked(); if (groove == -1) { first_item_on_screen_ -= max_items_onscreen(); if (first_item_on_screen_ <= 0) { first_item_on_screen_ = 0; } itemRects_.clear(); drawn_ = false; } else if (groove == 1) { first_item_on_screen_ += max_items_onscreen(); if (first_item_on_screen_ > last_top_idx) { first_item_on_screen_ = last_top_idx; } itemRects_.clear(); drawn_ = false; } } } if(!drawn_) { if (!scroll_in_use && show_scrollbar()) { int new_scrollpos = (first_item_on_screen_ * max_scroll_position) / last_top_idx; scrollbar_.set_grip_position(new_scrollpos); last_scroll_position = new_scrollpos; } draw(); } if(show_result_) { show_result_ = false; return selected_; } else { return -1; } } bool menu::show_scrollbar() const { return items_.size() > max_items_onscreen(); } bool menu::double_clicked() { bool old = double_clicked_; double_clicked_ = false; return old; } void menu::set_numeric_keypress_selection(bool value) { num_selects_ = value; } void menu::scroll(int pos) { } namespace { const char ImagePrefix = '&'; SDL_Rect item_size(const std::string& item) { SDL_Rect res = {0,0,0,0}; if(item.empty() == false && item[0] == ImagePrefix) { const std::string image_name(item.begin()+1,item.end()); SDL_Surface* const img = image::get_image(image_name,image::UNSCALED); if(img != NULL) { res.w = img->w; res.h = img->h; } } else { const SDL_Rect area = {0,0,10000,10000}; res = font::draw_text(NULL,area,menu_font_size,font::NORMAL_COLOUR,item,0,0); } return res; } } const std::vector& menu::column_widths() const { if(column_widths_.empty()) { for(size_t row = 0; row != items_.size(); ++row) { for(size_t col = 0; col != items_[row].size(); ++col) { static const SDL_Rect area = {0,0,display_->x(),display_->y()}; const SDL_Rect res = item_size(items_[row][col]); if(col == column_widths_.size()) { column_widths_.push_back(res.w + menu_cell_padding); } else if(res.w > column_widths_[col] - menu_cell_padding) { column_widths_[col] = res.w + menu_cell_padding; } } } } return column_widths_; } void menu::clear_item(int item) { SDL_Rect rect = get_item_rect(item); if(rect.w == 0) { return; } if(buffer_.get() != NULL) { const int ypos = (item-first_item_on_screen_)*rect.h; SDL_Rect srcrect = {0,ypos,rect.w,rect.h}; SDL_Rect dstrect = rect; SDL_BlitSurface(buffer_,&srcrect, display_->video().getSurface(),&dstrect); } } void menu::draw_item(int item) { SDL_Rect rect = get_item_rect(item); if(rect.w == 0) { return; } clear_item(item); gui::draw_solid_tinted_rectangle(x_,rect.y,width()-scrollbar_.get_width(),rect.h, item == selected_ ? 150:0,0,0, item == selected_ ? 0.6 : 0.2, display_->video().getSurface()); SDL_Rect area = display_->screen_area(); const std::vector& widths = column_widths(); int xpos = rect.x; for(size_t i = 0; i != items_[item].size(); ++i) { const std::string& str = items_[item][i]; if(str.empty() == false && str[0] == ImagePrefix) { const std::string image_name(str.begin()+1,str.end()); SDL_Surface* const img = image::get_image(image_name,image::UNSCALED); if(img != NULL && xpos+img->w < display_->x() && rect.y+img->h < display_->y()) { display_->blit_surface(xpos,rect.y,img); } } else { const SDL_Rect& text_size = font::text_area(str,menu_font_size); const size_t y = rect.y + (rect.h - text_size.h)/2; font::draw_text(display_,area,menu_font_size,font::NORMAL_COLOUR,str,xpos,y); } xpos += widths[i]; } } void menu::draw() { if(x_ == 0 && y_ == 0 || drawn_ && undrawn_items_.empty()) { return; } if(drawn_) { for(std::set::const_iterator i = undrawn_items_.begin(); i != undrawn_items_.end(); ++i) { if(*i < items_.size()) { draw_item(*i); update_rect(get_item_rect(*i)); } } undrawn_items_.clear(); return; } undrawn_items_.clear(); drawn_ = true; // update enabled/disabled status for up/down buttons if(show_scrollbar()) { uparrow_.hide(first_item_on_screen_ == 0); downarrow_.hide(first_item_on_screen_ >= items_.size() - max_items_onscreen()); } for(size_t i = 0; i != items_.size(); ++i) draw_item(i); update_rect(x_,y_,width(),height()); } int menu::hit(int x, int y) const { if(x > x_ && x < x_ + width() - scrollbar_.get_width() && y > y_ && y < y_ + height()) { for(size_t i = 0; i != items_.size(); ++i) { const SDL_Rect& rect = get_item_rect(i); if(y > rect.y && y < rect.y + rect.h) return i; } } return -1; } std::pair menu::hit_cell(int x, int y) const { if(x > x_ && x < x_ + width() - scrollbar_.get_width() && y > y_ && y < y_ + height()) { for(size_t i = 0; i != items_.size(); ++i) { const SDL_Rect& rect = get_item_rect(i); if(y > rect.y && y < rect.y + rect.h) { const std::vector& widths = column_widths(); for(std::vector::const_iterator w = widths.begin(); w != widths.end(); ++w) { x -= *w; if(x <= x_) { return std::pair(int(i),int(w-widths.begin())); } } } } } return std::pair(-1,-1); } SDL_Rect menu::get_item_rect(int item) const { const SDL_Rect empty_rect = {0,0,0,0}; if(item < first_item_on_screen_ || size_t(item) >= first_item_on_screen_ + max_items_onscreen()) { return empty_rect; } const std::map::const_iterator i = itemRects_.find(item); if(i != itemRects_.end()) return i->second; int y = y_; if(item != first_item_on_screen_) { const SDL_Rect& prev = get_item_rect(item-1); y = prev.y + prev.h; } const SDL_Rect screen_area = display_->screen_area(); SDL_Rect res = {x_, y, width() - scrollbar_.get_width(), get_item_height(item)}; if(res.x > screen_area.w) { return empty_rect; } else if(res.x + res.w > screen_area.w) { res.w = screen_area.w - res.x; } if(res.y > screen_area.h) { return empty_rect; } else if(res.y + res.h > screen_area.h) { res.h = screen_area.h - res.y; } //only insert into the cache if the menu's co-ordinates have //been initialized if(x_ > 0 && y_ > 0) itemRects_.insert(std::pair(item,res)); return res; } size_t menu::get_item_height_internal(int item) const { size_t res = 0; for(size_t n = 0; n != items_[item].size(); ++n) { SDL_Rect rect = item_size(items_[item][n]); res = maximum(rect.h,res); } return res; } size_t menu::get_item_height(int item) const { if(item_height_ != -1) return size_t(item_height_); size_t max_height = 0; for(size_t n = 0; n != items_.size(); ++n) { max_height = maximum(max_height,get_item_height_internal(n)); } return item_height_ = max_height; } void menu::process_help_string(int mousex, int mousey) { const std::pair loc = hit_cell(mousex,mousey); if(loc == cur_help_) { return; } else if(loc.first == -1) { display_->clear_help_string(help_string_); help_string_ = -1; } else { if(help_string_ != -1) { display_->clear_help_string(help_string_); help_string_ = -1; } if(size_t(loc.first) < help_.size()) { const std::vector& row = help_[loc.first]; if(size_t(loc.second) < help_.size()) { const std::string& help = row[loc.second]; help_string_ = display_->set_help_string(help); } } } cur_help_ = loc; } }