ui: reorganize display settings sources

parent 355bd1d9
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/ru/ximperlinux/tuner/Displays">
<file alias="displays-view.ui" preprocess="xml-stripblanks">ui/displays-view.ui</file>
<file alias="monitor-settings-content.ui" preprocess="xml-stripblanks">ui/monitor-settings-content.ui</file>
<file alias="pages/displays-page.ui" preprocess="xml-stripblanks">ui/pages/displays-page.ui</file>
<file alias="pages/monitor-settings-content.ui" preprocess="xml-stripblanks">ui/pages/monitor-settings-content.ui</file>
<file alias="widgets/backdrop-color-widget.ui" preprocess="xml-stripblanks">ui/widgets/backdrop-color-widget.ui</file>
<file alias="widgets/config-include-widget.ui" preprocess="xml-stripblanks">ui/widgets/config-include-widget.ui</file>
<file alias="widgets/monitor-layout.ui" preprocess="xml-stripblanks">ui/widgets/monitor-layout.ui</file>
</gresource>
</gresources>
blueprints = files(
'displays-view.blp',
'monitor-settings-content.blp',
'pages/displays-page.blp',
'pages/monitor-settings-content.blp',
'widgets/backdrop-color-widget.blp',
'widgets/config-include-widget.blp',
'widgets/monitor-layout.blp',
)
using Gtk 4.0;
using Adw 1;
translation-domain "tuner-displays";
template $TunerDisplaysMonitorSettingsContent : Adw.PreferencesPage {
Adw.PreferencesGroup basic_group {
title: _("Display");
Adw.SwitchRow enabled_row {
title: _("Enabled");
}
}
Adw.PreferencesGroup hyprland_group {
title: _("Hyprland");
}
Adw.PreferencesGroup hdr_group {
title: _("HDR / EDID overrides");
}
}
using Gtk 4.0;
using Tuner 1;
translation-domain "tuner-displays";
Tuner.Page displays_page {
title: _("Displays");
id: "displays";
category: "system";
icon-name: "video-display-symbolic";
priority: 900;
Tuner.Group {
id: "status";
$TunerDisplaysConfigIncludeWidget config_include_widget {
binding: $TunerDisplaysConfigIncludeBinding {
validator: $TunerDisplaysConfigIncludeValidator {};
};
}
$TunerDisplaysStatusWidget status_widget {
binding: $TunerDisplaysDisplaysVisibilityBinding {
mode: "status-group";
validator: $TunerDisplaysDisplaysVisibilityValidator {
group-mode: "status-group";
};
};
}
Tuner.Switch {
title: _("Mirror Displays");
binding: $TunerDisplaysMirrorModeBinding {
validator: $TunerDisplaysDisplaysVisibilityValidator {
mode: "mirror-switch";
group-mode: "status-group";
};
};
}
}
Tuner.Group {
id: "layout";
$TunerDisplaysMonitorLayoutWidget layout_widget {
binding: $TunerDisplaysDisplaysVisibilityBinding {
mode: "layout";
validator: $TunerDisplaysDisplaysVisibilityValidator {
group-mode: "layout-group";
};
};
}
$TunerDisplaysMirrorSettingsWidget mirror_settings_widget {}
}
Tuner.Group {
title: _("Details");
id: "monitors";
$TunerDisplaysPrimaryDisplayWidget primary_display_widget {}
$TunerDisplaysMonitorListWidget monitor_list_widget {
binding: $TunerDisplaysDisplaysVisibilityBinding {
mode: "details";
validator: $TunerDisplaysDisplaysVisibilityValidator {};
};
}
}
Tuner.Group {
id: "single-monitor";
$TunerDisplaysSingleMonitorWidget single_monitor_widget {
binding: $TunerDisplaysDisplaysVisibilityBinding {
mode: "single-monitor";
validator: $TunerDisplaysDisplaysVisibilityValidator {};
};
}
}
}
Button refresh_button {
icon-name: "view-refresh-symbolic";
tooltip-text: _("Refresh");
}
Button apply_button {
label: _("Apply");
tooltip-text: _("Apply");
styles ["suggested-action"]
}
Tuner.Page monitor_settings_page {
title: _("Monitor");
id: "display-monitor-settings";
content: Box monitor_page_content {
orientation: vertical;
hexpand: true;
vexpand: true;
};
title-widget: Label monitor_page_title {
label: _("Monitor");
};
}
Button monitor_apply_button {
label: _("Apply");
tooltip-text: _("Apply");
styles ["suggested-action"]
}
using Gtk 4.0;
using Adw 1;
translation-domain "tuner-displays";
template $TunerDisplaysBackdropColorContent : Adw.ActionRow {
title: _("Backdrop color");
[suffix]
ColorDialogButton color_button {
valign: center;
dialog: ColorDialog {
title: _("Backdrop color");
with-alpha: true;
};
}
[suffix]
Switch enabled_switch {
valign: center;
}
}
......@@ -3,28 +3,13 @@ using Adw 1;
translation-domain "tuner-displays";
template $TunerDisplaysDisplaysView : Adw.PreferencesPage {
title: _("Displays");
icon-name: "video-display-symbolic";
Adw.PreferencesGroup config_include_group {
Adw.ActionRow config_include_row {
visible: false;
title: _("Monitor configuration is not connected");
[suffix]
Button config_include_button {
valign: center;
label: _("Connect");
}
}
}
Adw.PreferencesGroup status_group {}
Adw.PreferencesGroup layout_group {}
Adw.PreferencesGroup monitors_group {
title: _("Details");
template $TunerDisplaysConfigIncludeContent : Adw.ActionRow {
visible: false;
title: _("Monitor configuration is not connected");
[suffix]
Button connect_button {
valign: center;
label: _("Connect");
}
}
using Gtk 4.0;
template $TunerDisplaysMonitorLayout : DrawingArea {
height-request: 320;
hexpand: true;
vexpand: true;
can-focus: false;
focusable: false;
}
data/ui/displays-view.blp
data/ui/monitor-settings-content.blp
data/ui/pages/displays-page.blp
data/ui/pages/monitor-settings-content.blp
data/ui/widgets/backdrop-color-widget.blp
data/ui/widgets/config-include-widget.blp
src/plugin.vala
src/backends/gnome-backend.vala
src/backends/hyprland-backend.vala
src/backends/niri-backend.vala
src/core/display-model.vala
src/ui/displays-view.vala
src/ui/monitor-row.vala
src/ui/monitor-settings-content.vala
src/ui/ui-helpers.vala
src/ui/common/displays-controller.vala
src/ui/common/ui-utils.vala
src/ui/pages/monitor-settings-content.vala
src/ui/settings/monitor-choice-loader.vala
src/ui/settings/monitor-setting-binding.vala
src/ui/widgets/config-include-widget.vala
src/ui/widgets/mirror-settings-widget.vala
src/ui/widgets/monitor-row.vala
src/ui/widgets/primary-display-widget.vala
src/ui/widgets/status-widget.vala
......@@ -12,12 +12,24 @@ sources = files(
'backends/niri-backend.vala',
'core/display-model.vala',
'core/shell-command.vala',
'ui/config-include-validator.vala',
'ui/displays-view.vala',
'ui/monitor-layout.vala',
'ui/monitor-row.vala',
'ui/monitor-settings-content.vala',
'ui/ui-helpers.vala',
'ui/common/displays-controller.vala',
'ui/common/displays-visibility.vala',
'ui/common/ui-utils.vala',
'ui/widgets/backdrop-color-widget.vala',
'ui/widgets/config-include-widget.vala',
'ui/widgets/mirror-settings-widget.vala',
'ui/widgets/monitor-list-widget.vala',
'ui/widgets/monitor-layout.vala',
'ui/widgets/monitor-layout-widget.vala',
'ui/widgets/monitor-row.vala',
'ui/widgets/primary-display-widget.vala',
'ui/widgets/single-monitor-widget.vala',
'ui/widgets/status-widget.vala',
'ui/pages/monitor-settings-content.vala',
'ui/settings/monitor-choice-loader.vala',
'ui/settings/monitor-setting-binding.vala',
'ui/settings/monitor-setting-validator.vala',
'ui/settings/monitor-settings-context.vala',
) + configure_file(
input: 'build.vala.in',
output: 'build.vala',
......
namespace TunerDisplays {
public class Addin : Tuner.Addin {
private DisplaysView view;
private Tuner.Page page;
private Tuner.Page monitor_page;
private Gtk.Box monitor_page_content;
......@@ -10,58 +9,56 @@ namespace TunerDisplays {
construct {
Intl.bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
view = new DisplaysView();
page = new Tuner.Page() {
title = _("Displays"),
id = "displays",
category = "system",
icon_name = "video-display-symbolic",
priority = 900,
content = view
};
var refresh = new Gtk.Button.from_icon_name("view-refresh-symbolic") {
tooltip_text = _("Refresh")
};
refresh.clicked.connect(view.reload);
DisplaysContext.controller = new DisplaysController();
typeof(ConfigIncludeBinding).ensure();
typeof(ConfigIncludeValidator).ensure();
typeof(ConfigIncludeContent).ensure();
typeof(ConfigIncludeWidget).ensure();
typeof(StatusWidget).ensure();
typeof(MirrorModeBinding).ensure();
typeof(DisplaysVisibilityBinding).ensure();
typeof(DisplaysVisibilityValidator).ensure();
typeof(MonitorLayout).ensure();
typeof(MonitorLayoutWidget).ensure();
typeof(MirrorSettingsWidget).ensure();
typeof(PrimaryDisplayWidget).ensure();
typeof(MonitorListWidget).ensure();
typeof(SingleMonitorWidget).ensure();
typeof(MonitorSettingBinding).ensure();
typeof(MonitorSettingValidator).ensure();
typeof(MonitorChoiceLoader).ensure();
typeof(BackdropColorContent).ensure();
typeof(BackdropColorWidget).ensure();
var builder = new Gtk.Builder.from_resource("/ru/ximperlinux/tuner/Displays/pages/displays-page.ui");
page = builder.get_object("displays_page") as Tuner.Page;
var refresh = builder.get_object("refresh_button") as Gtk.Button;
refresh.clicked.connect(DisplaysContext.controller.reload);
page.pack_end(refresh);
var apply = new Gtk.Button.with_label(_("Apply")) {
tooltip_text = _("Apply"),
sensitive = view.can_apply,
css_classes = { "suggested-action" }
};
apply.clicked.connect(view.apply_changes);
var apply = builder.get_object("apply_button") as Gtk.Button;
apply.sensitive = DisplaysContext.controller.can_apply;
apply.clicked.connect(DisplaysContext.controller.apply_changes);
page.pack_end(apply);
monitor_page_content = new Gtk.Box(Gtk.Orientation.VERTICAL, 0) {
hexpand = true,
vexpand = true
};
monitor_page_title = new Gtk.Label(_("Monitor")) {
single_line_mode = true,
ellipsize = Pango.EllipsizeMode.END
};
monitor_page = new Tuner.Page() {
title = _("Monitor"),
id = monitor_settings_page_id(),
content = monitor_page_content,
title_widget = monitor_page_title
};
var monitor_apply = new Gtk.Button.with_label(_("Apply")) {
tooltip_text = _("Apply"),
sensitive = view.can_apply,
css_classes = { "suggested-action" }
};
monitor_apply.clicked.connect(view.apply_changes);
monitor_page = builder.get_object("monitor_settings_page") as Tuner.Page;
monitor_page_content = builder.get_object("monitor_page_content") as Gtk.Box;
monitor_page_title = builder.get_object("monitor_page_title") as Gtk.Label;
monitor_page_title.single_line_mode = true;
monitor_page_title.ellipsize = Pango.EllipsizeMode.END;
var monitor_apply = builder.get_object("monitor_apply_button") as Gtk.Button;
monitor_apply.sensitive = DisplaysContext.controller.can_apply;
monitor_apply.clicked.connect(DisplaysContext.controller.apply_changes);
monitor_page.pack_end(monitor_apply);
page.add_page(monitor_page);
view.monitor_settings_requested.connect(show_monitor_settings);
DisplaysContext.controller.monitor_settings_requested.connect(show_monitor_settings);
add_page(page);
DisplaysContext.controller.reload();
}
private void show_monitor_settings(MonitorConfig monitor) {
......@@ -79,8 +76,8 @@ namespace TunerDisplays {
}
private MonitorSettingsContent create_monitor_page_content(MonitorConfig monitor) {
var content = new MonitorSettingsContent(monitor, view.display_backend, view.monitor_configs);
content.monitor_changed.connect(view.refresh_from_monitors);
var content = new MonitorSettingsContent(monitor, DisplaysContext.controller.backend, DisplaysContext.controller.monitors);
content.monitor_changed.connect(DisplaysContext.controller.refresh_from_monitors);
return content;
}
}
......
namespace TunerDisplays {
public class DisplaysContext : Object {
public static DisplaysController controller;
}
public class DisplaysController : Object {
public DisplayBackend backend { get; private set; }
public Gee.ArrayList<MonitorConfig> monitors { get; private set; default = new Gee.ArrayList<MonitorConfig>(); }
public string? status_title { get; private set; }
public string? status_subtitle { get; private set; }
private DBusConnection? session_bus;
private uint monitors_changed_id;
public bool can_apply { get { return backend.can_apply; } }
public bool has_status { get { return status_title != null; } }
public signal void changed();
public signal void monitor_settings_requested(MonitorConfig monitor);
public DisplaysController() {
backend = DisplayBackend.create_for_session();
subscribe_monitor_changes();
}
~DisplaysController() {
if (session_bus != null && monitors_changed_id != 0)
session_bus.signal_unsubscribe(monitors_changed_id);
}
public void reload() {
status_title = null;
status_subtitle = null;
try {
monitors = merge_loaded_monitors(monitors, backend.load());
} catch (Error err) {
monitors = new Gee.ArrayList<MonitorConfig>();
status_title = _("Failed to load monitors");
status_subtitle = err.message;
}
changed();
}
public void apply_changes() {
try {
backend.apply(monitors);
Tuner.toast(_("Monitor settings applied"));
} catch (Error err) {
Tuner.toast(err.message);
}
}
public void refresh_from_monitors() {
changed();
}
public void open_monitor_settings(MonitorConfig monitor) {
monitor_settings_requested(monitor);
}
public bool mirror_enabled() {
return backend.supports_global_mirroring && monitors.size > 0 && monitors[0].mirrored;
}
public bool single_monitor_mode() {
return monitors.size == 1;
}
public bool layout_visible() {
return !mirror_enabled() && !single_monitor_mode();
}
public bool layout_group_visible() {
return layout_visible() || mirror_enabled();
}
public bool mirror_mode_visible() {
return backend.supports_global_mirroring && monitors.size > 1;
}
public void set_mirror_enabled(bool active) {
foreach (var monitor in monitors) {
monitor.mirrored = active;
if (active) {
monitor.enabled = true;
monitor.x = 0;
monitor.y = 0;
} else {
place_monitor_after_active(monitor, monitors);
}
}
changed();
}
public void set_primary(MonitorConfig selected) {
foreach (var monitor in monitors)
monitor.primary = monitor == selected;
changed();
}
public Gee.ArrayList<MonitorConfig> enabled_monitors() {
var enabled = new Gee.ArrayList<MonitorConfig>();
foreach (var monitor in monitors) {
if (monitor.enabled)
enabled.add(monitor);
}
return enabled;
}
public Gee.ArrayList<DisplayMode> common_mirror_resolutions() {
var resolutions = new Gee.ArrayList<DisplayMode>();
foreach (var mode in common_mirror_modes()) {
bool exists = false;
foreach (var existing in resolutions) {
if (existing.width == mode.width && existing.height == mode.height) {
exists = true;
break;
}
}
if (!exists)
resolutions.add(mode);
}
return resolutions;
}
public DisplayMode? selected_common_mirror_mode() {
if (monitors.size == 0)
return null;
foreach (var mode in common_mirror_resolutions()) {
if (mode.width == monitors[0].width && mode.height == monitors[0].height)
return mode;
}
var modes = common_mirror_resolutions();
return modes.size > 0 ? modes[0] : null;
}
public Gee.ArrayList<double?> common_mirror_scales(DisplayMode mode) {
var scales = new Gee.ArrayList<double?>();
foreach (var scale in mode.supported_scales) {
bool supported = true;
foreach (var monitor in monitors) {
var compatible = find_compatible_mirror_mode(monitor, mode);
if (compatible == null || !compatible.supports_scale(scale)) {
supported = false;
break;
}
}
if (supported)
scales.add(scale);
}
return scales;
}
public void apply_mirror_mode(DisplayMode selected_mode) {
foreach (var monitor in monitors) {
var mode = find_compatible_mirror_mode(monitor, selected_mode);
if (mode == null)
continue;
monitor.width = mode.width;
monitor.height = mode.height;
monitor.refresh = mode.refresh;
monitor.variable_refresh_rate = mode.variable_refresh_rate;
if (!mode.supports_scale(monitor.scale))
monitor.scale = mode.preferred_scale;
}
changed();
}
public void apply_mirror_scale(double scale) {
foreach (var monitor in monitors)
monitor.scale = scale;
changed();
}
public void apply_mirror_transform(string transform) {
foreach (var monitor in monitors)
monitor.transform = transform;
changed();
}
private Gee.ArrayList<DisplayMode> common_mirror_modes() {
var modes = new Gee.ArrayList<DisplayMode>();
if (monitors.size == 0)
return modes;
foreach (var mode in monitors[0].modes) {
if (all_monitors_support_mode(mode))
modes.add(mode);
}
return modes;
}
private bool all_monitors_support_mode(DisplayMode mode) {
foreach (var monitor in monitors) {
if (find_compatible_mirror_mode(monitor, mode) == null)
return false;
}
return true;
}
private void subscribe_monitor_changes() {
if (!backend.supports_monitor_change_events)
return;
try {
session_bus = Bus.get_sync(BusType.SESSION);
monitors_changed_id = session_bus.signal_subscribe(
"org.gnome.Mutter.DisplayConfig",
"org.gnome.Mutter.DisplayConfig",
"MonitorsChanged",
"/org/gnome/Mutter/DisplayConfig",
null,
DBusSignalFlags.NONE,
() => {
Idle.add(() => {
reload();
return false;
});
}
);
} catch (Error err) {
warning("Failed to subscribe to GNOME monitor changes: %s", err.message);
}
}
private static DisplayMode? find_compatible_mirror_mode(MonitorConfig monitor, DisplayMode mode) {
foreach (var candidate in monitor.modes) {
if (candidate.width == mode.width
&& candidate.height == mode.height
&& Math.fabs(candidate.refresh - mode.refresh) < 0.02) {
return candidate;
}
}
foreach (var candidate in monitor.modes) {
if (candidate.width == mode.width && candidate.height == mode.height)
return candidate;
}
return null;
}
private static Gee.ArrayList<MonitorConfig> merge_loaded_monitors(Gee.ArrayList<MonitorConfig> current, Gee.ArrayList<MonitorConfig> loaded) {
var merged = new Gee.ArrayList<MonitorConfig>();
foreach (var loaded_monitor in loaded) {
var monitor = find_monitor_by_name(current, loaded_monitor.name);
if (monitor == null)
monitor = loaded_monitor;
else
monitor.copy_from(loaded_monitor);
merged.add(monitor);
}
return merged;
}
private static MonitorConfig? find_monitor_by_name(Gee.ArrayList<MonitorConfig> monitors, string name) {
foreach (var monitor in monitors) {
if (monitor.name == name)
return monitor;
}
return null;
}
}
}
namespace TunerDisplays {
public class MirrorModeBinding : Tuner.Binding {
construct {
DisplaysContext.controller.changed.connect(emit_changed);
}
public override Type expected_type { get { return typeof(bool); } }
public override bool get_value(ref Value value) {
value = DisplaysContext.controller.mirror_enabled();
return true;
}
public override void set_value(Value value) {
DisplaysContext.controller.set_mirror_enabled(value.get_boolean());
}
}
public class DisplaysVisibilityBinding : Tuner.Binding {
public string mode { get; set; default = "always"; }
construct {
DisplaysContext.controller.changed.connect(emit_changed);
}
public override Type expected_type { get { return typeof(bool); } }
public override bool get_value(ref Value value) {
value = displays_visibility(mode);
return true;
}
public override void set_value(Value value) {
}
}
public class DisplaysVisibilityValidator : Tuner.Validator {
public string mode { get; set; default = ""; }
public string group_mode { get; set; default = ""; }
public override void apply(Tuner.Binding binding, Gtk.Widget native_widget) {
update(binding, native_widget);
DisplaysContext.controller.changed.connect(() => update(binding, native_widget));
}
private void update(Tuner.Binding binding, Gtk.Widget widget) {
widget.visible = current_visibility(binding, mode);
var group = widget.get_ancestor(typeof(Adw.PreferencesGroup));
if (group != null)
group.visible = group_mode == "" ? widget.visible : displays_visibility(group_mode);
}
private static bool current_visibility(Tuner.Binding binding, string mode) {
if (mode != "")
return displays_visibility(mode);
Value value = Value(typeof(bool));
return binding.get_value(ref value) && value.get_boolean();
}
}
internal static bool displays_visibility(string mode) {
var controller = DisplaysContext.controller;
switch (mode) {
case "always":
return true;
case "mirror-switch":
return controller.mirror_mode_visible();
case "layout":
return controller.layout_visible();
case "layout-group":
return controller.layout_group_visible();
case "status-group":
return config_include_visible()
|| controller.has_status
|| !controller.can_apply
|| controller.mirror_mode_visible();
case "details":
return !controller.mirror_enabled() && !controller.single_monitor_mode();
case "single-monitor":
return controller.single_monitor_mode();
default:
return true;
}
}
internal static bool config_include_visible() {
var info = DisplaysContext.controller.backend.config_include_info();
return info.state == ConfigIncludeState.NOT_INCLUDED
|| info.state == ConfigIncludeState.MISSING_MAIN_CONFIG;
}
}
namespace TunerDisplays {
internal static Gtk.ListBox create_boxed_list() {
var list = new Gtk.ListBox() {
selection_mode = Gtk.SelectionMode.NONE
};
list.add_css_class("boxed-list");
return list;
}
internal static void clear_list(Gtk.ListBox list) {
var child = list.get_first_child();
while (child != null) {
var next = child.get_next_sibling();
list.remove(child);
child = next;
}
}
internal static bool list_has_children(Gtk.ListBox list) {
return list.get_first_child() != null;
}
public static string monitor_settings_page_id() {
return "display-monitor-settings";
}
......@@ -22,15 +43,6 @@ namespace TunerDisplays {
monitor.y = 0;
}
private static bool has_resolution(Gee.ArrayList<DisplayMode> modes, int width, int height) {
foreach (var mode in modes) {
if (mode.width == width && mode.height == height)
return true;
}
return false;
}
private static string refresh_rate_label(DisplayMode mode) {
return _("%.2f Hz").printf(mode.refresh);
}
......
namespace TunerDisplays {
public class MonitorSettingsContent : Tuner.PanelContent {
private MonitorSettingsContext context;
private Tuner.Page settings_page;
public signal void monitor_changed();
public MonitorSettingsContent(MonitorConfig monitor, DisplayBackend backend, Gee.ArrayList<MonitorConfig> all_monitors, bool show_enabled = true) {
Object();
context = new MonitorSettingsContext(monitor, backend, all_monitors, show_enabled);
MonitorSettingsState.current = context;
var builder = new Gtk.Builder.from_resource("/ru/ximperlinux/tuner/Displays/pages/monitor-settings-content.ui");
settings_page = builder.get_object("monitor_settings_content_page") as Tuner.Page;
page = settings_page;
context.changed.connect(() => monitor_changed());
build();
}
}
}
namespace TunerDisplays {
public class MonitorChoiceLoader : Tuner.ChoiceLoader {
public string field { get; set; default = ""; }
public override void load(ListStore model) {
switch (field) {
case "mode":
load_modes(model);
break;
case "resolution":
load_resolutions(model);
break;
case "refresh":
load_refresh_rates(model);
break;
case "scale":
load_scales(model);
break;
case "transform":
add_choice(model, _("Normal"), "normal");
add_choice(model, _("90 degrees"), "90");
add_choice(model, _("180 degrees"), "180");
add_choice(model, _("270 degrees"), "270");
add_choice(model, _("Flipped"), "flipped");
add_choice(model, _("Flipped 90 degrees"), "flipped-90");
add_choice(model, _("Flipped 180 degrees"), "flipped-180");
add_choice(model, _("Flipped 270 degrees"), "flipped-270");
break;
case "mirror":
load_mirrors(model);
break;
case "bitdepth":
add_choice(model, "8", "8");
add_choice(model, "10", "10");
break;
case "hypr-vrr":
add_choice(model, _("Off"), "0");
add_choice(model, _("On"), "1");
add_choice(model, _("Fullscreen"), "2");
add_choice(model, _("Fullscreen video/game"), "3");
break;
case "niri-vrr":
add_choice(model, _("Off"), "0");
add_choice(model, _("On"), "1");
add_choice(model, _("On demand"), "2");
break;
case "color-management":
foreach (var value in new string[] { "auto", "srgb", "dcip3", "dp3", "adobe", "wide", "edid", "hdr", "hdredid" })
add_choice(model, value, value);
break;
case "sdr-eotf":
foreach (var value in new string[] { "default", "gamma22", "srgb" })
add_choice(model, value, value);
break;
case "force-toggle":
add_choice(model, _("Auto"), "0");
add_choice(model, _("Off"), "-1");
add_choice(model, _("On"), "1");
break;
case "hot-corners":
add_choice(model, _("Default"), "");
add_choice(model, _("Off"), "off");
add_choice(model, _("All"), "all");
add_choice(model, _("Top left"), "top-left");
add_choice(model, _("Top right"), "top-right");
add_choice(model, _("Bottom left"), "bottom-left");
add_choice(model, _("Bottom right"), "bottom-right");
break;
}
}
private static void load_modes(ListStore model) {
foreach (var mode in MonitorSettingsState.current.monitor.modes) {
add_choice(
model,
mode.label,
MonitorSettingBinding.mode_key(mode.width, mode.height, mode.refresh, mode.variable_refresh_rate)
);
}
}
private static void load_resolutions(ListStore model) {
var values = new Gee.ArrayList<string>();
foreach (var mode in MonitorSettingsState.current.monitor.modes) {
var key = MonitorSettingBinding.resolution_key(mode.width, mode.height);
if (values.contains(key))
continue;
values.add(key);
add_choice(model, key, key);
}
}
private static void load_refresh_rates(ListStore model) {
var monitor = MonitorSettingsState.current.monitor;
foreach (var mode in monitor.modes) {
if (mode.width != monitor.width || mode.height != monitor.height || mode.variable_refresh_rate)
continue;
add_choice(model, refresh_rate_label(mode), MonitorSettingBinding.refresh_key(mode.refresh));
}
}
private static void load_scales(ListStore model) {
var mode = MonitorSettingBinding.find_best_mode_for_resolution(
MonitorSettingsState.current.monitor.width,
MonitorSettingsState.current.monitor.height
);
if (mode == null)
return;
foreach (var scale in mode.supported_scales)
add_choice(model, "%.0f%%".printf(scale * 100), "%.3f".printf(scale).replace(",", "."));
}
private static void load_mirrors(ListStore model) {
var context = MonitorSettingsState.current;
add_choice(model, _("None"), "");
foreach (var monitor in context.all_monitors) {
if (monitor != context.monitor)
add_choice(model, monitor.title, monitor.name);
}
}
private static void add_choice(ListStore model, string title, string value) {
model.append(new Tuner.Choice() {
title = title,
value = new Variant.string(value)
});
}
}
}
namespace TunerDisplays {
public class MonitorSettingValidator : Tuner.Validator {
public string feature { get; set; default = ""; }
public string group_feature { get; set; default = ""; }
public override void apply(Tuner.Binding binding, Gtk.Widget native_widget) {
update(native_widget);
MonitorSettingsState.current.changed.connect(() => update(native_widget));
}
private void update(Gtk.Widget widget) {
widget.visible = monitor_feature_visible(feature);
var group = widget.get_ancestor(typeof(Adw.PreferencesGroup));
if (group != null && group_feature != "")
group.visible = monitor_feature_visible(group_feature);
}
}
internal static bool monitor_feature_visible(string feature) {
var context = MonitorSettingsState.current;
var backend = context.backend;
var monitor = context.monitor;
switch (feature) {
case "":
case "always":
return true;
case "enabled":
return context.show_enabled;
case "combined-mode":
return !backend.uses_separate_refresh_rate_controls;
case "separate-refresh":
return backend.uses_separate_refresh_rate_controls;
case "fixed-refresh":
return backend.uses_separate_refresh_rate_controls && !monitor.variable_refresh_rate;
case "variable-refresh":
return backend.uses_separate_refresh_rate_controls
&& monitor.supports_variable_refresh_rate
&& MonitorSettingBinding.find_variable_mode_for_resolution(monitor.width, monitor.height) != null;
case "scale-choice":
return backend.uses_separate_refresh_rate_controls && selected_mode_has_scales();
case "scale-spin":
return !backend.uses_separate_refresh_rate_controls || !selected_mode_has_scales();
case "mirror":
return backend.supports_output_mirroring && context.all_monitors.size > 1;
case "adaptive-sync":
return backend.supports_adaptive_sync && monitor.supports_variable_refresh_rate;
case "focus-at-startup":
return backend.supports_focus_at_startup;
case "backdrop-color":
return backend.supports_backdrop_color;
case "hot-corners":
return backend.supports_hot_corners;
case "underscanning":
return backend.supports_underscanning;
case "hdr-toggle":
return backend.supports_hdr_toggle && monitor.supported_color_modes.contains(1);
case "description-identifier":
return backend.supports_description_identifier;
case "bit-depth":
return backend.supports_bit_depth;
case "vrr-modes":
return backend.supports_vrr_modes;
case "color-management":
return backend.supports_color_management;
case "hdr-metadata":
return backend.supports_hdr_metadata;
case "hyprland":
return backend.supports_description_identifier
|| backend.supports_bit_depth
|| backend.supports_vrr_modes
|| backend.supports_color_management;
case "hdr":
return backend.supports_hdr_metadata;
default:
return true;
}
}
private static bool selected_mode_has_scales() {
var monitor = MonitorSettingsState.current.monitor;
var mode = MonitorSettingBinding.find_best_mode_for_resolution(monitor.width, monitor.height);
return mode != null && mode.supported_scales.size > 0;
}
}
namespace TunerDisplays {
public class MonitorSettingsContext : Object {
public MonitorConfig monitor { get; construct; }
public DisplayBackend backend { get; construct; }
public Gee.ArrayList<MonitorConfig> all_monitors { get; construct; }
public bool show_enabled { get; construct; }
public signal void changed();
public MonitorSettingsContext(MonitorConfig monitor, DisplayBackend backend, Gee.ArrayList<MonitorConfig> all_monitors, bool show_enabled) {
Object(monitor: monitor, backend: backend, all_monitors: all_monitors, show_enabled: show_enabled);
}
public void emit_changed() {
changed();
}
}
public class MonitorSettingsState : Object {
public static MonitorSettingsContext current;
}
}
namespace TunerDisplays {
[GtkTemplate (ui = "/ru/ximperlinux/tuner/Displays/widgets/backdrop-color-widget.ui")]
public class BackdropColorContent : Adw.ActionRow {
[GtkChild] private unowned Gtk.Switch enabled_switch;
[GtkChild] private unowned Gtk.ColorDialogButton color_button;
private MonitorSettingBinding color_binding;
private bool updating;
public void setup(MonitorSettingBinding binding) {
color_binding = binding;
sync_from_binding();
enabled_switch.notify["active"].connect(() => {
if (updating)
return;
color_button.sensitive = enabled_switch.active;
color_binding.set_value(color_value());
});
color_button.notify["rgba"].connect(() => {
if (updating || !enabled_switch.active)
return;
color_binding.set_value(color_value());
});
binding.changed.connect(sync_from_binding);
}
private void sync_from_binding() {
if (color_binding == null)
return;
Value value = Value(typeof(string));
if (!color_binding.get_value(ref value))
return;
updating = true;
var text = value.get_string() ?? "";
enabled_switch.active = text != "";
color_button.sensitive = enabled_switch.active;
Gdk.RGBA color = { 0, 0, 0, 1 };
if (text != "")
color.parse(text);
color_button.rgba = color;
updating = false;
}
private Value color_value() {
Value value = Value(typeof(string));
value.set_string(enabled_switch.active ? rgba_to_css(color_button.get_rgba()) : "");
return value;
}
private static string rgba_to_css(Gdk.RGBA? rgba) {
if (rgba == null)
return "";
if (rgba.alpha >= 0.999) {
return "#%02x%02x%02x".printf(
(int) Math.round(rgba.red * 255),
(int) Math.round(rgba.green * 255),
(int) Math.round(rgba.blue * 255)
);
}
var alpha = "%.3f".printf(rgba.alpha).replace(",", ".");
return "rgba(%d, %d, %d, %s)".printf(
(int) Math.round(rgba.red * 255),
(int) Math.round(rgba.green * 255),
(int) Math.round(rgba.blue * 255),
alpha
);
}
}
public class BackdropColorWidget : Tuner.Widget {
public override Gtk.Widget? create() {
var row = new BackdropColorContent();
var color_binding = binding as MonitorSettingBinding;
if (color_binding != null)
row.setup(color_binding);
if (binding != null && binding.validator != null)
binding.validator.apply(binding, row);
return row;
}
}
}
namespace TunerDisplays {
public class ConfigIncludeBinding : Tuner.Binding {
private DisplayBackend backend;
public ConfigIncludeBinding(DisplayBackend backend) {
this.backend = backend;
}
public ConfigIncludeInfo info() {
return backend.config_include_info();
return DisplaysContext.controller.backend.config_include_info();
}
public void connect_config() throws Error {
backend.include_monitor_config();
DisplaysContext.controller.backend.include_monitor_config();
emit_changed();
DisplaysContext.controller.reload();
}
public override Type expected_type { get { return typeof(bool); } }
public override bool get_value(ref Value value) {
value = info().state == ConfigIncludeState.NOT_INCLUDED;
value = config_include_visible();
return true;
}
......@@ -41,13 +36,12 @@ namespace TunerDisplays {
private static void update(ConfigIncludeBinding binding, Adw.ActionRow row) {
var info = binding.info();
var visible = info.state == ConfigIncludeState.NOT_INCLUDED
|| info.state == ConfigIncludeState.MISSING_MAIN_CONFIG;
var visible = config_include_visible();
row.visible = visible;
var group = row.get_ancestor(typeof(Adw.PreferencesGroup));
if (group != null)
group.visible = visible;
group.visible = displays_visibility("status-group");
if (!visible)
return;
......@@ -56,4 +50,37 @@ namespace TunerDisplays {
row.subtitle = info.subtitle;
}
}
[GtkTemplate (ui = "/ru/ximperlinux/tuner/Displays/widgets/config-include-widget.ui")]
public class ConfigIncludeContent : Adw.ActionRow {
[GtkChild] private unowned Gtk.Button connect_button;
private ConfigIncludeBinding include_binding;
construct {
connect_button.clicked.connect(() => {
try {
include_binding.connect_config();
Tuner.toast(_("Monitor configuration connected"));
} catch (Error err) {
Tuner.toast(err.message);
}
});
}
public void setup(ConfigIncludeBinding binding) {
include_binding = binding;
}
}
public class ConfigIncludeWidget : Tuner.Widget {
public override Gtk.Widget? create() {
var row = new ConfigIncludeContent();
var include_binding = binding as ConfigIncludeBinding;
if (include_binding != null)
row.setup(include_binding);
if (binding != null && binding.validator != null)
binding.validator.apply(binding, row);
return row;
}
}
}
namespace TunerDisplays {
public class MirrorSettingsWidget : Tuner.Widget {
private Gtk.ListBox list;
public override Gtk.Widget? create() {
list = create_boxed_list();
DisplaysContext.controller.changed.connect(rebuild);
rebuild();
return list;
}
private void rebuild() {
clear_list(list);
var controller = DisplaysContext.controller;
if (!controller.mirror_enabled()) {
list.visible = false;
return;
}
add_mirror_mode_row(controller);
add_mirror_scale_row(controller);
add_mirror_transform_row(controller);
list.visible = list_has_children(list);
}
private void add_mirror_mode_row(DisplaysController controller) {
var modes = controller.common_mirror_resolutions();
if (modes.size == 0)
return;
var model = new Gtk.StringList(null);
uint selected = 0;
for (int i = 0; i < modes.size; i++) {
var mode = modes[i];
model.append("%dx%d".printf(mode.width, mode.height));
if (controller.monitors.size > 0
&& mode.width == controller.monitors[0].width
&& mode.height == controller.monitors[0].height) {
selected = i;
}
}
var row = new Adw.ComboRow() {
title = _("Resolution"),
model = model,
selected = selected
};
row.notify["selected"].connect(() => {
var index = (int) row.selected;
if (index >= 0 && index < modes.size)
controller.apply_mirror_mode(modes[index]);
});
list.append(row);
}
private void add_mirror_scale_row(DisplaysController controller) {
var mode = controller.selected_common_mirror_mode();
if (mode == null)
return;
var scales = controller.common_mirror_scales(mode);
if (scales.size == 0)
return;
var model = new Gtk.StringList(null);
uint selected = 0;
for (int i = 0; i < scales.size; i++) {
var scale = scales[i];
model.append("%.0f%%".printf(scale * 100));
if (controller.monitors.size > 0 && Math.fabs(scale - controller.monitors[0].scale) < 0.01)
selected = i;
}
var row = new Adw.ComboRow() {
title = _("Scale"),
model = model,
selected = selected
};
row.notify["selected"].connect(() => {
var index = (int) row.selected;
if (index >= 0 && index < scales.size)
controller.apply_mirror_scale(scales[index]);
});
list.append(row);
}
private void add_mirror_transform_row(DisplaysController controller) {
var model = new Gtk.StringList(null);
string[] titles = {
_("Normal"), _("90 degrees"), _("180 degrees"), _("270 degrees"),
_("Flipped"), _("Flipped 90 degrees"), _("Flipped 180 degrees"), _("Flipped 270 degrees")
};
foreach (var title in titles)
model.append(title);
var row = new Adw.ComboRow() {
title = _("Rotation"),
model = model,
selected = controller.monitors.size > 0 ? transform_to_index(controller.monitors[0].transform) : 0
};
row.notify["selected"].connect(() => controller.apply_mirror_transform(transform_from_index((int) row.selected)));
list.append(row);
}
}
}
namespace TunerDisplays {
public class MonitorLayoutWidget : Tuner.Widget {
private MonitorLayout content;
public override Gtk.Widget? create() {
var row = new Adw.PreferencesRow() {
activatable = false,
selectable = false,
can_focus = false,
focusable = false
};
content = new MonitorLayout();
update_layout();
content.layout_changed.connect(DisplaysContext.controller.refresh_from_monitors);
DisplaysContext.controller.changed.connect(update_layout);
if (binding != null && binding.validator != null)
binding.validator.apply(binding, row);
row.child = content;
return row;
}
private void update_layout() {
var controller = DisplaysContext.controller;
content.require_connected = controller.backend.requires_connected_layout;
content.set_monitors(controller.monitors);
}
}
}
namespace TunerDisplays {
[GtkTemplate (ui = "/ru/ximperlinux/tuner/Displays/widgets/monitor-layout.ui")]
public class MonitorLayout : Gtk.DrawingArea {
private const double EDGE_PADDING = 6;
private const double MARGIN_MON = 0.66;
......@@ -24,6 +25,7 @@ namespace TunerDisplays {
public signal void layout_changed();
public MonitorLayout() {
init_template();
set_draw_func(draw);
var drag = new Gtk.GestureDrag();
......@@ -68,6 +70,13 @@ namespace TunerDisplays {
}
public void set_monitors(Gee.ArrayList<MonitorConfig> monitors) {
if (this.monitors == monitors) {
if (!drag_active)
needs_recenter = true;
queue_draw();
return;
}
this.monitors = monitors;
dragged = -1;
needs_recenter = true;
......
namespace TunerDisplays {
public class MonitorListWidget : Tuner.Widget {
private Gtk.ListBox list;
public override Gtk.Widget? create() {
list = create_boxed_list();
DisplaysContext.controller.changed.connect(rebuild);
rebuild();
return list;
}
private void rebuild() {
clear_list(list);
var controller = DisplaysContext.controller;
if (controller.mirror_enabled() || controller.single_monitor_mode()) {
list.visible = false;
return;
}
foreach (var monitor in controller.monitors) {
var row = new MonitorRow(monitor, monitor_settings_page_id(), controller.monitors, controller.backend);
row.monitor_selected.connect(controller.open_monitor_settings);
row.monitor_changed.connect(controller.refresh_from_monitors);
list.append(row);
}
list.visible = list_has_children(list);
}
}
}
namespace TunerDisplays {
public class PrimaryDisplayWidget : Tuner.Widget {
private Gtk.ListBox list;
public override Gtk.Widget? create() {
list = create_boxed_list();
DisplaysContext.controller.changed.connect(rebuild);
rebuild();
return list;
}
private void rebuild() {
clear_list(list);
var controller = DisplaysContext.controller;
if (!controller.backend.supports_primary_display || controller.monitors.size <= 1 || controller.mirror_enabled()) {
list.visible = false;
return;
}
var model = new Gtk.StringList(null);
var values = controller.enabled_monitors();
uint selected = 0;
for (int i = 0; i < values.size; i++) {
var monitor = values[i];
model.append(monitor.title);
if (monitor.primary)
selected = i;
}
if (values.size <= 1) {
list.visible = false;
return;
}
var row = new Adw.ComboRow() {
title = _("Primary Display"),
model = model,
selected = selected
};
row.notify["selected"].connect(() => {
var index = (int) row.selected;
if (index >= 0 && index < values.size)
controller.set_primary(values[index]);
});
list.append(row);
list.visible = true;
}
}
}
namespace TunerDisplays {
public class SingleMonitorWidget : Tuner.Widget {
private Gtk.Box box;
private MonitorSettingsContent? content;
public override Gtk.Widget? create() {
box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
DisplaysContext.controller.changed.connect(rebuild);
rebuild();
if (binding != null && binding.validator != null)
binding.validator.apply(binding, box);
return box;
}
private void rebuild() {
var child = box.get_first_child();
while (child != null) {
var next = child.get_next_sibling();
box.remove(child);
child = next;
}
content = null;
var controller = DisplaysContext.controller;
if (!controller.single_monitor_mode()) {
box.visible = false;
return;
}
content = new MonitorSettingsContent(controller.monitors[0], controller.backend, controller.monitors, false);
content.monitor_changed.connect(controller.refresh_from_monitors);
box.append(content);
box.visible = true;
}
}
}
namespace TunerDisplays {
public class StatusWidget : Tuner.Widget {
private Gtk.ListBox list;
public override Gtk.Widget? create() {
list = create_boxed_list();
DisplaysContext.controller.changed.connect(rebuild);
rebuild();
if (binding != null && binding.validator != null)
binding.validator.apply(binding, list);
return list;
}
private void rebuild() {
clear_list(list);
var controller = DisplaysContext.controller;
if (controller.has_status) {
list.append(new Adw.ActionRow() {
title = controller.status_title,
subtitle = controller.status_subtitle
});
}
if (!controller.can_apply) {
list.append(new Adw.ActionRow() {
title = _("Read-only backend"),
subtitle = _("Applying monitor layouts is not supported by this backend.")
});
}
list.visible = list_has_children(list);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment