1 /** @file widgets/dialogwidget.cpp  Popup dialog.
2  *
3  * @authors Copyright (c) 2013-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  *
5  * @par License
6  * LGPL: http://www.gnu.org/licenses/lgpl.html
7  *
8  * <small>This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU Lesser General Public License as published by
10  * the Free Software Foundation; either version 3 of the License, or (at your
11  * option) any later version. This program is distributed in the hope that it
12  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
14  * General Public License for more details. You should have received a copy of
15  * the GNU Lesser General Public License along with this program; if not, see:
16  * http://www.gnu.org/licenses</small>
17  */
18 
19 #include "de/DialogWidget"
20 #include "de/ToggleWidget"
21 #include "de/ChoiceWidget"
22 #include "de/GuiRootWidget"
23 #include "de/SignalAction"
24 #include "de/DialogContentStylist"
25 #include "de/ui/FilteredData"
26 
27 #include <de/KeyEvent>
28 #include <de/MouseEvent>
29 #include <de/Untrapper>
30 #include <QEventLoop>
31 
32 namespace de {
33 
34 static TimeSpan const FLASH_ANIM_SPAN = 0.75;
35 
36 /**
37  * Compares dialog button items to determine the order in which they
38  * should appear in the UI.
39  *
40  * @param a  Dialog button item.
41  * @param b  Dialog button item.
42  *
43  * @return @c true, if a < b.
44  */
dialogButtonOrder(ui::Item const & a,ui::Item const & b)45 static bool dialogButtonOrder(ui::Item const &a, ui::Item const &b)
46 {
47     DialogButtonItem const &left  = a.as<DialogButtonItem>();
48     DialogButtonItem const &right = b.as<DialogButtonItem>();
49 
50     if (!left.role().testFlag(DialogWidget::Default) && right.role().testFlag(DialogWidget::Default))
51     {
52 #ifdef MACOSX
53         // Default buttons go to the right on macOS.
54         return true;
55 #else
56         // Default buttons to the left.
57         return false;
58 #endif
59     }
60     if (left.role().testFlag(DialogWidget::Default) && !right.role().testFlag(DialogWidget::Default))
61     {
62 #ifdef MACOSX
63         // Default buttons go to the right on macOS.
64         return false;
65 #else
66         // Default buttons to the left.
67         return true;
68 #endif
69     }
70     if (a.label().isEmpty() && !b.label().isEmpty())
71     {
72         // Label-less buttons go first.
73         return true;
74     }
75 
76     // Order unchanged.
77     return false;
78 }
79 
DENG_GUI_PIMPL(DialogWidget)80 DENG_GUI_PIMPL(DialogWidget)
81 , DENG2_OBSERVES(ChildWidgetOrganizer, WidgetCreation)
82 , DENG2_OBSERVES(ChildWidgetOrganizer, WidgetUpdate)
83 , DENG2_OBSERVES(ui::Data, Addition)
84 , DENG2_OBSERVES(ui::Data, Removal)
85 {
86     Modality modality;
87     Flags flags;
88     ScrollAreaWidget *area = nullptr;
89     ScrollAreaWidget *rightArea = nullptr;
90     LabelWidget *heading;
91     MenuWidget *buttons;
92     MenuWidget *extraButtons;
93     ui::ListData buttonItems;
94     ui::FilteredData mainButtonItems  { buttonItems };
95     ui::FilteredData extraButtonItems { buttonItems };
96     QEventLoop subloop;
97     de::Action *acceptAction;
98     Animation glow;
99     bool needButtonUpdate;
100     float normalGlow;
101     bool animatingGlow;
102     QScopedPointer<Untrapper> untrapper;
103     DialogContentStylist stylist;
104     IndirectRule *minWidth;
105     Rule const *maxContentHeight = nullptr;
106 
107     Impl(Public *i, Flags const &dialogFlags)
108         : Base(i)
109         , modality(Modal)
110         , flags(dialogFlags)
111         , heading(0)
112         , acceptAction(0)
113         , needButtonUpdate(false)
114         , animatingGlow(false)
115     {
116         minWidth = new IndirectRule;
117 
118         // Initialize the border glow.
119         normalGlow = style().colors().colorf("glow").w;
120         glow.setValue(normalGlow);
121         glow.setStyle(Animation::Linear);
122 
123         // Set up widget structure.
124         GuiWidget *container = new GuiWidget("container");
125 
126         area = new ScrollAreaWidget("area");
127 
128         buttons = new MenuWidget("buttons");
129         buttons->margins().setTop("");
130         buttons->setItems(mainButtonItems);
131         buttons->organizer().audienceForWidgetCreation() += this;
132         buttons->organizer().audienceForWidgetUpdate() += this;
133 
134         extraButtons = new MenuWidget("extra");
135         extraButtons->margins().setTop("");
136         extraButtons->setItems(extraButtonItems);
137         extraButtons->organizer().audienceForWidgetCreation() += this;
138         extraButtons->organizer().audienceForWidgetUpdate() += this;
139 
140         buttonItems.audienceForAddition() += this;
141         buttonItems.audienceForRemoval()  += this;
142 
143         // Segregate Action buttons into the extra buttons set.
144         mainButtonItems.setFilter([] (ui::Item const &it)
145         {
146             if (!is<DialogButtonItem>(it)) return false;
147             // Non-Action buttons only.
148             return !it.as<DialogButtonItem>().role().testFlag(Action);
149         });
150         extraButtonItems.setFilter([] (ui::Item const &it)
151         {
152             if (!is<DialogButtonItem>(it)) return false;
153             // Only Action buttons allowed.
154             return it.as<DialogButtonItem>().role().testFlag(Action);
155         });
156 
157         // The menu maintains its own width and height based on children.
158         // Set up one row with variable number of columns.
159         buttons->setGridSize(0, ui::Expand, 1, ui::Expand);
160         extraButtons->setGridSize(0, ui::Expand, 1, ui::Expand);
161 
162         area->rule()
163                 .setInput(Rule::Left,  self().rule().left())
164                 .setInput(Rule::Top,   self().rule().top())
165                 .setInput(Rule::Width, area->contentRule().width() + area->margins().width());
166 
167         // Will a title be included?
168         if (flags & WithHeading)
169         {
170             heading = new LabelWidget("heading");
171             heading->setFont("heading");
172             heading->margins()
173                     .setBottom("")
174                     .setTop (rule("gap") + rule("dialog.gap"))
175                     .setLeft(rule("gap") + rule("dialog.gap"));
176             heading->setSizePolicy(ui::Expand, ui::Expand);
177             heading->setTextColor("accent");
178             heading->setImageColor(style().colors().colorf("accent"));
179             heading->setOverrideImageSize(heading->font().ascent());
180             heading->setTextGap("dialog.gap");
181             heading->setAlignment(ui::AlignLeft);
182             heading->setTextAlignment(ui::AlignRight);
183             heading->setTextLineAlignment(ui::AlignLeft);
184             heading->setFillMode(LabelWidget::FillWithText);
185             container->add(heading);
186 
187             heading->rule()
188                     .setInput(Rule::Top,  self().rule().top())
189                     .setInput(Rule::Left, self().rule().left());
190 
191             area->rule().setInput(Rule::Top, heading->rule().bottom());
192         }
193 
194         area->rule().setInput(Rule::Height, container->rule().height() - buttons->rule().height());
195 
196         // Buttons below the area.
197         buttons->rule()
198                 .setInput(Rule::Bottom, container->rule().bottom())
199                 .setInput(Rule::Right, self().rule().right());
200         extraButtons->rule()
201                 .setInput(Rule::Top, buttons->rule().top())
202                 .setInput(Rule::Left, self().rule().left());
203 
204         // A blank container widget acts as the popup content parent.
205         container->rule().setInput(Rule::Width, OperatorRule::maximum(
206                                        area->rule().width(),
207                                        buttons->rule().width() + extraButtons->rule().width(),
208                                        *minWidth));
209 
210         if (flags.testFlag(WithHeading))
211         {
212             area->rule().setInput(Rule::Height,
213                                   container->rule().height()
214                                   - heading->rule().height()
215                                   - buttons->rule().height());
216         }
217 
218         container->add(area);
219         container->add(extraButtons);
220         container->add(buttons);
221         self().setContent(container);
222     }
223 
224     ~Impl()
225     {
226         releaseRef(minWidth);
227         releaseRef(maxContentHeight);
228         releaseRef(acceptAction);
229     }
230 
231     void setupForTwoColumns()
232     {
233         // Create an additional content area.
234         if (!rightArea)
235         {
236             rightArea = new ScrollAreaWidget("rightArea");
237             self().content().add(rightArea);
238 
239             rightArea->rule()
240                     .setInput(Rule::Top,    area->rule().top())
241                     .setInput(Rule::Left,   area->rule().right())
242                     .setInput(Rule::Height, area->rule().height())
243                     .setInput(Rule::Width,  rightArea->contentRule().width() + rightArea->margins().width());
244 
245             if (heading)
246             {
247                 heading->rule().setInput(Rule::Right, rightArea->rule().right());
248             }
249 
250             // Content size is now wider.
251             self().content().rule().setInput(Rule::Width, OperatorRule::maximum(
252                                            area->rule().width() + rightArea->rule().width(),
253                                            buttons->rule().width() + extraButtons->rule().width(),
254                                            *minWidth));
255 
256             if (self().isOpen()) updateContentHeight();
257         }
258     }
259 
260     void updateContentHeight()
261     {
262         // Determine suitable maximum height.
263         Rule const *maxHeight = holdRef(root().viewHeight());
264         if (self().openingDirection() == ui::Down)
265         {
266             changeRef(maxHeight, *maxHeight - self().anchor().top() - rule("gap"));
267         }
268         if (maxContentHeight)
269         {
270             changeRef(maxHeight, OperatorRule::minimum(*maxHeight, *maxContentHeight));
271         }
272 
273         // Scrollable area content height.
274         AutoRef<Rule> areaContentHeight = area->contentRule().height() + area->margins().height();
275         if (rightArea)
276         {
277             areaContentHeight.reset(OperatorRule::maximum(areaContentHeight,
278                     rightArea->contentRule().height() + rightArea->margins().height()));
279         }
280 
281         // The container's height is limited by the height of the view. Normally
282         // the dialog tries to show the full height of the content area.
283         if (!flags.testFlag(WithHeading))
284         {
285             self().content().rule().setInput(Rule::Height,
286                     OperatorRule::minimum(*maxHeight, areaContentHeight + buttons->rule().height()));
287         }
288         else
289         {
290             self().content().rule().setInput(Rule::Height,
291                     OperatorRule::minimum(*maxHeight, (heading? heading->rule().height() : Const(0)) +
292                                                        areaContentHeight + buttons->rule().height()));
293         }
294 
295         releaseRef(maxHeight);
296     }
297 
298     void dataItemAdded(ui::Data::Pos, ui::Item const &)
299     {
300         needButtonUpdate = true;
301     }
302 
303     void dataItemRemoved(ui::Data::Pos, ui::Item &)
304     {
305         needButtonUpdate = true;
306     }
307 
308     void updateButtonLayout()
309     {
310         buttonItems.stableSort(dialogButtonOrder);
311         needButtonUpdate = false;
312     }
313 
314     void widgetCreatedForItem(GuiWidget &widget, ui::Item const &item)
315     {
316         // Make sure all label-based widgets in the button area
317         // manage their own size.
318         if (LabelWidget *lab = maybeAs<LabelWidget>(widget))
319         {
320             lab->setSizePolicy(ui::Expand, ui::Expand);
321         }
322 
323         // Apply dialog button specific roles.
324         if (ButtonItem const *i = maybeAs<ButtonItem>(item))
325         {
326             ButtonWidget &but = widget.as<ButtonWidget>();
327             but.setColorTheme(self().colorTheme());
328             if (!i->action())
329             {
330                 if (i->role() & (Accept | Yes))
331                 {
332                     but.setAction(new SignalAction(thisPublic, SLOT(accept())));
333                 }
334                 else if (i->role() & (Reject | No))
335                 {
336                     but.setAction(new SignalAction(thisPublic, SLOT(reject())));
337                 }
338             }
339         }
340     }
341 
342     void widgetUpdatedForItem(GuiWidget &widget, ui::Item const &item)
343     {
344         if (ButtonItem const *i = maybeAs<ButtonItem>(item))
345         {
346             ButtonWidget &but = widget.as<ButtonWidget>();
347 
348             // Button images must be a certain size.
349             but.setOverrideImageSize(style().fonts().font("default").height());
350 
351             // Set default label?
352             if (item.label().isEmpty())
353             {
354                 if (i->role().testFlag(Accept))
355                 {
356                     but.setText(tr("OK"));
357                 }
358                 else if (i->role().testFlag(Reject))
359                 {
360                     but.setText(tr("Cancel"));
361                 }
362                 else if (i->role().testFlag(Yes))
363                 {
364                     but.setText(tr("Yes"));
365                 }
366                 else if (i->role().testFlag(No))
367                 {
368                     but.setText(tr("No"));
369                 }
370             }
371 
372             // Highlight the default button(s).
373             if (i->role().testFlag(Default))
374             {
375                 but.setTextColor(self().colorTheme() == Normal? "dialog.default" : "inverted.text");
376                 if (self().colorTheme() == Normal)
377                 {
378                     but.setHoverTextColor("dialog.default", ButtonWidget::ReplaceColor);
379                 }
380                 but.setText(_E(b) + but.text());
381             }
382             else
383             {
384                 but.setTextColor(self().colorTheme() == Normal? "text" : "inverted.text");
385             }
386         }
387     }
388 
389     ui::ActionItem const *findDefaultAction() const
390     {
391         // Note: extra buttons not searched because they shouldn't contain default actions.
392 
393         for (ui::Data::Pos i = 0; i < mainButtonItems.size(); ++i)
394         {
395             ButtonItem const *act = maybeAs<ButtonItem>(mainButtonItems.at(i));
396             if (act->role().testFlag(Default) &&
397                 buttons->organizer().itemWidget(i)->isEnabled())
398             {
399                 return act;
400             }
401         }
402         return 0;
403     }
404 
405     ButtonWidget *findDefaultButton() const
406     {
407         if (ui::ActionItem const *defaultAction = findDefaultAction())
408         {
409             return &buttonWidget(*defaultAction);
410         }
411         return nullptr;
412     }
413 
414     ButtonWidget &buttonWidget(ui::Item const &item) const
415     {
416         GuiWidget *w = extraButtons->organizer().itemWidget(item);
417         if (w) return w->as<ButtonWidget>();
418         // Try the normal buttons.
419         return buttons->organizer().itemWidget(item)->as<ButtonWidget>();
420     }
421 
422     void startBorderFlash()
423     {
424         animatingGlow = true;
425         glow.setValueFrom(1, normalGlow, FLASH_ANIM_SPAN);
426         Background bg = self().background();
427         bg.color.w = glow;
428         self().set(bg);
429     }
430 
431     void updateBorderFlash()
432     {
433         Background bg = self().background();
434         bg.color.w = glow;
435         self().set(bg);
436 
437         if (glow.done()) animatingGlow = false;
438     }
439 
440     void updateBackground()
441     {
442         Background bg = self().background();
443         if (self().isUsingInfoStyle())
444         {
445             bg = self().infoStyleBackground();
446         }
447         else if (style().isBlurringAllowed())
448         {
449             /// @todo Should use the Style for this.
450             bg.type = Background::SharedBlurWithBorderGlow;
451             bg.blur = style().sharedBlurWidget();
452             bg.solidFill = Vector4f(0, 0, 0, .65f);
453         }
454         else
455         {
456             bg.type = Background::BorderGlow;
457             bg.solidFill = style().colors().colorf("dialog.background");
458         }
459         self().set(bg);
460     }
461 };
462 
DialogWidget(String const & name,Flags const & flags)463 DialogWidget::DialogWidget(String const &name, Flags const &flags)
464     : PopupWidget(name), d(new Impl(this, flags))
465 {
466     d->stylist.setContainer(area());
467     setOpeningDirection(ui::NoDirection);
468     d->updateBackground();
469     area().enableIndicatorDraw(true);
470 }
471 
modality() const472 DialogWidget::Modality DialogWidget::modality() const
473 {
474     return d->modality;
475 }
476 
heading()477 LabelWidget &DialogWidget::heading()
478 {
479     DENG2_ASSERT(d->heading != 0);
480     return *d->heading;
481 }
482 
area()483 ScrollAreaWidget &DialogWidget::area()
484 {
485     return *d->area;
486 }
487 
leftArea()488 ScrollAreaWidget &DialogWidget::leftArea()
489 {
490     d->setupForTwoColumns();
491     return *d->area;
492 }
493 
rightArea()494 ScrollAreaWidget &DialogWidget::rightArea()
495 {
496     d->setupForTwoColumns();
497     return *d->rightArea;
498 }
499 
setMinimumContentWidth(Rule const & minWidth)500 void DialogWidget::setMinimumContentWidth(Rule const &minWidth)
501 {
502     d->minWidth->setSource(minWidth);
503 }
504 
setMaximumContentHeight(const de::Rule & maxHeight)505 void DialogWidget::setMaximumContentHeight(const de::Rule &maxHeight)
506 {
507     changeRef(d->maxContentHeight, maxHeight);
508 }
509 
buttonsMenu()510 MenuWidget &DialogWidget::buttonsMenu()
511 {
512     return *d->buttons;
513 }
514 
extraButtonsMenu()515 MenuWidget &DialogWidget::extraButtonsMenu()
516 {
517     return *d->extraButtons;
518 }
519 
buttons()520 ui::Data &DialogWidget::buttons()
521 {
522     return d->buttonItems;
523 }
524 
buttonWidget(String const & label) const525 ButtonWidget &DialogWidget::buttonWidget(String const &label) const
526 {
527     GuiWidget *w = d->buttons->organizer().itemWidget(label);
528     if (w) return w->as<ButtonWidget>();
529 
530     w = d->extraButtons->organizer().itemWidget(label);
531     if (w) return w->as<ButtonWidget>();
532 
533     throw UndefinedLabel("DialogWidget::buttonWidget", "Undefined label \"" + label + "\"");
534 }
535 
popupButtonWidget(String const & label) const536 PopupButtonWidget &DialogWidget::popupButtonWidget(String const &label) const
537 {
538     return buttonWidget(label).as<PopupButtonWidget>();
539 }
540 
buttonWidget(int roleId) const541 ButtonWidget *DialogWidget::buttonWidget(int roleId) const
542 {
543     for (uint i = 0; i < d->buttonItems.size(); ++i)
544     {
545         DialogButtonItem const &item = d->buttonItems.at(i).as<DialogButtonItem>();
546 
547         if ((item.role() & IdMask) == uint(roleId))
548         {
549             return &d->buttonWidget(item);
550         }
551     }
552     return nullptr;
553 }
554 
popupButtonWidget(int roleId) const555 PopupButtonWidget *DialogWidget::popupButtonWidget(int roleId) const
556 {
557     if (auto *btn = buttonWidget(roleId))
558     {
559         return &btn->as<PopupButtonWidget>();
560     }
561     return nullptr;
562 }
563 
buttonWidgets() const564 QList<ButtonWidget *> DialogWidget::buttonWidgets() const
565 {
566     QList<ButtonWidget *> buttons;
567     foreach (GuiWidget *w, d->buttons->childWidgets())
568     {
569         if (auto *but = maybeAs<ButtonWidget>(w))
570         {
571             buttons << but;
572         }
573     }
574     return buttons;
575 }
576 
setAcceptanceAction(RefArg<de::Action> action)577 void DialogWidget::setAcceptanceAction(RefArg<de::Action> action)
578 {
579     changeRef(d->acceptAction, action);
580 }
581 
acceptanceAction() const582 Action *DialogWidget::acceptanceAction() const
583 {
584     return d->acceptAction;
585 }
586 
exec(GuiRootWidget & root)587 int DialogWidget::exec(GuiRootWidget &root)
588 {
589     d->modality = Modal;
590 
591     // The widget is added to the root temporarily (as top child).
592     DENG2_ASSERT(!hasRoot());
593     root.add(this);
594 
595     prepare();
596 
597     int result = 0;
598     try
599     {
600 #if defined (DENG_MOBILE)
601         // The subloop will likely access the root independently.
602         root.unlock();
603 #endif
604 
605         result = d->subloop.exec();
606 
607 #if defined (DENG_MOBILE)
608         root.lock();
609 #endif
610     }
611     catch (...)
612     {
613 #if defined (DENG_MOBILE)
614         // The lock needs to be reacquired in any case.
615         root.lock();
616 #endif
617         throw;
618     }
619 
620     finish(result);
621     return result;
622 }
623 
open()624 void DialogWidget::open()
625 {
626     open(NonModal);
627 }
628 
open(Modality modality)629 void DialogWidget::open(Modality modality)
630 {
631     d->modality = modality;
632 
633     DENG2_ASSERT(hasRoot());
634     prepare(); // calls base class's open()
635 }
636 
defaultActionItem()637 ui::ActionItem *DialogWidget::defaultActionItem()
638 {
639     return const_cast<ui::ActionItem *>(d->findDefaultAction());
640 }
641 
offerFocus()642 void DialogWidget::offerFocus()
643 {
644     root().setFocus(d->findDefaultButton());
645 }
646 
update()647 void DialogWidget::update()
648 {
649     PopupWidget::update();
650 
651     if (d->needButtonUpdate)
652     {
653         d->updateButtonLayout();
654     }
655 
656     if (d->animatingGlow)
657     {
658         d->updateBorderFlash();
659     }
660 }
661 
handleEvent(Event const & event)662 bool DialogWidget::handleEvent(Event const &event)
663 {
664     if (!isOpen()) return false;
665 
666     if (event.isKeyDown())
667     {
668         KeyEvent const &key = event.as<KeyEvent>();
669 
670         if (key.ddKey() == DDKEY_ENTER  ||
671             key.ddKey() == DDKEY_RETURN ||
672             key.ddKey() == ' ')
673         {
674             if (ButtonWidget *but = d->findDefaultButton())
675             {
676                 but->trigger();
677                 return true;
678             }
679         }
680 
681         if (key.ddKey() == DDKEY_ESCAPE)
682         {
683             // Esc always cancels a dialog.
684             reject();
685             return true;
686         }
687     }
688 
689     if (d->modality == Modal)
690     {
691         // The event should already have been handled by the children.
692         if ((event.isKeyDown() && !event.as<KeyEvent>().isModifier()) ||
693             (event.type() == Event::MouseButton &&
694              event.as<MouseEvent>().state() == MouseEvent::Pressed &&
695              !hitTest(event)))
696         {
697             d->startBorderFlash();
698         }
699         return true;
700     }
701     else
702     {
703         if ((event.type() == Event::MouseButton || event.type() == Event::MousePosition ||
704              event.type() == Event::MouseWheel) &&
705            hitTest(event))
706         {
707             // Non-modal dialogs eat mouse clicks/position inside the dialog.
708             return true;
709         }
710     }
711 
712     return PopupWidget::handleEvent(event);
713 }
714 
accept(int result)715 void DialogWidget::accept(int result)
716 {
717     if (d->subloop.isRunning())
718     {
719         DENG2_ASSERT(d->modality == Modal);
720         d->subloop.exit(result);
721         emit accepted(result);
722     }
723     else
724     {
725         emit accepted(result);
726         finish(result);
727     }
728 }
729 
reject(int result)730 void DialogWidget::reject(int result)
731 {
732     if (d->subloop.isRunning())
733     {
734         DENG2_ASSERT(d->modality == Modal);
735         d->subloop.exit(result);
736         emit rejected(result);
737     }
738     else
739     {
740         emit rejected(result);
741         finish(result);
742     }
743 }
744 
prepare()745 void DialogWidget::prepare()
746 {
747     // Mouse needs to be untrapped for the user to be access the dialog.
748     d->untrapper.reset(new Untrapper(root().window()));
749 
750     if (openingDirection() == ui::NoDirection)
751     {
752         // Center the dialog.
753         setAnchor(root().viewWidth() / 2, root().viewHeight() / 2);
754     }
755 
756     d->updateContentHeight();
757 
758     PopupWidget::open();
759 }
760 
preparePanelForOpening()761 void DialogWidget::preparePanelForOpening()
762 {
763     PopupWidget::preparePanelForOpening();
764 
765     // Redo the layout (items visible now).
766     d->buttons->updateLayout();
767     d->extraButtons->updateLayout();
768 
769     d->updateBackground();
770 }
771 
finish(int result)772 void DialogWidget::finish(int result)
773 {
774     root().setFocus(nullptr);
775     close();
776 
777     d->untrapper.reset();
778 
779     if (result > 0)
780     {
781         // Success!
782         if (d->acceptAction)
783         {
784             AutoRef<de::Action> held = *d->acceptAction;
785             held->trigger();
786         }
787     }
788 }
789 
ButtonItem(RoleFlags flags,String const & label)790 DialogWidget::ButtonItem::ButtonItem(RoleFlags flags, String const &label)
791     : ui::ActionItem(itemSemantics(flags), label, 0)
792     , _role(flags)
793 {}
794 
ButtonItem(RoleFlags flags,Image const & image)795 DialogWidget::ButtonItem::ButtonItem(RoleFlags flags, Image const &image)
796     : ui::ActionItem(itemSemantics(flags), image)
797     , _role(flags)
798 {}
799 
ButtonItem(RoleFlags flags,String const & label,RefArg<de::Action> action)800 DialogWidget::ButtonItem::ButtonItem(RoleFlags flags, String const &label, RefArg<de::Action> action)
801     : ui::ActionItem(itemSemantics(flags), label, action)
802     , _role(flags)
803 {}
804 
ButtonItem(RoleFlags flags,Image const & image,RefArg<de::Action> action)805 DialogWidget::ButtonItem::ButtonItem(RoleFlags flags, Image const &image, RefArg<de::Action> action)
806     : ui::ActionItem(itemSemantics(flags), image, "", action)
807     , _role(flags)
808 {}
809 
ButtonItem(RoleFlags flags,Image const & image,String const & label,RefArg<de::Action> action)810 DialogWidget::ButtonItem::ButtonItem(RoleFlags flags, Image const &image, String const &label, RefArg<de::Action> action)
811     : ui::ActionItem(itemSemantics(flags), image, label, action)
812     , _role(flags)
813 {}
814 
itemSemantics(RoleFlags flags)815 ui::Item::Semantics DialogWidget::ButtonItem::itemSemantics(RoleFlags flags)
816 {
817     Semantics smt = ActivationClosesPopup | ShownAsButton;
818     if (flags & Popup) smt |= ShownAsPopupButton;
819     return smt;
820 }
821 
822 } // namespace de
823