123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- /*
- * This file is part of the Forge extension for GNOME
- *
- * This program 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.
- *
- * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
- // Gnome imports
- import Gio from "gi://Gio";
- import GLib from "gi://GLib";
- import GObject from "gi://GObject";
- // Application imports
- import { stringify, parse } from "../css/index.js";
- import { production } from "./settings.js";
- export class ThemeManagerBase extends GObject.Object {
- static {
- GObject.registerClass(this);
- }
- constructor({ configMgr, settings }) {
- super();
- this.configMgr = configMgr;
- this.settings = settings;
- this._importCss();
- this.defaultPalette = this.getDefaultPalette();
- // A random number to denote an update on the css, usually the possible next version
- // in extensions.gnome.org
- // TODO: need to research the most effective way to bring in CSS updates
- // since the schema css-last-update might be triggered when there is a
- // code change on the schema unrelated to css updates.
- // For now tagging works. See @this.patchCss() and @this._needUpdate().
- this.cssTag = 37;
- // TODO: should the patchCss() call be done here?
- }
- /**
- * @param {string} value
- */
- addPx(value) {
- return `${value}px`;
- }
- /**
- * @param {string} value
- */
- removePx(value) {
- return value.replace("px", "");
- }
- getDefaultPalette() {
- return {
- tiled: this.getDefaults("tiled"),
- split: this.getDefaults("split"),
- floated: this.getDefaults("floated"),
- stacked: this.getDefaults("stacked"),
- tabbed: this.getDefaults("tabbed"),
- };
- }
- /**
- * The scheme name is in between the CSS selector name
- * E.g. window-tiled-color should return `tiled`
- * @param {string} selector
- */
- getColorSchemeBySelector(selector) {
- if (!selector.includes("-")) return null;
- let firstDash = selector.indexOf("-");
- let secondDash = selector.indexOf("-", firstDash + 1);
- const scheme = selector.substr(firstDash + 1, secondDash - firstDash - 1);
- return scheme;
- }
- /**
- * @param {string} color
- */
- getDefaults(color) {
- return {
- color: this.getCssProperty(`.${color}`, "color").value,
- "border-width": this.removePx(this.getCssProperty(`.${color}`, "border-width").value),
- opacity: this.getCssProperty(`.${color}`, "opacity").value,
- };
- }
- /**
- * @param {any} selector
- */
- getCssRule(selector) {
- if (this.cssAst) {
- const rules = this.cssAst.stylesheet.rules;
- // return only the first match, Forge CSS authors should make sure class names are unique :)
- const matchRules = rules.filter((r) => r.selectors.filter((s) => s === selector).length > 0);
- return matchRules.length > 0 ? matchRules[0] : {};
- }
- return {};
- }
- /**
- * @param {string} selector
- * @param {string} propertyName
- */
- getCssProperty(selector, propertyName) {
- const cssRule = this.getCssRule(selector);
- if (cssRule) {
- const matchDeclarations = cssRule.declarations.filter((d) => d.property === propertyName);
- return matchDeclarations.length > 0 ? matchDeclarations[0] : {};
- }
- return {};
- }
- /**
- * @param {string} selector
- * @param {string} propertyName
- * @param {string} propertyValue
- */
- setCssProperty(selector, propertyName, propertyValue) {
- const cssProperty = this.getCssProperty(selector, propertyName);
- if (cssProperty) {
- cssProperty.value = propertyValue;
- this._updateCss();
- return true;
- }
- return false;
- }
- /**
- * Returns the AST for stylesheet.css
- */
- _importCss() {
- let cssFile = this.configMgr.stylesheetFile;
- if (!cssFile || !production) {
- cssFile = this.configMgr.defaultStylesheetFile;
- }
- let [success, contents] = cssFile.load_contents(null);
- if (success) {
- const cssContents = new TextDecoder().decode(contents);
- this.cssAst = parse(cssContents);
- }
- }
- /**
- * Writes the AST back to stylesheet.css and reloads the theme
- */
- _updateCss() {
- if (!this.cssAst) {
- return;
- }
- let cssFile = this.configMgr.stylesheetFile;
- if (!cssFile || !production) {
- cssFile = this.configMgr.defaultStylesheetFile;
- }
- const cssContents = stringify(this.cssAst);
- const PERMISSIONS_MODE = 0o744;
- if (GLib.mkdir_with_parents(cssFile.get_parent().get_path(), PERMISSIONS_MODE) === 0) {
- let [success, _tag] = cssFile.replace_contents(
- cssContents,
- null,
- false,
- Gio.FileCreateFlags.REPLACE_DESTINATION,
- null
- );
- if (success) {
- this.reloadStylesheet();
- }
- }
- }
- /**
- * BREAKING: Patches the CSS by overriding the $HOME/.config stylesheet
- * at the moment.
- *
- * TODO: work needed to consolidate the existing config stylesheet and
- * when the extension default stylesheet gets an update.
- */
- patchCss() {
- if (this._needUpdate()) {
- let originalCss = this.configMgr.defaultStylesheetFile;
- let configCss = this.configMgr.stylesheetFile;
- let copyConfigCss = Gio.File.new_for_path(this.configMgr.stylesheetFileName + ".bak");
- let backupFine = configCss.copy(copyConfigCss, Gio.FileCopyFlags.OVERWRITE, null, null);
- let copyFine = originalCss.copy(configCss, Gio.FileCopyFlags.OVERWRITE, null, null);
- if (backupFine && copyFine) {
- this.settings.set_uint("css-last-update", this.cssTag);
- return true;
- }
- }
- return false;
- }
- /**
- * Credits: ExtensionSystem.js:_callExtensionEnable()
- */
- reloadStylesheet() {
- throw new Error("Must implement reloadStylesheet");
- }
- _needUpdate() {
- let cssTag = this.cssTag;
- return this.settings.get_uint("css-last-update") !== cssTag;
- }
- }
- /**
- * Credits: Color Space conversion functions from CSS Tricks
- * https://css-tricks.com/converting-color-spaces-in-javascript/
- */
- export function RGBAToHexA(rgba) {
- let sep = rgba.indexOf(",") > -1 ? "," : " ";
- rgba = rgba.substr(5).split(")")[0].split(sep);
- // Strip the slash if using space-separated syntax
- if (rgba.indexOf("/") > -1) rgba.splice(3, 1);
- for (let R in rgba) {
- let r = rgba[R];
- if (r.indexOf("%") > -1) {
- let p = r.substr(0, r.length - 1) / 100;
- if (R < 3) {
- rgba[R] = Math.round(p * 255);
- } else {
- rgba[R] = p;
- }
- }
- }
- let r = (+rgba[0]).toString(16),
- g = (+rgba[1]).toString(16),
- b = (+rgba[2]).toString(16),
- a = Math.round(+rgba[3] * 255).toString(16);
- if (r.length == 1) r = "0" + r;
- if (g.length == 1) g = "0" + g;
- if (b.length == 1) b = "0" + b;
- if (a.length == 1) a = "0" + a;
- return "#" + r + g + b + a;
- }
- export function hexAToRGBA(h) {
- let r = 0,
- g = 0,
- b = 0,
- a = 1;
- if (h.length == 5) {
- r = "0x" + h[1] + h[1];
- g = "0x" + h[2] + h[2];
- b = "0x" + h[3] + h[3];
- a = "0x" + h[4] + h[4];
- } else if (h.length == 9) {
- r = "0x" + h[1] + h[2];
- g = "0x" + h[3] + h[4];
- b = "0x" + h[5] + h[6];
- a = "0x" + h[7] + h[8];
- }
- a = +(a / 255).toFixed(3);
- return "rgba(" + +r + "," + +g + "," + +b + "," + a + ")";
- }
|