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