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