diff --git a/doomsday/client/client.pro b/doomsday/client/client.pro index 367f2a672d..da5eb57dd6 100644 --- a/doomsday/client/client.pro +++ b/doomsday/client/client.pro @@ -404,6 +404,7 @@ DENG_HEADERS += \ include/ui/widgets/mpselectionwidget.h \ include/ui/widgets/multiplayermenuwidget.h \ include/ui/widgets/profilepickerwidget.h \ + include/ui/widgets/savegameselectionwidget.h \ include/ui/widgets/taskbarwidget.h \ include/ui/widgets/tutorialwidget.h \ include/ui/fi_main.h \ @@ -741,6 +742,7 @@ SOURCES += \ src/ui/widgets/mpselectionwidget.cpp \ src/ui/widgets/multiplayermenuwidget.cpp \ src/ui/widgets/profilepickerwidget.cpp \ + src/ui/widgets/savegameselectionwidget.cpp \ src/ui/widgets/taskbarwidget.cpp \ src/ui/widgets/tutorialwidget.cpp \ src/ui/zonedebug.cpp \ diff --git a/doomsday/client/include/ui/widgets/savegameselectionwidget.h b/doomsday/client/include/ui/widgets/savegameselectionwidget.h new file mode 100644 index 0000000000..aea5c4879e --- /dev/null +++ b/doomsday/client/include/ui/widgets/savegameselectionwidget.h @@ -0,0 +1,78 @@ +/** @file savegameselectionwidget.h + * + * @authors Copyright © 2014 Jaakko Keränen + * @authors Copyright © 2014 Daniel Swanson + * + * @par License + * GPL: http://www.gnu.org/licenses/gpl.html + * + * 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, see: + * http://www.gnu.org/licenses + */ + +#ifndef DENG_CLIENT_SAVEGAMESELECTIONWIDGET_H +#define DENG_CLIENT_SAVEGAMESELECTIONWIDGET_H + +#include +#include + +/** + * Menu that populates itself with available saved game sessions. + * + * @ingroup ui + */ +class SavegameSelectionWidget : public de::MenuWidget +{ + Q_OBJECT + +public: + DENG2_DEFINE_AUDIENCE(Selection, void gameSelected(de::game::SavedSession const &session)) + + /** + * Action for loading a saved session. + */ + class LoadAction : public de::Action + { + public: + LoadAction(de::game::SavedSession const &session); + void trigger(); + + private: + DENG2_PRIVATE(d) + }; + +public: + SavegameSelectionWidget(); + + /** + * Enables or disables loading saved game sessions by pressing the menu items in the + * widget. By default, this is enabled. If disabled, one will only get a notification + * about the selection. + * + * @param enableLoad @c true to allow automatic loading, @c false to disallow. + */ + void setLoadGameWhenSelected(bool enableLoad); + + void setColumns(int numberOfColumns); + + de::game::SavedSession const &savedSession(de::ui::DataPos pos) const; + + // Events. + void update(); + +signals: + void availabilityChanged(); + void gameSelected(); + +private: + DENG2_PRIVATE(d) +}; + +#endif // DENG_CLIENT_SAVEGAMESELECTIONWIDGET_H diff --git a/doomsday/client/src/game.cpp b/doomsday/client/src/game.cpp index 59ba29cba4..5fbd74a305 100644 --- a/doomsday/client/src/game.cpp +++ b/doomsday/client/src/game.cpp @@ -25,6 +25,7 @@ #include "filesys/manifest.h" #include +#include #include #include #include diff --git a/doomsday/client/src/ui/widgets/gameselectionwidget.cpp b/doomsday/client/src/ui/widgets/gameselectionwidget.cpp index c4c85f8f47..4102847ea8 100644 --- a/doomsday/client/src/ui/widgets/gameselectionwidget.cpp +++ b/doomsday/client/src/ui/widgets/gameselectionwidget.cpp @@ -1,6 +1,7 @@ /** @file gameselectionwidget.cpp * - * @authors Copyright (c) 2013 Jaakko Keränen + * @authors Copyright © 2013-2014 Jaakko Keränen + * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html @@ -20,6 +21,7 @@ #include "ui/widgets/gamesessionwidget.h" #include "ui/widgets/mpselectionwidget.h" #include "ui/widgets/gamefilterwidget.h" +#include "ui/widgets/savegameselectionwidget.h" #include "CommandAction" #include "clientapp.h" #include "games.h" @@ -79,7 +81,8 @@ DENG_GUI_PIMPL(GameSelectionWidget) { enum Type { NormalGames, - MultiplayerGames + MultiplayerGames, + SavedGames }; String titleText; @@ -113,6 +116,12 @@ DENG_GUI_PIMPL(GameSelectionWidget) QObject::connect(menu, SIGNAL(gameSelected()), owner->thisPublic, SIGNAL(gameSessionSelected())); QObject::connect(menu, SIGNAL(availabilityChanged()), owner->thisPublic, SLOT(updateSubsetLayout())); break; + + case SavedGames: + menu = new SavegameSelectionWidget; + QObject::connect(menu, SIGNAL(gameSelected()), owner->thisPublic, SIGNAL(gameSessionSelected())); + QObject::connect(menu, SIGNAL(availabilityChanged()), owner->thisPublic, SLOT(updateSubsetLayout())); + break; } menu->items().audienceForAddition() += this; @@ -210,6 +219,7 @@ DENG_GUI_PIMPL(GameSelectionWidget) SubsetWidget *available; SubsetWidget *incomplete; SubsetWidget *multi; + SubsetWidget *saved; QList subsets; // not owned Instance(Public *i) @@ -229,8 +239,12 @@ DENG_GUI_PIMPL(GameSelectionWidget) self.add(multi = new SubsetWidget("multi", SubsetWidget::MultiplayerGames, tr("Multiplayer Games"), this)); + // Menu of saved games. + self.add(saved = new SubsetWidget("saved", SubsetWidget::SavedGames, + tr("Saved Games"), this)); + // Keep all sets in a handy list. - subsets << available << incomplete << multi; + subsets << available << incomplete << multi << saved; self.add(filter = new GameFilterWidget); @@ -263,6 +277,9 @@ DENG_GUI_PIMPL(GameSelectionWidget) incomplete->show(sp); incomplete->title().show(sp); + saved->show(sp); + saved->title().show(sp); + multi->show(mp); multi->title().show(mp); } @@ -278,11 +295,11 @@ DENG_GUI_PIMPL(GameSelectionWidget) QList order; if(!App_GameLoaded()) { - order << available << multi << incomplete; + order << available << multi << saved << incomplete; } else { - order << multi << available << incomplete; + order << saved << multi << available << incomplete; } updateSubsetVisibility(); @@ -293,6 +310,7 @@ DENG_GUI_PIMPL(GameSelectionWidget) { order.removeOne(available); order.removeOne(incomplete); + order.removeOne(saved); } if(!flt.testFlag(GameFilterWidget::Multiplayer)) { diff --git a/doomsday/client/src/ui/widgets/savegameselectionwidget.cpp b/doomsday/client/src/ui/widgets/savegameselectionwidget.cpp new file mode 100644 index 0000000000..1bf3ece32e --- /dev/null +++ b/doomsday/client/src/ui/widgets/savegameselectionwidget.cpp @@ -0,0 +1,285 @@ +/** @file savegameselectionwidget.cpp + * + * @authors Copyright © 2014 Jaakko Keränen + * @authors Copyright © 2014 Daniel Swanson + * + * @par License + * GPL: http://www.gnu.org/licenses/gpl.html + * + * 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, see: + * http://www.gnu.org/licenses + */ + +#include "ui/widgets/savegameselectionwidget.h" +#include "ui/widgets/taskbarwidget.h" +#include "ui/widgets/gamesessionwidget.h" +#include "clientapp.h" +#include "dd_main.h" +#include "con_main.h" + +#include +#include +#include +#include +#include +#include + +using namespace de; +using de::game::SavedSession; +using de::game::SavedSessionRepository; + +DENG_GUI_PIMPL(SavegameSelectionWidget) +, DENG2_OBSERVES(App, StartupComplete) +, DENG2_OBSERVES(SavedSessionRepository, AvailabilityUpdate) +, DENG2_OBSERVES(ButtonWidget, Press) +, public ChildWidgetOrganizer::IWidgetFactory +{ + /** + * Data item with information about a saved game session. + */ + class SavegameListItem : public ui::Item + { + public: + SavegameListItem(SavedSession const &session) + { + setData(session.path().fileNameAndPathWithoutExtension()); + _session = &session; + } + + SavedSession const &savedSession() const + { + DENG2_ASSERT(_session != 0); + return *_session; + } + + private: + SavedSession const *_session; + }; + + /** + * Widget representing a SavegameItem in the dialog's menu. + */ + struct SavegameWidget : public GameSessionWidget + { + SavegameWidget() + { + loadButton().disable(); + loadButton().setHeightPolicy(ui::Expand); + } + + void updateFromItem(SavegameListItem const &item) + { + try + { + SavedSession const &ss = item.savedSession(); + + Game const &ssGame = App_Games().byIdentityKey(ss.metadata().gets("gameIdentityKey", "")); + if(style().images().has(ssGame.logoImageId())) + { + loadButton().setImage(style().images().image(ssGame.logoImageId())); + } + + loadButton().enable(ssGame.status() == Game::Loaded || + ssGame.status() == Game::Complete); + if(loadButton().isEnabled()) + { + loadButton().setAction(new LoadAction(ss)); + } + + loadButton().setText(String(_E(b) "%1" _E(.) "\n" _E(l)_E(D) "%2") + .arg(ss.metadata().gets("userDescription")) + .arg(ssGame.identityKey())); + + // Extra information. + document().setText(ss.description()); + } + catch(Error const &) + { + /// @todo + } + } + }; + + bool loadWhenSelected; + bool needUpdateFromRepository; + + Instance(Public *i) + : Base(i) + , loadWhenSelected(true) + , needUpdateFromRepository(false) + { + self.organizer().setWidgetFactory(*this); + + App::app().audienceForStartupComplete() += this; + ClientApp::resourceSystem().savedSessionRepository().audienceForAvailabilityUpdate() += this; + } + + ~Instance() + { + App::app().audienceForStartupComplete() -= this; + ClientApp::resourceSystem().savedSessionRepository().audienceForAvailabilityUpdate() -= this; + } + + GuiWidget *makeItemWidget(ui::Item const & /*item*/, GuiWidget const *) + { + SavegameWidget *w = new SavegameWidget; + w->loadButton().audienceForPress() += this; + w->rule().setInput(Rule::Height, w->loadButton().rule().height()); + + // Automatically close the info popup if the dialog is closed. + //QObject::connect(thisPublic, SIGNAL(closed()), w->info, SLOT(close())); + + return w; + } + + void updateItemWidget(GuiWidget &widget, ui::Item const &item) + { + SavegameWidget &w = widget.as(); + w.updateFromItem(item.as()); + + if(!loadWhenSelected) + { + // Only send notification. + w.loadButton().setAction(0); + } + } + + void buttonPressed(ButtonWidget &loadButton) + { + if(SavegameListItem const *it = self.organizer().findItemForWidget( + loadButton.parentWidget()->as())->maybeAs()) + { + DENG2_FOR_PUBLIC_AUDIENCE(Selection, i) + { + i->gameSelected(it->savedSession()); + } + } + + // A load button has been pressed. + emit self.gameSelected(); + } + + void appStartupCompleted() + { + // Startup resources for all games have been located. + // We can now determine which of the saved sessions are loadable. + for(ui::Data::Pos idx = 0; idx < self.items().size(); ++idx) + { + ui::Item const &item = self.items().at(idx); + updateItemWidget(*self.organizer().itemWidget(item), item); + } + } + + void updateItemsFromRepository() + { + SavedSessionRepository const &repository = ClientApp::resourceSystem().savedSessionRepository(); + bool changed = false; + + // Remove obsolete entries. + for(ui::Data::Pos idx = 0; idx < self.items().size(); ++idx) + { + String const fileName = self.items().at(idx).data().toString(); + if(!repository.has(fileName)) + { + self.items().remove(idx--); + changed = true; + } + } + + // Add new entries. + DENG2_FOR_EACH_CONST(SavedSessionRepository::All, i, repository.all()) + { + SavedSession const &session = *i->second; + ui::Data::Pos found = self.items().findData(session.path().fileNameAndPathWithoutExtension()); + if(found == ui::Data::InvalidPos) + { + // Needs to be added. + self.items().append(new SavegameListItem(session)); + changed = true; + } + } + + if(changed) + { + // Let others know that one or more games have appeared or disappeared from the menu. + emit self.availabilityChanged(); + } + } + + void repositoryAvailabilityUpdate(SavedSessionRepository const &) + { + if(!App::inMainThread()) + { + // We'll have to defer the update for now. + needUpdateFromRepository = true; + return; + } + updateItemsFromRepository(); + } +}; + +SavegameSelectionWidget::SavegameSelectionWidget() + : MenuWidget("savegame-selection"), d(new Instance(this)) +{ + setGridSize(3, ui::Filled, 0, ui::Expand); + d->needUpdateFromRepository = true; +} + +void SavegameSelectionWidget::setLoadGameWhenSelected(bool enableLoad) +{ + d->loadWhenSelected = enableLoad; +} + +void SavegameSelectionWidget::setColumns(int numberOfColumns) +{ + if(layout().maxGridSize().x != numberOfColumns) + { + setGridSize(numberOfColumns, ui::Filled, 0, ui::Expand); + } +} + +void SavegameSelectionWidget::update() +{ + if(d->needUpdateFromRepository) + { + d->updateItemsFromRepository(); + } + MenuWidget::update(); +} + +SavedSession const &SavegameSelectionWidget::savedSession(ui::DataPos pos) const +{ + DENG2_ASSERT(pos < items().size()); + return items().at(pos).as().savedSession(); +} + +DENG2_PIMPL_NOREF(SavegameSelectionWidget::LoadAction) +{ + String gameId; + String cmd; +}; + +SavegameSelectionWidget::LoadAction::LoadAction(SavedSession const &session) + : d(new Instance) +{ + d->gameId = session.metadata().gets("gameIdentityKey"); + d->cmd = "loadgame " + session.path().fileNameWithoutExtension() + " confirm"; +} + +void SavegameSelectionWidget::LoadAction::trigger() +{ + Action::trigger(); + + BusyMode_FreezeGameForBusyMode(); + ClientWindow::main().taskBar().close(); + + App_ChangeGame(App_Games().byIdentityKey(d->gameId), false /*no reload*/); + Con_Execute(CMDS_DDAY, d->cmd.toLatin1(), false, false); +}