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