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
-->
- Change background
-
-
- False
-
-
+ Change background
+
+
+ document-open-symbolic
+ True
+
+
+
+
+
-
+
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));