18 Commits

Author SHA1 Message Date
Dome 0a2c273a2b Update README.md 2025-07-30 00:10:54 +02:00
Dome 14357adb1e Update README.md 2025-07-30 00:09:41 +02:00
Dome 31a61478ad Update README.md 2025-07-30 00:08:25 +02:00
Dome 2eb304a16a Update LICENSE 2025-07-29 23:49:01 +02:00
Dome 8480e6ccaf Update exceptions.txt 2025-07-29 23:48:44 +02:00
Dome f7a86e51b1 Create Makefile 2025-07-29 23:48:26 +02:00
Dome 1afeb816c4 Create modern.js 2025-07-29 23:48:00 +02:00
Dome 83ceb4ce67 Update and rename extension.js to legacy.js 2025-07-29 23:47:41 +02:00
Dome 6e90d12ee9 Create metadata_modern.json.in 2025-07-29 23:47:07 +02:00
Dome 717ef0b16b Update and rename metadata.json to metadata_legacy.json.in 2025-07-29 23:46:47 +02:00
Dome 1896d992d0 Create prefs_modern.js 2025-07-29 23:46:15 +02:00
Dome 749e3a0275 Update and rename prefs.js to prefs_legacy.js 2025-07-29 23:45:53 +02:00
Dome 1daeb0e100 Update README.md 2025-07-28 08:40:59 +02:00
Dome defa7255df Update README.md 2025-07-28 08:40:21 +02:00
Dome 6031a36664 Update README.md 2025-07-28 08:39:59 +02:00
Dome 021c51040f Update README.md 2025-07-28 00:52:55 +02:00
Dome 98bfaaa3d5 Update README.md 2025-07-27 23:49:09 +02:00
Dome cb26f3aebb Update README.md 2025-07-27 23:47:22 +02:00
12 changed files with 1014 additions and 1058 deletions
+1 -1
View File
@@ -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
+87
View File
@@ -0,0 +1,87 @@
###############################################################################
# SimpleTiling  Makefile
#
# make build → baut beide ZIPPakete
# make build-legacy → nur LegacyZIP (Shell 3.3844)
# make build-modern → nur ModernZIP (Shell 4548)
# 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
# PrefDateien (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
###############################################################################
# LegacyBuild
###############################################################################
build-legacy:
@echo "==> Building LEGACY package (3.3844)…"
@rm -rf build && mkdir -p build/$(UUID)
$(call copies,$(COMMON_FILES),build/$(UUID))
# Schema kompilieren
@glib-compile-schemas build/$(UUID)/schemas
# Haupt und PrefSkript
@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
# ZipPaket
@cd build && zip -qr ../$(UUID)-legacy-v$(VERSION).zip .
@rm -rf build
@echo "✓ created $(UUID)-legacy-v$(VERSION).zip"
###############################################################################
# ModernBuild
###############################################################################
build-modern:
@echo "==> Building MODERN package (4548)…"
@rm -rf build && mkdir -p build/$(UUID)
$(call copies,$(COMMON_FILES),build/$(UUID))
# Schema kompilieren
@glib-compile-schemas build/$(UUID)/schemas
# Haupt und PrefSkript
@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
# ZipPaket
@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 "BuildOrdner und ZIPs entfernt."
+34 -22
View File
@@ -4,12 +4,11 @@ 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>
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![GNOME Shell Version](https://img.shields.io/badge/GNOME%20Shell-3.38-blue)
<img width="2560" height="1440" alt="Simple-Tiling-v6" src="https://github.com/user-attachments/assets/eb0f7cc3-6a5a-4036-8a1e-8f945c52e55c" /> <img width="2560" height="1440" alt="Simple-Tiling-v6" src="https://github.com/user-attachments/assets/eb0f7cc3-6a5a-4036-8a1e-8f945c52e55c" />
@@ -17,7 +16,7 @@ A lightweight, opinionated, and automatic tiling window manager for GNOME Shell
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
@@ -36,10 +35,10 @@ This extension was built from the ground up to be stable and performant on **GNO
## 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,26 +49,40 @@ Use the [GNOME Shell Extensions website](https://extensions.gnome.org/extension/
#### Manual Installation #### Manual Installation
1. **Navigate to your extensions folder:** The repository includes a Makefile that produces readytoinstall ZIP packages for the two supported GNOMEShell lines (a legacy build (Gnome-Shell 3.38 - 44) and a modern build for Gnome-Shell 45+).
1. **Clone the Source**
```bash
git clone https://github.com/YourUser/Simple-Tiling.git
cd Simple-Tiling
```
2 · **Create the package that matches your GNOME-Shell version**
Open the Terminal within the Simple-Tiling directory and run
```bash ```bash
cd ~/.local/share/gnome-shell/extensions/ make build
``` ```
3. **Clone the repository directly into a folder named after the extension's UUID:** **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.
```bash 3 · **Locate the output**
git clone https://github.com/Domoel/Simple-Tiling.git simple-tiling@domoel ```bash
``` ls -1 ../simple-tiling@domoel-*-v*.zip
5. **Compile the GSettings schema.** This is a mandatory step for the keyboard shortcuts to work. ```
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/)
```bash 5 · **Reload the shell**
cd ~/.local/share/gnome-shell/extensions/simple-tiling@domoel ```bash
glib-compile-schemas schemas/ Press Alt + F2, type r , hit ↩ (works for X11 and Wayland)
``` ```
6 · **Clean up (optional)**
3. **Restart GNOME Shell.** Press `Alt` + `F2`, type `r`, and press `Enter`. ```bash
make clean # removes build/ folder and generated ZIPs
5. **Enable the extension** using the GNOME Extensions app or GNOME Tweaks. ```
**Note:** You have to use "simple-tiling@domoel" as your extension folder / directory. Put all necessary files into this directory. Otherwise the extension will not show up in extension manager. **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.
@@ -113,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.
+9
View File
@@ -24,3 +24,12 @@
# --- Start of the Exception List --- # --- Start of the Exception List ---
ulauncher ulauncher
steam
element
totem
extension-manager
timeshift-gtk
gnome-screenshot
org.gnome.NautilusPreviewer
org.gnome.Shell.Extensions
evolution-alarm-notify
-41
View File
@@ -1,41 +0,0 @@
/////////////////////////////////////////////////////////////
// Simple-Tiling GLOBAL CONFIG //
// © 2025 domoel MIT //
//////////////////////////////////////////////////////////
// --- GLOBAL IMPORTS ---
'use strict';
const ExtensionUtils = imports.misc.extensionUtils;
const Config = imports.misc.config;
const Me = ExtensionUtils.getCurrentExtension();
const [SHELL_MAJOR] = Config.PACKAGE_VERSION.split('.').map(n => parseInt(n));
let extension = null;
function init() {
}
// --- SHELL SWITCH ---
async function enable() {
try {
if (SHELL_MAJOR >= 40) {
const module = await import('./modern.js');
extension = new module.default(Me.metadata);
} else {
const { LegacyExtension } = Me.imports.legacy;
extension = new LegacyExtension(Me.metadata);
}
extension.enable();
} catch (e) {
logError(e, `[Simple Tiling] Failed to enable extension`);
}
}
function disable() {
if (extension) {
extension.disable();
extension = null;
}
}
+317 -445
View File
@@ -1,110 +1,132 @@
///////////////////////////////////////////////////////////////
// SimpleTiling  LEGACY (GNOME Shell 3.38  44) //
// © 2025domoel  MIT //
///////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////
// Simple-Tiling LEGACY (for GNOME Shell 3.38) //
// © 2025 domoel MIT //
//////////////////////////////////////////////////////////
// --- GLOBAL IMPORTS ---
'use strict'; 'use strict';
// ── GLOBAL IMPORTS ────────────────────────────────────────
const Main = imports.ui.main; const Main = imports.ui.main;
const Meta = imports.gi.Meta; const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell; const Shell = imports.gi.Shell;
const Gio = imports.gi.Gio; const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib; const GLib = imports.gi.GLib;
const ExtensionUtils = imports.misc.extensionUtils; const ExtensionUtils= imports.misc.extensionUtils;
const ByteArray = imports.byteArray; const ByteArray = imports.byteArray;
const Me = ExtensionUtils.getCurrentExtension(); const Me = ExtensionUtils.getCurrentExtension();
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
const WM_SCHEMA = "org.gnome.desktop.wm.keybindings"; // ── 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 TILING_DELAY_MS = 20; // Change Tiling Window Delay
const CENTERING_DELAY_MS = 5; // Change Centered Window Delay const CENTERING_DELAY_MS = 5; // Change Centered Window Delay
const KEYBINDINGS = { const KEYBINDINGS = {
"swap-master-window": (self) => self._swapWithMaster(), 'swap-master-window': (self) => self._swapWithMaster(),
"swap-left-window": (self) => self._swapInDirection("left"), 'swap-left-window': (self) => self._swapInDirection('left'),
"swap-right-window": (self) => self._swapInDirection("right"), 'swap-right-window': (self) => self._swapInDirection('right'),
"swap-up-window": (self) => self._swapInDirection("up"), 'swap-up-window': (self) => self._swapInDirection('up'),
"swap-down-window": (self) => self._swapInDirection("down"), 'swap-down-window': (self) => self._swapInDirection('down'),
"focus-left": (self) => self._focusInDirection("left"), 'focus-left': (self) => self._focusInDirection('left'),
"focus-right": (self) => self._focusInDirection("right"), 'focus-right': (self) => self._focusInDirection('right'),
"focus-up": (self) => self._focusInDirection("up"), 'focus-up': (self) => self._focusInDirection('up'),
"focus-down": (self) => self._focusInDirection("down"), 'focus-down': (self) => self._focusInDirection('down'),
}; };
// --- INTERACTIONHANDLER --- // ── HELPERFUNCTION ────────────────────────────────────────
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 { class InteractionHandler {
constructor(tiler) { constructor(tiler) {
this.tiler = tiler; this.tiler = tiler;
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME); this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA }); this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
this._wmKeysToDisable = []; this._wmKeysToDisable = [];
this._savedWmShortcuts = {}; this._savedWmShortcuts= {};
this._grabOpIds = []; this._grabOpIds = [];
this._settingsChangedId = null; this._settingsChangedId = null;
this._onSettingsChanged = this._onSettingsChanged.bind(this); this._onSettingsChanged = this._onSettingsChanged.bind(this);
this._prepareWmShortcuts(); this._prepareWmShortcuts();
} }
enable() { enable() {
if (this._wmKeysToDisable.length) { if (this._wmKeysToDisable.length)
this._wmKeysToDisable.forEach((key) => this._wmKeysToDisable.forEach(k =>
this._wmSettings.set_value(key, new GLib.Variant("as", [])) this._wmSettings.set_value(k, new GLib.Variant('as', [])));
);
}
this._bindAllShortcuts(); this._bindAllShortcuts();
this._settingsChangedId = this._settings.connect( this._settingsChangedId =
"changed", this._settings.connect('changed', this._onSettingsChanged);
this._onSettingsChanged
);
this._grabOpIds.push( this._grabOpIds.push(
global.display.connect( global.display.connect('grab-op-begin',
"grab-op-begin", (display, screen, win) => {
(display, screen, window) => { if (this.tiler.windows.includes(win))
if (this.tiler.windows.includes(window)) { this.tiler.grabbedWindow = win;
this.tiler.grabbedWindow = window; }));
}
}
)
);
this._grabOpIds.push( this._grabOpIds.push(
global.display.connect("grab-op-end", this._onGrabEnd.bind(this)) global.display.connect('grab-op-end', this._onGrabEnd.bind(this)));
);
} }
disable() { disable() {
if (this._wmKeysToDisable.length) { if (this._wmKeysToDisable.length)
this._wmKeysToDisable.forEach((key) => this._wmKeysToDisable.forEach(k =>
this._wmSettings.set_value(key, this._savedWmShortcuts[key]) this._wmSettings.set_value(k, this._savedWmShortcuts[k]));
);
}
this._unbindAllShortcuts(); this._unbindAllShortcuts();
if (this._settingsChangedId) { if (this._settingsChangedId) {
this._settings.disconnect(this._settingsChangedId); this._settings.disconnect(this._settingsChangedId);
this._settingsChangedId = null; this._settingsChangedId = null;
} }
this._grabOpIds.forEach((id) => global.display.disconnect(id)); this._grabOpIds.forEach(id => global.display.disconnect(id));
this._grabOpIds = []; this._grabOpIds = [];
} }
_bind(key, callback) { _bind(key, callback) {
Main.wm.addKeybinding(key, this._settings, Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL, addKeybinding(key, this._settings,
Meta.KeyBindingFlags.NONE,
Shell.ActionMode.NORMAL,
() => callback(this)); () => callback(this));
} }
_bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k,h); }
_bindAllShortcuts() { _unbindAllShortcuts(){ for (const k in KEYBINDINGS) removeKeybinding(k); }
for (const [key, handler] of Object.entries(KEYBINDINGS)) {
this._bind(key, handler);
}
}
_unbindAllShortcuts() {
for (const key in KEYBINDINGS) {
Main.wm.removeKeybinding(key);
}
}
_onSettingsChanged() { _onSettingsChanged() {
this._unbindAllShortcuts(); this._unbindAllShortcuts();
@@ -114,242 +136,184 @@ class InteractionHandler {
_prepareWmShortcuts() { _prepareWmShortcuts() {
const schema = this._wmSettings.settings_schema; const schema = this._wmSettings.settings_schema;
const keys = []; const keys = [];
if (schema.has_key("toggle-tiled-left"))
keys.push("toggle-tiled-left", "toggle-tiled-right"); if (schema.has_key('toggle-tiled-left'))
else if (schema.has_key("tile-left")) keys.push('toggle-tiled-left','toggle-tiled-right');
keys.push("tile-left", "tile-right"); else if (schema.has_key('tile-left'))
if (schema.has_key("toggle-maximized")) keys.push("toggle-maximized"); keys.push('tile-left','tile-right');
if (schema.has_key('toggle-maximized'))
keys.push('toggle-maximized');
else { else {
if (schema.has_key("maximize")) keys.push("maximize"); if (schema.has_key('maximize')) keys.push('maximize');
if (schema.has_key("unmaximize")) keys.push("unmaximize"); if (schema.has_key('unmaximize')) keys.push('unmaximize');
} }
if (keys.length) { if (keys.length) {
this._wmKeysToDisable = keys; this._wmKeysToDisable = keys;
keys.forEach( keys.forEach(k => this._savedWmShortcuts[k] =
(key) => this._wmSettings.get_value(k));
(this._savedWmShortcuts[
key
] = this._wmSettings.get_value(key))
);
} }
} }
_focusInDirection(direction) { _focusInDirection(direction) {
const sourceWindow = global.display.get_focus_window(); const src = global.display.get_focus_window();
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return; if (!src || !this.tiler.windows.includes(src)) return;
const tgt = this._findTargetInDirection(src, direction);
const targetWindow = this._findTargetInDirection( if (tgt) tgt.activate(global.get_current_time());
sourceWindow,
direction
);
if (targetWindow) {
targetWindow.activate(global.get_current_time());
}
} }
_swapWithMaster() { _swapWithMaster() {
const windows = this.tiler.windows; const w = this.tiler.windows;
if (windows.length < 2) return; if (w.length < 2) return;
const focusedWindow = global.display.get_focus_window(); const foc = global.display.get_focus_window();
if (!focusedWindow || !windows.includes(focusedWindow)) return; if (!foc || !w.includes(foc)) return;
const focusedIndex = windows.indexOf(focusedWindow); const idx = w.indexOf(foc);
if (focusedIndex > 0) { if (idx > 0)
[windows[0], windows[focusedIndex]] = [ [w[0], w[idx]] = [w[idx], w[0]];
windows[focusedIndex], else
windows[0], [w[0], w[1]] = [w[1], w[0]];
];
} else if (focusedIndex === 0) {
[windows[0], windows[1]] = [windows[1], windows[0]];
}
this.tiler.tileNow(); this.tiler.tileNow();
if (windows.length > 0) windows[0].activate(global.get_current_time()); w[0]?.activate(global.get_current_time());
} }
_swapInDirection(direction) { _swapInDirection(direction) {
const sourceWindow = global.display.get_focus_window(); const src = global.display.get_focus_window();
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return; if (!src || !this.tiler.windows.includes(src)) return;
let targetWindow = null;
const sourceIndex = this.tiler.windows.indexOf(sourceWindow); let tgt = null;
if ( const srcIdx = this.tiler.windows.indexOf(src);
sourceIndex === 0 && if (srcIdx === 0 && direction === 'right' && this.tiler.windows.length>1)
direction === "right" && tgt = this.tiler.windows[1];
this.tiler.windows.length > 1 else
) { tgt = this._findTargetInDirection(src, direction);
targetWindow = this.tiler.windows[1];
} else { if (!tgt) return;
targetWindow = this._findTargetInDirection(sourceWindow, direction); const tgtIdx = this.tiler.windows.indexOf(tgt);
} [this.tiler.windows[srcIdx], this.tiler.windows[tgtIdx]] =
if (!targetWindow) return; [this.tiler.windows[tgtIdx], this.tiler.windows[srcIdx]];
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(); this.tiler.tileNow();
sourceWindow.activate(global.get_current_time()); src.activate(global.get_current_time());
} }
_findTargetInDirection(source, direction) { _findTargetInDirection(src, direction) {
const sourceRect = source.get_frame_rect(); const sRect = src.get_frame_rect();
let candidates = []; const cands = [];
for (const win of this.tiler.windows) { for (const win of this.tiler.windows) {
if (win === source) continue; if (win === src) continue;
const targetRect = win.get_frame_rect(); const tRect = win.get_frame_rect();
switch (direction) { switch (direction) {
case "left": case 'left': if (tRect.x < sRect.x) cands.push(win); break;
if (targetRect.x < sourceRect.x) candidates.push(win); case 'right': if (tRect.x > sRect.x) cands.push(win); break;
break; case 'up': if (tRect.y < sRect.y) cands.push(win); break;
case "right": case 'down': if (tRect.y > sRect.y) cands.push(win); break;
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; if (!cands.length) return null;
let bestTarget = null;
let minDeviation = Infinity; let best=null, min=Infinity;
for (const win of candidates) { for (const win of cands) {
const targetRect = win.get_frame_rect(); const tRect = win.get_frame_rect();
let deviation; const dev = (direction==='left'||direction==='right')
if (direction === "left" || direction === "right") { ? Math.abs(sRect.y - tRect.y)
deviation = Math.abs(sourceRect.y - targetRect.y); : Math.abs(sRect.x - tRect.x);
} else { if (dev < min) { min=dev; best=win; }
deviation = Math.abs(sourceRect.x - targetRect.x);
} }
if (deviation < minDeviation) { return best;
minDeviation = deviation;
bestTarget = win;
}
}
return bestTarget;
} }
_onGrabEnd() { _onGrabEnd() {
const grabbedWindow = this.tiler.grabbedWindow; const grabbed = this.tiler.grabbedWindow;
if (!grabbedWindow) return; if (!grabbed) return;
const targetWindow = this._findTargetUnderPointer(grabbedWindow);
if (targetWindow) { const tgt = this._findTargetUnderPointer(grabbed);
const sourceIndex = this.tiler.windows.indexOf(grabbedWindow); if (tgt) {
const targetIndex = this.tiler.windows.indexOf(targetWindow); const a = this.tiler.windows.indexOf(grabbed);
[ const b = this.tiler.windows.indexOf(tgt);
this.tiler.windows[sourceIndex], [this.tiler.windows[a], this.tiler.windows[b]] =
this.tiler.windows[targetIndex], [this.tiler.windows[b], this.tiler.windows[a]];
] = [
this.tiler.windows[targetIndex],
this.tiler.windows[sourceIndex],
];
} }
this.tiler.queueTile(); this.tiler.queueTile();
this.tiler.grabbedWindow = null; this.tiler.grabbedWindow = null;
} }
_findTargetUnderPointer(excludeWindow) { _findTargetUnderPointer(exclude) {
let [pointerX, pointerY] = global.get_pointer(); const [x,y] = getPointer();
let windows = global const wins = global.get_window_actors()
.get_window_actors() .map(a => a.meta_window)
.map((actor) => actor.meta_window) .filter(w => w && w!==exclude &&
.filter((win) => { this.tiler.windows.includes(w) &&
if ( ((()=>{ const f=w.get_frame_rect();
!win || return x>=f.x && x<f.x+f.width &&
win === excludeWindow || y>=f.y && y<f.y+f.height;})()));
!this.tiler.windows.includes(win) if (wins.length) return wins[wins.length-1];
)
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 best=null, max=0, sRect=exclude.get_frame_rect();
let maxOverlap = 0; for (const w of this.tiler.windows) {
const sourceFrame = excludeWindow.get_frame_rect(); if (w===exclude) continue;
for (const win of this.tiler.windows) { const tRect=w.get_frame_rect();
if (win === excludeWindow) continue; const ovX = Math.max(0, Math.min(sRect.x+sRect.width,
const targetFrame = win.get_frame_rect(); tRect.x+tRect.width) -
const overlapX = Math.max( Math.max(sRect.x, tRect.x));
0, const ovY = Math.max(0, Math.min(sRect.y+sRect.height,
Math.min( tRect.y+tRect.height) -
sourceFrame.x + sourceFrame.width, Math.max(sRect.y, tRect.y));
targetFrame.x + targetFrame.width const area = ovX*ovY;
) - Math.max(sourceFrame.x, targetFrame.x) if (area>max){ max=area; best=w; }
);
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 best;
return bestTarget;
} }
} }
// --- TILER --- // ── TILER ────────────────────────────────────────────────
class Tiler { class Tiler {
constructor() { constructor() {
this.windows = []; this.windows = [];
this.grabbedWindow = null; this.grabbedWindow = null;
this._settings = ExtensionUtils.getSettings(SCHEMA_NAME); this._settings = ExtensionUtils.getSettings(SCHEMA_NAME);
this._signalIds = new Map(); this._signalIds = new Map();
this._tileTimeoutId = null;
this._centerTimeoutIds= [];
this._tileInProgress = false; this._tileInProgress = false;
this._innerGap = this._settings.get_int("inner-gap"); this._innerGap = this._settings.get_int('inner-gap');
this._outerGapVertical = this._settings.get_int("outer-gap-vertical"); this._outerGapVertical= this._settings.get_int('outer-gap-vertical');
this._outerGapHorizontal = this._settings.get_int("outer-gap-horizontal"); this._outerGapHorizontal = this._settings.get_int('outer-gap-horizontal');
this._tilingDelay = TILING_DELAY_MS; this._tilingDelay = TILING_DELAY_MS;
this._centeringDelay = CENTERING_DELAY_MS; this._centeringDelay= CENTERING_DELAY_MS;
this._exceptions = []; this._exceptions = [];
this._interactionHandler = new InteractionHandler(this); this._interactionHandler = new InteractionHandler(this);
this._tileTimeoutId = null;
this._centerTimeoutIds = [];
this._onWindowAdded = this._onWindowAdded.bind(this); this._onWindowAdded = this._onWindowAdded.bind(this);
this._onWindowRemoved = this._onWindowRemoved.bind(this); this._onWindowRemoved= this._onWindowRemoved.bind(this);
this._onActiveWorkspaceChanged = this._onActiveWorkspaceChanged.bind( this._onActiveWorkspaceChanged =
this this._onActiveWorkspaceChanged.bind(this);
); this._onWindowMinimizedStateChanged =
this._onWindowMinimizedStateChanged = this._onWindowMinimizedStateChanged.bind( this._onWindowMinimizedStateChanged.bind(this);
this
);
this._onSettingsChanged = this._onSettingsChanged.bind(this); this._onSettingsChanged = this._onSettingsChanged.bind(this);
} }
enable() { enable() {
this._loadExceptions(); this._loadExceptions();
const workspaceManager = global.workspace_manager;
this._signalIds.set("workspace-changed", { const wm = global.workspace_manager;
object: workspaceManager, this._signalIds.set('workspace-changed', {
id: workspaceManager.connect( object: wm,
"active-workspace-changed", id: wm.connect('active-workspace-changed',
this._onActiveWorkspaceChanged this._onActiveWorkspaceChanged),
),
}); });
this._connectToWorkspace(); this._connectToWorkspace();
this._interactionHandler.enable(); this._interactionHandler.enable();
this._signalIds.set("settings-changed", {
this._signalIds.set('settings-changed', {
object: this._settings, object: this._settings,
id: this._settings.connect("changed", this._onSettingsChanged), id: this._settings.connect('changed', this._onSettingsChanged),
}); });
} }
@@ -363,147 +327,112 @@ class Tiler {
this._interactionHandler.disable(); this._interactionHandler.disable();
this._disconnectFromWorkspace(); this._disconnectFromWorkspace();
for (const [, signal] of this._signalIds) {
try { for (const [,sig] of this._signalIds) {
signal.object.disconnect(signal.id); try { sig.object.disconnect(sig.id); } catch {}
} catch (e) {}
} }
this._signalIds.clear(); this._signalIds.clear();
this.windows = []; this.windows = [];
} }
_onSettingsChanged() { _onSettingsChanged() {
this._innerGap = this._settings.get_int("inner-gap"); this._innerGap = this._settings.get_int('inner-gap');
this._outerGapVertical = this._settings.get_int("outer-gap-vertical"); this._outerGapVertical = this._settings.get_int('outer-gap-vertical');
this._outerGapHorizontal = this._settings.get_int("outer-gap-horizontal"); this._outerGapHorizontal= this._settings.get_int('outer-gap-horizontal');
this.queueTile(); this.queueTile();
} }
_loadExceptions() { _loadExceptions() {
const file = Gio.file_new_for_path(Me.path + "/exceptions.txt"); const file = Gio.File.new_for_path(Me.path + '/exceptions.txt');
if (!file.query_exists(null)) { if (!file.query_exists(null)) { this._exceptions=[]; return; }
this._exceptions = [];
return;
}
const [ok, data] = file.load_contents(null); const [ok, data] = file.load_contents(null);
this._exceptions = ok this._exceptions = ok ? decodeUtf8(data)
? ByteArray.toString(data) .split('\n')
.split("\n") .map(l => l.trim())
.map((l) => l.trim()) .filter(l => l && !l.startsWith('#'))
.filter((l) => l && !l.startsWith("#")) .map(l => l.toLowerCase())
.map((l) => l.toLowerCase())
: []; : [];
} }
_isException(win) { _isException(win) {
if (!win) return false; if (!win) return false;
const wmClass = (win.get_wm_class() || "").toLowerCase(); const wmClass = (win.get_wm_class() || '').toLowerCase();
const appId = (win.get_gtk_application_id() || "").toLowerCase(); const appId = (win.get_gtk_application_id() || '').toLowerCase();
return this._exceptions.includes(wmClass) || this._exceptions.includes(appId); return this._exceptions.includes(wmClass) || this._exceptions.includes(appId);
} }
_isTileable(win) { _isTileable(win) {
return ( return win && !win.minimized && !this._isException(win) &&
win && win.get_window_type() === Meta.WindowType.NORMAL;
!win.minimized &&
!this._isException(win) &&
win.get_window_type() === Meta.WindowType.NORMAL
);
} }
_centerWindow(win) { _centerWindow(win) {
const timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._centeringDelay, () => { const id = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
const index = this._centerTimeoutIds.indexOf(timeoutId); this._centeringDelay, () => {
if (index > -1) { const idx = this._centerTimeoutIds.indexOf(id);
this._centerTimeoutIds.splice(index, 1); if (idx>-1) this._centerTimeoutIds.splice(idx,1);
}
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE; if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
if (win.get_maximized()) { if (win.get_maximized())
win.unmaximize(Meta.MaximizeFlags.BOTH); win.unmaximize(Meta.MaximizeFlags.BOTH);
}
const monitorIndex = win.get_monitor(); const monitorIndex = win.get_monitor();
const workArea = Main.layoutManager.getWorkAreaForMonitor( const workArea = getWorkAreaForMonitor(monitorIndex);
monitorIndex
);
const frame = win.get_frame_rect(); const frame = win.get_frame_rect();
win.move_frame( win.move_frame(true,
true, workArea.x + Math.floor((workArea.width - frame.width )/2),
workArea.x + Math.floor((workArea.width - frame.width) / 2), workArea.y + Math.floor((workArea.height - frame.height)/2));
workArea.y + Math.floor((workArea.height - frame.height) / 2)
);
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
if (win.get_display()) { if (win.get_display()) {
if (typeof win.set_keep_above === "function") if (typeof win.set_keep_above === 'function')
win.set_keep_above(true); win.set_keep_above(true);
else if (typeof win.make_above === "function") else if (typeof win.make_above === 'function')
win.make_above(); win.make_above();
} }
return GLib.SOURCE_REMOVE; return GLib.SOURCE_REMOVE;
}); });
return GLib.SOURCE_REMOVE; return GLib.SOURCE_REMOVE;
}); });
this._centerTimeoutIds.push(id);
this._centerTimeoutIds.push(timeoutId);
} }
_onWindowMinimizedStateChanged() { _onWindowMinimizedStateChanged(){ this.queueTile(); }
this.queueTile();
}
_onWindowAdded(workspace, win) { _onWindowAdded(workspace, win) {
if (this.windows.includes(win)) return; if (this.windows.includes(win)) return;
if (this._isException(win)) { if (this._isException(win)) { this._centerWindow(win); return; }
this._centerWindow(win);
return;
}
if (this._isTileable(win)) { if (this._isTileable(win)) {
if (this._settings.get_string("new-window-behavior") === "master") { if (this._settings.get_string('new-window-behavior') === 'master')
this.windows.unshift(win); this.windows.unshift(win);
} else { else
this.windows.push(win); this.windows.push(win);
}
const id = win.get_id(); const id = win.get_id();
this._signalIds.set(`unmanaged-${id}`, { this._signalIds.set(`unmanaged-${id}`, {
object: win, object: win, id: win.connect('unmanaged',
id: win.connect("unmanaged", () => ()=>this._onWindowRemoved(null, win))});
this._onWindowRemoved(null, win) this._signalIds.set(`size-${id}`, {
), object: win, id: win.connect('size-changed',
}); ()=>{ if (!this.grabbedWindow) this.queueTile(); })});
this._signalIds.set(`size-changed-${id}`, { this._signalIds.set(`min-${id}`, {
object: win, object: win, id: win.connect('notify::minimized',
id: win.connect("size-changed", () => { this._onWindowMinimizedStateChanged)});
if (!this.grabbedWindow) this.queueTile();
}),
});
this._signalIds.set(`minimized-${id}`, {
object: win,
id: win.connect(
"notify::minimized",
this._onWindowMinimizedStateChanged
),
});
this.queueTile(); this.queueTile();
} }
} }
_onWindowRemoved(workspace, win) { _onWindowRemoved(workspace, win) {
const index = this.windows.indexOf(win); const idx = this.windows.indexOf(win);
if (index > -1) { if (idx>-1) this.windows.splice(idx,1);
this.windows.splice(index, 1);
}
["unmanaged", "size-changed", "minimized"].forEach((prefix) => { ['unmanaged','size','min'].forEach(pref=>{
const key = `${prefix}-${win.get_id()}`; const key = `${pref}-${win.get_id()}`;
if (this._signalIds.has(key)) { if (this._signalIds.has(key)) {
const { object, id } = this._signalIds.get(key); const {object,id} = this._signalIds.get(key);
try { try{ object.disconnect(id);}catch{}
object.disconnect(id);
} catch (e) {}
this._signalIds.delete(key); this._signalIds.delete(key);
} }
}); });
@@ -516,31 +445,21 @@ class Tiler {
} }
_connectToWorkspace() { _connectToWorkspace() {
const workspace = global.workspace_manager.get_active_workspace(); const ws = global.workspace_manager.get_active_workspace();
workspace ws.list_windows().forEach(w=>this._onWindowAdded(ws,w));
.list_windows() this._signalIds.set('win-add', {
.forEach((win) => this._onWindowAdded(workspace, win)); object: ws, id: ws.connect('window-added', this._onWindowAdded)});
this._signalIds.set("window-added", { this._signalIds.set('win-rem', {
object: workspace, object: ws, id: ws.connect('window-removed', this._onWindowRemoved)});
id: workspace.connect("window-added", this._onWindowAdded),
});
this._signalIds.set("window-removed", {
object: workspace,
id: workspace.connect("window-removed", this._onWindowRemoved),
});
this.queueTile(); this.queueTile();
} }
_disconnectFromWorkspace() { _disconnectFromWorkspace() {
this.windows.slice().forEach((win) => this._onWindowRemoved(null, win)); this.windows.slice().forEach(w=>this._onWindowRemoved(null,w));
['win-add','win-rem'].forEach(k=>{
["window-added", "window-removed"].forEach((key) => { if (this._signalIds.has(k)) {
if (this._signalIds.has(key)) { const {object,id}=this._signalIds.get(k);
const { object, id } = this._signalIds.get(key); try{ object.disconnect(id);}catch{}
try { this._signalIds.delete(k);
object.disconnect(id);
} catch (e) {}
this._signalIds.delete(key);
} }
}); });
} }
@@ -548,135 +467,88 @@ class Tiler {
queueTile() { queueTile() {
if (this._tileInProgress || this._tileTimeoutId) return; if (this._tileInProgress || this._tileTimeoutId) return;
this._tileInProgress = true; this._tileInProgress = true;
this._tileTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
this._tileTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._tilingDelay, () => { this._tilingDelay, () => {
this._tileWindows(); this._tileWindows();
this._tileInProgress = false; this._tileInProgress = false;
this._tileTimeoutId = null; this._tileTimeoutId = null;
return GLib.SOURCE_REMOVE; return GLib.SOURCE_REMOVE;
}); });
} }
tileNow() { if (!this._tileInProgress) this._tileWindows(); }
tileNow() {
if (!this._tileInProgress) {
this._tileWindows();
}
}
_splitLayout(windows, area) { _splitLayout(windows, area) {
if (windows.length === 0) return; if (!windows.length) return;
if (windows.length === 1) { if (windows.length === 1) {
windows[0].move_resize_frame( windows[0].move_resize_frame(true,
true, area.x, area.y, area.width, area.height);
area.x,
area.y,
area.width,
area.height
);
return; return;
} }
const gap = Math.floor(this._innerGap / 2); const gap = Math.floor(this._innerGap/2);
const primaryWindows = [windows[0]]; const prim = [windows[0]];
const secondaryWindows = windows.slice(1); const sec = windows.slice(1);
let primaryArea, secondaryArea;
let primArea, secArea;
if (area.width > area.height) { if (area.width > area.height) {
const primaryWidth = Math.floor(area.width / 2) - gap; const pW = Math.floor(area.width/2) - gap;
primaryArea = { primArea = {x: area.x, y: area.y,
x: area.x, width: pW, height: area.height};
y: area.y, secArea = {x: area.x+pW+this._innerGap, y: area.y,
width: primaryWidth, width: area.width-pW-this._innerGap,
height: area.height, height: area.height};
};
secondaryArea = {
x: area.x + primaryWidth + this._innerGap,
y: area.y,
width: area.width - primaryWidth - this._innerGap,
height: area.height,
};
} else { } else {
const primaryHeight = Math.floor(area.height / 2) - gap; const pH = Math.floor(area.height/2) - gap;
primaryArea = { primArea = {x: area.x, y: area.y,
x: area.x, width: area.width, height: pH};
y: area.y, secArea = {x: area.x, y: area.y+pH+this._innerGap,
width: area.width, width: area.width,
height: primaryHeight, height: area.height-pH-this._innerGap};
};
secondaryArea = {
x: area.x,
y: area.y + primaryHeight + this._innerGap,
width: area.width,
height: area.height - primaryHeight - this._innerGap,
};
} }
this._splitLayout(prim, primArea);
this._splitLayout(primaryWindows, primaryArea); this._splitLayout(sec, secArea);
this._splitLayout(secondaryWindows, secondaryArea);
} }
_tileWindows() { _tileWindows() {
const windowsToTile = this.windows.filter((win) => !win.minimized); const wins = this.windows.filter(w=>!w.minimized);
if (windowsToTile.length === 0) return; if (!wins.length) return;
const monitor = Main.layoutManager.primaryMonitor; const monitor = Main.layoutManager.primaryMonitor;
const workArea = Main.layoutManager.getWorkAreaForMonitor( const work = getWorkAreaForMonitor(monitor.index);
monitor.index const inner = { x: work.x + this._outerGapHorizontal,
); y: work.y + this._outerGapVertical,
const innerArea = { width: work.width - 2*this._outerGapHorizontal,
x: workArea.x + this._outerGapHorizontal, height: work.height - 2*this._outerGapVertical };
y: workArea.y + this._outerGapVertical,
width: workArea.width - 2 * this._outerGapHorizontal,
height: workArea.height - 2 * this._outerGapVertical,
};
windowsToTile.forEach((win) => { wins.forEach(w=>{ if (w.get_maximized())
if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH); w.unmaximize(Meta.MaximizeFlags.BOTH); });
});
if (windowsToTile.length === 1) { if (wins.length===1) {
windowsToTile[0].move_resize_frame( wins[0].move_resize_frame(true,
true, inner.x, inner.y, inner.width, inner.height);
innerArea.x,
innerArea.y,
innerArea.width,
innerArea.height
);
return; return;
} }
const gap = Math.floor(this._innerGap / 2); const gap = Math.floor(this._innerGap/2);
const masterWidth = Math.floor(innerArea.width / 2) - gap; const masterW = Math.floor(inner.width/2) - gap;
const master = windowsToTile[0]; const master = wins[0];
master.move_resize_frame( master.move_resize_frame(true,
true, inner.x, inner.y, masterW, inner.height);
innerArea.x,
innerArea.y, const stack = { x: inner.x + masterW + this._innerGap,
masterWidth, y: inner.y,
innerArea.height width: inner.width - masterW - this._innerGap,
); height: inner.height };
const stackArea = { this._splitLayout(wins.slice(1), stack);
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 --- // ── EXTENSIONWRAPPER ───────────────────────────────────
var LegacyExtension = class { class SimpleTilingExtension {
constructor() { enable() { this.tiler = new Tiler(); this.tiler.enable(); }
this.tiler = null; disable() { this.tiler?.disable(); this.tiler = null; }
} }
enable() {
this.tiler = new Tiler(); function init() {
this.tiler.enable(); return new SimpleTilingExtension();
} }
disable() {
if (this.tiler) {
this.tiler.disable();
this.tiler = null;
}
}
};
+18
View File
@@ -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__"
}
+3 -4
View File
@@ -1,10 +1,9 @@
{ {
"uuid": "simple-tiling@domoel", "uuid": "__UUID__",
"name": "Simple Tiling", "name": "Simple Tiling",
"description": "A Simple Tiling Extension for Gnome Shell.", "description": "A Simple Tiling Extension for Gnome Shell.",
"version": 6, "version": __VERSION__,
"shell-version": [ "shell-version": [
"3.38",
"45", "45",
"46", "46",
"47", "47",
@@ -13,5 +12,5 @@
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel", "settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
"preferences_ui": "prefs.js", "preferences_ui": "prefs.js",
"url": "https://github.com/Domoel/Simple-Tiling", "url": "https://github.com/Domoel/Simple-Tiling",
"gettext-domain": "simple-tiling-domoel" "gettext-domain": "__UUID__"
} }
+182 -277
View File
@@ -1,41 +1,63 @@
///////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////
// Simple-Tiling MODERN (for GNOME Shell 40+) // // SimpleTiling  MODERN (GNOME Shell 45+) //
// © 2025 domoel MIT // // ©2025domoel  MIT //
////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////
// --- GLOBAL IMPORTS --- // ── GLOBAL IMPORTS ────────────────────────────────────────
import { Extension } from "resource:///org/gnome/shell/extensions/extension.js"; import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from "resource:///org/gnome/shell/ui/main.js"; import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import Meta from "gi://Meta"; import Meta from 'gi://Meta';
import Shell from "gi://Shell"; import Shell from 'gi://Shell';
import Gio from "gi://Gio"; import Gio from 'gi://Gio';
import GLib from "gi://GLib"; import GLib from 'gi://GLib';
import Clutter from 'gi://Clutter';
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel"; // ── CONST ────────────────────────────────────────────
const WM_SCHEMA = "org.gnome.desktop.wm.keybindings"; const WM_SCHEMA = 'org.gnome.desktop.wm.keybindings';
const TILING_DELAY_MS = 20; // Change Tiling Window Delay const TILING_DELAY_MS = 20; // Change Tiling Window Delay
const CENTERING_DELAY_MS = 5; // Change Centered Window Delay const CENTERING_DELAY_MS = 5; // Change Centered Window Delay
const KEYBINDINGS = { const KEYBINDINGS = {
"swap-master-window": (self) => self._swapWithMaster(), 'swap-master-window': (self) => self._swapWithMaster(),
"swap-left-window": (self) => self._swapInDirection("left"), 'swap-left-window': (self) => self._swapInDirection('left'),
"swap-right-window": (self) => self._swapInDirection("right"), 'swap-right-window': (self) => self._swapInDirection('right'),
"swap-up-window": (self) => self._swapInDirection("up"), 'swap-up-window': (self) => self._swapInDirection('up'),
"swap-down-window": (self) => self._swapInDirection("down"), 'swap-down-window': (self) => self._swapInDirection('down'),
"focus-left": (self) => self._focusInDirection("left"), 'focus-left': (self) => self._focusInDirection('left'),
"focus-right": (self) => self._focusInDirection("right"), 'focus-right': (self) => self._focusInDirection('right'),
"focus-up": (self) => self._focusInDirection("up"), 'focus-up': (self) => self._focusInDirection('up'),
"focus-down": (self) => self._focusInDirection("down"), 'focus-down': (self) => self._focusInDirection('down'),
}; };
// --- INTERACTIONHANDLER --- // ── HELPERFUNCTION ────────────────────────────────────────
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 { class InteractionHandler {
constructor(tiler) { constructor(tiler) {
this.tiler = tiler; this.tiler = tiler;
this._settings = this.tiler.settings; this._settings = this.tiler.settings;
this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA }); this._wmSettings = new Gio.Settings({ schema: WM_SCHEMA });
this._wmKeysToDisable = []; this._wmKeysToDisable = [];
this._savedWmShortcuts = {}; this._savedWmShortcuts = {};
this._grabOpIds = []; this._grabOpIds = [];
@@ -44,66 +66,52 @@ class InteractionHandler {
enable() { enable() {
this._prepareWmShortcuts(); this._prepareWmShortcuts();
if (this._wmKeysToDisable.length) {
this._wmKeysToDisable.forEach((key) => if (this._wmKeysToDisable.length)
this._wmSettings.set_value(key, new GLib.Variant("as", [])) this._wmKeysToDisable.forEach(k =>
); this._wmSettings.set_value(k, new GLib.Variant('as', [])));
}
this._bindAllShortcuts(); this._bindAllShortcuts();
this._settingsChangedId = this._settings.connect("changed", () => this._settingsChangedId =
this._onSettingsChanged() 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( this._grabOpIds.push(
global.display.connect( global.display.connect('grab-op-end', () => this._onGrabEnd())
"grab-op-begin",
(display, screen, window) => {
if (this.tiler.windows.includes(window)) {
this.tiler.grabbedWindow = window;
}
}
)
);
this._grabOpIds.push(
global.display.connect("grab-op-end", () => this._onGrabEnd())
); );
} }
disable() { disable() {
if (this._wmKeysToDisable.length) { if (this._wmKeysToDisable.length)
this._wmKeysToDisable.forEach((key) => this._wmKeysToDisable.forEach(k =>
this._wmSettings.set_value(key, this._savedWmShortcuts[key]) this._wmSettings.set_value(k, this._savedWmShortcuts[k]));
);
}
this._unbindAllShortcuts(); this._unbindAllShortcuts();
if (this._settingsChangedId) { if (this._settingsChangedId) {
this._settings.disconnect(this._settingsChangedId); this._settings.disconnect(this._settingsChangedId);
this._settingsChangedId = null; this._settingsChangedId = null;
} }
this._grabOpIds.forEach((id) => global.display.disconnect(id)); this._grabOpIds.forEach(id => global.display.disconnect(id));
this._grabOpIds = []; this._grabOpIds = [];
} }
_bind(key, callback) { _bind(key, handler) {
global.display.add_keybinding( global.display.add_keybinding(
key, key,
this._settings, this._settings,
Meta.KeyBindingFlags.NONE, Meta.KeyBindingFlags.NONE,
Shell.ActionMode.NORMAL, Shell.ActionMode.NORMAL,
callback (..._args) => handler(this)
); );
} }
_bindAllShortcuts() { _bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k, h); }
for (const [key, handler] of Object.entries(KEYBINDINGS)) { _unbindAllShortcuts(){ for (const k in KEYBINDINGS) global.display.remove_keybinding(k); }
this._bind(key, () => handler(this));
}
}
_unbindAllShortcuts() {
for (const key in KEYBINDINGS) {
global.display.remove_keybinding(key);
}
}
_onSettingsChanged() { _onSettingsChanged() {
this._unbindAllShortcuts(); this._unbindAllShortcuts();
@@ -111,210 +119,128 @@ class InteractionHandler {
} }
_prepareWmShortcuts() { _prepareWmShortcuts() {
const schema = this._wmSettings const schema = this._wmSettings.settings_schema;
.get_schema_source()
.lookup(WM_SCHEMA, true);
if (!schema) return; if (!schema) return;
const addKeyIfExists = (keys, key) => {
if (schema.has_key(key)) keys.push(key);
};
const keys = []; const keys = [];
if (schema.has_key("toggle-tiled-left")) {
keys.push("toggle-tiled-left", "toggle-tiled-right"); const add = key => { if (schema.has_key(key)) keys.push(key); };
} else {
addKeyIfExists(keys, "tile-left"); if (schema.has_key('toggle-tiled-left'))
addKeyIfExists(keys, "tile-right"); keys.push('toggle-tiled-left', 'toggle-tiled-right');
else {
add('tile-left'); add('tile-right');
} }
if (schema.has_key("toggle-maximized")) { if (schema.has_key('toggle-maximized'))
keys.push("toggle-maximized"); keys.push('toggle-maximized');
} else { else {
addKeyIfExists(keys, "maximize"); add('maximize'); add('unmaximize');
addKeyIfExists(keys, "unmaximize");
} }
if (keys.length) { if (keys.length) {
this._wmKeysToDisable = keys; this._wmKeysToDisable = keys;
keys.forEach( keys.forEach(k => this._savedWmShortcuts[k] =
(key) => this._wmSettings.get_value(k));
(this._savedWmShortcuts[key] = this._wmSettings.get_value(
key
))
);
} }
} }
_focusInDirection(direction) { _focusInDirection(direction) {
const sourceWindow = global.display.get_focus_window(); const src = global.display.get_focus_window();
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return; if (!src || !this.tiler.windows.includes(src)) return;
const tgt = this._findTargetInDirection(src, direction);
const targetWindow = this._findTargetInDirection( if (tgt) tgt.activate(global.get_current_time());
sourceWindow,
direction
);
if (targetWindow) {
targetWindow.activate(global.get_current_time());
}
} }
_swapWithMaster() { _swapWithMaster() {
const windows = this.tiler.windows; const w = this.tiler.windows;
if (windows.length < 2) return; if (w.length < 2) return;
const focusedWindow = global.display.get_focus_window(); const foc = global.display.get_focus_window();
if (!focusedWindow || !windows.includes(focusedWindow)) return; if (!foc || !w.includes(foc)) return;
const focusedIndex = windows.indexOf(focusedWindow); const idx = w.indexOf(foc);
if (focusedIndex > 0) { if (idx > 0) [w[0], w[idx]] = [w[idx], w[0]];
[windows[0], windows[focusedIndex]] = [ else [w[0], w[1]] = [w[1], w[0]];
windows[focusedIndex],
windows[0],
];
} else if (focusedIndex === 0) {
[windows[0], windows[1]] = [windows[1], windows[0]];
}
this.tiler.tileNow(); this.tiler.tileNow();
if (windows.length > 0) windows[0].activate(global.get_current_time()); w[0]?.activate(global.get_current_time());
} }
_swapInDirection(direction) { _swapInDirection(direction) {
const sourceWindow = global.display.get_focus_window(); const src = global.display.get_focus_window();
if (!sourceWindow || !this.tiler.windows.includes(sourceWindow)) return; if (!src || !this.tiler.windows.includes(src)) return;
let targetWindow = null; let tgt = null;
const sourceIndex = this.tiler.windows.indexOf(sourceWindow); const idx = this.tiler.windows.indexOf(src);
if ( if (idx === 0 && direction==='right' && this.tiler.windows.length>1)
sourceIndex === 0 && tgt = this.tiler.windows[1];
direction === "right" && else
this.tiler.windows.length > 1 tgt = this._findTargetInDirection(src, direction);
) { if (!tgt) return;
targetWindow = this.tiler.windows[1]; const tidx = this.tiler.windows.indexOf(tgt);
} else { [this.tiler.windows[idx], this.tiler.windows[tidx]] =
targetWindow = this._findTargetInDirection(sourceWindow, direction); [this.tiler.windows[tidx], this.tiler.windows[idx]];
}
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(); this.tiler.tileNow();
sourceWindow.activate(global.get_current_time()); src.activate(global.get_current_time());
} }
_findTargetInDirection(source, direction) { _findTargetInDirection(src, dir) {
const sourceRect = source.get_frame_rect(); const sRect = src.get_frame_rect(), cand=[];
let candidates = [];
for (const win of this.tiler.windows) { for (const win of this.tiler.windows) {
if (win === source) continue; if (win===src) continue;
const targetRect = win.get_frame_rect(); const r=win.get_frame_rect();
switch (direction) { if (dir==='left' && r.x<sRect.x) cand.push(win);
case "left": if (dir==='right'&& r.x>sRect.x) cand.push(win);
if (targetRect.x < sourceRect.x) candidates.push(win); if (dir==='up' && r.y<sRect.y) cand.push(win);
break; if (dir==='down' && r.y>sRect.y) cand.push(win);
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 (!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;}
} }
if (candidates.length === 0) return null; return best;
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() { _onGrabEnd() {
const grabbedWindow = this.tiler.grabbedWindow; const grabbed = this.tiler.grabbedWindow;
if (!grabbedWindow) return; if (!grabbed) return;
const targetWindow = this._findTargetUnderPointer(grabbedWindow); const tgt = this._findTargetUnderPointer(grabbed);
if (targetWindow) { if (tgt) {
const sourceIndex = this.tiler.windows.indexOf(grabbedWindow); const a = this.tiler.windows.indexOf(grabbed);
const targetIndex = this.tiler.windows.indexOf(targetWindow); const b = this.tiler.windows.indexOf(tgt);
[ [this.tiler.windows[a], this.tiler.windows[b]] =
this.tiler.windows[sourceIndex], [this.tiler.windows[b], this.tiler.windows[a]];
this.tiler.windows[targetIndex],
] = [
this.tiler.windows[targetIndex],
this.tiler.windows[sourceIndex],
];
} }
this.tiler.queueTile(); this.tiler.queueTile();
this.tiler.grabbedWindow = null; this.tiler.grabbedWindow = null;
} }
_findTargetUnderPointer(excludeWindow) { _findTargetUnderPointer(exclude) {
let [pointerX, pointerY] = global.get_pointer(); const [x,y] = getPointerXY();
let windows = global const wins = global.get_window_actors()
.get_window_actors() .map(a=>a.meta_window)
.map((actor) => actor.meta_window) .filter(w=>w && w!==exclude &&
.filter((win) => { this.tiler.windows.includes(w) && (()=>{const f=w.get_frame_rect();
if ( return x>=f.x && x<f.x+f.width &&
!win || y>=f.y && y<f.y+f.height;})());
win === excludeWindow || if (wins.length) return wins[wins.length-1];
!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 best=null, max=0, sRect=exclude.get_frame_rect();
let maxOverlap = 0; for (const w of this.tiler.windows) {
const sourceFrame = excludeWindow.get_frame_rect(); if (w===exclude) continue;
for (const win of this.tiler.windows) { const r=w.get_frame_rect();
if (win === excludeWindow) continue; const ovX=Math.max(0, Math.min(sRect.x+sRect.width, r.x+r.width)-Math.max(sRect.x,r.x));
const targetFrame = win.get_frame_rect(); const ovY=Math.max(0, Math.min(sRect.y+sRect.height,r.y+r.height)-Math.max(sRect.y,r.y));
const overlapX = Math.max( const area=ovX*ovY;
0, if (area>max){max=area; best=w;}
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 best;
return bestTarget;
} }
} }
// --- TILER --- // ── TILER ────────────────────────────────────────────────
class Tiler { class Tiler {
constructor(extension) { constructor(extension) {
this._extension = extension; this._extension = extension;
@@ -325,11 +251,9 @@ class Tiler {
this._signalIds = new Map(); this._signalIds = new Map();
this._tileInProgress = false; this._tileInProgress = false;
this._innerGap = this.settings.get_int("inner-gap"); this._innerGap = this.settings.get_int('inner-gap');
this._outerGapVertical = this.settings.get_int("outer-gap-vertical"); this._outerGapVertical= this.settings.get_int('outer-gap-vertical');
this._outerGapHorizontal = this.settings.get_int( this._outerGapHorizontal = this.settings.get_int('outer-gap-horizontal');
"outer-gap-horizontal"
);
this._tilingDelay = TILING_DELAY_MS; this._tilingDelay = TILING_DELAY_MS;
this._centeringDelay = CENTERING_DELAY_MS; this._centeringDelay = CENTERING_DELAY_MS;
@@ -338,26 +262,25 @@ class Tiler {
this._interactionHandler = new InteractionHandler(this); this._interactionHandler = new InteractionHandler(this);
this._tileTimeoutId = null; this._tileTimeoutId = null;
this._centerTimeoutIds = []; this._centerTimeoutIds= [];
} }
enable() { enable() {
this._loadExceptions(); this._loadExceptions();
this._workspaceManager = global.workspace_manager; this._workspaceManager = global.workspace_manager;
this._signalIds.set("workspace-changed", { this._signalIds.set('workspace-changed', {
object: this._workspaceManager, object: this._workspaceManager,
id: this._workspaceManager.connect("active-workspace-changed", () => id: this._workspaceManager.connect('active-workspace-changed',
this._onActiveWorkspaceChanged() ()=>this._onActiveWorkspaceChanged())
),
}); });
this._connectToWorkspace(); this._connectToWorkspace();
this._interactionHandler.enable(); this._interactionHandler.enable();
this._signalIds.set("settings-changed", {
this._signalIds.set('settings-changed', {
object: this.settings, object: this.settings,
id: this.settings.connect("changed", () => id: this.settings.connect('changed', ()=>this._onSettingsChanged())
this._onSettingsChanged()
),
}); });
} }
@@ -366,47 +289,38 @@ class Tiler {
GLib.source_remove(this._tileTimeoutId); GLib.source_remove(this._tileTimeoutId);
this._tileTimeoutId = null; this._tileTimeoutId = null;
} }
this._centerTimeoutIds.forEach((id) => GLib.source_remove(id)); this._centerTimeoutIds.forEach(id=>GLib.source_remove(id));
this._centerTimeoutIds = []; this._centerTimeoutIds = [];
this._interactionHandler.disable(); this._interactionHandler.disable();
this._disconnectFromWorkspace(); this._disconnectFromWorkspace();
for (const [, signal] of this._signalIds) {
try { for (const [,sig] of this._signalIds) {
signal.object.disconnect(signal.id); try { sig.object.disconnect(sig.id); } catch {}
} catch (e) {}
} }
this._signalIds.clear(); this._signalIds.clear();
this.windows = []; this.windows = [];
} }
_onSettingsChanged() { _onSettingsChanged() {
this._innerGap = this.settings.get_int("inner-gap"); this._innerGap = this.settings.get_int('inner-gap');
this._outerGapVertical = this.settings.get_int("outer-gap-vertical"); this._outerGapVertical = this.settings.get_int('outer-gap-vertical');
this._outerGapHorizontal = this.settings.get_int( this._outerGapHorizontal= this.settings.get_int('outer-gap-horizontal');
"outer-gap-horizontal"
);
this.queueTile(); this.queueTile();
} }
_loadExceptions() { _loadExceptions() {
const file = Gio.File.new_for_path( const file = Gio.File.new_for_path(this._extension.path + '/exceptions.txt');
this._extension.path + "/exceptions.txt" if (!file.query_exists(null)) { this._exceptions=[]; return; }
);
if (!file.query_exists(null)) { const [ok,data] = file.load_contents(null);
this._exceptions = []; if (!ok) { this._exceptions=[]; return; }
return;
} const txt = new TextDecoder('utf-8').decode(data);
const [ok, data] = file.load_contents(null); this._exceptions = txt.split('\n')
if (ok) { .map(l=>l.trim())
this._exceptions = GLib.locale_from_utf8(data) .filter(l=>l && !l.startsWith('#'))
.split("\n") .map(l=>l.toLowerCase());
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#"))
.map((l) => l.toLowerCase());
} else {
this._exceptions = [];
}
} }
_isException(win) { _isException(win) {
@@ -677,17 +591,8 @@ class Tiler {
} }
} }
// --- MODERN EXTENSION WRAPPER --- // ── EXTENSIONWRAPPER ───────────────────────────────────
export default class ModernExtension extends Extension { export default class ModernExtension extends Extension {
enable() { enable() { this.tiler = new Tiler(this); this.tiler.enable(); }
this.tiler = new Tiler(this); disable() { this.tiler?.disable(); this.tiler = null; }
this.tiler.enable();
}
disable() {
if (this.tiler) {
this.tiler.disable();
this.tiler = null;
}
}
} }
-221
View File
@@ -1,221 +0,0 @@
///////////////////////////////////////////////////////
// --- Extension Settings Menu for Simple Tiling --- //
// --- © 2025 domoel MIT --- //
///////////////////////////////////////////////////////
// --- GLOBAL IMPORTS ---
'use strict';
const { Gtk, GObject, Gio, GLib } = imports.gi;
const ExtensionUtils = imports.misc.extensionUtils;
// --- Global Version Checkup ---
let Adw;
try {
Adw = imports.gi.Adw;
} catch (e) {
Adw = null;
}
// --- DEFINITIONS ---
// --- Definitions for GNOME Shell 40+ ---
const ModernPrefs = Adw ? class extends ExtensionPreferences {
fillPreferencesWindow(window) {
const settings = this.getSettings();
const page = new Adw.PreferencesPage();
window.add(page);
// --- Window Gaps ---
const groupGaps = new Adw.PreferencesGroup({ title: 'Window Gaps' });
page.add(groupGaps);
const rowInnerGap = new Adw.SpinRow({
title: 'Inner Gap',
subtitle: 'The gap between windows in pixels.',
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
});
groupGaps.add(rowInnerGap);
settings.bind('inner-gap', rowInnerGap, 'value', Gio.SettingsBindFlags.DEFAULT);
const rowOuterHGap = new Adw.SpinRow({
title: 'Outer Gap (horizontal)',
subtitle: 'The gap to the left and right screen edges.',
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
});
groupGaps.add(rowOuterHGap);
settings.bind('outer-gap-horizontal', rowOuterHGap, 'value', Gio.SettingsBindFlags.DEFAULT);
const rowOuterVGap = new Adw.SpinRow({
title: 'Outer Gap (vertical)',
subtitle: 'The gap to the top and bottom screen edges.',
adjustment: new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 }),
});
groupGaps.add(rowOuterVGap);
settings.bind('outer-gap-vertical', rowOuterVGap, 'value', Gio.SettingsBindFlags.DEFAULT);
// --- Window Behavior ---
const groupBehavior = new Adw.PreferencesGroup({ title: 'Window Behavior' });
page.add(groupBehavior);
const rowNewWindow = new Adw.ComboRow({
title: 'Open new windows as',
subtitle: 'Determines if a new window is added as master or stack window.',
model: new Gtk.StringList({ strings: ['Stack Window (Default)', 'Master Window'] }),
});
groupBehavior.add(rowNewWindow);
const mapping = new Gio.SettingsBindMapping({
settings: settings, key: 'new-window-behavior', property: 'selected',
get_mapping: (value, variant_type) => value === 'master' ? 1 : 0,
set_mapping: (value, param_type) => new GLib.Variant('s', value === 1 ? 'master' : 'stack'),
});
settings.bind_with_mapping('new-window-behavior', rowNewWindow, 'selected', Gio.SettingsBindFlags.DEFAULT, mapping);
// --- Keybindings ---
const groupKeys = new Adw.PreferencesGroup({ title: 'Keybindings' });
page.add(groupKeys);
const rowKeys = new Adw.ActionRow({
title: 'Configure Shortcuts',
subtitle: 'All shortcuts can be configured in GNOME\'s main Keyboard settings.',
});
groupKeys.add(rowKeys);
const button = new Gtk.Button({ label: 'Open Keyboard Settings', valign: Gtk.Align.CENTER });
button.connect('clicked', () => {
const appInfo = Gio.AppInfo.create_from_commandline(
'gnome-control-center keyboard', null, Gio.AppInfoCreateFlags.NONE
);
appInfo.launch([], null);
});
rowKeys.add_suffix(button);
rowKeys.set_activatable_widget(button);
}
} : null;
// --- Definitions for GNOME Shell 3.38 ---
const buildLegacyPrefsWidget = () => {
const settings = ExtensionUtils.getSettings("org.gnome.shell.extensions.simple-tiling.domoel");
const prefsWidget = new Gtk.Box({
orientation: Gtk.Orientation.VERTICAL,
margin_top: 20, margin_bottom: 20, margin_start: 20, margin_end: 20,
spacing: 18,
visible: true,
});
// --- Keybindings ---
const keysTitle = new Gtk.Label({ label: "<b>Keybindings</b>", use_markup: true, halign: Gtk.Align.START, visible: true });
const keysFrame = new Gtk.Frame({ label_widget: keysTitle, shadow_type: Gtk.ShadowType.NONE, visible: true });
let keysBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, margin: 12, spacing: 6, visible: true });
keysFrame.add(keysBox);
let store = new Gtk.ListStore();
store.set_column_types([ GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_INT, GObject.TYPE_INT ]);
const COLUMN_ID = 0, COLUMN_DESC = 1, COLUMN_KEY = 2, COLUMN_MODS = 3;
const addKeybinding = (id, desc) => {
let [key, mods] = [0, 0];
const strv = settings.get_strv(id);
if (strv && strv[0]) [key, mods] = Gtk.accelerator_parse(strv[0]);
let iter = store.append();
store.set(iter, [COLUMN_ID, COLUMN_DESC, COLUMN_KEY, COLUMN_MODS], [id, desc, key, mods]);
};
addKeybinding("swap-master-window", "Swap current window with master");
addKeybinding("swap-up-window", "Swap current window with window above");
addKeybinding("swap-down-window", "Swap current window with window below");
addKeybinding("swap-left-window", "Swap current window with window to the left");
addKeybinding("swap-right-window", "Swap current window with window to the right");
addKeybinding("focus-up", "Focus window above");
addKeybinding("focus-down", "Focus window below");
addKeybinding("focus-left", "Focus window to the left");
addKeybinding("focus-right", "Focus window to the right");
let treeView = new Gtk.TreeView({ model: store, headers_visible: false, hexpand: true, visible: true });
keysBox.add(treeView);
let descRenderer = new Gtk.CellRendererText();
let descColumn = new Gtk.TreeViewColumn({ expand: true });
descColumn.pack_start(descRenderer, true);
descColumn.add_attribute(descRenderer, "text", COLUMN_DESC);
treeView.append_column(descColumn);
let accelRenderer = new Gtk.CellRendererAccel({ "accel-mode": Gtk.CellRendererAccelMode.GTK, editable: true });
accelRenderer.connect("accel-edited", (r, path, key, mods) => {
let [ok, iter] = store.get_iter_from_string(path);
if (ok) {
store.set(iter, [COLUMN_KEY, COLUMN_MODS], [key, mods]);
settings.set_strv(store.get_value(iter, COLUMN_ID), [ Gtk.accelerator_name(key, mods) ]);
}
});
accelRenderer.connect("accel-cleared", (r, path) => {
let [ok, iter] = store.get_iter_from_string(path);
if (ok) {
store.set(iter, [COLUMN_KEY, COLUMN_MODS], [0, 0]);
settings.set_strv(store.get_value(iter, COLUMN_ID), []);
}
});
let accelColumn = new Gtk.TreeViewColumn();
accelColumn.pack_end(accelRenderer, false);
accelColumn.add_attribute(accelRenderer, "accel-key", COLUMN_KEY);
accelColumn.add_attribute(accelRenderer, "accel-mods", COLUMN_MODS);
treeView.append_column(accelColumn);
prefsWidget.add(keysFrame);
// --- Window Gaps ---
const gapsTitle = new Gtk.Label({ label: "<b>Window Gaps</b>", use_markup: true, halign: Gtk.Align.START, visible: true });
const gapsFrame = new Gtk.Frame({ label_widget: gapsTitle, shadow_type: Gtk.ShadowType.NONE, visible: true });
const gapsGrid = new Gtk.Grid({ margin: 12, column_spacing: 12, row_spacing: 12, visible: true });
gapsFrame.add(gapsGrid);
const addSpinButtonRow = (desc, key, pos) => {
const label = new Gtk.Label({ label: desc, halign: Gtk.Align.START, visible: true });
gapsGrid.attach(label, 0, pos, 1, 1);
const adj = new Gtk.Adjustment({ lower: 0, upper: 50, step_increment: 1 });
const spin = new Gtk.SpinButton({ adjustment: adj, climb_rate: 1, digits: 0, halign: Gtk.Align.END, visible: true });
settings.bind(key, spin, "value", Gio.SettingsBindFlags.DEFAULT);
gapsGrid.attach(spin, 1, pos, 1, 1);
};
addSpinButtonRow("Inner Gap", "inner-gap", 0);
addSpinButtonRow("Outer Gap (horizontal)", "outer-gap-horizontal", 1);
addSpinButtonRow("Outer Gap (vertical)", "outer-gap-vertical", 2);
prefsWidget.add(gapsFrame);
// --- Window Behavior ---
const behaviorTitle = new Gtk.Label({ label: "<b>Window Behavior</b>", use_markup: true, halign: Gtk.Align.START, visible: true });
const behaviorFrame = new Gtk.Frame({ label_widget: behaviorTitle, shadow_type: Gtk.ShadowType.NONE, visible: true });
const behaviorGrid = new Gtk.Grid({ margin: 12, column_spacing: 12, row_spacing: 12, visible: true });
behaviorFrame.add(behaviorGrid);
const label = new Gtk.Label({ label: "Open new windows as", halign: Gtk.Align.START, visible: true });
behaviorGrid.attach(label, 0, 0, 1, 1);
const combo = new Gtk.ComboBoxText({ visible: true, halign: Gtk.Align.END });
combo.append("stack", "Stack Window (Default)");
combo.append("master", "Master Window");
combo.set_active_id(settings.get_string("new-window-behavior"));
combo.connect("changed", () => {
settings.set_string("new-window-behavior", combo.get_active_id());
});
behaviorGrid.attach(combo, 1, 0, 1, 1);
prefsWidget.add(behaviorFrame);
return prefsWidget;
};
// --- MAIN ENTRY POINTS (called by GNOME Shell) ---
function init() {}
function buildPrefsWidget() {
return buildLegacyPrefsWidget();
}
if (Adw) {
var defaultExport = ModernPrefs;
}
+227
View File
@@ -0,0 +1,227 @@
///////////////////////////////////////////////////////////////
// SimpleTiling  LEGACY MENU (GNOME Shell 3.38  44) //
// © 2025domoel  MIT //
/////////////////////////////////////////////////////////////
// ── GLOBAL IMPORTS ────────────────────────────────────────
"use strict";
const { Gtk, GObject, Gio } = imports.gi;
const ExtensionUtils = imports.misc.extensionUtils;
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
// ── DEFINITIONS ────────────────────────────────────────────
const COLUMN_ID = 0;
const COLUMN_DESC = 1;
const COLUMN_KEY = 2;
const COLUMN_MODS = 3;
function init() {}
function buildPrefsWidget() {
const settings = ExtensionUtils.getSettings(SCHEMA_NAME);
const prefsWidget = new Gtk.Box({
orientation: Gtk.Orientation.VERTICAL,
margin_top: 20,
margin_bottom: 20,
margin_start: 20,
margin_end: 20,
spacing: 18,
visible: true,
});
// ── KEYBINDINGS ────────────────────────────────────────────
const keysTitle = new Gtk.Label({
label: "<b>Keybindings</b>",
use_markup: true,
halign: Gtk.Align.START,
visible: true,
});
const keysFrame = new Gtk.Frame({
label_widget: keysTitle,
shadow_type: Gtk.ShadowType.NONE,
visible: true,
});
let keysBox = new Gtk.Box({
orientation: Gtk.Orientation.VERTICAL,
margin: 12,
spacing: 6,
visible: true,
});
keysFrame.add(keysBox);
let store = new Gtk.ListStore();
store.set_column_types([
GObject.TYPE_STRING,
GObject.TYPE_STRING,
GObject.TYPE_INT,
GObject.TYPE_INT,
]);
addKeybinding(store, settings, "swap-master-window", "Swap current window with master");
addKeybinding(store, settings, "swap-up-window", "Swap current window with window above");
addKeybinding(store, settings, "swap-down-window", "Swap current window with window below");
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, "focus-up", "Focus window above");
addKeybinding(store, settings, "focus-down", "Focus window below");
addKeybinding(store, settings, "focus-left", "Focus window to the left");
addKeybinding(store, settings, "focus-right", "Focus window to the right");
let treeView = new Gtk.TreeView({
model: store,
headers_visible: false,
hexpand: true,
visible: true,
});
keysBox.add(treeView);
let descRenderer = new Gtk.CellRendererText();
let descColumn = new Gtk.TreeViewColumn({ expand: true });
descColumn.pack_start(descRenderer, true);
descColumn.add_attribute(descRenderer, "text", COLUMN_DESC);
treeView.append_column(descColumn);
let accelRenderer = new Gtk.CellRendererAccel({
"accel-mode": Gtk.CellRendererAccelMode.GTK,
editable: true,
});
let accelColumn = new Gtk.TreeViewColumn();
accelColumn.pack_end(accelRenderer, false);
accelColumn.add_attribute(accelRenderer, "accel-key", COLUMN_KEY);
accelColumn.add_attribute(accelRenderer, "accel-mods", COLUMN_MODS);
treeView.append_column(accelColumn);
accelRenderer.connect("accel-edited", (r, path, key, mods) => {
let [ok, iter] = store.get_iter_from_string(path);
if (ok) {
store.set(iter, [COLUMN_KEY, COLUMN_MODS], [key, mods]);
settings.set_strv(store.get_value(iter, COLUMN_ID), [
Gtk.accelerator_name(key, mods),
]);
}
});
accelRenderer.connect("accel-cleared", (r, path) => {
let [ok, iter] = store.get_iter_from_string(path);
if (ok) {
store.set(iter, [COLUMN_KEY, COLUMN_MODS], [0, 0]);
settings.set_strv(store.get_value(iter, COLUMN_ID), []);
}
});
prefsWidget.add(keysFrame);
// ── WINDOW GAPS ────────────────────────────────────────────
const gapsTitle = new Gtk.Label({
label: "<b>Window Gaps</b>",
use_markup: true,
halign: Gtk.Align.START,
visible: true,
});
const gapsFrame = new Gtk.Frame({
label_widget: gapsTitle,
shadow_type: Gtk.ShadowType.NONE,
visible: true,
});
const gapsGrid = new Gtk.Grid({
margin: 12,
column_spacing: 12,
row_spacing: 12,
visible: true,
});
gapsFrame.add(gapsGrid);
addSpinButtonRow(gapsGrid, settings, "Inner Gap", "inner-gap", 0);
addSpinButtonRow(gapsGrid, settings, "Outer Gap (horizontal)", "outer-gap-horizontal", 1);
addSpinButtonRow(gapsGrid, settings, "Outer Gap (vertical)", "outer-gap-vertical", 2);
prefsWidget.add(gapsFrame);
// ── WINDOW BEHAVIOR ────────────────────────────────────────────
const behaviorTitle = new Gtk.Label({
label: "<b>Window Behavior</b>",
use_markup: true,
halign: Gtk.Align.START,
visible: true,
});
const behaviorFrame = new Gtk.Frame({
label_widget: behaviorTitle,
shadow_type: Gtk.ShadowType.NONE,
visible: true,
});
const behaviorGrid = new Gtk.Grid({
margin: 12,
column_spacing: 12,
row_spacing: 12,
visible: true,
});
behaviorFrame.add(behaviorGrid);
addComboBoxRow(
behaviorGrid,
settings,
"Open new windows as",
"new-window-behavior",
0
);
prefsWidget.add(behaviorFrame);
prefsWidget.show_all();
return prefsWidget;
}
function addKeybinding(model, settings, id, desc) {
let [key, mods] = [0, 0];
const strv = settings.get_strv(id);
if (strv && strv[0]) {
[key, mods] = Gtk.accelerator_parse(strv[0]);
}
let iter = model.append();
model.set(
iter,
[COLUMN_ID, COLUMN_DESC, COLUMN_KEY, COLUMN_MODS],
[id, desc, key, mods]
);
}
function addSpinButtonRow(grid, settings, desc, key, pos) {
const label = new Gtk.Label({
label: desc,
halign: Gtk.Align.START,
visible: true,
});
grid.attach(label, 0, pos, 1, 1);
const adj = new Gtk.Adjustment({ lower: 0, upper: 100, step_increment: 1 });
const spin = new Gtk.SpinButton({
adjustment: adj,
climb_rate: 1,
digits: 0,
halign: Gtk.Align.END,
visible: true,
});
settings.bind(key, spin, "value", Gio.SettingsBindFlags.DEFAULT);
grid.attach(spin, 1, pos, 1, 1);
}
function addComboBoxRow(grid, settings, desc, key, pos) {
const label = new Gtk.Label({
label: desc,
halign: Gtk.Align.START,
visible: true,
});
grid.attach(label, 0, pos, 1, 1);
const combo = new Gtk.ComboBoxText({
visible: true,
halign: Gtk.Align.END,
});
combo.append("stack", "Stack Window (Default)");
combo.append("master", "Master Window");
combo.set_active_id(settings.get_string(key));
combo.connect("changed", () => {
settings.set_string(key, combo.get_active_id());
});
grid.attach(combo, 1, pos, 1, 1);
}
+89
View File
@@ -0,0 +1,89 @@
///////////////////////////////////////////////////////////////
// SimpleTiling  MODERN MENU (GNOME Shell 45+) //
// © 2025domoel  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);
}
}