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/Objects/VirtualMonitor.vala b/src/Objects/VirtualMonitor.vala index 450add74..d92994b8 100644 --- a/src/Objects/VirtualMonitor.vala +++ b/src/Objects/VirtualMonitor.vala @@ -1,64 +1,27 @@ -/*- - * 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; } - - 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); + public Display.MonitorMode current_mode { + owned get { + return monitors[0].current_mode; } } + public signal void modes_changed (); + /* * Used to distinguish two VirtualMonitors from each other. * We make up and ID by sum all hashes of @@ -96,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 () { @@ -147,31 +105,31 @@ public class Display.VirtualMonitor : GLib.Object { } } - private void update_available_scales () { - Scale[] scales = {}; - foreach (var mode in get_available_modes ()) { - if (!mode.is_current && !mode.is_preferred) { - continue; - } + public double[] get_frequencies_from_current_mode () { + double[] frequencies = {}; + int current_width, current_height; - foreach (var scale in mode.supported_scales) { - scales += new Scale (scale); - } + get_current_mode_size (out current_width, out current_height); - break; + foreach (var mode in get_available_modes ()) { + if (mode.width == current_width && mode.height == current_height) { + frequencies += mode.frequency; + } } - available_scales_store.splice (0, available_scales_store.get_n_items (), scales); + return frequencies; } - 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) { @@ -199,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/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 45796772..46a38742 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")); @@ -100,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."); } } @@ -130,11 +133,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-item"].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/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 ); diff --git a/src/Widgets/DisplayWidget.vala b/src/Widgets/DisplayWidget.vala index 2cbfbc5f..c253ff3c 100644 --- a/src/Widgets/DisplayWidget.vala +++ b/src/Widgets/DisplayWidget.vala @@ -1,31 +1,9 @@ -/*- - * 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 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 +21,14 @@ 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 Gtk.DropDown scale_drop_down; + private Display.ResolutionDropDown resolution_drop_down; + private Display.RotationDropDown rotation_drop_down; + private Display.RefreshRateDropDown refresh_rate_drop_down; + private Display.ScaleDropDown 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,173 +59,50 @@ 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 (); - - 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 + resolution_drop_down = new Display.ResolutionDropDown (virtual_monitor); + var resolution_label = new Granite.HeaderLabel (_("Resolution")) { + mnemonic_widget = resolution_drop_down }; - 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 }; + 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 +132,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,117 +151,11 @@ public class Display.DisplayWidget : Gtk.Box { add_css_class ("disabled"); } - resolution_combobox.changed.connect (() => { - // 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); - if (new_mode == null) { - return; - } - - virtual_monitor.set_current_mode (new_mode); - rotation_combobox.set_active (0); - populate_refresh_rates (); - configuration_changed (); - check_position (); - }); - - rotation_combobox.changed.connect (() => { - // 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; - - 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_combobox.changed.connect (() => { - // 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 (); - } - }); - - scale_drop_down.notify["selected-item"].connect (() => { - // Prevent breaking autohide by closing popover - popover.popdown (); - - configuration_changed (); - }); + 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)); - rotation_combobox.set_active ((int) virtual_monitor.transform); on_vm_transform_changed (); virtual_monitor.modes_changed.connect (on_monitor_modes_changed); @@ -446,109 +165,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) { @@ -601,4 +223,106 @@ 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/RefreshRateDropDown.vala b/src/Widgets/RefreshRateDropDown.vala new file mode 100644 index 00000000..6d06caa6 --- /dev/null +++ b/src/Widgets/RefreshRateDropDown.vala @@ -0,0 +1,138 @@ +/* + * 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; } + 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 (); + + populate_refresh_rates (); + + 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 current_mode = virtual_monitor.current_mode; + var modes = virtual_monitor.get_modes_for_resolution (current_mode.width, current_mode.height); + + var used_freqs = new Gee.HashSet (); + var options = new Gee.ArrayList (); + + // 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) { + 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); + } + } + + options.sort ((a, b) => { + if (a.mode.frequency < b.mode.frequency) { + return -1; + } else if (a.mode.frequency > b.mode.frequency) { + return 1; + } + return 0; + }); + + foreach (var option in options) { + refresh_rates.append (option); + } + } +} diff --git a/src/Widgets/ResolutionDropDown.vala b/src/Widgets/ResolutionDropDown.vala new file mode 100644 index 00000000..58157aac --- /dev/null +++ b/src/Widgets/ResolutionDropDown.vala @@ -0,0 +1,145 @@ +/* + * 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; } + 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_selected (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-item"].connect (() => { + var selected_resolution = get_selected_resolution (); + if (selected_resolution != null) { + resolution_selected (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..eb3796cb --- /dev/null +++ b/src/Widgets/RotationDropDown.vala @@ -0,0 +1,89 @@ +/* + * 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; } + 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); + } + } +} diff --git a/src/Widgets/ScaleDropDown.vala b/src/Widgets/ScaleDropDown.vala new file mode 100644 index 00000000..b5f471b6 --- /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); + } + } +} diff --git a/src/meson.build b/src/meson.build index 94aa9310..339bae1c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -15,10 +15,14 @@ plug_files = files( 'Views' / 'FiltersView.vala', 'Widgets/DisplayWidget.vala', 'Widgets/DisplaysOverlay.vala', + 'Widgets/ResolutionDropDown.vala', + 'Widgets/RotationDropDown.vala', + 'Widgets/RefreshRateDropDown.vala', + 'Widgets/ScaleDropDown.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(),