theme.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. /*
  2. * This file is part of the Forge extension for GNOME
  3. *
  4. * This program is free software: you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation, either version 3 of the License, or
  7. * (at your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. * GNU General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. *
  17. */
  18. // Gnome imports
  19. import Gio from "gi://Gio";
  20. import GLib from "gi://GLib";
  21. import GObject from "gi://GObject";
  22. // Application imports
  23. import { stringify, parse } from "../css/index.js";
  24. import { production } from "./settings.js";
  25. export class ThemeManagerBase extends GObject.Object {
  26. static {
  27. GObject.registerClass(this);
  28. }
  29. constructor({ configMgr, settings }) {
  30. super();
  31. this.configMgr = configMgr;
  32. this.settings = settings;
  33. this._importCss();
  34. this.defaultPalette = this.getDefaultPalette();
  35. // A random number to denote an update on the css, usually the possible next version
  36. // in extensions.gnome.org
  37. // TODO: need to research the most effective way to bring in CSS updates
  38. // since the schema css-last-update might be triggered when there is a
  39. // code change on the schema unrelated to css updates.
  40. // For now tagging works. See @this.patchCss() and @this._needUpdate().
  41. this.cssTag = 37;
  42. // TODO: should the patchCss() call be done here?
  43. }
  44. /**
  45. * @param {string} value
  46. */
  47. addPx(value) {
  48. return `${value}px`;
  49. }
  50. /**
  51. * @param {string} value
  52. */
  53. removePx(value) {
  54. return value.replace("px", "");
  55. }
  56. getDefaultPalette() {
  57. return {
  58. tiled: this.getDefaults("tiled"),
  59. split: this.getDefaults("split"),
  60. floated: this.getDefaults("floated"),
  61. stacked: this.getDefaults("stacked"),
  62. tabbed: this.getDefaults("tabbed"),
  63. };
  64. }
  65. /**
  66. * The scheme name is in between the CSS selector name
  67. * E.g. window-tiled-color should return `tiled`
  68. * @param {string} selector
  69. */
  70. getColorSchemeBySelector(selector) {
  71. if (!selector.includes("-")) return null;
  72. let firstDash = selector.indexOf("-");
  73. let secondDash = selector.indexOf("-", firstDash + 1);
  74. const scheme = selector.substr(firstDash + 1, secondDash - firstDash - 1);
  75. return scheme;
  76. }
  77. /**
  78. * @param {string} color
  79. */
  80. getDefaults(color) {
  81. return {
  82. color: this.getCssProperty(`.${color}`, "color").value,
  83. "border-width": this.removePx(this.getCssProperty(`.${color}`, "border-width").value),
  84. opacity: this.getCssProperty(`.${color}`, "opacity").value,
  85. };
  86. }
  87. /**
  88. * @param {any} selector
  89. */
  90. getCssRule(selector) {
  91. if (this.cssAst) {
  92. const rules = this.cssAst.stylesheet.rules;
  93. // return only the first match, Forge CSS authors should make sure class names are unique :)
  94. const matchRules = rules.filter((r) => r.selectors.filter((s) => s === selector).length > 0);
  95. return matchRules.length > 0 ? matchRules[0] : {};
  96. }
  97. return {};
  98. }
  99. /**
  100. * @param {string} selector
  101. * @param {string} propertyName
  102. */
  103. getCssProperty(selector, propertyName) {
  104. const cssRule = this.getCssRule(selector);
  105. if (cssRule) {
  106. const matchDeclarations = cssRule.declarations.filter((d) => d.property === propertyName);
  107. return matchDeclarations.length > 0 ? matchDeclarations[0] : {};
  108. }
  109. return {};
  110. }
  111. /**
  112. * @param {string} selector
  113. * @param {string} propertyName
  114. * @param {string} propertyValue
  115. */
  116. setCssProperty(selector, propertyName, propertyValue) {
  117. const cssProperty = this.getCssProperty(selector, propertyName);
  118. if (cssProperty) {
  119. cssProperty.value = propertyValue;
  120. this._updateCss();
  121. return true;
  122. }
  123. return false;
  124. }
  125. /**
  126. * Returns the AST for stylesheet.css
  127. */
  128. _importCss() {
  129. let cssFile = this.configMgr.stylesheetFile;
  130. if (!cssFile || !production) {
  131. cssFile = this.configMgr.defaultStylesheetFile;
  132. }
  133. let [success, contents] = cssFile.load_contents(null);
  134. if (success) {
  135. const cssContents = new TextDecoder().decode(contents);
  136. this.cssAst = parse(cssContents);
  137. }
  138. }
  139. /**
  140. * Writes the AST back to stylesheet.css and reloads the theme
  141. */
  142. _updateCss() {
  143. if (!this.cssAst) {
  144. return;
  145. }
  146. let cssFile = this.configMgr.stylesheetFile;
  147. if (!cssFile || !production) {
  148. cssFile = this.configMgr.defaultStylesheetFile;
  149. }
  150. const cssContents = stringify(this.cssAst);
  151. const PERMISSIONS_MODE = 0o744;
  152. if (GLib.mkdir_with_parents(cssFile.get_parent().get_path(), PERMISSIONS_MODE) === 0) {
  153. let [success, _tag] = cssFile.replace_contents(
  154. cssContents,
  155. null,
  156. false,
  157. Gio.FileCreateFlags.REPLACE_DESTINATION,
  158. null
  159. );
  160. if (success) {
  161. this.reloadStylesheet();
  162. }
  163. }
  164. }
  165. /**
  166. * BREAKING: Patches the CSS by overriding the $HOME/.config stylesheet
  167. * at the moment.
  168. *
  169. * TODO: work needed to consolidate the existing config stylesheet and
  170. * when the extension default stylesheet gets an update.
  171. */
  172. patchCss() {
  173. if (this._needUpdate()) {
  174. let originalCss = this.configMgr.defaultStylesheetFile;
  175. let configCss = this.configMgr.stylesheetFile;
  176. let copyConfigCss = Gio.File.new_for_path(this.configMgr.stylesheetFileName + ".bak");
  177. let backupFine = configCss.copy(copyConfigCss, Gio.FileCopyFlags.OVERWRITE, null, null);
  178. let copyFine = originalCss.copy(configCss, Gio.FileCopyFlags.OVERWRITE, null, null);
  179. if (backupFine && copyFine) {
  180. this.settings.set_uint("css-last-update", this.cssTag);
  181. return true;
  182. }
  183. }
  184. return false;
  185. }
  186. /**
  187. * Credits: ExtensionSystem.js:_callExtensionEnable()
  188. */
  189. reloadStylesheet() {
  190. throw new Error("Must implement reloadStylesheet");
  191. }
  192. _needUpdate() {
  193. let cssTag = this.cssTag;
  194. return this.settings.get_uint("css-last-update") !== cssTag;
  195. }
  196. }
  197. /**
  198. * Credits: Color Space conversion functions from CSS Tricks
  199. * https://css-tricks.com/converting-color-spaces-in-javascript/
  200. */
  201. export function RGBAToHexA(rgba) {
  202. let sep = rgba.indexOf(",") > -1 ? "," : " ";
  203. rgba = rgba.substr(5).split(")")[0].split(sep);
  204. // Strip the slash if using space-separated syntax
  205. if (rgba.indexOf("/") > -1) rgba.splice(3, 1);
  206. for (let R in rgba) {
  207. let r = rgba[R];
  208. if (r.indexOf("%") > -1) {
  209. let p = r.substr(0, r.length - 1) / 100;
  210. if (R < 3) {
  211. rgba[R] = Math.round(p * 255);
  212. } else {
  213. rgba[R] = p;
  214. }
  215. }
  216. }
  217. let r = (+rgba[0]).toString(16),
  218. g = (+rgba[1]).toString(16),
  219. b = (+rgba[2]).toString(16),
  220. a = Math.round(+rgba[3] * 255).toString(16);
  221. if (r.length == 1) r = "0" + r;
  222. if (g.length == 1) g = "0" + g;
  223. if (b.length == 1) b = "0" + b;
  224. if (a.length == 1) a = "0" + a;
  225. return "#" + r + g + b + a;
  226. }
  227. export function hexAToRGBA(h) {
  228. let r = 0,
  229. g = 0,
  230. b = 0,
  231. a = 1;
  232. if (h.length == 5) {
  233. r = "0x" + h[1] + h[1];
  234. g = "0x" + h[2] + h[2];
  235. b = "0x" + h[3] + h[3];
  236. a = "0x" + h[4] + h[4];
  237. } else if (h.length == 9) {
  238. r = "0x" + h[1] + h[2];
  239. g = "0x" + h[3] + h[4];
  240. b = "0x" + h[5] + h[6];
  241. a = "0x" + h[7] + h[8];
  242. }
  243. a = +(a / 255).toFixed(3);
  244. return "rgba(" + +r + "," + +g + "," + +b + "," + a + ")";
  245. }