From b0dc68215a83bf226b69734b21d0f64b767487c3 Mon Sep 17 00:00:00 2001 From: skyjake Date: Sun, 18 Aug 2013 10:29:48 +0300 Subject: [PATCH] UI|Client|Widgets: Improvements to DialogWidget (and popup, button) Dialogs now define some generic button roles (accept, reject, etc.) so that it is more convenient to construct dialogs. Dialogs also don't grow larger than the view height, and will allow scrolling up/down if the content doesn't fit. Cleanup: Applied the maybeAs() method --- doomsday/client/include/ui/uidefs.h | 8 + .../client/include/ui/widgets/buttonwidget.h | 2 + .../client/include/ui/widgets/dialogwidget.h | 40 ++++- .../client/include/ui/widgets/popupwidget.h | 2 + .../client/src/ui/widgets/aboutdialog.cpp | 19 +- .../client/src/ui/widgets/buttonwidget.cpp | 5 + .../client/src/ui/widgets/choicewidget.cpp | 4 +- .../client/src/ui/widgets/dialogwidget.cpp | 166 +++++++++++++++--- doomsday/client/src/ui/widgets/menuwidget.cpp | 5 +- .../client/src/ui/widgets/popupwidget.cpp | 7 +- 10 files changed, 221 insertions(+), 37 deletions(-) diff --git a/doomsday/client/include/ui/uidefs.h b/doomsday/client/include/ui/uidefs.h index 63c9eb16aa..f89caedaa5 100644 --- a/doomsday/client/include/ui/uidefs.h +++ b/doomsday/client/include/ui/uidefs.h @@ -45,6 +45,14 @@ inline Direction opposite(Direction dir) { } } +inline bool isHorizontal(Direction dir) { + return dir == ui::Left || dir == ui::Right; +} + +inline bool isVertical(Direction dir) { + return dir == ui::Up || dir == ui::Down; +} + /** * Flags for specifying alignment. */ diff --git a/doomsday/client/include/ui/widgets/buttonwidget.h b/doomsday/client/include/ui/widgets/buttonwidget.h index 654082dad8..bb08aef464 100644 --- a/doomsday/client/include/ui/widgets/buttonwidget.h +++ b/doomsday/client/include/ui/widgets/buttonwidget.h @@ -67,6 +67,8 @@ class ButtonWidget : public LabelWidget */ void setAction(de::Action *action); + de::Action *action() const; + State state() const; // Events. diff --git a/doomsday/client/include/ui/widgets/dialogwidget.h b/doomsday/client/include/ui/widgets/dialogwidget.h index 1bf845ab3c..83d6e713f4 100644 --- a/doomsday/client/include/ui/widgets/dialogwidget.h +++ b/doomsday/client/include/ui/widgets/dialogwidget.h @@ -50,6 +50,40 @@ class DialogWidget : public PopupWidget Nonmodal }; + enum RoleFlag { + None = 0, + Default = 0x1, ///< Pressing Space or Enter will activate this. + Accept = 0x2, + Reject = 0x4, + Yes = 0x8, + No = 0x10, + Action = 0x20 + }; + Q_DECLARE_FLAGS(RoleFlags, RoleFlag) + + class ButtonItem : public ui::ActionItem + { + public: + /** + * Button with the role's default label and action. + * @param flags Role flags for the button. + * @param label Label for the button. If empty, the default label will be used. + */ + ButtonItem(RoleFlags flags, de::String const &label = ""); + + /** + * Button with custom action. + * @param flags Role flags for the button. + * @param label Label for the button. If empty, the default label will be used. + */ + ButtonItem(RoleFlags flags, de::String const &label, de::Action *action); + + RoleFlags role() const { return _role; } + + private: + RoleFlags _role; + }; + public: DialogWidget(de::String const &name = ""); @@ -57,7 +91,7 @@ class DialogWidget : public PopupWidget Modality modality() const; - ScrollAreaWidget &content(); + ScrollAreaWidget &area(); MenuWidget &buttons(); @@ -105,4 +139,8 @@ public slots: DENG2_PRIVATE(d) }; +typedef DialogWidget::ButtonItem DialogButtonItem; + +Q_DECLARE_OPERATORS_FOR_FLAGS(DialogWidget::RoleFlags) + #endif // DENG_CLIENT_DIALOGWIDGET_H diff --git a/doomsday/client/include/ui/widgets/popupwidget.h b/doomsday/client/include/ui/widgets/popupwidget.h index e13b0833a8..8a85cda29d 100644 --- a/doomsday/client/include/ui/widgets/popupwidget.h +++ b/doomsday/client/include/ui/widgets/popupwidget.h @@ -65,6 +65,8 @@ class PopupWidget : public GuiWidget */ void setOpeningDirection(ui::Direction dir); + ui::Direction openingDirection() const; + bool isOpen() const; /** diff --git a/doomsday/client/src/ui/widgets/aboutdialog.cpp b/doomsday/client/src/ui/widgets/aboutdialog.cpp index f679db4491..e8387bc6b2 100644 --- a/doomsday/client/src/ui/widgets/aboutdialog.cpp +++ b/doomsday/client/src/ui/widgets/aboutdialog.cpp @@ -65,13 +65,13 @@ AboutDialog::AboutDialog() : DialogWidget("about") homepage->setSizePolicy(ui::Expand, ui::Expand); homepage->setAction(new SignalAction(&ClientApp::app(), SLOT(openHomepageInBrowser()))); - content().add(logo); - content().add(title); - content().add(info); - content().add(homepage); + area().add(logo); + area().add(title); + area().add(info); + area().add(homepage); // Position inside the content. - RuleRectangle const &cont = content().contentRule(); + RuleRectangle const &cont = area().contentRule(); logo->rule() .setLeftTop(cont.left(), cont.top()) .setInput(Rule::Width, width); @@ -87,11 +87,10 @@ AboutDialog::AboutDialog() : DialogWidget("about") .setAnchorPoint(Vector2f(.5f, 0)); // Total size of the dialog's content. - content().setContentWidth(width); - content().setContentHeight(logo->rule().height() + title->rule().height() + - info->rule().height() + homepage->rule().height()); + area().setContentWidth(width); + area().setContentHeight(logo->rule().height() + title->rule().height() + + info->rule().height() + homepage->rule().height()); - // Just an OK button. buttons().items() - << new ui::ActionItem(tr("Close"), new SignalAction(this, SLOT(accept()))); + << new DialogButtonItem(DialogWidget::Accept | DialogWidget::Default, tr("Close")); } diff --git a/doomsday/client/src/ui/widgets/buttonwidget.cpp b/doomsday/client/src/ui/widgets/buttonwidget.cpp index 38e199257d..17d901de73 100644 --- a/doomsday/client/src/ui/widgets/buttonwidget.cpp +++ b/doomsday/client/src/ui/widgets/buttonwidget.cpp @@ -147,6 +147,11 @@ void ButtonWidget::setAction(Action *action) } } +Action *ButtonWidget::action() const +{ + return d->action.data(); +} + ButtonWidget::State ButtonWidget::state() const { return d->state; diff --git a/doomsday/client/src/ui/widgets/choicewidget.cpp b/doomsday/client/src/ui/widgets/choicewidget.cpp index 37808edbba..a97366266b 100644 --- a/doomsday/client/src/ui/widgets/choicewidget.cpp +++ b/doomsday/client/src/ui/widgets/choicewidget.cpp @@ -63,11 +63,11 @@ DENG2_OBSERVES(ContextWidgetOrganizer, WidgetCreation) void widgetCreatedForItem(GuiWidget &widget, ui::Item const &item) { - if(widget.is()) + if(ButtonWidget *but = widget.maybeAs()) { // Make sure the created buttons have an action that updates the // selected item. - widget.as().setAction(new SelectAction(this, item)); + but->setAction(new SelectAction(this, item)); } } diff --git a/doomsday/client/src/ui/widgets/dialogwidget.cpp b/doomsday/client/src/ui/widgets/dialogwidget.cpp index a857b739e1..c09d8b1b3c 100644 --- a/doomsday/client/src/ui/widgets/dialogwidget.cpp +++ b/doomsday/client/src/ui/widgets/dialogwidget.cpp @@ -18,14 +18,17 @@ #include "ui/widgets/dialogwidget.h" #include "ui/widgets/guirootwidget.h" +#include "ui/signalaction.h" #include "dd_main.h" +#include #include using namespace de; DENG2_PIMPL(DialogWidget), -DENG2_OBSERVES(ContextWidgetOrganizer, WidgetCreation) +DENG2_OBSERVES(ContextWidgetOrganizer, WidgetCreation), +DENG2_OBSERVES(ContextWidgetOrganizer, WidgetUpdate) { Modality modality; ScrollAreaWidget *area; @@ -34,10 +37,13 @@ DENG2_OBSERVES(ContextWidgetOrganizer, WidgetCreation) Instance(Public *i) : Base(i), modality(Modal) { + GuiWidget *container = new GuiWidget("container"); + area = new ScrollAreaWidget("area"); buttons = new MenuWidget("buttons"); buttons->organizer().audienceForWidgetCreation += this; + buttons->organizer().audienceForWidgetUpdate += this; // The menu maintains its own width and height based on children. // Set up one row with variable number of columns. @@ -47,7 +53,8 @@ DENG2_OBSERVES(ContextWidgetOrganizer, WidgetCreation) .setInput(Rule::Left, self.rule().left()) .setInput(Rule::Top, self.rule().top()) .setInput(Rule::Width, area->contentRule().width() + area->margin() * 2) - .setInput(Rule::Height, area->contentRule().height() + area->margin() * 2); + .setInput(Rule::Height, container->rule().height() - buttons->rule().height() + + area->margin()); // Buttons below the area. buttons->rule() @@ -55,25 +62,103 @@ DENG2_OBSERVES(ContextWidgetOrganizer, WidgetCreation) .setInput(Rule::Right, self.rule().right()); // A blank container widget acts as the popup content parent. - GuiWidget *container = new GuiWidget("container"); - container->rule() - .setInput(Rule::Width, OperatorRule::maximum(area->rule().width(), buttons->rule().width())) - .setInput(Rule::Height, area->rule().height() + buttons->rule().height() - - area->margin()); + container->rule().setInput(Rule::Width, OperatorRule::maximum(area->rule().width(), + buttons->rule().width())); container->add(area); container->add(buttons); self.setContent(container); } - void widgetCreatedForItem(GuiWidget &widget, ui::Item const &) + void updateContentHeight() + { + // The container's height is limited by the height of the view. Normally + // the dialog tries to show the full height of the content area. + + DENG2_ASSERT(self.hasRoot()); + self.content().rule().setInput(Rule::Height, + OperatorRule::minimum(self.root().viewHeight(), + area->contentRule().height() + + area->margin() + + buttons->rule().height())); + } + + void widgetCreatedForItem(GuiWidget &widget, ui::Item const &item) { // Make sure all label-based widgets in the button area // manage their own size. - if(widget.is()) + if(LabelWidget *lab = widget.maybeAs()) + { + lab->setSizePolicy(ui::Expand, ui::Expand); + } + + // Apply dialog button specific roles. + if(ButtonItem const *i = item.maybeAs()) + { + ButtonWidget &but = widget.as(); + + if(i->role().testFlag(Accept)) + { + but.setAction(new SignalAction(thisPublic, SLOT(accept()))); + } + else if(i->role().testFlag(Reject)) + { + but.setAction(new SignalAction(thisPublic, SLOT(reject()))); + } + } + } + + void widgetUpdatedForItem(GuiWidget &widget, ui::Item const &item) + { + if(ButtonItem const *i = item.maybeAs()) { - widget.as().setSizePolicy(ui::Expand, ui::Expand); + ButtonWidget &but = widget.as(); + + // Set default label? + if(item.label().isEmpty()) + { + if(i->role().testFlag(Accept)) + { + but.setText(tr("OK")); + } + else if(i->role().testFlag(Reject)) + { + but.setText(tr("Cancel")); + } + else if(i->role().testFlag(Yes)) + { + but.setText(tr("Yes")); + } + else if(i->role().testFlag(No)) + { + but.setText(tr("No")); + } + } + + if(i->role().testFlag(Default)) + { + but.setText(_E(b) + but.text()); + } } } + + ui::ActionItem const *findDefaultAction() const + { + for(ui::Context::Pos i = 0; i < buttons->items().size(); ++i) + { + ButtonItem const *act = buttons->items().at(i).maybeAs(); + if(act->role().testFlag(Default) && + buttons->organizer().itemWidget(i)->isEnabled()) + { + return act; + } + } + return 0; + } + + ButtonWidget const &buttonWidget(ui::Item const &item) const + { + return buttons->organizer().itemWidget(item)->as(); + } }; DialogWidget::DialogWidget(String const &name) @@ -100,7 +185,7 @@ DialogWidget::Modality DialogWidget::modality() const return d->modality; } -ScrollAreaWidget &DialogWidget::content() +ScrollAreaWidget &DialogWidget::area() { return *d->area; } @@ -127,6 +212,29 @@ int DialogWidget::exec(GuiRootWidget &root) bool DialogWidget::handleEvent(Event const &event) { + if(event.isKeyDown()) + { + KeyEvent const &key = event.as(); + if(key.ddKey() == DDKEY_ENTER || + key.ddKey() == DDKEY_RETURN || + key.ddKey() == ' ') + { + ui::ActionItem const *defaultAction = d->findDefaultAction(); + ButtonWidget const &but = d->buttonWidget(*defaultAction); + if(but.action()) + { + but.action()->trigger(); + } + return true; + } + if(key.ddKey() == DDKEY_ESCAPE) + { + // Esc always cancels a dialog. + reject(); + return true; + } + } + if(d->modality == Modal) { // The event should already have been handled by the children. @@ -154,28 +262,42 @@ void DialogWidget::reject(int result) } } -void DialogWidget::preparePopupForOpening() -{ - PopupWidget::preparePopupForOpening(); - - // Redo the layout (items visible now). - d->buttons->updateLayout(); -} - void DialogWidget::prepare() { - // Center the dialog. - setAnchor(root().viewWidth() / 2, root().viewHeight() / 2); - d->buttons->updateLayout(); + if(openingDirection() == ui::NoDirection) + { + // Center the dialog. + setAnchor(root().viewWidth() / 2, root().viewHeight() / 2); + } + + d->updateContentHeight(); // Make sure the newly added widget knows the view size. viewResized(); notifyTree(&Widget::viewResized); + d->buttons->updateLayout(); + open(); } +void DialogWidget::preparePopupForOpening() +{ + PopupWidget::preparePopupForOpening(); + + // Redo the layout (items visible now). + d->buttons->updateLayout(); +} + void DialogWidget::finish(int) { close(); } + +DialogWidget::ButtonItem::ButtonItem(RoleFlags flags, String const &label) + : ui::ActionItem(label, 0), _role(flags) +{} + +DialogWidget::ButtonItem::ButtonItem(RoleFlags flags, String const &label, de::Action *action) + : ui::ActionItem(label, action), _role(flags) +{} diff --git a/doomsday/client/src/ui/widgets/menuwidget.cpp b/doomsday/client/src/ui/widgets/menuwidget.cpp index d5e3082261..bd28e1f4e5 100644 --- a/doomsday/client/src/ui/widgets/menuwidget.cpp +++ b/doomsday/client/src/ui/widgets/menuwidget.cpp @@ -229,7 +229,10 @@ public ContextWidgetOrganizer::IWidgetFactory b.setImage(act.image()); b.setText(act.label()); - b.setAction(act.action()->duplicate()); + if(act.action()) + { + b.setAction(act.action()->duplicate()); + } } else { diff --git a/doomsday/client/src/ui/widgets/popupwidget.cpp b/doomsday/client/src/ui/widgets/popupwidget.cpp index fa33fc514b..15b47d2ab9 100644 --- a/doomsday/client/src/ui/widgets/popupwidget.cpp +++ b/doomsday/client/src/ui/widgets/popupwidget.cpp @@ -94,7 +94,7 @@ DENG2_PIMPL(PopupWidget) bool isVerticalAnimation() const { - return dir == ui::Up || dir == ui::Down || dir == ui::NoDirection; + return isVertical(dir) || dir == ui::NoDirection; } void updateLayout() @@ -298,6 +298,11 @@ void PopupWidget::setOpeningDirection(ui::Direction dir) d->dir = dir; } +ui::Direction PopupWidget::openingDirection() const +{ + return d->dir; +} + bool PopupWidget::isOpen() const { return d->opened;