Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8f7ca7e7e | |||
| d249abe804 | |||
| 59b2fe1646 | |||
| 6e9fb687fe | |||
| f9c1aee610 | |||
| 70a0fd9eab | |||
| 2fee754776 | |||
| 114b7cc265 | |||
| 8e6d9ab2d9 | |||
| bcd14cb87d | |||
| 307908cd39 | |||
| fcfdf39059 | |||
| e7618cf3e5 | |||
| 811db01995 | |||
| ae59b60a7a | |||
| d9bacba373 | |||
| 4e26db91ea | |||
| 6f56a5c7c8 | |||
| 6549e5bca3 | |||
| 553a1599c6 | |||
| 921928bd2a | |||
| 07f1d40726 | |||
| 40f7ca64ef | |||
| 7d864878b4 | |||
| 0c79ce6da1 | |||
| 843ea1d819 | |||
| c7cf481e33 | |||
| 54172118e1 | |||
| 95eb37ddd0 | |||
| fe3b242476 | |||
| 5614e5fff2 | |||
| 4de841d8b0 | |||
| b0586e9c36 | |||
| c39a6799e2 | |||
| c495c85e95 | |||
| ca9ea8fcff | |||
| 0a122565b8 | |||
| 524098ce68 | |||
| c5e692bb45 | |||
| 421fb8796f | |||
| b144a90902 | |||
| 89982c883d | |||
| 5204d40168 | |||
| 7b45cffeff | |||
| 560ca61e41 | |||
| 6e35a92e9e | |||
| 38baf60286 | |||
| 65f5447ea2 | |||
| 290d067287 | |||
| 87ab2e8669 | |||
| 209421f479 | |||
| dd2bc5e589 | |||
| 9148ef63c6 | |||
| 56ee5c8933 | |||
| 7a313f7fc4 | |||
| 62d2792889 | |||
| 7aa9a30375 | |||
| ecfc739cc8 | |||
| 2a94a1c04f | |||
| 8c1dce1644 | |||
| 6a5f421fdf | |||
| 10e9faf528 | |||
| bf492ceeb3 | |||
| a5ba875df0 | |||
| 90b8dd952e | |||
| f3e1a30aae | |||
| 3a998f943a | |||
| 0bb95a577d | |||
| a47cac9f43 | |||
| 8408c357a5 | |||
| d58381e4ec | |||
| 7ed0b64ffb | |||
| b8dcc27bf8 | |||
| 136f49456a | |||
| 09a2740ffe | |||
| adcb02c953 | |||
| c9eb0c0212 | |||
| b0d00f03ec | |||
| 134c6b2cb6 | |||
| c4aee6f610 | |||
| a6045b73a1 | |||
| 005795cbd9 | |||
| da86ae9cd2 |
@@ -0,0 +1,6 @@
|
|||||||
|
# GNOME Shell Extension specific
|
||||||
|
schemas/gschemas.compiled
|
||||||
|
|
||||||
|
# Common temporary files
|
||||||
|
*~
|
||||||
|
*.swp
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
# Simple Tiling
|
|
||||||
|
<h1 align="center">
|
||||||
|
Simple Tiling
|
||||||
|
</span>
|
||||||
|
<h4 align="center">
|
||||||
|
<span style="display:inline-flex; align-items:center; gap:12px;">
|
||||||
|
A lightweight, opinionated, and automatic tiling window manager for GNOME Shell 3.38.
|
||||||
|
</span>
|
||||||
|
<p>
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||

|

|
||||||
|
|
||||||
A lightweight, opinionated, and automatic tiling window manager for GNOME Shell 3.38.
|
<img width="2560" height="1440" alt="Simple-Tiling-v6" src="https://github.com/user-attachments/assets/eb0f7cc3-6a5a-4036-8a1e-8f945c52e55c" />
|
||||||
|
|
||||||
<img width="2560" height="1440" alt="Simple-Tiling" src="https://github.com/user-attachments/assets/18fcbb34-3e0e-45b8-abe1-0e752b8d970c" />
|
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
@@ -17,13 +23,16 @@ This extension was built from the ground up to be stable and performant on **GNO
|
|||||||
|
|
||||||
* **Automatic Tiling:** Windows are automatically arranged into a master and stack layout without any manual intervention.
|
* **Automatic Tiling:** Windows are automatically arranged into a master and stack layout without any manual intervention.
|
||||||
* **Master & Fibonacci Stack Layout:** The first window becomes the "master," occupying the left half of the screen. All subsequent windows form a "stack" on the right half, which is tiled using a space-efficient Fibonacci-style algorithm.
|
* **Master & Fibonacci Stack Layout:** The first window becomes the "master," occupying the left half of the screen. All subsequent windows form a "stack" on the right half, which is tiled using a space-efficient Fibonacci-style algorithm.
|
||||||
|
* **Configurable New Window Behavior:** Choose whether new windows open as the new master or are appended to the end of the stack.
|
||||||
* **Tiling Lock:** The layout is strict by default. If you manually move a window with the mouse and drop it in an empty space, it will automatically "snap back" to its designated tile position, preserving the integrity of the layout.
|
* **Tiling Lock:** The layout is strict by default. If you manually move a window with the mouse and drop it in an empty space, it will automatically "snap back" to its designated tile position, preserving the integrity of the layout.
|
||||||
* **Interactive Window Swapping:**
|
* **Interactive Window Swapping:**
|
||||||
* **Drag & Drop:** Swap any two windows by simply dragging one and dropping it over the other.
|
* **Drag & Drop:** Swap any two windows by simply dragging one and dropping it over the other.
|
||||||
* **Keyboard Shortcuts:** A full set of keyboard shortcuts allows you to swap the focused window with the master or with its nearest neighbor in any direction (left, right, up, down).
|
* **Keyboard Shortcuts:** A full set of keyboard shortcuts allows you to swap the focused window with the master or with its nearest neighbor in any direction (left, right, up, down).
|
||||||
* **Configurable Gaps:** Easily configure inner and outer gaps by editing variables directly in the `extension.js` code to achieve your desired aesthetic.
|
* **Interactive Window Focus Switcher:** Change the current window focus with a set of customizable keyboard shortcuts in every direction (left, right, up, down).
|
||||||
|
* **Simple Settings Panel:** A simple settings panel within the gnome extension manager menu to adjust key bindings, window gaps / margins and window behavior.
|
||||||
* **External Exception List:** Use a simple `exceptions.txt` file to list applications (by their `WM_CLASS`) that should be ignored by the tiling manager.
|
* **External Exception List:** Use a simple `exceptions.txt` file to list applications (by their `WM_CLASS`) that should be ignored by the tiling manager.
|
||||||
* **Smart Pop-up Handling:** Windows on the exception list, as well as dialogs and other pop-ups, are automatically centered and kept "always on top" for a smooth workflow.
|
* **Smart Pop-up Handling:** Windows on the exception list, as well as dialogs and other pop-ups, are automatically centered and kept "always on top" for a smooth workflow.
|
||||||
|
* **Configurable Tiling Window Delays:** Easily configure the tiling window delays if you have race condition issues by editing variables directly in the `extension.js`.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -41,23 +50,34 @@ Use the [GNOME Shell Extensions website](https://extensions.gnome.org/extension/
|
|||||||
|
|
||||||
#### Manual Installation
|
#### Manual Installation
|
||||||
|
|
||||||
1. **Clone the repository** into your local extensions directory:
|
1. **Navigate to your extensions folder:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.local/share/gnome-shell/extensions/
|
||||||
|
```
|
||||||
|
3. **Clone the repository directly into a folder named after the extension's UUID:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Domoel/Simple-Tiling.git simple-tiling@domoel
|
||||||
|
```
|
||||||
|
5. **Compile the GSettings schema.** This is a mandatory step for the keyboard shortcuts to work.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Domoel/Simple-Tiling.git
|
cd ~/.local/share/gnome-shell/extensions/simple-tiling@domoel
|
||||||
```
|
|
||||||
2. **Compile the GSettings schema.** This is a mandatory step for the keyboard shortcuts to work.
|
|
||||||
```bash
|
|
||||||
cd ~/.local/share/gnome-shell/extensions/simple-tiling@domoel/
|
|
||||||
glib-compile-schemas schemas/
|
glib-compile-schemas schemas/
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Restart GNOME Shell.** Press `Alt` + `F2`, type `r`, and press `Enter`.
|
3. **Restart GNOME Shell.** Press `Alt` + `F2`, type `r`, and press `Enter`.
|
||||||
4. **Enable the extension** using the GNOME Extensions app or GNOME Tweaks.
|
|
||||||
|
5. **Enable the extension** using the GNOME Extensions app or GNOME Tweaks.
|
||||||
|
|
||||||
|
**Note:** You have to use "simple-tiling@domoel" as your extension folder / directory. Put all necessary files into this directory. Otherwise the extension will not show up in extension manager.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
#### Keyboard Shortcuts
|
#### Keyboard Shortcuts
|
||||||
|
|
||||||
All keyboard shortcuts can be configured through the standard GNOME Settings panel:
|
All keyboard shortcuts can be configured through the Settings panel of Simple Tiling (which can be found in the Gnome Extension Application):
|
||||||
1. Open **Settings**.
|
1. Open **Settings**.
|
||||||
2. Navigate to **Keyboard** -> **View and Customize Shortcuts**.
|
2. Navigate to **Keyboard** -> **View and Customize Shortcuts**.
|
||||||
3. Scroll down to the **Custom Shortcuts** section at the bottom.
|
3. Scroll down to the **Custom Shortcuts** section at the bottom.
|
||||||
@@ -74,3 +94,30 @@ To prevent an application from being tiled, you can add its `WM_CLASS` to the `e
|
|||||||
To find an application's `WM_CLASS`, open a terminal and run the command `xprop WM_CLASS`. Your cursor will turn into a crosshair. Click on the window of the application you want to exclude.
|
To find an application's `WM_CLASS`, open a terminal and run the command `xprop WM_CLASS`. Your cursor will turn into a crosshair. Click on the window of the application you want to exclude.
|
||||||
|
|
||||||
An Example of an exceptions.txt can be found in the repo.
|
An Example of an exceptions.txt can be found in the repo.
|
||||||
|
|
||||||
|
Ignored applications will be opened screen centered and kept above all other windows. These applications can be moved across the screen in floating mode.
|
||||||
|
|
||||||
|
#### Adjusting inner and/or outer Window Gaps / Margins
|
||||||
|
|
||||||
|
You can adjust the window gap margins (inner gaps between windows, outer gaps horizontal as well as vertical) in the Settings panel of Simple Tiling (which can be found in the Gnome Extension Application).
|
||||||
|
|
||||||
|
#### Configurable New Window Behavior
|
||||||
|
|
||||||
|
A toogle setting allows you to control the behavior for newly opened windows. You can choose to either have them become the new master window (pushing the old master into the stack) or have them appended to the stack as the last window (Default).
|
||||||
|
|
||||||
|
#### Adjusting Tiling Window Delays
|
||||||
|
|
||||||
|
If you have race condition issues between mutter (Gnome WM) and the Simple Tiling extension, you can adjust the window delay settings (both for tiling windows as well as for centered application from the exceptions list) directly in the extensions.js (~/.local/share/gnome-shell/extensions/simple-tiling@domoel/extension.js). You will find the parameter at line 17 & 18. Defaults to "20" for General Tiling Window Delay and "5" for centered Apps on the Exception List.
|
||||||
|
|
||||||
|
## Future Development
|
||||||
|
|
||||||
|
This extension was built to solve a specific need. However, future enhancements could include:
|
||||||
|
* Multi-monitor support.
|
||||||
|
* Support for newer Gnome shells
|
||||||
|
* Additional layout algorithms.
|
||||||
|
* A more detailed settings panel to configure other options via a GUI.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the `LICENSE` file for details.
|
||||||
|
|
||||||
|
|||||||
+23
-8
@@ -1,11 +1,26 @@
|
|||||||
# --- Ausnahmeliste für den Tiler ---
|
# --- Exception List for Tiling Windows ---
|
||||||
# Jede Zeile enthält die WM_CLASS einer Anwendung, die ignoriert werden soll.
|
# Each line contains an application identifier (WM_CLASS for X11, or App ID for Wayland)
|
||||||
# Die Groß- und Kleinschreibung wird ignoriert.
|
# that should be ignored by the tiling manager.
|
||||||
|
# For best results, add both identifiers for an application if they differ.
|
||||||
|
# Uppercase and lowercase letters are ignored.
|
||||||
|
|
||||||
# Befehl zum Finden der WM_CLASS:
|
# -----------------------------------------------------------
|
||||||
# 1. Terminal öffnen
|
# Finding the App ID (for Wayland & modern apps)
|
||||||
# 2. 'xprop WM_CLASS' eingeben und Enter drücken
|
# -----------------------------------------------------------
|
||||||
# 3. Mit dem Kreuz auf das gewünschte Fenster klicken
|
# 1. Press Alt + F2, type 'lg', and press Enter.
|
||||||
|
# 2. In the Looking Glass window, click the "Windows" tab.
|
||||||
|
# 3. Click on the desired window to see its details.
|
||||||
|
# 4. Find the value for "app id" and add it to a new line below.
|
||||||
|
|
||||||
gnome-screenshot
|
# -----------------------------------------------------------
|
||||||
|
# Finding the WM_CLASS (for X11)
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# 1. Open a terminal.
|
||||||
|
# 2. Type 'xprop WM_CLASS' and press Enter.
|
||||||
|
# 3. Your cursor will turn into a crosshair. Click on the desired window.
|
||||||
|
# 4. The terminal will output a line like: WM_CLASS(STRING) = "navigator", "Firefox".
|
||||||
|
# 5. Add one of these values (e.g., "firefox") to a new line below.
|
||||||
|
|
||||||
|
# --- Start of the Exception List ---
|
||||||
|
|
||||||
|
ulauncher
|
||||||
|
|||||||
+34
-439
@@ -1,446 +1,41 @@
|
|||||||
// Simple-Tiling – GNOME Shell 3.38 (X11) - Version 1.0
|
/////////////////////////////////////////////////////////////
|
||||||
// Features: Fibonacci-Stack-Layout (50/50) · Tiling-Lock · Drag- & Keyboard-Swap
|
// Simple-Tiling – GLOBAL CONFIG //
|
||||||
// Pop-ups: zentriert + Always-on-Top + Exception-List
|
// © 2025 domoel – MIT //
|
||||||
// © 2025 domoel – MIT
|
//////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
// --- GLOBAL IMPORTS ---
|
||||||
|
'use strict';
|
||||||
|
|
||||||
// Global Imports
|
|
||||||
const Main = imports.ui.main;
|
|
||||||
const Meta = imports.gi.Meta;
|
|
||||||
const Shell = imports.gi.Shell;
|
|
||||||
const Mainloop = imports.mainloop;
|
|
||||||
const Gio = imports.gi.Gio;
|
|
||||||
const GLib = imports.gi.GLib;
|
|
||||||
const ExtensionUtils = imports.misc.extensionUtils;
|
const ExtensionUtils = imports.misc.extensionUtils;
|
||||||
const ByteArray = imports.byteArray;
|
const Config = imports.misc.config;
|
||||||
|
|
||||||
const Me = ExtensionUtils.getCurrentExtension();
|
const Me = ExtensionUtils.getCurrentExtension();
|
||||||
const SCHEMA_NAME = 'org.gnome.shell.extensions.simple-tiling.domoel';
|
const [SHELL_MAJOR] = Config.PACKAGE_VERSION.split('.').map(n => parseInt(n));
|
||||||
const WM_SCHEMA = 'org.gnome.desktop.wm.keybindings';
|
|
||||||
|
|
||||||
// InteractionHandler
|
let extension = null;
|
||||||
// Verwaltung aller Nutzerinteraktionen (Maus & Tastatur)
|
|
||||||
class InteractionHandler {
|
|
||||||
constructor(tiler) {
|
|
||||||
this.tiler = tiler;
|
|
||||||
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
|
||||||
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
|
||||||
this._wmKeysToDisable = [];
|
|
||||||
this._savedWmShortcuts = {};
|
|
||||||
this._grabOpIds = [];
|
|
||||||
this._settingsChangedId = null;
|
|
||||||
|
|
||||||
this._onSettingsChanged = this._onSettingsChanged.bind(this);
|
|
||||||
this._prepareWmShortcuts();
|
|
||||||
}
|
|
||||||
|
|
||||||
enable() {
|
|
||||||
if (this._wmKeysToDisable.length) {
|
|
||||||
this._wmKeysToDisable.forEach(key => this._wmSettings.set_value(key, new GLib.Variant('as', [])));
|
|
||||||
}
|
|
||||||
this._bindAllShortcuts();
|
|
||||||
this._settingsChangedId = this._settings.connect('changed', this._onSettingsChanged);
|
|
||||||
this._grabOpIds.push(global.display.connect('grab-op-begin', (display, screen, window) => {
|
|
||||||
if (this.tiler.windows.includes(window)) { this.tiler.grabbedWindow = window; }
|
|
||||||
}));
|
|
||||||
this._grabOpIds.push(global.display.connect('grab-op-end', this._onGrabEnd.bind(this)));
|
|
||||||
}
|
|
||||||
|
|
||||||
disable() {
|
|
||||||
if (this._wmKeysToDisable.length) {
|
|
||||||
this._wmKeysToDisable.forEach(key => this._wmSettings.set_value(key, this._savedWmShortcuts[key]));
|
|
||||||
}
|
|
||||||
this._unbindAllShortcuts();
|
|
||||||
if (this._settingsChangedId) {
|
|
||||||
this._settings.disconnect(this._settingsChangedId);
|
|
||||||
this._settingsChangedId = null;
|
|
||||||
}
|
|
||||||
this._grabOpIds.forEach(id => global.display.disconnect(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
_bind(key, callback) {
|
|
||||||
Main.wm.addKeybinding(key, this._settings, Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL, callback.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
_bindAllShortcuts() {
|
|
||||||
this._bind('swap-master-window', this._swapWithMaster);
|
|
||||||
this._bind('swap-left-window', () => this._swapInDirection('left'));
|
|
||||||
this._bind('swap-right-window', () => this._swapInDirection('right'));
|
|
||||||
this._bind('swap-up-window', () => this._swapInDirection('up'));
|
|
||||||
this._bind('swap-down-window', () => this._swapInDirection('down'));
|
|
||||||
}
|
|
||||||
|
|
||||||
_unbindAllShortcuts() {
|
|
||||||
['swap-master-window', 'swap-left-window', 'swap-right-window', 'swap-up-window', 'swap-down-window']
|
|
||||||
.forEach(key => Main.wm.removeKeybinding(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSettingsChanged() {
|
|
||||||
this._unbindAllShortcuts();
|
|
||||||
this._bindAllShortcuts();
|
|
||||||
}
|
|
||||||
|
|
||||||
_prepareWmShortcuts() {
|
|
||||||
const schema = this._wmSettings.settings_schema;
|
|
||||||
const keys = [];
|
|
||||||
if (schema.has_key('toggle-tiled-left')) keys.push('toggle-tiled-left', 'toggle-tiled-right');
|
|
||||||
else if (schema.has_key('tile-left')) keys.push('tile-left', 'tile-right');
|
|
||||||
if (schema.has_key('toggle-maximized')) keys.push('toggle-maximized');
|
|
||||||
else {
|
|
||||||
if (schema.has_key('maximize')) keys.push('maximize');
|
|
||||||
if (schema.has_key('unmaximize')) keys.push('unmaximize');
|
|
||||||
}
|
|
||||||
if (keys.length) {
|
|
||||||
this._wmKeysToDisable = keys;
|
|
||||||
keys.forEach(key => this._savedWmShortcuts[key] = this.tiler.wmSettings.get_value(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_swapWithMaster() {
|
|
||||||
const windows = this.tiler.windows;
|
|
||||||
if (windows.length < 2) return;
|
|
||||||
const focusedWindow = global.display.get_focus_window();
|
|
||||||
if (!focusedWindow || !windows.includes(focusedWindow)) return;
|
|
||||||
const focusedIndex = windows.indexOf(focusedWindow);
|
|
||||||
if (focusedIndex > 0) {
|
|
||||||
[windows[0], windows[focusedIndex]] = [windows[focusedIndex], windows[0]];
|
|
||||||
} else if (focusedIndex === 0) {
|
|
||||||
[windows[0], windows[1]] = [windows[1], windows[0]];
|
|
||||||
}
|
|
||||||
this.tiler.tileNow();
|
|
||||||
if (windows.length > 0) windows[0].activate(global.get_current_time());
|
|
||||||
}
|
|
||||||
|
|
||||||
_swapInDirection(direction) {
|
|
||||||
const sourceWindow = global.display.get_focus_window();
|
|
||||||
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return;
|
|
||||||
let targetWindow = null;
|
|
||||||
const sourceIndex = this.tiler.windows.indexOf(sourceWindow);
|
|
||||||
if (sourceIndex === 0 && direction === 'right' && this.tiler.windows.length > 1) {
|
|
||||||
targetWindow = this.tiler.windows[1];
|
|
||||||
} else {
|
|
||||||
targetWindow = this._findTargetInDirection(sourceWindow, direction);
|
|
||||||
}
|
|
||||||
if (!targetWindow) return;
|
|
||||||
const targetIndex = this.tiler.windows.indexOf(targetWindow);
|
|
||||||
[this.tiler.windows[sourceIndex], this.tiler.windows[targetIndex]] = [this.tiler.windows[targetIndex], this.tiler.windows[sourceIndex]];
|
|
||||||
this.tiler.tileNow();
|
|
||||||
sourceWindow.activate(global.get_current_time());
|
|
||||||
}
|
|
||||||
|
|
||||||
_findTargetInDirection(source, direction) {
|
|
||||||
const sourceRect = source.get_frame_rect();
|
|
||||||
let candidates = [];
|
|
||||||
for (const win of this.tiler.windows) {
|
|
||||||
if (win === source) continue;
|
|
||||||
const targetRect = win.get_frame_rect();
|
|
||||||
switch (direction) {
|
|
||||||
case 'left': if (targetRect.x < sourceRect.x) candidates.push(win); break;
|
|
||||||
case 'right': if (targetRect.x > sourceRect.x) candidates.push(win); break;
|
|
||||||
case 'up': if (targetRect.y < sourceRect.y) candidates.push(win); break;
|
|
||||||
case 'down': if (targetRect.y > sourceRect.y) candidates.push(win); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (candidates.length === 0) return null;
|
|
||||||
let bestTarget = null;
|
|
||||||
let minDeviation = Infinity;
|
|
||||||
for (const win of candidates) {
|
|
||||||
const targetRect = win.get_frame_rect();
|
|
||||||
let deviation;
|
|
||||||
if (direction === 'left' || direction === 'right') {
|
|
||||||
deviation = Math.abs(sourceRect.y - targetRect.y);
|
|
||||||
} else {
|
|
||||||
deviation = Math.abs(sourceRect.x - targetRect.x);
|
|
||||||
}
|
|
||||||
if (deviation < minDeviation) {
|
|
||||||
minDeviation = deviation;
|
|
||||||
bestTarget = win;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bestTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onGrabEnd() {
|
|
||||||
const grabbedWindow = this.tiler.grabbedWindow;
|
|
||||||
if (!grabbedWindow) return;
|
|
||||||
const targetWindow = this._findTargetUnderPointer(grabbedWindow);
|
|
||||||
if (targetWindow) {
|
|
||||||
const sourceIndex = this.tiler.windows.indexOf(grabbedWindow);
|
|
||||||
const targetIndex = this.tiler.windows.indexOf(targetWindow);
|
|
||||||
[this.tiler.windows[sourceIndex], this.tiler.windows[targetIndex]] = [this.tiler.windows[targetIndex], this.tiler.windows[sourceIndex]];
|
|
||||||
}
|
|
||||||
this.tiler.queueTile();
|
|
||||||
this.tiler.grabbedWindow = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_findTargetUnderPointer(excludeWindow) {
|
|
||||||
let [pointerX, pointerY] = global.get_pointer();
|
|
||||||
let windows = global.get_window_actors().map(actor => actor.meta_window).filter(win => {
|
|
||||||
if (!win || win === excludeWindow || !this.tiler.windows.includes(win)) return false;
|
|
||||||
let frame = win.get_frame_rect();
|
|
||||||
return pointerX >= frame.x && pointerX < frame.x + frame.width &&
|
|
||||||
pointerY >= frame.y && pointerY < frame.y + frame.height;
|
|
||||||
});
|
|
||||||
if (windows.length > 0) { return windows[windows.length - 1]; }
|
|
||||||
|
|
||||||
let bestTarget = null;
|
|
||||||
let maxOverlap = 0;
|
|
||||||
const sourceFrame = excludeWindow.get_frame_rect();
|
|
||||||
for (const win of this.tiler.windows) {
|
|
||||||
if (win === excludeWindow) continue;
|
|
||||||
const targetFrame = win.get_frame_rect();
|
|
||||||
const overlapX = Math.max(0, Math.min(sourceFrame.x + sourceFrame.width, targetFrame.x + targetFrame.width) - Math.max(sourceFrame.x, targetFrame.x));
|
|
||||||
const overlapY = Math.max(0, Math.min(sourceFrame.y + sourceFrame.height, targetFrame.y + targetFrame.height) - Math.max(sourceFrame.y, targetFrame.y));
|
|
||||||
const overlapArea = overlapX * overlapY;
|
|
||||||
if (overlapArea > maxOverlap) {
|
|
||||||
maxOverlap = overlapArea;
|
|
||||||
bestTarget = win;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bestTarget;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Tiler
|
|
||||||
// Die Hauptklasse für die Tiling-Logik.
|
|
||||||
class Tiler {
|
|
||||||
constructor() {
|
|
||||||
this.windows = [];
|
|
||||||
this.grabbedWindow = null;
|
|
||||||
this.wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
|
||||||
this._signalIds = new Map();
|
|
||||||
this._tileInProgress = false;
|
|
||||||
|
|
||||||
// Layout-Konfiguration
|
|
||||||
this._innerGap = 10;
|
|
||||||
this._outerGapVertical = 5;
|
|
||||||
this._outerGapHorizontal = 10;
|
|
||||||
|
|
||||||
// Delay-Zeiten für das Tiling und Exception Windows
|
|
||||||
this._tilingDelay = 20;
|
|
||||||
this._centeringDelay = 5;
|
|
||||||
|
|
||||||
this._exceptions = [];
|
|
||||||
this._interactionHandler = new InteractionHandler(this);
|
|
||||||
|
|
||||||
this._onWindowAdded = this._onWindowAdded.bind(this);
|
|
||||||
this._onWindowRemoved = this._onWindowRemoved.bind(this);
|
|
||||||
this._onActiveWorkspaceChanged = this._onActiveWorkspaceChanged.bind(this);
|
|
||||||
this._onWindowMinimizedStateChanged = this._onWindowMinimizedStateChanged.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
enable() {
|
|
||||||
this._loadExceptions();
|
|
||||||
const workspaceManager = global.workspace_manager;
|
|
||||||
this._signalIds.set('workspace-changed', { object: workspaceManager, id: workspaceManager.connect('active-workspace-changed', this._onActiveWorkspaceChanged) });
|
|
||||||
this._connectToWorkspace();
|
|
||||||
this._interactionHandler.enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
disable() {
|
|
||||||
this._interactionHandler.disable();
|
|
||||||
this._disconnectFromWorkspace();
|
|
||||||
for (const [, signal] of this._signalIds) {
|
|
||||||
try { signal.object.disconnect(signal.id); } catch(e) {}
|
|
||||||
}
|
|
||||||
this._signalIds.clear();
|
|
||||||
this.windows = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadExceptions() {
|
|
||||||
const file = Gio.file_new_for_path(Me.path + '/exceptions.txt');
|
|
||||||
if (!file.query_exists(null)) { this._exceptions = []; return; }
|
|
||||||
const [ok, data] = file.load_contents(null);
|
|
||||||
this._exceptions = ok ? ByteArray.toString(data).split('\n')
|
|
||||||
.map(l => l.trim()).filter(l => l && !l.startsWith('#'))
|
|
||||||
.map(l => l.toLowerCase()) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
_isException(win) {
|
|
||||||
return !!win && this._exceptions.includes((win.get_wm_class() || '').toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
_isTileable(win) {
|
|
||||||
return win && !win.minimized && !this._isException(win) && win.get_window_type() === Meta.WindowType.NORMAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
_centerWindow(win) {
|
|
||||||
Mainloop.timeout_add(this._centeringDelay, () => {
|
|
||||||
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
|
|
||||||
if (win.get_maximized()) { win.unmaximize(Meta.MaximizeFlags.BOTH); }
|
|
||||||
const monitorIndex = win.get_monitor();
|
|
||||||
const workArea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
|
|
||||||
const frame = win.get_frame_rect();
|
|
||||||
win.move_frame(true,
|
|
||||||
workArea.x + Math.floor((workArea.width - frame.width) / 2),
|
|
||||||
workArea.y + Math.floor((workArea.height - frame.height) / 2));
|
|
||||||
Mainloop.idle_add(() => {
|
|
||||||
if (win.get_display()) {
|
|
||||||
if (typeof win.set_keep_above === 'function') win.set_keep_above(true);
|
|
||||||
else if (typeof win.make_above === 'function') win.make_above();
|
|
||||||
}
|
|
||||||
return GLib.SOURCE_REMOVE;
|
|
||||||
});
|
|
||||||
return GLib.SOURCE_REMOVE;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_onWindowMinimizedStateChanged() {
|
|
||||||
this.queueTile();
|
|
||||||
}
|
|
||||||
|
|
||||||
_onWindowAdded(workspace, win) {
|
|
||||||
if (this.windows.includes(win)) return;
|
|
||||||
|
|
||||||
if (this._isException(win)) {
|
|
||||||
this._centerWindow(win);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._isTileable(win)) {
|
|
||||||
this.windows.push(win);
|
|
||||||
const id = win.get_id();
|
|
||||||
this._signalIds.set(`unmanaged-${id}`, { object: win, id: win.connect('unmanaged', () => this._onWindowRemoved(null, win)) });
|
|
||||||
this._signalIds.set(`size-changed-${id}`, { object: win, id: win.connect('size-changed', () => { if (!this.grabbedWindow) this.queueTile(); }) });
|
|
||||||
this._signalIds.set(`minimized-${id}`, { object: win, id: win.connect('notify::minimized', this._onWindowMinimizedStateChanged) });
|
|
||||||
this.queueTile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onWindowRemoved(workspace, win) {
|
|
||||||
const index = this.windows.indexOf(win);
|
|
||||||
if (index > -1) {
|
|
||||||
this.windows.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
['unmanaged', 'size-changed', 'minimized'].forEach(prefix => {
|
|
||||||
const key = `${prefix}-${win.get_id()}`;
|
|
||||||
if (this._signalIds.has(key)) {
|
|
||||||
const { object, id } = this._signalIds.get(key);
|
|
||||||
try { object.disconnect(id); } catch(e) {}
|
|
||||||
this._signalIds.delete(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.queueTile();
|
|
||||||
}
|
|
||||||
|
|
||||||
_onActiveWorkspaceChanged() {
|
|
||||||
this._disconnectFromWorkspace();
|
|
||||||
this._connectToWorkspace();
|
|
||||||
}
|
|
||||||
|
|
||||||
_connectToWorkspace() {
|
|
||||||
const workspace = global.workspace_manager.get_active_workspace();
|
|
||||||
workspace.list_windows().forEach(win => this._onWindowAdded(workspace, win));
|
|
||||||
this._signalIds.set('window-added', { object: workspace, id: workspace.connect('window-added', this._onWindowAdded) });
|
|
||||||
this._signalIds.set('window-removed', { object: workspace, id: workspace.connect('window-removed', this._onWindowRemoved) });
|
|
||||||
this.queueTile();
|
|
||||||
}
|
|
||||||
|
|
||||||
_disconnectFromWorkspace() {
|
|
||||||
this.windows.slice().forEach(win => this._onWindowRemoved(null, win));
|
|
||||||
|
|
||||||
['window-added', 'window-removed'].forEach(key => {
|
|
||||||
if (this._signalIds.has(key)) {
|
|
||||||
const { object, id } = this._signalIds.get(key);
|
|
||||||
try { object.disconnect(id); } catch(e) {}
|
|
||||||
this._signalIds.delete(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
queueTile() {
|
|
||||||
if (this._tileInProgress) return;
|
|
||||||
this._tileInProgress = true;
|
|
||||||
Mainloop.timeout_add(this._tilingDelay, () => {
|
|
||||||
this._tileWindows();
|
|
||||||
this._tileInProgress = false;
|
|
||||||
return GLib.SOURCE_REMOVE;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
tileNow() {
|
|
||||||
if (!this._tileInProgress) {
|
|
||||||
this._tileWindows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_splitLayout(windows, area) {
|
|
||||||
if (windows.length === 0) return;
|
|
||||||
if (windows.length === 1) {
|
|
||||||
windows[0].move_resize_frame(true, area.x, area.y, area.width, area.height);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gap = Math.floor(this._innerGap / 2);
|
|
||||||
const primaryWindows = [windows[0]];
|
|
||||||
const secondaryWindows = windows.slice(1);
|
|
||||||
let primaryArea, secondaryArea;
|
|
||||||
|
|
||||||
if (area.width > area.height) {
|
|
||||||
const primaryWidth = Math.floor(area.width / 2) - gap;
|
|
||||||
primaryArea = { x: area.x, y: area.y, width: primaryWidth, height: area.height };
|
|
||||||
secondaryArea = { x: area.x + primaryWidth + this._innerGap, y: area.y, width: area.width - primaryWidth - this._innerGap, height: area.height };
|
|
||||||
} else {
|
|
||||||
const primaryHeight = Math.floor(area.height / 2) - gap;
|
|
||||||
primaryArea = { x: area.x, y: area.y, width: area.width, height: primaryHeight };
|
|
||||||
secondaryArea = { x: area.x, y: area.y + primaryHeight + this._innerGap, width: area.width, height: area.height - primaryHeight - this._innerGap };
|
|
||||||
}
|
|
||||||
|
|
||||||
this._splitLayout(primaryWindows, primaryArea);
|
|
||||||
this._splitLayout(secondaryWindows, secondaryArea);
|
|
||||||
}
|
|
||||||
|
|
||||||
_tileWindows() {
|
|
||||||
const windowsToTile = this.windows.filter(win => !win.minimized);
|
|
||||||
if (windowsToTile.length === 0) return;
|
|
||||||
const monitor = Main.layoutManager.primaryMonitor;
|
|
||||||
const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor.index);
|
|
||||||
const innerArea = {
|
|
||||||
x: workArea.x + this._outerGapHorizontal,
|
|
||||||
y: workArea.y + this._outerGapVertical,
|
|
||||||
width: workArea.width - 2 * this._outerGapHorizontal,
|
|
||||||
height: workArea.height - 2 * this._outerGapVertical
|
|
||||||
};
|
|
||||||
|
|
||||||
windowsToTile.forEach(win => { if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH); });
|
|
||||||
|
|
||||||
if (windowsToTile.length === 1) {
|
|
||||||
windowsToTile[0].move_resize_frame(true, innerArea.x, innerArea.y, innerArea.width, innerArea.height);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gap = Math.floor(this._innerGap / 2);
|
|
||||||
const masterWidth = Math.floor(innerArea.width / 2) - gap;
|
|
||||||
const master = windowsToTile[0];
|
|
||||||
master.move_resize_frame(true, innerArea.x, innerArea.y, masterWidth, innerArea.height);
|
|
||||||
const stackArea = {
|
|
||||||
x: innerArea.x + masterWidth + this._innerGap,
|
|
||||||
y: innerArea.y,
|
|
||||||
width: innerArea.width - masterWidth - this._innerGap,
|
|
||||||
height: innerArea.height
|
|
||||||
};
|
|
||||||
this._splitLayout(windowsToTile.slice(1), stackArea);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extension Wrapper
|
|
||||||
class SimpleTilingExtension {
|
|
||||||
constructor() {
|
|
||||||
this.tiler = null;
|
|
||||||
}
|
|
||||||
enable() {
|
|
||||||
this.tiler = new Tiler();
|
|
||||||
this.tiler.enable();
|
|
||||||
}
|
|
||||||
disable() {
|
|
||||||
if (this.tiler) {
|
|
||||||
this.tiler.disable();
|
|
||||||
this.tiler = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
return new SimpleTilingExtension();
|
}
|
||||||
|
|
||||||
|
// --- SHELL SWITCH ---
|
||||||
|
async function enable() {
|
||||||
|
try {
|
||||||
|
if (SHELL_MAJOR >= 40) {
|
||||||
|
const module = await import('./modern.js');
|
||||||
|
extension = new module.default(Me.metadata);
|
||||||
|
} else {
|
||||||
|
const { LegacyExtension } = Me.imports.legacy;
|
||||||
|
extension = new LegacyExtension(Me.metadata);
|
||||||
|
}
|
||||||
|
extension.enable();
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, `[Simple Tiling] Failed to enable extension`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disable() {
|
||||||
|
if (extension) {
|
||||||
|
extension.disable();
|
||||||
|
extension = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,682 @@
|
|||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
// Simple-Tiling – LEGACY (for GNOME Shell 3.38) //
|
||||||
|
// © 2025 domoel – MIT //
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
// --- GLOBAL IMPORTS ---
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Main = imports.ui.main;
|
||||||
|
const Meta = imports.gi.Meta;
|
||||||
|
const Shell = imports.gi.Shell;
|
||||||
|
const Gio = imports.gi.Gio;
|
||||||
|
const GLib = imports.gi.GLib;
|
||||||
|
const ExtensionUtils = imports.misc.extensionUtils;
|
||||||
|
const ByteArray = imports.byteArray;
|
||||||
|
|
||||||
|
const Me = ExtensionUtils.getCurrentExtension();
|
||||||
|
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
|
||||||
|
const WM_SCHEMA = "org.gnome.desktop.wm.keybindings";
|
||||||
|
|
||||||
|
const TILING_DELAY_MS = 20; // Change Tiling Window Delay
|
||||||
|
const CENTERING_DELAY_MS = 5; // Change Centered Window Delay
|
||||||
|
|
||||||
|
const KEYBINDINGS = {
|
||||||
|
"swap-master-window": (self) => self._swapWithMaster(),
|
||||||
|
"swap-left-window": (self) => self._swapInDirection("left"),
|
||||||
|
"swap-right-window": (self) => self._swapInDirection("right"),
|
||||||
|
"swap-up-window": (self) => self._swapInDirection("up"),
|
||||||
|
"swap-down-window": (self) => self._swapInDirection("down"),
|
||||||
|
"focus-left": (self) => self._focusInDirection("left"),
|
||||||
|
"focus-right": (self) => self._focusInDirection("right"),
|
||||||
|
"focus-up": (self) => self._focusInDirection("up"),
|
||||||
|
"focus-down": (self) => self._focusInDirection("down"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- INTERACTIONHANDLER ---
|
||||||
|
class InteractionHandler {
|
||||||
|
constructor(tiler) {
|
||||||
|
this.tiler = tiler;
|
||||||
|
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
||||||
|
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
||||||
|
this._wmKeysToDisable = [];
|
||||||
|
this._savedWmShortcuts = {};
|
||||||
|
this._grabOpIds = [];
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
this._onSettingsChanged = this._onSettingsChanged.bind(this);
|
||||||
|
this._prepareWmShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
if (this._wmKeysToDisable.length) {
|
||||||
|
this._wmKeysToDisable.forEach((key) =>
|
||||||
|
this._wmSettings.set_value(key, new GLib.Variant("as", []))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
this._settingsChangedId = this._settings.connect(
|
||||||
|
"changed",
|
||||||
|
this._onSettingsChanged
|
||||||
|
);
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect(
|
||||||
|
"grab-op-begin",
|
||||||
|
(display, screen, window) => {
|
||||||
|
if (this.tiler.windows.includes(window)) {
|
||||||
|
this.tiler.grabbedWindow = window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect("grab-op-end", this._onGrabEnd.bind(this))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._wmKeysToDisable.length) {
|
||||||
|
this._wmKeysToDisable.forEach((key) =>
|
||||||
|
this._wmSettings.set_value(key, this._savedWmShortcuts[key])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
if (this._settingsChangedId) {
|
||||||
|
this._settings.disconnect(this._settingsChangedId);
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
}
|
||||||
|
this._grabOpIds.forEach((id) => global.display.disconnect(id));
|
||||||
|
this._grabOpIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_bind(key, callback) {
|
||||||
|
Main.wm.addKeybinding(key, this._settings, Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL,
|
||||||
|
() => callback(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindAllShortcuts() {
|
||||||
|
for (const [key, handler] of Object.entries(KEYBINDINGS)) {
|
||||||
|
this._bind(key, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_unbindAllShortcuts() {
|
||||||
|
for (const key in KEYBINDINGS) {
|
||||||
|
Main.wm.removeKeybinding(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
_prepareWmShortcuts() {
|
||||||
|
const schema = this._wmSettings.settings_schema;
|
||||||
|
const keys = [];
|
||||||
|
if (schema.has_key("toggle-tiled-left"))
|
||||||
|
keys.push("toggle-tiled-left", "toggle-tiled-right");
|
||||||
|
else if (schema.has_key("tile-left"))
|
||||||
|
keys.push("tile-left", "tile-right");
|
||||||
|
if (schema.has_key("toggle-maximized")) keys.push("toggle-maximized");
|
||||||
|
else {
|
||||||
|
if (schema.has_key("maximize")) keys.push("maximize");
|
||||||
|
if (schema.has_key("unmaximize")) keys.push("unmaximize");
|
||||||
|
}
|
||||||
|
if (keys.length) {
|
||||||
|
this._wmKeysToDisable = keys;
|
||||||
|
keys.forEach(
|
||||||
|
(key) =>
|
||||||
|
(this._savedWmShortcuts[
|
||||||
|
key
|
||||||
|
] = this._wmSettings.get_value(key))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusInDirection(direction) {
|
||||||
|
const sourceWindow = global.display.get_focus_window();
|
||||||
|
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return;
|
||||||
|
|
||||||
|
const targetWindow = this._findTargetInDirection(
|
||||||
|
sourceWindow,
|
||||||
|
direction
|
||||||
|
);
|
||||||
|
if (targetWindow) {
|
||||||
|
targetWindow.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapWithMaster() {
|
||||||
|
const windows = this.tiler.windows;
|
||||||
|
if (windows.length < 2) return;
|
||||||
|
const focusedWindow = global.display.get_focus_window();
|
||||||
|
if (!focusedWindow || !windows.includes(focusedWindow)) return;
|
||||||
|
const focusedIndex = windows.indexOf(focusedWindow);
|
||||||
|
if (focusedIndex > 0) {
|
||||||
|
[windows[0], windows[focusedIndex]] = [
|
||||||
|
windows[focusedIndex],
|
||||||
|
windows[0],
|
||||||
|
];
|
||||||
|
} else if (focusedIndex === 0) {
|
||||||
|
[windows[0], windows[1]] = [windows[1], windows[0]];
|
||||||
|
}
|
||||||
|
this.tiler.tileNow();
|
||||||
|
if (windows.length > 0) windows[0].activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapInDirection(direction) {
|
||||||
|
const sourceWindow = global.display.get_focus_window();
|
||||||
|
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return;
|
||||||
|
let targetWindow = null;
|
||||||
|
const sourceIndex = this.tiler.windows.indexOf(sourceWindow);
|
||||||
|
if (
|
||||||
|
sourceIndex === 0 &&
|
||||||
|
direction === "right" &&
|
||||||
|
this.tiler.windows.length > 1
|
||||||
|
) {
|
||||||
|
targetWindow = this.tiler.windows[1];
|
||||||
|
} else {
|
||||||
|
targetWindow = this._findTargetInDirection(sourceWindow, direction);
|
||||||
|
}
|
||||||
|
if (!targetWindow) return;
|
||||||
|
const targetIndex = this.tiler.windows.indexOf(targetWindow);
|
||||||
|
[this.tiler.windows[sourceIndex], this.tiler.windows[targetIndex]] = [
|
||||||
|
this.tiler.windows[targetIndex],
|
||||||
|
this.tiler.windows[sourceIndex],
|
||||||
|
];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
sourceWindow.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetInDirection(source, direction) {
|
||||||
|
const sourceRect = source.get_frame_rect();
|
||||||
|
let candidates = [];
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win === source) continue;
|
||||||
|
const targetRect = win.get_frame_rect();
|
||||||
|
switch (direction) {
|
||||||
|
case "left":
|
||||||
|
if (targetRect.x < sourceRect.x) candidates.push(win);
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
if (targetRect.x > sourceRect.x) candidates.push(win);
|
||||||
|
break;
|
||||||
|
case "up":
|
||||||
|
if (targetRect.y < sourceRect.y) candidates.push(win);
|
||||||
|
break;
|
||||||
|
case "down":
|
||||||
|
if (targetRect.y > sourceRect.y) candidates.push(win);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
let bestTarget = null;
|
||||||
|
let minDeviation = Infinity;
|
||||||
|
for (const win of candidates) {
|
||||||
|
const targetRect = win.get_frame_rect();
|
||||||
|
let deviation;
|
||||||
|
if (direction === "left" || direction === "right") {
|
||||||
|
deviation = Math.abs(sourceRect.y - targetRect.y);
|
||||||
|
} else {
|
||||||
|
deviation = Math.abs(sourceRect.x - targetRect.x);
|
||||||
|
}
|
||||||
|
if (deviation < minDeviation) {
|
||||||
|
minDeviation = deviation;
|
||||||
|
bestTarget = win;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onGrabEnd() {
|
||||||
|
const grabbedWindow = this.tiler.grabbedWindow;
|
||||||
|
if (!grabbedWindow) return;
|
||||||
|
const targetWindow = this._findTargetUnderPointer(grabbedWindow);
|
||||||
|
if (targetWindow) {
|
||||||
|
const sourceIndex = this.tiler.windows.indexOf(grabbedWindow);
|
||||||
|
const targetIndex = this.tiler.windows.indexOf(targetWindow);
|
||||||
|
[
|
||||||
|
this.tiler.windows[sourceIndex],
|
||||||
|
this.tiler.windows[targetIndex],
|
||||||
|
] = [
|
||||||
|
this.tiler.windows[targetIndex],
|
||||||
|
this.tiler.windows[sourceIndex],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
this.tiler.queueTile();
|
||||||
|
this.tiler.grabbedWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetUnderPointer(excludeWindow) {
|
||||||
|
let [pointerX, pointerY] = global.get_pointer();
|
||||||
|
let windows = global
|
||||||
|
.get_window_actors()
|
||||||
|
.map((actor) => actor.meta_window)
|
||||||
|
.filter((win) => {
|
||||||
|
if (
|
||||||
|
!win ||
|
||||||
|
win === excludeWindow ||
|
||||||
|
!this.tiler.windows.includes(win)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
let frame = win.get_frame_rect();
|
||||||
|
return (
|
||||||
|
pointerX >= frame.x &&
|
||||||
|
pointerX < frame.x + frame.width &&
|
||||||
|
pointerY >= frame.y &&
|
||||||
|
pointerY < frame.y + frame.height
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (windows.length > 0) {
|
||||||
|
return windows[windows.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
let bestTarget = null;
|
||||||
|
let maxOverlap = 0;
|
||||||
|
const sourceFrame = excludeWindow.get_frame_rect();
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win === excludeWindow) continue;
|
||||||
|
const targetFrame = win.get_frame_rect();
|
||||||
|
const overlapX = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
sourceFrame.x + sourceFrame.width,
|
||||||
|
targetFrame.x + targetFrame.width
|
||||||
|
) - Math.max(sourceFrame.x, targetFrame.x)
|
||||||
|
);
|
||||||
|
const overlapY = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
sourceFrame.y + sourceFrame.height,
|
||||||
|
targetFrame.y + targetFrame.height
|
||||||
|
) - Math.max(sourceFrame.y, targetFrame.y)
|
||||||
|
);
|
||||||
|
const overlapArea = overlapX * overlapY;
|
||||||
|
if (overlapArea > maxOverlap) {
|
||||||
|
maxOverlap = overlapArea;
|
||||||
|
bestTarget = win;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TILER ---
|
||||||
|
class Tiler {
|
||||||
|
constructor() {
|
||||||
|
this.windows = [];
|
||||||
|
this.grabbedWindow = null;
|
||||||
|
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
||||||
|
this._signalIds = new Map();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
|
||||||
|
this._innerGap = this._settings.get_int("inner-gap");
|
||||||
|
this._outerGapVertical = this._settings.get_int("outer-gap-vertical");
|
||||||
|
this._outerGapHorizontal = this._settings.get_int("outer-gap-horizontal");
|
||||||
|
|
||||||
|
this._tilingDelay = TILING_DELAY_MS;
|
||||||
|
this._centeringDelay = CENTERING_DELAY_MS;
|
||||||
|
|
||||||
|
this._exceptions = [];
|
||||||
|
this._interactionHandler = new InteractionHandler(this);
|
||||||
|
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
this._centerTimeoutIds = [];
|
||||||
|
|
||||||
|
this._onWindowAdded = this._onWindowAdded.bind(this);
|
||||||
|
this._onWindowRemoved = this._onWindowRemoved.bind(this);
|
||||||
|
this._onActiveWorkspaceChanged = this._onActiveWorkspaceChanged.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
|
this._onWindowMinimizedStateChanged = this._onWindowMinimizedStateChanged.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
|
this._onSettingsChanged = this._onSettingsChanged.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._loadExceptions();
|
||||||
|
const workspaceManager = global.workspace_manager;
|
||||||
|
this._signalIds.set("workspace-changed", {
|
||||||
|
object: workspaceManager,
|
||||||
|
id: workspaceManager.connect(
|
||||||
|
"active-workspace-changed",
|
||||||
|
this._onActiveWorkspaceChanged
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this._connectToWorkspace();
|
||||||
|
this._interactionHandler.enable();
|
||||||
|
this._signalIds.set("settings-changed", {
|
||||||
|
object: this._settings,
|
||||||
|
id: this._settings.connect("changed", this._onSettingsChanged),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._tileTimeoutId) {
|
||||||
|
GLib.source_remove(this._tileTimeoutId);
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
}
|
||||||
|
this._centerTimeoutIds.forEach(id => GLib.source_remove(id));
|
||||||
|
this._centerTimeoutIds = [];
|
||||||
|
|
||||||
|
this._interactionHandler.disable();
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
for (const [, signal] of this._signalIds) {
|
||||||
|
try {
|
||||||
|
signal.object.disconnect(signal.id);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
this._signalIds.clear();
|
||||||
|
this.windows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._innerGap = this._settings.get_int("inner-gap");
|
||||||
|
this._outerGapVertical = this._settings.get_int("outer-gap-vertical");
|
||||||
|
this._outerGapHorizontal = this._settings.get_int("outer-gap-horizontal");
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadExceptions() {
|
||||||
|
const file = Gio.file_new_for_path(Me.path + "/exceptions.txt");
|
||||||
|
if (!file.query_exists(null)) {
|
||||||
|
this._exceptions = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [ok, data] = file.load_contents(null);
|
||||||
|
this._exceptions = ok
|
||||||
|
? ByteArray.toString(data)
|
||||||
|
.split("\n")
|
||||||
|
.map((l) => l.trim())
|
||||||
|
.filter((l) => l && !l.startsWith("#"))
|
||||||
|
.map((l) => l.toLowerCase())
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_isException(win) {
|
||||||
|
if (!win) return false;
|
||||||
|
const wmClass = (win.get_wm_class() || "").toLowerCase();
|
||||||
|
const appId = (win.get_gtk_application_id() || "").toLowerCase();
|
||||||
|
return this._exceptions.includes(wmClass) || this._exceptions.includes(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTileable(win) {
|
||||||
|
return (
|
||||||
|
win &&
|
||||||
|
!win.minimized &&
|
||||||
|
!this._isException(win) &&
|
||||||
|
win.get_window_type() === Meta.WindowType.NORMAL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_centerWindow(win) {
|
||||||
|
const timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._centeringDelay, () => {
|
||||||
|
const index = this._centerTimeoutIds.indexOf(timeoutId);
|
||||||
|
if (index > -1) {
|
||||||
|
this._centerTimeoutIds.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
|
||||||
|
if (win.get_maximized()) {
|
||||||
|
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
}
|
||||||
|
const monitorIndex = win.get_monitor();
|
||||||
|
const workArea = Main.layoutManager.getWorkAreaForMonitor(
|
||||||
|
monitorIndex
|
||||||
|
);
|
||||||
|
const frame = win.get_frame_rect();
|
||||||
|
win.move_frame(
|
||||||
|
true,
|
||||||
|
workArea.x + Math.floor((workArea.width - frame.width) / 2),
|
||||||
|
workArea.y + Math.floor((workArea.height - frame.height) / 2)
|
||||||
|
);
|
||||||
|
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||||
|
if (win.get_display()) {
|
||||||
|
if (typeof win.set_keep_above === "function")
|
||||||
|
win.set_keep_above(true);
|
||||||
|
else if (typeof win.make_above === "function")
|
||||||
|
win.make_above();
|
||||||
|
}
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
});
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._centerTimeoutIds.push(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowMinimizedStateChanged() {
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowAdded(workspace, win) {
|
||||||
|
if (this.windows.includes(win)) return;
|
||||||
|
|
||||||
|
if (this._isException(win)) {
|
||||||
|
this._centerWindow(win);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._isTileable(win)) {
|
||||||
|
if (this._settings.get_string("new-window-behavior") === "master") {
|
||||||
|
this.windows.unshift(win);
|
||||||
|
} else {
|
||||||
|
this.windows.push(win);
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = win.get_id();
|
||||||
|
this._signalIds.set(`unmanaged-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("unmanaged", () =>
|
||||||
|
this._onWindowRemoved(null, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this._signalIds.set(`size-changed-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("size-changed", () => {
|
||||||
|
if (!this.grabbedWindow) this.queueTile();
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
this._signalIds.set(`minimized-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect(
|
||||||
|
"notify::minimized",
|
||||||
|
this._onWindowMinimizedStateChanged
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowRemoved(workspace, win) {
|
||||||
|
const index = this.windows.indexOf(win);
|
||||||
|
if (index > -1) {
|
||||||
|
this.windows.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
["unmanaged", "size-changed", "minimized"].forEach((prefix) => {
|
||||||
|
const key = `${prefix}-${win.get_id()}`;
|
||||||
|
if (this._signalIds.has(key)) {
|
||||||
|
const { object, id } = this._signalIds.get(key);
|
||||||
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
|
this._signalIds.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onActiveWorkspaceChanged() {
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
this._connectToWorkspace();
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectToWorkspace() {
|
||||||
|
const workspace = global.workspace_manager.get_active_workspace();
|
||||||
|
workspace
|
||||||
|
.list_windows()
|
||||||
|
.forEach((win) => this._onWindowAdded(workspace, win));
|
||||||
|
this._signalIds.set("window-added", {
|
||||||
|
object: workspace,
|
||||||
|
id: workspace.connect("window-added", this._onWindowAdded),
|
||||||
|
});
|
||||||
|
this._signalIds.set("window-removed", {
|
||||||
|
object: workspace,
|
||||||
|
id: workspace.connect("window-removed", this._onWindowRemoved),
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disconnectFromWorkspace() {
|
||||||
|
this.windows.slice().forEach((win) => this._onWindowRemoved(null, win));
|
||||||
|
|
||||||
|
["window-added", "window-removed"].forEach((key) => {
|
||||||
|
if (this._signalIds.has(key)) {
|
||||||
|
const { object, id } = this._signalIds.get(key);
|
||||||
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
|
this._signalIds.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queueTile() {
|
||||||
|
if (this._tileInProgress || this._tileTimeoutId) return;
|
||||||
|
this._tileInProgress = true;
|
||||||
|
|
||||||
|
this._tileTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._tilingDelay, () => {
|
||||||
|
this._tileWindows();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tileNow() {
|
||||||
|
if (!this._tileInProgress) {
|
||||||
|
this._tileWindows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_splitLayout(windows, area) {
|
||||||
|
if (windows.length === 0) return;
|
||||||
|
if (windows.length === 1) {
|
||||||
|
windows[0].move_resize_frame(
|
||||||
|
true,
|
||||||
|
area.x,
|
||||||
|
area.y,
|
||||||
|
area.width,
|
||||||
|
area.height
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
|
const primaryWindows = [windows[0]];
|
||||||
|
const secondaryWindows = windows.slice(1);
|
||||||
|
let primaryArea, secondaryArea;
|
||||||
|
|
||||||
|
if (area.width > area.height) {
|
||||||
|
const primaryWidth = Math.floor(area.width / 2) - gap;
|
||||||
|
primaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
width: primaryWidth,
|
||||||
|
height: area.height,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x + primaryWidth + this._innerGap,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width - primaryWidth - this._innerGap,
|
||||||
|
height: area.height,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const primaryHeight = Math.floor(area.height / 2) - gap;
|
||||||
|
primaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width,
|
||||||
|
height: primaryHeight,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y + primaryHeight + this._innerGap,
|
||||||
|
width: area.width,
|
||||||
|
height: area.height - primaryHeight - this._innerGap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this._splitLayout(primaryWindows, primaryArea);
|
||||||
|
this._splitLayout(secondaryWindows, secondaryArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tileWindows() {
|
||||||
|
const windowsToTile = this.windows.filter((win) => !win.minimized);
|
||||||
|
if (windowsToTile.length === 0) return;
|
||||||
|
const monitor = Main.layoutManager.primaryMonitor;
|
||||||
|
const workArea = Main.layoutManager.getWorkAreaForMonitor(
|
||||||
|
monitor.index
|
||||||
|
);
|
||||||
|
const innerArea = {
|
||||||
|
x: workArea.x + this._outerGapHorizontal,
|
||||||
|
y: workArea.y + this._outerGapVertical,
|
||||||
|
width: workArea.width - 2 * this._outerGapHorizontal,
|
||||||
|
height: workArea.height - 2 * this._outerGapVertical,
|
||||||
|
};
|
||||||
|
|
||||||
|
windowsToTile.forEach((win) => {
|
||||||
|
if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (windowsToTile.length === 1) {
|
||||||
|
windowsToTile[0].move_resize_frame(
|
||||||
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
innerArea.width,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
|
const masterWidth = Math.floor(innerArea.width / 2) - gap;
|
||||||
|
const master = windowsToTile[0];
|
||||||
|
master.move_resize_frame(
|
||||||
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
masterWidth,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
|
const stackArea = {
|
||||||
|
x: innerArea.x + masterWidth + this._innerGap,
|
||||||
|
y: innerArea.y,
|
||||||
|
width: innerArea.width - masterWidth - this._innerGap,
|
||||||
|
height: innerArea.height,
|
||||||
|
};
|
||||||
|
this._splitLayout(windowsToTile.slice(1), stackArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- EXTENSION WRAPPER ---
|
||||||
|
var LegacyExtension = class {
|
||||||
|
constructor() {
|
||||||
|
this.tiler = null;
|
||||||
|
}
|
||||||
|
enable() {
|
||||||
|
this.tiler = new Tiler();
|
||||||
|
this.tiler.enable();
|
||||||
|
}
|
||||||
|
disable() {
|
||||||
|
if (this.tiler) {
|
||||||
|
this.tiler.disable();
|
||||||
|
this.tiler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
+9
-3
@@ -1,9 +1,15 @@
|
|||||||
{
|
{
|
||||||
"uuid": "simple-tiling@domoel",
|
"uuid": "simple-tiling@domoel",
|
||||||
"name": "Simple Tiling",
|
"name": "Simple Tiling",
|
||||||
"description": "A Simple Tiling Extension for Gnome Shell 3.38.",
|
"description": "A Simple Tiling Extension for Gnome Shell.",
|
||||||
"version": 1,
|
"version": 6,
|
||||||
"shell-version": [ "3.38" ],
|
"shell-version": [
|
||||||
|
"3.38",
|
||||||
|
"45",
|
||||||
|
"46",
|
||||||
|
"47",
|
||||||
|
"48"
|
||||||
|
],
|
||||||
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
|
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
|
||||||
"preferences_ui": "prefs.js",
|
"preferences_ui": "prefs.js",
|
||||||
"url": "https://github.com/Domoel/Simple-Tiling",
|
"url": "https://github.com/Domoel/Simple-Tiling",
|
||||||
|
|||||||
@@ -0,0 +1,693 @@
|
|||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
// Simple-Tiling – MODERN (for GNOME Shell 40+) //
|
||||||
|
// © 2025 domoel – MIT //
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
// --- GLOBAL IMPORTS ---
|
||||||
|
import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
|
||||||
|
import * as Main from "resource:///org/gnome/shell/ui/main.js";
|
||||||
|
import Meta from "gi://Meta";
|
||||||
|
import Shell from "gi://Shell";
|
||||||
|
import Gio from "gi://Gio";
|
||||||
|
import GLib from "gi://GLib";
|
||||||
|
|
||||||
|
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
|
||||||
|
const WM_SCHEMA = "org.gnome.desktop.wm.keybindings";
|
||||||
|
|
||||||
|
const TILING_DELAY_MS = 20; // Change Tiling Window Delay
|
||||||
|
const CENTERING_DELAY_MS = 5; // Change Centered Window Delay
|
||||||
|
|
||||||
|
const KEYBINDINGS = {
|
||||||
|
"swap-master-window": (self) => self._swapWithMaster(),
|
||||||
|
"swap-left-window": (self) => self._swapInDirection("left"),
|
||||||
|
"swap-right-window": (self) => self._swapInDirection("right"),
|
||||||
|
"swap-up-window": (self) => self._swapInDirection("up"),
|
||||||
|
"swap-down-window": (self) => self._swapInDirection("down"),
|
||||||
|
"focus-left": (self) => self._focusInDirection("left"),
|
||||||
|
"focus-right": (self) => self._focusInDirection("right"),
|
||||||
|
"focus-up": (self) => self._focusInDirection("up"),
|
||||||
|
"focus-down": (self) => self._focusInDirection("down"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- INTERACTIONHANDLER ---
|
||||||
|
class InteractionHandler {
|
||||||
|
constructor(tiler) {
|
||||||
|
this.tiler = tiler;
|
||||||
|
this._settings = this.tiler.settings;
|
||||||
|
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
|
||||||
|
this._wmKeysToDisable = [];
|
||||||
|
this._savedWmShortcuts = {};
|
||||||
|
this._grabOpIds = [];
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._prepareWmShortcuts();
|
||||||
|
if (this._wmKeysToDisable.length) {
|
||||||
|
this._wmKeysToDisable.forEach((key) =>
|
||||||
|
this._wmSettings.set_value(key, new GLib.Variant("as", []))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
this._settingsChangedId = this._settings.connect("changed", () =>
|
||||||
|
this._onSettingsChanged()
|
||||||
|
);
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect(
|
||||||
|
"grab-op-begin",
|
||||||
|
(display, screen, window) => {
|
||||||
|
if (this.tiler.windows.includes(window)) {
|
||||||
|
this.tiler.grabbedWindow = window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect("grab-op-end", () => this._onGrabEnd())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._wmKeysToDisable.length) {
|
||||||
|
this._wmKeysToDisable.forEach((key) =>
|
||||||
|
this._wmSettings.set_value(key, this._savedWmShortcuts[key])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
if (this._settingsChangedId) {
|
||||||
|
this._settings.disconnect(this._settingsChangedId);
|
||||||
|
this._settingsChangedId = null;
|
||||||
|
}
|
||||||
|
this._grabOpIds.forEach((id) => global.display.disconnect(id));
|
||||||
|
this._grabOpIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_bind(key, callback) {
|
||||||
|
global.display.add_keybinding(
|
||||||
|
key,
|
||||||
|
this._settings,
|
||||||
|
Meta.KeyBindingFlags.NONE,
|
||||||
|
Shell.ActionMode.NORMAL,
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindAllShortcuts() {
|
||||||
|
for (const [key, handler] of Object.entries(KEYBINDINGS)) {
|
||||||
|
this._bind(key, () => handler(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_unbindAllShortcuts() {
|
||||||
|
for (const key in KEYBINDINGS) {
|
||||||
|
global.display.remove_keybinding(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
_prepareWmShortcuts() {
|
||||||
|
const schema = this._wmSettings
|
||||||
|
.get_schema_source()
|
||||||
|
.lookup(WM_SCHEMA, true);
|
||||||
|
if (!schema) return;
|
||||||
|
|
||||||
|
const addKeyIfExists = (keys, key) => {
|
||||||
|
if (schema.has_key(key)) keys.push(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const keys = [];
|
||||||
|
if (schema.has_key("toggle-tiled-left")) {
|
||||||
|
keys.push("toggle-tiled-left", "toggle-tiled-right");
|
||||||
|
} else {
|
||||||
|
addKeyIfExists(keys, "tile-left");
|
||||||
|
addKeyIfExists(keys, "tile-right");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.has_key("toggle-maximized")) {
|
||||||
|
keys.push("toggle-maximized");
|
||||||
|
} else {
|
||||||
|
addKeyIfExists(keys, "maximize");
|
||||||
|
addKeyIfExists(keys, "unmaximize");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.length) {
|
||||||
|
this._wmKeysToDisable = keys;
|
||||||
|
keys.forEach(
|
||||||
|
(key) =>
|
||||||
|
(this._savedWmShortcuts[key] = this._wmSettings.get_value(
|
||||||
|
key
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusInDirection(direction) {
|
||||||
|
const sourceWindow = global.display.get_focus_window();
|
||||||
|
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return;
|
||||||
|
|
||||||
|
const targetWindow = this._findTargetInDirection(
|
||||||
|
sourceWindow,
|
||||||
|
direction
|
||||||
|
);
|
||||||
|
if (targetWindow) {
|
||||||
|
targetWindow.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapWithMaster() {
|
||||||
|
const windows = this.tiler.windows;
|
||||||
|
if (windows.length < 2) return;
|
||||||
|
const focusedWindow = global.display.get_focus_window();
|
||||||
|
if (!focusedWindow || !windows.includes(focusedWindow)) return;
|
||||||
|
const focusedIndex = windows.indexOf(focusedWindow);
|
||||||
|
if (focusedIndex > 0) {
|
||||||
|
[windows[0], windows[focusedIndex]] = [
|
||||||
|
windows[focusedIndex],
|
||||||
|
windows[0],
|
||||||
|
];
|
||||||
|
} else if (focusedIndex === 0) {
|
||||||
|
[windows[0], windows[1]] = [windows[1], windows[0]];
|
||||||
|
}
|
||||||
|
this.tiler.tileNow();
|
||||||
|
if (windows.length > 0) windows[0].activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapInDirection(direction) {
|
||||||
|
const sourceWindow = global.display.get_focus_window();
|
||||||
|
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return;
|
||||||
|
let targetWindow = null;
|
||||||
|
const sourceIndex = this.tiler.windows.indexOf(sourceWindow);
|
||||||
|
if (
|
||||||
|
sourceIndex === 0 &&
|
||||||
|
direction === "right" &&
|
||||||
|
this.tiler.windows.length > 1
|
||||||
|
) {
|
||||||
|
targetWindow = this.tiler.windows[1];
|
||||||
|
} else {
|
||||||
|
targetWindow = this._findTargetInDirection(sourceWindow, direction);
|
||||||
|
}
|
||||||
|
if (!targetWindow) return;
|
||||||
|
const targetIndex = this.tiler.windows.indexOf(targetWindow);
|
||||||
|
[this.tiler.windows[sourceIndex], this.tiler.windows[targetIndex]] = [
|
||||||
|
this.tiler.windows[targetIndex],
|
||||||
|
this.tiler.windows[sourceIndex],
|
||||||
|
];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
sourceWindow.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetInDirection(source, direction) {
|
||||||
|
const sourceRect = source.get_frame_rect();
|
||||||
|
let candidates = [];
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win === source) continue;
|
||||||
|
const targetRect = win.get_frame_rect();
|
||||||
|
switch (direction) {
|
||||||
|
case "left":
|
||||||
|
if (targetRect.x < sourceRect.x) candidates.push(win);
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
if (targetRect.x > sourceRect.x) candidates.push(win);
|
||||||
|
break;
|
||||||
|
case "up":
|
||||||
|
if (targetRect.y < sourceRect.y) candidates.push(win);
|
||||||
|
break;
|
||||||
|
case "down":
|
||||||
|
if (targetRect.y > sourceRect.y) candidates.push(win);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
let bestTarget = null;
|
||||||
|
let minDeviation = Infinity;
|
||||||
|
for (const win of candidates) {
|
||||||
|
const targetRect = win.get_frame_rect();
|
||||||
|
let deviation;
|
||||||
|
if (direction === "left" || direction === "right") {
|
||||||
|
deviation = Math.abs(sourceRect.y - targetRect.y);
|
||||||
|
} else {
|
||||||
|
deviation = Math.abs(sourceRect.x - targetRect.x);
|
||||||
|
}
|
||||||
|
if (deviation < minDeviation) {
|
||||||
|
minDeviation = deviation;
|
||||||
|
bestTarget = win;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onGrabEnd() {
|
||||||
|
const grabbedWindow = this.tiler.grabbedWindow;
|
||||||
|
if (!grabbedWindow) return;
|
||||||
|
const targetWindow = this._findTargetUnderPointer(grabbedWindow);
|
||||||
|
if (targetWindow) {
|
||||||
|
const sourceIndex = this.tiler.windows.indexOf(grabbedWindow);
|
||||||
|
const targetIndex = this.tiler.windows.indexOf(targetWindow);
|
||||||
|
[
|
||||||
|
this.tiler.windows[sourceIndex],
|
||||||
|
this.tiler.windows[targetIndex],
|
||||||
|
] = [
|
||||||
|
this.tiler.windows[targetIndex],
|
||||||
|
this.tiler.windows[sourceIndex],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
this.tiler.queueTile();
|
||||||
|
this.tiler.grabbedWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetUnderPointer(excludeWindow) {
|
||||||
|
let [pointerX, pointerY] = global.get_pointer();
|
||||||
|
let windows = global
|
||||||
|
.get_window_actors()
|
||||||
|
.map((actor) => actor.meta_window)
|
||||||
|
.filter((win) => {
|
||||||
|
if (
|
||||||
|
!win ||
|
||||||
|
win === excludeWindow ||
|
||||||
|
!this.tiler.windows.includes(win)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
let frame = win.get_frame_rect();
|
||||||
|
return (
|
||||||
|
pointerX >= frame.x &&
|
||||||
|
pointerX < frame.x + frame.width &&
|
||||||
|
pointerY >= frame.y &&
|
||||||
|
pointerY < frame.y + frame.height
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (windows.length > 0) {
|
||||||
|
return windows[windows.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
let bestTarget = null;
|
||||||
|
let maxOverlap = 0;
|
||||||
|
const sourceFrame = excludeWindow.get_frame_rect();
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win === excludeWindow) continue;
|
||||||
|
const targetFrame = win.get_frame_rect();
|
||||||
|
const overlapX = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
sourceFrame.x + sourceFrame.width,
|
||||||
|
targetFrame.x + targetFrame.width
|
||||||
|
) - Math.max(sourceFrame.x, targetFrame.x)
|
||||||
|
);
|
||||||
|
const overlapY = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
sourceFrame.y + sourceFrame.height,
|
||||||
|
targetFrame.y + targetFrame.height
|
||||||
|
) - Math.max(sourceFrame.y, targetFrame.y)
|
||||||
|
);
|
||||||
|
const overlapArea = overlapX * overlapY;
|
||||||
|
if (overlapArea > maxOverlap) {
|
||||||
|
maxOverlap = overlapArea;
|
||||||
|
bestTarget = win;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TILER ---
|
||||||
|
class Tiler {
|
||||||
|
constructor(extension) {
|
||||||
|
this._extension = extension;
|
||||||
|
this.settings = this._extension.getSettings();
|
||||||
|
|
||||||
|
this.windows = [];
|
||||||
|
this.grabbedWindow = null;
|
||||||
|
this._signalIds = new Map();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
|
||||||
|
this._innerGap = this.settings.get_int("inner-gap");
|
||||||
|
this._outerGapVertical = this.settings.get_int("outer-gap-vertical");
|
||||||
|
this._outerGapHorizontal = this.settings.get_int(
|
||||||
|
"outer-gap-horizontal"
|
||||||
|
);
|
||||||
|
|
||||||
|
this._tilingDelay = TILING_DELAY_MS;
|
||||||
|
this._centeringDelay = CENTERING_DELAY_MS;
|
||||||
|
|
||||||
|
this._exceptions = [];
|
||||||
|
this._interactionHandler = new InteractionHandler(this);
|
||||||
|
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
this._centerTimeoutIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._loadExceptions();
|
||||||
|
this._workspaceManager = global.workspace_manager;
|
||||||
|
|
||||||
|
this._signalIds.set("workspace-changed", {
|
||||||
|
object: this._workspaceManager,
|
||||||
|
id: this._workspaceManager.connect("active-workspace-changed", () =>
|
||||||
|
this._onActiveWorkspaceChanged()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this._connectToWorkspace();
|
||||||
|
this._interactionHandler.enable();
|
||||||
|
this._signalIds.set("settings-changed", {
|
||||||
|
object: this.settings,
|
||||||
|
id: this.settings.connect("changed", () =>
|
||||||
|
this._onSettingsChanged()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._tileTimeoutId) {
|
||||||
|
GLib.source_remove(this._tileTimeoutId);
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
}
|
||||||
|
this._centerTimeoutIds.forEach((id) => GLib.source_remove(id));
|
||||||
|
this._centerTimeoutIds = [];
|
||||||
|
|
||||||
|
this._interactionHandler.disable();
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
for (const [, signal] of this._signalIds) {
|
||||||
|
try {
|
||||||
|
signal.object.disconnect(signal.id);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
this._signalIds.clear();
|
||||||
|
this.windows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._innerGap = this.settings.get_int("inner-gap");
|
||||||
|
this._outerGapVertical = this.settings.get_int("outer-gap-vertical");
|
||||||
|
this._outerGapHorizontal = this.settings.get_int(
|
||||||
|
"outer-gap-horizontal"
|
||||||
|
);
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadExceptions() {
|
||||||
|
const file = Gio.File.new_for_path(
|
||||||
|
this._extension.path + "/exceptions.txt"
|
||||||
|
);
|
||||||
|
if (!file.query_exists(null)) {
|
||||||
|
this._exceptions = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [ok, data] = file.load_contents(null);
|
||||||
|
if (ok) {
|
||||||
|
this._exceptions = GLib.locale_from_utf8(data)
|
||||||
|
.split("\n")
|
||||||
|
.map((l) => l.trim())
|
||||||
|
.filter((l) => l && !l.startsWith("#"))
|
||||||
|
.map((l) => l.toLowerCase());
|
||||||
|
} else {
|
||||||
|
this._exceptions = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_isException(win) {
|
||||||
|
if (!win) return false;
|
||||||
|
const wmClass = (win.get_wm_class() || "").toLowerCase();
|
||||||
|
const appId = (win.get_gtk_application_id() || "").toLowerCase();
|
||||||
|
return this._exceptions.includes(wmClass) || this._exceptions.includes(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTileable(win) {
|
||||||
|
return (
|
||||||
|
win &&
|
||||||
|
!win.minimized &&
|
||||||
|
!this._isException(win) &&
|
||||||
|
win.get_window_type() === Meta.WindowType.NORMAL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_centerWindow(win) {
|
||||||
|
const timeoutId = GLib.timeout_add(
|
||||||
|
GLib.PRIORITY_DEFAULT,
|
||||||
|
this._centeringDelay,
|
||||||
|
() => {
|
||||||
|
const index = this._centerTimeoutIds.indexOf(timeoutId);
|
||||||
|
if (index > -1) this._centerTimeoutIds.splice(index, 1);
|
||||||
|
|
||||||
|
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
|
||||||
|
if (win.get_maximized())
|
||||||
|
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
|
||||||
|
const monitorIndex = win.get_monitor();
|
||||||
|
const workspace = this._workspaceManager.get_active_workspace();
|
||||||
|
const workArea = workspace.get_work_area_for_monitor(
|
||||||
|
monitorIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const frame = win.get_frame_rect();
|
||||||
|
win.move_frame(
|
||||||
|
true,
|
||||||
|
workArea.x + Math.floor((workArea.width - frame.width) / 2),
|
||||||
|
workArea.y +
|
||||||
|
Math.floor((workArea.height - frame.height) / 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||||
|
if (win.get_display()) {
|
||||||
|
if (typeof win.set_keep_above === "function")
|
||||||
|
win.set_keep_above(true);
|
||||||
|
else if (typeof win.make_above === "function")
|
||||||
|
win.make_above();
|
||||||
|
}
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
});
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this._centerTimeoutIds.push(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowMinimizedStateChanged() {
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowAdded(workspace, win) {
|
||||||
|
if (this.windows.includes(win)) return;
|
||||||
|
if (this._isException(win)) {
|
||||||
|
this._centerWindow(win);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._isTileable(win)) {
|
||||||
|
if (this.settings.get_string("new-window-behavior") === "master") {
|
||||||
|
this.windows.unshift(win);
|
||||||
|
} else {
|
||||||
|
this.windows.push(win);
|
||||||
|
}
|
||||||
|
const id = win.get_id();
|
||||||
|
this._signalIds.set(`unmanaged-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("unmanaged", () =>
|
||||||
|
this._onWindowRemoved(null, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this._signalIds.set(`size-changed-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("size-changed", () => {
|
||||||
|
if (!this.grabbedWindow) this.queueTile();
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
this._signalIds.set(`minimized-${id}`, {
|
||||||
|
object: win,
|
||||||
|
id: win.connect("notify::minimized", () =>
|
||||||
|
this._onWindowMinimizedStateChanged()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowRemoved(workspace, win) {
|
||||||
|
const index = this.windows.indexOf(win);
|
||||||
|
if (index > -1) this.windows.splice(index, 1);
|
||||||
|
|
||||||
|
["unmanaged", "size-changed", "minimized"].forEach((prefix) => {
|
||||||
|
const key = `${prefix}-${win.get_id()}`;
|
||||||
|
if (this._signalIds.has(key)) {
|
||||||
|
const { object, id } = this._signalIds.get(key);
|
||||||
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
|
this._signalIds.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onActiveWorkspaceChanged() {
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
this._connectToWorkspace();
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectToWorkspace() {
|
||||||
|
const workspace = this._workspaceManager.get_active_workspace();
|
||||||
|
workspace
|
||||||
|
.list_windows()
|
||||||
|
.forEach((win) => this._onWindowAdded(workspace, win));
|
||||||
|
this._signalIds.set("window-added", {
|
||||||
|
object: workspace,
|
||||||
|
id: workspace.connect("window-added", (ws, win) =>
|
||||||
|
this._onWindowAdded(ws, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this._signalIds.set("window-removed", {
|
||||||
|
object: workspace,
|
||||||
|
id: workspace.connect("window-removed", (ws, win) =>
|
||||||
|
this._onWindowRemoved(ws, win)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disconnectFromWorkspace() {
|
||||||
|
this.windows.slice().forEach((win) => this._onWindowRemoved(null, win));
|
||||||
|
["window-added", "window-removed"].forEach((key) => {
|
||||||
|
if (this._signalIds.has(key)) {
|
||||||
|
const { object, id } = this._signalIds.get(key);
|
||||||
|
try {
|
||||||
|
object.disconnect(id);
|
||||||
|
} catch (e) {}
|
||||||
|
this._signalIds.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queueTile() {
|
||||||
|
if (this._tileInProgress || this._tileTimeoutId) return;
|
||||||
|
this._tileInProgress = true;
|
||||||
|
this._tileTimeoutId = GLib.timeout_add(
|
||||||
|
GLib.PRIORITY_DEFAULT,
|
||||||
|
this._tilingDelay,
|
||||||
|
() => {
|
||||||
|
this._tileWindows();
|
||||||
|
this._tileInProgress = false;
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tileNow() {
|
||||||
|
if (!this._tileInProgress) {
|
||||||
|
this._tileWindows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_splitLayout(windows, area) {
|
||||||
|
if (windows.length === 0) return;
|
||||||
|
if (windows.length === 1) {
|
||||||
|
windows[0].move_resize_frame(
|
||||||
|
true,
|
||||||
|
area.x,
|
||||||
|
area.y,
|
||||||
|
area.width,
|
||||||
|
area.height
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
|
const primaryWindows = [windows[0]];
|
||||||
|
const secondaryWindows = windows.slice(1);
|
||||||
|
let primaryArea, secondaryArea;
|
||||||
|
if (area.width > area.height) {
|
||||||
|
const primaryWidth = Math.floor(area.width / 2) - gap;
|
||||||
|
primaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
width: primaryWidth,
|
||||||
|
height: area.height,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x + primaryWidth + this._innerGap,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width - primaryWidth - this._innerGap,
|
||||||
|
height: area.height,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const primaryHeight = Math.floor(area.height / 2) - gap;
|
||||||
|
primaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width,
|
||||||
|
height: primaryHeight,
|
||||||
|
};
|
||||||
|
secondaryArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y + primaryHeight + this._innerGap,
|
||||||
|
width: area.width,
|
||||||
|
height: area.height - primaryHeight - this._innerGap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this._splitLayout(primaryWindows, primaryArea);
|
||||||
|
this._splitLayout(secondaryWindows, secondaryArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tileWindows() {
|
||||||
|
const windowsToTile = this.windows.filter((win) => !win.minimized);
|
||||||
|
if (windowsToTile.length === 0) return;
|
||||||
|
|
||||||
|
const monitor = Main.layoutManager.primaryMonitor;
|
||||||
|
const workspace = this._workspaceManager.get_active_workspace();
|
||||||
|
const workArea = workspace.get_work_area_for_monitor(monitor.index);
|
||||||
|
|
||||||
|
const innerArea = {
|
||||||
|
x: workArea.x + this._outerGapHorizontal,
|
||||||
|
y: workArea.y + this._outerGapVertical,
|
||||||
|
width: workArea.width - 2 * this._outerGapHorizontal,
|
||||||
|
height: workArea.height - 2 * this._outerGapVertical,
|
||||||
|
};
|
||||||
|
windowsToTile.forEach((win) => {
|
||||||
|
if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||||||
|
});
|
||||||
|
if (windowsToTile.length === 1) {
|
||||||
|
windowsToTile[0].move_resize_frame(
|
||||||
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
innerArea.width,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gap = Math.floor(this._innerGap / 2);
|
||||||
|
const masterWidth = Math.floor(innerArea.width / 2) - gap;
|
||||||
|
const master = windowsToTile[0];
|
||||||
|
master.move_resize_frame(
|
||||||
|
true,
|
||||||
|
innerArea.x,
|
||||||
|
innerArea.y,
|
||||||
|
masterWidth,
|
||||||
|
innerArea.height
|
||||||
|
);
|
||||||
|
const stackArea = {
|
||||||
|
x: innerArea.x + masterWidth + this._innerGap,
|
||||||
|
y: innerArea.y,
|
||||||
|
width: innerArea.width - masterWidth - this._innerGap,
|
||||||
|
height: innerArea.height,
|
||||||
|
};
|
||||||
|
this._splitLayout(windowsToTile.slice(1), stackArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MODERN EXTENSION WRAPPER ---
|
||||||
|
export default class ModernExtension extends Extension {
|
||||||
|
enable() {
|
||||||
|
this.tiler = new Tiler(this);
|
||||||
|
this.tiler.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this.tiler) {
|
||||||
|
this.tiler.disable();
|
||||||
|
this.tiler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,108 +1,221 @@
|
|||||||
// Settings Menu for Simple-Tiling
|
///////////////////////////////////////////////////////
|
||||||
|
// --- Extension Settings Menu for Simple Tiling --- //
|
||||||
|
// --- © 2025 domoel – MIT --- //
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
// --- GLOBAL IMPORTS ---
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { Gtk, GObject } = imports.gi;
|
const { Gtk, GObject, Gio, GLib } = imports.gi;
|
||||||
const ExtensionUtils = imports.misc.extensionUtils;
|
const ExtensionUtils = imports.misc.extensionUtils;
|
||||||
|
|
||||||
const COLUMN_ID = 0; // z.B. 'swap-master-window'
|
// --- Global Version Checkup ---
|
||||||
const COLUMN_DESC = 1; // z.B. 'Master-Fenster tauschen'
|
let Adw;
|
||||||
const COLUMN_KEY = 2; // Der Key-Code (eine Zahl)
|
try {
|
||||||
const COLUMN_MODS = 3; // Die Modifier-Maske (eine Zahl)
|
Adw = imports.gi.Adw;
|
||||||
|
} catch (e) {
|
||||||
|
Adw = null;
|
||||||
|
}
|
||||||
|
|
||||||
function init() {}
|
|
||||||
|
|
||||||
function buildPrefsWidget() {
|
|
||||||
const settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.simple-tiling.domoel');
|
|
||||||
|
|
||||||
|
// --- DEFINITIONS ---
|
||||||
|
|
||||||
|
// --- Definitions for GNOME Shell 40+ ---
|
||||||
|
const ModernPrefs = Adw ? class extends ExtensionPreferences {
|
||||||
|
fillPreferencesWindow(window) {
|
||||||
|
const settings = this.getSettings();
|
||||||
|
const page = new Adw.PreferencesPage();
|
||||||
|
window.add(page);
|
||||||
|
|
||||||
|
// --- Window Gaps ---
|
||||||
|
const groupGaps = new Adw.PreferencesGroup({ title: 'Window Gaps' });
|
||||||
|
page.add(groupGaps);
|
||||||
|
|
||||||
|
const rowInnerGap = new Adw.SpinRow({
|
||||||
|
title: 'Inner Gap',
|
||||||
|
subtitle: 'The gap between windows in pixels.',
|
||||||
|
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
||||||
|
});
|
||||||
|
groupGaps.add(rowInnerGap);
|
||||||
|
settings.bind('inner-gap', rowInnerGap, 'value', Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
|
||||||
|
const rowOuterHGap = new Adw.SpinRow({
|
||||||
|
title: 'Outer Gap (horizontal)',
|
||||||
|
subtitle: 'The gap to the left and right screen edges.',
|
||||||
|
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
||||||
|
});
|
||||||
|
groupGaps.add(rowOuterHGap);
|
||||||
|
settings.bind('outer-gap-horizontal', rowOuterHGap, 'value', Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
|
||||||
|
const rowOuterVGap = new Adw.SpinRow({
|
||||||
|
title: 'Outer Gap (vertical)',
|
||||||
|
subtitle: 'The gap to the top and bottom screen edges.',
|
||||||
|
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
||||||
|
});
|
||||||
|
groupGaps.add(rowOuterVGap);
|
||||||
|
settings.bind('outer-gap-vertical', rowOuterVGap, 'value', Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
|
||||||
|
// --- Window Behavior ---
|
||||||
|
const groupBehavior = new Adw.PreferencesGroup({ title: 'Window Behavior' });
|
||||||
|
page.add(groupBehavior);
|
||||||
|
|
||||||
|
const rowNewWindow = new Adw.ComboRow({
|
||||||
|
title: 'Open new windows as',
|
||||||
|
subtitle: 'Determines if a new window is added as master or stack window.',
|
||||||
|
model: new Gtk.StringList({ strings: ['Stack Window (Default)', 'Master Window'] }),
|
||||||
|
});
|
||||||
|
groupBehavior.add(rowNewWindow);
|
||||||
|
|
||||||
|
const mapping = new Gio.SettingsBindMapping({
|
||||||
|
settings: settings, key: 'new-window-behavior', property: 'selected',
|
||||||
|
get_mapping: (value, variant_type) => value === 'master' ? 1 : 0,
|
||||||
|
set_mapping: (value, param_type) => new GLib.Variant('s', value === 1 ? 'master' : 'stack'),
|
||||||
|
});
|
||||||
|
settings.bind_with_mapping('new-window-behavior', rowNewWindow, 'selected', Gio.SettingsBindFlags.DEFAULT, mapping);
|
||||||
|
|
||||||
|
// --- Keybindings ---
|
||||||
|
const groupKeys = new Adw.PreferencesGroup({ title: 'Keybindings' });
|
||||||
|
page.add(groupKeys);
|
||||||
|
|
||||||
|
const rowKeys = new Adw.ActionRow({
|
||||||
|
title: 'Configure Shortcuts',
|
||||||
|
subtitle: 'All shortcuts can be configured in GNOME\'s main Keyboard settings.',
|
||||||
|
});
|
||||||
|
groupKeys.add(rowKeys);
|
||||||
|
|
||||||
|
const button = new Gtk.Button({ label: 'Open Keyboard Settings', valign: Gtk.Align.CENTER });
|
||||||
|
button.connect('clicked', () => {
|
||||||
|
const appInfo = Gio.AppInfo.create_from_commandline(
|
||||||
|
'gnome-control-center keyboard', null, Gio.AppInfoCreateFlags.NONE
|
||||||
|
);
|
||||||
|
appInfo.launch([], null);
|
||||||
|
});
|
||||||
|
rowKeys.add_suffix(button);
|
||||||
|
rowKeys.set_activatable_widget(button);
|
||||||
|
}
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
|
||||||
|
// --- Definitions for GNOME Shell 3.38 ---
|
||||||
|
const buildLegacyPrefsWidget = () => {
|
||||||
|
const settings = ExtensionUtils.getSettings("org.gnome.shell.extensions.simple-tiling.domoel");
|
||||||
const prefsWidget = new Gtk.Box({
|
const prefsWidget = new Gtk.Box({
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
orientation: Gtk.Orientation.VERTICAL,
|
||||||
margin: 20,
|
margin_top: 20, margin_bottom: 20, margin_start: 20, margin_end: 20,
|
||||||
spacing: 12,
|
spacing: 18,
|
||||||
visible: true
|
visible: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const title = new Gtk.Label({
|
// --- Keybindings ---
|
||||||
label: '<b>Tastenkürzel für Simple-Tiling</b>',
|
const keysTitle = new Gtk.Label({ label: "<b>Keybindings</b>", use_markup: true, halign: Gtk.Align.START, visible: true });
|
||||||
use_markup: true,
|
const keysFrame = new Gtk.Frame({ label_widget: keysTitle, shadow_type: Gtk.ShadowType.NONE, visible: true });
|
||||||
halign: Gtk.Align.START,
|
let keysBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, margin: 12, spacing: 6, visible: true });
|
||||||
visible: true
|
keysFrame.add(keysBox);
|
||||||
});
|
|
||||||
prefsWidget.add(title);
|
|
||||||
|
|
||||||
let store = new Gtk.ListStore();
|
let store = new Gtk.ListStore();
|
||||||
store.set_column_types([
|
store.set_column_types([ GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_INT, GObject.TYPE_INT ]);
|
||||||
GObject.TYPE_STRING, // COLUMN_ID
|
|
||||||
GObject.TYPE_STRING, // COLUMN_DESC
|
|
||||||
GObject.TYPE_INT, // COLUMN_KEY
|
|
||||||
GObject.TYPE_INT, // COLUMN_MODS
|
|
||||||
]);
|
|
||||||
|
|
||||||
addKeybinding(store, settings, 'swap-master-window', 'Master-Fenster tauschen');
|
const COLUMN_ID = 0, COLUMN_DESC = 1, COLUMN_KEY = 2, COLUMN_MODS = 3;
|
||||||
addKeybinding(store, settings, 'swap-left-window', 'Fenster nach links tauschen');
|
const addKeybinding = (id, desc) => {
|
||||||
addKeybinding(store, settings, 'swap-right-window', 'Fenster nach rechts tauschen');
|
let [key, mods] = [0, 0];
|
||||||
addKeybinding(store, settings, 'swap-up-window', 'Fenster nach oben tauschen');
|
const strv = settings.get_strv(id);
|
||||||
addKeybinding(store, settings, 'swap-down-window', 'Fenster nach unten tauschen');
|
if (strv && strv[0]) [key, mods] = Gtk.accelerator_parse(strv[0]);
|
||||||
|
let iter = store.append();
|
||||||
|
store.set(iter, [COLUMN_ID, COLUMN_DESC, COLUMN_KEY, COLUMN_MODS], [id, desc, key, mods]);
|
||||||
|
};
|
||||||
|
|
||||||
let treeView = new Gtk.TreeView({
|
addKeybinding("swap-master-window", "Swap current window with master");
|
||||||
model: store,
|
addKeybinding("swap-up-window", "Swap current window with window above");
|
||||||
headers_visible: false,
|
addKeybinding("swap-down-window", "Swap current window with window below");
|
||||||
hexpand: true,
|
addKeybinding("swap-left-window", "Swap current window with window to the left");
|
||||||
visible: true
|
addKeybinding("swap-right-window", "Swap current window with window to the right");
|
||||||
});
|
addKeybinding("focus-up", "Focus window above");
|
||||||
|
addKeybinding("focus-down", "Focus window below");
|
||||||
|
addKeybinding("focus-left", "Focus window to the left");
|
||||||
|
addKeybinding("focus-right", "Focus window to the right");
|
||||||
|
|
||||||
|
let treeView = new Gtk.TreeView({ model: store, headers_visible: false, hexpand: true, visible: true });
|
||||||
|
keysBox.add(treeView);
|
||||||
|
|
||||||
let descRenderer = new Gtk.CellRendererText();
|
let descRenderer = new Gtk.CellRendererText();
|
||||||
let descColumn = new Gtk.TreeViewColumn({ expand: true });
|
let descColumn = new Gtk.TreeViewColumn({ expand: true });
|
||||||
descColumn.pack_start(descRenderer, true);
|
descColumn.pack_start(descRenderer, true);
|
||||||
descColumn.add_attribute(descRenderer, 'text', COLUMN_DESC);
|
descColumn.add_attribute(descRenderer, "text", COLUMN_DESC);
|
||||||
treeView.append_column(descColumn);
|
treeView.append_column(descColumn);
|
||||||
|
|
||||||
let accelRenderer = new Gtk.CellRendererAccel({
|
let accelRenderer = new Gtk.CellRendererAccel({ "accel-mode": Gtk.CellRendererAccelMode.GTK, editable: true });
|
||||||
'accel-mode': Gtk.CellRendererAccelMode.GTK,
|
accelRenderer.connect("accel-edited", (r, path, key, mods) => {
|
||||||
'editable': true
|
let [ok, iter] = store.get_iter_from_string(path);
|
||||||
|
if (ok) {
|
||||||
|
store.set(iter, [COLUMN_KEY, COLUMN_MODS], [key, mods]);
|
||||||
|
settings.set_strv(store.get_value(iter, COLUMN_ID), [ Gtk.accelerator_name(key, mods) ]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
accelRenderer.connect("accel-cleared", (r, path) => {
|
||||||
|
let [ok, iter] = store.get_iter_from_string(path);
|
||||||
|
if (ok) {
|
||||||
|
store.set(iter, [COLUMN_KEY, COLUMN_MODS], [0, 0]);
|
||||||
|
settings.set_strv(store.get_value(iter, COLUMN_ID), []);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
let accelColumn = new Gtk.TreeViewColumn();
|
let accelColumn = new Gtk.TreeViewColumn();
|
||||||
accelColumn.pack_end(accelRenderer, false);
|
accelColumn.pack_end(accelRenderer, false);
|
||||||
accelColumn.add_attribute(accelRenderer, 'accel-key', COLUMN_KEY);
|
accelColumn.add_attribute(accelRenderer, "accel-key", COLUMN_KEY);
|
||||||
accelColumn.add_attribute(accelRenderer, 'accel-mods', COLUMN_MODS);
|
accelColumn.add_attribute(accelRenderer, "accel-mods", COLUMN_MODS);
|
||||||
treeView.append_column(accelColumn);
|
treeView.append_column(accelColumn);
|
||||||
|
prefsWidget.add(keysFrame);
|
||||||
|
|
||||||
|
// --- Window Gaps ---
|
||||||
|
const gapsTitle = new Gtk.Label({ label: "<b>Window Gaps</b>", use_markup: true, halign: Gtk.Align.START, visible: true });
|
||||||
|
const gapsFrame = new Gtk.Frame({ label_widget: gapsTitle, shadow_type: Gtk.ShadowType.NONE, visible: true });
|
||||||
|
const gapsGrid = new Gtk.Grid({ margin: 12, column_spacing: 12, row_spacing: 12, visible: true });
|
||||||
|
gapsFrame.add(gapsGrid);
|
||||||
|
|
||||||
|
const addSpinButtonRow = (desc, key, pos) => {
|
||||||
|
const label = new Gtk.Label({ label: desc, halign: Gtk.Align.START, visible: true });
|
||||||
|
gapsGrid.attach(label, 0, pos, 1, 1);
|
||||||
|
const adj = new Gtk.Adjustment({ lower: 0, upper: 50, step_increment: 1 });
|
||||||
|
const spin = new Gtk.SpinButton({ adjustment: adj, climb_rate: 1, digits: 0, halign: Gtk.Align.END, visible: true });
|
||||||
|
settings.bind(key, spin, "value", Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
gapsGrid.attach(spin, 1, pos, 1, 1);
|
||||||
|
};
|
||||||
|
addSpinButtonRow("Inner Gap", "inner-gap", 0);
|
||||||
|
addSpinButtonRow("Outer Gap (horizontal)", "outer-gap-horizontal", 1);
|
||||||
|
addSpinButtonRow("Outer Gap (vertical)", "outer-gap-vertical", 2);
|
||||||
|
prefsWidget.add(gapsFrame);
|
||||||
|
|
||||||
prefsWidget.add(treeView);
|
// --- Window Behavior ---
|
||||||
|
const behaviorTitle = new Gtk.Label({ label: "<b>Window Behavior</b>", use_markup: true, halign: Gtk.Align.START, visible: true });
|
||||||
accelRenderer.connect('accel-edited', (renderer, path_string, key, mods, hw_code) => {
|
const behaviorFrame = new Gtk.Frame({ label_widget: behaviorTitle, shadow_type: Gtk.ShadowType.NONE, visible: true });
|
||||||
let [ok, iter] = store.get_iter_from_string(path_string);
|
const behaviorGrid = new Gtk.Grid({ margin: 12, column_spacing: 12, row_spacing: 12, visible: true });
|
||||||
if (!ok) return;
|
behaviorFrame.add(behaviorGrid);
|
||||||
|
|
||||||
store.set(iter, [COLUMN_KEY, COLUMN_MODS], [key, mods]);
|
const label = new Gtk.Label({ label: "Open new windows as", halign: Gtk.Align.START, visible: true });
|
||||||
|
behaviorGrid.attach(label, 0, 0, 1, 1);
|
||||||
let id = store.get_value(iter, COLUMN_ID);
|
const combo = new Gtk.ComboBoxText({ visible: true, halign: Gtk.Align.END });
|
||||||
let accelString = Gtk.accelerator_name(key, mods);
|
combo.append("stack", "Stack Window (Default)");
|
||||||
settings.set_strv(id, [accelString]);
|
combo.append("master", "Master Window");
|
||||||
});
|
combo.set_active_id(settings.get_string("new-window-behavior"));
|
||||||
|
combo.connect("changed", () => {
|
||||||
accelRenderer.connect('accel-cleared', (renderer, path_string) => {
|
settings.set_string("new-window-behavior", combo.get_active_id());
|
||||||
let [ok, iter] = store.get_iter_from_string(path_string);
|
|
||||||
if (!ok) return;
|
|
||||||
|
|
||||||
store.set(iter, [COLUMN_KEY, COLUMN_MODS], [0, 0]);
|
|
||||||
let id = store.get_value(iter, COLUMN_ID);
|
|
||||||
settings.set_strv(id, []);
|
|
||||||
});
|
});
|
||||||
|
behaviorGrid.attach(combo, 1, 0, 1, 1);
|
||||||
|
prefsWidget.add(behaviorFrame);
|
||||||
|
|
||||||
return prefsWidget;
|
return prefsWidget;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// --- MAIN ENTRY POINTS (called by GNOME Shell) ---
|
||||||
|
|
||||||
|
function init() {}
|
||||||
|
|
||||||
|
function buildPrefsWidget() {
|
||||||
|
return buildLegacyPrefsWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addKeybinding(model, settings, id, description) {
|
if (Adw) {
|
||||||
let [key, mods] = [0, 0];
|
var defaultExport = ModernPrefs;
|
||||||
|
|
||||||
const strv = settings.get_strv(id);
|
|
||||||
if (strv && strv.length > 0 && strv[0]) {
|
|
||||||
[key, mods] = Gtk.accelerator_parse(strv[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let iter = model.append();
|
|
||||||
model.set(iter,
|
|
||||||
[COLUMN_ID, COLUMN_DESC, COLUMN_KEY, COLUMN_MODS],
|
|
||||||
[id, description, key, mods]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -1,34 +1,63 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<schemalist>
|
<schemalist>
|
||||||
<schema id="org.gnome.shell.extensions.simple-tiling.domoel"
|
<schema id="org.gnome.shell.extensions.simple-tiling.domoel" path="/org/gnome/shell/extensions/simple-tiling/domoel/">
|
||||||
path="/org/gnome/shell/extensions/simple-tiling-domoel/">
|
|
||||||
|
|
||||||
<!-- Master ↔ Stack -->
|
|
||||||
<key name="swap-master-window" type="as">
|
<key name="swap-master-window" type="as">
|
||||||
<default>['<Super>Return']</default>
|
<default><![CDATA[['<Super>Return']]]></default>
|
||||||
<summary>Tauscht Master-Fenster mit dem fokussierten Fenster</summary>
|
<summary>Swap current window with master.</summary>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<!-- Richtungs-Swaps -->
|
|
||||||
<key name="swap-left-window" type="as">
|
|
||||||
<default>['<Super>Left']</default>
|
|
||||||
<summary>Tauscht mit linkem Nachbarfenster</summary>
|
|
||||||
</key>
|
|
||||||
|
|
||||||
<key name="swap-right-window" type="as">
|
|
||||||
<default>['<Super>Right']</default>
|
|
||||||
<summary>Tauscht mit rechtem Nachbarfenster</summary>
|
|
||||||
</key>
|
|
||||||
|
|
||||||
<key name="swap-up-window" type="as">
|
<key name="swap-up-window" type="as">
|
||||||
<default>['<Super>Up']</default>
|
<default><![CDATA[['<Super>Up']]]></default>
|
||||||
<summary>Tauscht mit oberem Nachbarfenster</summary>
|
<summary>Swap current window with window above.</summary>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<key name="swap-down-window" type="as">
|
<key name="swap-down-window" type="as">
|
||||||
<default>['<Super>Down']</default>
|
<default><![CDATA[['<Super>Down']]]></default>
|
||||||
<summary>Tauscht mit unterem Nachbarfenster</summary>
|
<summary>Swap current window with window below.</summary>
|
||||||
|
</key>
|
||||||
|
<key name="swap-left-window" type="as">
|
||||||
|
<default><![CDATA[['<Super>Left']]]></default>
|
||||||
|
<summary>Swap current window with window to the left.</summary>
|
||||||
|
</key>
|
||||||
|
<key name="swap-right-window" type="as">
|
||||||
|
<default><![CDATA[['<Super>Right']]]></default>
|
||||||
|
<summary>Swap current window with window to the right.</summary>
|
||||||
|
</key>
|
||||||
|
|
||||||
|
<key name="focus-up" type="as">
|
||||||
|
<default><![CDATA[['<Alt>Up']]]></default>
|
||||||
|
<summary>Focus window above.</summary>
|
||||||
|
</key>
|
||||||
|
<key name="focus-down" type="as">
|
||||||
|
<default><![CDATA[['<Alt>Down']]]></default>
|
||||||
|
<summary>Focus window below.</summary>
|
||||||
|
</key>
|
||||||
|
<key name="focus-left" type="as">
|
||||||
|
<default><![CDATA[['<Alt>Left']]]></default>
|
||||||
|
<summary>Focus window to the left.</summary>
|
||||||
|
</key>
|
||||||
|
<key name="focus-right" type="as">
|
||||||
|
<default><![CDATA[['<Alt>Right']]]></default>
|
||||||
|
<summary>Focus window to the right.</summary>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
|
<key name="inner-gap" type="i">
|
||||||
|
<default>10</default>
|
||||||
|
<summary>The gap between windows in pixels.</summary>
|
||||||
|
</key>
|
||||||
|
<key name="outer-gap-horizontal" type="i">
|
||||||
|
<default>5</default>
|
||||||
|
<summary>The gap to the left and right screen edges.</summary>
|
||||||
|
</key>
|
||||||
|
<key name="outer-gap-vertical" type="i">
|
||||||
|
<default>5</default>
|
||||||
|
<summary>The gap to the top and bottom screen edges.</summary>
|
||||||
|
</key>
|
||||||
|
|
||||||
|
<key name="new-window-behavior" type="s">
|
||||||
|
<default>'stack'</default>
|
||||||
|
<summary>Behavior for newly opened windows.</summary>
|
||||||
|
<description>Determines if a new window is added as master or stack window.</description>
|
||||||
|
</key>
|
||||||
|
|
||||||
</schema>
|
</schema>
|
||||||
</schemalist>
|
</schemalist>
|
||||||
|
|||||||
Reference in New Issue
Block a user