1 //============================================================================
2 //
3 // SSSS tt lll lll
4 // SS SS tt ll ll
5 // SS tttttt eeee ll ll aaaa
6 // SSSS tt ee ee ll ll aa
7 // SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
8 // SS SS tt ee ll ll aa aa
9 // SSSS ttt eeeee llll llll aaaaa
10 //
11 // Copyright (c) 1995-2021 by Bradford W. Mott, Stephen Anthony
12 // and the Stella Team
13 //
14 // See the file "License.txt" for information on usage and redistribution of
15 // this file, and for a DISCLAIMER OF ALL WARRANTIES.
16 //============================================================================
17
18 #include "OSystem.hxx"
19 #include "Dialog.hxx"
20 #include "ToolTip.hxx"
21 #include "Stack.hxx"
22 #include "EventHandler.hxx"
23 #include "FrameBuffer.hxx"
24 #include "FBSurface.hxx"
25 #include "Joystick.hxx"
26 #include "bspf.hxx"
27 #include "DialogContainer.hxx"
28
29 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DialogContainer(OSystem & osystem)30 DialogContainer::DialogContainer(OSystem& osystem)
31 : myOSystem{osystem}
32 {
33 _DOUBLE_CLICK_DELAY = osystem.settings().getInt("mdouble");
34 _REPEAT_INITIAL_DELAY = osystem.settings().getInt("ctrldelay");
35 setControllerRate(osystem.settings().getInt("ctrlrate"));
36 reset();
37 }
38
39 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
updateTime(uInt64 time)40 void DialogContainer::updateTime(uInt64 time)
41 {
42 if(myDialogStack.empty())
43 return;
44
45 // We only need millisecond precision
46 myTime = time / 1000;
47
48 // Check for pending continuous events and send them to the active dialog box
49 Dialog* activeDialog = myDialogStack.top();
50
51 // Mouse button still pressed
52 if(myCurrentMouseDown.b != MouseButton::NONE && myClickRepeatTime < myTime)
53 {
54 activeDialog->handleMouseDown(myCurrentMouseDown.x - activeDialog->_x,
55 myCurrentMouseDown.y - activeDialog->_y,
56 myCurrentMouseDown.b, 1);
57 myClickRepeatTime = myTime + _REPEAT_SUSTAIN_DELAY;
58 }
59
60 // Joystick button still pressed
61 if(myCurrentButtonDown.stick != -1 && myButtonRepeatTime < myTime)
62 {
63 activeDialog->handleJoyDown(myCurrentButtonDown.stick, myCurrentButtonDown.button);
64 myButtonRepeatTime = myTime + _REPEAT_SUSTAIN_DELAY;
65 }
66
67 // Joystick has been pressed long
68 if(myCurrentButtonDown.stick != -1 && myButtonLongPressTime < myTime)
69 {
70 myButtonLongPress = true;
71 activeDialog->handleJoyDown(myCurrentButtonDown.stick, myCurrentButtonDown.button, true);
72 myButtonLongPressTime = myButtonRepeatTime = myTime + _REPEAT_NONE;
73 }
74
75 // Joystick axis still pressed
76 if(myCurrentAxisDown.stick != -1 && myAxisRepeatTime < myTime)
77 {
78 activeDialog->handleJoyAxis(myCurrentAxisDown.stick, myCurrentAxisDown.axis,
79 myCurrentAxisDown.adir);
80 myAxisRepeatTime = myTime + _REPEAT_SUSTAIN_DELAY;
81 }
82
83 // Joystick hat still pressed
84 if(myCurrentHatDown.stick != -1 && myHatRepeatTime < myTime)
85 {
86 activeDialog->handleJoyHat(myCurrentHatDown.stick, myCurrentHatDown.hat,
87 myCurrentHatDown.hdir);
88 myHatRepeatTime = myTime + _REPEAT_SUSTAIN_DELAY;
89 }
90 }
91
92 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
draw(bool full)93 void DialogContainer::draw(bool full)
94 {
95 if(myDialogStack.empty())
96 return;
97 #ifdef DEBUG_BUILD
98 //cerr << "draw " << full << " " << typeid(*this).name() << endl;
99 #endif
100
101 // Draw and render all dirty dialogs
102 myDialogStack.applyAll([&](Dialog*& d) {
103 if(full || d->needsRedraw())
104 d->redraw(full);
105 });
106 // Always render all surfaces, bottom to top
107 render();
108 }
109
110 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
tick()111 void DialogContainer::tick()
112 {
113 if(!myDialogStack.empty())
114 myDialogStack.top()->tick();
115 }
116
117 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
render()118 void DialogContainer::render()
119 {
120 if(myDialogStack.empty())
121 return;
122 #ifdef DEBUG_BUILD
123 //cerr << "full re-render " << typeid(*this).name() << endl;
124 #endif
125
126 // Make sure we start in a clean state (with zero'ed buffers)
127 if(!myOSystem.eventHandler().inTIAMode())
128 myOSystem.frameBuffer().clear();
129
130 // Render all dialogs
131 myDialogStack.applyAll([&](Dialog*& d) {
132 d->render();
133 });
134 }
135
136 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
needsRedraw() const137 bool DialogContainer::needsRedraw() const
138 {
139 return !myDialogStack.empty()
140 ? myDialogStack.top()->needsRedraw()
141 : false;
142 }
143
144 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
baseDialogIsActive() const145 bool DialogContainer::baseDialogIsActive() const
146 {
147 return myDialogStack.size() == 1;
148 }
149
150 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
addDialog(Dialog * d)151 int DialogContainer::addDialog(Dialog* d)
152 {
153 const Common::Rect& r = myOSystem.frameBuffer().imageRect();
154 const uInt32 scale = myOSystem.frameBuffer().hidpiScaleFactor();
155
156 if(uInt32(d->getWidth() * scale) > r.w() || uInt32(d->getHeight() * scale) > r.h())
157 myOSystem.frameBuffer().showTextMessage(
158 "Unable to show dialog box; FIX THE CODE", MessagePosition::BottomCenter, true);
159 else
160 {
161 // Close all open tooltips
162 if(!myDialogStack.empty())
163 myDialogStack.top()->tooltip().hide();
164
165 d->setDirty();
166 myDialogStack.push(d);
167 }
168 return myDialogStack.size();
169 }
170
171 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
removeDialog()172 void DialogContainer::removeDialog()
173 {
174 if(!myDialogStack.empty())
175 {
176 #ifdef DEBUG_BUILD
177 //cerr << "remove dialog " << typeid(*myDialogStack.top()).name() << endl;
178 #endif
179 myDialogStack.pop();
180
181 // Inform the frame buffer that it has to render all surfaces
182 myOSystem.frameBuffer().setPendingRender();
183 }
184 }
185
186 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
reStack()187 void DialogContainer::reStack()
188 {
189 // Pop all items from the stack, and then add the base menu
190 while(!myDialogStack.empty())
191 myDialogStack.top()->close();
192
193 // Make sure that all surfaces are cleared
194 myOSystem.frameBuffer().clear();
195
196 baseDialog()->open();
197
198 // Reset all continuous events
199 reset();
200 }
201
202 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleTextEvent(char text)203 void DialogContainer::handleTextEvent(char text)
204 {
205 if(myDialogStack.empty())
206 return;
207
208 // Send the event to the dialog box on the top of the stack
209 Dialog* activeDialog = myDialogStack.top();
210 activeDialog->handleText(text);
211 }
212
213 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleKeyEvent(StellaKey key,StellaMod mod,bool pressed,bool repeated)214 void DialogContainer::handleKeyEvent(StellaKey key, StellaMod mod, bool pressed, bool repeated)
215 {
216 if(myDialogStack.empty())
217 return;
218
219 // Send the event to the dialog box on the top of the stack
220 Dialog* activeDialog = myDialogStack.top();
221 if(pressed)
222 activeDialog->handleKeyDown(key, mod, repeated);
223 else
224 activeDialog->handleKeyUp(key, mod);
225 }
226
227 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleMouseMotionEvent(int x,int y)228 void DialogContainer::handleMouseMotionEvent(int x, int y)
229 {
230 if(myDialogStack.empty())
231 return;
232
233 // Send the event to the dialog box on the top of the stack
234 Dialog* activeDialog = myDialogStack.top();
235 activeDialog->surface().translateCoords(x, y);
236 activeDialog->handleMouseMoved(x - activeDialog->_x, y - activeDialog->_y);
237 }
238
239 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleMouseButtonEvent(MouseButton b,bool pressed,int x,int y)240 void DialogContainer::handleMouseButtonEvent(MouseButton b, bool pressed,
241 int x, int y)
242 {
243 if(myDialogStack.empty())
244 return;
245
246 // Send the event to the dialog box on the top of the stack
247 Dialog* activeDialog = myDialogStack.top();
248 activeDialog->surface().translateCoords(x, y);
249
250 switch(b)
251 {
252 case MouseButton::LEFT:
253 case MouseButton::RIGHT:
254 if(pressed)
255 {
256 // If more than two clicks have been recorded, we start over
257 if(myLastClick.count == 2)
258 {
259 myLastClick.x = myLastClick.y = 0;
260 myLastClick.time = 0;
261 myLastClick.count = 0;
262 }
263
264 if(myLastClick.count && (myTime < myLastClick.time + _DOUBLE_CLICK_DELAY)
265 && std::abs(myLastClick.x - x) < 3
266 && std::abs(myLastClick.y - y) < 3)
267 {
268 myLastClick.count++;
269 }
270 else
271 {
272 myLastClick.x = x;
273 myLastClick.y = y;
274 myLastClick.count = 1;
275 }
276 myLastClick.time = myTime;
277
278 // Now account for repeated mouse events (click and hold), but only
279 // if the dialog wants them
280 if(activeDialog->handleMouseClicks(x - activeDialog->_x, y - activeDialog->_y, b))
281 {
282 myCurrentMouseDown.x = x;
283 myCurrentMouseDown.y = y;
284 myCurrentMouseDown.b = b;
285 myClickRepeatTime = myTime + _REPEAT_INITIAL_DELAY;
286 }
287 else
288 myCurrentMouseDown.b = MouseButton::NONE;
289
290 activeDialog->handleMouseDown(x - activeDialog->_x, y - activeDialog->_y,
291 b, myLastClick.count);
292 }
293 else
294 {
295 activeDialog->handleMouseUp(x - activeDialog->_x, y - activeDialog->_y,
296 b, myLastClick.count);
297
298 if(b == myCurrentMouseDown.b)
299 myCurrentMouseDown.b = MouseButton::NONE;
300 }
301 break;
302
303 case MouseButton::WHEELUP:
304 activeDialog->handleMouseWheel(x - activeDialog->_x, y - activeDialog->_y, -1);
305 break;
306
307 case MouseButton::WHEELDOWN:
308 activeDialog->handleMouseWheel(x - activeDialog->_x, y - activeDialog->_y, 1);
309 break;
310
311 case MouseButton::NONE: // should never get here
312 break;
313 }
314 }
315
316 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleJoyBtnEvent(int stick,int button,bool pressed)317 void DialogContainer::handleJoyBtnEvent(int stick, int button, bool pressed)
318 {
319 if(myDialogStack.empty())
320 return;
321
322 // Send the event to the dialog box on the top of the stack
323 Dialog* activeDialog = myDialogStack.top();
324
325 if(pressed && myButtonRepeatTime < myTime) // prevent pending repeats after enabling repeat again
326 {
327 myCurrentButtonDown.stick = stick;
328 myCurrentButtonDown.button = button;
329 myButtonRepeatTime = myTime + (activeDialog->repeatEnabled() ? _REPEAT_INITIAL_DELAY : _REPEAT_NONE);
330 myButtonLongPressTime = myTime + _LONG_PRESS_DELAY;
331
332 activeDialog->handleJoyDown(stick, button);
333 }
334 else
335 {
336 // Only stop firing events if it's the current button
337 if(stick == myCurrentButtonDown.stick)
338 {
339 myCurrentButtonDown.stick = myCurrentButtonDown.button = -1;
340 myButtonRepeatTime = myButtonLongPressTime = 0;
341 }
342 if (myButtonLongPress)
343 myButtonLongPress = false;
344 else
345 activeDialog->handleJoyUp(stick, button);
346 }
347 }
348
349 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleJoyAxisEvent(int stick,JoyAxis axis,JoyDir adir,int button)350 void DialogContainer::handleJoyAxisEvent(int stick, JoyAxis axis, JoyDir adir, int button)
351 {
352 if(myDialogStack.empty())
353 return;
354
355 // Send the event to the dialog box on the top of the stack
356 Dialog* activeDialog = myDialogStack.top();
357
358 // Prevent long button press in button/axis combinations
359 myButtonLongPressTime = myTime + _REPEAT_NONE;
360
361 // Only stop firing events if it's the current stick
362 if(myCurrentAxisDown.stick == stick && adir == JoyDir::NONE)
363 {
364 myCurrentAxisDown.stick = -1;
365 myCurrentAxisDown.axis = JoyAxis::NONE;
366 myAxisRepeatTime = 0;
367 }
368 else if(adir != JoyDir::NONE && myAxisRepeatTime < myTime) // never repeat the 'off' event; prevent pending repeats after enabling repeat again
369 {
370 // Now account for repeated axis events (press and hold)
371 myCurrentAxisDown.stick = stick;
372 myCurrentAxisDown.axis = axis;
373 myCurrentAxisDown.adir = adir;
374 myAxisRepeatTime = myTime + (activeDialog->repeatEnabled() ? _REPEAT_INITIAL_DELAY : _REPEAT_NONE);
375 }
376 activeDialog->handleJoyAxis(stick, axis, adir, button);
377 }
378
379 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleJoyHatEvent(int stick,int hat,JoyHatDir hdir,int button)380 void DialogContainer::handleJoyHatEvent(int stick, int hat, JoyHatDir hdir, int button)
381 {
382 if(myDialogStack.empty())
383 return;
384
385 // Send the event to the dialog box on the top of the stack
386 Dialog* activeDialog = myDialogStack.top();
387
388 // Prevent long button press in button/hat combinations
389 myButtonLongPressTime = myTime + _REPEAT_NONE;
390
391 // Only stop firing events if it's the current stick
392 if(myCurrentHatDown.stick == stick && hdir == JoyHatDir::CENTER)
393 {
394 myCurrentHatDown.stick = myCurrentHatDown.hat = -1;
395 myHatRepeatTime = 0;
396 }
397 else if(hdir != JoyHatDir::CENTER && myHatRepeatTime < myTime) // never repeat the 'center' direction; prevent pending repeats after enabling repeat again
398 {
399 // Now account for repeated hat events (press and hold)
400 myCurrentHatDown.stick = stick;
401 myCurrentHatDown.hat = hat;
402 myCurrentHatDown.hdir = hdir;
403 myHatRepeatTime = myTime + (activeDialog->repeatEnabled() ? _REPEAT_INITIAL_DELAY : _REPEAT_NONE);
404 }
405 activeDialog->handleJoyHat(stick, hat, hdir, button);
406 }
407
408 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
reset()409 void DialogContainer::reset()
410 {
411 myCurrentMouseDown = { 0, 0, MouseButton::NONE };
412 myCurrentButtonDown = { -1, -1 };
413 myCurrentAxisDown = { -1, JoyAxis::NONE, JoyDir::NONE };
414 myCurrentHatDown = { -1, -1, JoyHatDir::CENTER };
415
416 myLastClick = { 0, 0, 0, 0 };
417 }
418
419 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
420 uInt64 DialogContainer::_DOUBLE_CLICK_DELAY = 500;
421 uInt64 DialogContainer::_REPEAT_INITIAL_DELAY = 400;
422 uInt64 DialogContainer::_REPEAT_SUSTAIN_DELAY = 50;
423