/* This file is part of CoverflowAltTab. CoverflowAltTab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. CoverflowAltTab 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 General Public License for more details. You should have received a copy of the GNU General Public License along with CoverflowAltTab. If not, see . */ /* * CoverflowAltTab * * Preferences dialog for "gnome-extensions prefs" tool * * Based on preferences in the following extensions: JustPerfection, dash2doc-lite, night theme switcher, and desktop cube * */ import Adw from 'gi://Adw'; import Gdk from 'gi://Gdk'; import GLib from 'gi://GLib'; import Gtk from 'gi://Gtk'; import Gio from 'gi://Gio'; import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js' const easing_options = [ { id: 'ease-linear', name: 'easeLinear' }, { id: 'ease-in-quad', name: "easeInQuad" }, { id: 'ease-out-quad', name: "easeOutQuad" }, { id: 'ease-in-out-quad', name: "easeInOutQuad" }, { id: 'ease-in-cubic', name: "easeInCubic" }, { id: 'ease-out-cubic', name: "easeOutCubic" }, { id: 'ease-in-out-cubic', name: "easeInOutCubic" }, { id: 'ease-in-quart', name: "easeInQuart" }, { id: 'ease-out-quart', name: "easeOutQuart" }, { id: 'ease-in-out-quart', name: "easeInOutQuart" }, { id: 'ease-in-quint', name: "easeInQuint" }, { id: 'ease-out-quint', name: "easeOutQuint" }, { id: 'ease-in-out-quint', name: "easeInOutQuint" }, { id: 'ease-in-sine', name: "easeInSine" }, { id: 'ease-out-sine', name: "easeOutSine" }, { id: 'ease-in-out-sine', name: "easeInOutSine" }, { id: 'ease-in-expo', name: "easeInExpo" }, { id: 'ease-out-expo', name: "easeOutExpo" }, { id: 'ease-in-out-expo', name: "easeInOutExpo" }, { id: 'ease-in-circ', name: "easeInCirc" }, { id: 'ease-out-circ', name: "easeOutCirc" }, { id: 'ease-in-out-circ', name: "easeInOutCirc" }, { id: 'ease-in-back', name: "easeInBack" }, { id: 'ease-out-back', name: "easeOutBack" }, { id: 'ease-in-out-back', name: "easeInOutBack" }, { id: 'ease-in-elastic', name: "easeInElastic" }, { id: 'ease-out-elastic', name: "easeOutElastic" }, { id: 'ease-in-out-elastic', name: "easeInOutElastic" }, { id: 'ease-in-bounce', name: "easeInBounce" }, { id: 'ease-out-bounce', name: "easeOutBounce" }, { id: 'ease-in-out-bounce', name: "easeInOutBounce" }, { id: 'random', name: "Random" }]; function getBaseString(translatedString) { switch (translatedString) { case _("Coverflow"): return "Coverflow"; case _("Timeline"): return "Timeline"; case _("Bottom"): return "Bottom"; case _("Top"): return "Top"; case _("Classic"): return "Classic"; case _("Overlay"): return "Overlay"; default: return translatedString; } } function makeResetButton() { return new Gtk.Button({ icon_name: "edit-clear-symbolic", tooltip_text: _("Reset to default value"), valign: Gtk.Align.CENTER, }); } export default class CoverflowAltTabPreferences extends ExtensionPreferences { constructor(metadata) { super(metadata); let IconsPath = GLib.build_filenamev([this.path, 'ui', 'icons']); let iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()); iconTheme.add_search_path(IconsPath); } getVersionString(_page) { return _('Version %d').format(this.metadata.version); } fillPreferencesWindow(window) { let settings = this.getSettings(); let general_page = new Adw.PreferencesPage({ title: _('General'), icon_name: 'general-symbolic', }); let switcher_pref_group = new Adw.PreferencesGroup({ title: _('Switcher'), }); let switcher_looping_method_buttons = new Map([ [_("Flip Stack"), [[],[]]], [_("Carousel"), [[],[]]]]); let switcher_looping_method_row = buildRadioAdw(settings, "switcher-looping-method", switcher_looping_method_buttons, _("Looping Method"), _("How to cycle through windows.")); switcher_pref_group.add(buildRadioAdw(settings, "switcher-style", new Map([ [_("Coverflow"), [[switcher_looping_method_row], []]], [_("Timeline"), [[],[switcher_looping_method_row]] ]]), _("Style"), _("Pick the type of switcher."))) switcher_pref_group.add(buildSpinAdw(settings, "offset", [-500, 500, 1, 10], _("Vertical Offset"), _("Positive value moves everything down, negative up."))); switcher_pref_group.add(buildRadioAdw(settings, "position", new Map([ [_("Bottom"), [[], []]], [_("Top"), [[],[]]]]), _("Window Title Position"), _("Place window title above or below the switcher."))); switcher_pref_group.add(buildSwitcherAdw(settings, "enforce-primary-monitor", [], [], _("Enforce Primary Monitor"), _("Always show on the primary monitor, otherwise, show on the active monitor."))); switcher_pref_group.add(switcher_looping_method_row); switcher_pref_group.add(buildSwitcherAdw(settings, "hide-panel", [], [], _("Hide Panel"), _("Hide panel when switching windows."))); switcher_pref_group.add(buildSwitcherAdw(settings, "invert-swipes", [], [], _("Invert Swipes"), _("Swipe content instead of view."))); let animation_pref_group = new Adw.PreferencesGroup({ title: _('Animation'), }); animation_pref_group.add(buildDropDownAdw(settings, "easing-function", easing_options, "Easing Function", "Determine how windows move.")); animation_pref_group.add(buildRangeAdw(settings, "animation-time", [0.01, 20, 0.001, [0.5, 1, 1.5]], _("Duration [s]"), "", true)); animation_pref_group.add(buildSwitcherAdw(settings, "randomize-animation-times", [], [], _("Randomize Durations"), _("Each animation duration assigned randomly between 0 and configured duration."))); let windows_pref_group = new Adw.PreferencesGroup({ title: _('Switcher Windows'), }); let options = [{ id: 'current', name: _("Current workspace only") }, { id: 'all', name: _("All workspaces") }, { id: 'all-currentfirst', name: _("All workspaces, current first") }]; windows_pref_group.add(buildDropDownAdw(settings, "current-workspace-only", options, _("Workspaces"), _("Switch between windows on current or on all workspaces."))); windows_pref_group.add(buildSwitcherAdw(settings, "switch-per-monitor", [], [], _("Current Monitor"), _("Switch between windows on current monitor."))); let icon_pref_group = new Adw.PreferencesGroup({ title: _("Icons"), }); let size_row = buildRangeAdw(settings, "overlay-icon-size", [16, 1024, 1, [32, 64, 128, 256, 512]], _("Overlay Icon Size"), _("Set the overlay icon size in pixels."), true); let opacity_row = buildRangeAdw(settings, "overlay-icon-opacity", [0, 1, 0.001, [0.25, 0.5, 0.75]], _("Overlay Icon Opacity"), _("Set the overlay icon opacity."), true); let buttons = new Map([[_("Classic"), [[],[size_row, opacity_row]]], [_("Overlay"), [[size_row, opacity_row], []]], [_("Attached"), [[size_row, opacity_row], []]]]); let style_row = buildRadioAdw(settings, "icon-style", buttons, _("Application Icon Style")); icon_pref_group.add(style_row); icon_pref_group.add(size_row); icon_pref_group.add(opacity_row); icon_pref_group.add(buildSwitcherAdw(settings, "icon-has-shadow", [], [], _("Icon Shadow"))); let window_size_pref_group = new Adw.PreferencesGroup({ title: _("Window Properties") }); window_size_pref_group.add(buildRangeAdw(settings, "preview-to-monitor-ratio", [0, 1, 0.001, [0.250, 0.500, 0.750]], _("Window Preview Size to Monitor Size Ratio"), _("Maximum ratio of window preview size to monitor size."), true)); window_size_pref_group.add(buildRangeAdw(settings, "preview-scaling-factor", [0, 1, 0.001, [0.250, 0.500, 0.800]], _("Off-center Size Factor"), _("Factor by which to successively shrink previews off to the side."), true)); let background_application_switcher_pref_group = new Adw.PreferencesGroup({ title: _('Application Switcher'), }); background_application_switcher_pref_group.add(buildSwitcherAdw(settings, "switch-application-behaves-like-switch-windows", [], [], _("Make the Application Switcher Behave Like the Window Switcher"), _("Don't group windows of the same application in a subswitcher."))); background_application_switcher_pref_group.add(buildRangeAdw(settings, "desaturate-factor", [0, 1, 0.001, [0.25, 0.5, 0.75]], _("Desaturate"), _("Larger means more desaturation."), true)); background_application_switcher_pref_group.add(buildSpinAdw(settings, "blur-radius", [0, 20, 1, 1], _("Blur"), _("Larger means blurrier."))); let color_row = new Adw.ExpanderRow({ title: _("Tint"), }); background_application_switcher_pref_group.add(color_row); let use_tint_switch = new Gtk.Switch({ valign: Gtk.Align.CENTER, active: settings.get_boolean("use-tint"), }); settings.bind("use-tint", use_tint_switch, "active", Gio.SettingsBindFlags.DEFAULT); color_row.add_suffix(use_tint_switch); let tint_chooser_row = new Adw.ActionRow({ title: _("Color") }); let choose_tint_box = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, spacing: 10, valign: Gtk.Align.CENTER, }); tint_chooser_row.add_suffix(choose_tint_box); color_row.add_row(tint_chooser_row); let color_dialog = new Gtk.ColorDialog({ with_alpha: false, }); let color_button = new Gtk.ColorDialogButton({ valign: Gtk.Align.CENTER, dialog: color_dialog, }); let use_theme_color_button = new Gtk.Button({ label: _("Set to Theme Color"), valign: Gtk.Align.CENTER, }); use_theme_color_button.connect('clicked', () => { let c = settings.get_value("switcher-background-color").deep_unpack(); let rgba = color_button.rgba; rgba.red = c[0]; rgba.green = c[1]; rgba.blue = c[2]; rgba.alpha = 1 color_button.set_rgba(rgba); }); choose_tint_box.append(use_theme_color_button); choose_tint_box.append(color_button); let c = settings.get_value("tint-color").deep_unpack(); let rgba = color_button.rgba; rgba.red = c[0]; rgba.green = c[1]; rgba.blue = c[2]; rgba.alpha = 1 color_button.set_rgba(rgba); color_button.connect('notify::rgba', _ => { let c = color_button.rgba; let val = new GLib.Variant("(ddd)", [c.red, c.green, c.blue]); settings.set_value("tint-color", val); }); use_tint_switch.connect('notify::active', function(widget) { color_row.set_expanded(widget.get_active()); }); let reset_button = makeResetButton(); reset_button.connect("clicked", function (widget) { settings.reset("use-tint"); }); color_row.add_suffix(reset_button); color_row.add_row(buildRangeAdw(settings, "tint-blend", [0, 1, 0.001, [0.25, 0.5, 0.75]], _("Blend"), _("How much to blend the tint color; bigger means more tint color."), true)); background_application_switcher_pref_group.add(buildSwitcherAdw(settings, "use-glitch-effect", [], [], _("Glitch"))); let background_pref_group = new Adw.PreferencesGroup({ title: _('Background'), }); background_pref_group.add(buildRangeAdw(settings, "dim-factor", [0, 1, 0.001, [0.25, 0.5, 0.75]], _("Dim-factor"), _("Bigger means darker."), true)); let keybinding_pref_group = new Adw.PreferencesGroup({ title: _("Keybindings"), }); keybinding_pref_group.add(buildSwitcherAdw(settings, "bind-to-switch-windows", [], [], _("Bind to 'switch-windows'"))); keybinding_pref_group.add(buildSwitcherAdw(settings, "bind-to-switch-applications", [background_application_switcher_pref_group], [], _("Bind to 'switch-applications'"))); let pcorrection_pref_group = new Adw.PreferencesGroup({ title: _("Perspective Correction") }) pcorrection_pref_group.add(buildDropDownAdw(settings, "perspective-correction-method", [ { id: "None", name: _("None") }, { id: "Move Camera", name: _("Move Camera") }, { id: "Adjust Angles", name: _("Adjust Angles") }], _("Perspective Correction"), ("Method to make off-center switcher look centered."))); let highlight_mouse_over_pref_group = new Adw.PreferencesGroup({ title: _("Highlight Window Under Mouse"), }); window_size_pref_group.add(buildSwitcherAdw(settings, "highlight-mouse-over", [], [], _("Highlight Window Under Mouse"), _("Draw embelishment on window under the mouse to know the effects of clicking."))); window_size_pref_group.add(buildSwitcherAdw(settings, "raise-mouse-over", [], [], _("Raise Window Under Mouse"), _("Raise the window under the mouse above all others."))); /*let tweaks_page = new Adw.PreferencesPage({ title: _('Tweaks'), icon_name: 'applications-symbolic', }); tweaks_page.add(pcorrection_pref_group); tweaks_page.add(highlight_mouse_over_pref_group);*/ general_page.add(switcher_pref_group); general_page.add(animation_pref_group); general_page.add(icon_pref_group); general_page.add(windows_pref_group); general_page.add(window_size_pref_group); general_page.add(background_pref_group); general_page.add(background_application_switcher_pref_group); general_page.add(pcorrection_pref_group); general_page.add(keybinding_pref_group); let contribution_page = new Adw.PreferencesPage({ title: _("Contribute"), icon_name: 'contribute-symbolic', }); let contribute_icon_pref_group = new Adw.PreferencesGroup(); let icon_box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, margin_top: 24, margin_bottom: 24, spacing: 18, }); let icon_image = new Gtk.Image({ icon_name: "coverflow-symbolic", pixel_size: 128, }); let label_box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 6, }); let label = new Gtk.Label({ label: "Coverflow Alt-Tab", wrap: true, }); let context = label.get_style_context(); context.add_class("title-1"); let another_label = new Gtk.Label({ label: this.getVersionString(), }); let links_pref_group = new Adw.PreferencesGroup(); let code_row = new Adw.ActionRow({ icon_name: "code-symbolic", title: _("Code (create pull requests, report issues, etc.)") }); let github_link = new Gtk.LinkButton({ label: "Github", uri: "https://github.com/dmo60/CoverflowAltTab", }); let donate_row = new Adw.ActionRow({ title: _("Donate"), icon_name: "support-symbolic", }) let donate_link = new Gtk.LinkButton({ label: "Liberapay", uri: "https://liberapay.com/dsheeler/donate", }); let donate_link_paypal = new Gtk.LinkButton({ label: "PayPal", uri: "https://paypal.me/DanielSheeler?country.x=US&locale.x=en_US", }); let donate_link_github = new Gtk.LinkButton({ label: "Github", uri: "https://github.com/sponsors/dsheeler", }); let translate_row = new Adw.ActionRow({ title: _("Translate"), icon_name: "translate-symbolic", }) let translate_link = new Gtk.LinkButton({ label: "Weblate", uri: "https://hosted.weblate.org/engage/coverflow-alt-tab/", }); code_row.add_suffix(github_link); code_row.set_activatable_widget(github_link); translate_row.add_suffix(translate_link); translate_row.set_activatable_widget(translate_link); donate_row.add_suffix(donate_link); donate_row.add_suffix(donate_link_paypal); donate_row.add_suffix(donate_link_github); links_pref_group.add(code_row); links_pref_group.add(translate_row); links_pref_group.add(donate_row); label_box.append(label); label_box.append(another_label); icon_box.append(icon_image); icon_box.append(label_box); contribute_icon_pref_group.add(icon_box); contribution_page.add(contribute_icon_pref_group); contribution_page.add(links_pref_group) window.add(general_page); // window.add(appearance_page); window.add(contribution_page); window.set_search_enabled(true); } } function buildSwitcherAdw(settings, key, dependant_widgets, inverse_dependant_widgets, title, subtitle=null) { let pref = new Adw.ActionRow({ title: title, }); if (subtitle != null) { pref.set_subtitle(subtitle); } let switcher = new Gtk.Switch({ valign: Gtk.Align.CENTER, active: settings.get_boolean(key) }); switcher.expand = false; switcher.connect('notify::active', function(widget) { settings.set_boolean(key, widget.active); }); pref.set_activatable_widget(switcher); pref.add_suffix(switcher); switcher.connect('notify::active', function(widget) { for (let dep of dependant_widgets) { dep.set_sensitive(widget.get_active()); } }); for (let widget of dependant_widgets) { widget.set_sensitive(switcher.get_active()); } switcher.connect('notify::active', function(widget) { for (let inv_dep of inverse_dependant_widgets) { inv_dep.set_sensitive(!widget.get_active()); } }); for (let widget of inverse_dependant_widgets) { widget.set_sensitive(!switcher.get_active()); } let reset_button = makeResetButton(); reset_button.connect("clicked", function(widget) { settings.reset(key); switcher.set_active(settings.get_boolean(key)); }) pref.add_suffix(reset_button); return pref; } function buildRangeAdw(settings, key, values, title, subtitle="", draw_value=false) { let [min, max, step, defvs] = values; let pref = new Adw.ActionRow({ title: title, }); if (subtitle !== null && subtitle !== "") { pref.set_subtitle(subtitle); } let range = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, min, max, step); range.set_value(settings.get_double(key)); if (draw_value) { range.set_draw_value(true); range.set_value_pos(Gtk.PositionType.RIGHT) } for (let defv of defvs) { range.add_mark(defv, Gtk.PositionType.BOTTOM, null); } range.set_size_request(200, -1); range.connect('value-changed', function(slider) { settings.set_double(key, slider.get_value()); }); pref.set_activatable_widget(range); pref.add_suffix(range) let reset_button = makeResetButton(); reset_button.connect("clicked", function(widget) { settings.reset(key); range.set_value(settings.get_double(key)); }); pref.add_suffix(reset_button); return pref; } function buildRadioAdw(settings, key, buttons, title, subtitle=null) { let pref = new Adw.ActionRow({ title: title, }); if (subtitle != null) { pref.set_subtitle(subtitle); } let hbox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, spacing: 10, valign: Gtk.Align.CENTER, }); let radio = new Gtk.ToggleButton(); let radio_for_button = {}; for (let button_name of buttons.keys()) { radio = new Gtk.ToggleButton({group: radio, label: button_name}); radio_for_button[button_name] = radio; if (getBaseString(button_name) == settings.get_string(key)) { radio.set_active(true); for (let pref_row of buttons.get(button_name)[0]) { pref_row.set_sensitive(radio_for_button[button_name].get_active()); } for (let pref_row of buttons.get(button_name)[1]) { pref_row.set_sensitive(!radio_for_button[button_name].get_active()); } } radio.connect('toggled', function(widget) { if (widget.get_active()) { settings.set_string(key, getBaseString(widget.get_label())); } for (let pref_row of buttons.get(button_name)[0]) { pref_row.set_sensitive(widget.get_active()); } for (let pref_row of buttons.get(button_name)[1]) { pref_row.set_sensitive(!widget.get_active()); } }); hbox.append(radio); }; let reset_button = makeResetButton(); reset_button.connect("clicked", function(widget) { settings.reset(key); for (let button of buttons.keys()) { if (getBaseString(button) == settings.get_string(key)) { radio_for_button[button].set_active(true); } } }); pref.set_activatable_widget(hbox); pref.add_suffix(hbox); pref.add_suffix(reset_button); return pref; }; function buildSpinAdw(settings, key, values, title, subtitle=null) { let [min, max, step, page] = values; let pref = new Adw.ActionRow({ title: title, }); if (subtitle != null) { pref.set_subtitle(subtitle); } let spin = new Gtk.SpinButton({ valign: Gtk.Align.CENTER }); spin.set_range(min, max); spin.set_increments(step, page); spin.set_value(settings.get_int(key)); spin.connect('value-changed', function(widget) { settings.set_int(key, widget.get_value()); }); pref.set_activatable_widget(spin); pref.add_suffix(spin); let reset_button = makeResetButton(); reset_button.connect("clicked", function(widget) { settings.reset(key); spin.set_value(settings.get_int(key)); }); pref.add_suffix(reset_button); return pref; } function buildDropDownAdw(settings, key, values, title, subtitle=null) { let pref = new Adw.ActionRow({ title: title, }); if (subtitle != null) { pref.set_subtitle(subtitle); } let model = new Gtk.StringList(); let chosen_idx = 0; for (let i = 0; i < values.length; i++) { let item = values[i]; model.append(item.name); if (item.id == settings.get_string(key)) { chosen_idx = i; } } let chooser = new Gtk.DropDown({ valign: Gtk.Align.CENTER, model: model, selected: chosen_idx, }); chooser.connect('notify::selected-item', function(c) { let idx = c.get_selected(); settings.set_string(key, values[idx].id); }); pref.set_activatable_widget(chooser); pref.add_suffix(chooser); let reset_button = makeResetButton(); reset_button.connect("clicked", function(widget) { settings.reset(key); for (let i = 0; i < values.length; i++) { let item = values[i]; if (item.id == settings.get_string(key)) { chooser.set_selected(i); break; } } }); pref.add_suffix(reset_button); return pref; }