/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "base/version.h" #include "common/config-manager.h" #include "common/events.h" #include "common/fs.h" #include "common/gui_options.h" #include "common/util.h" #include "common/system.h" #include "common/translation.h" #include "gui/about.h" #include "gui/browser.h" #include "gui/chooser.h" #include "gui/editgamedialog.h" #include "gui/launcher.h" #include "gui/massadd.h" #include "gui/message.h" #include "gui/gui-manager.h" #include "gui/options.h" #ifdef ENABLE_EVENTRECORDER #include "gui/onscreendialog.h" #include "gui/recorderdialog.h" #include "gui/EventRecorder.h" #endif #include "gui/saveload.h" #include "gui/unknown-game-dialog.h" #include "gui/widgets/edittext.h" #include "gui/widgets/list.h" #include "gui/widgets/tab.h" #include "gui/widgets/popup.h" #include "gui/ThemeEval.h" #include "graphics/cursorman.h" #if defined(USE_CLOUD) && defined(USE_LIBCURL) #include "backends/cloud/cloudmanager.h" #endif using Common::ConfigManager; namespace GUI { enum { kStartCmd = 'STRT', kAboutCmd = 'ABOU', kOptionsCmd = 'OPTN', kAddGameCmd = 'ADDG', kMassAddGameCmd = 'MADD', kEditGameCmd = 'EDTG', kRemoveGameCmd = 'REMG', kLoadGameCmd = 'LOAD', kRecordGameCmd = 'RECG', kQuitCmd = 'QUIT', kSearchCmd = 'SRCH', kListSearchCmd = 'LSSR', kSearchClearCmd = 'SRCL', kCmdGlobalGraphicsOverride = 'OGFX', kCmdGlobalAudioOverride = 'OSFX', kCmdGlobalMIDIOverride = 'OMID', kCmdGlobalMT32Override = 'OM32', kCmdGlobalVolumeOverride = 'OVOL', kCmdChooseSoundFontCmd = 'chsf', kCmdExtraBrowser = 'PEXT', kCmdExtraPathClear = 'PEXC', kCmdGameBrowser = 'PGME', kCmdSaveBrowser = 'PSAV', kCmdSavePathClear = 'PSAC' }; #pragma mark - LauncherDialog::LauncherDialog() : Dialog("Launcher") { _backgroundType = GUI::ThemeEngine::kDialogBackgroundMain; build(); GUI::GuiManager::instance()._launched = true; } void LauncherDialog::selectTarget(const String &target) { if (!target.empty()) { int itemToSelect = 0; StringArray::const_iterator iter; for (iter = _domains.begin(); iter != _domains.end(); ++iter, ++itemToSelect) { if (target == *iter) { _list->setSelected(itemToSelect); break; } } } } LauncherDialog::~LauncherDialog() { delete _browser; delete _loadDialog; } void LauncherDialog::build() { #ifndef DISABLE_FANCY_THEMES _logo = 0; if (g_gui.xmlEval()->getVar("Globals.ShowLauncherLogo") == 1 && g_gui.theme()->supportsImages()) { _logo = new GraphicsWidget(this, "Launcher.Logo"); _logo->useThemeTransparency(true); _logo->setGfx(g_gui.theme()->getImageSurface(ThemeEngine::kImageLogo)); new StaticTextWidget(this, "Launcher.Version", gScummVMVersionDate); } else new StaticTextWidget(this, "Launcher.Version", gScummVMFullVersion); #else // Show ScummVM version new StaticTextWidget(this, "Launcher.Version", gScummVMFullVersion); #endif new ButtonWidget(this, "Launcher.QuitButton", _("~Q~uit"), _("Quit ScummVM"), kQuitCmd); new ButtonWidget(this, "Launcher.AboutButton", _("A~b~out..."), _("About ScummVM"), kAboutCmd); new ButtonWidget(this, "Launcher.OptionsButton", _("~O~ptions..."), _("Change global ScummVM options"), kOptionsCmd); _startButton = new ButtonWidget(this, "Launcher.StartButton", _("~S~tart"), _("Start selected game"), kStartCmd); DropdownButtonWidget *loadButton = new DropdownButtonWidget(this, "Launcher.LoadGameButton", _("~L~oad..."), _("Load saved game for selected game"), kLoadGameCmd); #ifdef ENABLE_EVENTRECORDER loadButton->appendEntry(_("Record..."), kRecordGameCmd); #endif _loadButton = loadButton; // Above the lowest button rows: two more buttons (directly below the list box) if (g_system->getOverlayWidth() > 320) { DropdownButtonWidget *addButton = new DropdownButtonWidget(this, "Launcher.AddGameButton", _("~A~dd Game..."), _("Add games to the list"), kAddGameCmd); addButton->appendEntry(_("Mass Add..."), kMassAddGameCmd); _addButton = addButton; _editButton = new ButtonWidget(this, "Launcher.EditGameButton", _("~E~dit Game..."), _("Change game options"), kEditGameCmd); _removeButton = new ButtonWidget(this, "Launcher.RemoveGameButton", _("~R~emove Game"), _("Remove game from the list. The game data files stay intact"), kRemoveGameCmd); } else { DropdownButtonWidget *addButton = new DropdownButtonWidget(this, "Launcher.AddGameButton", _c("~A~dd Game...", "lowres"), _("Add games to the list"), kAddGameCmd); addButton->appendEntry(_c("Mass Add...", "lowres"), kMassAddGameCmd); _addButton = addButton; _editButton = new ButtonWidget(this, "Launcher.EditGameButton", _c("~E~dit Game...", "lowres"), _("Change game options"), kEditGameCmd); _removeButton = new ButtonWidget(this, "Launcher.RemoveGameButton", _c("~R~emove Game", "lowres"), _("Remove game from the list. The game data files stay intact"), kRemoveGameCmd); } // Search box _searchDesc = 0; #ifndef DISABLE_FANCY_THEMES _searchPic = 0; if (g_gui.xmlEval()->getVar("Globals.ShowSearchPic") == 1 && g_gui.theme()->supportsImages()) { _searchPic = new GraphicsWidget(this, "Launcher.SearchPic", _("Search in game list")); _searchPic->setGfx(g_gui.theme()->getImageSurface(ThemeEngine::kImageSearch)); } else #endif _searchDesc = new StaticTextWidget(this, "Launcher.SearchDesc", _("Search:")); _searchWidget = new EditTextWidget(this, "Launcher.Search", _search, 0, kSearchCmd); _searchClearButton = addClearButton(this, "Launcher.SearchClearButton", kSearchClearCmd); // Add list with game titles _list = new ListWidget(this, "Launcher.GameList", 0, kListSearchCmd); _list->setEditable(false); _list->setNumberingMode(kListNumberingOff); // Populate the list updateListing(); // Restore last selection String last(ConfMan.get("lastselectedgame", ConfigManager::kApplicationDomain)); selectTarget(last); // En-/disable the buttons depending on the list selection updateButtons(); // Create file browser dialog _browser = new BrowserDialog(_("Select directory with game data"), true); // Create Load dialog _loadDialog = new SaveLoadChooser(_("Load game:"), _("Load"), false); } void LauncherDialog::clean() { while (_firstWidget) { Widget* w = _firstWidget; removeWidget(w); // This is called from rebuild() which may result from handleCommand being called by // a child widget sendCommand call. In such a case sendCommand is still being executed // so we should not delete yet the child widget. Thus delay the deletion. g_gui.addToTrash(w, this); } delete _browser; delete _loadDialog; } void LauncherDialog::rebuild() { clean(); build(); reflowLayout(); setDefaultFocusedWidget(); } void LauncherDialog::open() { // Clear the active domain, in case we return to the dialog from a // failure to launch a game. Otherwise, pressing ESC will attempt to // re-launch the same game again. ConfMan.setActiveDomain(""); CursorMan.popAllCursors(); Dialog::open(); updateButtons(); } void LauncherDialog::close() { // Save last selection const int sel = _list->getSelected(); if (sel >= 0) ConfMan.set("lastselectedgame", _domains[sel], ConfigManager::kApplicationDomain); else ConfMan.removeKey("lastselectedgame", ConfigManager::kApplicationDomain); ConfMan.flushToDisk(); Dialog::close(); } void LauncherDialog::updateListing() { StringArray l; ListWidget::ColorList colors; ThemeEngine::FontColor color; // Retrieve a list of all games defined in the config file _domains.clear(); const ConfigManager::DomainMap &domains = ConfMan.getGameDomains(); ConfigManager::DomainMap::const_iterator iter; for (iter = domains.begin(); iter != domains.end(); ++iter) { #ifdef __DS__ // DS port uses an extra section called 'ds'. This prevents the section from being // detected as a game. if (iter->_key == "ds") { continue; } #endif String gameid(iter->_value.getVal("gameid")); String description(iter->_value.getVal("description")); Common::FSNode path(iter->_value.getVal("path")); if (gameid.empty()) gameid = iter->_key; if (description.empty()) { QualifiedGameDescriptor g = EngineMan.findTarget(iter->_key); if (!g.description.empty()) description = g.description; } if (description.empty()) { description = Common::String::format("Unknown (target %s, gameid %s)", iter->_key.c_str(), gameid.c_str()); } if (!gameid.empty() && !description.empty()) { // Insert the game into the launcher list int pos = 0, size = l.size(); while (pos < size && (scumm_stricmp(description.c_str(), l[pos].c_str()) > 0)) pos++; color = ThemeEngine::kFontColorNormal; if (!path.isDirectory()) { color = ThemeEngine::kFontColorAlternate; // If more conditions which grey out entries are added we should consider // enabling this so that it is easy to spot why a certain game entry cannot // be started. // description += Common::String::format(" (%s)", _("Not found")); } l.insert_at(pos, description); colors.insert_at(pos, color); _domains.insert_at(pos, iter->_key); } } const int oldSel = _list->getSelected(); _list->setList(l, &colors); if (oldSel < (int)l.size()) _list->setSelected(oldSel); // Restore the old selection else if (oldSel != -1) // Select the last entry if the list has been reduced _list->setSelected(_list->getList().size() - 1); updateButtons(); // Update the filter settings, those are lost when "setList" // is called. _list->setFilter(_searchWidget->getEditString()); } void LauncherDialog::addGame() { // Allow user to add a new game to the list. // 1) show a dir selection dialog which lets the user pick the directory // the game data resides in. // 2) try to auto detect which game is in the directory, if we cannot // determine it uniquely present a list of candidates to the user // to pick from // 3) Display the 'Edit' dialog for that item, letting the user specify // an alternate description (to distinguish multiple versions of the // game, e.g. 'Monkey German' and 'Monkey English') and set default // options for that game // 4) If no game is found in the specified directory, return to the // dialog. bool looping; do { looping = false; if (_browser->runModal() > 0) { // User made his choice... #if defined(USE_CLOUD) && defined(USE_LIBCURL) String selectedDirectory = _browser->getResult().getPath(); String bannedDirectory = CloudMan.getDownloadLocalDirectory(); if (selectedDirectory.size() && selectedDirectory.lastChar() != '/' && selectedDirectory.lastChar() != '\\') selectedDirectory += '/'; if (bannedDirectory.size() && bannedDirectory.lastChar() != '/' && bannedDirectory.lastChar() != '\\') { if (selectedDirectory.size()) { bannedDirectory += selectedDirectory.lastChar(); } else { bannedDirectory += '/'; } } if (selectedDirectory.equalsIgnoreCase(bannedDirectory)) { MessageDialog alert(_("This directory cannot be used yet, it is being downloaded into!")); alert.runModal(); return; } #endif looping = !doGameDetection(_browser->getResult().getPath()); } } while (looping); } void LauncherDialog::massAddGame() { MessageDialog alert(_("Do you really want to run the mass game detector? " "This could potentially add a huge number of games."), _("Yes"), _("No")); if (alert.runModal() == GUI::kMessageOK && _browser->runModal() > 0) { MassAddDialog massAddDlg(_browser->getResult()); massAddDlg.runModal(); // Update the ListWidget and force a redraw // If new target(s) were added, update the ListWidget and move // the selection to to first newly detected game. Common::String newTarget = massAddDlg.getFirstAddedTarget(); if (!newTarget.empty()) { updateListing(); selectTarget(newTarget); } g_gui.scheduleTopDialogRedraw(); } } void LauncherDialog::removeGame(int item) { MessageDialog alert(_("Do you really want to remove this game configuration?"), _("Yes"), _("No")); if (alert.runModal() == GUI::kMessageOK) { // Remove the currently selected game from the list assert(item >= 0); ConfMan.removeGameDomain(_domains[item]); // Write config to disk ConfMan.flushToDisk(); // Update the ListWidget and force a redraw updateListing(); g_gui.scheduleTopDialogRedraw(); } } void LauncherDialog::editGame(int item) { // Set game specific options. Most of these should be "optional", i.e. by // default set nothing and use the global ScummVM settings. E.g. the user // can set here an optional alternate music volume, or for specific games // a different music driver etc. // This is useful because e.g. MonkeyVGA needs AdLib music to have decent // music support etc. assert(item >= 0); EditGameDialog editDialog(_domains[item]); if (editDialog.runModal() > 0) { // User pressed OK, so make changes permanent // Write config to disk ConfMan.flushToDisk(); // Update the ListWidget, reselect the edited game and force a redraw updateListing(); selectTarget(editDialog.getDomain()); g_gui.scheduleTopDialogRedraw(); } } #ifdef ENABLE_EVENTRECORDER void LauncherDialog::recordGame(int item) { RecorderDialog recorderDialog; MessageDialog alert(_("Do you want to load saved game?"), _("Yes"), _("No")); switch(recorderDialog.runModal(_domains[item])) { default: // fallthrough intended case RecorderDialog::kRecordDialogClose: break; case RecorderDialog::kRecordDialogPlayback: ConfMan.setActiveDomain(_domains[item]); close(); ConfMan.set("record_mode", "playback", ConfigManager::kTransientDomain); ConfMan.set("record_file_name", recorderDialog.getFileName(), ConfigManager::kTransientDomain); break; case RecorderDialog::kRecordDialogRecord: ConfMan.setActiveDomain(_domains[item]); if (alert.runModal() == GUI::kMessageOK) { loadGame(item); } close(); g_eventRec.setAuthor(recorderDialog._author); g_eventRec.setName(recorderDialog._name); g_eventRec.setNotes(recorderDialog._notes); ConfMan.set("record_mode", "record", ConfigManager::kTransientDomain); break; } } #endif void LauncherDialog::loadGame(int item) { String target = _domains[item]; target.toLowercase(); EngineMan.upgradeTargetIfNecessary(target); // Look for the plugin const Plugin *plugin = nullptr; EngineMan.findTarget(target, &plugin); if (plugin) { const MetaEngine &metaEngine = plugin->get(); if (metaEngine.hasFeature(MetaEngine::kSupportsListSaves) && metaEngine.hasFeature(MetaEngine::kSupportsLoadingDuringStartup)) { int slot = _loadDialog->runModalWithPluginAndTarget(plugin, target); if (slot >= 0) { ConfMan.setActiveDomain(_domains[item]); ConfMan.setInt("save_slot", slot, Common::ConfigManager::kTransientDomain); close(); } } else { MessageDialog dialog (_("This game does not support loading games from the launcher."), _("OK")); dialog.runModal(); } } else { MessageDialog dialog(_("ScummVM could not find any engine capable of running the selected game!"), _("OK")); dialog.runModal(); } } void LauncherDialog::handleKeyDown(Common::KeyState state) { if (state.keycode == Common::KEYCODE_TAB) { // Toggle between the game list and the quick search field. if (getFocusWidget() == _searchWidget) { setFocusWidget(_list); } else if (getFocusWidget() == _list) { setFocusWidget(_searchWidget); } } Dialog::handleKeyDown(state); updateButtons(); } void LauncherDialog::handleKeyUp(Common::KeyState state) { Dialog::handleKeyUp(state); updateButtons(); } void LauncherDialog::handleOtherEvent(Common::Event evt) { Dialog::handleOtherEvent(evt); if (evt.type == Common::EVENT_DROP_FILE) { doGameDetection(evt.path); } } bool LauncherDialog::doGameDetection(const Common::String &path) { // Allow user to add a new game to the list. // 2) try to auto detect which game is in the directory, if we cannot // determine it uniquely present a list of candidates to the user // to pick from // 3) Display the 'Edit' dialog for that item, letting the user specify // an alternate description (to distinguish multiple versions of the // game, e.g. 'Monkey German' and 'Monkey English') and set default // options for that game // 4) If no game is found in the specified directory, return to the // dialog. // User made his choice... Common::FSNode dir(path); Common::FSList files; if (!dir.getChildren(files, Common::FSNode::kListAll)) { MessageDialog alert(_("ScummVM couldn't open the specified directory!")); alert.runModal(); return true; } // ...so let's determine a list of candidates, games that // could be contained in the specified directory. DetectionResults detectionResults = EngineMan.detectGames(files); if (detectionResults.foundUnknownGames()) { Common::String report = detectionResults.generateUnknownGameReport(false, 80); g_system->logMessage(LogMessageType::kInfo, report.c_str()); } Common::Array candidates = detectionResults.listDetectedGames(); int idx; if (candidates.empty()) { // No game was found in the specified directory MessageDialog alert(_("ScummVM could not find any game in the specified directory!")); alert.runModal(); idx = -1; return false; } else if (candidates.size() == 1) { // Exact match idx = 0; } else { // Display the candidates to the user and let her/him pick one StringArray list; for (idx = 0; idx < (int)candidates.size(); idx++) { Common::String description = candidates[idx].description; if (candidates[idx].hasUnknownFiles) { description += " - "; description += _("Unknown variant"); } list.push_back(description); } ChooserDialog dialog(_("Pick the game:")); dialog.setList(list); idx = dialog.runModal(); } if (0 <= idx && idx < (int)candidates.size()) { const DetectedGame &result = candidates[idx]; if (result.hasUnknownFiles) { UnknownGameDialog dialog(result); bool cancel = dialog.runModal() == -1; if (cancel) { idx = -1; } } } if (0 <= idx && idx < (int)candidates.size()) { const DetectedGame &result = candidates[idx]; Common::String domain = EngineMan.createTargetForGame(result); // Display edit dialog for the new entry EditGameDialog editDialog(domain); if (editDialog.runModal() > 0) { // User pressed OK, so make changes permanent // Write config to disk ConfMan.flushToDisk(); // Update the ListWidget, select the new item, and force a redraw updateListing(); selectTarget(editDialog.getDomain()); g_gui.scheduleTopDialogRedraw(); } else { // User aborted, remove the the new domain again ConfMan.removeGameDomain(domain); } } return true; } void LauncherDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) { int item = _list->getSelected(); switch (cmd) { case kAddGameCmd: addGame(); break; case kMassAddGameCmd: massAddGame(); break; case kRemoveGameCmd: removeGame(item); break; case kEditGameCmd: editGame(item); break; case kLoadGameCmd: loadGame(item); break; #ifdef ENABLE_EVENTRECORDER case kRecordGameCmd: recordGame(item); break; #endif case kOptionsCmd: { GlobalOptionsDialog options(this); options.runModal(); } break; case kAboutCmd: { AboutDialog about; about.runModal(); } break; case kStartCmd: case kListItemActivatedCmd: case kListItemDoubleClickedCmd: // Start the selected game. assert(item >= 0); ConfMan.setActiveDomain(_domains[item]); close(); break; case kListItemRemovalRequestCmd: removeGame(item); break; case kListSelectionChangedCmd: updateButtons(); break; case kQuitCmd: ConfMan.setActiveDomain(""); setResult(-1); close(); break; case kSearchCmd: // Update the active search filter. _list->setFilter(_searchWidget->getEditString()); break; case kSearchClearCmd: // Reset the active search filter, thus showing all games again _searchWidget->setEditString(""); _list->setFilter(""); break; default: Dialog::handleCommand(sender, cmd, data); } } void LauncherDialog::updateButtons() { bool enable = (_list->getSelected() >= 0); if (enable != _startButton->isEnabled()) { _startButton->setEnabled(enable); _startButton->markAsDirty(); } if (enable != _editButton->isEnabled()) { _editButton->setEnabled(enable); _editButton->markAsDirty(); } if (enable != _removeButton->isEnabled()) { _removeButton->setEnabled(enable); _removeButton->markAsDirty(); } int item = _list->getSelected(); bool en = enable; if (item >= 0) en = !(Common::checkGameGUIOption(GUIO_NOLAUNCHLOAD, ConfMan.get("guioptions", _domains[item]))); if (en != _loadButton->isEnabled()) { _loadButton->setEnabled(en); _loadButton->markAsDirty(); } } void LauncherDialog::reflowLayout() { #ifndef DISABLE_FANCY_THEMES if (g_gui.xmlEval()->getVar("Globals.ShowLauncherLogo") == 1 && g_gui.theme()->supportsImages()) { StaticTextWidget *ver = (StaticTextWidget *)findWidget("Launcher.Version"); if (ver) { ver->setAlign(g_gui.xmlEval()->getWidgetTextHAlign("Launcher.Version")); ver->setLabel(gScummVMVersionDate); } if (!_logo) _logo = new GraphicsWidget(this, "Launcher.Logo"); _logo->useThemeTransparency(true); _logo->setGfx(g_gui.theme()->getImageSurface(ThemeEngine::kImageLogo)); } else { StaticTextWidget *ver = (StaticTextWidget *)findWidget("Launcher.Version"); if (ver) { ver->setAlign(g_gui.xmlEval()->getWidgetTextHAlign("Launcher.Version")); ver->setLabel(gScummVMFullVersion); } if (_logo) { removeWidget(_logo); _logo->setNext(0); delete _logo; _logo = 0; } } if (g_gui.xmlEval()->getVar("Globals.ShowSearchPic") == 1 && g_gui.theme()->supportsImages()) { if (!_searchPic) _searchPic = new GraphicsWidget(this, "Launcher.SearchPic"); _searchPic->setGfx(g_gui.theme()->getImageSurface(ThemeEngine::kImageSearch)); if (_searchDesc) { removeWidget(_searchDesc); _searchDesc->setNext(0); delete _searchDesc; _searchDesc = 0; } } else { if (!_searchDesc) _searchDesc = new StaticTextWidget(this, "Launcher.SearchDesc", _("Search:")); if (_searchPic) { removeWidget(_searchPic); _searchPic->setNext(0); delete _searchPic; _searchPic = 0; } } removeWidget(_searchClearButton); _searchClearButton->setNext(0); delete _searchClearButton; _searchClearButton = addClearButton(this, "Launcher.SearchClearButton", kSearchClearCmd); #endif _w = g_system->getOverlayWidth(); _h = g_system->getOverlayHeight(); Dialog::reflowLayout(); } bool LauncherDialog::checkModifier(int checkedModifier) { int modifiers = g_system->getEventManager()->getModifierState(); return (modifiers & checkedModifier) != 0; } } // End of namespace GUI