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 #ifndef WL_UI_BASIC_DROPDOWN_H
21 #define WL_UI_BASIC_DROPDOWN_H
22 
23 #include <deque>
24 #include <memory>
25 
26 #include "graphic/graphic.h"
27 #include "graphic/image.h"
28 #include "notifications/note_ids.h"
29 #include "notifications/notifications.h"
30 #include "ui_basic/box.h"
31 #include "ui_basic/button.h"
32 #include "ui_basic/listselect.h"
33 #include "ui_basic/panel.h"
34 
35 namespace UI {
36 // We use this to make sure that only 1 dropdown is open at the same time.
37 struct NoteDropdown {
38 	CAN_BE_SENT_AS_NOTE(NoteId::Dropdown)
39 
40 	int id;
41 
NoteDropdownNoteDropdown42 	explicit NoteDropdown(int init_id) : id(init_id) {
43 	}
44 };
45 
46 /// The narrow textual dropdown omits the extra push button.
47 /// Use kPictorialMenu if you want to trigger an action without changing the menu button.
48 enum class DropdownType { kTextual, kTextualNarrow, kPictorial, kPictorialMenu };
49 
50 /// Implementation for a dropdown menu that lets the user select a value.
51 class BaseDropdown : public NamedPanel {
52 protected:
53 	/// \param parent             the parent panel
54 	/// \param name               a name so that we can reference the dropdown via Lua
55 	/// \param x                  the x-position within 'parent'
56 	/// \param y                  the y-position within 'parent'
57 	/// \param list_w             the dropdown's width
58 	/// \param max_list_items     the maximum number of items shown in the list before it starts
59 	/// using a scrollbar \param button_dimension   the width of the push button in textual
60 	/// dropdowns. For pictorial dropdowns, this is both the width and the height of the button.
61 	/// \param label              a label to prefix to the selected entry on the display button.
62 	/// \param type               whether this is a textual or pictorial dropdown
63 	/// \param style              the style used for buttons and background
64 	BaseDropdown(Panel* parent,
65 	             const std::string& name,
66 	             int32_t x,
67 	             int32_t y,
68 	             uint32_t list_w,
69 	             uint32_t max_list_items,
70 	             int button_dimension,
71 	             const std::string& label,
72 	             const DropdownType type,
73 	             PanelStyle style,
74 	             ButtonStyle button_style);
75 	~BaseDropdown() override;
76 
77 public:
78 	/// An entry was selected
79 	boost::signals2::signal<void()> selected;
80 
81 	/// \return true if an element has been selected from the list
82 	bool has_selection() const;
83 
84 	/// Sets a label that will be prefixed to the currently selected element's name
85 	/// and displayed on the display button.
86 	void set_label(const std::string& text);
87 
88 	/// Sets the image for the display button (for pictorial dropdowns).
89 	void set_image(const Image* image);
90 
91 	/// Sets the tooltip for the display button.
92 	void set_tooltip(const std::string& text);
93 
94 	/// Displays an error message on the button instead of the current selection.
95 	void set_errored(const std::string& error_message);
96 
97 	/// Enables/disables the dropdown selection.
98 	void set_enabled(bool on);
99 
100 	/// Whether the dropdown selection is enabled.
is_enabled()101 	bool is_enabled() const {
102 		return is_enabled_;
103 	}
104 
105 	/// Which visual style to use for disabled pictorial dropdowns.
106 	void set_disable_style(UI::ButtonDisableStyle disable_style);
107 
108 	/// Whether the dropdown has no elements to select.
empty()109 	bool empty() {
110 		return size() == 0;
111 	}
112 
113 	/// Whether the dropdown has been opened by the user.
114 	bool is_expanded() const;
115 
116 	/// Move the dropdown. The dropdown's position is relative to the parent in
117 	/// pixels.
118 	void set_pos(Vector2i point) override;
119 
120 	/// The number of elements listed in the dropdown.
121 	uint32_t size() const;
122 
123 	/// Handle keypresses
124 	bool handle_key(bool down, SDL_Keysym code) override;
125 
126 	/// Set maximum available height in the UI
127 	void set_height(int height);
128 
129 	/// Toggle the list on and off and position the mouse on the button so that the dropdown won't
130 	/// close on us. If this is a menu and nothing was selected yet, select the first item for easier
131 	/// keyboard navigation.
132 	void toggle();
133 
134 	/// If 'open', show the list and position the mouse on the button so that the dropdown won't
135 	/// close on us. If this is a menu and nothing was selected yet, select the first item for easier
136 	/// keyboard navigation. If not 'open', close the list.
137 	void set_list_visibility(bool open);
138 
139 	void set_size(int nw, int nh) override;
140 	void set_desired_size(int w, int h) override;
141 
142 	/// Expand display button to make enough room for each entry's text. Call this before adding any
143 	/// entries.
144 	void set_autoexpand_display_button();
145 
notify_list_deleted()146 	void notify_list_deleted() {
147 		list_ = nullptr;
148 	}
149 
150 protected:
151 	/// Add an element to the list
152 	/// \param name         the display name of the entry
153 	/// \param value        the index of the entry
154 	/// \param pic          an image to illustrate the entry. Can be nullptr for textual dropdowns.
155 	/// \param select_this  whether this element should be selected
156 	/// \param tooltip_text a tooltip for this entry
157 	/// \param hotkey       a hotkey tip if any
158 	///
159 	/// Text conventions: Title Case for the 'name', Sentence case for the 'tooltip_text'
160 	void add(const std::string& name,
161 	         uint32_t value,
162 	         const Image* pic,
163 	         const bool select_this,
164 	         const std::string& tooltip_text,
165 	         const std::string& hotkey);
166 
167 	/// \return the index of the selected element
168 	uint32_t get_selected() const;
169 
170 	/// Select the entry. Assumes that it exists. Does not trigger the 'selected' signal.
171 	void select(uint32_t entry);
172 
173 	/// Removes all elements from the list.
174 	void clear();
175 
176 	/// Automatically collapses the list if the mouse gets too far away from the dropdown, or if it
177 	/// loses focus.
178 	void think() override;
179 
180 private:
181 	static void layout_if_alive(int);
182 	void layout() override;
183 
184 	/// Updates the buttons
185 	void update();
186 
187 	/// Updates the title and tooltip of the display button and triggers a 'selected' signal.
188 	void set_value();
189 	/// Toggles the dropdown list on and off and sends a notification if the list is visible
190 	/// afterwards.
191 	void toggle_list();
192 	/// Toggle the list closed if the dropdown is currently expanded.
193 	void close();
194 
195 	/// Returns true if the mouse pointer left the vicinity of the dropdown.
196 	bool is_mouse_away() const;
197 
198 	/// Give each dropdown a unique ID
199 	static int next_id_;
200 	const int id_;
201 	std::unique_ptr<Notifications::Subscriber<NoteDropdown>> dropdown_subscriber_;
202 	std::unique_ptr<Notifications::Subscriber<GraphicResolutionChanged>>
203 	   graphic_resolution_changed_subscriber_;
204 
205 	// Dimensions
206 	unsigned int max_list_items_;
207 	unsigned int max_list_height_;
208 	int list_offset_x_;
209 	int list_offset_y_;
210 	const int base_height_;
211 	const int mouse_tolerance_;  // Allow mouse outside the panel a bit before autocollapse
212 	UI::Box button_box_;
213 	UI::Button* push_button_;  // Only used in textual dropdowns
214 	UI::Button display_button_;
215 	// The list needs to be a pointer for destruction, because we hook into the highest parent that
216 	// we can get.
217 	UI::Listselect<uintptr_t>* list_;
218 	std::string label_;
219 	std::string tooltip_;
220 	uint32_t current_selection_;
221 	DropdownType type_;
222 	bool is_enabled_;
223 	ButtonStyle button_style_;
224 	bool autoexpand_display_button_;
225 };
226 
227 /// A dropdown menu that lets the user select a value of the datatype 'Entry'.
228 template <typename Entry> class Dropdown : public BaseDropdown {
229 public:
230 	/// \param parent             the parent panel
231 	/// \param name               a name so that we can reference the dropdown via Lua
232 	/// \param x                  the x-position within 'parent'
233 	/// \param y                  the y-position within 'parent'
234 	/// \param list_w             the dropdown's width
235 	/// \param max_list_items     the maximum number of items shown in the list before it starts
236 	/// using a scrollbar \param button_dimension   the width of the push button in textual
237 	/// dropdowns. For pictorial dropdowns, this is both the width and the height of the button.
238 	/// \param label              a label to prefix to the selected entry on the display button.
239 	/// \param type               whether this is a textual or pictorial dropdown
240 	/// \param style              the style used for buttons and background
241 	/// Text conventions: Title Case for all elements
Dropdown(Panel * parent,const std::string & name,int32_t x,int32_t y,uint32_t list_w,uint32_t max_list_items,int button_dimension,const std::string & label,const DropdownType type,PanelStyle style,ButtonStyle button_style)242 	Dropdown(Panel* parent,
243 	         const std::string& name,
244 	         int32_t x,
245 	         int32_t y,
246 	         uint32_t list_w,
247 	         uint32_t max_list_items,
248 	         int button_dimension,
249 	         const std::string& label,
250 	         const DropdownType type,
251 	         PanelStyle style,
252 	         ButtonStyle button_style)
253 	   : BaseDropdown(parent,
254 	                  name,
255 	                  x,
256 	                  y,
257 	                  list_w,
258 	                  max_list_items,
259 	                  button_dimension,
260 	                  label,
261 	                  type,
262 	                  style,
263 	                  button_style) {
264 	}
~Dropdown()265 	~Dropdown() {
266 		entry_cache_.clear();
267 	}
268 
269 	/// Add an element to the list
270 	/// \param name         the display name of the entry
271 	/// \param value        the value for the entry
272 	/// \param pic          an image to illustrate the entry. Can be nullptr in textual dropdowns
273 	/// only.
274 	/// \param select_this  whether this element should be selected
275 	/// \param tooltip_text a tooltip for this entry
276 	/// \param hotkey       a hotkey tip if any
277 	void add(const std::string& name,
278 	         Entry value,
279 	         const Image* pic = nullptr,
280 	         const bool select_this = false,
281 	         const std::string& tooltip_text = std::string(),
282 	         const std::string& hotkey = std::string()) {
283 		entry_cache_.push_back(std::unique_ptr<Entry>(new Entry(value)));
284 		BaseDropdown::add(name, size(), pic, select_this, tooltip_text, hotkey);
285 	}
286 
287 	/// \return the selected element
get_selected()288 	const Entry& get_selected() const {
289 		assert(BaseDropdown::has_selection());
290 		return *entry_cache_[BaseDropdown::get_selected()];
291 	}
292 
293 	/// Select the entry if it exists. Does not trigger the 'selected' signal.
select(const Entry & entry)294 	void select(const Entry& entry) {
295 		for (uint32_t i = 0; i < entry_cache_.size(); ++i) {
296 			if (entry == *entry_cache_[i]) {
297 				BaseDropdown::select(i);
298 			}
299 		}
300 	}
301 
302 	/// Removes all elements from the list.
clear()303 	void clear() {
304 		BaseDropdown::clear();
305 		entry_cache_.clear();
306 	}
307 
308 private:
309 	// Contains the actual elements. The BaseDropdown registers the indices only.
310 	std::deque<std::unique_ptr<Entry>> entry_cache_;
311 };
312 
313 }  // namespace UI
314 
315 #endif  // end of include guard: WL_UI_BASIC_DROPDOWN_H
316