1 /** @file page.cpp  UI menu page.
2  *
3  * @authors Copyright © 2005-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  * @authors Copyright © 2005-2014 Daniel Swanson <danij@dengine.net>
5  *
6  * @par License
7  * GPL: http://www.gnu.org/licenses/gpl.html
8  *
9  * <small>This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by the
11  * Free Software Foundation; either version 2 of the License, or (at your
12  * option) any later version. This program is distributed in the hope that it
13  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
15  * Public License for more details. You should have received a copy of the GNU
16  * General Public License along with this program; if not, write to the Free
17  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18  * 02110-1301 USA</small>
19  */
20 
21 #include "common.h"
22 #include "menu/page.h"
23 
24 #include "hu_menu.h"
25 #include "hu_stuff.h"
26 
27 /// @todo Page should not need knowledge of Widget specializations - remove all.
28 #include "menu/widgets/buttonwidget.h"
29 #include "menu/widgets/cvarcoloreditwidget.h"
30 #include "menu/widgets/cvarinlinelistwidget.h"
31 #include "menu/widgets/cvarlineeditwidget.h"
32 #include "menu/widgets/cvarsliderwidget.h"
33 #include "menu/widgets/cvartextualsliderwidget.h"
34 #include "menu/widgets/inlinelistwidget.h"
35 #include "menu/widgets/inputbindingwidget.h"
36 #include "menu/widgets/labelwidget.h"
37 #include "menu/widgets/mobjpreviewwidget.h"
38 
39 #include <de/Animation>
40 
41 using namespace de;
42 
43 namespace common {
44 namespace menu {
45 
46 // Page draw state.
47 static mn_rendstate_t rs;
48 mn_rendstate_t const *mnRendState = &rs;
49 
DENG2_PIMPL(Page)50 DENG2_PIMPL(Page)
51 {
52     String   name; ///< Symbolic name/identifier.
53     Children children;
54 
55     Vector2i   origin;
56     Rectanglei geometry; ///< "Physical" geometry, in fixed 320x200 screen coordinate space.
57     Animation  scrollOrigin;
58     Rectanglei viewRegion;
59     int        leftColumnWidth = SCREENWIDTH * 6 / 10;
60 
61     String title;              ///< Title of this page.
62     Page * previous = nullptr; ///< Previous page.
63     int    focus    = -1;      ///< Index of the currently focused widget else @c -1
64     Flags  flags    = DefaultFlags;
65     int    timer    = 0;
66 
67     fontid_t fonts[MENU_FONT_COUNT]; ///< Predefined. Used by all widgets.
68     uint     colors[MENU_COLOR_COUNT]; ///< Predefined. Used by all widgets.
69 
70     OnActiveCallback onActiveCallback;
71     OnDrawCallback   drawer;
72     CommandResponder cmdResponder;
73 
74     // User data values.
75     QVariant userValue;
76 
77     Impl(Public *i) : Base(i)
78     {
79         fontid_t fontId = FID(GF_FONTA);
80         for(int i = 0; i < MENU_FONT_COUNT; ++i)
81         {
82             fonts[i] = fontId;
83         }
84 
85         de::zap(colors);
86         colors[1] = 1;
87         colors[2] = 2;
88     }
89 
90     ~Impl()
91     {
92         qDeleteAll(children);
93     }
94 
95     void updateAllChildGeometry()
96     {
97         for(Widget *wi : children)
98         {
99             wi->geometry().moveTopLeft(Vector2i(0, 0));
100             wi->updateGeometry();
101         }
102     }
103 
104     /**
105      * Returns the effective line height for the predefined @c MENU_FONT1.
106      *
107      * @param lineOffset  If not @c 0 the line offset is written here.
108      */
109     int lineHeight(int *lineOffset = 0)
110     {
111         /// @todo Kludge: We cannot yet query line height from the font...
112         const fontid_t oldFont = FR_Font();
113         FR_SetFont(self().predefinedFont(MENU_FONT1));
114         int lh = FR_TextHeight("{case}WyQ");
115         if (lineOffset)
116         {
117             *lineOffset = de::max(1.f, .5f + lh * .34f);
118         }
119         // Restore the old font.
120         FR_SetFont(oldFont);
121         return lh;
122     }
123 
124     void applyLayout()
125     {
126         geometry.topLeft = Vector2i(0, 0);
127         geometry.setSize(Vector2ui(0, 0));
128 
129         if (children.empty()) return;
130 
131         if (flags & FixedLayout)
132         {
133             for (Widget *wi : children)
134             {
135                 if (!wi->isHidden())
136                 {
137                     wi->geometry().moveTopLeft(wi->fixedOrigin());
138                     geometry |= wi->geometry();
139                 }
140             }
141             return;
142         }
143 
144         // This page uses a dynamic layout.
145         int       lineOffset;
146         const int lh = lineHeight(&lineOffset);
147         int       prevGroup = children.front()->group();
148         Widget *  prevWidget = nullptr;
149         int       usedColumns = 0; // column flags for current row
150         Vector2i  origin;
151         int       rowHeight = 0;
152 
153         for (auto *wi : children)
154         {
155             if (wi->isHidden())
156             {
157                 continue;
158             }
159 
160             // If the widget has a fixed position, we will ignore it while doing
161             // dynamic layout.
162             if (wi->flags() & Widget::PositionFixed)
163             {
164                 wi->geometry().moveTopLeft(wi->fixedOrigin());
165                 geometry |= wi->geometry();
166                 continue;
167             }
168 
169             // Extra spacing between object groups.
170             if (wi->group() != prevGroup)
171             {
172                 origin.y += lh;
173                 prevGroup = wi->group();
174             }
175 
176             // An additional offset requested?
177             if (wi->flags() & Widget::LayoutOffset)
178             {
179                 origin += wi->fixedOrigin();
180             }
181 
182             int widgetColumns = (wi->flags() & (Widget::LeftColumn | Widget::RightColumn));
183             if (widgetColumns == 0)
184             {
185                 // Use both columns if neither specified.
186                 widgetColumns = Widget::LeftColumn | Widget::RightColumn;
187             }
188 
189             // If this column is already used, move to the next row.
190             if ((usedColumns & widgetColumns) != 0)
191             {
192                 origin.y    += rowHeight;
193                 usedColumns = 0;
194                 rowHeight   = 0;
195             }
196             usedColumns |= widgetColumns;
197 
198             wi->geometry().moveTopLeft(origin);
199             rowHeight = MAX_OF(rowHeight, wi->geometry().height() + lineOffset);
200 
201             if (wi->flags() & Widget::RightColumn)
202             {
203                 // Move widget to the right side.
204                 wi->geometry().move(Vector2i(leftColumnWidth, 0));
205 
206                 if (prevWidget && prevWidget->flags() & Widget::LeftColumn)
207                 {
208                     // Align the shorter widget vertically.
209                     if (prevWidget->geometry().height() < wi->geometry().height())
210                     {
211                         prevWidget->geometry().move(Vector2i(
212                             0, (wi->geometry().height() - prevWidget->geometry().height()) / 2));
213                     }
214                     else
215                     {
216                         wi->geometry().move(Vector2i(
217                             0, (prevWidget->geometry().height() - wi->geometry().height()) / 2));
218                     }
219                 }
220             }
221 
222             geometry |= wi->geometry();
223 
224             prevWidget = wi;
225         }
226 
227         // Center horizontally.
228         this->origin.x = SCREENWIDTH / 2 - geometry.width() / 2;
229     }
230 
231     /// @pre @a wi is a child of this page.
232     void giveChildFocus(Widget *newFocus, bool allowRefocus = false)
233     {
234         DENG2_ASSERT(newFocus != 0);
235 
236         if(Widget *focused = self().focusWidget())
237         {
238             if(focused != newFocus)
239             {
240                 focused->execAction(Widget::FocusLost);
241                 focused->setFlags(Widget::Focused, UnsetFlags);
242             }
243             else if(!allowRefocus)
244             {
245                 return;
246             }
247         }
248 
249         focus = self().indexOf(newFocus);
250         newFocus->setFlags(Widget::Focused);
251         newFocus->execAction(Widget::FocusGained);
252     }
253 
254     void refocus()
255     {
256         // If we haven't yet visited this page then find a child widget to give focus.
257         if(focus < 0)
258         {
259             Widget *newFocus = nullptr;
260 
261             // First look for a child with the default focus flag. There should only be one
262             // but we'll choose the last with this flag...
263             for(Widget *wi : children)
264             {
265                 if(wi->isDisabled()) continue;
266                 if(wi->flags() & Widget::NoFocus) continue;
267                 if(!(wi->flags() & Widget::DefaultFocus)) continue;
268 
269                 newFocus = wi;
270             }
271 
272             // No default focus?
273             if(!newFocus)
274             {
275                 // Find the first focusable child.
276                 for(Widget *wi : children)
277                 {
278                     if(wi->isDisabled()) continue;
279                     if(wi->flags() & Widget::NoFocus) continue;
280 
281                     newFocus = wi;
282                     break;
283                 }
284             }
285 
286             if(newFocus)
287             {
288                 giveChildFocus(newFocus);
289             }
290             else
291             {
292                 LOGDEV_WARNING("No focusable widget");
293             }
294         }
295         else
296         {
297             // We've been here before; re-focus on the last focused object.
298             giveChildFocus(children[focus], true);
299         }
300     }
301 
302     void fetch()
303     {
304         for(Widget *wi : children)
305         {
306             if(CVarToggleWidget *tog = maybeAs<CVarToggleWidget>(wi))
307             {
308                 int value = Con_GetByte(tog->cvarPath()) & (tog->cvarValueMask()? tog->cvarValueMask() : ~0);
309                 tog->setState(value? CVarToggleWidget::Down : CVarToggleWidget::Up);
310                 tog->setText(tog->isDown()? tog->downText() : tog->upText());
311             }
312             if(CVarInlineListWidget *list = maybeAs<CVarInlineListWidget>(wi))
313             {
314                 int itemValue = Con_GetInteger(list->cvarPath());
315                 if(int valueMask = list->cvarValueMask())
316                     itemValue &= valueMask;
317                 list->selectItemByValue(itemValue);
318             }
319             if(CVarLineEditWidget *edit = maybeAs<CVarLineEditWidget>(wi))
320             {
321                 edit->setText(Con_GetString(edit->cvarPath()));
322             }
323             if(CVarSliderWidget *sldr = maybeAs<CVarSliderWidget>(wi))
324             {
325                 float value;
326                 if(sldr->floatMode())
327                     value = Con_GetFloat(sldr->cvarPath());
328                 else
329                     value = Con_GetInteger(sldr->cvarPath());
330                 sldr->setValue(value);
331             }
332             if(CVarColorEditWidget *cbox = maybeAs<CVarColorEditWidget>(wi))
333             {
334                 cbox->setColor(Vector4f(Con_GetFloat(cbox->redCVarPath()),
335                                         Con_GetFloat(cbox->greenCVarPath()),
336                                         Con_GetFloat(cbox->blueCVarPath()),
337                                         (cbox->rgbaMode()? Con_GetFloat(cbox->alphaCVarPath()) : 1.f)));
338             }
339         }
340     }
341 
342 #if 0
343     /**
344      * Determines the size of the menu cursor for a focused widget. If no widget is currently
345      * focused the default cursor size (i.e., the effective line height for @c MENU_FONT1)
346      * is used.
347      *
348      * (Which means this should @em not be called to determine whether the cursor is in use).
349      */
350     int cursorSizeFor(Widget *focused, int lineHeight)
351     {
352         return lineHeight;
353         /*
354         int focusedHeight = focused? focused->geometry().height() : 0;
355 
356         // Ensure the cursor is at least as tall as the effective line height for
357         // the page. This is necessary because some mods replace the menu button
358         // graphics with empty and/or tiny images (e.g., Hell Revealed 2).
359         /// @note Handling this correctly would mean separate physical/visual
360         /// geometries for menu widgets.
361         return de::max(focusedHeight, lineHeight);
362         */
363     }
364 #endif
365 };
366 
Page(String name,Vector2i const & origin,Flags const & flags,const OnDrawCallback & drawer,const CommandResponder & cmdResponder)367 Page::Page(String                  name,
368            Vector2i const &        origin,
369            Flags const &           flags,
370            const OnDrawCallback &  drawer,
371            const CommandResponder &cmdResponder)
372     : d(new Impl(this))
373 {
374     d->origin       = origin;
375     d->name         = name;
376     d->flags        = flags;
377     d->drawer       = drawer;
378     d->cmdResponder = cmdResponder;
379 }
380 
~Page()381 Page::~Page()
382 {}
383 
name() const384 String Page::name() const
385 {
386     return d->name;
387 }
388 
addWidget(Widget * widget)389 Widget &Page::addWidget(Widget *widget)
390 {
391     LOG_AS("Page");
392 
393     DENG2_ASSERT(widget);
394     d->children << widget;
395     widget->setPage(this)
396            .setFlags(Widget::Focused, UnsetFlags); // Not focused initially.
397     return *widget;
398 }
399 
children() const400 Page::Children const &Page::children() const
401 {
402     return d->children;
403 }
404 
setOnActiveCallback(const OnActiveCallback & newCallback)405 void Page::setOnActiveCallback(const OnActiveCallback &newCallback)
406 {
407     d->onActiveCallback = newCallback;
408 }
409 
410 #if __JDOOM__ || __JDOOM64__
subpageText(int page=0,int totalPages=0)411 static inline String subpageText(int page = 0, int totalPages = 0)
412 {
413     if(totalPages <= 0) return "";
414     return String("Page %1/%2").arg(page).arg(totalPages);
415 }
416 #endif
417 
drawNavigation(Vector2i const origin)418 static void drawNavigation(Vector2i const origin)
419 {
420     int const currentPage = 0;//(page->firstObject + page->numVisObjects/2) / page->numVisObjects + 1;
421     int const totalPages  = 1;//(int)ceil((float)page->objectsCount/page->numVisObjects);
422 #if __JDOOM__ || __JDOOM64__
423     DENG2_UNUSED(currentPage);
424 #endif
425 
426     if(totalPages <= 1) return;
427 
428 #if __JDOOM__ || __JDOOM64__
429 
430     DGL_Enable(DGL_TEXTURE_2D);
431     FR_SetFont(FID(GF_FONTA));
432     FR_SetColorv(cfg.common.menuTextColors[1]);
433     FR_SetAlpha(mnRendState->pageAlpha);
434 
435     FR_DrawTextXY3(subpageText(currentPage, totalPages).toUtf8().constData(), origin.x, origin.y,
436                    ALIGN_TOP, Hu_MenuMergeEffectWithDrawTextFlags(0));
437 
438     DGL_Disable(DGL_TEXTURE_2D);
439 #else
440     DGL_Enable(DGL_TEXTURE_2D);
441     DGL_Color4f(1, 1, 1, mnRendState->pageAlpha);
442 
443     GL_DrawPatch( pInvPageLeft[currentPage == 0 || (menuTime & 8)]           , origin - Vector2i(144, 0), ALIGN_RIGHT);
444     GL_DrawPatch(pInvPageRight[currentPage == totalPages-1 || (menuTime & 8)], origin + Vector2i(144, 0), ALIGN_LEFT);
445 
446     DGL_Disable(DGL_TEXTURE_2D);
447 #endif
448 }
449 
drawTitle(String const & title)450 static void drawTitle(String const &title)
451 {
452     if(title.isEmpty()) return;
453 
454     Vector2i origin(SCREENWIDTH / 2, (SCREENHEIGHT / 2) - ((SCREENHEIGHT / 2 - 5) / cfg.common.menuScale));
455 
456     FR_PushAttrib();
457     Hu_MenuDrawPageTitle(title, origin); origin.y += 16;
458     drawNavigation(origin);
459     FR_PopAttrib();
460 }
461 
setupRenderStateForPageDrawing(Page & page,float alpha)462 static void setupRenderStateForPageDrawing(Page &page, float alpha)
463 {
464     rs.pageAlpha   = alpha;
465     rs.textGlitter = cfg.common.menuTextGlitter;
466     rs.textShadow  = cfg.common.menuShadow;
467 
468     for(int i = 0; i < MENU_FONT_COUNT; ++i)
469     {
470         rs.textFonts[i] = page.predefinedFont(mn_page_fontid_t(i));
471     }
472     for(int i = 0; i < MENU_COLOR_COUNT; ++i)
473     {
474         rs.textColors[i] = Vector4f(page.predefinedColor(mn_page_colorid_t(i)), alpha);
475     }
476 
477     // Configure the font renderer (assume state has already been pushed if necessary).
478     FR_SetFont(rs.textFonts[0]);
479     FR_LoadDefaultAttrib();
480     FR_SetLeading(0);
481     FR_SetShadowStrength(rs.textShadow);
482     FR_SetGlitterStrength(rs.textGlitter);
483 }
484 
draw(float alpha,bool showFocusCursor)485 void Page::draw(float alpha, bool showFocusCursor)
486 {
487     alpha = de::clamp(0.f, alpha, 1.f);
488     if(alpha <= .0001f) return;
489 
490     // Object geometry is determined from properties defined in the
491     // render state, so configure render state before we begin.
492     setupRenderStateForPageDrawing(*this, alpha);
493 
494     d->updateAllChildGeometry();
495 
496     // We can now layout the widgets of this page.
497     /// @todo Do not modify the page layout here.
498     d->applyLayout();
499 
500     // Determine the origin of the cursor (this dictates the page scroll location).
501     Widget *focused = focusWidget();
502     if(focused && focused->isHidden())
503     {
504         focused = 0;
505     }
506 
507     Vector2i cursorOrigin;
508     if (focused)
509     {
510         // Determine the origin and dimensions of the cursor.
511         /// @todo Each object should define a focus origin...
512         cursorOrigin.x = -1;
513         cursorOrigin.y = focused->geometry().middle().y;
514 
515         /*
516         /// @kludge
517         /// We cannot yet query the subobjects of the list for these values
518         /// so we must calculate them ourselves, here.
519         if (ListWidget const *list = maybeAs<ListWidget>(focused))
520         {
521             if (focused->isActive() && list->selectionIsVisible())
522             {
523                 FR_PushAttrib();
524                 FR_SetFont(predefinedFont(mn_page_fontid_t(focused->font())));
525                 const int rowHeight = FR_CharHeight('A') * (1+MNDATA_LIST_LEADING);
526                 //cursorOrigin.y += (list->selection() - list->first()) * rowHeight + rowHeight/2;
527                 FR_PopAttrib();
528             }
529         }
530         // kludge end
531         */
532     }
533 
534     DGL_MatrixMode(DGL_MODELVIEW);
535     DGL_PushMatrix();
536     DGL_Translatef(d->origin.x, d->origin.y, 0);
537 
538     // Apply page scroll?
539     if (!(d->flags & NoScroll) && focused)
540     {
541         // Determine available screen region for the page.
542         d->viewRegion.topLeft = Vector2i(0, 0); //d->origin.y);
543         d->viewRegion.setSize(Vector2ui(SCREENWIDTH, SCREENHEIGHT - d->origin.y - 35 /*arbitrary but enough for the help message*/));
544 
545         // Is scrolling in effect?
546         if (d->geometry.height() > d->viewRegion.height())
547         {
548             d->scrollOrigin.setValue(
549                         de::min(de::max(0, int(cursorOrigin.y - d->viewRegion.height() / 2)),
550                                 int(d->geometry.height() - d->viewRegion.height())), .35);
551 
552             DGL_Translatef(0, -d->scrollOrigin, 0);
553         }
554     }
555     else
556     {
557         d->viewRegion = {{0, 0}, {SCREENWIDTH, SCREENHEIGHT}};
558     }
559 
560     // Draw all child widgets that aren't hidden.
561     for (Widget *wi : d->children)
562     {
563         if (!wi->isHidden())
564         {
565             FR_PushAttrib();
566             wi->draw();
567             FR_PopAttrib();
568         }
569     }
570 
571     // How about a focus cursor?
572     /// @todo cursor should be drawn on top of the page drawer.
573     if (showFocusCursor && focused)
574     {
575 #if defined (__JDOOM__) || defined (__JDOOM64__)
576         const float cursorScale = .75f;
577 #else
578         const float cursorScale = 1.f;
579 #endif
580         Hu_MenuDrawFocusCursor(cursorOrigin, cursorScale, alpha);
581     }
582 
583     DGL_MatrixMode(DGL_MODELVIEW);
584     DGL_PopMatrix();
585 
586     drawTitle(d->title);
587 
588     // The page has its own drawer.
589     if (d->drawer)
590     {
591         FR_PushAttrib();
592         d->drawer(*this, d->origin);
593         FR_PopAttrib();
594     }
595 
596     // How about some additional help/information for the focused item?
597     if (focused && !focused->helpInfo().isEmpty())
598     {
599         Vector2i helpOrigin(SCREENWIDTH / 2, SCREENHEIGHT - 5 / cfg.common.menuScale);
600         Hu_MenuDrawPageHelp(focused->helpInfo(), helpOrigin);
601     }
602 }
603 
setTitle(String const & newTitle)604 void Page::setTitle(String const &newTitle)
605 {
606     d->title = newTitle;
607 }
608 
title() const609 String Page::title() const
610 {
611     return d->title;
612 }
613 
setOrigin(Vector2i const & newOrigin)614 void Page::setOrigin(Vector2i const &newOrigin)
615 {
616     d->origin = newOrigin;
617 }
618 
origin() const619 Vector2i Page::origin() const
620 {
621     return d->origin;
622 }
623 
flags() const624 Page::Flags Page::flags() const
625 {
626     return d->flags;
627 }
628 
viewRegion() const629 Rectanglei Page::viewRegion() const
630 {
631     if (d->flags & NoScroll)
632     {
633         return {{0, 0}, {SCREENWIDTH, SCREENHEIGHT}};
634     }
635     return d->viewRegion.moved({0, int(d->scrollOrigin)});
636 }
637 
setX(int x)638 void Page::setX(int x)
639 {
640     d->origin.x = x;
641 }
642 
setY(int y)643 void Page::setY(int y)
644 {
645     d->origin.y = y;
646 }
647 
setLeftColumnWidth(float columnWidthPercentage)648 void Page::setLeftColumnWidth(float columnWidthPercentage)
649 {
650     d->leftColumnWidth = int(SCREENWIDTH * columnWidthPercentage);
651 }
652 
setPreviousPage(Page * newPrevious)653 void Page::setPreviousPage(Page *newPrevious)
654 {
655     d->previous = newPrevious;
656 }
657 
previousPage() const658 Page *Page::previousPage() const
659 {
660     return d->previous;
661 }
662 
focusWidget()663 Widget *Page::focusWidget()
664 {
665     if(d->children.isEmpty() || d->focus < 0) return 0;
666     return d->children[d->focus];
667 }
668 
findWidget(int flags,int group)669 Widget &Page::findWidget(int flags, int group)
670 {
671     if(Widget *wi = tryFindWidget(flags, group))
672     {
673         return *wi;
674     }
675     throw Error("Page::findWidget", QString("Failed to locate widget in group #%1 with flags %2").arg(group).arg(flags));
676 }
677 
tryFindWidget(int flags,int group)678 Widget *Page::tryFindWidget(int flags, int group)
679 {
680     for(Widget *wi : d->children)
681     {
682         if(wi->group() == group && int(wi->flags() & flags) == flags)
683             return wi;
684     }
685     return 0; // Not found.
686 }
687 
setFocus(Widget * newFocus)688 void Page::setFocus(Widget *newFocus)
689 {
690     // Are we clearing focus?
691     if(!newFocus)
692     {
693         if(Widget *focused = focusWidget())
694         {
695             if(focused->isActive()) return;
696         }
697 
698         d->focus = -1;
699         for(Widget *wi : d->children)
700         {
701             wi->setFlags(Widget::Focused, UnsetFlags);
702         }
703         d->refocus();
704         return;
705     }
706 
707     int index = indexOf(newFocus);
708     if(index < 0)
709     {
710         DENG2_ASSERT(!"Page::Focus: Failed to determine index-in-page for widget.");
711         return;
712     }
713     d->giveChildFocus(d->children[index]);
714 }
715 
activate()716 void Page::activate()
717 {
718     LOG_AS("Page");
719 
720     d->fetch();
721 
722     // Reset page timer.
723     d->timer = 0;
724 
725     if (d->children.empty())
726     {
727         return; // Presumably the widgets will be added later...
728     }
729 
730     // Notify widgets on the page.
731     for (Widget *wi : d->children)
732     {
733         wi->pageActivated();
734     }
735 
736     d->refocus();
737 
738     if (d->onActiveCallback)
739     {
740         d->onActiveCallback(*this);
741     }
742 }
743 
tick()744 void Page::tick()
745 {
746     // Call the ticker of each child widget.
747     for (Widget *wi : d->children)
748     {
749         wi->tick();
750     }
751     d->timer++;
752 }
753 
predefinedFont(mn_page_fontid_t id)754 fontid_t Page::predefinedFont(mn_page_fontid_t id)
755 {
756     DENG2_ASSERT(VALID_MNPAGE_FONTID(id));
757     return d->fonts[id];
758 }
759 
setPredefinedFont(mn_page_fontid_t id,fontid_t fontId)760 void Page::setPredefinedFont(mn_page_fontid_t id, fontid_t fontId)
761 {
762     DENG2_ASSERT(VALID_MNPAGE_FONTID(id));
763     d->fonts[id] = fontId;
764 }
765 
predefinedColor(mn_page_colorid_t id)766 Vector3f Page::predefinedColor(mn_page_colorid_t id)
767 {
768     DENG2_ASSERT(VALID_MNPAGE_COLORID(id));
769     uint const colorIndex = d->colors[id];
770     return Vector3f(cfg.common.menuTextColors[colorIndex]);
771 }
772 
timer()773 int Page::timer()
774 {
775     return d->timer;
776 }
777 
handleCommand(menucommand_e cmd)778 int Page::handleCommand(menucommand_e cmd)
779 {
780     // Maybe the currently focused widget will handle this?
781     if(Widget *focused = focusWidget())
782     {
783         if(int result = focused->cmdResponder(cmd))
784             return result;
785     }
786 
787     // Maybe a custom command responder for the page?
788     if(d->cmdResponder)
789     {
790         if(int result = d->cmdResponder(*this, cmd))
791             return result;
792     }
793 
794     // Default/fallback handling for the page:
795     switch(cmd)
796     {
797     case MCMD_NAV_PAGEUP:
798     case MCMD_NAV_PAGEDOWN:
799         /// @todo Why is the sound played here?
800         S_LocalSound(cmd == MCMD_NAV_PAGEUP? SFX_MENU_NAV_UP : SFX_MENU_NAV_DOWN, NULL);
801         return true;
802 
803     case MCMD_NAV_UP:
804     case MCMD_NAV_DOWN:
805         // Page navigation requires a focused widget.
806         if (Widget *focused = focusWidget())
807         {
808             int i = 0, giveFocus = indexOf(focused);
809             do
810             {
811                 giveFocus += (cmd == MCMD_NAV_UP? -1 : 1);
812                 if(giveFocus < 0)
813                     giveFocus = d->children.count() - 1;
814                 else if(giveFocus >= d->children.count())
815                     giveFocus = 0;
816             } while(++i < d->children.count() && (d->children[giveFocus]->flags() & (Widget::Disabled | Widget::NoFocus | Widget::Hidden)));
817 
818             if (giveFocus != indexOf(focusWidget()))
819             {
820                 S_LocalSound(cmd == MCMD_NAV_UP? SFX_MENU_NAV_UP : SFX_MENU_NAV_DOWN, NULL);
821                 setFocus(d->children[giveFocus]);
822                 d->timer = 0;
823             }
824         }
825         return true;
826 
827     case MCMD_NAV_OUT:
828         if(!d->previous)
829         {
830             S_LocalSound(SFX_MENU_CLOSE, NULL);
831             Hu_MenuCommand(MCMD_CLOSE);
832         }
833         else
834         {
835             S_LocalSound(SFX_MENU_CANCEL, NULL);
836             Hu_MenuSetPage(d->previous);
837         }
838         return true;
839 
840     default: break;
841     }
842 
843     return false; // Not handled.
844 }
845 
setUserValue(QVariant const & newValue)846 void Page::setUserValue(QVariant const &newValue)
847 {
848     d->userValue = newValue;
849 }
850 
userValue() const851 QVariant const &Page::userValue() const
852 {
853     return d->userValue;
854 }
855 
856 } // namespace menu
857 } // namespace common
858