1 /** @file videosettingsdialog.cpp  Dialog for video settings.
2  *
3  * @authors Copyright (c) 2013-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/dialogs/videosettingsdialog.h"
20 #include "ui/widgets/taskbarwidget.h"
21 #include "ui/widgets/cvarchoicewidget.h"
22 #include "ui/clientwindow.h"
23 #include "ui/clientwindowsystem.h"
24 #include "CommandAction"
25 #include "ConfigProfiles"
26 #include "clientapp.h"
27 #include "dd_main.h"
28 
29 #include <doomsday/console/exec.h>
30 #include <de/Config>
31 #include <de/VariableToggleWidget>
32 #include <de/VariableSliderWidget>
33 #include <de/ChoiceWidget>
34 #include <de/SequentialLayout>
35 #include <de/GridLayout>
36 #include <de/SignalAction>
37 #include <de/DisplayMode>
38 #include <QPoint>
39 
40 using namespace de;
41 using namespace de::ui;
42 
43 #if !defined (MACOSX)
44 #  define USE_REFRESH_RATE_CHOICE
45 #  define USE_COLOR_DEPTH_CHOICE
46 #endif
47 
DENG2_PIMPL(VideoSettingsDialog)48 DENG2_PIMPL(VideoSettingsDialog)
49 #if !defined (DENG_MOBILE)
50 , DENG2_OBSERVES(PersistentGLWindow, AttributeChange)
51 #endif
52 {
53     ClientWindow &win;
54     VariableToggleWidget *showFps;
55     ToggleWidget *fullscreen;
56     ToggleWidget *maximized;
57     ToggleWidget *centered;
58     VariableToggleWidget *fsaa;
59     VariableToggleWidget *vsync;
60     ToggleWidget *fpsLimiter = nullptr;
61     SliderWidget *fpsMax = nullptr;
62     ChoiceWidget *modes = nullptr;
63     ButtonWidget *windowButton = nullptr;
64 #ifdef USE_REFRESH_RATE_CHOICE
65     ChoiceWidget *refreshRates;
66 #endif
67 #ifdef USE_COLOR_DEPTH_CHOICE
68     ChoiceWidget *depths;
69 #endif
70     ListData stretchChoices;
71     CVarChoiceWidget *finaleAspect = nullptr;
72     CVarChoiceWidget *hudAspect    = nullptr;
73     CVarChoiceWidget *inludeAspect = nullptr;
74     CVarChoiceWidget *menuAspect   = nullptr;
75 
76     Impl(Public *i)
77         : Base(i)
78         , win(ClientWindow::main())
79     {
80         ScrollAreaWidget &area = self().area();
81 
82         area.add(showFps      = new VariableToggleWidget(App::config("window.main.showFps")));
83         area.add(fullscreen   = new ToggleWidget);
84         area.add(maximized    = new ToggleWidget);
85         area.add(centered     = new ToggleWidget);
86         area.add(fsaa         = new VariableToggleWidget(App::config("window.main.fsaa")));
87         area.add(vsync        = new VariableToggleWidget(App::config("window.main.vsync")));
88         if (gotDisplayMode())
89         {
90             area.add(modes        = new ChoiceWidget("modes"));
91             area.add(windowButton = new ButtonWidget);
92         }
93 #ifdef USE_REFRESH_RATE_CHOICE
94         area.add(refreshRates = new ChoiceWidget("refresh-rate"));
95 #endif
96 #ifdef USE_COLOR_DEPTH_CHOICE
97         area.add(depths       = new ChoiceWidget("depths"));
98 #endif
99 #if !defined (DENG_MOBILE)
100         win.audienceForAttributeChange() += this;
101 #endif
102 
103         area.add(fpsLimiter = new ToggleWidget);
104         fpsLimiter->margins().setTop("dialog.separator");
105         fpsLimiter->setText(tr("FPS Limiter"));
106         QObject::connect(fpsLimiter, &ToggleWidget::stateChangedByUser, [this] (ToggleWidget::ToggleState st) {
107             fpsMax->enable(st == ToggleWidget::Active);
108             applyFpsMax();
109         });
110 
111         area.add(fpsMax = new SliderWidget);
112         fpsMax->margins().setTop("dialog.separator");
113         fpsMax->setRange(Ranged(35, 60));
114         fpsMax->setPrecision(0);
115         fpsMax->rule().setInput(Rule::Width, centered->rule().width());
116         QObject::connect(fpsMax, &SliderWidget::valueChangedByUser, [this] (double) { applyFpsMax(); });
117 
118         if (App_GameLoaded())
119         {
120             stretchChoices
121                 << new ChoiceItem(tr("Smart"),        SCALEMODE_SMART_STRETCH)
122                 << new ChoiceItem(tr("Original 1:1"), SCALEMODE_NO_STRETCH)
123                 << new ChoiceItem(tr("Stretched"),    SCALEMODE_STRETCH);
124 
125             area.add(finaleAspect = new CVarChoiceWidget("rend-finale-stretch"));
126             area.add(hudAspect    = new CVarChoiceWidget("rend-hud-stretch"));
127             area.add(inludeAspect = new CVarChoiceWidget("inlude-stretch"));
128             area.add(menuAspect   = new CVarChoiceWidget("menu-stretch"));
129 
130             finaleAspect->setItems(stretchChoices);
131             hudAspect   ->setItems(stretchChoices);
132             inludeAspect->setItems(stretchChoices);
133             menuAspect  ->setItems(stretchChoices);
134         }
135     }
136 
137     ~Impl()
138     {
139         // The common stretchChoices is being deleted now, before the widget tree.
140         if (finaleAspect)
141         {
142             finaleAspect->useDefaultItems();
143             hudAspect->useDefaultItems();
144             inludeAspect->useDefaultItems();
145             menuAspect->useDefaultItems();
146         }
147 
148         //win.audienceForAttributeChange() -= this;
149     }
150 
151     /**
152      * Updates the widgets with the actual current state.
153      */
154     void fetch()
155     {
156 #if !defined (DENG_MOBILE)
157         fullscreen->setActive(win.isFullScreen());
158         maximized->setActive(win.isMaximized());
159         centered->setActive(win.isCentered());
160 
161         if (windowButton)
162         {
163             windowButton->enable(!win.isFullScreen() && !win.isMaximized());
164         }
165 
166         if (gotDisplayMode())
167         {
168             // Select the current resolution/size in the mode list.
169             GLWindow::Size current = win.fullscreenSize();
170 
171             // Update selected display mode.
172             ui::Data::Pos closest = ui::Data::InvalidPos;
173             int delta = 0;
174             for (ui::Data::Pos i = 0; i < modes->items().size(); ++i)
175             {
176                 QPoint const res = modes->items().at(i).data().toPoint();
177                 int dx = res.x() - current.x;
178                 int dy = res.y() - current.y;
179                 int d = dx*dx + dy*dy;
180                 if (closest == ui::Data::InvalidPos || d < delta)
181                 {
182                     closest = i;
183                     delta = d;
184                 }
185             }
186             modes->setSelected(closest);
187         }
188 
189 #ifdef USE_REFRESH_RATE_CHOICE
190         refreshRates->setSelected(refreshRates->items().findData(int(win.refreshRate() * 10)));
191 #endif
192 
193 #ifdef USE_COLOR_DEPTH_CHOICE
194         // Select the current color depth in the depth list.
195         depths->setSelected(depths->items().findData(win.colorDepthBits()));
196 #endif
197 
198 #endif // !DENG_MOBILE
199 
200         // FPS limit.
201         if (fpsMax)
202         {
203             const int max = Config::get().geti("window.main.maxFps", 0);
204             fpsLimiter->setActive(max != 0);
205             fpsMax->enable(max != 0);
206             fpsMax->setValue(!max ? 35 : max);
207         }
208 
209         foreach (GuiWidget *child, self().area().childWidgets())
210         {
211             if (ICVarWidget *cw = maybeAs<ICVarWidget>(child))
212                 cw->updateFromCVar();
213         }
214     }
215 
216 #if !defined (DENG_MOBILE)
217     void windowAttributesChanged(PersistentGLWindow &)
218     {
219         fetch();
220     }
221 #endif
222 
223     void applyFpsMax()
224     {
225         if (fpsMax)
226         {
227             Config::get().set("window.main.maxFps",
228                               fpsLimiter->isActive() ? int(fpsMax->value()) : 0);
229         }
230     }
231 
232     bool gotDisplayMode() const
233     {
234         return DisplayMode_Count() > 0;
235     }
236 };
237 
VideoSettingsDialog(String const & name)238 VideoSettingsDialog::VideoSettingsDialog(String const &name)
239     : DialogWidget(name, WithHeading), d(new Impl(this))
240 {
241     heading().setText(tr("Video Settings"));
242     heading().setImage(style().images().image("display"));
243 
244     // Toggles for video/window options.
245     d->fullscreen->setText(tr("Fullscreen"));
246     d->fullscreen->setAction(new CommandAction("togglefullscreen"));
247 
248     d->maximized->setText(tr("Maximized"));
249     d->maximized->setAction(new CommandAction("togglemaximized"));
250 
251     d->centered->setText(tr("Center Window"));
252     d->centered->setAction(new CommandAction("togglecentered"));
253 
254     d->showFps->setText(tr("Show FPS"));
255 
256     d->fsaa->setText(tr("Antialias"));
257 
258     d->vsync->setText(tr("VSync"));
259 
260 #ifdef USE_REFRESH_RATE_CHOICE
261     LabelWidget *refreshLabel = nullptr;
262 #endif
263 #ifdef USE_COLOR_DEPTH_CHOICE
264     LabelWidget *colorLabel = nullptr;
265 #endif
266     if (d->gotDisplayMode())
267     {
268         // Choice of display modes + 16/32-bit color depth.
269         d->modes->setOpeningDirection(ui::Up);
270         if (DisplayMode_Count() > 10)
271         {
272             d->modes->popup().menu().setGridSize(2, ui::Expand, 0, ui::Expand);
273         }
274         for (int i = 0; i < DisplayMode_Count(); ++i)
275         {
276             DisplayMode const *m = DisplayMode_ByIndex(i);
277             QPoint const res(m->width, m->height);
278 
279             if (d->modes->items().findData(res) != ui::Data::InvalidPos)
280             {
281                 // Got this already.
282                 continue;
283             }
284 
285             String desc = String("%1 x %2 (%3:%4)")
286                     .arg(m->width).arg(m->height)
287                     .arg(m->ratioX).arg(m->ratioY);
288 
289             d->modes->items() << new ChoiceItem(desc, res);
290         }
291 
292 #ifdef USE_REFRESH_RATE_CHOICE
293         {
294             refreshLabel = LabelWidget::newWithText(tr("Monitor Refresh:"), &area());
295 
296             QSet<int> rates;
297             rates.insert(0);
298             for (int i = 0; i < DisplayMode_Count(); ++i)
299             {
300                 rates.insert(int(DisplayMode_ByIndex(i)->refreshRate * 10));
301             }
302             foreach (int rate, rates)
303             {
304                 if (rate == 0)
305                 {
306                     d->refreshRates->items() << new ChoiceItem(tr("Default"), 0);
307                 }
308                 else
309                 {
310                     d->refreshRates->items()
311                         << new ChoiceItem(tr("%1 Hz").arg(float(rate) / 10.f, 0, 'f', 1), rate);
312                 }
313             }
314             d->refreshRates->items().sort([] (ui::Item const &a, ui::Item const &b) {
315                 int const i = a.data().toInt();
316                 int const j = b.data().toInt();
317                 if (!i) return true;
318                 if (!j) return false;
319                 return i < j;
320             });
321         }
322 #endif
323 
324 #ifdef USE_COLOR_DEPTH_CHOICE
325         {
326             colorLabel = LabelWidget::newWithText(tr("Colors:"), &area());
327             d->depths->items()
328                 << new ChoiceItem(tr("32-bit"), 32)
329                 << new ChoiceItem(tr("24-bit"), 24)
330                 << new ChoiceItem(tr("16-bit"), 16);
331         }
332 #endif
333     }
334 
335     buttons()
336             << new DialogButtonItem(DialogWidget::Accept | DialogWidget::Default, tr("Close"))
337             << new DialogButtonItem(DialogWidget::Action, tr("Reset to Defaults"),
338                                     new SignalAction(this, SLOT(resetToDefaults())));
339 
340     if (d->windowButton)
341     {
342         d->windowButton->setImage(style().images().image("window.icon"));
343         d->windowButton->setOverrideImageSize(style().fonts().font("default").height());
344         d->windowButton->setAction(new SignalAction(this, SLOT(showWindowMenu())));
345     }
346 
347     // Layout all widgets.
348     Rule const &gap = rule("dialog.gap");
349 
350     GridLayout layout(area().contentRule().left(),
351                       area().contentRule().top(), GridLayout::RowFirst);
352     layout.setGridSize(2, d->fpsLimiter? 4 : 3);
353     layout.setColumnPadding(rule(RuleBank::UNIT));
354     layout << *d->fsaa
355            << *d->vsync
356            << *d->showFps;
357     if (d->fpsLimiter) layout << *d->fpsLimiter;
358     layout << *d->fullscreen
359            << *d->maximized
360            << *d->centered;
361     if (d->fpsMax) layout << *d->fpsMax;
362 
363     GridLayout modeLayout(d->vsync->rule().left(),
364                           (d->fpsLimiter? d->fpsLimiter : d->showFps)->rule().bottom() + gap);
365     modeLayout.setGridSize(2, 0);
366     modeLayout.setColumnAlignment(0, ui::AlignRight);
367 
368     if (d->gotDisplayMode())
369     {
370         modeLayout << *LabelWidget::newWithText(tr("Resolution:"), &area());
371 
372         modeLayout.append(*d->modes, d->modes->rule().width() + d->windowButton->rule().width());
373 
374         d->windowButton->rule()
375                 .setInput(Rule::Top,  d->modes->rule().top())
376                 .setInput(Rule::Left, d->modes->rule().right());
377 
378 #ifdef USE_REFRESH_RATE_CHOICE
379         modeLayout << *refreshLabel << *d->refreshRates;
380 #endif
381 #ifdef USE_COLOR_DEPTH_CHOICE
382         modeLayout << *colorLabel << *d->depths;
383 #endif
384     }
385 
386     // Color adjustment.
387     {
388         auto *adjustButton = new ButtonWidget;
389         adjustButton->setText(tr("Color Adjustments..."));
390         adjustButton->setAction(new SignalAction(this, SLOT(showColorAdjustments())));
391         area().add(adjustButton);
392 
393         modeLayout << Const(0) << *adjustButton;
394     }
395 
396     if (d->inludeAspect)
397     {
398         // Aspect ratio options.
399 //        auto *aspectLabel = LabelWidget::newWithText(_E(D) + tr("Aspect Ratios"), &area());
400 //        aspectLabel->setFont("separator.label");
401 //        aspectLabel->margins().setTop("gap");
402 //        modeLayout.setCellAlignment(Vector2i(0, modeLayout.gridSize().y), ui::AlignLeft);
403 //        modeLayout.append(*aspectLabel, 2);
404         LabelWidget::appendSeparatorWithText("Aspect Ratios", &area(), &modeLayout);
405         modeLayout
406                 << *LabelWidget::newWithText(tr("Player Weapons:"), &area()) << *d->hudAspect
407                 << *LabelWidget::newWithText(tr("Intermissions:"), &area()) << *d->inludeAspect
408                 << *LabelWidget::newWithText(tr("Finales:"), &area()) << *d->finaleAspect
409                 << *LabelWidget::newWithText(tr("Menus:"), &area()) << *d->menuAspect;
410     }
411 
412     area().setContentSize(OperatorRule::maximum(layout.width(), modeLayout.width()),
413                           layout.height() + gap + modeLayout.height());
414 
415     d->fetch();
416 
417     if (d->gotDisplayMode())
418     {
419         connect(d->modes, SIGNAL(selectionChangedByUser(uint)), this, SLOT(changeMode(uint)));
420     }
421 
422 #ifdef USE_REFRESH_RATE_CHOICE
423     connect(d->refreshRates, SIGNAL(selectionChangedByUser(uint)), this, SLOT(changeRefreshRate(uint)));
424 #endif
425 #ifdef USE_COLOR_DEPTH_CHOICE
426     connect(d->depths, SIGNAL(selectionChangedByUser(uint)), this, SLOT(changeColorDepth(uint)));
427 #endif
428 }
429 
resetToDefaults()430 void VideoSettingsDialog::resetToDefaults()
431 {
432     ClientApp::windowSystem().settings().resetToDefaults();
433 
434     d->fetch();
435 }
436 
437 #if !defined (DENG_MOBILE)
438 
changeMode(uint selected)439 void VideoSettingsDialog::changeMode(uint selected)
440 {
441     DENG2_ASSERT(d->modes);
442 
443     QPoint const res = d->modes->items().at(selected).data().toPoint();
444 
445     int const attribs[] = {
446         ClientWindow::FullscreenWidth,  int(res.x()),
447         ClientWindow::FullscreenHeight, int(res.y()),
448         ClientWindow::End
449     };
450 
451     d->win.changeAttributes(attribs);
452 }
453 
changeColorDepth(uint selected)454 void VideoSettingsDialog::changeColorDepth(uint selected)
455 {
456 #ifdef USE_COLOR_DEPTH_CHOICE
457     Con_Executef(CMDS_DDAY, true, "setcolordepth %i",
458                  d->depths->items().at(selected).data().toInt());
459 #else
460     DENG2_UNUSED(selected);
461 #endif
462 }
463 
changeRefreshRate(uint selected)464 void VideoSettingsDialog::changeRefreshRate(uint selected)
465 {
466 #ifdef USE_REFRESH_RATE_CHOICE
467     float const rate = float(d->refreshRates->items().at(selected).data().toInt()) / 10.f;
468     int const attribs[] = {
469         ClientWindow::RefreshRate, int(rate * 1000), // milli-Hz
470         ClientWindow::End
471     };
472     d->win.changeAttributes(attribs);
473 #else
474     DENG2_UNUSED(selected);
475 #endif
476 }
477 
showColorAdjustments()478 void VideoSettingsDialog::showColorAdjustments()
479 {
480     d->win.showColorAdjustments();
481     d->win.taskBar().closeConfigMenu();
482 }
483 
showWindowMenu()484 void VideoSettingsDialog::showWindowMenu()
485 {
486     DENG2_ASSERT(d->windowButton);
487 
488     PopupMenuWidget *menu = new PopupMenuWidget;
489     menu->setDeleteAfterDismissed(true);
490     add(menu);
491 
492     menu->setAnchorAndOpeningDirection(d->windowButton->rule(), ui::Up);
493     menu->items()
494             << new ActionItem(tr("Apply to Window"),
495                               new SignalAction(this, SLOT(applyModeToWindow())));
496     menu->open();
497 }
498 
applyModeToWindow()499 void VideoSettingsDialog::applyModeToWindow()
500 {
501     QPoint const res = d->modes->selectedItem().data().toPoint();
502 
503     int attribs[] = {
504         ClientWindow::Width,  res.x(),
505         ClientWindow::Height, res.y(),
506         ClientWindow::End
507     };
508 
509     d->win.changeAttributes(attribs);
510 }
511 
512 #endif
513