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-2019 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 "EventHandler.hxx"
20#include "FrameBuffer.hxx"
21#include "FBSurface.hxx"
22#include "Font.hxx"
23#include "Dialog.hxx"
24#include "DialogContainer.hxx"
25#include "ScrollBarWidget.hxx"
26#include "ContextMenu.hxx"
27
28// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
29ContextMenu::ContextMenu(GuiObject* boss, const GUI::Font& font,
30 const VariantList& items, int cmd, int width)
31 : Dialog(boss->instance(), boss->parent(), font),
32 CommandSender(boss),
33 _rowHeight(font.getLineHeight()),
34 _firstEntry(0),
35 _numEntries(0),
36 _selectedOffset(0),
37 _selectedItem(-1),
38 _showScroll(false),
39 _isScrolling(false),
40 _scrollUpColor(kColor),
41 _scrollDnColor(kColor),
42 _cmd(cmd),
43 _xorig(0),
44 _yorig(0),
45 _maxWidth(width)
46{
47 addItems(items);
48}
49
50// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
51void ContextMenu::addItems(const VariantList& items)
52{
53 _entries.clear();
54 _entries = items;
55
56 // Resize to largest string
57 int maxwidth = _maxWidth;
58 for(const auto& e: _entries)
59 maxwidth = std::max(maxwidth, _font.getStringWidth(e.first));
60
61 _x = _y = 0;
62 _w = maxwidth + 23;
63 _h = 1; // recalculate this in ::recalc()
64
65 _scrollUpColor = _firstEntry > 0 ? kScrollColor : kColor;
66 _scrollDnColor = (_firstEntry + _numEntries < int(_entries.size())) ?
67 kScrollColor : kColor;
68}
69
70// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
71void ContextMenu::show(uInt32 x, uInt32 y, const Common::Rect& bossRect, int item)
72{
73 uInt32 scale = instance().frameBuffer().hidpiScaleFactor();
74 _xorig = bossRect.x() + x * scale;
75 _yorig = bossRect.y() + y * scale;
76
77 // Only show menu if we're inside the visible area
78 if(!bossRect.contains(_xorig, _yorig))
79 return;
80
81 recalc(instance().frameBuffer().imageRect());
82 open();
83 setSelectedIndex(item);
84 moveToSelected();
85}
86
87// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
88void ContextMenu::center()
89{
90 // First set position according to original coordinates
91 surface().setDstPos(_xorig, _yorig);
92
93 // Now make sure that the entire menu can fit inside the screen bounds
94 // If not, we reset its position
95 if(!instance().frameBuffer().screenRect().contains(
96 _xorig, _yorig, surface().dstRect()))
97 surface().setDstPos(_xorig, _yorig);
98}
99
100// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
101void ContextMenu::recalc(const Common::Rect& image)
102{
103 // Now is the time to adjust the height
104 // If it's higher than the screen, we need to scroll through
105 uInt32 maxentries = std::min(18u, (image.h() - 2) / _rowHeight);
106 if(_entries.size() > maxentries)
107 {
108 // We show two less than the max, so we have room for two scroll buttons
109 _numEntries = maxentries - 2;
110 _h = maxentries * _rowHeight + 2;
111 _showScroll = true;
112 }
113 else
114 {
115 _numEntries = int(_entries.size());
116 _h = int(_entries.size()) * _rowHeight + 2;
117 _showScroll = false;
118 }
119 _isScrolling = false;
120}
121
122// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
123void ContextMenu::setSelectedIndex(int idx)
124{
125 if(idx >= 0 && idx < int(_entries.size()))
126 _selectedItem = idx;
127 else
128 _selectedItem = -1;
129}
130
131// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
132void ContextMenu::setSelected(const Variant& tag, const Variant& defaultTag)
133{
134 if(tag != "") // indicates that the defaultTag should be used instead
135 {
136 for(uInt32 item = 0; item < _entries.size(); ++item)
137 {
138 if(BSPF::equalsIgnoreCase(_entries[item].second.toString(), tag.toString()))
139 {
140 setSelectedIndex(item);
141 return;
142 }
143 }
144 }
145
146 // If we get this far, the value wasn't found; use the default value
147 for(uInt32 item = 0; item < _entries.size(); ++item)
148 {
149 if(BSPF::equalsIgnoreCase(_entries[item].second.toString(), defaultTag.toString()))
150 {
151 setSelectedIndex(item);
152 return;
153 }
154 }
155}
156
157// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
158void ContextMenu::setSelectedMax()
159{
160 setSelectedIndex(int(_entries.size()) - 1);
161}
162
163// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
164void ContextMenu::clearSelection()
165{
166 _selectedItem = -1;
167}
168
169// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
170int ContextMenu::getSelected() const
171{
172 return _selectedItem;
173}
174
175// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
176const string& ContextMenu::getSelectedName() const
177{
178 return (_selectedItem >= 0) ? _entries[_selectedItem].first : EmptyString;
179}
180
181// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
182const Variant& ContextMenu::getSelectedTag() const
183{
184 return (_selectedItem >= 0) ? _entries[_selectedItem].second : EmptyVariant;
185}
186
187// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
188bool ContextMenu::sendSelectionUp()
189{
190 if(isVisible() || _selectedItem <= 0)
191 return false;
192
193 _selectedItem--;
194 sendCommand(_cmd ? _cmd : ContextMenu::kItemSelectedCmd, _selectedItem, -1);
195 return true;
196}
197
198// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
199bool ContextMenu::sendSelectionDown()
200{
201 if(isVisible() || _selectedItem >= int(_entries.size()) - 1)
202 return false;
203
204 _selectedItem++;
205 sendCommand(_cmd ? _cmd : ContextMenu::kItemSelectedCmd, _selectedItem, -1);
206 return true;
207}
208
209// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
210bool ContextMenu::sendSelectionFirst()
211{
212 if(isVisible())
213 return false;
214
215 _selectedItem = 0;
216 sendCommand(_cmd ? _cmd : ContextMenu::kItemSelectedCmd, _selectedItem, -1);
217 return true;
218}
219
220// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
221bool ContextMenu::sendSelectionLast()
222{
223 if(isVisible())
224 return false;
225
226 _selectedItem = int(_entries.size()) - 1;
227 sendCommand(_cmd ? _cmd : ContextMenu::kItemSelectedCmd, _selectedItem, -1);
228 return true;
229}
230
231// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
232void ContextMenu::handleMouseDown(int x, int y, MouseButton b, int clickCount)
233{
234 // Compute over which item the mouse is...
235 int item = findItem(x, y);
236
237 // Only do a selection when the left button is in the dialog
238 if(b == MouseButton::LEFT)
239 {
240 if(item != -1)
241 {
242 _isScrolling = _showScroll && ((item == 0) || (item == _numEntries+1));
243 sendSelection();
244 }
245 else
246 close();
247 }
248}
249
250// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
251void ContextMenu::handleMouseMoved(int x, int y)
252{
253 // Compute over which item the mouse is...
254 int item = findItem(x, y);
255 if(item == -1)
256 return;
257
258 // ...and update the selection accordingly
259 drawCurrentSelection(item);
260}
261
262// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
263bool ContextMenu::handleMouseClicks(int x, int y, MouseButton b)
264{
265 // Let continuous mouse clicks come through, as the scroll buttons need them
266 return true;
267}
268
269// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
270void ContextMenu::handleMouseWheel(int x, int y, int direction)
271{
272 // Wheel events are only relevant in scroll mode
273 if(_showScroll)
274 {
275 if(direction < 0)
276 scrollUp(ScrollBarWidget::getWheelLines());
277 else if(direction > 0)
278 scrollDown(ScrollBarWidget::getWheelLines());
279 }
280}
281
282// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
283void ContextMenu::handleKeyDown(StellaKey key, StellaMod mod, bool repeated)
284{
285 handleEvent(instance().eventHandler().eventForKey(EventMode::kMenuMode, key, mod));
286}
287
288// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
289void ContextMenu::handleJoyDown(int stick, int button, bool longPress)
290{
291 handleEvent(instance().eventHandler().eventForJoyButton(EventMode::kMenuMode, stick, button));
292}
293
294// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
295void ContextMenu::handleJoyAxis(int stick, JoyAxis axis, JoyDir adir, int button)
296{
297 if(adir != JoyDir::NONE) // we don't care about 'axis off' events
298 handleEvent(instance().eventHandler().eventForJoyAxis(EventMode::kMenuMode, stick, axis, adir, button));
299}
300
301// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
302bool ContextMenu::handleJoyHat(int stick, int hat, JoyHatDir hdir, int button)
303{
304 handleEvent(instance().eventHandler().eventForJoyHat(EventMode::kMenuMode, stick, hat, hdir, button));
305 return true;
306}
307
308// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
309void ContextMenu::handleEvent(Event::Type e)
310{
311 switch(e)
312 {
313 case Event::UISelect:
314 sendSelection();
315 break;
316 case Event::UIUp:
317 case Event::UILeft:
318 moveUp();
319 break;
320 case Event::UIDown:
321 case Event::UIRight:
322 moveDown();
323 break;
324 case Event::UIPgUp:
325 movePgUp();
326 break;
327 case Event::UIPgDown:
328 movePgDown();
329 break;
330 case Event::UIHome:
331 moveToFirst();
332 break;
333 case Event::UIEnd:
334 moveToLast();
335 break;
336 case Event::UICancel:
337 close();
338 break;
339 default:
340 break;
341 }
342}
343
344// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
345int ContextMenu::findItem(int x, int y) const
346{
347 if(x >= 0 && x < _w && y >= 0 && y < _h)
348 return (y - 4) / _rowHeight;
349
350 return -1;
351}
352
353// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
354void ContextMenu::drawCurrentSelection(int item)
355{
356 // Change selection
357 _selectedOffset = item;
358 setDirty();
359}
360
361// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
362void ContextMenu::sendSelection()
363{
364 // Select the correct item when scrolling; we have to take into account
365 // that the viewable items are no longer 1-to-1 with the entries
366 int item = _firstEntry + _selectedOffset;
367
368 if(_showScroll)
369 {
370 if(_selectedOffset == 0) // scroll up
371 return scrollUp();
372 else if(_selectedOffset == _numEntries+1) // scroll down
373 return scrollDown();
374 else if(_isScrolling)
375 return;
376 else
377 item--;
378 }
379
380 // We remove the dialog when the user has selected an item
381 // Make sure the dialog is removed before sending any commands,
382 // since one consequence of sending a command may be to add another
383 // dialog/menu
384 close();
385
386 // Send any command associated with the selection
387 _selectedItem = item;
388 sendCommand(_cmd ? _cmd : ContextMenu::kItemSelectedCmd, _selectedItem, -1);
389}
390
391// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
392void ContextMenu::moveUp()
393{
394 if(_showScroll)
395 {
396 // Reaching the top of the list means we have to scroll up, but keep the
397 // current item offset
398 // Otherwise, the offset should decrease by 1
399 if(_selectedOffset == 1)
400 scrollUp();
401 else if(_selectedOffset > 1)
402 drawCurrentSelection(_selectedOffset-1);
403 }
404 else
405 {
406 if(_selectedOffset > 0)
407 drawCurrentSelection(_selectedOffset-1);
408 }
409}
410
411// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
412void ContextMenu::moveDown()
413{
414 if(_showScroll)
415 {
416 // Reaching the bottom of the list means we have to scroll down, but keep the
417 // current item offset
418 // Otherwise, the offset should increase by 1
419 if(_selectedOffset == _numEntries)
420 scrollDown();
421 else if(_selectedOffset < int(_entries.size()))
422 drawCurrentSelection(_selectedOffset+1);
423 }
424 else
425 {
426 if(_selectedOffset < int(_entries.size()) - 1)
427 drawCurrentSelection(_selectedOffset+1);
428 }
429}
430
431// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
432void ContextMenu::movePgUp()
433{
434 if(_firstEntry == 0)
435 moveToFirst();
436 else
437 scrollUp(_numEntries);
438}
439
440// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
441void ContextMenu::movePgDown()
442{
443 if(_firstEntry == int(_entries.size() - _numEntries))
444 moveToLast();
445 else
446 scrollDown(_numEntries);
447}
448
449// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
450void ContextMenu::moveToFirst()
451{
452 _firstEntry = 0;
453 _scrollUpColor = kColor;
454 _scrollDnColor = kScrollColor;
455
456 drawCurrentSelection(_firstEntry + (_showScroll ? 1 : 0));
457}
458
459// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
460void ContextMenu::moveToLast()
461{
462 _firstEntry = int(_entries.size()) - _numEntries;
463 _scrollUpColor = kScrollColor;
464 _scrollDnColor = kColor;
465
466 drawCurrentSelection(_numEntries - (_showScroll ? 0 : 1));
467}
468
469// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
470void ContextMenu::moveToSelected()
471{
472 if(_selectedItem < 0 || _selectedItem >= int(_entries.size()))
473 return;
474
475 // First jump immediately to the item
476 _firstEntry = _selectedItem;
477 int offset = 0;
478
479 // Now check if we've gone past the current 'window' size, and scale
480 // back accordingly
481 int max_offset = int(_entries.size()) - _numEntries;
482 if(_firstEntry > max_offset)
483 {
484 offset = _firstEntry - max_offset;
485 _firstEntry -= offset;
486 }
487
488 _scrollUpColor = _firstEntry > 0 ? kScrollColor : kColor;
489 _scrollDnColor = _firstEntry < max_offset ? kScrollColor : kColor;
490
491 drawCurrentSelection(offset + (_showScroll ? 1 : 0));
492}
493
494// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
495void ContextMenu::scrollUp(int distance)
496{
497 if(_firstEntry == 0)
498 return;
499
500 _firstEntry = std::max(_firstEntry - distance, 0);
501 _scrollUpColor = _firstEntry > 0 ? kScrollColor : kColor;
502 _scrollDnColor = kScrollColor;
503
504 setDirty();
505}
506
507// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
508void ContextMenu::scrollDown(int distance)
509{
510 int max_offset = int(_entries.size()) - _numEntries;
511 if(_firstEntry == max_offset)
512 return;
513
514 _firstEntry = std::min(_firstEntry + distance, max_offset);
515 _scrollUpColor = kScrollColor;
516 _scrollDnColor = _firstEntry < max_offset ? kScrollColor : kColor;
517
518 setDirty();
519}
520
521// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
522void ContextMenu::drawDialog()
523{
524 static uInt32 up_arrow[8] = {
525 0b00011000,
526 0b00011000,
527 0b00111100,
528 0b00111100,
529 0b01111110,
530 0b01111110,
531 0b11111111,
532 0b11111111
533 };
534 static uInt32 down_arrow[8] = {
535 0b11111111,
536 0b11111111,
537 0b01111110,
538 0b01111110,
539 0b00111100,
540 0b00111100,
541 0b00011000,
542 0b00011000
543 };
544
545 // Normally we add widgets and let Dialog::draw() take care of this
546 // logic. But for some reason, this Dialog was written differently
547 // by the ScummVM guys, so I'm not going to mess with it.
548 FBSurface& s = surface();
549
550 // Draw menu border and background
551 s.fillRect(_x+1, _y+1, _w-2, _h-2, kWidColor);
552 s.frameRect(_x, _y, _w, _h, kTextColor);
553
554 // Draw the entries, taking scroll buttons into account
555 int x = _x + 1, y = _y + 1, w = _w - 2;
556
557 // Show top scroll area
558 int offset = _selectedOffset;
559 if(_showScroll)
560 {
561 s.hLine(x, y+_rowHeight-1, w+2, kColor);
562 s.drawBitmap(up_arrow, ((_w-_x)>>1)-4, (_rowHeight>>1)+y-4, _scrollUpColor, 8);
563 y += _rowHeight;
564 offset--;
565 }
566
567 for(int i = _firstEntry, current = 0; i < _firstEntry + _numEntries; ++i, ++current)
568 {
569 bool hilite = offset == current;
570 if(hilite) s.fillRect(x, y, w, _rowHeight, kTextColorHi);
571 s.drawString(_font, _entries[i].first, x + 1, y + 2, w,
572 !hilite ? kTextColor : kTextColorInv);
573 y += _rowHeight;
574 }
575
576 // Show bottom scroll area
577 if(_showScroll)
578 {
579 s.hLine(x, y, w+2, kColor);
580 s.drawBitmap(down_arrow, ((_w-_x)>>1)-4, (_rowHeight>>1)+y-4, _scrollDnColor, 8);
581 }
582
583 setDirty();
584}
585