Serenity Operating System
1/*
2 * Copyright (c) 2019-2020, Jesse Buhagiar <jooster669@gmail.com>
3 * Copyright (c) 2020-2021, Andreas Kling <kling@serenityos.org>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include "MonitorSettingsWidget.h"
9#include <Applications/DisplaySettings/MonitorSettingsGML.h>
10#include <LibGUI/BoxLayout.h>
11#include <LibGUI/Button.h>
12#include <LibGUI/ComboBox.h>
13#include <LibGUI/ConnectionToWindowServer.h>
14#include <LibGUI/ItemListModel.h>
15#include <LibGUI/Label.h>
16#include <LibGUI/MessageBox.h>
17#include <LibGUI/RadioButton.h>
18#include <LibGfx/SystemTheme.h>
19
20namespace DisplaySettings {
21
22MonitorSettingsWidget::MonitorSettingsWidget()
23{
24 create_resolution_list();
25 create_frame();
26 load_current_settings();
27}
28
29void MonitorSettingsWidget::create_resolution_list()
30{
31 // TODO: Find a better way to get the default resolution
32 m_resolutions.append({ 640, 480 });
33 m_resolutions.append({ 800, 600 });
34 m_resolutions.append({ 1024, 768 });
35 m_resolutions.append({ 1280, 720 });
36 m_resolutions.append({ 1280, 768 });
37 m_resolutions.append({ 1280, 960 });
38 m_resolutions.append({ 1280, 1024 });
39 m_resolutions.append({ 1360, 768 });
40 m_resolutions.append({ 1368, 768 });
41 m_resolutions.append({ 1440, 900 });
42 m_resolutions.append({ 1600, 900 });
43 m_resolutions.append({ 1600, 1200 });
44 m_resolutions.append({ 1920, 1080 });
45 m_resolutions.append({ 2048, 1152 });
46 m_resolutions.append({ 2256, 1504 });
47 m_resolutions.append({ 2560, 1080 });
48 m_resolutions.append({ 2560, 1440 });
49 m_resolutions.append({ 3440, 1440 });
50
51 for (auto resolution : m_resolutions) {
52 // Use Euclid's Algorithm to calculate greatest common factor
53 i32 a = resolution.width();
54 i32 b = resolution.height();
55 i32 gcf = 0;
56 for (;;) {
57 i32 r = a % b;
58 if (r == 0) {
59 gcf = b;
60 break;
61 }
62 a = b;
63 b = r;
64 }
65
66 i32 aspect_width = resolution.width() / gcf;
67 i32 aspect_height = resolution.height() / gcf;
68 m_resolution_strings.append(DeprecatedString::formatted("{}x{} ({}:{})", resolution.width(), resolution.height(), aspect_width, aspect_height));
69 }
70}
71
72void MonitorSettingsWidget::create_frame()
73{
74 load_from_gml(monitor_settings_window_gml).release_value_but_fixme_should_propagate_errors();
75
76 m_monitor_widget = *find_descendant_of_type_named<DisplaySettings::MonitorWidget>("monitor_widget");
77
78 m_screen_combo = *find_descendant_of_type_named<GUI::ComboBox>("screen_combo");
79 m_screen_combo->set_only_allow_values_from_model(true);
80 m_screen_combo->set_model(*GUI::ItemListModel<DeprecatedString>::create(m_screens));
81 m_screen_combo->on_change = [this](auto&, const GUI::ModelIndex& index) {
82 m_selected_screen_index = index.row();
83 selected_screen_index_or_resolution_changed();
84 };
85
86 m_resolution_combo = *find_descendant_of_type_named<GUI::ComboBox>("resolution_combo");
87 m_resolution_combo->set_only_allow_values_from_model(true);
88 m_resolution_combo->set_model(*GUI::ItemListModel<DeprecatedString>::create(m_resolution_strings));
89 m_resolution_combo->on_change = [this](auto&, const GUI::ModelIndex& index) {
90 auto& selected_screen = m_screen_layout.screens[m_selected_screen_index];
91 selected_screen.resolution = m_resolutions.at(index.row());
92 // Try to auto re-arrange things if there are overlaps or disconnected screens
93 m_screen_layout.normalize();
94 selected_screen_index_or_resolution_changed();
95 set_modified(true);
96 };
97
98 m_display_scale_radio_1x = *find_descendant_of_type_named<GUI::RadioButton>("scale_1x");
99 m_display_scale_radio_1x->on_checked = [this](bool checked) {
100 if (checked) {
101 auto& selected_screen = m_screen_layout.screens[m_selected_screen_index];
102 selected_screen.scale_factor = 1;
103 // Try to auto re-arrange things if there are overlaps or disconnected screens
104 m_screen_layout.normalize();
105 m_monitor_widget->set_desktop_scale_factor(1);
106 m_monitor_widget->update();
107 set_modified(true);
108 }
109 };
110 m_display_scale_radio_2x = *find_descendant_of_type_named<GUI::RadioButton>("scale_2x");
111 m_display_scale_radio_2x->on_checked = [this](bool checked) {
112 if (checked) {
113 auto& selected_screen = m_screen_layout.screens[m_selected_screen_index];
114 selected_screen.scale_factor = 2;
115 // Try to auto re-arrange things if there are overlaps or disconnected screens
116 m_screen_layout.normalize();
117 m_monitor_widget->set_desktop_scale_factor(2);
118 m_monitor_widget->update();
119 set_modified(true);
120 }
121 };
122
123 m_dpi_label = *find_descendant_of_type_named<GUI::Label>("display_dpi");
124}
125
126static DeprecatedString display_name_from_edid(EDID::Parser const& edid)
127{
128 auto manufacturer_name = edid.manufacturer_name();
129 auto product_name = edid.display_product_name();
130
131 auto build_manufacturer_product_name = [&]() {
132 if (product_name.is_null() || product_name.is_empty())
133 return manufacturer_name;
134 return DeprecatedString::formatted("{} {}", manufacturer_name, product_name);
135 };
136
137 if (auto screen_size = edid.screen_size(); screen_size.has_value()) {
138 auto diagonal_inch = hypot(screen_size.value().horizontal_cm(), screen_size.value().vertical_cm()) / 2.54;
139 return DeprecatedString::formatted("{} {}\"", build_manufacturer_product_name(), roundf(diagonal_inch));
140 }
141
142 return build_manufacturer_product_name();
143}
144
145void MonitorSettingsWidget::load_current_settings()
146{
147 m_screen_layout = GUI::ConnectionToWindowServer::the().get_screen_layout();
148
149 m_screens.clear();
150 m_screen_edids.clear();
151
152 size_t virtual_screen_count = 0;
153 for (size_t i = 0; i < m_screen_layout.screens.size(); i++) {
154 DeprecatedString screen_display_name;
155 if (m_screen_layout.screens[i].mode == WindowServer::ScreenLayout::Screen::Mode::Device) {
156 if (auto edid = EDID::Parser::from_display_connector_device(m_screen_layout.screens[i].device.value()); !edid.is_error()) { // TODO: multihead
157 screen_display_name = display_name_from_edid(edid.value());
158 m_screen_edids.append(edid.release_value());
159 } else {
160 dbgln("Error getting EDID from device {}: {}", m_screen_layout.screens[i].device.value(), edid.error());
161 screen_display_name = m_screen_layout.screens[i].device.value();
162 m_screen_edids.append({});
163 }
164 } else {
165 dbgln("Frame buffer {} is virtual.", i);
166 screen_display_name = DeprecatedString::formatted("Virtual screen {}", virtual_screen_count++);
167 m_screen_edids.append({});
168 }
169 if (i == m_screen_layout.main_screen_index)
170 m_screens.append(DeprecatedString::formatted("{}: {} (main screen)", i + 1, screen_display_name));
171 else
172 m_screens.append(DeprecatedString::formatted("{}: {}", i + 1, screen_display_name));
173 }
174 m_selected_screen_index = m_screen_layout.main_screen_index;
175 m_screen_combo->set_selected_index(m_selected_screen_index);
176 selected_screen_index_or_resolution_changed();
177}
178
179void MonitorSettingsWidget::selected_screen_index_or_resolution_changed()
180{
181 auto& screen = m_screen_layout.screens[m_selected_screen_index];
182
183 // Let's attempt to find the current resolution based on the screen layout settings
184 auto index = m_resolutions.find_first_index(screen.resolution).value_or(0);
185 Gfx::IntSize current_resolution = m_resolutions.at(index);
186
187 Optional<unsigned> screen_dpi;
188 DeprecatedString screen_dpi_tooltip;
189 if (m_screen_edids[m_selected_screen_index].has_value()) {
190 auto& edid = m_screen_edids[m_selected_screen_index];
191 if (auto screen_size = edid.value().screen_size(); screen_size.has_value()) {
192 auto x_cm = screen_size.value().horizontal_cm();
193 auto y_cm = screen_size.value().vertical_cm();
194 auto diagonal_inch = hypot(x_cm, y_cm) / 2.54;
195 auto diagonal_pixels = hypot(current_resolution.width(), current_resolution.height());
196 if (diagonal_pixels != 0.0) {
197 screen_dpi = diagonal_pixels / diagonal_inch;
198 screen_dpi_tooltip = DeprecatedString::formatted("{} inch display ({}cm x {}cm)", roundf(diagonal_inch), x_cm, y_cm);
199 }
200 }
201 }
202
203 if (screen_dpi.has_value()) {
204 m_dpi_label->set_tooltip(screen_dpi_tooltip);
205 m_dpi_label->set_text(DeprecatedString::formatted("{} dpi", screen_dpi.value()));
206 m_dpi_label->set_visible(true);
207 } else {
208 m_dpi_label->set_visible(false);
209 }
210
211 if (screen.scale_factor != 1 && screen.scale_factor != 2) {
212 dbgln("unexpected ScaleFactor {}, setting to 1", screen.scale_factor);
213 screen.scale_factor = 1;
214 }
215 (screen.scale_factor == 1 ? m_display_scale_radio_1x : m_display_scale_radio_2x)->set_checked(true, GUI::AllowCallback::No);
216 m_monitor_widget->set_desktop_scale_factor(screen.scale_factor);
217
218 // Select the current selected resolution as it may differ
219 m_monitor_widget->set_desktop_resolution(current_resolution);
220 m_resolution_combo->set_selected_index(index, GUI::AllowCallback::No);
221
222 m_monitor_widget->update();
223}
224
225void MonitorSettingsWidget::apply_settings()
226{
227 // Fetch the latest configuration again, in case it has been changed by someone else.
228 // This isn't technically race free, but if the user automates changing settings we can't help...
229 auto current_layout = GUI::ConnectionToWindowServer::the().get_screen_layout();
230 if (m_screen_layout != current_layout) {
231 auto result = GUI::ConnectionToWindowServer::the().set_screen_layout(m_screen_layout, false);
232 if (result.success()) {
233 load_current_settings(); // Refresh
234
235 auto seconds_until_revert = 10;
236
237 auto box_text = [&seconds_until_revert] {
238 return DeprecatedString::formatted("Do you want to keep the new settings? They will be reverted after {} {}.",
239 seconds_until_revert, seconds_until_revert == 1 ? "second" : "seconds");
240 };
241
242 auto box = GUI::MessageBox::construct(window(), box_text(), "Apply new screen layout"sv,
243 GUI::MessageBox::Type::Question, GUI::MessageBox::InputType::YesNo);
244 box->set_icon(window()->icon());
245
246 // If after 10 seconds the user doesn't close the message box, just close it.
247 auto revert_timer = Core::Timer::create_repeating(1000, [&] {
248 seconds_until_revert -= 1;
249 box->set_text(box_text());
250 if (seconds_until_revert <= 0) {
251 box->close();
252 }
253 }).release_value_but_fixme_should_propagate_errors();
254 revert_timer->start();
255
256 // If the user selects "No", closes the window or the window gets closed by the 10 seconds timer, revert the changes.
257 if (box->exec() == GUI::MessageBox::ExecResult::Yes) {
258 auto save_result = GUI::ConnectionToWindowServer::the().save_screen_layout();
259 if (!save_result.success()) {
260 GUI::MessageBox::show(window(), DeprecatedString::formatted("Error saving settings: {}", save_result.error_msg()),
261 "Unable to save setting"sv, GUI::MessageBox::Type::Error);
262 }
263 } else {
264 auto restore_result = GUI::ConnectionToWindowServer::the().set_screen_layout(current_layout, false);
265 if (!restore_result.success()) {
266 GUI::MessageBox::show(window(), DeprecatedString::formatted("Error restoring settings: {}", restore_result.error_msg()),
267 "Unable to restore setting"sv, GUI::MessageBox::Type::Error);
268 } else {
269 load_current_settings();
270 }
271 }
272 } else {
273 GUI::MessageBox::show(window(), DeprecatedString::formatted("Error setting screen layout: {}", result.error_msg()),
274 "Unable to apply changes"sv, GUI::MessageBox::Type::Error);
275 }
276 }
277}
278
279void MonitorSettingsWidget::show_screen_numbers(bool show)
280{
281 if (m_showing_screen_numbers == show)
282 return;
283 m_showing_screen_numbers = show;
284 GUI::ConnectionToWindowServer::the().async_show_screen_numbers(show);
285}
286
287void MonitorSettingsWidget::show_event(GUI::ShowEvent&)
288{
289 show_screen_numbers(true);
290}
291
292void MonitorSettingsWidget::hide_event(GUI::HideEvent&)
293{
294 show_screen_numbers(false);
295}
296
297}