OpenTTD
dropdown.cpp
Go to the documentation of this file.
1 /* $Id: dropdown.cpp 27381 2015-08-10 20:24:13Z michi_cc $ */
2 
3 /*
4  * This file is part of OpenTTD.
5  * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
6  * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
7  * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8  */
9 
12 #include "../stdafx.h"
13 #include "../window_gui.h"
14 #include "../string_func.h"
15 #include "../strings_func.h"
16 #include "../window_func.h"
17 #include "dropdown_type.h"
18 
19 #include "dropdown_widget.h"
20 
21 #include "../safeguards.h"
22 
23 
24 void DropDownListItem::Draw(int left, int right, int top, int bottom, bool sel, int bg_colour) const
25 {
26  int c1 = _colour_gradient[bg_colour][3];
27  int c2 = _colour_gradient[bg_colour][7];
28 
29  int mid = top + this->Height(0) / 2;
30  GfxFillRect(left + 1, mid - 2, right - 1, mid - 2, c1);
31  GfxFillRect(left + 1, mid - 1, right - 1, mid - 1, c2);
32 }
33 
34 uint DropDownListStringItem::Width() const
35 {
36  char buffer[512];
37  GetString(buffer, this->String(), lastof(buffer));
38  return GetStringBoundingBox(buffer).width;
39 }
40 
41 void DropDownListStringItem::Draw(int left, int right, int top, int bottom, bool sel, int bg_colour) const
42 {
43  DrawString(left + WD_FRAMERECT_LEFT, right - WD_FRAMERECT_RIGHT, top, this->String(), sel ? TC_WHITE : TC_BLACK);
44 }
45 
53 /* static */ int DropDownListStringItem::NatSortFunc(const DropDownListItem * const *first, const DropDownListItem * const * second)
54 {
55  char buffer1[512], buffer2[512];
56  GetString(buffer1, static_cast<const DropDownListStringItem*>(*first)->String(), lastof(buffer1));
57  GetString(buffer2, static_cast<const DropDownListStringItem*>(*second)->String(), lastof(buffer2));
58  return strnatcmp(buffer1, buffer2);
59 }
60 
61 StringID DropDownListParamStringItem::String() const
62 {
63  for (uint i = 0; i < lengthof(this->decode_params); i++) SetDParam(i, this->decode_params[i]);
64  return this->string;
65 }
66 
67 StringID DropDownListCharStringItem::String() const
68 {
69  SetDParamStr(0, this->raw_string);
70  return this->string;
71 }
72 
73 static const NWidgetPart _nested_dropdown_menu_widgets[] = {
76  NWidget(NWID_SELECTION, INVALID_COLOUR, WID_DM_SHOW_SCROLL),
78  EndContainer(),
79  EndContainer(),
80 };
81 
82 static WindowDesc _dropdown_desc(
83  WDP_MANUAL, NULL, 0, 0,
86  _nested_dropdown_menu_widgets, lengthof(_nested_dropdown_menu_widgets)
87 );
88 
94  const DropDownList *list;
96  byte click_delay;
97  bool drag_mode;
99  int scrolling;
101  Scrollbar *vscroll;
102 
116  DropdownWindow(Window *parent, const DropDownList *list, int selected, int button, bool instant_close, const Point &position, const Dimension &size, Colours wi_colour, bool scroll)
117  : Window(&_dropdown_desc)
118  {
119  assert(list->Length() > 0);
120 
121  this->position = position;
122 
123  this->CreateNestedTree();
124 
125  this->vscroll = this->GetScrollbar(WID_DM_SCROLL);
126 
127  uint items_width = size.width - (scroll ? NWidgetScrollbar::GetVerticalDimension().width : 0);
128  NWidgetCore *nwi = this->GetWidget<NWidgetCore>(WID_DM_ITEMS);
129  nwi->SetMinimalSize(items_width, size.height + 4);
130  nwi->colour = wi_colour;
131 
132  nwi = this->GetWidget<NWidgetCore>(WID_DM_SCROLL);
133  nwi->colour = wi_colour;
134 
135  this->GetWidget<NWidgetStacked>(WID_DM_SHOW_SCROLL)->SetDisplayedPlane(scroll ? 0 : SZSP_NONE);
136 
137  this->FinishInitNested(0);
138  CLRBITS(this->flags, WF_WHITE_BORDER);
139 
140  /* Total length of list */
141  int list_height = 0;
142  for (const DropDownListItem * const *it = list->Begin(); it != list->End(); ++it) {
143  const DropDownListItem *item = *it;
144  list_height += item->Height(items_width);
145  }
146 
147  /* Capacity is the average number of items visible */
148  this->vscroll->SetCapacity(size.height * (uint16)list->Length() / list_height);
149  this->vscroll->SetCount((uint16)list->Length());
150 
151  this->parent_wnd_class = parent->window_class;
152  this->parent_wnd_num = parent->window_number;
153  this->parent_button = button;
154  this->list = list;
155  this->selected_index = selected;
156  this->click_delay = 0;
157  this->drag_mode = true;
158  this->instant_close = instant_close;
159  }
160 
161  ~DropdownWindow()
162  {
163  /* Make the dropdown "invisible", so it doesn't affect new window placement.
164  * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
165  this->window_class = WC_INVALID;
166  this->SetDirty();
167 
169  if (w2 != NULL) {
170  Point pt = _cursor.pos;
171  pt.x -= w2->left;
172  pt.y -= w2->top;
173  w2->OnDropdownClose(pt, this->parent_button, this->selected_index, this->instant_close);
174  }
175  delete this->list;
176  }
177 
178  virtual Point OnInitialPosition(int16 sm_width, int16 sm_height, int window_number)
179  {
180  return this->position;
181  }
182 
188  bool GetDropDownItem(int &value)
189  {
190  if (GetWidgetFromPos(this, _cursor.pos.x - this->left, _cursor.pos.y - this->top) < 0) return false;
191 
192  NWidgetBase *nwi = this->GetWidget<NWidgetBase>(WID_DM_ITEMS);
193  int y = _cursor.pos.y - this->top - nwi->pos_y - 2;
194  int width = nwi->current_x - 4;
195  int pos = this->vscroll->GetPosition();
196 
197  const DropDownList *list = this->list;
198 
199  for (const DropDownListItem * const *it = list->Begin(); it != list->End(); ++it) {
200  /* Skip items that are scrolled up */
201  if (--pos >= 0) continue;
202 
203  const DropDownListItem *item = *it;
204  int item_height = item->Height(width);
205 
206  if (y < item_height) {
207  if (item->masked || !item->Selectable()) return false;
208  value = item->result;
209  return true;
210  }
211 
212  y -= item_height;
213  }
214 
215  return false;
216  }
217 
218  virtual void DrawWidget(const Rect &r, int widget) const
219  {
220  if (widget != WID_DM_ITEMS) return;
221 
222  Colours colour = this->GetWidget<NWidgetCore>(widget)->colour;
223 
224  int y = r.top + 2;
225  int pos = this->vscroll->GetPosition();
226  for (const DropDownListItem * const *it = this->list->Begin(); it != this->list->End(); ++it) {
227  const DropDownListItem *item = *it;
228  int item_height = item->Height(r.right - r.left + 1);
229 
230  /* Skip items that are scrolled up */
231  if (--pos >= 0) continue;
232 
233  if (y + item_height < r.bottom) {
234  bool selected = (this->selected_index == item->result);
235  if (selected) GfxFillRect(r.left + 2, y, r.right - 1, y + item_height - 1, PC_BLACK);
236 
237  item->Draw(r.left, r.right, y, y + item_height, selected, colour);
238 
239  if (item->masked) {
240  GfxFillRect(r.left + 1, y, r.right - 1, y + item_height - 1, _colour_gradient[colour][5], FILLRECT_CHECKER);
241  }
242  }
243  y += item_height;
244  }
245  }
246 
247  virtual void OnClick(Point pt, int widget, int click_count)
248  {
249  if (widget != WID_DM_ITEMS) return;
250  int item;
251  if (this->GetDropDownItem(item)) {
252  this->click_delay = 4;
253  this->selected_index = item;
254  this->SetDirty();
255  }
256  }
257 
258  virtual void OnTick()
259  {
260  if (this->scrolling != 0) {
261  int pos = this->vscroll->GetPosition();
262 
263  this->vscroll->UpdatePosition(this->scrolling);
264  this->scrolling = 0;
265 
266  if (pos != this->vscroll->GetPosition()) {
267  this->SetDirty();
268  }
269  }
270  }
271 
272  virtual void OnMouseLoop()
273  {
275  if (w2 == NULL) {
276  delete this;
277  return;
278  }
279 
280  if (this->click_delay != 0 && --this->click_delay == 0) {
281  /* Make the dropdown "invisible", so it doesn't affect new window placement.
282  * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
283  this->window_class = WC_INVALID;
284  this->SetDirty();
285 
287  delete this;
288  return;
289  }
290 
291  if (this->drag_mode) {
292  int item;
293 
294  if (!_left_button_clicked) {
295  this->drag_mode = false;
296  if (!this->GetDropDownItem(item)) {
297  if (this->instant_close) delete this;
298  return;
299  }
300  this->click_delay = 2;
301  } else {
302  if (_cursor.pos.y <= this->top + 2) {
303  /* Cursor is above the list, set scroll up */
304  this->scrolling = -1;
305  return;
306  } else if (_cursor.pos.y >= this->top + this->height - 2) {
307  /* Cursor is below list, set scroll down */
308  this->scrolling = 1;
309  return;
310  }
311 
312  if (!this->GetDropDownItem(item)) return;
313  }
314 
315  if (this->selected_index != item) {
316  this->selected_index = item;
317  this->SetDirty();
318  }
319  }
320  }
321 };
322 
337 void ShowDropDownListAt(Window *w, const DropDownList *list, int selected, int button, Rect wi_rect, Colours wi_colour, bool auto_width, bool instant_close)
338 {
340 
341  /* The preferred position is just below the dropdown calling widget */
342  int top = w->top + wi_rect.bottom + 1;
343 
344  /* The preferred width equals the calling widget */
345  uint width = wi_rect.right - wi_rect.left + 1;
346 
347  /* Longest item in the list, if auto_width is enabled */
348  uint max_item_width = 0;
349 
350  /* Total length of list */
351  int height = 0;
352 
353  for (const DropDownListItem * const *it = list->Begin(); it != list->End(); ++it) {
354  const DropDownListItem *item = *it;
355  height += item->Height(width);
356  if (auto_width) max_item_width = max(max_item_width, item->Width() + 5);
357  }
358 
359  /* Check if the status bar is visible, as we don't want to draw over it */
360  int screen_bottom = GetMainViewBottom();
361  bool scroll = false;
362 
363  /* Check if the dropdown will fully fit below the widget */
364  if (top + height + 4 >= screen_bottom) {
365  /* If not, check if it will fit above the widget */
366  if (w->top + wi_rect.top - height > GetMainViewTop()) {
367  top = w->top + wi_rect.top - height - 4;
368  } else {
369  /* ... and lastly if it won't, enable the scroll bar and fit the
370  * list in below the widget */
371  int avg_height = height / (int)list->Length();
372  int rows = (screen_bottom - 4 - top) / avg_height;
373  height = rows * avg_height;
374  scroll = true;
375  /* Add space for the scroll bar if we automatically determined
376  * the width of the list. */
377  max_item_width += NWidgetScrollbar::GetVerticalDimension().width;
378  }
379  }
380 
381  if (auto_width) width = max(width, max_item_width);
382 
383  Point dw_pos = { w->left + (_current_text_dir == TD_RTL ? wi_rect.right + 1 - (int)width : wi_rect.left), top};
384  Dimension dw_size = {width, (uint)height};
385  new DropdownWindow(w, list, selected, button, instant_close, dw_pos, dw_size, wi_colour, scroll);
386 }
387 
401 void ShowDropDownList(Window *w, const DropDownList *list, int selected, int button, uint width, bool auto_width, bool instant_close)
402 {
403  /* Our parent's button widget is used to determine where to place the drop
404  * down list window. */
405  Rect wi_rect;
406  NWidgetCore *nwi = w->GetWidget<NWidgetCore>(button);
407  wi_rect.left = nwi->pos_x;
408  wi_rect.right = nwi->pos_x + nwi->current_x - 1;
409  wi_rect.top = nwi->pos_y;
410  wi_rect.bottom = nwi->pos_y + nwi->current_y - 1;
411  Colours wi_colour = nwi->colour;
412 
413  if ((nwi->type & WWT_MASK) == NWID_BUTTON_DROPDOWN) {
415  } else {
416  w->LowerWidget(button);
417  }
418  w->SetWidgetDirty(button);
419 
420  if (width != 0) {
421  if (_current_text_dir == TD_RTL) {
422  wi_rect.left = wi_rect.right + 1 - width;
423  } else {
424  wi_rect.right = wi_rect.left + width - 1;
425  }
426  }
427 
428  ShowDropDownListAt(w, list, selected, button, wi_rect, wi_colour, auto_width, instant_close);
429 }
430 
442 void ShowDropDownMenu(Window *w, const StringID *strings, int selected, int button, uint32 disabled_mask, uint32 hidden_mask, uint width)
443 {
444  DropDownList *list = new DropDownList();
445 
446  for (uint i = 0; strings[i] != INVALID_STRING_ID; i++) {
447  if (!HasBit(hidden_mask, i)) {
448  *list->Append() = new DropDownListStringItem(strings[i], i, HasBit(disabled_mask, i));
449  }
450  }
451 
452  /* No entries in the list? */
453  if (list->Length() == 0) {
454  delete list;
455  return;
456  }
457 
458  ShowDropDownList(w, list, selected, button, width);
459 }
460 
467 {
468  Window *w;
469  FOR_ALL_WINDOWS_FROM_BACK(w) {
470  if (w->window_class != WC_DROPDOWN_MENU) continue;
471 
472  DropdownWindow *dw = dynamic_cast<DropdownWindow*>(w);
473  assert(dw != NULL);
474  if (pw->window_class == dw->parent_wnd_class &&
475  pw->window_number == dw->parent_wnd_num) {
476  int parent_button = dw->parent_button;
477  delete dw;
478  return parent_button;
479  }
480  }
481 
482  return -1;
483 }
484