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