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