[gnome] Update extensions
This commit is contained in:
@ -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.
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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")}: <Super>h, <Shift>g, <Shift><Super>h
|
||||
${_("Legend")}: <Super> - ${_("Windows key")}, <Primary> - ${_("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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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 + '>')
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 + ")";
|
||||
}
|
||||
Reference in New Issue
Block a user