scummvm/gui/launcher.cpp

493 lines
16 KiB
C++

/* ScummVM - Scumm Interpreter
* Copyright (C) 2002-2003 The ScummVM project
*
* 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* $Header$
*/
#include "stdafx.h"
#include "gui/about.h"
#include "gui/browser.h"
#include "gui/chooser.h"
#include "gui/launcher.h"
#include "gui/message.h"
#include "gui/newgui.h"
#include "gui/options.h"
#include "gui/EditTextWidget.h"
#include "gui/ListWidget.h"
#include "backends/fs/fs.h"
#include "base/gameDetector.h"
#include "base/plugins.h"
#include "common/config-file.h"
#include "base/engine.h"
enum {
kStartCmd = 'STRT',
kAboutCmd = 'ABOU',
kOptionsCmd = 'OPTN',
kAddGameCmd = 'ADDG',
kEditGameCmd = 'EDTG',
kRemoveGameCmd = 'REMG',
kQuitCmd = 'QUIT'
};
typedef ScummVM::List<const TargetSettings *> GameList;
/*
* A dialog that allows the user to edit a config game entry.
* TODO: add widgets for some/all of the following
* - Amiga/subtitles flag? Although those only make sense for Scumm games, not Simon
* - The music driver for that game (<Default> or custom)
* Of course this means we need an API to query the available music drivers.
* - Maybe scaler/graphics mode. But there are two problems:
* 1) Different backends can have different scalers with different names,
* so we first have to add a way to query those... no Ender, I don't
* think a bitmasked property() value is nice for this, because we would
* have to add to the bitmask values whenever a backends adds a new scaler).
* 2) At the time the launcher is running, the GFX backend is already setup.
* So when a game is run via the launcher, the custom scaler setting for it won't be
* used. So we'd also have to add an API to change the scaler during runtime
* (the SDL backend can already do that based on user input, but there is no API
* to achieve it)
* If the APIs for 1&2 are in place, we can think about adding this to the Edit&Option dialogs
* - Maybe SFX/Master/Music volumes?
*/
enum {
kOKCmd = 'OK '
};
class EditGameDialog : public Dialog {
typedef ScummVM::String String;
typedef ScummVM::StringList StringList;
public:
EditGameDialog(NewGui *gui, const String &domain, const TargetSettings *target);
virtual void handleCommand(CommandSender *sender, uint32 cmd, uint32 data);
protected:
Config &_config;
const String &_domain;
EditTextWidget *_descriptionWidget;
EditTextWidget *_domainWidget;
CheckboxWidget *_fullscreenCheckbox;
CheckboxWidget *_amigaCheckbox;
};
EditGameDialog::EditGameDialog(NewGui *gui, const String &domain, const TargetSettings *target)
: Dialog(gui, 8, 50, 320 - 2 * 8, 200 - 2 * 40), _config(*g_config), _domain(domain) {
// Determine the description string
String description(_config.get("description", _domain));
if (description.isEmpty()) {
description = target->description;
}
// Determine whether this is a SCUMM game
// FIXME: This check is evil, as it requires us to hard code GIDs.
bool isScumm = (GID_SCUMM_FIRST <= target->id && target->id <= GID_SCUMM_LAST);
// Label & edit widget for the game ID
new StaticTextWidget(this, 10, 10, 40, kLineHeight, "ID: ", kTextAlignRight);
_domainWidget =
new EditTextWidget(this, 50, 10, _w - 50 - 10, kLineHeight, _domain);
// Label & edit widget for the description
new StaticTextWidget(this, 10, 26, 40, kLineHeight, "Name: ", kTextAlignRight);
_descriptionWidget =
new EditTextWidget(this, 50, 26, _w - 50 - 10, kLineHeight, description);
// Path to game data (view only)
String path(_config.get("path", _domain));
new StaticTextWidget(this, 10, 42, 40, kLineHeight, "Path: ", kTextAlignRight);
new StaticTextWidget(this, 50, 42, _w - 50 - 10, kLineHeight, path, kTextAlignLeft);
// Full screen checkbox
_fullscreenCheckbox = new CheckboxWidget(this, 15, 62, 200, 16, "Use Fullscreen Mode", 0, 'F');
_fullscreenCheckbox->setState(_config.getBool("fullscreen", false, _domain));
// Display 'Amiga' checkbox, but only for Scumm games.
if (isScumm) {
_amigaCheckbox = new CheckboxWidget(this, 15, 82, 200, 16, "Amiga Version", 0, 'A');
_amigaCheckbox->setState(_config.getBool("amiga", false, _domain));
} else {
_amigaCheckbox = 0;
}
// Add OK & Cancel buttons
addButton(_w - 2 * (kButtonWidth + 10), _h - 24, "Cancel", kCloseCmd, 0);
addButton(_w - (kButtonWidth + 10), _h - 24, "OK", kOKCmd, 0);
}
void EditGameDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
if (cmd == kOKCmd) {
// Write back changes made to config object
String newDomain(_domainWidget->getLabel());
if (newDomain != _domain) {
if (newDomain.isEmpty() || _config.has_domain(newDomain)) {
MessageDialog alert(_gui, "This game ID is already taken. Please choose another one.");
alert.runModal();
return;
}
_config.rename_domain(_domain, newDomain);
}
_config.set("description", _descriptionWidget->getLabel(), newDomain);
if (_amigaCheckbox)
_config.setBool("amiga", _amigaCheckbox->getState(), newDomain);
_config.setBool("fullscreen", _fullscreenCheckbox->getState(), newDomain);
setResult(1);
close();
} else {
Dialog::handleCommand(sender, cmd, data);
}
}
/*
* TODO list
* - add an text entry widget
* - add an "Add Game..." button that opens a dialog where new games can be
* configured and added to the list of games
* - add an "Edit Game..." button that opens a dialog that allows to edit game
* settings, i.e. the datapath/savepath/sound driver/... for that game
* - add an "options" dialog
* - ...
*/
LauncherDialog::LauncherDialog(NewGui *gui, GameDetector &detector)
: Dialog(gui, 0, 0, 320, 200), _detector(detector) {
// Show game name
new StaticTextWidget(this, 10, 8, 300, kLineHeight, gScummVMFullVersion, kTextAlignCenter);
// Add three buttons at the bottom
const int border = 10;
const int space = 8;
const int buttons = 4;
const int width = (_w - 2 * border - space * (buttons - 1)) / buttons;
int x = border;
new ButtonWidget(this, x, _h - 24, width, 16, "Quit", kQuitCmd, 'Q'); x += space + width;
new ButtonWidget(this, x, _h - 24, width, 16, "About", kAboutCmd, 'B'); x += space + width;
new ButtonWidget(this, x, _h - 24, width, 16, "Options", kOptionsCmd, 'O'); x += space + width;
_startButton =
new ButtonWidget(this, x, _h - 24, width, 16, "Start", kStartCmd, 'S'); x += space + width;
// Add list with game titles
_list = new ListWidget(this, 10, 28, 300, 112);
_list->setEditable(false);
_list->setNumberingMode(kListNumberingOff);
// Two more buttons directly below the list box
const int kBigButtonWidth = 90;
new ButtonWidget(this, 10, 144, kBigButtonWidth, 16, "Add Game...", kAddGameCmd, 'A');
_editButton = new ButtonWidget(this, (320-kBigButtonWidth) / 2, 144, kBigButtonWidth, 16, "Edit Game...", kEditGameCmd, 'E');
_removeButton = new ButtonWidget(this, 320-kBigButtonWidth - 10, 144, kBigButtonWidth, 16, "Remove Game", kRemoveGameCmd, 'R');
// Populate the list
updateListing();
// TODO - make a default selection (maybe the game user played last?)
//_list->setSelected(0);
// En-/Disable the buttons depending on the list selection
updateButtons();
// Create file browser dialog
_browser = new BrowserDialog(_gui, "Select directory with game data");
}
LauncherDialog::~LauncherDialog() {
delete _browser;
}
void LauncherDialog::open() {
Dialog::open();
g_config->set_writing(true);
}
void LauncherDialog::close() {
g_config->flush();
g_config->set_writing(false);
Dialog::close();
}
void LauncherDialog::updateListing() {
int i;
ScummVM::StringList l;
// Retrieve a list of all games defined in the config file
_domains.clear();
StringList domains = g_config->get_domains();
for (i = 0; i < domains.size(); i++) {
String name(g_config->get("gameid", domains[i]));
String description(g_config->get("description", domains[i]));
if (name.isEmpty())
name = domains[i];
if (description.isEmpty()) {
const TargetSettings *v = _detector.findTarget(name.c_str());
if (v && v->description)
description = v->description;
}
if (!name.isEmpty() && !description.isEmpty()) {
// Insert the game into the launcher list
int pos = 0, size = l.size();
while (pos < size && (description > l[pos]))
pos++;
l.insert_at(pos, description);
_domains.insert_at(pos, domains[i]);
}
}
_list->setList(l);
updateButtons();
}
/*
* Return a list of all games which might be the game in the specified directory.
*/
GameList findGame(FilesystemNode *dir) {
GameList list;
FSList *files = dir->listDir(FilesystemNode::kListFilesOnly);
const int size = files->size();
char detectName[128];
char detectName2[128];
char detectName3[128];
// Iterate over all known games and for each check if it might be
// the game in the presented directory.
assert(g_pluginManager);
const PluginList &plugins = g_pluginManager->getPlugins();
int p;
for (p = 0; p < plugins.size(); p++) {
const TargetSettings *v = plugins[p]->getTargets();
while (v->targetName && v->description) {
// Determine the 'detectname' for this game, that is, the name of a
// file that *must* be presented if the directory contains the data
// for this game. For example, FOA requires atlantis.000
if (v->detectname) {
strcpy(detectName, v->detectname);
strcpy(detectName2, v->detectname);
strcat(detectName2, ".");
detectName3[0] = '\0';
} else {
strcpy(detectName, v->targetName);
strcpy(detectName2, v->targetName);
strcpy(detectName3, v->targetName);
strcat(detectName, ".000");
if (v->version >= 7) {
strcat(detectName2, ".la0");
} else
strcat(detectName2, ".sm0");
strcat(detectName3, ".he0");
}
// Iterate over all files in the given directory
for (int i = 0; i < size; i++) {
const char *targetName = (*files)[i].displayName().c_str();
if ((0 == scumm_stricmp(detectName, targetName)) ||
(0 == scumm_stricmp(detectName2, targetName)) ||
(0 == scumm_stricmp(detectName3, targetName))) {
// Match found, add to list of candidates, then abort inner loop.
list.push_back(v);
break;
}
}
v++;
}
}
return list;
}
void LauncherDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
int item = _list->getSelected();
switch (cmd) {
case kAddGameCmd:
// 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 preent 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.
if (_browser->runModal()) {
// User made his choice...
FilesystemNode *dir = _browser->getResult();
// ...so let's determine a list of candidates, games that
// could be contained in the specified directory.
GameList candidates = findGame(dir);
const TargetSettings *v = 0;
if (candidates.isEmpty()) {
// No game was found in the specified directory
MessageDialog alert(_gui, "ScummVM could not find any game in the specified directory!");
alert.runModal();
} else if (candidates.size() == 1) {
// Exact match
v = candidates[0];
} else {
// Display the candidates to the user and let her/him pick one
StringList list;
int i;
for (i = 0; i < candidates.size(); i++)
list.push_back(candidates[i]->description);
ChooserDialog dialog(_gui, "Pick the game:", list);
i = dialog.runModal();
if (0 <= i && i < candidates.size())
v = candidates[i];
}
if (v != 0) {
// The auto detector or the user made a choice.
// Pick a domain name which does not yet exist (after all, we
// are *adding* a game to the config, not replacing).
String domain(v->targetName);
if (g_config->has_domain(domain)) {
char suffix = 'a';
domain += suffix;
while (g_config->has_domain(domain)) {
domain.deleteLastChar();
suffix++;
domain += suffix;
}
g_config->set("gameid", v->targetName, domain);
g_config->set("description", v->description, domain);
}
g_config->set("path", dir->path(), domain);
// Display edit dialog for the new entry
EditGameDialog editDialog(_gui, domain, v);
if (editDialog.runModal()) {
// User pressed OK, so make changes permanent
// Write config to disk
g_config->flush();
// Update the ListWidget and force a redraw
updateListing();
draw();
} else {
// User aborted, remove the the new domain again
g_config->delete_domain(domain);
}
}
}
break;
case kRemoveGameCmd:
// Remove the currently selected game from the list
assert(item >= 0);
g_config->delete_domain(_domains[item]);
// Write config to disk
g_config->flush();
// Update the ListWidget and force a redraw
updateListing();
draw();
break;
case kEditGameCmd: {
// Set game specifc 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);
const char *gameId = g_config->get("gameid", _domains[item]);
if (!gameId)
gameId = _domains[item].c_str();
EditGameDialog editDialog(_gui, _domains[item], _detector.findTarget(gameId));
if (editDialog.runModal()) {
// User pressed OK, so make changes permanent
// Write config to disk
g_config->flush();
// Update the ListWidget and force a redraw
updateListing();
draw();
}
}
break;
case kOptionsCmd: {
// TODO - show up a generic options dialog with global options, including:
// - the save path (use _browser!)
// - music & graphics driver (but see also the comments on EditGameDialog
// for some techincal difficulties with this)
// - default volumes (sfx/master/music)
GlobalOptionsDialog options(_gui, _detector);
options.runModal();
}
break;
case kAboutCmd: {
AboutDialog about(_gui);
about.runModal();
}
break;
case kStartCmd:
case kListItemDoubleClickedCmd:
// Print out what was selected
assert(item >= 0);
_detector.setGame(_domains[item]);
close();
break;
case kListSelectionChangedCmd:
updateButtons();
break;
case kQuitCmd:
#ifdef __PALM_OS__
close();
#endif
g_system->quit();
break;
default:
Dialog::handleCommand(sender, cmd, data);
}
}
void LauncherDialog::updateButtons() {
bool enable = (_list->getSelected() >= 0);
if (enable != _startButton->isEnabled()) {
_startButton->setEnabled(enable);
_startButton->draw();
}
if (enable != _editButton->isEnabled()) {
_editButton->setEnabled(enable);
_editButton->draw();
}
if (enable != _removeButton->isEnabled()) {
_removeButton->setEnabled(enable);
_removeButton->draw();
}
}