Line data Source code
1 : /* Copyright (C) 2022 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. 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 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "IGUIObject.h"
21 :
22 : #include "gui/CGUI.h"
23 : #include "gui/CGUISetting.h"
24 : #include "gui/ObjectBases/IGUIObject.h"
25 : #include "gui/Scripting/JSInterface_GUIProxy.h"
26 : #include "js/Conversions.h"
27 : #include "ps/CLogger.h"
28 : #include "ps/Profile.h"
29 : #include "scriptinterface/Object.h"
30 : #include "scriptinterface/ScriptContext.h"
31 : #include "scriptinterface/ScriptExtraHeaders.h"
32 : #include "scriptinterface/ScriptConversions.h"
33 : #include "soundmanager/ISoundManager.h"
34 :
35 : #include <algorithm>
36 : #include <string_view>
37 : #include <unordered_map>
38 :
39 1 : const CStr IGUIObject::EventNameMouseEnter = "MouseEnter";
40 1 : const CStr IGUIObject::EventNameMouseMove = "MouseMove";
41 1 : const CStr IGUIObject::EventNameMouseLeave = "MouseLeave";
42 :
43 16 : IGUIObject::IGUIObject(CGUI& pGUI)
44 : : m_pGUI(pGUI),
45 : m_pParent(),
46 : m_MouseHovering(),
47 : m_LastClickTime(),
48 : m_Enabled(this, "enabled", true),
49 : m_Hidden(this, "hidden", false),
50 : m_Size(this, "size"),
51 : m_Style(this, "style"),
52 : m_Hotkey(this, "hotkey"),
53 : m_Z(this, "z"),
54 : m_Absolute(this, "absolute", true),
55 : m_Ghost(this, "ghost", false),
56 : m_AspectRatio(this, "aspectratio"),
57 : m_Tooltip(this, "tooltip"),
58 16 : m_TooltipStyle(this, "tooltip_style")
59 : {
60 16 : }
61 :
62 32 : IGUIObject::~IGUIObject()
63 : {
64 16 : if (!m_ScriptHandlers.empty())
65 2 : JS_RemoveExtraGCRootsTracer(ScriptRequest(m_pGUI.GetScriptInterface()).cx, Trace, this);
66 :
67 : // m_Children is deleted along all other GUI Objects in the CGUI destructor
68 16 : }
69 :
70 4 : void IGUIObject::RegisterChild(IGUIObject* child)
71 : {
72 4 : child->SetParent(this);
73 4 : m_Children.push_back(child);
74 4 : }
75 :
76 0 : void IGUIObject::UnregisterChild(IGUIObject* child)
77 : {
78 0 : std::vector<IGUIObject*>::iterator it = std::find(m_Children.begin(), m_Children.end(), child);
79 0 : if (it != m_Children.end())
80 : {
81 0 : (*it)->m_pParent = nullptr;
82 0 : m_Children.erase(it);
83 : }
84 0 : }
85 :
86 176 : void IGUIObject::RegisterSetting(const CStr& Name, IGUISetting* setting)
87 : {
88 176 : if (SettingExists(Name))
89 0 : LOGERROR("The setting '%s' already exists on the object '%s'!", Name.c_str(), GetPresentableName().c_str());
90 : else
91 176 : m_Settings.emplace(Name, setting);
92 176 : }
93 :
94 0 : void IGUIObject::ReregisterSetting(const CStr& Name, IGUISetting* setting)
95 : {
96 0 : if (!SettingExists(Name))
97 0 : LOGERROR("The setting '%s' must already exist on the object '%s'!", Name.c_str(), GetPresentableName().c_str());
98 : else
99 0 : m_Settings.at(Name) = setting;
100 0 : }
101 :
102 176 : bool IGUIObject::SettingExists(const CStr& Setting) const
103 : {
104 176 : return m_Settings.find(Setting) != m_Settings.end();
105 : }
106 :
107 0 : bool IGUIObject::SetSettingFromString(const CStr& Setting, const CStrW& Value, const bool SendMessage)
108 : {
109 0 : const std::map<CStr, IGUISetting*>::iterator it = m_Settings.find(Setting);
110 0 : if (it == m_Settings.end())
111 : {
112 0 : LOGERROR("GUI object '%s' has no property called '%s', can't set parse and set value '%s'", GetPresentableName().c_str(), Setting.c_str(), Value.ToUTF8().c_str());
113 0 : return false;
114 : }
115 0 : return it->second->FromString(Value, SendMessage);
116 : }
117 :
118 4 : void IGUIObject::SettingChanged(const CStr& Setting, const bool SendMessage)
119 : {
120 4 : if (Setting == "size")
121 : {
122 : // If setting was "size", we need to re-cache itself and all children
123 0 : RecurseObject(nullptr, &IGUIObject::UpdateCachedSize);
124 : }
125 4 : else if (Setting == "hidden")
126 : {
127 : // Hiding an object requires us to reset it and all children
128 0 : if (m_Hidden)
129 0 : RecurseObject(nullptr, &IGUIObject::ResetStates);
130 : }
131 4 : else if (Setting == "style")
132 0 : m_pGUI.SetObjectStyle(this, m_Style);
133 :
134 4 : if (SendMessage)
135 : {
136 0 : SGUIMessage msg(GUIM_SETTINGS_UPDATED, Setting);
137 0 : HandleMessage(msg);
138 : }
139 4 : }
140 :
141 0 : bool IGUIObject::IsMouseOver() const
142 : {
143 0 : return m_CachedActualSize.PointInside(m_pGUI.GetMousePos());
144 : }
145 :
146 0 : void IGUIObject::UpdateMouseOver(IGUIObject* const& pMouseOver)
147 : {
148 0 : if (pMouseOver == this)
149 : {
150 0 : if (!m_MouseHovering)
151 0 : SendMouseEvent(GUIM_MOUSE_ENTER,EventNameMouseEnter);
152 :
153 0 : m_MouseHovering = true;
154 :
155 0 : SendMouseEvent(GUIM_MOUSE_OVER, EventNameMouseMove);
156 : }
157 : else
158 : {
159 0 : if (m_MouseHovering)
160 : {
161 0 : m_MouseHovering = false;
162 0 : SendMouseEvent(GUIM_MOUSE_LEAVE, EventNameMouseLeave);
163 : }
164 : }
165 0 : }
166 :
167 8 : void IGUIObject::ChooseMouseOverAndClosest(IGUIObject*& pObject)
168 : {
169 8 : if (!IsMouseOver())
170 8 : return;
171 :
172 : // Check if we've got competition at all
173 0 : if (pObject == nullptr)
174 : {
175 0 : pObject = this;
176 0 : return;
177 : }
178 :
179 : // Or if it's closer
180 0 : if (GetBufferedZ() >= pObject->GetBufferedZ())
181 : {
182 0 : pObject = this;
183 0 : return;
184 : }
185 : }
186 :
187 0 : IGUIObject* IGUIObject::GetParent() const
188 : {
189 : // Important, we're not using GetParent() for these
190 : // checks, that could screw it up
191 0 : if (m_pParent && m_pParent->m_pParent == nullptr)
192 0 : return nullptr;
193 :
194 0 : return m_pParent;
195 : }
196 :
197 0 : void IGUIObject::ResetStates()
198 : {
199 : // Notify the gui that we aren't hovered anymore
200 0 : UpdateMouseOver(nullptr);
201 0 : }
202 :
203 4 : void IGUIObject::UpdateCachedSize()
204 : {
205 : // If absolute="false" and the object has got a parent,
206 : // use its cached size instead of the screen. Notice
207 : // it must have just been cached for it to work.
208 4 : if (!m_Absolute && m_pParent && !IsRootObject())
209 0 : m_CachedActualSize = m_Size->GetSize(m_pParent->m_CachedActualSize);
210 : else
211 4 : m_CachedActualSize = m_Size->GetSize(CRect(m_pGUI.GetWindowSize()));
212 :
213 : // In a few cases, GUI objects have to resize to fill the screen
214 : // but maintain a constant aspect ratio.
215 : // Adjust the size to be the max possible, centered in the original size:
216 4 : if (m_AspectRatio)
217 : {
218 0 : if (m_CachedActualSize.GetWidth() > m_CachedActualSize.GetHeight() * m_AspectRatio)
219 : {
220 0 : float delta = m_CachedActualSize.GetWidth() - m_CachedActualSize.GetHeight() * m_AspectRatio;
221 0 : m_CachedActualSize.left += delta/2.f;
222 0 : m_CachedActualSize.right -= delta/2.f;
223 : }
224 : else
225 : {
226 0 : float delta = m_CachedActualSize.GetHeight() - m_CachedActualSize.GetWidth() / m_AspectRatio;
227 0 : m_CachedActualSize.bottom -= delta/2.f;
228 0 : m_CachedActualSize.top += delta/2.f;
229 : }
230 : }
231 4 : }
232 :
233 0 : CRect IGUIObject::GetComputedSize()
234 : {
235 0 : UpdateCachedSize();
236 0 : return m_CachedActualSize;
237 : }
238 :
239 :
240 4 : bool IGUIObject::ApplyStyle(const CStr& StyleName)
241 : {
242 4 : if (!m_pGUI.HasStyle(StyleName))
243 : {
244 0 : LOGERROR("IGUIObject: Trying to use style '%s' that doesn't exist.", StyleName.c_str());
245 0 : return false;
246 : }
247 :
248 : // The default style may specify settings for any GUI object.
249 : // Other styles are reported if they specify a Setting that does not exist,
250 : // so that the XML author is informed and can correct the style.
251 :
252 4 : for (const std::pair<const CStr, CStrW>& p : m_pGUI.GetStyle(StyleName).m_SettingsDefaults)
253 : {
254 0 : if (SettingExists(p.first))
255 0 : m_Settings.at(p.first)->FromString(p.second, true);
256 0 : else if (StyleName != "default")
257 0 : LOGWARNING("GUI object has no setting \"%s\", but the style \"%s\" defines it", p.first, StyleName.c_str());
258 : }
259 4 : return true;
260 : }
261 :
262 4 : float IGUIObject::GetBufferedZ() const
263 : {
264 4 : if (m_Absolute)
265 4 : return m_Z;
266 :
267 0 : if (GetParent())
268 0 : return GetParent()->GetBufferedZ() + m_Z;
269 :
270 : // In philosophy, a parentless object shouldn't be able to have a relative sizing,
271 : // but we'll accept it so that absolute can be used as default without a complaint.
272 : // Also, you could consider those objects children to the screen resolution.
273 0 : return m_Z;
274 : }
275 :
276 0 : void IGUIObject::RegisterScriptHandler(const CStr& eventName, const CStr& Code, CGUI& pGUI)
277 : {
278 0 : ScriptRequest rq(pGUI.GetScriptInterface());
279 :
280 0 : const int paramCount = 1;
281 0 : const char* paramNames[paramCount] = { "mouse" };
282 :
283 : // Location to report errors from
284 0 : CStr CodeName = GetName() + " " + eventName;
285 :
286 : // Generate a unique name
287 : static int x = 0;
288 : char buf[64];
289 0 : sprintf_s(buf, ARRAY_SIZE(buf), "__eventhandler%d (%s)", x++, eventName.c_str());
290 :
291 : // TODO: this is essentially the same code as ScriptInterface::LoadScript (with a tweak for the argument).
292 0 : JS::CompileOptions options(rq.cx);
293 0 : options.setFileAndLine(CodeName.c_str(), 0);
294 0 : options.setIsRunOnce(false);
295 :
296 0 : JS::SourceText<mozilla::Utf8Unit> src;
297 0 : ENSURE(src.init(rq.cx, Code.c_str(), Code.length(), JS::SourceOwnership::Borrowed));
298 0 : JS::RootedObjectVector emptyScopeChain(rq.cx);
299 0 : JS::RootedFunction func(rq.cx, JS::CompileFunction(rq.cx, emptyScopeChain, options, buf, paramCount, paramNames, src));
300 0 : if (func == nullptr)
301 : {
302 0 : LOGERROR("RegisterScriptHandler: Failed to compile the script for %s", eventName.c_str());
303 0 : return;
304 : }
305 :
306 0 : JS::RootedObject funcObj(rq.cx, JS_GetFunctionObject(func));
307 0 : SetScriptHandler(eventName, funcObj);
308 : }
309 :
310 5 : void IGUIObject::SetScriptHandler(const CStr& eventName, JS::HandleObject Function)
311 : {
312 5 : if (m_ScriptHandlers.empty())
313 4 : JS_AddExtraGCRootsTracer(ScriptRequest(m_pGUI.GetScriptInterface()).cx, Trace, this);
314 :
315 5 : m_ScriptHandlers[eventName] = JS::Heap<JSObject*>(Function);
316 :
317 5 : if (std::find(m_pGUI.m_EventObjects[eventName].begin(), m_pGUI.m_EventObjects[eventName].end(), this) == m_pGUI.m_EventObjects[eventName].end())
318 4 : m_pGUI.m_EventObjects[eventName].emplace_back(this);
319 5 : }
320 :
321 4 : void IGUIObject::UnsetScriptHandler(const CStr& eventName)
322 : {
323 4 : std::map<CStr, JS::Heap<JSObject*> >::iterator it = m_ScriptHandlers.find(eventName);
324 :
325 4 : if (it == m_ScriptHandlers.end())
326 4 : return;
327 :
328 2 : m_ScriptHandlers.erase(it);
329 :
330 2 : if (m_ScriptHandlers.empty())
331 2 : JS_RemoveExtraGCRootsTracer(ScriptRequest(m_pGUI.GetScriptInterface()).cx, Trace, this);
332 :
333 2 : std::unordered_map<CStr, std::vector<IGUIObject*>>::iterator it2 = m_pGUI.m_EventObjects.find(eventName);
334 2 : if (it2 == m_pGUI.m_EventObjects.end())
335 0 : return;
336 :
337 2 : std::vector<IGUIObject*>& handlers = it2->second;
338 2 : handlers.erase(std::remove(handlers.begin(), handlers.end(), this), handlers.end());
339 :
340 2 : if (handlers.empty())
341 0 : m_pGUI.m_EventObjects.erase(it2);
342 : }
343 :
344 0 : InReaction IGUIObject::SendEvent(EGUIMessageType type, const CStr& eventName)
345 : {
346 0 : PROFILE2_EVENT("gui event");
347 0 : PROFILE2_ATTR("type: %s", eventName.c_str());
348 0 : PROFILE2_ATTR("object: %s", m_Name.c_str());
349 :
350 0 : SGUIMessage msg(type);
351 0 : HandleMessage(msg);
352 :
353 0 : ScriptEvent(eventName);
354 :
355 0 : return msg.skipped ? IN_PASS : IN_HANDLED;
356 : }
357 :
358 0 : InReaction IGUIObject::SendMouseEvent(EGUIMessageType type, const CStr& eventName)
359 : {
360 0 : PROFILE2_EVENT("gui mouse event");
361 0 : PROFILE2_ATTR("type: %s", eventName.c_str());
362 0 : PROFILE2_ATTR("object: %s", m_Name.c_str());
363 :
364 0 : SGUIMessage msg(type);
365 0 : HandleMessage(msg);
366 :
367 0 : ScriptRequest rq(m_pGUI.GetScriptInterface());
368 :
369 : // Set up the 'mouse' parameter
370 0 : JS::RootedValue mouse(rq.cx);
371 :
372 0 : const CVector2D& mousePos = m_pGUI.GetMousePos();
373 :
374 0 : Script::CreateObject(
375 : rq,
376 : &mouse,
377 : "x", mousePos.X,
378 : "y", mousePos.Y,
379 0 : "buttons", m_pGUI.GetMouseButtons());
380 0 : JS::RootedValueVector paramData(rq.cx);
381 0 : ignore_result(paramData.append(mouse));
382 0 : ScriptEvent(eventName, paramData);
383 :
384 0 : return msg.skipped ? IN_PASS : IN_HANDLED;
385 : }
386 :
387 5 : void IGUIObject::ScriptEvent(const CStr& eventName)
388 : {
389 5 : ScriptEventWithReturn(eventName);
390 5 : }
391 :
392 5 : bool IGUIObject::ScriptEventWithReturn(const CStr& eventName)
393 : {
394 5 : if (m_ScriptHandlers.find(eventName) == m_ScriptHandlers.end())
395 1 : return false;
396 :
397 8 : ScriptRequest rq(m_pGUI.GetScriptInterface());
398 8 : JS::RootedValueVector paramData(rq.cx);
399 4 : return ScriptEventWithReturn(eventName, paramData);
400 : }
401 :
402 0 : void IGUIObject::ScriptEvent(const CStr& eventName, const JS::HandleValueArray& paramData)
403 : {
404 0 : ScriptEventWithReturn(eventName, paramData);
405 0 : }
406 :
407 4 : bool IGUIObject::ScriptEventWithReturn(const CStr& eventName, const JS::HandleValueArray& paramData)
408 : {
409 4 : std::map<CStr, JS::Heap<JSObject*> >::iterator it = m_ScriptHandlers.find(eventName);
410 4 : if (it == m_ScriptHandlers.end())
411 0 : return false;
412 :
413 8 : ScriptRequest rq(m_pGUI.GetScriptInterface());
414 8 : JS::RootedObject obj(rq.cx, GetJSObject());
415 8 : JS::RootedValue handlerVal(rq.cx, JS::ObjectValue(*it->second));
416 8 : JS::RootedValue result(rq.cx);
417 :
418 4 : if (!JS_CallFunctionValue(rq.cx, obj, handlerVal, paramData, &result))
419 : {
420 0 : LOGERROR("Errors executing script event \"%s\"", eventName.c_str());
421 0 : ScriptException::CatchPending(rq);
422 0 : return false;
423 : }
424 4 : return JS::ToBoolean(result);
425 : }
426 :
427 9 : JSObject* IGUIObject::GetJSObject()
428 : {
429 : // Cache the object when somebody first asks for it, because otherwise
430 : // we end up doing far too much object allocation.
431 9 : if (!m_JSObject)
432 4 : CreateJSObject();
433 :
434 9 : return m_JSObject->Get();
435 : }
436 :
437 0 : bool IGUIObject::IsEnabled() const
438 : {
439 0 : return m_Enabled;
440 : }
441 :
442 0 : bool IGUIObject::IsHidden() const
443 : {
444 0 : return m_Hidden;
445 : }
446 :
447 16 : bool IGUIObject::IsHiddenOrGhost() const
448 : {
449 16 : return m_Hidden || m_Ghost;
450 : }
451 :
452 0 : void IGUIObject::PlaySound(const CStrW& soundPath) const
453 : {
454 0 : if (g_SoundManager && !soundPath.empty())
455 0 : g_SoundManager->PlayAsUI(soundPath.c_str(), false);
456 0 : }
457 :
458 0 : CStr IGUIObject::GetPresentableName() const
459 : {
460 : // __internal(), must be at least 13 letters to be able to be
461 : // an internal name
462 0 : if (m_Name.length() <= 12)
463 0 : return m_Name;
464 :
465 0 : if (std::string_view{m_Name}.substr(0, 10) == "__internal")
466 0 : return CStr("[unnamed object]");
467 : else
468 0 : return m_Name;
469 : }
470 :
471 0 : void IGUIObject::SetFocus()
472 : {
473 0 : m_pGUI.SetFocusedObject(this);
474 0 : }
475 :
476 0 : void IGUIObject::ReleaseFocus()
477 : {
478 0 : m_pGUI.SetFocusedObject(nullptr);
479 0 : }
480 :
481 0 : bool IGUIObject::IsFocused() const
482 : {
483 0 : return m_pGUI.GetFocusedObject() == this;
484 : }
485 :
486 61 : bool IGUIObject::IsBaseObject() const
487 : {
488 61 : return this == m_pGUI.GetBaseObject();
489 : }
490 :
491 0 : bool IGUIObject::IsRootObject() const
492 : {
493 0 : return m_pParent == m_pGUI.GetBaseObject();
494 : }
495 :
496 0 : void IGUIObject::TraceMember(JSTracer* trc)
497 : {
498 : // Please ensure to adapt the Tracer enabling and disabling in accordance with the GC things traced!
499 :
500 0 : for (std::pair<const CStr, JS::Heap<JSObject*>>& handler : m_ScriptHandlers)
501 0 : JS::TraceEdge(trc, &handler.second, "IGUIObject::m_ScriptHandlers");
502 3 : }
|