Line data Source code
1 : /* Copyright (C) 2021 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 "GUITooltip.h"
21 :
22 : #include "gui/CGUI.h"
23 : #include "gui/ObjectTypes/CTooltip.h"
24 : #include "lib/timer.h"
25 : #include "ps/CLogger.h"
26 :
27 : /*
28 : Tooltips:
29 : When holding the mouse stationary over an object for some amount of time,
30 : the tooltip is displayed. If the mouse moves off that object, the tooltip
31 : disappears. If the mouse re-enters an object within a short time, the new
32 : tooltip is displayed immediately. (This lets you run the mouse across a
33 : series of buttons, without waiting ages for the text to pop up every time.)
34 :
35 : See Visual Studio's toolbar buttons for an example.
36 :
37 :
38 : Implemented as a state machine:
39 :
40 : (where "*" lines are checked constantly, and "<" lines are handled
41 : on entry to that state)
42 :
43 : IN MOTION
44 : * If the mouse stops, check whether it should have a tooltip and move to
45 : 'STATIONARY, NO TOOLTIP' or 'STATIONARY, TOOLIP'
46 : * If the mouse enters an object with a tooltip delay of 0, switch to 'SHOWING'
47 :
48 : STATIONARY, NO TOOLTIP
49 : * If the mouse moves, switch to 'IN MOTION'
50 :
51 : STATIONARY, TOOLTIP
52 : < Set target time = now + tooltip time
53 : * If the mouse moves, switch to 'IN MOTION'
54 : * If now > target time, switch to 'SHOWING'
55 :
56 : SHOWING
57 : < Start displaying the tooltip
58 : * If the mouse leaves the object, check whether the new object has a tooltip
59 : and switch to 'SHOWING' or 'COOLING'
60 :
61 : COOLING (since I can't think of a better name)
62 : < Stop displaying the tooltip
63 : < Set target time = now + cooldown time
64 : * If the mouse has moved and is over a tooltipped object, switch to 'SHOWING'
65 : * If now > target time, switch to 'STATIONARY, NO TOOLTIP'
66 : */
67 :
68 : enum
69 : {
70 : ST_IN_MOTION,
71 : ST_STATIONARY_NO_TOOLTIP,
72 : ST_STATIONARY_TOOLTIP,
73 : ST_SHOWING,
74 : ST_COOLING
75 : };
76 :
77 12 : GUITooltip::GUITooltip()
78 12 : : m_State(ST_IN_MOTION), m_PreviousObject(nullptr), m_PreviousTooltipName()
79 : {
80 12 : }
81 :
82 : const double CooldownTime = 0.25; // TODO: Don't hard-code this value
83 :
84 1 : bool GUITooltip::GetTooltip(IGUIObject* obj, CStr& style)
85 : {
86 1 : if (!obj)
87 1 : return false;
88 :
89 0 : if (obj->GetTooltipText().empty())
90 0 : return false;
91 :
92 0 : style = obj->GetTooltipStyle();
93 0 : if (style.empty())
94 0 : style = "default";
95 0 : return true;
96 : }
97 :
98 0 : void GUITooltip::ShowTooltip(IGUIObject* obj, const CVector2D& pos, const CStr& style, CGUI& pGUI)
99 : {
100 0 : ENSURE(obj);
101 :
102 0 : if (style.empty())
103 0 : return;
104 :
105 : // Objects in __tooltip_ are guaranteed to be CTooltip* by the engine.
106 0 : CTooltip* tooltipobj = static_cast<CTooltip*>(pGUI.FindObjectByName("__tooltip_" + style));
107 0 : if (!tooltipobj || !tooltipobj->SettingExists("use_object"))
108 : {
109 0 : LOGERROR("Cannot find tooltip named '%s'", style.c_str());
110 0 : return;
111 : }
112 :
113 : IGUIObject* usedobj; // object actually used to display the tooltip in.
114 :
115 0 : const CStr& usedObjectName = tooltipobj->GetUsedObject();
116 0 : if (usedObjectName.empty())
117 : {
118 0 : usedobj = tooltipobj;
119 0 : tooltipobj->SetMousePos(pos);
120 : }
121 : else
122 : {
123 0 : usedobj = pGUI.FindObjectByName(usedObjectName);
124 0 : if (!usedobj)
125 : {
126 0 : LOGERROR("Cannot find object named '%s' used by tooltip '%s'", usedObjectName.c_str(), style.c_str());
127 0 : return;
128 : }
129 : }
130 :
131 0 : if (usedobj->SettingExists("caption"))
132 : {
133 0 : const CStrW& text = obj->GetTooltipText();
134 0 : usedobj->SetSettingFromString("caption", text, true);
135 : }
136 : else
137 : {
138 0 : LOGERROR("Object '%s' used by tooltip '%s' must have a caption setting!", usedobj->GetPresentableName().c_str(), style.c_str());
139 0 : return;
140 : }
141 :
142 0 : usedobj->SetHidden(false);
143 : }
144 :
145 0 : void GUITooltip::HideTooltip(const CStr& style, CGUI& pGUI)
146 : {
147 0 : if (style.empty())
148 0 : return;
149 :
150 : // Objects in __tooltip_ are guaranteed to be CTooltip* by the engine.
151 0 : CTooltip* tooltipobj = static_cast<CTooltip*>(pGUI.FindObjectByName("__tooltip_" + style));
152 0 : if (!tooltipobj || !tooltipobj->SettingExists("use_object") || !tooltipobj->SettingExists("hide_object"))
153 : {
154 0 : LOGERROR("Cannot find tooltip named '%s' or it is not a tooltip", style.c_str());
155 0 : return;
156 : }
157 0 : const CStr& usedObjectName = tooltipobj->GetUsedObject();
158 0 : if (!usedObjectName.empty())
159 : {
160 0 : IGUIObject* usedobj = pGUI.FindObjectByName(usedObjectName);
161 0 : if (usedobj && usedobj->SettingExists("caption"))
162 : {
163 0 : usedobj->SetSettingFromString("caption", L"", true);
164 : }
165 : else
166 : {
167 0 : LOGERROR("Object named '%s' used by tooltip '%s' does not exist or does not have a caption setting!", usedObjectName.c_str(), style.c_str());
168 0 : return;
169 : }
170 :
171 0 : if (tooltipobj->ShouldHideObject())
172 0 : usedobj->SetHidden(true);
173 : }
174 : else
175 0 : tooltipobj->SetHidden(true);
176 : }
177 :
178 0 : static i32 GetTooltipDelay(const CStr& style, CGUI& pGUI)
179 : {
180 : // Objects in __tooltip_ are guaranteed to be CTooltip* by the engine.
181 0 : CTooltip* tooltipobj = static_cast<CTooltip*>(pGUI.FindObjectByName("__tooltip_" + style));
182 :
183 0 : if (!tooltipobj)
184 : {
185 0 : LOGERROR("Cannot find tooltip object named '%s'", style.c_str());
186 0 : return 500;
187 : }
188 :
189 0 : return tooltipobj->GetTooltipDelay();
190 : }
191 :
192 2 : void GUITooltip::Update(IGUIObject* Nearest, const CVector2D& MousePos, CGUI& GUI)
193 : {
194 : // Called once per frame, so efficiency isn't vital
195 2 : double now = timer_Time();
196 :
197 4 : CStr style;
198 2 : int nextstate = -1;
199 :
200 2 : switch (m_State)
201 : {
202 1 : case ST_IN_MOTION:
203 1 : if (MousePos == m_PreviousMousePos)
204 : {
205 1 : if (GetTooltip(Nearest, style))
206 0 : nextstate = ST_STATIONARY_TOOLTIP;
207 : else
208 1 : nextstate = ST_STATIONARY_NO_TOOLTIP;
209 : }
210 : else
211 : {
212 : // Check for movement onto a zero-delayed tooltip
213 0 : if (GetTooltip(Nearest, style) && GetTooltipDelay(style, GUI)==0)
214 : {
215 : // Reset any previous tooltips completely
216 : //m_Time = now + (double)GetTooltipDelay(style, GUI) / 1000.;
217 0 : HideTooltip(m_PreviousTooltipName, GUI);
218 :
219 0 : nextstate = ST_SHOWING;
220 : }
221 : }
222 1 : break;
223 :
224 1 : case ST_STATIONARY_NO_TOOLTIP:
225 1 : if (MousePos != m_PreviousMousePos)
226 0 : nextstate = ST_IN_MOTION;
227 1 : break;
228 :
229 0 : case ST_STATIONARY_TOOLTIP:
230 0 : if (MousePos != m_PreviousMousePos)
231 0 : nextstate = ST_IN_MOTION;
232 0 : else if (now >= m_Time)
233 : {
234 : // Make sure the tooltip still exists
235 0 : if (GetTooltip(Nearest, style))
236 0 : nextstate = ST_SHOWING;
237 : else
238 : {
239 : // Failed to retrieve style - the object has probably been
240 : // altered, so just restart the process
241 0 : nextstate = ST_IN_MOTION;
242 : }
243 : }
244 0 : break;
245 :
246 0 : case ST_SHOWING:
247 : // Handle sub-object tooltips.
248 0 : if (Nearest == m_PreviousObject)
249 : {
250 : // Still showing the same object's tooltip, but the text might have changed
251 0 : if (GetTooltip(Nearest, style))
252 0 : ShowTooltip(Nearest, MousePos, style, GUI);
253 : else
254 0 : nextstate = ST_COOLING;
255 : }
256 : else
257 : {
258 : // Mouse moved onto a new object
259 0 : if (GetTooltip(Nearest, style))
260 : {
261 0 : CStr style_old;
262 :
263 : // If we're displaying a tooltip with no delay, then we want to
264 : // reset so that other object that should have delay can't
265 : // "ride this tail", it have to wait.
266 : // Notice that this doesn't apply to when you go from one delay=0
267 : // to another delay=0
268 0 : if (GetTooltip(m_PreviousObject, style_old) && GetTooltipDelay(style_old, GUI) == 0 &&
269 0 : GetTooltipDelay(style, GUI) != 0)
270 : {
271 0 : HideTooltip(m_PreviousTooltipName, GUI);
272 0 : nextstate = ST_IN_MOTION;
273 : }
274 : else
275 : {
276 : // Hide old scrollbar
277 0 : HideTooltip(m_PreviousTooltipName, GUI);
278 0 : nextstate = ST_SHOWING;
279 : }
280 : }
281 : else
282 0 : nextstate = ST_COOLING;
283 : }
284 0 : break;
285 :
286 0 : case ST_COOLING:
287 0 : if (GetTooltip(Nearest, style))
288 0 : nextstate = ST_SHOWING;
289 0 : else if (now >= m_Time)
290 0 : nextstate = ST_IN_MOTION;
291 0 : break;
292 : }
293 :
294 : // Handle state-entry code:
295 2 : if (nextstate != -1)
296 : {
297 1 : switch (nextstate)
298 : {
299 0 : case ST_STATIONARY_TOOLTIP:
300 0 : m_Time = now + (double)GetTooltipDelay(style, GUI) / 1000.;
301 0 : break;
302 :
303 0 : case ST_SHOWING:
304 0 : ShowTooltip(Nearest, MousePos, style, GUI);
305 0 : m_PreviousTooltipName = style;
306 0 : break;
307 :
308 0 : case ST_COOLING:
309 0 : HideTooltip(m_PreviousTooltipName, GUI);
310 0 : m_Time = now + CooldownTime;
311 0 : break;
312 : }
313 :
314 1 : m_State = nextstate;
315 : }
316 :
317 2 : m_PreviousMousePos = MousePos;
318 2 : m_PreviousObject = Nearest;
319 2 : }
|