1 /** @file popupwidget.cpp
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/PopupWidget"
20 #include "de/ButtonWidget"
21 #include "de/GuiRootWidget"
22 #include "de/Style"
23 #include "de/BaseGuiApp"
24 
25 #include <de/Drawable>
26 #include <de/MouseEvent>
27 #include <de/AnimationRule>
28 #include <de/Garbage>
29 #include <de/math.h>
30 #include <QTimer>
31 
32 namespace de {
33 
DENG_GUI_PIMPL(PopupWidget)34 DENG_GUI_PIMPL(PopupWidget)
35 {
36     ColorTheme colorTheme = Normal;
37     bool flexibleDir = true;
38     bool deleteAfterDismiss = false;
39     bool clickToClose = true;
40     bool outsideClickOngoing = false;
41     DotPath outlineColorId;
42     ColorBank::Colorf outlineColor;
43     SafeWidgetPtr<Widget> realParent;
44     RuleRectangle anchor;
45     Rule const *marker;
46     ButtonWidget *close = nullptr;
47 
48     Impl(Public *i) : Base(i)
49     {
50         // Style.
51         marker = &rule("gap");
52     }
53 
54     void flipOpeningDirectionIfNeeded()
55     {
56         ui::Direction openDir = self().openingDirection();
57 
58         // Opening direction depends on the anchor position: popup will open to
59         // direction that has more space available.
60         switch (openDir)
61         {
62         case ui::Up:
63             if (anchor.midY().value() < self().root().viewHeight().value()/2)
64             {
65                 openDir = ui::Down;
66             }
67             break;
68 
69         case ui::Down:
70             if (anchor.midY().value() > self().root().viewHeight().value()/2)
71             {
72                 openDir = ui::Up;
73             }
74             break;
75 
76         case ui::Left:
77             if (anchor.midX().value() < self().root().viewWidth().value()/2)
78             {
79                 openDir = ui::Right;
80             }
81             break;
82 
83         case ui::Right:
84             if (anchor.midX().value() > self().root().viewWidth().value()/2)
85             {
86                 openDir = ui::Left;
87             }
88             break;
89 
90         default:
91             break;
92         }
93 
94         self().setOpeningDirection(openDir);
95     }
96 
97     typedef Vector2<Rule const *> Vector2R;
98 
99     Vector2R anchorRule() const
100     {
101         switch (self().openingDirection())
102         {
103         case ui::Up:
104             return Vector2R(&anchor.midX(), &anchor.top());
105 
106         case ui::Down:
107             return Vector2R(&anchor.midX(), &anchor.bottom());
108 
109         case ui::Left:
110             return Vector2R(&anchor.left(), &anchor.midY());
111 
112         case ui::Right:
113             return Vector2R(&anchor.right(), &anchor.midY());
114 
115         default:
116             break;
117         }
118 
119         return Vector2R(&anchor.midX(), &anchor.midY());
120     }
121 
122     Vector2i anchorPos() const
123     {
124         auto rule = anchorRule();
125         return Vector2i(rule.x->valuei(), rule.y->valuei());
126     }
127 
128     void updateLayout()
129     {
130         self().rule()
131                 .clearInput(Rule::Left)
132                 .clearInput(Rule::Right)
133                 .clearInput(Rule::Top)
134                 .clearInput(Rule::Bottom)
135                 .clearInput(Rule::AnchorX)
136                 .clearInput(Rule::AnchorY);
137 
138         auto anchorPos = anchorRule();
139 
140         switch (self().openingDirection())
141         {
142         case ui::Up:
143             self().rule()
144                     .setInput(Rule::Bottom, OperatorRule::maximum(
145                                   *anchorPos.y - *marker,
146                                   self().rule().height()))
147                     .setInput(Rule::Left, OperatorRule::clamped(
148                                   *anchorPos.x - self().rule().width() / 2,
149                                   self().margins().left(),
150                                   self().root().viewWidth() - self().rule().width() - self().margins().right()));
151             break;
152 
153         case ui::Down:
154             self().rule()
155                     .setInput(Rule::Top,  OperatorRule::minimum(
156                                   *anchorPos.y + *marker,
157                                   self().root().viewHeight() - self().rule().height() - self().margins().bottom()))
158                     .setInput(Rule::Left, OperatorRule::clamped(
159                                   *anchorPos.x - self().rule().width() / 2,
160                                   self().margins().left(),
161                                   self().root().viewWidth() - self().rule().width() - self().margins().right()));
162             break;
163 
164         case ui::Left:
165             self().rule()
166                     .setInput(Rule::Right, OperatorRule::maximum(
167                                   *anchorPos.x - *marker,
168                                   self().rule().width()))
169                     .setInput(Rule::Top, OperatorRule::clamped(
170                                   *anchorPos.y - self().rule().height() / 2,
171                                   self().margins().top(),
172                                   self().root().viewHeight() - self().rule().height() -
173                                   self().margins().bottom() + self().margins().top()));
174             break;
175 
176         case ui::Right:
177             self().rule()
178                     .setInput(Rule::Left, OperatorRule::minimum(
179                                   *anchorPos.x + *marker,
180                                   self().root().viewWidth() - self().rule().width() - self().margins().right()))
181                     .setInput(Rule::Top,  OperatorRule::clamped(
182                                   *anchorPos.y - self().rule().height() / 2,
183                                   self().margins().top(),
184                                   self().root().viewHeight() - self().rule().height() - self().margins().bottom()));
185             break;
186 
187         case ui::NoDirection:
188             self().rule().setMidAnchorX(*anchorPos.x)
189                        .setMidAnchorY(*anchorPos.y);
190             break;
191         }
192     }
193 
194     void updateStyle()
195     {
196         Style const &st = style();
197         bool const opaqueBackground = (self().levelOfNesting() > 0);
198 
199         outlineColor = st.colors().colorf(outlineColorId);
200 
201         if (colorTheme == Inverted)
202         {
203             self().set(self().infoStyleBackground());
204         }
205         else
206         {
207             Background bg(st.colors().colorf("background"),
208                           !opaqueBackground && st.isBlurringAllowed()?
209                               Background::SharedBlurWithBorderGlow : Background::BorderGlow,
210                           st.colors().colorf("glow"),
211                           st.rules().rule("glow").valuei());
212             bg.blur = style().sharedBlurWidget();
213             self().set(bg);
214         }
215 
216         if (opaqueBackground)
217         {
218             // If nested, use an opaque background.
219             self().set(self().background().withSolidFillOpacity(1));
220         }
221     }
222 };
223 
PopupWidget(String const & name)224 PopupWidget::PopupWidget(String const &name) : PanelWidget(name), d(new Impl(this))
225 {
226     setOpeningDirection(ui::Up);
227     d->updateStyle();
228 }
229 
levelOfNesting() const230 int PopupWidget::levelOfNesting() const
231 {
232     int nesting = 0;
233     // GuiRootWidget is not a GuiWidget; root widget never has a parent.
234     for (GuiWidget const *p = d->realParent && d->realParent->parent()?
235                 static_cast<GuiWidget const *>(d->realParent.get()) : parentGuiWidget();
236          p; p = p->parentGuiWidget())
237     {
238         if (is<PopupWidget>(p))
239         {
240             ++nesting;
241         }
242     }
243     return nesting;
244 }
245 
setAnchorAndOpeningDirection(RuleRectangle const & rule,ui::Direction dir)246 void PopupWidget::setAnchorAndOpeningDirection(RuleRectangle const &rule, ui::Direction dir)
247 {
248     d->anchor.setRect(rule);
249     setOpeningDirection(dir);
250 }
251 
setAllowDirectionFlip(bool flex)252 void PopupWidget::setAllowDirectionFlip(bool flex)
253 {
254     d->flexibleDir = flex;
255 }
256 
setAnchor(Vector2i const & pos)257 void PopupWidget::setAnchor(Vector2i const &pos)
258 {
259     d->anchor.setLeftTop(Const(pos.x), Const(pos.y));
260     d->anchor.setRightBottom(d->anchor.left(), d->anchor.top());
261 }
262 
setAnchorX(int xPos)263 void PopupWidget::setAnchorX(int xPos)
264 {
265     d->anchor.setInput(Rule::Left,  Const(xPos))
266              .setInput(Rule::Right, Const(xPos));
267 }
268 
setAnchorY(int yPos)269 void PopupWidget::setAnchorY(int yPos)
270 {
271     d->anchor.setInput(Rule::Top,    Const(yPos))
272              .setInput(Rule::Bottom, Const(yPos));
273 }
274 
setAnchor(Rule const & x,Rule const & y)275 void PopupWidget::setAnchor(Rule const &x, Rule const &y)
276 {
277     setAnchorX(x);
278     setAnchorY(y);
279 }
280 
setAnchorX(Rule const & x)281 void PopupWidget::setAnchorX(Rule const &x)
282 {
283     d->anchor.setInput(Rule::Left,   x)
284              .setInput(Rule::Right,  x);
285 }
286 
setAnchorY(Rule const & y)287 void PopupWidget::setAnchorY(Rule const &y)
288 {
289     d->anchor.setInput(Rule::Top,    y)
290              .setInput(Rule::Bottom, y);
291 }
292 
anchor() const293 RuleRectangle const &PopupWidget::anchor() const
294 {
295     return d->anchor;
296 }
297 
detachAnchor()298 void PopupWidget::detachAnchor()
299 {
300     setAnchor(d->anchorPos());
301     d->updateLayout();
302 }
303 
setDeleteAfterDismissed(bool deleteAfterDismiss)304 void PopupWidget::setDeleteAfterDismissed(bool deleteAfterDismiss)
305 {
306     d->deleteAfterDismiss = deleteAfterDismiss;
307 }
308 
setClickToClose(bool clickCloses)309 void PopupWidget::setClickToClose(bool clickCloses)
310 {
311     d->clickToClose = clickCloses;
312 }
313 
useInfoStyle(bool yes)314 void PopupWidget::useInfoStyle(bool yes)
315 {
316     setColorTheme(yes? Inverted : Normal);
317 }
318 
isUsingInfoStyle()319 bool PopupWidget::isUsingInfoStyle()
320 {
321     return d->colorTheme == Inverted;
322 }
323 
setColorTheme(ColorTheme theme)324 void PopupWidget::setColorTheme(ColorTheme theme)
325 {
326     d->colorTheme = theme;
327     if (d->close) d->close->setColorTheme(theme);
328     d->updateStyle();
329 }
330 
colorTheme() const331 GuiWidget::ColorTheme PopupWidget::colorTheme() const
332 {
333     return d->colorTheme;
334 }
335 
setOutlineColor(const DotPath & outlineColor)336 void PopupWidget::setOutlineColor(const DotPath &outlineColor)
337 {
338     d->outlineColorId = outlineColor;
339     d->updateStyle();
340 }
341 
setCloseButtonVisible(bool enable)342 void PopupWidget::setCloseButtonVisible(bool enable)
343 {
344     if (enable && !d->close)
345     {
346         d->close = new ButtonWidget;
347         d->close->setColorTheme(d->colorTheme);
348         d->close->setStyleImage("close.ringless", "small");
349         d->close->margins().set("dialog.gap").setTopBottom(RuleBank::UNIT);
350         d->close->setImageColor(d->close->textColorf());
351         d->close->setSizePolicy(ui::Expand, ui::Expand);
352         d->close->setActionFn([this] () { close(); });
353         d->close->rule()
354                 .setInput(Rule::Top,   rule().top()   + margins().top())
355                 .setInput(Rule::Right, rule().right() - margins().right());
356         add(d->close);
357     }
358     else if (!enable && d->close)
359     {
360         delete d->close;
361         d->close = nullptr;
362     }
363 }
364 
closeButton()365 ButtonWidget &PopupWidget::closeButton()
366 {
367     setCloseButtonVisible(true);
368     return *d->close;
369 }
370 
offerFocus()371 void PopupWidget::offerFocus()
372 {
373     if (d->close)
374     {
375         root().setFocus(d->close);
376     }
377 }
378 
infoStyleBackground() const379 GuiWidget::Background PopupWidget::infoStyleBackground() const
380 {
381     return Background(style().colors().colorf("popup.info.background"),
382                       Background::BorderGlow,
383                       style().colors().colorf("popup.info.glow"),
384                       rule("glow").valuei());
385 }
386 
handleEvent(Event const & event)387 bool PopupWidget::handleEvent(Event const &event)
388 {
389     if (!isOpen()) return false;
390 
391     // Popups eat all mouse button events.
392     if (event.type() == Event::MouseButton)
393     {
394         //MouseEvent const &mouse = event.as<MouseEvent>();
395         bool const inside = hitTest(event);
396 
397         if (!inside && d->clickToClose)
398         {
399             close(0.1);
400         }
401     }
402 
403     if (event.type() == Event::KeyPress  ||
404         event.type() == Event::KeyRepeat ||
405         event.type() == Event::KeyRelease)
406     {
407         KeyEvent const &key = event.as<KeyEvent>();
408         if (event.isKeyDown() &&
409             (key.ddKey() == DDKEY_ESCAPE ||
410              key.ddKey() == DDKEY_ENTER  ||
411              key.ddKey() == DDKEY_RETURN ||
412              key.ddKey() == ' '))
413         {
414             close();
415             return true;
416         }
417 
418         // Popups should still allow global key bindings to be activated.
419         root().handleEventAsFallback(event);
420 
421         // Don't pass it further, though.
422         return true;
423     }
424 
425     return PanelWidget::handleEvent(event);
426 }
427 
glMakeGeometry(GuiVertexBuilder & verts)428 void PopupWidget::glMakeGeometry(GuiVertexBuilder &verts)
429 {
430     if (rule().recti().isNull()) return; // Still closed.
431 
432     PanelWidget::glMakeGeometry(verts);
433 
434     ui::Direction const dir = openingDirection();
435     if (dir == ui::NoDirection) return;
436 
437     // Anchor triangle.
438     GuiVertexBuilder tri;
439     GuiVertex v;
440 
441     v.rgba = background().solidFill;
442     v.texCoord = root().atlas().imageRectf(root().solidWhitePixel()).middle();
443 
444     int const marker = d->marker->valuei();
445     Vector2i anchorPos = d->anchorPos();
446     bool markerVisible = false;
447 
448     if (dir == ui::Up)
449     {
450         // Can't put the anchor too close to the edges.
451         anchorPos.x = clamp(2 * marker, anchorPos.x, int(root().viewSize().x) - 2*marker);
452 
453         if (anchorPos.y > rule().bottom().valuei())
454         {
455             v.pos = anchorPos; tri << v;
456             v.pos = anchorPos + Vector2i(-marker, -marker); tri << v;
457             v.pos = anchorPos + Vector2i(marker, -marker); tri << v;
458             markerVisible = true;
459         }
460     }
461     else if (dir == ui::Left)
462     {
463         // The anchor may still get clamped out of sight.
464         if (anchorPos.x > rule().right().valuei())
465         {
466             v.pos = anchorPos; tri << v;
467             v.pos = anchorPos + Vector2i(-marker, marker); tri << v;
468             v.pos = anchorPos + Vector2i(-marker, -marker); tri << v;
469             markerVisible = true;
470         }
471     }
472     else if (dir == ui::Right)
473     {
474         if (anchorPos.x < rule().left().valuei())
475         {
476             v.pos = anchorPos; tri << v;
477             v.pos = anchorPos + Vector2i(marker, -marker); tri << v;
478             v.pos = anchorPos + Vector2i(marker, marker); tri << v;
479             markerVisible = true;
480         }
481     }
482     else
483     {
484         if (anchorPos.y < rule().top().valuei())
485         {
486             v.pos = anchorPos; tri << v;
487             v.pos = anchorPos + Vector2i(marker, marker); tri << v;
488             v.pos = anchorPos + Vector2i(-marker, marker); tri << v;
489             markerVisible = true;
490         }
491     }
492 
493     // Outline.
494     if (d->outlineColor.w > 0.f)
495     {
496         tri << v; // discontinued
497 
498         Rectanglei const rect = rule().recti();
499         int const ow = GuiWidget::pointsToPixels(2);
500         int const halfOw = ow/2;
501         int const midOw = ow + halfOw;
502 
503         v.rgba = d->outlineColor;
504 
505         // Top edge.
506         v.pos = rect.topLeft + Vector2i(-ow, -ow); tri << v << v;
507         v.pos = rect.topLeft; tri << v;
508 
509         if (markerVisible && dir == ui::Down)
510         {
511             v.pos = Vector2i(anchorPos.x - marker - halfOw, rect.top() - ow); tri << v;
512             v.pos += Vector2i(halfOw, ow); tri << v;
513 
514             v.pos = anchorPos + Vector2i(0, -midOw); tri << v;
515             v.pos.y += midOw; tri << v;
516 
517             v.pos = Vector2i(anchorPos.x + marker + halfOw, rect.top() - ow); tri << v;
518             v.pos += Vector2i(-halfOw, ow); tri << v;
519         }
520 
521         // Right edge.
522         v.pos = rect.topRight() + Vector2i(ow, -ow); tri << v;
523         v.pos = rect.topRight(); tri << v;
524 
525         if (markerVisible && dir == ui::Left)
526         {
527             v.pos = Vector2i(rect.right() + ow, anchorPos.y - marker - halfOw); tri << v;
528             v.pos += Vector2i(-ow, halfOw); tri << v;
529 
530             v.pos = anchorPos + Vector2i(midOw, 0); tri << v;
531             v.pos.x += -midOw; tri << v;
532 
533             v.pos = Vector2i(rect.right() + ow, anchorPos.y + marker + halfOw); tri << v;
534             v.pos += Vector2i(-ow, -halfOw); tri << v;
535         }
536 
537         // Bottom edge.
538         v.pos = rect.bottomRight + Vector2i(ow, ow); tri << v;
539         v.pos = rect.bottomRight; tri << v;
540 
541         if (markerVisible && dir == ui::Up)
542         {
543             v.pos = Vector2i(anchorPos.x + marker + halfOw, rect.bottom() + ow); tri << v;
544             v.pos += Vector2i(-halfOw, -ow); tri << v;
545 
546             v.pos = anchorPos + Vector2i(0, midOw); tri << v;
547             v.pos.y += -midOw; tri << v;
548 
549             v.pos = Vector2i(anchorPos.x - marker - halfOw, rect.bottom() + ow); tri << v;
550             v.pos += Vector2i(halfOw, -ow); tri << v;
551         }
552 
553         // Left edge.
554         v.pos = rect.bottomLeft() + Vector2i(-ow, ow); tri << v;
555         v.pos = rect.bottomLeft(); tri << v;
556 
557         if (markerVisible && dir == ui::Right)
558         {
559             v.pos = Vector2i(rect.left() - ow, anchorPos.y + marker + halfOw); tri << v;
560             v.pos += Vector2i(ow, -halfOw); tri << v;
561 
562             v.pos = anchorPos + Vector2i(-midOw, 0); tri << v;
563             v.pos.x += midOw; tri << v;
564 
565             v.pos = Vector2i(rect.left() - ow, anchorPos.y - marker - halfOw); tri << v;
566             v.pos += Vector2i(ow, halfOw); tri << v;
567         }
568 
569         // Back to top.
570         v.pos = rect.topLeft + Vector2i(-ow, -ow); tri << v;
571         v.pos = rect.topLeft; tri << v;
572     }
573 
574     verts += tri;
575 }
576 
updateStyle()577 void PopupWidget::updateStyle()
578 {
579     PanelWidget::updateStyle();
580 
581     d->updateStyle();
582 }
583 
preparePanelForOpening()584 void PopupWidget::preparePanelForOpening()
585 {
586     d->updateStyle();
587 
588     PanelWidget::preparePanelForOpening();
589 
590     if (d->flexibleDir)
591     {
592         d->flipOpeningDirectionIfNeeded();
593     }
594 
595     // Reparent the popup into the root widget, on top of everything else.
596     d->realParent.reset(Widget::parent());
597     DENG2_ASSERT(d->realParent);
598     d->realParent->remove(*this);
599     d->realParent->root().as<GuiRootWidget>().addOnTop(this);
600 
601     d->updateLayout();
602 
603     root().pushFocus();
604     offerFocus();
605 }
606 
panelClosing()607 void PopupWidget::panelClosing()
608 {
609     PanelWidget::panelClosing();
610 
611     root().popFocus();
612 }
613 
panelDismissed()614 void PopupWidget::panelDismissed()
615 {
616     PanelWidget::panelDismissed();
617 
618     // Move back to the original parent widget.
619     if (!d->realParent)
620     {
621         // The real parent has been deleted.
622         d->realParent.reset(&root());
623         DENG2_ASSERT(d->realParent);
624     }
625     parentWidget()->remove(*this);
626 
627     if (d->deleteAfterDismiss)
628     {
629         // Don't bother putting it back in the original parent.
630         guiDeleteLater();
631     }
632     else
633     {
634         d->realParent->add(this);
635     }
636 
637     d->realParent.reset();
638 }
639 
640 } // namespace de
641