From a9b9483032ec844dea85ec3969156f93a242dd3a Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Sun, 1 Jun 2025 19:23:05 -0300 Subject: [PATCH 01/11] Resolve variant == null warning --- src/Views/FiltersView.vala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Views/FiltersView.vala b/src/Views/FiltersView.vala index d264d2ff..a7ce2b18 100644 --- a/src/Views/FiltersView.vala +++ b/src/Views/FiltersView.vala @@ -182,7 +182,7 @@ public class Display.FiltersView : Gtk.Box { return new Variant ("s", "none"); } - return null; + return new Variant.maybe (VariantType.STRING, null); }, null, null ); @@ -198,7 +198,7 @@ public class Display.FiltersView : Gtk.Box { return new Variant ("s", "protanopia"); } - return null; + return new Variant.maybe (VariantType.STRING, null); }, null, null ); @@ -214,7 +214,7 @@ public class Display.FiltersView : Gtk.Box { return new Variant ("s", "protanopia-high-contrast"); } - return null; + return new Variant.maybe (VariantType.STRING, null); }, null, null ); @@ -230,7 +230,7 @@ public class Display.FiltersView : Gtk.Box { return new Variant ("s", "deuteranopia"); } - return null; + return new Variant.maybe (VariantType.STRING, null); }, null, null ); @@ -246,7 +246,7 @@ public class Display.FiltersView : Gtk.Box { return new Variant ("s", "deuteranopia-high-contrast"); } - return null; + return new Variant.maybe (VariantType.STRING, null); }, null, null ); @@ -262,7 +262,7 @@ public class Display.FiltersView : Gtk.Box { return new Variant ("s", "tritanopia"); } - return null; + return new Variant.maybe (VariantType.STRING, null); }, null, null ); @@ -278,7 +278,7 @@ public class Display.FiltersView : Gtk.Box { return new Variant ("s", "none"); } - return null; + return new Variant.maybe (VariantType.STRING, null); }, null, null ); From 8c2a4f1a3102a767965905ff09f47e7247fb7e24 Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Fri, 6 Jun 2025 19:40:43 -0300 Subject: [PATCH 02/11] Replace deprecated comboboxes --- src/Objects/VirtualMonitor.vala | 30 ++- src/Utils.vala | 30 +++ src/Views/DisplaysView.vala | 21 +- src/Widgets/DisplayWidget.vala | 367 +++++---------------------- src/Widgets/RefreshRateDropDown.vala | 118 +++++++++ src/Widgets/ResolutionDropDown.vala | 138 ++++++++++ src/Widgets/RotationDropDown.vala | 82 ++++++ 7 files changed, 464 insertions(+), 322 deletions(-) create mode 100644 src/Widgets/RefreshRateDropDown.vala create mode 100644 src/Widgets/ResolutionDropDown.vala create mode 100644 src/Widgets/RotationDropDown.vala diff --git a/src/Objects/VirtualMonitor.vala b/src/Objects/VirtualMonitor.vala index 450add74..ff4e3474 100644 --- a/src/Objects/VirtualMonitor.vala +++ b/src/Objects/VirtualMonitor.vala @@ -40,6 +40,11 @@ public class Display.VirtualMonitor : GLib.Object { public DisplayTransform transform { get; set; } public bool primary { get; set; } public Gee.LinkedList monitors { get; construct; } + public Display.MonitorMode current_mode { + owned get { + return monitors[0].current_mode; + } + } public signal void modes_changed (); @@ -147,6 +152,23 @@ public class Display.VirtualMonitor : GLib.Object { } } + public double[] get_frequencies_from_current_mode () { + double[] frequencies = {}; + int current_width, current_height; + + get_current_mode_size (out current_width, out current_height); + + foreach (var mode in get_available_modes ()) { + if (mode.width == current_width && mode.height == current_height) { + frequencies += mode.frequency; + } + } + + frequencies = Utils.sort_and_deduplicate_double_array (frequencies); + + return frequencies; + } + private void update_available_scales () { Scale[] scales = {}; foreach (var mode in get_available_modes ()) { @@ -164,14 +186,16 @@ public class Display.VirtualMonitor : GLib.Object { available_scales_store.splice (0, available_scales_store.get_n_items (), scales); } - public Display.MonitorMode? get_mode_for_resolution (int width, int height) { + public Gee.LinkedList get_modes_for_resolution (int width, int height) { + var mode_list = new Gee.LinkedList (); + foreach (var mode in get_available_modes ()) { if (mode.width == width && mode.height == height) { - return mode; + mode_list.add (mode); } } - return null; + return mode_list; } public void set_current_mode (Display.MonitorMode current_mode) { diff --git a/src/Utils.vala b/src/Utils.vala index 54989352..3251afaf 100644 --- a/src/Utils.vala +++ b/src/Utils.vala @@ -65,4 +65,34 @@ namespace Display.Utils { return min_scale; } + + public double[] sort_and_deduplicate_double_array (double[] input) { + for (int i = 0; i < input.length - 1; i++) { + for (int j = 0; j < input.length - i - 1; j++) { + if (input[j] > input[j + 1]) { + double temp = input[j]; + input[j] = input[j + 1]; + input[j + 1] = temp; + } + } + } + + double[] result = {}; + + foreach (double v in input) { + bool found = false; + foreach (double r in result) { + if (Math.fabs (v - r) < 1e-9) { + found = true; + break; + } + } + + if (!found) { + result += v; + } + } + + return result; + } } diff --git a/src/Views/DisplaysView.vala b/src/Views/DisplaysView.vala index 45796772..7a7c1468 100644 --- a/src/Views/DisplaysView.vala +++ b/src/Views/DisplaysView.vala @@ -9,7 +9,7 @@ public class Display.DisplaysView : Gtk.Box { public DisplaysOverlay displays_overlay; - private Gtk.ComboBoxText dpi_combo; + private Gtk.DropDown dpi_dropdown; private Gtk.Box rotation_lock_box; private const string TOUCHSCREEN_SETTINGS_PATH = "org.gnome.settings-daemon.peripherals.touchscreen"; @@ -34,10 +34,11 @@ public class Display.DisplaysView : Gtk.Box { var dpi_label = new Gtk.Label (_("Scaling factor:")); - dpi_combo = new Gtk.ComboBoxText (); - dpi_combo.append_text (_("LoDPI") + " (1×)"); - dpi_combo.append_text (_("HiDPI") + " (2×)"); - dpi_combo.append_text (_("HiDPI") + " (3×)"); + dpi_dropdown = new Gtk.DropDown.from_strings ({ + _("LoDPI") + " (1×)", + _("HiDPI") + " (2×)", + _("HiDPI") + " (3×)" + }); var dpi_box = new Gtk.Box (HORIZONTAL, 6) { margin_top = 6, @@ -45,8 +46,9 @@ public class Display.DisplaysView : Gtk.Box { margin_bottom = 6, margin_start = 6 }; + dpi_box.append (dpi_label); - dpi_box.append (dpi_combo); + dpi_box.append (dpi_dropdown); var detect_button = new Gtk.Button.with_label (_("Detect Displays")); @@ -130,11 +132,12 @@ public class Display.DisplaysView : Gtk.Box { apply_button.sensitive = false; }); - dpi_combo.active = (int)monitor_manager.virtual_monitors[0].scale - 1; + dpi_dropdown.selected = (int)monitor_manager.virtual_monitors[0].scale - 1; - dpi_combo.changed.connect (() => { + dpi_dropdown.notify["selected"].connect (() => { try { - monitor_manager.set_scale_on_all_monitors ((double)(dpi_combo.active + 1)); + monitor_manager.set_scale_on_all_monitors ((double)(dpi_dropdown.selected + 1)); + warning ("Setting scale to %f", (double)(dpi_dropdown.selected + 1)); } catch (Error e) { show_error_dialog (e.message); } diff --git a/src/Widgets/DisplayWidget.vala b/src/Widgets/DisplayWidget.vala index 2cbfbc5f..b9aea812 100644 --- a/src/Widgets/DisplayWidget.vala +++ b/src/Widgets/DisplayWidget.vala @@ -18,14 +18,6 @@ * Boston, MA 02110-1301, USA. */ -public struct Display.Resolution { - int width; - int height; - int aspect; - bool is_preferred; - bool is_current; -} - public class Display.DisplayWidget : Gtk.Box { public signal void set_as_primary (); public signal void check_position (); @@ -43,39 +35,15 @@ public class Display.DisplayWidget : Gtk.Box { private Gtk.Button primary_image; private Granite.SwitchModelButton use_switch; - private Gtk.ComboBox resolution_combobox; - private Gtk.TreeStore resolution_tree_store; - - private Gtk.ComboBox rotation_combobox; - private Gtk.ListStore rotation_list_store; - - private Gtk.ComboBox refresh_combobox; - private Gtk.ListStore refresh_list_store; + private Display.ResolutionDropDown resolution_drop_down; + private Display.RotationDropDown rotation_drop_down; + private Display.RefreshRateDropDown refresh_rate_drop_down; private Gtk.DropDown scale_drop_down; private int real_width = 0; private int real_height = 0; - private enum ResolutionColumns { - NAME, - WIDTH, - HEIGHT, - TOTAL - } - - private enum RotationColumns { - NAME, - VALUE, - TOTAL - } - - private enum RefreshColumns { - NAME, - VALUE, - TOTAL - } - public DisplayWidget (Display.VirtualMonitor virtual_monitor, string bg_color, string text_color) { Object ( virtual_monitor: virtual_monitor, @@ -106,138 +74,20 @@ public class Display.DisplayWidget : Gtk.Box { vexpand = true }; - use_switch = new Granite.SwitchModelButton (_("Use This Display")); - - virtual_monitor.bind_property ("is-active", use_switch, "active", GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL); - - resolution_tree_store = new Gtk.TreeStore (ResolutionColumns.TOTAL, typeof (string), typeof (int), typeof (int)); - resolution_combobox = new Gtk.ComboBox.with_model (resolution_tree_store) { - margin_start = 12, - margin_end = 12 - }; - - var resolution_label = new Granite.HeaderLabel (_("Resolution")) { - mnemonic_widget = resolution_combobox - }; - - var text_renderer = new Gtk.CellRendererText (); - resolution_combobox.pack_start (text_renderer, true); - resolution_combobox.add_attribute (text_renderer, "text", ResolutionColumns.NAME); - - rotation_list_store = new Gtk.ListStore (RotationColumns.TOTAL, typeof (string), typeof (int)); - rotation_combobox = new Gtk.ComboBox.with_model (rotation_list_store) { - margin_start = 12, - margin_end = 12 - }; - + rotation_drop_down = new Display.RotationDropDown (virtual_monitor); var rotation_label = new Granite.HeaderLabel (_("Screen Rotation")) { - mnemonic_widget = rotation_combobox - }; - - text_renderer = new Gtk.CellRendererText (); - rotation_combobox.pack_start (text_renderer, true); - rotation_combobox.add_attribute (text_renderer, "text", RotationColumns.NAME); - - refresh_list_store = new Gtk.ListStore (RefreshColumns.TOTAL, typeof (string), typeof (Display.MonitorMode)); - refresh_combobox = new Gtk.ComboBox.with_model (refresh_list_store) { - margin_start = 12, - margin_end = 12 + mnemonic_widget = rotation_drop_down }; + refresh_rate_drop_down = new Display.RefreshRateDropDown (virtual_monitor); var refresh_label = new Granite.HeaderLabel (_("Refresh Rate")) { - mnemonic_widget = refresh_combobox + mnemonic_widget = refresh_rate_drop_down }; - text_renderer = new Gtk.CellRendererText (); - refresh_combobox.pack_start (text_renderer, true); - refresh_combobox.add_attribute (text_renderer, "text", RefreshColumns.NAME); - - for (int i = 0; i <= DisplayTransform.FLIPPED_ROTATION_270; i++) { - Gtk.TreeIter iter; - rotation_list_store.append (out iter); - rotation_list_store.set (iter, RotationColumns.NAME, ((DisplayTransform) i).to_string (), RotationColumns.VALUE, i); - } - - // Build resolution menu - // First, get list of unique resolutions from available modes. - Resolution[] resolutions = {}; - Resolution[] recommended_resolutions = {}; - Resolution[] other_resolutions = {}; - int max_width = -1; - int max_height = -1; - uint usable_resolutions = 0; - int current_width, current_height; - virtual_monitor.get_current_mode_size (out current_width, out current_height); - var resolution_set = new Gee.TreeSet (Display.MonitorMode.resolution_compare_func); - foreach (var mode in virtual_monitor.get_available_modes ()) { - resolution_set.add (mode); // Ensures resolutions unique and sorted - } - - foreach (var mode in resolution_set) { - var mode_width = mode.width; - var mode_height = mode.height; - if (mode.is_preferred) { - max_width = int.max (max_width, mode_width); - max_height = int.max (max_height, mode_height); - } - - Resolution res = {mode_width, mode_height, mode_width * 10 / mode_height, mode.is_preferred, mode.is_current}; - resolutions += res; - } - - var native_ratio = max_width * 10 / max_height; - // Split resolutions into recommended and other - foreach (var resolution in resolutions) { - // Reject all resolutions incompatible with elementary desktop - if (resolution.width < 1024 || resolution.height < 768) { - continue; - } - - if (resolution.is_preferred || resolution.is_current || resolution.aspect == native_ratio) { - recommended_resolutions += resolution; - } else { - other_resolutions += resolution; - } - - usable_resolutions++; - } - - foreach (var resolution in recommended_resolutions) { - Gtk.TreeIter iter; - resolution_tree_store.append (out iter, null); - resolution_tree_store.set (iter, - ResolutionColumns.NAME, MonitorMode.get_resolution_string (resolution.width, resolution.height, false), - ResolutionColumns.WIDTH, resolution.width, - ResolutionColumns.HEIGHT, resolution.height - ); - } - - if (other_resolutions.length > 0) { - Gtk.TreeIter iter; - Gtk.TreeIter parent_iter; - resolution_tree_store.append (out parent_iter, null); - resolution_tree_store.set (parent_iter, ResolutionColumns.NAME, _("Other…"), - ResolutionColumns.WIDTH, -1, - ResolutionColumns.HEIGHT, -1 - ); - - foreach (var resolution in other_resolutions) { - resolution_tree_store.append (out iter, parent_iter); - resolution_tree_store.set (iter, - ResolutionColumns.NAME, Display.MonitorMode.get_resolution_string (resolution.width, resolution.height, true), - ResolutionColumns.WIDTH, resolution.width, - ResolutionColumns.HEIGHT, resolution.height - ); - } - } - - if (!set_active_resolution_from_current_mode ()) { - resolution_combobox.set_active (0); - } - - resolution_combobox.sensitive = usable_resolutions > 1; - - populate_refresh_rates (); + resolution_drop_down = new Display.ResolutionDropDown (virtual_monitor); + var resolution_label = new Granite.HeaderLabel (_("Resolution")) { + mnemonic_widget = resolution_drop_down + }; var scale_drop_down_factory = new Gtk.SignalListItemFactory (); scale_drop_down_factory.setup.connect ((obj) => { @@ -256,23 +106,40 @@ public class Display.DisplayWidget : Gtk.Box { margin_end = 12, factory = scale_drop_down_factory }; - virtual_monitor.available_scales.bind_property ("selected", scale_drop_down, "selected", BIDIRECTIONAL | SYNC_CREATE); + virtual_monitor.available_scales.bind_property ( + "selected", + scale_drop_down, + "selected", BIDIRECTIONAL | SYNC_CREATE + ); var scale_label = new Granite.HeaderLabel (_("Scaling factor")) { mnemonic_widget = scale_drop_down }; + use_switch = new Granite.SwitchModelButton (_("Use This Display")); + use_switch.bind_property ("active", resolution_drop_down, "sensitive"); + use_switch.bind_property ("active", rotation_drop_down, "sensitive"); + use_switch.bind_property ("active", refresh_rate_drop_down, "sensitive"); + use_switch.bind_property ("active", scale_drop_down, "sensitive"); + + virtual_monitor.bind_property ( + "is-active", + use_switch, + "active", + GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL + ); + var popover_box = new Gtk.Box (VERTICAL, 0) { margin_top = 6, margin_bottom = 12 }; popover_box.append (use_switch); popover_box.append (resolution_label); - popover_box.append (resolution_combobox); + popover_box.append (resolution_drop_down); popover_box.append (rotation_label); - popover_box.append (rotation_combobox); + popover_box.append (rotation_drop_down); popover_box.append (refresh_label); - popover_box.append (refresh_combobox); + popover_box.append (refresh_rate_drop_down); if (!MonitorManager.get_default ().global_scale_required) { popover_box.append (scale_label); @@ -302,15 +169,10 @@ public class Display.DisplayWidget : Gtk.Box { set_primary (virtual_monitor.primary); - use_switch.bind_property ("active", resolution_combobox, "sensitive"); - use_switch.bind_property ("active", rotation_combobox, "sensitive"); - use_switch.bind_property ("active", refresh_combobox, "sensitive"); - use_switch.bind_property ("active", scale_drop_down, "sensitive"); - use_switch.notify["active"].connect (() => { - if (rotation_combobox.active == -1) rotation_combobox.set_active (0); - if (resolution_combobox.active == -1) resolution_combobox.set_active (0); - if (refresh_combobox.active == -1) refresh_combobox.set_active (0); + if (rotation_drop_down.selected == -1) rotation_drop_down.set_selected_rotation (0); + if (resolution_drop_down.selected == -1) resolution_drop_down.set_selected_resolution (0); + if (refresh_rate_drop_down.selected == -1) refresh_rate_drop_down.set_selected_refresh_rate (0); if (use_switch.active) { remove_css_class ("disabled"); @@ -326,45 +188,33 @@ public class Display.DisplayWidget : Gtk.Box { add_css_class ("disabled"); } - resolution_combobox.changed.connect (() => { + resolution_drop_down.resolution_changed.connect ((selected_option) => { // Prevent breaking autohide by closing popover popover.popdown (); - int active_width, active_height; - Gtk.TreeIter iter; - if (resolution_combobox.get_active_iter (out iter)) { - resolution_tree_store.get (iter, - ResolutionColumns.WIDTH, out active_width, - ResolutionColumns.HEIGHT, out active_height - ); - } else { - return; - } - - set_virtual_monitor_geometry (virtual_monitor.x, virtual_monitor.y, active_width, active_height); - var new_mode = virtual_monitor.get_mode_for_resolution (active_width, active_height); + set_virtual_monitor_geometry ( + virtual_monitor.x, + virtual_monitor.y, + selected_option.width, + selected_option.height + ); + var new_mode = virtual_monitor.get_modes_for_resolution (selected_option.width, selected_option.height); if (new_mode == null) { return; } - virtual_monitor.set_current_mode (new_mode); - rotation_combobox.set_active (0); - populate_refresh_rates (); + virtual_monitor.set_current_mode (new_mode.get (0)); + rotation_drop_down.set_selected_rotation (0); + refresh_rate_drop_down.update_refresh_rates (selected_option.width, selected_option.height); configuration_changed (); check_position (); }); - rotation_combobox.changed.connect (() => { + rotation_drop_down.rotation_selected.connect ((obj) => { // Prevent breaking autohide by closing popover popover.popdown (); - Value val; - Gtk.TreeIter iter; - rotation_combobox.get_active_iter (out iter); - rotation_list_store.get_value (iter, RotationColumns.VALUE, out val); - - var transform = (DisplayTransform)((int)val); - virtual_monitor.transform = transform; + var transform = (DisplayTransform)obj.value; label.css_classes = {""}; @@ -413,20 +263,14 @@ public class Display.DisplayWidget : Gtk.Box { check_position (); }); - refresh_combobox.changed.connect (() => { + refresh_rate_drop_down.refresh_rate_selected.connect ((obj) => { // Prevent breaking autohide by closing popover popover.popdown (); - - Value val; - Gtk.TreeIter iter; - if (refresh_combobox.get_active_iter (out iter)) { - refresh_list_store.get_value (iter, RefreshColumns.VALUE, out val); - Display.MonitorMode new_mode = (Display.MonitorMode) val; - virtual_monitor.set_current_mode (new_mode); - rotation_combobox.set_active (0); - configuration_changed (); - check_position (); - } + + virtual_monitor.set_current_mode (obj.mode); + rotation_drop_down.set_selected_rotation (0); + configuration_changed (); + check_position (); }); scale_drop_down.notify["selected-item"].connect (() => { @@ -436,7 +280,7 @@ public class Display.DisplayWidget : Gtk.Box { configuration_changed (); }); - rotation_combobox.set_active ((int) virtual_monitor.transform); + rotation_drop_down.set_selected_rotation ((int) virtual_monitor.transform); on_vm_transform_changed (); virtual_monitor.modes_changed.connect (on_monitor_modes_changed); @@ -446,109 +290,12 @@ public class Display.DisplayWidget : Gtk.Box { check_position (); } - private void populate_refresh_rates () { - refresh_list_store.clear (); - - Gtk.TreeIter iter; - int added = 0; - if (resolution_combobox.get_active_iter (out iter)) { - int active_width, active_height; - if (resolution_combobox.get_active_iter (out iter)) { - resolution_tree_store.get (iter, - ResolutionColumns.WIDTH, out active_width, - ResolutionColumns.HEIGHT, out active_height - ); - } else { - return; - } - - double[] frequencies = {}; - bool refresh_set = false; - foreach (var mode in virtual_monitor.get_available_modes ()) { - if (mode.width != active_width || mode.height != active_height) { - continue; - } - - if (mode.frequency in frequencies) { - continue; - } - - bool freq_already_added = false; - foreach (var freq in frequencies) { - if ((mode.frequency - freq).abs () < 1) { - freq_already_added = true; - break; - } - } - - if (freq_already_added) { - continue; - } - - frequencies += mode.frequency; - - var freq_name = _("%g Hz").printf (Math.roundf ((float)mode.frequency)); - refresh_list_store.append (out iter); - refresh_list_store.set (iter, ResolutionColumns.NAME, freq_name, RefreshColumns.VALUE, mode); - added++; - if (mode.is_current) { - refresh_combobox.set_active_iter (iter); - refresh_set = true; - } - } - - if (!refresh_set) { - refresh_combobox.set_active (0); - } - } - - refresh_combobox.sensitive = added > 1; - } - private void on_monitor_modes_changed () { - set_active_resolution_from_current_mode (); - } - - private bool set_active_resolution_from_current_mode () { - bool result = false; - foreach (var mode in virtual_monitor.get_available_modes ()) { - if (!mode.is_current) { - continue; - } - - resolution_tree_store.@foreach ((model, path, iter) => { - int width, height; - resolution_tree_store.get (iter, - ResolutionColumns.WIDTH, out width, - ResolutionColumns.HEIGHT, out height - ); - if (mode.width == width && mode.height == height) { - resolution_combobox.set_active_iter (iter); - result = true; - return true; - } - - return false; - }); - } - - return result; + resolution_drop_down.set_active_resolution_from_current_mode (); } private void on_vm_transform_changed () { - var transform = virtual_monitor.transform; - rotation_list_store.@foreach ((model, path, iter) => { - Value val; - rotation_list_store.get_value (iter, RotationColumns.VALUE, out val); - - var iter_transform = (DisplayTransform)((int)val); - if (iter_transform == transform) { - rotation_combobox.set_active_iter (iter); - return true; - } - - return false; - }); + rotation_drop_down.set_selected_rotation ((int) virtual_monitor.transform); } public void set_primary (bool is_primary) { diff --git a/src/Widgets/RefreshRateDropDown.vala b/src/Widgets/RefreshRateDropDown.vala new file mode 100644 index 00000000..acf3056b --- /dev/null +++ b/src/Widgets/RefreshRateDropDown.vala @@ -0,0 +1,118 @@ +public class Display.RefreshRateDropDown : Granite.Bin { + public class RefreshRateOption : Object { + public string label { get; set; } + public Display.MonitorMode mode { get; set; } + + public RefreshRateOption () { + Object (); + } + } + + public signal void refresh_rate_selected (RefreshRateOption refresh_rate); + + public Display.VirtualMonitor virtual_monitor { get; construct; } + public uint selected { get { return drop_down.get_selected (); } } + + private Gtk.DropDown drop_down; + private ListStore refresh_rates; + + public RefreshRateDropDown (Display.VirtualMonitor _virtual_monitor) { + Object ( + virtual_monitor: _virtual_monitor + ); + } + + construct { + refresh_rates = new ListStore (typeof (RefreshRateOption)); + + populate_refresh_rates (); + + var refresh_rate_factory = new Gtk.SignalListItemFactory (); + refresh_rate_factory.setup.connect ((obj) => { + var item = obj as Gtk.ListItem; + item.child = new Gtk.Label (null) { xalign = 0 }; + }); + refresh_rate_factory.bind.connect ((obj) => { + var item = obj as Gtk.ListItem; + var refresh_rate = item.get_item () as RefreshRateOption; + var item_child = item.child as Gtk.Label; + item_child.label = refresh_rate.label; + }); + + drop_down = new Gtk.DropDown (refresh_rates, null) { + factory = refresh_rate_factory, + margin_start = 12, + margin_end = 12 + }; + + drop_down.sensitive = refresh_rates.get_n_items () > 0; + + set_current_refresh_rate (); + + drop_down.notify["selected"].connect (() => { + var selected_refresh_rate = get_selected_refresh_rate (); + if (selected_refresh_rate != null) { + refresh_rate_selected (selected_refresh_rate); + } + }); + + child = drop_down; + } + + public RefreshRateOption get_selected_refresh_rate () { + return drop_down.get_selected_item () as RefreshRateOption; + } + + public void set_selected_refresh_rate (int refresh_rate) { + drop_down.set_selected (refresh_rate); + } + + public void update_refresh_rates (int width, int height) { + refresh_rates.remove_all (); + + var modes = virtual_monitor.get_modes_for_resolution (width, height); + + foreach (var mode in modes) { + var freq_name = _("%g Hz").printf (Math.roundf ((float)mode.frequency)); + + var option = new RefreshRateOption () { + label = freq_name, + mode = mode + }; + + refresh_rates.append (option); + } + + drop_down.set_selected (0); + + drop_down.sensitive = refresh_rates.get_n_items () > 0; + } + + private void set_current_refresh_rate () { + var current_refresh_rate = virtual_monitor.current_mode.frequency; + + for (int i = 0; i < refresh_rates.get_n_items (); i++) { + var item = refresh_rates.get_item (i) as RefreshRateOption; + + if (item.mode.frequency == current_refresh_rate) { + drop_down.set_selected (i); + return; + } + } + } + + private void populate_refresh_rates () { + var frequencies = virtual_monitor.get_frequencies_from_current_mode (); + + foreach (var frequency in frequencies) { + var freq_name = _("%g Hz").printf (Math.roundf ((float)frequency)); + + var option = new RefreshRateOption () { + label = freq_name, + mode = virtual_monitor.current_mode + }; + + refresh_rates.append (option); + } + } +} diff --git a/src/Widgets/ResolutionDropDown.vala b/src/Widgets/ResolutionDropDown.vala new file mode 100644 index 00000000..3e5bc724 --- /dev/null +++ b/src/Widgets/ResolutionDropDown.vala @@ -0,0 +1,138 @@ +public class Display.ResolutionDropDown : Granite.Bin { + public class ResolutionOption : Object { + public string label { get; set; } + public int width { get; set; } + public int height { get; set; } + + public ResolutionOption () { + Object (); + } + } + + public Display.VirtualMonitor virtual_monitor { get; construct; } + public uint selected { + get { + return drop_down.get_selected (); + } + } + + public signal void resolution_changed (ResolutionOption resolution); + + private Gtk.DropDown drop_down; + private ListStore resolutions; + + public ResolutionDropDown (Display.VirtualMonitor _virtual_monitor) { + Object ( + virtual_monitor: _virtual_monitor + ); + } + + construct { + resolutions = new ListStore (typeof (ResolutionOption)); + + populate_resolutions (); + + var resolution_factory = new Gtk.SignalListItemFactory (); + resolution_factory.setup.connect ((obj) => { + var item = obj as Gtk.ListItem; + item.child = new Gtk.Label (null) { xalign = 0 }; + }); + resolution_factory.bind.connect ((obj) => { + var item = obj as Gtk.ListItem; + var resolution = item.get_item () as ResolutionOption; + var item_child = item.child as Gtk.Label; + item_child.label = resolution.label; + }); + + drop_down = new Gtk.DropDown (resolutions, null) { + factory = resolution_factory, + margin_start = 12, + margin_end = 12 + }; + + drop_down.sensitive = resolutions.get_n_items () > 0; + + if (!set_active_resolution_from_current_mode ()) { + drop_down.set_selected (0); + } + + drop_down.notify["selected"].connect (() => { + var selected_resolution = get_selected_resolution (); + if (selected_resolution != null) { + resolution_changed (selected_resolution); + } + }); + + child = drop_down; + } + + public void set_selected_resolution (int index) { + drop_down.set_selected (index); + } + + public ResolutionOption? get_selected_resolution () { + var selected_item = drop_down.get_selected_item (); + if (selected_item == null) { + return null; + } + return selected_item as ResolutionOption; + } + + public uint get_selected_resolution_index () { + return drop_down.get_selected (); + } + + public bool set_active_resolution_from_current_mode () { + bool result = false; + + int current_width, current_height; + virtual_monitor.get_current_mode_size (out current_width, out current_height); + + for (uint i = 0; i < resolutions.get_n_items (); i++) { + var option = resolutions.get_item (i) as ResolutionOption?; + if (option == null) { + continue; + } + + if (option.width == current_width && option.height == current_height) { + drop_down.selected = (int)i; + result = true; + break; + } + } + + return result; + } + + private void populate_resolutions () { + // Build resolution menu + // First, get list of unique resolutions from available modes. + int max_width = -1; + int max_height = -1; + // Ensures resolutions unique and sorted + var resolution_set = new Gee.TreeSet (Display.MonitorMode.resolution_compare_func); + resolution_set.add_all (virtual_monitor.get_available_modes ()); + + foreach (var mode in resolution_set) { + var mode_width = mode.width; + var mode_height = mode.height; + + if (mode.is_preferred) { + max_width = int.max (max_width, mode_width); + max_height = int.max (max_height, mode_height); + } + + if (mode_width < 1024 || mode_height < 768) { + continue; + } + + var res = new ResolutionOption () { + label = MonitorMode.get_resolution_string (mode_width, mode_height, false), + width = mode_width, + height = mode_height + }; + + resolutions.append (res); + } + } +} diff --git a/src/Widgets/RotationDropDown.vala b/src/Widgets/RotationDropDown.vala new file mode 100644 index 00000000..4821a6d9 --- /dev/null +++ b/src/Widgets/RotationDropDown.vala @@ -0,0 +1,82 @@ +public class Display.RotationDropDown : Granite.Bin { + public class RotationOption : Object { + public string label { get; set; } + public int value { get; set; } + + public RotationOption () { + Object (); + } + } + + public Display.VirtualMonitor virtual_monitor { get; construct; } + public uint selected { + get { + return drop_down.get_selected (); + } + } + + private Gtk.DropDown drop_down; + private ListStore rotations; + + public signal void rotation_selected (RotationOption rotation); + + public RotationDropDown (Display.VirtualMonitor _virtual_monitor) { + Object ( + virtual_monitor: _virtual_monitor + ); + } + + construct { + rotations = new ListStore (typeof (RotationOption)); + + populate_rotations (); + + var rotation_factory = new Gtk.SignalListItemFactory (); + rotation_factory.setup.connect ((obj) => { + var item = obj as Gtk.ListItem; + item.child = new Gtk.Label (null) { xalign = 0 }; + }); + rotation_factory.bind.connect ((obj) => { + var item = obj as Gtk.ListItem; + var rotation = item.get_item () as RotationOption; + var item_child = item.child as Gtk.Label; + item_child.label = rotation.label; + }); + + drop_down = new Gtk.DropDown (rotations, null) { + factory = rotation_factory, + margin_start = 12, + margin_end = 12 + }; + + drop_down.sensitive = rotations.get_n_items () > 0; + + child = drop_down; + + drop_down.notify["selected"].connect (() => { + var selected_rotation = get_selected_rotation (); + if (selected_rotation != null) { + rotation_selected (selected_rotation); + } + }); + } + + public RotationOption get_selected_rotation () { + return drop_down.get_selected_item () as RotationOption; + } + + public void set_selected_rotation (int rotation) { + drop_down.set_selected (rotation); + } + + private void populate_rotations () { + for (int i = 0; i <= DisplayTransform.FLIPPED_ROTATION_270; i++) { + var option = new RotationOption () { + label = ((DisplayTransform) i).to_string (), + value = i + }; + + rotations.append (option); + } + } +} From cfc31212c30a1e714e00dbb3703ec18ba79bd33b Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Fri, 6 Jun 2025 19:41:00 -0300 Subject: [PATCH 03/11] Add new files and fix deprecated meson functions --- meson.build | 1 - src/meson.build | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 5711df2d..6c8a6793 100644 --- a/meson.build +++ b/meson.build @@ -9,7 +9,6 @@ gnome = import('gnome') i18n = import('i18n') prefix = get_option('prefix') -datadir = join_paths(prefix, get_option('datadir')) libdir = join_paths(prefix, get_option('libdir')) add_project_arguments( diff --git a/src/meson.build b/src/meson.build index 94aa9310..a11fa727 100644 --- a/src/meson.build +++ b/src/meson.build @@ -15,10 +15,13 @@ plug_files = files( 'Views' / 'FiltersView.vala', 'Widgets/DisplayWidget.vala', 'Widgets/DisplaysOverlay.vala', + 'Widgets/ResolutionDropDown.vala', + 'Widgets/RotationDropDown.vala', + 'Widgets/RefreshRateDropDown.vala', ) switchboard_dep = dependency('switchboard-3') -switchboard_plugsdir = switchboard_dep.get_pkgconfig_variable('plugsdir', define_variable: ['libdir', libdir]) +switchboard_plugsdir = switchboard_dep.get_variable(pkgconfig: 'plugsdir', pkgconfig_define: ['libdir', libdir]) shared_module( meson.project_name(), From f93116bf8c61b6f1193d7c0e77e17c875b3e0688 Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Mon, 9 Jun 2025 11:38:05 -0300 Subject: [PATCH 04/11] Fix Refresh Rate DropDown population --- src/Widgets/RefreshRateDropDown.vala | 68 ++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/src/Widgets/RefreshRateDropDown.vala b/src/Widgets/RefreshRateDropDown.vala index acf3056b..f8cde40e 100644 --- a/src/Widgets/RefreshRateDropDown.vala +++ b/src/Widgets/RefreshRateDropDown.vala @@ -1,3 +1,10 @@ +/* + * SPDX-License-Identifier: GPL-2.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. + * + * Authored by: Leonardo Lemos + */ + public class Display.RefreshRateDropDown : Granite.Bin { public class RefreshRateOption : Object { public string label { get; set; } @@ -70,18 +77,7 @@ public class Display.RefreshRateDropDown : Granite.Bin { public void update_refresh_rates (int width, int height) { refresh_rates.remove_all (); - var modes = virtual_monitor.get_modes_for_resolution (width, height); - - foreach (var mode in modes) { - var freq_name = _("%g Hz").printf (Math.roundf ((float)mode.frequency)); - - var option = new RefreshRateOption () { - label = freq_name, - mode = mode - }; - - refresh_rates.append (option); - } + populate_refresh_rates (); drop_down.set_selected (0); @@ -102,16 +98,50 @@ public class Display.RefreshRateDropDown : Granite.Bin { } private void populate_refresh_rates () { - var frequencies = virtual_monitor.get_frequencies_from_current_mode (); + var current_mode = virtual_monitor.current_mode; + var modes = virtual_monitor.get_modes_for_resolution (current_mode.width, current_mode.height); - foreach (var frequency in frequencies) { - var freq_name = _("%g Hz").printf (Math.roundf ((float)frequency)); + var used_ints = new Gee.HashSet (); // Nullable for Gee + var options = new Gee.ArrayList (); - var option = new RefreshRateOption () { - label = freq_name, - mode = virtual_monitor.current_mode - }; + // 1. Add only exact integer frequencies + foreach (var mode in modes) { + if (Math.fmod (mode.frequency, 1.0) == 0.0) { + int freq_int = (int) mode.frequency; + if (!used_ints.contains (freq_int)) { + var option = new RefreshRateOption () { + label = _("%g Hz").printf (mode.frequency), + mode = mode + }; + options.add (option); + used_ints.add (freq_int); + } + } + } + + // 2. Add non-integer frequencies whose rounded value hasn't been used yet + foreach (var mode in modes) { + if (Math.fmod (mode.frequency, 1.0) != 0.0) { + int rounded = (int) Math.roundf ((float)mode.frequency); + if (!used_ints.contains (rounded)) { + var option = new RefreshRateOption () { + label = _("%g Hz").printf (Math.roundf ((float)mode.frequency)), + mode = mode + }; + options.add (option); + used_ints.add (rounded); + } + } + } + + // Sort options by the actual frequency value + options.sort ((a, b) => { + if (a.mode.frequency < b.mode.frequency) return -1; + if (a.mode.frequency > b.mode.frequency) return 1; + return 0; + }); + foreach (var option in options) { refresh_rates.append (option); } } From 41847105cc16f617d23d1981456ac31ed0589bc2 Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Mon, 9 Jun 2025 11:39:57 -0300 Subject: [PATCH 05/11] Use SPDX header --- src/Widgets/RotationDropDown.vala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Widgets/RotationDropDown.vala b/src/Widgets/RotationDropDown.vala index 4821a6d9..eb3796cb 100644 --- a/src/Widgets/RotationDropDown.vala +++ b/src/Widgets/RotationDropDown.vala @@ -1,3 +1,10 @@ +/* + * SPDX-License-Identifier: GPL-2.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. + * + * Authored by: Leonardo Lemos + */ + public class Display.RotationDropDown : Granite.Bin { public class RotationOption : Object { public string label { get; set; } From 8695446028617d631817abcd35efff69c57941eb Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Mon, 9 Jun 2025 11:41:13 -0300 Subject: [PATCH 06/11] Refactor signal --- src/Widgets/ResolutionDropDown.vala | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Widgets/ResolutionDropDown.vala b/src/Widgets/ResolutionDropDown.vala index 3e5bc724..40e701ba 100644 --- a/src/Widgets/ResolutionDropDown.vala +++ b/src/Widgets/ResolutionDropDown.vala @@ -1,3 +1,10 @@ +/* + * SPDX-License-Identifier: GPL-2.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. + * + * Authored by: Leonardo Lemos + */ + public class Display.ResolutionDropDown : Granite.Bin { public class ResolutionOption : Object { public string label { get; set; } @@ -16,7 +23,7 @@ public class Display.ResolutionDropDown : Granite.Bin { } } - public signal void resolution_changed (ResolutionOption resolution); + public signal void resolution_selected (ResolutionOption resolution); private Gtk.DropDown drop_down; private ListStore resolutions; @@ -56,10 +63,10 @@ public class Display.ResolutionDropDown : Granite.Bin { drop_down.set_selected (0); } - drop_down.notify["selected"].connect (() => { + drop_down.notify["selected-item"].connect (() => { var selected_resolution = get_selected_resolution (); if (selected_resolution != null) { - resolution_changed (selected_resolution); + resolution_selected (selected_resolution); } }); From 876a3fb0d900692bdee168616b0b4cfe504a39c6 Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Mon, 9 Jun 2025 11:42:15 -0300 Subject: [PATCH 07/11] Use a dedicated Scale widget; Remove all UI logic from VirtualMonitor model; Clean code; --- src/Objects/VirtualMonitor.vala | 76 +--------- src/Views/DisplaysView.vala | 2 +- src/Widgets/DisplayWidget.vala | 243 ++++++++++++++------------------ src/Widgets/ScaleDropDown.vala | 86 +++++++++++ 4 files changed, 199 insertions(+), 208 deletions(-) create mode 100644 src/Widgets/ScaleDropDown.vala diff --git a/src/Objects/VirtualMonitor.vala b/src/Objects/VirtualMonitor.vala index ff4e3474..d92994b8 100644 --- a/src/Objects/VirtualMonitor.vala +++ b/src/Objects/VirtualMonitor.vala @@ -1,42 +1,16 @@ -/*- - * Copyright (c) 2018 elementary LLC. - * - * This software is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this software; if not, write to the - * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. +/* + * SPDX-License-Identifier: GPL-2.0-or-later + * SPDX-FileCopyrightText: 2018-2025 elementary, Inc. * * Authored by: Corentin Noël */ public class Display.VirtualMonitor : GLib.Object { - public class Scale : GLib.Object { - public double scale { get; construct; } - public string string_representation { get; construct; } - - public Scale (double scale) { - Object ( - scale: scale, - string_representation: "%d %%".printf ((int) Math.round (scale * 100)) - ); - } - } - public int x { get; set; } public int y { get; set; } public int current_x { get; set; } public int current_y { get; set; } - public Gtk.SingleSelection available_scales { get; construct; } + public double scale { get; set; } public DisplayTransform transform { get; set; } public bool primary { get; set; } public Gee.LinkedList monitors { get; construct; } @@ -48,22 +22,6 @@ public class Display.VirtualMonitor : GLib.Object { public signal void modes_changed (); - public double scale { - get { - return ((Scale) available_scales.selected_item).scale; - } - set { - update_available_scales (); - for (int i = 0; i < available_scales.get_n_items (); i++) { - if (value == ((Scale) available_scales.get_item (i)).scale) { - available_scales.selected = i; - return; - } - } - critical ("Unsupported scale %f for current mode", value); - } - } - /* * Used to distinguish two VirtualMonitors from each other. * We make up and ID by sum all hashes of @@ -101,13 +59,8 @@ public class Display.VirtualMonitor : GLib.Object { } } - private ListStore available_scales_store; - construct { monitors = new Gee.LinkedList (); - - available_scales_store = new ListStore (typeof (Scale)); - available_scales = new Gtk.SingleSelection (available_scales_store); } public unowned string get_display_name () { @@ -164,28 +117,9 @@ public class Display.VirtualMonitor : GLib.Object { } } - frequencies = Utils.sort_and_deduplicate_double_array (frequencies); - return frequencies; } - private void update_available_scales () { - Scale[] scales = {}; - foreach (var mode in get_available_modes ()) { - if (!mode.is_current && !mode.is_preferred) { - continue; - } - - foreach (var scale in mode.supported_scales) { - scales += new Scale (scale); - } - - break; - } - - available_scales_store.splice (0, available_scales_store.get_n_items (), scales); - } - public Gee.LinkedList get_modes_for_resolution (int width, int height) { var mode_list = new Gee.LinkedList (); @@ -223,8 +157,6 @@ public class Display.VirtualMonitor : GLib.Object { mode.is_current = mode == current_mode; } } - - scale = current_mode.preferred_scale; } public static string generate_id_from_monitors (MutterReadMonitorInfo[] infos) { diff --git a/src/Views/DisplaysView.vala b/src/Views/DisplaysView.vala index 7a7c1468..f0ecbabc 100644 --- a/src/Views/DisplaysView.vala +++ b/src/Views/DisplaysView.vala @@ -134,7 +134,7 @@ public class Display.DisplaysView : Gtk.Box { dpi_dropdown.selected = (int)monitor_manager.virtual_monitors[0].scale - 1; - dpi_dropdown.notify["selected"].connect (() => { + dpi_dropdown.notify["selected-item"].connect (() => { try { monitor_manager.set_scale_on_all_monitors ((double)(dpi_dropdown.selected + 1)); warning ("Setting scale to %f", (double)(dpi_dropdown.selected + 1)); diff --git a/src/Widgets/DisplayWidget.vala b/src/Widgets/DisplayWidget.vala index b9aea812..8942fe97 100644 --- a/src/Widgets/DisplayWidget.vala +++ b/src/Widgets/DisplayWidget.vala @@ -1,21 +1,7 @@ -/*- - * Copyright 2014–2024 elementary, Inc. - * 2014–2018 Corentin Noël - * - * This software is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this software; if not, write to the - * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. +/* + * SPDX-License-Identifier: LGPL-2.0-or-later + * SPDX-FileCopyrightText: 2014-2025 elementary, Inc. (https://elementary.io) + 2014-2018 Corentin Noël */ public class Display.DisplayWidget : Gtk.Box { @@ -38,8 +24,7 @@ public class Display.DisplayWidget : Gtk.Box { private Display.ResolutionDropDown resolution_drop_down; private Display.RotationDropDown rotation_drop_down; private Display.RefreshRateDropDown refresh_rate_drop_down; - - private Gtk.DropDown scale_drop_down; + private Display.ScaleDropDown scale_drop_down; private int real_width = 0; private int real_height = 0; @@ -89,29 +74,7 @@ public class Display.DisplayWidget : Gtk.Box { mnemonic_widget = resolution_drop_down }; - var scale_drop_down_factory = new Gtk.SignalListItemFactory (); - scale_drop_down_factory.setup.connect ((obj) => { - var list_item = (Gtk.ListItem) obj; - list_item.child = new Gtk.Label (null) { xalign = 0 }; - }); - scale_drop_down_factory.bind.connect ((obj) => { - var list_item = (Gtk.ListItem) obj; - var item = (VirtualMonitor.Scale) list_item.item; - var scale_label = (Gtk.Label) list_item.child; - scale_label.label = item.string_representation; - }); - - scale_drop_down = new Gtk.DropDown (virtual_monitor.available_scales, null) { - margin_start = 12, - margin_end = 12, - factory = scale_drop_down_factory - }; - virtual_monitor.available_scales.bind_property ( - "selected", - scale_drop_down, - "selected", BIDIRECTIONAL | SYNC_CREATE - ); - + scale_drop_down = new Display.ScaleDropDown (virtual_monitor); var scale_label = new Granite.HeaderLabel (_("Scaling factor")) { mnemonic_widget = scale_drop_down }; @@ -188,99 +151,11 @@ public class Display.DisplayWidget : Gtk.Box { add_css_class ("disabled"); } - resolution_drop_down.resolution_changed.connect ((selected_option) => { - // Prevent breaking autohide by closing popover - popover.popdown (); - - set_virtual_monitor_geometry ( - virtual_monitor.x, - virtual_monitor.y, - selected_option.width, - selected_option.height - ); - var new_mode = virtual_monitor.get_modes_for_resolution (selected_option.width, selected_option.height); - if (new_mode == null) { - return; - } - - virtual_monitor.set_current_mode (new_mode.get (0)); - rotation_drop_down.set_selected_rotation (0); - refresh_rate_drop_down.update_refresh_rates (selected_option.width, selected_option.height); - configuration_changed (); - check_position (); - }); - - rotation_drop_down.rotation_selected.connect ((obj) => { - // Prevent breaking autohide by closing popover - popover.popdown (); - - var transform = (DisplayTransform)obj.value; - - label.css_classes = {""}; - - switch (transform) { - case DisplayTransform.NORMAL: - virtual_monitor.get_current_mode_size (out real_width, out real_height); - label.label = virtual_monitor_name; - break; - case DisplayTransform.ROTATION_90: - virtual_monitor.get_current_mode_size (out real_height, out real_width); - label.add_css_class ("rotate-270"); - label.label = virtual_monitor_name; - break; - case DisplayTransform.ROTATION_180: - virtual_monitor.get_current_mode_size (out real_width, out real_height); - label.add_css_class ("rotate-180"); - label.label = virtual_monitor_name; - break; - case DisplayTransform.ROTATION_270: - virtual_monitor.get_current_mode_size (out real_height, out real_width); - label.add_css_class ("rotate-90"); - label.label = virtual_monitor_name; - break; - case DisplayTransform.FLIPPED: - virtual_monitor.get_current_mode_size (out real_width, out real_height); - label.label = virtual_monitor_name.reverse (); //mirroring simulation, because we can't really mirror the text - break; - case DisplayTransform.FLIPPED_ROTATION_90: - virtual_monitor.get_current_mode_size (out real_height, out real_width); - label.add_css_class ("rotate-270"); - label.label = virtual_monitor_name.reverse (); - break; - case DisplayTransform.FLIPPED_ROTATION_180: - virtual_monitor.get_current_mode_size (out real_width, out real_height); - label.add_css_class ("rotate-180"); - label.label = virtual_monitor_name.reverse (); - break; - case DisplayTransform.FLIPPED_ROTATION_270: - virtual_monitor.get_current_mode_size (out real_height, out real_width); - label.add_css_class ("rotate-90"); - label.label = virtual_monitor_name.reverse (); - break; - } - - configuration_changed (); - check_position (); - }); - - refresh_rate_drop_down.refresh_rate_selected.connect ((obj) => { - // Prevent breaking autohide by closing popover - popover.popdown (); - - virtual_monitor.set_current_mode (obj.mode); - rotation_drop_down.set_selected_rotation (0); - configuration_changed (); - check_position (); - }); + resolution_drop_down.resolution_selected.connect ((obj) => on_resolution_selected (obj, popover)); + rotation_drop_down.rotation_selected.connect ((obj) => on_rotation_selected (obj, popover, label)); + refresh_rate_drop_down.refresh_rate_selected.connect ((obj) => on_refresh_rate_selected (obj, popover)); + scale_drop_down.scale_selected.connect ((obj) => on_scale_selected (obj, popover)); - scale_drop_down.notify["selected-item"].connect (() => { - // Prevent breaking autohide by closing popover - popover.popdown (); - - configuration_changed (); - }); - - rotation_drop_down.set_selected_rotation ((int) virtual_monitor.transform); on_vm_transform_changed (); virtual_monitor.modes_changed.connect (on_monitor_modes_changed); @@ -348,4 +223,102 @@ public class Display.DisplayWidget : Gtk.Box { public bool equals (DisplayWidget sibling) { return virtual_monitor.id == sibling.virtual_monitor.id; } + + private void on_resolution_selected (Display.ResolutionDropDown.ResolutionOption selected_option, Gtk.Popover popover) { + // Prevent breaking autohide by closing popover + popover.popdown (); + + set_virtual_monitor_geometry ( + virtual_monitor.x, + virtual_monitor.y, + selected_option.width, + selected_option.height + ); + + var new_mode = virtual_monitor.get_modes_for_resolution (selected_option.width, selected_option.height); + if (new_mode == null) { + return; + } + + virtual_monitor.set_current_mode (new_mode.get (0)); + rotation_drop_down.set_selected_rotation (0); + refresh_rate_drop_down.update_refresh_rates (selected_option.width, selected_option.height); + scale_drop_down.update_available_scales (new_mode.get (0)); + configuration_changed (); + check_position (); + } + + private void on_rotation_selected (Display.RotationDropDown.RotationOption selected_option, Gtk.Popover popover, Gtk.Label label) { + // Prevent breaking autohide by closing popover + popover.popdown (); + + var transform = (DisplayTransform) selected_option.value; + var virtual_monitor_name = virtual_monitor.get_display_name (); + + // Set transformation on the virtual monitor + virtual_monitor.transform = transform; + + label.css_classes = {""}; + + switch (transform) { + case DisplayTransform.NORMAL: + virtual_monitor.get_current_mode_size (out real_width, out real_height); + label.label = virtual_monitor_name; + break; + case DisplayTransform.ROTATION_90: + virtual_monitor.get_current_mode_size (out real_height, out real_width); + label.add_css_class ("rotate-270"); + label.label = virtual_monitor_name; + break; + case DisplayTransform.ROTATION_180: + virtual_monitor.get_current_mode_size (out real_width, out real_height); + label.add_css_class ("rotate-180"); + label.label = virtual_monitor_name; + break; + case DisplayTransform.ROTATION_270: + virtual_monitor.get_current_mode_size (out real_height, out real_width); + label.add_css_class ("rotate-90"); + label.label = virtual_monitor_name; + break; + case DisplayTransform.FLIPPED: + virtual_monitor.get_current_mode_size (out real_width, out real_height); + label.label = virtual_monitor_name.reverse (); //mirroring simulation, because we can't really mirror the text + break; + case DisplayTransform.FLIPPED_ROTATION_90: + virtual_monitor.get_current_mode_size (out real_height, out real_width); + label.add_css_class ("rotate-270"); + label.label = virtual_monitor_name.reverse (); + break; + case DisplayTransform.FLIPPED_ROTATION_180: + virtual_monitor.get_current_mode_size (out real_width, out real_height); + label.add_css_class ("rotate-180"); + label.label = virtual_monitor_name.reverse (); + break; + case DisplayTransform.FLIPPED_ROTATION_270: + virtual_monitor.get_current_mode_size (out real_height, out real_width); + label.add_css_class ("rotate-90"); + label.label = virtual_monitor_name.reverse (); + break; + } + + configuration_changed (); + check_position (); + } + + private void on_refresh_rate_selected (Display.RefreshRateDropDown.RefreshRateOption selected_option, Gtk.Popover popover) { + // Prevent breaking autohide by closing popover + popover.popdown (); + + virtual_monitor.set_current_mode (selected_option.mode); + rotation_drop_down.set_selected_rotation (0); + configuration_changed (); + check_position (); + } + + private void on_scale_selected (Display.ScaleDropDown.ScaleOption selected_option, Gtk.Popover popover) { + // Prevent breaking autohide by closing popover + popover.popdown (); + + configuration_changed (); + } } diff --git a/src/Widgets/ScaleDropDown.vala b/src/Widgets/ScaleDropDown.vala new file mode 100644 index 00000000..847903c7 --- /dev/null +++ b/src/Widgets/ScaleDropDown.vala @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: GPL-2.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. + * + * Authored by: Leonardo Lemos + */ + +public class Display.ScaleDropDown : Granite.Bin { + public class ScaleOption : Object { + public string label { get; set; } + public double value { get; set; } + + public ScaleOption (double _value) { + Object (value: _value); + + label = "%d %%".printf ((int) Math.round (_value * 100)); + } + } + + public Display.VirtualMonitor virtual_monitor { get; construct; } + + private Gtk.DropDown drop_down; + private ListStore scales; + + public signal void scale_selected (ScaleOption scale); + + public ScaleDropDown (Display.VirtualMonitor _virtual_monitor) { + Object ( + virtual_monitor: _virtual_monitor + ); + } + + construct { + scales = new ListStore (typeof (ScaleOption)); + + populate_scales (); + + var scale_drop_down_factory = new Gtk.SignalListItemFactory (); + scale_drop_down_factory.setup.connect ((obj) => { + var list_item = obj as Gtk.ListItem; + list_item.child = new Gtk.Label (null) { xalign = 0 }; + }); + scale_drop_down_factory.bind.connect ((obj) => { + var list_item = obj as Gtk.ListItem; + var item = list_item.item as ScaleOption; + var scale_label = list_item.child as Gtk.Label; + scale_label.label = item.label; + }); + + drop_down = new Gtk.DropDown (scales, null) { + factory = scale_drop_down_factory, + margin_start = 12, + margin_end = 12 + }; + + drop_down.notify["selected-item"].connect (() => { + scale_selected (get_selected_scale ()); + }); + + child = drop_down; + } + + public void update_available_scales (Display.MonitorMode mode) { + var scales_to_replace = new ScaleOption[] {}; + + foreach (var scale in mode.supported_scales) { + scales_to_replace += new ScaleOption (scale); + } + + scales.splice (0, scales.get_n_items (), scales_to_replace); + } + + public ScaleOption get_selected_scale () { + return drop_down.get_selected_item () as ScaleOption; + } + + private void populate_scales () { + var current_mode = virtual_monitor.current_mode; + + foreach (var scale in current_mode.supported_scales) { + var option = new ScaleOption (scale); + + scales.append (option); + } + } +} \ No newline at end of file From b969922eb7168d85db41e6a79e148cf51a017ccc Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Mon, 9 Jun 2025 11:42:30 -0300 Subject: [PATCH 08/11] Remove unused function --- src/Utils.vala | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/Utils.vala b/src/Utils.vala index 3251afaf..54989352 100644 --- a/src/Utils.vala +++ b/src/Utils.vala @@ -65,34 +65,4 @@ namespace Display.Utils { return min_scale; } - - public double[] sort_and_deduplicate_double_array (double[] input) { - for (int i = 0; i < input.length - 1; i++) { - for (int j = 0; j < input.length - i - 1; j++) { - if (input[j] > input[j + 1]) { - double temp = input[j]; - input[j] = input[j + 1]; - input[j + 1] = temp; - } - } - } - - double[] result = {}; - - foreach (double v in input) { - bool found = false; - foreach (double r in result) { - if (Math.fabs (v - r) < 1e-9) { - found = true; - break; - } - } - - if (!found) { - result += v; - } - } - - return result; - } } From a3bc2f0b9a3c82c422178ce67f5e475bc4defcdb Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Mon, 9 Jun 2025 11:42:37 -0300 Subject: [PATCH 09/11] Add new class --- src/meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/src/meson.build b/src/meson.build index a11fa727..339bae1c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -18,6 +18,7 @@ plug_files = files( 'Widgets/ResolutionDropDown.vala', 'Widgets/RotationDropDown.vala', 'Widgets/RefreshRateDropDown.vala', + 'Widgets/ScaleDropDown.vala', ) switchboard_dep = dependency('switchboard-3') From 0d9cf34941fff655f1432c3c3227ea13ac4d135a Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Mon, 9 Jun 2025 11:49:43 -0300 Subject: [PATCH 10/11] Make lint happy --- src/Utils.vala | 3 ++- src/Views/DisplaysView.vala | 3 ++- src/Widgets/DisplayWidget.vala | 14 +++++++++----- src/Widgets/ResolutionDropDown.vala | 6 +++--- src/Widgets/ScaleDropDown.vala | 2 +- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Utils.vala b/src/Utils.vala index 54989352..7241d1c3 100644 --- a/src/Utils.vala +++ b/src/Utils.vala @@ -20,7 +20,8 @@ */ namespace Display.Utils { - public static Gee.LinkedList get_common_monitor_modes (Gee.LinkedList monitors) { + public static Gee.LinkedList get_common_monitor_modes ( + Gee.LinkedList monitors) { var common_modes = new Gee.LinkedList (); double min_scale = get_min_compatible_scale (monitors); bool first_monitor = true; diff --git a/src/Views/DisplaysView.vala b/src/Views/DisplaysView.vala index f0ecbabc..46a38742 100644 --- a/src/Views/DisplaysView.vala +++ b/src/Views/DisplaysView.vala @@ -102,7 +102,8 @@ public class Display.DisplaysView : Gtk.Box { var touchscreen_settings = new GLib.Settings (TOUCHSCREEN_SETTINGS_PATH); touchscreen_settings.bind ("orientation-lock", rotation_lock_switch, "active", DEFAULT); } else { - info ("Schema \"org.gnome.settings-daemon.peripherals.touchscreen\" is not installed on your system."); + info ("Schema \"org.gnome.settings-daemon.peripherals.touchscreen\" + is not installed on your system."); } } diff --git a/src/Widgets/DisplayWidget.vala b/src/Widgets/DisplayWidget.vala index 8942fe97..c253ff3c 100644 --- a/src/Widgets/DisplayWidget.vala +++ b/src/Widgets/DisplayWidget.vala @@ -224,7 +224,8 @@ public class Display.DisplayWidget : Gtk.Box { return virtual_monitor.id == sibling.virtual_monitor.id; } - private void on_resolution_selected (Display.ResolutionDropDown.ResolutionOption selected_option, Gtk.Popover popover) { + private void on_resolution_selected (Display.ResolutionDropDown.ResolutionOption selected_option, + Gtk.Popover popover) { // Prevent breaking autohide by closing popover popover.popdown (); @@ -234,7 +235,7 @@ public class Display.DisplayWidget : Gtk.Box { selected_option.width, selected_option.height ); - + var new_mode = virtual_monitor.get_modes_for_resolution (selected_option.width, selected_option.height); if (new_mode == null) { return; @@ -248,7 +249,9 @@ public class Display.DisplayWidget : Gtk.Box { check_position (); } - private void on_rotation_selected (Display.RotationDropDown.RotationOption selected_option, Gtk.Popover popover, Gtk.Label label) { + private void on_rotation_selected (Display.RotationDropDown.RotationOption selected_option, + Gtk.Popover popover, + Gtk.Label label) { // Prevent breaking autohide by closing popover popover.popdown (); @@ -305,10 +308,11 @@ public class Display.DisplayWidget : Gtk.Box { check_position (); } - private void on_refresh_rate_selected (Display.RefreshRateDropDown.RefreshRateOption selected_option, Gtk.Popover popover) { + private void on_refresh_rate_selected (Display.RefreshRateDropDown.RefreshRateOption selected_option, + Gtk.Popover popover) { // Prevent breaking autohide by closing popover popover.popdown (); - + virtual_monitor.set_current_mode (selected_option.mode); rotation_drop_down.set_selected_rotation (0); configuration_changed (); diff --git a/src/Widgets/ResolutionDropDown.vala b/src/Widgets/ResolutionDropDown.vala index 40e701ba..58157aac 100644 --- a/src/Widgets/ResolutionDropDown.vala +++ b/src/Widgets/ResolutionDropDown.vala @@ -50,7 +50,7 @@ public class Display.ResolutionDropDown : Granite.Bin { var item_child = item.child as Gtk.Label; item_child.label = resolution.label; }); - + drop_down = new Gtk.DropDown (resolutions, null) { factory = resolution_factory, margin_start = 12, @@ -119,11 +119,11 @@ public class Display.ResolutionDropDown : Granite.Bin { // Ensures resolutions unique and sorted var resolution_set = new Gee.TreeSet (Display.MonitorMode.resolution_compare_func); resolution_set.add_all (virtual_monitor.get_available_modes ()); - + foreach (var mode in resolution_set) { var mode_width = mode.width; var mode_height = mode.height; - + if (mode.is_preferred) { max_width = int.max (max_width, mode_width); max_height = int.max (max_height, mode_height); diff --git a/src/Widgets/ScaleDropDown.vala b/src/Widgets/ScaleDropDown.vala index 847903c7..b5f471b6 100644 --- a/src/Widgets/ScaleDropDown.vala +++ b/src/Widgets/ScaleDropDown.vala @@ -83,4 +83,4 @@ public class Display.ScaleDropDown : Granite.Bin { scales.append (option); } } -} \ No newline at end of file +} From 617931bce48dfa13664a271852f9d34e2605ecd4 Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Mon, 9 Jun 2025 12:07:59 -0300 Subject: [PATCH 11/11] Simplify function --- src/Widgets/RefreshRateDropDown.vala | 50 +++++++++++----------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/src/Widgets/RefreshRateDropDown.vala b/src/Widgets/RefreshRateDropDown.vala index f8cde40e..6d06caa6 100644 --- a/src/Widgets/RefreshRateDropDown.vala +++ b/src/Widgets/RefreshRateDropDown.vala @@ -101,43 +101,33 @@ public class Display.RefreshRateDropDown : Granite.Bin { var current_mode = virtual_monitor.current_mode; var modes = virtual_monitor.get_modes_for_resolution (current_mode.width, current_mode.height); - var used_ints = new Gee.HashSet (); // Nullable for Gee + var used_freqs = new Gee.HashSet (); var options = new Gee.ArrayList (); - // 1. Add only exact integer frequencies + // Prefer a frequency that is already rounded (e.g. 60.0) + // over a different frequency rounded to the same value + // (e.g. 59.76 rounded to 60.0). foreach (var mode in modes) { - if (Math.fmod (mode.frequency, 1.0) == 0.0) { - int freq_int = (int) mode.frequency; - if (!used_ints.contains (freq_int)) { - var option = new RefreshRateOption () { - label = _("%g Hz").printf (mode.frequency), - mode = mode - }; - options.add (option); - used_ints.add (freq_int); - } + int freq_int = (int) Math.roundf ((float) mode.frequency); + if (!used_freqs.contains (freq_int)) { + var freq_display = mode.frequency % 1.0 == 0 + ? mode.frequency + : Math.roundf ((float) mode.frequency); + var label = _("%g Hz").printf (freq_display); + options.add (new RefreshRateOption () { + label = label, + mode = mode + }); + used_freqs.add (freq_int); } } - // 2. Add non-integer frequencies whose rounded value hasn't been used yet - foreach (var mode in modes) { - if (Math.fmod (mode.frequency, 1.0) != 0.0) { - int rounded = (int) Math.roundf ((float)mode.frequency); - if (!used_ints.contains (rounded)) { - var option = new RefreshRateOption () { - label = _("%g Hz").printf (Math.roundf ((float)mode.frequency)), - mode = mode - }; - options.add (option); - used_ints.add (rounded); - } - } - } - - // Sort options by the actual frequency value options.sort ((a, b) => { - if (a.mode.frequency < b.mode.frequency) return -1; - if (a.mode.frequency > b.mode.frequency) return 1; + if (a.mode.frequency < b.mode.frequency) { + return -1; + } else if (a.mode.frequency > b.mode.frequency) { + return 1; + } return 0; });