[gnome] Move to Grimble tiling manager

This commit is contained in:
2026-01-14 00:55:43 -05:00
parent de41369f5f
commit 941c727d38
90 changed files with 3999 additions and 9602 deletions

View File

@ -1,221 +0,0 @@
// Gnome imports
import Adw from "gi://Adw";
import GObject from "gi://GObject";
import Gdk from "gi://Gdk";
// Extension imports
import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
// Shared state
import { ConfigManager } from "../shared/settings.js";
import { PrefsThemeManager } from "./prefs-theme-manager.js";
// Prefs UI
import { ColorRow, PreferencesPage, ResetButton, SpinButtonRow, SwitchRow } from "./widgets.js";
import { Logger } from "../shared/logger.js";
export class AppearancePage extends PreferencesPage {
static {
GObject.registerClass(this);
}
/**
* @param {string} selector
*/
static getCssSelectorAsMessage(selector) {
switch (selector) {
// TODO: make separate color selection for preview hint
case ".window-tiled-border":
return _("Tiled window");
case ".window-tabbed-border":
return _("Tabbed window");
case ".window-stacked-border":
return _("Stacked window");
case ".window-floated-border":
return _("Floating window");
case ".window-split-border":
return _("Split direction hint");
}
}
constructor({ settings, dir }) {
super({ title: _("Appearance"), icon_name: "brush-symbolic" });
this.settings = settings;
let configMgr = new ConfigManager({ dir });
this.themeMgr = new PrefsThemeManager({ configMgr: configMgr, settings: settings });
this.add_group({
title: _("Gaps"),
description: _("Change the gap size between windows"),
children: [
new SpinButtonRow({
title: _("Gap size"),
range: [0, 32, 1],
settings,
bind: "window-gap-size",
}),
new SpinButtonRow({
title: _("Gap size multiplier"),
range: [0, 32, 1],
settings,
bind: "window-gap-size-increment",
}),
new SwitchRow({
title: _("Disable gaps for single window"),
subtitle: _("Disables window gaps when only a single window is present"),
settings,
bind: "window-gap-hidden-on-single",
}),
],
});
this.add_group({
title: _("Style"),
description: _("Change how the shell looks"),
children: [
new SwitchRow({
title: _("Preview hint"),
subtitle: _("Shows where the window will be tiled when you let go of it"),
experimental: true,
settings,
bind: "preview-hint-enabled",
}),
new SwitchRow({
title: _("Border around focused window"),
subtitle: _("Display a colored border around the focused window"),
settings,
bind: "focus-border-toggle",
}),
new SwitchRow({
title: _("Window split hint border"),
subtitle: _("Show split direction border on focused window"),
settings,
bind: "split-border-toggle",
}),
new SwitchRow({
title: _("Forge in quick settings"),
subtitle: _("Toggles the Forge tile in quick settings"),
experimental: true,
settings,
bind: "quick-settings-enabled",
}),
],
});
this.add_group({
title: _("Color"),
description: _("Changes the focused window's border and preview hint colors"),
children: [
"window-tiled-border",
"window-tabbed-border",
"window-stacked-border",
"window-floated-border",
"window-split-border",
].map((x) => this._createColorOptionWidget(x)),
});
}
/**
* @param {string} prefix
*/
_createColorOptionWidget(prefix) {
const selector = `.${prefix}`;
const theme = this.themeMgr;
const title = AppearancePage.getCssSelectorAsMessage(selector);
const colorScheme = theme.getColorSchemeBySelector(selector);
const row = new Adw.ExpanderRow({ title });
const borderSizeRow = new SpinButtonRow({
title: _("Border size"),
range: [1, 6, 1],
// subtitle: 'Properties of the focus hint',
max_width_chars: 1,
max_length: 1,
width_chars: 2,
xalign: 1,
init: theme.removePx(theme.getCssProperty(selector, "border-width").value),
onChange: (value) => {
const px = theme.addPx(value);
Logger.debug(`Setting border width for selector: ${selector} ${px}`);
theme.setCssProperty(selector, "border-width", px);
},
});
borderSizeRow.add_suffix(
new ResetButton({
onReset: () => {
const borderDefault = theme.defaultPalette[colorScheme]["border-width"];
theme.setCssProperty(selector, "border-width", theme.addPx(borderDefault));
borderSizeRow.activatable_widget.value = borderDefault;
},
})
);
const updateCssColors = (rgbaString) => {
const rgba = new Gdk.RGBA();
if (rgba.parse(rgbaString)) {
Logger.debug(`Setting color for selector: ${selector} ${rgbaString}`);
const previewBorderRgba = rgba.copy();
const previewBackgroundRgba = rgba.copy();
const overviewBackgroundRgba = rgba.copy();
previewBorderRgba.alpha = 0.3;
previewBackgroundRgba.alpha = 0.2;
overviewBackgroundRgba.alpha = 0.5;
// The primary color updates the focus hint:
theme.setCssProperty(selector, "border-color", rgba.to_string());
// Only apply below on the tabbed scheme
if (colorScheme === "tabbed") {
const tabBorderRgba = rgba.copy();
const tabActiveBackgroundRgba = rgba.copy();
tabBorderRgba.alpha = 0.6;
theme.setCssProperty(
`.window-${colorScheme}-tab`,
"border-color",
tabBorderRgba.to_string()
);
theme.setCssProperty(
`.window-${colorScheme}-tab-active`,
"background-color",
tabActiveBackgroundRgba.to_string()
);
}
// And then finally the preview when doing drag/drop tiling:
theme.setCssProperty(
`.window-tilepreview-${colorScheme}`,
"border-color",
previewBorderRgba.to_string()
);
theme.setCssProperty(
`.window-tilepreview-${colorScheme}`,
"background-color",
previewBackgroundRgba.to_string()
);
}
};
const borderColorRow = new ColorRow({
title: _("Border color"),
init: theme.getCssProperty(selector, "border-color").value,
onChange: updateCssColors,
});
borderColorRow.add_suffix(
new ResetButton({
onReset: () => {
const selectorColor = theme.defaultPalette[colorScheme].color;
updateCssColors(selectorColor);
const rgba = new Gdk.RGBA();
if (rgba.parse(selectorColor)) {
borderColorRow.colorButton.set_rgba(rgba);
}
},
})
);
row.add_row(borderColorRow);
row.add_row(borderSizeRow);
return row;
}
}

View File

@ -1,82 +0,0 @@
// Gtk imports
import Gtk from "gi://Gtk";
import GObject from "gi://GObject";
// Gnome imports
import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
// Extension imports
import { PreferencesPage, RemoveItemRow, ResetButton } from "./widgets.js";
import { ConfigManager } from "../shared/settings.js";
export class FloatingPage extends PreferencesPage {
static {
GObject.registerClass(this);
}
constructor({ settings, dir }) {
super({ title: _("Windows"), icon_name: "window-symbolic" });
this.settings = settings;
this.configMgr = new ConfigManager({ dir });
let overrides = this.configMgr.windowProps.overrides;
this.rows = this.loadItemsFromConfig(overrides);
this.floatingWindowGroup = this.add_group({
title: _("Floating Windows"),
description: _("Windows that will not be tiled"),
header_suffix: new ResetButton({ onReset: () => this.onResetHandler() }),
children: this.rows,
});
}
loadItemsFromConfig(overrides) {
let children = [];
for (let override of overrides) {
if (override.mode === "float") {
let itemrow = new RemoveItemRow({
title: override.wmTitle ?? override.wmClass,
subtitle: override.wmClass,
onRemove: (item, parent) => this.onRemoveHandler(item, parent),
});
children.push(itemrow);
}
}
return children;
}
onRemoveHandler(item, parent) {
this.floatingWindowGroup.remove(parent);
this.rows = this.rows.filter((row) => row != parent);
const existing = this.configMgr.windowProps.overrides;
const modified = existing.filter((row) => item != row.wmClass);
this.saveOverrides(modified);
}
saveOverrides(modified) {
if (modified) {
this.configMgr.windowProps = {
overrides: modified,
};
// Signal the main extension to reload floating overrides
const changed = Math.floor(Date.now() / 1000);
this.settings.set_uint("window-overrides-reload-trigger", changed);
}
}
onResetHandler() {
const defaultWindowProps = this.configMgr.loadDefaultWindowConfigContents();
const original = defaultWindowProps.overrides;
this.saveOverrides(original);
for (const child of this.rows) {
this.floatingWindowGroup.remove(child);
}
this.rows = this.loadItemsFromConfig(original);
for (const item of this.rows) {
this.floatingWindowGroup.add(item);
}
}
}

View File

@ -1,103 +0,0 @@
// Gnome imports
import Adw from "gi://Adw";
import GObject from "gi://GObject";
import Gtk from "gi://Gtk";
// Extension Imports
import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
// Prefs UI
import { EntryRow, PreferencesPage, RadioRow } from "./widgets.js";
import { Logger } from "../shared/logger.js";
export class KeyboardPage extends PreferencesPage {
static {
GObject.registerClass(this);
}
constructor({ kbdSettings }) {
super({ title: _("Keyboard"), icon_name: "input-keyboard-symbolic" });
this.add_group({
title: _("Drag-and-drop modifier key"),
description: _(
"Change the modifier key for tiling windows via drag-and-drop. Select 'None' to always tile"
),
children: [
new RadioRow({
title: _("Modifier key"),
settings: kbdSettings,
bind: "mod-mask-mouse-tile",
options: {
Super: _("Super"),
Ctrl: _("Ctrl"),
Alt: _("Alt"),
None: _("None"),
},
}),
],
});
this.add_group({
title: _("Shortcuts"),
description: _(
'Change the tiling shortcuts. To clear a shortcut clear the input field. To apply a shortcut press enter. <a href="https://github.com/forge-ext/forge/wiki/Keyboard-Shortcuts">Syntax examples</a>'
),
children: Object.entries({
window: "Tiling shortcuts",
con: "Container shortcuts",
workspace: "Workspace shortcuts",
focus: "Appearance shortcuts",
prefs: "Other shortcuts",
}).map(([prefix, gettextKey]) =>
KeyboardPage.makeKeygroupExpander(prefix, gettextKey, kbdSettings)
),
});
}
static makeKeygroupExpander(prefix, gettextKey, settings) {
const expander = new Adw.ExpanderRow({ title: _(gettextKey) });
KeyboardPage.createKeyList(settings, prefix).forEach((key) =>
expander.add_row(
new EntryRow({
title: key,
settings,
bind: key,
map: {
from(settings, bind) {
return settings.get_strv(bind).join(",");
},
to(settings, bind, value) {
if (!!value) {
const mappings = value.split(",").map((x) => {
const [, key, mods] = Gtk.accelerator_parse(x);
return Gtk.accelerator_valid(key, mods) && Gtk.accelerator_name(key, mods);
});
if (mappings.every((x) => !!x)) {
Logger.info("setting", bind, "to", mappings);
settings.set_strv(bind, mappings);
}
} else {
// If value deleted, unset the mapping
settings.set_strv(bind, []);
}
},
},
})
)
);
return expander;
}
static createKeyList(settings, categoryName) {
return settings
.list_keys()
.filter((keyName) => !!keyName && !!categoryName && keyName.startsWith(categoryName))
.sort((a, b) => {
const aUp = a.toUpperCase();
const bUp = b.toUpperCase();
if (aUp < bUp) return -1;
if (aUp > bUp) return 1;
return 0;
});
}
}

View File

@ -1,2 +0,0 @@
export const developers = Object.entries([
].reduce((acc, x) => ({ ...acc, [x.email]: acc[x.email] ?? x.name }), {})).map(([email, name]) => name + ' <' + email + '>')

View File

@ -1,13 +0,0 @@
import GObject from "gi://GObject";
import { ThemeManagerBase } from "../shared/theme.js";
export class PrefsThemeManager extends ThemeManagerBase {
static {
GObject.registerClass(this);
}
reloadStylesheet() {
this.settings.set_string("css-updated", Date.now().toString());
}
}

View File

@ -1,150 +0,0 @@
// Gnome imports
import Adw from "gi://Adw";
import Gtk from "gi://Gtk";
import GObject from "gi://GObject";
// Shared state
import { Logger } from "../shared/logger.js";
import { production } from "../shared/settings.js";
// Prefs UI
import { DropDownRow, SwitchRow, PreferencesPage, EntryRow } from "./widgets.js";
// Extension imports
import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
import { PACKAGE_VERSION } from "resource:///org/gnome/Shell/Extensions/js/misc/config.js";
import { developers } from "./metadata.js";
function showAboutWindow(parent, { version, description: comments }) {
version = version ?? "development";
const abt = new Adw.AboutWindow({
...(parent && { transient_for: parent }),
// TODO: fetch these from github at build time
application_name: _("Forge"),
application_icon: "forge-logo-symbolic",
version: `${PACKAGE_VERSION}-${version.toString()}`,
copyright: `© 2021-${new Date().getFullYear()} jmmaranan`,
issue_url: "https://github.com/forge-ext/forge/issues/new",
license_type: Gtk.License.GPL_3_0,
website: "https://github.com/forge-ext/forge",
developers,
comments,
designers: [],
translator_credits: _("translator-credits"),
});
abt.present();
}
function makeAboutButton(parent, metadata) {
const button = new Gtk.Button({
icon_name: "help-about-symbolic",
tooltip_text: _("About"),
valign: Gtk.Align.CENTER,
});
button.connect("clicked", () => showAboutWindow(parent, metadata));
return button;
}
export class SettingsPage extends PreferencesPage {
static {
GObject.registerClass(this);
}
constructor({ settings, window, metadata }) {
super({ title: _("Tiling"), icon_name: "view-grid-symbolic" });
this.add_group({
title: _("Behavior"),
description: _("Change how the tiling behaves"),
header_suffix: makeAboutButton(window, metadata),
children: [
new SwitchRow({
title: _("Focus on Hover"),
subtitle: _("Window focus follows the pointer"),
experimental: true,
settings,
bind: "focus-on-hover-enabled",
}),
new SwitchRow({
title: _("Move pointer with focused window"),
subtitle: _("Moves the pointer when focusing or swapping via keyboard"),
experimental: true,
settings,
bind: "move-pointer-focus-enabled",
}),
new SwitchRow({
title: _("Quarter tiling"),
subtitle: _("Places new windows in a clock-wise fashion"),
experimental: true,
settings,
bind: "auto-split-enabled",
}),
new SwitchRow({
title: _("Stacked tiling"),
subtitle: _("Stacks windows on top of each other while still tiling them"),
experimental: true,
settings,
bind: "stacked-tiling-mode-enabled",
}),
new SwitchRow({
title: _("Tabbed tiling"),
subtitle: _("Groups windows as tabs"),
experimental: true,
settings,
bind: "tabbed-tiling-mode-enabled",
}),
new SwitchRow({
title: _("Auto exit tabbed tiling"),
subtitle: _("Exit tabbed tiling mode when only a single tab remains"),
settings,
bind: "auto-exit-tabbed",
bind: "move-pointer-focus-enabled",
}),
new DropDownRow({
title: _("Drag-and-drop behavior"),
subtitle: _("What to do when dragging one window on top of another"),
settings,
type: "s",
bind: "dnd-center-layout",
items: [
{ id: "swap", name: _("Swap") },
{ id: "tabbed", name: _("Tabbed") },
{ id: "stacked", name: _("Stacked") },
],
}),
new SwitchRow({
title: _("Always on Top mode for floating windows"),
subtitle: _("Makes floating windows appear above tiled windows"),
experimental: true,
settings,
bind: "float-always-on-top-enabled",
}),
],
});
this.add_group({
title: _("Non-tiling workspaces"),
description: _("Disables tiling on specified workspaces. Starts from 0, separated by commas"),
children: [
new EntryRow({
title: _("Example: 0,1,2"),
settings,
bind: "workspace-skip-tile",
}),
],
});
if (!production) {
this.add_group({
title: _("Logger"),
children: [
new DropDownRow({
title: _("Log level"),
settings,
bind: "log-level",
items: Object.entries(Logger.LOG_LEVELS).map(([name, id]) => ({ id, name })),
type: "u",
}),
],
});
}
}
}

View File

@ -1,356 +0,0 @@
/** @license (c) aylur. GPL v3 */
import Adw from "gi://Adw";
import Gio from "gi://Gio";
import Gdk from "gi://Gdk";
import Gtk from "gi://Gtk";
import GObject from "gi://GObject";
// GNOME imports
import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
// Shared state
import { Logger } from "../shared/logger.js";
export class PreferencesPage extends Adw.PreferencesPage {
static {
GObject.registerClass(this);
}
add_group({ title, description = "", children, header_suffix = "" }) {
const group = new Adw.PreferencesGroup({ title, description });
for (const child of children) group.add(child);
if (header_suffix) group.set_header_suffix(header_suffix);
this.add(group);
return group;
}
}
export class SwitchRow extends Adw.ActionRow {
static {
GObject.registerClass(this);
}
constructor({ title, settings, bind, subtitle = "", experimental = false }) {
super({ title, subtitle });
const gswitch = new Gtk.Switch({
active: settings.get_boolean(bind),
valign: Gtk.Align.CENTER,
});
settings.bind(bind, gswitch, "active", Gio.SettingsBindFlags.DEFAULT);
if (experimental) {
const icon = new Gtk.Image({ icon_name: "bug-symbolic" });
icon.set_tooltip_markup(
_("<b>CAUTION</b>: Enabling this setting can lead to bugs or cause the shell to crash")
);
this.add_suffix(icon);
}
this.add_suffix(gswitch);
this.activatable_widget = gswitch;
}
}
export class ColorRow extends Adw.ActionRow {
static {
GObject.registerClass(this);
}
constructor({ title, init, onChange, subtitle = "" }) {
super({ title, subtitle });
let rgba = new Gdk.RGBA();
rgba.parse(init);
this.colorButton = new Gtk.ColorButton({ rgba, use_alpha: true, valign: Gtk.Align.CENTER });
this.colorButton.connect("color-set", () => {
onChange(this.colorButton.get_rgba().to_string());
});
this.add_suffix(this.colorButton);
this.activatable_widget = this.colorButton;
}
}
export class SpinButtonRow extends Adw.ActionRow {
static {
GObject.registerClass(this);
}
constructor({
title,
range: [low, high, step],
subtitle = "",
init = undefined,
onChange = undefined,
max_width_chars = undefined,
max_length = undefined,
width_chars = undefined,
xalign = undefined,
settings = undefined,
bind = undefined,
}) {
super({ title, subtitle });
const gspin = Gtk.SpinButton.new_with_range(low, high, step);
gspin.xalign = 1;
if (bind && settings) {
settings.bind(bind, gspin, "value", Gio.SettingsBindFlags.DEFAULT);
} else if (init) {
gspin.value = init;
gspin.connect("value-changed", (widget) => {
onChange?.(widget.value);
});
}
this.add_suffix(gspin);
this.set_css_classes(["spin"]);
this.activatable_widget = gspin;
}
}
export class DropDownRow extends Adw.ActionRow {
static {
GObject.registerClass(this);
}
/**
* @type {string}
* Name of the gsetting key to bind to
*/
bind;
/**
* @type {'b'|'y'|'n'|'q'|'i'|'u'|'x'|'t'|'h'|'d'|'s'|'o'|'g'|'?'|'a'|'m'}
* - b: the type string of G_VARIANT_TYPE_BOOLEAN; a boolean value.
* - y: the type string of G_VARIANT_TYPE_BYTE; a byte.
* - n: the type string of G_VARIANT_TYPE_INT16; a signed 16 bit integer.
* - q: the type string of G_VARIANT_TYPE_UINT16; an unsigned 16 bit integer.
* - i: the type string of G_VARIANT_TYPE_INT32; a signed 32 bit integer.
* - u: the type string of G_VARIANT_TYPE_UINT32; an unsigned 32 bit integer.
* - x: the type string of G_VARIANT_TYPE_INT64; a signed 64 bit integer.
* - t: the type string of G_VARIANT_TYPE_UINT64; an unsigned 64 bit integer.
* - h: the type string of G_VARIANT_TYPE_HANDLE; a signed 32 bit value that, by convention, is used as an index into an array of file descriptors that are sent alongside a D-Bus message.
* - d: the type string of G_VARIANT_TYPE_DOUBLE; a double precision floating point value.
* - s: the type string of G_VARIANT_TYPE_STRING; a string.
* - o: the type string of G_VARIANT_TYPE_OBJECT_PATH; a string in the form of a D-Bus object path.
* - g: the type string of G_VARIANT_TYPE_SIGNATURE; a string in the form of a D-Bus type signature.
* - ?: the type string of G_VARIANT_TYPE_BASIC; an indefinite type that is a supertype of any of the basic types.
* - v: the type string of G_VARIANT_TYPE_VARIANT; a container type that contain any other type of value.
* - a: used as a prefix on another type string to mean an array of that type; the type string “ai”, for example, is the type of an array of signed 32-bit integers.
* - m: used as a prefix on another type string to mean a “maybe”, or “nullable”, version of that type; the type string “ms”, for example, is the type of a value that maybe contains a string, or maybe contains nothing.
*/
type;
selected = 0;
/** @type {{name: string; id: string}[]} */
items;
model = new Gtk.StringList();
/** @type {Gtk.DropDown} */
dropdown;
constructor({ title, settings, bind, items, subtitle = "", type }) {
super({ title, subtitle });
this.settings = settings;
this.items = items;
this.bind = bind;
this.type = type ?? this.settings.get_value(bind)?.get_type() ?? "?";
this.#build();
this.add_suffix(this.dropdown);
this.add_suffix(new ResetButton({ settings, bind, onReset: () => this.reset() }));
}
reset() {
this.dropdown.selected = 0;
this.selected = 0;
}
#build() {
for (const { name, id } of this.items) {
this.model.append(name);
if (this.#get() === id) this.selected = this.items.findIndex((x) => x.id === id);
}
const { model, selected } = this;
this.dropdown = new Gtk.DropDown({ valign: Gtk.Align.CENTER, model, selected });
this.dropdown.connect("notify::selected", () => this.#onSelected());
this.activatable_widget = this.dropdown;
}
#onSelected() {
this.selected = this.dropdown.selected;
const { id } = this.items[this.selected];
Logger.debug("setting", id, this.selected);
this.#set(this.bind, id);
}
static #settingsTypes = {
b: "boolean",
y: "byte",
n: "int16",
q: "uint16",
i: "int32",
u: "uint",
x: "int64",
t: "uint64",
d: "double",
s: "string",
o: "objv",
};
/**
* @param {string} x
*/
#get(x = this.bind) {
const methodName = `get_${DropDownRow.#settingsTypes[this.type] ?? "value"}`;
return this.settings[methodName]?.(x);
}
/**
* @param {string} x
* @param {unknown} y
*/
#set(x, y) {
const methodName = `set_${DropDownRow.#settingsTypes[this.type] ?? "value"}`;
Logger.log(`${methodName}(${x}, ${y})`);
return this.settings[methodName]?.(x, y);
}
}
export class ClearButton extends Gtk.Button {
static {
GObject.registerClass(this);
}
constructor({ settings = undefined, bind = undefined, onClear }) {
super({
icon_name: "edit-clear-symbolic",
tooltip_text: _("Clear shortcut"),
css_classes: ["flat", "circular"],
valign: Gtk.Align.CENTER,
});
this.connect("clicked", () => {
onClear?.();
});
}
}
export class ResetButton extends Gtk.Button {
static {
GObject.registerClass(this);
}
constructor({ settings = undefined, bind = undefined, onReset }) {
super({
icon_name: "edit-undo-symbolic",
tooltip_text: _("Reset to default"),
css_classes: ["flat", "circular"],
valign: Gtk.Align.CENTER,
});
this.connect("clicked", () => {
settings?.reset(bind);
onReset?.();
});
}
}
export class RemoveButton extends Gtk.Button {
static {
GObject.registerClass(this);
}
constructor({ item, parent, onRemove }) {
super({
icon_name: "edit-delete-symbolic",
tooltip_text: _("Remove Item"),
css_classes: ["flat", "circular"],
valign: Gtk.Align.CENTER,
});
this.connect("clicked", () => {
onRemove?.(item, parent);
});
}
}
export class EntryRow extends Adw.EntryRow {
static {
GObject.registerClass(this);
}
constructor({ title, settings, bind, map }) {
super({ title });
this.connect("changed", () => {
const text = this.get_text();
if (typeof text === "string")
if (map) {
map.to(settings, bind, text);
} else {
settings.set_string(bind, text);
}
});
const current = map ? map.from(settings, bind) : settings.get_string(bind);
this.set_text(current ?? "");
this.add_suffix(
new ClearButton({
settings,
bind,
onClear: () => {
this.set_text("");
},
})
);
this.add_suffix(
new ResetButton({
settings,
bind,
onReset: () => {
this.set_text((map ? map.from(settings, bind) : settings.get_string(bind)) ?? "");
},
})
);
}
}
export class RadioRow extends Adw.ActionRow {
static {
GObject.registerClass(this);
}
static orientation = Gtk.Orientation.HORIZONTAL;
static spacing = 3;
static valign = Gtk.Align.CENTER;
constructor({ title, subtitle = "", settings, bind, options }) {
super({ title, subtitle });
const current = settings.get_string(bind);
const labels = Object.fromEntries(Object.entries(options).map(([k, v]) => [v, k]));
const { orientation, spacing, valign } = RadioRow;
const hbox = new Gtk.Box({ orientation, spacing, valign });
let group;
for (const [key, label] of Object.entries(options)) {
const toggle = new Gtk.ToggleButton({ label, ...(group && { group }) });
group ||= toggle;
toggle.active = key === current;
toggle.set_css_classes(["flat"]);
toggle.connect("clicked", () => {
if (toggle.active) {
settings.set_string(bind, labels[toggle.label]);
}
});
hbox.append(toggle);
}
this.add_suffix(hbox);
}
}
export class RemoveItemRow extends Adw.ActionRow {
static {
GObject.registerClass(this);
}
constructor({ title, subtitle = "", onRemove = undefined }) {
super({ title, subtitle });
const rmbutton = new RemoveButton({
item: subtitle,
parent: this,
onRemove: onRemove,
});
this.add_suffix(rmbutton);
}
}