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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
29 | 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
51 | void ContextMenu::(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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
71 | void ContextMenu::(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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
88 | void ContextMenu::() |
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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
101 | void ContextMenu::(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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
123 | void ContextMenu::(int idx) |
124 | { |
125 | if(idx >= 0 && idx < int(_entries.size())) |
126 | _selectedItem = idx; |
127 | else |
128 | _selectedItem = -1; |
129 | } |
130 | |
131 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
132 | void ContextMenu::(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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
158 | void ContextMenu::() |
159 | { |
160 | setSelectedIndex(int(_entries.size()) - 1); |
161 | } |
162 | |
163 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
164 | void ContextMenu::() |
165 | { |
166 | _selectedItem = -1; |
167 | } |
168 | |
169 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
170 | int ContextMenu::() const |
171 | { |
172 | return _selectedItem; |
173 | } |
174 | |
175 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
176 | const string& ContextMenu::() const |
177 | { |
178 | return (_selectedItem >= 0) ? _entries[_selectedItem].first : EmptyString; |
179 | } |
180 | |
181 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
182 | const Variant& ContextMenu::() const |
183 | { |
184 | return (_selectedItem >= 0) ? _entries[_selectedItem].second : EmptyVariant; |
185 | } |
186 | |
187 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
188 | bool ContextMenu::() |
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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
199 | bool ContextMenu::() |
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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
210 | bool ContextMenu::() |
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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
221 | bool ContextMenu::() |
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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
232 | void 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
251 | void 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
263 | bool 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
270 | void 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
283 | void ContextMenu::handleKeyDown(StellaKey key, StellaMod mod, bool repeated) |
284 | { |
285 | handleEvent(instance().eventHandler().eventForKey(EventMode::kMenuMode, key, mod)); |
286 | } |
287 | |
288 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
289 | void ContextMenu::handleJoyDown(int stick, int button, bool longPress) |
290 | { |
291 | handleEvent(instance().eventHandler().eventForJoyButton(EventMode::kMenuMode, stick, button)); |
292 | } |
293 | |
294 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
295 | void 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
302 | bool 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
309 | void 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
345 | int ContextMenu::(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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
354 | void ContextMenu::(int item) |
355 | { |
356 | // Change selection |
357 | _selectedOffset = item; |
358 | setDirty(); |
359 | } |
360 | |
361 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
362 | void ContextMenu::() |
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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
392 | void ContextMenu::() |
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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
412 | void ContextMenu::() |
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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
432 | void ContextMenu::() |
433 | { |
434 | if(_firstEntry == 0) |
435 | moveToFirst(); |
436 | else |
437 | scrollUp(_numEntries); |
438 | } |
439 | |
440 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
441 | void ContextMenu::() |
442 | { |
443 | if(_firstEntry == int(_entries.size() - _numEntries)) |
444 | moveToLast(); |
445 | else |
446 | scrollDown(_numEntries); |
447 | } |
448 | |
449 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
450 | void ContextMenu::() |
451 | { |
452 | _firstEntry = 0; |
453 | _scrollUpColor = kColor; |
454 | _scrollDnColor = kScrollColor; |
455 | |
456 | drawCurrentSelection(_firstEntry + (_showScroll ? 1 : 0)); |
457 | } |
458 | |
459 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
460 | void ContextMenu::() |
461 | { |
462 | _firstEntry = int(_entries.size()) - _numEntries; |
463 | _scrollUpColor = kScrollColor; |
464 | _scrollDnColor = kColor; |
465 | |
466 | drawCurrentSelection(_numEntries - (_showScroll ? 0 : 1)); |
467 | } |
468 | |
469 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
470 | void ContextMenu::() |
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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
495 | void ContextMenu::(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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
508 | void ContextMenu::(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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
522 | void ContextMenu::() |
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 | |