[gnome] Update extensions

This commit is contained in:
2024-02-06 01:31:09 -05:00
parent fb9f4f5824
commit 26aa549720
75 changed files with 13080 additions and 0 deletions

View File

@ -0,0 +1,9 @@
(The MIT License)
Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,100 @@
# CSS module in Forge
API to work with CSS code and files and update the extension's stylesheet.css
## CSS Parser from ReworkCSS
Credits: https://github.com/reworkcss/css
Modified to work in GNOME-Shell by Forge
### Usage
```js
import {
parse,
stringify,
write,
load,
} from './css/index.js';
// Raw APIs from ReworkCSS
let obj = parse('body { font-size: 12px; }');
let code = stringify(obj);
// Convenience
write(code, "/path/to/stylesheet.css");
let ast = load("/path/to/stylesheet.css");
// ... Do something with AST ...
```
### Example
CSS:
```css
body {
background: #eee;
color: #888;
}
```
Parse tree:
```json
{
"type": "stylesheet",
"stylesheet": {
"rules": [
{
"type": "rule",
"selectors": [
"body"
],
"declarations": [
{
"type": "declaration",
"property": "background",
"value": "#eee",
"position": {
"start": {
"line": 2,
"column": 3
},
"end": {
"line": 2,
"column": 19
}
}
},
{
"type": "declaration",
"property": "color",
"value": "#888",
"position": {
"start": {
"line": 3,
"column": 3
},
"end": {
"line": 3,
"column": 14
}
}
}
],
"position": {
"start": {
"line": 1,
"column": 1
},
"end": {
"line": 4,
"column": 2
}
}
}
]
}
}
```

View File

@ -0,0 +1,888 @@
// Credits: https://github.com/reworkcss/css/tree/master/lib/parse
// Licensed under MIT
// Forge: modified while{} loop declarations on some lines to reduce errors on logging
// http://www.w3.org/TR/CSS21/grammar.html
// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
const commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g;
export function parse(css, options) {
options = options || {};
/**
* Positional.
*/
var lineno = 1;
var column = 1;
/**
* Update lineno and column based on `str`.
*/
function updatePosition(str) {
var lines = str.match(/\n/g);
if (lines) lineno += lines.length;
var i = str.lastIndexOf("\n");
column = ~i ? str.length - i : column + str.length;
}
/**
* Mark position and patch `node.position`.
*/
function position() {
var start = { line: lineno, column: column };
return function (node) {
node.position = new Position(start);
whitespace();
return node;
};
}
/**
* Store position information for a node
*/
class Position {
/**
* Non-enumerable source string
*/
content = css;
constructor(start) {
this.start = start;
this.end = { line: lineno, column: column };
this.source = options.source;
}
}
/**
* Error `msg`.
*/
var errorsList = [];
function error(msg) {
var err = new Error(options.source + ":" + lineno + ":" + column + ": " + msg);
err.reason = msg;
err.filename = options.source;
err.line = lineno;
err.column = column;
err.source = css;
if (options.silent) {
errorsList.push(err);
} else {
throw err;
}
}
/**
* Parse stylesheet.
*/
function stylesheet() {
var rulesList = rules();
return {
type: "stylesheet",
stylesheet: {
source: options.source,
rules: rulesList,
parsingErrors: errorsList,
},
};
}
/**
* Opening brace.
*/
function open() {
return match(/^{\s*/);
}
/**
* Closing brace.
*/
function close() {
return match(/^}/);
}
/**
* Parse ruleset.
*/
function rules() {
var node;
var rules = [];
whitespace();
comments(rules);
while (css.length && css.charAt(0) != "}" && (node = atrule() || rule())) {
if (node !== false) {
rules.push(node);
comments(rules);
}
}
return rules;
}
/**
* Match `re` and return captures.
*/
function match(re) {
var m = re.exec(css);
if (!m) return;
var str = m[0];
updatePosition(str);
css = css.slice(str.length);
return m;
}
/**
* Parse whitespace.
*/
function whitespace() {
match(/^\s*/);
}
/**
* Parse comments;
*/
function comments(rules) {
rules = rules || [];
for (var c; (c = comment()); ) {
if (c !== false) {
rules.push(c);
}
}
return rules;
}
/**
* Parse comment.
*/
function comment() {
var pos = position();
if ("/" != css.charAt(0) || "*" != css.charAt(1)) return;
var i = 2;
while ("" != css.charAt(i) && ("*" != css.charAt(i) || "/" != css.charAt(i + 1))) ++i;
i += 2;
if ("" === css.charAt(i - 1)) {
return error("End of comment missing");
}
var str = css.slice(2, i - 2);
column += 2;
updatePosition(str);
css = css.slice(i);
column += 2;
return pos({
type: "comment",
comment: str,
});
}
/**
* Parse selector.
*/
function selector() {
var m = match(/^([^{]+)/);
if (!m) return;
/* @fix Remove all comments from selectors
* http://ostermiller.org/findcomment.html */
return trim(m[0])
.replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, "")
.replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function (m) {
return m.replace(/,/g, "\u200C");
})
.split(/\s*(?![^(]*\)),\s*/)
.map(function (s) {
return s.replace(/\u200C/g, ",");
});
}
/**
* Parse declaration.
*/
function declaration() {
var pos = position();
// prop
var prop = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
if (!prop) return;
prop = trim(prop[0]);
// :
if (!match(/^:\s*/)) return error("property missing ':'");
// val
var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);
var ret = pos({
type: "declaration",
property: prop.replace(commentre, ""),
value: val ? trim(val[0]).replace(commentre, "") : "",
});
// ;
match(/^[;\s]*/);
return ret;
}
/**
* Parse declarations.
*/
function declarations() {
var decls = [];
if (!open()) return error("missing '{'");
comments(decls);
// declarations
for (var decl; (decl = declaration()); ) {
if (decl !== false) {
decls.push(decl);
comments(decls);
}
}
if (!close()) return error("missing '}'");
return decls;
}
/**
* Parse keyframe.
*/
function keyframe() {
var vals = [];
var pos = position();
for (var m; (m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)); ) {
vals.push(m[1]);
match(/^,\s*/);
}
if (!vals.length) return;
return pos({
type: "keyframe",
values: vals,
declarations: declarations(),
});
}
/**
* Parse keyframes.
*/
function atkeyframes() {
var pos = position();
var m = match(/^@([-\w]+)?keyframes\s*/);
if (!m) return;
var vendor = m[1];
// identifier
var m = match(/^([-\w]+)\s*/);
if (!m) return error("@keyframes missing name");
var name = m[1];
if (!open()) return error("@keyframes missing '{'");
var frames = comments();
for (var frame; (frame = keyframe()); ) {
frames.push(frame);
frames = frames.concat(comments());
}
if (!close()) return error("@keyframes missing '}'");
return pos({
type: "keyframes",
name: name,
vendor: vendor,
keyframes: frames,
});
}
/**
* Parse supports.
*/
function atsupports() {
var pos = position();
var m = match(/^@supports *([^{]+)/);
if (!m) return;
var supports = trim(m[1]);
if (!open()) return error("@supports missing '{'");
var style = comments().concat(rules());
if (!close()) return error("@supports missing '}'");
return pos({
type: "supports",
supports: supports,
rules: style,
});
}
/**
* Parse host.
*/
function athost() {
var pos = position();
var m = match(/^@host\s*/);
if (!m) return;
if (!open()) return error("@host missing '{'");
var style = comments().concat(rules());
if (!close()) return error("@host missing '}'");
return pos({
type: "host",
rules: style,
});
}
/**
* Parse media.
*/
function atmedia() {
var pos = position();
var m = match(/^@media *([^{]+)/);
if (!m) return;
var media = trim(m[1]);
if (!open()) return error("@media missing '{'");
var style = comments().concat(rules());
if (!close()) return error("@media missing '}'");
return pos({
type: "media",
media: media,
rules: style,
});
}
/**
* Parse custom-media.
*/
function atcustommedia() {
var pos = position();
var m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/);
if (!m) return;
return pos({
type: "custom-media",
name: trim(m[1]),
media: trim(m[2]),
});
}
/**
* Parse paged media.
*/
function atpage() {
var pos = position();
var m = match(/^@page */);
if (!m) return;
var sel = selector() || [];
if (!open()) return error("@page missing '{'");
var decls = comments();
// declarations
for (var decl; (decl = declaration()); ) {
decls.push(decl);
decls = decls.concat(comments());
}
if (!close()) return error("@page missing '}'");
return pos({
type: "page",
selectors: sel,
declarations: decls,
});
}
/**
* Parse document.
*/
function atdocument() {
var pos = position();
var m = match(/^@([-\w]+)?document *([^{]+)/);
if (!m) return;
var vendor = trim(m[1]);
var doc = trim(m[2]);
if (!open()) return error("@document missing '{'");
var style = comments().concat(rules());
if (!close()) return error("@document missing '}'");
return pos({
type: "document",
document: doc,
vendor: vendor,
rules: style,
});
}
/**
* Parse font-face.
*/
function atfontface() {
var pos = position();
var m = match(/^@font-face\s*/);
if (!m) return;
if (!open()) return error("@font-face missing '{'");
var decls = comments();
// declarations
for (var decl; (decl = declaration()); ) {
decls.push(decl);
decls = decls.concat(comments());
}
if (!close()) return error("@font-face missing '}'");
return pos({
type: "font-face",
declarations: decls,
});
}
/**
* Parse import
*/
var atimport = _compileAtrule("import");
/**
* Parse charset
*/
var atcharset = _compileAtrule("charset");
/**
* Parse namespace
*/
var atnamespace = _compileAtrule("namespace");
/**
* Parse non-block at-rules
*/
function _compileAtrule(name) {
var re = new RegExp("^@" + name + "\\s*([^;]+);");
return function () {
var pos = position();
var m = match(re);
if (!m) return;
var ret = { type: name };
ret[name] = m[1].trim();
return pos(ret);
};
}
/**
* Parse at rule.
*/
function atrule() {
if (css[0] != "@") return;
return (
atkeyframes() ||
atmedia() ||
atcustommedia() ||
atsupports() ||
atimport() ||
atcharset() ||
atnamespace() ||
atdocument() ||
atpage() ||
athost() ||
atfontface()
);
}
/**
* Parse rule.
*/
function rule() {
var pos = position();
var sel = selector();
if (!sel) return error("selector missing");
comments();
return pos({
type: "rule",
selectors: sel,
declarations: declarations(),
});
}
return addParent(stylesheet());
}
/**
* Trim `str`.
*/
export function trim(str) {
return str ? str.replace(/^\s+|\s+$/g, "") : "";
}
/**
* Adds non-enumerable parent node reference to each node.
*/
export function addParent(obj, parent) {
var isNode = obj && typeof obj.type === "string";
var childParent = isNode ? obj : parent;
for (var k in obj) {
var value = obj[k];
if (Array.isArray(value)) {
value.forEach(function (v) {
addParent(v, childParent);
});
} else if (value && typeof value === "object") {
addParent(value, childParent);
}
}
if (isNode) {
Object.defineProperty(obj, "parent", {
configurable: true,
writable: true,
enumerable: false,
value: parent || null,
});
}
return obj;
}
// Credits: https://github.com/reworkcss/css/blob/master/lib/stringify
// Forge: derived the identity.js module only
export class Compiler {
constructor(options) {
options ||= {};
this.indentation = typeof options.indent === "string" ? options.indent : " ";
}
/**
* Emit `str`
*/
emit(str) {
return str;
}
/**
* Visit `node`.
*/
visit(node) {
return this[node.type](node);
}
/**
* Map visit over array of `nodes`, optionally using a `delim`
*/
mapVisit(nodes, delim) {
var buf = "";
delim = delim || "";
for (var i = 0, length = nodes.length; i < length; i++) {
buf += this.visit(nodes[i]);
if (delim && i < length - 1) buf += this.emit(delim);
}
return buf;
}
/**
* Compile `node`.
*/
compile(node) {
return this.stylesheet(node);
}
/**
* Visit stylesheet node.
*/
stylesheet(node) {
return this.mapVisit(node.stylesheet.rules, "\n\n");
}
/**
* Visit comment node.
*/
comment(node) {
return this.emit(this.indent() + "/*" + node.comment + "*/", node.position);
}
/**
* Visit import node.
*/
import(node) {
return this.emit("@import " + node.import + ";", node.position);
}
/**
* Visit media node.
*/
media(node) {
return (
this.emit("@media " + node.media, node.position) +
this.emit(" {\n" + this.indent(1)) +
this.mapVisit(node.rules, "\n\n") +
this.emit(this.indent(-1) + "\n}")
);
}
/**
* Visit document node.
*/
document(node) {
var doc = "@" + (node.vendor || "") + "document " + node.document;
return (
this.emit(doc, node.position) +
this.emit(" " + " {\n" + this.indent(1)) +
this.mapVisit(node.rules, "\n\n") +
this.emit(this.indent(-1) + "\n}")
);
}
/**
* Visit charset node.
*/
charset(node) {
return this.emit("@charset " + node.charset + ";", node.position);
}
/**
* Visit namespace node.
*/
namespace(node) {
return this.emit("@namespace " + node.namespace + ";", node.position);
}
/**
* Visit supports node.
*/
supports(node) {
return (
this.emit("@supports " + node.supports, node.position) +
this.emit(" {\n" + this.indent(1)) +
this.mapVisit(node.rules, "\n\n") +
this.emit(this.indent(-1) + "\n}")
);
}
/**
* Visit keyframes node.
*/
keyframes(node) {
return (
this.emit("@" + (node.vendor || "") + "keyframes " + node.name, node.position) +
this.emit(" {\n" + this.indent(1)) +
this.mapVisit(node.keyframes, "\n") +
this.emit(this.indent(-1) + "}")
);
}
/**
* Visit keyframe node.
*/
keyframe(node) {
var decls = node.declarations;
return (
this.emit(this.indent()) +
this.emit(node.values.join(", "), node.position) +
this.emit(" {\n" + this.indent(1)) +
this.mapVisit(decls, "\n") +
this.emit(this.indent(-1) + "\n" + this.indent() + "}\n")
);
}
/**
* Visit page node.
*/
page(node) {
var sel = node.selectors.length ? node.selectors.join(", ") + " " : "";
return (
this.emit("@page " + sel, node.position) +
this.emit("{\n") +
this.emit(this.indent(1)) +
this.mapVisit(node.declarations, "\n") +
this.emit(this.indent(-1)) +
this.emit("\n}")
);
}
/**
* Visit font-face node.
*/
["font-face"] = function (node) {
return (
this.emit("@font-face ", node.position) +
this.emit("{\n") +
this.emit(this.indent(1)) +
this.mapVisit(node.declarations, "\n") +
this.emit(this.indent(-1)) +
this.emit("\n}")
);
};
/**
* Visit host node.
*/
host(node) {
return (
this.emit("@host", node.position) +
this.emit(" {\n" + this.indent(1)) +
this.mapVisit(node.rules, "\n\n") +
this.emit(this.indent(-1) + "\n}")
);
}
/**
* Visit custom-media node.
*/
["custom-media"] = function (node) {
return this.emit("@custom-media " + node.name + " " + node.media + ";", node.position);
};
/**
* Visit rule node.
*/
rule(node) {
var indent = this.indent();
var decls = node.declarations;
if (!decls.length) return "";
return (
this.emit(
node.selectors
.map(function (s) {
return indent + s;
})
.join(",\n"),
node.position
) +
this.emit(" {\n") +
this.emit(this.indent(1)) +
this.mapVisit(decls, "\n") +
this.emit(this.indent(-1)) +
this.emit("\n" + this.indent() + "}")
);
}
/**
* Visit declaration node.
*/
declaration(node) {
return (
this.emit(this.indent()) +
this.emit(node.property + ": " + node.value, node.position) +
this.emit(";")
);
}
/**
* Increase, decrease or return current indentation.
*/
indent(level) {
this.level = this.level || 1;
if (null != level) {
this.level += level;
return "";
}
return Array(this.level).join(this.indentation);
}
}
// Credits: https://github.com/reworkcss/css/tree/master/lib/stringify
// Licensed under MIT
// Forge: removed unused options
/**
* Stringfy the given AST `node`.
*
* @param {Object} node
* @param {Object} [_options]
* @return {String}
* @api public
*/
export function stringify(node, _options) {
_options ||= {};
var compiler = new Compiler(_options);
var code = compiler.compile(node);
return code;
}

View File

@ -0,0 +1,43 @@
import GObject from "gi://GObject";
import St from "gi://St";
import { ThemeManagerBase } from "../shared/theme.js";
import { Logger } from "../shared/logger.js";
import { production } from "../shared/settings.js";
export class ExtensionThemeManager extends ThemeManagerBase {
static {
GObject.registerClass(this);
}
/**
* @param {import("../../extension.js").default} extension
*/
constructor(extension) {
super(extension);
this.metadata = extension.metadata;
}
reloadStylesheet() {
const uuid = this.metadata.uuid;
const stylesheetFile = this.configMgr.stylesheetFile;
const defaultStylesheetFile = this.configMgr.defaultStylesheetFile;
let theme = St.ThemeContext.get_for_stage(global.stage).get_theme();
try {
theme.unload_stylesheet(defaultStylesheetFile);
theme.unload_stylesheet(stylesheetFile);
if (production) {
theme.load_stylesheet(stylesheetFile);
this.stylesheet = stylesheetFile;
} else {
theme.load_stylesheet(defaultStylesheetFile);
this.stylesheet = defaultStylesheetFile;
}
} catch (e) {
Logger.error(`${uuid} - ${e}`);
return;
}
}
}

View File

@ -0,0 +1,121 @@
import GObject from "gi://GObject";
import Gio from "gi://Gio";
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import { gettext as _ } from "resource:///org/gnome/shell/extensions/extension.js";
import { QuickMenuToggle, SystemIndicator } from "resource:///org/gnome/shell/ui/quickSettings.js";
import {
PopupSwitchMenuItem,
PopupSeparatorMenuItem,
} from "resource:///org/gnome/shell/ui/popupMenu.js";
import * as Utils from "./utils.js";
import { Logger } from "../shared/logger.js";
const iconName = "view-grid-symbolic";
/** @typedef {import('../../extension.js').default} ForgeExtension */
class SettingsPopupSwitch extends PopupSwitchMenuItem {
static {
GObject.registerClass(this);
}
/** @type {ForgeExtension} extension */
extension;
/**
* @param {string} title
* @param {ForgeExtension} extension
* @param {string} bind
*/
constructor(title, extension, bind) {
const active = !!extension.settings.get_boolean(bind);
super(title, active);
this.extension = extension;
Logger.info(bind, active);
this.connect("toggled", (item) => this.extension.settings.set_boolean(bind, item.state));
}
}
export class FeatureMenuToggle extends QuickMenuToggle {
static {
GObject.registerClass(this);
}
constructor(extension) {
const title = _("Tiling");
const initSettings = Utils.isGnomeGTE(45)
? { title, iconName, toggleMode: true }
: { label: title, iconName, toggleMode: true };
super(initSettings);
this.extension = extension;
this.extension.settings.bind(
"tiling-mode-enabled",
this,
"checked",
Gio.SettingsBindFlags.DEFAULT
);
this.extension.settings.bind(
"quick-settings-enabled",
this,
"visible",
Gio.SettingsBindFlags.DEFAULT
);
this.menu.setHeader(iconName, _("Forge"), _("Tiling Window Management"));
this.menu.addMenuItem(
(this._singleSwitch = new SettingsPopupSwitch(
_("Gaps Hidden when Single"),
this.extension,
"window-gap-hidden-on-single"
))
);
this.menu.addMenuItem(
(this._focusHintSwitch = new SettingsPopupSwitch(
_("Show Focus Hint Border"),
this.extension,
"focus-border-toggle"
))
);
// Add an entry-point for more settings
this.menu.addMenuItem(new PopupSeparatorMenuItem());
const settingsItem = this.menu.addAction(_("Settings"), () => this.extension.openPreferences());
// Ensure the settings are unavailable when the screen is locked
settingsItem.visible = Main.sessionMode.allowSettings;
this.menu._settingsActions[this.extension.uuid] = settingsItem;
}
}
export class FeatureIndicator extends SystemIndicator {
static {
GObject.registerClass(this);
}
constructor(extension) {
super();
this.extension = extension;
// Create the icon for the indicator
this._indicator = this._addIndicator();
this._indicator.icon_name = iconName;
const tilingModeEnabled = this.extension.settings.get_boolean("tiling-mode-enabled");
const quickSettingsEnabled = this.extension.settings.get_boolean("quick-settings-enabled");
this._indicator.visible = tilingModeEnabled && quickSettingsEnabled;
this.extension.settings.connect("changed", (_, name) => {
switch (name) {
case "tiling-mode-enabled":
case "quick-settings-enabled":
this._indicator.visible = this.extension.settings.get_boolean(name);
}
});
}
}

View File

@ -0,0 +1,493 @@
/*
* 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 GObject from "gi://GObject";
import Meta from "gi://Meta";
import Shell from "gi://Shell";
// Gnome Shell imports
import * as Main from "resource:///org/gnome/shell/ui/main.js";
// Shared state
import { Logger } from "../shared/logger.js";
export class Keybindings extends GObject.Object {
static {
GObject.registerClass(this);
}
/** @type {import('./extension.js').default} */
ext;
constructor(ext) {
super();
Logger.debug(`created keybindings`);
this._grabbers = new Map();
// this._bindSignals();
this.ext = ext;
this.extWm = ext.extWm;
this.kbdSettings = ext.kbdSettings;
this.settings = ext.settings;
this.buildBindingDefinitions();
}
// @deprecated
_acceleratorActivate(action) {
let grabber = this._grabbers.get(action);
if (grabber) {
Logger.debug(`Firing accelerator ${grabber.accelerator} : ${grabber.name}`);
grabber.callback();
} else {
Logger.error(`No listeners [action={${action}}]`);
}
}
// @deprecated
_bindSignals() {
global.display.connect("accelerator-activated", (_display, action, _deviceId, _timestamp) => {
this._acceleratorActivate(action);
});
}
enable() {
let keybindings = this._bindings;
for (const key in keybindings) {
Main.wm.addKeybinding(
key,
this.kbdSettings,
Meta.KeyBindingFlags.NONE,
Shell.ActionMode.NORMAL,
keybindings[key]
);
}
Logger.debug(`keybindings:enable`);
}
disable() {
let keybindings = this._bindings;
for (const key in keybindings) {
Main.wm.removeKeybinding(key);
}
Logger.debug(`keybindings:disable`);
}
// @deprecated
enableListenForBindings() {
windowConfig.forEach((config) => {
config.shortcut.forEach((shortcut) => {
this.listenFor(shortcut, () => {
config.actions.forEach((action) => {
this.extWm.command(action);
});
});
});
});
}
// @deprecated
disableListenForBindings() {
// The existing grabber items are from the custom config by
// this extension.
this._grabbers.forEach((grabber) => {
global.display.ungrab_accelerator(grabber.action);
Main.wm.allowKeybinding(grabber.name, Shell.ActionMode.NONE);
});
this._grabbers.clear();
}
/**
* API for quick binding of keys to function. This is going to be useful with SpaceMode
*
* @param {String} accelerator - keybinding combinations
* @param {Function} callback - function to call when the accelerator is invoked
*
* Credits:
* - https://superuser.com/a/1182899
* - Adapted based on current Gnome-shell API or syntax
*/
listenFor(accelerator, callback) {
let grabFlags = Meta.KeyBindingFlags.NONE;
let action = global.display.grab_accelerator(accelerator, grabFlags);
if (action == Meta.KeyBindingAction.NONE) {
Logger.error(`Unable to grab accelerator [binding={${accelerator}}]`);
// TODO - check the gnome keybindings for conflicts and notify the user
} else {
let name = Meta.external_binding_name_for_action(action);
Logger.debug(`Requesting WM to allow binding [name={${name}}]`);
Main.wm.allowKeybinding(name, Shell.ActionMode.ALL);
this._grabbers.set(action, {
name: name,
accelerator: accelerator,
callback: callback,
action: action,
});
}
}
get modifierState() {
const [_x, _y, state] = this.extWm.getPointer();
return state;
}
allowDragDropTile() {
const tileModifier = this.kbdSettings.get_string("mod-mask-mouse-tile");
const modState = this.modifierState;
// Using Clutter.ModifierType values and also testing for pointer
// being grabbed (256). E.g. grabbed + pressing Super = 256 + 64 = 320
// See window.js#_handleMoving() - an overlay preview is shown.
// See window.js#_handleGrabOpEnd() - when the drag has been dropped
switch (tileModifier) {
case "Super":
return modState === 64 || modState === 320;
case "Alt":
return modState === 8 || modState === 264;
case "Ctrl":
return modState === 4 || modState === 260;
case "None":
return true;
}
return false;
}
buildBindingDefinitions() {
this._bindings = {
"window-toggle-float": () => {
let actions = [
{
name: "FloatToggle",
mode: "float",
x: "center",
y: "center",
width: 0.65,
height: 0.75,
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"window-toggle-always-float": () => {
let action = {
name: "FloatClassToggle",
mode: "float",
x: "center",
y: "center",
width: 0.65,
height: 0.75,
};
this.extWm.command(action);
},
"window-focus-left": () => {
let actions = [
{
name: "Focus",
direction: "Left",
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"window-focus-down": () => {
let actions = [
{
name: "Focus",
direction: "Down",
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"window-focus-up": () => {
let actions = [
{
name: "Focus",
direction: "Up",
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"window-focus-right": () => {
let actions = [
{
name: "Focus",
direction: "Right",
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"window-swap-left": () => {
let actions = [
{
name: "Swap",
direction: "Left",
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"window-swap-down": () => {
let actions = [
{
name: "Swap",
direction: "Down",
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"window-swap-up": () => {
let actions = [
{
name: "Swap",
direction: "Up",
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"window-swap-right": () => {
let actions = [
{
name: "Swap",
direction: "Right",
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"window-move-left": () => {
let actions = [
{
name: "Move",
direction: "Left",
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"window-move-down": () => {
let actions = [
{
name: "Move",
direction: "Down",
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"window-move-up": () => {
let actions = [
{
name: "Move",
direction: "Up",
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"window-move-right": () => {
let actions = [
{
name: "Move",
direction: "Right",
},
];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"con-split-layout-toggle": () => {
let actions = [{ name: "LayoutToggle" }];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"con-split-vertical": () => {
let actions = [{ name: "Split", orientation: "vertical" }];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"con-split-horizontal": () => {
let actions = [{ name: "Split", orientation: "horizontal" }];
actions.forEach((action) => {
this.extWm.command(action);
});
},
"con-stacked-layout-toggle": () => {
let action = { name: "LayoutStackedToggle" };
this.extWm.command(action);
},
"con-tabbed-layout-toggle": () => {
let action = { name: "LayoutTabbedToggle" };
this.extWm.command(action);
},
"con-tabbed-showtab-decoration-toggle": () => {
let action = { name: "ShowTabDecorationToggle" };
this.extWm.command(action);
},
"focus-border-toggle": () => {
let action = { name: "FocusBorderToggle" };
this.extWm.command(action);
},
"prefs-tiling-toggle": () => {
let action = { name: "TilingModeToggle" };
this.extWm.command(action);
},
"window-gap-size-increase": () => {
let action = { name: "GapSize", amount: 1 };
this.extWm.command(action);
},
"window-gap-size-decrease": () => {
let action = { name: "GapSize", amount: -1 };
this.extWm.command(action);
},
"workspace-active-tile-toggle": () => {
let action = { name: "WorkspaceActiveTileToggle" };
this.extWm.command(action);
},
"prefs-open": () => {
let action = { name: "PrefsOpen" };
this.extWm.command(action);
},
"window-swap-last-active": () => {
let action = {
name: "WindowSwapLastActive",
};
this.extWm.command(action);
},
"window-snap-one-third-right": () => {
let action = {
name: "SnapLayoutMove",
direction: "Right",
amount: 1 / 3,
};
this.extWm.command(action);
},
"window-snap-two-third-right": () => {
let action = {
name: "SnapLayoutMove",
direction: "Right",
amount: 2 / 3,
};
this.extWm.command(action);
},
"window-snap-one-third-left": () => {
let action = {
name: "SnapLayoutMove",
direction: "Left",
amount: 1 / 3,
};
this.extWm.command(action);
},
"window-snap-two-third-left": () => {
let action = {
name: "SnapLayoutMove",
direction: "Left",
amount: 2 / 3,
};
this.extWm.command(action);
},
"window-snap-center": () => {
let action = {
name: "SnapLayoutMove",
direction: "Center",
};
this.extWm.command(action);
},
"window-resize-top-increase": () => {
let action = {
name: "WindowResizeTop",
amount: this.settings.get_uint("resize-amount"),
};
this.extWm.command(action);
},
"window-resize-top-decrease": () => {
let action = {
name: "WindowResizeTop",
amount: -1 * this.settings.get_uint("resize-amount"),
};
this.extWm.command(action);
},
"window-resize-bottom-increase": () => {
let action = {
name: "WindowResizeBottom",
amount: this.settings.get_uint("resize-amount"),
};
this.extWm.command(action);
},
"window-resize-bottom-decrease": () => {
let action = {
name: "WindowResizeBottom",
amount: -1 * this.settings.get_uint("resize-amount"),
};
this.extWm.command(action);
},
"window-resize-left-increase": () => {
let action = {
name: "WindowResizeLeft",
amount: this.settings.get_uint("resize-amount"),
};
this.extWm.command(action);
},
"window-resize-left-decrease": () => {
let action = {
name: "WindowResizeLeft",
amount: -1 * this.settings.get_uint("resize-amount"),
};
this.extWm.command(action);
},
"window-resize-right-increase": () => {
let action = {
name: "WindowResizeRight",
amount: this.settings.get_uint("resize-amount"),
};
this.extWm.command(action);
},
"window-resize-right-decrease": () => {
let action = {
name: "WindowResizeRight",
amount: -1 * this.settings.get_uint("resize-amount"),
};
this.extWm.command(action);
},
};
}
}

View File

@ -0,0 +1,381 @@
/*
* 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/>.
*
* Credits:
* This file has some code from Dash-To-Panel extension: convenience.js
* Some code was also adapted from the upstream Gnome Shell source code.
*/
// Gnome imports
import Meta from "gi://Meta";
import St from "gi://St";
// Gnome-shell imports
import { PACKAGE_VERSION } from "resource:///org/gnome/shell/misc/config.js";
// App imports
import { ORIENTATION_TYPES, LAYOUT_TYPES, POSITION } from "./tree.js";
import { GRAB_TYPES } from "./window.js";
const [major] = PACKAGE_VERSION.split(".").map((s) => Number(s));
/**
*
* Turns an array into an immutable enum-like object
*
*/
export function createEnum(anArray) {
const enumObj = {};
for (const val of anArray) {
enumObj[val] = val;
}
return Object.freeze(enumObj);
}
export function resolveX(rectRequest, metaWindow) {
let metaRect = metaWindow.get_frame_rect();
let monitorRect = metaWindow.get_work_area_current_monitor();
let val = metaRect.x;
let x = rectRequest.x;
switch (typeof x) {
case "string": //center,
switch (x) {
case "center":
val = monitorRect.width * 0.5 - this.resolveWidth(rectRequest, metaWindow) * 0.5;
break;
case "left":
val = 0;
break;
case "right":
val = monitorRect.width - this.resolveWidth(rectRequest, metaWindow);
break;
default:
break;
}
break;
case "number":
val = x;
break;
default:
break;
}
val = monitorRect.x + val;
return val;
}
export function resolveY(rectRequest, metaWindow) {
let metaRect = metaWindow.get_frame_rect();
let monitorRect = metaWindow.get_work_area_current_monitor();
let val = metaRect.y;
let y = rectRequest.y;
switch (typeof y) {
case "string": //center,
switch (y) {
case "center":
val = monitorRect.height * 0.5 - this.resolveHeight(rectRequest, metaWindow) * 0.5;
break;
case "top":
val = 0;
break;
case "bottom": // inverse of y=0
val = monitorRect.height - this.resolveHeight(rectRequest, metaWindow);
break;
default:
break;
}
break;
case "number":
val = y;
break;
default:
break;
}
val = monitorRect.y + val;
return val;
}
export function resolveWidth(rectRequest, metaWindow) {
let metaRect = metaWindow.get_frame_rect();
let monitorRect = metaWindow.get_work_area_current_monitor();
let val = metaRect.width;
let width = rectRequest.width;
switch (typeof width) {
case "number":
if (Number.isInteger(width) && width != 1) {
val = width;
} else {
let monitorWidth = monitorRect.width;
val = monitorWidth * width;
}
break;
default:
break;
}
return val;
}
export function resolveHeight(rectRequest, metaWindow) {
let metaRect = metaWindow.get_frame_rect();
let monitorRect = metaWindow.get_work_area_current_monitor();
let val = metaRect.height;
let height = rectRequest.height;
switch (typeof height) {
case "number":
if (Number.isInteger(height) && height != 1) {
val = height;
} else {
let monitorHeight = monitorRect.height;
val = monitorHeight * height;
}
break;
default:
break;
}
return val;
}
export function orientationFromDirection(direction) {
return direction === Meta.MotionDirection.LEFT || direction === Meta.MotionDirection.RIGHT
? ORIENTATION_TYPES.HORIZONTAL
: ORIENTATION_TYPES.VERTICAL;
}
export function orientationFromLayout(layout) {
switch (layout) {
case LAYOUT_TYPES.HSPLIT:
case LAYOUT_TYPES.TABBED:
return ORIENTATION_TYPES.HORIZONTAL;
case LAYOUT_TYPES.VSPLIT:
case LAYOUT_TYPES.STACKED:
return ORIENTATION_TYPES.VERTICAL;
default:
break;
}
}
export function positionFromDirection(direction) {
return direction === Meta.MotionDirection.LEFT || direction === Meta.MotionDirection.UP
? POSITION.BEFORE
: POSITION.AFTER;
}
export function resolveDirection(directionString) {
if (directionString) {
directionString = directionString.toUpperCase();
if (directionString === "LEFT") {
return Meta.MotionDirection.LEFT;
}
if (directionString === "RIGHT") {
return Meta.MotionDirection.RIGHT;
}
if (directionString === "UP") {
return Meta.MotionDirection.UP;
}
if (directionString === "DOWN") {
return Meta.MotionDirection.DOWN;
}
}
return null;
}
export function directionFrom(position, orientation) {
if (position === POSITION.AFTER) {
if (orientation === ORIENTATION_TYPES.HORIZONTAL) {
return Meta.DisplayDirection.RIGHT;
} else {
return Meta.DisplayDirection.DOWN;
}
} else if (position === POSITION.BEFORE) {
if (orientation === ORIENTATION_TYPES.HORIZONTAL) {
return Meta.DisplayDirection.LEFT;
} else {
return Meta.DisplayDirection.UP;
}
}
}
export function rectContainsPoint(rect, pointP) {
if (!(rect && pointP)) return false;
return (
rect.x <= pointP[0] &&
pointP[0] <= rect.x + rect.width &&
rect.y <= pointP[1] &&
pointP[1] <= rect.y + rect.height
);
}
export function orientationFromGrab(grabOp) {
if (
grabOp === Meta.GrabOp.RESIZING_N ||
grabOp === Meta.GrabOp.RESIZING_S ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_N ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_S
) {
return ORIENTATION_TYPES.VERTICAL;
} else if (
grabOp === Meta.GrabOp.RESIZING_E ||
grabOp === Meta.GrabOp.RESIZING_W ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_E ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_W
) {
return ORIENTATION_TYPES.HORIZONTAL;
}
return ORIENTATION_TYPES.NONE;
}
export function positionFromGrabOp(grabOp) {
if (
grabOp === Meta.GrabOp.RESIZING_W ||
grabOp === Meta.GrabOp.RESIZING_N ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_W ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_N
) {
return POSITION.BEFORE;
} else if (
grabOp === Meta.GrabOp.RESIZING_E ||
grabOp === Meta.GrabOp.RESIZING_S ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_E ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_S
) {
return POSITION.AFTER;
}
return POSITION.UNKNOWN;
}
export function allowResizeGrabOp(grabOp) {
return (
grabOp === Meta.GrabOp.RESIZING_N ||
grabOp === Meta.GrabOp.RESIZING_E ||
grabOp === Meta.GrabOp.RESIZING_W ||
grabOp === Meta.GrabOp.RESIZING_S ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_N ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_E ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_W ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_S ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_UNKNOWN
);
}
export function grabMode(grabOp) {
if (
grabOp === Meta.GrabOp.RESIZING_N ||
grabOp === Meta.GrabOp.RESIZING_E ||
grabOp === Meta.GrabOp.RESIZING_W ||
grabOp === Meta.GrabOp.RESIZING_S ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_N ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_E ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_W ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_S ||
grabOp === Meta.GrabOp.KEYBOARD_RESIZING_UNKNOWN
) {
return GRAB_TYPES.RESIZING;
} else if (
grabOp === Meta.GrabOp.KEYBOARD_MOVING ||
grabOp === Meta.GrabOp.MOVING ||
grabOp === Meta.GrabOp.MOVING_UNCONSTRAINED
) {
return GRAB_TYPES.MOVING;
}
return GRAB_TYPES.UNKNOWN;
}
export function directionFromGrab(grabOp) {
if (grabOp === Meta.GrabOp.RESIZING_E || grabOp === Meta.GrabOp.KEYBOARD_RESIZING_E) {
return Meta.MotionDirection.RIGHT;
} else if (grabOp === Meta.GrabOp.RESIZING_W || grabOp === Meta.GrabOp.KEYBOARD_RESIZING_W) {
return Meta.MotionDirection.LEFT;
} else if (grabOp === Meta.GrabOp.RESIZING_N || grabOp === Meta.GrabOp.KEYBOARD_RESIZING_N) {
return Meta.MotionDirection.UP;
} else if (grabOp === Meta.GrabOp.RESIZING_S || grabOp === Meta.GrabOp.KEYBOARD_RESIZING_S) {
return Meta.MotionDirection.DOWN;
}
}
export function removeGapOnRect(rectWithGap, gap) {
rectWithGap.x = rectWithGap.x -= gap;
rectWithGap.y = rectWithGap.y -= gap;
rectWithGap.width = rectWithGap.width += gap * 2;
rectWithGap.height = rectWithGap.height += gap * 2;
return rectWithGap;
}
// Credits: PopShell
export function findWindowWith(title) {
let display = global.display;
let type = Meta.TabList.NORMAL_ALL;
let workspaceMgr = display.get_workspace_manager();
let workspaces = workspaceMgr.get_n_workspaces();
for (let wsId = 1; wsId <= workspaces; wsId++) {
let workspace = workspaceMgr.get_workspace_by_index(wsId);
for (const metaWindow of display.get_tab_list(type, workspace)) {
if (
metaWindow.title &&
title &&
(metaWindow.title === title || metaWindow.title.includes(title))
) {
return metaWindow;
}
}
}
return undefined;
}
export function oppositeDirectionOf(direction) {
if (direction === Meta.MotionDirection.LEFT) {
return Meta.MotionDirection.RIGHT;
} else if (direction === Meta.MotionDirection.RIGHT) {
return Meta.MotionDirection.LEFT;
} else if (direction === Meta.MotionDirection.UP) {
return Meta.MotionDirection.DOWN;
} else if (direction === Meta.MotionDirection.DOWN) {
return Meta.MotionDirection.UP;
}
}
export function monitorIndex(monitorValue) {
if (!monitorValue) return -1;
let wsIndex = monitorValue.indexOf("ws");
let indexVal = monitorValue.slice(0, wsIndex);
indexVal = indexVal.replace("mo", "");
return parseInt(indexVal);
}
export function _disableDecorations() {
let decos = global.window_group.get_children().filter((a) => a.type != null);
decos.forEach((d) => {
global.window_group.remove_child(d);
d.destroy();
});
}
export function dpi() {
return St.ThemeContext.get_for_stage(global.stage).scale_factor;
}
export function isGnome(majorVersion) {
return major == majorVersion;
}
export function isGnomeGTE(majorVersion) {
return major >= majorVersion;
}

View File

@ -0,0 +1,190 @@
// 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) {
case ".window-tiled-border":
return _("Tiled Focus Hint and Preview");
case ".window-floated-border":
return _("Floated Focus Hint");
case ".window-split-border":
return _("Split Direction Hint");
case ".window-stacked-border":
return _("Stacked Focus Hint and Preview");
case ".window-tabbed-border":
return _("Tabbed Focus Hint and Preview");
}
}
constructor({ settings, dir }) {
super({ title: _("Appearance"), icon_name: "brush-symbolic" });
this.settings = settings;
this.configMgr = new ConfigManager({ dir });
this.themeMgr = new PrefsThemeManager(this);
this.add_group({
title: _("Gaps"),
children: [
// Gaps size
new SpinButtonRow({
title: _("Gaps Size"),
range: [0, 32, 1],
settings,
bind: "window-gap-size",
}),
// Gaps size multiplier
new SpinButtonRow({
title: _("Gaps Size Multiplier"),
range: [0, 8, 1],
settings,
bind: "window-gap-size-increment",
}),
// Gap Hidden when Single Window
new SwitchRow({
title: _("Gaps Hidden when Single"),
settings,
bind: "window-gap-hidden-on-single",
}),
],
});
this.add_group({
title: _("Color"),
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

@ -0,0 +1,108 @@
// 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" });
const description = `${_("Syntax")}: &lt;Super&gt;h, &lt;Shift&gt;g, &lt;Shift&gt;&lt;Super&gt;h
${_("Legend")}: &lt;Super&gt; - ${_("Windows key")}, &lt;Primary&gt; - ${_("Control key")}
${_("Delete text to unset. Press Return key to accept. Focus out to ignore.")} <i>${_(
"Resets"
)}</i> ${_("to previous value when invalid")}`;
this.add_group({
title: _("Update Shortcuts"),
description,
children: Object.entries({
window: "Window Shortcuts",
workspace: "Workspace Shortcuts",
con: "Container Shortcuts",
focus: "Focus Shortcuts",
prefs: "Other Shortcuts",
}).map(([prefix, gettextKey]) =>
KeyboardPage.makeKeygroupExpander(prefix, gettextKey, kbdSettings)
),
});
this.add_group({
title: _("Drag-Drop Tiling Modifier Key Options"),
description: `<i>${_(
"Change the modifier for <b>tiling</b> windows via mouse/drag-drop"
)}</i> ${_("Select <i>None</i> to <u>always tile immediately</u> by default")}`,
children: [
new RadioRow({
title: _("Tile Modifier"),
settings: kbdSettings,
bind: "mod-mask-mouse-tile",
options: {
Super: _("Super"),
Ctrl: _("Ctrl"),
Alt: _("Alt"),
None: _("None"),
},
}),
],
});
}
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

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

View File

@ -0,0 +1,13 @@
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

@ -0,0 +1,147 @@
// 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 } 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 }) {
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: _("Settings"), icon_name: "settings-symbolic" });
this.add_group({
title: _("Settings"),
description: _("Toggle Forge's high-level features"),
header_suffix: makeAboutButton(window, metadata),
children: [
new SwitchRow({
title: _("Stacked Tiling Mode"),
subtitle: _("Stack windows on top of each other while still being tiled"),
experimental: true,
settings,
bind: "stacked-tiling-mode-enabled",
}),
new SwitchRow({
title: _("Tabbed Tiling Mode"),
subtitle: _("Group tiles windows as tabs"),
experimental: true,
settings,
bind: "tabbed-tiling-mode-enabled",
}),
],
});
this.add_group({
title: _("Tiling"),
children: [
new SwitchRow({
title: _("Preview Hint Toggle"),
experimental: true,
settings,
bind: "preview-hint-enabled",
}),
new SwitchRow({
title: _("Show Focus Hint Border"),
subtitle: _("Display a colored border around the focused window"),
settings,
bind: "focus-border-toggle",
}),
new SwitchRow({
title: _("Show Window Split Hint Border"),
subtitle: _("Show split direction border on focused window"),
settings,
bind: "split-border-toggle",
}),
new DropDownRow({
title: _("Default Drag-and-Drop Center Layout"),
settings,
type: "s",
bind: "dnd-center-layout",
items: [
{ id: "tabbed", name: _("Tabbed") },
{ id: "stacked", name: _("Stacked") },
],
}),
new SwitchRow({
title: _("Auto Split"),
subtitle: _("Quarter Tiling"),
experimental: true,
settings,
bind: "auto-split-enabled",
}),
new SwitchRow({
title: _("Float Mode Always On Top"),
subtitle: _("Floating windows always above tiling windows"),
experimental: true,
settings,
bind: "float-always-on-top-enabled",
}),
new SwitchRow({
title: _("Show Tiling Quick Settings"),
subtitle: _("Toggle showing Forge on quick settings"),
experimental: true,
settings,
bind: "quick-settings-enabled",
}),
],
});
if (!production) {
this.add_group({
title: _("Logger"),
children: [
new DropDownRow({
title: _("Logger Level"),
settings,
bind: "log-level",
items: Object.entries(Logger.LOG_LEVELS).map(([name, id]) => ({ id, name })),
type: "u",
}),
],
});
}
}
}

View File

@ -0,0 +1,293 @@
/** @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);
}
}
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.valign = Gtk.Align.CENTER;
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.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 ResetButton extends Gtk.Button {
static {
GObject.registerClass(this);
}
constructor({ settings = undefined, bind = undefined, onReset }) {
super({
icon_name: "edit-clear-symbolic",
tooltip_text: _("Reset"),
valign: Gtk.Align.CENTER,
});
this.connect("clicked", () => {
settings?.reset(bind);
onReset?.();
});
}
}
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 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 = 10;
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.connect("clicked", () => {
if (toggle.active) {
settings.set_string(bind, labels[toggle.label]);
}
});
hbox.append(toggle);
}
this.add_suffix(hbox);
}
}

View File

@ -0,0 +1,29 @@
// Gnome imports
import GObject from "gi://GObject";
import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
import { EntryRow, PreferencesPage } from "./widgets.js";
export class WorkspacePage extends PreferencesPage {
static {
GObject.registerClass(this);
}
constructor({ settings }) {
super({ title: _("Workspace"), icon_name: "shell-overview-symbolic" });
this.add_group({
title: _("Update Workspace Settings"),
description: _(
"Provide workspace indices to skip. E.g. 0,1. Empty text to disable. Enter to accept"
),
children: [
new EntryRow({
title: _("Skip Workspace Tiling"),
settings,
bind: "workspace-skip-tile",
}),
],
});
}
}

View File

@ -0,0 +1,80 @@
/*
* 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/>.
*
*/
import { production } from "./settings.js";
export class Logger {
static #settings;
static LOG_LEVELS = {
OFF: 0,
FATAL: 1,
ERROR: 2,
WARN: 3,
INFO: 4,
DEBUG: 5,
TRACE: 6,
ALL: 7,
};
static init(settings) {
this.#settings = settings;
}
static get #level() {
if (this.#settings?.get_boolean?.("logging-enabled")) {
return production
? Logger.LOG_LEVELS.OFF
: this.#settings?.get_uint?.("log-level") ?? Logger.LOG_LEVELS.OFF;
}
return Logger.LOG_LEVELS.OFF;
}
// TODO: use console.* methods
static format(msg, ...params) {
return params.reduce((acc, val) => acc.replace("{}", val), msg);
}
static fatal(...args) {
if (this.#level > Logger.LOG_LEVELS.OFF) log(`[Forge] [FATAL]`, ...args);
}
static error(...args) {
if (this.#level > Logger.LOG_LEVELS.FATAL) log(`[Forge] [ERROR]`, ...args);
}
static warn(...args) {
if (this.#level > Logger.LOG_LEVELS.ERROR) log(`[Forge] [WARN]`, ...args);
}
static info(...args) {
if (this.#level > Logger.LOG_LEVELS.WARN) log(`[Forge] [INFO]`, ...args);
}
static debug(...args) {
if (this.#level > Logger.LOG_LEVELS.INFO) log(`[Forge] [DEBUG]`, ...args);
}
static trace(...args) {
if (this.#level > Logger.LOG_LEVELS.DEBUG) log(`[Forge] [TRACE]`, ...args);
}
static log(...args) {
if (this.#level > Logger.LOG_LEVELS.OFF) log(`[Forge] [LOG]`, ...args);
}
}

View File

@ -0,0 +1,155 @@
/*
* This file is part of the Forge Window Manager extension for Gnome 3
*
* 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";
import { Logger } from "./logger.js";
// Dev or Prod mode, see Makefile:debug
export const production = true;
export class ConfigManager extends GObject.Object {
static {
GObject.registerClass(this);
}
#confDir = GLib.get_user_config_dir();
constructor({ dir }) {
super();
this.extensionPath = dir.get_path();
}
get confDir() {
return `${this.#confDir}/forge`;
}
get defaultStylesheetFile() {
const defaultStylesheet = GLib.build_filenamev([this.extensionPath, `stylesheet.css`]);
Logger.trace(`default-stylesheet: ${defaultStylesheet}`);
const defaultStylesheetFile = Gio.File.new_for_path(defaultStylesheet);
if (defaultStylesheetFile.query_exists(null)) {
return defaultStylesheetFile;
}
return null;
}
get stylesheetFile() {
const profileSettingPath = `${this.confDir}/stylesheet/forge`;
const settingFile = "stylesheet.css";
const defaultSettingFile = this.defaultStylesheetFile;
return this.loadFile(profileSettingPath, settingFile, defaultSettingFile);
}
get defaultWindowConfigFile() {
const defaultWindowConfig = GLib.build_filenamev([
this.extensionPath,
`config`,
`windows.json`,
]);
Logger.trace(`default-window-config: ${defaultWindowConfig}`);
const defaultWindowConfigFile = Gio.File.new_for_path(defaultWindowConfig);
if (defaultWindowConfigFile.query_exists(null)) {
return defaultWindowConfigFile;
}
return null;
}
get windowConfigFile() {
const profileSettingPath = `${this.confDir}/config`;
const settingFile = "windows.json";
const defaultSettingFile = this.defaultWindowConfigFile;
return this.loadFile(profileSettingPath, settingFile, defaultSettingFile);
}
loadFile(path, file, defaultFile) {
const customSetting = GLib.build_filenamev([path, file]);
Logger.trace(`custom-setting-file: ${customSetting}`);
const customSettingFile = Gio.File.new_for_path(customSetting);
if (customSettingFile.query_exists(null)) {
return customSettingFile;
} else {
const profileCustomSettingDir = Gio.File.new_for_path(path);
if (!profileCustomSettingDir.query_exists(null)) {
if (profileCustomSettingDir.make_directory_with_parents(null)) {
const createdStream = customSettingFile.create(Gio.FileCreateFlags.NONE, null);
const defaultContents = this.loadFileContents(defaultFile);
Logger.trace(defaultContents);
createdStream.write_all(defaultContents, null);
}
}
}
return null;
}
loadFileContents(configFile) {
let [success, contents] = configFile.load_contents(null);
if (success) {
const stringContents = imports.byteArray.toString(contents);
return stringContents;
}
}
get windowProps() {
let windowConfigFile = this.windowConfigFile;
let windowProps = null;
if (!windowConfigFile || !production) {
windowConfigFile = this.defaultWindowConfigFile;
}
let [success, contents] = windowConfigFile.load_contents(null);
if (success) {
const windowConfigContents = imports.byteArray.toString(contents);
Logger.trace(`${windowConfigContents}`);
windowProps = JSON.parse(windowConfigContents);
}
return windowProps;
}
set windowProps(props) {
let windowConfigFile = this.windowConfigFile;
if (!windowConfigFile || !production) {
windowConfigFile = this.defaultWindowConfigFile;
}
let windowConfigContents = JSON.stringify(props, null, 4);
const PERMISSIONS_MODE = 0o744;
if (GLib.mkdir_with_parents(windowConfigFile.get_parent().get_path(), PERMISSIONS_MODE) === 0) {
let [_, _tag] = windowConfigFile.replace_contents(
windowConfigContents,
null,
false,
Gio.FileCreateFlags.REPLACE_DESTINATION,
null
);
}
}
}

View File

@ -0,0 +1,279 @@
/*
* 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 + ")";
}