1//************************************ bs::framework - Copyright 2018 Marko Pintera **************************************//
2//*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********//
3#include "GUI/BsGUIDropDownContent.h"
4#include "GUI/BsGUITexture.h"
5#include "GUI/BsGUIButton.h"
6#include "GUI/BsGUILabel.h"
7#include "GUI/BsGUIWidget.h"
8#include "GUI/BsGUIToggle.h"
9#include "GUI/BsGUISkin.h"
10#include "GUI/BsGUIMouseEvent.h"
11#include "GUI/BsGUICommandEvent.h"
12
13#include <climits>
14
15using namespace std::placeholders;
16
17namespace bs
18{
19 constexpr const char* GUIDropDownContent::ENTRY_TOGGLE_STYLE_TYPE;
20 constexpr const char* GUIDropDownContent::ENTRY_STYLE_TYPE;
21 constexpr const char* GUIDropDownContent::ENTRY_EXP_STYLE_TYPE;
22 constexpr const char* GUIDropDownContent::SEPARATOR_STYLE_TYPE;
23
24 GUIDropDownContent::GUIDropDownContent(GUIDropDownMenu::DropDownSubMenu* parent, const GUIDropDownData& dropDownData,
25 const String& style, const GUIDimensions& dimensions)
26 : GUIElementContainer(dimensions, style), mDropDownData(dropDownData), mStates(dropDownData.states)
27 , mSelectedIdx(UINT_MAX), mRangeStart(0), mRangeEnd(0), mParent(parent), mKeyboardFocus(true)
28 , mIsToggle(parent->getType() == GUIDropDownType::MultiListBox)
29 {
30
31 }
32
33 GUIDropDownContent* GUIDropDownContent::create(GUIDropDownMenu::DropDownSubMenu* parent,
34 const GUIDropDownData& dropDownData, const String& style)
35 {
36 const String* curStyle = &style;
37 if (*curStyle == StringUtil::BLANK)
38 curStyle = &GUIDropDownContent::getGUITypeName();
39
40 return new (bs_alloc<GUIDropDownContent>()) GUIDropDownContent(parent, dropDownData, *curStyle, GUIDimensions::create());
41 }
42
43 GUIDropDownContent* GUIDropDownContent::create(GUIDropDownMenu::DropDownSubMenu* parent,
44 const GUIDropDownData& dropDownData, const GUIOptions& options,
45 const String& style)
46 {
47 const String* curStyle = &style;
48 if (*curStyle == StringUtil::BLANK)
49 curStyle = &GUIDropDownContent::getGUITypeName();
50
51 return new (bs_alloc<GUIDropDownContent>()) GUIDropDownContent(parent, dropDownData, *curStyle, GUIDimensions::create(options));
52 }
53
54 void GUIDropDownContent::styleUpdated()
55 {
56 for (auto& visElem : mVisibleElements)
57 {
58 GUIDropDownDataEntry& element = mDropDownData.entries[visElem.idx];
59
60 if (element.isSeparator())
61 visElem.separator->setStyle(getSubStyleName(SEPARATOR_STYLE_TYPE));
62 else if (element.isSubMenu())
63 visElem.button->setStyle(getSubStyleName(ENTRY_EXP_STYLE_TYPE));
64 else
65 {
66 if (mIsToggle)
67 visElem.button->setStyle(getSubStyleName(ENTRY_TOGGLE_STYLE_TYPE));
68 else
69 visElem.button->setStyle(getSubStyleName(ENTRY_STYLE_TYPE));
70 }
71 }
72 }
73
74 void GUIDropDownContent::setRange(UINT32 start, UINT32 end)
75 {
76 std::function<void(UINT32, UINT32)> onHover =
77 [&](UINT32 idx, UINT32 visIdx)
78 {
79 setSelected(visIdx);
80 mParent->elementSelected(idx);
81 };
82
83 std::function<void(UINT32, UINT32)> onClick =
84 [&](UINT32 idx, UINT32 visIdx)
85 {
86 setSelected(visIdx);
87
88 if (mIsToggle)
89 mStates[idx] = !mStates[idx];
90
91 mParent->elementActivated(idx, mVisibleElements[visIdx].button->_getLayoutData().area);
92 };
93
94 // Remove all elements
95 while (_getNumChildren() > 0)
96 {
97 GUIElementBase* child = _getChild(_getNumChildren() - 1);
98 assert(child->_getType() == GUIElementBase::Type::Element);
99
100 GUIElement::destroy(static_cast<GUIElement*>(child));
101 }
102
103 mRangeStart = start;
104 mRangeEnd = end;
105
106 UINT32 range = end - start;
107 if (mSelectedIdx != UINT_MAX && mSelectedIdx >= range)
108 mSelectedIdx = UINT_MAX;
109
110 mVisibleElements.clear();
111 UINT32 curVisIdx = 0;
112 for (UINT32 i = start; i < end; i++)
113 {
114 mVisibleElements.push_back(VisibleElement());
115 VisibleElement& visElem = mVisibleElements.back();
116 visElem.idx = i;
117 GUIDropDownDataEntry& element = mDropDownData.entries[i];
118
119 if (element.isSeparator())
120 {
121 visElem.separator = GUITexture::create(TextureScaleMode::StretchToFit, getSubStyleName(SEPARATOR_STYLE_TYPE));
122 _registerChildElement(visElem.separator);
123 }
124 else if (element.isSubMenu())
125 {
126 visElem.button = GUIButton::create(getElementLocalizedName(i), getSubStyleName(ENTRY_EXP_STYLE_TYPE));
127 visElem.button->onHover.connect(std::bind(onClick, i, curVisIdx));
128 _registerChildElement(visElem.button);
129 }
130 else
131 {
132 if (mIsToggle)
133 {
134 GUIToggle* toggle = GUIToggle::create(getElementLocalizedName(i), getSubStyleName(ENTRY_TOGGLE_STYLE_TYPE));
135 if (mStates[i])
136 toggle->toggleOn();
137
138 visElem.button = toggle;
139 }
140 else
141 visElem.button = GUIButton::create(getElementLocalizedName(i), getSubStyleName(ENTRY_STYLE_TYPE));
142
143 visElem.button->onHover.connect(std::bind(onHover, i, curVisIdx));
144 visElem.button->onClick.connect(std::bind(onClick, i, curVisIdx));
145 _registerChildElement(visElem.button);
146
147 const String& shortcutTag = element.getShortcutTag();
148 if (!shortcutTag.empty())
149 {
150 visElem.shortcutLabel = GUILabel::create(HString(shortcutTag), "RightAlignedLabel");
151 _registerChildElement(visElem.shortcutLabel);
152 }
153 }
154
155 curVisIdx++;
156 }
157
158 _markLayoutAsDirty();
159 }
160
161 UINT32 GUIDropDownContent::getElementHeight(UINT32 idx) const
162 {
163 if (_getParentWidget() == nullptr)
164 return 14; // Arbitrary
165
166 if (mDropDownData.entries[idx].isSeparator())
167 return _getParentWidget()->getSkin().getStyle(getSubStyleName(SEPARATOR_STYLE_TYPE))->height;
168 else if (mDropDownData.entries[idx].isSubMenu())
169 return _getParentWidget()->getSkin().getStyle(getSubStyleName(ENTRY_EXP_STYLE_TYPE))->height;
170 else
171 {
172 if (mIsToggle)
173 return _getParentWidget()->getSkin().getStyle(getSubStyleName(ENTRY_TOGGLE_STYLE_TYPE))->height;
174 else
175 return _getParentWidget()->getSkin().getStyle(getSubStyleName(ENTRY_STYLE_TYPE))->height;
176 }
177 }
178
179 HString GUIDropDownContent::getElementLocalizedName(UINT32 idx) const
180 {
181 const String& label = mDropDownData.entries[idx].getLabel();
182
183 auto findLocalizedName = mDropDownData.localizedNames.find(label);
184 if (findLocalizedName != mDropDownData.localizedNames.end())
185 return findLocalizedName->second;
186 else
187 return HString(label);
188 }
189
190 void GUIDropDownContent::setKeyboardFocus(bool focus)
191 {
192 mKeyboardFocus = focus;
193 setFocus(focus);
194 }
195
196 bool GUIDropDownContent::_commandEvent(const GUICommandEvent& ev)
197 {
198 bool baseReturn = GUIElementContainer::_commandEvent(ev);
199
200 if (!mKeyboardFocus)
201 return baseReturn;
202
203 switch (ev.getType())
204 {
205 case GUICommandEventType::MoveDown:
206 if (mSelectedIdx == UINT_MAX)
207 selectNext(0);
208 else
209 selectNext(mVisibleElements[mSelectedIdx].idx + 1);
210 return true;
211 case GUICommandEventType::MoveUp:
212 if (mSelectedIdx == UINT_MAX)
213 selectNext(0);
214 else
215 selectPrevious(mVisibleElements[mSelectedIdx].idx - 1);
216 return true;
217 case GUICommandEventType::Escape:
218 case GUICommandEventType::MoveLeft:
219 mParent->close();
220 return true;
221 case GUICommandEventType::MoveRight:
222 {
223 if (mSelectedIdx == UINT_MAX)
224 selectNext(0);
225 else
226 {
227 GUIDropDownDataEntry& entry = mDropDownData.entries[mVisibleElements[mSelectedIdx].idx];
228 if (entry.isSubMenu())
229 mParent->elementActivated(mVisibleElements[mSelectedIdx].idx, mVisibleElements[mSelectedIdx].button->_getLayoutData().area);
230 }
231 }
232 return true;
233 case GUICommandEventType::Confirm:
234 if (mSelectedIdx == UINT_MAX)
235 selectNext(0);
236 else
237 {
238 if (mIsToggle)
239 mVisibleElements[mSelectedIdx].button->_setOn(!mVisibleElements[mSelectedIdx].button->_isOn());
240
241 mParent->elementActivated(mVisibleElements[mSelectedIdx].idx, mVisibleElements[mSelectedIdx].button->_getLayoutData().area);
242 }
243 return true;
244 default:
245 break;
246 }
247
248 return baseReturn;
249 }
250
251 bool GUIDropDownContent::_mouseEvent(const GUIMouseEvent& ev)
252 {
253 if (ev.getType() == GUIMouseEventType::MouseWheelScroll)
254 {
255 if (ev.getWheelScrollAmount() < 0)
256 mParent->scrollDown();
257 else
258 mParent->scrollUp();
259
260 return true;
261 }
262
263 return false;
264 }
265
266 void GUIDropDownContent::setSelected(UINT32 idx)
267 {
268 if (mSelectedIdx != UINT_MAX)
269 {
270 if (mVisibleElements[mSelectedIdx].button->_isOn())
271 mVisibleElements[mSelectedIdx].button->_setState(GUIElementState::NormalOn);
272 else
273 mVisibleElements[mSelectedIdx].button->_setState(GUIElementState::Normal);
274 }
275
276 mSelectedIdx = idx;
277 if (mVisibleElements[mSelectedIdx].button->_isOn())
278 mVisibleElements[mSelectedIdx].button->_setState(GUIElementState::HoverOn);
279 else
280 mVisibleElements[mSelectedIdx].button->_setState(GUIElementState::Hover);
281
282 mParent->elementSelected(mVisibleElements[mSelectedIdx].idx);
283 }
284
285 void GUIDropDownContent::selectNext(UINT32 startIdx)
286 {
287 UINT32 numElements = (UINT32)mDropDownData.entries.size();
288
289 bool gotNextIndex = false;
290 UINT32 nextIdx = startIdx;
291 for (UINT32 i = 0; i < numElements; i++)
292 {
293 if (nextIdx >= numElements)
294 nextIdx = 0; // Wrap around
295
296 GUIDropDownDataEntry& entry = mDropDownData.entries[nextIdx];
297 if (!entry.isSeparator())
298 {
299 gotNextIndex = true;
300 break;
301 }
302
303 nextIdx++;
304 }
305
306 if (gotNextIndex)
307 {
308 while (nextIdx < mRangeStart || nextIdx >= mRangeEnd)
309 mParent->scrollDown();
310
311 UINT32 visIdx = 0;
312 for (auto& visElem : mVisibleElements)
313 {
314 if (visElem.idx == nextIdx)
315 {
316 setSelected(visIdx);
317 break;
318 }
319
320 visIdx++;
321 }
322 }
323 }
324
325 void GUIDropDownContent::selectPrevious(UINT32 startIdx)
326 {
327 UINT32 numElements = (UINT32)mDropDownData.entries.size();
328
329 bool gotNextIndex = false;
330 INT32 prevIdx = (INT32)startIdx;
331
332 for (UINT32 i = 0; i < numElements; i++)
333 {
334 if (prevIdx < 0)
335 prevIdx = numElements - 1; // Wrap around
336
337 GUIDropDownDataEntry& entry = mDropDownData.entries[prevIdx];
338 if (!entry.isSeparator())
339 {
340 gotNextIndex = true;
341 break;
342 }
343
344 prevIdx--;
345 }
346
347 if (gotNextIndex)
348 {
349 while (prevIdx < (INT32)mRangeStart || prevIdx >= (INT32)mRangeEnd)
350 mParent->scrollUp();
351
352 UINT32 visIdx = 0;
353 for (auto& visElem : mVisibleElements)
354 {
355 if (visElem.idx == (UINT32)prevIdx)
356 {
357 setSelected(visIdx);
358 break;
359 }
360
361 visIdx++;
362 }
363 }
364 }
365
366 Vector2I GUIDropDownContent::_getOptimalSize() const
367 {
368 Vector2I optimalSize;
369 for (auto& visElem : mVisibleElements)
370 {
371 const GUIDropDownDataEntry& element = mDropDownData.entries[visElem.idx];
372
373 optimalSize.y += (INT32)getElementHeight(visElem.idx);
374
375 if (element.isSeparator())
376 optimalSize.x = std::max(optimalSize.x, visElem.separator->_getOptimalSize().x);
377 else
378 optimalSize.x = std::max(optimalSize.x, visElem.button->_getOptimalSize().x);
379 }
380
381 return optimalSize;
382 }
383
384 void GUIDropDownContent::_updateLayoutInternal(const GUILayoutData& data)
385 {
386 GUILayoutData childData = data;
387 INT32 yOffset = data.area.y;
388
389 for (auto& visElem : mVisibleElements)
390 {
391 const GUIDropDownDataEntry& element = mDropDownData.entries[visElem.idx];
392
393 GUIElement* guiMainElement = nullptr;
394 if (element.isSeparator())
395 guiMainElement = visElem.separator;
396 else
397 guiMainElement = visElem.button;
398
399 childData.area.y = yOffset;
400 childData.area.height = getElementHeight(visElem.idx);
401
402 yOffset += childData.area.height;
403
404 guiMainElement->_setLayoutData(childData);
405
406 // Shortcut label
407 GUILabel* shortcutLabel = visElem.shortcutLabel;
408 if (shortcutLabel != nullptr)
409 shortcutLabel->_setLayoutData(childData);
410 }
411 }
412
413 const String& GUIDropDownContent::getGUITypeName()
414 {
415 static String typeName = "GUIDropDownContent";
416 return typeName;
417 }
418}
419