stella/src/gui/LauncherDialog.cxx

653 lines
22 KiB
C++

//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2020 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//============================================================================
#include "bspf.hxx"
#include "Bankswitch.hxx"
#include "BrowserDialog.hxx"
#include "ContextMenu.hxx"
#include "DialogContainer.hxx"
#include "Dialog.hxx"
#include "EditTextWidget.hxx"
#include "FileListWidget.hxx"
#include "FSNode.hxx"
#include "MD5.hxx"
#include "OptionsDialog.hxx"
#include "GlobalPropsDialog.hxx"
#include "StellaSettingsDialog.hxx"
#include "MessageBox.hxx"
#include "OSystem.hxx"
#include "FrameBuffer.hxx"
#include "FBSurface.hxx"
#include "EventHandler.hxx"
#include "StellaKeys.hxx"
#include "Props.hxx"
#include "PropsSet.hxx"
#include "RomInfoWidget.hxx"
#include "TIAConstants.hxx"
#include "Settings.hxx"
#include "Widget.hxx"
#include "Font.hxx"
#include "StellaFont.hxx"
#include "ConsoleBFont.hxx"
#include "ConsoleMediumBFont.hxx"
#include "StellaMediumFont.hxx"
#include "StellaLargeFont.hxx"
#include "Stella12x24tFont.hxx"
#include "Stella14x28tFont.hxx"
#include "Stella16x32tFont.hxx"
#include "Version.hxx"
#include "LauncherDialog.hxx"
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent,
int x, int y, int w, int h)
: Dialog(osystem, parent, x, y, w, h)
{
myUseMinimalUI = instance().settings().getBool("minimal_ui");
const GUI::Font& font = instance().frameBuffer().launcherFont();
const int HBORDER = 10;
const int BUTTON_GAP = 8;
const int fontWidth = font.getMaxCharWidth(),
fontHeight = font.getFontHeight(),
lineHeight = font.getLineHeight(),
bwidth = (_w - 2 * HBORDER - BUTTON_GAP * (4 - 1)),
bheight = myUseMinimalUI ? lineHeight - 4 : lineHeight + 4,
LBL_GAP = fontWidth;
int xpos = 0, ypos = 0, lwidth = 0, lwidth2 = 0;
WidgetArray wid;
string lblRom = "Select a ROM from the list" + ELLIPSIS;
const string& lblFilter = "Filter";
const string& lblAllFiles = "Show all files";
const string& lblFound = "XXXX items found";
lwidth = font.getStringWidth(lblRom);
lwidth2 = font.getStringWidth(lblAllFiles) + 20;
int lwidth3 = font.getStringWidth(lblFilter);
int lwidth4 = font.getStringWidth(lblFound);
if(w < HBORDER * 2 + lwidth + lwidth2 + lwidth3 + lwidth4 + fontWidth * 6 + LBL_GAP * 8)
{
// make sure there is space for at least 6 characters in the filter field
lblRom = "Select a ROM" + ELLIPSIS;
lwidth = font.getStringWidth(lblRom);
}
if(myUseMinimalUI)
{
// App information
ostringstream ver;
ver << "Stella " << STELLA_VERSION;
#if defined(RETRON77)
ver << " for RetroN 77";
#endif
ypos += 8;
new StaticTextWidget(this, font, xpos, ypos, _w - 20, fontHeight,
ver.str(), TextAlign::Center);
ypos += fontHeight - 4;
}
// Show the header
xpos += HBORDER; ypos += 8;
new StaticTextWidget(this, font, xpos, ypos, lblRom);
// Shop the files counter
xpos = _w - HBORDER - lwidth4;
myRomCount = new StaticTextWidget(this, font, xpos, ypos,
lwidth4, fontHeight,
"", TextAlign::Right);
// Add filter that can narrow the results shown in the listing
// It has to fit between both labels
if(!myUseMinimalUI && w >= 640)
{
int fwidth = std::min(15 * fontWidth, xpos - lwidth3 - lwidth2 - lwidth - HBORDER - LBL_GAP * 8);
// Show the filter input field
xpos -= fwidth + LBL_GAP;
myPattern = new EditTextWidget(this, font, xpos, ypos - 2, fwidth, lineHeight, "");
// Show the "Filter" label
xpos -= lwidth3 + LBL_GAP;
new StaticTextWidget(this, font, xpos, ypos, lblFilter);
// Show the checkbox for all files
xpos -= lwidth2 + LBL_GAP * 3;
myAllFiles = new CheckboxWidget(this, font, xpos, ypos, lblAllFiles, kAllfilesCmd);
wid.push_back(myAllFiles);
wid.push_back(myPattern);
}
// Add list with game titles
// Before we add the list, we need to know the size of the RomInfoWidget
int listHeight = _h - 43 - bheight - fontHeight - lineHeight;
float imgZoom = getRomInfoZoom(listHeight);
int romWidth = imgZoom * TIAConstants::viewableWidth;
if(romWidth > 0) romWidth += 10;
int listWidth = _w - (romWidth > 0 ? romWidth+8 : 0) - 20;
xpos = HBORDER; ypos += lineHeight + 4;
myList = new FileListWidget(this, font, xpos, ypos, listWidth, listHeight);
myList->setEditable(false);
myList->setListMode(FilesystemNode::ListMode::All);
wid.push_back(myList);
// Add ROM info area (if enabled)
if(romWidth > 0)
{
xpos += myList->getWidth() + 8;
// Initial surface size is the same as the viewable area
Common::Size imgSize(TIAConstants::viewableWidth*imgZoom,
TIAConstants::viewableHeight*imgZoom);
// Calculate font area, and in the process the font that can be used
Common::Size fontArea(romWidth - 16, myList->getHeight() - imgSize.h - 12);
setRomInfoFont(fontArea);
myRomInfoWidget = new RomInfoWidget(this, *myROMInfoFont,
xpos, ypos, romWidth, myList->getHeight(), imgSize);
}
// Add textfield to show current directory
xpos = HBORDER;
ypos += myList->getHeight() + 8;
lwidth = font.getStringWidth("Path") + LBL_GAP;
myDirLabel = new StaticTextWidget(this, font, xpos, ypos+2, lwidth, fontHeight,
"Path", TextAlign::Left);
xpos += lwidth;
myDir = new EditTextWidget(this, font, xpos, ypos, _w - xpos - HBORDER, lineHeight, "");
myDir->setEditable(false, true);
myDir->clearFlags(Widget::FLAG_RETAIN_FOCUS);
if(!myUseMinimalUI)
{
// Add four buttons at the bottom
xpos = HBORDER; ypos += myDir->getHeight() + 8;
#ifndef BSPF_MACOS
myStartButton = new ButtonWidget(this, font, xpos, ypos, (bwidth + 0) / 4, bheight,
"Select", kLoadROMCmd);
wid.push_back(myStartButton);
xpos += (bwidth + 0) / 4 + BUTTON_GAP;
myPrevDirButton = new ButtonWidget(this, font, xpos, ypos, (bwidth + 1) / 4, bheight,
"Go Up", kPrevDirCmd);
wid.push_back(myPrevDirButton);
xpos += (bwidth + 1) / 4 + BUTTON_GAP;
myOptionsButton = new ButtonWidget(this, font, xpos, ypos, (bwidth + 2) / 4, bheight,
"Options" + ELLIPSIS, kOptionsCmd);
wid.push_back(myOptionsButton);
xpos += (bwidth + 2) / 4 + BUTTON_GAP;
myQuitButton = new ButtonWidget(this, font, xpos, ypos, (bwidth + 3) / 4, bheight,
"Quit", kQuitCmd);
wid.push_back(myQuitButton);
#else
myQuitButton = new ButtonWidget(this, font, xpos, ypos, (bwidth + 0) / 4, bheight,
"Quit", kQuitCmd);
wid.push_back(myQuitButton);
xpos += (bwidth + 0) / 4 + BUTTON_GAP;
myOptionsButton = new ButtonWidget(this, font, xpos, ypos, (bwidth + 1) / 4, bheight,
"Options" + ELLIPSIS, kOptionsCmd);
wid.push_back(myOptionsButton);
xpos += (bwidth + 1) / 4 + BUTTON_GAP;
myPrevDirButton = new ButtonWidget(this, font, xpos, ypos, (bwidth + 2) / 4, bheight,
"Go Up", kPrevDirCmd);
wid.push_back(myPrevDirButton);
xpos += (bwidth + 2) / 4 + BUTTON_GAP;
myStartButton = new ButtonWidget(this, font, xpos, ypos, (bwidth + 3) / 4, bheight,
"Select", kLoadROMCmd);
wid.push_back(myStartButton);
#endif
}
if(myUseMinimalUI) // Highlight 'Rom Listing'
mySelectedItem = 0;
else
mySelectedItem = 2;
addToFocusList(wid);
// Create (empty) context menu for ROM list options
myMenu = make_unique<ContextMenu>(this, osystem.frameBuffer().font(), EmptyVarList);
// Create global props dialog, which is used to temporarily override
// ROM properties
myGlobalProps = make_unique<GlobalPropsDialog>(this,
myUseMinimalUI ? osystem.frameBuffer().launcherFont() : osystem.frameBuffer().font());
// Do we show only ROMs or all files?
bool onlyROMs = instance().settings().getBool("launcherroms");
showOnlyROMs(onlyROMs);
if(myAllFiles)
myAllFiles->setState(!onlyROMs);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const string& LauncherDialog::selectedRom() const
{
return currentNode().getPath();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const string& LauncherDialog::selectedRomMD5()
{
if(currentNode().isDirectory() || !Bankswitch::isValidRomName(currentNode()))
return EmptyString;
// Attempt to conserve memory
if(myMD5List.size() > 500)
myMD5List.clear();
// Lookup MD5, and if not present, cache it
auto iter = myMD5List.find(currentNode().getPath());
if(iter == myMD5List.end())
myMD5List[currentNode().getPath()] = MD5::hash(currentNode());
return myMD5List[currentNode().getPath()];
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const FilesystemNode& LauncherDialog::currentNode() const
{
return myList->selected();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::reload()
{
myMD5List.clear();
myList->reload();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::loadConfig()
{
// Should we use a temporary directory specified on the commandline, or the
// default one specified by the settings?
const string& tmpromdir = instance().settings().getString("tmpromdir");
const string& romdir = tmpromdir != "" ? tmpromdir :
instance().settings().getString("romdir");
// Assume that if the list is empty, this is the first time that loadConfig()
// has been called (and we should reload the list)
if(myList->getList().empty())
{
FilesystemNode node(romdir == "" ? "~" : romdir);
if(!(node.exists() && node.isDirectory()))
node = FilesystemNode("~");
myList->setDirectory(node, instance().settings().getString("lastrom"));
updateUI();
}
Dialog::setFocus(getFocusList()[mySelectedItem]);
if(myRomInfoWidget)
myRomInfoWidget->reloadProperties(currentNode());
myList->clearFlags(Widget::FLAG_WANTS_RAWDATA); // always reset this
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::updateUI()
{
// Only hilite the 'up' button if there's a parent directory
if(myPrevDirButton)
myPrevDirButton->setEnabled(myList->currentDir().hasParent());
// Show current directory
myDir->setText(myList->currentDir().getShortPath());
// Indicate how many files were found
ostringstream buf;
buf << (myList->getList().size() - 1) << " items found";
myRomCount->setLabel(buf.str());
// Update ROM info UI item
loadRomInfo();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::applyFiltering()
{
myList->setNameFilter(
[&](const FilesystemNode& node) {
if(!node.isDirectory())
{
// Do we want to show only ROMs or all files?
if(myShowOnlyROMs && !Bankswitch::isValidRomName(node))
return false;
// Skip over files that don't match the pattern in the 'pattern' textbox
if(myPattern && myPattern->getText() != "" &&
!BSPF::containsIgnoreCase(node.getName(), myPattern->getText()))
return false;
}
return true;
}
);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
float LauncherDialog::getRomInfoZoom(int listHeight) const
{
// The ROM info area is some multiple of the minimum TIA image size
float zoom = instance().settings().getFloat("romviewer");
if(zoom > 0.F)
{
// upper zoom limit - at least 24 launchers chars/line and 8 ROM info lines
if((_w - 58 - zoom * TIAConstants::viewableWidth)
/ instance().frameBuffer().launcherFont().getMaxCharWidth() < MIN_LAUNCHER_CHARS)
{
zoom = float(_w - 58 - MIN_LAUNCHER_CHARS * instance().frameBuffer().launcherFont().getMaxCharWidth())
/ TIAConstants::viewableWidth;
}
if((listHeight - 12 - zoom * TIAConstants::viewableHeight) <
MIN_ROMINFO_ROWS * instance().frameBuffer().smallFont().getLineHeight() +
MIN_ROMINFO_LINES * instance().frameBuffer().smallFont().getFontHeight())
{
zoom = float(listHeight - 12 -
MIN_ROMINFO_ROWS * instance().frameBuffer().smallFont().getLineHeight() -
MIN_ROMINFO_LINES * instance().frameBuffer().smallFont().getFontHeight())
/ TIAConstants::viewableHeight;
}
// lower zoom limit - at least 24 ROM info chars/line
if((zoom * TIAConstants::viewableWidth)
/ instance().frameBuffer().smallFont().getMaxCharWidth() < MIN_ROMINFO_CHARS + 6)
{
zoom = float(MIN_ROMINFO_CHARS * instance().frameBuffer().smallFont().getMaxCharWidth() + 6)
/ TIAConstants::viewableWidth;
}
}
return zoom;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::setRomInfoFont(const Common::Size& area)
{
// TODO: Perhaps offer a setting to override the font used?
FontDesc FONTS[7] = {
GUI::stella16x32tDesc, GUI::stella14x28tDesc, GUI::stella12x24tDesc,
GUI::stellaLargeDesc, GUI::stellaMediumDesc,
GUI::consoleMediumBDesc, GUI::consoleBDesc
};
// Try to pick a font that works best, based on the available area
for(int i = 0; i < sizeof(FONTS) / sizeof(FontDesc); ++i)
{
// only use fonts <= launcher fonts
if(instance().frameBuffer().launcherFont().getFontHeight() >= FONTS[i].height)
{
if(area.h >= uInt32(MIN_ROMINFO_ROWS * FONTS[i].height + 2
+ MIN_ROMINFO_LINES * FONTS[i].height)
&& area.w >= uInt32(MIN_ROMINFO_CHARS * FONTS[i].maxwidth))
{
myROMInfoFont = make_unique<GUI::Font>(FONTS[i]);
return;
}
}
}
myROMInfoFont = make_unique<GUI::Font>(GUI::stellaDesc);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::loadRomInfo()
{
if(!myRomInfoWidget)
return;
const string& md5 = selectedRomMD5();
if(md5 != EmptyString)
{
// Get the properties for this entry
Properties props;
instance().propSet().getMD5WithInsert(currentNode(), md5, props);
myRomInfoWidget->setProperties(props, currentNode());
}
else
myRomInfoWidget->clearProperties();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::handleContextMenu()
{
const string& cmd = myMenu->getSelectedTag().toString();
if(cmd == "override")
myGlobalProps->open();
else if(cmd == "reload")
reload();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::showOnlyROMs(bool state)
{
myShowOnlyROMs = state;
instance().settings().setValue("launcherroms", state);
applyFiltering();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::handleKeyDown(StellaKey key, StellaMod mod, bool repeated)
{
// Grab the key before passing it to the actual dialog and check for
// Control-R (reload ROM listing)
if(StellaModTest::isControl(mod) && key == KBDK_R)
reload();
else
#if defined(RETRON77)
// handle keys used by R77
switch(key)
{
case KBDK_F8: // front ("Skill P2")
if (!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode()))
myGlobalProps->open();
break;
case KBDK_F4: // back ("COLOR", "B/W")
openSettings();
break;
case KBDK_F11: // front ("LOAD")
// convert unused previous item key into page-up event
_focusedWidget->handleEvent(Event::UIPgUp);
break;
case KBDK_F1: // front ("MODE")
// convert unused next item key into page-down event
_focusedWidget->handleEvent(Event::UIPgDown);
break;
default:
Dialog::handleKeyDown(key, mod);
break;
}
#else
Dialog::handleKeyDown(key, mod);
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::handleJoyDown(int stick, int button, bool longPress)
{
myEventHandled = false;
myList->setFlags(Widget::FLAG_WANTS_RAWDATA); // allow handling long button press
Dialog::handleJoyDown(stick, button, longPress);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::handleJoyUp(int stick, int button)
{
// open power-up options and settings for 2nd and 4th button if not mapped otherwise
Event::Type e = instance().eventHandler().eventForJoyButton(EventMode::kMenuMode, stick, button);
if (button == 1 && (e == Event::UIOK || e == Event::NoType) &&
!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode()))
myGlobalProps->open();
if (button == 3 && (e == Event::Event::UITabPrev || e == Event::NoType))
openSettings();
else if (!myEventHandled)
Dialog::handleJoyUp(stick, button);
myList->clearFlags(Widget::FLAG_WANTS_RAWDATA); // stop allowing to handle long button press
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Event::Type LauncherDialog::getJoyAxisEvent(int stick, JoyAxis axis, JoyDir adir, int button)
{
Event::Type e = instance().eventHandler().eventForJoyAxis(EventMode::kMenuMode, stick, axis, adir, button);
if(myUseMinimalUI)
{
// map axis events for launcher
switch(e)
{
case Event::UINavPrev:
// convert unused previous item event into page-up event
e = Event::UIPgUp;
break;
case Event::UINavNext:
// convert unused next item event into page-down event
e = Event::UIPgDown;
break;
default:
break;
}
}
return e;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::handleMouseDown(int x, int y, MouseButton b, int clickCount)
{
// Grab right mouse button for context menu, send left to base class
if(b == MouseButton::RIGHT)
{
// Dynamically create context menu for ROM list options
VariantList items;
if (!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode()))
VarList::push_back(items, "Power-on options" + ELLIPSIS, "override");
VarList::push_back(items, "Reload listing", "reload");
myMenu->addItems(items);
// Add menu at current x,y mouse location
myMenu->show(x + getAbsX(), y + getAbsY(), surface().dstRect());
}
else
Dialog::handleMouseDown(x, y, b, clickCount);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::handleCommand(CommandSender* sender, int cmd,
int data, int id)
{
switch (cmd)
{
case kAllfilesCmd:
showOnlyROMs(myAllFiles ? !myAllFiles->getState() : true);
reload();
break;
case kLoadROMCmd:
case FileListWidget::ItemActivated:
loadRom();
break;
case kOptionsCmd:
openSettings();
break;
case kPrevDirCmd:
myList->selectParent();
break;
case FileListWidget::ItemChanged:
updateUI();
break;
case ListWidget::kLongButtonPressCmd:
if (!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode()))
myGlobalProps->open();
myEventHandled = true;
break;
case EditableWidget::kChangedCmd:
applyFiltering(); // pattern matching taken care of directly in this method
reload();
break;
case kQuitCmd:
close();
instance().eventHandler().quit();
break;
case kRomDirChosenCmd:
{
FilesystemNode node(instance().settings().getString("romdir"));
if(!(node.exists() && node.isDirectory()))
node = FilesystemNode("~");
myList->setDirectory(node);
break;
}
case ContextMenu::kItemSelectedCmd:
handleContextMenu();
break;
default:
Dialog::handleCommand(sender, cmd, data, 0);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::loadRom()
{
const string& result = instance().createConsole(currentNode(), selectedRomMD5());
if(result == EmptyString)
{
instance().settings().setValue("lastrom", myList->getSelectedString());
// If romdir has never been set, set it now based on the selected rom
if(instance().settings().getString("romdir") == EmptyString)
instance().settings().setValue("romdir", currentNode().getParent().getShortPath());
}
else
instance().frameBuffer().showMessage(result, MessagePosition::MiddleCenter, true);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::openSettings()
{
// Create an options dialog, similar to the in-game one
if (instance().settings().getBool("basic_settings"))
{
if (myStellaSettingsDialog == nullptr)
myStellaSettingsDialog = make_unique<StellaSettingsDialog>(instance(), parent(),
instance().frameBuffer().launcherFont(), _w, _h, Menu::AppMode::launcher);
myStellaSettingsDialog->open();
}
else
{
if (myOptionsDialog == nullptr)
myOptionsDialog = make_unique<OptionsDialog>(instance(), parent(), this, _w, _h,
Menu::AppMode::launcher);
myOptionsDialog->open();
}
}