diff --git a/doomsday/client/include/ui/widgets/choicewidget.h b/doomsday/client/include/ui/widgets/choicewidget.h index 20abd8a050..655a11099e 100644 --- a/doomsday/client/include/ui/widgets/choicewidget.h +++ b/doomsday/client/include/ui/widgets/choicewidget.h @@ -20,17 +20,53 @@ #define DENG_CLIENT_CHOICEWIDGET_H #include "buttonwidget.h" +#include "popupmenuwidget.h" +#include "actionitem.h" /** * Widget for choosing an item from a set of alternatives. + * + * The items of the widget should be ChoiceItem instances, or at least derived + * from ActionItem/ChoiceItem. + * + * The default opening direction for the popup is to the right. */ class ChoiceWidget : public ButtonWidget { + Q_OBJECT + +public: + /** + * The items of the widget are expected to be instanced of + * ChoiceWidget::Item or derived from it (or at least ui::ActionItem). + */ + class Item : public ui::ActionItem + { + public: + Item(de::String const &label, de::Image const &image = de::Image()) + : ui::ActionItem(image, label) {} + }; + public: ChoiceWidget(de::String const &name = ""); + ui::Context &items(); + + PopupMenuWidget &popup(); + + void setSelected(ui::Context::Pos pos); + + ui::Context::Pos selected() const; + + ui::Item const &selectedItem() const; + +signals: + void selectionChanged(unsigned int pos); + private: DENG2_PRIVATE(d) }; +typedef ChoiceWidget::Item ChoiceItem; + #endif // DENG_CLIENT_CHOICEWIDGET_H diff --git a/doomsday/client/src/ui/widgets/choicewidget.cpp b/doomsday/client/src/ui/widgets/choicewidget.cpp index 8312e44e9a..37808edbba 100644 --- a/doomsday/client/src/ui/widgets/choicewidget.cpp +++ b/doomsday/client/src/ui/widgets/choicewidget.cpp @@ -20,12 +20,150 @@ #include "ui/widgets/popupmenuwidget.h" using namespace de; +using namespace ui; -DENG2_PIMPL(ChoiceWidget) -{ - Instance(Public *i) : Base(i) - {} +DENG2_PIMPL(ChoiceWidget), +DENG2_OBSERVES(Context, Addition), +DENG2_OBSERVES(Context, Removal), +DENG2_OBSERVES(ContextWidgetOrganizer, WidgetCreation) +{ + struct SelectAction : public de::Action + { + Instance *d; + ui::Item const &selItem; + + SelectAction(Instance *inst, ui::Item const &item) : d(inst), selItem(item) {} + + void trigger() + { + Action::trigger(); + d->selected = d->items().find(selItem); + d->updateButtonWithSelection(); + } + + Action *duplicate() const + { + DENG2_ASSERT(false); // not needed + return 0; + } + }; + + PopupMenuWidget *choices; + Context::Pos selected; ///< One item is always selected. + + Instance(Public *i) : Base(i), selected(0) + { + choices = new PopupMenuWidget; + choices->setAnchorAndOpeningDirection(self.hitRule(), ui::Right); + choices->menu().organizer().audienceForWidgetCreation += this; + self.add(choices); + + updateButtonWithSelection(); + } + + void widgetCreatedForItem(GuiWidget &widget, ui::Item const &item) + { + if(widget.is()) + { + // Make sure the created buttons have an action that updates the + // selected item. + widget.as().setAction(new SelectAction(this, item)); + } + } + + Context const &items() const + { + return choices->menu().items(); + } + + bool isValidSelection() const + { + return selected < items().size(); + } + + void contextItemAdded(Context::Pos id, ui::Item const &) + { + if(selected >= items().size()) + { + // If the previous selection was invalid, make a valid one now. + selected = 0; + + updateButtonWithSelection(); + } + + if(id <= selected) + { + // New item added before/at the selection. + selected++; + } + } + + void contextItemBeingRemoved(Context::Pos id, ui::Item const &) + { + if(id <= selected && selected > 0) + { + selected--; + } + + if(!isValidSelection()) + { + updateButtonWithSelection(); + } + } + + void updateButtonWithSelection() + { + if(isValidSelection()) + { + ui::Item const &item = items().at(selected); + self.setText(item.label()); + + ActionItem const *act = dynamic_cast(&item); + if(act) + { + self.setImage(act->image()); + } + } + else + { + // No valid selection. + self.setText(""); + self.setImage(Image()); + } + + emit self.selectionChanged(selected); + } }; ChoiceWidget::ChoiceWidget(String const &name) : ButtonWidget(name), d(new Instance(this)) {} + +PopupMenuWidget &ChoiceWidget::popup() +{ + return *d->choices; +} + +void ChoiceWidget::setSelected(Context::Pos pos) +{ + if(d->selected != pos) + { + d->selected = pos; + d->updateButtonWithSelection(); + } +} + +Context::Pos ChoiceWidget::selected() const +{ + return d->selected; +} + +Item const &ChoiceWidget::selectedItem() const +{ + DENG2_ASSERT(d->isValidSelection()); + return d->items().at(d->selected); +} + +ui::Context &ChoiceWidget::items() +{ + return d->choices->menu().items(); +} diff --git a/doomsday/libdeng2/include/de/widgets/widget.h b/doomsday/libdeng2/include/de/widgets/widget.h index f4acca8c1d..730066c950 100644 --- a/doomsday/libdeng2/include/de/widgets/widget.h +++ b/doomsday/libdeng2/include/de/widgets/widget.h @@ -93,6 +93,11 @@ class DENG2_PUBLIC Widget Widget(String const &name = ""); virtual ~Widget(); + template + bool is() const { + return dynamic_cast(this) != 0; + } + template Type &as() { DENG2_ASSERT(dynamic_cast(this) != 0);