1 /*
2 * Copyright (C) 2016-2020 by the Widelands Development Team
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 *
18 */
19
20 #include "ui_basic/dropdown.h"
21
22 #include "base/i18n.h"
23 #include "graphic/font_handler.h"
24 #include "graphic/rendertarget.h"
25 #include "graphic/text_layout.h"
26 #include "ui_basic/tabpanel.h"
27 #include "ui_basic/window.h"
28
29 namespace {
base_height(int button_dimension,UI::PanelStyle style)30 int base_height(int button_dimension, UI::PanelStyle style) {
31 int result =
32 std::max(button_dimension, text_height(g_gr->styles().table_style(style).enabled()) + 2);
33 return result;
34 }
35 } // namespace
36
37 namespace UI {
38
39 int BaseDropdown::next_id_ = 0;
40
41 // Dropdowns hook into parent elements to be notified of layouting changes. We need to keep track of
42 // whether a dropdown actually still exists when notified to avoid heap-use-after-free's.
43 static std::map<int, BaseDropdown*> living_dropdowns_;
44 // static
layout_if_alive(int id)45 void BaseDropdown::layout_if_alive(int id) {
46 auto it = living_dropdowns_.find(id);
47 if (it != living_dropdowns_.end()) {
48 it->second->layout();
49 }
50 }
51
BaseDropdown(UI::Panel * parent,const std::string & name,int32_t x,int32_t y,uint32_t w,uint32_t max_list_items,int button_dimension,const std::string & label,const DropdownType type,UI::PanelStyle style,ButtonStyle button_style)52 BaseDropdown::BaseDropdown(UI::Panel* parent,
53 const std::string& name,
54 int32_t x,
55 int32_t y,
56 uint32_t w,
57 uint32_t max_list_items,
58 int button_dimension,
59 const std::string& label,
60 const DropdownType type,
61 UI::PanelStyle style,
62 ButtonStyle button_style)
63 : UI::NamedPanel(parent,
64 name,
65 x,
66 y,
67 (type == DropdownType::kPictorial || type == DropdownType::kPictorialMenu) ?
68 button_dimension :
69 w,
70 // Height only to fit the button, so we can use this in Box layout.
71 base_height(button_dimension, style)),
72 id_(next_id_++),
73 max_list_items_(max_list_items),
74 max_list_height_(std::numeric_limits<uint32_t>::max()),
75 list_offset_x_(0),
76 list_offset_y_(0),
77 base_height_(base_height(button_dimension, style)),
78 mouse_tolerance_(50),
79 button_box_(this, 0, 0, UI::Box::Horizontal, w, get_h()),
80 push_button_(type == DropdownType::kTextual ?
81 new UI::Button(&button_box_,
82 "dropdown_select",
83 0,
84 0,
85 button_dimension,
86 get_h(),
87 button_style,
88 g_gr->images().get("images/ui_basic/scrollbar_down.png")) :
89 nullptr),
90 display_button_(&button_box_,
91 "dropdown_label",
92 0,
93 0,
94 type == DropdownType::kTextual ?
95 w - button_dimension :
96 type == DropdownType::kTextualNarrow ? w : button_dimension,
97 get_h(),
98 type == DropdownType::kTextual ?
99 (style == UI::PanelStyle::kFsMenu ? UI::ButtonStyle::kFsMenuSecondary :
100 UI::ButtonStyle::kWuiSecondary) :
101 button_style,
102 label),
103 label_(label),
104 type_(type),
105 is_enabled_(true),
106 button_style_(button_style),
107 autoexpand_display_button_(false) {
108 if (label.empty()) {
109 set_tooltip(pgettext("dropdown", "Select Item"));
110 } else {
111 set_tooltip(label);
112 }
113
114 // Close whenever another dropdown is opened
115 dropdown_subscriber_ = Notifications::subscribe<NoteDropdown>([this](const NoteDropdown& note) {
116 if (id_ != note.id) {
117 close();
118 }
119 });
120 graphic_resolution_changed_subscriber_ = Notifications::subscribe<GraphicResolutionChanged>(
121 [this](const GraphicResolutionChanged&) { layout(); });
122
123 assert(max_list_items_ > 0);
124 // Hook into highest parent that we can get so that we can drop down outside the panel.
125 UI::Panel* list_parent = &display_button_;
126 while (list_parent->get_parent()) {
127 list_parent = list_parent->get_parent();
128 }
129 list_ =
130 new UI::Listselect<uintptr_t>(list_parent, 0, 0, w, 0, style, ListselectLayout::kDropdown);
131 list_->set_notify_on_delete(this);
132
133 list_->set_visible(false);
134 button_box_.add(&display_button_, UI::Box::Resizing::kExpandBoth);
135 display_button_.sigclicked.connect([this]() { toggle_list(); });
136 if (push_button_ != nullptr) {
137 display_button_.set_perm_pressed(true);
138 button_box_.add(push_button_, UI::Box::Resizing::kFullSize);
139 push_button_->sigclicked.connect([this]() { toggle_list(); });
140 }
141 button_box_.set_size(w, get_h());
142 list_->clicked.connect([this]() { set_value(); });
143 list_->clicked.connect([this]() { toggle_list(); });
144 set_can_focus(true);
145 set_value();
146
147 const int serial = id_; // Not a member variable, because when the lambda below is triggered we
148 // might no longer exist
149 living_dropdowns_.insert(std::make_pair(serial, this));
150 // Find parent windows, boxes etc. so that we can move the list along with them
151 UI::Panel* ancestor = this;
152 while ((ancestor = ancestor->get_parent()) != nullptr) {
153 ancestor->position_changed.connect([serial] { layout_if_alive(serial); });
154 }
155 layout();
156 }
157
~BaseDropdown()158 BaseDropdown::~BaseDropdown() {
159 // The list needs to be able to drop outside of windows, so it won't close with the window.
160 // So, we tell it to die.
161 if (list_) {
162 list_->set_notify_on_delete(nullptr);
163 list_->die();
164 }
165
166 // Unsubscribe from layouting hooks
167 assert(living_dropdowns_.find(id_) != living_dropdowns_.end());
168 living_dropdowns_.erase(living_dropdowns_.find(id_));
169 }
170
set_height(int height)171 void BaseDropdown::set_height(int height) {
172 max_list_height_ = height - base_height_;
173 layout();
174 }
175
layout()176 void BaseDropdown::layout() {
177 int list_width = list_->calculate_desired_width();
178
179 const int new_list_height = std::min(max_list_height_ / list_->get_lineheight(),
180 std::min(list_->size(), max_list_items_)) *
181 list_->get_lineheight();
182 list_->set_size(std::max(list_width, button_box_.get_w()), new_list_height);
183
184 // Update list position. The list is hooked into the highest parent that we can get so that we
185 // can drop down outside the panel.
186 UI::Panel* parent = &display_button_;
187 int new_list_x = display_button_.get_x();
188 int new_list_y = display_button_.get_y();
189 while (parent->get_parent()) {
190 parent = parent->get_parent();
191 new_list_x += parent->get_x() + parent->get_lborder();
192 new_list_y += parent->get_y() + parent->get_tborder();
193 }
194
195 // Drop up instead of down if it doesn't fit
196 if (new_list_y + list_->get_h() > g_gr->get_yres()) {
197 list_offset_y_ = -list_->get_h();
198 } else {
199 list_offset_y_ = display_button_.get_h();
200 }
201
202 // Right align instead of left align if it doesn't fit
203 if (new_list_x + list_->get_w() > g_gr->get_xres()) {
204 list_offset_x_ = display_button_.get_w() - list_->get_w();
205 if (push_button_ != nullptr) {
206 list_offset_x_ += push_button_->get_w();
207 }
208 }
209
210 list_->set_pos(Vector2i(new_list_x + list_offset_x_, new_list_y + list_offset_y_));
211
212 // Keep open list on top while dragging
213 // TODO(GunChleoc): It would be better to close the list if any other panel is clicked,
214 // but we'd need a global "clicked" signal in the Panel class for that.
215 // This will imply a complete overhaul of the signal names.
216 if (list_->is_visible()) {
217 list_->move_to_top();
218 }
219 }
220
set_size(int nw,int nh)221 void BaseDropdown::set_size(int nw, int nh) {
222 button_box_.set_size(nw, nh);
223 Panel::set_size(nw, nh);
224 layout();
225 }
set_desired_size(int nw,int nh)226 void BaseDropdown::set_desired_size(int nw, int nh) {
227 button_box_.set_desired_size(nw, nh);
228 Panel::set_desired_size(nw, nh);
229 layout();
230 }
231
set_autoexpand_display_button()232 void BaseDropdown::set_autoexpand_display_button() {
233 autoexpand_display_button_ = true;
234 }
235
add(const std::string & name,const uint32_t value,const Image * pic,const bool select_this,const std::string & tooltip_text,const std::string & hotkey)236 void BaseDropdown::add(const std::string& name,
237 const uint32_t value,
238 const Image* pic,
239 const bool select_this,
240 const std::string& tooltip_text,
241 const std::string& hotkey) {
242 assert(pic != nullptr || type_ != DropdownType::kPictorial);
243 list_->add(name, value, pic, select_this, tooltip_text, hotkey);
244 if (select_this) {
245 set_value();
246 }
247
248 if (autoexpand_display_button_) {
249 /// Fit width of display button to make enough room for the entry's text
250 const std::string fitme =
251 label_.empty() ? name : (boost::format(_("%1%: %2%")) % label_ % name).str();
252 const int new_width =
253 text_width(fitme, g_gr->styles().button_style(button_style_).enabled().font()) + 8;
254 if (new_width > display_button_.get_w()) {
255 set_desired_size(get_w() + new_width - display_button_.get_w(), get_h());
256 set_size(get_w() + new_width - display_button_.get_w(), get_h());
257 }
258 }
259 layout();
260 }
261
has_selection() const262 bool BaseDropdown::has_selection() const {
263 return list_->has_selection();
264 }
265
get_selected() const266 uint32_t BaseDropdown::get_selected() const {
267 assert(has_selection());
268 return list_->get_selected();
269 }
270
select(uint32_t entry)271 void BaseDropdown::select(uint32_t entry) {
272 assert(entry < list_->size());
273 list_->select(entry);
274 current_selection_ = list_->selection_index();
275 update();
276 }
277
set_label(const std::string & text)278 void BaseDropdown::set_label(const std::string& text) {
279 label_ = text;
280 if (type_ != DropdownType::kPictorial && type_ != DropdownType::kPictorialMenu) {
281 display_button_.set_title(label_);
282 }
283 }
284
set_image(const Image * image)285 void BaseDropdown::set_image(const Image* image) {
286 display_button_.set_pic(image);
287 }
288
set_tooltip(const std::string & text)289 void BaseDropdown::set_tooltip(const std::string& text) {
290 tooltip_ = text;
291 display_button_.set_tooltip(tooltip_);
292 if (push_button_) {
293 push_button_->set_tooltip(push_button_->enabled() ? tooltip_ : "");
294 }
295 }
296
set_errored(const std::string & error_message)297 void BaseDropdown::set_errored(const std::string& error_message) {
298 set_tooltip((boost::format(_("%1%: %2%")) % _("Error") % error_message).str());
299 if (type_ != DropdownType::kPictorial && type_ != DropdownType::kPictorialMenu) {
300 set_label(_("Error"));
301 } else {
302 set_image(g_gr->images().get("images/ui_basic/different.png"));
303 }
304 }
305
set_enabled(bool on)306 void BaseDropdown::set_enabled(bool on) {
307 is_enabled_ = on;
308 set_can_focus(on);
309 if (push_button_ != nullptr) {
310 push_button_->set_enabled(on);
311 push_button_->set_tooltip(on ? tooltip_ : "");
312 }
313 display_button_.set_enabled(on);
314 list_->set_visible(false);
315 }
316
set_disable_style(UI::ButtonDisableStyle disable_style)317 void BaseDropdown::set_disable_style(UI::ButtonDisableStyle disable_style) {
318 display_button_.set_disable_style(disable_style);
319 }
320
is_expanded() const321 bool BaseDropdown::is_expanded() const {
322 return list_->is_visible();
323 }
324
set_pos(Vector2i point)325 void BaseDropdown::set_pos(Vector2i point) {
326 UI::Panel::set_pos(point);
327 layout();
328 }
329
clear()330 void BaseDropdown::clear() {
331 close();
332 list_->clear();
333 current_selection_ = list_->selection_index();
334 list_->set_size(list_->get_w(), 0);
335 list_->set_visible(false);
336 set_layout_toplevel(false);
337 }
338
think()339 void BaseDropdown::think() {
340 if (list_->is_visible()) {
341 // Autocollapse with a bit of tolerance for the mouse movement to make it less fiddly.
342 if (!(has_focus() || list_->has_focus()) || is_mouse_away()) {
343 toggle_list();
344 }
345 }
346 }
347
size() const348 uint32_t BaseDropdown::size() const {
349 return list_->size();
350 }
351
update()352 void BaseDropdown::update() {
353 if (type_ == DropdownType::kPictorialMenu) {
354 // Menus never change their main image and text
355 return;
356 }
357
358 const std::string name = list_->has_selection() ?
359 list_->get_selected_name() :
360 /** TRANSLATORS: Selection in Dropdown menus. */
361 pgettext("dropdown", "Not Selected");
362
363 if (type_ != DropdownType::kPictorial) {
364 if (label_.empty()) {
365 display_button_.set_title(name);
366 } else {
367 /** TRANSLATORS: Label: Value. */
368 display_button_.set_title((boost::format(_("%1%: %2%")) % label_ % (name)).str());
369 }
370 display_button_.set_tooltip(list_->has_selection() ? list_->get_selected_tooltip() :
371 tooltip_);
372 } else {
373 display_button_.set_pic(list_->has_selection() ?
374 list_->get_selected_image() :
375 g_gr->images().get("images/ui_basic/different.png"));
376 display_button_.set_tooltip((boost::format(_("%1%: %2%")) % label_ % name).str());
377 }
378 }
379
set_value()380 void BaseDropdown::set_value() {
381 update();
382 selected();
383 current_selection_ = list_->selection_index();
384 }
385
toggle()386 void BaseDropdown::toggle() {
387 set_list_visibility(!list_->is_visible());
388 }
389
set_list_visibility(bool open)390 void BaseDropdown::set_list_visibility(bool open) {
391 if (!is_enabled_) {
392 list_->set_visible(false);
393 return;
394 }
395 list_->set_visible(open);
396 if (list_->is_visible()) {
397 list_->move_to_top();
398 focus();
399 set_mouse_pos(Vector2i(display_button_.get_x() + (display_button_.get_w() * 3 / 5),
400 display_button_.get_y() + (display_button_.get_h() * 2 / 5)));
401 if (type_ == DropdownType::kPictorialMenu && !has_selection() && !list_->empty()) {
402 select(0);
403 }
404 }
405 if (type_ != DropdownType::kTextual) {
406 display_button_.set_perm_pressed(list_->is_visible());
407 }
408 // Make sure that the list covers and deactivates the elements below it
409 set_layout_toplevel(list_->is_visible());
410 }
411
toggle_list()412 void BaseDropdown::toggle_list() {
413 if (!is_enabled_) {
414 list_->set_visible(false);
415 return;
416 }
417 list_->set_visible(!list_->is_visible());
418 if (type_ != DropdownType::kTextual) {
419 display_button_.set_perm_pressed(list_->is_visible());
420 }
421 if (list_->is_visible()) {
422 list_->move_to_top();
423 focus();
424 Notifications::publish(NoteDropdown(id_));
425 }
426 // Make sure that the list covers and deactivates the elements below it
427 set_layout_toplevel(list_->is_visible());
428 }
429
close()430 void BaseDropdown::close() {
431 if (is_expanded()) {
432 toggle_list();
433 }
434 }
435
is_mouse_away() const436 bool BaseDropdown::is_mouse_away() const {
437 return (get_mouse_position().x + mouse_tolerance_) < list_offset_x_ ||
438 get_mouse_position().x > (list_offset_x_ + list_->get_w() + mouse_tolerance_) ||
439 (get_mouse_position().y + mouse_tolerance_) < list_offset_y_ ||
440 get_mouse_position().y > (list_offset_y_ + get_h() + list_->get_h() + mouse_tolerance_);
441 }
442
handle_key(bool down,SDL_Keysym code)443 bool BaseDropdown::handle_key(bool down, SDL_Keysym code) {
444 if (down) {
445 switch (code.sym) {
446 case SDLK_KP_ENTER:
447 case SDLK_RETURN:
448 if (list_->is_visible()) {
449 set_value();
450 return true;
451 }
452 break;
453 case SDLK_ESCAPE:
454 if (list_->is_visible()) {
455 list_->select(current_selection_);
456 toggle_list();
457 return true;
458 }
459 break;
460 default:
461 break; // not handled
462 }
463 }
464 if (list_->is_visible()) {
465 return list_->handle_key(down, code);
466 }
467 return false;
468 }
469
470 } // namespace UI
471