Compare commits
1 Commits
v0.2.1
...
afa45aedd2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa45aedd2 |
@@ -40,7 +40,7 @@ jobs:
|
|||||||
run: just build-package
|
run: just build-package
|
||||||
|
|
||||||
- name: Upload extension package
|
- name: Upload extension package
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: aerospike-extension
|
name: aerospike-extension
|
||||||
path: aerospike.zip
|
path: aerospike.zip
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: aerospike-extension
|
name: aerospike-extension
|
||||||
|
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -1,30 +1 @@
|
|||||||
# Aerospike Gnome (Tiling Window Manager)
|
# Aerospike Gnome (Tiling Window Manager)
|
||||||
|
|
||||||
Aerospike is a simple and opinionated tiling window manager for gnome.
|
|
||||||
|
|
||||||
This project takes inspiration from previous gnome tiling window managers such
|
|
||||||
as [forge](https://github.com/forge-ext/forge)
|
|
||||||
and [cosmic-shell](https://github.com/pop-os/gnome-shell-extension-pop-cosmic) as well as
|
|
||||||
MacOS tiling WMs, mainly [Aerospace](https://github.com/nikitabobko/AeroSpace) (not affiliated).
|
|
||||||
|
|
||||||
## Current Features
|
|
||||||
|
|
||||||
- Auto-tiling
|
|
||||||
- Accordion layouts with variable sizes
|
|
||||||
- Tabbed layouts
|
|
||||||
- Tree-based (albeit set depth of 2 for now) container-window paired layout similar to Aerospace
|
|
||||||
- Diagram for this is pending
|
|
||||||
|
|
||||||
## Planned functionality
|
|
||||||
|
|
||||||
- Full Keyboard control w/window movement
|
|
||||||
- Sub-containers (more tree layers than the 1 supported for now)
|
|
||||||
- Gap size customization
|
|
||||||
|
|
||||||
## Not currently planned
|
|
||||||
|
|
||||||
- Active window borders
|
|
||||||
- See my other extension for a rainbow or static border - [PrettyBorders](https://github.com//pretty-borders)
|
|
||||||
- complicated window dragging features and uis
|
|
||||||
- Aerospace supports control + drag to combine windows while moving with the mouse, and normal window dragging.
|
|
||||||
|
|
||||||
|
|||||||
130
extension.ts
130
extension.ts
@@ -4,7 +4,6 @@ import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
|||||||
import Gio from 'gi://Gio';
|
import Gio from 'gi://Gio';
|
||||||
import Shell from 'gi://Shell';
|
import Shell from 'gi://Shell';
|
||||||
import WindowManager from './src/wm/windowManager.js'
|
import WindowManager from './src/wm/windowManager.js'
|
||||||
import {Direction} from './src/wm/container.js'
|
|
||||||
import {Logger} from "./src/utils/logger.js";
|
import {Logger} from "./src/utils/logger.js";
|
||||||
|
|
||||||
export default class aerospike extends Extension {
|
export default class aerospike extends Extension {
|
||||||
@@ -16,19 +15,14 @@ export default class aerospike extends Extension {
|
|||||||
super(metadata);
|
super(metadata);
|
||||||
this.settings = this.getSettings('org.gnome.shell.extensions.aerospike');
|
this.settings = this.getSettings('org.gnome.shell.extensions.aerospike');
|
||||||
this.keyBindings = new Map();
|
this.keyBindings = new Map();
|
||||||
this.windowManager = new WindowManager(this.settings);
|
this.windowManager = new WindowManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
try {
|
|
||||||
Logger.log("STARTING AEROSPIKE!")
|
Logger.log("STARTING AEROSPIKE!")
|
||||||
this.bindSettings();
|
this.bindSettings();
|
||||||
this.setupKeybindings();
|
this.setupKeybindings();
|
||||||
this.windowManager.enable()
|
this.windowManager.enable()
|
||||||
Logger.log("AEROSPIKE ENABLED SUCCESSFULLY")
|
|
||||||
} catch (e) {
|
|
||||||
Logger.error("AEROSPIKE ENABLE FAILED", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disable() {
|
disable() {
|
||||||
@@ -36,41 +30,85 @@ export default class aerospike extends Extension {
|
|||||||
this.removeKeybindings()
|
this.removeKeybindings()
|
||||||
}
|
}
|
||||||
|
|
||||||
private keybindingActions(): Record<string, () => void> {
|
|
||||||
return {
|
|
||||||
'print-tree': () => { this.windowManager.printTreeStructure(); },
|
|
||||||
'toggle-orientation': () => { this.windowManager.toggleActiveContainerOrientation(); },
|
|
||||||
'reset-ratios': () => { this.windowManager.resetActiveContainerRatios(); },
|
|
||||||
'toggle-tabbed': () => { this.windowManager.toggleActiveContainerTabbed(); },
|
|
||||||
'focus-left': () => { this.windowManager.focusInDirection(Direction.LEFT); },
|
|
||||||
'focus-right': () => { this.windowManager.focusInDirection(Direction.RIGHT); },
|
|
||||||
'focus-up': () => { this.windowManager.focusInDirection(Direction.UP); },
|
|
||||||
'focus-down': () => { this.windowManager.focusInDirection(Direction.DOWN); },
|
|
||||||
'move-left': () => { this.windowManager.moveInDirection(Direction.LEFT); },
|
|
||||||
'move-right': () => { this.windowManager.moveInDirection(Direction.RIGHT); },
|
|
||||||
'move-up': () => { this.windowManager.moveInDirection(Direction.UP); },
|
|
||||||
'move-down': () => { this.windowManager.moveInDirection(Direction.DOWN); },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private bindSettings() {
|
private bindSettings() {
|
||||||
const keybindings = Object.keys(this.keybindingActions());
|
// Monitor settings changes
|
||||||
keybindings.forEach(name => {
|
this.settings.connect('changed::move-left', () => {
|
||||||
this.settings.connect(`changed::${name}`, () => {
|
log(`Keybinding 1 changed to: ${this.settings.get_strv('move-left')}`);
|
||||||
log(`${name} keybinding changed to: ${this.settings.get_strv(name)}`);
|
this.refreshKeybinding('move-left');
|
||||||
this.refreshKeybinding(name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.settings.connect('changed::move-right', () => {
|
||||||
|
log(`Keybinding 2 changed to: ${this.settings.get_strv('move-right')}`);
|
||||||
|
this.refreshKeybinding('move-right');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.connect('changed::join-with-left', () => {
|
||||||
|
log(`Keybinding 3 changed to: ${this.settings.get_strv('join-with-left')}`);
|
||||||
|
this.refreshKeybinding('join-with-left');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.connect('changed::join-with-right', () => {
|
||||||
|
log(`Keybinding 4 changed to: ${this.settings.get_strv('join-with-right')}`);
|
||||||
|
this.refreshKeybinding('join-with-right');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.connect('changed::print-tree', () => {
|
||||||
|
log(`Print tree keybinding changed to: ${this.settings.get_strv('print-tree')}`);
|
||||||
|
this.refreshKeybinding('print-tree');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.connect('changed::toggle-orientation', () => {
|
||||||
|
log(`Toggle orientation keybinding changed to: ${this.settings.get_strv('toggle-orientation')}`);
|
||||||
|
this.refreshKeybinding('toggle-orientation');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.connect('changed::dropdown-option', () => {
|
||||||
|
log(`Dropdown option changed to: ${this.settings.get_string('dropdown-option')}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.connect('changed::color-selection', () => {
|
||||||
|
log(`Color selection changed to: ${this.settings.get_string('color-selection')}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshKeybinding(settingName: string) {
|
private refreshKeybinding(settingName: string) {
|
||||||
if (this.keyBindings.has(settingName)) {
|
if (this.keyBindings.has(settingName)) {
|
||||||
Main.wm.removeKeybinding(settingName);
|
Main.wm.removeKeybinding(settingName);
|
||||||
this.keyBindings.delete(settingName);
|
this.keyBindings.delete(settingName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = this.keybindingActions()[settingName];
|
switch (settingName) {
|
||||||
if (action) this.bindKeybinding(settingName, action);
|
case 'move-left':
|
||||||
|
this.bindKeybinding('move-left', () => {
|
||||||
|
Logger.info('Keybinding 1 was pressed!');
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'move-right':
|
||||||
|
this.bindKeybinding('move-right', () => {
|
||||||
|
Logger.info('Keybinding 2 was pressed!');
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'join-with-left':
|
||||||
|
this.bindKeybinding('join-with-left', () => {
|
||||||
|
Logger.info('Keybinding 3 was pressed!');
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'join-with-right':
|
||||||
|
this.bindKeybinding('join-with-right', () => {
|
||||||
|
Logger.info('Keybinding 4 was pressed!');
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'print-tree':
|
||||||
|
this.bindKeybinding('print-tree', () => {
|
||||||
|
this.windowManager.printTreeStructure();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'toggle-orientation':
|
||||||
|
this.bindKeybinding('toggle-orientation', () => {
|
||||||
|
this.windowManager.toggleActiveContainerOrientation();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeKeybindings() {
|
private removeKeybindings() {
|
||||||
@@ -81,10 +119,29 @@ export default class aerospike extends Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupKeybindings() {
|
private setupKeybindings() {
|
||||||
const actions = this.keybindingActions();
|
this.bindKeybinding('move-left', () => {
|
||||||
for (const [name, action] of Object.entries(actions)) {
|
Logger.info('Keybinding 1 was pressed!');
|
||||||
this.bindKeybinding(name, action);
|
});
|
||||||
}
|
|
||||||
|
this.bindKeybinding('move-right', () => {
|
||||||
|
Logger.info('Keybinding 2 was pressed!');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bindKeybinding('join-with-left', () => {
|
||||||
|
Logger.info('Keybinding 3 was pressed!');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bindKeybinding('join-with-right', () => {
|
||||||
|
Logger.info('Keybinding 4 was pressed!');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bindKeybinding('print-tree', () => {
|
||||||
|
this.windowManager.printTreeStructure();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bindKeybinding('toggle-orientation', () => {
|
||||||
|
this.windowManager.toggleActiveContainerOrientation();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private bindKeybinding(settingName: string, callback: () => void) {
|
private bindKeybinding(settingName: string, callback: () => void) {
|
||||||
@@ -104,4 +161,7 @@ export default class aerospike extends Extension {
|
|||||||
|
|
||||||
this.keyBindings.set(settingName, keyBindingAction);
|
this.keyBindings.set(settingName, keyBindingAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aerospike",
|
"name": "aerospike",
|
||||||
"version": "0.2.1",
|
"version": "0.2.0",
|
||||||
"description": "A TypeScript GNOME Extension for Pretty Borders",
|
"description": "A TypeScript GNOME Extension for Pretty Borders",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"url": "git+https://gitlab.com/lucasoskorep/aerospike"
|
"url": "git+https://gitlab.com/lucasoskorep/aerospike"
|
||||||
},
|
},
|
||||||
"author": "Lucas Oskorep <lucas.oskorep@gmail.com>",
|
"author": "Lucas Oskorep <lucas.oskorep@gmail.com>",
|
||||||
"license": "GPL-3.0-only",
|
"license": "LGPL-3.0-or-later",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/example/my-extension/issues"
|
"url": "https://github.com/example/my-extension/issues"
|
||||||
},
|
},
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"@girs/gnome-shell": "49.1.0",
|
"@girs/gnome-shell": "49.1.0",
|
||||||
"@jest/globals": "^30.0.0",
|
"@jest/globals": "^30.0.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"eslint": "^10.0.0",
|
"eslint": "^9.36.0",
|
||||||
"eslint-plugin-jsdoc": "^62.0.0",
|
"eslint-plugin-jsdoc": "^62.0.0",
|
||||||
"jest": "^30.0.0",
|
"jest": "^30.0.0",
|
||||||
"ts-jest": "^29.1.2",
|
"ts-jest": "^29.1.2",
|
||||||
|
|||||||
225
pnpm-lock.yaml
generated
225
pnpm-lock.yaml
generated
@@ -43,11 +43,11 @@ importers:
|
|||||||
specifier: ^30.0.0
|
specifier: ^30.0.0
|
||||||
version: 30.0.0
|
version: 30.0.0
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^10.0.0
|
specifier: ^9.36.0
|
||||||
version: 10.0.0
|
version: 9.39.2
|
||||||
eslint-plugin-jsdoc:
|
eslint-plugin-jsdoc:
|
||||||
specifier: ^62.0.0
|
specifier: ^62.0.0
|
||||||
version: 62.4.1(eslint@10.0.0)
|
version: 62.5.0(eslint@9.39.2)
|
||||||
jest:
|
jest:
|
||||||
specifier: ^30.0.0
|
specifier: ^30.0.0
|
||||||
version: 30.2.0(@types/node@25.1.0)
|
version: 30.2.0(@types/node@25.1.0)
|
||||||
@@ -252,25 +252,33 @@ packages:
|
|||||||
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
||||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||||
|
|
||||||
'@eslint/config-array@0.23.0':
|
'@eslint/config-array@0.21.1':
|
||||||
resolution: {integrity: sha512-T5Swqd+PZxBekRuMsIFCySM3NUE8GjuqyksIIsXgkF2GCuiDaqpxKyPkv9VMEKpq5D7r5DLss1tM8tCsvRSjeg==}
|
resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/config-helpers@0.5.2':
|
'@eslint/config-helpers@0.4.2':
|
||||||
resolution: {integrity: sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==}
|
resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/core@1.1.0':
|
'@eslint/core@0.17.0':
|
||||||
resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==}
|
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/object-schema@3.0.0':
|
'@eslint/eslintrc@3.3.3':
|
||||||
resolution: {integrity: sha512-nWl20RtHQP2A2yvKU6Fee62Xo1AoNRqBLixtyg45zJhU8ljPFCyBK90d8e8XTnPns1RggSf4HH3bM8AhevkPVg==}
|
resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/plugin-kit@0.6.0':
|
'@eslint/js@9.39.2':
|
||||||
resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==}
|
resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@eslint/object-schema@2.1.7':
|
||||||
|
resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@eslint/plugin-kit@0.4.1':
|
||||||
|
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@girs/accountsservice-1.0@1.0.0-4.0.0-beta.38':
|
'@girs/accountsservice-1.0@1.0.0-4.0.0-beta.38':
|
||||||
resolution: {integrity: sha512-6QzytM5dztmMynF2bxN73EuNK9ArMFxkP2L8wUC7IH45zBeBOfYcqL85BFh2PmkGmqRk+Rli5EFR8dAkx3Ig5Q==}
|
resolution: {integrity: sha512-6QzytM5dztmMynF2bxN73EuNK9ArMFxkP2L8wUC7IH45zBeBOfYcqL85BFh2PmkGmqRk+Rli5EFR8dAkx3Ig5Q==}
|
||||||
@@ -417,14 +425,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
'@isaacs/balanced-match@4.0.1':
|
|
||||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
|
||||||
engines: {node: 20 || >=22}
|
|
||||||
|
|
||||||
'@isaacs/brace-expansion@5.0.1':
|
|
||||||
resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==}
|
|
||||||
engines: {node: 20 || >=22}
|
|
||||||
|
|
||||||
'@isaacs/cliui@8.0.2':
|
'@isaacs/cliui@8.0.2':
|
||||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -574,9 +574,6 @@ packages:
|
|||||||
'@types/babel__traverse@7.28.0':
|
'@types/babel__traverse@7.28.0':
|
||||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||||
|
|
||||||
'@types/esrecurse@4.3.1':
|
|
||||||
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
|
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
@@ -765,6 +762,9 @@ packages:
|
|||||||
argparse@1.0.10:
|
argparse@1.0.10:
|
||||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||||
|
|
||||||
|
argparse@2.0.1:
|
||||||
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
babel-jest@30.2.0:
|
babel-jest@30.2.0:
|
||||||
resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==}
|
resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==}
|
||||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
@@ -943,27 +943,31 @@ packages:
|
|||||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
eslint-plugin-jsdoc@62.4.1:
|
eslint-plugin-jsdoc@62.5.0:
|
||||||
resolution: {integrity: sha512-HgX2iN4j104D/mCUqRbhtzSZbph+KO9jfMHiIJjJ19Q+IwLQ5Na2IqvOJYq4S+4kgvEk1w6KYF4vVus6H2wcHg==}
|
resolution: {integrity: sha512-D+1haMVDzW/ZMoPwOnsbXCK07rJtsq98Z1v+ApvDKxSzYTTcPgmFc/nyUDCGmxm2cP7g7hszyjYHO7Zodl/43w==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
|
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
|
||||||
|
|
||||||
eslint-scope@9.1.0:
|
eslint-scope@8.4.0:
|
||||||
resolution: {integrity: sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==}
|
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
eslint-visitor-keys@3.4.3:
|
eslint-visitor-keys@3.4.3:
|
||||||
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
|
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
|
||||||
|
eslint-visitor-keys@4.2.1:
|
||||||
|
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
eslint-visitor-keys@5.0.0:
|
eslint-visitor-keys@5.0.0:
|
||||||
resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==}
|
resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
eslint@10.0.0:
|
eslint@9.39.2:
|
||||||
resolution: {integrity: sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==}
|
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
jiti: '*'
|
jiti: '*'
|
||||||
@@ -971,6 +975,10 @@ packages:
|
|||||||
jiti:
|
jiti:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
espree@10.4.0:
|
||||||
|
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
espree@11.1.0:
|
espree@11.1.0:
|
||||||
resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==}
|
resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
@@ -1077,12 +1085,15 @@ packages:
|
|||||||
|
|
||||||
glob@10.5.0:
|
glob@10.5.0:
|
||||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
glob@7.2.3:
|
glob@7.2.3:
|
||||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
deprecated: Glob versions prior to v9 are no longer supported
|
||||||
|
|
||||||
|
globals@14.0.0:
|
||||||
|
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
@@ -1110,6 +1121,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
import-fresh@3.3.1:
|
||||||
|
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
import-local@3.2.0:
|
import-local@3.2.0:
|
||||||
resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==}
|
resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1314,6 +1329,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
|
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
js-yaml@4.1.1:
|
||||||
|
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
jsdoc-type-pratt-parser@7.1.0:
|
jsdoc-type-pratt-parser@7.1.0:
|
||||||
resolution: {integrity: sha512-SX7q7XyCwzM/MEDCYz0l8GgGbJAACGFII9+WfNYr5SLEKukHWRy2Jk3iWRe7P+lpYJNs7oQ+OSei4JtKGUjd7A==}
|
resolution: {integrity: sha512-SX7q7XyCwzM/MEDCYz0l8GgGbJAACGFII9+WfNYr5SLEKukHWRy2Jk3iWRe7P+lpYJNs7oQ+OSei4JtKGUjd7A==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -1365,6 +1384,9 @@ packages:
|
|||||||
lodash.memoize@4.1.2:
|
lodash.memoize@4.1.2:
|
||||||
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
||||||
|
|
||||||
|
lodash.merge@4.6.2:
|
||||||
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
|
|
||||||
lru-cache@10.4.3:
|
lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
@@ -1392,10 +1414,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
minimatch@10.1.2:
|
|
||||||
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
|
|
||||||
engines: {node: 20 || >=22}
|
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
|
|
||||||
@@ -1475,6 +1493,10 @@ packages:
|
|||||||
package-json-from-dist@1.0.1:
|
package-json-from-dist@1.0.1:
|
||||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||||
|
|
||||||
|
parent-module@1.0.1:
|
||||||
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
parse-imports-exports@0.2.4:
|
parse-imports-exports@0.2.4:
|
||||||
resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==}
|
resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==}
|
||||||
|
|
||||||
@@ -1550,6 +1572,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==}
|
resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
resolve-from@4.0.0:
|
||||||
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
resolve-from@5.0.0:
|
resolve-from@5.0.0:
|
||||||
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2005,34 +2031,50 @@ snapshots:
|
|||||||
|
|
||||||
'@es-joy/resolve.exports@1.2.0': {}
|
'@es-joy/resolve.exports@1.2.0': {}
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.9.1(eslint@10.0.0)':
|
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 10.0.0
|
eslint: 9.39.2
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.12.2': {}
|
'@eslint-community/regexpp@4.12.2': {}
|
||||||
|
|
||||||
'@eslint/config-array@0.23.0':
|
'@eslint/config-array@0.21.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/object-schema': 3.0.0
|
'@eslint/object-schema': 2.1.7
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
minimatch: 10.1.2
|
minimatch: 3.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@eslint/config-helpers@0.5.2':
|
'@eslint/config-helpers@0.4.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/core': 1.1.0
|
'@eslint/core': 0.17.0
|
||||||
|
|
||||||
'@eslint/core@1.1.0':
|
'@eslint/core@0.17.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.15
|
||||||
|
|
||||||
'@eslint/object-schema@3.0.0': {}
|
'@eslint/eslintrc@3.3.3':
|
||||||
|
|
||||||
'@eslint/plugin-kit@0.6.0':
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/core': 1.1.0
|
ajv: 6.12.6
|
||||||
|
debug: 4.4.3
|
||||||
|
espree: 10.4.0
|
||||||
|
globals: 14.0.0
|
||||||
|
ignore: 5.3.2
|
||||||
|
import-fresh: 3.3.1
|
||||||
|
js-yaml: 4.1.1
|
||||||
|
minimatch: 3.1.2
|
||||||
|
strip-json-comments: 3.1.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@eslint/js@9.39.2': {}
|
||||||
|
|
||||||
|
'@eslint/object-schema@2.1.7': {}
|
||||||
|
|
||||||
|
'@eslint/plugin-kit@0.4.1':
|
||||||
|
dependencies:
|
||||||
|
'@eslint/core': 0.17.0
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
'@girs/accountsservice-1.0@1.0.0-4.0.0-beta.38':
|
'@girs/accountsservice-1.0@1.0.0-4.0.0-beta.38':
|
||||||
@@ -2499,12 +2541,6 @@ snapshots:
|
|||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
'@isaacs/balanced-match@4.0.1': {}
|
|
||||||
|
|
||||||
'@isaacs/brace-expansion@5.0.1':
|
|
||||||
dependencies:
|
|
||||||
'@isaacs/balanced-match': 4.0.1
|
|
||||||
|
|
||||||
'@isaacs/cliui@8.0.2':
|
'@isaacs/cliui@8.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 5.1.2
|
string-width: 5.1.2
|
||||||
@@ -2772,8 +2808,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.28.6
|
'@babel/types': 7.28.6
|
||||||
|
|
||||||
'@types/esrecurse@4.3.1': {}
|
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
'@types/istanbul-lib-coverage@2.0.6': {}
|
'@types/istanbul-lib-coverage@2.0.6': {}
|
||||||
@@ -2908,6 +2942,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sprintf-js: 1.0.3
|
sprintf-js: 1.0.3
|
||||||
|
|
||||||
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
babel-jest@30.2.0(@babel/core@7.28.6):
|
babel-jest@30.2.0(@babel/core@7.28.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.6
|
'@babel/core': 7.28.6
|
||||||
@@ -3074,7 +3110,7 @@ snapshots:
|
|||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
|
|
||||||
eslint-plugin-jsdoc@62.4.1(eslint@10.0.0):
|
eslint-plugin-jsdoc@62.5.0(eslint@9.39.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@es-joy/jsdoccomment': 0.83.0
|
'@es-joy/jsdoccomment': 0.83.0
|
||||||
'@es-joy/resolve.exports': 1.2.0
|
'@es-joy/resolve.exports': 1.2.0
|
||||||
@@ -3082,7 +3118,7 @@ snapshots:
|
|||||||
comment-parser: 1.4.5
|
comment-parser: 1.4.5
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint: 10.0.0
|
eslint: 9.39.2
|
||||||
espree: 11.1.0
|
espree: 11.1.0
|
||||||
esquery: 1.7.0
|
esquery: 1.7.0
|
||||||
html-entities: 2.6.0
|
html-entities: 2.6.0
|
||||||
@@ -3094,36 +3130,39 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-scope@9.1.0:
|
eslint-scope@8.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/esrecurse': 4.3.1
|
|
||||||
'@types/estree': 1.0.8
|
|
||||||
esrecurse: 4.3.0
|
esrecurse: 4.3.0
|
||||||
estraverse: 5.3.0
|
estraverse: 5.3.0
|
||||||
|
|
||||||
eslint-visitor-keys@3.4.3: {}
|
eslint-visitor-keys@3.4.3: {}
|
||||||
|
|
||||||
|
eslint-visitor-keys@4.2.1: {}
|
||||||
|
|
||||||
eslint-visitor-keys@5.0.0: {}
|
eslint-visitor-keys@5.0.0: {}
|
||||||
|
|
||||||
eslint@10.0.0:
|
eslint@9.39.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0)
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2)
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@eslint/config-array': 0.23.0
|
'@eslint/config-array': 0.21.1
|
||||||
'@eslint/config-helpers': 0.5.2
|
'@eslint/config-helpers': 0.4.2
|
||||||
'@eslint/core': 1.1.0
|
'@eslint/core': 0.17.0
|
||||||
'@eslint/plugin-kit': 0.6.0
|
'@eslint/eslintrc': 3.3.3
|
||||||
|
'@eslint/js': 9.39.2
|
||||||
|
'@eslint/plugin-kit': 0.4.1
|
||||||
'@humanfs/node': 0.16.7
|
'@humanfs/node': 0.16.7
|
||||||
'@humanwhocodes/module-importer': 1.0.1
|
'@humanwhocodes/module-importer': 1.0.1
|
||||||
'@humanwhocodes/retry': 0.4.3
|
'@humanwhocodes/retry': 0.4.3
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 9.1.0
|
eslint-scope: 8.4.0
|
||||||
eslint-visitor-keys: 5.0.0
|
eslint-visitor-keys: 4.2.1
|
||||||
espree: 11.1.0
|
espree: 10.4.0
|
||||||
esquery: 1.7.0
|
esquery: 1.7.0
|
||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
@@ -3134,12 +3173,19 @@ snapshots:
|
|||||||
imurmurhash: 0.1.4
|
imurmurhash: 0.1.4
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
json-stable-stringify-without-jsonify: 1.0.1
|
json-stable-stringify-without-jsonify: 1.0.1
|
||||||
minimatch: 10.1.2
|
lodash.merge: 4.6.2
|
||||||
|
minimatch: 3.1.2
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
optionator: 0.9.4
|
optionator: 0.9.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
espree@10.4.0:
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.15.0
|
||||||
|
acorn-jsx: 5.3.2(acorn@8.15.0)
|
||||||
|
eslint-visitor-keys: 4.2.1
|
||||||
|
|
||||||
espree@11.1.0:
|
espree@11.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
@@ -3258,6 +3304,8 @@ snapshots:
|
|||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
path-is-absolute: 1.0.1
|
path-is-absolute: 1.0.1
|
||||||
|
|
||||||
|
globals@14.0.0: {}
|
||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
handlebars@4.7.8:
|
handlebars@4.7.8:
|
||||||
@@ -3279,6 +3327,11 @@ snapshots:
|
|||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
|
import-fresh@3.3.1:
|
||||||
|
dependencies:
|
||||||
|
parent-module: 1.0.1
|
||||||
|
resolve-from: 4.0.0
|
||||||
|
|
||||||
import-local@3.2.0:
|
import-local@3.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pkg-dir: 4.2.0
|
pkg-dir: 4.2.0
|
||||||
@@ -3666,6 +3719,10 @@ snapshots:
|
|||||||
argparse: 1.0.10
|
argparse: 1.0.10
|
||||||
esprima: 4.0.1
|
esprima: 4.0.1
|
||||||
|
|
||||||
|
js-yaml@4.1.1:
|
||||||
|
dependencies:
|
||||||
|
argparse: 2.0.1
|
||||||
|
|
||||||
jsdoc-type-pratt-parser@7.1.0: {}
|
jsdoc-type-pratt-parser@7.1.0: {}
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
@@ -3703,6 +3760,8 @@ snapshots:
|
|||||||
|
|
||||||
lodash.memoize@4.1.2: {}
|
lodash.memoize@4.1.2: {}
|
||||||
|
|
||||||
|
lodash.merge@4.6.2: {}
|
||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
@@ -3728,10 +3787,6 @@ snapshots:
|
|||||||
|
|
||||||
mimic-fn@2.1.0: {}
|
mimic-fn@2.1.0: {}
|
||||||
|
|
||||||
minimatch@10.1.2:
|
|
||||||
dependencies:
|
|
||||||
'@isaacs/brace-expansion': 5.0.1
|
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.12
|
brace-expansion: 1.1.12
|
||||||
@@ -3801,6 +3856,10 @@ snapshots:
|
|||||||
|
|
||||||
package-json-from-dist@1.0.1: {}
|
package-json-from-dist@1.0.1: {}
|
||||||
|
|
||||||
|
parent-module@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
callsites: 3.1.0
|
||||||
|
|
||||||
parse-imports-exports@0.2.4:
|
parse-imports-exports@0.2.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
parse-statements: 1.0.11
|
parse-statements: 1.0.11
|
||||||
@@ -3859,6 +3918,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
resolve-from: 5.0.0
|
resolve-from: 5.0.0
|
||||||
|
|
||||||
|
resolve-from@4.0.0: {}
|
||||||
|
|
||||||
resolve-from@5.0.0: {}
|
resolve-from@5.0.0: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|||||||
@@ -1,72 +1,40 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<schemalist>
|
<schemalist>
|
||||||
<schema id="org.gnome.shell.extensions.aerospike" path="/org/gnome/shell/extensions/aerospike/">
|
<schema id="org.gnome.shell.extensions.aerospike" path="/org/gnome/shell/extensions/aerospike/">
|
||||||
|
<key name="dropdown-option" type="s">
|
||||||
<key name="focus-left" type="as">
|
<default>'option1'</default>
|
||||||
<default><![CDATA[['<Super>h']]]></default>
|
<summary>Dropdown selection</summary>
|
||||||
<summary>Focus window to the left</summary>
|
<description>Option selected from the dropdown menu</description>
|
||||||
<description>Move focus to the window to the left of the current window. In tabbed mode, switches to the previous tab.</description>
|
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<key name="focus-right" type="as">
|
<key name="color-selection" type="s">
|
||||||
<default><![CDATA[['<Super>l']]]></default>
|
<default>'rgb(255,0,0)'</default>
|
||||||
<summary>Focus window to the right</summary>
|
<summary>Selected color</summary>
|
||||||
<description>Move focus to the window to the right of the current window. In tabbed mode, switches to the next tab.</description>
|
<description>Color chosen from the color picker</description>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<key name="focus-up" type="as">
|
|
||||||
<default><![CDATA[['<Super>k']]]></default>
|
|
||||||
<summary>Focus window above</summary>
|
|
||||||
<description>Move focus to the window above the current window.</description>
|
|
||||||
</key>
|
|
||||||
|
|
||||||
<key name="focus-down" type="as">
|
|
||||||
<default><![CDATA[['<Super>j']]]></default>
|
|
||||||
<summary>Focus window below</summary>
|
|
||||||
<description>Move focus to the window below the current window.</description>
|
|
||||||
</key>
|
|
||||||
|
|
||||||
|
|
||||||
<key name="move-left" type="as">
|
<key name="move-left" type="as">
|
||||||
<default><![CDATA[['<Super><Shift>h']]]></default>
|
<default><![CDATA[['<Super>1']]]></default>
|
||||||
<summary>Move window to the left</summary>
|
<summary>Keybinding for action 1</summary>
|
||||||
<description>Move the active window one position to the left within its container</description>
|
<description>Keyboard shortcut for triggering action 1</description>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<key name="move-right" type="as">
|
<key name="move-right" type="as">
|
||||||
<default><![CDATA[['<Super><Shift>l']]]></default>
|
<default><![CDATA[['<Super>2']]]></default>
|
||||||
<summary>Move window to the right</summary>
|
<summary>Keybinding for action 2</summary>
|
||||||
<description>Move the active window one position to the right within its container</description>
|
<description>Keyboard shortcut for triggering action 2</description>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<key name="move-up" type="as">
|
<key name="join-with-left" type="as">
|
||||||
<default><![CDATA[['<Super><Shift>k']]]></default>
|
<default><![CDATA[['<Super>3']]]></default>
|
||||||
<summary>Move window up</summary>
|
<summary>Keybinding for action 3</summary>
|
||||||
<description>Move the active window one position up within its container</description>
|
<description>Keyboard shortcut for triggering action 3</description>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<key name="move-down" type="as">
|
<key name="join-with-right" type="as">
|
||||||
<default><![CDATA[['<Super><Shift>j']]]></default>
|
<default><![CDATA[['<Super>4']]]></default>
|
||||||
<summary>Move window down</summary>
|
<summary>Keybinding for action 4</summary>
|
||||||
<description>Move the active window one position down within its container</description>
|
<description>Keyboard shortcut for triggering action 4</description>
|
||||||
</key>
|
|
||||||
|
|
||||||
<key name="toggle-orientation" type="as">
|
|
||||||
<default><![CDATA[['<Super>comma']]]></default>
|
|
||||||
<summary>Toggle active container orientation</summary>
|
|
||||||
<description>Toggles the orientation of the container holding the active window between horizontal and vertical</description>
|
|
||||||
</key>
|
|
||||||
|
|
||||||
<key name="reset-ratios" type="as">
|
|
||||||
<default><![CDATA[['<Super>z']]]></default>
|
|
||||||
<summary>Reset container ratios to equal splits</summary>
|
|
||||||
<description>Resets all window size ratios in the active window's container to equal splits</description>
|
|
||||||
</key>
|
|
||||||
|
|
||||||
<key name="toggle-tabbed" type="as">
|
|
||||||
<default><![CDATA[['<Super>slash']]]></default>
|
|
||||||
<summary>Toggle tabbed container mode</summary>
|
|
||||||
<description>Toggles the active window's container between tabbed and accordion layout modes</description>
|
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<key name="print-tree" type="as">
|
<key name="print-tree" type="as">
|
||||||
@@ -75,5 +43,11 @@
|
|||||||
<description>Prints the current tree of containers and windows per monitor to logs</description>
|
<description>Prints the current tree of containers and windows per monitor to logs</description>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
|
<key name="toggle-orientation" type="as">
|
||||||
|
<default><![CDATA[['<Super><Shift>comma']]]></default>
|
||||||
|
<summary>Toggle active container orientation</summary>
|
||||||
|
<description>Toggles the orientation of the container holding the active window between horizontal and vertical</description>
|
||||||
|
</key>
|
||||||
|
|
||||||
</schema>
|
</schema>
|
||||||
</schemalist>
|
</schemalist>
|
||||||
@@ -20,38 +20,25 @@ jest.mock('../utils/events.js', () => ({
|
|||||||
|
|
||||||
describe('Container Logic Tests', () => {
|
describe('Container Logic Tests', () => {
|
||||||
describe('Orientation Toggle Logic', () => {
|
describe('Orientation Toggle Logic', () => {
|
||||||
enum Layout {
|
enum Orientation {
|
||||||
HORIZONTAL = 0,
|
HORIZONTAL = 0,
|
||||||
VERTICAL = 1,
|
VERTICAL = 1,
|
||||||
TABBED = 2,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleOrientation = (current: Layout): Layout => {
|
const toggleOrientation = (current: Orientation): Orientation => {
|
||||||
if (current === Layout.TABBED) return Layout.HORIZONTAL;
|
return current === Orientation.HORIZONTAL
|
||||||
return current === Layout.HORIZONTAL
|
? Orientation.VERTICAL
|
||||||
? Layout.VERTICAL
|
: Orientation.HORIZONTAL;
|
||||||
: Layout.HORIZONTAL;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should toggle from HORIZONTAL to VERTICAL', () => {
|
test('should toggle from HORIZONTAL to VERTICAL', () => {
|
||||||
const result = toggleOrientation(Layout.HORIZONTAL);
|
const result = toggleOrientation(Orientation.HORIZONTAL);
|
||||||
expect(result).toBe(Layout.VERTICAL);
|
expect(result).toBe(Orientation.VERTICAL);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should toggle from VERTICAL to HORIZONTAL', () => {
|
test('should toggle from VERTICAL to HORIZONTAL', () => {
|
||||||
const result = toggleOrientation(Layout.VERTICAL);
|
const result = toggleOrientation(Orientation.VERTICAL);
|
||||||
expect(result).toBe(Layout.HORIZONTAL);
|
expect(result).toBe(Orientation.HORIZONTAL);
|
||||||
});
|
|
||||||
|
|
||||||
test('should toggle from TABBED to HORIZONTAL', () => {
|
|
||||||
const result = toggleOrientation(Layout.TABBED);
|
|
||||||
expect(result).toBe(Layout.HORIZONTAL);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('enum reverse mapping should return string names', () => {
|
|
||||||
expect(Layout[Layout.HORIZONTAL]).toBe('HORIZONTAL');
|
|
||||||
expect(Layout[Layout.VERTICAL]).toBe('VERTICAL');
|
|
||||||
expect(Layout[Layout.TABBED]).toBe('TABBED');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,71 +100,6 @@ describe('Container Logic Tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Tabbed Bounds Calculation', () => {
|
|
||||||
const TAB_BAR_HEIGHT = 24;
|
|
||||||
|
|
||||||
test('should give all items the same content rect in tabbed mode', () => {
|
|
||||||
const workArea = { x: 100, y: 0, width: 1000, height: 500 };
|
|
||||||
const itemCount = 3;
|
|
||||||
|
|
||||||
const contentRect = {
|
|
||||||
x: workArea.x,
|
|
||||||
y: workArea.y + TAB_BAR_HEIGHT,
|
|
||||||
width: workArea.width,
|
|
||||||
height: workArea.height - TAB_BAR_HEIGHT,
|
|
||||||
};
|
|
||||||
|
|
||||||
const bounds = Array.from({ length: itemCount }, () => contentRect);
|
|
||||||
|
|
||||||
expect(bounds.length).toBe(3);
|
|
||||||
// All bounds should be identical
|
|
||||||
bounds.forEach(b => {
|
|
||||||
expect(b.x).toBe(100);
|
|
||||||
expect(b.y).toBe(TAB_BAR_HEIGHT);
|
|
||||||
expect(b.width).toBe(1000);
|
|
||||||
expect(b.height).toBe(500 - TAB_BAR_HEIGHT);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('tab bar rect should occupy top of work area', () => {
|
|
||||||
const workArea = { x: 200, y: 50, width: 800, height: 600 };
|
|
||||||
|
|
||||||
const tabBarRect = {
|
|
||||||
x: workArea.x,
|
|
||||||
y: workArea.y,
|
|
||||||
width: workArea.width,
|
|
||||||
height: TAB_BAR_HEIGHT,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(tabBarRect.x).toBe(200);
|
|
||||||
expect(tabBarRect.y).toBe(50);
|
|
||||||
expect(tabBarRect.width).toBe(800);
|
|
||||||
expect(tabBarRect.height).toBe(TAB_BAR_HEIGHT);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('active tab index should clamp after removal', () => {
|
|
||||||
let activeTabIndex = 2;
|
|
||||||
const itemCount = 2; // after removing one from 3
|
|
||||||
|
|
||||||
if (activeTabIndex >= itemCount) {
|
|
||||||
activeTabIndex = itemCount - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(activeTabIndex).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('active tab index should stay at 0 when first item removed', () => {
|
|
||||||
let activeTabIndex = 0;
|
|
||||||
const itemCount = 2; // after removing one from 3
|
|
||||||
|
|
||||||
if (activeTabIndex >= itemCount) {
|
|
||||||
activeTabIndex = itemCount - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(activeTabIndex).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Window Index Finding', () => {
|
describe('Window Index Finding', () => {
|
||||||
test('should find window index in array', () => {
|
test('should find window index in array', () => {
|
||||||
const windows = [
|
const windows = [
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Adw from 'gi://Adw';
|
|||||||
import Gio from 'gi://Gio';
|
import Gio from 'gi://Gio';
|
||||||
import Gtk from 'gi://Gtk';
|
import Gtk from 'gi://Gtk';
|
||||||
import Gdk from 'gi://Gdk';
|
import Gdk from 'gi://Gdk';
|
||||||
import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
||||||
import {Logger} from "../utils/logger.js";
|
import {Logger} from "../utils/logger.js";
|
||||||
import {EntryRow} from "./keybindings.js";
|
import {EntryRow} from "./keybindings.js";
|
||||||
|
|
||||||
@@ -11,115 +11,160 @@ export default class AerospikeExtensions extends ExtensionPreferences {
|
|||||||
// Create settings object
|
// Create settings object
|
||||||
const settings = this.getSettings('org.gnome.shell.extensions.aerospike');
|
const settings = this.getSettings('org.gnome.shell.extensions.aerospike');
|
||||||
|
|
||||||
// Create keybindings page (top-level)
|
// Create a preferences page
|
||||||
const keybindingsPage = new Adw.PreferencesPage({
|
const page = new Adw.PreferencesPage({
|
||||||
title: _('Keybindings'),
|
title: _('Settings'),
|
||||||
icon_name: 'input-keyboard-symbolic',
|
icon_name: 'preferences-system-symbolic',
|
||||||
});
|
});
|
||||||
window.add(keybindingsPage);
|
window.add(page);
|
||||||
|
|
||||||
const keybindingMap = this.createKeybindingMap();
|
|
||||||
|
|
||||||
// Top-level Keybindings header group with syntax help
|
// Create options group
|
||||||
const keybindingsHeader = new Adw.PreferencesGroup({
|
const optionsGroup = new Adw.PreferencesGroup({
|
||||||
title: _('Keybindings'),
|
title: _('Options'),
|
||||||
|
});
|
||||||
|
page.add(optionsGroup);
|
||||||
|
|
||||||
|
// Add dropdown
|
||||||
|
const dropdownRow = new Adw.ComboRow({
|
||||||
|
title: _('Select an option'),
|
||||||
|
});
|
||||||
|
optionsGroup.add(dropdownRow);
|
||||||
|
|
||||||
|
// Create dropdown model
|
||||||
|
const dropdownModel = new Gtk.StringList();
|
||||||
|
dropdownModel.append(_('Option 1'));
|
||||||
|
dropdownModel.append(_('Option 2'));
|
||||||
|
dropdownModel.append(_('Option 3'));
|
||||||
|
dropdownModel.append(_('Option 4'));
|
||||||
|
|
||||||
|
dropdownRow.set_model(dropdownModel);
|
||||||
|
|
||||||
|
// Set the active option based on settings
|
||||||
|
const currentOption = settings.get_string('dropdown-option');
|
||||||
|
switch (currentOption) {
|
||||||
|
case 'option1':
|
||||||
|
dropdownRow.set_selected(0);
|
||||||
|
break;
|
||||||
|
case 'option2':
|
||||||
|
dropdownRow.set_selected(1);
|
||||||
|
break;
|
||||||
|
case 'option3':
|
||||||
|
dropdownRow.set_selected(2);
|
||||||
|
break;
|
||||||
|
case 'option4':
|
||||||
|
dropdownRow.set_selected(3);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
dropdownRow.set_selected(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect dropdown change signal
|
||||||
|
dropdownRow.connect('notify::selected', () => {
|
||||||
|
const selected = dropdownRow.get_selected();
|
||||||
|
let optionValue: string;
|
||||||
|
|
||||||
|
switch (selected) {
|
||||||
|
case 0:
|
||||||
|
optionValue = 'option1';
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
optionValue = 'option2';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
optionValue = 'option3';
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
optionValue = 'option4';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
optionValue = 'option1';
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.set_string('dropdown-option', optionValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add color button
|
||||||
|
const colorRow = new Adw.ActionRow({
|
||||||
|
title: _('Choose a color'),
|
||||||
|
});
|
||||||
|
optionsGroup.add(colorRow);
|
||||||
|
|
||||||
|
const colorButton = new Gtk.ColorButton();
|
||||||
|
colorRow.add_suffix(colorButton);
|
||||||
|
colorRow.set_activatable_widget(colorButton);
|
||||||
|
|
||||||
|
// Set current color from settings
|
||||||
|
const colorStr = settings.get_string('color-selection');
|
||||||
|
const rgba = new Gdk.RGBA();
|
||||||
|
rgba.parse(colorStr);
|
||||||
|
colorButton.set_rgba(rgba);
|
||||||
|
|
||||||
|
// Connect color button signal
|
||||||
|
colorButton.connect('color-set', () => {
|
||||||
|
const color = colorButton.get_rgba().to_string();
|
||||||
|
settings.set_string('color-selection', color);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create keybindings group
|
||||||
|
const keybindingsGroup = new Adw.PreferencesGroup({
|
||||||
|
title: _('Keyboard Shortcuts'),
|
||||||
description: `${_("Syntax")}: <Super>h, <Shift>g, <Super><Shift>h
|
description: `${_("Syntax")}: <Super>h, <Shift>g, <Super><Shift>h
|
||||||
${_("Legend")}: <Super> - ${_("Windows key")}, <Primary> - ${_("Control key")}
|
${_("Legend")}: <Super> - ${_("Windows key")}, <Primary> - ${_("Control key")}
|
||||||
${_("Delete text to unset. Press Return key to accept.")}`,
|
${_("Delete text to unset. Press Return key to accept.")}`,
|
||||||
});
|
});
|
||||||
keybindingsPage.add(keybindingsHeader);
|
page.add(keybindingsGroup);
|
||||||
|
|
||||||
// --- Focus group ---
|
// Add keybinding rows as EntryRows with proper mapping
|
||||||
const focusGroup = new Adw.PreferencesGroup({
|
// Use the helper function to create the map object
|
||||||
title: _('Focus'),
|
const keybindingMap = this.createKeybindingMap();
|
||||||
});
|
|
||||||
keybindingsPage.add(focusGroup);
|
|
||||||
|
|
||||||
focusGroup.add(
|
keybindingsGroup.add(
|
||||||
new EntryRow({
|
new EntryRow({
|
||||||
title: _('Focus Left'),
|
title: _('Action 1'),
|
||||||
settings: settings,
|
|
||||||
bind: 'focus-left',
|
|
||||||
map: keybindingMap
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
focusGroup.add(
|
|
||||||
new EntryRow({
|
|
||||||
title: _('Focus Right'),
|
|
||||||
settings: settings,
|
|
||||||
bind: 'focus-right',
|
|
||||||
map: keybindingMap
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
focusGroup.add(
|
|
||||||
new EntryRow({
|
|
||||||
title: _('Focus Up'),
|
|
||||||
settings: settings,
|
|
||||||
bind: 'focus-up',
|
|
||||||
map: keybindingMap
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
focusGroup.add(
|
|
||||||
new EntryRow({
|
|
||||||
title: _('Focus Down'),
|
|
||||||
settings: settings,
|
|
||||||
bind: 'focus-down',
|
|
||||||
map: keybindingMap
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Move group ---
|
|
||||||
const moveGroup = new Adw.PreferencesGroup({
|
|
||||||
title: _('Move'),
|
|
||||||
});
|
|
||||||
keybindingsPage.add(moveGroup);
|
|
||||||
|
|
||||||
moveGroup.add(
|
|
||||||
new EntryRow({
|
|
||||||
title: _('Move Left'),
|
|
||||||
settings: settings,
|
settings: settings,
|
||||||
bind: 'move-left',
|
bind: 'move-left',
|
||||||
map: keybindingMap
|
map: keybindingMap
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
moveGroup.add(
|
keybindingsGroup.add(
|
||||||
new EntryRow({
|
new EntryRow({
|
||||||
title: _('Move Right'),
|
title: _('Action 2'),
|
||||||
settings: settings,
|
settings: settings,
|
||||||
bind: 'move-right',
|
bind: 'move-right',
|
||||||
map: keybindingMap
|
map: keybindingMap
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
moveGroup.add(
|
keybindingsGroup.add(
|
||||||
new EntryRow({
|
new EntryRow({
|
||||||
title: _('Move Up'),
|
title: _('Action 3'),
|
||||||
settings: settings,
|
settings: settings,
|
||||||
bind: 'move-up',
|
bind: 'join-with-left',
|
||||||
map: keybindingMap
|
map: keybindingMap
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
moveGroup.add(
|
keybindingsGroup.add(
|
||||||
new EntryRow({
|
new EntryRow({
|
||||||
title: _('Move Down'),
|
title: _('Action 4'),
|
||||||
settings: settings,
|
settings: settings,
|
||||||
bind: 'move-down',
|
bind: 'join-with-right',
|
||||||
map: keybindingMap
|
map: keybindingMap
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Container Interactions group ---
|
keybindingsGroup.add(
|
||||||
const containerGroup = new Adw.PreferencesGroup({
|
new EntryRow({
|
||||||
title: _('Container Interactions'),
|
title: _('Print Tree Structure'),
|
||||||
});
|
settings: settings,
|
||||||
keybindingsPage.add(containerGroup);
|
bind: 'print-tree',
|
||||||
|
map: keybindingMap
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
containerGroup.add(
|
keybindingsGroup.add(
|
||||||
new EntryRow({
|
new EntryRow({
|
||||||
title: _('Toggle Orientation'),
|
title: _('Toggle Orientation'),
|
||||||
settings: settings,
|
settings: settings,
|
||||||
@@ -128,38 +173,6 @@ export default class AerospikeExtensions extends ExtensionPreferences {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
containerGroup.add(
|
|
||||||
new EntryRow({
|
|
||||||
title: _('Reset Container Ratios to Equal'),
|
|
||||||
settings: settings,
|
|
||||||
bind: 'reset-ratios',
|
|
||||||
map: keybindingMap
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
containerGroup.add(
|
|
||||||
new EntryRow({
|
|
||||||
title: _('Toggle Tabbed Mode'),
|
|
||||||
settings: settings,
|
|
||||||
bind: 'toggle-tabbed',
|
|
||||||
map: keybindingMap
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Debugging group ---
|
|
||||||
const debuggingGroup = new Adw.PreferencesGroup({
|
|
||||||
title: _('Debugging'),
|
|
||||||
});
|
|
||||||
keybindingsPage.add(debuggingGroup);
|
|
||||||
|
|
||||||
debuggingGroup.add(
|
|
||||||
new EntryRow({
|
|
||||||
title: _('Print Tree Structure'),
|
|
||||||
settings: settings,
|
|
||||||
bind: 'print-tree',
|
|
||||||
map: keybindingMap
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import GLib from "gi://GLib";
|
import GLib from "gi://GLib";
|
||||||
|
|
||||||
|
|
||||||
export type QueuedEvent = {
|
export type QueuedEvent = {
|
||||||
name: string;
|
name: string;
|
||||||
callback: () => void;
|
callback: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingEvents: Map<string, QueuedEvent> = new Map();
|
const queuedEvents: QueuedEvent[] = [];
|
||||||
|
|
||||||
export default function queueEvent(event: QueuedEvent, interval = 200) {
|
export default function queueEvent(event: QueuedEvent, interval = 200) {
|
||||||
pendingEvents.set(event.name, event);
|
queuedEvents.push(event);
|
||||||
|
|
||||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => {
|
GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => {
|
||||||
const e = pendingEvents.get(event.name);
|
const e = queuedEvents.pop()
|
||||||
if (e && e === event) {
|
if (e) {
|
||||||
pendingEvents.delete(event.name);
|
|
||||||
e.callback();
|
e.callback();
|
||||||
}
|
}
|
||||||
return GLib.SOURCE_REMOVE;
|
return queuedEvents.length !== 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,229 +1,52 @@
|
|||||||
import {WindowWrapper} from "./window.js";
|
import {WindowWrapper} from "./window.js";
|
||||||
import {Logger} from "../utils/logger.js";
|
import {Logger} from "../utils/logger.js";
|
||||||
|
import Meta from "gi://Meta";
|
||||||
import queueEvent from "../utils/events.js";
|
import queueEvent from "../utils/events.js";
|
||||||
import {Rect} from "../utils/rect.js";
|
import {Rect} from "../utils/rect.js";
|
||||||
import {TabBar, TAB_BAR_HEIGHT} from "./tabBar.js";
|
|
||||||
|
|
||||||
export enum Layout {
|
enum Orientation {
|
||||||
ACC_HORIZONTAL = 0,
|
HORIZONTAL = 0,
|
||||||
ACC_VERTICAL = 1,
|
VERTICAL = 1,
|
||||||
TABBED = 2,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Direction {
|
|
||||||
LEFT = 'left',
|
|
||||||
RIGHT = 'right',
|
|
||||||
UP = 'up',
|
|
||||||
DOWN = 'down',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns equal ratios summing exactly to 1.0, with float drift absorbed by the last slot.
|
|
||||||
function equalRatios(n: number): number[] {
|
|
||||||
if (n <= 0) return [];
|
|
||||||
const base = 1 / n;
|
|
||||||
const ratios = Array(n).fill(base);
|
|
||||||
const sumExceptLast = ratios.slice(0, -1).reduce((a, b) => a + b, 0);
|
|
||||||
ratios[n - 1] = 1 - sumExceptLast;
|
|
||||||
return ratios;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class WindowContainer {
|
export default class WindowContainer {
|
||||||
|
|
||||||
_tiledItems: (WindowWrapper | WindowContainer)[];
|
_tiledItems: (WindowWrapper | WindowContainer)[];
|
||||||
_tiledWindowLookup: Map<number, WindowWrapper>;
|
_tiledWindowLookup: Map<number, WindowWrapper>;
|
||||||
_orientation: Layout = Layout.ACC_HORIZONTAL;
|
_orientation: Orientation = Orientation.HORIZONTAL;
|
||||||
_workArea: Rect;
|
_workArea: Rect;
|
||||||
|
|
||||||
// -- Accordion Mode States
|
constructor(workspaceArea: Rect,) {
|
||||||
|
|
||||||
_splitRatios: number[];
|
|
||||||
|
|
||||||
// -- Tabbed mode state -----------------------------------------------------
|
|
||||||
_activeTabIndex: number = 0;
|
|
||||||
_tabBar: TabBar | null = null;
|
|
||||||
|
|
||||||
constructor(workspaceArea: Rect) {
|
|
||||||
this._tiledItems = [];
|
this._tiledItems = [];
|
||||||
this._tiledWindowLookup = new Map<number, WindowWrapper>();
|
this._tiledWindowLookup = new Map<number, WindowWrapper>();
|
||||||
this._workArea = workspaceArea;
|
this._workArea = workspaceArea;
|
||||||
this._splitRatios = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helpers ----------------------------------------------------------------
|
|
||||||
|
|
||||||
private _resetRatios(): void {
|
|
||||||
this._splitRatios = equalRatios(this._tiledItems.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proportionally shrink existing ratios to carve out space for a new item
|
|
||||||
* at the given index. If no index is supplied the ratio is appended at the end.
|
|
||||||
*/
|
|
||||||
private _addRatioForNewWindow(index?: number): void {
|
|
||||||
const n = this._tiledItems.length;
|
|
||||||
if (n <= 1) {
|
|
||||||
this._splitRatios = [1.0];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newRatio = 1 / n;
|
|
||||||
const scale = 1 - newRatio;
|
|
||||||
const scaled = this._splitRatios.map(r => r * scale);
|
|
||||||
const partialSum = scaled.reduce((a, b) => a + b, 0) + newRatio;
|
|
||||||
scaled[scaled.length - 1] += (1.0 - partialSum);
|
|
||||||
|
|
||||||
const insertAt = index ?? scaled.length;
|
|
||||||
scaled.splice(insertAt, 0, newRatio);
|
|
||||||
this._splitRatios = scaled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _totalDimension(): number {
|
|
||||||
return this._orientation === Layout.ACC_HORIZONTAL
|
|
||||||
? this._workArea.width
|
|
||||||
: this._workArea.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
isTabbed(): boolean {
|
|
||||||
return this._orientation === Layout.TABBED;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Public API -------------------------------------------------------------
|
|
||||||
|
|
||||||
move(rect: Rect): void {
|
move(rect: Rect): void {
|
||||||
this._workArea = rect;
|
this._workArea = rect;
|
||||||
this.drawWindows();
|
this.tileWindows();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleOrientation(): void {
|
toggleOrientation(): void {
|
||||||
if (this._orientation === Layout.TABBED) {
|
this._orientation = this._orientation === Orientation.HORIZONTAL
|
||||||
// Tabbed → Horizontal: restore accordion mode
|
? Orientation.VERTICAL
|
||||||
this.setAccordion(Layout.ACC_HORIZONTAL);
|
: Orientation.HORIZONTAL;
|
||||||
} else {
|
Logger.info(`Container orientation toggled to ${this._orientation === Orientation.HORIZONTAL ? 'HORIZONTAL' : 'VERTICAL'}`);
|
||||||
this._orientation = this._orientation === Layout.ACC_HORIZONTAL
|
this.tileWindows();
|
||||||
? Layout.ACC_VERTICAL
|
|
||||||
: Layout.ACC_HORIZONTAL;
|
|
||||||
Logger.info(`Container orientation toggled to ${Layout[this._orientation]}`);
|
|
||||||
this.drawWindows();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
addWindow(winWrap: WindowWrapper): void {
|
||||||
* Switch this container to tabbed mode.
|
this._tiledItems.push(winWrap);
|
||||||
*/
|
|
||||||
setTabbed(): void {
|
|
||||||
if (this._orientation === Layout.TABBED) return;
|
|
||||||
|
|
||||||
Logger.info("Container switching to TABBED mode");
|
|
||||||
this._orientation = Layout.TABBED;
|
|
||||||
|
|
||||||
// Clamp active tab index
|
|
||||||
if (this._activeTabIndex < 0 || this._activeTabIndex >= this._tiledItems.length) {
|
|
||||||
this._activeTabIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create tab bar
|
|
||||||
this._tabBar = new TabBar((index) => {
|
|
||||||
this.setActiveTab(index);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.drawWindows();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switch this container back to accordion (H or V) mode.
|
|
||||||
*/
|
|
||||||
setAccordion(orientation: Layout.ACC_HORIZONTAL | Layout.ACC_VERTICAL): void {
|
|
||||||
if (this._orientation !== Layout.TABBED) {
|
|
||||||
// Already accordion — just set the orientation
|
|
||||||
this._orientation = orientation;
|
|
||||||
this.drawWindows();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(`Container switching from TABBED to ${Layout[orientation]}`);
|
|
||||||
this._orientation = orientation;
|
|
||||||
|
|
||||||
// Destroy tab bar
|
|
||||||
if (this._tabBar) {
|
|
||||||
this._tabBar.destroy();
|
|
||||||
this._tabBar = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show all windows (they may have been hidden in tabbed mode)
|
|
||||||
this._showAllWindows();
|
|
||||||
|
|
||||||
this.drawWindows();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the active tab by index. Shows that window, hides others, updates tab bar.
|
|
||||||
*/
|
|
||||||
setActiveTab(index: number): void {
|
|
||||||
if (!this.isTabbed()) return;
|
|
||||||
if (index < 0 || index >= this._tiledItems.length) return;
|
|
||||||
|
|
||||||
this._activeTabIndex = index;
|
|
||||||
Logger.info(`Active tab set to ${index}`);
|
|
||||||
|
|
||||||
this._applyTabVisibility();
|
|
||||||
this._updateTabBar();
|
|
||||||
|
|
||||||
// Tile to resize the active window to the content area
|
|
||||||
this.drawWindows();
|
|
||||||
}
|
|
||||||
|
|
||||||
getActiveTabIndex(): number {
|
|
||||||
return this._activeTabIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the given window is a tab in this container, make it the active tab.
|
|
||||||
* Returns true if the window was found and activated.
|
|
||||||
*/
|
|
||||||
focusWindowTab(windowId: number): boolean {
|
|
||||||
if (!this.isTabbed()) return false;
|
|
||||||
|
|
||||||
const index = this._getIndexOfWindow(windowId);
|
|
||||||
if (index !== -1 && index !== this._activeTabIndex) {
|
|
||||||
this.setActiveTab(index);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return index !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideTabBar(): void {
|
|
||||||
this._tabBar?.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
showTabBar(): void {
|
|
||||||
if (this.isTabbed() && this._tabBar) {
|
|
||||||
this._tabBar.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a window to this container.
|
|
||||||
* If `index` is omitted the window is appended at the end.
|
|
||||||
* A negative index (e.g. -1) is treated as "append at end".
|
|
||||||
*/
|
|
||||||
addWindow(winWrap: WindowWrapper, index?: number): void {
|
|
||||||
const insertAt = (index === undefined || index < 0)
|
|
||||||
? this._tiledItems.length
|
|
||||||
: Math.min(index, this._tiledItems.length);
|
|
||||||
|
|
||||||
this._tiledItems.splice(insertAt, 0, winWrap);
|
|
||||||
this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap);
|
this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap);
|
||||||
this._addRatioForNewWindow(insertAt);
|
|
||||||
|
|
||||||
if (this.isTabbed()) {
|
|
||||||
// TODO: make it so that when tabs are added they are made the current active tab
|
|
||||||
this._applyTabVisibility();
|
|
||||||
this._updateTabBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
queueEvent({
|
queueEvent({
|
||||||
name: "tiling-windows",
|
name: "tiling-windows",
|
||||||
callback: () => this.drawWindows(),
|
callback: () => {
|
||||||
}, 100);
|
this.tileWindows();
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getWindow(win_id: number): WindowWrapper | undefined {
|
getWindow(win_id: number): WindowWrapper | undefined {
|
||||||
@@ -233,48 +56,34 @@ export default class WindowContainer {
|
|||||||
for (const item of this._tiledItems) {
|
for (const item of this._tiledItems) {
|
||||||
if (item instanceof WindowContainer) {
|
if (item instanceof WindowContainer) {
|
||||||
const win = item.getWindow(win_id);
|
const win = item.getWindow(win_id);
|
||||||
if (win) return win;
|
if (win) {
|
||||||
|
return win;
|
||||||
|
}
|
||||||
} else if (item.getWindowId() === win_id) {
|
} else if (item.getWindowId() === win_id) {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
_getIndexOfWindow(win_id: number): number {
|
_getIndexOfWindow(win_id: number) {
|
||||||
for (let i = 0; i < this._tiledItems.length; i++) {
|
for (let i = 0; i < this._tiledItems.length; i++) {
|
||||||
const item = this._tiledItems[i];
|
const item = this._tiledItems[i];
|
||||||
if (item instanceof WindowWrapper && item.getWindowId() === win_id) {
|
if (item instanceof WindowWrapper && item.getWindowId() === win_id) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
removeWindow(win_id: number): void {
|
removeWindow(win_id: number): void {
|
||||||
if (this._tiledWindowLookup.has(win_id)) {
|
if (this._tiledWindowLookup.has(win_id)) {
|
||||||
|
// Get index before deleting from lookup to avoid race condition
|
||||||
const index = this._getIndexOfWindow(win_id);
|
const index = this._getIndexOfWindow(win_id);
|
||||||
this._tiledWindowLookup.delete(win_id);
|
this._tiledWindowLookup.delete(win_id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
// If removing the window that was hidden in tabbed mode,
|
|
||||||
// make sure to show it first so it doesn't stay invisible
|
|
||||||
const item = this._tiledItems[index];
|
|
||||||
if (item instanceof WindowWrapper) {
|
|
||||||
item.showWindow();
|
|
||||||
}
|
|
||||||
this._tiledItems.splice(index, 1);
|
this._tiledItems.splice(index, 1);
|
||||||
}
|
}
|
||||||
this._resetRatios();
|
|
||||||
|
|
||||||
if (this.isTabbed()) {
|
|
||||||
if (this._tiledItems.length === 0) {
|
|
||||||
this._activeTabIndex = 0;
|
|
||||||
} else if (this._activeTabIndex >= this._tiledItems.length) {
|
|
||||||
this._activeTabIndex = this._tiledItems.length - 1;
|
|
||||||
}
|
|
||||||
this._applyTabVisibility();
|
|
||||||
this._updateTabBar();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
for (const item of this._tiledItems) {
|
for (const item of this._tiledItems) {
|
||||||
if (item instanceof WindowContainer) {
|
if (item instanceof WindowContainer) {
|
||||||
@@ -282,243 +91,94 @@ export default class WindowContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.drawWindows();
|
this.tileWindows()
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectSignals(): void {
|
disconnectSignals(): void {
|
||||||
this._tiledItems.forEach((item) => {
|
this._tiledItems.forEach((item) => {
|
||||||
if (item instanceof WindowContainer) {
|
if (item instanceof WindowContainer) {
|
||||||
item.disconnectSignals();
|
item.disconnectSignals()
|
||||||
} else {
|
} else {
|
||||||
item.disconnectWindowSignals();
|
item.disconnectWindowSignals();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllWindows(): void {
|
removeAllWindows(): void {
|
||||||
// tabbed mode hides all windows - this ensures they are available before removal
|
this._tiledItems = []
|
||||||
this._showAllWindows();
|
this._tiledWindowLookup.clear()
|
||||||
|
|
||||||
if (this._tabBar) {
|
|
||||||
this._tabBar.destroy();
|
|
||||||
this._tabBar = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._tiledItems = [];
|
tileWindows() {
|
||||||
this._tiledWindowLookup.clear();
|
Logger.log("TILING WINDOWS IN CONTAINER")
|
||||||
this._splitRatios = [];
|
|
||||||
this._activeTabIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
drawWindows(): void {
|
|
||||||
Logger.log("TILING WINDOWS IN CONTAINER");
|
|
||||||
Logger.log("WorkArea", this._workArea);
|
Logger.log("WorkArea", this._workArea);
|
||||||
|
|
||||||
if (this.isTabbed()) {
|
this._tileItems()
|
||||||
this._tileTab();
|
|
||||||
} else {
|
return true
|
||||||
this._tileAccordion();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_tileAccordion() {
|
_tileItems() {
|
||||||
if (this._tiledItems.length === 0) return;
|
if (this._tiledItems.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const bounds = this.getBounds();
|
const bounds = this.getBounds();
|
||||||
Logger.info(`_tileAccordion: ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}] bounds=[${bounds.map(b => `(${b.x},${b.y},${b.width},${b.height})`).join(', ')}]`);
|
|
||||||
this._tiledItems.forEach((item, index) => {
|
this._tiledItems.forEach((item, index) => {
|
||||||
const rect = bounds[index];
|
const rect = bounds[index];
|
||||||
if (item instanceof WindowContainer) {
|
if (item instanceof WindowContainer) {
|
||||||
item.move(rect);
|
item.move(rect);
|
||||||
} else {
|
} else {
|
||||||
Logger.info(`_tileAccordion: window[${index}] id=${item.getWindowId()} dragging=${item._dragging} → rect=(${rect.x},${rect.y},${rect.width},${rect.height})`);
|
|
||||||
item.safelyResizeWindow(rect);
|
item.safelyResizeWindow(rect);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _tileTab(): void {
|
|
||||||
if (this._tiledItems.length === 0) return;
|
|
||||||
|
|
||||||
const tabBarRect: Rect = {
|
|
||||||
x: this._workArea.x,
|
|
||||||
y: this._workArea.y,
|
|
||||||
width: this._workArea.width,
|
|
||||||
height: TAB_BAR_HEIGHT,
|
|
||||||
};
|
|
||||||
|
|
||||||
const contentRect: Rect = {
|
|
||||||
x: this._workArea.x,
|
|
||||||
y: this._workArea.y + TAB_BAR_HEIGHT,
|
|
||||||
width: this._workArea.width,
|
|
||||||
height: this._workArea.height - TAB_BAR_HEIGHT,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Position and show the tab bar
|
|
||||||
if (this._tabBar) {
|
|
||||||
this._tabBar.setPosition(tabBarRect);
|
|
||||||
if (!this._tabBar.isVisible()) {
|
|
||||||
this._rebuildAndShowTabBar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._applyTabVisibility();
|
|
||||||
|
|
||||||
const activeItem = this._tiledItems[this._activeTabIndex];
|
|
||||||
if (activeItem) {
|
|
||||||
if (activeItem instanceof WindowContainer) {
|
|
||||||
activeItem.move(contentRect);
|
|
||||||
} else {
|
|
||||||
Logger.info(`_tileTabbed: active tab[${this._activeTabIndex}] id=${activeItem.getWindowId()} → rect=(${contentRect.x},${contentRect.y},${contentRect.width},${contentRect.height})`);
|
|
||||||
activeItem.safelyResizeWindow(contentRect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the active tab window, hide all others.
|
|
||||||
*/
|
|
||||||
private _applyTabVisibility(): void {
|
|
||||||
this._tiledItems.forEach((item, index) => {
|
|
||||||
if (item instanceof WindowWrapper) {
|
|
||||||
if (index === this._activeTabIndex) {
|
|
||||||
item.showWindow();
|
|
||||||
} else {
|
|
||||||
item.hideWindow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show all windows (used when leaving tabbed mode).
|
|
||||||
*/
|
|
||||||
private _showAllWindows(): void {
|
|
||||||
this._tiledItems.forEach((item) => {
|
|
||||||
if (item instanceof WindowWrapper) {
|
|
||||||
item.showWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rebuild the tab bar buttons and show it.
|
|
||||||
*/
|
|
||||||
private _rebuildAndShowTabBar(): void {
|
|
||||||
if (!this._tabBar) return;
|
|
||||||
|
|
||||||
const windowItems = this._tiledItems.filter(
|
|
||||||
(item): item is WindowWrapper => item instanceof WindowWrapper
|
|
||||||
);
|
|
||||||
|
|
||||||
this._tabBar.rebuild(windowItems, this._activeTabIndex);
|
|
||||||
this._tabBar.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Public entry point to refresh tab titles (e.g. when a window title changes).
|
|
||||||
*/
|
|
||||||
refreshTabTitles(): void {
|
|
||||||
this._updateTabBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update tab bar state (active highlight, titles) without a full rebuild.
|
|
||||||
*/
|
|
||||||
private _updateTabBar(): void {
|
|
||||||
if (!this._tabBar) return;
|
|
||||||
|
|
||||||
// Rebuild is cheap — just recreate buttons from the current items
|
|
||||||
const windowItems = this._tiledItems.filter(
|
|
||||||
(item): item is WindowWrapper => item instanceof WindowWrapper
|
|
||||||
);
|
|
||||||
|
|
||||||
this._tabBar.rebuild(windowItems, this._activeTabIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
getBounds(): Rect[] {
|
getBounds(): Rect[] {
|
||||||
if (this._orientation === Layout.TABBED) {
|
if (this._orientation === Orientation.HORIZONTAL) {
|
||||||
// In tabbed mode, all items share the same content rect
|
return this.getHorizontalBounds();
|
||||||
const contentRect: Rect = {
|
}
|
||||||
|
return this.getVerticalBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
getVerticalBounds(): Rect[] {
|
||||||
|
const items = this._tiledItems
|
||||||
|
const containerHeight = Math.floor(this._workArea.height / items.length);
|
||||||
|
return items.map((_, index) => {
|
||||||
|
const y = this._workArea.y + (index * containerHeight);
|
||||||
|
return {
|
||||||
x: this._workArea.x,
|
x: this._workArea.x,
|
||||||
y: this._workArea.y + TAB_BAR_HEIGHT,
|
y: y,
|
||||||
width: this._workArea.width,
|
width: this._workArea.width,
|
||||||
height: this._workArea.height - TAB_BAR_HEIGHT,
|
height: containerHeight
|
||||||
};
|
} as Rect;
|
||||||
return this._tiledItems.map(() => contentRect);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._orientation === Layout.ACC_HORIZONTAL
|
|
||||||
? this._computeBounds('horizontal')
|
|
||||||
: this._computeBounds('vertical');
|
|
||||||
}
|
|
||||||
|
|
||||||
private _computeBounds(axis: 'horizontal' | 'vertical'): Rect[] {
|
|
||||||
const isHorizontal = axis === 'horizontal';
|
|
||||||
const total = isHorizontal ? this._workArea.width : this._workArea.height;
|
|
||||||
let used = 0;
|
|
||||||
|
|
||||||
return this._tiledItems.map((_, index) => {
|
|
||||||
const offset = used;
|
|
||||||
const size = index === this._tiledItems.length - 1
|
|
||||||
? total - used
|
|
||||||
: Math.floor(this._splitRatios[index] * total);
|
|
||||||
used += size;
|
|
||||||
|
|
||||||
return isHorizontal
|
|
||||||
? {x: this._workArea.x + offset, y: this._workArea.y, width: size, height: this._workArea.height}
|
|
||||||
: {x: this._workArea.x, y: this._workArea.y + offset, width: this._workArea.width, height: size};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
adjustBoundary(boundaryIndex: number, deltaPixels: number): boolean {
|
getHorizontalBounds(): Rect[] {
|
||||||
// No boundary adjustment in tabbed mode
|
const windowWidth = Math.floor(this._workArea.width / this._tiledItems.length);
|
||||||
if (this.isTabbed()) return false;
|
|
||||||
|
|
||||||
if (boundaryIndex < 0 || boundaryIndex >= this._tiledItems.length - 1) {
|
return this._tiledItems.map((_, index) => {
|
||||||
Logger.warn(`adjustBoundary: invalid boundaryIndex ${boundaryIndex}`);
|
const x = this._workArea.x + (index * windowWidth);
|
||||||
return false;
|
return {
|
||||||
}
|
x: x,
|
||||||
|
y: this._workArea.y,
|
||||||
const totalDim = this._totalDimension();
|
width: windowWidth,
|
||||||
if (totalDim === 0) return false;
|
height: this._workArea.height
|
||||||
|
} as Rect;
|
||||||
const ratioDelta = deltaPixels / totalDim;
|
});
|
||||||
const newLeft = this._splitRatios[boundaryIndex] + ratioDelta;
|
|
||||||
const newRight = this._splitRatios[boundaryIndex + 1] - ratioDelta;
|
|
||||||
|
|
||||||
if (newLeft <= 0 || newRight <= 0) {
|
|
||||||
Logger.log(`adjustBoundary: clamped — newLeft=${newLeft.toFixed(3)}, newRight=${newRight.toFixed(3)}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._splitRatios[boundaryIndex] = newLeft;
|
|
||||||
this._splitRatios[boundaryIndex + 1] = newRight;
|
|
||||||
|
|
||||||
Logger.info(`adjustBoundary: boundary=${boundaryIndex} ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Container Lookup --------------------------------------------------------
|
|
||||||
|
|
||||||
getContainerForWindow(win_id: number): WindowContainer | null {
|
|
||||||
for (const item of this._tiledItems) {
|
|
||||||
if (item instanceof WindowWrapper && item.getWindowId() === win_id) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (item instanceof WindowContainer) {
|
|
||||||
const found = item.getContainerForWindow(win_id);
|
|
||||||
if (found !== null) return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getIndexOfItemNested(item: WindowWrapper): number {
|
getIndexOfItemNested(item: WindowWrapper): number {
|
||||||
for (let i = 0; i < this._tiledItems.length; i++) {
|
for (let i = 0; i < this._tiledItems.length; i++) {
|
||||||
const container = this._tiledItems[i];
|
const container = this._tiledItems[i];
|
||||||
if (container instanceof WindowContainer) {
|
if (container instanceof WindowContainer) {
|
||||||
if (container.getIndexOfItemNested(item) !== -1) return i;
|
const index = container.getIndexOfItemNested(item);
|
||||||
|
if (index !== -1) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
} else if (container.getWindowId() === item.getWindowId()) {
|
} else if (container.getWindowId() === item.getWindowId()) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
@@ -528,177 +188,25 @@ export default class WindowContainer {
|
|||||||
|
|
||||||
// TODO: update this to work with nested containers - all other logic should already be working
|
// TODO: update this to work with nested containers - all other logic should already be working
|
||||||
itemDragged(item: WindowWrapper, x: number, y: number): void {
|
itemDragged(item: WindowWrapper, x: number, y: number): void {
|
||||||
// In tabbed mode, dragging reorders tabs but doesn't change layout
|
let original_index = this.getIndexOfItemNested(item);
|
||||||
if (this.isTabbed()) {
|
|
||||||
// Don't reorder during tabbed mode — tabs have a fixed visual layout
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const original_index = this.getIndexOfItemNested(item);
|
|
||||||
|
|
||||||
if (original_index === -1) {
|
if (original_index === -1) {
|
||||||
Logger.error("Item not found in container during drag op", item.getWindowId());
|
Logger.error("Item not found in container during drag op", item.getWindowId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let new_index = this.getIndexOfItemNested(item);
|
||||||
let new_index = original_index;
|
|
||||||
this.getBounds().forEach((rect, index) => {
|
this.getBounds().forEach((rect, index) => {
|
||||||
if (rect.x < x && rect.x + rect.width > x && rect.y < y && rect.y + rect.height > y) {
|
if (rect.x < x && rect.x + rect.width > x && rect.y < y && rect.y + rect.height > y) {
|
||||||
new_index = index;
|
new_index = index;
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
if (original_index !== new_index) {
|
if (original_index !== new_index) {
|
||||||
Logger.info(`itemDragged: swapped slots ${original_index}<->${new_index}, ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`);
|
this._tiledItems.splice(original_index, 1);
|
||||||
[this._tiledItems[original_index], this._tiledItems[new_index]] =
|
this._tiledItems.splice(new_index, 0, item);
|
||||||
[this._tiledItems[new_index], this._tiledItems[original_index]];
|
this.tileWindows()
|
||||||
this.drawWindows();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetRatios(): void {
|
|
||||||
this._resetRatios();
|
|
||||||
this.drawWindows();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Directional Move (swap) ------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swap the window at `windowId` with its neighbour in the given direction.
|
|
||||||
* Returns true if the swap occurred, false if the window is already at the edge
|
|
||||||
* or the direction is perpendicular to the container axis.
|
|
||||||
*/
|
|
||||||
swapWindowInDirection(windowId: number, direction: Direction): boolean {
|
|
||||||
const currentIndex = this._getIndexOfWindow(windowId);
|
|
||||||
if (currentIndex === -1) return false;
|
|
||||||
|
|
||||||
if (this.isTabbed()) {
|
|
||||||
// Tabbed: left/up = swap toward start, right/down = swap toward end
|
|
||||||
const delta = (direction === Direction.LEFT || direction === Direction.UP) ? -1 : 1;
|
|
||||||
const newIndex = currentIndex + delta;
|
|
||||||
if (newIndex < 0 || newIndex >= this._tiledItems.length) return false;
|
|
||||||
|
|
||||||
this._swapItems(currentIndex, newIndex);
|
|
||||||
this._activeTabIndex = newIndex;
|
|
||||||
this._updateTabBar();
|
|
||||||
this.drawWindows();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accordion mode — only swap along the container's axis
|
|
||||||
const isAlongAxis =
|
|
||||||
(this._orientation === Layout.ACC_HORIZONTAL && (direction === Direction.LEFT || direction === Direction.RIGHT)) ||
|
|
||||||
(this._orientation === Layout.ACC_VERTICAL && (direction === Direction.UP || direction === Direction.DOWN));
|
|
||||||
|
|
||||||
if (!isAlongAxis) return false;
|
|
||||||
|
|
||||||
const delta = (direction === Direction.LEFT || direction === Direction.UP) ? -1 : 1;
|
|
||||||
const newIndex = currentIndex + delta;
|
|
||||||
if (newIndex < 0 || newIndex >= this._tiledItems.length) return false;
|
|
||||||
|
|
||||||
this._swapItems(currentIndex, newIndex);
|
|
||||||
this.drawWindows();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swap two items in `_tiledItems` and their corresponding split ratios.
|
|
||||||
*/
|
|
||||||
private _swapItems(indexA: number, indexB: number): void {
|
|
||||||
[this._tiledItems[indexA], this._tiledItems[indexB]] =
|
|
||||||
[this._tiledItems[indexB], this._tiledItems[indexA]];
|
|
||||||
[this._splitRatios[indexA], this._splitRatios[indexB]] =
|
|
||||||
[this._splitRatios[indexB], this._splitRatios[indexA]];
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Directional Navigation ------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a window inside this container and a direction, return the window ID
|
|
||||||
* that should receive focus, or null if the edge of the container is reached.
|
|
||||||
*
|
|
||||||
* Behaviour by layout mode:
|
|
||||||
* - ACC_HORIZONTAL: left/right moves to the prev/next item; up/down → null
|
|
||||||
* - ACC_VERTICAL: up/down moves to the prev/next item; left/right → null
|
|
||||||
* - TABBED: left/right moves to the prev/next tab; up/down → null
|
|
||||||
*/
|
|
||||||
getAdjacentWindowId(windowId: number, direction: Direction): number | null {
|
|
||||||
const currentIndex = this._getIndexOfWindow(windowId);
|
|
||||||
if (currentIndex === -1) return null;
|
|
||||||
|
|
||||||
if (this.isTabbed()) {
|
|
||||||
// Tabbed: left/right cycle through tabs
|
|
||||||
if (direction === Direction.LEFT || direction === Direction.UP) {
|
|
||||||
const newIndex = currentIndex - 1;
|
|
||||||
if (newIndex < 0) return null;
|
|
||||||
return this._windowIdAtIndex(newIndex);
|
|
||||||
}
|
|
||||||
if (direction === Direction.RIGHT || direction === Direction.DOWN) {
|
|
||||||
const newIndex = currentIndex + 1;
|
|
||||||
if (newIndex >= this._tiledItems.length) return null;
|
|
||||||
return this._windowIdAtIndex(newIndex);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accordion mode – only navigate along the container's axis
|
|
||||||
const isAlongAxis =
|
|
||||||
(this._orientation === Layout.ACC_HORIZONTAL && (direction === Direction.LEFT || direction === Direction.RIGHT)) ||
|
|
||||||
(this._orientation === Layout.ACC_VERTICAL && (direction === Direction.UP || direction === Direction.DOWN));
|
|
||||||
|
|
||||||
if (!isAlongAxis) return null;
|
|
||||||
|
|
||||||
const delta = (direction === Direction.LEFT || direction === Direction.UP) ? -1 : 1;
|
|
||||||
const newIndex = currentIndex + delta;
|
|
||||||
if (newIndex < 0 || newIndex >= this._tiledItems.length) return null;
|
|
||||||
|
|
||||||
return this._windowIdAtIndex(newIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the "representative" window ID for the item at `index`.
|
|
||||||
* If the item is a WindowWrapper, return its ID directly.
|
|
||||||
* If it's a nested WindowContainer, return the first (or last) leaf window.
|
|
||||||
*/
|
|
||||||
private _windowIdAtIndex(index: number): number | null {
|
|
||||||
const item = this._tiledItems[index];
|
|
||||||
if (!item) return null;
|
|
||||||
|
|
||||||
if (item instanceof WindowWrapper) {
|
|
||||||
return item.getWindowId();
|
|
||||||
}
|
|
||||||
if (item instanceof WindowContainer) {
|
|
||||||
return item._firstLeafWindowId();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the window ID of the first leaf window in this container (depth-first).
|
|
||||||
*/
|
|
||||||
_firstLeafWindowId(): number | null {
|
|
||||||
for (const item of this._tiledItems) {
|
|
||||||
if (item instanceof WindowWrapper) return item.getWindowId();
|
|
||||||
if (item instanceof WindowContainer) {
|
|
||||||
const id = item._firstLeafWindowId();
|
|
||||||
if (id !== null) return id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the window ID of the last leaf window in this container (depth-first from end).
|
|
||||||
*/
|
|
||||||
_lastLeafWindowId(): number | null {
|
|
||||||
for (let i = this._tiledItems.length - 1; i >= 0; i--) {
|
|
||||||
const item = this._tiledItems[i];
|
|
||||||
if (item instanceof WindowWrapper) return item.getWindowId();
|
|
||||||
if (item instanceof WindowContainer) {
|
|
||||||
const id = item._lastLeafWindowId();
|
|
||||||
if (id !== null) return id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import {WindowWrapper} from "./window.js";
|
import {WindowWrapper} from "./window.js";
|
||||||
import {Rect} from "../utils/rect.js";
|
import {Rect} from "../utils/rect.js";
|
||||||
|
import queueEvent from "../utils/events.js";
|
||||||
import {Logger} from "../utils/logger.js";
|
import {Logger} from "../utils/logger.js";
|
||||||
|
import Meta from "gi://Meta";
|
||||||
|
import Mtk from "@girs/mtk-17";
|
||||||
|
|
||||||
import WindowContainer from "./container.js";
|
import WindowContainer from "./container.js";
|
||||||
|
import Window = Meta.Window;
|
||||||
|
|
||||||
export default class Monitor {
|
export default class Monitor {
|
||||||
|
|
||||||
@@ -16,7 +20,7 @@ export default class Monitor {
|
|||||||
this._workArea = workspace.get_work_area_for_monitor(this._id);
|
this._workArea = workspace.get_work_area_for_monitor(this._id);
|
||||||
Logger.log("CREATING MONITOR", monitorId);
|
Logger.log("CREATING MONITOR", monitorId);
|
||||||
Logger.log("WorkArea", this._workArea.x, this._workArea.y, this._workArea.width, this._workArea.height);
|
Logger.log("WorkArea", this._workArea.x, this._workArea.y, this._workArea.width, this._workArea.height);
|
||||||
const workspaceCount = global.workspace_manager.get_n_workspaces();
|
const workspaceCount = global.workspace_manager.get_n_workspaces()
|
||||||
Logger.log("Workspace Count", workspaceCount);
|
Logger.log("Workspace Count", workspaceCount);
|
||||||
for (let i = 0; i < workspaceCount; i++) {
|
for (let i = 0; i < workspaceCount; i++) {
|
||||||
this._workspaces.push(new WindowContainer(this._workArea));
|
this._workspaces.push(new WindowContainer(this._workArea));
|
||||||
@@ -38,7 +42,9 @@ export default class Monitor {
|
|||||||
getWindow(windowId: number): WindowWrapper | undefined {
|
getWindow(windowId: number): WindowWrapper | undefined {
|
||||||
for (const container of this._workspaces) {
|
for (const container of this._workspaces) {
|
||||||
const win = container.getWindow(windowId);
|
const win = container.getWindow(windowId);
|
||||||
if (win) return win;
|
if (win) {
|
||||||
|
return win;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -46,22 +52,23 @@ export default class Monitor {
|
|||||||
removeWindow(winWrap: WindowWrapper) {
|
removeWindow(winWrap: WindowWrapper) {
|
||||||
const windowId = winWrap.getWindowId();
|
const windowId = winWrap.getWindowId();
|
||||||
for (const container of this._workspaces) {
|
for (const container of this._workspaces) {
|
||||||
if (container.getWindow(windowId)) {
|
const win = container.getWindow(windowId);
|
||||||
|
if (win) {
|
||||||
container.removeWindow(windowId);
|
container.removeWindow(windowId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addWindow(winWrap: WindowWrapper, index?: number) {
|
addWindow(winWrap: WindowWrapper) {
|
||||||
const window_workspace = winWrap.getWindow().get_workspace().index();
|
const window_workspace = winWrap.getWindow().get_workspace().index();
|
||||||
this._workspaces[window_workspace].addWindow(winWrap, index);
|
this._workspaces[window_workspace].addWindow(winWrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
tileWindows(): void {
|
tileWindows(): void {
|
||||||
|
this._workArea = global.workspace_manager.get_active_workspace().get_work_area_for_monitor(this._id);
|
||||||
const activeWorkspace = global.workspace_manager.get_active_workspace();
|
const activeWorkspace = global.workspace_manager.get_active_workspace();
|
||||||
this._workArea = activeWorkspace.get_work_area_for_monitor(this._id);
|
|
||||||
// move() calls tileWindows() internally
|
|
||||||
this._workspaces[activeWorkspace.index()].move(this._workArea);
|
this._workspaces[activeWorkspace.index()].move(this._workArea);
|
||||||
|
this._workspaces[activeWorkspace.index()].tileWindows()
|
||||||
}
|
}
|
||||||
|
|
||||||
removeWorkspace(workspaceId: number): void {
|
removeWorkspace(workspaceId: number): void {
|
||||||
@@ -72,32 +79,8 @@ export default class Monitor {
|
|||||||
this._workspaces.push(new WindowContainer(this._workArea));
|
this._workspaces.push(new WindowContainer(this._workArea));
|
||||||
}
|
}
|
||||||
|
|
||||||
focusWindowTab(windowId: number): void {
|
|
||||||
for (const container of this._workspaces) {
|
|
||||||
if (container.focusWindowTab(windowId)) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshTabTitlesForWindow(winWrap: WindowWrapper): void {
|
|
||||||
const wsId = winWrap.getWorkspace();
|
|
||||||
if (wsId >= 0 && wsId < this._workspaces.length) {
|
|
||||||
this._workspaces[wsId].refreshTabTitles();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hideTabBars(): void {
|
|
||||||
for (const container of this._workspaces) {
|
|
||||||
container.hideTabBar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showTabBars(): void {
|
|
||||||
for (const container of this._workspaces) {
|
|
||||||
container.showTabBar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemDragged(item: WindowWrapper, x: number, y: number): void {
|
itemDragged(item: WindowWrapper, x: number, y: number): void {
|
||||||
this._workspaces[item.getWorkspace()].itemDragged(item, x, y);
|
this._workspaces[item.getWorkspace()].itemDragged(item, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
131
src/wm/tabBar.ts
131
src/wm/tabBar.ts
@@ -1,131 +0,0 @@
|
|||||||
import Clutter from 'gi://Clutter';
|
|
||||||
import Pango from 'gi://Pango';
|
|
||||||
import St from 'gi://St';
|
|
||||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
|
||||||
import {Logger} from "../utils/logger.js";
|
|
||||||
import {WindowWrapper} from "./window.js";
|
|
||||||
import {Rect} from "../utils/rect.js";
|
|
||||||
|
|
||||||
export const TAB_BAR_HEIGHT = 24;
|
|
||||||
|
|
||||||
type TabClickedCallback = (index: number) => void;
|
|
||||||
|
|
||||||
export class TabBar {
|
|
||||||
private _bar: St.BoxLayout;
|
|
||||||
private _buttons: St.Button[] = [];
|
|
||||||
private _activeIndex: number = 0;
|
|
||||||
private _onTabClicked: TabClickedCallback;
|
|
||||||
private _visible: boolean = false;
|
|
||||||
|
|
||||||
constructor(onTabClicked: TabClickedCallback) {
|
|
||||||
this._onTabClicked = onTabClicked;
|
|
||||||
this._bar = new St.BoxLayout({
|
|
||||||
style_class: 'aerospike-tab-bar',
|
|
||||||
vertical: false,
|
|
||||||
reactive: true,
|
|
||||||
can_focus: false,
|
|
||||||
track_hover: false,
|
|
||||||
});
|
|
||||||
// Force all tabs to equal width regardless of text length
|
|
||||||
(this._bar.layout_manager as Clutter.BoxLayout).homogeneous = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rebuild all tab buttons from the current list of window items.
|
|
||||||
*/
|
|
||||||
rebuild(items: WindowWrapper[], activeIndex: number): void {
|
|
||||||
// Remove old buttons
|
|
||||||
this._bar.destroy_all_children();
|
|
||||||
this._buttons = [];
|
|
||||||
|
|
||||||
items.forEach((item, index) => {
|
|
||||||
const label = new St.Label({
|
|
||||||
text: item.getTabLabel(),
|
|
||||||
style_class: 'aerospike-tab-label',
|
|
||||||
y_align: Clutter.ActorAlign.CENTER,
|
|
||||||
x_align: Clutter.ActorAlign.CENTER,
|
|
||||||
x_expand: true,
|
|
||||||
});
|
|
||||||
label.clutter_text.ellipsize = Pango.EllipsizeMode.END;
|
|
||||||
|
|
||||||
const button = new St.Button({
|
|
||||||
style_class: 'aerospike-tab',
|
|
||||||
reactive: true,
|
|
||||||
can_focus: false,
|
|
||||||
track_hover: true,
|
|
||||||
x_expand: true,
|
|
||||||
child: label,
|
|
||||||
});
|
|
||||||
|
|
||||||
button.connect('clicked', () => {
|
|
||||||
this._onTabClicked(index);
|
|
||||||
});
|
|
||||||
|
|
||||||
this._bar.add_child(button);
|
|
||||||
this._buttons.push(button);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setActive(activeIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update just the title text of a single tab (e.g. when a window title changes).
|
|
||||||
*/
|
|
||||||
updateTabTitle(index: number, title: string): void {
|
|
||||||
if (index < 0 || index >= this._buttons.length) return;
|
|
||||||
const label = this._buttons[index].get_child() as St.Label;
|
|
||||||
if (label) label.set_text(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Highlight the active tab and dim the rest.
|
|
||||||
*/
|
|
||||||
setActive(index: number): void {
|
|
||||||
this._activeIndex = index;
|
|
||||||
this._buttons.forEach((btn, i) => {
|
|
||||||
if (i === index) {
|
|
||||||
btn.add_style_class_name('aerospike-tab-active');
|
|
||||||
} else {
|
|
||||||
btn.remove_style_class_name('aerospike-tab-active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Position and size the tab bar at the given screen rect.
|
|
||||||
*/
|
|
||||||
setPosition(rect: Rect): void {
|
|
||||||
this._bar.set_position(rect.x, rect.y);
|
|
||||||
this._bar.set_size(rect.width, rect.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
show(): void {
|
|
||||||
if (this._visible) return;
|
|
||||||
this._visible = true;
|
|
||||||
Main.layoutManager.uiGroup.add_child(this._bar);
|
|
||||||
this._bar.show();
|
|
||||||
Logger.log("TabBar shown");
|
|
||||||
}
|
|
||||||
|
|
||||||
hide(): void {
|
|
||||||
if (!this._visible) return;
|
|
||||||
this._visible = false;
|
|
||||||
this._bar.hide();
|
|
||||||
if (this._bar.get_parent()) {
|
|
||||||
Main.layoutManager.uiGroup.remove_child(this._bar);
|
|
||||||
}
|
|
||||||
Logger.log("TabBar hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(): void {
|
|
||||||
this.hide();
|
|
||||||
this._bar.destroy_all_children();
|
|
||||||
this._buttons = [];
|
|
||||||
this._bar.destroy();
|
|
||||||
Logger.log("TabBar destroyed");
|
|
||||||
}
|
|
||||||
|
|
||||||
isVisible(): boolean {
|
|
||||||
return this._visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
124
src/wm/window.ts
124
src/wm/window.ts
@@ -11,8 +11,6 @@ type WindowMinimizedHandler = (window: WindowWrapper) => void;
|
|||||||
type WindowWorkspaceChangedHandler = (window: WindowWrapper) => void;
|
type WindowWorkspaceChangedHandler = (window: WindowWrapper) => void;
|
||||||
|
|
||||||
export class WindowWrapper {
|
export class WindowWrapper {
|
||||||
private static readonly RESIZE_TOLERANCE = 2;
|
|
||||||
|
|
||||||
readonly _window: Meta.Window;
|
readonly _window: Meta.Window;
|
||||||
readonly _windowMinimizedHandler: WindowMinimizedHandler;
|
readonly _windowMinimizedHandler: WindowMinimizedHandler;
|
||||||
readonly _signals: number[] = [];
|
readonly _signals: number[] = [];
|
||||||
@@ -47,54 +45,44 @@ export class WindowWrapper {
|
|||||||
return this._window.get_frame_rect();
|
return this._window.get_frame_rect();
|
||||||
}
|
}
|
||||||
|
|
||||||
getTabLabel(): string {
|
|
||||||
const rawAppName = this._window.get_wm_class() ?? '';
|
|
||||||
// Strip reverse-domain prefix (e.g. "org.gnome.Nautilus" -> "Nautilus")
|
|
||||||
const lastName = rawAppName.includes('.')
|
|
||||||
? (rawAppName.split('.').pop() ?? rawAppName)
|
|
||||||
: rawAppName;
|
|
||||||
// Capitalize first letter
|
|
||||||
const appName = lastName.charAt(0).toUpperCase() + lastName.slice(1);
|
|
||||||
const title = this._window.get_title() ?? 'Untitled';
|
|
||||||
if (appName && appName.toLowerCase() !== title.toLowerCase()) {
|
|
||||||
return `${appName} | ${title}`;
|
|
||||||
}
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideWindow(): void {
|
|
||||||
const actor = this._window.get_compositor_private() as Clutter.Actor | null;
|
|
||||||
if (actor) actor.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
showWindow(): void {
|
|
||||||
const actor = this._window.get_compositor_private() as Clutter.Actor | null;
|
|
||||||
if (actor) actor.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
startDragging(): void {
|
startDragging(): void {
|
||||||
this._dragging = true;
|
this._dragging = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
stopDragging(): void {
|
stopDragging(): void {
|
||||||
Logger.log("STOPPED DRAGGING")
|
Logger.log("STOPPED DRAGGING")
|
||||||
this._dragging = false;
|
this._dragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectWindowSignals(windowManager: IWindowManager): void {
|
// setParent(parent: WindowContainer): void {
|
||||||
const windowId = this._window.get_id();
|
// this._parent = parent;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// getParent(): WindowContainer | null {
|
||||||
|
// if (this._parent == null) {
|
||||||
|
// Logger.warn(`Attempting to get parent for window without parent ${JSON.stringify(this)}`);
|
||||||
|
// }
|
||||||
|
// return this._parent
|
||||||
|
// }
|
||||||
|
|
||||||
|
connectWindowSignals(
|
||||||
|
windowManager: IWindowManager,
|
||||||
|
): void {
|
||||||
|
const windowId = this._window.get_id()
|
||||||
|
// Handle window destruction
|
||||||
this._signals.push(
|
this._signals.push(
|
||||||
this._window.connect('unmanaging', () => {
|
this._window.connect('unmanaging', window => {
|
||||||
Logger.log("REMOVING WINDOW", windowId);
|
Logger.log("REMOVING WINDOW", windowId);
|
||||||
windowManager.handleWindowClosed(this);
|
windowManager.handleWindowClosed(this)
|
||||||
}),
|
}),
|
||||||
this._window.connect('notify::minimized', () => {
|
this._window.connect('notify::minimized', (we) => {
|
||||||
if (this._window.minimized) {
|
if (this._window.minimized) {
|
||||||
Logger.log(`Window minimized: ${windowId}`);
|
Logger.log(`Window minimized: ${windowId}`);
|
||||||
windowManager.handleWindowMinimized(this);
|
windowManager.handleWindowMinimized(this);
|
||||||
} else {
|
|
||||||
|
} else if (!this._window.minimized) {
|
||||||
Logger.log(`Window unminimized: ${windowId}`);
|
Logger.log(`Window unminimized: ${windowId}`);
|
||||||
windowManager.handleWindowUnminimized(this);
|
windowManager.handleWindowUnminimized(this);
|
||||||
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
this._window.connect('notify::maximized-horizontally', () => {
|
this._window.connect('notify::maximized-horizontally', () => {
|
||||||
@@ -104,23 +92,18 @@ export class WindowWrapper {
|
|||||||
Logger.log(`Window unmaximized: ${windowId}`);
|
Logger.log(`Window unmaximized: ${windowId}`);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
this._window.connect("workspace-changed", () => {
|
this._window.connect("workspace-changed", (_metaWindow) => {
|
||||||
Logger.log("WORKSPACE CHANGED FOR WINDOW", this._window.get_id());
|
Logger.log("WORKSPACE CHANGED FOR WINDOW", this._window.get_id());
|
||||||
windowManager.handleWindowChangedWorkspace(this);
|
windowManager.handleWindowChangedWorkspace(this);
|
||||||
}),
|
}),
|
||||||
this._window.connect("position-changed", () => {
|
this._window.connect("position-changed", (_metaWindow) => {
|
||||||
windowManager.handleWindowPositionChanged(this);
|
windowManager.handleWindowPositionChanged(this);
|
||||||
}),
|
}),
|
||||||
this._window.connect("size-changed", () => {
|
|
||||||
windowManager.handleWindowPositionChanged(this);
|
|
||||||
}),
|
|
||||||
this._window.connect('notify::title', () => {
|
|
||||||
windowManager.handleWindowTitleChanged(this);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectWindowSignals(): void {
|
disconnectWindowSignals(): void {
|
||||||
|
|
||||||
if (this._signals) {
|
if (this._signals) {
|
||||||
this._signals.forEach(signal => {
|
this._signals.forEach(signal => {
|
||||||
try {
|
try {
|
||||||
@@ -134,54 +117,37 @@ export class WindowWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
safelyResizeWindow(rect: Rect, _retry: number = 3): void {
|
safelyResizeWindow(rect: Rect, _retry: number = 2): void {
|
||||||
|
// Keep minimal logging
|
||||||
if (this._dragging) {
|
if (this._dragging) {
|
||||||
Logger.info("STOPPED RESIZE BECAUSE ITEM IS BEING DRAGGED");
|
Logger.info("STOPPED RESIZE BECAUSE ITEM IS BEING DRAGGED")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Logger.log("SAFELY RESIZE", rect.x, rect.y, rect.width, rect.height);
|
||||||
|
const actor = this._window.get_compositor_private();
|
||||||
|
|
||||||
const actor = this._window.get_compositor_private() as Clutter.Actor | null;
|
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
Logger.log("No actor available, can't resize safely yet");
|
Logger.log("No actor available, can't resize safely yet");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let windowActor = this._window.get_compositor_private() as Clutter.Actor;
|
||||||
actor.remove_all_transitions();
|
if (!windowActor) return;
|
||||||
|
windowActor.remove_all_transitions();
|
||||||
// Move first to guarantee the window reaches the correct position even
|
// Logger.info("MOVING")
|
||||||
// if the subsequent resize is clamped by minimum-size hints.
|
|
||||||
this._window.move_frame(true, rect.x, rect.y);
|
this._window.move_frame(true, rect.x, rect.y);
|
||||||
|
// Logger.info("RESIZING MOVING")
|
||||||
this._window.move_resize_frame(true, rect.x, rect.y, rect.width, rect.height);
|
this._window.move_resize_frame(true, rect.x, rect.y, rect.width, rect.height);
|
||||||
|
let new_rect = this._window.get_frame_rect();
|
||||||
const new_rect = this._window.get_frame_rect();
|
if ( _retry > 0 && (new_rect.x != rect.x || rect.y != new_rect.y || rect.width < new_rect.width || rect.height < new_rect.height)) {
|
||||||
const mismatch =
|
Logger.warn("RESIZING FAILED AS SMALLER", new_rect.x, new_rect.y, new_rect.width, new_rect.height, rect.x, rect.y, rect.width, rect.height);
|
||||||
Math.abs(new_rect.x - rect.x) > WindowWrapper.RESIZE_TOLERANCE ||
|
|
||||||
Math.abs(new_rect.y - rect.y) > WindowWrapper.RESIZE_TOLERANCE ||
|
|
||||||
Math.abs(new_rect.width - rect.width) > WindowWrapper.RESIZE_TOLERANCE ||
|
|
||||||
Math.abs(new_rect.height - rect.height) > WindowWrapper.RESIZE_TOLERANCE;
|
|
||||||
|
|
||||||
if (_retry > 0 && mismatch) {
|
|
||||||
// If the window's actual size is larger than requested, it has a
|
|
||||||
// minimum-size constraint — retrying won't help. Just make sure
|
|
||||||
// it's at the correct position with its actual size.
|
|
||||||
const sizeConstrained =
|
|
||||||
new_rect.width > rect.width + WindowWrapper.RESIZE_TOLERANCE ||
|
|
||||||
new_rect.height > rect.height + WindowWrapper.RESIZE_TOLERANCE;
|
|
||||||
|
|
||||||
if (sizeConstrained) {
|
|
||||||
Logger.info("Window has min-size constraint, accepting actual size",
|
|
||||||
`want(${rect.x},${rect.y},${rect.width},${rect.height})`,
|
|
||||||
`actual(${new_rect.x},${new_rect.y},${new_rect.width},${new_rect.height})`);
|
|
||||||
this._window.move_frame(true, rect.x, rect.y);
|
|
||||||
} else {
|
|
||||||
Logger.warn("RESIZE MISMATCH, retrying",
|
|
||||||
`want(${rect.x},${rect.y},${rect.width},${rect.height})`,
|
|
||||||
`got(${new_rect.x},${new_rect.y},${new_rect.width},${new_rect.height})`);
|
|
||||||
queueEvent({
|
queueEvent({
|
||||||
name: `delayed_resize_${this.getWindowId()}`,
|
name: "attempting_delayed_resize",
|
||||||
callback: () => this.safelyResizeWindow(rect, _retry - 1),
|
callback: () => {
|
||||||
}, 50);
|
this.safelyResizeWindow(rect, _retry-1);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import Meta from "gi://Meta";
|
import Meta from "gi://Meta";
|
||||||
import Gio from "gi://Gio";
|
// import Gio from "gi://Gio";
|
||||||
|
// import GLib from "gi://GLib";
|
||||||
|
|
||||||
import {WindowWrapper} from './window.js';
|
import {WindowWrapper} from './window.js';
|
||||||
import * as Main from "resource:///org/gnome/shell/ui/main.js";
|
import * as Main from "resource:///org/gnome/shell/ui/main.js";
|
||||||
|
// import Mtk from "@girs/mtk-16";
|
||||||
import {Logger} from "../utils/logger.js";
|
import {Logger} from "../utils/logger.js";
|
||||||
import Monitor from "./monitor.js";
|
import Monitor from "./monitor.js";
|
||||||
import WindowContainer, {Direction, Layout} from "./container.js";
|
import WindowContainer from "./container.js";
|
||||||
import {Rect} from "../utils/rect.js";
|
|
||||||
|
|
||||||
|
|
||||||
export interface IWindowManager {
|
export interface IWindowManager {
|
||||||
_activeWindowId: number | null;
|
_activeWindowId: number | null;
|
||||||
|
|
||||||
|
// addWindow(window: Meta.Window): void;
|
||||||
|
|
||||||
handleWindowClosed(winWrap: WindowWrapper): void;
|
handleWindowClosed(winWrap: WindowWrapper): void;
|
||||||
|
|
||||||
handleWindowMinimized(winWrap: WindowWrapper): void;
|
handleWindowMinimized(winWrap: WindowWrapper): void;
|
||||||
@@ -22,14 +25,12 @@ export interface IWindowManager {
|
|||||||
|
|
||||||
handleWindowPositionChanged(winWrap: WindowWrapper): void;
|
handleWindowPositionChanged(winWrap: WindowWrapper): void;
|
||||||
|
|
||||||
handleWindowTitleChanged(winWrap: WindowWrapper): void;
|
|
||||||
|
|
||||||
syncActiveWindow(): number | null;
|
syncActiveWindow(): number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const _UNUSED_MONITOR_ID = -1;
|
const _UNUSED_MONITOR_ID = -1
|
||||||
const _UNUSED_WINDOW_ID = -1;
|
const _UNUSED_WINDOW_ID = -1
|
||||||
|
|
||||||
export default class WindowManager implements IWindowManager {
|
export default class WindowManager implements IWindowManager {
|
||||||
_displaySignals: number[] = [];
|
_displaySignals: number[] = [];
|
||||||
@@ -39,29 +40,23 @@ export default class WindowManager implements IWindowManager {
|
|||||||
|
|
||||||
_activeWindowId: number | null = null;
|
_activeWindowId: number | null = null;
|
||||||
_monitors: Map<number, Monitor> = new Map<number, Monitor>();
|
_monitors: Map<number, Monitor> = new Map<number, Monitor>();
|
||||||
|
|
||||||
_minimizedItems: Map<number, WindowWrapper> = new Map<number, WindowWrapper>();
|
_minimizedItems: Map<number, WindowWrapper> = new Map<number, WindowWrapper>();
|
||||||
|
|
||||||
_grabbedWindowMonitor: number = _UNUSED_MONITOR_ID;
|
_grabbedWindowMonitor: number = _UNUSED_MONITOR_ID;
|
||||||
_grabbedWindowId: number = _UNUSED_WINDOW_ID;
|
_grabbedWindowId: number = _UNUSED_WINDOW_ID;
|
||||||
_changingGrabbedMonitor: boolean = false;
|
_changingGrabbedMonitor: boolean = false;
|
||||||
|
|
||||||
_showingOverview: boolean = false;
|
_showingOverview: boolean = false;
|
||||||
|
|
||||||
// -- Resize-drag tracking --------------------------------------------------
|
constructor() {
|
||||||
_isResizeDrag: boolean = false;
|
|
||||||
_resizeDragWindowId: number = _UNUSED_WINDOW_ID;
|
|
||||||
_resizeDragOp: Meta.GrabOp = Meta.GrabOp.NONE;
|
|
||||||
_resizeDragLastMouseX: number = 0;
|
|
||||||
_resizeDragLastMouseY: number = 0;
|
|
||||||
_isTiling: boolean = false;
|
|
||||||
|
|
||||||
private readonly _settings: Gio.Settings;
|
|
||||||
|
|
||||||
constructor(settings: Gio.Settings) {
|
|
||||||
this._settings = settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enable(): void {
|
public enable(): void {
|
||||||
Logger.log("Starting Aerospike Window Manager");
|
Logger.log("Starting Aerospike Window Manager");
|
||||||
|
// Connect window signals
|
||||||
this.instantiateDisplaySignals();
|
this.instantiateDisplaySignals();
|
||||||
|
|
||||||
const mon_count = global.display.get_n_monitors();
|
const mon_count = global.display.get_n_monitors();
|
||||||
@@ -70,6 +65,8 @@ export default class WindowManager implements IWindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.captureExistingWindows();
|
this.captureExistingWindows();
|
||||||
|
|
||||||
|
// Sync the initially focused window
|
||||||
this.syncActiveWindow();
|
this.syncActiveWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +93,7 @@ export default class WindowManager implements IWindowManager {
|
|||||||
global.display.connect('notify::focus-window', () => {
|
global.display.connect('notify::focus-window', () => {
|
||||||
this.syncActiveWindow();
|
this.syncActiveWindow();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
global.display.connect("showing-desktop-changed", () => {
|
global.display.connect("showing-desktop-changed", () => {
|
||||||
Logger.log("SHOWING DESKTOP CHANGED");
|
Logger.log("SHOWING DESKTOP CHANGED");
|
||||||
}),
|
}),
|
||||||
@@ -105,9 +103,14 @@ export default class WindowManager implements IWindowManager {
|
|||||||
}),
|
}),
|
||||||
global.display.connect("in-fullscreen-changed", () => {
|
global.display.connect("in-fullscreen-changed", () => {
|
||||||
Logger.log("IN FULL SCREEN CHANGED");
|
Logger.log("IN FULL SCREEN CHANGED");
|
||||||
this._syncFullscreenTabBars();
|
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
|
// this._windowManagerSignals = [
|
||||||
|
// global.window_manager.connect("show-tile-preview", (_, _metaWindow, _rect, _num) => {
|
||||||
|
// Logger.log("SHOW TITLE PREVIEW!")
|
||||||
|
// }),
|
||||||
|
// ];
|
||||||
|
|
||||||
this._workspaceManagerSignals = [
|
this._workspaceManagerSignals = [
|
||||||
global.workspace_manager.connect("showing-desktop-changed", () => {
|
global.workspace_manager.connect("showing-desktop-changed", () => {
|
||||||
@@ -132,37 +135,45 @@ export default class WindowManager implements IWindowManager {
|
|||||||
|
|
||||||
this._overviewSignals = [
|
this._overviewSignals = [
|
||||||
Main.overview.connect("hiding", () => {
|
Main.overview.connect("hiding", () => {
|
||||||
|
// this.fromOverview = true;
|
||||||
Logger.log("HIDING OVERVIEW")
|
Logger.log("HIDING OVERVIEW")
|
||||||
this._showingOverview = false;
|
this._showingOverview = false;
|
||||||
this._tileMonitors();
|
this._tileMonitors();
|
||||||
for (const monitor of this._monitors.values()) {
|
// const eventObj = {
|
||||||
monitor.showTabBars();
|
// name: "focus-after-overview",
|
||||||
}
|
// callback: () => {
|
||||||
|
// Logger.log("FOCUSING AFTER OVERVIEW");
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// this.queueEvent(eventObj);
|
||||||
}),
|
}),
|
||||||
Main.overview.connect("showing", () => {
|
Main.overview.connect("showing", () => {
|
||||||
this._showingOverview = true;
|
this._showingOverview = true;
|
||||||
Logger.log("SHOWING OVERVIEW");
|
Logger.log("SHOWING OVERVIEW");
|
||||||
for (const monitor of this._monitors.values()) {
|
|
||||||
monitor.hideTabBars();
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public disable(): void {
|
public disable(): void {
|
||||||
Logger.log("DISABLED AEROSPIKE WINDOW MANAGER!")
|
Logger.log("DISABLED AEROSPIKE WINDOW MANAGER!")
|
||||||
|
// Disconnect the focus signal and remove any existing borders
|
||||||
this.disconnectSignals();
|
this.disconnectSignals();
|
||||||
this.removeAllWindows();
|
this.removeAllWindows();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllWindows(): void {
|
removeAllWindows(): void {
|
||||||
|
// Disconnect signals from minimized windows before clearing
|
||||||
this.disconnectMinimizedSignals();
|
this.disconnectMinimizedSignals();
|
||||||
this._minimizedItems.clear();
|
this._minimizedItems.clear();
|
||||||
|
|
||||||
this._monitors.forEach((monitor: Monitor) => {
|
this._monitors.forEach((monitor: Monitor) => {
|
||||||
monitor.removeAllWindows();
|
monitor.removeAllWindows();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
disconnectSignals(): void {
|
disconnectSignals(): void {
|
||||||
this.disconnectDisplaySignals();
|
this.disconnectDisplaySignals();
|
||||||
this.disconnectMonitorSignals();
|
this.disconnectMonitorSignals();
|
||||||
@@ -196,61 +207,35 @@ export default class WindowManager implements IWindowManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_isResizeOp(op: Meta.GrabOp): boolean {
|
|
||||||
return op === Meta.GrabOp.RESIZING_E ||
|
|
||||||
op === Meta.GrabOp.RESIZING_W ||
|
|
||||||
op === Meta.GrabOp.RESIZING_N ||
|
|
||||||
op === Meta.GrabOp.RESIZING_S ||
|
|
||||||
op === Meta.GrabOp.RESIZING_NE ||
|
|
||||||
op === Meta.GrabOp.RESIZING_NW ||
|
|
||||||
op === Meta.GrabOp.RESIZING_SE ||
|
|
||||||
op === Meta.GrabOp.RESIZING_SW;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleGrabOpBegin(display: Meta.Display, window: Meta.Window, op: Meta.GrabOp): void {
|
handleGrabOpBegin(display: Meta.Display, window: Meta.Window, op: Meta.GrabOp): void {
|
||||||
Logger.log("Grab Op Start", op);
|
if (op === Meta.GrabOp.MOVING_UNCONSTRAINED){
|
||||||
|
|
||||||
if (this._isResizeOp(op)) {
|
}
|
||||||
Logger.log("Resize drag begin, op=", op);
|
Logger.log("Grab Op Start", op);
|
||||||
this._isResizeDrag = true;
|
Logger.log(display, window, op)
|
||||||
this._resizeDragWindowId = window.get_id();
|
Logger.log(window.get_monitor())
|
||||||
this._resizeDragOp = op;
|
|
||||||
const [startMouseX, startMouseY] = global.get_pointer();
|
|
||||||
this._resizeDragLastMouseX = startMouseX;
|
|
||||||
this._resizeDragLastMouseY = startMouseY;
|
|
||||||
this._getWrappedWindow(window)?.startDragging();
|
|
||||||
} else {
|
|
||||||
this._getWrappedWindow(window)?.startDragging();
|
this._getWrappedWindow(window)?.startDragging();
|
||||||
this._grabbedWindowMonitor = window.get_monitor();
|
this._grabbedWindowMonitor = window.get_monitor();
|
||||||
this._grabbedWindowId = window.get_id();
|
this._grabbedWindowId = window.get_id();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
handleGrabOpEnd(display: Meta.Display, window: Meta.Window, op: Meta.GrabOp): void {
|
handleGrabOpEnd(display: Meta.Display, window: Meta.Window, op: Meta.GrabOp): void {
|
||||||
Logger.log("Grab Op End ", op);
|
Logger.log("Grab Op End ", op);
|
||||||
|
Logger.log("primary display", display.get_primary_monitor())
|
||||||
if (this._isResizeDrag) {
|
|
||||||
Logger.log("Resize drag end, op=", op);
|
|
||||||
this._isResizeDrag = false;
|
|
||||||
this._resizeDragWindowId = _UNUSED_WINDOW_ID;
|
|
||||||
this._resizeDragLastMouseX = 0;
|
|
||||||
this._resizeDragLastMouseY = 0;
|
|
||||||
this._resizeDragOp = Meta.GrabOp.NONE;
|
|
||||||
this._getWrappedWindow(window)?.stopDragging();
|
|
||||||
this._tileMonitors();
|
|
||||||
} else {
|
|
||||||
this._grabbedWindowId = _UNUSED_WINDOW_ID;
|
this._grabbedWindowId = _UNUSED_WINDOW_ID;
|
||||||
this._getWrappedWindow(window)?.stopDragging();
|
this._getWrappedWindow(window)?.stopDragging();
|
||||||
this._tileMonitors();
|
this._tileMonitors();
|
||||||
Logger.info("monitor_start and monitor_end", this._grabbedWindowMonitor, window.get_monitor());
|
Logger.info("monitor_start and monitor_end", this._grabbedWindowMonitor, window.get_monitor());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_getWrappedWindow(window: Meta.Window): WindowWrapper | undefined {
|
_getWrappedWindow(window: Meta.Window): WindowWrapper | undefined {
|
||||||
let wrapped: WindowWrapper | undefined = undefined;
|
let wrapped: WindowWrapper | undefined = undefined;
|
||||||
for (const monitor of this._monitors.values()) {
|
for (const monitor of this._monitors.values()) {
|
||||||
wrapped = monitor.getWindow(window.get_id());
|
wrapped = monitor.getWindow(window.get_id());
|
||||||
if (wrapped !== undefined) break;
|
if (wrapped !== undefined) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return wrapped;
|
return wrapped;
|
||||||
}
|
}
|
||||||
@@ -280,13 +265,9 @@ export default class WindowManager implements IWindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public handleWindowPositionChanged(winWrap: WindowWrapper): void {
|
public handleWindowPositionChanged(winWrap: WindowWrapper): void {
|
||||||
if (this._isTiling || this._changingGrabbedMonitor) return;
|
if (this._changingGrabbedMonitor) {
|
||||||
|
|
||||||
if (this._isResizeDrag && winWrap.getWindowId() === this._resizeDragWindowId) {
|
|
||||||
this._handleResizeDragUpdate(winWrap);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (winWrap.getWindowId() === this._grabbedWindowId) {
|
if (winWrap.getWindowId() === this._grabbedWindowId) {
|
||||||
const [mouseX, mouseY, _] = global.get_pointer();
|
const [mouseX, mouseY, _] = global.get_pointer();
|
||||||
|
|
||||||
@@ -299,82 +280,19 @@ export default class WindowManager implements IWindowManager {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (monitorIndex === -1) return;
|
if (monitorIndex === -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (monitorIndex !== this._grabbedWindowMonitor) {
|
if (monitorIndex !== this._grabbedWindowMonitor) {
|
||||||
this._changingGrabbedMonitor = true;
|
this._changingGrabbedMonitor = true;
|
||||||
this._moveWindowToMonitor(winWrap.getWindow(), monitorIndex);
|
this._moveWindowToMonitor(winWrap.getWindow(), monitorIndex);
|
||||||
this._changingGrabbedMonitor = false;
|
this._changingGrabbedMonitor = false
|
||||||
}
|
}
|
||||||
|
|
||||||
this._isTiling = true;
|
|
||||||
try {
|
|
||||||
this._monitors.get(monitorIndex)?.itemDragged(winWrap, mouseX, mouseY);
|
this._monitors.get(monitorIndex)?.itemDragged(winWrap, mouseX, mouseY);
|
||||||
} finally {
|
|
||||||
this._isTiling = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleResizeDragUpdate(winWrap: WindowWrapper): void {
|
|
||||||
const op = this._resizeDragOp;
|
|
||||||
const winId = winWrap.getWindowId();
|
|
||||||
|
|
||||||
const [mouseX, mouseY] = global.get_pointer();
|
|
||||||
const dx = mouseX - this._resizeDragLastMouseX;
|
|
||||||
const dy = mouseY - this._resizeDragLastMouseY;
|
|
||||||
|
|
||||||
if (dx === 0 && dy === 0) return;
|
|
||||||
|
|
||||||
this._resizeDragLastMouseX = mouseX;
|
|
||||||
this._resizeDragLastMouseY = mouseY;
|
|
||||||
|
|
||||||
const container = this._findContainerForWindowAcrossMonitors(winId);
|
|
||||||
if (!container) {
|
|
||||||
Logger.warn("_handleResizeDragUpdate: no container found for window", winId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemIndex = container._getIndexOfWindow(winId);
|
|
||||||
if (itemIndex === -1) return;
|
|
||||||
|
|
||||||
const isHorizontal = container._orientation === Layout.ACC_HORIZONTAL;
|
|
||||||
|
|
||||||
// E/S edge → boundary after the item; W/N edge → boundary before it.
|
|
||||||
let adjusted = false;
|
|
||||||
if (isHorizontal) {
|
|
||||||
if (op === Meta.GrabOp.RESIZING_E || op === Meta.GrabOp.RESIZING_NE || op === Meta.GrabOp.RESIZING_SE) {
|
|
||||||
adjusted = container.adjustBoundary(itemIndex, dx);
|
|
||||||
} else if (op === Meta.GrabOp.RESIZING_W || op === Meta.GrabOp.RESIZING_NW || op === Meta.GrabOp.RESIZING_SW) {
|
|
||||||
adjusted = container.adjustBoundary(itemIndex - 1, dx);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (op === Meta.GrabOp.RESIZING_S || op === Meta.GrabOp.RESIZING_SE || op === Meta.GrabOp.RESIZING_SW) {
|
|
||||||
adjusted = container.adjustBoundary(itemIndex, dy);
|
|
||||||
} else if (op === Meta.GrabOp.RESIZING_N || op === Meta.GrabOp.RESIZING_NE || op === Meta.GrabOp.RESIZING_NW) {
|
|
||||||
adjusted = container.adjustBoundary(itemIndex - 1, dy);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (adjusted) {
|
|
||||||
this._isTiling = true;
|
|
||||||
try {
|
|
||||||
container.drawWindows();
|
|
||||||
} finally {
|
|
||||||
this._isTiling = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _findContainerForWindowAcrossMonitors(winId: number): WindowContainer | null {
|
|
||||||
const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index();
|
|
||||||
for (const monitor of this._monitors.values()) {
|
|
||||||
if (activeWorkspaceIndex >= monitor._workspaces.length) continue;
|
|
||||||
const container = monitor._workspaces[activeWorkspaceIndex].getContainerForWindow(winId);
|
|
||||||
if (container !== null) return container;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleWindowMinimized(winWrap: WindowWrapper): void {
|
public handleWindowMinimized(winWrap: WindowWrapper): void {
|
||||||
const monitor_id = winWrap.getWindow().get_monitor()
|
const monitor_id = winWrap.getWindow().get_monitor()
|
||||||
@@ -389,6 +307,7 @@ export default class WindowManager implements IWindowManager {
|
|||||||
this._tileMonitors()
|
this._tileMonitors()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public handleWindowChangedWorkspace(winWrap: WindowWrapper): void {
|
public handleWindowChangedWorkspace(winWrap: WindowWrapper): void {
|
||||||
const monitor = winWrap.getWindow().get_monitor();
|
const monitor = winWrap.getWindow().get_monitor();
|
||||||
this._monitors.get(monitor)?.removeWindow(winWrap);
|
this._monitors.get(monitor)?.removeWindow(winWrap);
|
||||||
@@ -407,31 +326,41 @@ export default class WindowManager implements IWindowManager {
|
|||||||
this._tileMonitors();
|
this._tileMonitors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleWindowCreated(display: Meta.Display, window: Meta.Window) {
|
handleWindowCreated(display: Meta.Display, window: Meta.Window) {
|
||||||
Logger.log("WINDOW CREATED ON DISPLAY", window, display);
|
Logger.log("WINDOW CREATED ON DISPLAY", window, display);
|
||||||
if (!this._isWindowTileable(window)) return;
|
if (!this._isWindowTileable(window)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Logger.log("WINDOW IS TILABLE");
|
Logger.log("WINDOW IS TILABLE");
|
||||||
this.addWindowToMonitor(window);
|
this.addWindowToMonitor(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle window closed event
|
||||||
|
*/
|
||||||
handleWindowClosed(window: WindowWrapper): void {
|
handleWindowClosed(window: WindowWrapper): void {
|
||||||
|
|
||||||
const mon_id = window._window.get_monitor();
|
const mon_id = window._window.get_monitor();
|
||||||
|
|
||||||
this._monitors.get(mon_id)?.removeWindow(window);
|
this._monitors.get(mon_id)?.removeWindow(window);
|
||||||
|
|
||||||
window.disconnectWindowSignals()
|
window.disconnectWindowSignals()
|
||||||
|
// Remove from managed windows
|
||||||
this.syncActiveWindow();
|
this.syncActiveWindow();
|
||||||
|
// Retile remaining windows
|
||||||
this._tileMonitors();
|
this._tileMonitors();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowTitleChanged(window: WindowWrapper): void {
|
|
||||||
const mon_id = window._window.get_monitor();
|
|
||||||
this._monitors.get(mon_id)?.refreshTabTitlesForWindow(window);
|
|
||||||
}
|
|
||||||
|
|
||||||
public addWindowToMonitor(window: Meta.Window) {
|
public addWindowToMonitor(window: Meta.Window) {
|
||||||
|
|
||||||
Logger.log("ADDING WINDOW TO MONITOR", window, window);
|
Logger.log("ADDING WINDOW TO MONITOR", window, window);
|
||||||
var wrapper = new WindowWrapper(window, (winWrap) => this.handleWindowMinimized(winWrap))
|
var wrapper = new WindowWrapper(window, (winWrap) => this.handleWindowMinimized(winWrap))
|
||||||
wrapper.connectWindowSignals(this);
|
wrapper.connectWindowSignals(this);
|
||||||
this._addWindowWrapperToMonitor(wrapper);
|
this._addWindowWrapperToMonitor(wrapper);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_addWindowWrapperToMonitor(winWrap: WindowWrapper) {
|
_addWindowWrapperToMonitor(winWrap: WindowWrapper) {
|
||||||
@@ -442,26 +371,10 @@ export default class WindowManager implements IWindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _syncFullscreenTabBars(): void {
|
|
||||||
for (const [monitorId, monitor] of this._monitors.entries()) {
|
|
||||||
if (global.display.get_monitor_in_fullscreen(monitorId)) {
|
|
||||||
monitor.hideTabBars();
|
|
||||||
} else if (!this._showingOverview) {
|
|
||||||
monitor.showTabBars();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_tileMonitors(): void {
|
_tileMonitors(): void {
|
||||||
this._isTiling = true;
|
|
||||||
try {
|
|
||||||
for (const monitor of this._monitors.values()) {
|
for (const monitor of this._monitors.values()) {
|
||||||
monitor.tileWindows();
|
monitor.tileWindows()
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Logger.error("_tileMonitors FAILED", e);
|
|
||||||
} finally {
|
|
||||||
this._isTiling = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +382,7 @@ export default class WindowManager implements IWindowManager {
|
|||||||
"org.gnome.Shell.Extensions",
|
"org.gnome.Shell.Extensions",
|
||||||
]
|
]
|
||||||
|
|
||||||
_isWindowTilingBlocked(window: Meta.Window): boolean {
|
_isWindowTilingBlocked(window: Meta.Window) : boolean {
|
||||||
Logger.info("title", window.get_title());
|
Logger.info("title", window.get_title());
|
||||||
Logger.info("description", window.get_description());
|
Logger.info("description", window.get_description());
|
||||||
Logger.info("class", window.get_wm_class());
|
Logger.info("class", window.get_wm_class());
|
||||||
@@ -484,12 +397,17 @@ export default class WindowManager implements IWindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isWindowTileable(window: Meta.Window) {
|
_isWindowTileable(window: Meta.Window) {
|
||||||
if (!window || !window.get_compositor_private()) return false;
|
|
||||||
if (this._isWindowTilingBlocked(window)) return false;
|
|
||||||
|
|
||||||
|
if (!window || !window.get_compositor_private()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this._isWindowTilingBlocked(window)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const windowType = window.get_window_type();
|
const windowType = window.get_window_type();
|
||||||
Logger.log("WINDOW TILING CHECK",);
|
Logger.log("WINDOW TILING CHECK",);
|
||||||
|
|
||||||
|
// Skip certain types of windows
|
||||||
return !window.is_skip_taskbar() &&
|
return !window.is_skip_taskbar() &&
|
||||||
windowType !== Meta.WindowType.DESKTOP &&
|
windowType !== Meta.WindowType.DESKTOP &&
|
||||||
windowType !== Meta.WindowType.DOCK &&
|
windowType !== Meta.WindowType.DOCK &&
|
||||||
@@ -499,15 +417,19 @@ export default class WindowManager implements IWindowManager {
|
|||||||
windowType !== Meta.WindowType.MENU;
|
windowType !== Meta.WindowType.MENU;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes the active window with GNOME's currently active window
|
||||||
|
*
|
||||||
|
* This function queries GNOME Shell for the current focused window and
|
||||||
|
* updates the extension's active window tracking to match.
|
||||||
|
*
|
||||||
|
* @returns The window ID of the active window, or null if no window is active
|
||||||
|
*/
|
||||||
public syncActiveWindow(): number | null {
|
public syncActiveWindow(): number | null {
|
||||||
const focusWindow = global.display.focus_window;
|
const focusWindow = global.display.focus_window;
|
||||||
if (focusWindow) {
|
if (focusWindow) {
|
||||||
this._activeWindowId = focusWindow.get_id();
|
this._activeWindowId = focusWindow.get_id();
|
||||||
Logger.debug(`Active window changed to: ${this._activeWindowId} (${focusWindow.get_title()})`);
|
Logger.debug(`Active window changed to: ${this._activeWindowId} (${focusWindow.get_title()})`);
|
||||||
|
|
||||||
// If the focused window is inside a tabbed container, make it the active tab
|
|
||||||
const monId = focusWindow.get_monitor();
|
|
||||||
this._monitors.get(monId)?.focusWindowTab(this._activeWindowId);
|
|
||||||
} else {
|
} else {
|
||||||
this._activeWindowId = null;
|
this._activeWindowId = null;
|
||||||
Logger.debug('No active window');
|
Logger.debug('No active window');
|
||||||
@@ -515,253 +437,83 @@ export default class WindowManager implements IWindowManager {
|
|||||||
return this._activeWindowId;
|
return this._activeWindowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the orientation of the active container (the container holding the active window)
|
||||||
|
*/
|
||||||
public toggleActiveContainerOrientation(): void {
|
public toggleActiveContainerOrientation(): void {
|
||||||
if (this._activeWindowId === null) {
|
if (this._activeWindowId === null) {
|
||||||
Logger.warn("No active window, cannot toggle container orientation");
|
Logger.warn("No active window, cannot toggle container orientation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
|
|
||||||
if (container) {
|
|
||||||
container.toggleOrientation();
|
|
||||||
} else {
|
|
||||||
Logger.warn("Could not find container for active window");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetActiveContainerRatios(): void {
|
// Find the active window's container
|
||||||
if (this._activeWindowId === null) {
|
const activeContainer = this._findActiveContainer();
|
||||||
Logger.warn("No active window, cannot reset container ratios");
|
if (activeContainer) {
|
||||||
return;
|
activeContainer.toggleOrientation();
|
||||||
}
|
|
||||||
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
|
|
||||||
if (container) {
|
|
||||||
Logger.info("Resetting container ratios to equal splits");
|
|
||||||
container.resetRatios();
|
|
||||||
} else {
|
|
||||||
Logger.warn("Could not find container for active window");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public toggleActiveContainerTabbed(): void {
|
|
||||||
if (this._activeWindowId === null) {
|
|
||||||
Logger.warn("No active window, cannot toggle tabbed mode");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
|
|
||||||
if (container) {
|
|
||||||
if (container.isTabbed()) {
|
|
||||||
container.setAccordion(Layout.ACC_HORIZONTAL);
|
|
||||||
} else {
|
|
||||||
// Set the active tab to the focused window
|
|
||||||
const activeIndex = container._getIndexOfWindow(this._activeWindowId);
|
|
||||||
if (activeIndex !== -1) {
|
|
||||||
container._activeTabIndex = activeIndex;
|
|
||||||
}
|
|
||||||
container.setTabbed();
|
|
||||||
}
|
|
||||||
this._tileMonitors();
|
|
||||||
} else {
|
} else {
|
||||||
Logger.warn("Could not find container for active window");
|
Logger.warn("Could not find container for active window");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the active window in the given direction.
|
* Finds the container that directly contains the active window
|
||||||
*
|
* @returns The container holding the active window, or null if not found
|
||||||
* 1. Find the container holding the active window.
|
|
||||||
* 2. Try to swap within the container (adjacent neighbour).
|
|
||||||
* 3. If already at the container edge, move the window to the
|
|
||||||
* nearest monitor in that direction instead.
|
|
||||||
* 4. Re-tile to apply the new layout.
|
|
||||||
*/
|
*/
|
||||||
public moveInDirection(direction: Direction): void {
|
private _findActiveContainer(): WindowContainer | null {
|
||||||
if (this._activeWindowId === null) {
|
if (this._activeWindowId === null) {
|
||||||
Logger.warn("No active window, cannot move in direction");
|
return null;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
|
|
||||||
if (!container) {
|
|
||||||
Logger.warn("Could not find container for active window");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const swapped = container.swapWindowInDirection(this._activeWindowId, direction);
|
|
||||||
if (swapped) {
|
|
||||||
Logger.info(`Moved window ${this._activeWindowId} ${direction}`);
|
|
||||||
this._tileMonitors();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._moveWindowCrossMonitor(this._activeWindowId, direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move focus to the adjacent window in the given direction.
|
|
||||||
*
|
|
||||||
* 1. Find the container holding the active window.
|
|
||||||
* 2. Ask the container for the adjacent window in that direction.
|
|
||||||
* 3. If the container returns null (at the edge), try cross-monitor navigation.
|
|
||||||
* 4. Activate (focus) the target window.
|
|
||||||
*/
|
|
||||||
public focusInDirection(direction: Direction): void {
|
|
||||||
if (this._activeWindowId === null) {
|
|
||||||
Logger.warn("No active window, cannot focus in direction");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
|
|
||||||
if (!container) {
|
|
||||||
Logger.warn("Could not find container for active window");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetId = container.getAdjacentWindowId(this._activeWindowId, direction);
|
|
||||||
if (targetId !== null) {
|
|
||||||
this._activateWindowById(targetId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// At the edge of the container — try cross-monitor navigation
|
|
||||||
const crossMonitorId = this._findCrossMonitorWindow(direction);
|
|
||||||
if (crossMonitorId !== null) {
|
|
||||||
this._activateWindowById(crossMonitorId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focus a window by its ID. Finds the Meta.Window and calls activate().
|
|
||||||
*/
|
|
||||||
private _activateWindowById(windowId: number): void {
|
|
||||||
for (const monitor of this._monitors.values()) {
|
for (const monitor of this._monitors.values()) {
|
||||||
const wrapped = monitor.getWindow(windowId);
|
const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index();
|
||||||
if (wrapped) {
|
|
||||||
const metaWindow = wrapped.getWindow();
|
// Bounds check to prevent accessing invalid workspace
|
||||||
metaWindow.activate(global.get_current_time());
|
if (activeWorkspaceIndex >= monitor._workspaces.length || activeWorkspaceIndex < 0) {
|
||||||
return;
|
Logger.warn(`Active workspace index ${activeWorkspaceIndex} out of bounds for monitor with ${monitor._workspaces.length} workspaces`);
|
||||||
}
|
continue;
|
||||||
}
|
|
||||||
Logger.warn(`_activateWindowById: window ${windowId} not found in any monitor`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const workspace = monitor._workspaces[activeWorkspaceIndex];
|
||||||
* Find the adjacent monitor in the given direction from a current monitor.
|
|
||||||
* Returns the monitor ID or null if none exists in that direction.
|
|
||||||
*/
|
|
||||||
private _findAdjacentMonitorId(currentMonitorId: number, direction: Direction): number | null {
|
|
||||||
const currentMonitor = this._monitors.get(currentMonitorId)!;
|
|
||||||
const currentArea = currentMonitor._workArea;
|
|
||||||
const currentCenterX = currentArea.x + currentArea.width / 2;
|
|
||||||
const currentCenterY = currentArea.y + currentArea.height / 2;
|
|
||||||
|
|
||||||
let bestMonitorId: number | null = null;
|
// Check if the window is directly in the workspace container
|
||||||
let bestDistance = Infinity;
|
const windowWrapper = workspace.getWindow(this._activeWindowId);
|
||||||
|
if (windowWrapper) {
|
||||||
for (const [monId, monitor] of this._monitors.entries()) {
|
// Try to find the parent container
|
||||||
if (monId === currentMonitorId) continue;
|
const container = this._findContainerHoldingWindow(workspace, this._activeWindowId);
|
||||||
|
return container;
|
||||||
const area = monitor._workArea;
|
|
||||||
const centerX = area.x + area.width / 2;
|
|
||||||
const centerY = area.y + area.height / 2;
|
|
||||||
|
|
||||||
let isInDirection = false;
|
|
||||||
let distance = Infinity;
|
|
||||||
|
|
||||||
switch (direction) {
|
|
||||||
case Direction.LEFT:
|
|
||||||
isInDirection = centerX < currentCenterX;
|
|
||||||
distance = currentCenterX - centerX;
|
|
||||||
break;
|
|
||||||
case Direction.RIGHT:
|
|
||||||
isInDirection = centerX > currentCenterX;
|
|
||||||
distance = centerX - currentCenterX;
|
|
||||||
break;
|
|
||||||
case Direction.UP:
|
|
||||||
isInDirection = centerY < currentCenterY;
|
|
||||||
distance = currentCenterY - centerY;
|
|
||||||
break;
|
|
||||||
case Direction.DOWN:
|
|
||||||
isInDirection = centerY > currentCenterY;
|
|
||||||
distance = centerY - currentCenterY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInDirection && distance < bestDistance) {
|
|
||||||
bestDistance = distance;
|
|
||||||
bestMonitorId = monId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bestMonitorId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the monitor ID that contains the given window, or null.
|
|
||||||
*/
|
|
||||||
private _findMonitorIdForWindow(windowId: number): number | null {
|
|
||||||
for (const [monId, monitor] of this._monitors.entries()) {
|
|
||||||
if (monitor.getWindow(windowId) !== undefined) return monId;
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When at the edge of a container, find the nearest window on the adjacent
|
* Recursively finds the container that directly contains a specific window
|
||||||
* monitor in the given direction.
|
* @param container The container to search
|
||||||
*
|
* @param windowId The window ID to find
|
||||||
* On the target monitor, picks the edge-most window:
|
* @returns The container that directly contains the window, or null if not found
|
||||||
* - Navigating LEFT/UP → last (far-edge) leaf window
|
|
||||||
* - Navigating RIGHT/DOWN → first (near-edge) leaf window
|
|
||||||
*/
|
*/
|
||||||
private _findCrossMonitorWindow(direction: Direction): number | null {
|
private _findContainerHoldingWindow(container: WindowContainer, windowId: number): WindowContainer | null {
|
||||||
if (this._activeWindowId === null) return null;
|
// Check if this container directly contains the window
|
||||||
|
for (const item of container._tiledItems) {
|
||||||
|
if (item instanceof WindowContainer) {
|
||||||
|
// Recursively search nested containers
|
||||||
|
const result = this._findContainerHoldingWindow(item, windowId);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} else if (item.getWindowId() === windowId) {
|
||||||
|
// Found it! Return this container as it directly holds the window
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const currentMonitorId = this._findMonitorIdForWindow(this._activeWindowId);
|
return null;
|
||||||
if (currentMonitorId === null) return null;
|
|
||||||
|
|
||||||
const targetMonitorId = this._findAdjacentMonitorId(currentMonitorId, direction);
|
|
||||||
if (targetMonitorId === null) return null;
|
|
||||||
|
|
||||||
const targetMonitor = this._monitors.get(targetMonitorId)!;
|
|
||||||
const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index();
|
|
||||||
if (activeWorkspaceIndex >= targetMonitor._workspaces.length) return null;
|
|
||||||
|
|
||||||
const targetContainer = targetMonitor._workspaces[activeWorkspaceIndex];
|
|
||||||
if (targetContainer._tiledItems.length === 0) return null;
|
|
||||||
|
|
||||||
return (direction === Direction.LEFT || direction === Direction.UP)
|
|
||||||
? targetContainer._lastLeafWindowId()
|
|
||||||
: targetContainer._firstLeafWindowId();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move a window to the adjacent monitor in the given direction.
|
* Prints the tree structure of all monitors, workspaces, containers, and windows to the logs
|
||||||
*
|
|
||||||
* The window is inserted at the "entry edge" of the target container:
|
|
||||||
* - Moving RIGHT/DOWN → position 0 (near edge)
|
|
||||||
* - Moving LEFT/UP → end of the container (far edge)
|
|
||||||
*/
|
*/
|
||||||
private _moveWindowCrossMonitor(windowId: number, direction: Direction): void {
|
|
||||||
const currentMonitorId = this._findMonitorIdForWindow(windowId);
|
|
||||||
if (currentMonitorId === null) return;
|
|
||||||
|
|
||||||
const targetMonitorId = this._findAdjacentMonitorId(currentMonitorId, direction);
|
|
||||||
if (targetMonitorId === null) return;
|
|
||||||
|
|
||||||
const currentMonitor = this._monitors.get(currentMonitorId)!;
|
|
||||||
const wrapped = currentMonitor.getWindow(windowId);
|
|
||||||
if (!wrapped) return;
|
|
||||||
|
|
||||||
const targetMonitor = this._monitors.get(targetMonitorId)!;
|
|
||||||
const insertIndex = (direction === Direction.RIGHT || direction === Direction.DOWN) ? 0 : undefined;
|
|
||||||
|
|
||||||
currentMonitor.removeWindow(wrapped);
|
|
||||||
targetMonitor.addWindow(wrapped, insertIndex);
|
|
||||||
|
|
||||||
this._tileMonitors();
|
|
||||||
Logger.info(`Moved window ${windowId} to monitor ${targetMonitorId} (${direction})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public printTreeStructure(): void {
|
public printTreeStructure(): void {
|
||||||
Logger.info("=".repeat(80));
|
Logger.info("=".repeat(80));
|
||||||
Logger.info("WINDOW TREE STRUCTURE");
|
Logger.info("WINDOW TREE STRUCTURE");
|
||||||
@@ -774,18 +526,19 @@ export default class WindowManager implements IWindowManager {
|
|||||||
this._monitors.forEach((monitor: Monitor, monitorId: number) => {
|
this._monitors.forEach((monitor: Monitor, monitorId: number) => {
|
||||||
const isActiveMonitor = this._activeWindowId !== null &&
|
const isActiveMonitor = this._activeWindowId !== null &&
|
||||||
monitor.getWindow(this._activeWindowId) !== undefined;
|
monitor.getWindow(this._activeWindowId) !== undefined;
|
||||||
|
const monitorMarker = isActiveMonitor ? ' *' : '';
|
||||||
|
|
||||||
Logger.info(`Monitor ${monitorId}${isActiveMonitor ? ' *' : ''}:`);
|
Logger.info(`Monitor ${monitorId}${monitorMarker}:`);
|
||||||
Logger.info(` Work Area: x=${monitor._workArea.x}, y=${monitor._workArea.y}, w=${monitor._workArea.width}, h=${monitor._workArea.height}`);
|
Logger.info(` Work Area: x=${monitor._workArea.x}, y=${monitor._workArea.y}, w=${monitor._workArea.width}, h=${monitor._workArea.height}`);
|
||||||
|
|
||||||
monitor._workspaces.forEach((workspace, workspaceIndex) => {
|
monitor._workspaces.forEach((workspace, workspaceIndex) => {
|
||||||
const isActiveWorkspace = workspaceIndex === activeWorkspaceIndex;
|
const isActiveWorkspace = workspaceIndex === activeWorkspaceIndex;
|
||||||
Logger.info(` Workspace ${workspaceIndex}${isActiveWorkspace && isActiveMonitor ? ' *' : ''}:`);
|
const workspaceMarker = isActiveWorkspace && isActiveMonitor ? ' *' : '';
|
||||||
Logger.info(` Orientation: ${Layout[workspace._orientation]}`);
|
|
||||||
|
Logger.info(` Workspace ${workspaceIndex}${workspaceMarker}:`);
|
||||||
|
Logger.info(` Orientation: ${workspace._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'}`);
|
||||||
Logger.info(` Items: ${workspace._tiledItems.length}`);
|
Logger.info(` Items: ${workspace._tiledItems.length}`);
|
||||||
if (workspace.isTabbed()) {
|
|
||||||
Logger.info(` Active Tab: ${workspace._activeTabIndex}`);
|
|
||||||
}
|
|
||||||
this._printContainerTree(workspace, 4);
|
this._printContainerTree(workspace, 4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -793,20 +546,31 @@ export default class WindowManager implements IWindowManager {
|
|||||||
Logger.info("=".repeat(80));
|
Logger.info("=".repeat(80));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _printContainerTree(container: WindowContainer, indentLevel: number): void {
|
/**
|
||||||
|
* Recursively prints the container tree structure
|
||||||
|
* @param container The container to print
|
||||||
|
* @param indentLevel The indentation level (number of spaces)
|
||||||
|
*/
|
||||||
|
private _printContainerTree(container: any, indentLevel: number): void {
|
||||||
const indent = " ".repeat(indentLevel);
|
const indent = " ".repeat(indentLevel);
|
||||||
|
|
||||||
container._tiledItems.forEach((item, index) => {
|
container._tiledItems.forEach((item: any, index: number) => {
|
||||||
if (item instanceof WindowContainer) {
|
if (item instanceof WindowContainer) {
|
||||||
const containsActive = this._activeWindowId !== null &&
|
// Check if this container contains the active window
|
||||||
|
const containsActiveWindow = this._activeWindowId !== null &&
|
||||||
item.getWindow(this._activeWindowId) !== undefined;
|
item.getWindow(this._activeWindowId) !== undefined;
|
||||||
Logger.info(`${indent}[${index}] Container (${Layout[item._orientation]})${containsActive ? ' *' : ''}:`);
|
const containerMarker = containsActiveWindow ? ' *' : '';
|
||||||
|
|
||||||
|
Logger.info(`${indent}[${index}] Container (${item._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'})${containerMarker}:`);
|
||||||
Logger.info(`${indent} Items: ${item._tiledItems.length}`);
|
Logger.info(`${indent} Items: ${item._tiledItems.length}`);
|
||||||
Logger.info(`${indent} Work Area: x=${item._workArea.x}, y=${item._workArea.y}, w=${item._workArea.width}, h=${item._workArea.height}`);
|
Logger.info(`${indent} Work Area: x=${item._workArea.x}, y=${item._workArea.y}, w=${item._workArea.width}, h=${item._workArea.height}`);
|
||||||
this._printContainerTree(item, indentLevel + 4);
|
this._printContainerTree(item, indentLevel + 4);
|
||||||
} else {
|
} else {
|
||||||
const window = item.getWindow();
|
const window = item.getWindow();
|
||||||
Logger.info(`${indent}[${index}] Window ID: ${item.getWindowId()}${this._activeWindowId === item.getWindowId() ? ' *' : ''}`);
|
const isActiveWindow = this._activeWindowId === item.getWindowId();
|
||||||
|
const windowMarker = isActiveWindow ? ' *' : '';
|
||||||
|
|
||||||
|
Logger.info(`${indent}[${index}] Window ID: ${item.getWindowId()}${windowMarker}`);
|
||||||
Logger.info(`${indent} Title: "${window.get_title()}"`);
|
Logger.info(`${indent} Title: "${window.get_title()}"`);
|
||||||
Logger.info(`${indent} Class: ${window.get_wm_class()}`);
|
Logger.info(`${indent} Class: ${window.get_wm_class()}`);
|
||||||
const rect = item.getRect();
|
const rect = item.getRect();
|
||||||
@@ -814,4 +578,6 @@ export default class WindowManager implements IWindowManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,37 @@
|
|||||||
.aerospike-tab-bar {
|
/* Add your custom extension styling here */
|
||||||
background-color: rgba(30, 30, 30, 0.95);
|
.active-window-border {
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
/*border: 2px solid rgba(191, 0, 255, 0.8);*/
|
||||||
spacing: 1px;
|
/*border-radius: 3px;*/
|
||||||
padding: 2px 2px 0 2px;
|
|
||||||
|
/* border-image-source: linear-gradient(to left, #743ad5, #d53a9d);*/
|
||||||
|
/* !*border: 4px solid transparent;*!*/
|
||||||
|
/* !*border-radius: 5px;*!*/
|
||||||
|
|
||||||
|
/* !*!* Gradient border using border-image *!*/
|
||||||
|
/* border-image: linear-gradient(45deg, red, orange, yellow, green, blue, indigo, violet) 1;*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.aerospike-tab {
|
/*.border-gradient-purple {*/
|
||||||
background-color: rgba(50, 50, 50, 0.8);
|
/* border-image-source: linear-gradient(to left, #743ad5, #d53a9d);*/
|
||||||
border-radius: 6px 6px 0 0;
|
/*}*/
|
||||||
padding: 2px 12px;
|
|
||||||
margin: 0 1px;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 400;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aerospike-tab:hover {
|
/*@keyframes rainbow-border {*/
|
||||||
background-color: rgba(70, 70, 70, 0.9);
|
/* 0% {*/
|
||||||
color: rgba(255, 255, 255, 0.8);
|
/* border-image: linear-gradient(0deg, red, orange, yellow, green, blue, indigo, violet, red) 1;*/
|
||||||
}
|
/* }*/
|
||||||
|
/* 100% {*/
|
||||||
|
/* border-image: linear-gradient(360deg, red, orange, yellow, green, blue, indigo, violet, red) 1;*/
|
||||||
|
/* }*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
.aerospike-tab-active {
|
/*.active-window-border {*/
|
||||||
background-color: rgba(80, 80, 80, 1);
|
/* border: 4px solid transparent;*/
|
||||||
color: rgba(255, 255, 255, 1);
|
/* border-radius: 5px;*/
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aerospike-tab-label {
|
/* !* Initial gradient border *!*/
|
||||||
font-size: 11px;
|
/* border-image: linear-gradient(0deg, red, orange, yellow, green, blue, indigo, violet, red) 1;*/
|
||||||
min-width: 0;
|
|
||||||
}
|
/* !* Apply animation *!*/
|
||||||
|
/* animation: rainbow-border 5s linear infinite;*/
|
||||||
|
/*}*/
|
||||||
Reference in New Issue
Block a user