1/*
2 * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient>
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 */
22
23#include "uimanager.h"
24#include "ui.h"
25
26#include <framework/otml/otml.h>
27#include <framework/graphics/graphics.h>
28#include <framework/platform/platformwindow.h>
29#include <framework/core/eventdispatcher.h>
30#include <framework/core/application.h>
31#include <framework/core/resourcemanager.h>
32#include <framework/util/extras.h>
33
34UIManager g_ui;
35
36void UIManager::init()
37{
38 // creates root widget
39 m_rootWidget = UIWidgetPtr(new UIWidget);
40 m_rootWidget->setId("root");
41 m_mouseReceiver = m_rootWidget;
42 m_keyboardReceiver = m_rootWidget;
43}
44
45void UIManager::terminate()
46{
47 // destroy root widget and its children
48 m_rootWidget->destroy();
49 m_mouseReceiver = nullptr;
50 m_keyboardReceiver = nullptr;
51 m_rootWidget = nullptr;
52 m_draggingWidget = nullptr;
53 m_hoveredWidget = nullptr;
54 for(auto& widget : m_pressedWidget)
55 widget = nullptr;
56 m_styles.clear();
57 m_destroyedWidgets.clear();
58 m_checkEvent = nullptr;
59}
60
61void UIManager::render(Fw::DrawPane drawPane)
62{
63 m_rootWidget->draw(m_rootWidget->getRect(), drawPane);
64}
65
66void UIManager::resize(const Size& size)
67{
68 m_rootWidget->setSize(size);
69 m_moveTimer.restart();
70}
71
72void UIManager::inputEvent(const InputEvent& event)
73{
74 UIWidgetList widgetList;
75 switch(event.type) {
76 case Fw::KeyTextInputEvent:
77 g_lua.callGlobalField("g_ui", "onKeyText", event.keyText);
78 m_keyboardReceiver->propagateOnKeyText(event.keyText);
79 break;
80 case Fw::KeyDownInputEvent:
81 g_lua.callGlobalField("g_ui", "onKeyDown", event.keyCode, event.keyboardModifiers);
82 m_keyboardReceiver->propagateOnKeyDown(event.keyCode, event.keyboardModifiers);
83 break;
84 case Fw::KeyPressInputEvent:
85 g_lua.callGlobalField("g_ui", "onKeyPress", event.keyCode, event.keyboardModifiers, event.autoRepeatTicks);
86 m_keyboardReceiver->propagateOnKeyPress(event.keyCode, event.keyboardModifiers, event.autoRepeatTicks);
87 break;
88 case Fw::KeyUpInputEvent:
89 g_lua.callGlobalField("g_ui", "onKeyUp", event.keyCode, event.keyboardModifiers);
90 m_keyboardReceiver->propagateOnKeyUp(event.keyCode, event.keyboardModifiers);
91 break;
92 case Fw::MousePressInputEvent:
93 g_lua.callGlobalField("g_ui", "onMousePress", event.mousePos, event.mouseButton);
94
95 if(m_mouseReceiver->isVisible() && (event.mouseButton == Fw::MouseLeftButton || event.mouseButton == Fw::MouseTouch2 || event.mouseButton == Fw::MouseTouch3)) {
96 UIWidgetPtr pressedWidget = m_mouseReceiver->recursiveGetChildByPos(event.mousePos, false);
97 if(pressedWidget && !pressedWidget->isEnabled())
98 pressedWidget = nullptr;
99 updatePressedWidget(event.mouseButton, pressedWidget, event.mousePos);
100 }
101
102 m_mouseReceiver->propagateOnMouseEvent(event.mousePos, widgetList);
103 for(const UIWidgetPtr& widget : widgetList) {
104 widget->recursiveFocus(Fw::MouseFocusReason);
105 if(widget->onMousePress(event.mousePos, event.mouseButton))
106 break;
107 }
108
109 break;
110 case Fw::MouseReleaseInputEvent: {
111 g_lua.callGlobalField("g_ui", "onMouseRelease", event.mousePos, event.mouseButton);
112
113 // release dragging widget
114 bool accepted = false;
115 if(m_draggingWidget && event.mouseButton == Fw::MouseLeftButton)
116 accepted = updateDraggingWidget(nullptr, event.mousePos);
117
118 if(!accepted) {
119 m_mouseReceiver->propagateOnMouseEvent(event.mousePos, widgetList);
120
121 // mouse release is always fired first on the pressed widget
122 if(m_pressedWidget[event.mouseButton]) {
123 auto it = std::find(widgetList.begin(), widgetList.end(), m_pressedWidget[event.mouseButton]);
124 if(it != widgetList.end())
125 widgetList.erase(it);
126 widgetList.push_front(m_pressedWidget[event.mouseButton]);
127 }
128
129 for(const UIWidgetPtr& widget : widgetList) {
130 if(widget->onMouseRelease(event.mousePos, event.mouseButton))
131 break;
132 }
133 }
134
135 if (event.mouseButton == Fw::MouseLeftButton || event.mouseButton == Fw::MouseTouch2 || event.mouseButton == Fw::MouseTouch3) {
136 if(m_pressedWidget[event.mouseButton]) {
137 updatePressedWidget(event.mouseButton, nullptr, event.mousePos, !accepted);
138 }
139 }
140 break;
141 }
142 case Fw::MouseMoveInputEvent: {
143 g_lua.callGlobalField("g_ui", "onMouseMove", event.mousePos, event.mouseMoved);
144
145 // start dragging when moving a pressed widget
146 if(m_pressedWidget[Fw::MouseLeftButton] && m_pressedWidget[Fw::MouseLeftButton]->isDraggable() && m_draggingWidget != m_pressedWidget[Fw::MouseLeftButton]) {
147 // only drags when moving more than 4 pixels
148 if((event.mousePos - m_pressedWidget[Fw::MouseLeftButton]->getLastClickPosition()).length() >= 4)
149 updateDraggingWidget(m_pressedWidget[Fw::MouseLeftButton], event.mousePos - event.mouseMoved);
150 }
151
152 // mouse move can change hovered widgets
153 updateHoveredWidget(true);
154
155 // first fire dragging move
156 if(m_draggingWidget) {
157 if(m_draggingWidget->onDragMove(event.mousePos, event.mouseMoved))
158 break;
159 }
160
161 if (m_pressedWidget[Fw::MouseLeftButton]) {
162 if (m_pressedWidget[Fw::MouseLeftButton]->onMouseMove(event.mousePos, event.mouseMoved)) {
163 break;
164 }
165 }
166
167 //m_mouseReceiver->propagateOnMouseMove(event.mousePos, event.mouseMoved, widgetList);
168 m_rootWidget->propagateOnMouseMove(event.mousePos, event.mouseMoved, widgetList);
169 for(const UIWidgetPtr& widget : widgetList) {
170 if(widget->onMouseMove(event.mousePos, event.mouseMoved))
171 break;
172 }
173 break;
174 }
175 case Fw::MouseWheelInputEvent:
176 g_lua.callGlobalField("g_ui", "onMouseWheel", event.mousePos, event.wheelDirection);
177
178 m_rootWidget->propagateOnMouseEvent(event.mousePos, widgetList);
179 for(const UIWidgetPtr& widget : widgetList) {
180 if(widget->onMouseWheel(event.mousePos, event.wheelDirection))
181 break;
182 }
183 break;
184 default:
185 break;
186 };
187}
188
189void UIManager::updatePressedWidget(Fw::MouseButton button, const UIWidgetPtr& newPressedWidget, const Point& clickedPos, bool fireClicks)
190{
191 UIWidgetPtr oldPressedWidget = m_pressedWidget[button];
192 m_pressedWidget[button] = newPressedWidget;
193
194 // when releasing mouse inside pressed widget area send onClick event
195 if (fireClicks && oldPressedWidget && oldPressedWidget->isEnabled() && oldPressedWidget->containsPoint(clickedPos))
196 oldPressedWidget->onClick(clickedPos);
197
198 if(newPressedWidget)
199 newPressedWidget->updateState(Fw::PressedState);
200
201 if(oldPressedWidget)
202 oldPressedWidget->updateState(Fw::PressedState);
203}
204
205bool UIManager::updateDraggingWidget(const UIWidgetPtr& draggingWidget, const Point& clickedPos)
206{
207 bool accepted = false;
208
209 UIWidgetPtr oldDraggingWidget = m_draggingWidget;
210 m_draggingWidget = nullptr;
211 if(oldDraggingWidget) {
212 UIWidgetPtr droppedWidget;
213 if(!clickedPos.isNull()) {
214 auto clickedChildren = m_rootWidget->recursiveGetChildrenByPos(clickedPos);
215 for(const UIWidgetPtr& child : clickedChildren) {
216 if(child->onDrop(oldDraggingWidget, clickedPos)) {
217 droppedWidget = child;
218 break;
219 }
220 }
221 }
222
223 accepted = oldDraggingWidget->onDragLeave(droppedWidget, clickedPos);
224 oldDraggingWidget->updateState(Fw::DraggingState);
225 }
226
227 if(draggingWidget) {
228 if(draggingWidget->onDragEnter(clickedPos)) {
229 m_draggingWidget = draggingWidget;
230 draggingWidget->updateState(Fw::DraggingState);
231 accepted = true;
232 }
233 }
234
235 return accepted;
236}
237
238void UIManager::updateHoveredWidget(bool now)
239{
240 if(m_hoverUpdateScheduled && !now)
241 return;
242
243 auto func = [this] {
244 if(!m_rootWidget)
245 return;
246
247 m_hoverUpdateScheduled = false;
248 UIWidgetPtr hoveredWidget;
249 //if(!g_window.isMouseButtonPressed(Fw::MouseLeftButton) && !g_window.isMouseButtonPressed(Fw::MouseRightButton)) {
250 hoveredWidget = m_rootWidget->recursiveGetChildByPos(g_window.getMousePosition(), false);
251 if(hoveredWidget && !hoveredWidget->isEnabled())
252 hoveredWidget = nullptr;
253 //}
254
255 if(hoveredWidget != m_hoveredWidget) {
256 UIWidgetPtr oldHovered = m_hoveredWidget;
257 m_hoveredWidget = hoveredWidget;
258 if(oldHovered) {
259 oldHovered->updateState(Fw::HoverState);
260 oldHovered->onHoverChange(false);
261 }
262 if(hoveredWidget) {
263 hoveredWidget->updateState(Fw::HoverState);
264 hoveredWidget->onHoverChange(true);
265 }
266 }
267 };
268
269 if(now)
270 func();
271 else {
272 m_hoverUpdateScheduled = true;
273 g_dispatcher.addEvent(func);
274 }
275}
276
277void UIManager::onWidgetAppear(const UIWidgetPtr& widget)
278{
279 if(widget->containsPoint(g_window.getMousePosition()))
280 updateHoveredWidget();
281}
282
283void UIManager::onWidgetDisappear(const UIWidgetPtr& widget)
284{
285 if(widget->containsPoint(g_window.getMousePosition()))
286 updateHoveredWidget();
287}
288
289void UIManager::onWidgetDestroy(const UIWidgetPtr& widget)
290{
291 AutoStat s(STATS_MAIN, "UIManager::onWidgetDestroy", stdext::format("%s (%s)", widget->getId(), widget->getParent() ? widget->getParent()->getId() : ""));
292
293 // release input grabs
294 if(m_keyboardReceiver == widget)
295 resetKeyboardReceiver();
296
297 if(m_mouseReceiver == widget)
298 resetMouseReceiver();
299
300 if(m_hoveredWidget == widget)
301 updateHoveredWidget();
302
303 for (int i = 0; i <= Fw::MouseButtonLast + 1; ++i) {
304 if (m_pressedWidget[i] == widget) {
305 updatePressedWidget((Fw::MouseButton)i, nullptr);
306 }
307 }
308
309 if(m_draggingWidget == widget)
310 updateDraggingWidget(nullptr);
311
312 if (!g_extras.debugWidgets)
313 return;
314
315 if(widget == m_rootWidget || !m_rootWidget)
316 return;
317
318 m_destroyedWidgets.push_back(widget);
319
320 if(m_checkEvent && !m_checkEvent->isExecuted())
321 return;
322
323 m_checkEvent = g_dispatcher.scheduleEvent([this] {
324 g_lua.collectGarbage();
325 UIWidgetList backupList(std::move(m_destroyedWidgets));
326 m_destroyedWidgets.clear();
327 g_dispatcher.scheduleEvent([backupList] {
328 g_lua.collectGarbage();
329 for(const UIWidgetPtr& widget : backupList) {
330 if(widget->ref_count() != 1)
331 g_logger.warning(stdext::format("widget '%s' (parent: '%s' (%s), source: '%s') destroyed but still have %d reference(s) left", widget->getId(), widget->getParent() ? widget->getParent()->getId() : "", widget->getParentId(), widget->getSource(), widget->getUseCount()-1));
332 }
333 }, 1);
334 }, 1000);
335}
336
337void UIManager::clearStyles()
338{
339 m_styles.clear();
340}
341
342bool UIManager::importStyle(std::string file)
343{
344 try {
345 file = g_resources.guessFilePath(file, "otui");
346
347 OTMLDocumentPtr doc = OTMLDocument::parse(file);
348
349 for(const OTMLNodePtr& styleNode : doc->children())
350 importStyleFromOTML(styleNode);
351 return true;
352 } catch(stdext::exception& e) {
353 g_logger.error(stdext::format("Failed to import UI styles from '%s': %s", file, e.what()));
354 return false;
355 }
356}
357
358bool UIManager::importStyleFromString(std::string data)
359{
360 try {
361 OTMLDocumentPtr doc = OTMLDocument::parseString(data, g_lua.getCurrentSourcePath());
362 for(const OTMLNodePtr& styleNode : doc->children())
363 importStyleFromOTML(styleNode);
364 return true;
365 } catch(stdext::exception& e) {
366 g_logger.error(stdext::format("Failed to import UI styles from '%s': %s", g_lua.getCurrentSourcePath(), e.what()));
367 return false;
368 }
369}
370
371void UIManager::importStyleFromOTML(const OTMLNodePtr& styleNode)
372{
373 std::string tag = styleNode->tag();
374 std::vector<std::string> split = stdext::split(tag, "<");
375 if(split.size() != 2)
376 throw OTMLException(styleNode, "not a valid style declaration");
377
378 std::string name = split[0];
379 std::string base = split[1];
380 bool unique = false;
381
382 stdext::trim(name);
383 stdext::trim(base);
384
385 if(name[0] == '#') {
386 name = name.substr(1);
387 unique = true;
388
389 styleNode->setTag(name);
390 styleNode->writeAt("__unique", true);
391 }
392
393 OTMLNodePtr oldStyle = m_styles[name];
394
395 // Warn about redefined styles
396 /*
397 if(!g_app.isRunning() && (oldStyle && !oldStyle->valueAt("__unique", false))) {
398 auto it = m_styles.find(name);
399 if(it != m_styles.end())
400 g_logger.warning(stdext::format("style '%s' is being redefined", name));
401 }
402 */
403
404 if(!oldStyle || !oldStyle->valueAt("__unique", false) || unique) {
405 OTMLNodePtr originalStyle = getStyle(base);
406 if(!originalStyle)
407 stdext::throw_exception(stdext::format("base style '%s', is not defined", base));
408 OTMLNodePtr style = originalStyle->clone();
409 style->merge(styleNode);
410 style->setTag(name);
411 m_styles[name] = style;
412 }
413}
414
415OTMLNodePtr UIManager::getStyle(const std::string& styleName)
416{
417 auto it = m_styles.find(styleName);
418 if(it != m_styles.end())
419 return m_styles[styleName];
420
421 // styles starting with UI are automatically defined
422 if(stdext::starts_with(styleName, "UI")) {
423 OTMLNodePtr node = OTMLNode::create(styleName);
424 node->writeAt("__class", styleName);
425 m_styles[styleName] = node;
426 return node;
427 }
428
429 return nullptr;
430}
431
432std::string UIManager::getStyleClass(const std::string& styleName)
433{
434 OTMLNodePtr style = getStyle(styleName);
435 if(style && style->get("__class"))
436 return style->valueAt("__class");
437 return "";
438}
439
440
441UIWidgetPtr UIManager::loadUIFromString(const std::string& data, const UIWidgetPtr& parent)
442{
443 try {
444 std::stringstream sstream;
445 sstream.clear(std::ios::goodbit);
446 sstream.write(&data[0], data.length());
447 sstream.seekg(0, std::ios::beg);
448 OTMLDocumentPtr doc = OTMLDocument::parse(sstream, "(string)");
449 UIWidgetPtr widget;
450 for (const OTMLNodePtr& node : doc->children()) {
451 std::string tag = node->tag();
452
453 // import styles in these files too
454 if (tag.find("<") != std::string::npos)
455 importStyleFromOTML(node);
456 else {
457 if (widget)
458 stdext::throw_exception("cannot have multiple main widgets in otui files");
459 widget = createWidgetFromOTML(node, parent);
460 }
461 }
462
463 return widget;
464 } catch (stdext::exception& e) {
465 g_logger.error(stdext::format("failed to load UI from string: %s", e.what()));
466 return nullptr;
467 }
468}
469
470UIWidgetPtr UIManager::loadUI(std::string file, const UIWidgetPtr& parent)
471{
472 try {
473 file = g_resources.guessFilePath(file, "otui");
474
475 OTMLDocumentPtr doc = OTMLDocument::parse(file);
476 UIWidgetPtr widget;
477 for(const OTMLNodePtr& node : doc->children()) {
478 std::string tag = node->tag();
479
480 // import styles in these files too
481 if(tag.find("<") != std::string::npos)
482 importStyleFromOTML(node);
483 else {
484 if(widget)
485 stdext::throw_exception("cannot have multiple main widgets in otui files");
486 widget = createWidgetFromOTML(node, parent);
487 }
488 }
489
490 return widget;
491 } catch(stdext::exception& e) {
492 g_logger.error(stdext::format("failed to load UI from '%s': %s", file, e.what()));
493 return nullptr;
494 }
495}
496
497UIWidgetPtr UIManager::createWidget(const std::string& styleName, const UIWidgetPtr& parent)
498{
499 OTMLNodePtr node = OTMLNode::create(styleName);
500 try {
501 return createWidgetFromOTML(node, parent);
502 } catch(stdext::exception& e) {
503 g_logger.error(stdext::format("failed to create widget from style '%s': %s", styleName, e.what()));
504 return nullptr;
505 }
506}
507
508UIWidgetPtr UIManager::createWidgetFromOTML(const OTMLNodePtr& widgetNode, const UIWidgetPtr& parent)
509{
510 OTMLNodePtr originalStyleNode = getStyle(widgetNode->tag());
511 if(!originalStyleNode)
512 stdext::throw_exception(stdext::format("'%s' is not a defined style", widgetNode->tag()));
513
514 OTMLNodePtr styleNode = originalStyleNode->clone();
515 styleNode->merge(widgetNode);
516
517 std::string widgetType = styleNode->valueAt("__class");
518
519 // call widget creation from lua
520 UIWidgetPtr widget = g_lua.callGlobalField<UIWidgetPtr>(widgetType, "create");
521 if(parent)
522 parent->addChild(widget);
523
524 if(widget) {
525 widget->callLuaField("onCreate");
526
527 widget->setStyleFromNode(styleNode);
528
529 for(const OTMLNodePtr& childNode : styleNode->children()) {
530 if(!childNode->isUnique()) {
531 createWidgetFromOTML(childNode, widget);
532 styleNode->removeChild(childNode);
533 }
534 }
535 } else
536 stdext::throw_exception(stdext::format("unable to create widget of type '%s'", widgetType));
537
538 widget->callLuaField("onSetup");
539
540 return widget;
541}
542