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 "bspf.hxx"
19 #include "Bankswitch.hxx"
20 #include "BrowserDialog.hxx"
21 #include "ContextMenu.hxx"
22 #include "DialogContainer.hxx"
23 #include "Dialog.hxx"
24 #include "EditTextWidget.hxx"
25 #include "FileListWidget.hxx"
26 #include "FSNode.hxx"
27 #include "MD5.hxx"
28 #include "OptionsDialog.hxx"
29 #include "HighScoresDialog.hxx"
30 #include "HighScoresManager.hxx"
31 #include "GlobalPropsDialog.hxx"
32 #include "StellaSettingsDialog.hxx"
33 #include "WhatsNewDialog.hxx"
34 #include "ProgressDialog.hxx"
35 #include "MessageBox.hxx"
36 #include "ToolTip.hxx"
37 #include "TimerManager.hxx"
38 #include "OSystem.hxx"
39 #include "FrameBuffer.hxx"
40 #include "FBSurface.hxx"
41 #include "EventHandler.hxx"
42 #include "StellaKeys.hxx"
43 #include "Props.hxx"
44 #include "PropsSet.hxx"
45 #include "RomInfoWidget.hxx"
46 #include "TIAConstants.hxx"
47 #include "Settings.hxx"
48 #include "Widget.hxx"
49 #include "Font.hxx"
50 #include "StellaFont.hxx"
51 #include "ConsoleBFont.hxx"
52 #include "ConsoleMediumBFont.hxx"
53 #include "StellaMediumFont.hxx"
54 #include "StellaLargeFont.hxx"
55 #include "Stella12x24tFont.hxx"
56 #include "Stella14x28tFont.hxx"
57 #include "Stella16x32tFont.hxx"
58 #include "Version.hxx"
59 #include "MediaFactory.hxx"
60 #include "LauncherDialog.hxx"
61 
62 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
LauncherDialog(OSystem & osystem,DialogContainer & parent,int x,int y,int w,int h)63 LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent,
64                                int x, int y, int w, int h)
65   : Dialog(osystem, parent, osystem.frameBuffer().launcherFont(), "",
66            x, y, w, h)
67 {
68   myUseMinimalUI = instance().settings().getBool("minimal_ui");
69   const int lineHeight   = Dialog::lineHeight(),
70             fontHeight   = Dialog::fontHeight(),
71             fontWidth    = Dialog::fontWidth(),
72             BUTTON_GAP   = Dialog::buttonGap(),
73             VBORDER      = Dialog::vBorder(),
74             HBORDER      = Dialog::hBorder(),
75             VGAP         = Dialog::vGap();
76   const int LBL_GAP      = fontWidth,
77             buttonHeight = myUseMinimalUI ? lineHeight - VGAP * 2: Dialog::buttonHeight(),
78             buttonWidth  = (_w - 2 * HBORDER - BUTTON_GAP * (4 - 1));
79 
80   int xpos = HBORDER, ypos = VBORDER;
81   WidgetArray wid;
82   string lblSelect = "Select a ROM from the list" + ELLIPSIS;
83   string lblAllFiles = "Show all files";
84   const string& lblFilter = "Filter";
85   string lblSubDirs = "Incl. subdirectories";
86   string lblFound = "12345 items found";
87 
88   tooltip().setFont(_font);
89 
90   int lwSelect = _font.getStringWidth(lblSelect);
91   int cwAllFiles = _font.getStringWidth(lblAllFiles) + CheckboxWidget::prefixSize(_font);
92   int cwSubDirs = _font.getStringWidth(lblSubDirs) + CheckboxWidget::prefixSize(_font);
93   int lwFilter = _font.getStringWidth(lblFilter);
94   int lwFound = _font.getStringWidth(lblFound);
95   int wTotal = HBORDER * 2 + lwSelect + cwAllFiles + cwSubDirs + lwFilter + lwFound
96     + EditTextWidget::calcWidth(_font, "123456") + LBL_GAP * 7;
97   bool noSelect = false;
98 
99   if(w < wTotal)
100   {
101     // make sure there is space for at least 6 characters in the filter field
102     lblSelect = "Select a ROM" + ELLIPSIS;
103     int lwSelectShort = _font.getStringWidth(lblSelect);
104 
105     wTotal -= lwSelect - lwSelectShort;
106     lwSelect = lwSelectShort;
107   }
108   if(w < wTotal)
109   {
110     // make sure there is space for at least 6 characters in the filter field
111     lblSubDirs = "Subdir.";
112     int cwSubDirsShort = _font.getStringWidth(lblSubDirs) + CheckboxWidget::prefixSize(_font);
113 
114     wTotal -= cwSubDirs - cwSubDirsShort;
115     cwSubDirs = cwSubDirsShort;
116   }
117   if(w < wTotal)
118   {
119     // make sure there is space for at least 6 characters in the filter field
120     lblAllFiles = "All files";
121     int cwAllFilesShort = _font.getStringWidth(lblAllFiles) + CheckboxWidget::prefixSize(_font);
122 
123     wTotal -= cwAllFiles - cwAllFilesShort;
124     cwAllFiles = cwAllFilesShort;
125   }
126   if(w < wTotal)
127   {
128     // make sure there is space for at least 6 characters in the filter field
129     lblFound = "12345 found";
130     int lwFoundShort = _font.getStringWidth(lblFound);
131 
132     wTotal -= lwFound - lwFoundShort;
133     lwFound = lwFoundShort;
134     myShortCount = true;
135   }
136   if(w < wTotal)
137   {
138     // make sure there is space for at least 6 characters in the filter field
139     lblSelect = "";
140     int lwSelectShort = _font.getStringWidth(lblSelect);
141 
142     // wTotal -= lwSelect - lwSelectShort; // dead code
143     lwSelect = lwSelectShort;
144     noSelect = true;
145   }
146 
147   if(myUseMinimalUI)
148   {
149     // App information
150     ostringstream ver;
151     ver << "Stella " << STELLA_VERSION;
152   #if defined(RETRON77)
153     ver << " for RetroN 77";
154   #endif
155     new StaticTextWidget(this, _font, 0, ypos, _w, fontHeight,
156                          ver.str(), TextAlign::Center);
157     ypos += lineHeight;
158   }
159 
160   // Show the header
161   new StaticTextWidget(this, _font, xpos, ypos, lblSelect);
162   // Shop the files counter
163   xpos = _w - HBORDER - lwFound;
164   myRomCount = new StaticTextWidget(this, _font, xpos, ypos,
165                                     lwFound, fontHeight,
166                                     "", TextAlign::Right);
167 
168   // Add filter that can narrow the results shown in the listing
169   // It has to fit between both labels
170   if(!myUseMinimalUI && w >= 640)
171   {
172     int fwFilter = std::min(EditTextWidget::calcWidth(_font, "123456789012345"),
173                             xpos - cwSubDirs - lwFilter - cwAllFiles
174                             - lwSelect - HBORDER - LBL_GAP * (noSelect ? 5 : 7));
175 
176     // Show the filter input field
177     xpos -= fwFilter + LBL_GAP;
178     myPattern = new EditTextWidget(this, _font, xpos, ypos - 2, fwFilter, lineHeight, "");
179     myPattern->setToolTip("Enter filter text to reduce file list.\n"
180                           "Use '*' and '?' as wildcards.");
181 
182     // Show the "Filter" label
183     xpos -= lwFilter + LBL_GAP;
184     new StaticTextWidget(this, _font, xpos, ypos, lblFilter);
185 
186     // Show the subdirectories checkbox
187     xpos -= cwSubDirs + LBL_GAP * 2;
188     mySubDirs = new CheckboxWidget(this, _font, xpos, ypos, lblSubDirs, kSubDirsCmd);
189     ostringstream tip;
190     tip << "Search files in subdirectories too.";
191     mySubDirs->setToolTip(tip.str());
192 
193     // Show the checkbox for all files
194     if(noSelect)
195       xpos = HBORDER;
196     else
197       xpos -= cwAllFiles + LBL_GAP;
198     myAllFiles = new CheckboxWidget(this, _font, xpos, ypos, lblAllFiles, kAllfilesCmd);
199     myAllFiles->setToolTip("Uncheck to show ROM files only.");
200 
201     wid.push_back(myAllFiles);
202     wid.push_back(myPattern);
203     wid.push_back(mySubDirs);
204   }
205 
206   // Add list with game titles
207   // Before we add the list, we need to know the size of the RomInfoWidget
208   int listHeight = _h - VBORDER * 2 - buttonHeight - lineHeight * 2 - VGAP * 6;
209   float imgZoom = getRomInfoZoom(listHeight);
210   int romWidth = imgZoom * TIAConstants::viewableWidth;
211   if(romWidth > 0) romWidth += HBORDER;
212   int listWidth = _w - (romWidth > 0 ? romWidth + fontWidth : 0) - HBORDER * 2;
213   xpos = HBORDER;  ypos += lineHeight + VGAP;
214   myList = new FileListWidget(this, _font, xpos, ypos, listWidth, listHeight);
215   myList->setEditable(false);
216   myList->setListMode(FilesystemNode::ListMode::All);
217   wid.push_back(myList);
218 
219   // Add ROM info area (if enabled)
220   if(romWidth > 0)
221   {
222     xpos += myList->getWidth() + fontWidth;
223 
224     // Initial surface size is the same as the viewable area
225     Common::Size imgSize(TIAConstants::viewableWidth*imgZoom,
226                          TIAConstants::viewableHeight*imgZoom);
227 
228     // Calculate font area, and in the process the font that can be used
229     Common::Size fontArea(romWidth - fontWidth * 2, myList->getHeight() - imgSize.h - VGAP * 3);
230 
231     setRomInfoFont(fontArea);
232     myRomInfoWidget = new RomInfoWidget(this, *myROMInfoFont,
233         xpos, ypos, romWidth, myList->getHeight(), imgSize);
234   }
235 
236   // Add textfield to show current directory
237   xpos = HBORDER;
238   ypos += myList->getHeight() + VGAP;
239   lwSelect = _font.getStringWidth("Path") + LBL_GAP;
240   myDirLabel = new StaticTextWidget(this, _font, xpos, ypos+2, lwSelect, fontHeight,
241                                     "Path", TextAlign::Left);
242   xpos += lwSelect;
243   myDir = new EditTextWidget(this, _font, xpos, ypos, _w - xpos - HBORDER, lineHeight, "");
244   myDir->setEditable(false, true);
245   myDir->clearFlags(Widget::FLAG_RETAIN_FOCUS);
246 
247   if(!myUseMinimalUI)
248   {
249     // Add four buttons at the bottom
250     xpos = HBORDER;  ypos = _h - VBORDER - buttonHeight;
251   #ifndef BSPF_MACOS
252     myStartButton = new ButtonWidget(this, _font, xpos, ypos, (buttonWidth + 0) / 4, buttonHeight,
253                                      "Select", kLoadROMCmd);
254     wid.push_back(myStartButton);
255 
256     xpos += (buttonWidth + 0) / 4 + BUTTON_GAP;
257     myPrevDirButton = new ButtonWidget(this, _font, xpos, ypos, (buttonWidth + 1) / 4, buttonHeight,
258                                        "Go Up", kPrevDirCmd);
259     wid.push_back(myPrevDirButton);
260 
261     xpos += (buttonWidth + 1) / 4 + BUTTON_GAP;
262     myOptionsButton = new ButtonWidget(this, _font, xpos, ypos, (buttonWidth + 3) / 4, buttonHeight,
263                                        "Options" + ELLIPSIS, kOptionsCmd);
264     wid.push_back(myOptionsButton);
265 
266     xpos += (buttonWidth + 2) / 4 + BUTTON_GAP;
267     myQuitButton = new ButtonWidget(this, _font, xpos, ypos, (buttonWidth + 4) / 4, buttonHeight,
268                                     "Quit", kQuitCmd);
269     wid.push_back(myQuitButton);
270   #else
271     myQuitButton = new ButtonWidget(this, _font, xpos, ypos, (buttonWidth + 0) / 4, buttonHeight,
272                                     "Quit", kQuitCmd);
273     wid.push_back(myQuitButton);
274 
275     xpos += (buttonWidth + 0) / 4 + BUTTON_GAP;
276     myOptionsButton = new ButtonWidget(this, _font, xpos, ypos, (buttonWidth + 1) / 4, buttonHeight,
277                                        "Options" + ELLIPSIS, kOptionsCmd);
278     wid.push_back(myOptionsButton);
279 
280     xpos += (buttonWidth + 1) / 4 + BUTTON_GAP;
281     myPrevDirButton = new ButtonWidget(this, _font, xpos, ypos, (buttonWidth + 2) / 4, buttonHeight,
282                                        "Go Up", kPrevDirCmd);
283     wid.push_back(myPrevDirButton);
284 
285     xpos += (buttonWidth + 2) / 4 + BUTTON_GAP;
286     myStartButton = new ButtonWidget(this, _font, xpos, ypos, (buttonWidth + 3) / 4, buttonHeight,
287                                      "Select", kLoadROMCmd);
288     wid.push_back(myStartButton);
289   #endif
290     myStartButton->setToolTip("Start emulation of selected ROM\nor switch to selected directory.");
291   }
292   if(myUseMinimalUI) // Highlight 'Rom Listing'
293     mySelectedItem = 0;
294   else
295     mySelectedItem = 3;
296 
297   addToFocusList(wid);
298 
299   // since we cannot know how many files there are, use are really high value here
300   myList->progress().setRange(0, 50000, 5);
301   myList->progress().setMessage("        Filtering files" + ELLIPSIS + "        ");
302 
303   // Do we show only ROMs or all files?
304   bool onlyROMs = instance().settings().getBool("launcherroms");
305   showOnlyROMs(onlyROMs);
306   if(myAllFiles)
307     myAllFiles->setState(!onlyROMs);
308 
309   setHelpAnchor("ROMInfo");
310 }
311 
312 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
selectedRom() const313 const string& LauncherDialog::selectedRom() const
314 {
315   return currentNode().getPath();
316 }
317 
318 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
selectedRomMD5()319 const string& LauncherDialog::selectedRomMD5()
320 {
321   if(currentNode().isDirectory() || !Bankswitch::isValidRomName(currentNode()))
322     return EmptyString;
323 
324   // Attempt to conserve memory
325   if(myMD5List.size() > 500)
326     myMD5List.clear();
327 
328   // Lookup MD5, and if not present, cache it
329   auto iter = myMD5List.find(currentNode().getPath());
330   if(iter == myMD5List.end())
331     myMD5List[currentNode().getPath()] = MD5::hash(currentNode());
332 
333   return myMD5List[currentNode().getPath()];
334 }
335 
336 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
currentNode() const337 const FilesystemNode& LauncherDialog::currentNode() const
338 {
339   return myList->selected();
340 }
341 
342 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
currentDir() const343 const FilesystemNode& LauncherDialog::currentDir() const
344 {
345   return myList->currentDir();
346 }
347 
348 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
reload()349 void LauncherDialog::reload()
350 {
351   myMD5List.clear();
352   myList->reload();
353   myPendingReload = false;
354 }
355 
356 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
tick()357 void LauncherDialog::tick()
358 {
359   if(myPendingReload && myReloadTime < TimerManager::getTicks() / 1000)
360     reload();
361 
362   Dialog::tick();
363 }
364 
365 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
loadConfig()366 void LauncherDialog::loadConfig()
367 {
368   // Should we use a temporary directory specified on the commandline, or the
369   // default one specified by the settings?
370   const string& tmpromdir = instance().settings().getString("tmpromdir");
371   const string& romdir = tmpromdir != "" ? tmpromdir :
372       instance().settings().getString("romdir");
373   const string& version = instance().settings().getString("stella.version");
374 
375   // Show "What's New" message when a new version of Stella is run for the first time
376   if(version != STELLA_VERSION)
377   {
378     openWhatsNew();
379     instance().settings().setValue("stella.version", STELLA_VERSION);
380   }
381 
382   bool subDirs = instance().settings().getBool("launchersubdirs");
383   if (mySubDirs) mySubDirs->setState(subDirs);
384   myList->setIncludeSubDirs(subDirs);
385 
386   // Assume that if the list is empty, this is the first time that loadConfig()
387   // has been called (and we should reload the list)
388   if(myList->getList().empty())
389   {
390     FilesystemNode node(romdir == "" ? "~" : romdir);
391     if(!(node.exists() && node.isDirectory()))
392       node = FilesystemNode("~");
393 
394     myList->setDirectory(node, instance().settings().getString("lastrom"));
395     updateUI();
396   }
397   Dialog::setFocus(getFocusList()[mySelectedItem]);
398 
399   if(myRomInfoWidget)
400     myRomInfoWidget->reloadProperties(currentNode());
401 
402   myList->clearFlags(Widget::FLAG_WANTS_RAWDATA); // always reset this
403 }
404 
405 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
saveConfig()406 void LauncherDialog::saveConfig()
407 {
408   if (mySubDirs)
409     instance().settings().setValue("launchersubdirs", mySubDirs->getState());
410   if(instance().settings().getBool("followlauncher"))
411     instance().settings().setValue("romdir", myList->currentDir().getShortPath());
412 }
413 
414 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
updateUI()415 void LauncherDialog::updateUI()
416 {
417   // Only hilite the 'up' button if there's a parent directory
418   if(myPrevDirButton)
419     myPrevDirButton->setEnabled(myList->currentDir().hasParent());
420 
421   // Show current directory
422   myDir->setText(myList->currentDir().getShortPath());
423 
424   // Indicate how many files were found
425   ostringstream buf;
426   buf << (myList->getList().size() - (currentDir().hasParent() ? 1 : 0))
427     << (myShortCount ? " found" : " items found");
428   myRomCount->setLabel(buf.str());
429 
430   // Update ROM info UI item
431   loadRomInfo();
432 }
433 
434 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
matchWithJoker(const string & str,const string & pattern)435 size_t LauncherDialog::matchWithJoker(const string& str, const string& pattern)
436 {
437   if(str.length() >= pattern.length())
438   {
439     // optimize a bit
440     if(pattern.find('?') != string::npos)
441     {
442       for(size_t pos = 0; pos < str.length() - pattern.length() + 1; ++pos)
443       {
444         bool found = true;
445 
446         for(size_t i = 0; found && i < pattern.length(); ++i)
447           if(pattern[i] != str[pos + i] && pattern[i] != '?')
448             found = false;
449 
450         if(found)
451           return pos;
452       }
453     }
454     else
455       return str.find(pattern);
456   }
457   return string::npos;
458 }
459 
460 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
matchWithWildcards(const string & str,const string & pattern)461 bool LauncherDialog::matchWithWildcards(const string& str, const string& pattern)
462 {
463   string pat = pattern;
464 
465   // remove leading and trailing '*'
466   size_t i = 0;
467   while(pat[i++] == '*');
468   pat = pat.substr(i - 1);
469 
470   i = pat.length();
471   while(pat[--i] == '*');
472   pat.erase(i + 1);
473 
474   // Search for first '*'
475   size_t pos = pat.find('*');
476 
477   if(pos != string::npos)
478   {
479     // '*' found, split pattern into left and right part, search recursively
480     const string leftPat = pat.substr(0, pos);
481     const string rightPat = pat.substr(pos + 1);
482     size_t posLeft = matchWithJoker(str, leftPat);
483 
484     if(posLeft != string::npos)
485       return matchWithWildcards(str.substr(pos + posLeft), rightPat);
486     else
487       return false;
488   }
489   // no further '*' found
490   return matchWithJoker(str, pat) != string::npos;
491 }
492 
493 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
matchWithWildcardsIgnoreCase(const string & str,const string & pattern)494 bool LauncherDialog::matchWithWildcardsIgnoreCase(const string& str, const string& pattern)
495 {
496   string in = str;
497   string pat = pattern;
498 
499   BSPF::toUpperCase(in);
500   BSPF::toUpperCase(pat);
501 
502   return matchWithWildcards(in, pat);
503 }
504 
505 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
applyFiltering()506 void LauncherDialog::applyFiltering()
507 {
508   myList->setNameFilter(
509     [&](const FilesystemNode& node) {
510       myList->incProgress();
511       if(!node.isDirectory())
512       {
513         // Do we want to show only ROMs or all files?
514         if(myShowOnlyROMs && !Bankswitch::isValidRomName(node))
515           return false;
516 
517         // Skip over files that don't match the pattern in the 'pattern' textbox
518         if(myPattern && myPattern->getText() != "" &&
519            !matchWithWildcardsIgnoreCase(node.getName(), myPattern->getText()))
520           return false;
521       }
522       return true;
523     }
524   );
525 }
526 
527 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
getRomInfoZoom(int listHeight) const528 float LauncherDialog::getRomInfoZoom(int listHeight) const
529 {
530   // The ROM info area is some multiple of the minimum TIA image size
531   float zoom = instance().settings().getFloat("romviewer");
532 
533   if(zoom > 0.F)
534   {
535     const GUI::Font& smallFont = instance().frameBuffer().smallFont();
536     const int fontWidth = Dialog::fontWidth(),
537               HBORDER   = Dialog::hBorder();
538 
539     // upper zoom limit - at least 24 launchers chars/line and 7 + 4 ROM info lines
540     if((_w - (HBORDER * 2 + fontWidth + 30) - zoom * TIAConstants::viewableWidth)
541        / fontWidth < MIN_LAUNCHER_CHARS)
542     {
543       zoom = float(_w - (HBORDER * 2 + fontWidth + 30) - MIN_LAUNCHER_CHARS * fontWidth)
544         / TIAConstants::viewableWidth;
545     }
546     if((listHeight - 12 - zoom * TIAConstants::viewableHeight) <
547        MIN_ROMINFO_ROWS * smallFont.getLineHeight() +
548        MIN_ROMINFO_LINES * smallFont.getFontHeight())
549     {
550       zoom = float(listHeight - 12 -
551                    MIN_ROMINFO_ROWS * smallFont.getLineHeight() -
552                    MIN_ROMINFO_LINES * smallFont.getFontHeight())
553         / TIAConstants::viewableHeight;
554     }
555 
556     // lower zoom limit - at least 30 ROM info chars/line
557     if((zoom * TIAConstants::viewableWidth)
558        / smallFont.getMaxCharWidth() < MIN_ROMINFO_CHARS + 6)
559     {
560       zoom = float(MIN_ROMINFO_CHARS * smallFont.getMaxCharWidth() + 6)
561         / TIAConstants::viewableWidth;
562     }
563   }
564   return zoom;
565 }
566 
567 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
setRomInfoFont(const Common::Size & area)568 void LauncherDialog::setRomInfoFont(const Common::Size& area)
569 {
570   // TODO: Perhaps offer a setting to override the font used?
571 
572   FontDesc FONTS[7] = {
573     GUI::stella16x32tDesc, GUI::stella14x28tDesc, GUI::stella12x24tDesc,
574     GUI::stellaLargeDesc, GUI::stellaMediumDesc,
575     GUI::consoleMediumBDesc, GUI::consoleBDesc
576   };
577 
578   // Try to pick a font that works best, based on the available area
579   for(size_t i = 0; i < sizeof(FONTS) / sizeof(FontDesc); ++i)
580   {
581     // only use fonts <= launcher fonts
582     if(Dialog::fontHeight() >= FONTS[i].height)
583     {
584       if(area.h >= uInt32(MIN_ROMINFO_ROWS * FONTS[i].height + 2
585          + MIN_ROMINFO_LINES * FONTS[i].height)
586          && area.w >= uInt32(MIN_ROMINFO_CHARS * FONTS[i].maxwidth))
587       {
588         myROMInfoFont = make_unique<GUI::Font>(FONTS[i]);
589         return;
590       }
591     }
592   }
593   myROMInfoFont = make_unique<GUI::Font>(GUI::stellaDesc);
594 }
595 
596 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
loadRomInfo()597 void LauncherDialog::loadRomInfo()
598 {
599   if(!myRomInfoWidget)
600     return;
601 
602   const string& md5 = selectedRomMD5();
603   if(md5 != EmptyString)
604     myRomInfoWidget->setProperties(currentNode(), md5);
605   else
606     myRomInfoWidget->clearProperties();
607 }
608 
609 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleContextMenu()610 void LauncherDialog::handleContextMenu()
611 {
612   const string& cmd = menu().getSelectedTag().toString();
613 
614   if(cmd == "override")
615     openGlobalProps();
616   else if(cmd == "reload")
617     reload();
618   else if(cmd == "highscores")
619     openHighScores();
620 }
621 
622 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
menu()623 ContextMenu& LauncherDialog::menu()
624 {
625   if(myMenu == nullptr)
626     // Create (empty) context menu for ROM list options
627     myMenu = make_unique<ContextMenu>(this, _font, EmptyVarList);
628 
629 
630   return *myMenu;
631 }
632 
633 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
showOnlyROMs(bool state)634 void LauncherDialog::showOnlyROMs(bool state)
635 {
636   myShowOnlyROMs = state;
637   instance().settings().setValue("launcherroms", state);
638   applyFiltering();
639 }
640 
641 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleKeyDown(StellaKey key,StellaMod mod,bool repeated)642 void LauncherDialog::handleKeyDown(StellaKey key, StellaMod mod, bool repeated)
643 {
644   // Grab the key before passing it to the actual dialog and check for
645   // context menu keys
646   bool handled = false;
647 
648   if(StellaModTest::isControl(mod))
649   {
650     handled = true;
651     switch(key)
652     {
653       case KBDK_P:
654         openGlobalProps();
655         break;
656 
657       case KBDK_H:
658         if(instance().highScores().enabled())
659           openHighScores();
660         break;
661 
662       case KBDK_R:
663         reload();
664         break;
665 
666       default:
667         handled = false;
668         break;
669     }
670   }
671   if(!handled)
672 #if defined(RETRON77)
673     // handle keys used by R77
674     switch(key)
675     {
676       case KBDK_F8: // front  ("Skill P2")
677         if (!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode()))
678           openGlobalProps();
679         break;
680       case KBDK_F4: // back ("COLOR", "B/W")
681         openSettings();
682         break;
683 
684       case KBDK_F11: // front ("LOAD")
685         // convert unused previous item key into page-up event
686         _focusedWidget->handleEvent(Event::UIPgUp);
687         break;
688 
689       case KBDK_F1: // front ("MODE")
690         // convert unused next item key into page-down event
691         _focusedWidget->handleEvent(Event::UIPgDown);
692         break;
693 
694       default:
695         Dialog::handleKeyDown(key, mod);
696         break;
697     }
698 #else
699     Dialog::handleKeyDown(key, mod);
700 #endif
701 }
702 
703 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleJoyDown(int stick,int button,bool longPress)704 void LauncherDialog::handleJoyDown(int stick, int button, bool longPress)
705 {
706   myEventHandled = false;
707   myList->setFlags(Widget::FLAG_WANTS_RAWDATA); // allow handling long button press
708   Dialog::handleJoyDown(stick, button, longPress);
709 }
710 
711 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleJoyUp(int stick,int button)712 void LauncherDialog::handleJoyUp(int stick, int button)
713 {
714   // open power-up options and settings for 2nd and 4th button if not mapped otherwise
715   Event::Type e = instance().eventHandler().eventForJoyButton(EventMode::kMenuMode, stick, button);
716 
717   if (button == 1 && (e == Event::UIOK || e == Event::NoType) &&
718       !currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode()))
719     openGlobalProps();
720   if (button == 3 && (e == Event::Event::UITabPrev || e == Event::NoType))
721     openSettings();
722   else if (!myEventHandled)
723     Dialog::handleJoyUp(stick, button);
724 
725   myList->clearFlags(Widget::FLAG_WANTS_RAWDATA); // stop allowing to handle long button press
726 }
727 
728 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
getJoyAxisEvent(int stick,JoyAxis axis,JoyDir adir,int button)729 Event::Type LauncherDialog::getJoyAxisEvent(int stick, JoyAxis axis, JoyDir adir, int button)
730 {
731   Event::Type e = instance().eventHandler().eventForJoyAxis(EventMode::kMenuMode, stick, axis, adir, button);
732 
733   if(myUseMinimalUI)
734   {
735     // map axis events for launcher
736     switch(e)
737     {
738       case Event::UINavPrev:
739         // convert unused previous item event into page-up event
740         e = Event::UIPgUp;
741         break;
742 
743       case Event::UINavNext:
744         // convert unused next item event into page-down event
745         e = Event::UIPgDown;
746         break;
747 
748       default:
749         break;
750     }
751   }
752   return e;
753 }
754 
755 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleMouseDown(int x,int y,MouseButton b,int clickCount)756 void LauncherDialog::handleMouseDown(int x, int y, MouseButton b, int clickCount)
757 {
758   // Grab right mouse button for context menu, send left to base class
759   if(b == MouseButton::RIGHT
760      && x + getAbsX() >= myList->getLeft() && x + getAbsX() <= myList->getRight()
761      && y + getAbsY() >= myList->getTop() && y + getAbsY() <= myList->getBottom())
762   {
763     // Dynamically create context menu for ROM list options
764     VariantList items;
765 
766     if(!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode()))
767       VarList::push_back(items, " Power-on options" + ELLIPSIS + "   Ctrl+P", "override");
768     if(instance().highScores().enabled())
769       VarList::push_back(items, " High scores" + ELLIPSIS + "        Ctrl+H", "highscores");
770     VarList::push_back(items, " Reload listing      Ctrl+R ", "reload");
771     menu().addItems(items);
772 
773     // Add menu at current x,y mouse location
774     menu().show(x + getAbsX(), y + getAbsY(), surface().dstRect());
775   }
776   else
777     Dialog::handleMouseDown(x, y, b, clickCount);
778 }
779 
780 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleCommand(CommandSender * sender,int cmd,int data,int id)781 void LauncherDialog::handleCommand(CommandSender* sender, int cmd,
782                                    int data, int id)
783 {
784   switch (cmd)
785   {
786     case kAllfilesCmd:
787       showOnlyROMs(myAllFiles ? !myAllFiles->getState() : true);
788       reload();
789       break;
790 
791     case kSubDirsCmd:
792       myList->setIncludeSubDirs(mySubDirs->getState());
793       reload();
794       break;
795 
796     case kLoadROMCmd:
797       if(myList->selected().isDirectory())
798       {
799         if(myList->selected().getName() == " [..]")
800           myList->selectParent();
801         else
802           myList->selectDirectory();
803         break;
804       }
805       [[fallthrough]];
806     case FileListWidget::ItemActivated:
807       saveConfig();
808       loadRom();
809       break;
810 
811     case kOptionsCmd:
812       openSettings();
813       break;
814 
815     case kPrevDirCmd:
816       myList->selectParent();
817       break;
818 
819     case FileListWidget::ItemChanged:
820       updateUI();
821       break;
822 
823     case ListWidget::kLongButtonPressCmd:
824       if (!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode()))
825         openGlobalProps();
826       myEventHandled = true;
827       break;
828 
829     case EditableWidget::kChangedCmd:
830     case EditableWidget::kAcceptCmd:
831     {
832       bool subDirs = mySubDirs->getState();
833 
834       myList->setIncludeSubDirs(subDirs);
835       applyFiltering();  // pattern matching taken care of directly in this method
836 
837       if(subDirs && cmd == EditableWidget::kChangedCmd)
838       {
839         // delay (potentially slow) subdirectories reloads until user stops typing
840         myReloadTime = TimerManager::getTicks() / 1000 + myList->getQuickSelectDelay();
841         myPendingReload = true;
842       }
843       else
844         reload();
845       break;
846     }
847 
848     case kQuitCmd:
849       saveConfig();
850       close();
851       instance().eventHandler().quit();
852       break;
853 
854     case kRomDirChosenCmd:
855     {
856       string romDir = instance().settings().getString("romdir");
857 
858       if(myList->currentDir().getPath() != romDir)
859       {
860         FilesystemNode node(romDir);
861 
862         if(!(node.exists() && node.isDirectory()))
863           node = FilesystemNode("~");
864 
865         myList->setDirectory(node);
866       }
867       break;
868     }
869 
870     case ContextMenu::kItemSelectedCmd:
871       handleContextMenu();
872       break;
873 
874     case RomInfoWidget::kClickedCmd:
875     {
876       const string url = myRomInfoWidget->getUrl();
877 
878       if(url != EmptyString)
879         MediaFactory::openURL(url);
880       break;
881     }
882 
883     default:
884       Dialog::handleCommand(sender, cmd, data, 0);
885   }
886 }
887 
888 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
loadRom()889 void LauncherDialog::loadRom()
890 {
891   const string& result = instance().createConsole(currentNode(), selectedRomMD5());
892   if(result == EmptyString)
893   {
894     instance().settings().setValue("lastrom", myList->getSelectedString());
895 
896     // If romdir has never been set, set it now based on the selected rom
897     if(instance().settings().getString("romdir") == EmptyString)
898       instance().settings().setValue("romdir", currentNode().getParent().getShortPath());
899   }
900   else
901     instance().frameBuffer().showTextMessage(result, MessagePosition::MiddleCenter, true);
902 }
903 
904 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
setDefaultDir()905 void LauncherDialog::setDefaultDir()
906 {
907   instance().settings().setValue("romdir", myList->currentDir().getShortPath());
908 }
909 
910 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
openGlobalProps()911 void LauncherDialog::openGlobalProps()
912 {
913   // Create global props dialog, which is used to temporarily override
914   // ROM properties
915   myDialog = make_unique<GlobalPropsDialog>(this, myUseMinimalUI
916                                             ? _font
917                                             : instance().frameBuffer().font());
918   myDialog->open();
919 }
920 
921 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
openSettings()922 void LauncherDialog::openSettings()
923 {
924   saveConfig();
925 
926   // Create an options dialog, similar to the in-game one
927   if (instance().settings().getBool("basic_settings"))
928     myDialog = make_unique<StellaSettingsDialog>(instance(), parent(),
929                                                  _w, _h, AppMode::launcher);
930   else
931     myDialog = make_unique<OptionsDialog>(instance(), parent(), this, _w, _h,
932                                           AppMode::launcher);
933   myDialog->open();
934 }
935 
936 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
openHighScores()937 void LauncherDialog::openHighScores()
938 {
939   // Create an high scores dialog, similar to the in-game one
940   myDialog = make_unique<HighScoresDialog>(instance(), parent(), _w, _h,
941                                            AppMode::launcher);
942   myDialog->open();
943 }
944 
945 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
openWhatsNew()946 void LauncherDialog::openWhatsNew()
947 {
948   myDialog = make_unique<WhatsNewDialog>(instance(), parent(), _w, _h);
949   myDialog->open();
950 }
951