diff --git a/package-lock.json b/package-lock.json index b6d519b..49889aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,14 @@ "@girs/adw-1": "^4.1.0", "@girs/gdesktopenums-3.0": "^4.1.0", "@girs/gdk-4.0": "^4.1.0", - "@girs/gdkpixbuf-2.0": "^4.1.0", "@girs/geoclue-2.0": "^4.1.0", "@girs/gio-2.0": "^4.1.0", "@girs/glib-2.0": "^4.1.0", + "@girs/gly-2": "^4.1.0", + "@girs/glygtk4-2": "^4.1.0", "@girs/gobject-2.0": "^4.1.0", + "@girs/graphene-1.0": "^4.1.0", + "@girs/gsk-4.0": "^4.1.0", "@girs/gtk-4.0": "^4.1.0", "@girs/meta-18": "^4.1.0", "@girs/shell-18": "^4.1.0", @@ -444,6 +447,42 @@ "@girs/gobject-2.0": "^4.1.0" } }, + "node_modules/@girs/gly-2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@girs/gly-2/-/gly-2-4.1.0.tgz", + "integrity": "sha512-blF/PuZpQp5NhYYkIJOu9Xq+vEX3vxq2bzbjrNS3XodSh8gjX6wxX8Gxp9808YK7b2/6+MYVt3fVeZQ34ybHuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@girs/gio-2.0": "^4.1.0", + "@girs/gjs": "^4.1.0", + "@girs/glib-2.0": "^4.1.0", + "@girs/gmodule-2.0": "^4.1.0", + "@girs/gobject-2.0": "^4.1.0" + } + }, + "node_modules/@girs/glygtk4-2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@girs/glygtk4-2/-/glygtk4-2-4.1.0.tgz", + "integrity": "sha512-gD1kjxo1cnkq/rXKDVOY1gJfaxvgqtgl2jn5q56asN+9yk0Cp3jZyDPlPcqrC1Gdv8deD8j81P7nybo4jdJZwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@girs/cairo-1.0": "^4.1.0", + "@girs/freetype2-2.0": "^4.1.0", + "@girs/gdk-4.0": "^4.1.0", + "@girs/gdkpixbuf-2.0": "^4.1.0", + "@girs/gio-2.0": "^4.1.0", + "@girs/gjs": "^4.1.0", + "@girs/glib-2.0": "^4.1.0", + "@girs/gly-2": "^4.1.0", + "@girs/gmodule-2.0": "^4.1.0", + "@girs/gobject-2.0": "^4.1.0", + "@girs/harfbuzz-0.0": "^4.1.0", + "@girs/pango-1.0": "^4.1.0", + "@girs/pangocairo-1.0": "^4.1.0" + } + }, "node_modules/@girs/gmodule-2.0": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@girs/gmodule-2.0/-/gmodule-2.0-4.1.0.tgz", @@ -2116,6 +2155,40 @@ "@girs/gobject-2.0": "^4.1.0" } }, + "@girs/gly-2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@girs/gly-2/-/gly-2-4.1.0.tgz", + "integrity": "sha512-blF/PuZpQp5NhYYkIJOu9Xq+vEX3vxq2bzbjrNS3XodSh8gjX6wxX8Gxp9808YK7b2/6+MYVt3fVeZQ34ybHuw==", + "dev": true, + "requires": { + "@girs/gio-2.0": "^4.1.0", + "@girs/gjs": "^4.1.0", + "@girs/glib-2.0": "^4.1.0", + "@girs/gmodule-2.0": "^4.1.0", + "@girs/gobject-2.0": "^4.1.0" + } + }, + "@girs/glygtk4-2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@girs/glygtk4-2/-/glygtk4-2-4.1.0.tgz", + "integrity": "sha512-gD1kjxo1cnkq/rXKDVOY1gJfaxvgqtgl2jn5q56asN+9yk0Cp3jZyDPlPcqrC1Gdv8deD8j81P7nybo4jdJZwg==", + "dev": true, + "requires": { + "@girs/cairo-1.0": "^4.1.0", + "@girs/freetype2-2.0": "^4.1.0", + "@girs/gdk-4.0": "^4.1.0", + "@girs/gdkpixbuf-2.0": "^4.1.0", + "@girs/gio-2.0": "^4.1.0", + "@girs/gjs": "^4.1.0", + "@girs/glib-2.0": "^4.1.0", + "@girs/gly-2": "^4.1.0", + "@girs/gmodule-2.0": "^4.1.0", + "@girs/gobject-2.0": "^4.1.0", + "@girs/harfbuzz-0.0": "^4.1.0", + "@girs/pango-1.0": "^4.1.0", + "@girs/pangocairo-1.0": "^4.1.0" + } + }, "@girs/gmodule-2.0": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@girs/gmodule-2.0/-/gmodule-2.0-4.1.0.tgz", diff --git a/package.json b/package.json index 2b16a04..515cd93 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,14 @@ "@girs/adw-1": "^4.1.0", "@girs/gdesktopenums-3.0": "^4.1.0", "@girs/gdk-4.0": "^4.1.0", - "@girs/gdkpixbuf-2.0": "^4.1.0", "@girs/geoclue-2.0": "^4.1.0", "@girs/gio-2.0": "^4.1.0", "@girs/glib-2.0": "^4.1.0", + "@girs/gly-2": "^4.1.0", + "@girs/glygtk4-2": "^4.1.0", "@girs/gobject-2.0": "^4.1.0", + "@girs/graphene-1.0": "^4.1.0", + "@girs/gsk-4.0": "^4.1.0", "@girs/gtk-4.0": "^4.1.0", "@girs/meta-18": "^4.1.0", "@girs/shell-18": "^4.1.0", diff --git a/src/data/assets/preview-window-dark.svg b/src/data/assets/preview-window-dark.svg new file mode 100644 index 0000000..d3ae90a --- /dev/null +++ b/src/data/assets/preview-window-dark.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/src/data/assets/preview-window-light.svg b/src/data/assets/preview-window-light.svg new file mode 100644 index 0000000..65aa6a3 --- /dev/null +++ b/src/data/assets/preview-window-light.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/src/data/org.gnome.shell.extensions.nightthemeswitcher.gschema.xml b/src/data/org.gnome.shell.extensions.nightthemeswitcher.gschema.xml index 5c9c9fe..e0ab318 100644 --- a/src/data/org.gnome.shell.extensions.nightthemeswitcher.gschema.xml +++ b/src/data/org.gnome.shell.extensions.nightthemeswitcher.gschema.xml @@ -4,6 +4,18 @@ SPDX-FileCopyrightText: Night Theme Switcher Contributors SPDX-License-Identifier: GPL-3.0-or-later --> + + + + + + + + + + + + @@ -59,4 +71,12 @@ SPDX-License-Identifier: GPL-3.0-or-later "prefer-dark" + + + "unset" + + + "unset" + + diff --git a/src/data/preferences.gresource.xml b/src/data/preferences.gresource.xml index d1c9743..bd86cf9 100644 --- a/src/data/preferences.gresource.xml +++ b/src/data/preferences.gresource.xml @@ -5,6 +5,8 @@ SPDX-License-Identifier: GPL-3.0-or-later --> + assets/preview-window-dark.svg + assets/preview-window-light.svg icons/code-symbolic.svg icons/translate-symbolic.svg icons/nightthemeswitcher-symbolic.svg @@ -12,6 +14,7 @@ SPDX-License-Identifier: GPL-3.0-or-later icons/commands-symbolic.svg icons/contribute-symbolic.svg icons/schedule-symbolic.svg + ui/AppearanceChooser.ui ui/AppearancePage.ui ui/BackgroundButton.ui ui/CommandsPage.ui @@ -19,5 +22,6 @@ SPDX-License-Identifier: GPL-3.0-or-later ui/SchedulePage.ui ui/ShortcutButton.ui ui/TimeChooser.ui + style.css diff --git a/src/data/style.css b/src/data/style.css new file mode 100644 index 0000000..dfd9480 --- /dev/null +++ b/src/data/style.css @@ -0,0 +1,59 @@ +/* +SPDX-FileCopyrightText: Night Theme Switcher Contributors +SPDX-License-Identifier: GPL-3.0-or-later +*/ + +color-chooser button { + min-width: 22px; + min-height: 22px; + margin: 3px; + padding: 0; + border-radius: 50%; + background-color: var(--accent-color); + outline: 3px solid transparent; + outline-offset: 3px; +} + +color-chooser button:focus { + outline-color: alpha(var(--accent-color), 0.5); +} + +color-chooser button:checked { + outline-color: var(--accent-color); +} + +color-chooser button.blue { + --accent-color: var(--accent-blue); +} + +color-chooser button.green { + --accent-color: var(--accent-green); +} + +color-chooser button.orange { + --accent-color: var(--accent-orange); +} + +color-chooser button.red { + --accent-color: var(--accent-red); +} + +color-chooser button.pink { + --accent-color: var(--accent-pink); +} + +color-chooser button.purple { + --accent-color: var(--accent-purple); +} + +color-chooser button.slate { + --accent-color: var(--accent-slate); +} + +color-chooser button.teal { + --accent-color: var(--accent-teal); +} + +color-chooser button.yellow { + --accent-color: var(--accent-yellow); +} diff --git a/src/data/ui/AppearanceChooser.ui b/src/data/ui/AppearanceChooser.ui new file mode 100644 index 0000000..e481811 --- /dev/null +++ b/src/data/ui/AppearanceChooser.ui @@ -0,0 +1,91 @@ + + + + + diff --git a/src/data/ui/AppearancePage.ui b/src/data/ui/AppearancePage.ui index 07806bc..ac5156f 100644 --- a/src/data/ui/AppearancePage.ui +++ b/src/data/ui/AppearancePage.ui @@ -10,29 +10,44 @@ SPDX-License-Identifier: GPL-3.0-or-later nightthemeswitcher-appearance-symbolic - Background switching is handled by the Shell, these settings allow you to change the images it uses. - - Day background - day_button + + vertical + 24 - - 12 - 12 - center + + stack - - - - - Night background - night_button - - 12 - 12 - center + + True + + + day + Day + + + + + + + + night + Night + + + + + + + + + + + Settings are applied when the extension is enabled. diff --git a/src/data/ui/BackgroundButton.ui b/src/data/ui/BackgroundButton.ui index adcfbd3..334e2f2 100644 --- a/src/data/ui/BackgroundButton.ui +++ b/src/data/ui/BackgroundButton.ui @@ -5,18 +5,22 @@ SPDX-License-Identifier: GPL-3.0-or-later --> True diff --git a/src/enums/AccentColor.js b/src/enums/AccentColor.js new file mode 100644 index 0000000..fb9d518 --- /dev/null +++ b/src/enums/AccentColor.js @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Night Theme Switcher Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +/** + * Accent colors. + * @readonly + * @enum {string} + */ +export const AccentColor = { + BLUE: "blue", + GREEN: "green", + ORANGE: "orange", + RED: "red", + PINK: "pink", + PURPLE: "purple", + SLATE: "slate", + TEAL: "teal", + YELLOW: "yellow", +}; diff --git a/src/enums/ColorScheme.js b/src/enums/ColorScheme.js index 54d0800..f5f0d9f 100644 --- a/src/enums/ColorScheme.js +++ b/src/enums/ColorScheme.js @@ -9,5 +9,5 @@ export const ColorScheme = { DEFAULT: "default", PREFER_DARK: "prefer-dark", - PREFER_LIGHT: "prefer-night", + PREFER_LIGHT: "prefer-light", }; diff --git a/src/meson.build b/src/meson.build index 315d93f..c3d8aae 100644 --- a/src/meson.build +++ b/src/meson.build @@ -19,6 +19,7 @@ main = [ 'prefs.js', ] enums = [ + 'enums/AccentColor.js', 'enums/ColorScheme.js', 'enums/Time.js', ] @@ -33,8 +34,11 @@ modules = [ 'modules/Timer.js', ] preferences = [ + 'preferences/AppearanceChooser.js', 'preferences/AppearancePage.js', + 'preferences/AppearancePreview.js', 'preferences/BackgroundButton.js', + 'preferences/ColorChooser.js', 'preferences/CommandsPage.js', 'preferences/ContributePage.js', 'preferences/SchedulePage.js', diff --git a/src/preferences/AppearanceChooser.js b/src/preferences/AppearanceChooser.js new file mode 100644 index 0000000..95846e8 --- /dev/null +++ b/src/preferences/AppearanceChooser.js @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Night Theme Switcher Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +import GObject from "gi://GObject"; +import Gtk from "gi://Gtk"; + +export class AppearanceChooser extends Gtk.Widget { + static { + GObject.registerClass( + { + GTypeName: "AppearanceChooser", + Template: "resource:///org/gnome/Shell/Extensions/nightthemeswitcher/preferences/ui/AppearanceChooser.ui", + Properties: { + "accent-color": GObject.ParamSpec.string( + "accent-color", + "Accent color", + "Accent color", + GObject.ParamFlags.READWRITE, + null, + ), + "background-uri": GObject.ParamSpec.string( + "background-uri", + "Background URI", + "URI to the background file", + GObject.ParamFlags.READWRITE, + null, + ), + "color-scheme": GObject.ParamSpec.string( + "color-scheme", + "Color scheme", + "Color scheme", + GObject.ParamFlags.READWRITE, + null, + ), + }, + }, + this, + ); + } +} diff --git a/src/preferences/AppearancePage.js b/src/preferences/AppearancePage.js index 04ada2e..f682fb9 100644 --- a/src/preferences/AppearancePage.js +++ b/src/preferences/AppearancePage.js @@ -11,17 +11,39 @@ export class AppearancePage extends Adw.PreferencesPage { { GTypeName: "AppearancePage", Template: "resource:///org/gnome/Shell/Extensions/nightthemeswitcher/preferences/ui/AppearancePage.ui", - InternalChildren: ["day_button", "night_button"], + InternalChildren: ["day_appearance_chooser", "night_appearance_chooser"], }, this, ); } - constructor({ ...params } = {}) { + constructor({ accentColorSettings, colorSchemeSettings, ...params } = {}) { super(params); - const settings = new Gio.Settings({ schema: "org.gnome.desktop.background" }); + const backgroundSettings = new Gio.Settings({ schema: "org.gnome.desktop.background" }); + const interfaceSettings = new Gio.Settings({ schema: "org.gnome.desktop.interface" }); - settings.bind("picture-uri", this._day_button, "uri", Gio.SettingsBindFlags.DEFAULT); - settings.bind("picture-uri-dark", this._night_button, "uri", Gio.SettingsBindFlags.DEFAULT); + if (accentColorSettings.get_string("day") === "unset") + accentColorSettings.set_string("day", interfaceSettings.get_string("accent-color")); + if (accentColorSettings.get_string("night") === "unset") + accentColorSettings.set_string("night", interfaceSettings.get_string("accent-color")); + + accentColorSettings.bind("day", this._day_appearance_chooser, "accent-color", Gio.SettingsBindFlags.DEFAULT); + accentColorSettings.bind("night", this._night_appearance_chooser, "accent-color", Gio.SettingsBindFlags.DEFAULT); + + colorSchemeSettings.bind("day", this._day_appearance_chooser, "color-scheme", Gio.SettingsBindFlags.DEFAULT); + colorSchemeSettings.bind("night", this._night_appearance_chooser, "color-scheme", Gio.SettingsBindFlags.DEFAULT); + + backgroundSettings.bind( + "picture-uri", + this._day_appearance_chooser, + "background-uri", + Gio.SettingsBindFlags.DEFAULT, + ); + backgroundSettings.bind( + "picture-uri-dark", + this._night_appearance_chooser, + "background-uri", + Gio.SettingsBindFlags.DEFAULT, + ); } } diff --git a/src/preferences/AppearancePreview.js b/src/preferences/AppearancePreview.js new file mode 100644 index 0000000..b07df58 --- /dev/null +++ b/src/preferences/AppearancePreview.js @@ -0,0 +1,333 @@ +// SPDX-FileCopyrightText: Night Theme Switcher Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +import Gdk from "gi://Gdk"; +import Gio from "gi://Gio"; +import Gly from "gi://Gly"; +import GlyGtk4 from "gi://GlyGtk4"; +import GObject from "gi://GObject"; +import Graphene from "gi://Graphene"; +import Gsk from "gi://Gsk"; +import Gtk from "gi://Gtk"; +import { AccentColor } from "../enums/AccentColor.js"; +import { ColorScheme } from "../enums/ColorScheme.js"; + +Gio._promisify(Gly.Loader.prototype, "load_async", "load_finish"); + +const PREVIEW_HEIGHT = 200; + +const PANEL_HEIGHT = 15; + +const WINDOW_WIDTH = 160; +const WINDOW_HEIGHT = 120; + +const BUTTON_WIDTH = 60; +const BUTTON_HEIGHT = 20; + +export class AppearancePreview extends Gtk.Widget { + /** @type {?Gdk.Texture} */ + #backgroundTexture = null; + /** @type {Gdk.Texture} */ + #windowDarkTexture; + /** @type {Gdk.Texture} */ + #windowLightTexture; + + /** @type {?AccentColor} */ + #accentColor = null; + /** @type {?string} */ + #backgroundUri = null; + /** @type {?ColorScheme} */ + #colorScheme = null; + + static { + GObject.registerClass( + { + GTypeName: "AppearancePreview", + CssName: "appearance-preview", + Properties: { + "accent-color": GObject.ParamSpec.string( + "accent-color", + "Accent color", + "Accent color", + GObject.ParamFlags.READWRITE, + null, + ), + "background-uri": GObject.ParamSpec.string( + "background-uri", + "Background URI", + "URI to the background file", + GObject.ParamFlags.READWRITE, + null, + ), + "color-scheme": GObject.ParamSpec.string( + "color-scheme", + "Color scheme", + "Color scheme", + GObject.ParamFlags.READWRITE, + null, + ), + }, + }, + this, + ); + } + + constructor({ ...params } = {}) { + super(params); + + this.#windowDarkTexture = Gdk.Texture.new_from_resource( + "/org/gnome/Shell/Extensions/nightthemeswitcher/preferences/assets/preview-window-dark.svg", + ); + this.#windowLightTexture = Gdk.Texture.new_from_resource( + "/org/gnome/Shell/Extensions/nightthemeswitcher/preferences/assets/preview-window-light.svg", + ); + } + + get accentColor() { + return this.#accentColor; + } + + set accentColor(color) { + if (!color || color === this.#accentColor || !Object.values(AccentColor).includes(color)) return; + this.#accentColor = color; + this.notify("accent-color"); + this.queue_draw(); + } + + get backgroundUri() { + return this.#backgroundUri; + } + + set backgroundUri(uri) { + if (!uri || uri === this.#backgroundUri) return; + this.#backgroundUri = uri; + this.notify("background-uri"); + this.#updateBackgroundTexture(); + } + + get colorScheme() { + return this.#colorScheme; + } + + set colorScheme(scheme) { + if (!scheme || scheme === this.#colorScheme || !Object.values(ColorScheme).includes(scheme)) return; + this.#colorScheme = scheme; + this.notify("color-scheme"); + this.queue_draw(); + } + + /** + * @param {Gtk.Orientation} _orientation The orientation to measure. + * @param {number} _for_size Size for the opposite of orientation. + * @returns {[number, number, number, number]} Minimum size, natural size, minimum baseline, natural baseline. + */ + vfunc_measure(_orientation, _for_size) { + return [PREVIEW_HEIGHT, PREVIEW_HEIGHT, -1, -1]; + } + + /** + * @param {Gtk.Snapshot} snapshot The snapshot to populate. + */ + vfunc_snapshot(snapshot) { + const previewWidth = this.get_width(); + const previewHeight = this.get_height(); + + snapshot.push_fill(buildGskRoundedRectanglePath(previewWidth, previewHeight, 12), Gsk.FillRule.WINDING); + + this.#snapshotBackground(snapshot, previewWidth, previewHeight); + this.#snapshotWindows(snapshot, previewWidth, previewHeight, this.colorScheme, this.accentColor); + this.#snapshotPanel(snapshot, previewWidth, this.colorScheme); + + snapshot.pop(); + } + + async #updateBackgroundTexture() { + try { + const loader = new Gly.Loader({ file: Gio.File.new_for_uri(this.backgroundUri) }); + const image = await loader.load_async(null); + this.#backgroundTexture = GlyGtk4.frame_get_texture(image.next_frame()); + } catch (e) { + console.error(e); + } + this.queue_draw(); + } + + /** + * @param {Gtk.Snapshot} snapshot The snapshot to populate. + * @param {number} width The width to populate. + * @param {number} height The height to populate. + */ + #snapshotBackground(snapshot, width, height) { + const ratio = width / height; + if (!this.#backgroundTexture) { + snapshot.append_color( + new Gdk.RGBA({ red: 0.08, green: 0.235, blue: 0.533, alpha: 1 }), + new Graphene.Rect({ origin: new Graphene.Point({ x: 0, y: 0 }), size: { width, height } }), + ); + return; + } + + let backgroundSnapshotWidth, backgroundSnapshotHeight; + const backgroundRatio = this.#backgroundTexture.get_intrinsic_aspect_ratio(); + + if (backgroundRatio > ratio) { + backgroundSnapshotWidth = height; + backgroundSnapshotHeight = height / backgroundRatio; + } else { + backgroundSnapshotWidth = width * backgroundRatio; + backgroundSnapshotHeight = width; + } + + const x = (width - backgroundSnapshotWidth) / 2; + const y = (height - backgroundSnapshotHeight) / 2; + + snapshot.save(); + snapshot.translate(new Graphene.Point({ x, y })); + this.#backgroundTexture.snapshot(snapshot, backgroundSnapshotWidth, backgroundSnapshotHeight); + snapshot.restore(); + } + + /** + * @param {Gtk.Snapshot} snapshot The snapshot to populate. + * @param {number} width The width to populate. + * @param {number} height The height to populate. + * @param {ColorScheme} colorScheme The color scheme to render. + * @param {AccentColor} accentColor The accent color to render. + */ + #snapshotWindows(snapshot, width, height, colorScheme, accentColor) { + const usableHeight = height - PANEL_HEIGHT; + + const backgroundWindowX = Math.ceil(width / 2 - WINDOW_WIDTH + WINDOW_WIDTH / 2 - width / 16); + const backgroundWindowY = Math.ceil( + usableHeight / 2 - WINDOW_HEIGHT + WINDOW_HEIGHT / 2 - usableHeight / 16 + PANEL_HEIGHT, + ); + const foregroundWindowX = Math.ceil(width / 2 - WINDOW_WIDTH / 2 + width / 16); + const foregroundWindowY = Math.ceil(usableHeight / 2 - WINDOW_HEIGHT / 2 + usableHeight / 16 + PANEL_HEIGHT); + + switch (colorScheme) { + case ColorScheme.DEFAULT: + this.#snapshotWindow(snapshot, backgroundWindowX, backgroundWindowY, ColorScheme.PREFER_DARK); + this.#snapshotWindow(snapshot, foregroundWindowX, foregroundWindowY, ColorScheme.PREFER_LIGHT); + break; + case ColorScheme.PREFER_DARK: + this.#snapshotWindow(snapshot, backgroundWindowX, backgroundWindowY, ColorScheme.PREFER_DARK); + this.#snapshotWindow(snapshot, foregroundWindowX, foregroundWindowY, ColorScheme.PREFER_DARK); + break; + case ColorScheme.PREFER_LIGHT: + this.#snapshotWindow(snapshot, backgroundWindowX, backgroundWindowY, ColorScheme.PREFER_LIGHT); + this.#snapshotWindow(snapshot, foregroundWindowX, foregroundWindowY, ColorScheme.PREFER_LIGHT); + break; + } + + const buttonX = foregroundWindowX + WINDOW_WIDTH - BUTTON_WIDTH - 20; + const buttonY = foregroundWindowY + WINDOW_HEIGHT - BUTTON_HEIGHT - 20; + + this.#snapshotButton(snapshot, buttonX, buttonY, accentColor); + } + + /** + * @param {Gtk.Snapshot} snapshot The snapshot to populate. + * @param {number} x The X position at which to populate. + * @param {number} y The Y position at which to populate. + * @param {ColorScheme} colorScheme The color scheme to render. + */ + #snapshotWindow(snapshot, x, y, colorScheme) { + const texture = colorScheme === ColorScheme.PREFER_DARK ? this.#windowDarkTexture : this.#windowLightTexture; + snapshot.save(); + snapshot.translate(new Graphene.Point({ x, y })); + texture.snapshot(snapshot, WINDOW_WIDTH, WINDOW_HEIGHT); + snapshot.restore(); + } + + /** + * @param {Gtk.Snapshot} snapshot The snapshot to populate. + * @param {number} x The X position at which to populate. + * @param {number} y The Y position at which to populate. + * @param {AccentColor} accentColor The accent color to render. + */ + #snapshotButton(snapshot, x, y, accentColor) { + let color; + switch (accentColor) { + case AccentColor.BLUE: + color = new Gdk.RGBA({ red: 0.208, green: 0.518, blue: 0.894, alpha: 1 }); + break; + case AccentColor.GREEN: + color = new Gdk.RGBA({ red: 0.227, green: 0.58, blue: 0.29, alpha: 1 }); + break; + case AccentColor.ORANGE: + color = new Gdk.RGBA({ red: 0.929, green: 0.357, blue: 0, alpha: 1 }); + break; + case AccentColor.PINK: + color = new Gdk.RGBA({ red: 0.835, green: 0.38, blue: 0.6, alpha: 1 }); + break; + case AccentColor.PURPLE: + color = new Gdk.RGBA({ red: 0.569, green: 0.255, blue: 0.675, alpha: 1 }); + break; + case AccentColor.RED: + color = new Gdk.RGBA({ red: 0.902, green: 0.176, blue: 0.259, alpha: 1 }); + break; + case AccentColor.SLATE: + color = new Gdk.RGBA({ red: 0.435, green: 0.514, blue: 0.588, alpha: 1 }); + break; + case AccentColor.TEAL: + color = new Gdk.RGBA({ red: 0.129, green: 0.565, blue: 0.643, alpha: 1 }); + break; + case AccentColor.YELLOW: + color = new Gdk.RGBA({ red: 0.784, green: 0.533, blue: 0, alpha: 1 }); + break; + } + if (!color) return; + snapshot.save(); + snapshot.translate(new Graphene.Point({ x, y })); + snapshot.append_fill(buildGskRoundedRectanglePath(BUTTON_WIDTH, BUTTON_HEIGHT, 3), Gsk.FillRule.WINDING, color); + snapshot.restore(); + } + + /** + * @param {Gtk.Snapshot} snapshot The snapshot to populate. + * @param {number} width The width to populate. + * @param {ColorScheme} colorScheme The color scheme to render. + */ + #snapshotPanel(snapshot, width, colorScheme) { + const backgroundColor = + colorScheme === ColorScheme.PREFER_LIGHT + ? new Gdk.RGBA({ red: 0.98, green: 0.98, blue: 0.984, alpha: 1 }) + : new Gdk.RGBA({ red: 0, green: 0, blue: 0, alpha: 1 }); + + snapshot.append_color( + backgroundColor, + new Graphene.Rect({ + origin: new Graphene.Point({ x: 0, y: 0 }), + size: new Graphene.Size({ width, height: PANEL_HEIGHT }), + }), + ); + } +} + +/** + * Build a rounded rectangle path until GJS supports `Gsk.RoundedRect`. + * @param {number} width The width of the rectangle. + * @param {number} height The height of the rectangle. + * @param {number} radius The radius of the corners. + * @returns {Gsk.Path} The path. + */ +function buildGskRoundedRectanglePath(width, height, radius) { + const builder = new Gsk.PathBuilder(); + builder.add_circle(new Graphene.Point({ x: radius, y: radius }), radius); + builder.add_circle(new Graphene.Point({ x: width - radius, y: radius }), radius); + builder.add_circle(new Graphene.Point({ x: radius, y: height - radius }), radius); + builder.add_circle(new Graphene.Point({ x: width - radius, y: height - radius }), radius); + builder.add_rect( + new Graphene.Rect({ + origin: new Graphene.Point({ x: radius, y: 0 }), + size: new Graphene.Size({ width: width - 2 * radius, height }), + }), + ); + builder.add_rect( + new Graphene.Rect({ + origin: new Graphene.Point({ x: 0, y: radius }), + size: new Graphene.Size({ width, height: height - 2 * radius }), + }), + ); + return builder.to_path(); +} diff --git a/src/preferences/BackgroundButton.js b/src/preferences/BackgroundButton.js index bed5fdd..6536550 100644 --- a/src/preferences/BackgroundButton.js +++ b/src/preferences/BackgroundButton.js @@ -3,9 +3,9 @@ import Adw from "gi://Adw"; import Gdk from "gi://Gdk"; -import GdkPixbuf from "gi://GdkPixbuf"; import Gio from "gi://Gio"; import GLib from "gi://GLib"; +import Gly from "gi://Gly"; import GObject from "gi://GObject"; import Gtk from "gi://Gtk"; @@ -14,34 +14,14 @@ import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensio Gio._promisify(Gtk.FileDialog.prototype, "open", "open_finish"); export class BackgroundButton extends Gtk.Button { - #uri; - static { GObject.registerClass( { GTypeName: "BackgroundButton", Template: "resource:///org/gnome/Shell/Extensions/nightthemeswitcher/preferences/ui/BackgroundButton.ui", - InternalChildren: ["file_dialog", "thumbnail"], + InternalChildren: ["file_dialog"], Properties: { uri: GObject.ParamSpec.string("uri", "URI", "URI to the background file", GObject.ParamFlags.READWRITE, null), - thumbWidth: GObject.ParamSpec.int( - "thumb-width", - "Thumbnail width", - "Width of the displayed thumbnail", - GObject.ParamFlags.READWRITE, - 0, - 600, - 180, - ), - thumbHeight: GObject.ParamSpec.int( - "thumb-height", - "Thumbnail height", - "Height of the displayed thumbnail", - GObject.ParamFlags.READWRITE, - 0, - 600, - 180, - ), }, }, this, @@ -50,33 +30,10 @@ export class BackgroundButton extends Gtk.Button { constructor({ ...params } = {}) { super(params); - this.#setupSize(); this.#setupDropTarget(); this.#setupFileDialog(); } - get uri() { - return this.#uri || null; - } - - set uri(uri) { - if (uri === this.#uri) return; - this.#uri = uri; - this.notify("uri"); - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - this.#updateThumbnail(); - return GLib.SOURCE_REMOVE; - }); - } - - #setupSize() { - const display = Gdk.Display.get_default(); - const monitor = display.get_monitors().get_item(0); - if (monitor.width_mm === 0 || monitor.height_mm === 0) return; - if (monitor.width_mm > monitor.height_mm) this.thumbHeight *= monitor.height_mm / monitor.width_mm; - else this.thumbWidth *= monitor.width_mm / monitor.height_mm; - } - #setupDropTarget() { const dropTarget = Gtk.DropTarget.new(Gio.File.$gtype, Gdk.DragAction.COPY); dropTarget.connect("drop", (_target, file, _x, _y) => { @@ -108,9 +65,7 @@ export class BackgroundButton extends Gtk.Button { } #getSupportedContentTypes() { - return GdkPixbuf.Pixbuf.get_formats() - .flatMap((format) => format.get_mime_types()) - .concat("application/xml"); + return Gly.Loader.get_mime_types().concat("application/xml"); } #isContentTypeSupported(contentType) { @@ -130,61 +85,13 @@ export class BackgroundButton extends Gtk.Button { async #setURIFromFileDialog() { try { + this._file_dialog.initial_folder = Gio.File.new_for_uri(this.uri).get_parent(); const file = await this._file_dialog.open(this.get_root(), null); this.uri = file.get_uri(); } catch {} } - #updateThumbnail() { - this._thumbnail.paintable = null; - - if (!this.uri) return; - - const file = Gio.File.new_for_uri(this.uri); - const contentType = Gio.content_type_guess(file.get_basename(), null)[0]; - - if (!this.#isContentTypeSupported(contentType)) return; - - let path; - if (Gio.content_type_equals(contentType, "application/xml")) { - const decoder = new TextDecoder("utf-8"); - const contents = decoder.decode(file.load_contents(null)[1]); - try { - path = contents.match(/(.+)<\/file>/m)[1]; - if (!this.#isContentTypeSupported(Gio.content_type_guess(path, null)[0])) throw new Error(); - } catch (e) { - console.error(`No suitable background file found in ${file.get_path()}.\n${e}`); - return; - } - } else { - path = file.get_path(); - } - - const pixbuf = GdkPixbuf.Pixbuf.new_from_file(path); - const scale = - pixbuf.width / pixbuf.height > this.thumbWidth / this.thumbHeight - ? this.thumbHeight / pixbuf.height - : this.thumbWidth / pixbuf.width; - const thumbPixbuf = GdkPixbuf.Pixbuf.new( - pixbuf.colorspace, - pixbuf.has_alpha, - pixbuf.bits_per_sample, - this.thumbWidth, - this.thumbHeight, - ); - pixbuf.scale( - thumbPixbuf, - 0, - 0, - this.thumbWidth, - this.thumbHeight, - -(pixbuf.width * scale - this.thumbWidth) / 2, - -(pixbuf.height * scale - this.thumbHeight) / 2, - scale, - scale, - GdkPixbuf.InterpType.TILES, - ); - - this._thumbnail.paintable = Gdk.Texture.new_for_pixbuf(thumbPixbuf); + _getBackgroundFileName(_widget, uri) { + return uri ? GLib.filename_display_basename(uri) : ""; } } diff --git a/src/preferences/ColorChooser.js b/src/preferences/ColorChooser.js new file mode 100644 index 0000000..c39d33e --- /dev/null +++ b/src/preferences/ColorChooser.js @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: Night Theme Switcher Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +import GObject from "gi://GObject"; +import Gtk from "gi://Gtk"; + +import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js"; + +import { AccentColor } from "../enums/AccentColor.js"; + +export class ColorChooser extends Gtk.Widget { + /** @type {Gtk.ToggleButton[]} */ + #buttons = []; + + /** @type {AccentColor} */ + #activeColor; + + static { + GObject.registerClass( + { + GTypeName: "ColorChooser", + CssName: "color-chooser", + Properties: { + color: GObject.ParamSpec.string( + "active-color", + "Active color", + "Active color", + GObject.ParamFlags.READWRITE, + null, + ), + }, + }, + this, + ); + } + + constructor({ ...params } = {}) { + super({ layoutManager: new Gtk.BoxLayout({ spacing: 6 }), ...params }); + + const colors = [ + { accentColor: AccentColor.BLUE, name: _("Blue") }, + { accentColor: AccentColor.TEAL, name: _("Teal") }, + { accentColor: AccentColor.GREEN, name: _("Green") }, + { accentColor: AccentColor.YELLOW, name: _("Yellow") }, + { accentColor: AccentColor.ORANGE, name: _("Orange") }, + { accentColor: AccentColor.RED, name: _("Red") }, + { accentColor: AccentColor.PINK, name: _("Pink") }, + { accentColor: AccentColor.PURPLE, name: _("Purple") }, + { accentColor: AccentColor.SLATE, name: _("Slate") }, + ]; + + let toggleGroup; + for (const color of colors) { + const button = new Gtk.ToggleButton({ + tooltipText: color.name, + cssClasses: ["circular", color.accentColor], + active: this.activeColor === color.accentColor, + }); + + if (!toggleGroup) toggleGroup = button; + else button.group = toggleGroup; + + button.connect("toggled", () => { + if (!button.active) return; + this.activeColor = color.accentColor; + }); + + this.#buttons.push(button); + + button.set_parent(this); + } + } + + get activeColor() { + return this.#activeColor; + } + + set activeColor(color) { + if (!color || color === this.#activeColor || !Object.values(AccentColor).includes(color)) return; + this.#activeColor = color; + this.notify("active-color"); + this.#updateButtons(); + } + + #updateButtons() { + this.#buttons.forEach((button) => { + button.active = button.cssClasses.includes(this.activeColor); + }); + } +} diff --git a/src/prefs.js b/src/prefs.js index 953bd2b..c0b62a0 100644 --- a/src/prefs.js +++ b/src/prefs.js @@ -21,17 +21,30 @@ export default class NightThemeSwitcherPreferences extends ExtensionPreferences const resource = Gio.Resource.load(GLib.build_filenamev([this.path, "resources", "preferences.gresource"])); Gio.resources_register(resource); + // Load stylesheet + const provider = new Gtk.CssProvider(); + provider.load_from_resource("/org/gnome/Shell/Extensions/nightthemeswitcher/preferences/style.css"); + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + // Load icons const iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()); iconTheme.add_resource_path("/org/gnome/Shell/Extensions/nightthemeswitcher/preferences/icons"); // Set window properties window.search_enabled = true; + window.set_size_request(420, 320); window.set_default_size(500, 630); // Dynamically import all classes - const { BackgroundButton } = await import("./preferences/BackgroundButton.js"); + const { AppearanceChooser } = await import("./preferences/AppearanceChooser.js"); const { AppearancePage } = await import("./preferences/AppearancePage.js"); + const { AppearancePreview } = await import("./preferences/AppearancePreview.js"); + const { BackgroundButton } = await import("./preferences/BackgroundButton.js"); + const { ColorChooser } = await import("./preferences/ColorChooser.js"); const { CommandsPage } = await import("./preferences/CommandsPage.js"); const { ContributePage } = await import("./preferences/ContributePage.js"); const { SchedulePage } = await import("./preferences/SchedulePage.js"); @@ -39,8 +52,11 @@ export default class NightThemeSwitcherPreferences extends ExtensionPreferences const { TimeChooser } = await import("./preferences/TimeChooser.js"); // Make sure all GObjects are registered - GObject.type_ensure(BackgroundButton); + GObject.type_ensure(AppearanceChooser); GObject.type_ensure(AppearancePage); + GObject.type_ensure(AppearancePreview); + GObject.type_ensure(BackgroundButton); + GObject.type_ensure(ColorChooser); GObject.type_ensure(CommandsPage); GObject.type_ensure(ContributePage); GObject.type_ensure(SchedulePage); @@ -50,7 +66,10 @@ export default class NightThemeSwitcherPreferences extends ExtensionPreferences // Add all pages [ new SchedulePage({ settings: this.getSettings(`${this.metadata["settings-schema"]}.time`) }), - new AppearancePage(), + new AppearancePage({ + accentColorSettings: this.getSettings(`${this.metadata["settings-schema"]}.accent-color`), + colorSchemeSettings: this.getSettings(`${this.metadata["settings-schema"]}.color-scheme`), + }), new CommandsPage({ settings: this.getSettings(`${this.metadata["settings-schema"]}.commands`) }), new ContributePage(), ].forEach((page) => window.add(page));