Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a2c273a2b | |||
| 14357adb1e | |||
| 31a61478ad | |||
| 2eb304a16a | |||
| 8480e6ccaf | |||
| f7a86e51b1 | |||
| 1afeb816c4 | |||
| 83ceb4ce67 | |||
| 6e90d12ee9 | |||
| 717ef0b16b | |||
| 1896d992d0 | |||
| 749e3a0275 | |||
| 1daeb0e100 | |||
| defa7255df | |||
| 6031a36664 | |||
| 021c51040f | |||
| 98bfaaa3d5 | |||
| cb26f3aebb | |||
| 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 |
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Dome
|
Copyright (c) 2025 Domoel (https://github.com/Domoel/)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
###############################################################################
|
||||||
|
# Simple‑Tiling – Makefile
|
||||||
|
#
|
||||||
|
# make build → baut beide ZIP‑Pakete
|
||||||
|
# make build-legacy → nur Legacy‑ZIP (Shell 3.38‑44)
|
||||||
|
# make build-modern → nur Modern‑ZIP (Shell 45‑48)
|
||||||
|
# make clean → räumt auf
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
UUID := simple-tiling@domoel
|
||||||
|
VERSION := 6
|
||||||
|
|
||||||
|
# Dateien/Ordner, die in *beide* Pakete gehören
|
||||||
|
COMMON_FILES := schemas exceptions.txt locale *.css README.md LICENSE
|
||||||
|
|
||||||
|
# Pref‑Dateien (zwei Varianten)
|
||||||
|
LEGACY_PREFS := prefs_legacy.js
|
||||||
|
MODERN_PREFS := prefs_modern.js
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Helfer: copies <file list> <dest>
|
||||||
|
###############################################################################
|
||||||
|
define copies
|
||||||
|
@for f in $(1) ; do \
|
||||||
|
if [ -e $$f ] ; then \
|
||||||
|
cp -r $$f $(2)/ ; \
|
||||||
|
fi ; \
|
||||||
|
done
|
||||||
|
endef
|
||||||
|
|
||||||
|
.PHONY: build build-legacy build-modern clean
|
||||||
|
build: build-legacy build-modern
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Legacy‑Build
|
||||||
|
###############################################################################
|
||||||
|
build-legacy:
|
||||||
|
@echo "==> Building LEGACY package (3.38‑44)…"
|
||||||
|
@rm -rf build && mkdir -p build/$(UUID)
|
||||||
|
$(call copies,$(COMMON_FILES),build/$(UUID))
|
||||||
|
|
||||||
|
# Schema kompilieren
|
||||||
|
@glib-compile-schemas build/$(UUID)/schemas
|
||||||
|
|
||||||
|
# Haupt‑ und Pref‑Skript
|
||||||
|
@cp legacy.js build/$(UUID)/extension.js
|
||||||
|
@cp $(LEGACY_PREFS) build/$(UUID)/prefs.js
|
||||||
|
|
||||||
|
# metadata.json anpassen
|
||||||
|
@sed -e "s/__UUID__/$(UUID)/g" \
|
||||||
|
-e "s/__VERSION__/$(VERSION)/g" \
|
||||||
|
metadata_legacy.json.in > build/$(UUID)/metadata.json
|
||||||
|
|
||||||
|
# Zip‑Paket
|
||||||
|
@cd build && zip -qr ../$(UUID)-legacy-v$(VERSION).zip .
|
||||||
|
@rm -rf build
|
||||||
|
@echo "✓ created $(UUID)-legacy-v$(VERSION).zip"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Modern‑Build
|
||||||
|
###############################################################################
|
||||||
|
build-modern:
|
||||||
|
@echo "==> Building MODERN package (45‑48)…"
|
||||||
|
@rm -rf build && mkdir -p build/$(UUID)
|
||||||
|
$(call copies,$(COMMON_FILES),build/$(UUID))
|
||||||
|
|
||||||
|
# Schema kompilieren
|
||||||
|
@glib-compile-schemas build/$(UUID)/schemas
|
||||||
|
|
||||||
|
# Haupt‑ und Pref‑Skript
|
||||||
|
@cp modern.js build/$(UUID)/extension.js
|
||||||
|
@cp $(MODERN_PREFS) build/$(UUID)/prefs.js
|
||||||
|
|
||||||
|
# metadata.json anpassen
|
||||||
|
@sed -e "s/__UUID__/$(UUID)/g" \
|
||||||
|
-e "s/__VERSION__/$(VERSION)/g" \
|
||||||
|
metadata_modern.json.in > build/$(UUID)/metadata.json
|
||||||
|
|
||||||
|
# Zip‑Paket
|
||||||
|
@cd build && zip -qr ../$(UUID)-modern-v$(VERSION).zip .
|
||||||
|
@rm -rf build
|
||||||
|
@echo "✓ created $(UUID)-modern-v$(VERSION).zip"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
clean:
|
||||||
|
@rm -rf build $(UUID)-legacy-v$(VERSION).zip $(UUID)-modern-v$(VERSION).zip
|
||||||
|
@echo "Build‑Ordner und ZIPs entfernt."
|
||||||
@@ -4,20 +4,19 @@ Simple Tiling
|
|||||||
</span>
|
</span>
|
||||||
<h4 align="center">
|
<h4 align="center">
|
||||||
<span style="display:inline-flex; align-items:center; gap:12px;">
|
<span style="display:inline-flex; align-items:center; gap:12px;">
|
||||||
A lightweight, opinionated, and automatic tiling window manager for GNOME Shell 3.38.
|
A lightweight, opinionated, and automatic tiling window manager for GNOME Shell
|
||||||
</span>
|
</span>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||

|
|
||||||
|
|
||||||
<img width="2560" height="1440" alt="Simple Tiling v4" src="https://github.com/user-attachments/assets/b080483e-40fe-4ea2-b0dd-56fcb587f9b8" />
|
<img width="2560" height="1440" alt="Simple-Tiling-v6" src="https://github.com/user-attachments/assets/eb0f7cc3-6a5a-4036-8a1e-8f945c52e55c" />
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Simple Tiling is a GNOME Shell extension created for users who want a clean, predictable, and automatic tiling layout without the complexity of larger, more feature-heavy tiling extensions. It is designed to be simple to configure and intuitive to use, focusing on a core set of essential tiling features.
|
Simple Tiling is a GNOME Shell extension created for users who want a clean, predictable, and automatic tiling layout without the complexity of larger, more feature-heavy tiling extensions. It is designed to be simple to configure and intuitive to use, focusing on a core set of essential tiling features.
|
||||||
|
|
||||||
This extension was built from the ground up to be stable and performant on **GNOME Shell 3.38**.
|
This extension was built from the ground up to be stable and performant on **GNOME Shell 3.38**. However it is now also supporting modern gnome shells up to **version 48**.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -28,18 +27,18 @@ This extension was built from the ground up to be stable and performant on **GNO
|
|||||||
* **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).
|
||||||
* **Interactive Window Focus Switcher:** Change the current window focus with a set of customizable keyboard shortcuts in every direction.
|
* **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 and window gaps / margins.
|
* **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`.
|
* **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
|
||||||
|
|
||||||
Please note that this extension has been developed for a very specific environment:
|
Please note that this extension has been developed for a very specific environment. However, with the latest updates, I have ensured that modern Gnome Shells and Wayland are also supported.
|
||||||
|
|
||||||
* **GNOME Shell Version:** **3.38**
|
* **GNOME Shell Version:** **3.38 - 48**
|
||||||
* **Session Type:** **X11** (Wayland is not supported).
|
* **Session Type:** **X11** (Wayland is still in beta but should be fine!).
|
||||||
* **Monitor Setup:** **Single monitor only.** Multi-monitor support is not yet implemented.
|
* **Monitor Setup:** **Single monitor only.** Multi-monitor support is not yet implemented.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -50,23 +49,48 @@ Use the [GNOME Shell Extensions website](https://extensions.gnome.org/extension/
|
|||||||
|
|
||||||
#### Manual Installation
|
#### Manual Installation
|
||||||
|
|
||||||
1. **Clone the repository** into your local extensions directory:
|
The repository includes a Makefile that produces ready‑to‑install ZIP packages for the two supported GNOME‑Shell lines (a legacy build (Gnome-Shell 3.38 - 44) and a modern build for Gnome-Shell 45+).
|
||||||
```bash
|
|
||||||
git clone https://github.com/Domoel/Simple-Tiling.git
|
1. **Clone the Source**
|
||||||
```
|
```bash
|
||||||
2. **Compile the GSettings schema.** This is a mandatory step for the keyboard shortcuts to work.
|
git clone https://github.com/YourUser/Simple-Tiling.git
|
||||||
```bash
|
cd Simple-Tiling
|
||||||
cd ~/.local/share/gnome-shell/extensions/simple-tiling@domoel/
|
```
|
||||||
glib-compile-schemas schemas/
|
|
||||||
```
|
2 · **Create the package that matches your GNOME-Shell version**
|
||||||
3. **Restart GNOME Shell.** Press `Alt` + `F2`, type `r`, and press `Enter`.
|
Open the Terminal within the Simple-Tiling directory and run
|
||||||
4. **Enable the extension** using the GNOME Extensions app or GNOME Tweaks.
|
```bash
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
**Note:** This will create a ready to go .zip archive of both, the modern and the legacy version of the extension ready to be used. Alternativley you can also run "make build-legacy" or "make build-modern" to only compile one of both versions.
|
||||||
|
|
||||||
|
3 · **Locate the output**
|
||||||
|
```bash
|
||||||
|
ls -1 ../simple-tiling@domoel-*-v*.zip
|
||||||
|
```
|
||||||
|
4 · **Install & enable**
|
||||||
|
```bash
|
||||||
|
gnome-extensions install ../simple-tiling@domoel-legacy-v6.zip
|
||||||
|
gnome-extensions enable simple-tiling@domoel
|
||||||
|
```
|
||||||
|
**Note:** You can also unzip the file and put the folder right into your extensions directory (~/.local/share/gnome-shell/extensions/)
|
||||||
|
|
||||||
|
5 · **Reload the shell**
|
||||||
|
```bash
|
||||||
|
Press Alt + F2, type r , hit ↩ (works for X11 and Wayland)
|
||||||
|
```
|
||||||
|
6 · **Clean up (optional)**
|
||||||
|
```bash
|
||||||
|
make clean # removes build/ folder and generated ZIPs
|
||||||
|
```
|
||||||
|
|
||||||
|
**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 Settings panel of Simple Tiling (which can be found in the Gnome Extesion Application):
|
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.
|
||||||
@@ -102,7 +126,6 @@ If you have race condition issues between mutter (Gnome WM) and the Simple Tilin
|
|||||||
|
|
||||||
This extension was built to solve a specific need. However, future enhancements could include:
|
This extension was built to solve a specific need. However, future enhancements could include:
|
||||||
* Multi-monitor support.
|
* Multi-monitor support.
|
||||||
* Support for newer Gnome shells
|
|
||||||
* Additional layout algorithms.
|
* Additional layout algorithms.
|
||||||
* A more detailed settings panel to configure other options via a GUI.
|
* A more detailed settings panel to configure other options via a GUI.
|
||||||
|
|
||||||
|
|||||||
+32
-8
@@ -1,11 +1,35 @@
|
|||||||
# --- 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.
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# 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
|
||||||
|
steam
|
||||||
|
element
|
||||||
|
totem
|
||||||
|
extension-manager
|
||||||
|
timeshift-gtk
|
||||||
gnome-screenshot
|
gnome-screenshot
|
||||||
|
org.gnome.NautilusPreviewer
|
||||||
|
org.gnome.Shell.Extensions
|
||||||
|
evolution-alarm-notify
|
||||||
|
|||||||
-675
@@ -1,675 +0,0 @@
|
|||||||
// ---------------------------------------------------- //
|
|
||||||
// Simple-Tiling – GNOME Shell 3.38 (X11) - Version 4 //
|
|
||||||
// © 2025 domoel – MIT //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
// 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 ByteArray = imports.byteArray;
|
|
||||||
const TILING_DELAY_MS = 20; // Change Tiling Window Delay
|
|
||||||
const CENTERING_DELAY_MS = 5; // Change Centered Window Delay
|
|
||||||
|
|
||||||
const Me = ExtensionUtils.getCurrentExtension();
|
|
||||||
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
|
|
||||||
const WM_SCHEMA = "org.gnome.desktop.wm.keybindings";
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
_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 //
|
|
||||||
// Main Classes for Tiling Logic //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
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._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() {
|
|
||||||
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) {
|
|
||||||
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)) {
|
|
||||||
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) 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() {
|
|
||||||
return new SimpleTilingExtension();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
// Simple‑Tiling – LEGACY (GNOME Shell 3.38 ‑ 44) //
|
||||||
|
// © 2025 domoel – MIT //
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ── GLOBAL IMPORTS ────────────────────────────────────────
|
||||||
|
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 ────────────────────────────────────────────
|
||||||
|
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'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── HELPER‑FUNCTION ────────────────────────────────────────
|
||||||
|
function addKeybinding(name, settings, flags, mode, handler) {
|
||||||
|
if (Main.wm?.addKeybinding)
|
||||||
|
Main.wm.addKeybinding(name, settings, flags, mode, handler);
|
||||||
|
else
|
||||||
|
global.display.add_keybinding(name, settings, flags, mode, handler);
|
||||||
|
}
|
||||||
|
function removeKeybinding(name) {
|
||||||
|
if (Main.wm?.removeKeybinding)
|
||||||
|
Main.wm.removeKeybinding(name);
|
||||||
|
else
|
||||||
|
global.display.remove_keybinding(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkAreaForMonitor(monitorIndex) {
|
||||||
|
if (Main.layoutManager?.getWorkAreaForMonitor)
|
||||||
|
return Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
|
||||||
|
|
||||||
|
return global.workspace_manager
|
||||||
|
.get_active_workspace()
|
||||||
|
.get_work_area_for_monitor(monitorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeUtf8(bytes) {
|
||||||
|
if (typeof ByteArray !== 'undefined')
|
||||||
|
return ByteArray.toString(bytes);
|
||||||
|
return new TextDecoder('utf-8').decode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPointer() {
|
||||||
|
return global.get_pointer ? global.get_pointer()
|
||||||
|
: global.display.get_pointer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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(k =>
|
||||||
|
this._wmSettings.set_value(k, 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, win) => {
|
||||||
|
if (this.tiler.windows.includes(win))
|
||||||
|
this.tiler.grabbedWindow = win;
|
||||||
|
}));
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect('grab-op-end', this._onGrabEnd.bind(this)));
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._wmKeysToDisable.length)
|
||||||
|
this._wmKeysToDisable.forEach(k =>
|
||||||
|
this._wmSettings.set_value(k, this._savedWmShortcuts[k]));
|
||||||
|
|
||||||
|
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) {
|
||||||
|
addKeybinding(key, this._settings,
|
||||||
|
Meta.KeyBindingFlags.NONE,
|
||||||
|
Shell.ActionMode.NORMAL,
|
||||||
|
() => callback(this));
|
||||||
|
}
|
||||||
|
_bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k,h); }
|
||||||
|
_unbindAllShortcuts(){ for (const k in KEYBINDINGS) removeKeybinding(k); }
|
||||||
|
|
||||||
|
_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(k => this._savedWmShortcuts[k] =
|
||||||
|
this._wmSettings.get_value(k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusInDirection(direction) {
|
||||||
|
const src = global.display.get_focus_window();
|
||||||
|
if (!src || !this.tiler.windows.includes(src)) return;
|
||||||
|
const tgt = this._findTargetInDirection(src, direction);
|
||||||
|
if (tgt) tgt.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapWithMaster() {
|
||||||
|
const w = this.tiler.windows;
|
||||||
|
if (w.length < 2) return;
|
||||||
|
const foc = global.display.get_focus_window();
|
||||||
|
if (!foc || !w.includes(foc)) return;
|
||||||
|
const idx = w.indexOf(foc);
|
||||||
|
if (idx > 0)
|
||||||
|
[w[0], w[idx]] = [w[idx], w[0]];
|
||||||
|
else
|
||||||
|
[w[0], w[1]] = [w[1], w[0]];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
w[0]?.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
_swapInDirection(direction) {
|
||||||
|
const src = global.display.get_focus_window();
|
||||||
|
if (!src || !this.tiler.windows.includes(src)) return;
|
||||||
|
|
||||||
|
let tgt = null;
|
||||||
|
const srcIdx = this.tiler.windows.indexOf(src);
|
||||||
|
if (srcIdx === 0 && direction === 'right' && this.tiler.windows.length>1)
|
||||||
|
tgt = this.tiler.windows[1];
|
||||||
|
else
|
||||||
|
tgt = this._findTargetInDirection(src, direction);
|
||||||
|
|
||||||
|
if (!tgt) return;
|
||||||
|
const tgtIdx = this.tiler.windows.indexOf(tgt);
|
||||||
|
[this.tiler.windows[srcIdx], this.tiler.windows[tgtIdx]] =
|
||||||
|
[this.tiler.windows[tgtIdx], this.tiler.windows[srcIdx]];
|
||||||
|
|
||||||
|
this.tiler.tileNow();
|
||||||
|
src.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetInDirection(src, direction) {
|
||||||
|
const sRect = src.get_frame_rect();
|
||||||
|
const cands = [];
|
||||||
|
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win === src) continue;
|
||||||
|
const tRect = win.get_frame_rect();
|
||||||
|
switch (direction) {
|
||||||
|
case 'left': if (tRect.x < sRect.x) cands.push(win); break;
|
||||||
|
case 'right': if (tRect.x > sRect.x) cands.push(win); break;
|
||||||
|
case 'up': if (tRect.y < sRect.y) cands.push(win); break;
|
||||||
|
case 'down': if (tRect.y > sRect.y) cands.push(win); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!cands.length) return null;
|
||||||
|
|
||||||
|
let best=null, min=Infinity;
|
||||||
|
for (const win of cands) {
|
||||||
|
const tRect = win.get_frame_rect();
|
||||||
|
const dev = (direction==='left'||direction==='right')
|
||||||
|
? Math.abs(sRect.y - tRect.y)
|
||||||
|
: Math.abs(sRect.x - tRect.x);
|
||||||
|
if (dev < min) { min=dev; best=win; }
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onGrabEnd() {
|
||||||
|
const grabbed = this.tiler.grabbedWindow;
|
||||||
|
if (!grabbed) return;
|
||||||
|
|
||||||
|
const tgt = this._findTargetUnderPointer(grabbed);
|
||||||
|
if (tgt) {
|
||||||
|
const a = this.tiler.windows.indexOf(grabbed);
|
||||||
|
const b = this.tiler.windows.indexOf(tgt);
|
||||||
|
[this.tiler.windows[a], this.tiler.windows[b]] =
|
||||||
|
[this.tiler.windows[b], this.tiler.windows[a]];
|
||||||
|
}
|
||||||
|
this.tiler.queueTile();
|
||||||
|
this.tiler.grabbedWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetUnderPointer(exclude) {
|
||||||
|
const [x,y] = getPointer();
|
||||||
|
const wins = global.get_window_actors()
|
||||||
|
.map(a => a.meta_window)
|
||||||
|
.filter(w => w && w!==exclude &&
|
||||||
|
this.tiler.windows.includes(w) &&
|
||||||
|
((()=>{ const f=w.get_frame_rect();
|
||||||
|
return x>=f.x && x<f.x+f.width &&
|
||||||
|
y>=f.y && y<f.y+f.height;})()));
|
||||||
|
if (wins.length) return wins[wins.length-1];
|
||||||
|
|
||||||
|
let best=null, max=0, sRect=exclude.get_frame_rect();
|
||||||
|
for (const w of this.tiler.windows) {
|
||||||
|
if (w===exclude) continue;
|
||||||
|
const tRect=w.get_frame_rect();
|
||||||
|
const ovX = Math.max(0, Math.min(sRect.x+sRect.width,
|
||||||
|
tRect.x+tRect.width) -
|
||||||
|
Math.max(sRect.x, tRect.x));
|
||||||
|
const ovY = Math.max(0, Math.min(sRect.y+sRect.height,
|
||||||
|
tRect.y+tRect.height) -
|
||||||
|
Math.max(sRect.y, tRect.y));
|
||||||
|
const area = ovX*ovY;
|
||||||
|
if (area>max){ max=area; best=w; }
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TILER ────────────────────────────────────────────────
|
||||||
|
class Tiler {
|
||||||
|
constructor() {
|
||||||
|
this.windows = [];
|
||||||
|
this.grabbedWindow = null;
|
||||||
|
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
|
||||||
|
|
||||||
|
this._signalIds = new Map();
|
||||||
|
this._tileTimeoutId = null;
|
||||||
|
this._centerTimeoutIds= [];
|
||||||
|
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._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 wm = global.workspace_manager;
|
||||||
|
this._signalIds.set('workspace-changed', {
|
||||||
|
object: wm,
|
||||||
|
id: wm.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 [,sig] of this._signalIds) {
|
||||||
|
try { sig.object.disconnect(sig.id); } catch {}
|
||||||
|
}
|
||||||
|
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 ? decodeUtf8(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 id = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
|
||||||
|
this._centeringDelay, () => {
|
||||||
|
const idx = this._centerTimeoutIds.indexOf(id);
|
||||||
|
if (idx>-1) this._centerTimeoutIds.splice(idx,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 = 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(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_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-${id}`, {
|
||||||
|
object: win, id: win.connect('size-changed',
|
||||||
|
()=>{ if (!this.grabbedWindow) this.queueTile(); })});
|
||||||
|
this._signalIds.set(`min-${id}`, {
|
||||||
|
object: win, id: win.connect('notify::minimized',
|
||||||
|
this._onWindowMinimizedStateChanged)});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWindowRemoved(workspace, win) {
|
||||||
|
const idx = this.windows.indexOf(win);
|
||||||
|
if (idx>-1) this.windows.splice(idx,1);
|
||||||
|
|
||||||
|
['unmanaged','size','min'].forEach(pref=>{
|
||||||
|
const key = `${pref}-${win.get_id()}`;
|
||||||
|
if (this._signalIds.has(key)) {
|
||||||
|
const {object,id} = this._signalIds.get(key);
|
||||||
|
try{ object.disconnect(id);}catch{}
|
||||||
|
this._signalIds.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onActiveWorkspaceChanged() {
|
||||||
|
this._disconnectFromWorkspace();
|
||||||
|
this._connectToWorkspace();
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectToWorkspace() {
|
||||||
|
const ws = global.workspace_manager.get_active_workspace();
|
||||||
|
ws.list_windows().forEach(w=>this._onWindowAdded(ws,w));
|
||||||
|
this._signalIds.set('win-add', {
|
||||||
|
object: ws, id: ws.connect('window-added', this._onWindowAdded)});
|
||||||
|
this._signalIds.set('win-rem', {
|
||||||
|
object: ws, id: ws.connect('window-removed', this._onWindowRemoved)});
|
||||||
|
this.queueTile();
|
||||||
|
}
|
||||||
|
_disconnectFromWorkspace() {
|
||||||
|
this.windows.slice().forEach(w=>this._onWindowRemoved(null,w));
|
||||||
|
['win-add','win-rem'].forEach(k=>{
|
||||||
|
if (this._signalIds.has(k)) {
|
||||||
|
const {object,id}=this._signalIds.get(k);
|
||||||
|
try{ object.disconnect(id);}catch{}
|
||||||
|
this._signalIds.delete(k);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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 prim = [windows[0]];
|
||||||
|
const sec = windows.slice(1);
|
||||||
|
|
||||||
|
let primArea, secArea;
|
||||||
|
if (area.width > area.height) {
|
||||||
|
const pW = Math.floor(area.width/2) - gap;
|
||||||
|
primArea = {x: area.x, y: area.y,
|
||||||
|
width: pW, height: area.height};
|
||||||
|
secArea = {x: area.x+pW+this._innerGap, y: area.y,
|
||||||
|
width: area.width-pW-this._innerGap,
|
||||||
|
height: area.height};
|
||||||
|
} else {
|
||||||
|
const pH = Math.floor(area.height/2) - gap;
|
||||||
|
primArea = {x: area.x, y: area.y,
|
||||||
|
width: area.width, height: pH};
|
||||||
|
secArea = {x: area.x, y: area.y+pH+this._innerGap,
|
||||||
|
width: area.width,
|
||||||
|
height: area.height-pH-this._innerGap};
|
||||||
|
}
|
||||||
|
this._splitLayout(prim, primArea);
|
||||||
|
this._splitLayout(sec, secArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tileWindows() {
|
||||||
|
const wins = this.windows.filter(w=>!w.minimized);
|
||||||
|
if (!wins.length) return;
|
||||||
|
|
||||||
|
const monitor = Main.layoutManager.primaryMonitor;
|
||||||
|
const work = getWorkAreaForMonitor(monitor.index);
|
||||||
|
const inner = { x: work.x + this._outerGapHorizontal,
|
||||||
|
y: work.y + this._outerGapVertical,
|
||||||
|
width: work.width - 2*this._outerGapHorizontal,
|
||||||
|
height: work.height - 2*this._outerGapVertical };
|
||||||
|
|
||||||
|
wins.forEach(w=>{ if (w.get_maximized())
|
||||||
|
w.unmaximize(Meta.MaximizeFlags.BOTH); });
|
||||||
|
|
||||||
|
if (wins.length===1) {
|
||||||
|
wins[0].move_resize_frame(true,
|
||||||
|
inner.x, inner.y, inner.width, inner.height);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gap = Math.floor(this._innerGap/2);
|
||||||
|
const masterW = Math.floor(inner.width/2) - gap;
|
||||||
|
const master = wins[0];
|
||||||
|
master.move_resize_frame(true,
|
||||||
|
inner.x, inner.y, masterW, inner.height);
|
||||||
|
|
||||||
|
const stack = { x: inner.x + masterW + this._innerGap,
|
||||||
|
y: inner.y,
|
||||||
|
width: inner.width - masterW - this._innerGap,
|
||||||
|
height: inner.height };
|
||||||
|
this._splitLayout(wins.slice(1), stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── EXTENSION‑WRAPPER ───────────────────────────────────
|
||||||
|
class SimpleTilingExtension {
|
||||||
|
enable() { this.tiler = new Tiler(); this.tiler.enable(); }
|
||||||
|
disable() { this.tiler?.disable(); this.tiler = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
return new SimpleTilingExtension();
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"uuid": "simple-tiling@domoel",
|
|
||||||
"name": "Simple Tiling",
|
|
||||||
"description": "A Simple Tiling Extension for Gnome Shell 3.38.",
|
|
||||||
"version": 4,
|
|
||||||
"shell-version": [ "3.38" ],
|
|
||||||
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
|
|
||||||
"preferences_ui": "prefs.js",
|
|
||||||
"url": "https://github.com/Domoel/Simple-Tiling",
|
|
||||||
"gettext-domain": "simple-tiling-domoel"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"uuid": "__UUID__",
|
||||||
|
"name": "Simple Tiling",
|
||||||
|
"description": "A Simple Tiling Extension for Gnome Shell.",
|
||||||
|
"version": __VERSION__,
|
||||||
|
"shell-version": [
|
||||||
|
"3.38",
|
||||||
|
"40",
|
||||||
|
"41",
|
||||||
|
"42",
|
||||||
|
"43",
|
||||||
|
"44"
|
||||||
|
],
|
||||||
|
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
|
||||||
|
"preferences_ui": "prefs.js",
|
||||||
|
"url": "https://github.com/Domoel/Simple-Tiling",
|
||||||
|
"gettext-domain": "__UUID__"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"uuid": "__UUID__",
|
||||||
|
"name": "Simple Tiling",
|
||||||
|
"description": "A Simple Tiling Extension for Gnome Shell.",
|
||||||
|
"version": __VERSION__,
|
||||||
|
"shell-version": [
|
||||||
|
"45",
|
||||||
|
"46",
|
||||||
|
"47",
|
||||||
|
"48"
|
||||||
|
],
|
||||||
|
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
|
||||||
|
"preferences_ui": "prefs.js",
|
||||||
|
"url": "https://github.com/Domoel/Simple-Tiling",
|
||||||
|
"gettext-domain": "__UUID__"
|
||||||
|
}
|
||||||
@@ -0,0 +1,598 @@
|
|||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
// Simple‑Tiling – MODERN (GNOME Shell 45+) //
|
||||||
|
// © 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';
|
||||||
|
import Clutter from 'gi://Clutter';
|
||||||
|
|
||||||
|
// ── CONST ────────────────────────────────────────────
|
||||||
|
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'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── HELPER‑FUNCTION ────────────────────────────────────────
|
||||||
|
function getPointerXY() {
|
||||||
|
if (global.get_pointer) {
|
||||||
|
const [x, y] = global.get_pointer();
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ev = Clutter.get_current_event();
|
||||||
|
if (ev) {
|
||||||
|
const coords = ev.get_coords();
|
||||||
|
if (Array.isArray(coords))
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = Clutter.get_default_backend()
|
||||||
|
.get_default_seat()
|
||||||
|
.get_pointer();
|
||||||
|
return device ? device.get_position() : [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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(k =>
|
||||||
|
this._wmSettings.set_value(k, new GLib.Variant('as', [])));
|
||||||
|
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
this._settingsChangedId =
|
||||||
|
this._settings.connect('changed', () => this._onSettingsChanged());
|
||||||
|
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect('grab-op-begin',
|
||||||
|
(_, __, win) => { if (this.tiler.windows.includes(win))
|
||||||
|
this.tiler.grabbedWindow = win; })
|
||||||
|
);
|
||||||
|
this._grabOpIds.push(
|
||||||
|
global.display.connect('grab-op-end', () => this._onGrabEnd())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._wmKeysToDisable.length)
|
||||||
|
this._wmKeysToDisable.forEach(k =>
|
||||||
|
this._wmSettings.set_value(k, this._savedWmShortcuts[k]));
|
||||||
|
|
||||||
|
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, handler) {
|
||||||
|
global.display.add_keybinding(
|
||||||
|
key,
|
||||||
|
this._settings,
|
||||||
|
Meta.KeyBindingFlags.NONE,
|
||||||
|
Shell.ActionMode.NORMAL,
|
||||||
|
(..._args) => handler(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k, h); }
|
||||||
|
_unbindAllShortcuts(){ for (const k in KEYBINDINGS) global.display.remove_keybinding(k); }
|
||||||
|
|
||||||
|
_onSettingsChanged() {
|
||||||
|
this._unbindAllShortcuts();
|
||||||
|
this._bindAllShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
_prepareWmShortcuts() {
|
||||||
|
const schema = this._wmSettings.settings_schema;
|
||||||
|
if (!schema) return;
|
||||||
|
|
||||||
|
const keys = [];
|
||||||
|
|
||||||
|
const add = key => { if (schema.has_key(key)) keys.push(key); };
|
||||||
|
|
||||||
|
if (schema.has_key('toggle-tiled-left'))
|
||||||
|
keys.push('toggle-tiled-left', 'toggle-tiled-right');
|
||||||
|
else {
|
||||||
|
add('tile-left'); add('tile-right');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.has_key('toggle-maximized'))
|
||||||
|
keys.push('toggle-maximized');
|
||||||
|
else {
|
||||||
|
add('maximize'); add('unmaximize');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.length) {
|
||||||
|
this._wmKeysToDisable = keys;
|
||||||
|
keys.forEach(k => this._savedWmShortcuts[k] =
|
||||||
|
this._wmSettings.get_value(k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusInDirection(direction) {
|
||||||
|
const src = global.display.get_focus_window();
|
||||||
|
if (!src || !this.tiler.windows.includes(src)) return;
|
||||||
|
const tgt = this._findTargetInDirection(src, direction);
|
||||||
|
if (tgt) tgt.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapWithMaster() {
|
||||||
|
const w = this.tiler.windows;
|
||||||
|
if (w.length < 2) return;
|
||||||
|
const foc = global.display.get_focus_window();
|
||||||
|
if (!foc || !w.includes(foc)) return;
|
||||||
|
const idx = w.indexOf(foc);
|
||||||
|
if (idx > 0) [w[0], w[idx]] = [w[idx], w[0]];
|
||||||
|
else [w[0], w[1]] = [w[1], w[0]];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
w[0]?.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapInDirection(direction) {
|
||||||
|
const src = global.display.get_focus_window();
|
||||||
|
if (!src || !this.tiler.windows.includes(src)) return;
|
||||||
|
let tgt = null;
|
||||||
|
const idx = this.tiler.windows.indexOf(src);
|
||||||
|
if (idx === 0 && direction==='right' && this.tiler.windows.length>1)
|
||||||
|
tgt = this.tiler.windows[1];
|
||||||
|
else
|
||||||
|
tgt = this._findTargetInDirection(src, direction);
|
||||||
|
if (!tgt) return;
|
||||||
|
const tidx = this.tiler.windows.indexOf(tgt);
|
||||||
|
[this.tiler.windows[idx], this.tiler.windows[tidx]] =
|
||||||
|
[this.tiler.windows[tidx], this.tiler.windows[idx]];
|
||||||
|
this.tiler.tileNow();
|
||||||
|
src.activate(global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetInDirection(src, dir) {
|
||||||
|
const sRect = src.get_frame_rect(), cand=[];
|
||||||
|
for (const win of this.tiler.windows) {
|
||||||
|
if (win===src) continue;
|
||||||
|
const r=win.get_frame_rect();
|
||||||
|
if (dir==='left' && r.x<sRect.x) cand.push(win);
|
||||||
|
if (dir==='right'&& r.x>sRect.x) cand.push(win);
|
||||||
|
if (dir==='up' && r.y<sRect.y) cand.push(win);
|
||||||
|
if (dir==='down' && r.y>sRect.y) cand.push(win);
|
||||||
|
}
|
||||||
|
if (!cand.length) return null;
|
||||||
|
let best=null, min=Infinity;
|
||||||
|
for (const w of cand) {
|
||||||
|
const r=w.get_frame_rect();
|
||||||
|
const dev = (dir==='left'||dir==='right')
|
||||||
|
? Math.abs(sRect.y - r.y)
|
||||||
|
: Math.abs(sRect.x - r.x);
|
||||||
|
if (dev<min){min=dev; best=w;}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onGrabEnd() {
|
||||||
|
const grabbed = this.tiler.grabbedWindow;
|
||||||
|
if (!grabbed) return;
|
||||||
|
const tgt = this._findTargetUnderPointer(grabbed);
|
||||||
|
if (tgt) {
|
||||||
|
const a = this.tiler.windows.indexOf(grabbed);
|
||||||
|
const b = this.tiler.windows.indexOf(tgt);
|
||||||
|
[this.tiler.windows[a], this.tiler.windows[b]] =
|
||||||
|
[this.tiler.windows[b], this.tiler.windows[a]];
|
||||||
|
}
|
||||||
|
this.tiler.queueTile();
|
||||||
|
this.tiler.grabbedWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTargetUnderPointer(exclude) {
|
||||||
|
const [x,y] = getPointerXY();
|
||||||
|
const wins = global.get_window_actors()
|
||||||
|
.map(a=>a.meta_window)
|
||||||
|
.filter(w=>w && w!==exclude &&
|
||||||
|
this.tiler.windows.includes(w) && (()=>{const f=w.get_frame_rect();
|
||||||
|
return x>=f.x && x<f.x+f.width &&
|
||||||
|
y>=f.y && y<f.y+f.height;})());
|
||||||
|
if (wins.length) return wins[wins.length-1];
|
||||||
|
|
||||||
|
let best=null, max=0, sRect=exclude.get_frame_rect();
|
||||||
|
for (const w of this.tiler.windows) {
|
||||||
|
if (w===exclude) continue;
|
||||||
|
const r=w.get_frame_rect();
|
||||||
|
const ovX=Math.max(0, Math.min(sRect.x+sRect.width, r.x+r.width)-Math.max(sRect.x,r.x));
|
||||||
|
const ovY=Math.max(0, Math.min(sRect.y+sRect.height,r.y+r.height)-Math.max(sRect.y,r.y));
|
||||||
|
const area=ovX*ovY;
|
||||||
|
if (area>max){max=area; best=w;}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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 [,sig] of this._signalIds) {
|
||||||
|
try { sig.object.disconnect(sig.id); } catch {}
|
||||||
|
}
|
||||||
|
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=[]; return; }
|
||||||
|
|
||||||
|
const txt = new TextDecoder('utf-8').decode(data);
|
||||||
|
this._exceptions = txt.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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── EXTENSION‑WRAPPER ───────────────────────────────────
|
||||||
|
export default class ModernExtension extends Extension {
|
||||||
|
enable() { this.tiler = new Tiler(this); this.tiler.enable(); }
|
||||||
|
disable() { this.tiler?.disable(); this.tiler = null; }
|
||||||
|
}
|
||||||
+32
-76
@@ -1,11 +1,10 @@
|
|||||||
// ------------------------------------------------------ //
|
///////////////////////////////////////////////////////////////
|
||||||
// Extension Settings Menu for Simple Tiling - Version 4 //
|
// Simple‑Tiling – LEGACY MENU (GNOME Shell 3.38 ‑ 44) //
|
||||||
// © 2025 domoel – MIT //
|
// © 2025 domoel – MIT //
|
||||||
// ------------------------------------------------------ //
|
/////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
// Global Imports //
|
// ── GLOBAL IMPORTS ────────────────────────────────────────
|
||||||
// ---------------------------------------------------- //
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const { Gtk, GObject, Gio } = imports.gi;
|
const { Gtk, GObject, Gio } = imports.gi;
|
||||||
@@ -13,9 +12,7 @@ const ExtensionUtils = imports.misc.extensionUtils;
|
|||||||
|
|
||||||
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
|
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
// ── DEFINITIONS ────────────────────────────────────────────
|
||||||
// Definition of Row Model //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
const COLUMN_ID = 0;
|
const COLUMN_ID = 0;
|
||||||
const COLUMN_DESC = 1;
|
const COLUMN_DESC = 1;
|
||||||
const COLUMN_KEY = 2;
|
const COLUMN_KEY = 2;
|
||||||
@@ -36,11 +33,9 @@ function buildPrefsWidget() {
|
|||||||
visible: true,
|
visible: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
// ── KEYBINDINGS ────────────────────────────────────────────
|
||||||
// Section for Keybindings //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
const keysTitle = new Gtk.Label({
|
const keysTitle = new Gtk.Label({
|
||||||
label: "<b>Tastenkürzel</b>",
|
label: "<b>Keybindings</b>",
|
||||||
use_markup: true,
|
use_markup: true,
|
||||||
halign: Gtk.Align.START,
|
halign: Gtk.Align.START,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -66,41 +61,17 @@ function buildPrefsWidget() {
|
|||||||
GObject.TYPE_INT,
|
GObject.TYPE_INT,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
addKeybinding(
|
addKeybinding(store, settings, "swap-master-window", "Swap current window with master");
|
||||||
store,
|
|
||||||
settings,
|
addKeybinding(store, settings, "swap-up-window", "Swap current window with window above");
|
||||||
"swap-master-window",
|
addKeybinding(store, settings, "swap-down-window", "Swap current window with window below");
|
||||||
"Master-Fenster tauschen"
|
addKeybinding(store, settings, "swap-left-window", "Swap current window with window to the left");
|
||||||
);
|
addKeybinding(store, settings, "swap-right-window", "Swap current window with window to the right");
|
||||||
addKeybinding(
|
|
||||||
store,
|
|
||||||
settings,
|
|
||||||
"swap-left-window",
|
|
||||||
"Fenster nach links tauschen"
|
|
||||||
);
|
|
||||||
addKeybinding(
|
|
||||||
store,
|
|
||||||
settings,
|
|
||||||
"swap-right-window",
|
|
||||||
"Fenster nach rechts tauschen"
|
|
||||||
);
|
|
||||||
addKeybinding(
|
|
||||||
store,
|
|
||||||
settings,
|
|
||||||
"swap-up-window",
|
|
||||||
"Fenster nach oben tauschen"
|
|
||||||
);
|
|
||||||
addKeybinding(
|
|
||||||
store,
|
|
||||||
settings,
|
|
||||||
"swap-down-window",
|
|
||||||
"Fenster nach unten tauschen"
|
|
||||||
);
|
|
||||||
|
|
||||||
addKeybinding(store, settings, "focus-left", "Fokus nach links wechseln");
|
addKeybinding(store, settings, "focus-up", "Focus window above");
|
||||||
addKeybinding(store, settings, "focus-right", "Fokus nach rechts wechseln");
|
addKeybinding(store, settings, "focus-down", "Focus window below");
|
||||||
addKeybinding(store, settings, "focus-up", "Fokus nach oben wechseln");
|
addKeybinding(store, settings, "focus-left", "Focus window to the left");
|
||||||
addKeybinding(store, settings, "focus-down", "Fokus nach unten wechseln");
|
addKeybinding(store, settings, "focus-right", "Focus window to the right");
|
||||||
|
|
||||||
let treeView = new Gtk.TreeView({
|
let treeView = new Gtk.TreeView({
|
||||||
model: store,
|
model: store,
|
||||||
@@ -146,11 +117,9 @@ function buildPrefsWidget() {
|
|||||||
|
|
||||||
prefsWidget.add(keysFrame);
|
prefsWidget.add(keysFrame);
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
// ── WINDOW GAPS ────────────────────────────────────────────
|
||||||
// Section for Window Gaps //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
const gapsTitle = new Gtk.Label({
|
const gapsTitle = new Gtk.Label({
|
||||||
label: "<b>Fensterabstände (Gaps)</b>",
|
label: "<b>Window Gaps</b>",
|
||||||
use_markup: true,
|
use_markup: true,
|
||||||
halign: Gtk.Align.START,
|
halign: Gtk.Align.START,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -168,29 +137,15 @@ function buildPrefsWidget() {
|
|||||||
});
|
});
|
||||||
gapsFrame.add(gapsGrid);
|
gapsFrame.add(gapsGrid);
|
||||||
|
|
||||||
addSpinButtonRow(gapsGrid, settings, "Innerer Abstand", "inner-gap", 0);
|
addSpinButtonRow(gapsGrid, settings, "Inner Gap", "inner-gap", 0);
|
||||||
addSpinButtonRow(
|
addSpinButtonRow(gapsGrid, settings, "Outer Gap (horizontal)", "outer-gap-horizontal", 1);
|
||||||
gapsGrid,
|
addSpinButtonRow(gapsGrid, settings, "Outer Gap (vertical)", "outer-gap-vertical", 2);
|
||||||
settings,
|
|
||||||
"Äußerer Abstand (horizontal)",
|
|
||||||
"outer-gap-horizontal",
|
|
||||||
1
|
|
||||||
);
|
|
||||||
addSpinButtonRow(
|
|
||||||
gapsGrid,
|
|
||||||
settings,
|
|
||||||
"Äußerer Abstand (vertikal)",
|
|
||||||
"outer-gap-vertical",
|
|
||||||
2
|
|
||||||
);
|
|
||||||
|
|
||||||
prefsWidget.add(gapsFrame);
|
prefsWidget.add(gapsFrame);
|
||||||
|
|
||||||
// ---------------------------------------------------- //
|
// ── WINDOW BEHAVIOR ────────────────────────────────────────────
|
||||||
// Section for Window Behavior (Master vs. Stack) //
|
|
||||||
// ---------------------------------------------------- //
|
|
||||||
const behaviorTitle = new Gtk.Label({
|
const behaviorTitle = new Gtk.Label({
|
||||||
label: "<b>Fensterverhalten</b>",
|
label: "<b>Window Behavior</b>",
|
||||||
use_markup: true,
|
use_markup: true,
|
||||||
halign: Gtk.Align.START,
|
halign: Gtk.Align.START,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -210,12 +165,13 @@ function buildPrefsWidget() {
|
|||||||
addComboBoxRow(
|
addComboBoxRow(
|
||||||
behaviorGrid,
|
behaviorGrid,
|
||||||
settings,
|
settings,
|
||||||
"Neues Fenster öffnen als",
|
"Open new windows as",
|
||||||
"new-window-behavior",
|
"new-window-behavior",
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
prefsWidget.add(behaviorFrame);
|
prefsWidget.add(behaviorFrame);
|
||||||
|
|
||||||
|
prefsWidget.show_all();
|
||||||
return prefsWidget;
|
return prefsWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +195,7 @@ function addSpinButtonRow(grid, settings, desc, key, pos) {
|
|||||||
visible: true,
|
visible: true,
|
||||||
});
|
});
|
||||||
grid.attach(label, 0, pos, 1, 1);
|
grid.attach(label, 0, pos, 1, 1);
|
||||||
const adj = new Gtk.Adjustment({ lower: 0, upper: 50, step_increment: 1 });
|
const adj = new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 });
|
||||||
const spin = new Gtk.SpinButton({
|
const spin = new Gtk.SpinButton({
|
||||||
adjustment: adj,
|
adjustment: adj,
|
||||||
climb_rate: 1,
|
climb_rate: 1,
|
||||||
@@ -261,8 +217,8 @@ function addComboBoxRow(grid, settings, desc, key, pos) {
|
|||||||
visible: true,
|
visible: true,
|
||||||
halign: Gtk.Align.END,
|
halign: Gtk.Align.END,
|
||||||
});
|
});
|
||||||
combo.append("stack", "Stack-Fenster (Standard)");
|
combo.append("stack", "Stack Window (Default)");
|
||||||
combo.append("master", "Master-Fenster");
|
combo.append("master", "Master Window");
|
||||||
combo.set_active_id(settings.get_string(key));
|
combo.set_active_id(settings.get_string(key));
|
||||||
combo.connect("changed", () => {
|
combo.connect("changed", () => {
|
||||||
settings.set_string(key, combo.get_active_id());
|
settings.set_string(key, combo.get_active_id());
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
// Simple‑Tiling – MODERN MENU (GNOME Shell 45+) //
|
||||||
|
// © 2025 domoel – MIT //
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// ── GLOBAL IMPORTS ────────────────────────────────────────
|
||||||
|
import { ExtensionPreferences } from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||||
|
import Adw from 'gi://Adw';
|
||||||
|
import Gio from 'gi://Gio';
|
||||||
|
import Gtk from 'gi://Gtk';
|
||||||
|
|
||||||
|
export default class SimpleTilingPrefs 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',
|
||||||
|
description: 'Adjust spacing between windows and screen edges.'
|
||||||
|
});
|
||||||
|
page.add(groupGaps);
|
||||||
|
|
||||||
|
const rowInnerGap = new Adw.SpinRow({
|
||||||
|
title: 'Inner Gap',
|
||||||
|
subtitle: 'Space between tiled windows (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 rowOuterH = new Adw.SpinRow({
|
||||||
|
title: 'Outer Gap (horizontal)',
|
||||||
|
subtitle: 'Left / right screen edges (pixels)',
|
||||||
|
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
||||||
|
});
|
||||||
|
groupGaps.add(rowOuterH);
|
||||||
|
settings.bind('outer-gap-horizontal', rowOuterH, 'value', Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
|
||||||
|
const rowOuterV = new Adw.SpinRow({
|
||||||
|
title: 'Outer Gap (vertical)',
|
||||||
|
subtitle: 'Top / bottom screen edges (pixels)',
|
||||||
|
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
|
||||||
|
});
|
||||||
|
groupGaps.add(rowOuterV);
|
||||||
|
settings.bind('outer-gap-vertical', rowOuterV, '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: 'Whether a new window starts as Master or Stack',
|
||||||
|
model: new Gtk.StringList({
|
||||||
|
strings: ['Stack Window (Default)', 'Master Window'],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
groupBehavior.add(rowNewWindow);
|
||||||
|
|
||||||
|
rowNewWindow.selected = settings.get_string('new-window-behavior') === 'master' ? 1 : 0;
|
||||||
|
|
||||||
|
rowNewWindow.connect('notify::selected', () => {
|
||||||
|
const newVal = rowNewWindow.selected === 1 ? 'master' : 'stack';
|
||||||
|
settings.set_string('new-window-behavior', newVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── KEYBINDINGS ────────────────────────────────────────────
|
||||||
|
const groupKeys = new Adw.PreferencesGroup({ title: 'Keybindings' });
|
||||||
|
page.add(groupKeys);
|
||||||
|
|
||||||
|
const rowKeys = new Adw.ActionRow({
|
||||||
|
title: 'Configure Shortcuts',
|
||||||
|
subtitle: 'Adjust all shortcuts in GNOME Keyboard settings.',
|
||||||
|
});
|
||||||
|
groupKeys.add(rowKeys);
|
||||||
|
|
||||||
|
const btnOpenKeyboard = new Gtk.Button({ label: 'Open Keyboard Settings' });
|
||||||
|
btnOpenKeyboard.connect('clicked', () => {
|
||||||
|
const appInfo = Gio.AppInfo.create_from_commandline(
|
||||||
|
'gnome-control-center keyboard', null, Gio.AppInfoCreateFlags.NONE
|
||||||
|
);
|
||||||
|
appInfo.launch([], null);
|
||||||
|
});
|
||||||
|
rowKeys.add_suffix(btnOpenKeyboard);
|
||||||
|
rowKeys.set_activatable_widget(btnOpenKeyboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -1,62 +1,62 @@
|
|||||||
<?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" path="/org/gnome/shell/extensions/simple-tiling-domoel/">
|
<schema id="org.gnome.shell.extensions.simple-tiling.domoel" path="/org/gnome/shell/extensions/simple-tiling/domoel/">
|
||||||
|
|
||||||
<key name="swap-master-window" type="as">
|
<key name="swap-master-window" type="as">
|
||||||
<default><![CDATA[['<Super>Return']]]></default>
|
<default><![CDATA[['<Super>Return']]]></default>
|
||||||
<summary>Tauscht das fokussierte Fenster mit dem Master.</summary>
|
<summary>Swap current window with master.</summary>
|
||||||
</key>
|
|
||||||
<key name="swap-left-window" type="as">
|
|
||||||
<default><![CDATA[['<Super>Left']]]></default>
|
|
||||||
<summary>Tauscht das Fenster mit dem linken Nachbarn.</summary>
|
|
||||||
</key>
|
|
||||||
<key name="swap-right-window" type="as">
|
|
||||||
<default><![CDATA[['<Super>Right']]]></default>
|
|
||||||
<summary>Tauscht das Fenster mit dem rechten Nachbarn.</summary>
|
|
||||||
</key>
|
</key>
|
||||||
<key name="swap-up-window" type="as">
|
<key name="swap-up-window" type="as">
|
||||||
<default><![CDATA[['<Super>Up']]]></default>
|
<default><![CDATA[['<Super>Up']]]></default>
|
||||||
<summary>Tauscht das Fenster mit dem oberen Nachbarn.</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><![CDATA[['<Super>Down']]]></default>
|
<default><![CDATA[['<Super>Down']]]></default>
|
||||||
<summary>Tauscht das Fenster mit dem unteren Nachbarn.</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>
|
||||||
|
|
||||||
<key name="focus-up" type="as">
|
<key name="focus-up" type="as">
|
||||||
<default><![CDATA[['<Alt>k']]]></default>
|
<default><![CDATA[['<Alt>Up']]]></default>
|
||||||
<summary>Fokus zum oberen Fenster wechseln.</summary>
|
<summary>Focus window above.</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="focus-down" type="as">
|
<key name="focus-down" type="as">
|
||||||
<default><![CDATA[['<Alt>j']]]></default>
|
<default><![CDATA[['<Alt>Down']]]></default>
|
||||||
<summary>Fokus zum unteren Fenster wechseln.</summary>
|
<summary>Focus window below.</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="focus-left" type="as">
|
<key name="focus-left" type="as">
|
||||||
<default><![CDATA[['<Alt>h']]]></default>
|
<default><![CDATA[['<Alt>Left']]]></default>
|
||||||
<summary>Fokus zum linken Fenster wechseln.</summary>
|
<summary>Focus window to the left.</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="focus-right" type="as">
|
<key name="focus-right" type="as">
|
||||||
<default><![CDATA[['<Alt>l']]]></default>
|
<default><![CDATA[['<Alt>Right']]]></default>
|
||||||
<summary>Fokus zum rechten Fenster wechseln.</summary>
|
<summary>Focus window to the right.</summary>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<key name="inner-gap" type="i">
|
<key name="inner-gap" type="i">
|
||||||
<default>10</default>
|
<default>10</default>
|
||||||
<summary>Der Abstand zwischen den Fenstern in Pixeln.</summary>
|
<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>
|
||||||
<key name="outer-gap-vertical" type="i">
|
<key name="outer-gap-vertical" type="i">
|
||||||
<default>5</default>
|
<default>5</default>
|
||||||
<summary>Der Abstand zum oberen und unteren Bildschirmrand.</summary>
|
<summary>The gap to the top and bottom screen edges.</summary>
|
||||||
</key>
|
|
||||||
<key name="outer-gap-horizontal" type="i">
|
|
||||||
<default>10</default>
|
|
||||||
<summary>Der Abstand zum linken und rechten Bildschirmrand.</summary>
|
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<key name="new-window-behavior" type="s">
|
<key name="new-window-behavior" type="s">
|
||||||
<default>'stack'</default>
|
<default>'stack'</default>
|
||||||
<summary>Verhalten für neu geöffnete Fenster.</summary>
|
<summary>Behavior for newly opened windows.</summary>
|
||||||
<description>Legt fest, ob ein neues Fenster als Master oder als Teil des Stacks hinzugefügt wird.</description>
|
<description>Determines if a new window is added as master or stack window.</description>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
</schema>
|
</schema>
|
||||||
|
|||||||
Reference in New Issue
Block a user