Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d73ed80a68 | |||
| cbaa802797 | |||
| e2a1792388 | |||
| 656e448927 | |||
| 93516b31fb | |||
| 918c07c419 | |||
| 15188b9990 | |||
| 19696298d0 | |||
| 4be7602316 | |||
| 8f6e8582c9 |
+36
-108
@@ -15,15 +15,19 @@ export default class aerospike extends Extension {
|
||||
super(metadata);
|
||||
this.settings = this.getSettings('org.gnome.shell.extensions.aerospike');
|
||||
this.keyBindings = new Map();
|
||||
this.windowManager = new WindowManager();
|
||||
this.windowManager = new WindowManager(this.settings);
|
||||
}
|
||||
|
||||
enable() {
|
||||
Logger.log("STARTING AEROSPIKE!")
|
||||
this.windowManager._settings = this.settings;
|
||||
this.bindSettings();
|
||||
this.setupKeybindings();
|
||||
this.windowManager.enable()
|
||||
try {
|
||||
Logger.log("STARTING AEROSPIKE!")
|
||||
this.bindSettings();
|
||||
this.setupKeybindings();
|
||||
this.windowManager.enable()
|
||||
Logger.log("AEROSPIKE ENABLED SUCCESSFULLY")
|
||||
} catch (e) {
|
||||
Logger.error("AEROSPIKE ENABLE FAILED", e);
|
||||
}
|
||||
}
|
||||
|
||||
disable() {
|
||||
@@ -31,42 +35,26 @@ export default class aerospike extends Extension {
|
||||
this.removeKeybindings()
|
||||
}
|
||||
|
||||
private keybindingActions(): Record<string, () => void> {
|
||||
return {
|
||||
'move-left': () => { Logger.info('Keybinding 1 was pressed!'); },
|
||||
'move-right': () => { Logger.info('Keybinding 2 was pressed!'); },
|
||||
'join-with-left': () => { Logger.info('Keybinding 3 was pressed!'); },
|
||||
'join-with-right': () => { Logger.info('Keybinding 4 was pressed!'); },
|
||||
'print-tree': () => { this.windowManager.printTreeStructure(); },
|
||||
'toggle-orientation': () => { this.windowManager.toggleActiveContainerOrientation(); },
|
||||
'reset-ratios': () => { this.windowManager.resetActiveContainerRatios(); },
|
||||
'toggle-tabbed': () => { this.windowManager.toggleActiveContainerTabbed(); },
|
||||
};
|
||||
}
|
||||
|
||||
private bindSettings() {
|
||||
// Monitor settings changes
|
||||
this.settings.connect('changed::move-left', () => {
|
||||
log(`Keybinding 1 changed to: ${this.settings.get_strv('move-left')}`);
|
||||
this.refreshKeybinding('move-left');
|
||||
});
|
||||
|
||||
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::reset-ratios', () => {
|
||||
log(`Reset ratios keybinding changed to: ${this.settings.get_strv('reset-ratios')}`);
|
||||
this.refreshKeybinding('reset-ratios');
|
||||
const keybindings = Object.keys(this.keybindingActions());
|
||||
keybindings.forEach(name => {
|
||||
this.settings.connect(`changed::${name}`, () => {
|
||||
log(`${name} keybinding changed to: ${this.settings.get_strv(name)}`);
|
||||
this.refreshKeybinding(name);
|
||||
});
|
||||
});
|
||||
|
||||
this.settings.connect('changed::dropdown-option', () => {
|
||||
@@ -77,49 +65,15 @@ export default class aerospike extends Extension {
|
||||
log(`Color selection changed to: ${this.settings.get_string('color-selection')}`);
|
||||
});
|
||||
}
|
||||
|
||||
private refreshKeybinding(settingName: string) {
|
||||
if (this.keyBindings.has(settingName)) {
|
||||
Main.wm.removeKeybinding(settingName);
|
||||
this.keyBindings.delete(settingName);
|
||||
}
|
||||
|
||||
switch (settingName) {
|
||||
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;
|
||||
case 'reset-ratios':
|
||||
this.bindKeybinding('reset-ratios', () => {
|
||||
this.windowManager.resetActiveContainerRatios();
|
||||
});
|
||||
break;
|
||||
}
|
||||
const action = this.keybindingActions()[settingName];
|
||||
if (action) this.bindKeybinding(settingName, action);
|
||||
}
|
||||
|
||||
private removeKeybindings() {
|
||||
@@ -130,33 +84,10 @@ export default class aerospike extends Extension {
|
||||
}
|
||||
|
||||
private setupKeybindings() {
|
||||
this.bindKeybinding('move-left', () => {
|
||||
Logger.info('Keybinding 1 was pressed!');
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
this.bindKeybinding('reset-ratios', () => {
|
||||
this.windowManager.resetActiveContainerRatios();
|
||||
});
|
||||
const actions = this.keybindingActions();
|
||||
for (const [name, action] of Object.entries(actions)) {
|
||||
this.bindKeybinding(name, action);
|
||||
}
|
||||
}
|
||||
|
||||
private bindKeybinding(settingName: string, callback: () => void) {
|
||||
@@ -176,7 +107,4 @@ export default class aerospike extends Extension {
|
||||
|
||||
this.keyBindings.set(settingName, keyBindingAction);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
"@girs/gnome-shell": "49.1.0",
|
||||
"@jest/globals": "^30.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-plugin-jsdoc": "^62.0.0",
|
||||
"jest": "^30.0.0",
|
||||
"ts-jest": "^29.1.2",
|
||||
|
||||
Generated
+118
-169
@@ -43,11 +43,11 @@ importers:
|
||||
specifier: ^30.0.0
|
||||
version: 30.0.0
|
||||
eslint:
|
||||
specifier: ^9.36.0
|
||||
version: 9.39.2
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.2
|
||||
eslint-plugin-jsdoc:
|
||||
specifier: ^62.0.0
|
||||
version: 62.4.1(eslint@9.39.2)
|
||||
version: 62.4.1(eslint@10.0.2)
|
||||
jest:
|
||||
specifier: ^30.0.0
|
||||
version: 30.2.0(@types/node@25.1.0)
|
||||
@@ -252,33 +252,25 @@ packages:
|
||||
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||
|
||||
'@eslint/config-array@0.21.1':
|
||||
resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
'@eslint/config-array@0.23.2':
|
||||
resolution: {integrity: sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
'@eslint/config-helpers@0.4.2':
|
||||
resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
'@eslint/config-helpers@0.5.2':
|
||||
resolution: {integrity: sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
'@eslint/core@0.17.0':
|
||||
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
'@eslint/core@1.1.0':
|
||||
resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
'@eslint/eslintrc@3.3.3':
|
||||
resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
'@eslint/object-schema@3.0.2':
|
||||
resolution: {integrity: sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
'@eslint/js@9.39.2':
|
||||
resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
|
||||
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}
|
||||
'@eslint/plugin-kit@0.6.0':
|
||||
resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
'@girs/accountsservice-1.0@1.0.0-4.0.0-beta.38':
|
||||
resolution: {integrity: sha512-6QzytM5dztmMynF2bxN73EuNK9ArMFxkP2L8wUC7IH45zBeBOfYcqL85BFh2PmkGmqRk+Rli5EFR8dAkx3Ig5Q==}
|
||||
@@ -574,6 +566,9 @@ packages:
|
||||
'@types/babel__traverse@7.28.0':
|
||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||
|
||||
'@types/esrecurse@4.3.1':
|
||||
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
@@ -719,13 +714,13 @@ packages:
|
||||
peerDependencies:
|
||||
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
|
||||
acorn@8.15.0:
|
||||
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
ajv@6.14.0:
|
||||
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
||||
|
||||
ansi-escapes@4.3.2:
|
||||
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
|
||||
@@ -762,9 +757,6 @@ packages:
|
||||
argparse@1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
babel-jest@30.2.0:
|
||||
resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
@@ -793,6 +785,10 @@ packages:
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
balanced-match@4.0.4:
|
||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
baseline-browser-mapping@2.9.19:
|
||||
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
|
||||
hasBin: true
|
||||
@@ -803,6 +799,10 @@ packages:
|
||||
brace-expansion@2.0.2:
|
||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||
|
||||
brace-expansion@5.0.4:
|
||||
resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -949,25 +949,21 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
|
||||
|
||||
eslint-scope@8.4.0:
|
||||
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
eslint-scope@9.1.1:
|
||||
resolution: {integrity: sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
eslint-visitor-keys@3.4.3:
|
||||
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
|
||||
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:
|
||||
resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==}
|
||||
eslint-visitor-keys@5.0.1:
|
||||
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
eslint@9.39.2:
|
||||
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
eslint@10.0.2:
|
||||
resolution: {integrity: sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
jiti: '*'
|
||||
@@ -975,14 +971,14 @@ packages:
|
||||
jiti:
|
||||
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:
|
||||
resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
espree@11.1.1:
|
||||
resolution: {integrity: sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
esprima@4.0.1:
|
||||
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1048,8 +1044,8 @@ packages:
|
||||
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
flatted@3.3.3:
|
||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||
flatted@3.3.4:
|
||||
resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
@@ -1085,15 +1081,12 @@ packages:
|
||||
|
||||
glob@10.5.0:
|
||||
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
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
globals@14.0.0:
|
||||
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
||||
engines: {node: '>=18'}
|
||||
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
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
@@ -1121,10 +1114,6 @@ packages:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
import-local@3.2.0:
|
||||
resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1329,10 +1318,6 @@ packages:
|
||||
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
|
||||
hasBin: true
|
||||
|
||||
js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
jsdoc-type-pratt-parser@7.1.0:
|
||||
resolution: {integrity: sha512-SX7q7XyCwzM/MEDCYz0l8GgGbJAACGFII9+WfNYr5SLEKukHWRy2Jk3iWRe7P+lpYJNs7oQ+OSei4JtKGUjd7A==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
@@ -1384,9 +1369,6 @@ packages:
|
||||
lodash.memoize@4.1.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
@@ -1414,11 +1396,15 @@ packages:
|
||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
minimatch@10.2.4:
|
||||
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
minimatch@3.1.5:
|
||||
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
|
||||
|
||||
minimatch@9.0.9:
|
||||
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimist@1.2.8:
|
||||
@@ -1493,10 +1479,6 @@ packages:
|
||||
package-json-from-dist@1.0.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==}
|
||||
|
||||
@@ -1572,10 +1554,6 @@ packages:
|
||||
resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
resolve-from@5.0.0:
|
||||
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2031,50 +2009,34 @@ snapshots:
|
||||
|
||||
'@es-joy/resolve.exports@1.2.0': {}
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)':
|
||||
'@eslint-community/eslint-utils@4.9.1(eslint@10.0.2)':
|
||||
dependencies:
|
||||
eslint: 9.39.2
|
||||
eslint: 10.0.2
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.12.2': {}
|
||||
|
||||
'@eslint/config-array@0.21.1':
|
||||
'@eslint/config-array@0.23.2':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 2.1.7
|
||||
'@eslint/object-schema': 3.0.2
|
||||
debug: 4.4.3
|
||||
minimatch: 3.1.2
|
||||
minimatch: 10.2.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@eslint/config-helpers@0.4.2':
|
||||
'@eslint/config-helpers@0.5.2':
|
||||
dependencies:
|
||||
'@eslint/core': 0.17.0
|
||||
'@eslint/core': 1.1.0
|
||||
|
||||
'@eslint/core@0.17.0':
|
||||
'@eslint/core@1.1.0':
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
'@eslint/eslintrc@3.3.3':
|
||||
'@eslint/object-schema@3.0.2': {}
|
||||
|
||||
'@eslint/plugin-kit@0.6.0':
|
||||
dependencies:
|
||||
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
|
||||
'@eslint/core': 1.1.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@girs/accountsservice-1.0@1.0.0-4.0.0-beta.38':
|
||||
@@ -2808,6 +2770,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.28.6
|
||||
|
||||
'@types/esrecurse@4.3.1': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/istanbul-lib-coverage@2.0.6': {}
|
||||
@@ -2902,13 +2866,13 @@ snapshots:
|
||||
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
||||
optional: true
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||
acorn-jsx@5.3.2(acorn@8.16.0):
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
acorn: 8.16.0
|
||||
|
||||
acorn@8.15.0: {}
|
||||
acorn@8.16.0: {}
|
||||
|
||||
ajv@6.12.6:
|
||||
ajv@6.14.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
@@ -2942,8 +2906,6 @@ snapshots:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
babel-jest@30.2.0(@babel/core@7.28.6):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
@@ -2998,6 +2960,8 @@ snapshots:
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
balanced-match@4.0.4: {}
|
||||
|
||||
baseline-browser-mapping@2.9.19: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
@@ -3009,6 +2973,10 @@ snapshots:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
brace-expansion@5.0.4:
|
||||
dependencies:
|
||||
balanced-match: 4.0.4
|
||||
|
||||
braces@3.0.3:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
@@ -3110,7 +3078,7 @@ snapshots:
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
eslint-plugin-jsdoc@62.4.1(eslint@9.39.2):
|
||||
eslint-plugin-jsdoc@62.4.1(eslint@10.0.2):
|
||||
dependencies:
|
||||
'@es-joy/jsdoccomment': 0.83.0
|
||||
'@es-joy/resolve.exports': 1.2.0
|
||||
@@ -3118,7 +3086,7 @@ snapshots:
|
||||
comment-parser: 1.4.5
|
||||
debug: 4.4.3
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint: 9.39.2
|
||||
eslint: 10.0.2
|
||||
espree: 11.1.0
|
||||
esquery: 1.7.0
|
||||
html-entities: 2.6.0
|
||||
@@ -3130,39 +3098,36 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-scope@8.4.0:
|
||||
eslint-scope@9.1.1:
|
||||
dependencies:
|
||||
'@types/esrecurse': 4.3.1
|
||||
'@types/estree': 1.0.8
|
||||
esrecurse: 4.3.0
|
||||
estraverse: 5.3.0
|
||||
|
||||
eslint-visitor-keys@3.4.3: {}
|
||||
|
||||
eslint-visitor-keys@4.2.1: {}
|
||||
eslint-visitor-keys@5.0.1: {}
|
||||
|
||||
eslint-visitor-keys@5.0.0: {}
|
||||
|
||||
eslint@9.39.2:
|
||||
eslint@10.0.2:
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2)
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2)
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@eslint/config-array': 0.21.1
|
||||
'@eslint/config-helpers': 0.4.2
|
||||
'@eslint/core': 0.17.0
|
||||
'@eslint/eslintrc': 3.3.3
|
||||
'@eslint/js': 9.39.2
|
||||
'@eslint/plugin-kit': 0.4.1
|
||||
'@eslint/config-array': 0.23.2
|
||||
'@eslint/config-helpers': 0.5.2
|
||||
'@eslint/core': 1.1.0
|
||||
'@eslint/plugin-kit': 0.6.0
|
||||
'@humanfs/node': 0.16.7
|
||||
'@humanwhocodes/module-importer': 1.0.1
|
||||
'@humanwhocodes/retry': 0.4.3
|
||||
'@types/estree': 1.0.8
|
||||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
ajv: 6.14.0
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 8.4.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
espree: 10.4.0
|
||||
eslint-scope: 9.1.1
|
||||
eslint-visitor-keys: 5.0.1
|
||||
espree: 11.1.1
|
||||
esquery: 1.7.0
|
||||
esutils: 2.0.3
|
||||
fast-deep-equal: 3.1.3
|
||||
@@ -3173,24 +3138,23 @@ snapshots:
|
||||
imurmurhash: 0.1.4
|
||||
is-glob: 4.0.3
|
||||
json-stable-stringify-without-jsonify: 1.0.1
|
||||
lodash.merge: 4.6.2
|
||||
minimatch: 3.1.2
|
||||
minimatch: 10.2.4
|
||||
natural-compare: 1.4.0
|
||||
optionator: 0.9.4
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
acorn-jsx: 5.3.2(acorn@8.15.0)
|
||||
eslint-visitor-keys: 5.0.0
|
||||
acorn: 8.16.0
|
||||
acorn-jsx: 5.3.2(acorn@8.16.0)
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
espree@11.1.1:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
acorn-jsx: 5.3.2(acorn@8.16.0)
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
esprima@4.0.1: {}
|
||||
|
||||
@@ -3259,10 +3223,10 @@ snapshots:
|
||||
|
||||
flat-cache@4.0.1:
|
||||
dependencies:
|
||||
flatted: 3.3.3
|
||||
flatted: 3.3.4
|
||||
keyv: 4.5.4
|
||||
|
||||
flatted@3.3.3: {}
|
||||
flatted@3.3.4: {}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
@@ -3290,7 +3254,7 @@ snapshots:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minimatch: 9.0.9
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
@@ -3300,12 +3264,10 @@ snapshots:
|
||||
fs.realpath: 1.0.0
|
||||
inflight: 1.0.6
|
||||
inherits: 2.0.4
|
||||
minimatch: 3.1.2
|
||||
minimatch: 3.1.5
|
||||
once: 1.4.0
|
||||
path-is-absolute: 1.0.1
|
||||
|
||||
globals@14.0.0: {}
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
handlebars@4.7.8:
|
||||
@@ -3327,11 +3289,6 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
pkg-dir: 4.2.0
|
||||
@@ -3719,10 +3676,6 @@ snapshots:
|
||||
argparse: 1.0.10
|
||||
esprima: 4.0.1
|
||||
|
||||
js-yaml@4.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsdoc-type-pratt-parser@7.1.0: {}
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
@@ -3760,8 +3713,6 @@ snapshots:
|
||||
|
||||
lodash.memoize@4.1.2: {}
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
@@ -3787,11 +3738,15 @@ snapshots:
|
||||
|
||||
mimic-fn@2.1.0: {}
|
||||
|
||||
minimatch@3.1.2:
|
||||
minimatch@10.2.4:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.4
|
||||
|
||||
minimatch@3.1.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
|
||||
minimatch@9.0.5:
|
||||
minimatch@9.0.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
@@ -3856,10 +3811,6 @@ snapshots:
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
parse-imports-exports@0.2.4:
|
||||
dependencies:
|
||||
parse-statements: 1.0.11
|
||||
@@ -3918,8 +3869,6 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-from: 5.0.0
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
@@ -4007,7 +3956,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
glob: 7.2.3
|
||||
minimatch: 3.1.2
|
||||
minimatch: 3.1.5
|
||||
|
||||
tmpl@1.0.5: {}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</key>
|
||||
|
||||
<key name="toggle-orientation" type="as">
|
||||
<default><![CDATA[['<Super><Shift>comma']]]></default>
|
||||
<default><![CDATA[['<Primary>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>
|
||||
@@ -55,11 +55,10 @@
|
||||
<description>Resets all window size ratios in the active window's container to equal splits</description>
|
||||
</key>
|
||||
|
||||
<key name="min-window-size-percent" type="d">
|
||||
<default>0.10</default>
|
||||
<range min="0.01" max="0.49"/>
|
||||
<summary>Minimum window size percentage</summary>
|
||||
<description>Minimum fraction of a container that any single window may occupy when resizing boundaries</description>
|
||||
<key name="toggle-tabbed" type="as">
|
||||
<default><![CDATA[['<Primary>slash']]]></default>
|
||||
<summary>Toggle tabbed container mode</summary>
|
||||
<description>Toggles the active window's container between tabbed and accordion layout modes</description>
|
||||
</key>
|
||||
|
||||
</schema>
|
||||
|
||||
@@ -20,25 +20,38 @@ jest.mock('../utils/events.js', () => ({
|
||||
|
||||
describe('Container Logic Tests', () => {
|
||||
describe('Orientation Toggle Logic', () => {
|
||||
enum Orientation {
|
||||
enum Layout {
|
||||
HORIZONTAL = 0,
|
||||
VERTICAL = 1,
|
||||
TABBED = 2,
|
||||
}
|
||||
|
||||
const toggleOrientation = (current: Orientation): Orientation => {
|
||||
return current === Orientation.HORIZONTAL
|
||||
? Orientation.VERTICAL
|
||||
: Orientation.HORIZONTAL;
|
||||
const toggleOrientation = (current: Layout): Layout => {
|
||||
if (current === Layout.TABBED) return Layout.HORIZONTAL;
|
||||
return current === Layout.HORIZONTAL
|
||||
? Layout.VERTICAL
|
||||
: Layout.HORIZONTAL;
|
||||
};
|
||||
|
||||
test('should toggle from HORIZONTAL to VERTICAL', () => {
|
||||
const result = toggleOrientation(Orientation.HORIZONTAL);
|
||||
expect(result).toBe(Orientation.VERTICAL);
|
||||
const result = toggleOrientation(Layout.HORIZONTAL);
|
||||
expect(result).toBe(Layout.VERTICAL);
|
||||
});
|
||||
|
||||
test('should toggle from VERTICAL to HORIZONTAL', () => {
|
||||
const result = toggleOrientation(Orientation.VERTICAL);
|
||||
expect(result).toBe(Orientation.HORIZONTAL);
|
||||
const result = toggleOrientation(Layout.VERTICAL);
|
||||
expect(result).toBe(Layout.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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,6 +113,71 @@ 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', () => {
|
||||
test('should find window index in array', () => {
|
||||
const windows = [
|
||||
|
||||
+8
-34
@@ -182,40 +182,14 @@ export default class AerospikeExtensions extends ExtensionPreferences {
|
||||
})
|
||||
);
|
||||
|
||||
// Create sizing group
|
||||
const sizingGroup = new Adw.PreferencesGroup({
|
||||
title: _('Window Sizing'),
|
||||
});
|
||||
page.add(sizingGroup);
|
||||
|
||||
// Minimum window size percentage spinner
|
||||
const minSizeRow = new Adw.ActionRow({
|
||||
title: _('Minimum Window Size'),
|
||||
subtitle: _('Smallest fraction of a container any window may occupy when resizing (e.g. 0.10 = 10%)'),
|
||||
});
|
||||
sizingGroup.add(minSizeRow);
|
||||
|
||||
const minSizeSpin = new Gtk.SpinButton({
|
||||
adjustment: new Gtk.Adjustment({
|
||||
lower: 0.01,
|
||||
upper: 0.49,
|
||||
step_increment: 0.01,
|
||||
page_increment: 0.05,
|
||||
value: settings.get_double('min-window-size-percent'),
|
||||
}),
|
||||
digits: 2,
|
||||
valign: Gtk.Align.CENTER,
|
||||
});
|
||||
minSizeRow.add_suffix(minSizeSpin);
|
||||
minSizeRow.set_activatable_widget(minSizeSpin);
|
||||
|
||||
minSizeSpin.connect('value-changed', () => {
|
||||
settings.set_double('min-window-size-percent', minSizeSpin.get_value());
|
||||
});
|
||||
|
||||
settings.connect('changed::min-window-size-percent', () => {
|
||||
minSizeSpin.set_value(settings.get_double('min-window-size-percent'));
|
||||
});
|
||||
keybindingsGroup.add(
|
||||
new EntryRow({
|
||||
title: _('Toggle Tabbed Mode'),
|
||||
settings: settings,
|
||||
bind: 'toggle-tabbed',
|
||||
map: keybindingMap
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
import GLib from "gi://GLib";
|
||||
|
||||
|
||||
export type QueuedEvent = {
|
||||
name: string;
|
||||
callback: () => void;
|
||||
}
|
||||
|
||||
// Pending events indexed by name so that duplicate events collapse into one.
|
||||
// Only the most-recently-queued callback for a given name is kept.
|
||||
const pendingEvents: Map<string, QueuedEvent> = new Map();
|
||||
|
||||
export default function queueEvent(event: QueuedEvent, interval = 200) {
|
||||
// Overwrite any earlier pending event with the same name — the latest
|
||||
// callback is always the most up-to-date one.
|
||||
pendingEvents.set(event.name, event);
|
||||
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => {
|
||||
const e = pendingEvents.get(event.name);
|
||||
if (e && e === event) {
|
||||
// Only fire if this is still the current callback for this name
|
||||
// (a newer call may have replaced it).
|
||||
pendingEvents.delete(event.name);
|
||||
e.callback();
|
||||
}
|
||||
|
||||
+305
-163
@@ -1,45 +1,40 @@
|
||||
import {WindowWrapper} from "./window.js";
|
||||
import {Logger} from "../utils/logger.js";
|
||||
import Meta from "gi://Meta";
|
||||
import queueEvent from "../utils/events.js";
|
||||
import {Rect} from "../utils/rect.js";
|
||||
import {TabBar, TAB_BAR_HEIGHT} from "./tabBar.js";
|
||||
|
||||
enum Orientation {
|
||||
HORIZONTAL = 0,
|
||||
VERTICAL = 1,
|
||||
export enum Layout {
|
||||
ACC_HORIZONTAL = 0,
|
||||
ACC_VERTICAL = 1,
|
||||
TABBED = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a split-ratio array of length `n` where every element equals 1/n,
|
||||
* with the last slot absorbing any floating-point remainder so the array
|
||||
* always sums to exactly 1.0.
|
||||
*/
|
||||
// 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);
|
||||
// Fix floating-point drift: make last slot exact
|
||||
const sumExceptLast = ratios.slice(0, -1).reduce((a, b) => a + b, 0);
|
||||
ratios[n - 1] = 1 - sumExceptLast;
|
||||
return ratios;
|
||||
}
|
||||
|
||||
|
||||
export default class WindowContainer {
|
||||
|
||||
_tiledItems: (WindowWrapper | WindowContainer)[];
|
||||
_tiledWindowLookup: Map<number, WindowWrapper>;
|
||||
_orientation: Orientation = Orientation.HORIZONTAL;
|
||||
_orientation: Layout = Layout.ACC_HORIZONTAL;
|
||||
_workArea: Rect;
|
||||
|
||||
/**
|
||||
* Per-child split ratios. Always satisfies:
|
||||
* _splitRatios.length === _tiledItems.length
|
||||
* _splitRatios.reduce((a,b) => a+b, 0) === 1.0 (within floating-point epsilon)
|
||||
* every element >= MIN_RATIO
|
||||
*/
|
||||
// -- Accordion Mode States
|
||||
|
||||
_splitRatios: number[];
|
||||
|
||||
// -- Tabbed mode state -----------------------------------------------------
|
||||
_activeTabIndex: number = 0;
|
||||
_tabBar: TabBar | null = null;
|
||||
|
||||
constructor(workspaceArea: Rect) {
|
||||
this._tiledItems = [];
|
||||
this._tiledWindowLookup = new Map<number, WindowWrapper>();
|
||||
@@ -47,22 +42,12 @@ export default class WindowContainer {
|
||||
this._splitRatios = [];
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
// --- Helpers ----------------------------------------------------------------
|
||||
|
||||
/** Rebuild _splitRatios as equal fractions after any structural change. */
|
||||
private _resetRatios(): void {
|
||||
this._splitRatios = equalRatios(this._tiledItems.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a new item has been pushed onto _tiledItems.
|
||||
* The new window (last slot) gets 1/n of the space; existing windows
|
||||
* are scaled down proportionally so their ratios relative to each other
|
||||
* are preserved and the total remains 1.0.
|
||||
*
|
||||
* e.g. [0.33, 0.166, 0.5] + new → new=0.25, existing scaled by 0.75
|
||||
* → [0.2475, 0.1245, 0.375, 0.25]
|
||||
*/
|
||||
private _addRatioForNewWindow(): void {
|
||||
const n = this._tiledItems.length;
|
||||
if (n <= 1) {
|
||||
@@ -70,45 +55,136 @@ export default class WindowContainer {
|
||||
return;
|
||||
}
|
||||
const newRatio = 1 / n;
|
||||
const scale = 1 - newRatio; // existing windows share this fraction
|
||||
const scaled = this._splitRatios.map(r => r * scale);
|
||||
// Absorb all floating-point drift into the last slot so sum is exactly 1.0
|
||||
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);
|
||||
this._splitRatios = [...scaled, newRatio];
|
||||
}
|
||||
|
||||
/** Total dimension for the active orientation (width for H, height for V). */
|
||||
private _totalDimension(): number {
|
||||
return this._orientation === Orientation.HORIZONTAL
|
||||
return this._orientation === Layout.ACC_HORIZONTAL
|
||||
? this._workArea.width
|
||||
: this._workArea.height;
|
||||
}
|
||||
|
||||
// ─── Public API ─────────────────────────────────────────────────────────────
|
||||
isTabbed(): boolean {
|
||||
return this._orientation === Layout.TABBED;
|
||||
}
|
||||
|
||||
// --- Public API -------------------------------------------------------------
|
||||
|
||||
move(rect: Rect): void {
|
||||
this._workArea = rect;
|
||||
this.tileWindows();
|
||||
this.drawWindows();
|
||||
}
|
||||
|
||||
toggleOrientation(): void {
|
||||
this._orientation = this._orientation === Orientation.HORIZONTAL
|
||||
? Orientation.VERTICAL
|
||||
: Orientation.HORIZONTAL;
|
||||
Logger.info(`Container orientation toggled to ${this._orientation === Orientation.HORIZONTAL ? 'HORIZONTAL' : 'VERTICAL'}`);
|
||||
this.tileWindows();
|
||||
if (this._orientation === Layout.TABBED) {
|
||||
// Tabbed → Horizontal: restore accordion mode
|
||||
this.setAccordion(Layout.ACC_HORIZONTAL);
|
||||
} else {
|
||||
this._orientation = this._orientation === Layout.ACC_HORIZONTAL
|
||||
? Layout.ACC_VERTICAL
|
||||
: Layout.ACC_HORIZONTAL;
|
||||
Logger.info(`Container orientation toggled to ${Layout[this._orientation]}`);
|
||||
this.drawWindows();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch this container to tabbed mode.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
hideTabBar(): void {
|
||||
this._tabBar?.hide();
|
||||
}
|
||||
|
||||
showTabBar(): void {
|
||||
if (this.isTabbed() && this._tabBar) {
|
||||
this._tabBar.show();
|
||||
}
|
||||
}
|
||||
|
||||
addWindow(winWrap: WindowWrapper): void {
|
||||
this._tiledItems.push(winWrap);
|
||||
this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap);
|
||||
this._addRatioForNewWindow();
|
||||
|
||||
if (this.isTabbed()) {
|
||||
// TODO: make it so that when tabs are added they are made the current active tab
|
||||
this._applyTabVisibility();
|
||||
this._updateTabBar();
|
||||
}
|
||||
|
||||
queueEvent({
|
||||
name: "tiling-windows",
|
||||
callback: () => {
|
||||
this.tileWindows();
|
||||
}
|
||||
callback: () => this.drawWindows(),
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -119,9 +195,7 @@ export default class WindowContainer {
|
||||
for (const item of this._tiledItems) {
|
||||
if (item instanceof WindowContainer) {
|
||||
const win = item.getWindow(win_id);
|
||||
if (win) {
|
||||
return win;
|
||||
}
|
||||
if (win) return win;
|
||||
} else if (item.getWindowId() === win_id) {
|
||||
return item;
|
||||
}
|
||||
@@ -144,9 +218,25 @@ export default class WindowContainer {
|
||||
const index = this._getIndexOfWindow(win_id);
|
||||
this._tiledWindowLookup.delete(win_id);
|
||||
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._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 {
|
||||
for (const item of this._tiledItems) {
|
||||
if (item instanceof WindowContainer) {
|
||||
@@ -154,7 +244,7 @@ export default class WindowContainer {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.tileWindows();
|
||||
this.drawWindows();
|
||||
}
|
||||
|
||||
disconnectSignals(): void {
|
||||
@@ -168,104 +258,185 @@ export default class WindowContainer {
|
||||
}
|
||||
|
||||
removeAllWindows(): void {
|
||||
// tabbed mode hides all windows - this ensures they are available before removal
|
||||
this._showAllWindows();
|
||||
|
||||
if (this._tabBar) {
|
||||
this._tabBar.destroy();
|
||||
this._tabBar = null;
|
||||
}
|
||||
|
||||
this._tiledItems = [];
|
||||
this._tiledWindowLookup.clear();
|
||||
this._splitRatios = [];
|
||||
this._activeTabIndex = 0;
|
||||
}
|
||||
|
||||
tileWindows() {
|
||||
drawWindows(): void {
|
||||
Logger.log("TILING WINDOWS IN CONTAINER");
|
||||
Logger.log("WorkArea", this._workArea);
|
||||
this._tileItems();
|
||||
return true;
|
||||
|
||||
if (this.isTabbed()) {
|
||||
this._tileTab();
|
||||
} else {
|
||||
this._tileAccordion();
|
||||
}
|
||||
}
|
||||
|
||||
_tileItems() {
|
||||
if (this._tiledItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
_tileAccordion() {
|
||||
if (this._tiledItems.length === 0) return;
|
||||
|
||||
const bounds = this.getBounds();
|
||||
Logger.info(`_tileItems: ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}] bounds=[${bounds.map(b => `(${b.x},${b.y},${b.width},${b.height})`).join(', ')}]`);
|
||||
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) => {
|
||||
const rect = bounds[index];
|
||||
if (item instanceof WindowContainer) {
|
||||
item.move(rect);
|
||||
} else {
|
||||
Logger.info(`_tileItems: window[${index}] id=${item.getWindowId()} dragging=${item._dragging} → rect=(${rect.x},${rect.y},${rect.width},${rect.height})`);
|
||||
Logger.info(`_tileAccordion: window[${index}] id=${item.getWindowId()} dragging=${item._dragging} → rect=(${rect.x},${rect.y},${rect.width},${rect.height})`);
|
||||
item.safelyResizeWindow(rect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Bounds Calculation ──────────────────────────────────────────────────────
|
||||
private _tileTab(): void {
|
||||
if (this._tiledItems.length === 0) return;
|
||||
|
||||
getBounds(): Rect[] {
|
||||
if (this._orientation === Orientation.HORIZONTAL) {
|
||||
return this.getHorizontalBounds();
|
||||
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();
|
||||
}
|
||||
}
|
||||
return this.getVerticalBounds();
|
||||
}
|
||||
|
||||
getVerticalBounds(): Rect[] {
|
||||
const items = this._tiledItems;
|
||||
const totalHeight = this._workArea.height;
|
||||
let usedHeight = 0;
|
||||
this._applyTabVisibility();
|
||||
|
||||
return items.map((_, index) => {
|
||||
const y = this._workArea.y + usedHeight;
|
||||
let height: number;
|
||||
if (index === items.length - 1) {
|
||||
// Last item gets the remainder to avoid pixel gaps from rounding
|
||||
height = totalHeight - usedHeight;
|
||||
const activeItem = this._tiledItems[this._activeTabIndex];
|
||||
if (activeItem) {
|
||||
if (activeItem instanceof WindowContainer) {
|
||||
activeItem.move(contentRect);
|
||||
} else {
|
||||
height = Math.floor(this._splitRatios[index] * totalHeight);
|
||||
Logger.info(`_tileTabbed: active tab[${this._activeTabIndex}] id=${activeItem.getWindowId()} → rect=(${contentRect.x},${contentRect.y},${contentRect.width},${contentRect.height})`);
|
||||
activeItem.safelyResizeWindow(contentRect);
|
||||
}
|
||||
usedHeight += height;
|
||||
return {
|
||||
x: this._workArea.x,
|
||||
y: y,
|
||||
width: this._workArea.width,
|
||||
height: height,
|
||||
} as Rect;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getHorizontalBounds(): Rect[] {
|
||||
const totalWidth = this._workArea.width;
|
||||
let usedWidth = 0;
|
||||
|
||||
return this._tiledItems.map((_, index) => {
|
||||
const x = this._workArea.x + usedWidth;
|
||||
let width: number;
|
||||
if (index === this._tiledItems.length - 1) {
|
||||
// Last item gets the remainder to avoid pixel gaps from rounding
|
||||
width = totalWidth - usedWidth;
|
||||
} else {
|
||||
width = Math.floor(this._splitRatios[index] * totalWidth);
|
||||
}
|
||||
usedWidth += width;
|
||||
return {
|
||||
x: x,
|
||||
y: this._workArea.y,
|
||||
width: width,
|
||||
height: this._workArea.height,
|
||||
} as Rect;
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Boundary / Ratio Adjustment ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Adjust the boundary between item[boundaryIndex] and item[boundaryIndex+1]
|
||||
* by deltaPixels (positive = move right/down, negative = move left/up).
|
||||
*
|
||||
* Both affected ratios are clamped to [_minRatio, 1 - _minRatio] so no
|
||||
* window can be squashed below the configured minimum.
|
||||
*
|
||||
* Returns true if the adjustment was applied, false if it was rejected
|
||||
* (e.g. out of bounds index or clamping would violate minimum).
|
||||
* Show the active tab window, hide all others.
|
||||
*/
|
||||
adjustBoundary(boundaryIndex: number, deltaPixels: number, minRatio: number = 0.10): boolean {
|
||||
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[] {
|
||||
if (this._orientation === Layout.TABBED) {
|
||||
// In tabbed mode, all items share the same content rect
|
||||
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,
|
||||
};
|
||||
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 {
|
||||
// No boundary adjustment in tabbed mode
|
||||
if (this.isTabbed()) return false;
|
||||
|
||||
if (boundaryIndex < 0 || boundaryIndex >= this._tiledItems.length - 1) {
|
||||
Logger.warn(`adjustBoundary: invalid boundaryIndex ${boundaryIndex}`);
|
||||
return false;
|
||||
@@ -275,54 +446,28 @@ export default class WindowContainer {
|
||||
if (totalDim === 0) return false;
|
||||
|
||||
const ratioDelta = deltaPixels / totalDim;
|
||||
|
||||
const newLeft = this._splitRatios[boundaryIndex] + ratioDelta;
|
||||
const newLeft = this._splitRatios[boundaryIndex] + ratioDelta;
|
||||
const newRight = this._splitRatios[boundaryIndex + 1] - ratioDelta;
|
||||
|
||||
if (newLeft < minRatio || newRight < minRatio) {
|
||||
Logger.log(`adjustBoundary: clamped — newLeft=${newLeft.toFixed(3)}, newRight=${newRight.toFixed(3)}, min=${minRatio}`);
|
||||
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] = newLeft;
|
||||
this._splitRatios[boundaryIndex + 1] = newRight;
|
||||
|
||||
Logger.info(`adjustBoundary: boundary=${boundaryIndex} ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust boundaries on BOTH axes simultaneously for corner resize ops.
|
||||
* horizontalDelta applies to this container if HORIZONTAL, verticalDelta if VERTICAL.
|
||||
* For nested containers the perpendicular delta is forwarded to the child container.
|
||||
*
|
||||
* boundaryIndex: the slot index whose right/bottom edge is being dragged.
|
||||
*/
|
||||
adjustBoundaryBothAxes(
|
||||
boundaryIndex: number,
|
||||
horizontalDelta: number,
|
||||
verticalDelta: number,
|
||||
): void {
|
||||
if (this._orientation === Orientation.HORIZONTAL) {
|
||||
this.adjustBoundary(boundaryIndex, horizontalDelta);
|
||||
} else {
|
||||
this.adjustBoundary(boundaryIndex, verticalDelta);
|
||||
}
|
||||
}
|
||||
// --- Container Lookup --------------------------------------------------------
|
||||
|
||||
// ─── Container Lookup ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns the direct-parent WindowContainer that contains win_id as an
|
||||
* immediate child (not recursed further). Returns null if not found.
|
||||
*/
|
||||
getContainerForWindow(win_id: number): WindowContainer | null {
|
||||
for (const item of this._tiledItems) {
|
||||
if (item instanceof WindowWrapper && item.getWindowId() === win_id) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
for (const item of this._tiledItems) {
|
||||
if (item instanceof WindowContainer) {
|
||||
const found = item.getContainerForWindow(win_id);
|
||||
if (found !== null) return found;
|
||||
@@ -335,10 +480,7 @@ export default class WindowContainer {
|
||||
for (let i = 0; i < this._tiledItems.length; i++) {
|
||||
const container = this._tiledItems[i];
|
||||
if (container instanceof WindowContainer) {
|
||||
const index = container.getIndexOfItemNested(item);
|
||||
if (index !== -1) {
|
||||
return i;
|
||||
}
|
||||
if (container.getIndexOfItemNested(item) !== -1) return i;
|
||||
} else if (container.getWindowId() === item.getWindowId()) {
|
||||
return i;
|
||||
}
|
||||
@@ -348,36 +490,36 @@ export default class WindowContainer {
|
||||
|
||||
// TODO: update this to work with nested containers - all other logic should already be working
|
||||
itemDragged(item: WindowWrapper, x: number, y: number): void {
|
||||
let original_index = this.getIndexOfItemNested(item);
|
||||
// In tabbed mode, dragging reorders tabs but doesn't change layout
|
||||
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) {
|
||||
Logger.error("Item not found in container during drag op", item.getWindowId());
|
||||
return;
|
||||
}
|
||||
|
||||
let new_index = original_index;
|
||||
this.getBounds().forEach((rect, index) => {
|
||||
if (rect.x < x && rect.x + rect.width > x && rect.y < y && rect.y + rect.height > y) {
|
||||
new_index = index;
|
||||
}
|
||||
});
|
||||
|
||||
if (original_index !== new_index) {
|
||||
// Swap only the items — ratios stay with their slots.
|
||||
// e.g. slot 0 = 40%, slot 1 = 60%: when the window in slot 1 drags
|
||||
// into slot 0, it takes slot 0's 40% size. The window it displaces
|
||||
// moves to slot 1 and takes the 60% size. The slot ratios are unchanged.
|
||||
Logger.info(`itemDragged: swapped slots ${original_index}<->${new_index}, ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`);
|
||||
[this._tiledItems[original_index], this._tiledItems[new_index]] =
|
||||
[this._tiledItems[new_index], this._tiledItems[original_index]];
|
||||
Logger.info(`itemDragged: swapped slots ${original_index}<->${new_index}, ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`);
|
||||
this.tileWindows();
|
||||
this.drawWindows();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all split ratios in this container to equal fractions.
|
||||
* Called when the user explicitly requests an equal-split reset (e.g. Ctrl+Z).
|
||||
*/
|
||||
resetRatios(): void {
|
||||
this._resetRatios();
|
||||
this.tileWindows();
|
||||
this.drawWindows();
|
||||
}
|
||||
}
|
||||
|
||||
+25
-14
@@ -1,12 +1,8 @@
|
||||
import {WindowWrapper} from "./window.js";
|
||||
import {Rect} from "../utils/rect.js";
|
||||
import queueEvent from "../utils/events.js";
|
||||
import {Logger} from "../utils/logger.js";
|
||||
import Meta from "gi://Meta";
|
||||
import Mtk from "@girs/mtk-17";
|
||||
|
||||
import WindowContainer from "./container.js";
|
||||
import Window = Meta.Window;
|
||||
|
||||
export default class Monitor {
|
||||
|
||||
@@ -20,7 +16,7 @@ export default class Monitor {
|
||||
this._workArea = workspace.get_work_area_for_monitor(this._id);
|
||||
Logger.log("CREATING MONITOR", monitorId);
|
||||
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);
|
||||
for (let i = 0; i < workspaceCount; i++) {
|
||||
this._workspaces.push(new WindowContainer(this._workArea));
|
||||
@@ -42,9 +38,7 @@ export default class Monitor {
|
||||
getWindow(windowId: number): WindowWrapper | undefined {
|
||||
for (const container of this._workspaces) {
|
||||
const win = container.getWindow(windowId);
|
||||
if (win) {
|
||||
return win;
|
||||
}
|
||||
if (win) return win;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -52,8 +46,7 @@ export default class Monitor {
|
||||
removeWindow(winWrap: WindowWrapper) {
|
||||
const windowId = winWrap.getWindowId();
|
||||
for (const container of this._workspaces) {
|
||||
const win = container.getWindow(windowId);
|
||||
if (win) {
|
||||
if (container.getWindow(windowId)) {
|
||||
container.removeWindow(windowId);
|
||||
}
|
||||
}
|
||||
@@ -65,9 +58,9 @@ export default class Monitor {
|
||||
}
|
||||
|
||||
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();
|
||||
// move() already calls tileWindows() internally — don't call it again
|
||||
this._workArea = activeWorkspace.get_work_area_for_monitor(this._id);
|
||||
// move() calls tileWindows() internally
|
||||
this._workspaces[activeWorkspace.index()].move(this._workArea);
|
||||
}
|
||||
|
||||
@@ -79,8 +72,26 @@ export default class Monitor {
|
||||
this._workspaces.push(new WindowContainer(this._workArea));
|
||||
}
|
||||
|
||||
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 {
|
||||
this._workspaces[item.getWorkspace()].itemDragged(item, x, y);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
+68
-43
@@ -11,6 +11,8 @@ type WindowMinimizedHandler = (window: WindowWrapper) => void;
|
||||
type WindowWorkspaceChangedHandler = (window: WindowWrapper) => void;
|
||||
|
||||
export class WindowWrapper {
|
||||
private static readonly RESIZE_TOLERANCE = 2;
|
||||
|
||||
readonly _window: Meta.Window;
|
||||
readonly _windowMinimizedHandler: WindowMinimizedHandler;
|
||||
readonly _signals: number[] = [];
|
||||
@@ -45,44 +47,54 @@ export class WindowWrapper {
|
||||
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 {
|
||||
this._dragging = true;
|
||||
}
|
||||
|
||||
stopDragging(): void {
|
||||
Logger.log("STOPPED DRAGGING")
|
||||
this._dragging = false;
|
||||
}
|
||||
|
||||
// setParent(parent: WindowContainer): void {
|
||||
// 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
|
||||
connectWindowSignals(windowManager: IWindowManager): void {
|
||||
const windowId = this._window.get_id();
|
||||
this._signals.push(
|
||||
this._window.connect('unmanaging', window => {
|
||||
this._window.connect('unmanaging', () => {
|
||||
Logger.log("REMOVING WINDOW", windowId);
|
||||
windowManager.handleWindowClosed(this)
|
||||
windowManager.handleWindowClosed(this);
|
||||
}),
|
||||
this._window.connect('notify::minimized', (we) => {
|
||||
this._window.connect('notify::minimized', () => {
|
||||
if (this._window.minimized) {
|
||||
Logger.log(`Window minimized: ${windowId}`);
|
||||
windowManager.handleWindowMinimized(this);
|
||||
|
||||
} else if (!this._window.minimized) {
|
||||
} else {
|
||||
Logger.log(`Window unminimized: ${windowId}`);
|
||||
windowManager.handleWindowUnminimized(this);
|
||||
|
||||
}
|
||||
}),
|
||||
this._window.connect('notify::maximized-horizontally', () => {
|
||||
@@ -92,21 +104,23 @@ export class WindowWrapper {
|
||||
Logger.log(`Window unmaximized: ${windowId}`);
|
||||
}
|
||||
}),
|
||||
this._window.connect("workspace-changed", (_metaWindow) => {
|
||||
this._window.connect("workspace-changed", () => {
|
||||
Logger.log("WORKSPACE CHANGED FOR WINDOW", this._window.get_id());
|
||||
windowManager.handleWindowChangedWorkspace(this);
|
||||
}),
|
||||
this._window.connect("position-changed", (_metaWindow) => {
|
||||
this._window.connect("position-changed", () => {
|
||||
windowManager.handleWindowPositionChanged(this);
|
||||
}),
|
||||
this._window.connect("size-changed", (_metaWindow) => {
|
||||
this._window.connect("size-changed", () => {
|
||||
windowManager.handleWindowPositionChanged(this);
|
||||
}),
|
||||
this._window.connect('notify::title', () => {
|
||||
windowManager.handleWindowTitleChanged(this);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
disconnectWindowSignals(): void {
|
||||
|
||||
if (this._signals) {
|
||||
this._signals.forEach(signal => {
|
||||
try {
|
||||
@@ -134,29 +148,40 @@ export class WindowWrapper {
|
||||
|
||||
actor.remove_all_transitions();
|
||||
|
||||
// Single call: move + resize atomically
|
||||
// Move first to guarantee the window reaches the correct position even
|
||||
// if the subsequent resize is clamped by minimum-size hints.
|
||||
this._window.move_frame(true, rect.x, rect.y);
|
||||
this._window.move_resize_frame(true, rect.x, rect.y, rect.width, rect.height);
|
||||
|
||||
const new_rect = this._window.get_frame_rect();
|
||||
const TOLERANCE = 2; // pixels — allow compositor rounding
|
||||
const mismatch =
|
||||
Math.abs(new_rect.x - rect.x) > TOLERANCE ||
|
||||
Math.abs(new_rect.y - rect.y) > TOLERANCE ||
|
||||
Math.abs(new_rect.width - rect.width) > TOLERANCE ||
|
||||
Math.abs(new_rect.height - rect.height) > TOLERANCE;
|
||||
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) {
|
||||
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({
|
||||
name: `delayed_resize_${this.getWindowId()}`,
|
||||
callback: () => {
|
||||
this.safelyResizeWindow(rect, _retry - 1);
|
||||
}
|
||||
}, 50);
|
||||
// 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({
|
||||
name: `delayed_resize_${this.getWindowId()}`,
|
||||
callback: () => this.safelyResizeWindow(rect, _retry - 1),
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+83
-231
@@ -1,21 +1,17 @@
|
||||
import Meta from "gi://Meta";
|
||||
import Gio from "gi://Gio";
|
||||
// import GLib from "gi://GLib";
|
||||
|
||||
import {WindowWrapper} from './window.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 Monitor from "./monitor.js";
|
||||
import WindowContainer from "./container.js";
|
||||
import WindowContainer, {Layout} from "./container.js";
|
||||
import {Rect} from "../utils/rect.js";
|
||||
|
||||
|
||||
export interface IWindowManager {
|
||||
_activeWindowId: number | null;
|
||||
|
||||
// addWindow(window: Meta.Window): void;
|
||||
|
||||
handleWindowClosed(winWrap: WindowWrapper): void;
|
||||
|
||||
handleWindowMinimized(winWrap: WindowWrapper): void;
|
||||
@@ -26,12 +22,14 @@ export interface IWindowManager {
|
||||
|
||||
handleWindowPositionChanged(winWrap: WindowWrapper): void;
|
||||
|
||||
handleWindowTitleChanged(winWrap: WindowWrapper): void;
|
||||
|
||||
syncActiveWindow(): number | null;
|
||||
}
|
||||
|
||||
|
||||
const _UNUSED_MONITOR_ID = -1
|
||||
const _UNUSED_WINDOW_ID = -1
|
||||
const _UNUSED_MONITOR_ID = -1;
|
||||
const _UNUSED_WINDOW_ID = -1;
|
||||
|
||||
export default class WindowManager implements IWindowManager {
|
||||
_displaySignals: number[] = [];
|
||||
@@ -41,37 +39,29 @@ export default class WindowManager implements IWindowManager {
|
||||
|
||||
_activeWindowId: number | null = null;
|
||||
_monitors: Map<number, Monitor> = new Map<number, Monitor>();
|
||||
|
||||
_minimizedItems: Map<number, WindowWrapper> = new Map<number, WindowWrapper>();
|
||||
|
||||
_grabbedWindowMonitor: number = _UNUSED_MONITOR_ID;
|
||||
_grabbedWindowId: number = _UNUSED_WINDOW_ID;
|
||||
_changingGrabbedMonitor: boolean = false;
|
||||
|
||||
_showingOverview: boolean = false;
|
||||
|
||||
// ── Resize-drag tracking ──────────────────────────────────────────────────
|
||||
// -- Resize-drag tracking --------------------------------------------------
|
||||
_isResizeDrag: boolean = false;
|
||||
_resizeDragWindowId: number = _UNUSED_WINDOW_ID;
|
||||
_resizeDragOp: Meta.GrabOp = Meta.GrabOp.NONE;
|
||||
/** Mouse position at the start of each incremental resize step. */
|
||||
_resizeDragLastMouseX: number = 0;
|
||||
_resizeDragLastMouseY: number = 0;
|
||||
/** Re-entrancy guard: true while tileWindows is propagating position-changed events. */
|
||||
_isTiling: boolean = false;
|
||||
|
||||
_settings: Gio.Settings | null = null;
|
||||
private readonly _settings: Gio.Settings;
|
||||
|
||||
constructor() {}
|
||||
|
||||
/** Returns the live min-ratio value from settings, falling back to 0.10. */
|
||||
private _getMinRatio(): number {
|
||||
return this._settings?.get_double('min-window-size-percent') ?? 0.10;
|
||||
constructor(settings: Gio.Settings) {
|
||||
this._settings = settings;
|
||||
}
|
||||
|
||||
public enable(): void {
|
||||
Logger.log("Starting Aerospike Window Manager");
|
||||
// Connect window signals
|
||||
this.instantiateDisplaySignals();
|
||||
|
||||
const mon_count = global.display.get_n_monitors();
|
||||
@@ -80,8 +70,6 @@ export default class WindowManager implements IWindowManager {
|
||||
}
|
||||
|
||||
this.captureExistingWindows();
|
||||
|
||||
// Sync the initially focused window
|
||||
this.syncActiveWindow();
|
||||
}
|
||||
|
||||
@@ -108,7 +96,6 @@ export default class WindowManager implements IWindowManager {
|
||||
global.display.connect('notify::focus-window', () => {
|
||||
this.syncActiveWindow();
|
||||
}),
|
||||
|
||||
global.display.connect("showing-desktop-changed", () => {
|
||||
Logger.log("SHOWING DESKTOP CHANGED");
|
||||
}),
|
||||
@@ -118,14 +105,9 @@ export default class WindowManager implements IWindowManager {
|
||||
}),
|
||||
global.display.connect("in-fullscreen-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 = [
|
||||
global.workspace_manager.connect("showing-desktop-changed", () => {
|
||||
@@ -150,45 +132,37 @@ export default class WindowManager implements IWindowManager {
|
||||
|
||||
this._overviewSignals = [
|
||||
Main.overview.connect("hiding", () => {
|
||||
// this.fromOverview = true;
|
||||
Logger.log("HIDING OVERVIEW")
|
||||
this._showingOverview = false;
|
||||
this._tileMonitors();
|
||||
// const eventObj = {
|
||||
// name: "focus-after-overview",
|
||||
// callback: () => {
|
||||
// Logger.log("FOCUSING AFTER OVERVIEW");
|
||||
// },
|
||||
// };
|
||||
// this.queueEvent(eventObj);
|
||||
for (const monitor of this._monitors.values()) {
|
||||
monitor.showTabBars();
|
||||
}
|
||||
}),
|
||||
Main.overview.connect("showing", () => {
|
||||
this._showingOverview = true;
|
||||
Logger.log("SHOWING OVERVIEW");
|
||||
for (const monitor of this._monitors.values()) {
|
||||
monitor.hideTabBars();
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
}
|
||||
|
||||
public disable(): void {
|
||||
Logger.log("DISABLED AEROSPIKE WINDOW MANAGER!")
|
||||
// Disconnect the focus signal and remove any existing borders
|
||||
this.disconnectSignals();
|
||||
this.removeAllWindows();
|
||||
}
|
||||
|
||||
removeAllWindows(): void {
|
||||
// Disconnect signals from minimized windows before clearing
|
||||
this.disconnectMinimizedSignals();
|
||||
this._minimizedItems.clear();
|
||||
|
||||
this._monitors.forEach((monitor: Monitor) => {
|
||||
monitor.removeAllWindows();
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
disconnectSignals(): void {
|
||||
this.disconnectDisplaySignals();
|
||||
this.disconnectMonitorSignals();
|
||||
@@ -222,10 +196,6 @@ export default class WindowManager implements IWindowManager {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the grab op is a resize operation (any edge or corner).
|
||||
*/
|
||||
_isResizeOp(op: Meta.GrabOp): boolean {
|
||||
return op === Meta.GrabOp.RESIZING_E ||
|
||||
op === Meta.GrabOp.RESIZING_W ||
|
||||
@@ -241,7 +211,6 @@ export default class WindowManager implements IWindowManager {
|
||||
Logger.log("Grab Op Start", op);
|
||||
|
||||
if (this._isResizeOp(op)) {
|
||||
// ── Resize drag ──────────────────────────────────────────────────
|
||||
Logger.log("Resize drag begin, op=", op);
|
||||
this._isResizeDrag = true;
|
||||
this._resizeDragWindowId = window.get_id();
|
||||
@@ -249,11 +218,8 @@ export default class WindowManager implements IWindowManager {
|
||||
const [startMouseX, startMouseY] = global.get_pointer();
|
||||
this._resizeDragLastMouseX = startMouseX;
|
||||
this._resizeDragLastMouseY = startMouseY;
|
||||
// Mark the window as dragging so safelyResizeWindow skips it while
|
||||
// we tile the other windows in response to ratio changes.
|
||||
this._getWrappedWindow(window)?.startDragging();
|
||||
} else {
|
||||
// ── Move drag (existing behaviour) ───────────────────────────────
|
||||
this._getWrappedWindow(window)?.startDragging();
|
||||
this._grabbedWindowMonitor = window.get_monitor();
|
||||
this._grabbedWindowId = window.get_id();
|
||||
@@ -264,19 +230,15 @@ export default class WindowManager implements IWindowManager {
|
||||
Logger.log("Grab Op End ", op);
|
||||
|
||||
if (this._isResizeDrag) {
|
||||
// ── Resize drag end ──────────────────────────────────────────────
|
||||
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;
|
||||
// Stop suppressing the window, then snap everything to computed ratios
|
||||
this._getWrappedWindow(window)?.stopDragging();
|
||||
this._tileMonitors();
|
||||
} else {
|
||||
// ── Move drag end (existing behaviour) ───────────────────────────
|
||||
Logger.log("primary display", display.get_primary_monitor())
|
||||
this._grabbedWindowId = _UNUSED_WINDOW_ID;
|
||||
this._getWrappedWindow(window)?.stopDragging();
|
||||
this._tileMonitors();
|
||||
@@ -288,9 +250,7 @@ export default class WindowManager implements IWindowManager {
|
||||
let wrapped: WindowWrapper | undefined = undefined;
|
||||
for (const monitor of this._monitors.values()) {
|
||||
wrapped = monitor.getWindow(window.get_id());
|
||||
if (wrapped !== undefined) {
|
||||
break;
|
||||
}
|
||||
if (wrapped !== undefined) break;
|
||||
}
|
||||
return wrapped;
|
||||
}
|
||||
@@ -320,21 +280,13 @@ export default class WindowManager implements IWindowManager {
|
||||
}
|
||||
|
||||
public handleWindowPositionChanged(winWrap: WindowWrapper): void {
|
||||
// Ignore position changes that we triggered ourselves via tileWindows
|
||||
if (this._isTiling) {
|
||||
return;
|
||||
}
|
||||
if (this._changingGrabbedMonitor) {
|
||||
return;
|
||||
}
|
||||
if (this._isTiling || this._changingGrabbedMonitor) return;
|
||||
|
||||
// ── Live resize-drag handling ─────────────────────────────────────────
|
||||
if (this._isResizeDrag && winWrap.getWindowId() === this._resizeDragWindowId) {
|
||||
this._handleResizeDragUpdate(winWrap);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Move-drag handling (existing behaviour) ───────────────────────────
|
||||
if (winWrap.getWindowId() === this._grabbedWindowId) {
|
||||
const [mouseX, mouseY, _] = global.get_pointer();
|
||||
|
||||
@@ -347,17 +299,14 @@ export default class WindowManager implements IWindowManager {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (monitorIndex === -1) {
|
||||
return;
|
||||
}
|
||||
if (monitorIndex === -1) return;
|
||||
|
||||
if (monitorIndex !== this._grabbedWindowMonitor) {
|
||||
this._changingGrabbedMonitor = true;
|
||||
this._moveWindowToMonitor(winWrap.getWindow(), monitorIndex);
|
||||
this._changingGrabbedMonitor = false;
|
||||
}
|
||||
// Guard _isTiling so that tileWindows() calls triggered by itemDragged
|
||||
// (which repositions the displaced window) don't re-enter this handler.
|
||||
|
||||
this._isTiling = true;
|
||||
try {
|
||||
this._monitors.get(monitorIndex)?.itemDragged(winWrap, mouseX, mouseY);
|
||||
@@ -367,29 +316,19 @@ export default class WindowManager implements IWindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on every position-changed event while a resize drag is in progress.
|
||||
* Computes the pixel delta from the drag-start rect, maps it to the correct
|
||||
* container boundary, and calls adjustBoundary() for live feedback.
|
||||
*/
|
||||
private _handleResizeDragUpdate(winWrap: WindowWrapper): void {
|
||||
const op = this._resizeDragOp;
|
||||
const winId = winWrap.getWindowId();
|
||||
|
||||
// Read the current mouse position — this is unclamped by the compositor
|
||||
// and always reflects the true user intent, unlike the window's frame rect
|
||||
// which gets clamped when adjacent windows block expansion.
|
||||
const [mouseX, mouseY] = global.get_pointer();
|
||||
const dx = mouseX - this._resizeDragLastMouseX;
|
||||
const dy = mouseY - this._resizeDragLastMouseY;
|
||||
|
||||
if (dx === 0 && dy === 0) return;
|
||||
|
||||
// Update last position first so even if we return early the baseline advances
|
||||
this._resizeDragLastMouseX = mouseX;
|
||||
this._resizeDragLastMouseY = mouseY;
|
||||
|
||||
// Find the container that directly holds this window
|
||||
const container = this._findContainerForWindowAcrossMonitors(winId);
|
||||
if (!container) {
|
||||
Logger.warn("_handleResizeDragUpdate: no container found for window", winId);
|
||||
@@ -399,59 +338,44 @@ export default class WindowManager implements IWindowManager {
|
||||
const itemIndex = container._getIndexOfWindow(winId);
|
||||
if (itemIndex === -1) return;
|
||||
|
||||
const isHorizontal = container._orientation === 0; // Orientation.HORIZONTAL
|
||||
const isHorizontal = container._orientation === Layout.ACC_HORIZONTAL;
|
||||
|
||||
// Map the mouse delta to the correct boundary.
|
||||
//
|
||||
// East/South edge → boundary AFTER the item (boundaryIndex = itemIndex)
|
||||
// positive dx/dy grows this item, shrinks the next one.
|
||||
// West/North edge → boundary BEFORE the item (boundaryIndex = itemIndex - 1)
|
||||
// positive dx/dy moves the left edge right, growing the left neighbour
|
||||
// and shrinking this item — so we negate the delta.
|
||||
|
||||
const minRatio = this._getMinRatio();
|
||||
// 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, minRatio);
|
||||
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, minRatio);
|
||||
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, minRatio);
|
||||
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, minRatio);
|
||||
adjusted = container.adjustBoundary(itemIndex - 1, dy);
|
||||
}
|
||||
}
|
||||
|
||||
// Tile all windows with the updated ratios, guarded so the resulting
|
||||
// position-changed events don't re-enter this handler.
|
||||
if (adjusted) {
|
||||
this._isTiling = true;
|
||||
try {
|
||||
container.tileWindows();
|
||||
container.drawWindows();
|
||||
} finally {
|
||||
this._isTiling = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches all monitors for the WindowContainer that directly holds win_id.
|
||||
*/
|
||||
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 workspace = monitor._workspaces[activeWorkspaceIndex];
|
||||
const container = workspace.getContainerForWindow(winId);
|
||||
const container = monitor._workspaces[activeWorkspaceIndex].getContainerForWindow(winId);
|
||||
if (container !== null) return container;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public handleWindowMinimized(winWrap: WindowWrapper): void {
|
||||
const monitor_id = winWrap.getWindow().get_monitor()
|
||||
this._minimizedItems.set(winWrap.getWindowId(), winWrap);
|
||||
@@ -465,7 +389,6 @@ export default class WindowManager implements IWindowManager {
|
||||
this._tileMonitors()
|
||||
}
|
||||
|
||||
|
||||
public handleWindowChangedWorkspace(winWrap: WindowWrapper): void {
|
||||
const monitor = winWrap.getWindow().get_monitor();
|
||||
this._monitors.get(monitor)?.removeWindow(winWrap);
|
||||
@@ -484,41 +407,31 @@ export default class WindowManager implements IWindowManager {
|
||||
this._tileMonitors();
|
||||
}
|
||||
|
||||
|
||||
handleWindowCreated(display: Meta.Display, window: Meta.Window) {
|
||||
Logger.log("WINDOW CREATED ON DISPLAY", window, display);
|
||||
if (!this._isWindowTileable(window)) {
|
||||
return;
|
||||
}
|
||||
if (!this._isWindowTileable(window)) return;
|
||||
Logger.log("WINDOW IS TILABLE");
|
||||
this.addWindowToMonitor(window);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle window closed event
|
||||
*/
|
||||
handleWindowClosed(window: WindowWrapper): void {
|
||||
|
||||
const mon_id = window._window.get_monitor();
|
||||
|
||||
this._monitors.get(mon_id)?.removeWindow(window);
|
||||
|
||||
window.disconnectWindowSignals()
|
||||
// Remove from managed windows
|
||||
this.syncActiveWindow();
|
||||
// Retile remaining windows
|
||||
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) {
|
||||
|
||||
Logger.log("ADDING WINDOW TO MONITOR", window, window);
|
||||
var wrapper = new WindowWrapper(window, (winWrap) => this.handleWindowMinimized(winWrap))
|
||||
wrapper.connectWindowSignals(this);
|
||||
this._addWindowWrapperToMonitor(wrapper);
|
||||
|
||||
}
|
||||
|
||||
_addWindowWrapperToMonitor(winWrap: WindowWrapper) {
|
||||
@@ -529,12 +442,24 @@ 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 {
|
||||
this._isTiling = true;
|
||||
try {
|
||||
for (const monitor of this._monitors.values()) {
|
||||
monitor.tileWindows();
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error("_tileMonitors FAILED", e);
|
||||
} finally {
|
||||
this._isTiling = false;
|
||||
}
|
||||
@@ -544,7 +469,7 @@ export default class WindowManager implements IWindowManager {
|
||||
"org.gnome.Shell.Extensions",
|
||||
]
|
||||
|
||||
_isWindowTilingBlocked(window: Meta.Window) : boolean {
|
||||
_isWindowTilingBlocked(window: Meta.Window): boolean {
|
||||
Logger.info("title", window.get_title());
|
||||
Logger.info("description", window.get_description());
|
||||
Logger.info("class", window.get_wm_class());
|
||||
@@ -559,17 +484,12 @@ export default class WindowManager implements IWindowManager {
|
||||
}
|
||||
|
||||
_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();
|
||||
Logger.log("WINDOW TILING CHECK",);
|
||||
|
||||
// Skip certain types of windows
|
||||
return !window.is_skip_taskbar() &&
|
||||
windowType !== Meta.WindowType.DESKTOP &&
|
||||
windowType !== Meta.WindowType.DOCK &&
|
||||
@@ -579,14 +499,6 @@ export default class WindowManager implements IWindowManager {
|
||||
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 {
|
||||
const focusWindow = global.display.focus_window;
|
||||
if (focusWindow) {
|
||||
@@ -599,102 +511,56 @@ export default class WindowManager implements IWindowManager {
|
||||
return this._activeWindowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the orientation of the active container (the container holding the active window)
|
||||
*/
|
||||
public toggleActiveContainerOrientation(): void {
|
||||
if (this._activeWindowId === null) {
|
||||
Logger.warn("No active window, cannot toggle container orientation");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the active window's container
|
||||
const activeContainer = this._findActiveContainer();
|
||||
if (activeContainer) {
|
||||
activeContainer.toggleOrientation();
|
||||
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
|
||||
if (container) {
|
||||
container.toggleOrientation();
|
||||
} else {
|
||||
Logger.warn("Could not find container for active window");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all split ratios in the active window's container to equal fractions.
|
||||
* Bound to Ctrl+Z by default.
|
||||
*/
|
||||
public resetActiveContainerRatios(): void {
|
||||
if (this._activeWindowId === null) {
|
||||
Logger.warn("No active window, cannot reset container ratios");
|
||||
return;
|
||||
}
|
||||
|
||||
const activeContainer = this._findActiveContainer();
|
||||
if (activeContainer) {
|
||||
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
|
||||
if (container) {
|
||||
Logger.info("Resetting container ratios to equal splits");
|
||||
activeContainer.resetRatios();
|
||||
container.resetRatios();
|
||||
} else {
|
||||
Logger.warn("Could not find container for active window");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the container that directly contains the active window
|
||||
* @returns The container holding the active window, or null if not found
|
||||
*/
|
||||
private _findActiveContainer(): WindowContainer | null {
|
||||
public toggleActiveContainerTabbed(): void {
|
||||
if (this._activeWindowId === null) {
|
||||
return null;
|
||||
Logger.warn("No active window, cannot toggle tabbed mode");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const monitor of this._monitors.values()) {
|
||||
const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index();
|
||||
|
||||
// Bounds check to prevent accessing invalid workspace
|
||||
if (activeWorkspaceIndex >= monitor._workspaces.length || activeWorkspaceIndex < 0) {
|
||||
Logger.warn(`Active workspace index ${activeWorkspaceIndex} out of bounds for monitor with ${monitor._workspaces.length} workspaces`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspace = monitor._workspaces[activeWorkspaceIndex];
|
||||
|
||||
// Check if the window is directly in the workspace container
|
||||
const windowWrapper = workspace.getWindow(this._activeWindowId);
|
||||
if (windowWrapper) {
|
||||
// Try to find the parent container
|
||||
const container = this._findContainerHoldingWindow(workspace, this._activeWindowId);
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively finds the container that directly contains a specific window
|
||||
* @param container The container to search
|
||||
* @param windowId The window ID to find
|
||||
* @returns The container that directly contains the window, or null if not found
|
||||
*/
|
||||
private _findContainerHoldingWindow(container: WindowContainer, windowId: number): WindowContainer | 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;
|
||||
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;
|
||||
}
|
||||
} else if (item.getWindowId() === windowId) {
|
||||
// Found it! Return this container as it directly holds the window
|
||||
return container;
|
||||
container.setTabbed();
|
||||
}
|
||||
this._tileMonitors();
|
||||
} else {
|
||||
Logger.warn("Could not find container for active window");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the tree structure of all monitors, workspaces, containers, and windows to the logs
|
||||
*/
|
||||
public printTreeStructure(): void {
|
||||
Logger.info("=".repeat(80));
|
||||
Logger.info("WINDOW TREE STRUCTURE");
|
||||
@@ -707,19 +573,18 @@ export default class WindowManager implements IWindowManager {
|
||||
this._monitors.forEach((monitor: Monitor, monitorId: number) => {
|
||||
const isActiveMonitor = this._activeWindowId !== null &&
|
||||
monitor.getWindow(this._activeWindowId) !== undefined;
|
||||
const monitorMarker = isActiveMonitor ? ' *' : '';
|
||||
|
||||
Logger.info(`Monitor ${monitorId}${monitorMarker}:`);
|
||||
Logger.info(`Monitor ${monitorId}${isActiveMonitor ? ' *' : ''}:`);
|
||||
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) => {
|
||||
const isActiveWorkspace = workspaceIndex === activeWorkspaceIndex;
|
||||
const workspaceMarker = isActiveWorkspace && isActiveMonitor ? ' *' : '';
|
||||
|
||||
Logger.info(` Workspace ${workspaceIndex}${workspaceMarker}:`);
|
||||
Logger.info(` Orientation: ${workspace._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'}`);
|
||||
Logger.info(` Workspace ${workspaceIndex}${isActiveWorkspace && isActiveMonitor ? ' *' : ''}:`);
|
||||
Logger.info(` Orientation: ${Layout[workspace._orientation]}`);
|
||||
Logger.info(` Items: ${workspace._tiledItems.length}`);
|
||||
|
||||
if (workspace.isTabbed()) {
|
||||
Logger.info(` Active Tab: ${workspace._activeTabIndex}`);
|
||||
}
|
||||
this._printContainerTree(workspace, 4);
|
||||
});
|
||||
});
|
||||
@@ -727,31 +592,20 @@ export default class WindowManager implements IWindowManager {
|
||||
Logger.info("=".repeat(80));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
private _printContainerTree(container: WindowContainer, indentLevel: number): void {
|
||||
const indent = " ".repeat(indentLevel);
|
||||
|
||||
container._tiledItems.forEach((item: any, index: number) => {
|
||||
container._tiledItems.forEach((item, index) => {
|
||||
if (item instanceof WindowContainer) {
|
||||
// Check if this container contains the active window
|
||||
const containsActiveWindow = this._activeWindowId !== null &&
|
||||
item.getWindow(this._activeWindowId) !== undefined;
|
||||
const containerMarker = containsActiveWindow ? ' *' : '';
|
||||
|
||||
Logger.info(`${indent}[${index}] Container (${item._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'})${containerMarker}:`);
|
||||
const containsActive = this._activeWindowId !== null &&
|
||||
item.getWindow(this._activeWindowId) !== undefined;
|
||||
Logger.info(`${indent}[${index}] Container (${Layout[item._orientation]})${containsActive ? ' *' : ''}:`);
|
||||
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}`);
|
||||
this._printContainerTree(item, indentLevel + 4);
|
||||
} else {
|
||||
const window = item.getWindow();
|
||||
const isActiveWindow = this._activeWindowId === item.getWindowId();
|
||||
const windowMarker = isActiveWindow ? ' *' : '';
|
||||
|
||||
Logger.info(`${indent}[${index}] Window ID: ${item.getWindowId()}${windowMarker}`);
|
||||
Logger.info(`${indent}[${index}] Window ID: ${item.getWindowId()}${this._activeWindowId === item.getWindowId() ? ' *' : ''}`);
|
||||
Logger.info(`${indent} Title: "${window.get_title()}"`);
|
||||
Logger.info(`${indent} Class: ${window.get_wm_class()}`);
|
||||
const rect = item.getRect();
|
||||
@@ -759,6 +613,4 @@ export default class WindowManager implements IWindowManager {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+28
-32
@@ -1,37 +1,33 @@
|
||||
/* Add your custom extension styling here */
|
||||
.active-window-border {
|
||||
/*border: 2px solid rgba(191, 0, 255, 0.8);*/
|
||||
/*border-radius: 3px;*/
|
||||
|
||||
/* 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-bar {
|
||||
background-color: rgba(30, 30, 30, 0.95);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
spacing: 1px;
|
||||
padding: 2px 2px 0 2px;
|
||||
}
|
||||
|
||||
/*.border-gradient-purple {*/
|
||||
/* border-image-source: linear-gradient(to left, #743ad5, #d53a9d);*/
|
||||
/*}*/
|
||||
.aerospike-tab {
|
||||
background-color: rgba(50, 50, 50, 0.8);
|
||||
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;
|
||||
}
|
||||
|
||||
/*@keyframes rainbow-border {*/
|
||||
/* 0% {*/
|
||||
/* 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:hover {
|
||||
background-color: rgba(70, 70, 70, 0.9);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/*.active-window-border {*/
|
||||
/* border: 4px solid transparent;*/
|
||||
/* border-radius: 5px;*/
|
||||
.aerospike-tab-active {
|
||||
background-color: rgba(80, 80, 80, 1);
|
||||
color: rgba(255, 255, 255, 1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* !* Initial gradient border *!*/
|
||||
/* border-image: linear-gradient(0deg, red, orange, yellow, green, blue, indigo, violet, red) 1;*/
|
||||
|
||||
/* !* Apply animation *!*/
|
||||
/* animation: rainbow-border 5s linear infinite;*/
|
||||
/*}*/
|
||||
.aerospike-tab-label {
|
||||
font-size: 11px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user