diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..df95e0c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,28 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] + +[*.js] +indent_size = 4 + +[*.[ch]] +indent_size = 2 + +[*.gresource.xml] +indent_size = 2 + +[*.ui] +indent_size = 2 + +[*.{yml,yaml}] +indent_size = 2 + +[meson.build] +indent_size = 2 diff --git a/.github/workflows/run-ci.yml b/.github/workflows/run-ci.yml index 617d674..2ccf7a7 100644 --- a/.github/workflows/run-ci.yml +++ b/.github/workflows/run-ci.yml @@ -11,7 +11,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-node@v3 with: - node-version: "*" + node-version: '*' - name: Prepare Linters run: npm i - name: Run ESLint (*.js) @@ -19,6 +19,11 @@ jobs: git diff --name-only --diff-filter=ACMTUXB origin/${{ github.base_ref }} HEAD | grep -E "\.js$" | xargs -r npx eslint + - name: Run Prettier + if: success() || failure() + run: > + git diff --name-only --diff-filter=ACMTUXB origin/${{ github.base_ref }} HEAD | + xargs -r npx prettier -c --ignore-unknown - name: Run ShellCheck (*.sh) if: success() || failure() run: > @@ -35,7 +40,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-node@v3 with: - node-version: "*" + node-version: '*' - run: pip install codespell # The case sensitivity of codespeller for ignore-words seems to be buggy. See # issue tracker. According to the help page it should be case sensitive but diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..83b6947 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +CHANGELOG.md \ No newline at end of file diff --git a/README.md b/README.md index bbc1d79..28147e1 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,15 @@ The [metadata](https://github.com/Leleat/Tiling-Assistant/blob/main/tiling-assis Here is a table showing the GNOME Shell releases and the latest extension version supporting them. | GNOME Shell | Tiling Assistant | -|:-------------:|:-----------:| -| 45 | 44 | -| 44 | 43 | -| 43 | 43 | -| 42 | 36 | -| 41 | 32 | -| 40 | 32 | -| 3.38 | 23 | -| 3.36 | 23 | +| :---------: | :--------------: | +| 45 | 44 | +| 44 | 43 | +| 43 | 43 | +| 42 | 36 | +| 41 | 32 | +| 40 | 32 | +| 3.38 | 23 | +| 3.36 | 23 | ## Installation diff --git a/eslint.config.js b/eslint.config.js index a88a03f..0e5864b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,286 +1,11 @@ -// Based on https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/blob/main/lint/eslintrc-gjs.yml#L67 - -import js from "@eslint/js"; +import eslintConfigPrettier from 'eslint-config-prettier'; +import gjsConfig from './lint/eslintrc-gjs.config.js'; +import gnomeShellConfig from './lint/eslintrc-shell.config.js'; +import extensionConfig from './lint/eslintrc-extension.config.js'; export default [ - js.configs.recommended, - { - files: ["tiling-assistant@leleat-on-github/**/*.js"], - languageOptions: { - globals: { - ARGV: "readonly", - Debugger: "readonly", - GIRepositoryGType: "readonly", - globalThis: "readonly", - global: "readonly", - imports: "readonly", - Intl: "readonly", - log: "readonly", - logError: "readonly", - print: "readonly", - printerr: "readonly", - window: "readonly", - TextEncoder: "readonly", - TextDecoder: "readonly" - } - }, - rules: { - "array-bracket-newline": [ - "error", - "consistent" - ], - "array-bracket-spacing": [ - "error", - "never" - ], - "array-callback-return": "error", - "arrow-parens": [ - "error", - "as-needed" - ], - "arrow-spacing": "error", - "block-scoped-var": "error", - "block-spacing": "error", - "comma-dangle": "error", - "comma-spacing": [ - "error", - { - "before": false, - "after": true - } - ], - "comma-style": [ - "error", - "last" - ], - "computed-property-spacing": "error", - "curly": [ - "error", - "multi-or-nest", - "consistent" - ], - "dot-location": [ - "error", - "property" - ], - "eol-last": "error", - "eqeqeq": "error", - "func-call-spacing": "error", - "func-name-matching": "error", - "func-style": [ - "error", - "declaration", - { - "allowArrowFunctions": true - } - ], - "indent": [ - "error", - 4, - { - "FunctionExpression": { - "parameters": 2 - }, - "SwitchCase": 1, - "ignoredNodes": ["CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child"], - "MemberExpression": "off" - - } - ], - "key-spacing": [ - "error", - { - "beforeColon": false, - "afterColon": true - } - ], - "keyword-spacing": [ - "error", - { - "before": true, - "after": true - } - ], - "linebreak-style": [ - "error", - "unix" - ], - "max-nested-callbacks": "error", - "max-statements-per-line": [ - "error", - { "max": 2 } - ], - "new-parens": "error", - "no-array-constructor": "error", - "no-await-in-loop": "error", - "no-caller": "error", - "no-constant-condition": [ - "error", - { "checkLoops": false } - ], - "no-div-regex": "error", - "no-empty": [ - "error", - { "allowEmptyCatch": true } - ], - "no-extra-bind": "error", - "no-extra-boolean-cast": "off", - "no-extra-parens": [ - "error", - "all", - { - "conditionalAssign": false, - "nestedBinaryExpressions": false, - "returnAssign": false - } - ], - "no-implicit-coercion": [ - "error", - { "allow": ["!!"] } - ], - "no-iterator": "error", - "no-label-var": "error", - "no-lonely-if": "error", - "no-loop-func": "error", - "no-multiple-empty-lines": "error", - "no-multi-spaces": "error", - "no-nested-ternary": "error", - "no-new-object": "error", - "no-new-wrappers": "error", - "no-octal-escape": "error", - "no-proto": "error", - "no-prototype-builtins": "off", - "no-restricted-properties": [ - "error", - { - "object": "imports", - "property": "format", - "message": "Use template strings" - }, - { - "object": "pkg", - "property": "initFormat", - "message": "Use template strings" - }, - { - "object": "Lang", - "property": "copyProperties", - "message": "Use Object.assign()" - }, - { - "object": "Lang", - "property": "bind", - "message": "Use arrow notation or Function.prototype.bind()" - }, - { - "object": "Lang", - "property": "Class", - "message": "Use ES6 classes" - } - ], - "no-return-assign": "error", - "no-return-await": "error", - "no-self-compare": "error", - "no-shadow": "error", - "no-shadow-restricted-names": "error", - "no-spaced-func": "error", - "no-tabs": "error", - "no-template-curly-in-string": "error", - "no-throw-literal": "error", - "no-trailing-spaces": "error", - "no-undef": "error", - "no-unneeded-ternary": "error", - "no-unused-vars": [ - "error", - { - "vars": "local", - "varsIgnorePattern": "(^unused|_$)", - "argsIgnorePattern": "^(unused|_)" - } - ], - "no-useless-call": "error", - "no-useless-computed-key": "error", - "no-useless-concat": "error", - "no-useless-constructor": "error", - "no-useless-rename": "error", - "no-useless-return": "error", - "no-whitespace-before-property": "error", - "no-with": "error", - "nonblock-statement-body-position": [ - "error", - "below" - ], - "object-curly-newline": [ - "error", - { - "consistent": true, - "multiline": true - } - ], - "object-curly-spacing": [ - "error", - "always" - ], - "object-shorthand": "error", - "operator-assignment": "error", - "operator-linebreak": "error", - "padded-blocks": [ - "error", - "never" - ], - "prefer-numeric-literals": "error", - "prefer-promise-reject-errors": "error", - "prefer-rest-params": "error", - "prefer-spread": "error", - "prefer-template": "error", - "quotes": [ - "error", - "single", - { "avoidEscape": true } - ], - "require-await": "error", - "rest-spread-spacing": "error", - "semi": [ - "error", - "always" - ], - "semi-spacing": [ - "error", - { - "before": false, - "after": true - } - ], - "semi-style": "error", - "space-before-blocks": [ - "error", - "always" - ], - "space-before-function-paren": [ - "error", - { - "named": "never", - "anonymous": "always", - "asyncArrow": "always" - } - ], - "space-in-parens": "error", - "space-infix-ops": [ - "error", - { "int32Hint": false } - ], - "space-unary-ops": "error", - "switch-colon-spacing": "error", - "symbol-description": "error", - "template-curly-spacing": "error", - "template-tag-spacing": "error", - "unicode-bom": "error", - "wrap-iife": [ - "error", - "inside" - ], - "yield-star-spacing": "error", - "yoda": "error" - }, - } + ...gjsConfig, + ...gnomeShellConfig, + eslintConfigPrettier, + ...extensionConfig, ]; diff --git a/lint/eslintrc-extension.config.js b/lint/eslintrc-extension.config.js new file mode 100644 index 0000000..1b7a9ce --- /dev/null +++ b/lint/eslintrc-extension.config.js @@ -0,0 +1,32 @@ +// Override GJS, GNOME Shell and prettier config options here + +export default [ + { + rules: { + curly: ['error', 'all'], + }, + }, + { + files: ['tiling-assistant@leleat-on-github/**'], + ignores: ['tiling-assistant@leleat-on-github/prefs**'], + languageOptions: { + globals: { + global: 'readonly', + _: 'readonly', + C_: 'readonly', + N_: 'readonly', + ngettext: 'readonly', + }, + }, + }, + { + files: ['tiling-assistant@leleat-on-github/prefs**'], + languageOptions: { + globals: { + _: 'readonly', + C_: 'readonly', + N_: 'readonly', + }, + }, + }, +]; diff --git a/lint/eslintrc-gjs.config.js b/lint/eslintrc-gjs.config.js new file mode 100644 index 0000000..9e4dee0 --- /dev/null +++ b/lint/eslintrc-gjs.config.js @@ -0,0 +1,309 @@ +// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/14ba1c2ffad7b204eb61be3a31bddecc029b0c1a/lint/eslintrc-gjs.yml +// but adapted to flat config files + +import js from '@eslint/js'; +import jsdoc from 'eslint-plugin-jsdoc'; +import globals from 'globals'; + +export default [ + js.configs.recommended, + { + plugins: { + jsdoc, + }, + settings: { + jsdoc: { + mode: 'typescript', + }, + }, + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + ...globals.es2021, + ARGV: 'readonly', + Debugger: 'readonly', + GIRepositoryGType: 'readonly', + globalThis: 'readonly', + imports: 'readonly', + Intl: 'readonly', + log: 'readonly', + logError: 'readonly', + print: 'readonly', + printerr: 'readonly', + window: 'readonly', + TextEncoder: 'readonly', + TextDecoder: 'readonly', + console: 'readonly', + setTimeout: 'readonly', + setInterval: 'readonly', + clearTimeout: 'readonly', + clearInterval: 'readonly', + }, + }, + rules: { + 'array-bracket-newline': ['error', 'consistent'], + 'array-bracket-spacing': ['error', 'never'], + 'array-callback-return': 'error', + 'arrow-parens': ['error', 'as-needed'], + 'arrow-spacing': 'error', + 'block-scoped-var': 'error', + 'block-spacing': 'error', + 'brace-style': 'error', + 'comma-dangle': [ + 'error', + { + arrays: 'always-multiline', + objects: 'always-multiline', + functions: 'never', + }, + ], + 'comma-spacing': [ + 'error', + { + before: false, + after: true, + }, + ], + 'comma-style': ['error', 'last'], + 'computed-property-spacing': 'error', + curly: ['error', 'multi-or-nest', 'consistent'], + 'dot-location': ['error', 'property'], + 'eol-last': 'error', + eqeqeq: 'error', + 'func-call-spacing': 'error', + 'func-name-matching': 'error', + 'func-style': [ + 'error', + 'declaration', + { + allowArrowFunctions: true, + }, + ], + indent: [ + 'error', + 4, + { + ignoredNodes: [ + 'CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child', + ], + MemberExpression: 'off', + }, + ], + 'jsdoc/check-alignment': 'error', + 'jsdoc/check-param-names': 'error', + 'jsdoc/check-tag-names': 'error', + 'jsdoc/check-types': 'error', + 'jsdoc/implements-on-classes': 'error', + 'jsdoc/tag-lines': [ + 'error', + 'any', + { + startLines: 1, + }, + ], + 'jsdoc/require-jsdoc': 'error', + 'jsdoc/require-param': 'error', + 'jsdoc/require-param-description': 'error', + 'jsdoc/require-param-name': 'error', + 'jsdoc/require-param-type': 'error', + 'key-spacing': [ + 'error', + { + beforeColon: false, + afterColon: true, + }, + ], + 'keyword-spacing': [ + 'error', + { + before: true, + after: true, + }, + ], + 'linebreak-style': ['error', 'unix'], + 'lines-between-class-members': [ + 'error', + 'always', + { + exceptAfterSingleLine: true, + }, + ], + 'max-nested-callbacks': 'error', + 'max-statements-per-line': 'error', + 'new-parens': 'error', + 'no-array-constructor': 'error', + 'no-await-in-loop': 'error', + 'no-caller': 'error', + 'no-constant-condition': [ + 'error', + { + checkLoops: false, + }, + ], + 'no-div-regex': 'error', + 'no-empty': [ + 'error', + { + allowEmptyCatch: true, + }, + ], + 'no-extra-bind': 'error', + 'no-extra-parens': [ + 'error', + 'all', + { + conditionalAssign: false, + nestedBinaryExpressions: false, + returnAssign: false, + }, + ], + 'no-implicit-coercion': [ + 'error', + { + allow: ['!!'], + }, + ], + 'no-invalid-this': 'error', + 'no-iterator': 'error', + 'no-label-var': 'error', + 'no-lonely-if': 'error', + 'no-loop-func': 'error', + 'no-nested-ternary': 'error', + 'no-new-object': 'error', + 'no-new-wrappers': 'error', + 'no-octal-escape': 'error', + 'no-proto': 'error', + 'no-prototype-builtins': 'off', + 'no-restricted-globals': ['error', 'window'], + 'no-restricted-properties': [ + 'error', + { + object: 'Lang', + property: 'copyProperties', + message: 'Use Object.assign()', + }, + { + object: 'Lang', + property: 'bind', + message: 'Use arrow notation or Function.prototype.bind()', + }, + { + object: 'Lang', + property: 'Class', + message: 'Use ES6 classes', + }, + ], + 'no-restricted-syntax': [ + 'error', + { + selector: + 'MethodDefinition[key.name="_init"] > FunctionExpression[params.length=1] > BlockStatement[body.length=1] CallExpression[arguments.length=1][callee.object.type="Super"][callee.property.name="_init"] > Identifier:first-child', + message: + '_init() that only calls super._init() is unnecessary', + }, + { + selector: + 'MethodDefinition[key.name="_init"] > FunctionExpression[params.length=0] > BlockStatement[body.length=1] CallExpression[arguments.length=0][callee.object.type="Super"][callee.property.name="_init"]', + message: + '_init() that only calls super._init() is unnecessary', + }, + { + selector: + 'BinaryExpression[operator="instanceof"][right.name="Array"]', + message: 'Use Array.isArray()', + }, + ], + 'no-return-assign': 'error', + 'no-return-await': 'error', + 'no-self-compare': 'error', + 'no-shadow': 'error', + 'no-shadow-restricted-names': 'error', + 'no-spaced-func': 'error', + 'no-tabs': 'error', + 'no-template-curly-in-string': 'error', + 'no-throw-literal': 'error', + 'no-trailing-spaces': 'error', + 'no-undef-init': 'error', + 'no-unneeded-ternary': 'error', + 'no-unused-expressions': 'error', + 'no-unused-vars': [ + 'error', + { + varsIgnorePattern: '(^unused|_$)', + argsIgnorePattern: '^(unused|_)', + }, + ], + 'no-useless-call': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-concat': 'error', + 'no-useless-constructor': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'no-whitespace-before-property': 'error', + 'no-with': 'error', + 'nonblock-statement-body-position': ['error', 'below'], + 'object-curly-newline': [ + 'error', + { + consistent: true, + multiline: true, + }, + ], + 'object-curly-spacing': 'error', + 'object-shorthand': 'error', + 'operator-assignment': 'error', + 'operator-linebreak': 'error', + 'padded-blocks': ['error', 'never'], + 'prefer-numeric-literals': 'error', + 'prefer-promise-reject-errors': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'error', + quotes: [ + 'error', + 'single', + { + avoidEscape: true, + }, + ], + 'require-await': 'error', + 'rest-spread-spacing': 'error', + semi: ['error', 'always'], + 'semi-spacing': [ + 'error', + { + before: false, + after: true, + }, + ], + 'semi-style': 'error', + 'space-before-blocks': 'error', + 'space-before-function-paren': [ + 'error', + { + named: 'never', + anonymous: 'always', + asyncArrow: 'always', + }, + ], + 'space-in-parens': 'error', + 'space-infix-ops': [ + 'error', + { + int32Hint: false, + }, + ], + 'space-unary-ops': 'error', + 'spaced-comment': 'error', + 'switch-colon-spacing': 'error', + 'symbol-description': 'error', + 'template-curly-spacing': 'error', + 'template-tag-spacing': 'error', + 'unicode-bom': 'error', + 'wrap-iife': ['error', 'inside'], + 'yield-star-spacing': 'error', + yoda: 'error', + }, + }, +]; diff --git a/lint/eslintrc-shell.config.js b/lint/eslintrc-shell.config.js new file mode 100644 index 0000000..1a56bd0 --- /dev/null +++ b/lint/eslintrc-shell.config.js @@ -0,0 +1,60 @@ +// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/14ba1c2ffad7b204eb61be3a31bddecc029b0c1a/lint/eslintrc-shell.yml +// but adapted to flat config files + +export default [ + { + rules: { + camelcase: [ + 'error', + { + properties: 'never', + allow: ['^vfunc_', '^on_'], + }, + ], + 'consistent-return': 'error', + eqeqeq: ['error', 'smart'], + 'key-spacing': [ + 'error', + { + mode: 'minimum', + beforeColon: false, + afterColon: true, + }, + ], + 'prefer-arrow-callback': 'error', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-jsdoc': [ + 'error', + { + exemptEmptyFunctions: true, + publicOnly: { + esm: true, + }, + }, + ], + }, + }, + { + files: ['js/**', 'tests/shell/**'], + ignores: ['js/portalHelper/*', 'js/extensions/*'], + languageOptions: { + globals: { + global: 'readonly', + _: 'readonly', + C_: 'readonly', + N_: 'readonly', + ngettext: 'readonly', + }, + }, + }, + { + files: ['subprojects/extensions-app/js/**'], + languageOptions: { + globals: { + _: 'readonly', + C_: 'readonly', + N_: 'readonly', + }, + }, + }, +]; diff --git a/package-lock.json b/package-lock.json index 46c7450..cd8e6cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,10 @@ "license": "GPL-2.0-or-later", "devDependencies": { "@eslint/js": "^9.0.0", - "eslint": "^9.0.0" + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jsdoc": "^48.2.4", + "prettier": "^3.2.5" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -22,6 +25,23 @@ "node": ">=0.10.0" } }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.43.0.tgz", + "integrity": "sha512-Q1CnsQrytI3TlCB1IVWXWeqUIPGVEKGaE7IbVdt13Nq/3i0JESAkQQERrfiQkmlpijl+++qyqPgaS31Bvc1jRQ==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.56.5", + "@types/estree": "^1.0.5", + "@typescript-eslint/types": "^7.2.0", + "comment-parser": "1.4.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -158,6 +178,41 @@ "node": ">= 8" } }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@typescript-eslint/types": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz", + "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -219,6 +274,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -241,6 +305,18 @@ "concat-map": "0.0.1" } }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -284,6 +360,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -390,6 +475,41 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "48.2.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.2.4.tgz", + "integrity": "sha512-3ebvVgCJFy06gpmuS2ynz13uh9iFSzZ1C1dDkgcSAqVVg82zlORKMk2fvjq708pAO6bwfs5YLttknFEbaoDiGw==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.43.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.6.0", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, "node_modules/eslint-scope": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", @@ -624,6 +744,21 @@ "node": ">=0.8.19" } }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -672,6 +807,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -843,6 +987,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -914,6 +1073,18 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -935,6 +1106,28 @@ "node": ">=8" } }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "dev": true + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", diff --git a/package.json b/package.json index 8f680bb..ad7a40d 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,18 @@ "private": true, "homepage": "https://github.com/Leleat/Tiling-Assistant", "main": "tiling-assistant@leleat-on-github/extension.js", - "scripts": {}, + "scripts": { + "check:lint": "npx eslint tiling-assistant@leleat-on-github", + "check:format": "npx prettier -c tiling-assistant@leleat-on-github" + }, "author": "Leleat", "license": "GPL-2.0-or-later", "type": "module", "devDependencies": { "@eslint/js": "^9.0.0", - "eslint": "^9.0.0" + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jsdoc": "^48.2.4", + "prettier": "^3.2.5" } } diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..7fe8665 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,15 @@ +/** @type {import("prettier").Config} */ +export default { + tabWidth: 4, + singleQuote: true, + bracketSpacing: false, + experimentalTernaries: true, + overrides: [ + { + files: ['*.yml', '*.yaml'], + options: { + tabWidth: 2, + }, + }, + ], +}; diff --git a/tiling-assistant@leleat-on-github/extension.js b/tiling-assistant@leleat-on-github/extension.js index 4cf3f93..d86457a 100644 --- a/tiling-assistant@leleat-on-github/extension.js +++ b/tiling-assistant@leleat-on-github/extension.js @@ -16,8 +16,8 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { Gio, GLib, Meta } from './src/dependencies/gi.js'; -import { Extension, Main } from './src/dependencies/shell.js'; +import {Gio, GLib, Meta} from './src/dependencies/gi.js'; +import {Extension, Main} from './src/dependencies/shell.js'; import MoveHandler from './src/extension/moveHandler.js'; import ResizeHandler from './src/extension/resizeHandler.js'; @@ -25,7 +25,7 @@ import KeybindingHandler from './src/extension/keybindingHandler.js'; import LayoutsManager from './src/extension/layoutsManager.js'; import AltTabOverride from './src/extension/altTab.js'; import FocusHintManager from './src/extension/focusHint.js'; -import { Rect } from './src/extension/utility.js'; +import {Rect} from './src/extension/utility.js'; /** * 2 entry points: @@ -42,28 +42,37 @@ class SettingsOverrider { this._overrides = new Map(); this._originalSettings = new Map(); this._maybeNullValue = GLib.Variant.new_maybe( - new GLib.VariantType('b'), null); + new GLib.VariantType('b'), + null, + ); - const savedSettings = this._settings.getUserValue('overridden-settings'); + const savedSettings = this._settings.getUserValue( + 'overridden-settings', + ); this._wasOverridden = savedSettings !== null; } _maybeUpdateOverriden(schemaId, key, value) { - if (this._wasOverridden) + if (this._wasOverridden) { return undefined; + } - const savedSettings = this._settings.getValue( - 'overridden-settings').deepUnpack(); + const savedSettings = this._settings + .getValue('overridden-settings') + .deepUnpack(); const prefKey = `${schemaId}.${key}`; const oldValue = savedSettings[prefKey]; - if (value !== undefined) + if (value !== undefined) { savedSettings[prefKey] = value ?? this._maybeNullValue; - else + } else { delete savedSettings[prefKey]; + } - this._settings.setValue('overridden-settings', - new GLib.Variant('a{sv}', savedSettings)); + this._settings.setValue( + 'overridden-settings', + new GLib.Variant('a{sv}', savedSettings), + ); return oldValue; } @@ -73,31 +82,38 @@ class SettingsOverrider { const userValue = settings.get_user_value(key); const values = this._overrides.get(settings.schemaId) ?? new Map(); - if (!values.size) + if (!values.size) { this._overrides.set(settings.schemaId, values); + } values.set(key, userValue); settings.set_value(key, value); - this._maybeUpdateOverriden(settings.schemaId, key, - userValue ?? this._maybeNullValue); + this._maybeUpdateOverriden( + settings.schemaId, + key, + userValue ?? this._maybeNullValue, + ); } remove(schema, key) { const settings = this._originalSettings.get(schema); - if (!settings) + if (!settings) { return; + } const values = this._overrides.get(settings.schemaId); const value = values?.get(key); - if (value === undefined) + if (value === undefined) { return; + } - if (value) + if (value) { settings.set_value(key, value); - else + } else { settings.reset(key); + } values.delete(key); this._maybeUpdateOverriden(settings.schemaId, key, undefined); @@ -105,30 +121,34 @@ class SettingsOverrider { _clear() { if (this._wasOverridden) { - const savedSettings = this._settings.getValue( - 'overridden-settings').unpack(); + const savedSettings = this._settings + .getValue('overridden-settings') + .unpack(); Object.entries(savedSettings).forEach(([path, value]) => { const splits = path.split('.'); const key = splits.at(-1); const schemaId = splits.slice(0, -1).join('.'); - const settings = this._originalSettings.get(schemaId) ?? - new Gio.Settings({ schema_id: schemaId }); + const settings = + this._originalSettings.get(schemaId) ?? + new Gio.Settings({schema_id: schemaId}); value = value.get_variant(); - if (value.equal(this._maybeNullValue)) + if (value.equal(this._maybeNullValue)) { settings.reset(key); - else + } else { settings.set_value(key, value); + } }); } else { - this._originalSettings.forEach(settings => { + this._originalSettings.forEach((settings) => { this._overrides.get(settings.schemaId).forEach((value, key) => { - if (value) + if (value) { settings.set_value(key, value); - else + } else { settings.reset(key); + } }); }); } @@ -151,7 +171,9 @@ export default class TilingAssistantExtension extends Extension { this.settings.initialize(this.getSettings()); this._settingsOverrider = new SettingsOverrider(this.settings); - const twmModule = await import('./src/extension/tilingWindowManager.js'); + const twmModule = await import( + './src/extension/tilingWindowManager.js' + ); this._twm = twmModule.TilingWindowManager; this._twm.initialize(); @@ -164,55 +186,95 @@ export default class TilingAssistantExtension extends Extension { this._altTabOverride = new AltTabOverride(); // Disable native tiling. - this._settingsOverrider.add(new Gio.Settings({ - schema_id: 'org.gnome.mutter' - }), 'edge-tiling', new GLib.Variant('b', false)); + this._settingsOverrider.add( + new Gio.Settings({ + schema_id: 'org.gnome.mutter', + }), + 'edge-tiling', + new GLib.Variant('b', false), + ); // Disable native keybindings for Super+Up/Down/Left/Right const gnomeMutterKeybindings = new Gio.Settings({ - schema_id: 'org.gnome.mutter.keybindings' + schema_id: 'org.gnome.mutter.keybindings', }); const gnomeDesktopKeybindings = new Gio.Settings({ - schema_id: 'org.gnome.desktop.wm.keybindings' + schema_id: 'org.gnome.desktop.wm.keybindings', }); const emptyStrvVariant = new GLib.Variant('as', []); - if (gnomeDesktopKeybindings.get_strv('maximize').includes('Up') && - this.settings.getStrv('tile-maximize').includes('Up')) { - this._settingsOverrider.add(gnomeDesktopKeybindings, - 'maximize', emptyStrvVariant); + if ( + gnomeDesktopKeybindings + .get_strv('maximize') + .includes('Up') && + this.settings.getStrv('tile-maximize').includes('Up') + ) { + this._settingsOverrider.add( + gnomeDesktopKeybindings, + 'maximize', + emptyStrvVariant, + ); } - if (gnomeDesktopKeybindings.get_strv('unmaximize').includes('Down') && - this.settings.getStrv('restore-window').includes('Down')) { - this._settingsOverrider.add(gnomeDesktopKeybindings, - 'unmaximize', emptyStrvVariant); + if ( + gnomeDesktopKeybindings + .get_strv('unmaximize') + .includes('Down') && + this.settings.getStrv('restore-window').includes('Down') + ) { + this._settingsOverrider.add( + gnomeDesktopKeybindings, + 'unmaximize', + emptyStrvVariant, + ); } - if (gnomeMutterKeybindings.get_strv('toggle-tiled-left').includes('Left') && - this.settings.getStrv('tile-left-half').includes('Left')) { - this._settingsOverrider.add(gnomeMutterKeybindings, - 'toggle-tiled-left', emptyStrvVariant); + if ( + gnomeMutterKeybindings + .get_strv('toggle-tiled-left') + .includes('Left') && + this.settings.getStrv('tile-left-half').includes('Left') + ) { + this._settingsOverrider.add( + gnomeMutterKeybindings, + 'toggle-tiled-left', + emptyStrvVariant, + ); } - if (gnomeMutterKeybindings.get_strv('toggle-tiled-right').includes('Right') && - this.settings.getStrv('tile-right-half').includes('Right')) { - this._settingsOverrider.add(gnomeMutterKeybindings, - 'toggle-tiled-right', emptyStrvVariant); + if ( + gnomeMutterKeybindings + .get_strv('toggle-tiled-right') + .includes('Right') && + this.settings.getStrv('tile-right-half').includes('Right') + ) { + this._settingsOverrider.add( + gnomeMutterKeybindings, + 'toggle-tiled-right', + emptyStrvVariant, + ); } // Include tiled windows when dragging from the top panel. - this._getDraggableWindowForPosition = Main.panel._getDraggableWindowForPosition; + this._getDraggableWindowForPosition = + Main.panel._getDraggableWindowForPosition; Main.panel._getDraggableWindowForPosition = function (stageX) { const workspaceManager = global.workspace_manager; - const windows = workspaceManager.get_active_workspace().list_windows(); - const allWindowsByStacking = global.display.sort_windows_by_stacking(windows).reverse(); - - return allWindowsByStacking.find(w => { + const windows = workspaceManager + .get_active_workspace() + .list_windows(); + const allWindowsByStacking = global.display + .sort_windows_by_stacking(windows) + .reverse(); + + return allWindowsByStacking.find((w) => { const rect = w.get_frame_rect(); const workArea = w.get_work_area_current_monitor(); - return w.is_on_primary_monitor() && - w.showing_on_its_workspace() && - w.get_window_type() !== Meta.WindowType.DESKTOP && - (w.maximized_vertically || w.tiledRect?.y === workArea.y) && - stageX > rect.x && stageX < rect.x + rect.width; + return ( + w.is_on_primary_monitor() && + w.showing_on_its_workspace() && + w.get_window_type() !== Meta.WindowType.DESKTOP && + (w.maximized_vertically || w.tiledRect?.y === workArea.y) && + stageX > rect.x && + stageX < rect.x + rect.width + ); }); }; @@ -252,12 +314,16 @@ export default class TilingAssistantExtension extends Extension { this.settings = null; // Restore old functions. - Main.panel._getDraggableWindowForPosition = this._getDraggableWindowForPosition; + Main.panel._getDraggableWindowForPosition = + this._getDraggableWindowForPosition; this._getDraggableWindowForPosition = null; // Delete custom tiling properties. - const openWindows = global.display.get_tab_list(Meta.TabList.NORMAL, null); - openWindows.forEach(w => { + const openWindows = global.display.get_tab_list( + Meta.TabList.NORMAL, + null, + ); + openWindows.forEach((w) => { delete w.isTiled; delete w.tiledRect; delete w.untiledRect; @@ -269,41 +335,50 @@ export default class TilingAssistantExtension extends Extension { * properties of windows before locking the screen. */ _saveBeforeSessionLock() { - if (!Main.sessionMode.isLocked) + if (!Main.sessionMode.isLocked) { return; + } this._wasLocked = true; const userPath = GLib.get_user_config_dir(); - const parentPath = GLib.build_filenamev([userPath, '/tiling-assistant']); + const parentPath = GLib.build_filenamev([ + userPath, + '/tiling-assistant', + ]); const parent = Gio.File.new_for_path(parentPath); try { parent.make_directory_with_parents(null); } catch (e) { - if (e.code !== Gio.IOErrorEnum.EXISTS) + if (e.code !== Gio.IOErrorEnum.EXISTS) { throw e; + } } - const path = GLib.build_filenamev([parentPath, '/tiledSessionRestore2.json']); + const path = GLib.build_filenamev([ + parentPath, + '/tiledSessionRestore2.json', + ]); const file = Gio.File.new_for_path(path); try { file.create(Gio.FileCreateFlags.NONE, null); } catch (e) { - if (e.code !== Gio.IOErrorEnum.EXISTS) + if (e.code !== Gio.IOErrorEnum.EXISTS) { throw e; + } } file.replace_contents( JSON.stringify({ windows: Object.fromEntries(this._twm.getTileStates()), - tileGroups: Object.fromEntries(this._twm.getTileGroups()) + tileGroups: Object.fromEntries(this._twm.getTileGroups()), }), null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, - null + null, ); } @@ -312,52 +387,63 @@ export default class TilingAssistantExtension extends Extension { * reload them here. */ _loadAfterSessionLock() { - if (!this._wasLocked) + if (!this._wasLocked) { return; + } this._wasLocked = false; const userPath = GLib.get_user_config_dir(); - const path = GLib.build_filenamev([userPath, '/tiling-assistant/tiledSessionRestore2.json']); + const path = GLib.build_filenamev([ + userPath, + '/tiling-assistant/tiledSessionRestore2.json', + ]); const file = Gio.File.new_for_path(path); - if (!file.query_exists(null)) + if (!file.query_exists(null)) { return; + } try { file.create(Gio.FileCreateFlags.NONE, null); } catch (e) { - if (e.code !== Gio.IOErrorEnum.EXISTS) + if (e.code !== Gio.IOErrorEnum.EXISTS) { throw e; + } } const [success, contents] = file.load_contents(null); - if (!success || !contents.length) + if (!success || !contents.length) { return; + } const states = JSON.parse(new TextDecoder().decode(contents)); - const keysAsNumbers = entries => entries.map(([key, value]) => [parseInt(key), value]); - const tileGroups = new Map(keysAsNumbers(Object.entries(states.tileGroups))); - const tileStates = new Map(keysAsNumbers(Object.entries(states.windows))); + const keysAsNumbers = (entries) => + entries.map(([key, value]) => [parseInt(key), value]); + const tileGroups = new Map( + keysAsNumbers(Object.entries(states.tileGroups)), + ); + const tileStates = new Map( + keysAsNumbers(Object.entries(states.windows)), + ); const openWindows = global.display.list_all_windows(); this._twm.setTileGroups(tileGroups); this._twm.setTileStates(tileStates); - openWindows.forEach(window => { + openWindows.forEach((window) => { const tileState = tileStates.get(window.get_id()); if (tileState) { - const { isTiled, tiledRect, untiledRect } = tileState; - const jsToRect = jsRect => jsRect && new Rect( - jsRect.x, jsRect.y, jsRect.width, jsRect.height - ); + const {isTiled, tiledRect, untiledRect} = tileState; + const jsToRect = (jsRect) => + jsRect && + new Rect(jsRect.x, jsRect.y, jsRect.width, jsRect.height); window.isTiled = isTiled; window.tiledRect = jsToRect(tiledRect); window.untiledRect = jsToRect(untiledRect); } - if (tileGroups.has(window.get_id())) { const group = this._twm.getTileGroupFor(window); this._twm.updateTileGroup(group); diff --git a/tiling-assistant@leleat-on-github/metadata.json b/tiling-assistant@leleat-on-github/metadata.json index c8a2f28..6e815d7 100644 --- a/tiling-assistant@leleat-on-github/metadata.json +++ b/tiling-assistant@leleat-on-github/metadata.json @@ -1,12 +1,10 @@ { - "description": "Expand GNOME's 2 column tiling and add a Windows-snap-assist-inspired popup...", - "name": "Tiling Assistant", - "shell-version": [ - "47" - ], - "url": "https://github.com/Leleat/Tiling-Assistant", - "uuid": "tiling-assistant@leleat-on-github", - "gettext-domain": "tiling-assistant@leleat-on-github", - "settings-schema": "org.gnome.shell.extensions.tiling-assistant", - "version": 48 + "description": "Expand GNOME's 2 column tiling and add a Windows-snap-assist-inspired popup...", + "name": "Tiling Assistant", + "shell-version": ["47"], + "url": "https://github.com/Leleat/Tiling-Assistant", + "uuid": "tiling-assistant@leleat-on-github", + "gettext-domain": "tiling-assistant@leleat-on-github", + "settings-schema": "org.gnome.shell.extensions.tiling-assistant", + "version": 48 } diff --git a/tiling-assistant@leleat-on-github/prefs.js b/tiling-assistant@leleat-on-github/prefs.js index c335efd..a832325 100644 --- a/tiling-assistant@leleat-on-github/prefs.js +++ b/tiling-assistant@leleat-on-github/prefs.js @@ -1,10 +1,10 @@ -import { Gdk, Gio, GLib, Gtk } from './src/dependencies/prefs/gi.js'; -import { ExtensionPreferences } from './src/dependencies/prefs.js'; +import {Gdk, Gio, GLib, Gtk} from './src/dependencies/prefs/gi.js'; +import {ExtensionPreferences} from './src/dependencies/prefs.js'; import LayoutPrefs from './src/prefs/layoutsPrefs.js'; -import { Shortcuts } from './src/common.js'; +import {Shortcuts} from './src/common.js'; // eslint-disable-next-line no-unused-vars -import { ShortcutListener } from './src/prefs/shortcutListener.js'; +import {ShortcutListener} from './src/prefs/shortcutListener.js'; export default class Prefs extends ExtensionPreferences { fillPreferencesWindow(window) { @@ -15,7 +15,7 @@ export default class Prefs extends ExtensionPreferences { Gtk.StyleContext.add_provider_for_display( Gdk.Display.get_default(), provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, ); window.set_can_navigate_back(true); @@ -33,14 +33,24 @@ export default class Prefs extends ExtensionPreferences { // Add layouts preference page on condition of advanced setting const layoutsPage = builder.get_object('layouts'); - settings.connect('changed::enable-advanced-experimental-features', () => { - settings.get_boolean('enable-advanced-experimental-features') - ? window.add(layoutsPage) - : window.remove(layoutsPage); - }); + settings.connect( + 'changed::enable-advanced-experimental-features', + () => { + if ( + settings.get_boolean( + 'enable-advanced-experimental-features', + ) + ) { + window.add(layoutsPage); + } else { + window.remove(layoutsPage); + } + }, + ); - if (settings.get_boolean('enable-advanced-experimental-features')) + if (settings.get_boolean('enable-advanced-experimental-features')) { window.add(layoutsPage); + } // Bind settings to GUI this._bindSwitches(settings, builder); @@ -62,8 +72,8 @@ export default class Prefs extends ExtensionPreferences { } /* - * Bind GUI switches to settings. - */ + * Bind GUI switches to settings. + */ _bindSwitches(settings, builder) { const switches = [ 'enable-tiling-popup', @@ -80,18 +90,18 @@ export default class Prefs extends ExtensionPreferences { 'enable-tile-animations', 'enable-untile-animations', 'enable-hold-maximize-inverse-landscape', - 'enable-hold-maximize-inverse-portrait' + 'enable-hold-maximize-inverse-portrait', ]; - switches.forEach(key => { + switches.forEach((key) => { const widget = builder.get_object(key.replaceAll('-', '_')); settings.bind(key, widget, 'active', Gio.SettingsBindFlags.DEFAULT); }); } /* - * Bind GUI spinbuttons to settings. - */ + * Bind GUI spinbuttons to settings. + */ _bindSpinbuttons(settings, builder) { const spinButtons = [ 'window-gap', @@ -103,42 +113,53 @@ export default class Prefs extends ExtensionPreferences { 'focus-hint-outline-size', 'toggle-maximize-tophalf-timer', 'vertical-preview-area', - 'horizontal-preview-area' + 'horizontal-preview-area', ]; - spinButtons.forEach(key => { + spinButtons.forEach((key) => { const widget = builder.get_object(key.replaceAll('-', '_')); settings.bind(key, widget, 'value', Gio.SettingsBindFlags.DEFAULT); }); } - /* - * Bind GUI AdwComboRows to settings. - */ + /** + * Bind GUI AdwComboRows to settings. + * + * @param {Gio.Settings} settings + * @param {Gtk.Builder} builder + */ _bindComboRows(settings, builder) { const comboRows = [ 'move-adaptive-tiling-mod', 'move-favorite-layout-mod', - 'ignore-ta-mod' + 'ignore-ta-mod', ]; - comboRows.forEach(key => { + comboRows.forEach((key) => { const widget = builder.get_object(key.replaceAll('-', '_')); - settings.bind(key, widget, 'selected', Gio.SettingsBindFlags.DEFAULT); + settings.bind( + key, + widget, + 'selected', + Gio.SettingsBindFlags.DEFAULT, + ); widget.set_selected(settings.get_int(key)); }); } - /* - * Bind GUI color buttons to settings. - */ + /** + * Bind GUI color buttons to settings. + * + * @param {Gio.Settings} settings + * @param {Gtk.Builder} builder + */ _bindColorButtons(settings, builder) { - const switches = [ - 'focus-hint-color' - ]; + const switches = ['focus-hint-color']; - switches.forEach(key => { - const widget = builder.get_object(`${key.replaceAll('-', '_')}_button`); + switches.forEach((key) => { + const widget = builder.get_object( + `${key.replaceAll('-', '_')}_button`, + ); widget.connect('color-set', () => { settings.set_string(key, widget.get_rgba().to_string()); }); @@ -150,9 +171,12 @@ export default class Prefs extends ExtensionPreferences { }); } - /* - * Bind radioButtons to settings. - */ + /** + * Bind radioButtons to settings. + * + * @param {Gio.Settings} settings + * @param {Gtk.Builder} builder + */ _bindRadioButtons(settings, builder) { // These 'radioButtons' are basically just used as a 'fake ComboBox' with // explanations for the different options. So there is just *one* gsetting @@ -165,8 +189,8 @@ export default class Prefs extends ExtensionPreferences { 'dynamic_keybinding_window_focus_row', 'dynamic_keybinding_tiling_state_row', 'dynamic_keybinding_tiling_state_windows_row', - 'dynamic_keybinding_favorite_layout_row' - ] + 'dynamic_keybinding_favorite_layout_row', + ], }, { key: 'focus-hint', @@ -174,8 +198,8 @@ export default class Prefs extends ExtensionPreferences { 'disabled_focus_hint_row', 'animated_outline_focus_hint_row', 'animated_upscale_focus_hint_row', - 'static_outline_focus_hint_row' - ] + 'static_outline_focus_hint_row', + ], }, { key: 'default-move-mode', @@ -183,32 +207,38 @@ export default class Prefs extends ExtensionPreferences { 'edge_tiling_row', 'adaptive_tiling_row', 'favorite_layout_row', - 'ignore_ta_row' - ] - } + 'ignore_ta_row', + ], + }, ]; - radioButtons.forEach(({ key, rowNames }) => { + radioButtons.forEach(({key, rowNames}) => { const currActive = settings.get_int(key); rowNames.forEach((name, idx) => { const row = builder.get_object(name.replaceAll('-', '_')); const checkButton = row.activatable_widget; - checkButton.connect('toggled', () => settings.set_int(key, idx)); + checkButton.connect('toggled', () => + settings.set_int(key, idx), + ); // Set initial state - if (idx === currActive) + if (idx === currActive) { checkButton.activate(); + } }); }); } - /* - * Bind keybinding widgets to settings. - */ + /** + * Bind keybinding widgets to settings. + * + * @param {Gio.Settings} settings + * @param {Gtk.Builder} builder + */ _bindKeybindings(settings, builder) { const shortcuts = Shortcuts.getAllKeys(); - shortcuts.forEach(key => { + shortcuts.forEach((key) => { const shortcut = builder.get_object(key.replaceAll('-', '_')); shortcut.initialize(key, settings); }); @@ -220,29 +250,43 @@ export default class Prefs extends ExtensionPreferences { * discoverable through the GUI and need to first be set with the gsetting. * The normal rows should have the id of: GSETTING_WITH_UNDERSCORES_row. * ShortcutListeners have the format of GSETTING_WITH_UNDERSCORES. + * + * @param {Gio.Settings} settings + * @param {Gtk.Builder} builder */ _setDeprecatedSettings(settings, builder) { // Keybindings - ['toggle-tiling-popup', 'auto-tile'].forEach(s => { - const isNonDefault = settings.get_strv(s)[0] !== settings.get_default_value(s).get_strv()[0]; - builder.get_object(s.replaceAll('-', '_')).set_visible(isNonDefault); + ['toggle-tiling-popup', 'auto-tile'].forEach((s) => { + const isNonDefault = + settings.get_strv(s)[0] !== + settings.get_default_value(s).get_strv()[0]; + builder + .get_object(s.replaceAll('-', '_')) + .set_visible(isNonDefault); }); // Switches - ['tilegroups-in-app-switcher'].forEach(s => { - const isNonDefault = settings.get_boolean(s) !== settings.get_default_value(s).get_boolean(); - builder.get_object(s.replaceAll('-', '_')).set_visible(isNonDefault); + ['tilegroups-in-app-switcher'].forEach((s) => { + const isNonDefault = + settings.get_boolean(s) !== + settings.get_default_value(s).get_boolean(); + builder + .get_object(s.replaceAll('-', '_')) + .set_visible(isNonDefault); }); } + /** + * + * @param {Adw.fillPreferencesWindow} window - The preferences window + * @param {Gio.Settings} settings - The settings object + * @param {Gtk.Builder} builder - The builder object + */ _addHeaderBarInfoButton(window, settings, builder) { // Add headerBar button for menu // TODO: is this a 'reliable' method to access the headerbar? const page = builder.get_object('general'); - const gtkStack = page - .get_parent() - .get_parent() - .get_parent(); + const gtkStack = page.get_parent().get_parent().get_parent(); const adwHeaderBar = gtkStack .get_next_sibling() .get_first_child() @@ -255,47 +299,100 @@ export default class Prefs extends ExtensionPreferences { const actionGroup = new Gio.SimpleActionGroup(); window.insert_action_group('prefs', actionGroup); - const bugReportAction = new Gio.SimpleAction({ name: 'open-bug-report' }); - bugReportAction.connect('activate', this._openBugReport.bind(this, window)); + const bugReportAction = new Gio.SimpleAction({name: 'open-bug-report'}); + bugReportAction.connect( + 'activate', + this._openBugReport.bind(this, window), + ); actionGroup.add_action(bugReportAction); - const userGuideAction = new Gio.SimpleAction({ name: 'open-user-guide' }); - userGuideAction.connect('activate', this._openUserGuide.bind(this, window)); + const userGuideAction = new Gio.SimpleAction({name: 'open-user-guide'}); + userGuideAction.connect( + 'activate', + this._openUserGuide.bind(this, window), + ); actionGroup.add_action(userGuideAction); - const changelogAction = new Gio.SimpleAction({ name: 'open-changelog' }); - changelogAction.connect('activate', this._openChangelog.bind(this, window)); + const changelogAction = new Gio.SimpleAction({name: 'open-changelog'}); + changelogAction.connect( + 'activate', + this._openChangelog.bind(this, window), + ); actionGroup.add_action(changelogAction); - const licenseAction = new Gio.SimpleAction({ name: 'open-license' }); + const licenseAction = new Gio.SimpleAction({name: 'open-license'}); licenseAction.connect('activate', this._openLicense.bind(this, window)); actionGroup.add_action(licenseAction); - const hiddenSettingsAction = new Gio.SimpleAction({ name: 'open-hidden-settings' }); - hiddenSettingsAction.connect('activate', this._openHiddenSettings.bind(this, window, builder)); + const hiddenSettingsAction = new Gio.SimpleAction({ + name: 'open-hidden-settings', + }); + hiddenSettingsAction.connect( + 'activate', + this._openHiddenSettings.bind(this, window, builder), + ); actionGroup.add_action(hiddenSettingsAction); // Button to return to main settings page - const returnButton = builder.get_object('hidden_settings_return_button'); + const returnButton = builder.get_object( + 'hidden_settings_return_button', + ); returnButton.connect('clicked', () => window.close_subpage()); } + /** + * + * @param {Adw.PreferencesWindow} window + */ _openBugReport(window) { - Gtk.show_uri(window, 'https://github.com/Leleat/Tiling-Assistant/issues', Gdk.CURRENT_TIME); + Gtk.show_uri( + window, + 'https://github.com/Leleat/Tiling-Assistant/issues', + Gdk.CURRENT_TIME, + ); } + /** + * + * @param {Adw.PreferencesWindow} window + */ _openUserGuide(window) { - Gtk.show_uri(window, 'https://github.com/Leleat/Tiling-Assistant/wiki', Gdk.CURRENT_TIME); + Gtk.show_uri( + window, + 'https://github.com/Leleat/Tiling-Assistant/wiki', + Gdk.CURRENT_TIME, + ); } + /** + * + * @param {Adw.PreferencesWindow} window + */ _openChangelog(window) { - Gtk.show_uri(window, 'https://github.com/Leleat/Tiling-Assistant/blob/main/CHANGELOG.md', Gdk.CURRENT_TIME); + Gtk.show_uri( + window, + 'https://github.com/Leleat/Tiling-Assistant/blob/main/CHANGELOG.md', + Gdk.CURRENT_TIME, + ); } + /** + * + * @param {Adw.PreferencesWindow} window + */ _openLicense(window) { - Gtk.show_uri(window, 'https://github.com/Leleat/Tiling-Assistant/blob/main/LICENSE', Gdk.CURRENT_TIME); + Gtk.show_uri( + window, + 'https://github.com/Leleat/Tiling-Assistant/blob/main/LICENSE', + Gdk.CURRENT_TIME, + ); } + /** + * + * @param {Adw.PreferencesWindow} window + * @param {Gtk.Builder} builder + */ _openHiddenSettings(window, builder) { const hiddenSettingsPage = builder.get_object('hidden_settings'); window.present_subpage(hiddenSettingsPage); diff --git a/tiling-assistant@leleat-on-github/src/common.js b/tiling-assistant@leleat-on-github/src/common.js index 9d24013..c5ab497 100644 --- a/tiling-assistant@leleat-on-github/src/common.js +++ b/tiling-assistant@leleat-on-github/src/common.js @@ -140,7 +140,7 @@ export class Shortcuts { 'tile-bottomleft-quarter-ignore-ta', 'tile-bottomright-quarter-ignore-ta', 'debugging-show-tiled-rects', - 'debugging-free-rects' + 'debugging-free-rects', ]; } } @@ -158,7 +158,7 @@ export const FocusHint = Object.freeze({ DISABLED: 0, ANIMATED_OUTLINE: 1, ANIMATED_UPSCALE: 2, - STATIC_OUTLINE: 3 + STATIC_OUTLINE: 3, }); export class MoveModes { @@ -182,14 +182,18 @@ export class Direction { static opposite(dir) { let opposite = 0; - if (dir & this.N) + if (dir & this.N) { opposite |= this.S; - if (dir & this.S) + } + if (dir & this.S) { opposite |= this.N; - if (dir & this.W) + } + if (dir & this.W) { opposite |= this.E; - if (dir & this.E) + } + if (dir & this.E) { opposite |= this.W; + } return opposite; } @@ -251,9 +255,9 @@ export class Layout { * @returns {LayoutItem[]} */ getItems(filterOutEmptyRects = true) { - return filterOutEmptyRects - ? this._items.filter(i => Object.keys(i.rect).length === 4) - : this._items; + return filterOutEmptyRects ? + this._items.filter((i) => Object.keys(i.rect).length === 4) + : this._items; } /** @@ -268,9 +272,9 @@ export class Layout { * @returns {number} */ getItemCount(filterOutEmptyRects = false) { - return filterOutEmptyRects - ? this.getItems().length - : this._items.length; + return filterOutEmptyRects ? + this.getItems().length + : this._items.length; } /** @@ -278,30 +282,50 @@ export class Layout { * a potential error message. */ validate() { - const rects = this.getItems().map(i => i.rect); - if (!rects.length) + const rects = this.getItems().map((i) => i.rect); + if (!rects.length) { return [false, 'No valid rectangles defined.', -1]; + } const getOverlapArea = (r1, r2) => { - return Math.max(0, Math.min(r1.x + r1.width, r2.x + r2.width) - Math.max(r1.x, r2.x)) * - Math.max(0, Math.min(r1.y + r1.height, r2.y + r2.height) - Math.max(r1.y, r2.y)); + return ( + Math.max( + 0, + Math.min(r1.x + r1.width, r2.x + r2.width) - + Math.max(r1.x, r2.x), + ) * + Math.max( + 0, + Math.min(r1.y + r1.height, r2.y + r2.height) - + Math.max(r1.y, r2.y), + ) + ); }; for (let i = 0; i < rects.length; i++) { const rect = rects[i]; - if (rect.width <= 0 || rect.width > 1) + if (rect.width <= 0 || rect.width > 1) { return [false, `Rectangle ${i} has an invalid width.`, i]; + } - if (rect.height <= 0 || rect.height > 1) + if (rect.height <= 0 || rect.height > 1) { return [false, `Rectangle ${i} has an invalid height.`, i]; + } - if (rect.x < 0 || rect.y < 0 || rect.x + rect.width > 1 || rect.y + rect.height > 1) + if ( + rect.x < 0 || + rect.y < 0 || + rect.x + rect.width > 1 || + rect.y + rect.height > 1 + ) { return [false, `Rectangle ${i} extends beyond the screen.`, i]; + } for (let j = i + 1; j < rects.length; j++) { - if (getOverlapArea(rect, rects[j]) !== 0) + if (getOverlapArea(rect, rects[j]) !== 0) { return [false, `Rectangles ${i} and ${j} overlap.`, j]; + } } } diff --git a/tiling-assistant@leleat-on-github/src/dependencies/gi.js b/tiling-assistant@leleat-on-github/src/dependencies/gi.js index 937881a..9f6783b 100644 --- a/tiling-assistant@leleat-on-github/src/dependencies/gi.js +++ b/tiling-assistant@leleat-on-github/src/dependencies/gi.js @@ -1,9 +1,9 @@ -export { default as Atk } from 'gi://Atk'; -export { default as Clutter } from 'gi://Clutter'; -export { default as Gio } from 'gi://Gio'; -export { default as GLib } from 'gi://GLib'; -export { default as GObject } from 'gi://GObject'; -export { default as Meta } from 'gi://Meta'; -export { default as Mtk } from 'gi://Mtk'; -export { default as Shell } from 'gi://Shell'; -export { default as St } from 'gi://St'; +export {default as Atk} from 'gi://Atk'; +export {default as Clutter} from 'gi://Clutter'; +export {default as Gio} from 'gi://Gio'; +export {default as GLib} from 'gi://GLib'; +export {default as GObject} from 'gi://GObject'; +export {default as Meta} from 'gi://Meta'; +export {default as Mtk} from 'gi://Mtk'; +export {default as Shell} from 'gi://Shell'; +export {default as St} from 'gi://St'; diff --git a/tiling-assistant@leleat-on-github/src/dependencies/prefs.js b/tiling-assistant@leleat-on-github/src/dependencies/prefs.js index 30f203e..d273388 100644 --- a/tiling-assistant@leleat-on-github/src/dependencies/prefs.js +++ b/tiling-assistant@leleat-on-github/src/dependencies/prefs.js @@ -1,4 +1,4 @@ export { ExtensionPreferences, - gettext as _ + gettext as _, } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; diff --git a/tiling-assistant@leleat-on-github/src/dependencies/prefs/gi.js b/tiling-assistant@leleat-on-github/src/dependencies/prefs/gi.js index 974c9c2..8d96872 100644 --- a/tiling-assistant@leleat-on-github/src/dependencies/prefs/gi.js +++ b/tiling-assistant@leleat-on-github/src/dependencies/prefs/gi.js @@ -1,6 +1,6 @@ -export { default as Adw } from 'gi://Adw'; -export { default as Gdk } from 'gi://Gdk'; -export { default as Gio } from 'gi://Gio'; -export { default as GLib } from 'gi://GLib'; -export { default as GObject } from 'gi://GObject'; -export { default as Gtk } from 'gi://Gtk'; +export {default as Adw} from 'gi://Adw'; +export {default as Gdk} from 'gi://Gdk'; +export {default as Gio} from 'gi://Gio'; +export {default as GLib} from 'gi://GLib'; +export {default as GObject} from 'gi://GObject'; +export {default as Gtk} from 'gi://Gtk'; diff --git a/tiling-assistant@leleat-on-github/src/dependencies/shell.js b/tiling-assistant@leleat-on-github/src/dependencies/shell.js index 507806f..095e618 100644 --- a/tiling-assistant@leleat-on-github/src/dependencies/shell.js +++ b/tiling-assistant@leleat-on-github/src/dependencies/shell.js @@ -1,6 +1,6 @@ export { Extension, - gettext as _ + gettext as _, } from 'resource:///org/gnome/shell/extensions/extension.js'; export * as AppFavorites from 'resource:///org/gnome/shell/ui/appFavorites.js'; diff --git a/tiling-assistant@leleat-on-github/src/dependencies/unexported/altTab.js b/tiling-assistant@leleat-on-github/src/dependencies/unexported/altTab.js index ba8e793..a0282f9 100644 --- a/tiling-assistant@leleat-on-github/src/dependencies/unexported/altTab.js +++ b/tiling-assistant@leleat-on-github/src/dependencies/unexported/altTab.js @@ -1,16 +1,25 @@ -import { Meta } from '../gi.js'; +import {Meta} from '../gi.js'; export const baseIconSizes = [96, 64, 48, 32, 22]; export const APP_ICON_HOVER_TIMEOUT = 200; // milliseconds +/** + * + * @param {Meta.Workspace} workspace + */ export function getWindows(workspace) { // We ignore skip-taskbar windows in switchers, but if they are attached // to their parent, their position in the MRU list may be more appropriate // than the parent; so start with the complete list ... - let windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, workspace); + let windows = global.display.get_tab_list( + Meta.TabList.NORMAL_ALL, + workspace, + ); // ... map windows to their parent where appropriate ... - return windows.map(w => { - return w.is_attached_dialog() ? w.get_transient_for() : w; - // ... and filter out skip-taskbar windows and duplicates - }).filter((w, i, a) => !w.skip_taskbar && a.indexOf(w) === i); + return windows + .map((w) => { + return w.is_attached_dialog() ? w.get_transient_for() : w; + // ... and filter out skip-taskbar windows and duplicates + }) + .filter((w, i, a) => !w.skip_taskbar && a.indexOf(w) === i); } diff --git a/tiling-assistant@leleat-on-github/src/extension/altTab.js b/tiling-assistant@leleat-on-github/src/extension/altTab.js index 2561863..11c509c 100644 --- a/tiling-assistant@leleat-on-github/src/extension/altTab.js +++ b/tiling-assistant@leleat-on-github/src/extension/altTab.js @@ -6,36 +6,36 @@ import { GObject, Meta, Shell, - St + St, } from '../dependencies/gi.js'; -import { - AltTab, - Extension, - Main, - SwitcherPopup -} from '../dependencies/shell.js'; +import {AltTab, Extension, Main, SwitcherPopup} from '../dependencies/shell.js'; import { baseIconSizes, - APP_ICON_HOVER_TIMEOUT + APP_ICON_HOVER_TIMEOUT, } from '../dependencies/unexported/altTab.js'; -import { Settings } from '../common.js'; -import { TilingWindowManager as Twm } from './tilingWindowManager.js'; +import {Settings} from '../common.js'; +import {TilingWindowManager as Twm} from './tilingWindowManager.js'; /** * Optionally, override GNOME's altTab / appSwitcher to group tileGroups */ export default class AltTabOverride { constructor() { - if (Settings.getBoolean('tilegroups-in-app-switcher')) + if (Settings.getBoolean('tilegroups-in-app-switcher')) { this._overrideNativeAppSwitcher(); + } - this._settingsId = Settings.changed('tilegroups-in-app-switcher', () => { - if (Settings.getBoolean('tilegroups-in-app-switcher')) - this._overrideNativeAppSwitcher(); - else - this._restoreNativeAppSwitcher(); - }); + this._settingsId = Settings.changed( + 'tilegroups-in-app-switcher', + () => { + if (Settings.getBoolean('tilegroups-in-app-switcher')) { + this._overrideNativeAppSwitcher(); + } else { + this._restoreNativeAppSwitcher(); + } + }, + ); } destroy() { @@ -47,7 +47,7 @@ export default class AltTabOverride { Main.wm.setCustomKeybindingHandler( 'switch-applications', Shell.ActionMode.NORMAL, - this._startSwitcher.bind(this) + this._startSwitcher.bind(this), ); } @@ -55,7 +55,7 @@ export default class AltTabOverride { Main.wm.setCustomKeybindingHandler( 'switch-applications', Shell.ActionMode.NORMAL, - Main.wm._startSwitcher.bind(Main.wm) + Main.wm._startSwitcher.bind(Main.wm), ); } @@ -67,420 +67,524 @@ export default class AltTabOverride { * @param {*} binding - */ _startSwitcher(display, window, binding) { - if (Main.wm._workspaceSwitcherPopup !== null) + if (Main.wm._workspaceSwitcherPopup !== null) { Main.wm._workspaceSwitcherPopup.destroy(); + } const tabPopup = new TilingAppSwitcherPopup(); - if (!tabPopup.show(binding.is_reversed(), binding.get_name(), binding.get_mask())) + if ( + !tabPopup.show( + binding.is_reversed(), + binding.get_name(), + binding.get_mask(), + ) + ) { tabPopup.destroy(); + } } } export const TilingAppSwitcherPopup = GObject.registerClass( -class TilingAppSwitcherPopup extends AltTab.AppSwitcherPopup { - _init() { - SwitcherPopup.SwitcherPopup.prototype._init.call(this); - - const settings = new Gio.Settings({ schema_id: 'org.gnome.shell.app-switcher' }); - const workspace = settings.get_boolean('current-workspace-only') - ? global.workspace_manager.get_active_workspace() - : null; - const windows = global.display.get_tab_list(Meta.TabList.NORMAL, workspace); - - this._switcherList = new TilingAppSwitcher(this, windows); - this._items = this._switcherList.icons; - } + class TilingAppSwitcherPopup extends AltTab.AppSwitcherPopup { + _init() { + SwitcherPopup.SwitcherPopup.prototype._init.call(this); - // Called when closing an entire app / tileGroup - _quitApplication(index) { - const item = this._items[index]; - if (!item) - return; - - item.cachedWindows.forEach(w => w.delete(global.get_current_time())); - item.cachedWindows = []; - this._switcherList._removeIcon(item); - } + const settings = new Gio.Settings({ + schema_id: 'org.gnome.shell.app-switcher', + }); + const workspace = + settings.get_boolean('current-workspace-only') ? + global.workspace_manager.get_active_workspace() + : null; + const windows = global.display.get_tab_list( + Meta.TabList.NORMAL, + workspace, + ); + + this._switcherList = new TilingAppSwitcher(this, windows); + this._items = this._switcherList.icons; + } - // Called when closing a window with the thumbnail switcher - // meaning that .cachedWindow of an item was updated via signals - _windowRemoved(thumbnailSwitcher, n) { - const item = this._items[this._selectedIndex]; - if (!item) - return; + // Called when closing an entire app / tileGroup + _quitApplication(index) { + const item = this._items[index]; + if (!item) { + return; + } - if (item.cachedWindows.length) { - const newIndex = Math.min(n, item.cachedWindows.length - 1); - this._select(this._selectedIndex, newIndex); + item.cachedWindows.forEach((w) => + w.delete(global.get_current_time()), + ); + item.cachedWindows = []; + this._switcherList._removeIcon(item); } - item.updateAppIcons(); - } -}); + // Called when closing a window with the thumbnail switcher + // meaning that .cachedWindow of an item was updated via signals + _windowRemoved(thumbnailSwitcher, n) { + const item = this._items[this._selectedIndex]; + if (!item) { + return; + } -export const TilingAppSwitcher = GObject.registerClass( -class TilingAppSwitcher extends SwitcherPopup.SwitcherList { - _init(altTabPopup, windows) { - // Don't make the SwitcherButtons squares since 1 SwitcherButton - // may contain multiple AppIcons for a tileGroup. - super._init(false); + if (item.cachedWindows.length) { + const newIndex = Math.min(n, item.cachedWindows.length - 1); + this._select(this._selectedIndex, newIndex); + } - this.icons = []; - this._arrows = []; - this._apps = []; - this._altTabPopup = altTabPopup; - this._delayedHighlighted = -1; - this._mouseTimeOutId = 0; + item.updateAppIcons(); + } + }, +); - const winTracker = Shell.WindowTracker.get_default(); - let groupedWindows; +export const TilingAppSwitcher = GObject.registerClass( + class TilingAppSwitcher extends SwitcherPopup.SwitcherList { + _init(altTabPopup, windows) { + // Don't make the SwitcherButtons squares since 1 SwitcherButton + // may contain multiple AppIcons for a tileGroup. + super._init(false); + + this.icons = []; + this._arrows = []; + this._apps = []; + this._altTabPopup = altTabPopup; + this._delayedHighlighted = -1; + this._mouseTimeOutId = 0; - // Group windows based on their tileGroup, if tileGroup.length > 1. - // Otherwise group them based on their respective apps. - if (Settings.getBoolean('tilegroups-in-app-switcher')) { - groupedWindows = windows.reduce((allGroups, w) => { - for (const group of allGroups) { - if (w.isTiled && Twm.getTileGroupFor(w).length > 1) { - if (Twm.getTileGroupFor(w).includes(group[0])) { + const winTracker = Shell.WindowTracker.get_default(); + let groupedWindows; + + // Group windows based on their tileGroup, if tileGroup.length > 1. + // Otherwise group them based on their respective apps. + if (Settings.getBoolean('tilegroups-in-app-switcher')) { + groupedWindows = windows.reduce((allGroups, w) => { + for (const group of allGroups) { + if (w.isTiled && Twm.getTileGroupFor(w).length > 1) { + if (Twm.getTileGroupFor(w).includes(group[0])) { + group.push(w); + return allGroups; + } + } else if ( + (!group[0].isTiled || + (group[0].isTiled && + Twm.getTileGroupFor(group[0]).length <= + 1)) && + winTracker.get_window_app(group[0]) === + winTracker.get_window_app(w) + ) { group.push(w); return allGroups; } - } else if ((!group[0].isTiled || group[0].isTiled && Twm.getTileGroupFor(group[0]).length <= 1) && - winTracker.get_window_app(group[0]) === winTracker.get_window_app(w)) { - group.push(w); - return allGroups; } - } - const newGroup = [w]; - allGroups.push(newGroup); - return allGroups; - }, []); - - // Group windows based on apps - } else { - groupedWindows = windows.reduce((allGroups, w) => { - for (const group of allGroups) { - if (winTracker.get_window_app(group[0]) === winTracker.get_window_app(w)) { - group.push(w); - return allGroups; + const newGroup = [w]; + allGroups.push(newGroup); + return allGroups; + }, []); + + // Group windows based on apps + } else { + groupedWindows = windows.reduce((allGroups, w) => { + for (const group of allGroups) { + if ( + winTracker.get_window_app(group[0]) === + winTracker.get_window_app(w) + ) { + group.push(w); + return allGroups; + } } - } - - const newGroup = [w]; - allGroups.push(newGroup); - return allGroups; - }, []); - } - // Construct the AppIcons and add them to the popup. - groupedWindows.forEach(group => { - const item = new AppSwitcherItem(group); - item.connect('all-icons-removed', () => this._removeIcon(item)); - this._addIcon(item); - }); - - // Listen for the app stop state in case the app got closed outside - // of the app switcher along with closing via the app switcher - const allApps = windows.map(w => winTracker.get_window_app(w)); - this._apps = [...new Set(allApps)]; - this._stateChangedIds = this._apps.map(app => app.connect('notify::state', () => { - if (app.state !== Shell.AppState.RUNNING) - this.icons.forEach(item => item.removeApp(app)); - })); - - this.connect('destroy', this._onDestroy.bind(this)); - } + const newGroup = [w]; + allGroups.push(newGroup); + return allGroups; + }, []); + } - _onDestroy() { - if (this._mouseTimeOutId !== 0) - GLib.source_remove(this._mouseTimeOutId); + // Construct the AppIcons and add them to the popup. + groupedWindows.forEach((group) => { + const item = new AppSwitcherItem(group); + item.connect('all-icons-removed', () => this._removeIcon(item)); + this._addIcon(item); + }); - this._stateChangedIds?.forEach((id, index) => this._apps[index].disconnect(id)); - this._stateChangedIds = []; - this._apps = []; - } + // Listen for the app stop state in case the app got closed outside + // of the app switcher along with closing via the app switcher + const allApps = windows.map((w) => winTracker.get_window_app(w)); + this._apps = [...new Set(allApps)]; + this._stateChangedIds = this._apps.map((app) => + app.connect('notify::state', () => { + if (app.state !== Shell.AppState.RUNNING) { + this.icons.forEach((item) => item.removeApp(app)); + } + }), + ); - _setIconSize() { - let j = 0; - while (this._items.length > 1 && this._items[j].style_class !== 'item-box') - j++; - - let themeNode = this._items[j].get_theme_node(); - this._list.ensure_style(); - - let iconPadding = themeNode.get_horizontal_padding(); - let iconBorder = themeNode.get_border_width(St.Side.LEFT) + themeNode.get_border_width(St.Side.RIGHT); - let [, labelNaturalHeight] = this.icons[j].label.get_preferred_height(-1); - let iconSpacing = labelNaturalHeight + iconPadding + iconBorder; - let totalSpacing = this._list.spacing * (this._items.length - 1); - - // We just assume the whole screen here due to weirdness happening with the passed width - let primary = Main.layoutManager.primaryMonitor; - let parentPadding = this.get_parent().get_theme_node().get_horizontal_padding(); - let availWidth = primary.width - parentPadding - this.get_theme_node().get_horizontal_padding(); - - let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; - let iconSizes = baseIconSizes.map(s => s * scaleFactor); - let iconSize = baseIconSizes[0]; - - if (this._items.length > 1) { - for (let i = 0; i < baseIconSizes.length; i++) { - iconSize = baseIconSizes[i]; - let height = iconSizes[i] + iconSpacing; - let w = height * this._items.length + totalSpacing; - if (w <= availWidth) - break; - } + this.connect('destroy', this._onDestroy.bind(this)); } - this._iconSize = iconSize; + _onDestroy() { + if (this._mouseTimeOutId !== 0) { + GLib.source_remove(this._mouseTimeOutId); + } - for (let i = 0; i < this.icons.length; i++) { - // eslint-disable-next-line eqeqeq - if (this.icons[i].icon != null) - break; - this.icons[i].set_size(iconSize); + this._stateChangedIds?.forEach((id, index) => + this._apps[index].disconnect(id), + ); + this._stateChangedIds = []; + this._apps = []; } - } - vfunc_get_preferred_height(forWidth) { - if (!this._iconSize) - this._setIconSize(); - - return super.vfunc_get_preferred_height(forWidth); - } - - vfunc_allocate(box) { - // Allocate the main list items - super.vfunc_allocate(box); + _setIconSize() { + let j = 0; + while ( + this._items.length > 1 && + this._items[j].style_class !== 'item-box' + ) { + j++; + } - let contentBox = this.get_theme_node().get_content_box(box); + let themeNode = this._items[j].get_theme_node(); + this._list.ensure_style(); + + let iconPadding = themeNode.get_horizontal_padding(); + let iconBorder = + themeNode.get_border_width(St.Side.LEFT) + + themeNode.get_border_width(St.Side.RIGHT); + let [, labelNaturalHeight] = + this.icons[j].label.get_preferred_height(-1); + let iconSpacing = labelNaturalHeight + iconPadding + iconBorder; + let totalSpacing = this._list.spacing * (this._items.length - 1); + + // We just assume the whole screen here due to weirdness happening with the passed width + let primary = Main.layoutManager.primaryMonitor; + let parentPadding = this.get_parent() + .get_theme_node() + .get_horizontal_padding(); + let availWidth = + primary.width - + parentPadding - + this.get_theme_node().get_horizontal_padding(); + + let scaleFactor = St.ThemeContext.get_for_stage( + global.stage, + ).scale_factor; + let iconSizes = baseIconSizes.map((s) => s * scaleFactor); + let iconSize = baseIconSizes[0]; + + if (this._items.length > 1) { + for (let i = 0; i < baseIconSizes.length; i++) { + iconSize = baseIconSizes[i]; + let height = iconSizes[i] + iconSpacing; + let w = height * this._items.length + totalSpacing; + if (w <= availWidth) { + break; + } + } + } - let arrowHeight = Math.floor(this.get_theme_node().get_padding(St.Side.BOTTOM) / 3); - let arrowWidth = arrowHeight * 2; + this._iconSize = iconSize; - // Now allocate each arrow underneath its item - let childBox = new Clutter.ActorBox(); - for (let i = 0; i < this._items.length; i++) { - let itemBox = this._items[i].allocation; - childBox.x1 = contentBox.x1 + Math.floor(itemBox.x1 + (itemBox.x2 - itemBox.x1 - arrowWidth) / 2); - childBox.x2 = childBox.x1 + arrowWidth; - childBox.y1 = contentBox.y1 + itemBox.y2 + arrowHeight; - childBox.y2 = childBox.y1 + arrowHeight; - this._arrows[i].allocate(childBox); + for (let i = 0; i < this.icons.length; i++) { + if (this.icons[i].icon != null) { + break; + } + this.icons[i].set_size(iconSize); + } } - } - // We override SwitcherList's _onItemMotion method to delay - // activation when the thumbnail list is open - _onItemMotion(item) { - if (item === this._items[this._highlighted] || - item === this._items[this._delayedHighlighted]) - return Clutter.EVENT_PROPAGATE; - - const index = this._items.indexOf(item); + vfunc_get_preferred_height(forWidth) { + if (!this._iconSize) { + this._setIconSize(); + } - if (this._mouseTimeOutId !== 0) { - GLib.source_remove(this._mouseTimeOutId); - this._delayedHighlighted = -1; - this._mouseTimeOutId = 0; + return super.vfunc_get_preferred_height(forWidth); } - if (this._altTabPopup.thumbnailsVisible) { - this._delayedHighlighted = index; - this._mouseTimeOutId = GLib.timeout_add( - GLib.PRIORITY_DEFAULT, - APP_ICON_HOVER_TIMEOUT, - () => { - this._enterItem(index); - this._delayedHighlighted = -1; - this._mouseTimeOutId = 0; - return GLib.SOURCE_REMOVE; - }); - GLib.Source.set_name_by_id(this._mouseTimeOutId, '[gnome-shell] this._enterItem'); - } else { - this._itemEntered(index); + vfunc_allocate(box) { + // Allocate the main list items + super.vfunc_allocate(box); + + let contentBox = this.get_theme_node().get_content_box(box); + + let arrowHeight = Math.floor( + this.get_theme_node().get_padding(St.Side.BOTTOM) / 3, + ); + let arrowWidth = arrowHeight * 2; + + // Now allocate each arrow underneath its item + let childBox = new Clutter.ActorBox(); + for (let i = 0; i < this._items.length; i++) { + let itemBox = this._items[i].allocation; + childBox.x1 = + contentBox.x1 + + Math.floor( + itemBox.x1 + (itemBox.x2 - itemBox.x1 - arrowWidth) / 2, + ); + childBox.x2 = childBox.x1 + arrowWidth; + childBox.y1 = contentBox.y1 + itemBox.y2 + arrowHeight; + childBox.y2 = childBox.y1 + arrowHeight; + this._arrows[i].allocate(childBox); + } } - return Clutter.EVENT_PROPAGATE; - } + // We override SwitcherList's _onItemMotion method to delay + // activation when the thumbnail list is open + _onItemMotion(item) { + if ( + item === this._items[this._highlighted] || + item === this._items[this._delayedHighlighted] + ) { + return Clutter.EVENT_PROPAGATE; + } - _enterItem(index) { - let [x, y] = global.get_pointer(); - let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y); - if (this._items[index].contains(pickedActor)) - this._itemEntered(index); - } + const index = this._items.indexOf(item); - // We override SwitcherList's highlight() method to also deal with - // the AppSwitcher->ThumbnailSwitcher arrows. Apps with only 1 window - // will hide their arrows by default, but show them when their - // thumbnails are visible (ie, when the app icon is supposed to be - // in justOutline mode). Apps with multiple windows will normally - // show a dim arrow, but show a bright arrow when they are - // highlighted. - highlight(n, justOutline) { - if (this.icons[this._highlighted]) { - if (this.icons[this._highlighted].cachedWindows.length === 1) - this._arrows[this._highlighted].hide(); - else - this._arrows[this._highlighted].remove_style_pseudo_class('highlighted'); - } + if (this._mouseTimeOutId !== 0) { + GLib.source_remove(this._mouseTimeOutId); + this._delayedHighlighted = -1; + this._mouseTimeOutId = 0; + } - super.highlight(n, justOutline); + if (this._altTabPopup.thumbnailsVisible) { + this._delayedHighlighted = index; + this._mouseTimeOutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + APP_ICON_HOVER_TIMEOUT, + () => { + this._enterItem(index); + this._delayedHighlighted = -1; + this._mouseTimeOutId = 0; + return GLib.SOURCE_REMOVE; + }, + ); + GLib.Source.set_name_by_id( + this._mouseTimeOutId, + '[gnome-shell] this._enterItem', + ); + } else { + this._itemEntered(index); + } - if (this._highlighted !== -1) { - if (justOutline && this.icons[this._highlighted].cachedWindows.length === 1) - this._arrows[this._highlighted].show(); - else - this._arrows[this._highlighted].add_style_pseudo_class('highlighted'); + return Clutter.EVENT_PROPAGATE; } - } - _addIcon(appIcon) { - this.icons.push(appIcon); - let item = this.addItem(appIcon, appIcon.label); + _enterItem(index) { + let [x, y] = global.get_pointer(); + let pickedActor = global.stage.get_actor_at_pos( + Clutter.PickMode.ALL, + x, + y, + ); + if (this._items[index].contains(pickedActor)) { + this._itemEntered(index); + } + } - appIcon.app.connectObject('notify::state', app => { - if (app.state !== Shell.AppState.RUNNING) - this._removeIcon(app); - }, this); + // We override SwitcherList's highlight() method to also deal with + // the AppSwitcher->ThumbnailSwitcher arrows. Apps with only 1 window + // will hide their arrows by default, but show them when their + // thumbnails are visible (ie, when the app icon is supposed to be + // in justOutline mode). Apps with multiple windows will normally + // show a dim arrow, but show a bright arrow when they are + // highlighted. + highlight(n, justOutline) { + if (this.icons[this._highlighted]) { + if (this.icons[this._highlighted].cachedWindows.length === 1) { + this._arrows[this._highlighted].hide(); + } else { + this._arrows[this._highlighted].remove_style_pseudo_class( + 'highlighted', + ); + } + } - let arrow = new St.DrawingArea({ style_class: 'switcher-arrow' }); - arrow.connect('repaint', () => SwitcherPopup.drawArrow(arrow, St.Side.BOTTOM)); - this.add_child(arrow); - this._arrows.push(arrow); + super.highlight(n, justOutline); + + if (this._highlighted !== -1) { + if ( + justOutline && + this.icons[this._highlighted].cachedWindows.length === 1 + ) { + this._arrows[this._highlighted].show(); + } else { + this._arrows[this._highlighted].add_style_pseudo_class( + 'highlighted', + ); + } + } + } - if (appIcon.cachedWindows.length === 1) - arrow.hide(); - else - item.add_accessible_state(Atk.StateType.EXPANDABLE); - } + _addIcon(appIcon) { + this.icons.push(appIcon); + let item = this.addItem(appIcon, appIcon.label); + + appIcon.app.connectObject( + 'notify::state', + (app) => { + if (app.state !== Shell.AppState.RUNNING) { + this._removeIcon(app); + } + }, + this, + ); + + let arrow = new St.DrawingArea({style_class: 'switcher-arrow'}); + arrow.connect('repaint', () => + SwitcherPopup.drawArrow(arrow, St.Side.BOTTOM), + ); + this.add_child(arrow); + this._arrows.push(arrow); + + if (appIcon.cachedWindows.length === 1) { + arrow.hide(); + } else { + item.add_accessible_state(Atk.StateType.EXPANDABLE); + } + } - _removeIcon(item) { - const index = this.icons.findIndex(i => i === item); - if (index === -1) - return; + _removeIcon(item) { + const index = this.icons.findIndex((i) => i === item); + if (index === -1) { + return; + } - this._arrows[index].destroy(); - this._arrows.splice(index, 1); + this._arrows[index].destroy(); + this._arrows.splice(index, 1); - this.icons.splice(index, 1); - this.removeItem(index); - } -}); + this.icons.splice(index, 1); + this.removeItem(index); + } + }, +); /** * Replace AltTab.AppIcon and insert this into the TilingAppSwitcher instead. * This may contain multiple AppIcons to represent a tileGroup with chain icons * between the AppIcons. */ -const AppSwitcherItem = GObject.registerClass({ - Signals: { 'all-icons-removed': {} } -}, class AppSwitcherItem extends St.BoxLayout { - _init(windows) { - super._init({ vertical: false }); - - // A tiled window in a tileGroup of length 1, doesn't get a separate - // AppSwitcherItem. It gets added to the non-tiled windows' AppSwitcherItem - const tileGroup = windows[0].isTiled && Twm.getTileGroupFor(windows[0]); - this.isTileGroup = tileGroup && tileGroup.every(w => windows.includes(w)) && tileGroup?.length > 1; - this.cachedWindows = windows; - this.appIcons = []; - this.chainIcons = []; - - // Compatibility with AltTab.AppIcon - this.set_size = size => this.appIcons.forEach(i => i.set_size(size)); - this.label = null; - this.app = { - // Only raise the first window since we split up apps and tileGroups - activate_window: (window, timestamp) => { - Main.activateWindow(this.cachedWindows[0], timestamp); - }, - // Listening to the app-stop now happens in the custom _init func - // So prevent signal connection. here.. careful in case signal - // connection in the future is used for more... - connectObject: () => {} - }; +const AppSwitcherItem = GObject.registerClass( + { + Signals: {'all-icons-removed': {}}, + }, + class AppSwitcherItem extends St.BoxLayout { + _init(windows) { + super._init({vertical: false}); + + // A tiled window in a tileGroup of length 1, doesn't get a separate + // AppSwitcherItem. It gets added to the non-tiled windows' AppSwitcherItem + const tileGroup = + windows[0].isTiled && Twm.getTileGroupFor(windows[0]); + this.isTileGroup = + tileGroup && + tileGroup.every((w) => windows.includes(w)) && + tileGroup?.length > 1; + this.cachedWindows = windows; + this.appIcons = []; + this.chainIcons = []; + + // Compatibility with AltTab.AppIcon + this.set_size = (size) => + this.appIcons.forEach((i) => i.set_size(size)); + this.label = null; + this.app = { + // Only raise the first window since we split up apps and tileGroups + activate_window: (window, timestamp) => { + Main.activateWindow(this.cachedWindows[0], timestamp); + }, + // Listening to the app-stop now happens in the custom _init func + // So prevent signal connection. here.. careful in case signal + // connection in the future is used for more... + connectObject: () => {}, + }; + + this.updateAppIcons(); + } - this.updateAppIcons(); - } + // Re/Create the AppIcons based on the cached window list + updateAppIcons() { + this.appIcons.forEach((i) => i.destroy()); + this.appIcons = []; + this.chainIcons.forEach((i) => i.destroy()); + this.chainIcons = []; + + const winTracker = Shell.WindowTracker.get_default(); + const path = Extension.lookupByURL(import.meta.url) + .dir.get_child('media/insert-link-symbolic.svg') + .get_path(); + const icon = new Gio.FileIcon({file: Gio.File.new_for_path(path)}); + + const apps = + this.isTileGroup ? + // All apps (even duplicates) + this.cachedWindows.map((w) => winTracker.get_window_app(w)) + // Only unique apps + : this.cachedWindows.reduce((allApps, w) => { + const a = winTracker.get_window_app(w); + + if (!allApps.includes(a)) { + allApps.push(a); + } - // Re/Create the AppIcons based on the cached window list - updateAppIcons() { - this.appIcons.forEach(i => i.destroy()); - this.appIcons = []; - this.chainIcons.forEach(i => i.destroy()); - this.chainIcons = []; - - const winTracker = Shell.WindowTracker.get_default(); - const path = Extension.lookupByURL(import.meta.url) - .dir.get_child('media/insert-link-symbolic.svg') - .get_path(); - const icon = new Gio.FileIcon({ file: Gio.File.new_for_path(path) }); - - const apps = this.isTileGroup - // All apps (even duplicates) - ? this.cachedWindows.map(w => winTracker.get_window_app(w)) - // Only unique apps - : this.cachedWindows.reduce((allApps, w) => { - const a = winTracker.get_window_app(w); - !allApps.includes(a) && allApps.push(a); - return allApps; - }, []); - - apps.forEach((app, idx) => { - // AppIcon - const appIcon = new AppIcon(app); - this.add_child(appIcon); - this.appIcons.push(appIcon); - - // Add chain to the right AppIcon except for the last AppIcon - if (idx >= apps.length - 1) - return; + return allApps; + }, []); - // Chain - const chain = new St.Icon({ - gicon: icon, - icon_size: 18 + apps.forEach((app, idx) => { + // AppIcon + const appIcon = new AppIcon(app); + this.add_child(appIcon); + this.appIcons.push(appIcon); + + // Add chain to the right AppIcon except for the last AppIcon + if (idx >= apps.length - 1) { + return; + } + + // Chain + const chain = new St.Icon({ + gicon: icon, + icon_size: 18, + }); + this.add_child(chain); + this.chainIcons.push(chain); }); - this.add_child(chain); - this.chainIcons.push(chain); - }); - if (!this.appIcons.length) { - this.emit('all-icons-removed'); - return; + if (!this.appIcons.length) { + this.emit('all-icons-removed'); + return; + } + + this.label = this.appIcons[0].label; } - this.label = this.appIcons[0].label; - } + // Remove an AppIcon to the corresponding app. + // This doesn't update cached window list! + removeApp(app) { + for (let i = this.appIcons.length - 1; i >= 0; i--) { + const appIcon = this.appIcons[i]; + if (appIcon.app !== app) { + continue; + } - // Remove an AppIcon to the corresponding app. - // This doesn't update cached window list! - removeApp(app) { - for (let i = this.appIcons.length - 1; i >= 0; i--) { - const appIcon = this.appIcons[i]; - if (appIcon.app !== app) - continue; - - this.appIcons.splice(i, 1); - appIcon.destroy(); - const chain = this.chainIcons.splice(Math.max(0, i - 1), 1)[0]; - chain?.destroy(); - } + this.appIcons.splice(i, 1); + appIcon.destroy(); + const chain = this.chainIcons.splice(Math.max(0, i - 1), 1)[0]; + chain?.destroy(); + } - if (!this.appIcons.length) - this.emit('all-icons-removed'); - } -}); + if (!this.appIcons.length) { + this.emit('all-icons-removed'); + } + } + }, +); const AppIcon = GObject.registerClass( -class AppIcon extends AltTab.AppIcon { - // Don't make the SwitcherButtons squares since 1 SwitcherButton - // may contain multiple AppIcons for a tileGroup. - vfunc_get_preferred_width() { - return this.get_preferred_height(-1); - } -}); + class AppIcon extends AltTab.AppIcon { + // Don't make the SwitcherButtons squares since 1 SwitcherButton + // may contain multiple AppIcons for a tileGroup. + vfunc_get_preferred_width() { + return this.get_preferred_height(-1); + } + }, +); diff --git a/tiling-assistant@leleat-on-github/src/extension/focusHint.js b/tiling-assistant@leleat-on-github/src/extension/focusHint.js index 93bc863..73df5cd 100644 --- a/tiling-assistant@leleat-on-github/src/extension/focusHint.js +++ b/tiling-assistant@leleat-on-github/src/extension/focusHint.js @@ -1,20 +1,13 @@ -import { - Clutter, - Gio, - GLib, - Meta, - Shell, - St -} from '../dependencies/gi.js'; +import {Clutter, Gio, GLib, Meta, Shell, St} from '../dependencies/gi.js'; import { AppFavorites, Main, OsdWindow, - SwitcherPopup + SwitcherPopup, } from '../dependencies/shell.js'; import * as AltTab from '../dependencies/unexported/altTab.js'; -import { FocusHint, Settings } from '../common.js'; +import {FocusHint, Settings} from '../common.js'; export default class FocusHintManager { _hint = null; @@ -23,13 +16,16 @@ export default class FocusHintManager { // On a fresh install no color is set for the hint yet. Use the bg color // from the tile preview style by using a temporary widget. if (Settings.getString('focus-hint-color') === '') { - const widget = new St.Widget({ style_class: 'tile-preview' }); + const widget = new St.Widget({style_class: 'tile-preview'}); global.stage.add_child(widget); const color = widget.get_theme_node().get_background_color(); - const { red, green, blue } = color; + const {red, green, blue} = color; - Settings.setString('focus-hint-color', `rgb(${red},${green},${blue})`); + Settings.setString( + 'focus-hint-color', + `rgb(${red},${green},${blue})`, + ); widget.destroy(); } @@ -37,12 +33,13 @@ export default class FocusHintManager { this._settingsChangedId = Settings.changed( 'focus-hint', () => this._setHint(), - this + this, ); this._setHint(); - if (this._hint?.shouldIndicate(initialWindow)) + if (this._hint?.shouldIndicate(initialWindow)) { this._hint.indicate(initialWindow); + } } destroy() { @@ -69,7 +66,7 @@ export default class FocusHintManager { this._hint = null; } } -}; +} class Hint { _actors = []; @@ -102,7 +99,7 @@ class Hint { } resetAnimation() { - this._actors.forEach(actor => actor.destroy()); + this._actors.forEach((actor) => actor.destroy()); this._actors = []; } @@ -110,18 +107,24 @@ class Hint { const idleMonitor = global.backend.get_core_idle_monitor(); const idleTime = 120 * 1000; - this._activeWatchId && idleMonitor.remove_watch(this._activeWatchId); - this._activeWatchId = 0; + if (this._activeWatchId) { + idleMonitor.remove_watch(this._activeWatchId); + this._activeWatchId = 0; + } + + if (this._idleWatchId) { + idleMonitor.remove_watch(this._idleWatchId); + } - this._idleWatchId && idleMonitor.remove_watch(this._idleWatchId); this._idleWatchId = idleMonitor.add_idle_watch(idleTime, () => { this._activeWatchId = idleMonitor.add_user_active_watch(() => { this._activeWatchId = 0; const focus = global.display.focus_window; - if (this.shouldIndicate(focus)) + if (this.shouldIndicate(focus)) { this.indicate(focus); + } }); }); } @@ -130,7 +133,7 @@ class Hint { return [ Meta.WindowType.NORMAL, Meta.WindowType.DIALOG, - Meta.WindowType.MODAL_DIALOG + Meta.WindowType.MODAL_DIALOG, ].includes(type); } @@ -138,17 +141,18 @@ class Hint { global.display.connectObject( 'window-created', (_, metaWindow) => this._onWindowCreated(metaWindow), - this + this, ); global .get_window_actors() - .forEach(actor => this._onWindowCreated(actor.get_meta_window())); + .forEach((actor) => this._onWindowCreated(actor.get_meta_window())); } _onWindowCreated(window) { - if (!this._allowedWindowType(window.get_window_type())) + if (!this._allowedWindowType(window.get_window_type())) { return; + } window.connectObject( 'unmanaged', @@ -157,12 +161,13 @@ class Hint { const focus = global.display.focus_window; - if (focus && this.shouldIndicate(focus)) + if (focus && this.shouldIndicate(focus)) { this.indicate(focus); - else + } else { this.resetAnimation(); + } }, - this + this, ); } @@ -196,8 +201,9 @@ class Hint { if (global.display.remove_keybinding(key)) { const handler = (_, __, keybinding) => { - if (!Main.sessionMode.hasOverview) + if (!Main.sessionMode.hasOverview) { return; + } const [, , , target] = keybinding.get_name().split('-'); const apps = AppFavorites.getAppFavorites().getFavorites(); @@ -224,9 +230,11 @@ class Hint { global.display.add_keybinding( key, - new Gio.Settings({ schema_id: 'org.gnome.shell.keybindings' }), + new Gio.Settings({ + schema_id: 'org.gnome.shell.keybindings', + }), Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, - handler + handler, ); } } @@ -239,24 +247,25 @@ class Hint { const that = this; Main.wm._workspaceAnimation.animateSwitch = function ( - from, - to, - direction, - onComplete + from, + to, + direction, + onComplete, ) { that._originalWorkspaceAnimationSwitch.call( this, from, to, direction, - onComplete + onComplete, ); // This is set if the focused window moved to the new workspace // along with the workspace switch animation. E. g. when using // Shift + Super + Alt + Arrow_Keys. - if (this.movingWindow) + if (this.movingWindow) { return; + } // There are 2 different 'focus behaviors' during a workspace // animation. 1: When the workspace switch is initiated by an app or @@ -276,27 +285,31 @@ class Hint { global.workspace_manager.get_workspace_by_index(to); const [newFocus] = AltTab.getWindows(newWorkspace); - if (that.shouldIndicate(newFocus)) + if (that.shouldIndicate(newFocus)) { that.indicate(newFocus); - else + } else { that.resetAnimation(); - } + } + }, ); }; } shouldIndicate(window) { - if (!window || !window.get_compositor_private()) + if (!window || !window.get_compositor_private()) { return false; + } - if (!this._allowedWindowType(window.get_window_type())) + if (!this._allowedWindowType(window.get_window_type())) { return false; + } if ( window.is_fullscreen() || window.get_maximized() === Meta.MaximizeFlags.BOTH - ) + ) { return false; + } return true; } @@ -304,11 +317,15 @@ class Hint { _removeIdleWatcher() { const idleMonitor = global.backend.get_core_idle_monitor(); - this._activeWatchId && idleMonitor.remove_watch(this._activeWatchId); - this._activeWatchId = 0; + if (this._activeWatchId) { + idleMonitor.remove_watch(this._activeWatchId); + this._activeWatchId = 0; + } - this._idleWatchId && idleMonitor.remove_watch(this._idleWatchId); - this._idleWatchId = 0; + if (this._idleWatchId) { + idleMonitor.remove_watch(this._idleWatchId); + this._idleWatchId = 0; + } } _restoreSwitcherPopupFinish() { @@ -325,10 +342,12 @@ class Hint { if (global.display.remove_keybinding(key)) { Main.wm.addKeybinding( key, - new Gio.Settings({ schema_id: 'org.gnome.shell.keybindings' }), + new Gio.Settings({ + schema_id: 'org.gnome.shell.keybindings', + }), Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, - Main.wm._switchToApplication.bind(Main.wm) + Main.wm._switchToApplication.bind(Main.wm), ); } } @@ -344,7 +363,7 @@ class Hint { _stopIndicatingOnWindowClose() { global.display.disconnectObject(this); - global.get_window_actors().forEach(actor => { + global.get_window_actors().forEach((actor) => { actor.get_meta_window().disconnectObject(this); }); } @@ -364,14 +383,24 @@ class AnimatedOutlineHint extends Hint { }); this._outlineSize = Settings.getInt('focus-hint-outline-size'); - this._outlineSizeChangeId = Settings.changed('focus-hint-outline-size', () => { - this._outlineSize = Settings.getInt('focus-hint-outline-size'); - }); + this._outlineSizeChangeId = Settings.changed( + 'focus-hint-outline-size', + () => { + this._outlineSize = Settings.getInt('focus-hint-outline-size'); + }, + ); - this._outlineBorderRadius = Settings.getInt('focus-hint-outline-border-radius'); - this._outlineBorderRadiusChangeId = Settings.changed('focus-hint-outline-border-radius', () => { - this._outlineBorderRadius = Settings.getInt('focus-hint-outline-border-radius'); - }); + this._outlineBorderRadius = Settings.getInt( + 'focus-hint-outline-border-radius', + ); + this._outlineBorderRadiusChangeId = Settings.changed( + 'focus-hint-outline-border-radius', + () => { + this._outlineBorderRadius = Settings.getInt( + 'focus-hint-outline-border-radius', + ); + }, + ); } destroy() { @@ -385,33 +414,31 @@ class AnimatedOutlineHint extends Hint { indicate(window, workspaceSwitchAnimationDuration = 250) { this.resetAnimation(); - if (!this.shouldIndicate(window)) + if (!this.shouldIndicate(window)) { return; + } const windowActor = window.get_compositor_private(); const workspaceAnimationWindowClone = findWindowCloneForWorkspaceAnimation( windowActor, - !!Main.wm._workspaceAnimation._switchData + !!Main.wm._workspaceAnimation._switchData, ); const [monitorContainer, workspaceContainer] = createContainers( window, workspaceAnimationWindowClone, - workspaceSwitchAnimationDuration + workspaceSwitchAnimationDuration, ); this._actors.push(monitorContainer); - const customClone = createWindowClone( - windowActor, - monitorContainer - ); + const customClone = createWindowClone(windowActor, monitorContainer); const outline = this._createOutline(window, monitorContainer); const { x: windowFrameX, y: windowFrameY, width: windowFrameWidth, - height: windowFrameHeight + height: windowFrameHeight, } = window.get_frame_rect(); workspaceContainer.add_child(outline); @@ -424,9 +451,10 @@ class AnimatedOutlineHint extends Hint { y: windowFrameY - monitorContainer.y - this._outlineSize, width: windowFrameWidth + 2 * this._outlineSize, height: windowFrameHeight + 2 * this._outlineSize, - delay: workspaceAnimationWindowClone - ? (175 / 250) * workspaceSwitchAnimationDuration - : 0, + delay: + workspaceAnimationWindowClone ? + (175 / 250) * workspaceSwitchAnimationDuration + : 0, duration: 150, mode: Clutter.AnimationMode.EASE_OUT_BACK, onComplete: () => { @@ -437,20 +465,20 @@ class AnimatedOutlineHint extends Hint { height: windowFrameHeight, duration: 100, mode: Clutter.AnimationMode.EASE_IN, - onComplete: () => this.resetAnimation() + onComplete: () => this.resetAnimation(), }); - } + }, }); } _createOutline(window, monitorContainer) { - const { x, y, width, height } = window.get_frame_rect(); + const {x, y, width, height} = window.get_frame_rect(); const outline = new St.Widget({ style: this._getCssStyle(), x: x - monitorContainer.x, y: y - monitorContainer.y, width, - height + height, }); return outline; @@ -470,28 +498,26 @@ class AnimatedUpscaleHint extends Hint { indicate(window, workspaceSwitchAnimationDuration = 250) { this.resetAnimation(); - if (!this.shouldIndicate(window)) + if (!this.shouldIndicate(window)) { return; + } const windowActor = window.get_compositor_private(); const workspaceAnimationWindowClone = findWindowCloneForWorkspaceAnimation( windowActor, - !!Main.wm._workspaceAnimation._switchData + !!Main.wm._workspaceAnimation._switchData, ); const [monitorContainer, workspaceContainer] = createContainers( window, workspaceAnimationWindowClone, - workspaceSwitchAnimationDuration + workspaceSwitchAnimationDuration, ); this._actors.push(monitorContainer); - const customClone = createWindowClone( - windowActor, - monitorContainer - ); - const { x, y, width, height } = customClone; + const customClone = createWindowClone(windowActor, monitorContainer); + const {x, y, width, height} = customClone; workspaceContainer.add_child(customClone); @@ -503,9 +529,10 @@ class AnimatedUpscaleHint extends Hint { y: y - this._scaleAmount, width: width + 2 * this._scaleAmount, height: height + 2 * this._scaleAmount, - delay: workspaceAnimationWindowClone - ? (175 / 250) * workspaceSwitchAnimationDuration - : 0, + delay: + workspaceAnimationWindowClone ? + (175 / 250) * workspaceSwitchAnimationDuration + : 0, duration: 100, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { @@ -516,14 +543,14 @@ class AnimatedUpscaleHint extends Hint { height, duration: 150, mode: Clutter.AnimationMode.EASE_OUT_QUAD, - onComplete: () => this.resetAnimation() + onComplete: () => this.resetAnimation(), }); - } + }, }); } resetAnimation() { - global.get_window_actors().forEach(a => a.set_opacity(255)); + global.get_window_actors().forEach((a) => a.set_opacity(255)); super.resetAnimation(); } } @@ -535,7 +562,7 @@ class StaticOutlineHint extends AnimatedOutlineHint { constructor() { super(); - this._outline = new St.Widget({ style: this._getCssStyle() }); + this._outline = new St.Widget({style: this._getCssStyle()}); global.window_group.add_child(this._outline); // Originally, only `notify::focus-window` was used but that had issues @@ -546,7 +573,7 @@ class StaticOutlineHint extends AnimatedOutlineHint { () => this._updateOutline(), 'notify::focus-window', () => this._updateOutline(), - this + this, ); this._updateOutline(); @@ -558,7 +585,7 @@ class StaticOutlineHint extends AnimatedOutlineHint { () => this._updateOutline(), 'changed::focus-hint-outline-border-radius', () => this._updateOutline(), - this + this, ); } @@ -589,35 +616,34 @@ class StaticOutlineHint extends AnimatedOutlineHint { indicate(window, workspaceSwitchAnimationDuration = 250) { this.resetAnimation(); - if (!this.shouldIndicate(window)) + if (!this.shouldIndicate(window)) { return; + } const animatingWorkspaceSwitch = !!Main.wm._workspaceAnimation._switchData; // Only need to use an animation to indicate the focus when switching // workspaces. In the other cases, there is the static `this._outline`. - if (!animatingWorkspaceSwitch) + if (!animatingWorkspaceSwitch) { return; + } const windowActor = window.get_compositor_private(); const workspaceAnimationWindowClone = findWindowCloneForWorkspaceAnimation( windowActor, - animatingWorkspaceSwitch + animatingWorkspaceSwitch, ); const [monitorContainer, workspaceContainer] = createContainers( window, workspaceAnimationWindowClone, - workspaceSwitchAnimationDuration + workspaceSwitchAnimationDuration, ); this._actors.push(monitorContainer); - const customClone = createWindowClone( - windowActor, - monitorContainer - ); + const customClone = createWindowClone(windowActor, monitorContainer); const outline = this._createOutline(window, monitorContainer); workspaceContainer.add_child(outline); @@ -631,7 +657,7 @@ class StaticOutlineHint extends AnimatedOutlineHint { () => { this.resetAnimation(); this._resetTimer = 0; - } + }, ); } @@ -643,13 +669,13 @@ class StaticOutlineHint extends AnimatedOutlineHint { } _createOutline(window, monitorContainer) { - const { x, y, width, height } = window.get_frame_rect(); + const {x, y, width, height} = window.get_frame_rect(); const outline = new St.Widget({ style: this._getCssStyle(), x: x - monitorContainer.x - this._outlineSize, y: y - monitorContainer.y - this._outlineSize, width: width + 2 * this._outlineSize, - height: height + 2 * this._outlineSize + height: height + 2 * this._outlineSize, }); return outline; @@ -658,8 +684,9 @@ class StaticOutlineHint extends AnimatedOutlineHint { _queueGeometryUpdate() { const windowActor = this._window.get_compositor_private(); - if (!windowActor) + if (!windowActor) { return; + } this._laterID = global.compositor .get_laters() @@ -670,7 +697,7 @@ class StaticOutlineHint extends AnimatedOutlineHint { global.window_group.set_child_below_sibling( this._outline, - windowActor + windowActor, ); this._laterID = 0; @@ -696,26 +723,27 @@ class StaticOutlineHint extends AnimatedOutlineHint { () => this._updateGeometry(), 'size-changed', () => this._updateGeometry(), - this + this, ); if ( this._window.is_fullscreen() || this._window.get_maximized() === Meta.MaximizeFlags.BOTH - ) + ) { this._outline.hide(); - else + } else { this._queueGeometryUpdate(); + } } _updateGeometry() { - const { x, y, width, height } = this._window.get_frame_rect(); + const {x, y, width, height} = this._window.get_frame_rect(); this._outline.set({ x: x - this._outlineSize, y: y - this._outlineSize, width: width + this._outlineSize * 2, - height: height + this._outlineSize * 2 + height: height + this._outlineSize * 2, }); } } @@ -729,7 +757,7 @@ class StaticOutlineHint extends AnimatedOutlineHint { * @returns {{x: number, y: number}} */ function getAbsPos(actor) { - const pos = { x: actor.x, y: actor.y }; + const pos = {x: actor.x, y: actor.y}; let parent = actor.get_parent(); while (parent) { @@ -756,22 +784,25 @@ function getAbsPos(actor) { function createContainers( window, workspaceAnimationWindowClone, - workspaceSwitchAnimationDuration + workspaceSwitchAnimationDuration, ) { const monitorNr = window.get_monitor(); const monitorRect = global.display.get_monitor_geometry(monitorNr); let startingPos; if (workspaceAnimationWindowClone) { - const actorAbsPos = getAbsPos(window.get_compositor_private(), monitorNr); + const actorAbsPos = getAbsPos( + window.get_compositor_private(), + monitorNr, + ); const cloneAbsPos = getAbsPos(workspaceAnimationWindowClone, monitorNr); startingPos = { x: monitorRect.x + cloneAbsPos.x - actorAbsPos.x, - y: monitorRect.y + cloneAbsPos.y - actorAbsPos.y + y: monitorRect.y + cloneAbsPos.y - actorAbsPos.y, }; } else { - startingPos = { x: 0, y: 0 }; + startingPos = {x: 0, y: 0}; } const monitorContainer = new Clutter.Actor({ @@ -779,7 +810,7 @@ function createContainers( x: monitorRect.x, y: monitorRect.y, width: monitorRect.width, - height: monitorRect.height + height: monitorRect.height, }); // Allow tiled window to be animate above the panel. Also, When changing @@ -787,12 +818,13 @@ function createContainers( if (workspaceAnimationWindowClone) { const osdWindow = Main.uiGroup .get_children() - .find(child => child instanceof OsdWindow.OsdWindow); + .find((child) => child instanceof OsdWindow.OsdWindow); - if (osdWindow) + if (osdWindow) { Main.uiGroup.insert_child_below(monitorContainer, osdWindow); - else + } else { Main.uiGroup.add_child(monitorContainer); + } } else { global.window_group.add_child(monitorContainer); } @@ -801,7 +833,7 @@ function createContainers( x: startingPos.x, y: startingPos.y, width: monitorContainer.width, - height: monitorContainer.height + height: monitorContainer.height, }); monitorContainer.add_child(workspaceContainer); @@ -810,7 +842,7 @@ function createContainers( x: 0, y: 0, duration: workspaceSwitchAnimationDuration, - mode: Clutter.AnimationMode.EASE_OUT_CUBIC + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, }); return [monitorContainer, workspaceContainer]; @@ -827,14 +859,14 @@ function createContainers( */ function createWindowClone(windowActor, container) { const monitor = windowActor.get_meta_window().get_monitor(); - const { x, y } = getAbsPos(windowActor, monitor); + const {x, y} = getAbsPos(windowActor, monitor); const windowClone = new Clutter.Clone({ source: windowActor, x: x - container.x, y: y - container.y, width: windowActor.width, - height: windowActor.height + height: windowActor.height, }); return windowClone; @@ -851,21 +883,23 @@ function createWindowClone(windowActor, container) { */ function findWindowCloneForWorkspaceAnimation( windowActor, - animatingWorkspaceSwitch + animatingWorkspaceSwitch, ) { - if (!animatingWorkspaceSwitch) + if (!animatingWorkspaceSwitch) { return null; + } const switchData = Main.wm._workspaceAnimation._switchData; let clone = null; - switchData.monitors.find(monitorGroup => { - return monitorGroup._workspaceGroups.find(workspaceGroup => { - return workspaceGroup._windowRecords.find(record => { + switchData.monitors.find((monitorGroup) => { + return monitorGroup._workspaceGroups.find((workspaceGroup) => { + return workspaceGroup._windowRecords.find((record) => { const foundClone = record.windowActor === windowActor; - if (foundClone) - ({ clone } = record); + if (foundClone) { + ({clone} = record); + } return foundClone; }); diff --git a/tiling-assistant@leleat-on-github/src/extension/keybindingHandler.js b/tiling-assistant@leleat-on-github/src/extension/keybindingHandler.js index 6898d47..de52019 100644 --- a/tiling-assistant@leleat-on-github/src/extension/keybindingHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/keybindingHandler.js @@ -1,9 +1,9 @@ -import { Clutter, Meta, Shell, St } from '../dependencies/gi.js'; -import { _, Main } from '../dependencies/shell.js'; +import {Clutter, Meta, Shell, St} from '../dependencies/gi.js'; +import {_, Main} from '../dependencies/shell.js'; -import { Direction, DynamicKeybindings, Settings, Shortcuts } from '../common.js'; -import { Rect, Util } from './utility.js'; -import { TilingWindowManager as Twm } from './tilingWindowManager.js'; +import {Direction, DynamicKeybindings, Settings, Shortcuts} from '../common.js'; +import {Rect, Util} from './utility.js'; +import {TilingWindowManager as Twm} from './tilingWindowManager.js'; /** * Class to handle the keyboard shortcuts (on the extension side) except the @@ -14,20 +14,22 @@ export default class TilingKeybindingHandler { constructor() { const allowInOverview = ['toggle-tiling-popup']; this._keyBindings = Shortcuts.getAllKeys(); - this._keyBindings.forEach(key => { + this._keyBindings.forEach((key) => { Main.wm.addKeybinding( key, Settings.getGioObject(), Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, - Shell.ActionMode.NORMAL | (allowInOverview.includes(key) && Shell.ActionMode.OVERVIEW), - this._onCustomKeybindingPressed.bind(this, key) + Shell.ActionMode.NORMAL | + (allowInOverview.includes(key) && + Shell.ActionMode.OVERVIEW), + this._onCustomKeybindingPressed.bind(this, key), ); }); } destroy() { - this._keyBindings.forEach(key => Main.wm.removeKeybinding(key)); - this._debuggingIndicators?.forEach(i => i.destroy()); + this._keyBindings.forEach((key) => Main.wm.removeKeybinding(key)); + this._debuggingIndicators?.forEach((i) => i.destroy()); } /** @@ -35,34 +37,42 @@ export default class TilingKeybindingHandler { */ async _onCustomKeybindingPressed(shortcutName) { // Debugging - const debugging = ['debugging-show-tiled-rects', 'debugging-free-rects']; + const debugging = [ + 'debugging-show-tiled-rects', + 'debugging-free-rects', + ]; if (debugging.includes(shortcutName)) { if (this._debuggingIndicators) { - this._debuggingIndicators.forEach(i => i.destroy()); + this._debuggingIndicators.forEach((i) => i.destroy()); this._debuggingIndicators = null; } else { - const createIndicators = shortcutName === 'debugging-show-tiled-rects' - ? Util.___debugShowTiledRects - : Util.___debugShowFreeScreenRects; + const createIndicators = + shortcutName === 'debugging-show-tiled-rects' ? + Util.___debugShowTiledRects + : Util.___debugShowFreeScreenRects; this._debuggingIndicators = await createIndicators.call(Util); } return; - // Toggle the Tiling Popup + // Toggle the Tiling Popup } else if (shortcutName === 'toggle-tiling-popup') { const toggleTo = !Settings.getBoolean('enable-tiling-popup'); Settings.setBoolean('enable-tiling-popup', toggleTo); - Main.notify('Tiling Assistant', toggleTo - // Translators: This is the notification text when the Tiling Popup is enabled/disabled via the keyboard shortcut - ? _('Tiling popup enabled') - // Translators: This is the notification text when the Tiling Popup is enabled/disabled via the keyboard shortcut - : _('Tiling popup was disabled')); + Main.notify( + 'Tiling Assistant', + toggleTo ? + // Translators: This is the notification text when the Tiling Popup is enabled/disabled via the keyboard shortcut + _('Tiling popup enabled') + // Translators: This is the notification text when the Tiling Popup is enabled/disabled via the keyboard shortcut + : _('Tiling popup was disabled'), + ); return; } const window = global.display.focus_window; - if (!window) + if (!window) { return; + } // Auto-tile: tile to empty space. If there's none: untile, // if it's already tiled else maximize @@ -70,23 +80,31 @@ export default class TilingKeybindingHandler { if (Twm.isMaximized(window)) { Twm.untile(window); } else { - const topTileGroup = Twm.getTopTileGroup({ skipTopWindow: !window.isTiled }); - const tRects = topTileGroup.map(w => w.tiledRect); - const tileRect = Twm.getBestFreeRect(tRects, { currRect: window.tiledRect }); + const topTileGroup = Twm.getTopTileGroup({ + skipTopWindow: !window.isTiled, + }); + const tRects = topTileGroup.map((w) => w.tiledRect); + const tileRect = Twm.getBestFreeRect(tRects, { + currRect: window.tiledRect, + }); Twm.toggleTiling(window, tileRect); } - // Tile Editing Mode + // Tile Editing Mode } else if (shortcutName === 'tile-edit-mode') { const TileEditingMode = await import('./tileEditingMode.js'); const tileEditor = new TileEditingMode.TileEditor(); tileEditor.open(); - // Toggle always-on-top + // Toggle always-on-top } else if (shortcutName === 'toggle-always-on-top') { - window.is_above() ? window.unmake_above() : window.make_above(); + if (window.is_above()) { + window.unmake_above(); + } else { + window.make_above(); + } - // Toggle maximization vertically + // Toggle maximization vertically } else if (shortcutName === 'tile-maximize-vertically') { const workArea = new Rect(window.get_work_area_current_monitor()); const currRect = window.tiledRect ?? window.get_frame_rect(); @@ -95,34 +113,49 @@ export default class TilingKeybindingHandler { if (window.untiledRect && currRect.height === workArea.height) { // Is maximized if (currRect.width === workArea.width) { - const tileRect = new Rect(workArea.x, window.untiledRect.y, workArea.width, window.untiledRect.height); + const tileRect = new Rect( + workArea.x, + window.untiledRect.y, + workArea.width, + window.untiledRect.height, + ); Twm.tile(window, tileRect); - // Is tiled + // Is tiled } else { Twm.untile(window); } - // is tiled normally + // is tiled normally } else if (window.untiledRect) { - const tileRect = new Rect(currRect.x, workArea.y, currRect.width, workArea.height); + const tileRect = new Rect( + currRect.x, + workArea.y, + currRect.width, + workArea.height, + ); Twm.tile(window, tileRect); - // is floating + // is floating } else { const width = Math.min( currRect.width + Settings.getInt('window-gap'), - workArea.width + workArea.width, ); const constrainX = Math.max( currRect.x - Settings.getInt('window-gap') / 2, - workArea.x + workArea.x, ); const finalX = Math.min(constrainX, workArea.x2 - width); - const tileRect = new Rect(finalX, workArea.y, width, workArea.height); + const tileRect = new Rect( + finalX, + workArea.y, + width, + workArea.height, + ); Twm.tile(window, tileRect); } - // Toggle maximization horizontally + // Toggle maximization horizontally } else if (shortcutName === 'tile-maximize-horizontally') { const workArea = new Rect(window.get_work_area_current_monitor()); const currRect = window.tiledRect ?? window.get_frame_rect(); @@ -131,41 +164,58 @@ export default class TilingKeybindingHandler { if (window.untiledRect && currRect.width === workArea.width) { // Is maximized if (currRect.height === workArea.height) { - const tileRect = new Rect(window.untiledRect.x, workArea.y, window.untiledRect.width, workArea.height); + const tileRect = new Rect( + window.untiledRect.x, + workArea.y, + window.untiledRect.width, + workArea.height, + ); Twm.tile(window, tileRect); - // Is tiled + // Is tiled } else { Twm.untile(window); } - // is tiled normally + // is tiled normally } else if (window.untiledRect) { - const tileRect = new Rect(workArea.x, currRect.y, workArea.width, currRect.height); + const tileRect = new Rect( + workArea.x, + currRect.y, + workArea.width, + currRect.height, + ); Twm.tile(window, tileRect); - // is floating + // is floating } else { const height = Math.min( currRect.height + Settings.getInt('window-gap'), - workArea.height + workArea.height, ); const constrainY = Math.max( currRect.y - Settings.getInt('window-gap') / 2, - workArea.y + workArea.y, ); const finalY = Math.min(constrainY, workArea.y2 - height); - const tileRect = new Rect(workArea.x, finalY, workArea.width, height); + const tileRect = new Rect( + workArea.x, + finalY, + workArea.width, + height, + ); Twm.tile(window, tileRect); } - // Restore window size + // Restore window size } else if (shortcutName === 'restore-window') { - if (window.untiledRect) // Tiled & maximized with gaps - Twm.untile(window, { clampToWorkspace: true }); - else if (window.get_maximized()) + if (window.untiledRect) { + // Tiled & maximized with gaps + Twm.untile(window, {clampToWorkspace: true}); + } else if (window.get_maximized()) { window.unmaximize(window.get_maximized()); + } - // Center window + // Center window } else if (shortcutName === 'center-window') { const workArea = new Rect(window.get_work_area_current_monitor()); if (window.isTiled) { @@ -174,50 +224,69 @@ export default class TilingKeybindingHandler { workArea.center.x - Math.floor(currRect.width / 2), workArea.center.y - Math.floor(currRect.height / 2), currRect.width, - currRect.height + currRect.height, ); - if (tileRect.equal(currRect)) + if (tileRect.equal(currRect)) { return; + } - Twm.tile(window, tileRect, { openTilingPopup: false }); + Twm.tile(window, tileRect, {openTilingPopup: false}); } else if (!Twm.isMaximized(window)) { - if (!window.allows_move()) + if (!window.allows_move()) { return; + } const currRect = window.get_frame_rect(); const x = workArea.center.x - Math.floor(currRect.width / 2); const y = workArea.center.y - Math.floor(currRect.height / 2); - if (x === currRect.x && y === currRect.y) + if (x === currRect.x && y === currRect.y) { return; + } const wActor = window.get_compositor_private(); - wActor && Main.wm._prepareAnimationInfo( - global.window_manager, - wActor, - currRect, - Meta.SizeChange.UNMAXIMIZE - ); + + if (wActor) { + Main.wm._prepareAnimationInfo( + global.window_manager, + wActor, + currRect, + Meta.SizeChange.UNMAXIMIZE, + ); + } + window.move_frame(false, x, y); } - // Tile a window but ignore T-A features - } else if (['tile-top-half-ignore-ta', 'tile-bottom-half-ignore-ta', - 'tile-left-half-ignore-ta', 'tile-right-half-ignore-ta', - 'tile-topleft-quarter-ignore-ta', 'tile-topright-quarter-ignore-ta', - 'tile-bottomleft-quarter-ignore-ta', - 'tile-bottomright-quarter-ignore-ta'].includes(shortcutName) + // Tile a window but ignore T-A features + } else if ( + [ + 'tile-top-half-ignore-ta', + 'tile-bottom-half-ignore-ta', + 'tile-left-half-ignore-ta', + 'tile-right-half-ignore-ta', + 'tile-topleft-quarter-ignore-ta', + 'tile-topright-quarter-ignore-ta', + 'tile-bottomleft-quarter-ignore-ta', + 'tile-bottomright-quarter-ignore-ta', + ].includes(shortcutName) ) { const workArea = new Rect(window.get_work_area_current_monitor()); const rect = Twm.getDefaultTileFor(shortcutName, workArea); - Twm.toggleTiling(window, rect, { ignoreTA: true }); - // Tile a window + Twm.toggleTiling(window, rect, {ignoreTA: true}); + // Tile a window } else { - const dynamicSetting = Settings.getInt('dynamic-keybinding-behavior'); + const dynamicSetting = Settings.getInt( + 'dynamic-keybinding-behavior', + ); const windowsStyle = DynamicKeybindings.TILING_STATE_WINDOWS; const isWindowsStyle = dynamicSetting === windowsStyle; const workArea = new Rect(window.get_work_area_current_monitor()); - const rect = Twm.getTileFor(shortcutName, workArea, window.get_monitor()); + const rect = Twm.getTileFor( + shortcutName, + workArea, + window.get_monitor(), + ); switch (dynamicSetting) { case DynamicKeybindings.FOCUS: @@ -225,7 +294,11 @@ export default class TilingKeybindingHandler { break; case DynamicKeybindings.TILING_STATE: case DynamicKeybindings.TILING_STATE_WINDOWS: - this._dynamicTilingState(window, shortcutName, isWindowsStyle); + this._dynamicTilingState( + window, + shortcutName, + isWindowsStyle, + ); break; case DynamicKeybindings.FAVORITE_LAYOUT: this._dynamicFavoriteLayout(window, shortcutName); @@ -244,13 +317,17 @@ export default class TilingKeybindingHandler { * the focus to. */ _dynamicFocus(window, shortcutName) { - const topTileGroup = Twm.getTopTileGroup({ skipTopWindow: true }); + const topTileGroup = Twm.getTopTileGroup({skipTopWindow: true}); const workArea = new Rect(window.get_work_area_current_monitor()); // Toggle tile state of the window, if it isn't tiled // or if it is the only window which is. if (!window.isTiled || topTileGroup.length === 1) { - const rect = Twm.getTileFor(shortcutName, workArea, window.get_monitor()); + const rect = Twm.getTileFor( + shortcutName, + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; } @@ -275,11 +352,15 @@ export default class TilingKeybindingHandler { window, topTileGroup, direction, - false + false, ); if (!nearestWindow) { - const rect = Twm.getTileFor(shortcutName, workArea, window.get_monitor()); + const rect = Twm.getTileFor( + shortcutName, + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; } @@ -296,7 +377,7 @@ export default class TilingKeybindingHandler { x: fromRect.x, y: fromRect.y, width: fromRect.width, - height: fromRect.height + height: fromRect.height, }); Main.uiGroup.add_child(focusIndicator); const toRect = nearestWindow.get_frame_rect(); @@ -314,9 +395,9 @@ export default class TilingKeybindingHandler { duration: 200, mode: Clutter.AnimationMode.EASE_IN_OUT_CIRC, delay: 100, - onComplete: () => focusIndicator.destroy() + onComplete: () => focusIndicator.destroy(), }); - } + }, }); } @@ -337,14 +418,24 @@ export default class TilingKeybindingHandler { switch (shortcutName) { case 'tile-maximize': case 'tile-top-half': { - const rect = Twm.getTileFor('tile-top-half', workArea, window.get_monitor()); - Twm.tile(window, rect, { skipAnim: true }); + const rect = Twm.getTileFor( + 'tile-top-half', + workArea, + window.get_monitor(), + ); + Twm.tile(window, rect, {skipAnim: true}); break; - } case 'tile-bottom-half': { + } + case 'tile-bottom-half': { Twm.untile(window); break; - } default: { - const rect = Twm.getTileFor(shortcutName, workArea, window.get_monitor()); + } + default: { + const rect = Twm.getTileFor( + shortcutName, + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); } } @@ -357,9 +448,14 @@ export default class TilingKeybindingHandler { window.minimize(); break; } - // falls through - } default: { - const rect = Twm.getTileFor(shortcutName, workArea, window.get_monitor()); + // falls through + } + default: { + const rect = Twm.getTileFor( + shortcutName, + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); } } @@ -414,11 +510,19 @@ export default class TilingKeybindingHandler { switch (shortcutName) { case 'tile-top-half': case 'tile-maximize': - rect = Twm.getTileFor('tile-topleft-quarter', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-topleft-quarter', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-bottom-half': - rect = Twm.getTileFor('tile-bottomleft-quarter', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-bottomleft-quarter', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-right-half': @@ -429,11 +533,19 @@ export default class TilingKeybindingHandler { switch (shortcutName) { case 'tile-top-half': case 'tile-maximize': - rect = Twm.getTileFor('tile-topright-quarter', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-topright-quarter', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-bottom-half': - rect = Twm.getTileFor('tile-bottomright-quarter', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-bottomright-quarter', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-left-half': @@ -443,15 +555,27 @@ export default class TilingKeybindingHandler { } else if (isTopHalf) { switch (shortcutName) { case 'tile-top-half': - rect = Twm.getTileFor('tile-maximize', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-maximize', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-left-half': - rect = Twm.getTileFor('tile-topleft-quarter', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-topleft-quarter', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-right-half': - rect = Twm.getTileFor('tile-topright-quarter', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-topright-quarter', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-bottom-half': @@ -461,11 +585,19 @@ export default class TilingKeybindingHandler { } else if (isBottomHalf) { switch (shortcutName) { case 'tile-left-half': - rect = Twm.getTileFor('tile-bottomleft-quarter', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-bottomleft-quarter', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-right-half': - rect = Twm.getTileFor('tile-bottomright-quarter', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-bottomright-quarter', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-top-half': @@ -473,29 +605,55 @@ export default class TilingKeybindingHandler { Twm.untile(window); return; case 'tile-bottom-half': - rect = Twm.getTileFor('tile-bottom-half', workArea, window.get_monitor()); - isWindowsStyle ? window.minimize() : Twm.toggleTiling(window, rect); + rect = Twm.getTileFor( + 'tile-bottom-half', + workArea, + window.get_monitor(), + ); + + if (isWindowsStyle) { + window.minimize(); + } else { + Twm.toggleTiling(window, rect); + } + return; } } else if (isTopLeftQuarter) { switch (shortcutName) { case 'tile-right-half': - rect = Twm.getTileFor('tile-top-half', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-top-half', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-bottom-half': - rect = Twm.getTileFor('tile-left-half', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-left-half', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; } } else if (isTopRightQuarter) { switch (shortcutName) { case 'tile-left-half': - rect = Twm.getTileFor('tile-top-half', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-top-half', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-bottom-half': - rect = Twm.getTileFor('tile-right-half', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-right-half', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; } @@ -503,43 +661,86 @@ export default class TilingKeybindingHandler { switch (shortcutName) { case 'tile-top-half': case 'tile-maximize': - rect = Twm.getTileFor('tile-left-half', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-left-half', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-right-half': - rect = Twm.getTileFor('tile-bottom-half', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-bottom-half', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-bottom-half': - rect = Twm.getTileFor('tile-bottom-half', workArea, window.get_monitor()); - isWindowsStyle ? window.minimize() : Twm.toggleTiling(window, rect); + rect = Twm.getTileFor( + 'tile-bottom-half', + workArea, + window.get_monitor(), + ); + + if (isWindowsStyle) { + window.minimize(); + } else { + Twm.toggleTiling(window, rect); + } + return; } } else if (isBottomRightQuarter) { switch (shortcutName) { case 'tile-top-half': case 'tile-maximize': - rect = Twm.getTileFor('tile-right-half', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-right-half', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-left-half': - rect = Twm.getTileFor('tile-bottom-half', workArea, window.get_monitor()); + rect = Twm.getTileFor( + 'tile-bottom-half', + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); return; case 'tile-bottom-half': - rect = Twm.getTileFor('tile-bottom-half', workArea, window.get_monitor()); - isWindowsStyle ? window.minimize() : Twm.toggleTiling(window, rect); + rect = Twm.getTileFor( + 'tile-bottom-half', + workArea, + window.get_monitor(), + ); + + if (isWindowsStyle) { + window.minimize(); + } else { + Twm.toggleTiling(window, rect); + } + return; } } - Twm.toggleTiling(window, Twm.getTileFor(shortcutName, workArea, window.get_monitor())); + Twm.toggleTiling( + window, + Twm.getTileFor(shortcutName, workArea, window.get_monitor()), + ); } _dynamicFavoriteLayout(window, shortcutName) { const workArea = new Rect(window.get_work_area_current_monitor()); const toggleTiling = () => { - const rect = Twm.getTileFor(shortcutName, workArea, window.get_monitor()); + const rect = Twm.getTileFor( + shortcutName, + workArea, + window.get_monitor(), + ); Twm.toggleTiling(window, rect); }; @@ -571,8 +772,11 @@ export default class TilingKeybindingHandler { } if (direction) { - const neighbor = window.tiledRect.getNeighbor(direction, favoriteLayout); - Twm.tile(window, neighbor, { openTilingPopup: false }); + const neighbor = window.tiledRect.getNeighbor( + direction, + favoriteLayout, + ); + Twm.tile(window, neighbor, {openTilingPopup: false}); } else { toggleTiling(); } diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutsManager.js b/tiling-assistant@leleat-on-github/src/extension/layoutsManager.js index c81274c..beda29b 100644 --- a/tiling-assistant@leleat-on-github/src/extension/layoutsManager.js +++ b/tiling-assistant@leleat-on-github/src/extension/layoutsManager.js @@ -1,15 +1,15 @@ -import { Clutter, Gio, GObject, Meta, Shell, St } from '../dependencies/gi.js'; +import {Clutter, Gio, GObject, Meta, Shell, St} from '../dependencies/gi.js'; import { _, Extension, Main, PanelMenu, - PopupMenu + PopupMenu, } from '../dependencies/shell.js'; -import { Layout, Settings } from '../common.js'; -import { Rect, Util } from './utility.js'; -import { TilingWindowManager as Twm } from './tilingWindowManager.js'; +import {Layout, Settings} from '../common.js'; +import {Rect, Util} from './utility.js'; +import {TilingWindowManager as Twm} from './tilingWindowManager.js'; /** * Here are the classes to handle PopupLayouts on the shell / extension side. @@ -61,7 +61,7 @@ export default class TilingLayoutsManager { Settings.getGioObject(), Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, Shell.ActionMode.NORMAL, - this.startLayouting.bind(this, i) + this.startLayouting.bind(this, i), ); } @@ -71,25 +71,35 @@ export default class TilingLayoutsManager { Settings.getGioObject(), Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, Shell.ActionMode.NORMAL, - this.openPopupSearch.bind(this) + this.openPopupSearch.bind(this), ); // Add panel indicator this._panelIndicator = new PanelIndicator(); Main.panel.addToStatusArea( 'tiling-assistant@leleat-on-github', - this._panelIndicator); - this._settingsId = Settings.changed('show-layout-panel-indicator', () => { - this._panelIndicator.visible = Settings.getBoolean('show-layout-panel-indicator'); - }); - this._panelIndicator.visible = Settings.getBoolean('show-layout-panel-indicator'); - this._panelIndicator.connect('layout-activated', (src, idx) => this.startLayouting(idx)); + this._panelIndicator, + ); + this._settingsId = Settings.changed( + 'show-layout-panel-indicator', + () => { + this._panelIndicator.visible = Settings.getBoolean( + 'show-layout-panel-indicator', + ); + }, + ); + this._panelIndicator.visible = Settings.getBoolean( + 'show-layout-panel-indicator', + ); + this._panelIndicator.connect('layout-activated', (src, idx) => + this.startLayouting(idx), + ); } destroy() { Settings.disconnect(this._settingsId); this._finishLayouting(); - this._keyBindings.forEach(key => Main.wm.removeKeybinding(key)); + this._keyBindings.forEach((key) => Main.wm.removeKeybinding(key)); this._panelIndicator.destroy(); this._panelIndicator = null; } @@ -107,7 +117,9 @@ export default class TilingLayoutsManager { } const search = new LayoutSearch(layouts); - search.connect('item-activated', (s, index) => this.startLayouting(index)); + search.connect('item-activated', (s, index) => + this.startLayouting(index), + ); } /** @@ -117,8 +129,9 @@ export default class TilingLayoutsManager { */ startLayouting(index) { const layout = Util.getLayouts()?.[index]; - if (!layout) + if (!layout) { return; + } const allWs = Settings.getBoolean('tiling-popup-all-workspace'); this._remainingWindows = Twm.getWindows(allWs); @@ -133,7 +146,7 @@ export default class TilingLayoutsManager { style_class: 'tile-preview', opacity: 0, x: workArea.x + workArea.width / 2, - y: workArea.y + workArea.height / 2 + y: workArea.y + workArea.height / 2, }); Main.layoutManager.addChrome(this._rectPreview); @@ -170,24 +183,33 @@ export default class TilingLayoutsManager { // Scale the item's rect to the workArea const activeWs = global.workspace_manager.get_active_workspace(); const monitor = global.display.get_current_monitor(); - const workArea = new Rect(activeWs.get_work_area_for_monitor(monitor)); + const workArea = new Rect( + activeWs.get_work_area_for_monitor(monitor), + ); const rectRatios = this._currItem.rect; this._currRect = new Rect( workArea.x + Math.floor(rectRatios.x * workArea.width), workArea.y + Math.floor(rectRatios.y * workArea.height), Math.ceil(rectRatios.width * workArea.width), - Math.ceil(rectRatios.height * workArea.height) + Math.ceil(rectRatios.height * workArea.height), ); // Try to compensate possible rounding errors when scaling up the // rect by aligning it with the rects, which were already tiled // using this layout and the workArea. - this._tiledWithLayout.forEach(w => this._currRect.tryAlignWith(w.tiledRect)); + this._tiledWithLayout.forEach((w) => + this._currRect.tryAlignWith(w.tiledRect), + ); this._currRect.tryAlignWith(workArea); } const appId = this._currItem.appId; - appId ? this._openAppTiled(appId) : this._openTilingPopup(); + + if (appId) { + this._openAppTiled(appId); + } else { + this._openTilingPopup(); + } } _openAppTiled(appId) { @@ -200,14 +222,19 @@ export default class TilingLayoutsManager { } const winTracker = Shell.WindowTracker.get_default(); - const idx = this._remainingWindows.findIndex(w => winTracker.get_window_app(w) === app); + const idx = this._remainingWindows.findIndex( + (w) => winTracker.get_window_app(w) === app, + ); const window = this._remainingWindows[idx]; - idx !== -1 && this._remainingWindows.splice(idx, 1); + + if (idx !== -1) { + this._remainingWindows.splice(idx, 1); + } if (window) { Twm.tile(window, this._currRect, { openTilingPopup: false, - skipAnim: true + skipAnim: true, }); } else if (app.can_open_new_window()) { Twm.openAppTiled(app, this._currRect); @@ -233,7 +260,7 @@ export default class TilingLayoutsManager { height: this._currRect.height, opacity: 255, duration: 200, - mode: Clutter.AnimationMode.EASE_OUT_QUAD + mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); // Create the Tiling Popup @@ -245,9 +272,11 @@ export default class TilingLayoutsManager { // allow the Tiling Popup itself to spawn another instance of // a Tiling Popup, if there is free screen space. this._currItem === this._items.at(-1) && !this._currItem.loopType, - true + true, + ); + const stacked = global.display.sort_windows_by_stacking( + this._tiledWithLayout, ); - const stacked = global.display.sort_windows_by_stacking(this._tiledWithLayout); const tileGroup = stacked.reverse(); if (!popup.show(tileGroup)) { popup.destroy(); @@ -279,12 +308,13 @@ export default class TilingLayoutsManager { this._tiledWithLoop.push(tiledWindow); this._tiledWithLoop.forEach((w, idx) => { const rect = this._currRect.copy(); - const [pos, dimension] = this._currItem.loopType === 'h' - ? ['y', 'height'] - : ['x', 'width']; + const [pos, dimension] = + this._currItem.loopType === 'h' ? + ['y', 'height'] + : ['x', 'width']; rect[dimension] /= this._tiledWithLoop.length; rect[pos] += idx * rect[dimension]; - Twm.tile(w, rect, { openTilingPopup: false, skipAnim: true }); + Twm.tile(w, rect, {openTilingPopup: false, skipAnim: true}); }); } @@ -296,271 +326,331 @@ export default class TilingLayoutsManager { /** * The GUI class for the Layout search. */ -const LayoutSearch = GObject.registerClass({ - Signals: { 'item-activated': { param_types: [GObject.TYPE_INT] } } -}, class TilingLayoutsSearch extends St.Widget { - _init(layouts) { - const activeWs = global.workspace_manager.get_active_workspace(); - super._init({ - reactive: true, - x: Main.uiGroup.x, - y: Main.uiGroup.y, - width: Main.uiGroup.width, - height: Main.uiGroup.height - }); - Main.uiGroup.add_child(this); +const LayoutSearch = GObject.registerClass( + { + Signals: {'item-activated': {param_types: [GObject.TYPE_INT]}}, + }, + class TilingLayoutsSearch extends St.Widget { + _init(layouts) { + const activeWs = global.workspace_manager.get_active_workspace(); + super._init({ + reactive: true, + x: Main.uiGroup.x, + y: Main.uiGroup.y, + width: Main.uiGroup.width, + height: Main.uiGroup.height, + }); + Main.uiGroup.add_child(this); - const grab = Main.pushModal(this); - // We expect at least a keyboard grab here - if ((grab.get_seat_state() & Clutter.GrabState.KEYBOARD) === 0) { - Main.popModal(grab); - return false; - } + const grab = Main.pushModal(this); + // We expect at least a keyboard grab here + if ((grab.get_seat_state() & Clutter.GrabState.KEYBOARD) === 0) { + Main.popModal(grab); + return; + } - this._grab = grab; - this._haveModal = true; - this._focused = -1; - this._items = []; + this._grab = grab; + this._haveModal = true; + this._focused = -1; + this._items = []; - this.connect('button-press-event', () => this.destroy()); + this.connect('button-press-event', () => this.destroy()); - const popup = new St.BoxLayout({ - style_class: 'switcher-list', - vertical: true, - width: 500 - }); - this.add_child(popup); + const popup = new St.BoxLayout({ + style_class: 'switcher-list', + vertical: true, + width: 500, + }); + this.add_child(popup); - const fontSize = 16; - const entry = new St.Entry({ - style: `font-size: ${fontSize}px;\ + const fontSize = 16; + const entry = new St.Entry({ + style: `font-size: ${fontSize}px;\ border-radius: 16px; margin-bottom: 12px;`, - // Translators: This is the placeholder text for a search field. - hint_text: ` ${_('Type to search...')}` - }); - const entryClutterText = entry.get_clutter_text(); - entryClutterText.connect('key-press-event', this._onKeyPressed.bind(this)); - entryClutterText.connect('text-changed', this._onTextChanged.bind(this)); - popup.add_child(entry); - - this._items = layouts.map(layout => { - const item = new SearchItem(layout._name, fontSize); - item.connect('button-press-event', this._onItemClicked.bind(this)); - popup.add_child(item); - return item; - }); - - if (!this._items.length) { - this.destroy(); - return; - } + // Translators: This is the placeholder text for a search field. + hint_text: ` ${_('Type to search...')}`, + }); + const entryClutterText = entry.get_clutter_text(); + entryClutterText.connect( + 'key-press-event', + this._onKeyPressed.bind(this), + ); + entryClutterText.connect( + 'text-changed', + this._onTextChanged.bind(this), + ); + popup.add_child(entry); + + this._items = layouts.map((layout) => { + const item = new SearchItem(layout._name, fontSize); + item.connect( + 'button-press-event', + this._onItemClicked.bind(this), + ); + popup.add_child(item); + return item; + }); - const monitor = global.display.get_current_monitor(); - const workArea = activeWs.get_work_area_for_monitor(monitor); - popup.set_position(workArea.x + workArea.width / 2 - popup.width / 2, - workArea.y + workArea.height / 2 - popup.height / 2); + if (!this._items.length) { + this.destroy(); + return; + } - entry.grab_key_focus(); - this._focus(0); - } + const monitor = global.display.get_current_monitor(); + const workArea = activeWs.get_work_area_for_monitor(monitor); + popup.set_position( + workArea.x + workArea.width / 2 - popup.width / 2, + workArea.y + workArea.height / 2 - popup.height / 2, + ); - destroy() { - if (this._haveModal) { - Main.popModal(this._grab); - this._haveModal = false; + entry.grab_key_focus(); + this._focus(0); } - super.destroy(); - } + destroy() { + if (this._haveModal) { + Main.popModal(this._grab); + this._haveModal = false; + } - _onKeyPressed(clutterText, event) { - const keySym = event.get_key_symbol(); - if (keySym === Clutter.KEY_Escape) { - this.destroy(); - return Clutter.EVENT_STOP; - } else if (keySym === Clutter.KEY_Return || + super.destroy(); + } + + _onKeyPressed(clutterText, event) { + const keySym = event.get_key_symbol(); + if (keySym === Clutter.KEY_Escape) { + this.destroy(); + return Clutter.EVENT_STOP; + } else if ( + keySym === Clutter.KEY_Return || keySym === Clutter.KEY_KP_Enter || - keySym === Clutter.KEY_ISO_Enter) { - this._activate(); - return Clutter.EVENT_STOP; - } else if (keySym === Clutter.KEY_Down) { - this._focusNext(); - return Clutter.EVENT_STOP; - } else if (keySym === Clutter.KEY_Up) { - this._focusPrev(); - return Clutter.EVENT_STOP; + keySym === Clutter.KEY_ISO_Enter + ) { + this._activate(); + return Clutter.EVENT_STOP; + } else if (keySym === Clutter.KEY_Down) { + this._focusNext(); + return Clutter.EVENT_STOP; + } else if (keySym === Clutter.KEY_Up) { + this._focusPrev(); + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; } - return Clutter.EVENT_PROPAGATE; - } + _onTextChanged(clutterText) { + const filterText = clutterText.get_text(); + this._items.forEach((item) => { + if ( + item.text.toLowerCase().includes(filterText.toLowerCase()) + ) { + item.show(); + } else { + item.hide(); + } + }); + const nextVisibleIdx = this._items.findIndex( + (item) => item.visible, + ); + this._focus(nextVisibleIdx); + } - _onTextChanged(clutterText) { - const filterText = clutterText.get_text(); - this._items.forEach(item => { - item.text.toLowerCase().includes(filterText.toLowerCase()) - ? item.show() - : item.hide(); - }); - const nextVisibleIdx = this._items.findIndex(item => item.visible); - this._focus(nextVisibleIdx); - } + _onItemClicked(item) { + this._focused = this._items.indexOf(item); + this._activate(); + } - _onItemClicked(item) { - this._focused = this._items.indexOf(item); - this._activate(); - } + _focusPrev() { + this._focus( + (this._focused + this._items.length - 1) % this._items.length, + ); + } - _focusPrev() { - this._focus((this._focused + this._items.length - 1) % this._items.length); - } + _focusNext() { + this._focus((this._focused + 1) % this._items.length); + } - _focusNext() { - this._focus((this._focused + 1) % this._items.length); - } + _focus(newIdx) { + const prevItem = this._items[this._focused]; + const newItem = this._items[newIdx]; + this._focused = newIdx; - _focus(newIdx) { - const prevItem = this._items[this._focused]; - const newItem = this._items[newIdx]; - this._focused = newIdx; + prevItem?.remove_style_class_name('tiling-layout-search-highlight'); + newItem?.add_style_class_name('tiling-layout-search-highlight'); + } - prevItem?.remove_style_class_name('tiling-layout-search-highlight'); - newItem?.add_style_class_name('tiling-layout-search-highlight'); - } + _activate() { + if (this._focused !== -1) { + this.emit('item-activated', this._focused); + } - _activate() { - this._focused !== -1 && this.emit('item-activated', this._focused); - this.destroy(); - } -}); + this.destroy(); + } + }, +); /** * An Item representing a Layout within the Popup Layout search. */ const SearchItem = GObject.registerClass( -class TilingLayoutsSearchItem extends St.Label { - _init(text, fontSize) { - super._init({ - // Translators: This is the text that will be displayed as the name of the user-defined tiling layout if it hasn't been given a name. - text: ` ${text || _('Nameless layout...')}`, - style: `font-size: ${fontSize}px;\ + class TilingLayoutsSearchItem extends St.Label { + _init(text, fontSize) { + super._init({ + // Translators: This is the text that will be displayed as the name of the user-defined tiling layout if it hasn't been given a name. + text: ` ${text || _('Nameless layout...')}`, + style: `font-size: ${fontSize}px;\ text-align: left;\ padding: 8px\ margin-bottom: 2px`, - reactive: true - }); - } -}); + reactive: true, + }); + } + }, +); /** * A panel indicator to activate and favoritize a layout. */ -const PanelIndicator = GObject.registerClass({ - Signals: { 'layout-activated': { param_types: [GObject.TYPE_INT] } } -}, class PanelIndicator extends PanelMenu.Button { - _init() { - super._init(0.0, 'Layout Indicator (Tiling Assistant)'); - - const path = Extension.lookupByURL(import.meta.url) - .dir - .get_child('media/preferences-desktop-apps-symbolic.svg') - .get_path(); - const gicon = new Gio.FileIcon({ file: Gio.File.new_for_path(path) }); - this.add_child(new St.Icon({ - gicon, - style_class: 'system-status-icon' - })); - - const menuAlignment = 0.0; - this.setMenu(new PopupMenu.PopupMenu(this, menuAlignment, St.Side.TOP)); - } +const PanelIndicator = GObject.registerClass( + { + Signals: {'layout-activated': {param_types: [GObject.TYPE_INT]}}, + }, + class PanelIndicator extends PanelMenu.Button { + _init() { + super._init(0.0, 'Layout Indicator (Tiling Assistant)'); + + const path = Extension.lookupByURL(import.meta.url) + .dir.get_child('media/preferences-desktop-apps-symbolic.svg') + .get_path(); + const gicon = new Gio.FileIcon({file: Gio.File.new_for_path(path)}); + this.add_child( + new St.Icon({ + gicon, + style_class: 'system-status-icon', + }), + ); - vfunc_event(event) { - if (this.menu && - (event.type() === Clutter.EventType.TOUCH_BEGIN || - event.type() === Clutter.EventType.BUTTON_PRESS) - ) { - this._updateItems(); - this.menu.toggle(); + const menuAlignment = 0.0; + this.setMenu( + new PopupMenu.PopupMenu(this, menuAlignment, St.Side.TOP), + ); } - return Clutter.EVENT_PROPAGATE; - } + vfunc_event(event) { + if ( + this.menu && + (event.type() === Clutter.EventType.TOUCH_BEGIN || + event.type() === Clutter.EventType.BUTTON_PRESS) + ) { + this._updateItems(); + this.menu.toggle(); + } - _updateItems() { - this.menu.removeAll(); + return Clutter.EVENT_PROPAGATE; + } - const layouts = Util.getLayouts(); - if (!layouts.length) { - // Translators: This is a placeholder text within a popup, if the user didn't define a tiling layout. - const item = new PopupMenu.PopupMenuItem(_('No valid layouts defined.')); - item.setSensitive(false); - this.menu.addMenuItem(item); - } else { - // Update favorites with monitor count and fill with '-1', if necessary - const tmp = Settings.getStrv('favorite-layouts'); - const count = Math.max(Main.layoutManager.monitors.length, tmp.length); - const favorites = [...new Array(count)].map((m, monitorIndex) => { - return tmp[monitorIndex] ?? '-1'; - }); - Settings.setStrv('favorite-layouts', favorites); - - // Create popup menu items - layouts.forEach((layout, idx) => { - const name = layout._name || `Layout ${idx + 1}`; - const item = new PopupFavoriteMenuItem(name, idx); - item.connect('activate', () => { - Main.overview.hide(); - this.emit('layout-activated', idx); - }); - item.connect('favorite-changed', this._updateItems.bind(this)); + _updateItems() { + this.menu.removeAll(); + + const layouts = Util.getLayouts(); + if (!layouts.length) { + // Translators: This is a placeholder text within a popup, if the user didn't define a tiling layout. + const item = new PopupMenu.PopupMenuItem( + _('No valid layouts defined.'), + ); + item.setSensitive(false); this.menu.addMenuItem(item); - }); - } + } else { + // Update favorites with monitor count and fill with '-1', if necessary + const tmp = Settings.getStrv('favorite-layouts'); + const count = Math.max( + Main.layoutManager.monitors.length, + tmp.length, + ); + const favorites = [...new Array(count)].map( + (m, monitorIndex) => { + return tmp[monitorIndex] ?? '-1'; + }, + ); + Settings.setStrv('favorite-layouts', favorites); + + // Create popup menu items + layouts.forEach((layout, idx) => { + const name = layout._name || `Layout ${idx + 1}`; + const item = new PopupFavoriteMenuItem(name, idx); + item.connect('activate', () => { + Main.overview.hide(); + this.emit('layout-activated', idx); + }); + item.connect( + 'favorite-changed', + this._updateItems.bind(this), + ); + this.menu.addMenuItem(item); + }); + } - this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); - const settingsButton = new PopupMenu.PopupImageMenuItem(_('Preferences'), 'emblem-system-symbolic'); - // Center button without changing the size (for the hover highlight) - settingsButton._icon.set_x_expand(true); - settingsButton.label.set_x_expand(true); - settingsButton.connect('activate', - () => Extension.lookupByURL(import.meta.url).openPreferences()); - this.menu.addMenuItem(settingsButton); - } -}); + const settingsButton = new PopupMenu.PopupImageMenuItem( + _('Preferences'), + 'emblem-system-symbolic', + ); + // Center button without changing the size (for the hover highlight) + settingsButton._icon.set_x_expand(true); + settingsButton.label.set_x_expand(true); + settingsButton.connect('activate', () => + Extension.lookupByURL(import.meta.url).openPreferences(), + ); + this.menu.addMenuItem(settingsButton); + } + }, +); /** * A PopupMenuItem for the PopupMenu of the PanelIndicator. */ -const PopupFavoriteMenuItem = GObject.registerClass({ - Signals: { 'favorite-changed': { param_types: [GObject.TYPE_INT] } } -}, class PopupFavoriteMenuItem extends PopupMenu.PopupBaseMenuItem { - _init(text, layoutIndex) { - super._init(); - - this.add_child(new St.Label({ - text, - x_expand: true - })); - - const favorites = Settings.getStrv('favorite-layouts'); - Main.layoutManager.monitors.forEach((m, monitorIndex) => { - const favoriteButton = new St.Button({ - child: new St.Icon({ - icon_name: favorites[monitorIndex] === `${layoutIndex}` ? 'starred-symbolic' : 'non-starred-symbolic', - style_class: 'popup-menu-icon' - }) - }); - this.add_child(favoriteButton); - - // Update gSetting with new Favorite (act as a toggle button) - favoriteButton.connect('clicked', () => { - const currFavorites = Settings.getStrv('favorite-layouts'); - currFavorites[monitorIndex] = currFavorites[monitorIndex] === `${layoutIndex}` ? '-1' : `${layoutIndex}`; - Settings.setStrv('favorite-layouts', currFavorites); - this.emit('favorite-changed', monitorIndex); +const PopupFavoriteMenuItem = GObject.registerClass( + { + Signals: {'favorite-changed': {param_types: [GObject.TYPE_INT]}}, + }, + class PopupFavoriteMenuItem extends PopupMenu.PopupBaseMenuItem { + _init(text, layoutIndex) { + super._init(); + + this.add_child( + new St.Label({ + text, + x_expand: true, + }), + ); + + const favorites = Settings.getStrv('favorite-layouts'); + Main.layoutManager.monitors.forEach((m, monitorIndex) => { + const favoriteButton = new St.Button({ + child: new St.Icon({ + icon_name: + favorites[monitorIndex] === `${layoutIndex}` ? + 'starred-symbolic' + : 'non-starred-symbolic', + style_class: 'popup-menu-icon', + }), + }); + this.add_child(favoriteButton); + + // Update gSetting with new Favorite (act as a toggle button) + favoriteButton.connect('clicked', () => { + const currFavorites = Settings.getStrv('favorite-layouts'); + currFavorites[monitorIndex] = + currFavorites[monitorIndex] === `${layoutIndex}` ? + '-1' + : `${layoutIndex}`; + Settings.setStrv('favorite-layouts', currFavorites); + this.emit('favorite-changed', monitorIndex); + }); }); - }); - } -}); + } + }, +); diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js index abfdbcb..f9399b7 100644 --- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js @@ -1,10 +1,10 @@ -import { Clutter, GLib, GObject, Gio, Meta, Mtk } from '../dependencies/gi.js'; -import { Main, WindowManager } from '../dependencies/shell.js'; -import { WINDOW_ANIMATION_TIME } from '../dependencies/unexported/windowManager.js'; +import {Clutter, GLib, GObject, Gio, Meta, Mtk} from '../dependencies/gi.js'; +import {Main, WindowManager} from '../dependencies/shell.js'; +import {WINDOW_ANIMATION_TIME} from '../dependencies/unexported/windowManager.js'; -import { Orientation, MoveModes, Settings } from '../common.js'; -import { Rect, Util } from './utility.js'; -import { TilingWindowManager as Twm } from './tilingWindowManager.js'; +import {Orientation, MoveModes, Settings} from '../common.js'; +import {Rect, Util} from './utility.js'; +import {TilingWindowManager as Twm} from './tilingWindowManager.js'; /** * This class gets to handle the move events (grab & monitor change) of windows. @@ -23,16 +23,17 @@ export default class TilingMoveHandler { (src, window, grabOp) => { grabOp &= ~1024; // META_GRAB_OP_WINDOW_FLAG_UNCONSTRAINED - if (window && moveOps.includes(grabOp)) + if (window && moveOps.includes(grabOp)) { this._onMoveStarted(window, grabOp); + } }, - this + this, ); global.display.connectObject( 'window-entered-monitor', this._onMonitorEntered.bind(this), - this + this, ); // Save the windows, which need to make space for the @@ -52,7 +53,7 @@ export default class TilingMoveHandler { const modKeys = [ 'move-adaptive-tiling-mod', 'move-favorite-layout-mod', - 'ignore-ta-mod' + 'ignore-ta-mod', ]; const handleWindowActionKeyConflict = () => { const currMod = this._wmPrefs.get_string('mouse-button-modifier'); @@ -60,25 +61,29 @@ export default class TilingMoveHandler { if (currMod === '') { for (const key of modKeys) { const mod = Settings.getInt(key); - if (mod === 2) // Alt + if (mod === 2) { + // Alt Settings.setInt(key, 0); + } } } else if (currMod === '') { for (const key of modKeys) { const mod = Settings.getInt(key); - if (mod === 4) // Super + if (mod === 4) { + // Super Settings.setInt(key, 0); + } } } }; this._wmPrefs = new Gio.Settings({ - schema_id: 'org.gnome.desktop.wm.preferences' + schema_id: 'org.gnome.desktop.wm.preferences', }); this._wmPrefs.connectObject( 'changed::mouse-button-modifier', () => handleWindowActionKeyConflict(), - this + this, ); handleWindowActionKeyConflict(); } @@ -113,10 +118,11 @@ export default class TilingMoveHandler { } _onMonitorEntered(src, monitorNr, window) { - if (this._isGrabOp) + if (this._isGrabOp) { // Reset preview mode: // Currently only needed to grab the favorite layout for the new monitor. this._preparePreviewModeChange(this._currPreviewMode, window); + } } _onMoveStarted(window, grabOp) { @@ -129,43 +135,50 @@ export default class TilingMoveHandler { // Try to restore the window size if (window.tiledRect || this._wasMaximizedOnStart) { let counter = 0; - this._restoreSizeTimerId && GLib.Source.remove(this._restoreSizeTimerId); - this._restoreSizeTimerId = GLib.timeout_add(GLib.PRIORITY_HIGH_IDLE, 10, () => { - if (!global.display.is_grabbed()) { - this._restoreSizeTimerId = null; - return GLib.SOURCE_REMOVE; - } + if (this._restoreSizeTimerId) { + GLib.Source.remove(this._restoreSizeTimerId); + } - counter += 10; - if (counter >= 400) { - this._restoreSizeAndRestartGrab(window, x, y, grabOp); - this._restoreSizeTimerId = null; - return GLib.SOURCE_REMOVE; - } + this._restoreSizeTimerId = GLib.timeout_add( + GLib.PRIORITY_HIGH_IDLE, + 10, + () => { + if (!global.display.is_grabbed()) { + this._restoreSizeTimerId = null; + return GLib.SOURCE_REMOVE; + } - const [currX, currY] = global.get_pointer(); - const currPoint = { x: currX, y: currY }; - const oldPoint = { x, y }; - const moveDist = Util.getDistance(currPoint, oldPoint); - if (moveDist > 10) { - this._restoreSizeAndRestartGrab(window, x, y, grabOp); - this._restoreSizeTimerId = null; - return GLib.SOURCE_REMOVE; - } + counter += 10; + if (counter >= 400) { + this._restoreSizeAndRestartGrab(window, x, y, grabOp); + this._restoreSizeTimerId = null; + return GLib.SOURCE_REMOVE; + } - return GLib.SOURCE_CONTINUE; - }); + const [currX, currY] = global.get_pointer(); + const currPoint = {x: currX, y: currY}; + const oldPoint = {x, y}; + const moveDist = Util.getDistance(currPoint, oldPoint); + if (moveDist > 10) { + this._restoreSizeAndRestartGrab(window, x, y, grabOp); + this._restoreSizeTimerId = null; + return GLib.SOURCE_REMOVE; + } - // Tile preview + return GLib.SOURCE_CONTINUE; + }, + ); + + // Tile preview } else { this._isGrabOp = true; this._monitorNr = global.display.get_current_monitor(); this._lastMonitorNr = this._monitorNr; - this._lastPointerPos = { x, y }; + this._lastPointerPos = {x, y}; this._pointerDidntMove = false; this._movingTimerDuration = 20; this._movingTimeoutsSinceUpdate = 0; - this._topTileGroup = Twm.getTopTileGroup({ skipTopWindow: true }); + this._topTileGroup = Twm.getTopTileGroup({skipTopWindow: true}); // When low performance mode is enabled we use a timer to periodically // update the tile previews so that we don't update the tile preview @@ -174,12 +187,7 @@ export default class TilingMoveHandler { this._movingTimerId = GLib.timeout_add( GLib.PRIORITY_IDLE, this._movingTimerDuration, - this._onMoving.bind( - this, - grabOp, - window, - true - ) + this._onMoving.bind(this, grabOp, window, true), ); const id = global.display.connect('grab-op-end', () => { @@ -191,16 +199,12 @@ export default class TilingMoveHandler { this._onMoveFinished(window); }); - // Otherwise we will update the tile preview whenever the window is - // moved as often as necessary. + // Otherwise we will update the tile preview whenever the window is + // moved as often as necessary. } else { - this._posChangedId = window.connect('position-changed', - this._onMoving.bind( - this, - grabOp, - window, - false - ) + this._posChangedId = window.connect( + 'position-changed', + this._onMoving.bind(this, grabOp, window, false), ); const id = global.display.connect('grab-op-end', () => { @@ -220,31 +224,46 @@ export default class TilingMoveHandler { // with at least 1 window being part of multiple tile groups. let isCtrlReplacement = false; const ctrlReplacedTileGroup = []; - const topTileGroup = Twm.getTopTileGroup({ skipTopWindow: true }); - const pointerPos = { x: global.get_pointer()[0], y: global.get_pointer()[1] }; - const twHovered = topTileGroup.some(w => w.tiledRect.containsPoint(pointerPos)); - if (this._currPreviewMode === MoveModes.ADAPTIVE_TILING && !this._splitRects.size && twHovered) { + const topTileGroup = Twm.getTopTileGroup({skipTopWindow: true}); + const pointerPos = { + x: global.get_pointer()[0], + y: global.get_pointer()[1], + }; + const twHovered = topTileGroup.some((w) => + w.tiledRect.containsPoint(pointerPos), + ); + if ( + this._currPreviewMode === MoveModes.ADAPTIVE_TILING && + !this._splitRects.size && + twHovered + ) { isCtrlReplacement = true; ctrlReplacedTileGroup.push(window); - topTileGroup.forEach(w => { - if (!this._tileRect.containsRect(w.tiledRect)) + topTileGroup.forEach((w) => { + if (!this._tileRect.containsRect(w.tiledRect)) { ctrlReplacedTileGroup.push(w); + } }); } - this._splitRects.forEach((rect, w) => Twm.tile(w, rect, { openTilingPopup: false })); + this._splitRects.forEach((rect, w) => + Twm.tile(w, rect, {openTilingPopup: false}), + ); this._splitRects.clear(); Twm.tile(window, this._tileRect, { monitorNr: this._monitorNr, - openTilingPopup: this._currPreviewMode !== MoveModes.ADAPTIVE_TILING, - ignoreTA: this._ignoreTA + openTilingPopup: + this._currPreviewMode !== MoveModes.ADAPTIVE_TILING, + ignoreTA: this._ignoreTA, }); this._tileRect = null; // Create a new tile group, in which some windows are already part // of a different tile group, with ctrl-(super)-drag. The window may // be maximized by ctrl-super-drag. - isCtrlReplacement && window.isTiled && Twm.updateTileGroup(ctrlReplacedTileGroup); + if (isCtrlReplacement && window.isTiled) { + Twm.updateTileGroup(ctrlReplacedTileGroup); + } } } finally { if (this._posChangedId) { @@ -253,7 +272,7 @@ export default class TilingMoveHandler { } this._favoriteLayout = []; - this._favoritePreviews?.forEach(p => p.destroy()); + this._favoritePreviews?.forEach((p) => p.destroy()); this._favoritePreviews = []; this._freeScreenRects = []; this._anchorRect = null; @@ -275,7 +294,7 @@ export default class TilingMoveHandler { // moved (by listening to the position-changed signal) _onMoving(grabOp, window, lowPerfMode = false) { const [x, y] = global.get_pointer(); - const currPointerPos = { x, y }; + const currPointerPos = {x, y}; if (lowPerfMode) { if (!this._isGrabOp) { @@ -283,7 +302,10 @@ export default class TilingMoveHandler { return GLib.SOURCE_REMOVE; } - const movementDist = Util.getDistance(this._lastPointerPos, currPointerPos); + const movementDist = Util.getDistance( + this._lastPointerPos, + currPointerPos, + ); const movementDetectionThreshold = 10; let forceMoveUpdate = false; this._movingTimeoutsSinceUpdate++; @@ -303,9 +325,11 @@ export default class TilingMoveHandler { // Only update the tile preview every 500 ms for better performance. // Force an early update, if the pointer movement state changed. const updateInterval = 500; - const timeSinceLastUpdate = this._movingTimerDuration * this._movingTimeoutsSinceUpdate; - if (timeSinceLastUpdate < updateInterval && !forceMoveUpdate) + const timeSinceLastUpdate = + this._movingTimerDuration * this._movingTimeoutsSinceUpdate; + if (timeSinceLastUpdate < updateInterval && !forceMoveUpdate) { return GLib.SOURCE_CONTINUE; + } this._movingTimeoutsSinceUpdate = 0; } @@ -316,43 +340,54 @@ export default class TilingMoveHandler { const altL = Clutter.ModifierType.MOD1_MASK; const altGr = Clutter.ModifierType.MOD5_MASK; const meta = Clutter.ModifierType.MOD4_MASK; - const rmb = Meta.is_wayland_compositor() - ? Clutter.ModifierType.BUTTON2_MASK - : Clutter.ModifierType.BUTTON3_MASK; - const pressed = [ // idxs come from settings + const rmb = + Meta.is_wayland_compositor() ? + Clutter.ModifierType.BUTTON2_MASK + : Clutter.ModifierType.BUTTON3_MASK; + const pressed = [ + // idxs come from settings false, // Dummy for disabled state so that we can use the correct idxs Util.isModPressed(ctrl), Util.isModPressed(altL) || Util.isModPressed(altGr), Util.isModPressed(rmb), - Util.isModPressed(meta) + Util.isModPressed(meta), ]; const defaultMode = Settings.getInt('default-move-mode'); const adaptiveMod = Settings.getInt('move-adaptive-tiling-mod'); const favMod = Settings.getInt('move-favorite-layout-mod'); const ignoreTAMod = Settings.getInt('ignore-ta-mod'); - const noMod = !pressed[adaptiveMod] && !pressed[ignoreTAMod] && !pressed[ignoreTAMod]; - - const useAdaptiveTiling = defaultMode !== MoveModes.ADAPTIVE_TILING && pressed[adaptiveMod] || - noMod && defaultMode === MoveModes.ADAPTIVE_TILING; - const usefavLayout = defaultMode !== MoveModes.FAVORITE_LAYOUT && pressed[favMod] || - noMod && defaultMode === MoveModes.FAVORITE_LAYOUT; - const useIgnoreTa = defaultMode !== MoveModes.IGNORE_TA && pressed[ignoreTAMod] || - noMod && defaultMode === MoveModes.IGNORE_TA; + const noMod = + !pressed[adaptiveMod] && + !pressed[ignoreTAMod] && + !pressed[ignoreTAMod]; + + const useAdaptiveTiling = + (defaultMode !== MoveModes.ADAPTIVE_TILING && + pressed[adaptiveMod]) || + (noMod && defaultMode === MoveModes.ADAPTIVE_TILING); + const usefavLayout = + (defaultMode !== MoveModes.FAVORITE_LAYOUT && pressed[favMod]) || + (noMod && defaultMode === MoveModes.FAVORITE_LAYOUT); + const useIgnoreTa = + (defaultMode !== MoveModes.IGNORE_TA && pressed[ignoreTAMod]) || + (noMod && defaultMode === MoveModes.IGNORE_TA); let newMode = ''; - if (useAdaptiveTiling) + if (useAdaptiveTiling) { newMode = MoveModes.ADAPTIVE_TILING; - else if (usefavLayout) + } else if (usefavLayout) { newMode = MoveModes.FAVORITE_LAYOUT; - else if (useIgnoreTa) + } else if (useIgnoreTa) { newMode = MoveModes.IGNORE_TA; - else + } else { newMode = MoveModes.EDGE_TILING; + } - if (this._currPreviewMode !== newMode) + if (this._currPreviewMode !== newMode) { this._preparePreviewModeChange(newMode, window); + } switch (newMode) { case MoveModes.IGNORE_TA: @@ -374,12 +409,12 @@ export default class TilingMoveHandler { _preparePreviewModeChange(newMode, window) { this._tileRect = null; this._ignoreTA = false; - this._topTileGroup = Twm.getTopTileGroup({ skipTopWindow: true }); + this._topTileGroup = Twm.getTopTileGroup({skipTopWindow: true}); const activeWs = global.workspace_manager.get_active_workspace(); const monitor = global.display.get_current_monitor(); const workArea = new Rect(activeWs.get_work_area_for_monitor(monitor)); - const tRects = this._topTileGroup.map(w => w.tiledRect); + const tRects = this._topTileGroup.map((w) => w.tiledRect); this._freeScreenRects = workArea.minus(tRects); switch (this._currPreviewMode) { @@ -390,12 +425,12 @@ export default class TilingMoveHandler { break; case MoveModes.FAVORITE_LAYOUT: this._monitorNr = global.display.get_current_monitor(); - this._favoritePreviews.forEach(p => { + this._favoritePreviews.forEach((p) => { p.ease({ opacity: 0, duration: 100, mode: Clutter.AnimationMode.EASE_OUT_QUAD, - onComplete: () => p.destroy() + onComplete: () => p.destroy(), }); }); this._favoritePreviews = []; @@ -408,11 +443,11 @@ export default class TilingMoveHandler { break; case MoveModes.FAVORITE_LAYOUT: this._favoriteLayout = Util.getFavoriteLayout(); - this._favoriteLayout.forEach(rect => { + this._favoriteLayout.forEach((rect) => { const tilePreview = new TilePreview(); tilePreview.open(window, rect, this._monitorNr, { opacity: 255, - duration: 150 + duration: 150, }); this._favoritePreviews.push(tilePreview); }); @@ -423,7 +458,7 @@ export default class TilingMoveHandler { Twm.untile(window, { restoreFullPos: false, xAnchor: px, - skipAnim: this._wasMaximizedOnStart + skipAnim: this._wasMaximizedOnStart, }); this._onMoveStarted(window, grabOp); @@ -442,23 +477,35 @@ export default class TilingMoveHandler { // the user doesn't have to slowly inch the mouse to the monitor edge // just because there is another monitor at that edge. const currMonitorNr = global.display.get_current_monitor(); - const useGracePeriod = Settings.getBoolean('monitor-switch-grace-period'); + const useGracePeriod = Settings.getBoolean( + 'monitor-switch-grace-period', + ); if (useGracePeriod) { if (this._lastMonitorNr !== currMonitorNr) { this._monitorNr = this._lastMonitorNr; let timerId = 0; - this._latestMonitorLockTimerId && GLib.Source.remove(this._latestMonitorLockTimerId); - this._latestMonitorLockTimerId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 150, () => { - // Only update the monitorNr, if the latest timer timed out. - if (timerId === this._latestMonitorLockTimerId) { - this._monitorNr = global.display.get_current_monitor(); - if (global.display.is_grabbed()) - this._edgeTilingPreview(window, grabOp); - } - this._latestMonitorLockTimerId = null; - return GLib.SOURCE_REMOVE; - }); + if (this._latestMonitorLockTimerId) { + GLib.Source.remove(this._latestMonitorLockTimerId); + } + + this._latestMonitorLockTimerId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 150, + () => { + // Only update the monitorNr, if the latest timer timed out. + if (timerId === this._latestMonitorLockTimerId) { + this._monitorNr = + global.display.get_current_monitor(); + if (global.display.is_grabbed()) { + this._edgeTilingPreview(window, grabOp); + } + } + + this._latestMonitorLockTimerId = null; + return GLib.SOURCE_REMOVE; + }, + ); timerId = this._latestMonitorLockTimerId; } } else { @@ -468,14 +515,20 @@ export default class TilingMoveHandler { this._lastMonitorNr = currMonitorNr; const wRect = window.get_frame_rect(); - const workArea = new Rect(window.get_work_area_for_monitor(this._monitorNr)); + const workArea = new Rect( + window.get_work_area_for_monitor(this._monitorNr), + ); const vDetectionSize = Settings.getInt('vertical-preview-area'); - const pointerAtTopEdge = this._lastPointerPos.y <= workArea.y + vDetectionSize; - const pointerAtBottomEdge = this._lastPointerPos.y >= workArea.y2 - vDetectionSize; + const pointerAtTopEdge = + this._lastPointerPos.y <= workArea.y + vDetectionSize; + const pointerAtBottomEdge = + this._lastPointerPos.y >= workArea.y2 - vDetectionSize; const hDetectionSize = Settings.getInt('horizontal-preview-area'); - const pointerAtLeftEdge = this._lastPointerPos.x <= workArea.x + hDetectionSize; - const pointerAtRightEdge = this._lastPointerPos.x >= workArea.x2 - hDetectionSize; + const pointerAtLeftEdge = + this._lastPointerPos.x <= workArea.x + hDetectionSize; + const pointerAtRightEdge = + this._lastPointerPos.x >= workArea.x2 - hDetectionSize; // Also use window's pos for top and bottom area detection for quarters // because global.get_pointer's y isn't accurate (no idea why...) when // grabbing the titlebar & slowly going from the left/right sides to @@ -483,71 +536,161 @@ export default class TilingMoveHandler { const titleBarGrabbed = this._lastPointerPos.y - wRect.y < 50; const windowAtTopEdge = titleBarGrabbed && wRect.y === workArea.y; const windowAtBottomEdge = wRect.y >= workArea.y2 - 75; - const tileTopLeftQuarter = pointerAtLeftEdge && (pointerAtTopEdge || windowAtTopEdge); - const tileTopRightQuarter = pointerAtRightEdge && (pointerAtTopEdge || windowAtTopEdge); - const tileBottomLeftQuarter = pointerAtLeftEdge && (pointerAtBottomEdge || windowAtBottomEdge); - const tileBottomRightQuarter = pointerAtRightEdge && (pointerAtBottomEdge || windowAtBottomEdge); + const tileTopLeftQuarter = + pointerAtLeftEdge && (pointerAtTopEdge || windowAtTopEdge); + const tileTopRightQuarter = + pointerAtRightEdge && (pointerAtTopEdge || windowAtTopEdge); + const tileBottomLeftQuarter = + pointerAtLeftEdge && (pointerAtBottomEdge || windowAtBottomEdge); + const tileBottomRightQuarter = + pointerAtRightEdge && (pointerAtBottomEdge || windowAtBottomEdge); if (tileTopLeftQuarter) { - this._tileRect = Twm.getTileFor('tile-topleft-quarter', workArea, this._monitorNr); - this._tilePreview.open(window, this._tileRect.meta, this._monitorNr); + this._tileRect = Twm.getTileFor( + 'tile-topleft-quarter', + workArea, + this._monitorNr, + ); + this._tilePreview.open( + window, + this._tileRect.meta, + this._monitorNr, + ); } else if (tileTopRightQuarter) { - this._tileRect = Twm.getTileFor('tile-topright-quarter', workArea, this._monitorNr); - this._tilePreview.open(window, this._tileRect.meta, this._monitorNr); + this._tileRect = Twm.getTileFor( + 'tile-topright-quarter', + workArea, + this._monitorNr, + ); + this._tilePreview.open( + window, + this._tileRect.meta, + this._monitorNr, + ); } else if (tileBottomLeftQuarter) { - this._tileRect = Twm.getTileFor('tile-bottomleft-quarter', workArea, this._monitorNr); - this._tilePreview.open(window, this._tileRect.meta, this._monitorNr); + this._tileRect = Twm.getTileFor( + 'tile-bottomleft-quarter', + workArea, + this._monitorNr, + ); + this._tilePreview.open( + window, + this._tileRect.meta, + this._monitorNr, + ); } else if (tileBottomRightQuarter) { - this._tileRect = Twm.getTileFor('tile-bottomright-quarter', workArea, this._monitorNr); - this._tilePreview.open(window, this._tileRect.meta, this._monitorNr); + this._tileRect = Twm.getTileFor( + 'tile-bottomright-quarter', + workArea, + this._monitorNr, + ); + this._tilePreview.open( + window, + this._tileRect.meta, + this._monitorNr, + ); } else if (pointerAtTopEdge) { // Switch between maximize & top tiling when keeping the mouse at the top edge. - const monitorRect = global.display.get_monitor_geometry(this._monitorNr); + const monitorRect = global.display.get_monitor_geometry( + this._monitorNr, + ); const isLandscape = monitorRect.width >= monitorRect.height; const shouldMaximize = - isLandscape && !Settings.getBoolean('enable-hold-maximize-inverse-landscape') || - !isLandscape && !Settings.getBoolean('enable-hold-maximize-inverse-portrait'); - const tileRect = shouldMaximize - ? workArea - : Twm.getTileFor('tile-top-half', workArea, this._monitorNr); - const holdTileRect = shouldMaximize - ? Twm.getTileFor('tile-top-half', workArea, this._monitorNr) - : workArea; + (isLandscape && + !Settings.getBoolean( + 'enable-hold-maximize-inverse-landscape', + )) || + (!isLandscape && + !Settings.getBoolean( + 'enable-hold-maximize-inverse-portrait', + )); + const tileRect = + shouldMaximize ? workArea : ( + Twm.getTileFor('tile-top-half', workArea, this._monitorNr) + ); + const holdTileRect = + shouldMaximize ? + Twm.getTileFor('tile-top-half', workArea, this._monitorNr) + : workArea; // Dont open preview / start new timer if preview was already one for the top - if (this._tilePreview._rect && - (holdTileRect.equal(this._tilePreview._rect) || - this._tilePreview._rect.equal(tileRect.meta))) + if ( + this._tilePreview._rect && + (holdTileRect.equal(this._tilePreview._rect) || + this._tilePreview._rect.equal(tileRect.meta)) + ) { return; + } this._tileRect = tileRect; - this._tilePreview.open(window, this._tileRect.meta, this._monitorNr); + this._tilePreview.open( + window, + this._tileRect.meta, + this._monitorNr, + ); let timerId = 0; - this._latestPreviewTimerId && GLib.Source.remove(this._latestPreviewTimerId); - this._latestPreviewTimerId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, - Settings.getInt('toggle-maximize-tophalf-timer'), () => { - // Only open the alternative preview, if the timeout-ed timer - // is the same as the one which started last - if (timerId === this._latestPreviewTimerId && + + if (this._latestPreviewTimerId) { + GLib.Source.remove(this._latestPreviewTimerId); + } + + this._latestPreviewTimerId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + Settings.getInt('toggle-maximize-tophalf-timer'), + () => { + // Only open the alternative preview, if the timeout-ed timer + // is the same as the one which started last + if ( + timerId === this._latestPreviewTimerId && this._tilePreview._showing && - this._tilePreview._rect.equal(tileRect.meta)) { + this._tilePreview._rect.equal(tileRect.meta) + ) { this._tileRect = holdTileRect; - this._tilePreview.open(window, this._tileRect.meta, this._monitorNr); + this._tilePreview.open( + window, + this._tileRect.meta, + this._monitorNr, + ); } this._latestPreviewTimerId = null; return GLib.SOURCE_REMOVE; - }); + }, + ); timerId = this._latestPreviewTimerId; } else if (pointerAtBottomEdge) { - this._tileRect = Twm.getTileFor('tile-bottom-half', workArea, this._monitorNr); - this._tilePreview.open(window, this._tileRect.meta, this._monitorNr); + this._tileRect = Twm.getTileFor( + 'tile-bottom-half', + workArea, + this._monitorNr, + ); + this._tilePreview.open( + window, + this._tileRect.meta, + this._monitorNr, + ); } else if (pointerAtLeftEdge) { - this._tileRect = Twm.getTileFor('tile-left-half', workArea, this._monitorNr); - this._tilePreview.open(window, this._tileRect.meta, this._monitorNr); + this._tileRect = Twm.getTileFor( + 'tile-left-half', + workArea, + this._monitorNr, + ); + this._tilePreview.open( + window, + this._tileRect.meta, + this._monitorNr, + ); } else if (pointerAtRightEdge) { - this._tileRect = Twm.getTileFor('tile-right-half', workArea, this._monitorNr); - this._tilePreview.open(window, this._tileRect.meta, this._monitorNr); + this._tileRect = Twm.getTileFor( + 'tile-right-half', + workArea, + this._monitorNr, + ); + this._tilePreview.open( + window, + this._tileRect.meta, + this._monitorNr, + ); } else { this._tileRect = null; this._tilePreview.close(); @@ -571,40 +714,59 @@ export default class TilingMoveHandler { } const screenRects = this._topTileGroup - .map(w => w.tiledRect) + .map((w) => w.tiledRect) .concat(this._freeScreenRects); - const hoveredRect = screenRects.find(r => r.containsPoint(this._lastPointerPos)); + const hoveredRect = screenRects.find((r) => + r.containsPoint(this._lastPointerPos), + ); if (!hoveredRect) { this._tilePreview.close(); this._tileRect = null; return; } - const isSuperPressed = Util.isModPressed(Clutter.ModifierType.MOD4_MASK); + const isSuperPressed = Util.isModPressed( + Clutter.ModifierType.MOD4_MASK, + ); if (isSuperPressed) { this._anchorRect = this._anchorRect ?? hoveredRect; this._tileRect = hoveredRect.union(this._anchorRect); this._splitRects.clear(); - this._tilePreview.open(window, this._tileRect.meta, this._monitorNr, { - x: this._tileRect.x, - y: this._tileRect.y, - width: this._tileRect.width, - height: this._tileRect.height, - opacity: 200 - }); + this._tilePreview.open( + window, + this._tileRect.meta, + this._monitorNr, + { + x: this._tileRect.x, + y: this._tileRect.y, + width: this._tileRect.width, + height: this._tileRect.height, + opacity: 200, + }, + ); } else { this._anchorRect = null; const edgeRadius = 50; - const atTopEdge = this._lastPointerPos.y < hoveredRect.y + edgeRadius; - const atBottomEdge = this._lastPointerPos.y > hoveredRect.y2 - edgeRadius; - const atLeftEdge = this._lastPointerPos.x < hoveredRect.x + edgeRadius; - const atRightEdge = this._lastPointerPos.x > hoveredRect.x2 - edgeRadius; - - atTopEdge || atBottomEdge || atLeftEdge || atRightEdge - ? this._adaptiveTilingPreviewGroup(window, hoveredRect, - { atTopEdge, atBottomEdge, atLeftEdge, atRightEdge }) - : this._adaptiveTilingPreviewSingle(window, hoveredRect); + const atTopEdge = + this._lastPointerPos.y < hoveredRect.y + edgeRadius; + const atBottomEdge = + this._lastPointerPos.y > hoveredRect.y2 - edgeRadius; + const atLeftEdge = + this._lastPointerPos.x < hoveredRect.x + edgeRadius; + const atRightEdge = + this._lastPointerPos.x > hoveredRect.x2 - edgeRadius; + + if (atTopEdge || atBottomEdge || atLeftEdge || atRightEdge) { + this._adaptiveTilingPreviewGroup(window, hoveredRect, { + atTopEdge, + atBottomEdge, + atLeftEdge, + atRightEdge, + }); + } else { + this._adaptiveTilingPreviewSingle(window, hoveredRect); + } } } @@ -621,40 +783,49 @@ export default class TilingMoveHandler { * @param {Rect} hoveredRect */ _adaptiveTilingPreviewSingle(window, hoveredRect) { - const atTop = this._lastPointerPos.y < hoveredRect.y + hoveredRect.height * .25; - const atBottom = this._lastPointerPos.y > hoveredRect.y + hoveredRect.height * .75; - const atRight = this._lastPointerPos.x > hoveredRect.x + hoveredRect.width * .75; - const atLeft = this._lastPointerPos.x < hoveredRect.x + hoveredRect.width * .25; + const atTop = + this._lastPointerPos.y < hoveredRect.y + hoveredRect.height * 0.25; + const atBottom = + this._lastPointerPos.y > hoveredRect.y + hoveredRect.height * 0.75; + const atRight = + this._lastPointerPos.x > hoveredRect.x + hoveredRect.width * 0.75; + const atLeft = + this._lastPointerPos.x < hoveredRect.x + hoveredRect.width * 0.25; const splitVertically = atTop || atBottom; const splitHorizontally = atLeft || atRight; if (splitHorizontally || splitVertically) { - const idx = atTop && !atRight || atLeft ? 0 : 1; - const size = splitHorizontally ? hoveredRect.width : hoveredRect.height; - const orientation = splitHorizontally ? Orientation.V : Orientation.H; + const idx = (atTop && !atRight) || atLeft ? 0 : 1; + const size = + splitHorizontally ? hoveredRect.width : hoveredRect.height; + const orientation = + splitHorizontally ? Orientation.V : Orientation.H; this._tileRect = hoveredRect.getUnitAt(idx, size / 2, orientation); } else { this._tileRect = hoveredRect.copy(); } - if (!this._tilePreview.needsUpdate(this._tileRect)) + if (!this._tilePreview.needsUpdate(this._tileRect)) { return; + } const monitor = global.display.get_current_monitor(); this._tilePreview.open(window, this._tileRect.meta, monitor); this._splitRects.clear(); - const hoveredWindow = this._topTileGroup.find(w => { + const hoveredWindow = this._topTileGroup.find((w) => { return w.tiledRect.containsPoint(this._lastPointerPos); }); - if (!hoveredWindow) + if (!hoveredWindow) { return; + } // Don't halve the window, if we compelety cover it i. e. // the user is hovering the tiled window at the center. - if (hoveredWindow.tiledRect.equal(this._tileRect)) + if (hoveredWindow.tiledRect.equal(this._tileRect)) { return; + } const splitRect = hoveredWindow.tiledRect.minus(this._tileRect)[0]; this._splitRects.set(hoveredWindow, splitRect); @@ -678,17 +849,41 @@ export default class TilingMoveHandler { // of the tileGroup via Rect.minus(). const smallestWindow = this._topTileGroup.reduce((smallest, w) => { if (hovered.atTopEdge) { - if (w.tiledRect.y === hoveredRect.y || w.tiledRect.y2 === hoveredRect.y) - return w.tiledRect.height < smallest.tiledRect.height ? w : smallest; + if ( + w.tiledRect.y === hoveredRect.y || + w.tiledRect.y2 === hoveredRect.y + ) { + return w.tiledRect.height < smallest.tiledRect.height ? + w + : smallest; + } } else if (hovered.atBottomEdge) { - if (w.tiledRect.y === hoveredRect.y2 || w.tiledRect.y2 === hoveredRect.y2) - return w.tiledRect.height < smallest.tiledRect.height ? w : smallest; + if ( + w.tiledRect.y === hoveredRect.y2 || + w.tiledRect.y2 === hoveredRect.y2 + ) { + return w.tiledRect.height < smallest.tiledRect.height ? + w + : smallest; + } } else if (hovered.atLeftEdge) { - if (w.tiledRect.x === hoveredRect.x || w.tiledRect.x2 === hoveredRect.x) - return w.tiledRect.width < smallest.tiledRect.width ? w : smallest; + if ( + w.tiledRect.x === hoveredRect.x || + w.tiledRect.x2 === hoveredRect.x + ) { + return w.tiledRect.width < smallest.tiledRect.width ? + w + : smallest; + } } else if (hovered.atRightEdge) { - if (w.tiledRect.x === hoveredRect.x2 || w.tiledRect.x2 === hoveredRect.x2) - return w.tiledRect.width < smallest.tiledRect.width ? w : smallest; + if ( + w.tiledRect.x === hoveredRect.x2 || + w.tiledRect.x2 === hoveredRect.x2 + ) { + return w.tiledRect.width < smallest.tiledRect.width ? + w + : smallest; + } } return smallest; @@ -700,75 +895,110 @@ export default class TilingMoveHandler { // determine the final size of the grabbed window. Use half of the size // factor, if we are at the screen edges. The cases for the bottom and // right screen edges are covered further down. - const factor = hovered.atLeftEdge && hoveredRect.x === workArea.x || - hovered.atTopEdge && hoveredRect.y === workArea.y - ? 1 / 3 - : 2 / 3; + const factor = + ( + (hovered.atLeftEdge && hoveredRect.x === workArea.x) || + (hovered.atTopEdge && hoveredRect.y === workArea.y) + ) ? + 1 / 3 + : 2 / 3; // The grabbed window will be horizontal. The horizontal size (x1 - x2) // is determined by the furthest left- and right-reaching windows that // align with the hovered rect. The vertical size (height) is a fraction // of the smallestWindow. if (hovered.atTopEdge || hovered.atBottomEdge) { - const getX1X2 = alignsAt => { - return this._topTileGroup.reduce((x1x2, w) => { - const currX = x1x2[0]; - const currX2 = x1x2[1]; - return alignsAt(w) - ? [Math.min(w.tiledRect.x, currX), Math.max(w.tiledRect.x2, currX2)] - : x1x2; - }, [hoveredRect.x, hoveredRect.x2]); + const getX1X2 = (alignsAt) => { + return this._topTileGroup.reduce( + (x1x2, w) => { + const currX = x1x2[0]; + const currX2 = x1x2[1]; + return alignsAt(w) ? + [ + Math.min(w.tiledRect.x, currX), + Math.max(w.tiledRect.x2, currX2), + ] + : x1x2; + }, + [hoveredRect.x, hoveredRect.x2], + ); }; - const alignTopEdge = w => { - return hoveredRect.y === w.tiledRect.y || - hoveredRect.y === w.tiledRect.y2; + const alignTopEdge = (w) => { + return ( + hoveredRect.y === w.tiledRect.y || + hoveredRect.y === w.tiledRect.y2 + ); }; - const alignBottomEdge = w => { - return hoveredRect.y2 === w.tiledRect.y2 || - hoveredRect.y2 === w.tiledRect.y; + const alignBottomEdge = (w) => { + return ( + hoveredRect.y2 === w.tiledRect.y2 || + hoveredRect.y2 === w.tiledRect.y + ); }; - const [x1, x2] = getX1X2(hovered.atTopEdge ? alignTopEdge : alignBottomEdge); + const [x1, x2] = getX1X2( + hovered.atTopEdge ? alignTopEdge : alignBottomEdge, + ); const size = Math.ceil(smallestWindow.tiledRect.height * factor); // Keep within workArea bounds. - const y = Math.max(workArea.y, Math.floor(hovered.atTopEdge - ? hoveredRect.y - size / 2 - : hoveredRect.y2 - size / 2 - )); + const y = Math.max( + workArea.y, + Math.floor( + hovered.atTopEdge ? + hoveredRect.y - size / 2 + : hoveredRect.y2 - size / 2, + ), + ); const height = Math.min(size, workArea.y2 - y); this._tileRect = new Rect(x1, y, x2 - x1, height); - // The grabbed window will be vertical. The vertical size (y1 - y2) is - // determined by the furthest top- and bottom-reaching windows that align - // with the hovered rect. The horizontal size (width) is a fraction of - // the smallestWindow. + // The grabbed window will be vertical. The vertical size (y1 - y2) is + // determined by the furthest top- and bottom-reaching windows that align + // with the hovered rect. The horizontal size (width) is a fraction of + // the smallestWindow. } else { - const getY1Y2 = alignsAt => { - return this._topTileGroup.reduce((y1y2, w) => { - const currY = y1y2[0]; - const currY2 = y1y2[1]; - return alignsAt(w) - ? [Math.min(w.tiledRect.y, currY), Math.max(w.tiledRect.y2, currY2)] - : y1y2; - }, [hoveredRect.y, hoveredRect.y2]); + const getY1Y2 = (alignsAt) => { + return this._topTileGroup.reduce( + (y1y2, w) => { + const currY = y1y2[0]; + const currY2 = y1y2[1]; + return alignsAt(w) ? + [ + Math.min(w.tiledRect.y, currY), + Math.max(w.tiledRect.y2, currY2), + ] + : y1y2; + }, + [hoveredRect.y, hoveredRect.y2], + ); }; - const alignLeftEdge = w => { - return hoveredRect.x === w.tiledRect.x || - hoveredRect.x === w.tiledRect.x2; + const alignLeftEdge = (w) => { + return ( + hoveredRect.x === w.tiledRect.x || + hoveredRect.x === w.tiledRect.x2 + ); }; - const alignRightEdge = w => { - return hoveredRect.x2 === w.tiledRect.x2 || - hoveredRect.x2 === w.tiledRect.x; + const alignRightEdge = (w) => { + return ( + hoveredRect.x2 === w.tiledRect.x2 || + hoveredRect.x2 === w.tiledRect.x + ); }; - const [y1, y2] = getY1Y2(hovered.atLeftEdge ? alignLeftEdge : alignRightEdge); + const [y1, y2] = getY1Y2( + hovered.atLeftEdge ? alignLeftEdge : alignRightEdge, + ); const size = Math.ceil(smallestWindow.tiledRect.width * factor); // Keep within workArea bounds. - const x = Math.max(workArea.x, Math.floor(hovered.atLeftEdge - ? hoveredRect.x - size / 2 - : hoveredRect.x2 - size / 2 - )); + const x = Math.max( + workArea.x, + Math.floor( + hovered.atLeftEdge ? + hoveredRect.x - size / 2 + : hoveredRect.x2 - size / 2, + ), + ); const width = Math.min(size, workArea.x2 - x); this._tileRect = new Rect(x, y1, width, y2 - y1); @@ -776,18 +1006,20 @@ export default class TilingMoveHandler { this._tileRect.tryAlignWith(workArea); - if (!this._tilePreview.needsUpdate(this._tileRect)) + if (!this._tilePreview.needsUpdate(this._tileRect)) { return; + } this._tilePreview.open(window, this._tileRect.meta, monitor); this._splitRects.clear(); - this._topTileGroup.forEach(w => { + this._topTileGroup.forEach((w) => { const leftOver = w.tiledRect.minus(this._tileRect); const splitRect = leftOver[0]; // w isn't an affected window. - if (splitRect?.equal(this._tileRect) ?? true) + if (splitRect?.equal(this._tileRect) ?? true) { return; + } this._splitRects.set(w, splitRect); }); @@ -796,7 +1028,9 @@ export default class TilingMoveHandler { _favoriteLayoutTilingPreview(window) { // Holding Super will make the window span multiple rects of the favorite // layout starting from the rect, which the user starting holding Super in. - const isSuperPressed = Util.isModPressed(Clutter.ModifierType.MOD4_MASK); + const isSuperPressed = Util.isModPressed( + Clutter.ModifierType.MOD4_MASK, + ); for (const rect of this._favoriteLayout) { if (rect.containsPoint(this._lastPointerPos)) { if (isSuperPressed) { @@ -807,13 +1041,18 @@ export default class TilingMoveHandler { this._anchorRect = null; } - this._tilePreview.open(window, this._tileRect.meta, this._monitorNr, { - x: this._tileRect.x, - y: this._tileRect.y, - width: this._tileRect.width, - height: this._tileRect.height, - opacity: 200 - }); + this._tilePreview.open( + window, + this._tileRect.meta, + this._monitorNr, + { + x: this._tileRect.x, + y: this._tileRect.y, + width: this._tileRect.width, + height: this._tileRect.height, + opacity: 200, + }, + ); return; } } @@ -824,71 +1063,92 @@ export default class TilingMoveHandler { } const TilePreview = GObject.registerClass( -class TilePreview extends WindowManager.TilePreview { - _init() { - super._init(); - this.set_style_class_name('tile-preview'); - } + class TilePreview extends WindowManager.TilePreview { + _init() { + super._init(); + this.set_style_class_name('tile-preview'); + } - needsUpdate(rect) { - return !this._rect || !rect.equal(this._rect); - } + needsUpdate(rect) { + return !this._rect || !rect.equal(this._rect); + } - // Added param for animation and removed style for rounded corners - open(window, tileRect, monitorIndex, animateTo = undefined) { - const windowActor = window.get_compositor_private(); - if (!windowActor) - return; + // Added param for animation and removed style for rounded corners + open(window, tileRect, monitorIndex, animateTo = undefined) { + const windowActor = window.get_compositor_private(); + if (!windowActor) { + return; + } - global.window_group.set_child_below_sibling(this, windowActor); + global.window_group.set_child_below_sibling(this, windowActor); - if (this._rect && this._rect.equal(tileRect)) - return; + if (this._rect && this._rect.equal(tileRect)) { + return; + } - const changeMonitor = this._monitorIndex === -1 || - this._monitorIndex !== monitorIndex; - - this._monitorIndex = monitorIndex; - this._rect = tileRect; - - const monitor = Main.layoutManager.monitors[monitorIndex]; - - if (!this._showing || changeMonitor) { - const monitorRect = new Mtk.Rectangle({ - x: monitor.x, - y: monitor.y, - width: monitor.width, - height: monitor.height - }); - const [, rect] = window.get_frame_rect().intersect(monitorRect); - this.set_size(rect.width, rect.height); - this.set_position(rect.x, rect.y); - this.opacity = 0; - } + const changeMonitor = + this._monitorIndex === -1 || + this._monitorIndex !== monitorIndex; - this._showing = true; - this.show(); - - if (!animateTo) { - animateTo = { - x: tileRect.x, - y: tileRect.y, - width: tileRect.width, - height: tileRect.height, - opacity: 255, - duration: WINDOW_ANIMATION_TIME, - mode: Clutter.AnimationMode.EASE_OUT_QUAD - }; - } else { - animateTo.x === undefined && this.set_x(tileRect.x); - animateTo.y === undefined && this.set_y(tileRect.y); - animateTo.width === undefined && this.set_width(tileRect.width); - animateTo.height === undefined && this.set_height(tileRect.height); - animateTo.opacity === undefined && this.set_opacity(255); - animateTo.duration = animateTo.duration ?? WINDOW_ANIMATION_TIME; - animateTo.mode = animateTo.mode ?? Clutter.AnimationMode.EASE_OUT_QUAD; - } + this._monitorIndex = monitorIndex; + this._rect = tileRect; - this.ease(animateTo); - } -}); + const monitor = Main.layoutManager.monitors[monitorIndex]; + + if (!this._showing || changeMonitor) { + const monitorRect = new Mtk.Rectangle({ + x: monitor.x, + y: monitor.y, + width: monitor.width, + height: monitor.height, + }); + const [, rect] = window.get_frame_rect().intersect(monitorRect); + this.set_size(rect.width, rect.height); + this.set_position(rect.x, rect.y); + this.opacity = 0; + } + + this._showing = true; + this.show(); + + if (!animateTo) { + animateTo = { + x: tileRect.x, + y: tileRect.y, + width: tileRect.width, + height: tileRect.height, + opacity: 255, + duration: WINDOW_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }; + } else { + if (animateTo.x === undefined) { + this.set_x(tileRect.x); + } + + if (animateTo.y === undefined) { + this.set_y(tileRect.y); + } + + if (animateTo.width === undefined) { + this.set_width(tileRect.width); + } + + if (animateTo.height === undefined) { + this.set_height(tileRect.height); + } + + if (animateTo.opacity === undefined) { + this.set_opacity(255); + } + + animateTo.duration = + animateTo.duration ?? WINDOW_ANIMATION_TIME; + animateTo.mode = + animateTo.mode ?? Clutter.AnimationMode.EASE_OUT_QUAD; + } + + this.ease(animateTo); + } + }, +); diff --git a/tiling-assistant@leleat-on-github/src/extension/resizeHandler.js b/tiling-assistant@leleat-on-github/src/extension/resizeHandler.js index 559e8f6..71438c8 100644 --- a/tiling-assistant@leleat-on-github/src/extension/resizeHandler.js +++ b/tiling-assistant@leleat-on-github/src/extension/resizeHandler.js @@ -1,15 +1,15 @@ -import { Clutter, Meta } from '../dependencies/gi.js'; +import {Clutter, Meta} from '../dependencies/gi.js'; -import { Orientation } from '../common.js'; -import { Rect, Util } from './utility.js'; -import { TilingWindowManager as Twm } from './tilingWindowManager.js'; +import {Orientation} from '../common.js'; +import {Rect, Util} from './utility.js'; +import {TilingWindowManager as Twm} from './tilingWindowManager.js'; const Side = { NONE: 0, SAME_H: 1, OPPOSING_H: 2, SAME_V: 4, - OPPOSING_V: 8 + OPPOSING_V: 8, }; /** @@ -21,7 +21,7 @@ const Side = { export default class TilingResizeHandler { constructor() { - const isResizing = grabOp => { + const isResizing = (grabOp) => { switch (grabOp) { case Meta.GrabOp.RESIZING_N: case Meta.GrabOp.RESIZING_NW: @@ -43,20 +43,22 @@ export default class TilingResizeHandler { (d, window, grabOp) => { grabOp &= ~1024; // META_GRAB_OP_WINDOW_FLAG_UNCONSTRAINED - if (window && isResizing(grabOp)) + if (window && isResizing(grabOp)) { this._onResizeStarted(window, grabOp); + } }, - this + this, ); global.display.connectObject( 'grab-op-end', (d, window, grabOp) => { grabOp &= ~1024; // META_GRAB_OP_WINDOW_FLAG_UNCONSTRAINED - if (window && isResizing(grabOp)) + if (window && isResizing(grabOp)) { this._onResizeFinished(window, grabOp); + } }, - this + this, ); this._preGrabRects = new Map(); @@ -72,22 +74,24 @@ export default class TilingResizeHandler { } _onResizeStarted(window, grabOp) { - if (!window.isTiled) + if (!window.isTiled) { return; + } // Use the same margin for the alignment and equality check below. const margin = 5; let topTileGroup = Twm.getTopTileGroup(); - topTileGroup.forEach(w => { + topTileGroup.forEach((w) => { this._preGrabRects.set(w, new Rect(w.get_frame_rect())); - if (w !== window) + if (w !== window) { // There is no snapping for tiled windows, if the user set a window // gap. So the windows may not align properly, if the user tried // to manually resize them to be edge to edge. In that case, assume // that windows that are within a certain margin distance to each // other are meant to align and resize them together. w.tiledRect.tryAlignWith(window.tiledRect, margin); + } }); // Windows can be part of multiple tile groups. We however only resize @@ -96,24 +100,30 @@ export default class TilingResizeHandler { // the resize op those windows will no longer match with the lower tile // group's tiles. So remove the shared windows from the lower tile group. const allWindows = Twm.getWindows(); - allWindows.forEach(w => { - if (!w.isTiled) + allWindows.forEach((w) => { + if (!w.isTiled) { return; + } - if (topTileGroup.includes(w)) + if (topTileGroup.includes(w)) { return; + } // Gets a tile group of windows without the ones // which are about to be resized const group = Twm.getTileGroupFor(w); const newGroup = group.reduce((gr, win) => { - !topTileGroup.includes(win) && gr.push(win); + if (!topTileGroup.includes(win)) { + gr.push(win); + } + return gr; }, []); // Tile groups are the same - if (group.length === newGroup.length) + if (group.length === newGroup.length) { return; + } // Remove old tile group and create new one Twm.clearTilingProps(w.get_id()); @@ -127,18 +137,25 @@ export default class TilingResizeHandler { // Holding Ctrl allows resizing windows which only directly (or transitively) // border the window being actively resized (instead of the entire tileGroup) - const isCtrlPressed = Util.isModPressed(Clutter.ModifierType.CONTROL_MASK); + const isCtrlPressed = Util.isModPressed( + Clutter.ModifierType.CONTROL_MASK, + ); const singleEdgeResizeOp = [ Meta.GrabOp.RESIZING_N, Meta.GrabOp.RESIZING_S, Meta.GrabOp.RESIZING_W, - Meta.GrabOp.RESIZING_E + Meta.GrabOp.RESIZING_E, ]; - if (isCtrlPressed && singleEdgeResizeOp.includes(grabOp)) - topTileGroup = this._getWindowsForIndividualResize(window, topTileGroup, grabOp); + if (isCtrlPressed && singleEdgeResizeOp.includes(grabOp)) { + topTileGroup = this._getWindowsForIndividualResize( + window, + topTileGroup, + grabOp, + ); + } switch (grabOp) { - // Resizing cardinal directions + // Resizing cardinal directions case Meta.GrabOp.RESIZING_N: for (const otherWindow of topTileGroup) { const otherRect = otherWindow.tiledRect; @@ -146,15 +163,17 @@ export default class TilingResizeHandler { Util.equal(grabbedRect.y, otherRect.y, margin), Util.equal(grabbedRect.y, otherRect.y2, margin), false, - false + false, ); - resizeOp && this._resizeOps.set(otherWindow, resizeOp); + if (resizeOp) { + this._resizeOps.set(otherWindow, resizeOp); + } } window.connectObject( 'size-changed', this._onResizing.bind(this, window, grabOp, null), - this + this, ); break; @@ -165,15 +184,17 @@ export default class TilingResizeHandler { Util.equal(grabbedRect.y2, otherRect.y2, margin), Util.equal(grabbedRect.y2, otherRect.y, margin), false, - false + false, ); - resizeOp && this._resizeOps.set(otherWindow, resizeOp); + if (resizeOp) { + this._resizeOps.set(otherWindow, resizeOp); + } } window.connectObject( 'size-changed', this._onResizing.bind(this, window, grabOp, null), - this + this, ); break; @@ -184,15 +205,17 @@ export default class TilingResizeHandler { false, false, Util.equal(grabbedRect.x2, otherRect.x2, margin), - Util.equal(grabbedRect.x2, otherRect.x, margin) + Util.equal(grabbedRect.x2, otherRect.x, margin), ); - resizeOp && this._resizeOps.set(otherWindow, resizeOp); + if (resizeOp) { + this._resizeOps.set(otherWindow, resizeOp); + } } window.connectObject( 'size-changed', this._onResizing.bind(this, window, null, grabOp), - this + this, ); break; @@ -203,19 +226,21 @@ export default class TilingResizeHandler { false, false, Util.equal(grabbedRect.x, otherRect.x, margin), - Util.equal(grabbedRect.x, otherRect.x2, margin) + Util.equal(grabbedRect.x, otherRect.x2, margin), ); - resizeOp && this._resizeOps.set(otherWindow, resizeOp); + if (resizeOp) { + this._resizeOps.set(otherWindow, resizeOp); + } } window.connectObject( 'size-changed', this._onResizing.bind(this, window, null, grabOp), - this + this, ); break; - // Resizing intercardinal directions: + // Resizing intercardinal directions: case Meta.GrabOp.RESIZING_NW: for (const otherWindow of topTileGroup) { const otherRect = otherWindow.tiledRect; @@ -223,15 +248,22 @@ export default class TilingResizeHandler { Util.equal(grabbedRect.y, otherRect.y, margin), Util.equal(grabbedRect.y, otherRect.y2, margin), Util.equal(grabbedRect.x, otherRect.x, margin), - Util.equal(grabbedRect.x, otherRect.x2, margin) + Util.equal(grabbedRect.x, otherRect.x2, margin), ); - resizeOp && this._resizeOps.set(otherWindow, resizeOp); + if (resizeOp) { + this._resizeOps.set(otherWindow, resizeOp); + } } window.connectObject( 'size-changed', - this._onResizing.bind(this, window, Meta.GrabOp.RESIZING_N, Meta.GrabOp.RESIZING_W), - this + this._onResizing.bind( + this, + window, + Meta.GrabOp.RESIZING_N, + Meta.GrabOp.RESIZING_W, + ), + this, ); break; @@ -242,15 +274,22 @@ export default class TilingResizeHandler { Util.equal(grabbedRect.y, otherRect.y, margin), Util.equal(grabbedRect.y, otherRect.y2, margin), Util.equal(grabbedRect.x2, otherRect.x2, margin), - Util.equal(grabbedRect.x2, otherRect.x, margin) + Util.equal(grabbedRect.x2, otherRect.x, margin), ); - resizeOp && this._resizeOps.set(otherWindow, resizeOp); + if (resizeOp) { + this._resizeOps.set(otherWindow, resizeOp); + } } window.connectObject( 'size-changed', - this._onResizing.bind(this, window, Meta.GrabOp.RESIZING_N, Meta.GrabOp.RESIZING_E), - this + this._onResizing.bind( + this, + window, + Meta.GrabOp.RESIZING_N, + Meta.GrabOp.RESIZING_E, + ), + this, ); break; @@ -261,15 +300,22 @@ export default class TilingResizeHandler { Util.equal(grabbedRect.y2, otherRect.y2, margin), Util.equal(grabbedRect.y2, otherRect.y, margin), Util.equal(grabbedRect.x, otherRect.x, margin), - Util.equal(grabbedRect.x, otherRect.x2, margin) + Util.equal(grabbedRect.x, otherRect.x2, margin), ); - resizeOp && this._resizeOps.set(otherWindow, resizeOp); + if (resizeOp) { + this._resizeOps.set(otherWindow, resizeOp); + } } window.connectObject( 'size-changed', - this._onResizing.bind(this, window, Meta.GrabOp.RESIZING_S, Meta.GrabOp.RESIZING_W), - this + this._onResizing.bind( + this, + window, + Meta.GrabOp.RESIZING_S, + Meta.GrabOp.RESIZING_W, + ), + this, ); break; @@ -280,15 +326,22 @@ export default class TilingResizeHandler { Util.equal(grabbedRect.y2, otherRect.y2, margin), Util.equal(grabbedRect.y2, otherRect.y, margin), Util.equal(grabbedRect.x2, otherRect.x2, margin), - Util.equal(grabbedRect.x2, otherRect.x, margin) + Util.equal(grabbedRect.x2, otherRect.x, margin), ); - resizeOp && this._resizeOps.set(otherWindow, resizeOp); + if (resizeOp) { + this._resizeOps.set(otherWindow, resizeOp); + } } window.connectObject( 'size-changed', - this._onResizing.bind(this, window, Meta.GrabOp.RESIZING_S, Meta.GrabOp.RESIZING_E), - this + this._onResizing.bind( + this, + window, + Meta.GrabOp.RESIZING_S, + Meta.GrabOp.RESIZING_E, + ), + this, ); } } @@ -297,16 +350,19 @@ export default class TilingResizeHandler { _onResizeFinished(window, grabOp) { window.disconnectObject(this); - if (!window.isTiled) + if (!window.isTiled) { return; + } const monitor = window.get_monitor(); - const screenTopGap = Util.useIndividualGaps(monitor) - ? Util.getScaledGap('screen-top-gap', monitor) - : Util.getScaledGap('single-screen-gap', monitor); - const screenLeftGap = Util.useIndividualGaps(monitor) - ? Util.getScaledGap('screen-left-gap', monitor) - : Util.getScaledGap('single-screen-gap', monitor); + const screenTopGap = + Util.useIndividualGaps(monitor) ? + Util.getScaledGap('screen-top-gap', monitor) + : Util.getScaledGap('single-screen-gap', monitor); + const screenLeftGap = + Util.useIndividualGaps(monitor) ? + Util.getScaledGap('screen-left-gap', monitor) + : Util.getScaledGap('single-screen-gap', monitor); const windowGap = Util.getScaledGap('window-gap', monitor); const workArea = window.get_work_area_for_monitor(monitor); @@ -319,37 +375,53 @@ export default class TilingResizeHandler { const isResizingW = (grabOp & Meta.GrabOp.RESIZING_W) > 1; // Shift the tiledRect by the resize amount - let newGrabbedTiledRectX = window.tiledRect.x + (grabbedsNewRect.x - grabbedsOldRect.x); + let newGrabbedTiledRectX = + window.tiledRect.x + (grabbedsNewRect.x - grabbedsOldRect.x); // Switch the screenGap for a windowGap - if (isResizingW && window.tiledRect.x === workArea.x) - newGrabbedTiledRectX = newGrabbedTiledRectX + screenLeftGap - windowGap / 2; + if (isResizingW && window.tiledRect.x === workArea.x) { + newGrabbedTiledRectX = + newGrabbedTiledRectX + screenLeftGap - windowGap / 2; + } // Same as W but different orientation const isResizingN = (grabOp & Meta.GrabOp.RESIZING_N) > 1; - let newGrabbedTiledRectY = window.tiledRect.y + (grabbedsNewRect.y - grabbedsOldRect.y); - if (isResizingN && window.tiledRect.y === workArea.y) - newGrabbedTiledRectY = newGrabbedTiledRectY + screenTopGap - windowGap / 2; + let newGrabbedTiledRectY = + window.tiledRect.y + (grabbedsNewRect.y - grabbedsOldRect.y); + if (isResizingN && window.tiledRect.y === workArea.y) { + newGrabbedTiledRectY = + newGrabbedTiledRectY + screenTopGap - windowGap / 2; + } // If resizing on the E side, you can simply rely on get_frame_rect's // new width else x2 should stick to where it was (manual calc due // special cases like gnome-terminal) const isResizingE = (grabOp & Meta.GrabOp.RESIZING_E) > 1; - const newGrabbedTiledRectWidth = isResizingE - ? grabbedsNewRect.width + windowGap / 2 + (workArea.x === newGrabbedTiledRectX ? screenLeftGap : windowGap / 2) - : window.tiledRect.x2 - newGrabbedTiledRectX; + const newGrabbedTiledRectWidth = + isResizingE ? + grabbedsNewRect.width + + windowGap / 2 + + (workArea.x === newGrabbedTiledRectX ? + screenLeftGap + : windowGap / 2) + : window.tiledRect.x2 - newGrabbedTiledRectX; // Same principal applies to the height and resizing on the S side const isResizingS = (grabOp & Meta.GrabOp.RESIZING_S) > 1; - const newGrabbedTiledRectHeight = isResizingS - ? grabbedsNewRect.height + windowGap / 2 + (workArea.y === newGrabbedTiledRectY ? screenTopGap : windowGap / 2) - : window.tiledRect.y2 - newGrabbedTiledRectY; + const newGrabbedTiledRectHeight = + isResizingS ? + grabbedsNewRect.height + + windowGap / 2 + + (workArea.y === newGrabbedTiledRectY ? + screenTopGap + : windowGap / 2) + : window.tiledRect.y2 - newGrabbedTiledRectY; const grabbedsOldTiledRect = window.tiledRect; window.tiledRect = new Rect( newGrabbedTiledRectX, newGrabbedTiledRectY, newGrabbedTiledRectWidth, - newGrabbedTiledRectHeight + newGrabbedTiledRectHeight, ); Twm.saveTileState(window); @@ -359,12 +431,15 @@ export default class TilingResizeHandler { // and after the grab. const tiledRectDiffX = window.tiledRect.x - grabbedsOldTiledRect.x; const tiledRectDiffY = window.tiledRect.y - grabbedsOldTiledRect.y; - const tiledRectDiffWidth = window.tiledRect.width - grabbedsOldTiledRect.width; - const tiledRectDiffHeight = window.tiledRect.height - grabbedsOldTiledRect.height; + const tiledRectDiffWidth = + window.tiledRect.width - grabbedsOldTiledRect.width; + const tiledRectDiffHeight = + window.tiledRect.height - grabbedsOldTiledRect.height; this._resizeOps.forEach((resizeOp, win) => { - if (win === window) + if (win === window) { return; + } if (resizeOp.side & Side.SAME_H) { win.tiledRect.x += tiledRectDiffX; @@ -391,31 +466,55 @@ export default class TilingResizeHandler { _onResizing(resizedWindow, grabOpV, grabOpH) { this._resizeOps.forEach((resizeOp, window) => { - const rectV = this._getPassiveResizedRect(grabOpV, resizedWindow, window, - resizeOp.side & Side.SAME_V, resizeOp.side & Side.OPPOSING_V); - - const rectH = this._getPassiveResizedRect(grabOpH, resizedWindow, window, - resizeOp.side & Side.SAME_H, resizeOp.side & Side.OPPOSING_H); - - if (rectV && rectH) - window.move_resize_frame(false, rectH[0], rectV[1], rectH[2], rectV[3]); - else if (rectV) + const rectV = this._getPassiveResizedRect( + grabOpV, + resizedWindow, + window, + resizeOp.side & Side.SAME_V, + resizeOp.side & Side.OPPOSING_V, + ); + + const rectH = this._getPassiveResizedRect( + grabOpH, + resizedWindow, + window, + resizeOp.side & Side.SAME_H, + resizeOp.side & Side.OPPOSING_H, + ); + + if (rectV && rectH) { + window.move_resize_frame( + false, + rectH[0], + rectV[1], + rectH[2], + rectV[3], + ); + } else if (rectV) { window.move_resize_frame(false, ...rectV); - else if (rectH) + } else if (rectH) { window.move_resize_frame(false, ...rectH); + } }); } // Gets the rect for the non-grabbed window adapted to the resized // grabbed window *but* only adapted for 1 side (either vertically // or horizontally) at a time based on grabOp - _getPassiveResizedRect(grabOp, resizedWindow, window, - resizeOnSameSide, resizeOnOpposingSide) { - if (!grabOp) + _getPassiveResizedRect( + grabOp, + resizedWindow, + window, + resizeOnSameSide, + resizeOnOpposingSide, + ) { + if (!grabOp) { return null; + } - if (!resizeOnSameSide && !resizeOnOpposingSide) + if (!resizeOnSameSide && !resizeOnOpposingSide) { return null; + } const resizedRect = new Rect(resizedWindow.get_frame_rect()); const wRect = new Rect(window.get_frame_rect()); @@ -424,25 +523,67 @@ export default class TilingResizeHandler { switch (grabOp) { case Meta.GrabOp.RESIZING_N: - return resizeOnSameSide - ? [wRect.x, resizedRect.y, wRect.width, preGrabRect.y2 - resizedRect.y] - : [wRect.x, wRect.y, wRect.width, resizedRect.y - wRect.y - windowGap]; + return resizeOnSameSide ? + [ + wRect.x, + resizedRect.y, + wRect.width, + preGrabRect.y2 - resizedRect.y, + ] + : [ + wRect.x, + wRect.y, + wRect.width, + resizedRect.y - wRect.y - windowGap, + ]; case Meta.GrabOp.RESIZING_S: - return resizeOnSameSide - ? [wRect.x, wRect.y, wRect.width, resizedRect.y2 - preGrabRect.y] - : [wRect.x, resizedRect.y2 + windowGap, wRect.width, preGrabRect.y2 - resizedRect.y2 - windowGap]; + return resizeOnSameSide ? + [ + wRect.x, + wRect.y, + wRect.width, + resizedRect.y2 - preGrabRect.y, + ] + : [ + wRect.x, + resizedRect.y2 + windowGap, + wRect.width, + preGrabRect.y2 - resizedRect.y2 - windowGap, + ]; case Meta.GrabOp.RESIZING_W: - return resizeOnSameSide - ? [resizedRect.x, wRect.y, preGrabRect.x2 - resizedRect.x, wRect.height] - : [wRect.x, wRect.y, resizedRect.x - wRect.x - windowGap, wRect.height]; + return resizeOnSameSide ? + [ + resizedRect.x, + wRect.y, + preGrabRect.x2 - resizedRect.x, + wRect.height, + ] + : [ + wRect.x, + wRect.y, + resizedRect.x - wRect.x - windowGap, + wRect.height, + ]; case Meta.GrabOp.RESIZING_E: - return resizeOnSameSide - ? [wRect.x, wRect.y, resizedRect.x2 - preGrabRect.x, wRect.height] - : [resizedRect.x2 + windowGap, wRect.y, preGrabRect.x2 - resizedRect.x2 - windowGap, wRect.height]; + return resizeOnSameSide ? + [ + wRect.x, + wRect.y, + resizedRect.x2 - preGrabRect.x, + wRect.height, + ] + : [ + resizedRect.x2 + windowGap, + wRect.y, + preGrabRect.x2 - resizedRect.x2 - windowGap, + wRect.height, + ]; } + + return null; } /** @@ -461,20 +602,31 @@ export default class TilingResizeHandler { const sameSide = [window]; // Resizes on the opposite side as the one being resized by the user const oppositeSide = []; - const resizeIsNOrW = [Meta.GrabOp.RESIZING_N, Meta.GrabOp.RESIZING_W].includes(grabOp); - const orientation = [Meta.GrabOp.RESIZING_N, Meta.GrabOp.RESIZING_S].includes(grabOp) - ? Orientation.V : Orientation.H; + const resizeIsNOrW = [ + Meta.GrabOp.RESIZING_N, + Meta.GrabOp.RESIZING_W, + ].includes(grabOp); + const orientation = + [Meta.GrabOp.RESIZING_N, Meta.GrabOp.RESIZING_S].includes(grabOp) ? + Orientation.V + : Orientation.H; // Checks if the w1 and w2 border each other at a certain edge. const borders = (w1, w2, w1IsAfterW2) => { - const [start, end] = orientation === Orientation.H ? ['x', 'x2'] : ['y', 'y2']; - const overlap = orientation === Orientation.H ? 'vertOverlap' : 'horizOverlap'; + const [start, end] = + orientation === Orientation.H ? ['x', 'x2'] : ['y', 'y2']; + const overlap = + orientation === Orientation.H ? 'vertOverlap' : 'horizOverlap'; if (w1IsAfterW2) { - return w1.tiledRect[start] === w2.tiledRect[end] && - w1.tiledRect[overlap](w2.tiledRect); + return ( + w1.tiledRect[start] === w2.tiledRect[end] && + w1.tiledRect[overlap](w2.tiledRect) + ); } else { - return w1.tiledRect[end] === w2.tiledRect[start] && - w1.tiledRect[overlap](w2.tiledRect); + return ( + w1.tiledRect[end] === w2.tiledRect[start] && + w1.tiledRect[overlap](w2.tiledRect) + ); } }; @@ -489,14 +641,19 @@ export default class TilingResizeHandler { * bordering is checked on. It's the relation of the checkingWindows * to the actively resized windows. */ - const findBorderingWindows = (uncheckedWindows, checkingWindows, - borderingWindows, sideDeterminant) => { + const findBorderingWindows = ( + uncheckedWindows, + checkingWindows, + borderingWindows, + sideDeterminant, + ) => { const oldCount = borderingWindows.length; - checkingWindows.forEach(w => { - uncheckedWindows.forEach(unchecked => { - if (borders(w, unchecked, sideDeterminant)) + checkingWindows.forEach((w) => { + uncheckedWindows.forEach((unchecked) => { + if (borders(w, unchecked, sideDeterminant)) { borderingWindows.push(unchecked); + } }); }); @@ -505,10 +662,12 @@ export default class TilingResizeHandler { // flipping the checkingWindows and borderingWindows arrays as well as // the side that is checked with the borders function. findBorderingWindows( - uncheckedWindows.filter(w => !borderingWindows.includes(w)), + uncheckedWindows.filter( + (w) => !borderingWindows.includes(w), + ), borderingWindows, checkingWindows, - !sideDeterminant + !sideDeterminant, ); } }; @@ -542,20 +701,26 @@ const ResizeOp = class ResizeOp { * @param {boolean} resizeOnOpposingSideH * @returns {ResizeOp|null} */ - static createResizeOp(resizeOnSameSideV, resizeOnOpposingSideV, - resizeOnSameSideH, resizeOnOpposingSideH) { + static createResizeOp( + resizeOnSameSideV, + resizeOnOpposingSideV, + resizeOnSameSideH, + resizeOnOpposingSideH, + ) { let verticalResizeSide = Side.NONE; let horizontalResizeSide = Side.NONE; - if (resizeOnSameSideV) + if (resizeOnSameSideV) { verticalResizeSide = Side.SAME_V; - else if (resizeOnOpposingSideV) + } else if (resizeOnOpposingSideV) { verticalResizeSide = Side.OPPOSING_V; + } - if (resizeOnSameSideH) + if (resizeOnSameSideH) { horizontalResizeSide = Side.SAME_H; - else if (resizeOnOpposingSideH) + } else if (resizeOnOpposingSideH) { horizontalResizeSide = Side.OPPOSING_H; + } const resizeSide = verticalResizeSide | horizontalResizeSide; return resizeSide ? new ResizeOp(resizeSide) : null; diff --git a/tiling-assistant@leleat-on-github/src/extension/tileEditingMode.js b/tiling-assistant@leleat-on-github/src/extension/tileEditingMode.js index 8d454ce..d725b28 100644 --- a/tiling-assistant@leleat-on-github/src/extension/tileEditingMode.js +++ b/tiling-assistant@leleat-on-github/src/extension/tileEditingMode.js @@ -1,9 +1,9 @@ -import { Clutter, GObject, Meta, St } from '../dependencies/gi.js'; -import { _, Main } from '../dependencies/shell.js'; +import {Clutter, GObject, Meta, St} from '../dependencies/gi.js'; +import {_, Main} from '../dependencies/shell.js'; -import { Direction, Orientation, Settings } from '../common.js'; -import { Rect, Util } from './utility.js'; -import { TilingWindowManager as Twm } from './tilingWindowManager.js'; +import {Direction, Orientation, Settings} from '../common.js'; +import {Rect, Util} from './utility.js'; +import {TilingWindowManager as Twm} from './tilingWindowManager.js'; const SCALE_SIZE = 100; const Modes = { @@ -11,7 +11,7 @@ const Modes = { SWAP: 2, RESIZE: 4, MOVE: 8, - CLOSE: 16 + CLOSE: 16, }; /** @@ -23,216 +23,247 @@ const Modes = { */ export const TileEditor = GObject.registerClass( -class TileEditingMode extends St.Widget { - _init() { - super._init({ reactive: true }); - - this._haveModal = false; - // The windows managed by the Tile Editor, that means the tiled windows - // that aren't overlapped by other windows; in other words: the top tile Group - this._windows = []; - // Indicate the active selection by the user. Added to `this`. - this._selectIndicator = null; - this._mode = Modes.DEFAULT; - // Handler of keyboard events depending on the mode. - this._keyHandler = null; - - Main.uiGroup.add_child(this); - - this.connect('key-press-event', (__, event) => - this._onKeyPressEvent(event)); - } + class TileEditingMode extends St.Widget { + _init() { + super._init({reactive: true}); + + this._haveModal = false; + // The windows managed by the Tile Editor, that means the tiled windows + // that aren't overlapped by other windows; in other words: the top tile Group + this._windows = []; + // Indicate the active selection by the user. Added to `this`. + this._selectIndicator = null; + this._mode = Modes.DEFAULT; + // Handler of keyboard events depending on the mode. + this._keyHandler = null; + + Main.uiGroup.add_child(this); + + this.connect('key-press-event', (__, event) => + this._onKeyPressEvent(event), + ); + } + + open() { + this._windows = Twm.getTopTileGroup(); + + const grab = Main.pushModal(this); + // We expect at least a keyboard grab here + if ((grab.get_seat_state() & Clutter.GrabState.KEYBOARD) === 0) { + Main.popModal(grab); + return false; + } + + this._grab = grab; + this._haveModal = true; - open() { - this._windows = Twm.getTopTileGroup(); + const openWindows = Twm.getWindows(); + if (!openWindows.length || !this._windows.length) { + // Translators: This is a notification that pops up if the user tries to enter the Tile Editing Mode via a keyboard shortcut. + const msg = _( + "Can't enter 'Tile Editing Mode', if no tiled window is visible.", + ); + Main.notify('Tiling Assistant', msg); + this.close(); + return false; + } + + this.monitor = this._windows[0].get_monitor(); + const display = global.display.get_monitor_geometry(this.monitor); + this.set_position(display.x, display.y); + this.set_size(display.width, display.height); + + // Enter initial state. + this._mode = Modes.DEFAULT; + this._keyHandler = new DefaultKeyHandler(this); + + // The windows may not be at the foreground. They just weren't + // overlapping other windows. So raise the entire tile group. + this._windows.forEach((w) => { + if (w.raise_and_make_recent_on_workspace) { + w.raise_and_make_recent_on_workspace( + global.workspace_manager.get_active_workspace(), + ); + } else { + w.raise_and_make_recent(); + } + }); + + // Create the active selection indicator. + const window = this._windows[0]; + const params = {style_class: 'tile-preview'}; + this._selectIndicator = new Indicator( + window.tiledRect, + this.monitor, + params, + ); + this._selectIndicator.focus(window.tiledRect, window); + this.add_child(this._selectIndicator); - const grab = Main.pushModal(this); - // We expect at least a keyboard grab here - if ((grab.get_seat_state() & Clutter.GrabState.KEYBOARD) === 0) { - Main.popModal(grab); - return false; + return true; } - this._grab = grab; - this._haveModal = true; + close() { + if (this._haveModal) { + Main.popModal(this._grab); + this._haveModal = false; + } + + this._windows = []; + this._keyHandler = null; + + // this._selectIndicator may be undefined, if Tile Editing Mode is + // left as soon as it's entered (e. g. when there's no tile group). + if (this._selectIndicator) { + this._selectIndicator.window?.activate( + global.get_current_time(), + ); + this._selectIndicator.ease({ + x: this._selectIndicator.x + SCALE_SIZE / 2, + y: this._selectIndicator.y + SCALE_SIZE / 2, + width: this._selectIndicator.width - SCALE_SIZE, + height: this._selectIndicator.height - SCALE_SIZE, + opacity: 0, + duration: 150, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => this.destroy(), + }); + } else { + this.destroy(); + } + } - const openWindows = Twm.getWindows(); - if (!openWindows.length || !this._windows.length) { - // Translators: This is a notification that pops up if the user tries to enter the Tile Editing Mode via a keyboard shortcut. - const msg = _("Can't enter 'Tile Editing Mode', if no tiled window is visible."); - Main.notify('Tiling Assistant', msg); + vfunc_button_press_event() { + this._keyHandler.prepareLeave(); this.close(); - return; } - this.monitor = this._windows[0].get_monitor(); - const display = global.display.get_monitor_geometry(this.monitor); - this.set_position(display.x, display.y); - this.set_size(display.width, display.height); - - // Enter initial state. - this._mode = Modes.DEFAULT; - this._keyHandler = new DefaultKeyHandler(this); - - // The windows may not be at the foreground. They just weren't - // overlapping other windows. So raise the entire tile group. - this._windows.forEach(w => { - if (w.raise_and_make_recent_on_workspace) - w.raise_and_make_recent_on_workspace(global.workspace_manager.get_active_workspace()); - else - w.raise_and_make_recent(); - }); + async _onKeyPressEvent(keyEvent) { + const mods = keyEvent.get_state(); + let newMode; - // Create the active selection indicator. - const window = this._windows[0]; - const params = { style_class: 'tile-preview' }; - this._selectIndicator = new Indicator(window.tiledRect, this.monitor, params); - this._selectIndicator.focus(window.tiledRect, window); - this.add_child(this._selectIndicator); - } + // Swap windows + if (mods & Clutter.ModifierType.CONTROL_MASK) { + newMode = Modes.SWAP; + } + // Move group to different workspace / monitor + else if (mods & Clutter.ModifierType.SHIFT_MASK) { + newMode = Modes.MOVE; + } + // Resize windows + else if (mods & Clutter.ModifierType.MOD4_MASK) { + newMode = Modes.RESIZE; + } + // Default keys + else { + newMode = Modes.DEFAULT; + } - close() { - if (this._haveModal) { - Main.popModal(this._grab); - this._haveModal = false; - } + // First switch mode, if a new mod is pressed. + if (newMode !== this._mode) { + this._switchMode(newMode); + } - this._windows = []; - this._keyHandler = null; - - // this._selectIndicator may be undefined, if Tile Editing Mode is - // left as soon as it's entered (e. g. when there's no tile group). - this._selectIndicator?.window?.activate(global.get_current_time()); - this._selectIndicator?.ease({ - x: this._selectIndicator.x + SCALE_SIZE / 2, - y: this._selectIndicator.y + SCALE_SIZE / 2, - width: this._selectIndicator.width - SCALE_SIZE, - height: this._selectIndicator.height - SCALE_SIZE, - opacity: 0, - duration: 150, - mode: Clutter.AnimationMode.EASE_OUT_QUAD, - onComplete: () => this.destroy() - }) ?? this.destroy(); - } + // Handle the key press and get mode depending on that. + newMode = await this._keyHandler.handleKeyPress(keyEvent); - vfunc_button_press_event() { - this._keyHandler.prepareLeave(); - this.close(); - } + if (newMode && newMode !== this._mode) { + this._switchMode(newMode); + } + } - async _onKeyPressEvent(keyEvent) { - const mods = keyEvent.get_state(); - let newMode; - - // Swap windows - if (mods & Clutter.ModifierType.CONTROL_MASK) - newMode = Modes.SWAP; - // Move group to different workspace / monitor - else if (mods & Clutter.ModifierType.SHIFT_MASK) - newMode = Modes.MOVE; - // Resize windows - else if (mods & Clutter.ModifierType.MOD4_MASK) - newMode = Modes.RESIZE; - // Default keys - else - newMode = Modes.DEFAULT; - - // First switch mode, if a new mod is pressed. - if (newMode !== this._mode) - this._switchMode(newMode); - - // Handle the key press and get mode depending on that. - newMode = await this._keyHandler.handleKeyPress(keyEvent); - - if (newMode && newMode !== this._mode) - this._switchMode(newMode); - } + vfunc_key_release_event(keyEvent) { + const newMode = this._keyHandler.handleKeyRelease(keyEvent); + if (newMode && newMode !== this._mode) { + this._switchMode(newMode); + } + } - vfunc_key_release_event(keyEvent) { - const newMode = this._keyHandler.handleKeyRelease(keyEvent); - if (newMode && newMode !== this._mode) - this._switchMode(newMode); - } + _switchMode(newMode) { + if (!newMode) { + return; + } - _switchMode(newMode) { - if (!newMode) - return; + this._mode = newMode; + this._keyHandler.prepareLeave(); - this._mode = newMode; - this._keyHandler.prepareLeave(); - - switch (newMode) { - case Modes.DEFAULT: - this._keyHandler = new DefaultKeyHandler(this); - break; - case Modes.SWAP: - this._keyHandler = new SwapKeyHandler(this); - break; - case Modes.MOVE: - this._keyHandler = new MoveKeyHandler(this); - break; - case Modes.RESIZE: - this._keyHandler = new ResizeKeyHandler(this); - break; - case Modes.CLOSE: - this.close(); + switch (newMode) { + case Modes.DEFAULT: + this._keyHandler = new DefaultKeyHandler(this); + break; + case Modes.SWAP: + this._keyHandler = new SwapKeyHandler(this); + break; + case Modes.MOVE: + this._keyHandler = new MoveKeyHandler(this); + break; + case Modes.RESIZE: + this._keyHandler = new ResizeKeyHandler(this); + break; + case Modes.CLOSE: + this.close(); + } } - } -}); + }, +); /** * Indicate the user selection or other stuff. */ -const Indicator = GObject.registerClass(class TileEditingModeIndicator extends St.Widget { - /** - * @param {string} widgetParams - * @param {Rect} rect the final rect / pos of the indicator - * @param {number} monitor - */ - _init(rect, monitor, widgetParams = {}) { - // Start from a scaled down position. - super._init({ - ...widgetParams, - x: rect.x + SCALE_SIZE / 2, - y: rect.y + SCALE_SIZE / 2, - width: rect.width - SCALE_SIZE, - height: rect.height - SCALE_SIZE, - opacity: 0 - }); +const Indicator = GObject.registerClass( + class TileEditingModeIndicator extends St.Widget { + /** + * @param {Rect} rect the final rect / pos of the indicator + * @param {number} monitor + * @param {object} [widgetParams] + */ + _init(rect, monitor, widgetParams = {}) { + // Start from a scaled down position. + super._init({ + ...widgetParams, + x: rect.x + SCALE_SIZE / 2, + y: rect.y + SCALE_SIZE / 2, + width: rect.width - SCALE_SIZE, + height: rect.height - SCALE_SIZE, + opacity: 0, + }); - this.rect = null; - this.window = null; - this._monitor = monitor; - } + this.rect = null; + this.window = null; + this._monitor = monitor; + } - /** - * Animate the indicator to a specific position. - * - * @param {Rect} rect the position the indicator will animate to. - * @param {Meta.Window|null} window the window at `rect`'s position. - */ - focus(rect, window = null) { - const display = global.display.get_monitor_geometry(this._monitor); - const activeWs = global.workspace_manager.get_active_workspace(); - const workArea = new Rect(activeWs.get_work_area_for_monitor(this._monitor)); - - // Adjusted for window / screen gaps - const { x, y, width, height } = rect.addGaps(workArea); - - this.ease({ - x: x - display.x, - y: y - display.y, - width, - height, - opacity: 255, - duration: 150, - mode: Clutter.AnimationMode.EASE_OUT_QUAD - }); + /** + * Animate the indicator to a specific position. + * + * @param {Rect} rect the position the indicator will animate to. + * @param {Meta.Window|null} window the window at `rect`'s position. + */ + focus(rect, window = null) { + const display = global.display.get_monitor_geometry(this._monitor); + const activeWs = global.workspace_manager.get_active_workspace(); + const workArea = new Rect( + activeWs.get_work_area_for_monitor(this._monitor), + ); - this.rect = rect; - this.window = window; - } -}); + // Adjusted for window / screen gaps + const {x, y, width, height} = rect.addGaps(workArea); + + this.ease({ + x: x - display.x, + y: y - display.y, + width, + height, + opacity: 255, + duration: 150, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + + this.rect = rect; + this.window = window; + } + }, +); /** * Base class for other keyboard handlers and the default handler itself. @@ -247,8 +278,7 @@ const DefaultKeyHandler = class DefaultKeyHandler { /** * Automatically called when leaving a mode. */ - prepareLeave() { - } + prepareLeave() {} /** * Automatically called on a keyEvent. @@ -264,94 +294,131 @@ const DefaultKeyHandler = class DefaultKeyHandler { if (dir) { this._focusInDir(dir); - // [E]xpand to fill the available space + // [E]xpand to fill the available space } else if (keyVal === Clutter.KEY_e || keyVal === Clutter.KEY_E) { const window = this._selectIndicator.window; - if (!window) + if (!window) { return Modes.DEFAULT; + } - const tiledRect = this._windows.map(w => w.tiledRect); - const tileRect = Twm.getBestFreeRect(tiledRect, { currRect: window.tiledRect }); - if (window.tiledRect.equal(tileRect)) + const tiledRect = this._windows.map((w) => w.tiledRect); + const tileRect = Twm.getBestFreeRect(tiledRect, { + currRect: window.tiledRect, + }); + if (window.tiledRect.equal(tileRect)) { return Modes.DEFAULT; + } const workArea = window.get_work_area_current_monitor(); const maximize = tileRect.equal(workArea); - if (maximize && this._windows.length > 1) + if (maximize && this._windows.length > 1) { return Modes.DEFAULT; + } - Twm.tile(window, tileRect, { openTilingPopup: false }); + Twm.tile(window, tileRect, {openTilingPopup: false}); - if (maximize) + if (maximize) { return Modes.CLOSE; + } this._selectIndicator.focus(window.tiledRect, window); - // [C]ycle through halves of the available space around the window + // [C]ycle through halves of the available space around the window } else if (keyVal === Clutter.KEY_c || keyVal === Clutter.KEY_C) { const window = this._selectIndicator.window; - if (!window) + if (!window) { return Modes.DEFAULT; + } - const tiledRects = this._windows.map(w => w.tiledRect); - const fullRect = Twm.getBestFreeRect(tiledRects, { currRect: window.tiledRect }); - const topHalf = fullRect.getUnitAt(0, fullRect.height / 2, Orientation.H); - const rightHalf = fullRect.getUnitAt(1, fullRect.width / 2, Orientation.V); - const bottomHalf = fullRect.getUnitAt(1, fullRect.height / 2, Orientation.H); - const leftHalf = fullRect.getUnitAt(0, fullRect.width / 2, Orientation.V); + const tiledRects = this._windows.map((w) => w.tiledRect); + const fullRect = Twm.getBestFreeRect(tiledRects, { + currRect: window.tiledRect, + }); + const topHalf = fullRect.getUnitAt( + 0, + fullRect.height / 2, + Orientation.H, + ); + const rightHalf = fullRect.getUnitAt( + 1, + fullRect.width / 2, + Orientation.V, + ); + const bottomHalf = fullRect.getUnitAt( + 1, + fullRect.height / 2, + Orientation.H, + ); + const leftHalf = fullRect.getUnitAt( + 0, + fullRect.width / 2, + Orientation.V, + ); const rects = [topHalf, rightHalf, bottomHalf, leftHalf]; - const currIdx = rects.findIndex(r => r.equal(window.tiledRect)); + const currIdx = rects.findIndex((r) => r.equal(window.tiledRect)); const newIndex = (currIdx + 1) % 4; - Twm.tile(window, rects[newIndex], { openTilingPopup: false }); + Twm.tile(window, rects[newIndex], {openTilingPopup: false}); this._selectIndicator.focus(window.tiledRect, window); - // [Q]uit a window + // [Q]uit a window } else if (keyVal === Clutter.KEY_q || keyVal === Clutter.KEY_Q) { const window = this._selectIndicator.window; - if (!window) + if (!window) { return Modes.DEFAULT; + } this._windows.splice(this._windows.indexOf(window), 1); window.delete(global.get_current_time()); const newWindow = this._windows[0]; - if (!newWindow) + if (!newWindow) { return Modes.CLOSE; + } this._selectIndicator.focus(newWindow.tiledRect, newWindow); - // [R]estore a window's size + // [R]estore a window's size } else if (keyVal === Clutter.KEY_r || keyVal === Clutter.KEY_R) { const window = this._selectIndicator.window; - if (!window) + if (!window) { return Modes.DEFAULT; + } const selectedRect = window.tiledRect.copy(); this._windows.splice(this._windows.indexOf(window), 1); Twm.untile(window); - if (!this._windows.length) + if (!this._windows.length) { return Modes.CLOSE; + } // Re-raise tile group, so it isn't below the just-untiled window - if (this._windows[0].raise_and_make_recent_on_workspace) - this._windows[0].raise_and_make_recent_on_workspace(global.workspace_manager.get_active_workspace()); - else + if (this._windows[0].raise_and_make_recent_on_workspace) { + this._windows[0].raise_and_make_recent_on_workspace( + global.workspace_manager.get_active_workspace(), + ); + } else { this._windows[0].raise_and_make_recent(); + } this._selectIndicator.focus(selectedRect, null); - // [Enter] / [Esc]ape Tile Editing Mode - } else if (keyVal === Clutter.KEY_Escape || keyVal === Clutter.KEY_Return) { + // [Enter] / [Esc]ape Tile Editing Mode + } else if ( + keyVal === Clutter.KEY_Escape || + keyVal === Clutter.KEY_Return + ) { return Modes.CLOSE; - // [Space] to activate the Tiling Popup + // [Space] to activate the Tiling Popup } else if (keyVal === Clutter.KEY_space) { const allWs = Settings.getBoolean('tiling-popup-all-workspace'); - const openWindows = Twm.getWindows(allWs).filter(w => !this._windows.includes(w)); + const openWindows = Twm.getWindows(allWs).filter( + (w) => !this._windows.includes(w), + ); const TilingPopup = await import('./tilingPopup.js'); const tilingPopup = new TilingPopup.TilingSwitcherPopup( openWindows, this._selectIndicator.rect, - false + false, ); if (!tilingPopup.show(this._windows)) { @@ -360,12 +427,18 @@ const DefaultKeyHandler = class DefaultKeyHandler { } tilingPopup.connect('closed', (popup, canceled) => { - if (canceled) + if (canceled) { return; + } + + const {tiledWindow} = popup; + const replaced = this._windows.findIndex((w) => + w.tiledRect.equal(tiledWindow.tiledRect), + ); - const { tiledWindow } = popup; - const replaced = this._windows.findIndex(w => w.tiledRect.equal(tiledWindow.tiledRect)); - replaced !== -1 && this._windows.splice(replaced, 1); + if (replaced !== -1) { + this._windows.splice(replaced, 1); + } // Create the new tile group to allow 1 window to be part of multiple tile groups Twm.updateTileGroup([tiledWindow, ...this._windows]); @@ -382,9 +455,11 @@ const DefaultKeyHandler = class DefaultKeyHandler { * Automatically called on a keyEvent. * * @param {number} keyEvent + * * @returns {Modes|undefined} The mode to enter after the event was handled. */ - handleKeyRelease() { + // eslint-disable-next-line no-unused-vars + handleKeyRelease(keyEvent) { return undefined; } @@ -395,15 +470,26 @@ const DefaultKeyHandler = class DefaultKeyHandler { */ _focusInDir(dir) { const activeWs = global.workspace_manager.get_active_workspace(); - const workArea = new Rect(activeWs.get_work_area_for_monitor(this._tileEditor.monitor)); - const tiledRects = this._windows.map(w => w.tiledRect); + const workArea = new Rect( + activeWs.get_work_area_for_monitor(this._tileEditor.monitor), + ); + const tiledRects = this._windows.map((w) => w.tiledRect); const screenRects = tiledRects.concat(workArea.minus(tiledRects)); - const nearestRect = this._selectIndicator.rect.getNeighbor(dir, screenRects); - if (!nearestRect) + const nearestRect = this._selectIndicator.rect.getNeighbor( + dir, + screenRects, + ); + if (!nearestRect) { return; + } - const newWindow = this._windows.find(w => w.tiledRect.equal(nearestRect)); - this._selectIndicator.focus(newWindow?.tiledRect ?? nearestRect, newWindow); + const newWindow = this._windows.find((w) => + w.tiledRect.equal(nearestRect), + ); + this._selectIndicator.focus( + newWindow?.tiledRect ?? nearestRect, + newWindow, + ); } get _windows() { @@ -426,12 +512,21 @@ const SwapKeyHandler = class SwapKeyHandler extends DefaultKeyHandler { super(tileEditor); // Create an 'anchor indicator' to indicate the window that will be swapped - const color = this._selectIndicator.get_theme_node().get_background_color(); - const { red, green, blue, alpha } = color; - this._anchorIndicator = new Indicator(this._selectIndicator.rect, tileEditor.monitor, { - style: `background-color: rgba(${red}, ${green}, ${blue}, ${alpha / 255})` - }); - this._anchorIndicator.focus(this._selectIndicator.rect, this._selectIndicator.window); + const color = this._selectIndicator + .get_theme_node() + .get_background_color(); + const {red, green, blue, alpha} = color; + this._anchorIndicator = new Indicator( + this._selectIndicator.rect, + tileEditor.monitor, + { + style: `background-color: rgba(${red}, ${green}, ${blue}, ${alpha / 255})`, + }, + ); + this._anchorIndicator.focus( + this._selectIndicator.rect, + this._selectIndicator.window, + ); this._tileEditor.add_child(this._anchorIndicator); } @@ -443,12 +538,13 @@ const SwapKeyHandler = class SwapKeyHandler extends DefaultKeyHandler { const direction = Util.getDirection(keyEvent.get_key_symbol()); // [Directions] to choose a window to swap with WASD, hjkl or arrow keys - if (direction) + if (direction) { this._focusInDir(direction); - + } // [Esc]ape Tile Editing Mode - else if (keyEvent.get_key_symbol() === Clutter.KEY_Escape) + else if (keyEvent.get_key_symbol() === Clutter.KEY_Escape) { return Modes.DEFAULT; + } return Modes.SWAP; } @@ -466,18 +562,22 @@ const SwapKeyHandler = class SwapKeyHandler extends DefaultKeyHandler { } _swap() { - if (this._anchorIndicator.window) - { Twm.tile(this._anchorIndicator.window, this._selectIndicator.rect, { - openTilingPopup: false - }); } - - if (this._selectIndicator.window) - { Twm.tile(this._selectIndicator.window, this._anchorIndicator.rect, { - openTilingPopup: false - }); } - - this._selectIndicator.focus(this._selectIndicator.rect, - this._anchorIndicator.window); + if (this._anchorIndicator.window) { + Twm.tile(this._anchorIndicator.window, this._selectIndicator.rect, { + openTilingPopup: false, + }); + } + + if (this._selectIndicator.window) { + Twm.tile(this._selectIndicator.window, this._anchorIndicator.rect, { + openTilingPopup: false, + }); + } + + this._selectIndicator.focus( + this._selectIndicator.rect, + this._anchorIndicator.window, + ); } }; @@ -489,53 +589,66 @@ const SwapKeyHandler = class SwapKeyHandler extends DefaultKeyHandler { const MoveKeyHandler = class MoveKeyHandler extends DefaultKeyHandler { handleKeyPress(keyEvent) { const direction = Util.getDirection(keyEvent.get_key_symbol()); - const moveWorkspace = keyEvent.get_state() & Clutter.ModifierType.MOD1_MASK; + const moveWorkspace = + keyEvent.get_state() & Clutter.ModifierType.MOD1_MASK; // [Directions] to move the tile group if (direction) { // To new workspace if (moveWorkspace) { let metaDir = Meta.MotionDirection.UP; - if (direction === Direction.N) + if (direction === Direction.N) { metaDir = Meta.MotionDirection.UP; - else if (direction === Direction.S) + } else if (direction === Direction.S) { metaDir = Meta.MotionDirection.DOWN; - else if (direction === Direction.W) + } else if (direction === Direction.W) { metaDir = Meta.MotionDirection.LEFT; - else if (direction === Direction.E) + } else if (direction === Direction.E) { metaDir = Meta.MotionDirection.RIGHT; + } - const activeWs = global.workspace_manager.get_active_workspace(); + const activeWs = + global.workspace_manager.get_active_workspace(); const newWs = activeWs.get_neighbor(metaDir); - if (activeWs === newWs) + if (activeWs === newWs) { return Modes.MOVE; + } Twm.moveGroupToWorkspace(this._tileEditor._windows, newWs); - // To new monitor + // To new monitor } else { let metaDir = Meta.DisplayDirection.UP; - if (direction === Direction.N) + if (direction === Direction.N) { metaDir = Meta.DisplayDirection.UP; - else if (direction === Direction.S) + } else if (direction === Direction.S) { metaDir = Meta.DisplayDirection.DOWN; - else if (direction === Direction.W) + } else if (direction === Direction.W) { metaDir = Meta.DisplayDirection.LEFT; - else if (direction === Direction.E) + } else if (direction === Direction.E) { metaDir = Meta.DisplayDirection.RIGHT; + } // get_current_monitor isn't accurate for our case const currMonitor = this._tileEditor.monitor; - const newMonitor = global.display.get_monitor_neighbor_index(currMonitor, metaDir); - if (newMonitor === -1) + const newMonitor = global.display.get_monitor_neighbor_index( + currMonitor, + metaDir, + ); + if (newMonitor === -1) { return Modes.MOVE; + } - Twm.moveGroupToMonitor(this._tileEditor._windows, currMonitor, newMonitor); + Twm.moveGroupToMonitor( + this._tileEditor._windows, + currMonitor, + newMonitor, + ); } return Modes.CLOSE; - // [Esc] to return to default mode + // [Esc] to return to default mode } else if (keyEvent.get_key_symbol() === Clutter.KEY_Escape) { return Modes.DEFAULT; } @@ -567,8 +680,9 @@ const ResizeKeyHandler = class ResizeKeyHandler extends DefaultKeyHandler { const direction = Util.getDirection(keyEvent.get_key_symbol()); if (direction) { const window = this._selectIndicator.window; - if (!window) + if (!window) { return Modes.DEFAULT; + } // First call: Go to an edge. if (!this._currEdge) { @@ -576,7 +690,7 @@ const ResizeKeyHandler = class ResizeKeyHandler extends DefaultKeyHandler { this._createResizeIndicator(); return Modes.RESIZE; - // Change resize orientation from H to V + // Change resize orientation from H to V } else if ([Direction.N, Direction.S].includes(this._currEdge)) { if ([Direction.W, Direction.E].includes(direction)) { this._currEdge = direction; @@ -584,7 +698,7 @@ const ResizeKeyHandler = class ResizeKeyHandler extends DefaultKeyHandler { return Modes.RESIZE; } - // Change resize orientation from V to H + // Change resize orientation from V to H } else if ([Direction.W, Direction.E].includes(this._currEdge)) { if ([Direction.N, Direction.S].includes(direction)) { this._currEdge = direction; @@ -601,7 +715,7 @@ const ResizeKeyHandler = class ResizeKeyHandler extends DefaultKeyHandler { // Update resize side indicator this._resizeSideIndicator.updatePos(window.tiledRect); - // [Esc]ape Tile Editing Mode + // [Esc]ape Tile Editing Mode } else if (keyEvent.get_key_symbol() === Clutter.KEY_Escape) { return Modes.CLOSE; } @@ -623,17 +737,19 @@ const ResizeKeyHandler = class ResizeKeyHandler extends DefaultKeyHandler { let resizeAmount = 50; // Limit resizeAmount to the workArea - if (this._currEdge === Direction.N && keyDir === Direction.N) + if (this._currEdge === Direction.N && keyDir === Direction.N) { resizeAmount = Math.min(resizeAmount, resizedRect.y - workArea.y); - else if (this._currEdge === Direction.S && keyDir === Direction.S) + } else if (this._currEdge === Direction.S && keyDir === Direction.S) { resizeAmount = Math.min(resizeAmount, workArea.y2 - resizedRect.y2); - else if (this._currEdge === Direction.W && keyDir === Direction.W) + } else if (this._currEdge === Direction.W && keyDir === Direction.W) { resizeAmount = Math.min(resizeAmount, resizedRect.x - workArea.x); - else if (this._currEdge === Direction.E && keyDir === Direction.E) + } else if (this._currEdge === Direction.E && keyDir === Direction.E) { resizeAmount = Math.min(resizeAmount, workArea.x2 - resizedRect.x2); + } - if (resizeAmount <= 0) + if (resizeAmount <= 0) { return; + } // Function to update the passed rect by the resizeAmount depending on // the edge that is resized. Some windows will resize on the same edge @@ -644,30 +760,30 @@ const ResizeKeyHandler = class ResizeKeyHandler extends DefaultKeyHandler { switch (resizeOnEdge) { case Direction.N: rect.y -= resizeAmount * growDir; - // falls through + // falls through case Direction.S: rect.height += resizeAmount * growDir; break; case Direction.W: rect.x -= resizeAmount * growDir; - // falls through + // falls through case Direction.E: rect.width += resizeAmount * growDir; } }; // Actually resize the windows here. - this._windows.forEach(w => { + this._windows.forEach((w) => { // The window, which is resized by the user, is included in this. if (this._isSameSide(resizedRect, w.tiledRect)) { const newRect = w.tiledRect.copy(); updateRectSize(newRect, this._currEdge); - Twm.tile(w, newRect, { openTilingPopup: false }); + Twm.tile(w, newRect, {openTilingPopup: false}); } else if (this._isOppositeSide(resizedRect, w.tiledRect)) { const newRect = w.tiledRect.copy(); updateRectSize(newRect, Direction.opposite(this._currEdge)); - Twm.tile(w, newRect, { openTilingPopup: false }); + Twm.tile(w, newRect, {openTilingPopup: false}); } }); } @@ -705,79 +821,85 @@ const ResizeKeyHandler = class ResizeKeyHandler extends DefaultKeyHandler { _createResizeIndicator() { this._resizeSideIndicator?.destroy(); this._resizeSideIndicator = new ResizeSideIndicator( - this._currEdge, this._selectIndicator.rect); + this._currEdge, + this._selectIndicator.rect, + ); Main.uiGroup.add_child(this._resizeSideIndicator); } }; const ResizeSideIndicator = GObject.registerClass( -class ResizeSideIndicator extends St.Widget { - _init(edge, activeRect) { - const [width, height] = [Direction.N, Direction.S].includes(edge) - ? [200, 20] - : [20, 200]; - - super._init({ - width, - height, - opacity: 0, - style: 'background-color: black;\ - border-radius: 999px;' - }); - - this._edge = edge; - this._moveDist = 100; - - this.updatePos(activeRect); - - // Inner pill - const innerWidth = this.width < this.height ? 4 : 75; - const innerHeight = this.width < this.height ? 75 : 4; - this.add_child(new St.Widget({ - x: this.width / 2 - innerWidth / 2, - y: this.height / 2 - innerHeight / 2, - width: innerWidth, - height: innerHeight, - style: 'background-color: #ebebeb;\ - border-radius: 999px;' - })); - } + class ResizeSideIndicator extends St.Widget { + _init(edge, activeRect) { + const [width, height] = + [Direction.N, Direction.S].includes(edge) ? + [200, 20] + : [20, 200]; + + super._init({ + width, + height, + opacity: 0, + style: 'background-color: black;\ + border-radius: 999px;', + }); - destroy() { - this.ease({ - opacity: 0, - duration: 100, - mode: Clutter.AnimationMode.EASE_OUT_QUAD, - onComplete: () => super.destroy() - }); - } + this._edge = edge; + this._moveDist = 100; + + this.updatePos(activeRect); + + // Inner pill + const innerWidth = this.width < this.height ? 4 : 75; + const innerHeight = this.width < this.height ? 75 : 4; + this.add_child( + new St.Widget({ + x: this.width / 2 - innerWidth / 2, + y: this.height / 2 - innerHeight / 2, + width: innerWidth, + height: innerHeight, + style: 'background-color: #ebebeb;\ + border-radius: 999px;', + }), + ); + } - updatePos(rect) { - let x, y; - switch (this._edge) { - case Direction.N: - x = rect.center.x - this.width / 2; - y = rect.y - this.height / 2; - break; - case Direction.S: - x = rect.center.x - this.width / 2; - y = rect.y2 - this.height / 2; - break; - case Direction.W: - x = rect.x - this.width / 2; - y = rect.center.y - this.height / 2; - break; - case Direction.E: - x = rect.x2 - this.width / 2; - y = rect.center.y - this.height / 2; + destroy() { + this.ease({ + opacity: 0, + duration: 100, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => super.destroy(), + }); } - this.ease({ - x, - y, - opacity: 255, - duration: 150, - mode: Clutter.AnimationMode.EASE_OUT_QUAD - }); - } -}); + updatePos(rect) { + let x, y; + switch (this._edge) { + case Direction.N: + x = rect.center.x - this.width / 2; + y = rect.y - this.height / 2; + break; + case Direction.S: + x = rect.center.x - this.width / 2; + y = rect.y2 - this.height / 2; + break; + case Direction.W: + x = rect.x - this.width / 2; + y = rect.center.y - this.height / 2; + break; + case Direction.E: + x = rect.x2 - this.width / 2; + y = rect.center.y - this.height / 2; + } + + this.ease({ + x, + y, + opacity: 255, + duration: 150, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + }, +); diff --git a/tiling-assistant@leleat-on-github/src/extension/tilingPopup.js b/tiling-assistant@leleat-on-github/src/extension/tilingPopup.js index bfce1c9..4d40ef5 100644 --- a/tiling-assistant@leleat-on-github/src/extension/tilingPopup.js +++ b/tiling-assistant@leleat-on-github/src/extension/tilingPopup.js @@ -1,9 +1,9 @@ -import { Clutter, GObject, Meta, St } from '../dependencies/gi.js'; -import { Main, SwitcherPopup } from '../dependencies/shell.js'; +import {Clutter, GObject, Meta, St} from '../dependencies/gi.js'; +import {Main, SwitcherPopup} from '../dependencies/shell.js'; -import { Direction, Orientation } from '../common.js'; -import { Util } from './utility.js'; -import { TilingWindowManager as Twm } from './tilingWindowManager.js'; +import {Direction, Orientation} from '../common.js'; +import {Util} from './utility.js'; +import {TilingWindowManager as Twm} from './tilingWindowManager.js'; import * as AltTab from './altTab.js'; /** @@ -12,347 +12,423 @@ import * as AltTab from './altTab.js'; * Mostly based on GNOME's altTab.js */ -export const TilingSwitcherPopup = GObject.registerClass({ - Signals: { - // Bool indicates whether the Tiling Popup was canceled - // (or if a window was tiled with this popup) - 'closed': { param_types: [GObject.TYPE_BOOLEAN] } - } -}, class TilingSwitcherPopup extends AltTab.TilingAppSwitcherPopup { - /** - * @param {Meta.Windows[]} openWindows an array of Meta.Windows, which this - * popup offers to tile. - * @param {Rect} freeScreenRect the Rect, which the popup will tile a window - * to. The popup will be centered in this rect. - * @param {boolean} allowConsecutivePopup allow the popup to create another - * Tiling Popup, if there is still unambiguous free screen space after - * this popup tiled a window. - * @param {boolean} skipAnim - */ - _init(openWindows, freeScreenRect, allowConsecutivePopup = true, skipAnim = false) { - this._freeScreenRect = freeScreenRect; - this._shadeBG = null; - this._monitor = -1; - - SwitcherPopup.SwitcherPopup.prototype._init.call(this); - - this._thumbnails = null; - this._thumbnailTimeoutId = 0; - this._currentWindow = -1; - this.thumbnailsVisible = false; - // The window, which was tiled with the Tiling Popup after it's closed - // or null, if the popup was closed with tiling a window - this.tiledWindow = null; - this._allowConsecutivePopup = allowConsecutivePopup; - this._skipAnim = skipAnim; - - this._switcherList = new TSwitcherList(this, openWindows); - this._items = this._switcherList.icons; - - // Destroy popup when touching outside of popup - this.connect('touch-event', () => { - if (Meta.is_wayland_compositor()) - this.fadeAndDestroy(); - - return Clutter.EVENT_PROPAGATE; - }); - } - - /** - * @param {Array} tileGroup an array of Meta.Windows. When the popup - * appears it will shade the background. These windows will won't - * be affected by that. - * @returns if the popup was successfully shown. - */ - show(tileGroup) { - this._monitor = tileGroup[0]?.get_monitor() ?? global.display.get_current_monitor(); - - if (!this._items.length) - return false; - - const grab = Main.pushModal(this); - // We expect at least a keyboard grab here - if ((grab.get_seat_state() & Clutter.GrabState.KEYBOARD) === 0) { - Main.popModal(grab); - return false; - } +export const TilingSwitcherPopup = GObject.registerClass( + { + Signals: { + // Bool indicates whether the Tiling Popup was canceled + // (or if a window was tiled with this popup) + closed: {param_types: [GObject.TYPE_BOOLEAN]}, + }, + }, + class TilingSwitcherPopup extends AltTab.TilingAppSwitcherPopup { + /** + * @param {Meta.Windows[]} openWindows an array of Meta.Windows, which this + * popup offers to tile. + * @param {Rect} freeScreenRect the Rect, which the popup will tile a window + * to. The popup will be centered in this rect. + * @param {boolean} allowConsecutivePopup allow the popup to create another + * Tiling Popup, if there is still unambiguous free screen space after + * this popup tiled a window. + * @param {boolean} skipAnim + */ + _init( + openWindows, + freeScreenRect, + allowConsecutivePopup = true, + skipAnim = false, + ) { + this._freeScreenRect = freeScreenRect; + this._shadeBG = null; + this._monitor = -1; + + SwitcherPopup.SwitcherPopup.prototype._init.call(this); + + this._thumbnails = null; + this._thumbnailTimeoutId = 0; + this._currentWindow = -1; + this.thumbnailsVisible = false; + // The window, which was tiled with the Tiling Popup after it's closed + // or null, if the popup was closed with tiling a window + this.tiledWindow = null; + this._allowConsecutivePopup = allowConsecutivePopup; + this._skipAnim = skipAnim; + + this._switcherList = new TSwitcherList(this, openWindows); + this._items = this._switcherList.icons; + + // Destroy popup when touching outside of popup + this.connect('touch-event', () => { + if (Meta.is_wayland_compositor()) { + this.fadeAndDestroy(); + } - this._grab = grab; - this._haveModal = true; - - this._switcherList.connect('item-activated', this._itemActivated.bind(this)); - this._switcherList.connect('item-entered', this._itemEntered.bind(this)); - this._switcherList.connect('item-removed', this._itemRemoved.bind(this)); - this.add_child(this._switcherList); - - // Need to force an allocation so we can figure out - // whether we need to scroll when selecting - this.visible = true; - this.get_allocation_box(); - - this._select(0); - - Main.osdWindowManager.hideAll(); - - this._shadeBackground(tileGroup); - this.opacity = 0; - this.ease({ - opacity: 255, - duration: 200, - mode: Clutter.AnimationMode.EASE_OUT_QUAD - }); - - return true; - } - - _shadeBackground(tileGroup) { - const tiledWindow = tileGroup[0]; - const activeWs = global.workspace_manager.get_active_workspace(); - const workArea = activeWs.get_work_area_for_monitor(this._monitor); - - this._shadeBG = new St.Widget({ - style: 'background-color : black', - x: workArea.x, - y: workArea.y, - width: workArea.width, - height: workArea.height, - opacity: 0 - }); - global.window_group.add_child(this._shadeBG); - this._shadeBG.ease({ - opacity: 180, - duration: 200, - mode: Clutter.AnimationMode.EASE_OUT_QUAD - }); - - if (!tiledWindow) - return; - - // Clones to correctly shade the background for consecutive tiling. - for (let i = 1; i < tileGroup.length; i++) { - const wActor = tileGroup[i].get_compositor_private(); - const clone = new Clutter.Clone({ - source: wActor, - x: wActor.x, - y: wActor.y - }); - global.window_group.add_child(clone); - wActor.hide(); - this.connect('destroy', () => { - wActor.show(); - clone.destroy(); + return Clutter.EVENT_PROPAGATE; }); } - const tActor = tiledWindow.get_compositor_private(); - global.window_group.set_child_above_sibling(tActor, this._shadeBG); - } + /** + * @param {Array} tileGroup an array of Meta.Windows. When the popup + * appears it will shade the background. These windows will won't + * be affected by that. + * @returns if the popup was successfully shown. + */ + show(tileGroup) { + this._monitor = + tileGroup[0]?.get_monitor() ?? + global.display.get_current_monitor(); + + if (!this._items.length) { + return false; + } - vfunc_allocate(box) { - this.set_allocation(box); + const grab = Main.pushModal(this); + // We expect at least a keyboard grab here + if ((grab.get_seat_state() & Clutter.GrabState.KEYBOARD) === 0) { + Main.popModal(grab); + return false; + } - const freeScreenRect = this._freeScreenRect; - const childBox = new Clutter.ActorBox(); + this._grab = grab; + this._haveModal = true; - const leftPadding = this.get_theme_node().get_padding(St.Side.LEFT); - const rightPadding = this.get_theme_node().get_padding(St.Side.RIGHT); - const hPadding = leftPadding + rightPadding; + this._switcherList.connect( + 'item-activated', + this._itemActivated.bind(this), + ); + this._switcherList.connect( + 'item-entered', + this._itemEntered.bind(this), + ); + this._switcherList.connect( + 'item-removed', + this._itemRemoved.bind(this), + ); + this.add_child(this._switcherList); - const [, childNaturalHeight] = this._switcherList.get_preferred_height( - freeScreenRect.width - hPadding); - const [, childNaturalWidth] = this._switcherList.get_preferred_width(childNaturalHeight); + // Need to force an allocation so we can figure out + // whether we need to scroll when selecting + this.visible = true; + this.get_allocation_box(); - childBox.x1 = Math.max(freeScreenRect.x + leftPadding, - freeScreenRect.x + Math.floor((freeScreenRect.width - childNaturalWidth) / 2)); - childBox.x2 = Math.min(freeScreenRect.x2 - rightPadding, - childBox.x1 + childNaturalWidth); - childBox.y1 = freeScreenRect.y + Math.floor((freeScreenRect.height - childNaturalHeight) / 2); - childBox.y2 = childBox.y1 + childNaturalHeight; + this._select(0); - this._switcherList.allocate(childBox); + Main.osdWindowManager.hideAll(); - if (this._thumbnails) { - const cbox = this._switcherList.get_allocation_box(); - const monitor = global.display.get_monitor_geometry(this._monitor); + this._shadeBackground(tileGroup); + this.opacity = 0; + this.ease({ + opacity: 255, + duration: 200, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); - const leftPadd = this.get_theme_node().get_padding(St.Side.LEFT); - const rightPadd = this.get_theme_node().get_padding(St.Side.RIGHT); - const bottomPadding = this.get_theme_node().get_padding(St.Side.BOTTOM); - const hPadd = leftPadd + rightPadd; + return true; + } - const icon = this._items[this._selectedIndex]; - const [posX] = icon.get_transformed_position(); - const thumbnailCenter = posX + icon.width / 2; - const [, cNatWidth] = this._thumbnails.get_preferred_width(-1); - cbox.x1 = Math.max(monitor.x + leftPadd, - Math.floor(thumbnailCenter - cNatWidth / 2) - ); - if (cbox.x1 + cNatWidth > monitor.x + monitor.width - hPadd) { - const offset = cbox.x1 + cNatWidth - monitor.width + hPadd; - cbox.x1 = Math.max(monitor.x + leftPadd, cbox.x1 - offset - hPadd); + _shadeBackground(tileGroup) { + const tiledWindow = tileGroup[0]; + const activeWs = global.workspace_manager.get_active_workspace(); + const workArea = activeWs.get_work_area_for_monitor(this._monitor); + + this._shadeBG = new St.Widget({ + style: 'background-color : black', + x: workArea.x, + y: workArea.y, + width: workArea.width, + height: workArea.height, + opacity: 0, + }); + global.window_group.add_child(this._shadeBG); + this._shadeBG.ease({ + opacity: 180, + duration: 200, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + + if (!tiledWindow) { + return; + } + + // Clones to correctly shade the background for consecutive tiling. + for (let i = 1; i < tileGroup.length; i++) { + const wActor = tileGroup[i].get_compositor_private(); + const clone = new Clutter.Clone({ + source: wActor, + x: wActor.x, + y: wActor.y, + }); + global.window_group.add_child(clone); + wActor.hide(); + this.connect('destroy', () => { + wActor.show(); + clone.destroy(); + }); } - const spacing = this.get_theme_node().get_length('spacing'); + const tActor = tiledWindow.get_compositor_private(); + global.window_group.set_child_above_sibling(tActor, this._shadeBG); + } - cbox.x2 = cbox.x1 + cNatWidth; - if (cbox.x2 > monitor.x + monitor.width - rightPadd) - cbox.x2 = monitor.x + monitor.width - rightPadd; - cbox.y1 = this._switcherList.allocation.y2 + spacing; - this._thumbnails.addClones(monitor.y + monitor.height - bottomPadding - cbox.y1); - const [, cNatHeight] = this._thumbnails.get_preferred_height(-1); - cbox.y2 = cbox.y1 + cNatHeight; + vfunc_allocate(box) { + this.set_allocation(box); - this._thumbnails.allocate(cbox); + const freeScreenRect = this._freeScreenRect; + const childBox = new Clutter.ActorBox(); + + const leftPadding = this.get_theme_node().get_padding(St.Side.LEFT); + const rightPadding = this.get_theme_node().get_padding( + St.Side.RIGHT, + ); + const hPadding = leftPadding + rightPadding; + + const [, childNaturalHeight] = + this._switcherList.get_preferred_height( + freeScreenRect.width - hPadding, + ); + const [, childNaturalWidth] = + this._switcherList.get_preferred_width(childNaturalHeight); + + childBox.x1 = Math.max( + freeScreenRect.x + leftPadding, + freeScreenRect.x + + Math.floor((freeScreenRect.width - childNaturalWidth) / 2), + ); + childBox.x2 = Math.min( + freeScreenRect.x2 - rightPadding, + childBox.x1 + childNaturalWidth, + ); + childBox.y1 = + freeScreenRect.y + + Math.floor((freeScreenRect.height - childNaturalHeight) / 2); + childBox.y2 = childBox.y1 + childNaturalHeight; + + this._switcherList.allocate(childBox); + + if (this._thumbnails) { + const cbox = this._switcherList.get_allocation_box(); + const monitor = global.display.get_monitor_geometry( + this._monitor, + ); + + const leftPadd = this.get_theme_node().get_padding( + St.Side.LEFT, + ); + const rightPadd = this.get_theme_node().get_padding( + St.Side.RIGHT, + ); + const bottomPadding = this.get_theme_node().get_padding( + St.Side.BOTTOM, + ); + const hPadd = leftPadd + rightPadd; + + const icon = this._items[this._selectedIndex]; + const [posX] = icon.get_transformed_position(); + const thumbnailCenter = posX + icon.width / 2; + const [, cNatWidth] = this._thumbnails.get_preferred_width(-1); + cbox.x1 = Math.max( + monitor.x + leftPadd, + Math.floor(thumbnailCenter - cNatWidth / 2), + ); + if (cbox.x1 + cNatWidth > monitor.x + monitor.width - hPadd) { + const offset = cbox.x1 + cNatWidth - monitor.width + hPadd; + cbox.x1 = Math.max( + monitor.x + leftPadd, + cbox.x1 - offset - hPadd, + ); + } + + const spacing = this.get_theme_node().get_length('spacing'); + + cbox.x2 = cbox.x1 + cNatWidth; + if (cbox.x2 > monitor.x + monitor.width - rightPadd) { + cbox.x2 = monitor.x + monitor.width - rightPadd; + } + cbox.y1 = this._switcherList.allocation.y2 + spacing; + this._thumbnails.addClones( + monitor.y + monitor.height - bottomPadding - cbox.y1, + ); + const [, cNatHeight] = + this._thumbnails.get_preferred_height(-1); + cbox.y2 = cbox.y1 + cNatHeight; + + this._thumbnails.allocate(cbox); + } } - } - vfunc_button_press_event(buttonEvent) { - const btn = buttonEvent.get_button(); - if (btn === Clutter.BUTTON_MIDDLE || btn === Clutter.BUTTON_SECONDARY) { - this._finish(global.get_current_time()); - return Clutter.EVENT_PROPAGATE; + vfunc_button_press_event(buttonEvent) { + const btn = buttonEvent.get_button(); + if ( + btn === Clutter.BUTTON_MIDDLE || + btn === Clutter.BUTTON_SECONDARY + ) { + this._finish(global.get_current_time()); + return Clutter.EVENT_PROPAGATE; + } + + return super.vfunc_button_press_event(buttonEvent); } - return super.vfunc_button_press_event(buttonEvent); - } - - _keyPressHandler(keysym) { - const moveUp = Util.isDirection(keysym, Direction.N); - const moveDown = Util.isDirection(keysym, Direction.S); - const moveLeft = Util.isDirection(keysym, Direction.W); - const moveRight = Util.isDirection(keysym, Direction.E); - - if (this._thumbnailsFocused) { - if (moveLeft) - this._select(this._selectedIndex, this._previousWindow()); - else if (moveRight) - this._select(this._selectedIndex, this._nextWindow()); - else if (moveUp || moveDown) - this._select(this._selectedIndex, null, true); - else + _keyPressHandler(keysym) { + const moveUp = Util.isDirection(keysym, Direction.N); + const moveDown = Util.isDirection(keysym, Direction.S); + const moveLeft = Util.isDirection(keysym, Direction.W); + const moveRight = Util.isDirection(keysym, Direction.E); + + if (this._thumbnailsFocused) { + if (moveLeft) { + this._select(this._selectedIndex, this._previousWindow()); + } else if (moveRight) { + this._select(this._selectedIndex, this._nextWindow()); + } else if (moveUp || moveDown) { + this._select(this._selectedIndex, null, true); + } else { + return Clutter.EVENT_PROPAGATE; + } + } else if (moveLeft) { + this._select(this._previous()); + } else if (moveRight) { + this._select(this._next()); + } else if (moveDown || moveUp) { + this._select(this._selectedIndex, 0); + } else { return Clutter.EVENT_PROPAGATE; - } else if (moveLeft) { - this._select(this._previous()); - } else if (moveRight) { - this._select(this._next()); - } else if (moveDown || moveUp) { - this._select(this._selectedIndex, 0); - } else { - return Clutter.EVENT_PROPAGATE; + } + + return Clutter.EVENT_STOP; } - return Clutter.EVENT_STOP; - } - - _windowActivated(thumbnailSwitcher, n) { - const window = this._items[this._selectedIndex].cachedWindows[n]; - this._tileWindow(window); - this.fadeAndDestroy(); - } - - _finish(timestamp) { - const appIcon = this._items[this._selectedIndex]; - const window = appIcon.cachedWindows[Math.max(0, this._currentWindow)]; - this._tileWindow(window); - SwitcherPopup.SwitcherPopup.prototype._finish.call(this, timestamp); - } - - fadeAndDestroy() { - if (this._alreadyDestroyed) - return; - - this._alreadyDestroyed = true; - - const canceled = !this.tiledWindow; - this.emit('closed', canceled); - - this._shadeBG?.destroy(); - this._shadeBG = null; - super.fadeAndDestroy(); - } - - _tileWindow(window) { - let rect = this._freeScreenRect; - - // Halve the tile rect. - // If isShiftPressed, then put the window at the top / left side; - // if isAltPressed, then put it at the bottom / right side. - // The orientation depends on the available screen space. - const isShiftPressed = Util.isModPressed(Clutter.ModifierType.SHIFT_MASK); - const isAltPressed = Util.isModPressed(Clutter.ModifierType.MOD1_MASK); - if (isShiftPressed || isAltPressed) { - // Prefer vertical a bit more (because screens are usually horizontal) - const vertical = rect.width >= rect.height * 1.25; - const size = vertical ? 'width' : 'height'; - const orientation = vertical ? Orientation.V : Orientation.H; - const idx = isShiftPressed ? 0 : 1; - rect = rect.getUnitAt(idx, rect[size] / 2, orientation); + _windowActivated(thumbnailSwitcher, n) { + const window = this._items[this._selectedIndex].cachedWindows[n]; + this._tileWindow(window); + this.fadeAndDestroy(); } - this.tiledWindow = window; - - window.change_workspace(global.workspace_manager.get_active_workspace()); - - // We want to activate/focus the window after it was tiled with the - // Tiling Popup. Calling activate/focus() after tile() doesn't seem to - // work for GNOME Terminal if it is maximized before trying to tile it. - // It won't be tiled properly in that case for some reason... Instead - // activate first but clear the tiling signals before so that the old - // tile group won't be accidentally raised. - Twm.clearTilingProps(window.get_id()); - window.activate(global.get_current_time()); - Twm.tile(window, rect, { - monitorNr: this._monitor, - openTilingPopup: this._allowConsecutivePopup, - skipAnim: this._skipAnim - }); - } - - // Dont _finish(), if no mods are pressed - _resetNoModsTimeout() { - } -}); + _finish(timestamp) { + const appIcon = this._items[this._selectedIndex]; + const window = + appIcon.cachedWindows[Math.max(0, this._currentWindow)]; + this._tileWindow(window); + SwitcherPopup.SwitcherPopup.prototype._finish.call(this, timestamp); + } -const TSwitcherList = GObject.registerClass( -class TilingSwitcherList extends AltTab.TilingAppSwitcher { - _setIconSize() { - let j = 0; - while (this._items.length > 1 && this._items[j].style_class !== 'item-box') - j++; - - const themeNode = this._items[j].get_theme_node(); - this._list.ensure_style(); - - const iconPadding = themeNode.get_horizontal_padding(); - const iconBorder = themeNode.get_border_width(St.Side.LEFT) + - themeNode.get_border_width(St.Side.RIGHT); - const [, labelNaturalHeight] = this.icons[j].label.get_preferred_height(-1); - const iconSpacing = labelNaturalHeight + iconPadding + iconBorder; - const totalSpacing = this._list.spacing * (this._items.length - 1); - - const freeScreenRect = this._altTabPopup._freeScreenRect; - const parentPadding = this.get_parent().get_theme_node().get_horizontal_padding(); - const availWidth = freeScreenRect.width - parentPadding - - this.get_theme_node().get_horizontal_padding(); - - const scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; - const baseIconSizes = [96, 64, 48, 32, 22]; - const iconSizes = baseIconSizes.map(s => s * scaleFactor); - let iconSize = baseIconSizes[0]; - - if (this._items.length > 1) { - for (let i = 0; i < baseIconSizes.length; i++) { - iconSize = baseIconSizes[i]; - const height = iconSizes[i] + iconSpacing; - const w = height * this._items.length + totalSpacing; - if (w <= availWidth) - break; + fadeAndDestroy() { + if (this._alreadyDestroyed) { + return; } + + this._alreadyDestroyed = true; + + const canceled = !this.tiledWindow; + this.emit('closed', canceled); + + this._shadeBG?.destroy(); + this._shadeBG = null; + super.fadeAndDestroy(); } - this._iconSize = iconSize; + _tileWindow(window) { + let rect = this._freeScreenRect; + + // Halve the tile rect. + // If isShiftPressed, then put the window at the top / left side; + // if isAltPressed, then put it at the bottom / right side. + // The orientation depends on the available screen space. + const isShiftPressed = Util.isModPressed( + Clutter.ModifierType.SHIFT_MASK, + ); + const isAltPressed = Util.isModPressed( + Clutter.ModifierType.MOD1_MASK, + ); + if (isShiftPressed || isAltPressed) { + // Prefer vertical a bit more (because screens are usually horizontal) + const vertical = rect.width >= rect.height * 1.25; + const size = vertical ? 'width' : 'height'; + const orientation = vertical ? Orientation.V : Orientation.H; + const idx = isShiftPressed ? 0 : 1; + rect = rect.getUnitAt(idx, rect[size] / 2, orientation); + } + + this.tiledWindow = window; + + window.change_workspace( + global.workspace_manager.get_active_workspace(), + ); - for (let i = 0; i < this.icons.length; i++) - this.icons[i].set_size(iconSize); - } -}); + // We want to activate/focus the window after it was tiled with the + // Tiling Popup. Calling activate/focus() after tile() doesn't seem to + // work for GNOME Terminal if it is maximized before trying to tile it. + // It won't be tiled properly in that case for some reason... Instead + // activate first but clear the tiling signals before so that the old + // tile group won't be accidentally raised. + Twm.clearTilingProps(window.get_id()); + window.activate(global.get_current_time()); + Twm.tile(window, rect, { + monitorNr: this._monitor, + openTilingPopup: this._allowConsecutivePopup, + skipAnim: this._skipAnim, + }); + } + + // Dont _finish(), if no mods are pressed + _resetNoModsTimeout() {} + }, +); + +const TSwitcherList = GObject.registerClass( + class TilingSwitcherList extends AltTab.TilingAppSwitcher { + _setIconSize() { + let j = 0; + while ( + this._items.length > 1 && + this._items[j].style_class !== 'item-box' + ) { + j++; + } + + const themeNode = this._items[j].get_theme_node(); + this._list.ensure_style(); + + const iconPadding = themeNode.get_horizontal_padding(); + const iconBorder = + themeNode.get_border_width(St.Side.LEFT) + + themeNode.get_border_width(St.Side.RIGHT); + const [, labelNaturalHeight] = + this.icons[j].label.get_preferred_height(-1); + const iconSpacing = labelNaturalHeight + iconPadding + iconBorder; + const totalSpacing = this._list.spacing * (this._items.length - 1); + + const freeScreenRect = this._altTabPopup._freeScreenRect; + const parentPadding = this.get_parent() + .get_theme_node() + .get_horizontal_padding(); + const availWidth = + freeScreenRect.width - + parentPadding - + this.get_theme_node().get_horizontal_padding(); + + const scaleFactor = St.ThemeContext.get_for_stage( + global.stage, + ).scale_factor; + const baseIconSizes = [96, 64, 48, 32, 22]; + const iconSizes = baseIconSizes.map((s) => s * scaleFactor); + let iconSize = baseIconSizes[0]; + + if (this._items.length > 1) { + for (let i = 0; i < baseIconSizes.length; i++) { + iconSize = baseIconSizes[i]; + const height = iconSizes[i] + iconSpacing; + const w = height * this._items.length + totalSpacing; + if (w <= availWidth) { + break; + } + } + } + + this._iconSize = iconSize; + + for (let i = 0; i < this.icons.length; i++) { + this.icons[i].set_size(iconSize); + } + } + }, +); diff --git a/tiling-assistant@leleat-on-github/src/extension/tilingWindowManager.js b/tiling-assistant@leleat-on-github/src/extension/tilingWindowManager.js index 0bc3f57..8209574 100644 --- a/tiling-assistant@leleat-on-github/src/extension/tilingWindowManager.js +++ b/tiling-assistant@leleat-on-github/src/extension/tilingWindowManager.js @@ -1,9 +1,9 @@ -import { Clutter, GLib, GObject, Meta, Mtk, Shell } from '../dependencies/gi.js'; -import { Main } from '../dependencies/shell.js'; -import { getWindows } from '../dependencies/unexported/altTab.js'; +import {Clutter, GLib, GObject, Meta, Mtk, Shell} from '../dependencies/gi.js'; +import {Main} from '../dependencies/shell.js'; +import {getWindows} from '../dependencies/unexported/altTab.js'; -import { Orientation, Settings } from '../common.js'; -import { Rect, Util } from './utility.js'; +import {Orientation, Settings} from '../common.js'; +import {Rect, Util} from './utility.js'; /** * Singleton responsible for tiling. Implement the signals in a separate Clutter @@ -21,7 +21,7 @@ export class TilingWindowManager { */ this._tileStates = new Map(); - const assertExistenceFor = window => { + const assertExistenceFor = (window) => { window.assertExistence = () => {}; window.connectObject( @@ -29,30 +29,30 @@ export class TilingWindowManager { () => { window.assertExistence = () => { throw new Error( - 'Trying to operate on an unmanaging window!' + 'Trying to operate on an unmanaging window!', ); }; }, - this + this, ); }; - global.display.list_all_windows().forEach(w => assertExistenceFor(w)); + global.display.list_all_windows().forEach((w) => assertExistenceFor(w)); global.display.connectObject( 'window-created', (_, window) => assertExistenceFor(window), - this + this, ); global.workspace_manager.connectObject( 'workspace-added', this._onWorkspaceAdded.bind(this), - this + this, ); global.workspace_manager.connectObject( 'workspace-removed', this._onWorkspaceRemoved.bind(this), - this + this, ); } @@ -63,7 +63,7 @@ export class TilingWindowManager { global.workspace_manager.disconnectObject(this); global.display.disconnectObject(this); - global.display.list_all_windows().forEach(w => { + global.display.list_all_windows().forEach((w) => { w.disconnectObject(this); delete w.assertExistence; @@ -114,16 +114,18 @@ export class TilingWindowManager { // The open windows are not sorted properly when tiling with the Tiling // Popup because altTab sorts by focus. const sorted = global.display.sort_windows_by_stacking(openWindows); - return sorted.reverse().filter(w => { + return sorted.reverse().filter((w) => { // I don't think this should normally happen but if it does, this // extension can crash GNOME Shell.. so guard against it. A way to // have a window's monitor be -1, for example, is explained here: // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/4713 - if (w.get_monitor() === -1) + if (w.get_monitor() === -1) { return false; + } // Assumption: a maximized window can also resize (once unmaximized) - const canResize = w.allows_move() && w.allows_resize() || this.isMaximized(w); + const canResize = + (w.allows_move() && w.allows_resize()) || this.isMaximized(w); return canResize; }); } @@ -136,8 +138,10 @@ export class TilingWindowManager { */ static isMaximized(window, workArea = null) { const area = workArea ?? window.get_work_area_current_monitor(); - return window.get_maximized() === Meta.MaximizeFlags.BOTH || - window.tiledRect?.equal(area); + return ( + window.get_maximized() === Meta.MaximizeFlags.BOTH || + window.tiledRect?.equal(area) + ); } /** @@ -145,38 +149,46 @@ export class TilingWindowManager { * * @param {Meta.Window} window a Meta.Window to tile. * @param {Rect} newRect the Rect the `window` will be tiled to. - * @param {boolean} [openTilingPopup=true] decides, if we open a Tiling + * @param {object} param + * @param {boolean} [param.openTilingPopup] decides, if we open a Tiling * Popup after the window is tiled and there is unambiguous free * screen space. - * @param {number} [number=null] is used to get the workArea in which the + * @param {boolean} [param.ignoreTA] + * @param {number} [param.monitorNr] is used to get the workArea in which the * window tiles on. It's used for gap calculation. We can't always rely on * window.get_monitor with its monitor or global.display.get_current_monitor * (the pointer monitor) because of the 'grace period' during a quick dnd * towards a screen border since the pointer and the window will be on the * 'wrong' monitor. - * @param {boolean} [skipAnim=false] decides, if we skip the tile animation. - * @param {boolean} [tileGroup=null] forces the creation of this tile group. - * @param {boolean} [fakeTile=false] don't create a new tile group, don't + * @param {boolean} [param.skipAnim] decides, if we skip the tile animation. + * @param {boolean} [param.fakeTile] don't create a new tile group, don't * emit 'tiled' signal or open the Tiling Popup */ - static async tile(window, newRect, { - openTilingPopup = true, - ignoreTA = false, - monitorNr = null, - skipAnim = false, - fakeTile = false - } = {}) { - if (!window || window.is_skip_taskbar()) + static async tile( + window, + newRect, + { + openTilingPopup = true, + ignoreTA = false, + monitorNr = null, + skipAnim = false, + fakeTile = false, + } = {}, + ) { + if (!window || window.is_skip_taskbar()) { return; + } const wasMaximized = window.get_maximized(); - if (wasMaximized) + if (wasMaximized) { window.unmaximize(wasMaximized); + } window.unmake_fullscreen(); - if (!window.allows_resize() || !window.allows_move()) + if (!window.allows_resize() || !window.allows_move()) { return; + } // Remove window from the other windows' tileGroups so it // doesn't falsely get raised with them. @@ -186,10 +198,13 @@ export class TilingWindowManager { window.unminimize(); // Raise window since tiling with the popup means that // the window can be below others. - if (window.raise_and_make_recent_on_workspace) - window.raise_and_make_recent_on_workspace(global.workspace_manager.get_active_workspace()); - else + if (window.raise_and_make_recent_on_workspace) { + window.raise_and_make_recent_on_workspace( + global.workspace_manager.get_active_workspace(), + ); + } else { window.raise_and_make_recent(); + } const oldRect = new Rect(window.get_frame_rect()); const monitor = monitorNr ?? window.get_monitor(); @@ -197,8 +212,9 @@ export class TilingWindowManager { const maximize = newRect.equal(workArea); window.isTiled = !maximize; - if (!window.untiledRect) + if (!window.untiledRect) { window.untiledRect = oldRect; + } if (maximize && !Settings.getBoolean('maximize-with-gap')) { window.tiledRect = null; @@ -215,11 +231,15 @@ export class TilingWindowManager { // For ex. which only resize in full rows/columns like gnome-terminal window.tiledRect = newRect.copy(); - const { x, y, width, height } = newRect.addGaps(workArea, monitor); + const {x, y, width, height} = newRect.addGaps(workArea, monitor); // Animations const wActor = window.get_compositor_private(); - if (Settings.getBoolean('enable-tile-animations') && wActor && !skipAnim) { + if ( + Settings.getBoolean('enable-tile-animations') && + wActor && + !skipAnim + ) { wActor.remove_all_transitions(); // HACK => journalctl: 'error in size change accounting'...? // TODO: no animation if going from maximized -> tiled and back to back multiple times? @@ -227,7 +247,7 @@ export class TilingWindowManager { global.window_manager, wActor, oldRect.meta, - Meta.SizeChange.MAXIMIZE + Meta.SizeChange.MAXIMIZE, ); } @@ -254,7 +274,7 @@ export class TilingWindowManager { this._updateGappedMaxWindowSignals(window); this.saveTileState(window); - // Tiled window + // Tiled window } else if (!fakeTile) { // Make the tile group only consist of the window itself to stop // resizing or raising together. Also don't call the Tiling Popup. @@ -271,8 +291,9 @@ export class TilingWindowManager { this.emit('window-tiled', window); - if (openTilingPopup) + if (openTilingPopup) { await this.tryOpeningTilingPopup(); + } } } @@ -280,31 +301,50 @@ export class TilingWindowManager { * Untiles a tiled window and delete all tiling properties. * * @param {Meta.Window} window a Meta.Window to untile. - * @param {boolean} [restoreFullPos=true] decides, if we restore the + * @param {object} param + * @param {boolean} [param.restoreFullPos] decides, if we restore the * pre-tile position or whether the size while keeping the titlebar * at the relative same position. - * @param {number} [xAnchor=undefined] used when wanting to restore the + * @param {number} [param.xAnchor] used when wanting to restore the * size while keeping titlebar at the relative x position. By default, * we use the pointer position. - * @param {boolean} [skipAnim=false] decides, if we skip the until animation. + * @param {boolean} [param.skipAnim] decides, if we skip the until animation. + * @param {boolean} [param.clampToWorkspace] */ - static untile(window, { restoreFullPos = true, xAnchor = undefined, skipAnim = false, clampToWorkspace = false } = {}) { + static untile( + window, + { + restoreFullPos = true, + xAnchor = undefined, + skipAnim = false, + clampToWorkspace = false, + } = {}, + ) { const wasMaximized = window.get_maximized(); - if (wasMaximized) + if (wasMaximized) { window.unmaximize(wasMaximized); + } - if (!window.untiledRect || !window.allows_resize() || !window.allows_move()) + if ( + !window.untiledRect || + !window.allows_resize() || + !window.allows_move() + ) { return; + } // If you tiled a window and then used the popup to tile more // windows, the consecutive windows will be raised above the first // one. So untiling the initial window after tiling more windows with // the popup (without re-focusing the initial window), means the // untiled window will be below the others. - if (window.raise_and_make_recent_on_workspace) - window.raise_and_make_recent_on_workspace(global.workspace_manager.get_active_workspace()); - else + if (window.raise_and_make_recent_on_workspace) { + window.raise_and_make_recent_on_workspace( + global.workspace_manager.get_active_workspace(), + ); + } else { window.raise_and_make_recent(); + } // Animation const untileAnim = Settings.getBoolean('enable-untile-animations'); @@ -315,7 +355,7 @@ export class TilingWindowManager { global.window_manager, wActor, window.get_frame_rect(), - Meta.SizeChange.UNMAXIMIZE + Meta.SizeChange.UNMAXIMIZE, ); } @@ -324,18 +364,33 @@ export class TilingWindowManager { const userOp = !clampToWorkspace; const oldRect = window.untiledRect; if (restoreFullPos) { - window.move_resize_frame(userOp, oldRect.x, oldRect.y, oldRect.width, oldRect.height); + window.move_resize_frame( + userOp, + oldRect.x, + oldRect.y, + oldRect.width, + oldRect.height, + ); } else { // Resize the window while keeping the relative x pos (of the pointer) const currWindowFrame = new Rect(window.get_frame_rect()); xAnchor = xAnchor ?? global.get_pointer()[0]; - const relativeMouseX = (xAnchor - currWindowFrame.x) / currWindowFrame.width; + const relativeMouseX = + (xAnchor - currWindowFrame.x) / currWindowFrame.width; const newPosX = xAnchor - oldRect.width * relativeMouseX; // Wayland workaround for DND / restore position - Meta.is_wayland_compositor() && window.move_frame(true, newPosX, currWindowFrame.y); + if (Meta.is_wayland_compositor()) { + window.move_frame(true, newPosX, currWindowFrame.y); + } - window.move_resize_frame(userOp, newPosX, currWindowFrame.y, oldRect.width, oldRect.height); + window.move_resize_frame( + userOp, + newPosX, + currWindowFrame.y, + oldRect.width, + oldRect.height, + ); } this.clearTilingProps(window.get_id()); @@ -355,7 +410,7 @@ export class TilingWindowManager { * @param {Meta.Workspace} workspace */ static moveGroupToWorkspace(tileGroup, workspace) { - tileGroup.forEach(w => { + tileGroup.forEach((w) => { this._blockTilingSignalsFor(w); w.change_workspace(workspace); this._unblockTilingSignalsFor(w); @@ -371,28 +426,43 @@ export class TilingWindowManager { */ static moveGroupToMonitor(tileGroup, oldMon, newMon) { const activeWs = global.workspace_manager.get_active_workspace(); - const oldWorkArea = new Rect(activeWs.get_work_area_for_monitor(oldMon)); - const newWorkArea = new Rect(activeWs.get_work_area_for_monitor(newMon)); + const oldWorkArea = new Rect( + activeWs.get_work_area_for_monitor(oldMon), + ); + const newWorkArea = new Rect( + activeWs.get_work_area_for_monitor(newMon), + ); const hScale = oldWorkArea.width / newWorkArea.width; const vScale = oldWorkArea.height / newWorkArea.height; tileGroup.forEach((w, idx) => { const newTile = w.tiledRect.copy(); - newTile.x = newWorkArea.x + Math.floor(newWorkArea.width * ((w.tiledRect.x - oldWorkArea.x) / oldWorkArea.width)); - newTile.y = newWorkArea.y + Math.floor(newWorkArea.height * ((w.tiledRect.y - oldWorkArea.y) / oldWorkArea.height)); + newTile.x = + newWorkArea.x + + Math.floor( + newWorkArea.width * + ((w.tiledRect.x - oldWorkArea.x) / oldWorkArea.width), + ); + newTile.y = + newWorkArea.y + + Math.floor( + newWorkArea.height * + ((w.tiledRect.y - oldWorkArea.y) / oldWorkArea.height), + ); newTile.width = Math.floor(w.tiledRect.width * (1 / hScale)); newTile.height = Math.floor(w.tiledRect.height * (1 / vScale)); // Try to align with all previously scaled tiles and the workspace to prevent gaps - for (let i = 0; i < idx; i++) + for (let i = 0; i < idx; i++) { newTile.tryAlignWith(tileGroup[i].tiledRect); + } newTile.tryAlignWith(newWorkArea, 10); this.tile(w, newTile, { skipAnim: true, - fakeTile: true + fakeTile: true, }); }); @@ -443,11 +513,14 @@ export class TilingWindowManager { * together. */ static updateTileGroup(tileGroup) { - tileGroup.forEach(window => { + tileGroup.forEach((window) => { const windowId = window.get_id(); const signals = this._signals.getSignalsFor(windowId); - this._tileGroups.set(windowId, tileGroup.map(w => w.get_id())); + this._tileGroups.set( + windowId, + tileGroup.map((w) => w.get_id()), + ); /** * clearTilingProps may have been called before this function, @@ -457,7 +530,9 @@ export class TilingWindowManager { // Reconnect unmanaging signal const unmanagingSignal = signals.get(TilingSignals.UNMANAGING); - unmanagingSignal && window.disconnect(unmanagingSignal); + if (unmanagingSignal) { + window.disconnect(unmanagingSignal); + } const umId = window.connect('unmanaging', () => { this.clearTilingProps(windowId); @@ -466,22 +541,31 @@ export class TilingWindowManager { // Reconnect ws-changed signal const wsChangeSignal = signals.get(TilingSignals.WS_CHANGED); - wsChangeSignal && window.disconnect(wsChangeSignal); + if (wsChangeSignal) { + window.disconnect(wsChangeSignal); + } - const wsId = window.connect('workspace-changed', () => this._onWindowWorkspaceChanged(window)); + const wsId = window.connect('workspace-changed', () => + this._onWindowWorkspaceChanged(window), + ); signals.set(TilingSignals.WS_CHANGED, wsId); // Reconnect raise signal const raiseSignal = signals.get(TilingSignals.RAISE); - raiseSignal && window.disconnect(raiseSignal); + if (raiseSignal) { + window.disconnect(raiseSignal); + } - const raiseId = window.connect('raised', raisedWindow => { + const raiseId = window.connect('raised', (raisedWindow) => { const raisedWindowId = raisedWindow.get_id(); if (Settings.getBoolean('enable-raise-tile-group')) { - const raisedWindowsTileGroup = this._tileGroups.get(raisedWindowId); - raisedWindowsTileGroup.forEach(wId => { + const raisedWindowsTileGroup = + this._tileGroups.get(raisedWindowId); + raisedWindowsTileGroup.forEach((wId) => { const w = this._getWindow(wId); - const otherRaiseId = this._signals.getSignalsFor(wId).get(TilingSignals.RAISE); + const otherRaiseId = this._signals + .getSignalsFor(wId) + .get(TilingSignals.RAISE); // May be undefined, if w was just closed. This would // automatically call clearTilingProps() with the signal // but in case I missed / don't know about other cases where @@ -493,22 +577,30 @@ export class TilingWindowManager { // Prevent an infinite loop of windows raising each other w.block_signal_handler(otherRaiseId); - if (w.raise_and_make_recent_on_workspace) - w.raise_and_make_recent_on_workspace(global.workspace_manager.get_active_workspace()); - else + if (w.raise_and_make_recent_on_workspace) { + w.raise_and_make_recent_on_workspace( + global.workspace_manager.get_active_workspace(), + ); + } else { w.raise_and_make_recent(); + } w.unblock_signal_handler(otherRaiseId); }); // Re-raise the just raised window so it may not be below // other tiled windows otherwise when untiling via keyboard // it may be below other tiled windows. - const signalId = this._signals.getSignalsFor(raisedWindowId).get(TilingSignals.RAISE); + const signalId = this._signals + .getSignalsFor(raisedWindowId) + .get(TilingSignals.RAISE); raisedWindow.block_signal_handler(signalId); - if (raisedWindow.raise_and_make_recent_on_workspace) - raisedWindow.raise_and_make_recent_on_workspace(global.workspace_manager.get_active_workspace()); - else + if (raisedWindow.raise_and_make_recent_on_workspace) { + raisedWindow.raise_and_make_recent_on_workspace( + global.workspace_manager.get_active_workspace(), + ); + } else { raisedWindow.raise_and_make_recent(); + } raisedWindow.unblock_signal_handler(signalId); } @@ -536,43 +628,50 @@ export class TilingWindowManager { const signals = this._signals.getSignalsFor(windowId); if (signals.get(TilingSignals.RAISE)) { - window && window.disconnect(signals.get(TilingSignals.RAISE)); + window?.disconnect(signals.get(TilingSignals.RAISE)); signals.set(TilingSignals.RAISE, 0); } if (signals.get(TilingSignals.WS_CHANGED)) { - window && window.disconnect(signals.get(TilingSignals.WS_CHANGED)); + window?.disconnect(signals.get(TilingSignals.WS_CHANGED)); signals.set(TilingSignals.WS_CHANGED, 0); } if (signals.get(TilingSignals.UNMANAGING)) { - window && window.disconnect(signals.get(TilingSignals.UNMANAGING)); + window?.disconnect(signals.get(TilingSignals.UNMANAGING)); signals.set(TilingSignals.UNMANAGING, 0); } - if (!this._tileGroups.has(windowId)) + if (!this._tileGroups.has(windowId)) { return; + } // Delete window's tileGroup this._tileGroups.delete(windowId); // Delete window from other windows' tileGroup - this._tileGroups.forEach(tileGroup => { + this._tileGroups.forEach((tileGroup) => { const idx = tileGroup.indexOf(windowId); - idx !== -1 && tileGroup.splice(idx, 1); + if (idx !== -1) { + tileGroup.splice(idx, 1); + } }); } /** * @param {Meta.Window} window a Meta.Window. + * * @returns {Meta.Window[]} an array of Meta.Windows, which are in `window`'s * tile group (including the `window` itself). */ static getTileGroupFor(window) { const tileGroup = this._tileGroups.get(window.get_id()); - if (!tileGroup) + if (!tileGroup) { return []; + } - return this._getAllWindows().filter(w => tileGroup.includes(w.get_id())); + return this._getAllWindows().filter((w) => + tileGroup.includes(w.get_id()), + ); } /** @@ -581,58 +680,69 @@ export class TilingWindowManager { * *tracked* tile groups since floating windows may overlap some tiled * windows *at the moment* when this function is called. * - * @param {boolean} [skipTopWindow=true] whether we ignore the focused window + * @param {object} param + * @param {boolean} [param.skipTopWindow] whether we ignore the focused window * in the active search for the top tile group. The focused window may * still be part of the returned array if it is part of another high- * stacked window's tile group. This is mainly only useful, if the * focused window isn't tiled (for example when dnd-ing a window). - * @param {number} [monitor=null] get the group for the monitor number. + * @param {number} [param.monitor] get the group for the monitor number. + * * @returns {Meta.Windows[]} an array of tiled Meta.Windows. */ - static getTopTileGroup({ skipTopWindow = false, monitor = null } = {}) { + static getTopTileGroup({skipTopWindow = false, monitor = null} = {}) { // 'Raise Tile Group' setting is enabled so we just return the tracked // tile group. Same thing for the setting 'Disable Tile Groups' because // it's implemented by just making the tile groups consist of single // windows (the tiled window itself). - if (Settings.getBoolean('enable-raise-tile-group') || + if ( + Settings.getBoolean('enable-raise-tile-group') || Settings.getBoolean('disable-tile-groups') ) { const openWindows = this.getWindows(); - if (!openWindows.length) + if (!openWindows.length) { return []; + } if (skipTopWindow) { // the focused window isn't necessarily the top window due to always // on top windows. const idx = openWindows.indexOf(global.display.focus_window); - idx !== -1 && openWindows.splice(idx, 1); + if (idx !== -1) { + openWindows.splice(idx, 1); + } } const ignoredWindows = []; - const mon = monitor ?? + const mon = + monitor ?? global.display.focus_window?.get_monitor() ?? openWindows[0].get_monitor(); for (const window of openWindows) { - if (window.get_monitor() !== mon) + if (window.get_monitor() !== mon) { continue; + } // Ignore non-tiled windows, which are always-on-top, for the // calculation since they are probably some utility apps etc. - if (window.is_above() && !window.isTiled) + if (window.is_above() && !window.isTiled) { continue; + } // Find the first not overlapped tile group, if it exists if (window.isTiled) { - const overlapsIgnoredWindow = ignoredWindows.some(w => { - const rect = w.tiledRect ?? new Rect(w.get_frame_rect()); + const overlapsIgnoredWindow = ignoredWindows.some((w) => { + const rect = + w.tiledRect ?? new Rect(w.get_frame_rect()); return rect.overlap(window.tiledRect); }); - if (overlapsIgnoredWindow) + if (overlapsIgnoredWindow) { ignoredWindows.push(window); - else + } else { return this.getTileGroupFor(window); + } } else { ignoredWindows.push(window); } @@ -640,10 +750,10 @@ export class TilingWindowManager { return []; - // 'Raise Tile Group' setting is disabled so we get thetop most - // non-overlapped/ing tiled windows ignoring the tile groups. + // 'Raise Tile Group' setting is disabled so we get thetop most + // non-overlapped/ing tiled windows ignoring the tile groups. } else { - return this._getTopTiledWindows({ skipTopWindow, monitor }); + return this._getTopTiledWindows({skipTopWindow, monitor}); } } @@ -662,25 +772,36 @@ export class TilingWindowManager { const monitor = monitorNr ?? global.display.get_current_monitor(); const workArea = new Rect(activeWs.get_work_area_for_monitor(monitor)); const freeScreenRects = workArea.minus(rectList); - if (!freeScreenRects.length) + if (!freeScreenRects.length) { return null; + } // Create the union of all freeScreenRects and calculate the sum // of their areas. If the area of the union-rect equals the area // of the individual rects, the individual rects align properly. - const startRect = new Rect(freeScreenRects[0].x, freeScreenRects[0].y, 0, 0); - const { checkSum, combinedRect } = freeScreenRects.reduce((result, rect) => { - result.checkSum += rect.area; - result.combinedRect = result.combinedRect.union(rect); - return result; - }, { checkSum: 0, combinedRect: startRect }); - - if (combinedRect.area !== checkSum) + const startRect = new Rect( + freeScreenRects[0].x, + freeScreenRects[0].y, + 0, + 0, + ); + const {checkSum, combinedRect} = freeScreenRects.reduce( + (result, rect) => { + result.checkSum += rect.area; + result.combinedRect = result.combinedRect.union(rect); + return result; + }, + {checkSum: 0, combinedRect: startRect}, + ); + + if (combinedRect.area !== checkSum) { return null; + } // Random min. size requirement - if (combinedRect.width < 250 || combinedRect.height < 250) + if (combinedRect.width < 250 || combinedRect.height < 250) { return null; + } return combinedRect; } @@ -692,19 +813,25 @@ export class TilingWindowManager { * * @param {Rect[]} rectList an array of Rects, which occupy the screen. * Like usual, they shouldn't overlap each other. - * @param {Rect} [currRect=null] a Rect, which may be expanded. - * @param {Orientation} [orientation=null] The orientation we want to expand + * @param {object} param + * @param {Rect} [param.currRect] a Rect, which may be expanded. + * @param {Orientation} [param.orientation] The orientation we want to expand * `currRect` into. If `null`, expand in both orientations. - * @param {Rect} [monitor=null] defaults to pointer monitor. + * @param {Rect} [param.monitorNr] defaults to pointer monitor. + * * @returns {Rect} a new Rect. */ - static getBestFreeRect(rectList, { currRect = null, orientation = null, monitorNr = null } = {}) { + static getBestFreeRect( + rectList, + {currRect = null, orientation = null, monitorNr = null} = {}, + ) { const activeWs = global.workspace_manager.get_active_workspace(); const monitor = monitorNr ?? global.display.get_current_monitor(); const workArea = new Rect(activeWs.get_work_area_for_monitor(monitor)); const freeRects = workArea.minus(rectList); - if (!freeRects.length) + if (!freeRects.length) { return currRect ?? new Rect(workArea); + } // Try to expand the currRect to fill the rest of the space // that is available around it. @@ -717,22 +844,33 @@ export class TilingWindowManager { // currRect and sort the array so that the free rects are ordered // from the left to the right or from the top to the bottom. See // below for the reasoning. - const borderingRects = freeRects.filter(r => { - const axis1 = currRect[xpndPos1] === r[xpndPos2] || currRect[xpndPos2] === r[xpndPos1]; - const axis2 = isVert ? currRect.horizOverlap(r) : currRect.vertOverlap(r); - return axis1 && axis2; - }).sort((a, b) => a[unxpndPos1] - b[unxpndPos1]); + const borderingRects = freeRects + .filter((r) => { + const axis1 = + currRect[xpndPos1] === r[xpndPos2] || + currRect[xpndPos2] === r[xpndPos1]; + const axis2 = + isVert ? + currRect.horizOverlap(r) + : currRect.vertOverlap(r); + return axis1 && axis2; + }) + .sort((a, b) => a[unxpndPos1] - b[unxpndPos1]); // Separate the rects into the ones that come before (left / top) // or after (right / bottom) the current rect. - const { before, after } = borderingRects.reduce((result, r) => { - if (currRect[xpndPos1] === r[xpndPos2]) - result.before.push(r); - else if (currRect[xpndPos2] === r[xpndPos1]) - result.after.push(r); - - return result; - }, { before: [], after: [] }); + const {before, after} = borderingRects.reduce( + (result, r) => { + if (currRect[xpndPos1] === r[xpndPos2]) { + result.before.push(r); + } else if (currRect[xpndPos2] === r[xpndPos1]) { + result.after.push(r); + } + + return result; + }, + {before: [], after: []}, + ); // If we want to check whether the current rect can expand on a certain // side (let's say we expand the height), we need to check the *other* @@ -746,17 +884,26 @@ export class TilingWindowManager { // Orientation doesn't matter here since we are always comparing sides // of the same orientation. So just make the side always horizontal. - const makeSide = (startPoint, endPoint) => new Mtk.Rectangle({ - x: startPoint, - width: endPoint - startPoint, - height: 1 - }); - const freeRectsContainCurrRectSide = rects => { - const currRectSide = makeSide(currRect[unxpndPos1], currRect[unxpndPos2]); - const linkedSides = rects.reduce((linked, r) => { - const side = makeSide(r[unxpndPos1], r[unxpndPos2]); - return linked.overlap(side) ? linked.union(side) : linked; - }, makeSide(rects[0][unxpndPos1], rects[0][unxpndPos2])); + const makeSide = (startPoint, endPoint) => + new Mtk.Rectangle({ + x: startPoint, + width: endPoint - startPoint, + height: 1, + }); + const freeRectsContainCurrRectSide = (rects) => { + const currRectSide = makeSide( + currRect[unxpndPos1], + currRect[unxpndPos2], + ); + const linkedSides = rects.reduce( + (linked, r) => { + const side = makeSide(r[unxpndPos1], r[unxpndPos2]); + return linked.overlap(side) ? + linked.union(side) + : linked; + }, + makeSide(rects[0][unxpndPos1], rects[0][unxpndPos2]), + ); return linkedSides.contains_rect(currRectSide); }; @@ -790,30 +937,33 @@ export class TilingWindowManager { // if orientation is null, we expanded vertically. Now we want // to expand horizontally as well. rectList = [...rectList]; - const currRectIdx = rectList.findIndex(r => r.equal(currRect)); + const currRectIdx = rectList.findIndex((r) => + r.equal(currRect), + ); rectList.splice(currRectIdx, 1); rectList.push(newRect); return newRect.union( this.getBestFreeRect(rectList, { currRect: newRect, orientation: Orientation.H, - monitorNr: monitor - })); + monitorNr: monitor, + }), + ); } else { return newRect; } - // No currRect was passed, so we just choose the single biggest free rect - // and expand it using this function. This is a naive approach and doesn't - // guarantee that we get the best combination of free screen rects... but - // it should be good enough. + // No currRect was passed, so we just choose the single biggest free rect + // and expand it using this function. This is a naive approach and doesn't + // guarantee that we get the best combination of free screen rects... but + // it should be good enough. } else { const biggestSingle = freeRects.reduce((currBiggest, rect) => { return currBiggest.area >= rect.area ? currBiggest : rect; }); rectList.push(biggestSingle); - return this.getBestFreeRect(rectList, { currRect: biggestSingle }); + return this.getBestFreeRect(rectList, {currRect: biggestSingle}); } } @@ -831,13 +981,14 @@ export class TilingWindowManager { * @returns {Meta.Window|null} the nearest Meta.Window. */ static getNearestWindow(currWindow, windows, dir, wrap = true) { - const getRect = w => w.tiledRect ?? new Rect(w.get_frame_rect()); - const rects = windows.map(w => getRect(w)); + const getRect = (w) => w.tiledRect ?? new Rect(w.get_frame_rect()); + const rects = windows.map((w) => getRect(w)); const nearestRect = getRect(currWindow).getNeighbor(dir, rects, wrap); - if (!nearestRect) + if (!nearestRect) { return null; + } - return windows.find(w => getRect(w).equal(nearestRect)); + return windows.find((w) => getRect(w).equal(nearestRect)); } /** @@ -858,14 +1009,19 @@ export class TilingWindowManager { * used to implement the 'grace period' to enable quickly tiling a * window using the screen edges even if there is another monitor * at that edge. - * @returns a Rect. + * + * @returns {Rect} a Rect. */ static getTileFor(shortcut, workArea, monitor = null) { // Don't try to adapt a tile rect - if (Settings.getBoolean('disable-tile-groups')) + if (Settings.getBoolean('disable-tile-groups')) { return this.getDefaultTileFor(shortcut, workArea); + } - const topTileGroup = this.getTopTileGroup({ skipTopWindow: true, monitor }); + const topTileGroup = this.getTopTileGroup({ + skipTopWindow: true, + monitor, + }); // getTileFor is used to get the adaptive tiles for dnd & tiling keyboard // shortcuts. That's why the top most window needs to be ignored when // calculating the new tile rect. The top most window is already ignored @@ -874,85 +1030,190 @@ export class TilingWindowManager { // part of the returned array if it's part of another high-stackeing // window's tile group. const idx = topTileGroup.indexOf(global.display.focus_window); - idx !== -1 && topTileGroup.splice(idx, 1); + + if (idx !== -1) { + topTileGroup.splice(idx, 1); + } + const favLayout = Util.getFavoriteLayout(monitor); - const useFavLayout = favLayout.length && Settings.getBoolean('adapt-edge-tiling-to-favorite-layout'); - const twRects = useFavLayout && favLayout || topTileGroup.map(w => w.tiledRect); + const useFavLayout = + favLayout.length && + Settings.getBoolean('adapt-edge-tiling-to-favorite-layout'); + const twRects = + (useFavLayout && favLayout) || topTileGroup.map((w) => w.tiledRect); - if (!twRects.length) + if (!twRects.length) { return this.getDefaultTileFor(shortcut, workArea); + } // Return the adapted rect only if it doesn't overlap an existing tile. // Ignore an overlap, if a fav layout is used since we always prefer the // user set layout in that case. - const getTile = rect => { - if (useFavLayout) + const getTile = (rect) => { + if (useFavLayout) { return rect; + } - const overlapsTiles = twRects.some(r => r.overlap(rect)); - return overlapsTiles ? this.getDefaultTileFor(shortcut, workArea) : rect; + const overlapsTiles = twRects.some((r) => r.overlap(rect)); + return overlapsTiles ? + this.getDefaultTileFor(shortcut, workArea) + : rect; }; const screenRects = twRects.concat(workArea.minus(twRects)); switch (shortcut) { case 'tile-maximize': { return workArea.copy(); - } case 'tile-left-half': { - const left = screenRects.find(r => r.x === workArea.x && r.width !== workArea.width); - const { width } = left ?? workArea.getUnitAt(0, workArea.width / 2, Orientation.V); - const result = new Rect(workArea.x, workArea.y, width, workArea.height); + } + case 'tile-left-half': { + const left = screenRects.find( + (r) => r.x === workArea.x && r.width !== workArea.width, + ); + const {width} = + left ?? + workArea.getUnitAt(0, workArea.width / 2, Orientation.V); + const result = new Rect( + workArea.x, + workArea.y, + width, + workArea.height, + ); return getTile(result); - } case 'tile-right-half': { - const right = screenRects.find(r => r.x2 === workArea.x2 && r.width !== workArea.width); - const { width } = right ?? workArea.getUnitAt(1, workArea.width / 2, Orientation.V); - const result = new Rect(workArea.x2 - width, workArea.y, width, workArea.height); + } + case 'tile-right-half': { + const right = screenRects.find( + (r) => r.x2 === workArea.x2 && r.width !== workArea.width, + ); + const {width} = + right ?? + workArea.getUnitAt(1, workArea.width / 2, Orientation.V); + const result = new Rect( + workArea.x2 - width, + workArea.y, + width, + workArea.height, + ); return getTile(result); - } case 'tile-top-half': { - const top = screenRects.find(r => r.y === workArea.y && r.height !== workArea.height); - const { height } = top ?? workArea.getUnitAt(0, workArea.height / 2, Orientation.H); - const result = new Rect(workArea.x, workArea.y, workArea.width, height); + } + case 'tile-top-half': { + const top = screenRects.find( + (r) => r.y === workArea.y && r.height !== workArea.height, + ); + const {height} = + top ?? + workArea.getUnitAt(0, workArea.height / 2, Orientation.H); + const result = new Rect( + workArea.x, + workArea.y, + workArea.width, + height, + ); return getTile(result); - } case 'tile-bottom-half': { - const bottom = screenRects.find(r => r.y2 === workArea.y2 && r.height !== workArea.height); - const { height } = bottom ?? workArea.getUnitAt(1, workArea.height / 2, Orientation.H); - const result = new Rect(workArea.x, workArea.y2 - height, workArea.width, height); + } + case 'tile-bottom-half': { + const bottom = screenRects.find( + (r) => r.y2 === workArea.y2 && r.height !== workArea.height, + ); + const {height} = + bottom ?? + workArea.getUnitAt(1, workArea.height / 2, Orientation.H); + const result = new Rect( + workArea.x, + workArea.y2 - height, + workArea.width, + height, + ); return getTile(result); - } case 'tile-topleft-quarter': { - const left = screenRects.find(r => r.x === workArea.x && r.width !== workArea.width); - const { width } = left ?? workArea.getUnitAt(0, workArea.width / 2, Orientation.V); - const top = screenRects.find(r => r.y === workArea.y && r.height !== workArea.height); - const { height } = top ?? workArea.getUnitAt(0, workArea.height / 2, Orientation.H); + } + case 'tile-topleft-quarter': { + const left = screenRects.find( + (r) => r.x === workArea.x && r.width !== workArea.width, + ); + const {width} = + left ?? + workArea.getUnitAt(0, workArea.width / 2, Orientation.V); + const top = screenRects.find( + (r) => r.y === workArea.y && r.height !== workArea.height, + ); + const {height} = + top ?? + workArea.getUnitAt(0, workArea.height / 2, Orientation.H); const result = new Rect(workArea.x, workArea.y, width, height); return getTile(result); - } case 'tile-topright-quarter': { - const right = screenRects.find(r => r.x2 === workArea.x2 && r.width !== workArea.width); - const { width } = right ?? workArea.getUnitAt(1, workArea.width / 2, Orientation.V); - const top = screenRects.find(r => r.y === workArea.y && r.height !== workArea.height); - const { height } = top ?? workArea.getUnitAt(0, workArea.height / 2, Orientation.H); - const result = new Rect(workArea.x2 - width, workArea.y, width, height); + } + case 'tile-topright-quarter': { + const right = screenRects.find( + (r) => r.x2 === workArea.x2 && r.width !== workArea.width, + ); + const {width} = + right ?? + workArea.getUnitAt(1, workArea.width / 2, Orientation.V); + const top = screenRects.find( + (r) => r.y === workArea.y && r.height !== workArea.height, + ); + const {height} = + top ?? + workArea.getUnitAt(0, workArea.height / 2, Orientation.H); + const result = new Rect( + workArea.x2 - width, + workArea.y, + width, + height, + ); return getTile(result); - } case 'tile-bottomleft-quarter': { - const left = screenRects.find(r => r.x === workArea.x && r.width !== workArea.width); - const { width } = left ?? workArea.getUnitAt(0, workArea.width / 2, Orientation.V); - const bottom = screenRects.find(r => r.y2 === workArea.y2 && r.height !== workArea.height); - const { height } = bottom ?? workArea.getUnitAt(1, workArea.height / 2, Orientation.H); - const result = new Rect(workArea.x, workArea.y2 - height, width, height); + } + case 'tile-bottomleft-quarter': { + const left = screenRects.find( + (r) => r.x === workArea.x && r.width !== workArea.width, + ); + const {width} = + left ?? + workArea.getUnitAt(0, workArea.width / 2, Orientation.V); + const bottom = screenRects.find( + (r) => r.y2 === workArea.y2 && r.height !== workArea.height, + ); + const {height} = + bottom ?? + workArea.getUnitAt(1, workArea.height / 2, Orientation.H); + const result = new Rect( + workArea.x, + workArea.y2 - height, + width, + height, + ); return getTile(result); - } case 'tile-bottomright-quarter': { - const right = screenRects.find(r => r.x2 === workArea.x2 && r.width !== workArea.width); - const { width } = right ?? workArea.getUnitAt(1, workArea.width / 2, Orientation.V); - const bottom = screenRects.find(r => r.y2 === workArea.y2 && r.height !== workArea.height); - const { height } = bottom ?? workArea.getUnitAt(1, workArea.height / 2, Orientation.H); - const result = new Rect(workArea.x2 - width, workArea.y2 - height, width, height); + } + case 'tile-bottomright-quarter': { + const right = screenRects.find( + (r) => r.x2 === workArea.x2 && r.width !== workArea.width, + ); + const {width} = + right ?? + workArea.getUnitAt(1, workArea.width / 2, Orientation.V); + const bottom = screenRects.find( + (r) => r.y2 === workArea.y2 && r.height !== workArea.height, + ); + const {height} = + bottom ?? + workArea.getUnitAt(1, workArea.height / 2, Orientation.H); + const result = new Rect( + workArea.x2 - width, + workArea.y2 - height, + width, + height, + ); return getTile(result); } } + + throw new Error('Invalid shortcut'); } /** * @param {string} shortcut determines, which half/quarter to get the tile for * @param {Rect} workArea - * @returns + * + * @returns {Rect} */ static getDefaultTileFor(shortcut, workArea) { switch (shortcut) { @@ -966,23 +1227,41 @@ export class TilingWindowManager { return workArea.getUnitAt(1, workArea.width / 2, Orientation.V); case 'tile-top-half': case 'tile-top-half-ignore-ta': - return workArea.getUnitAt(0, workArea.height / 2, Orientation.H); + return workArea.getUnitAt( + 0, + workArea.height / 2, + Orientation.H, + ); case 'tile-bottom-half': case 'tile-bottom-half-ignore-ta': - return workArea.getUnitAt(1, workArea.height / 2, Orientation.H); + return workArea.getUnitAt( + 1, + workArea.height / 2, + Orientation.H, + ); case 'tile-topleft-quarter': case 'tile-topleft-quarter-ignore-ta': - return workArea.getUnitAt(0, workArea.width / 2, Orientation.V).getUnitAt(0, workArea.height / 2, Orientation.H); + return workArea + .getUnitAt(0, workArea.width / 2, Orientation.V) + .getUnitAt(0, workArea.height / 2, Orientation.H); case 'tile-topright-quarter': case 'tile-topright-quarter-ignore-ta': - return workArea.getUnitAt(1, workArea.width / 2, Orientation.V).getUnitAt(0, workArea.height / 2, Orientation.H); + return workArea + .getUnitAt(1, workArea.width / 2, Orientation.V) + .getUnitAt(0, workArea.height / 2, Orientation.H); case 'tile-bottomleft-quarter': case 'tile-bottomleft-quarter-ignore-ta': - return workArea.getUnitAt(0, workArea.width / 2, Orientation.V).getUnitAt(1, workArea.height / 2, Orientation.H); + return workArea + .getUnitAt(0, workArea.width / 2, Orientation.V) + .getUnitAt(1, workArea.height / 2, Orientation.H); case 'tile-bottomright-quarter': case 'tile-bottomright-quarter-ignore-ta': - return workArea.getUnitAt(1, workArea.width / 2, Orientation.V).getUnitAt(1, workArea.height / 2, Orientation.H); + return workArea + .getUnitAt(1, workArea.width / 2, Orientation.V) + .getUnitAt(1, workArea.height / 2, Orientation.H); } + + throw new Error('Invalid shortcut'); } /** @@ -990,26 +1269,35 @@ export class TilingWindowManager { * and offer to tile an open window to that spot. */ static async tryOpeningTilingPopup() { - if (!Settings.getBoolean('enable-tiling-popup')) + if (!Settings.getBoolean('enable-tiling-popup')) { return; + } const allWs = Settings.getBoolean('tiling-popup-all-workspace'); const openWindows = this.getWindows(allWs); const topTileGroup = this.getTopTileGroup(); - topTileGroup.forEach(w => openWindows.splice(openWindows.indexOf(w), 1)); - if (!openWindows.length) + topTileGroup.forEach((w) => + openWindows.splice(openWindows.indexOf(w), 1), + ); + if (!openWindows.length) { return; + } - const tRects = topTileGroup.map(w => w.tiledRect); + const tRects = topTileGroup.map((w) => w.tiledRect); const monitor = topTileGroup[0]?.get_monitor(); // for the grace period const freeSpace = this.getFreeScreen(tRects, monitor); - if (!freeSpace) + if (!freeSpace) { return; + } const TilingPopup = await import('./tilingPopup.js'); - const popup = new TilingPopup.TilingSwitcherPopup(openWindows, freeSpace); - if (!popup.show(topTileGroup)) + const popup = new TilingPopup.TilingSwitcherPopup( + openWindows, + freeSpace, + ); + if (!popup.show(topTileGroup)) { popup.destroy(); + } } /** @@ -1017,15 +1305,20 @@ export class TilingWindowManager { * * @param {Meta.Window} window a Meta.Window. * @param {Rect} rect the Rect the `window` tiles to or untiles from. + * @param {object} params */ static toggleTiling(window, rect, params = {}) { const workArea = window.get_work_area_current_monitor(); const equalsWA = rect.equal(workArea); const equalsTile = window.tiledRect && rect.equal(window.tiledRect); - if (window.isTiled && equalsTile || this.isMaximized(window) && equalsWA) + if ( + (window.isTiled && equalsTile) || + (this.isMaximized(window) && equalsWA) + ) { this.untile(window, params); - else + } else { this.tile(window, rect, params); + } } /** @@ -1037,54 +1330,83 @@ export class TilingWindowManager { * appear, if there is free screen space after the `app` was tiled. */ static openAppTiled(app, rect, openTilingPopup = false) { - if (!app?.can_open_new_window()) + if (!app?.can_open_new_window()) { return; + } - let createId = global.display.connect('window-created', (src, window) => { - const wActor = window.get_compositor_private(); - let firstFrameId = wActor?.connect('first-frame', () => { - wActor.disconnect(firstFrameId); - firstFrameId = 0; - - const winTracker = Shell.WindowTracker.get_default(); - const openedWindowApp = winTracker.get_window_app(window); - // Check, if the created window is from the app and if it allows - // to be moved and resized because, for example, Steam uses a - // WindowType.Normal window for their loading screen, which we - // don't want to trigger the tiling for. - if (createId && openedWindowApp && openedWindowApp === app && - (window.allows_resize() && window.allows_move() || window.get_maximized()) - ) { - global.display.disconnect(createId); - createId = 0; - this.tile(window, rect, { openTilingPopup, skipAnim: true }); + let createId = global.display.connect( + 'window-created', + (src, window) => { + const wActor = window.get_compositor_private(); + let firstFrameId = wActor?.connect('first-frame', () => { + wActor.disconnect(firstFrameId); + firstFrameId = 0; + + const winTracker = Shell.WindowTracker.get_default(); + const openedWindowApp = winTracker.get_window_app(window); + // Check, if the created window is from the app and if it allows + // to be moved and resized because, for example, Steam uses a + // WindowType.Normal window for their loading screen, which we + // don't want to trigger the tiling for. + if ( + createId && + openedWindowApp && + openedWindowApp === app && + ((window.allows_resize() && window.allows_move()) || + window.get_maximized()) + ) { + global.display.disconnect(createId); + createId = 0; + this.tile(window, rect, { + openTilingPopup, + skipAnim: true, + }); + } + }); + + // Don't immediately disconnect the signal in case the launched + // window doesn't match the original app. It may be a loading screen + // or the user started an app in between etc... but in case the checks/ + // signals above fail disconnect the signals after 1 min at the latest + if (this._openAppTiledTimerId) { + GLib.Source.remove(this._openAppTiledTimerId); } - }); - // Don't immediately disconnect the signal in case the launched - // window doesn't match the original app. It may be a loading screen - // or the user started an app in between etc... but in case the checks/ - // signals above fail disconnect the signals after 1 min at the latest - this._openAppTiledTimerId && GLib.Source.remove(this._openAppTiledTimerId); - this._openAppTiledTimerId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 60000, () => { - createId && global.display.disconnect(createId); - createId = 0; - firstFrameId && wActor.disconnect(firstFrameId); - firstFrameId = 0; - this._openAppTiledTimerId = null; - return GLib.SOURCE_REMOVE; - }); - }); + this._openAppTiledTimerId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 60000, + () => { + if (createId) { + global.display.disconnect(createId); + createId = 0; + } + + if (firstFrameId) { + wActor.disconnect(firstFrameId); + firstFrameId = 0; + } + + this._openAppTiledTimerId = null; + return GLib.SOURCE_REMOVE; + }, + ); + }, + ); app.open_new_window(-1); } static saveTileState(window) { const windowState = this._tileStates.get(window.get_id()); - const rectToJsObject = rect => { - return rect - ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } - : undefined; + const rectToJsObject = (rect) => { + return rect ? + { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + } + : undefined; }; if (windowState) { @@ -1092,14 +1414,11 @@ export class TilingWindowManager { windowState.tiledRect = rectToJsObject(window.tiledRect); windowState.untiledRect = rectToJsObject(window.untiledRect); } else { - this._tileStates.set( - window.get_id(), - { - isTiled: window.isTiled, - tiledRect: rectToJsObject(window.tiledRect), - untiledRect: rectToJsObject(window.untiledRect) - } - ); + this._tileStates.set(window.get_id(), { + isTiled: window.isTiled, + tiledRect: rectToJsObject(window.tiledRect), + untiledRect: rectToJsObject(window.untiledRect), + }); } } @@ -1110,29 +1429,39 @@ export class TilingWindowManager { /** * Gets the top windows, which are supposed to be in a tile group. That * means windows, which are tiled, and don't overlap each other. + * + * @param {number} [monitor] */ static _getWindowsForBuildingTileGroup(monitor = null) { const openWindows = this.getWindows(); - if (!openWindows.length) + if (!openWindows.length) { return []; + } const ignoredWindows = []; const result = []; - const mon = monitor ?? + const mon = + monitor ?? global.display.focus_window?.get_monitor() ?? openWindows[0].get_monitor(); for (const window of openWindows) { - if (window.get_monitor() !== mon) + if (window.get_monitor() !== mon) { continue; + } - if (window.is_above() && !window.isTiled) + if (window.is_above() && !window.isTiled) { continue; + } if (window.isTiled) { // Window was already checked as part of another's tileGroup. - if (ignoredWindows.includes(window) || result.includes(window)) + if ( + ignoredWindows.includes(window) || + result.includes(window) + ) { continue; + } // Check for the other windows in the tile group as well regardless // of the 'raise tile group' setting so that once the setting is @@ -1147,18 +1476,27 @@ export class TilingWindowManager { continue; } - const tileGroupOverlaps = tileGroup.some(w => - result.some(r => r.tiledRect.overlap(w.tiledRect)) || - ignoredWindows.some(r => (r.tiledRect ?? new Rect(r.get_frame_rect())).overlap(w.tiledRect))); - - tileGroupOverlaps - ? tileGroup.forEach(w => ignoredWindows.push(w)) - : tileGroup.forEach(w => result.push(w)); + const tileGroupOverlaps = tileGroup.some( + (w) => + result.some((r) => r.tiledRect.overlap(w.tiledRect)) || + ignoredWindows.some((r) => + ( + r.tiledRect ?? new Rect(r.get_frame_rect()) + ).overlap(w.tiledRect), + ), + ); + + if (tileGroupOverlaps) { + tileGroup.forEach((w) => ignoredWindows.push(w)); + } else { + tileGroup.forEach((w) => result.push(w)); + } } else { // The window is maximized, so all windows below it can't belong // to this group anymore. - if (this.isMaximized(window)) + if (this.isMaximized(window)) { break; + } ignoredWindows.push(window); } @@ -1173,30 +1511,36 @@ export class TilingWindowManager { * * @param {{boolean, number}} param1 */ - static _getTopTiledWindows({ skipTopWindow = false, monitor = null } = {}) { + static _getTopTiledWindows({skipTopWindow = false, monitor = null} = {}) { const openWindows = this.getWindows(); - if (!openWindows.length) + if (!openWindows.length) { return []; + } if (skipTopWindow) { // the focused window isn't necessarily the top window due to always // on top windows. const idx = openWindows.indexOf(global.display.focus_window); - idx !== -1 && openWindows.splice(idx, 1); + if (idx !== -1) { + openWindows.splice(idx, 1); + } } const topTiledWindows = []; const ignoredWindows = []; - const mon = monitor ?? + const mon = + monitor ?? global.display.focus_window?.get_monitor() ?? openWindows[0].get_monitor(); for (const window of openWindows) { - if (window.get_monitor() !== mon) + if (window.get_monitor() !== mon) { continue; + } - if (window.is_above() && !window.isTiled) + if (window.is_above() && !window.isTiled) { continue; + } if (window.isTiled) { const wRect = window.tiledRect; @@ -1204,21 +1548,26 @@ export class TilingWindowManager { // If a ignored window in a higher stack order overlaps the // currently tested tiled window, the currently tested tiled // window isn't part of the top tile group. - const overlapsIgnoredWindow = ignoredWindows.some(w => { + const overlapsIgnoredWindow = ignoredWindows.some((w) => { const rect = w.tiledRect ?? new Rect(w.get_frame_rect()); return rect.overlap(wRect); }); // Same applies for already grouped windows - const overlapsTopTiledWindows = topTiledWindows.some(w => w.tiledRect.overlap(wRect)); + const overlapsTopTiledWindows = topTiledWindows.some((w) => + w.tiledRect.overlap(wRect), + ); - overlapsIgnoredWindow || overlapsTopTiledWindows - ? ignoredWindows.push(window) - : topTiledWindows.push(window); + if (overlapsIgnoredWindow || overlapsTopTiledWindows) { + ignoredWindows.push(window); + } else { + topTiledWindows.push(window); + } } else { // The window is maximized, so all windows below it can't belong // to this group anymore. - if (this.isMaximized(window)) + if (this.isMaximized(window)) { break; + } ignoredWindows.push(window); } @@ -1234,10 +1583,16 @@ export class TilingWindowManager { */ static _blockTilingSignalsFor(window) { const signals = this._signals.getSignalsFor(window.get_id()); - const blockedSignals = [TilingSignals.RAISE, TilingSignals.WS_CHANGED, TilingSignals.UNMANAGING]; - blockedSignals.forEach(s => { + const blockedSignals = [ + TilingSignals.RAISE, + TilingSignals.WS_CHANGED, + TilingSignals.UNMANAGING, + ]; + blockedSignals.forEach((s) => { const id = signals.get(s); - id && window.block_signal_handler(id); + if (id) { + window.block_signal_handler(id); + } }); } @@ -1249,10 +1604,16 @@ export class TilingWindowManager { */ static _unblockTilingSignalsFor(window) { const signals = this._signals.getSignalsFor(window.get_id()); - const blockedSignals = [TilingSignals.RAISE, TilingSignals.WS_CHANGED, TilingSignals.UNMANAGING]; - blockedSignals.forEach(s => { + const blockedSignals = [ + TilingSignals.RAISE, + TilingSignals.WS_CHANGED, + TilingSignals.UNMANAGING, + ]; + blockedSignals.forEach((s) => { const id = signals.get(s); - id && window.unblock_signal_handler(id); + if (id) { + window.unblock_signal_handler(id); + } }); } @@ -1267,7 +1628,9 @@ export class TilingWindowManager { // Refresh 'unmanaging' signal const unmanagingSignal = signals.get(TilingSignals.UNMANAGING); - unmanagingSignal && window.disconnect(unmanagingSignal); + if (unmanagingSignal) { + window.disconnect(unmanagingSignal); + } const umId = window.connect('unmanaging', () => { this.clearTilingProps(window.get_id()); @@ -1275,7 +1638,9 @@ export class TilingWindowManager { signals.set(TilingSignals.UNMANAGING, umId); // Refresh 'workspace-changed' signal - const wsId = window.connect('workspace-changed', () => this._onWindowWorkspaceChanged(window)); + const wsId = window.connect('workspace-changed', () => + this._onWindowWorkspaceChanged(window), + ); this._signals.getSignalsFor(wId).set(TilingSignals.WS_CHANGED, wsId); } @@ -1291,10 +1656,10 @@ export class TilingWindowManager { * Gets the window matching a window id * * @param {number} id - * @returns {Meta.Window} + * @returns {Meta.Window|undefined} */ static _getWindow(id) { - return this._getAllWindows().find(w => w.get_id() === id); + return this._getAllWindows().find((w) => w.get_id() === id); } /** @@ -1307,7 +1672,11 @@ export class TilingWindowManager { */ static _onWorkspaceAdded() { this._ignoreWsChange = true; - this._wsAddedTimer && GLib.Source.remove(this._wsAddedTimer); + + if (this._wsAddedTimer) { + GLib.Source.remove(this._wsAddedTimer); + } + this._wsAddedTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => { this._ignoreWsChange = false; this._wsAddedTimer = null; @@ -1325,12 +1694,20 @@ export class TilingWindowManager { */ static _onWorkspaceRemoved() { this._ignoreWsChange = true; - this._wsRemovedTimer && GLib.Source.remove(this._wsRemovedTimer); - this._wsRemovedTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => { - this._ignoreWsChange = false; - this._wsRemovedTimer = null; - return GLib.SOURCE_REMOVE; - }); + + if (this._wsRemovedTimer) { + GLib.Source.remove(this._wsRemovedTimer); + } + + this._wsRemovedTimer = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 50, + () => { + this._ignoreWsChange = false; + this._wsRemovedTimer = null; + return GLib.SOURCE_REMOVE; + }, + ); } /** @@ -1359,18 +1736,27 @@ export class TilingWindowManager { return; } - if (this._ignoreWsChange) + if (this._ignoreWsChange) { return; + } if (this.isMaximized(window)) { const wA = window.get_work_area_for_monitor(window.get_monitor()); const workArea = new Rect(wA); - if (workArea.equal(window.tiledRect)) + if (workArea.equal(window.tiledRect)) { return; + } - this.tile(window, workArea, { openTilingPopup: false, skipAnim: true }); + this.tile(window, workArea, { + openTilingPopup: false, + skipAnim: true, + }); } else if (window.isTiled) { - this.untile(window, { restoreFullPos: false, clampToWorkspace: true, skipAnim: Main.overview.visible }); + this.untile(window, { + restoreFullPos: false, + clampToWorkspace: true, + skipAnim: Main.overview.visible, + }); } } } @@ -1382,52 +1768,61 @@ export class TilingWindowManager { * Ws-changed: for untiling a tiled window after its ws changed. * Unmanaging: to remove unmanaging tiled windows from the other tileGroups. */ -const TilingSignals = GObject.registerClass({ - Signals: { - 'window-tiled': { param_types: [Meta.Window.$gtype] }, - 'window-untiled': { param_types: [Meta.Window.$gtype] } - } -}, class TilingSignals extends Clutter.Actor { - // Relevant 'signal types' (sorta used as an enum / key for the signal map). - // Tiled windows use all 3 signals; maximized-with-gaps windows only use the - // workspace-changed and unmanaging signal. - static RAISE = 'RAISE'; - static WS_CHANGED = 'WS_CHANGED'; - static UNMANAGING = 'UNMANAGING'; - - _init() { - super._init(); - - // { windowId1: { RAISE: signalId1, WS_CHANGED: signalId2, UNMANAGING: signalId3 }, ... } - this._ids = new Map(); - } +const TilingSignals = GObject.registerClass( + { + Signals: { + 'window-tiled': {param_types: [Meta.Window.$gtype]}, + 'window-untiled': {param_types: [Meta.Window.$gtype]}, + }, + }, + class TilingSignals extends Clutter.Actor { + // Relevant 'signal types' (sorta used as an enum / key for the signal map). + // Tiled windows use all 3 signals; maximized-with-gaps windows only use the + // workspace-changed and unmanaging signal. + static RAISE = 'RAISE'; + static WS_CHANGED = 'WS_CHANGED'; + static UNMANAGING = 'UNMANAGING'; + + _init() { + super._init(); + + // { windowId1: { RAISE: signalId1, WS_CHANGED: signalId2, UNMANAGING: signalId3 }, ... } + this._ids = new Map(); + } - destroy() { - // Disconnect remaining signals - const allWindows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null); - this._ids.forEach((signals, windowId) => { - const window = allWindows.find(w => w.get_id() === windowId); - window && signals.forEach(s => s && window.disconnect(s)); - }); + destroy() { + // Disconnect remaining signals + const allWindows = global.display.get_tab_list( + Meta.TabList.NORMAL_ALL, + null, + ); + this._ids.forEach((signals, windowId) => { + const window = allWindows.find((w) => w.get_id() === windowId); - super.destroy(); - } + if (window) { + signals.forEach((s) => s && window.disconnect(s)); + } + }); - /** - * Gets the signal ids for the raise, ws-changed and unmanaging signals - * for a specific window - * - * @param {number} windowId Meta.Window's id - * @returns {Map} the tiling signal ids for the window (id) - * with a 'signal type' as the keys - */ - getSignalsFor(windowId) { - let ret = this._ids.get(windowId); - if (!ret) { - ret = new Map(); - this._ids.set(windowId, ret); + super.destroy(); } - return ret; - } -}); + /** + * Gets the signal ids for the raise, ws-changed and unmanaging signals + * for a specific window + * + * @param {number} windowId Meta.Window's id + * @returns {Map} the tiling signal ids for the window (id) + * with a 'signal type' as the keys + */ + getSignalsFor(windowId) { + let ret = this._ids.get(windowId); + if (!ret) { + ret = new Map(); + this._ids.set(windowId, ret); + } + + return ret; + } + }, +); diff --git a/tiling-assistant@leleat-on-github/src/extension/utility.js b/tiling-assistant@leleat-on-github/src/extension/utility.js index af41d1c..4cf85d2 100644 --- a/tiling-assistant@leleat-on-github/src/extension/utility.js +++ b/tiling-assistant@leleat-on-github/src/extension/utility.js @@ -1,7 +1,7 @@ -import { Clutter, Gio, GLib, Mtk, St } from '../dependencies/gi.js'; -import { Main } from '../dependencies/shell.js'; +import {Clutter, Gio, GLib, Mtk, St} from '../dependencies/gi.js'; +import {Main} from '../dependencies/shell.js'; -import { Direction, Orientation, Settings } from '../common.js'; +import {Direction, Orientation, Settings} from '../common.js'; /** * Library of commonly used functions for the extension.js' files @@ -47,24 +47,40 @@ export class Util { static isDirection(keyVal, direction) { switch (direction) { case Direction.N: - return keyVal === Clutter.KEY_Up || - keyVal === Clutter.KEY_w || keyVal === Clutter.KEY_W || - keyVal === Clutter.KEY_k || keyVal === Clutter.KEY_K; + return ( + keyVal === Clutter.KEY_Up || + keyVal === Clutter.KEY_w || + keyVal === Clutter.KEY_W || + keyVal === Clutter.KEY_k || + keyVal === Clutter.KEY_K + ); case Direction.S: - return keyVal === Clutter.KEY_Down || - keyVal === Clutter.KEY_s || keyVal === Clutter.KEY_S || - keyVal === Clutter.KEY_j || keyVal === Clutter.KEY_J; + return ( + keyVal === Clutter.KEY_Down || + keyVal === Clutter.KEY_s || + keyVal === Clutter.KEY_S || + keyVal === Clutter.KEY_j || + keyVal === Clutter.KEY_J + ); case Direction.W: - return keyVal === Clutter.KEY_Left || - keyVal === Clutter.KEY_a || keyVal === Clutter.KEY_A || - keyVal === Clutter.KEY_h || keyVal === Clutter.KEY_H; + return ( + keyVal === Clutter.KEY_Left || + keyVal === Clutter.KEY_a || + keyVal === Clutter.KEY_A || + keyVal === Clutter.KEY_h || + keyVal === Clutter.KEY_H + ); case Direction.E: - return keyVal === Clutter.KEY_Right || - keyVal === Clutter.KEY_d || keyVal === Clutter.KEY_D || - keyVal === Clutter.KEY_l || keyVal === Clutter.KEY_L; + return ( + keyVal === Clutter.KEY_Right || + keyVal === Clutter.KEY_d || + keyVal === Clutter.KEY_D || + keyVal === Clutter.KEY_l || + keyVal === Clutter.KEY_L + ); } return false; @@ -75,22 +91,23 @@ export class Util { * @returns {Direction} */ static getDirection(keyVal) { - if (this.isDirection(keyVal, Direction.N)) + if (this.isDirection(keyVal, Direction.N)) { return Direction.N; - else if (this.isDirection(keyVal, Direction.S)) + } else if (this.isDirection(keyVal, Direction.S)) { return Direction.S; - else if (this.isDirection(keyVal, Direction.W)) + } else if (this.isDirection(keyVal, Direction.W)) { return Direction.W; - else if (this.isDirection(keyVal, Direction.E)) + } else if (this.isDirection(keyVal, Direction.E)) { return Direction.E; - else + } else { return null; + } } /** * Get the window or screen gaps scaled to the monitor scale. * - * @param {String} settingsKey the key for the gap + * @param {string} settingsKey the key for the gap * @param {number} monitor the number of the monitor to scale the gap to * @returns {number} the scaled gap as a even number since the window gap * will be divided by 2. @@ -107,7 +124,9 @@ export class Util { const screenLeftGap = this.getScaledGap('screen-left-gap', monitor); const screenRightGap = this.getScaledGap('screen-right-gap', monitor); const screenBottomGap = this.getScaledGap('screen-bottom-gap', monitor); - return screenTopGap || screenLeftGap || screenRightGap || screenBottomGap; + return ( + screenTopGap || screenLeftGap || screenRightGap || screenBottomGap + ); } /** @@ -126,12 +145,14 @@ export class Util { const pathArr = [userDir, '/tiling-assistant/layouts.json']; const path = GLib.build_filenamev(pathArr); const file = Gio.File.new_for_path(path); - if (!file.query_exists(null)) + if (!file.query_exists(null)) { return []; + } const [success, contents] = file.load_contents(null); - if (!success || !contents.length) + if (!success || !contents.length) { return []; + } return JSON.parse(new TextDecoder().decode(contents)); } @@ -150,8 +171,9 @@ export class Util { const layouts = this.getLayouts(); const layout = layouts?.[Settings.getStrv('favorite-layouts')[monitor]]; - if (!layout) + if (!layout) { return []; + } const activeWs = global.workspace_manager.get_active_workspace(); const workArea = new Rect(activeWs.get_work_area_for_monitor(monitor)); @@ -159,20 +181,21 @@ export class Util { // Scale the rect's ratios to the workArea. Try to align the rects to // each other and the workArea to workaround possible rounding errors // due to the scaling. - layout._items.forEach(({ rect: rectRatios }, idx) => { + layout._items.forEach(({rect: rectRatios}, idx) => { const rect = new Rect( workArea.x + Math.floor(rectRatios.x * workArea.width), workArea.y + Math.floor(rectRatios.y * workArea.height), Math.ceil(rectRatios.width * workArea.width), - Math.ceil(rectRatios.height * workArea.height) + Math.ceil(rectRatios.height * workArea.height), ); favoriteLayout.push(rect); - for (let i = 0; i < idx; i++) + for (let i = 0; i < idx; i++) { rect.tryAlignWith(favoriteLayout[i]); + } }); - favoriteLayout.forEach(rect => rect.tryAlignWith(workArea)); + favoriteLayout.forEach((rect) => rect.tryAlignWith(workArea)); return favoriteLayout; } @@ -182,7 +205,8 @@ export class Util { * @returns {St.Widget[]} an array of St.Widgets to indicate the tiled rects. */ static async ___debugShowTiledRects() { - const twm = (await import('./tilingWindowManager.js')).TilingWindowManager; + const twm = (await import('./tilingWindowManager.js')) + .TilingWindowManager; const topTileGroup = twm.getTopTileGroup(); if (!topTileGroup.length) { Main.notify('Tiling Assistant', 'No tiled windows / tiled rects.'); @@ -190,14 +214,14 @@ export class Util { } const indicators = []; - topTileGroup.forEach(w => { + topTileGroup.forEach((w) => { const indicator = new St.Widget({ style_class: 'tile-preview', opacity: 160, x: w.tiledRect.x, y: w.tiledRect.y, width: w.tiledRect.width, - height: w.tiledRect.height + height: w.tiledRect.height, }); Main.uiGroup.add_child(indicator); indicators.push(indicator); @@ -216,24 +240,26 @@ export class Util { const activeWs = global.workspace_manager.get_active_workspace(); const monitor = global.display.get_current_monitor(); const workArea = new Rect(activeWs.get_work_area_for_monitor(monitor)); - const twm = (await import('./tilingWindowManager.js')).TilingWindowManager; + const twm = (await import('./tilingWindowManager.js')) + .TilingWindowManager; const topTileGroup = twm.getTopTileGroup(); - const tRects = topTileGroup.map(w => w.tiledRect); + const tRects = topTileGroup.map((w) => w.tiledRect); const freeScreenSpace = twm.getFreeScreen(tRects); - const rects = freeScreenSpace ? [freeScreenSpace] : workArea.minus(tRects); + const rects = + freeScreenSpace ? [freeScreenSpace] : workArea.minus(tRects); if (!rects.length) { Main.notify('Tiling Assistant', 'No free screen rects to show.'); return null; } const indicators = []; - rects.forEach(rect => { + rects.forEach((rect) => { const indicator = new St.Widget({ style_class: 'tile-preview', x: rect.x, y: rect.y, width: rect.width, - height: rect.height + height: rect.height, }); Main.uiGroup.add_child(indicator); indicators.push(indicator); @@ -249,13 +275,14 @@ export class Util { log('--- Tiling Assistant: Start ---'); const twm = await import('./tilingWindowManager.js'); const openWindows = twm.getWindows(); - openWindows.forEach(w => { - if (!w.isTiled) + openWindows.forEach((w) => { + if (!w.isTiled) { return; + } log(`Tile group for: ${w.get_wm_class()}`); const tileGroup = twm.getTileGroupFor(w); - tileGroup.forEach(tw => log(tw.get_wm_class())); + tileGroup.forEach((tw) => log(tw.get_wm_class())); log('---'); }); log('--- Tiling Assistant: End ---'); @@ -292,7 +319,9 @@ export class Rect { break; default: - log('Tiling Assistant: Invalid param count for Rect constructor!'); + log( + 'Tiling Assistant: Invalid param count for Rect constructor!', + ); } } @@ -300,7 +329,7 @@ export class Rect { * Gets a new rectangle where the screen and window gaps were * added/subbed to/from `this`. * - * @param {Rect} rect a tiled Rect + * @param {Rect} workArea * @param {number} monitor the number of the monitor to scale the gap to * @returns {Rect} the rectangle after the gaps were taken into account */ @@ -315,9 +344,10 @@ export class Rect { // Prefer individual gaps if (Util.useIndividualGaps(monitor)) { - [['x', 'width', screenLeftGap, screenRightGap], - ['y', 'height', screenTopGap, screenBottomGap]] - .forEach(([pos, dim, posGap, dimGap]) => { + [ + ['x', 'width', screenLeftGap, screenRightGap], + ['y', 'height', screenTopGap, screenBottomGap], + ].forEach(([pos, dim, posGap, dimGap]) => { if (this[pos] === workArea[pos]) { r[pos] = this[pos] + posGap; r[dim] -= posGap; @@ -326,14 +356,18 @@ export class Rect { r[dim] -= windowGap / 2; } - if (this[pos] + this[dim] === workArea[pos] + workArea[dim]) + if (this[pos] + this[dim] === workArea[pos] + workArea[dim]) { r[dim] -= dimGap; - else + } else { r[dim] -= windowGap / 2; + } }); - // Use the single screen gap + // Use the single screen gap } else { - [['x', 'width'], ['y', 'height']].forEach(([pos, dim]) => { + [ + ['x', 'width'], + ['y', 'height'], + ].forEach(([pos, dim]) => { if (this[pos] === workArea[pos]) { r[pos] = this[pos] + singleScreenGap; r[dim] -= singleScreenGap; @@ -342,10 +376,11 @@ export class Rect { r[dim] -= windowGap / 2; } - if (this[pos] + this[dim] === workArea[pos] + workArea[dim]) + if (this[pos] + this[dim] === workArea[pos] + workArea[dim]) { r[dim] -= singleScreenGap; - else + } else { r[dim] -= windowGap / 2; + } }); } @@ -377,8 +412,12 @@ export class Rect { * @returns {boolean} */ containsPoint(point) { - return point.x >= this.x && point.x <= this.x2 && - point.y >= this.y && point.y <= this.y2; + return ( + point.x >= this.x && + point.x <= this.x2 && + point.y >= this.y && + point.y <= this.y2 + ); } /** @@ -440,24 +479,27 @@ export class Rect { // nearest on the non-compared axis ('nonCmprProp'). The x property // in the this example. let startProp, cmprProp, nonCmprProp; - if (dir === Direction.N) + if (dir === Direction.N) { [startProp, cmprProp, nonCmprProp] = ['y', 'y2', 'x']; - else if (dir === Direction.S) + } else if (dir === Direction.S) { [startProp, cmprProp, nonCmprProp] = ['y2', 'y', 'x']; - else if (dir === Direction.W) + } else if (dir === Direction.W) { [startProp, cmprProp, nonCmprProp] = ['x', 'x2', 'y']; - else if (dir === Direction.E) + } else if (dir === Direction.E) { [startProp, cmprProp, nonCmprProp] = ['x2', 'x', 'y']; + } // Put rects into a Map with their relevenat pos'es as the keys and // filter out `this`. const posMap = rects.reduce((map, rect) => { - if (rect.equal(this)) + if (rect.equal(this)) { return map; + } const pos = rect[cmprProp]; - if (!map.has(pos)) + if (!map.has(pos)) { map.set(pos, []); + } map.get(pos).push(rect); return map; @@ -466,24 +508,29 @@ export class Rect { // Sort the pos'es in an ascending / descending order. const goForward = [Direction.S, Direction.E].includes(dir); const sortedPoses = [...posMap.keys()].sort((a, b) => - goForward ? a - b : b - a); + goForward ? a - b : b - a, + ); - const neighborPos = goForward - ? sortedPoses.find(pos => pos >= this[startProp]) - : sortedPoses.find(pos => pos <= this[startProp]); + const neighborPos = + goForward ? + sortedPoses.find((pos) => pos >= this[startProp]) + : sortedPoses.find((pos) => pos <= this[startProp]); - if (!neighborPos && !wrap) + if (!neighborPos && !wrap) { return null; + } // Since the sortedPoses array is in descending order when 'going // backwards', we always wrap by getting the 0-th item, if there // is no actual neighbor. const neighbors = posMap.get(neighborPos ?? sortedPoses[0]); return neighbors.reduce((currNearest, rect) => { - return Math.abs(currNearest[nonCmprProp] - this[nonCmprProp]) <= - Math.abs(rect[nonCmprProp] - this[nonCmprProp]) - ? currNearest - : rect; + return ( + Math.abs(currNearest[nonCmprProp] - this[nonCmprProp]) <= + Math.abs(rect[nonCmprProp] - this[nonCmprProp]) + ) ? + currNearest + : rect; }); } @@ -506,7 +553,8 @@ export class Rect { unitSize = Math.floor(unitSize); const isVertical = orientation === Orientation.V; - const lastIndex = Math.round(this[isVertical ? 'width' : 'height'] / unitSize) - 1; + const lastIndex = + Math.round(this[isVertical ? 'width' : 'height'] / unitSize) - 1; const getLastRect = () => { const margin = unitSize * index; @@ -514,7 +562,7 @@ export class Rect { isVertical ? this.x + margin : this.x, isVertical ? this.y : this.y + margin, isVertical ? this.width - margin : this.width, - isVertical ? this.height : this.height - margin + isVertical ? this.height : this.height - margin, ); }; const getNonLastRect = (remainingRect, idx) => { @@ -522,7 +570,7 @@ export class Rect { remainingRect.x, remainingRect.y, isVertical ? unitSize : remainingRect.width, - isVertical ? remainingRect.height : unitSize + isVertical ? remainingRect.height : unitSize, ); if (idx <= 0) { @@ -533,10 +581,11 @@ export class Rect { } }; - if (index === lastIndex) + if (index === lastIndex) { return getLastRect(); - else + } else { return getNonLastRect(this, index); + } } /** @@ -586,24 +635,32 @@ export class Rect { */ _minusRect(rect) { rect = rect instanceof Mtk.Rectangle ? new Rect(rect) : rect; - if (rect.containsRect(this)) + if (rect.containsRect(this)) { return []; + } const [intersect] = this.intersect(rect); - if (!intersect) + if (!intersect) { return [this.copy()]; + } const resultRects = []; // Left rect const leftRectWidth = rect.x - this.x; - if (leftRectWidth > 0 && this.height > 0) - resultRects.push(new Rect(this.x, this.y, leftRectWidth, this.height)); + if (leftRectWidth > 0 && this.height > 0) { + resultRects.push( + new Rect(this.x, this.y, leftRectWidth, this.height), + ); + } // Right rect const rightRectWidth = this.x2 - rect.x2; - if (rightRectWidth > 0 && this.height > 0) - resultRects.push(new Rect(rect.x2, this.y, rightRectWidth, this.height)); + if (rightRectWidth > 0 && this.height > 0) { + resultRects.push( + new Rect(rect.x2, this.y, rightRectWidth, this.height), + ); + } const vertRectsX1 = rect.x > this.x ? rect.x : this.x; const vertRectsX2 = rect.x2 < this.x2 ? rect.x2 : this.x2; @@ -611,13 +668,24 @@ export class Rect { // Top rect const topRectHeight = rect.y - this.y; - if (topRectHeight > 0 && vertRectsWidth > 0) - resultRects.push(new Rect(vertRectsX1, this.y, vertRectsWidth, topRectHeight)); + if (topRectHeight > 0 && vertRectsWidth > 0) { + resultRects.push( + new Rect(vertRectsX1, this.y, vertRectsWidth, topRectHeight), + ); + } // Bottom rect const bottomRectHeight = this.y2 - rect.y2; - if (bottomRectHeight > 0 && vertRectsWidth > 0) - resultRects.push(new Rect(vertRectsX1, rect.y2, vertRectsWidth, bottomRectHeight)); + if (bottomRectHeight > 0 && vertRectsWidth > 0) { + resultRects.push( + new Rect( + vertRectsX1, + rect.y2, + vertRectsWidth, + bottomRectHeight, + ), + ); + } return resultRects; } @@ -630,12 +698,13 @@ export class Rect { * @returns {Rect[]} an array of the remaining Rects. */ _minusRectArray(rects) { - if (!rects.length) + if (!rects.length) { return [this.copy()]; + } // First cut off all rects individually from `this`. The result is an // array of leftover rects (which are arrays themselves) from `this`. - const individualLeftOvers = rects.map(r => this.minus(r)); + const individualLeftOvers = rects.map((r) => this.minus(r)); // Get the final result by intersecting all leftover rects. return individualLeftOvers.reduce((result, currLeftOvers) => { @@ -644,7 +713,10 @@ export class Rect { for (const leftOver of currLeftOvers) { for (const currFreeRect of result) { const [ok, inters] = currFreeRect.intersect(leftOver); - ok && intersections.push(new Rect(inters)); + + if (ok) { + intersections.push(new Rect(inters)); + } } } @@ -673,27 +745,32 @@ export class Rect { */ tryAlignWith(rect, margin = 4) { rect = rect instanceof Mtk.Rectangle ? new Rect(rect) : rect; - const equalApprox = (value1, value2) => Math.abs(value1 - value2) <= margin; + const equalApprox = (value1, value2) => + Math.abs(value1 - value2) <= margin; - if (equalApprox(rect.x, this.x)) + if (equalApprox(rect.x, this.x)) { this.x = rect.x; - else if (equalApprox(rect.x2, this.x)) + } else if (equalApprox(rect.x2, this.x)) { this.x = rect.x2; + } - if (equalApprox(rect.y, this.y)) + if (equalApprox(rect.y, this.y)) { this.y = rect.y; - else if (equalApprox(rect.y2, this.y)) + } else if (equalApprox(rect.y2, this.y)) { this.y = rect.y2; + } - if (equalApprox(rect.x, this.x2)) + if (equalApprox(rect.x, this.x2)) { this.width = rect.x - this.x; - else if (equalApprox(rect.x2, this.x2)) + } else if (equalApprox(rect.x2, this.x2)) { this.width = rect.x2 - this.x; + } - if (equalApprox(rect.y, this.y2)) + if (equalApprox(rect.y, this.y2)) { this.height = rect.y - this.y; - else if (equalApprox(rect.y2, this.y2)) + } else if (equalApprox(rect.y2, this.y2)) { this.height = rect.y2 - this.y; + } return this; } @@ -747,7 +824,7 @@ export class Rect { get center() { return { x: this.x + Math.floor(this.width / 2), - y: this.y + Math.floor(this.height / 2) + y: this.y + Math.floor(this.height / 2), }; } diff --git a/tiling-assistant@leleat-on-github/src/layouts_example.json b/tiling-assistant@leleat-on-github/src/layouts_example.json index 71e7c04..896f3e9 100644 --- a/tiling-assistant@leleat-on-github/src/layouts_example.json +++ b/tiling-assistant@leleat-on-github/src/layouts_example.json @@ -109,4 +109,4 @@ } ] } -] \ No newline at end of file +] diff --git a/tiling-assistant@leleat-on-github/src/prefs/layoutRow.js b/tiling-assistant@leleat-on-github/src/prefs/layoutRow.js index 437b97c..4f21eba 100644 --- a/tiling-assistant@leleat-on-github/src/prefs/layoutRow.js +++ b/tiling-assistant@leleat-on-github/src/prefs/layoutRow.js @@ -1,8 +1,8 @@ -import { Gdk, Gtk, GObject } from '../dependencies/prefs/gi.js'; -import { _ } from '../dependencies/prefs.js'; +import {Gdk, Gtk, GObject} from '../dependencies/prefs/gi.js'; +import {_} from '../dependencies/prefs.js'; -import { Layout } from '../common.js'; -import { LayoutRowEntry } from './layoutRowEntry.js'; +import {Layout} from '../common.js'; +import {LayoutRowEntry} from './layoutRowEntry.js'; /** * 1 LayoutRow represents 1 Layout in the preference window. It's just instanced @@ -12,210 +12,232 @@ import { LayoutRowEntry } from './layoutRowEntry.js'; * { rect, appId, loopType }. The rect is mandatory, the rest not. */ -export const LayoutRow = GObject.registerClass({ - GTypeName: 'TilingLayoutRow', - Template: import.meta.url.replace(/prefs\/(.*)\.js$/, 'ui/$1.ui'), - InternalChildren: [ - 'addRowEntryButton', - 'deleteButton', - 'drawingArea', - 'entryBox', - 'errorLabel', - 'expanderButton', - 'nameEntry', - 'rectCountLabel', - 'shortcut', - 'revealer' - ], - Signals: { 'changed': { param_types: [GObject.TYPE_BOOLEAN] } } -}, class TilingLayoutRow extends Gtk.ListBoxRow { - // Use a static variable to make sure the indices are unique since just using - // something like the child index isn't enough because the user may add *and* - // delete rows at random... so 1 child index may appear multiple times - static instanceCount = 0; - - /** - * @returns {number} the number of created LayoutRows since the last time - * the layouts were loaded into the preference window. - */ - static getInstanceCount() { - return TilingLayoutRow.instanceCount; - } - - static resetInstanceCount() { - TilingLayoutRow.instanceCount = 0; - } - - /** - * @param {{_name: string, _items: {rect: object, appId: ?string, loopType: ?string}[] - * }|null} layout a parsed JS object representing a layout from the - * layouts.json file. - */ - _init(layout, settings) { - super._init(); - - this._settings = settings; - this._layout = new Layout(layout); - this._idx = TilingLayoutRow.instanceCount++; - this._shortcutKey = `activate-layout${this._idx}`; - - // Initialize shortcut and its clear-button - this._shortcut.initialize(this._shortcutKey, this._settings); - - // Set name. Don't use a placeholder, if there is one because of a bug - // when reloading the layouts - const name = this._layout.getName(); - this._nameEntry.get_buffer().set_text(name, -1); - this._nameEntry.set_placeholder_text(name ? '' : 'Nameless Layout...'); - - // Load the entries with values from the layout - const items = this._layout.getItems(); - items.forEach((item, idx) => { - const rowEntry = new LayoutRowEntry(idx, item); - rowEntry.connect('changed', this._onRowEntryChanged.bind(this)); - this._entryBox.append(rowEntry); - }); - - // Show the nr of rects for a quicker overview. - this._rectCountLabel.set_label(items.length ? `(${items.length})` : ''); - - // Add one empty entry row - this._onAddRowEntryButtonClicked(); - - // Update the preview / show the errorLabel - this._updatePreview(); - } - - destroy() { - this.get_parent().remove(this); - } - - activate() { - this._nameEntry.grab_focus(); - } - - /** - * toggles whether the layout's rects are visible. - */ - toggleReveal() { - this._revealer.reveal_child = !this._revealer.reveal_child; - } - - /** - * @returns {number} the index of this layout. - */ - getIdx() { - return this._idx; - } - - /** - * @returns {{_name: string, _items: {rect: object, appId: ?string, loopType: ?string}[] - * }|null} the layout object represented by this row. - */ - getLayout() { - // First, filter out empty rows (i. e. rows without valid rects) - this._layout.setItems(this._layout.getItems()); - - // Then, remove problematic items, if the rects have problems. E. g., - // they may overlap each other, extend outside of the screen etc... - // This is irreversible but fine since this function is only called - // when the user presses the save button. Before that there will be - // error messages shown in the preview area. - let [ok, , idx] = this._layout.validate(); - while (this._layout.getItemCount() && !ok) { - this._layout.removeItem(idx); - [ok, , idx] = this._layout.validate(); +export const LayoutRow = GObject.registerClass( + { + GTypeName: 'TilingLayoutRow', + Template: import.meta.url.replace(/prefs\/(.*)\.js$/, 'ui/$1.ui'), + InternalChildren: [ + 'addRowEntryButton', + 'deleteButton', + 'drawingArea', + 'entryBox', + 'errorLabel', + 'expanderButton', + 'nameEntry', + 'rectCountLabel', + 'shortcut', + 'revealer', + ], + Signals: {changed: {param_types: [GObject.TYPE_BOOLEAN]}}, + }, + class TilingLayoutRow extends Gtk.ListBoxRow { + // Use a static variable to make sure the indices are unique since just using + // something like the child index isn't enough because the user may add *and* + // delete rows at random... so 1 child index may appear multiple times + static instanceCount = 0; + + /** + * @returns {number} the number of created LayoutRows since the last time + * the layouts were loaded into the preference window. + */ + static getInstanceCount() { + return TilingLayoutRow.instanceCount; } - return this._layout.getItemCount() ? this._layout : null; - } - - /** - * @returns {[boolean, string]} whether the preview was successful and a - * potential error message. - */ - _updatePreview() { - const [ok, errMsg] = this._layout.validate(); - if (!ok) { - // Print error in the preview area - this._errorLabel.set_label(errMsg); - this._drawingArea.set_draw_func(() => {}); - } else { - // Draw the actual preview for the rects - this._errorLabel.set_label(''); - this._drawingArea.set_draw_func((drawingArea, cr) => { - const color = new Gdk.RGBA(); - const width = drawingArea.get_allocated_width(); - const height = drawingArea.get_allocated_height(); - - cr.setLineWidth(1.0); - - this._layout.getItems().forEach(item => { - // Rects are in a slightly transparent white with a 1px outline - // and a 5px gap between the different rects - const rect = item.rect; - color.parse('rgba(255, 255, 255, .2)'); - Gdk.cairo_set_source_rgba(cr, color); - cr.moveTo(rect.x * width + 5, rect.y * height + 5); - cr.lineTo((rect.x + rect.width) * width - 5, rect.y * height + 5); - cr.lineTo((rect.x + rect.width) * width - 5, (rect.y + rect.height) * height - 5); - cr.lineTo(rect.x * width + 5, (rect.y + rect.height) * height - 5); - cr.lineTo(rect.x * width + 5, rect.y * height + 5); - cr.strokePreserve(); - - // Fill the rects in transparent black. - // If the rect is a 'loop', lower the transparency. - color.parse(`rgba(0, 0, 0, ${item.loopType ? .1 : .3})`); - Gdk.cairo_set_source_rgba(cr, color); - cr.fill(); - }); + static resetInstanceCount() { + TilingLayoutRow.instanceCount = 0; + } - cr.$dispose(); + /** + * @param {{_name: string, _items: {rect: object, appId: ?string, loopType: ?string}[] + * }|null} layout a parsed JS object representing a layout from the + * layouts.json file. + * @param {Gio.Settings} settings + */ + _init(layout, settings) { + super._init(); + + this._settings = settings; + this._layout = new Layout(layout); + this._idx = TilingLayoutRow.instanceCount++; + this._shortcutKey = `activate-layout${this._idx}`; + + // Initialize shortcut and its clear-button + this._shortcut.initialize(this._shortcutKey, this._settings); + + // Set name. Don't use a placeholder, if there is one because of a bug + // when reloading the layouts + const name = this._layout.getName(); + this._nameEntry.get_buffer().set_text(name, -1); + this._nameEntry.set_placeholder_text( + name ? '' : 'Nameless Layout...', + ); + + // Load the entries with values from the layout + const items = this._layout.getItems(); + items.forEach((item, idx) => { + const rowEntry = new LayoutRowEntry(idx, item); + rowEntry.connect('changed', this._onRowEntryChanged.bind(this)); + this._entryBox.append(rowEntry); }); + + // Show the nr of rects for a quicker overview. + this._rectCountLabel.set_label( + items.length ? `(${items.length})` : '', + ); + + // Add one empty entry row + this._onAddRowEntryButtonClicked(); + + // Update the preview / show the errorLabel + this._updatePreview(); + } + + destroy() { + this.get_parent().remove(this); + } + + activate() { + this._nameEntry.grab_focus(); + } + + /** + * toggles whether the layout's rects are visible. + */ + toggleReveal() { + this._revealer.reveal_child = !this._revealer.reveal_child; + } + + /** + * @returns {number} the index of this layout. + */ + getIdx() { + return this._idx; + } + + /** + * @returns {{_name: string, _items: {rect: object, appId: ?string, loopType: ?string}[] + * }|null} the layout object represented by this row. + */ + getLayout() { + // First, filter out empty rows (i. e. rows without valid rects) + this._layout.setItems(this._layout.getItems()); + + // Then, remove problematic items, if the rects have problems. E. g., + // they may overlap each other, extend outside of the screen etc... + // This is irreversible but fine since this function is only called + // when the user presses the save button. Before that there will be + // error messages shown in the preview area. + let [ok, , idx] = this._layout.validate(); + while (this._layout.getItemCount() && !ok) { + this._layout.removeItem(idx); + [ok, , idx] = this._layout.validate(); + } + + return this._layout.getItemCount() ? this._layout : null; + } + + /** + * @returns {[boolean, string]} whether the preview was successful and a + * potential error message. + */ + _updatePreview() { + const [ok, errMsg] = this._layout.validate(); + if (!ok) { + // Print error in the preview area + this._errorLabel.set_label(errMsg); + this._drawingArea.set_draw_func(() => {}); + } else { + // Draw the actual preview for the rects + this._errorLabel.set_label(''); + this._drawingArea.set_draw_func((drawingArea, cr) => { + const color = new Gdk.RGBA(); + const width = drawingArea.get_allocated_width(); + const height = drawingArea.get_allocated_height(); + + cr.setLineWidth(1.0); + + this._layout.getItems().forEach((item) => { + // Rects are in a slightly transparent white with a 1px outline + // and a 5px gap between the different rects + const rect = item.rect; + color.parse('rgba(255, 255, 255, .2)'); + Gdk.cairo_set_source_rgba(cr, color); + cr.moveTo(rect.x * width + 5, rect.y * height + 5); + cr.lineTo( + (rect.x + rect.width) * width - 5, + rect.y * height + 5, + ); + cr.lineTo( + (rect.x + rect.width) * width - 5, + (rect.y + rect.height) * height - 5, + ); + cr.lineTo( + rect.x * width + 5, + (rect.y + rect.height) * height - 5, + ); + cr.lineTo(rect.x * width + 5, rect.y * height + 5); + cr.strokePreserve(); + + // Fill the rects in transparent black. + // If the rect is a 'loop', lower the transparency. + color.parse( + `rgba(0, 0, 0, ${item.loopType ? 0.1 : 0.3})`, + ); + Gdk.cairo_set_source_rgba(cr, color); + cr.fill(); + }); + + cr.$dispose(); + }); + } + + this._drawingArea.queue_draw(); + return [ok, errMsg]; } - this._drawingArea.queue_draw(); - return [ok, errMsg]; - } - - _onNameEntryChanged() { - const name = this._nameEntry.get_buffer().get_text(); - this._nameEntry.set_tooltip_text(name); - this._layout.setName(name); - const [ok] = this._layout.validate(); - this.emit('changed', ok); - } - - _onDeleteButtonClicked() { - this._settings.set_strv(this._shortcutKey, []); - this.emit('changed', true); - this.destroy(); - } - - _onExpanderButtonClicked() { - this.toggleReveal(); - } - - _onClearShortcutButtonClicked() { - this._settings.set_strv(`activate-layout${this._idx}`, []); - } - - _onAddRowEntryButtonClicked() { - const rowEntry = new LayoutRowEntry(this._layout.getItemCount(), this._layout.addItem()); - rowEntry.connect('changed', this._onRowEntryChanged.bind(this)); - this._entryBox.append(rowEntry); - } - - _onRowEntryChanged(entry, ok) { - // ok only is about the change being ok for the *individual* entry - // i. e. whether their format is correct - if (!ok) { + _onNameEntryChanged() { + const name = this._nameEntry.get_buffer().get_text(); + this._nameEntry.set_tooltip_text(name); + this._layout.setName(name); + const [ok] = this._layout.validate(); this.emit('changed', ok); - return; } - // allOk is about whether the guiEntries are also valid as a whole - const [allOk] = this._updatePreview(); - this.emit('changed', allOk); - } -}); + _onDeleteButtonClicked() { + this._settings.set_strv(this._shortcutKey, []); + this.emit('changed', true); + this.destroy(); + } + + _onExpanderButtonClicked() { + this.toggleReveal(); + } + + _onClearShortcutButtonClicked() { + this._settings.set_strv(`activate-layout${this._idx}`, []); + } + + _onAddRowEntryButtonClicked() { + const rowEntry = new LayoutRowEntry( + this._layout.getItemCount(), + this._layout.addItem(), + ); + rowEntry.connect('changed', this._onRowEntryChanged.bind(this)); + this._entryBox.append(rowEntry); + } + + _onRowEntryChanged(entry, ok) { + // ok only is about the change being ok for the *individual* entry + // i. e. whether their format is correct + if (!ok) { + this.emit('changed', ok); + return; + } + + // allOk is about whether the guiEntries are also valid as a whole + const [allOk] = this._updatePreview(); + this.emit('changed', allOk); + } + }, +); diff --git a/tiling-assistant@leleat-on-github/src/prefs/layoutRowEntry.js b/tiling-assistant@leleat-on-github/src/prefs/layoutRowEntry.js index 90adde7..bf339ba 100644 --- a/tiling-assistant@leleat-on-github/src/prefs/layoutRowEntry.js +++ b/tiling-assistant@leleat-on-github/src/prefs/layoutRowEntry.js @@ -1,125 +1,132 @@ -import { Gio, Gtk, GObject } from '../dependencies/prefs/gi.js'; -import { _ } from '../dependencies/prefs.js'; +import {Gio, Gtk, GObject} from '../dependencies/prefs/gi.js'; +import {_} from '../dependencies/prefs.js'; /** * Multiple LayoutRowEntries make up a LayoutRow.js. See that file for more info. */ -export const LayoutRowEntry = GObject.registerClass({ - GTypeName: 'TilingLayoutRowEntry', - Template: import.meta.url.replace(/prefs\/(.*)\.js$/, 'ui/$1.ui'), - InternalChildren: [ - 'rectEntry', - 'rectLabel', - 'rectAppButton' - ], - Signals: { 'changed': { param_types: [GObject.TYPE_BOOLEAN] } } -}, class TilingLayoutRowEntry extends Gtk.Box { - _init(idx, item) { - super._init({ - orientation: Gtk.Orientation.HORIZONTAL, - spacing: 8 - }); +export const LayoutRowEntry = GObject.registerClass( + { + GTypeName: 'TilingLayoutRowEntry', + Template: import.meta.url.replace(/prefs\/(.*)\.js$/, 'ui/$1.ui'), + InternalChildren: ['rectEntry', 'rectLabel', 'rectAppButton'], + Signals: {changed: {param_types: [GObject.TYPE_BOOLEAN]}}, + }, + class TilingLayoutRowEntry extends Gtk.Box { + _init(idx, item) { + super._init({ + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 8, + }); - this._item = item; + this._item = item; - this._rectLabel.set_label(`Rect ${idx}`); - const loop = item.loopType ? `--${item.loopType}` : ''; - const rect = item.rect; - const text = Object.keys(rect).length !== 0 - ? `${rect.x}--${rect.y}--${rect.width}--${rect.height}${loop}` - : ''; - this._rectEntry.get_buffer().set_text(text, -1); + this._rectLabel.set_label( + `Rect ${idx}`, + ); + const loop = item.loopType ? `--${item.loopType}` : ''; + const rect = item.rect; + const text = + Object.keys(rect).length !== 0 ? + `${rect.x}--${rect.y}--${rect.width}--${rect.height}${loop}` + : ''; + this._rectEntry.get_buffer().set_text(text, -1); - // Show a placeholder on the first entry, if it's empty - if (!text) { - if (idx === 0) { - // Translators: This is a placeholder text of an entry in the prefs when defining a tiling layout. - const placeholder = _("'User Guide' for help..."); - this._rectEntry.set_placeholder_text(placeholder); - } else { - this._rectEntry.set_placeholder_text('x--y--width--height[--h|v]'); + // Show a placeholder on the first entry, if it's empty + if (!text) { + if (idx === 0) { + // Translators: This is a placeholder text of an entry in the prefs when defining a tiling layout. + const placeholder = _("'User Guide' for help..."); + this._rectEntry.set_placeholder_text(placeholder); + } else { + this._rectEntry.set_placeholder_text( + 'x--y--width--height[--h|v]', + ); + } } - } - const appInfo = item.appId && Gio.DesktopAppInfo.new(item.appId); - const iconName = appInfo?.get_icon().to_string() ?? 'list-add-symbolic'; - this._rectAppButton.set_icon_name(iconName); - } + const appInfo = item.appId && Gio.DesktopAppInfo.new(item.appId); + const iconName = + appInfo?.get_icon().to_string() ?? 'list-add-symbolic'; + this._rectAppButton.set_icon_name(iconName); + } - /** - * @param {Gtk.Button} appButton src of the event. - */ - _onAppButtonClicked() { - // Reset app button, if it already has an app attached - if (this._item.appId) { - this._rectAppButton.set_icon_name('list-add-symbolic'); - this._item.appId = null; - this.emit('changed', true); + _onAppButtonClicked() { + // Reset app button, if it already has an app attached + if (this._item.appId) { + this._rectAppButton.set_icon_name('list-add-symbolic'); + this._item.appId = null; + this.emit('changed', true); - // Attach app to the button - } else { - const chooserDialog = new Gtk.AppChooserDialog({ modal: true }); - chooserDialog.get_widget().set({ show_all: true, show_other: true }); - chooserDialog.connect('response', (dlg, id) => { - if (id === Gtk.ResponseType.OK) { - const appInfo = chooserDialog.get_widget().get_app_info(); - const iconName = appInfo.get_icon().to_string(); - this._rectAppButton.set_icon_name(iconName); - this._item.appId = appInfo.get_id(); - this.emit('changed', true); - } + // Attach app to the button + } else { + const chooserDialog = new Gtk.AppChooserDialog({modal: true}); + chooserDialog + .get_widget() + .set({show_all: true, show_other: true}); + chooserDialog.connect('response', (dlg, id) => { + if (id === Gtk.ResponseType.OK) { + const appInfo = chooserDialog + .get_widget() + .get_app_info(); + const iconName = appInfo.get_icon().to_string(); + this._rectAppButton.set_icon_name(iconName); + this._item.appId = appInfo.get_id(); + this.emit('changed', true); + } - chooserDialog.destroy(); - }); + chooserDialog.destroy(); + }); - chooserDialog.show(); + chooserDialog.show(); + } } - } - /** - * @param {Gtk.Entry} entry src of the event. - */ - _onRectEntryChanged(entry) { - const text = entry.get_buffer().get_text(); - const [ok] = this._validateFormat(text); - if (ok) { - const values = text.split('--'); - this._item.rect = { - x: parseFloat(values[0].trim()), - y: parseFloat(values[1].trim()), - width: parseFloat(values[2].trim()), - height: parseFloat(values[3].trim()) - }; - this._item.loopType = values[4] || null; - } else { - this._item.rect = {}; - this._item.loopType = null; - } + /** + * @param {Gtk.Entry} entry src of the event. + */ + _onRectEntryChanged(entry) { + const text = entry.get_buffer().get_text(); + const [ok] = this._validateFormat(text); + if (ok) { + const values = text.split('--'); + this._item.rect = { + x: parseFloat(values[0].trim()), + y: parseFloat(values[1].trim()), + width: parseFloat(values[2].trim()), + height: parseFloat(values[3].trim()), + }; + this._item.loopType = values[4] || null; + } else { + this._item.rect = {}; + this._item.loopType = null; + } - this.emit('changed', ok); - } + this.emit('changed', ok); + } - /** - * Validates whether `text` follows the format \ - * 'Float--Float--Float--Float[--String]' - * - * @param {string} text - * @returns {[boolean, string]} whether the `text` is valid and a - * potential error message. - */ - _validateFormat(text) { - const values = text.split('--'); - // 4 -> x, y, width, height; 5 -> additionally, a loopType - if (values.length < 4 || values.length > 5) - return [false, 'Wrong format: invalid count.']; + /** + * Validates whether `text` follows the format \ + * 'Float--Float--Float--Float[--String]' + * + * @param {string} text + * @returns {[boolean, string]} whether the `text` is valid and a + * potential error message. + */ + _validateFormat(text) { + const values = text.split('--'); + // 4 -> x, y, width, height; 5 -> additionally, a loopType + if (values.length < 4 || values.length > 5) { + return [false, 'Wrong format: invalid count.']; + } - const notJustNrs = ['x', 'y', 'width', 'height'].some((p, idx) => { - return Number.isNaN(parseFloat(values[idx].trim())); - }); + const notJustNrs = ['x', 'y', 'width', 'height'].some((p, idx) => { + return Number.isNaN(parseFloat(values[idx].trim())); + }); - return notJustNrs - ? [false, 'Wrong format: only numbers are allowed.'] - : [true, '']; - } -}); + return notJustNrs ? + [false, 'Wrong format: only numbers are allowed.'] + : [true, '']; + } + }, +); diff --git a/tiling-assistant@leleat-on-github/src/prefs/layoutsPrefs.js b/tiling-assistant@leleat-on-github/src/prefs/layoutsPrefs.js index 83e52d5..6ed0d30 100644 --- a/tiling-assistant@leleat-on-github/src/prefs/layoutsPrefs.js +++ b/tiling-assistant@leleat-on-github/src/prefs/layoutsPrefs.js @@ -1,6 +1,6 @@ -import { Gio, GLib } from '../dependencies/prefs/gi.js'; +import {Gio, GLib} from '../dependencies/prefs/gi.js'; -import { LayoutRow } from './layoutRow.js'; +import {LayoutRow} from './layoutRow.js'; /** * This class takes care of everything related to layouts (at least on the @@ -62,7 +62,7 @@ export default class { }); // Bind the general layouts keyboard shortcuts. - ['search-popup-layout'].forEach(key => { + ['search-popup-layout'].forEach((key) => { const shortcut = builder.get_object(key.replaceAll('-', '_')); shortcut.initialize(key, this._settings); }); @@ -74,14 +74,15 @@ export default class { _loadLayouts(path) { this._applySaveButtonStyle(''); - this._forEachLayoutRow(row => row.destroy()); + this._forEachLayoutRow((row) => row.destroy()); LayoutRow.resetInstanceCount(); // Try to load layouts file. const saveFile = this._makeFile(); const [success, contents] = saveFile.load_contents(null); - if (!success) + if (!success) { return; + } let layouts = []; @@ -90,25 +91,36 @@ export default class { layouts = JSON.parse(new TextDecoder().decode(contents)); // Ensure at least 1 empty row otherwise the listbox won't have // a height but a weird looking shadow only. - layouts.length - ? layouts.forEach((layout, idx) => this._createLayoutRow(idx, layout)) - : this._createLayoutRow(0); + if (layouts.length) { + layouts.forEach((layout, idx) => + this._createLayoutRow(idx, layout), + ); + } else { + this._createLayoutRow(0); + } - // Otherwise import the examples... but only do it once! - // Use a setting as a flag. + // Otherwise import the examples... but only do it once! + // Use a setting as a flag. } else { const importExamples = 'import-layout-examples'; - if (!this._settings.get_boolean(importExamples)) + if (!this._settings.get_boolean(importExamples)) { return; + } this._settings.set_boolean(importExamples, false); - const exampleFile = this._makeFile(`${path}/src`, 'layouts_example.json'); + const exampleFile = this._makeFile( + `${path}/src`, + 'layouts_example.json', + ); const [succ, c] = exampleFile.load_contents(null); - if (!succ) + if (!succ) { return; + } layouts = c.length ? JSON.parse(new TextDecoder().decode(c)) : []; - layouts.forEach((layout, idx) => this._createLayoutRow(idx, layout)); + layouts.forEach((layout, idx) => + this._createLayoutRow(idx, layout), + ); this._saveLayouts(); } } @@ -117,7 +129,7 @@ export default class { this._applySaveButtonStyle(''); const layouts = []; - this._forEachLayoutRow(layoutRow => { + this._forEachLayoutRow((layoutRow) => { const lay = layoutRow.getLayout(); if (lay) { layouts.push(lay); @@ -125,22 +137,34 @@ export default class { // Check, if all layoutRows were valid so far. Use getIdx() // instead of forEach's idx because a layoutRow may have been // deleted by the user. - if (layoutRow.getIdx() === layouts.length - 1) + if (layoutRow.getIdx() === layouts.length - 1) { return; + } // Invalid or empty layouts are ignored. For example, the user // defined a valid layout with a keybinding on row idx 3 but left // the row at idx 2 empty. When saving, the layout at idx 2 gets // removed and layout at idx 3 takes its place (i. e. becomes // idx 2). We need to update the keybindings to reflect that. - const keys = this._settings.get_strv(`activate-layout${layoutRow.getIdx()}`); - this._settings.set_strv(`activate-layout${layouts.length - 1}`, keys); - this._settings.set_strv(`activate-layout${layoutRow.getIdx()}`, []); + const keys = this._settings.get_strv( + `activate-layout${layoutRow.getIdx()}`, + ); + this._settings.set_strv( + `activate-layout${layouts.length - 1}`, + keys, + ); + this._settings.set_strv( + `activate-layout${layoutRow.getIdx()}`, + [], + ); } else { // Remove keyboard shortcuts, if they aren't assigned to a // valid layout, because they won't be visible to the user // since invalid layouts get removed - this._settings.set_strv(`activate-layout${layoutRow.getIdx()}`, []); + this._settings.set_strv( + `activate-layout${layoutRow.getIdx()}`, + [], + ); } }); @@ -150,7 +174,7 @@ export default class { null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, - null + null, ); } @@ -162,8 +186,9 @@ export default class { _makeFile(parentPath = '', fileName = '') { // Create directory structure, if it doesn't exist. const userConfigDir = GLib.get_user_config_dir(); - const dirLocation = parentPath || - GLib.build_filenamev([userConfigDir, '/tiling-assistant']); + const dirLocation = + parentPath || + GLib.build_filenamev([userConfigDir, '/tiling-assistant']); const parentDir = Gio.File.new_for_path(dirLocation); try { @@ -200,32 +225,41 @@ export default class { // (e. g. when changes were invalid) const actions = ['suggested-action', 'destructive-action']; const context = this._saveLayoutsButton.get_style_context(); - actions.forEach(a => a === actionName - ? context.add_class(a) - : context.remove_class(a)); + actions.forEach((a) => + a === actionName ? context.add_class(a) : context.remove_class(a), + ); } /** * @param {number} index the index of the new layouts row. * @param {Layout} layout the parsed JS Object from the layouts file. + * + * @returns {LayoutRow|undefined} the new LayoutRow. */ _createLayoutRow(index, layout = null) { // Layouts are limited to 20 since there are only // that many keybindings in the schemas.xml file - if (index >= 20) - return; + if (index >= 20) { + return null; + } const layoutRow = new LayoutRow(layout, this._settings); layoutRow.connect('changed', (row, ok) => { // Un / Highlight the save button, if the user made in / valid changes. - this._applySaveButtonStyle(ok ? 'suggested-action' : 'destructive-action'); + this._applySaveButtonStyle( + ok ? 'suggested-action' : 'destructive-action', + ); }); this._layoutsListBox.append(layoutRow); return layoutRow; } _forEachLayoutRow(callback) { - for (let i = 0, child = this._layoutsListBox.get_first_child(); !!child; i++) { + for ( + let i = 0, child = this._layoutsListBox.get_first_child(); + child; + i++ + ) { // Get a ref to the next widget in case the curr widget // gets destroyed during the function call. const nxtSibling = child.get_next_sibling(); diff --git a/tiling-assistant@leleat-on-github/src/prefs/shortcutListener.js b/tiling-assistant@leleat-on-github/src/prefs/shortcutListener.js index 4b093c0..6027fe4 100644 --- a/tiling-assistant@leleat-on-github/src/prefs/shortcutListener.js +++ b/tiling-assistant@leleat-on-github/src/prefs/shortcutListener.js @@ -1,5 +1,5 @@ -import { Adw, Gdk, GObject, Gtk } from '../dependencies/prefs/gi.js'; -import { _ } from '../dependencies/prefs.js'; +import {Adw, Gdk, GObject, Gtk} from '../dependencies/prefs/gi.js'; +import {_} from '../dependencies/prefs.js'; /** * A Widget to implement the shortcuts in the preference window. @@ -11,199 +11,239 @@ import { _ } from '../dependencies/prefs.js'; * https://gitlab.com/rmnvgr/nightthemeswitcher-gnome-shell-extension/-/blob/main/src/utils.js */ -export const ShortcutListener = GObject.registerClass({ - GTypeName: 'ShortcutListener', - Template: import.meta.url.replace(/prefs\/(.*)\.js$/, 'ui/$1.ui'), - InternalChildren: ['keybindingLabel', 'clearButton', 'eventKeyController'], - Properties: { - keybinding: GObject.ParamSpec.string( - 'keybinding', - 'Keybinding', - 'Key sequence', - GObject.ParamFlags.READWRITE, - null - ) - } -}, class ShortcutListener extends Adw.ActionRow { - /** - * Only allow 1 active ShortcutListener at a time - */ - static isListening = false; - static isAppendingShortcut = false; - static listener = null; - static listeningText = 'Press a shortcut...'; - static appendingText = 'Append a new shortcut...'; - - /** - * Starts listening for a keyboard shortcut. - * - * @param {ShortcutListener} shortcutListener the new active ShortcutListener - */ - static listen(shortcutListener) { - if (shortcutListener === ShortcutListener.listener) - return; - - ShortcutListener.stopListening(); - - shortcutListener.isActive = true; - shortcutListener.setKeybindingLabel(ShortcutListener.listeningText); - ShortcutListener.listener = shortcutListener; - ShortcutListener.isListening = true; - } - - /** - * Stops listening for a keyboard shortcut. - */ - static stopListening() { - if (!ShortcutListener.isListening) - return; - - ShortcutListener.isListening = false; - ShortcutListener.isAppendingShortcut = false; - ShortcutListener.listener.isActive = false; - ShortcutListener.listener.setKeybindingLabel(ShortcutListener.listener.getKeybindingLabel()); - ShortcutListener.listener = null; - } - - initialize(key, setting) { - this._key = key; - this._setting = setting; - this.isActive = false; - - this.connect('realize', () => this.get_root().add_controller(this._eventKeyController)); - - this.keybinding = this._setting.get_strv(key) ?? []; - } - - /* - * Sets the label of the keybinding. - */ - setKeybindingLabel(label) { - this._keybindingLabel.set_label(label); - } - - /** - * Gets the keybinding in a more pleasant to read format. - * For example: [e,a] will become - * 'Ctrl+Super+E / Super+A' or 'Disabled' - * - * @returns {string} - */ - getKeybindingLabel() { - const kbLabel = this.keybinding.reduce((label, kb) => { - const [, keyval, mask] = Gtk.accelerator_parse(kb); - const l = Gtk.accelerator_get_label(keyval, mask); - if (!label) - return l; - - return l ? `${label} / ${l}` : label; - }, ''); - - return kbLabel || _('Disabled'); - } - - _onActivated() { - this.isActive ? ShortcutListener.stopListening() : ShortcutListener.listen(this); - } - - _onKeybindingChanged() { - this._setting.set_strv(this._key, this.keybinding); - this._clearButton.set_sensitive(this.keybinding.length); - this.setKeybindingLabel(this.getKeybindingLabel()); - } - - _onClearButtonClicked() { - this.keybinding = []; - ShortcutListener.stopListening(); - } - - _onKeyPressed(eventControllerKey, keyval, keycode, state) { - if (this !== ShortcutListener.listener) - return Gdk.EVENT_PROPAGATE; - - let mask = state & Gtk.accelerator_get_default_mod_mask(); - mask &= ~Gdk.ModifierType.LOCK_MASK; - - if (mask === 0) { - switch (keyval) { - case Gdk.KEY_BackSpace: - this.keybinding = []; - // falls through - case Gdk.KEY_Escape: - ShortcutListener.stopListening(); - return Gdk.EVENT_STOP; - case Gdk.KEY_KP_Enter: - case Gdk.KEY_Return: - case Gdk.KEY_space: - ShortcutListener.isAppendingShortcut = !ShortcutListener.isAppendingShortcut; - this.setKeybindingLabel(ShortcutListener.isAppendingShortcut - ? ShortcutListener.appendingText - : ShortcutListener.listeningText - ); - return Gdk.EVENT_STOP; +export const ShortcutListener = GObject.registerClass( + { + GTypeName: 'ShortcutListener', + Template: import.meta.url.replace(/prefs\/(.*)\.js$/, 'ui/$1.ui'), + InternalChildren: [ + 'keybindingLabel', + 'clearButton', + 'eventKeyController', + ], + Properties: { + keybinding: GObject.ParamSpec.string( + 'keybinding', + 'Keybinding', + 'Key sequence', + GObject.ParamFlags.READWRITE, + null, + ), + }, + }, + class ShortcutListener extends Adw.ActionRow { + /** + * Only allow 1 active ShortcutListener at a time + */ + static isListening = false; + static isAppendingShortcut = false; + static listener = null; + static listeningText = 'Press a shortcut...'; + static appendingText = 'Append a new shortcut...'; + + /** + * Starts listening for a keyboard shortcut. + * + * @param {ShortcutListener} shortcutListener the new active ShortcutListener + */ + static listen(shortcutListener) { + if (shortcutListener === ShortcutListener.listener) { + return; } + + ShortcutListener.stopListening(); + + shortcutListener.isActive = true; + shortcutListener.setKeybindingLabel(ShortcutListener.listeningText); + ShortcutListener.listener = shortcutListener; + ShortcutListener.isListening = true; } - if (!this._isBindingValid({ mask, keycode, keyval }) || - !Gtk.accelerator_valid(keyval, mask)) - return Gdk.EVENT_STOP; + /** + * Stops listening for a keyboard shortcut. + */ + static stopListening() { + if (!ShortcutListener.isListening) { + return; + } + + ShortcutListener.isListening = false; + ShortcutListener.isAppendingShortcut = false; + ShortcutListener.listener.isActive = false; + ShortcutListener.listener.setKeybindingLabel( + ShortcutListener.listener.getKeybindingLabel(), + ); + ShortcutListener.listener = null; + } + + initialize(key, setting) { + this._key = key; + this._setting = setting; + this.isActive = false; + + this.connect('realize', () => + this.get_root().add_controller(this._eventKeyController), + ); + + this.keybinding = this._setting.get_strv(key) ?? []; + } + + /* + * Sets the label of the keybinding. + */ + setKeybindingLabel(label) { + this._keybindingLabel.set_label(label); + } + + /** + * Gets the keybinding in a more pleasant to read format. + * For example: [e,a] will become + * 'Ctrl+Super+E / Super+A' or 'Disabled' + * + * @returns {string} + */ + getKeybindingLabel() { + const kbLabel = this.keybinding.reduce((label, kb) => { + const [, keyval, mask] = Gtk.accelerator_parse(kb); + const l = Gtk.accelerator_get_label(keyval, mask); + if (!label) { + return l; + } + + return l ? `${label} / ${l}` : label; + }, ''); + + return kbLabel || _('Disabled'); + } + + _onActivated() { + if (this.isActive) { + ShortcutListener.stopListening(); + } else { + ShortcutListener.listen(this); + } + } + + _onKeybindingChanged() { + this._setting.set_strv(this._key, this.keybinding); + this._clearButton.set_sensitive(this.keybinding.length); + this.setKeybindingLabel(this.getKeybindingLabel()); + } + + _onClearButtonClicked() { + this.keybinding = []; + ShortcutListener.stopListening(); + } + + _onKeyPressed(eventControllerKey, keyval, keycode, state) { + if (this !== ShortcutListener.listener) { + return Gdk.EVENT_PROPAGATE; + } + + let mask = state & Gtk.accelerator_get_default_mod_mask(); + mask &= ~Gdk.ModifierType.LOCK_MASK; + + if (mask === 0) { + switch (keyval) { + case Gdk.KEY_BackSpace: + this.keybinding = []; + // falls through + case Gdk.KEY_Escape: + ShortcutListener.stopListening(); + return Gdk.EVENT_STOP; + case Gdk.KEY_KP_Enter: + case Gdk.KEY_Return: + case Gdk.KEY_space: + ShortcutListener.isAppendingShortcut = + !ShortcutListener.isAppendingShortcut; + this.setKeybindingLabel( + ShortcutListener.isAppendingShortcut ? + ShortcutListener.appendingText + : ShortcutListener.listeningText, + ); + return Gdk.EVENT_STOP; + } + } - const sc = Gtk.accelerator_name_with_keycode(null, keyval, keycode, mask); - this.keybinding = ShortcutListener.isAppendingShortcut ? [...this.keybinding, sc] : [sc]; - - ShortcutListener.stopListening(); - return Gdk.EVENT_STOP; - } - - /** - * Checks, if the given key combo is a valid binding. - * - * @param {{mask: number, keycode: number, keyval:number}} combo An object - * representing the key combo. - * @returns {boolean} `true` if the key combo is a valid binding. - */ - _isBindingValid({ mask, keycode, keyval }) { - if ((mask === 0 || mask === Gdk.SHIFT_MASK) && keycode !== 0) { if ( - (keyval >= Gdk.KEY_a && keyval <= Gdk.KEY_z) || - (keyval >= Gdk.KEY_A && keyval <= Gdk.KEY_Z) || - (keyval >= Gdk.KEY_0 && keyval <= Gdk.KEY_9) || - (keyval >= Gdk.KEY_kana_fullstop && keyval <= Gdk.KEY_semivoicedsound) || - (keyval >= Gdk.KEY_Arabic_comma && keyval <= Gdk.KEY_Arabic_sukun) || - (keyval >= Gdk.KEY_Serbian_dje && keyval <= Gdk.KEY_Cyrillic_HARDSIGN) || - (keyval >= Gdk.KEY_Greek_ALPHAaccent && keyval <= Gdk.KEY_Greek_omega) || - (keyval >= Gdk.KEY_hebrew_doublelowline && keyval <= Gdk.KEY_hebrew_taf) || - (keyval >= Gdk.KEY_Thai_kokai && keyval <= Gdk.KEY_Thai_lekkao) || - (keyval >= Gdk.KEY_Hangul_Kiyeog && keyval <= Gdk.KEY_Hangul_J_YeorinHieuh) || - (keyval === Gdk.KEY_space && mask === 0) || - this._isKeyvalForbidden(keyval) - ) - return false; + !this._isBindingValid({mask, keycode, keyval}) || + !Gtk.accelerator_valid(keyval, mask) + ) { + return Gdk.EVENT_STOP; + } + + const sc = Gtk.accelerator_name_with_keycode( + null, + keyval, + keycode, + mask, + ); + this.keybinding = + ShortcutListener.isAppendingShortcut ? + [...this.keybinding, sc] + : [sc]; + + ShortcutListener.stopListening(); + return Gdk.EVENT_STOP; + } + + /** + * Checks, if the given key combo is a valid binding. + * + * @param {{mask: number, keycode: number, keyval:number}} combo An object + * representing the key combo. + * @returns {boolean} `true` if the key combo is a valid binding. + */ + _isBindingValid({mask, keycode, keyval}) { + if ((mask === 0 || mask === Gdk.SHIFT_MASK) && keycode !== 0) { + if ( + (keyval >= Gdk.KEY_a && keyval <= Gdk.KEY_z) || + (keyval >= Gdk.KEY_A && keyval <= Gdk.KEY_Z) || + (keyval >= Gdk.KEY_0 && keyval <= Gdk.KEY_9) || + (keyval >= Gdk.KEY_kana_fullstop && + keyval <= Gdk.KEY_semivoicedsound) || + (keyval >= Gdk.KEY_Arabic_comma && + keyval <= Gdk.KEY_Arabic_sukun) || + (keyval >= Gdk.KEY_Serbian_dje && + keyval <= Gdk.KEY_Cyrillic_HARDSIGN) || + (keyval >= Gdk.KEY_Greek_ALPHAaccent && + keyval <= Gdk.KEY_Greek_omega) || + (keyval >= Gdk.KEY_hebrew_doublelowline && + keyval <= Gdk.KEY_hebrew_taf) || + (keyval >= Gdk.KEY_Thai_kokai && + keyval <= Gdk.KEY_Thai_lekkao) || + (keyval >= Gdk.KEY_Hangul_Kiyeog && + keyval <= Gdk.KEY_Hangul_J_YeorinHieuh) || + (keyval === Gdk.KEY_space && mask === 0) || + this._isKeyvalForbidden(keyval) + ) { + return false; + } + } + return true; + } + + /** + * Checks, if the given keyval is forbidden. + * + * @param {number} keyval The keyval number. + * @returns {boolean} `true` if the keyval is forbidden. + */ + _isKeyvalForbidden(keyval) { + const forbiddenKeyvals = [ + Gdk.KEY_Home, + Gdk.KEY_Left, + Gdk.KEY_Up, + Gdk.KEY_Right, + Gdk.KEY_Down, + Gdk.KEY_Page_Up, + Gdk.KEY_Page_Down, + Gdk.KEY_End, + Gdk.KEY_Tab, + Gdk.KEY_KP_Enter, + Gdk.KEY_Return, + Gdk.KEY_Mode_switch, + ]; + return forbiddenKeyvals.includes(keyval); } - return true; - } - - /** - * Checks, if the given keyval is forbidden. - * - * @param {number} keyval The keyval number. - * @returns {boolean} `true` if the keyval is forbidden. - */ - _isKeyvalForbidden(keyval) { - const forbiddenKeyvals = [ - Gdk.KEY_Home, - Gdk.KEY_Left, - Gdk.KEY_Up, - Gdk.KEY_Right, - Gdk.KEY_Down, - Gdk.KEY_Page_Up, - Gdk.KEY_Page_Down, - Gdk.KEY_End, - Gdk.KEY_Tab, - Gdk.KEY_KP_Enter, - Gdk.KEY_Return, - Gdk.KEY_Mode_switch - ]; - return forbiddenKeyvals.includes(keyval); - } -}); + }, +);