From faf46547ac41de3e3fb765d7296363fb4b014204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaakko=20Ker=C3=A4nen?= Date: Sun, 27 Mar 2016 16:33:44 +0300 Subject: [PATCH] UI|Home: Creating and editing game profiles Basic functionality of adding profiles, editing them, and deleting profiles via the right-click context menu. --- .../include/ui/dialogs/createprofiledialog.h | 52 ++++++ .../include/ui/widgets/homeitemwidget.h | 1 + .../include/ui/widgets/packagesbuttonwidget.h | 47 +++++ .../src/ui/dialogs/createprofiledialog.cpp | 155 ++++++++++++++++ .../client/src/ui/home/gamecolumnwidget.cpp | 168 ++++++++++++++++-- .../src/ui/home/gamepanelbuttonwidget.cpp | 69 +++---- .../client/src/ui/widgets/homeitemwidget.cpp | 36 +++- .../src/ui/widgets/packagesbuttonwidget.cpp | 109 ++++++++++++ 8 files changed, 574 insertions(+), 63 deletions(-) create mode 100644 doomsday/apps/client/include/ui/dialogs/createprofiledialog.h create mode 100644 doomsday/apps/client/include/ui/widgets/packagesbuttonwidget.h create mode 100644 doomsday/apps/client/src/ui/dialogs/createprofiledialog.cpp create mode 100644 doomsday/apps/client/src/ui/widgets/packagesbuttonwidget.cpp diff --git a/doomsday/apps/client/include/ui/dialogs/createprofiledialog.h b/doomsday/apps/client/include/ui/dialogs/createprofiledialog.h new file mode 100644 index 0000000000..5f4f4e7fb7 --- /dev/null +++ b/doomsday/apps/client/include/ui/dialogs/createprofiledialog.h @@ -0,0 +1,52 @@ +/** @file + * + * @authors Copyright (c) 2016 Jaakko Keränen + * + * @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_UI_CREATEPROFILEDIALOG_H +#define DENG_CLIENT_UI_CREATEPROFILEDIALOG_H + +#include +#include + +/** + * Dialog for creating a game profile. + */ +class CreateProfileDialog : public de::InputDialog +{ +public: + CreateProfileDialog(de::String const &gameFamily); + + /** + * Creates a new profile based on the dialog's current selections. + * @return New game profile. Caller gets ownership. + */ + GameProfile *makeProfile() const; + + void fetchFrom(GameProfile const &profile); + + void applyTo(GameProfile &profile) const; + + de::String profileName() const; + + static CreateProfileDialog *editProfile(de::String const &gameFamily, + GameProfile &profile); + +private: + DENG2_PRIVATE(d) +}; + +#endif // DENG_CLIENT_UI_CREATEPROFILEDIALOG_H diff --git a/doomsday/apps/client/include/ui/widgets/homeitemwidget.h b/doomsday/apps/client/include/ui/widgets/homeitemwidget.h index 745079bd58..b4a63b8d6f 100644 --- a/doomsday/apps/client/include/ui/widgets/homeitemwidget.h +++ b/doomsday/apps/client/include/ui/widgets/homeitemwidget.h @@ -48,6 +48,7 @@ class HomeItemWidget : public de::GuiWidget signals: void mouseActivity(); void doubleClicked(); + void openContextMenu(); private: DENG2_PRIVATE(d) diff --git a/doomsday/apps/client/include/ui/widgets/packagesbuttonwidget.h b/doomsday/apps/client/include/ui/widgets/packagesbuttonwidget.h new file mode 100644 index 0000000000..52cf80df22 --- /dev/null +++ b/doomsday/apps/client/include/ui/widgets/packagesbuttonwidget.h @@ -0,0 +1,47 @@ +/** @file packagesbuttonwidget.h + * + * @authors Copyright (c) 2016 Jaakko Keränen + * + * @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_UI_PACKAGESBUTTONWIDGET_H +#define DENG_CLIENT_UI_PACKAGESBUTTONWIDGET_H + +#include +#include + +/** + * Button for selecting packages. + */ +class PackagesButtonWidget : public de::ButtonWidget +{ + Q_OBJECT + +public: + PackagesButtonWidget(); + + void setNoneLabel(de::String const &noneLabel); + void setDialogTitle(de::String const &title); + void setPackages(de::StringList const &packageIds); + de::StringList packages() const; + +signals: + void packageSelectionChanged(QStringList packageIds); + +private: + DENG2_PRIVATE(d) +}; + +#endif // DENG_CLIENT_UI_PACKAGESBUTTONWIDGET_H diff --git a/doomsday/apps/client/src/ui/dialogs/createprofiledialog.cpp b/doomsday/apps/client/src/ui/dialogs/createprofiledialog.cpp new file mode 100644 index 0000000000..c9f526f586 --- /dev/null +++ b/doomsday/apps/client/src/ui/dialogs/createprofiledialog.cpp @@ -0,0 +1,155 @@ +/** @file createprofiledialog.cpp + * + * @authors Copyright (c) 2016 Jaakko Keränen + * + * @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/dialogs/createprofiledialog.h" +#include "ui/widgets/packagesbuttonwidget.h" + +#include +#include +#include + +#include +#include +#include + +using namespace de; + +DENG_GUI_PIMPL(CreateProfileDialog) +{ + ChoiceWidget *gameChoice; + PackagesButtonWidget *packages; + DialogContentStylist stylist; + bool editing = false; + String oldName; + + Instance(Public *i) : Base(i) {} + + void checkValidProfileName() + { + bool valid = false; + String const entry = self.profileName(); + if(!entry.isEmpty()) + { + if(editing && oldName == entry) + { + valid = true; + } + else + { + // Must be a new, unique name. + valid = DoomsdayApp::gameProfiles().forAll([this, &entry] (GameProfile &prof) { + if(!entry.compareWithoutCase(prof.name())) { + return LoopAbort; + } + return LoopContinue; + }) == LoopContinue; + } + } + self.buttonWidget(Id1)->enable(valid); + } +}; + +CreateProfileDialog::CreateProfileDialog(String const &gameFamily) + : InputDialog("create-profile") + , d(new Instance(this)) +{ + title() .setText(tr("New Profile")); + message().setText(tr("Enter a name for the new game profile. Only unique names are allowed.")); + + auto *form = new GuiWidget; + d->stylist.setContainer(*form); + area().add(form); + + // Populate games list. + form->add(d->gameChoice = new ChoiceWidget); + DoomsdayApp::games().forAll([this, &gameFamily] (Game &game) + { + if(game.family() == gameFamily) + { + d->gameChoice->items() << new ChoiceItem(game.title(), game.id()); + } + return LoopContinue; + }); + d->gameChoice->items().sort(); + + // Packages selection. + form->add(d->packages = new PackagesButtonWidget); + d->packages->setNoneLabel(tr("None")); + + GridLayout layout(form->rule().left(), form->rule().top() + rule("dialog.gap")); + layout.setGridSize(2, 0); + layout.setColumnAlignment(0, ui::AlignRight); + layout << *LabelWidget::newWithText(tr("Game:"), form) + << *d->gameChoice + << *LabelWidget::newWithText(tr("Packages:"), form) + << *d->packages; + + form->rule().setSize(layout.width(), layout.height()); + + buttons().clear() + << new DialogButtonItem(Id1 | Default | Accept, tr("Create")) + << new DialogButtonItem(Reject); + + // The Create button is enabled when a valid name is entered. + buttonWidget(Id1)->disable(); + + updateLayout(); + + connect(&editor(), &LineEditWidget::editorContentChanged, + [this] () { d->checkValidProfileName(); }); +} + +GameProfile *CreateProfileDialog::makeProfile() const +{ + auto *prof = new GameProfile(profileName()); + prof->setUserCreated(true); + applyTo(*prof); + return prof; +} + +void CreateProfileDialog::fetchFrom(GameProfile const &profile) +{ + editor().setText(profile.name()); + d->gameChoice->setSelected(d->gameChoice->items().findData(profile.game())); + d->packages->setPackages(profile.packages()); +} + +void CreateProfileDialog::applyTo(GameProfile &profile) const +{ + profile.setName(profileName()); + profile.setGame(d->gameChoice->selectedItem().data().toString()); + profile.setPackages(d->packages->packages()); +} + +String CreateProfileDialog::profileName() const +{ + return editor().text().strip(); +} + +CreateProfileDialog *CreateProfileDialog::editProfile(String const &gameFamily, + GameProfile &profile) +{ + auto *dlg = new CreateProfileDialog(gameFamily); + dlg->d->editing = true; + dlg->d->oldName = profile.name(); + dlg->title() .setText(tr("Edit Profile")); + dlg->message().setText(tr("Game profile name:")); + dlg->buttonWidget(Id1)->setText(_E(b) + tr("OK")); + dlg->fetchFrom(profile); + return dlg; +} diff --git a/doomsday/apps/client/src/ui/home/gamecolumnwidget.cpp b/doomsday/apps/client/src/ui/home/gamecolumnwidget.cpp index 6c49e498a8..5d0308f546 100644 --- a/doomsday/apps/client/src/ui/home/gamecolumnwidget.cpp +++ b/doomsday/apps/client/src/ui/home/gamecolumnwidget.cpp @@ -20,6 +20,7 @@ #include "ui/home/headerwidget.h" #include "ui/home/gamepanelbuttonwidget.h" #include "ui/widgets/homemenuwidget.h" +#include "ui/dialogs/createprofiledialog.h" #include #include @@ -28,6 +29,9 @@ #include #include #include +#include +#include +#include #include #include @@ -37,17 +41,20 @@ DENG_GUI_PIMPL(GameColumnWidget) , DENG2_OBSERVES(Games, Readiness) , DENG2_OBSERVES(Profiles, Addition) , DENG2_OBSERVES(Variable, Change) +, DENG2_OBSERVES(ButtonWidget, StateChange) , public ChildWidgetOrganizer::IWidgetFactory { - // Item for a particular loadable game. - struct MenuItem + /** + * Menu item for a game profile. + */ + struct ProfileItem : public ui::Item - , DENG2_OBSERVES(Deletable, Deletion) + , DENG2_OBSERVES(Deletable, Deletion) // profile deletion { GameColumnWidget::Instance *d; GameProfile *profile; - MenuItem(GameColumnWidget::Instance *d, GameProfile &gameProfile) + ProfileItem(GameColumnWidget::Instance *d, GameProfile &gameProfile) : d(d) , profile(&gameProfile) { @@ -55,7 +62,7 @@ DENG_GUI_PIMPL(GameColumnWidget) profile->audienceForDeletion += this; } - ~MenuItem() + ~ProfileItem() { if(profile) profile->audienceForDeletion -= this; } @@ -87,6 +94,7 @@ DENG_GUI_PIMPL(GameColumnWidget) String gameFamily; SavedSessionListData const &savedItems; HomeMenuWidget *menu; + ButtonWidget *newProfileButton; int restoredSelected = -1; Instance(Public *i, @@ -100,11 +108,40 @@ DENG_GUI_PIMPL(GameColumnWidget) area.add(menu = new HomeMenuWidget); menu->organizer().setWidgetFactory(*this); - menu->rule() .setInput(Rule::Width, area.contentRule().width()) .setInput(Rule::Left, area.contentRule().left()) .setInput(Rule::Top, self.header().rule().bottom()); + menu->margins().setBottom(""); + + area.add(newProfileButton = new ButtonWidget); + newProfileButton->audienceForStateChange() += this; + newProfileButton->setText(tr("New Profile...")); + newProfileButton->setImage(new StyleProceduralImage("create", *newProfileButton)); + newProfileButton->setOverrideImageSize(style().fonts().font("default").height().value() * 1.5f); + newProfileButton->set(Background()); + newProfileButton->setSizePolicy(ui::Filled, ui::Expand); + newProfileButton->setTextAlignment(ui::AlignRight); + newProfileButton->setOpacity(actionOpacity()); + newProfileButton->setActionFn([this] () + { + auto *dlg = new CreateProfileDialog(this->gameFamily); + dlg->setDeleteAfterDismissed(true); + dlg->setAnchorAndOpeningDirection(newProfileButton->rule(), ui::Up); + if(dlg->exec(root())) + { + // Adding the profile has the side effect that a widget is + // created for it. + DoomsdayApp::gameProfiles().add(dlg->makeProfile()); + + sortItems(); + //menu->setSelectedIndex(menu->childCount() - 1); // depends on "New Profile" being at the end + } + }); + newProfileButton->rule() + .setInput(Rule::Left, area.contentRule().left()) + .setInput(Rule::Width, area.contentRule().width()) + .setInput(Rule::Top, menu->rule().bottom()); DoomsdayApp::games().audienceForReadiness() += this; DoomsdayApp::gameProfiles().audienceForAddition() += this; @@ -138,7 +175,7 @@ DENG_GUI_PIMPL(GameColumnWidget) { if(games[profile.game()].family() == gameFamily) { - menu->items() << new MenuItem(this, profile); + menu->items() << new ProfileItem(this, profile); } } } @@ -171,9 +208,27 @@ DENG_GUI_PIMPL(GameColumnWidget) { menu->items().sort([] (ui::Item const &a, ui::Item const &b) { - // Sorting by game release date. - return a.as().game().releaseDate().year() < - b.as().game().releaseDate().year(); + GameProfile const &prof1 = *a.as().profile; + GameProfile const &prof2 = *b.as().profile; + + // User-created profiles in the end. + if(prof1.isUserCreated() && !prof2.isUserCreated()) + { + return false; + } + if(!prof1.isUserCreated() && prof2.isUserCreated()) + { + return true; + } + if(prof1.isUserCreated() && prof2.isUserCreated()) + { + // Sorted alphabetically. + return prof1.name().compareWithoutCase(prof2.name()) < 0; + } + + // Sort games by release date. + return a.as().game().releaseDate().year() < + b.as().game().releaseDate().year(); }); } @@ -181,7 +236,7 @@ DENG_GUI_PIMPL(GameColumnWidget) { menu->items().forAll([] (ui::Item const &item) { - item.as().update(); + item.as().update(); return LoopContinue; }); menu->updateLayout(); @@ -189,7 +244,6 @@ DENG_GUI_PIMPL(GameColumnWidget) void gameReadinessUpdated() { - //updateItems(); populateItems(); // Restore earlier selection? @@ -209,7 +263,58 @@ DENG_GUI_PIMPL(GameColumnWidget) GuiWidget *makeItemWidget(ui::Item const &item, GuiWidget const *) { - auto *button = new GamePanelButtonWidget(*item.as().profile, savedItems); + auto const *profileItem = &item.as(); + auto *button = new GamePanelButtonWidget(*profileItem->profile, savedItems); + + // Right-clicking on game items shows additional functions. + QObject::connect(button, &HomeItemWidget::openContextMenu, [this, button, profileItem] () + { + auto *popup = new PopupMenuWidget; + button->add(popup); + popup->setDeleteAfterDismissed(true); + popup->setAnchorAndOpeningDirection(button->rule(), ui::Down); + + // Items suitable for all types of profiles. + popup->items() + << new ui::ActionItem(tr("Clear Packages"), new CallbackAction([this, profileItem] () + { + profileItem->profile->setPackages(StringList()); + profileItem->update(); + })); + + // Items for user profiles. + if(profileItem->profile->isUserCreated()) + { + auto *deleteSub = new ui::SubmenuItem(style().images().image("close.ring"), + tr("Delete"), ui::Left); + deleteSub->items() + << new ui::Item(ui::Item::Separator, tr("Are you sure?")) + << new ui::ActionItem(tr("Delete Profile"), + new CallbackAction([this, profileItem, popup] () + { + popup->detachAnchor(); + delete profileItem->profile; + })) + << new ui::ActionItem(tr("Cancel"), new Action); + + popup->items() + << new ui::Item(ui::Item::Separator) + << new ui::ActionItem(tr("Edit..."), new CallbackAction([this, button, profileItem] () + { + auto *dlg = CreateProfileDialog::editProfile(gameFamily, *profileItem->profile); + dlg->setAnchorAndOpeningDirection(button->rule(), ui::Up); + dlg->setDeleteAfterDismissed(true); + if(dlg->exec(root())) + { + dlg->applyTo(*profileItem->profile); + profileItem->update(); + } + })) + << deleteSub; + } + popup->open(); + }); + return button; } @@ -220,13 +325,44 @@ DENG_GUI_PIMPL(GameColumnWidget) if(!App::config().getb("home.showUnplayableGames")) { - drawer.show(item.as().game().isPlayable()); + drawer.show(item.as().game().isPlayable()); } else { drawer.show(); } } + +//- Actions ----------------------------------------------------------------------------- + + float actionOpacity() const + { + return self.isHighlighted()? .4f : 0.f; + } + + void buttonStateChanged(ButtonWidget &button, ButtonWidget::State state) + { + TimeDelta const SPAN = 0.25; + switch(state) + { + case ButtonWidget::Up: + button.setOpacity(actionOpacity(), SPAN); + break; + + case ButtonWidget::Hover: + button.setOpacity(.8f, SPAN); + break; + + case ButtonWidget::Down: + button.setOpacity(1); + break; + } + } + + void showActions(bool show) + { + newProfileButton->setOpacity(show? actionOpacity() : 0, 0.6); + } }; GameColumnWidget::GameColumnWidget(String const &gameFamily, @@ -238,7 +374,8 @@ GameColumnWidget::GameColumnWidget(String const &gameFamily, scrollArea().setContentSize(maximumContentWidth(), header().rule().height() + rule("gap") + - d->menu->rule().height()); + d->menu->rule().height() + + d->newProfileButton->rule().height()); header().title().setText(String(_E(s) "%1\n" _E(.)_E(w) "%2") .arg( gameFamily == "DOOM"? "id Software" : @@ -316,6 +453,7 @@ void GameColumnWidget::setHighlighted(bool highlighted) { d->menu->unselectAll(); } + d->showActions(highlighted); } void GameColumnWidget::operator >> (PersistentState &toState) const diff --git a/doomsday/apps/client/src/ui/home/gamepanelbuttonwidget.cpp b/doomsday/apps/client/src/ui/home/gamepanelbuttonwidget.cpp index b2b2866749..537b103269 100644 --- a/doomsday/apps/client/src/ui/home/gamepanelbuttonwidget.cpp +++ b/doomsday/apps/client/src/ui/home/gamepanelbuttonwidget.cpp @@ -20,6 +20,7 @@ #include "ui/home/savelistwidget.h" #include "ui/savedsessionlistdata.h" #include "ui/dialogs/packagesdialog.h" +#include "ui/widgets/packagesbuttonwidget.h" #include "dd_main.h" #include @@ -40,7 +41,7 @@ DENG_GUI_PIMPL(GamePanelButtonWidget) Game const &game; SavedSessionListData const &savedItems; SaveListWidget *saves; - ButtonWidget *packagesButton; + PackagesButtonWidget *packagesButton; ButtonWidget *playButton; ButtonWidget *deleteSaveButton; @@ -50,15 +51,19 @@ DENG_GUI_PIMPL(GamePanelButtonWidget) , game(DoomsdayApp::games()[profile.game()]) , savedItems(savedItems) { - packagesButton = new ButtonWidget; - packagesButton->setImage(new StyleProceduralImage("package", self)); - packagesButton->setOverrideImageSize(style().fonts().font("default").height().value()); - packagesButton->setSizePolicy(ui::Expand, ui::Expand); - packagesButton->setTextAlignment(ui::AlignLeft); - packagesButton->setActionFn([this] () { packagesButtonPressed(); }); - updatePackagesButton(); + packagesButton = new PackagesButtonWidget; + packagesButton->setDialogTitle(profile.name()); self.addButton(packagesButton); + QObject::connect(packagesButton, + &PackagesButtonWidget::packageSelectionChanged, + [this] (QStringList ids) + { + StringList pkgs; + for(auto const &i : ids) pkgs << i; + gameProfile.setPackages(pkgs); + }); + playButton = new ButtonWidget; playButton->useInfoStyle(); playButton->setImage(new StyleProceduralImage("play", self)); @@ -87,38 +92,6 @@ DENG_GUI_PIMPL(GamePanelButtonWidget) self.panel().open(); } - void packagesButtonPressed() - { - // The Packages dialog allows selecting which packages are loaded, and in - // which order. One can also browse the available packages. - auto *dlg = new PackagesDialog(game.title()); - dlg->setDeleteAfterDismissed(true); - dlg->setSelectedPackages(gameProfile.packages()); - dlg->setAcceptanceAction(new CallbackAction([this, dlg] () - { - gameProfile.setPackages(dlg->selectedPackages()); - updatePackagesButton(); - })); - root().addOnTop(dlg); - dlg->open(); - } - - void updatePackagesButton() - { - if(gameProfile.packages().isEmpty()) - { - packagesButton->setText(""); - packagesButton->setTextColor("text"); - packagesButton->setImageColor(style().colors().colorf("text")); - } - else - { - packagesButton->setText(String::format("%i", gameProfile.packages().count())); - packagesButton->setTextColor("accent"); - packagesButton->setImageColor(style().colors().colorf("accent")); - } - } - void playButtonPressed() { BusyMode_FreezeGameForBusyMode(); @@ -138,7 +111,8 @@ DENG_GUI_PIMPL(GamePanelButtonWidget) } /// Action that deletes a savegame folder. - struct DeleteAction : public Action { + struct DeleteAction : public Action + { GamePanelButtonWidget *widget; String savePath; DeleteAction(GamePanelButtonWidget *wgt, String const &path) @@ -179,6 +153,9 @@ DENG_GUI_PIMPL(GamePanelButtonWidget) bool isItemAccepted(ChildWidgetOrganizer const &, ui::Data const &data, ui::Data::Pos pos) const { + // User-created profiles currently have no saves associated with them. + if(gameProfile.isUserCreated()) return false; + // Only saved sessions for this game are to be included. auto const &item = data.at(pos).as(); return item.gameId() == game.id(); @@ -202,15 +179,17 @@ void GamePanelButtonWidget::setSelected(bool selected) if(!selected) { unselectSave(); - updateContent(); } + + updateContent(); } void GamePanelButtonWidget::updateContent() { enable(d->game.isPlayable()); - String meta = String::number(d->game.releaseDate().year()); + String meta = !d->gameProfile.isUserCreated()? String::number(d->game.releaseDate().year()) + : d->game.title(); if(isSelected()) { @@ -231,8 +210,10 @@ void GamePanelButtonWidget::updateContent() } label().setText(String(_E(b) "%1\n" _E(l) "%2") - .arg(d->game.title()) + .arg(d->gameProfile.name()) .arg(meta)); + + d->packagesButton->setPackages(d->gameProfile.packages()); } void GamePanelButtonWidget::unselectSave() diff --git a/doomsday/apps/client/src/ui/widgets/homeitemwidget.cpp b/doomsday/apps/client/src/ui/widgets/homeitemwidget.cpp index 88f615eff9..e59ab0d91b 100644 --- a/doomsday/apps/client/src/ui/widgets/homeitemwidget.cpp +++ b/doomsday/apps/client/src/ui/widgets/homeitemwidget.cpp @@ -32,20 +32,48 @@ DENG_GUI_PIMPL(HomeItemWidget) ClickHandler(Public &owner) : owner(owner) {} - bool handleEvent(GuiWidget &, Event const &event) + void acquireFocus() { + owner.root().setFocus(owner.d->background); + emit owner.mouseActivity(); + } + + bool handleEvent(GuiWidget &widget, Event const &event) + { + if(widget.isDisabled()) return false; + if(event.type() == Event::MouseButton) { MouseEvent const &mouse = event.as(); if(owner.hitTest(event)) { + if(mouse.button() == MouseEvent::Right) + { + switch(widget.handleMouseClick(event, MouseEvent::Right)) + { + case MouseClickStarted: + acquireFocus(); + return true; + + case MouseClickAborted: + return true; + + case MouseClickFinished: + emit owner.openContextMenu(); + return true; + + default: + return false; // Ignore. + } + } + if(mouse.state() == MouseEvent::Pressed || mouse.state() == MouseEvent::DoubleClick) { - owner.root().setFocus(owner.d->background); - emit owner.mouseActivity(); + acquireFocus(); } - if(mouse.state() == MouseEvent::DoubleClick) + if(mouse.state() == MouseEvent::DoubleClick && + mouse.button() == MouseEvent::Left) { emit owner.doubleClicked(); return true; diff --git a/doomsday/apps/client/src/ui/widgets/packagesbuttonwidget.cpp b/doomsday/apps/client/src/ui/widgets/packagesbuttonwidget.cpp new file mode 100644 index 0000000000..3372e7bae9 --- /dev/null +++ b/doomsday/apps/client/src/ui/widgets/packagesbuttonwidget.cpp @@ -0,0 +1,109 @@ +/** @file packagesbuttonwidget.cpp + * + * @authors Copyright (c) 2016 Jaakko Keränen + * + * @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/packagesbuttonwidget.h" +#include "ui/dialogs/packagesdialog.h" + +#include +#include + +using namespace de; + +DENG_GUI_PIMPL(PackagesButtonWidget) +{ + StringList packages; + String dialogTitle; + String noneLabel; + + Instance(Public *i) : Base(i) + {} + + void updateLabel() + { + self.setImage(new StyleProceduralImage("package", self)); + + if(packages.isEmpty()) + { + self.setText(noneLabel); + self.setTextColor("text"); + self.setImageColor(style().colors().colorf("text")); + + if(!noneLabel.isEmpty()) self.setImage(nullptr); + } + else + { + self.setText(String::format("%i", packages.count())); + self.setTextColor("accent"); + self.setImageColor(style().colors().colorf("accent")); + } + } + + void pressed() + { + // The Packages dialog allows selecting which packages are loaded, and in + // which order. One can also browse the available packages. + auto *dlg = new PackagesDialog(dialogTitle); + dlg->setDeleteAfterDismissed(true); + dlg->setSelectedPackages(packages); + dlg->setAcceptanceAction(new CallbackAction([this, dlg] () + { + packages = dlg->selectedPackages(); + updateLabel(); + + // Notify. + QStringList ids; + for(auto const &p : packages) ids << p; + emit self.packageSelectionChanged(ids); + })); + root().addOnTop(dlg); + dlg->open(); + } +}; + +PackagesButtonWidget::PackagesButtonWidget() + : d(new Instance(this)) +{ + setOverrideImageSize(style().fonts().font("default").height().value()); + setSizePolicy(ui::Expand, ui::Expand); + setTextAlignment(ui::AlignLeft); + connect(this, &ButtonWidget::pressed, [this] () { d->pressed(); }); + + d->updateLabel(); +} + +void PackagesButtonWidget::setNoneLabel(String const &noneLabel) +{ + d->noneLabel = noneLabel; + d->updateLabel(); +} + +void PackagesButtonWidget::setDialogTitle(String const &title) +{ + d->dialogTitle = title; +} + +void PackagesButtonWidget::setPackages(StringList const &packageIds) +{ + d->packages = packageIds; + d->updateLabel(); +} + +StringList PackagesButtonWidget::packages() const +{ + return d->packages; +}