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