1 /** @file homeitemwidget.cpp
2  *
3  * @authors Copyright (c) 2016-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  *
5  * @par License
6  * GPL: http://www.gnu.org/licenses/gpl.html
7  *
8  * <small>This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by the
10  * Free Software Foundation; either version 2 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 General
14  * Public License for more details. You should have received a copy of the GNU
15  * General Public License along with this program; if not, see:
16  * http://www.gnu.org/licenses</small>
17  */
18 
19 #include "ui/widgets/homeitemwidget.h"
20 #include "ui/widgets/homemenuwidget.h"
21 #include "ui/home/columnwidget.h"
22 #include "resource/idtech1image.h"
23 
24 #include <doomsday/LumpCatalog>
25 #include <doomsday/Game>
26 #include <de/SequentialLayout>
27 #include <QTimer>
28 
29 using namespace de;
30 
DENG_GUI_PIMPL(HomeItemWidget)31 DENG_GUI_PIMPL(HomeItemWidget)
32 , DENG2_OBSERVES(MenuWidget, ItemTriggered)
33 {
34     // Event handler for mouse clicks on the item.
35     struct ClickHandler : public GuiWidget::IEventHandler
36     {
37         Public &owner;
38 
39         ClickHandler(Public &owner)
40             : owner(owner) {}
41 
42         void acquireFocus()
43         {
44             owner.acquireFocus();
45         }
46 
47         bool handleEvent(GuiWidget &widget, Event const &event)
48         {
49             if (widget.isDisabled()) return false;
50 
51             if (event.type() == Event::MouseButton)
52             {
53                 MouseEvent const &mouse = event.as<MouseEvent>();
54                 if (owner.hitTest(event))
55                 {
56                     if (mouse.button() == MouseEvent::Right)
57                     {
58                         switch (widget.handleMouseClick(event, MouseEvent::Right))
59                         {
60                         case MouseClickStarted:
61                             acquireFocus();
62                             return true;
63 
64                         case MouseClickAborted:
65                             return true;
66 
67                         case MouseClickFinished:
68                             owner.itemRightClicked();
69                             emit owner.openContextMenu();
70                             return true;
71 
72                         default:
73                             return false; // Ignore.
74                         }
75                     }
76 
77                     if (mouse.state() == MouseEvent::Pressed ||
78                         mouse.state() == MouseEvent::DoubleClick)
79                     {
80                         acquireFocus();
81                     }
82                     if (mouse.state()  == MouseEvent::DoubleClick &&
83                         mouse.button() == MouseEvent::Left)
84                     {
85                         emit owner.doubleClicked();
86                         return true;
87                     }
88                 }
89             }
90             return false;
91         }
92     };
93 
94     Flags flags;
95     AssetGroup assets;
96     LabelWidget *background;
97     LabelWidget *icon { nullptr };
98     LabelWidget *label;
99     QList<GuiWidget *> buttons;
100     AnimationRule *labelRightMargin;
101     IndirectRule *labelMinRightMargin = new IndirectRule;
102     Rule const *buttonsWidth = nullptr;
103     bool selected = false;
104     bool keepButtonsVisible = false;
105     bool buttonsShown = false;
106     DotPath bgColor           { "transparent" };
107     DotPath selectedBgColor   { "background" };
108     DotPath textColor         { "text" };
109     DotPath selectedTextColor { "text" };
110     QTimer buttonHideTimer;
111 
112     Impl(Public *i, Flags flags) : Base(i), flags(flags)
113     {
114         labelRightMargin = new AnimationRule(0);
115 
116         self().add(background = new LabelWidget);
117         if (!(flags & WithoutIcon))
118         {
119             self().add(icon = new LabelWidget);
120         }
121         self().add(label = new LabelWidget);
122 
123         // Observe state of the labels.
124         assets += *background;
125         assets += *label;
126 
127         if (icon)
128         {
129             assets += *icon;
130 
131             icon->setBehavior(ContentClipping);
132             icon->setImageFit(ui::CoverArea | ui::OriginalAspectRatio);
133             icon->setSizePolicy(ui::Filled, ui::Filled);
134             icon->margins().setZero();
135         }
136 
137         label->setSizePolicy(ui::Filled, ui::Expand);
138         label->setTextLineAlignment(ui::AlignLeft);
139         label->setAlignment(ui::AlignLeft);
140         label->setBehavior(ChildVisibilityClipping);
141 
142         //background->setBehavior(Focusable);
143 
144         buttonHideTimer.setSingleShot(true);
145         QObject::connect(&buttonHideTimer, &QTimer::timeout, [this] ()
146         {
147             for (auto *button : buttons)
148             {
149                 button->setAttribute(DontDrawContent);
150             }
151         });
152     }
153 
154     ~Impl()
155     {
156         releaseRef(labelRightMargin);
157         releaseRef(labelMinRightMargin);
158         releaseRef(buttonsWidth);
159     }
160 
161     void updateButtonLayout()
162     {
163         SequentialLayout layout(label->rule().right() - *labelRightMargin,
164                                 label->rule().top(), ui::Right);
165         for (auto *button : buttons)
166         {
167             if (!button->behavior().testFlag(Hidden))
168             {
169                 layout << *button;
170             }
171             button->rule().setMidAnchorY(label->rule().midY());
172         }
173         changeRef(buttonsWidth, layout.width() + rule("gap"));
174 
175         if (buttonsShown)
176         {
177             labelRightMargin->set(*buttonsWidth,
178                                   labelRightMargin->animation().done()? TimeSpan(0.4) :
179                                   labelRightMargin->animation().remainingTime());
180         }
181     }
182 
183     void showButtons(bool show)
184     {
185         buttonsShown = show;
186         if (!buttonsWidth) return;
187 
188         if (show)
189         {
190             buttonHideTimer.stop();
191             for (auto *button : buttons)
192             {
193                 button->setAttribute(DontDrawContent, false);
194                 button->setBehavior(Focusable);
195             }
196         }
197         else
198         {
199             for (auto *button : buttons)
200             {
201                 button->setBehavior(Focusable, false);
202             }
203         }
204 
205         TimeSpan const SPAN = (self().hasBeenUpdated()? 0.4 : 0.0);
206         if (show)
207         {
208             labelRightMargin->set(*buttonsWidth, SPAN/2);
209         }
210         else
211         {
212             labelRightMargin->set(-rule("halfunit"), SPAN);
213             buttonHideTimer.setInterval(SPAN.asMilliSeconds());
214             buttonHideTimer.start();
215         }
216     }
217 
218     void menuItemTriggered(ui::Item const &actionItem) override
219     {
220         // Let the parent menu know which of its items is being interacted with.
221         self().parentMenu()->setInteractedItem(self().parentMenu()->organizer()
222                                              .findItemForWidget(self()),
223                                              &actionItem);
224     }
225 
226     void updateColors()
227     {
228         auto bg = Background(style().colors().colorf(selected? selectedBgColor : bgColor));
229         background->set(bg);
230         label->setTextColor(selected? selectedTextColor : textColor);
231         // Icon matches text color.
232         if (icon)
233         {
234             icon->setImageColor(label->textColorf());
235             icon->set(bg);
236         }
237     }
238 
239     /**
240      * Determines if this item is inside a Home column. This is not true if the item
241      * is used in a standalone dialog, for example.
242      */
243     bool hasColumnAncestor() const
244     {
245         for (Widget *i = self().parentWidget(); i; i = i->parent())
246         {
247             if (is<ColumnWidget>(i)) return true;
248         }
249         return false;
250     }
251 };
252 
HomeItemWidget(Flags flags,String const & name)253 HomeItemWidget::HomeItemWidget(Flags flags, String const &name)
254     : GuiWidget(name)
255     , d(new Impl(this, flags))
256 {
257     setBehavior(Focusable | ContentClipping);
258     setAttribute(AutomaticOpacity);
259     addEventHandler(new Impl::ClickHandler(*this));
260 
261     AutoRef<Rule> height;
262     if (flags.testFlag(AnimatedHeight))
263     {
264         height.reset(new AnimationRule(d->label->rule().height(), 0.3));
265     }
266     else
267     {
268         height.reset(d->label->rule().height());
269     }
270 
271     d->background->rule()
272             .setInput(Rule::Top,    rule().top())
273             .setInput(Rule::Height, height)
274             .setInput(Rule::Left,   d->icon? d->icon->rule().right() : rule().left())
275             .setInput(Rule::Right,  rule().right());
276 
277     if (d->icon)
278     {
279         d->icon->rule()
280                 .setSize(d->label->margins().height() +
281                          style().fonts().font("default").height() +
282                          style().fonts().font("default").lineSpacing(),
283                          height)
284                 .setInput(Rule::Left, rule().left())
285                 .setInput(Rule::Top,  rule().top());
286         d->icon->set(Background(Background::BorderGlow,
287                                 style().colors().colorf("home.icon.shadow"), 20));
288     }
289 
290     d->label->rule()
291             .setInput(Rule::Top,   rule().top())
292             .setInput(Rule::Left,  d->icon? d->icon->rule().right() : rule().left())
293             .setInput(Rule::Right, rule().right());
294     d->label->margins().setRight(OperatorRule::maximum(*d->labelMinRightMargin,
295                                                        *d->labelRightMargin) + rule("gap"));
296 
297     // Use an animated height rule for smoother list layout behavior.
298     rule().setInput(Rule::Height, height);
299 }
300 
assets()301 AssetGroup &HomeItemWidget::assets()
302 {
303     return d->assets;
304 }
305 
icon()306 LabelWidget &HomeItemWidget::icon()
307 {
308     DENG2_ASSERT(d->icon);
309     return *d->icon;
310 }
311 
label()312 LabelWidget &HomeItemWidget::label()
313 {
314     return *d->label;
315 }
316 
label() const317 LabelWidget const &HomeItemWidget::label() const
318 {
319     return *d->label;
320 }
321 
setSelected(bool selected)322 void HomeItemWidget::setSelected(bool selected)
323 {
324     if (d->selected != selected)
325     {
326         d->selected = selected;
327         if (selected)
328         {
329             d->showButtons(true);
330         }
331         else if (!d->keepButtonsVisible)
332         {
333             d->showButtons(false);
334         }
335         d->updateColors();
336     }
337 }
338 
isSelected() const339 bool HomeItemWidget::isSelected() const
340 {
341     return d->selected;
342 }
343 
useNormalStyle()344 void HomeItemWidget::useNormalStyle()
345 {
346     useColorTheme(Normal);
347 }
348 
useInvertedStyle()349 void HomeItemWidget::useInvertedStyle()
350 {
351     useColorTheme(Inverted);
352 }
353 
useColorTheme(ColorTheme style)354 void HomeItemWidget::useColorTheme(ColorTheme style)
355 {
356     useColorTheme(style, style);
357 }
358 
useColorTheme(ColorTheme unselected,ColorTheme selected)359 void HomeItemWidget::useColorTheme(ColorTheme unselected, ColorTheme selected)
360 {
361     // Color for a non-selected item.
362     if (unselected == Inverted)
363     {
364         d->bgColor   = "inverted.background";
365         d->textColor = "inverted.text";
366     }
367     else
368     {
369         d->bgColor   = "transparent";
370         d->textColor = "text";
371     }
372 
373     // Color for a selected item.
374     if (selected == Inverted)
375     {
376         d->selectedBgColor   = "home.item.background.selected.inverted";
377         d->selectedTextColor = "inverted.text";
378     }
379     else
380     {
381         d->selectedBgColor   = "background";
382         d->selectedTextColor = "text";
383     }
384 
385     d->updateColors();
386 }
387 
textColorId() const388 DotPath const &HomeItemWidget::textColorId() const
389 {
390     return d->textColor;
391 }
392 
acquireFocus()393 void HomeItemWidget::acquireFocus()
394 {
395     root().setFocus(this);
396 }
397 
parentMenu()398 HomeMenuWidget *HomeItemWidget::parentMenu()
399 {
400     return maybeAs<HomeMenuWidget>(parentWidget());
401 }
402 
handleEvent(Event const & event)403 bool HomeItemWidget::handleEvent(Event const &event)
404 {
405     if (hasFocus() && event.isKey())
406     {
407         auto const &key = event.as<KeyEvent>();
408 
409         if (key.ddKey() == DDKEY_LEFTARROW || key.ddKey() == DDKEY_RIGHTARROW ||
410             key.ddKey() == DDKEY_UPARROW   || key.ddKey() == DDKEY_DOWNARROW)
411         {
412             if ( ! ((key.ddKey() == DDKEY_UPARROW    && isFirstChild()) ||
413                     (key.ddKey() == DDKEY_DOWNARROW  && isLastChild())  ||
414                     (key.ddKey() == DDKEY_LEFTARROW  && !d->hasColumnAncestor()) ||
415                     (key.ddKey() == DDKEY_RIGHTARROW && !d->hasColumnAncestor())) )
416             {
417                 // Fall back to menu and HomeWidget for navigation.
418                 return false;
419             }
420         }
421     }
422     return GuiWidget::handleEvent(event);
423 }
424 
focusGained()425 void HomeItemWidget::focusGained()
426 {
427     setSelected(true);
428     emit selected();
429     emit mouseActivity();
430 }
431 
focusLost()432 void HomeItemWidget::focusLost()
433 {
434     //setSelected(false);
435     //emit deselected();
436 }
437 
itemRightClicked()438 void HomeItemWidget::itemRightClicked()
439 {}
440 
addButton(GuiWidget * widget)441 void HomeItemWidget::addButton(GuiWidget *widget)
442 {
443     // Common styling.
444     if (auto *label = maybeAs<LabelWidget>(widget))
445     {
446         label->setSizePolicy(ui::Expand, ui::Expand);
447     }
448 
449     // Observing triggers.
450     if (auto *menu = maybeAs<MenuWidget>(widget))
451     {
452         menu->audienceForItemTriggered() += d;
453     }
454 
455     d->buttons << widget;
456     d->label->add(widget);
457     widget->setAttribute(DontDrawContent);
458     widget->setBehavior(Focusable, false);
459     d->updateButtonLayout();
460 }
461 
buttonWidget(int index) const462 GuiWidget &HomeItemWidget::buttonWidget(int index) const
463 {
464     return *d->buttons.at(index);
465 }
466 
setKeepButtonsVisible(bool yes)467 void HomeItemWidget::setKeepButtonsVisible(bool yes)
468 {
469     d->keepButtonsVisible = yes;
470     if (yes)
471     {
472         d->showButtons(true);
473     }
474     else if (!d->selected)
475     {
476         d->showButtons(false);
477     }
478 }
479 
setLabelMinimumRightMargin(Rule const & rule)480 void HomeItemWidget::setLabelMinimumRightMargin(Rule const &rule)
481 {
482     d->labelMinRightMargin->setSource(rule);
483 }
484 
updateButtonLayout()485 void HomeItemWidget::updateButtonLayout()
486 {
487     d->updateButtonLayout();
488 }
489