15 Commits

Author SHA1 Message Date
Dome a8f7ca7e7e Update README.md 2025-07-28 08:41:19 +02:00
Dome d249abe804 Update README.md 2025-07-28 00:53:12 +02:00
Dome 59b2fe1646 Update modern.js 2025-07-28 00:41:54 +02:00
Dome 6e9fb687fe Update legacy.js 2025-07-28 00:41:24 +02:00
Dome f9c1aee610 Update modern.js 2025-07-28 00:40:55 +02:00
Dome 70a0fd9eab Update exceptions.txt 2025-07-28 00:40:38 +02:00
Dome 2fee754776 Update modern.js 2025-07-28 00:24:08 +02:00
Dome 114b7cc265 Update README.md 2025-07-27 23:49:33 +02:00
Dome 8e6d9ab2d9 Update README.md 2025-07-27 23:47:56 +02:00
Dome bcd14cb87d Update modern.js 2025-07-27 21:42:54 +02:00
Dome 307908cd39 Create legacy.js 2025-07-27 21:18:17 +02:00
Dome fcfdf39059 Update extension.js 2025-07-27 21:17:59 +02:00
Dome e7618cf3e5 Update metadata.json 2025-07-27 21:17:35 +02:00
Dome 811db01995 Create modern.js 2025-07-27 21:17:22 +02:00
Dome ae59b60a7a Update prefs.js 2025-07-27 21:16:51 +02:00
20 changed files with 620 additions and 2210 deletions
+6
View File
@@ -0,0 +1,6 @@
# GNOME Shell Extension specific
schemas/gschemas.compiled
# Common temporary files
*~
*.swp
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Domoel (https://github.com/Domoel/)
Copyright (c) 2025 Dome
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
-146
View File
@@ -1,146 +0,0 @@
###############################################################################
# Simple-Tiling Makefile
#
# make build → Erzeugt alle vier Versionen als Archivdatei
# make build-legacy → Erzeugt Legacy-ZIP (Shell 3.38)
# make build-enterprise → Erzeugt Enterprise-ZIP (Shell 40)
# make build-interim → Erzeugt Interim-ZIP (Shell 41-44)
# make build-modern → Erzeugt Modern-ZIP (Shell 45+)
# make install-legacy → Installiert Legacy Extension
# make install-enterprise → Installiert Enterprise Extension
# make install-interim → Installiert Interim Extension
# make install-modern → Installiert Modern Extension
# make clean → Bereinigt das Ausgangsverzeichnis
###############################################################################
UUID := simple-tiling@domoel
VERSION := 7.6
EXTDIR := $(HOME)/.local/share/gnome-shell/extensions
COMMON_FILES := schemas exceptions.txt locale *.css README.md LICENSE
LEGACY_PREFS := prefs_legacy.js
ENTERPRISE_PREFS := prefs_enterprise.js
INTERIM_PREFS := prefs_interim.js
MODERN_PREFS := prefs_modern.js
###############################################################################
# Helper: 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-enterprise build-interim build-modern \
install-legacy install-enterprise install-interim install-modern clean
build: build-legacy build-enterprise build-interim build-modern
###############################################################################
# Erzeugt Legacy-ZIP (Shell 3.38)
###############################################################################
build-legacy:
@echo "==> Building LEGACY zip (for GNOME 3.38)..."
@rm -rf build && mkdir -p build/$(UUID)
$(call copies,$(COMMON_FILES),build/$(UUID))
@glib-compile-schemas build/$(UUID)/schemas
@cp legacy.js build/$(UUID)/extension.js
@cp $(LEGACY_PREFS) build/$(UUID)/prefs.js
@sed -e "s/__UUID__/$(UUID)/g" \
-e "s/__VERSION__/$(VERSION)/g" \
metadata_legacy.json.in > build/$(UUID)/metadata.json
@cd build && zip -qr ../$(UUID)-legacy-v$(VERSION).zip .
@rm -rf build
@echo "$(UUID)-legacy-v$(VERSION).zip created"
###############################################################################
# Erzeugt Enterprise-ZIP (Shell 40)
###############################################################################
build-enterprise:
@echo "==> Building ENTERPRISE zip (for GNOME 40)..."
@rm -rf build && mkdir -p build/$(UUID)
$(call copies,$(COMMON_FILES),build/$(UUID))
@glib-compile-schemas build/$(UUID)/schemas
@cp enterprise.js build/$(UUID)/extension.js
@cp $(ENTERPRISE_PREFS) build/$(UUID)/prefs.js
@sed -e "s/__UUID__/$(UUID)/g" \
-e "s/__VERSION__/$(VERSION)/g" \
metadata_enterprise.json.in > build/$(UUID)/metadata.json
@cd build && zip -qr ../$(UUID)-enterprise-v$(VERSION).zip .
@rm -rf build
@echo "$(UUID)-enterprise-v$(VERSION).zip created"
###############################################################################
# Erzeugt Interim-ZIP (Shell 41-44)
###############################################################################
build-interim:
@echo "==> Building INTERIM zip (for GNOME 41-44)..."
@rm -rf build && mkdir -p build/$(UUID)
$(call copies,$(COMMON_FILES),build/$(UUID))
@glib-compile-schemas build/$(UUID)/schemas
@cp interim.js build/$(UUID)/extension.js
@cp $(INTERIM_PREFS) build/$(UUID)/prefs.js
@sed -e "s/__UUID__/$(UUID)/g" \
-e "s/__VERSION__/$(VERSION)/g" \
metadata_interim.json.in > build/$(UUID)/metadata.json
@cd build && zip -qr ../$(UUID)-interim-v$(VERSION).zip .
@rm -rf build
@echo "$(UUID)-interim-v$(VERSION).zip created"
###############################################################################
# Erzeugt Modern-ZIP (Shell 45+)
###############################################################################
build-modern:
@echo "==> Building MODERN zip (for GNOME 45+)..."
@rm -rf build && mkdir -p build/$(UUID)
$(call copies,$(COMMON_FILES),build/$(UUID))
@glib-compile-schemas build/$(UUID)/schemas
@cp modern.js build/$(UUID)/extension.js
@cp $(MODERN_PREFS) build/$(UUID)/prefs.js
@sed -e "s/__UUID__/$(UUID)/g" \
-e "s/__VERSION__/$(VERSION)/g" \
metadata_modern.json.in > build/$(UUID)/metadata.json
@cd build && zip -qr ../$(UUID)-modern-v$(VERSION).zip .
@rm -rf build
@echo "$(UUID)-modern-v$(VERSION).zip created"
###############################################################################
# Installiert die verschiedenen Versionen
###############################################################################
install-legacy: build-legacy
@echo "==> Installing LEGACY Extension..."
@rm -rf $(EXTDIR)/$(UUID)
@unzip -q $(UUID)-legacy-v$(VERSION).zip -d $(EXTDIR)/$(UUID)
@rm -f $(UUID)-legacy-v$(VERSION).zip
@echo "✓ Legacy Extension installed to $(EXTDIR)/$(UUID). Restart GNOME Shell to apply."
install-enterprise: build-enterprise
@echo "==> Installing ENTERPRISE Extension..."
@rm -rf $(EXTDIR)/$(UUID)
@unzip -q $(UUID)-enterprise-v$(VERSION).zip -d $(EXTDIR)/$(UUID)
@rm -f $(UUID)-enterprise-v$(VERSION).zip
@echo "✓ Enterprise Extension installed to $(EXTDIR)/$(UUID). Restart GNOME Shell to apply."
install-interim: build-interim
@echo "==> Installing INTERIM Extension..."
@rm -rf $(EXTDIR)/$(UUID)
@unzip -q $(UUID)-interim-v$(VERSION).zip -d $(EXTDIR)/$(UUID)
@rm -f $(UUID)-interim-v$(VERSION).zip
@echo "✓ Interim Extension installed to $(EXTDIR)/$(UUID). Restart GNOME Shell to apply."
install-modern: build-modern
@echo "==> Installing MODERN Extension..."
@rm -rf $(EXTDIR)/$(UUID)
@unzip -q $(UUID)-modern-v$(VERSION).zip -d $(EXTDIR)/$(UUID)
@rm -f $(UUID)-modern-v$(VERSION).zip
@echo "✓ Modern Extension installed to $(EXTDIR)/$(UUID). Restart GNOME Shell to apply."
###############################################################################
# Bereinigt das Ausgangsverzeichnis
###############################################################################
clean:
@rm -f $(UUID)-*.zip
@echo "Build directory and ZIPs removed."
+25 -45
View File
@@ -1,28 +1,15 @@
<p align="center">
<a href="https://ztfr.eu/matrix">
<img src="assets/community-badge.png" alt="Join Zeitfresser Matrix Community" height="70" />
</a>
</p>
<h1 align="center">
Simple Tiling
</span>
<h4 align="center">
<span style="display:inline-flex; align-items:center; gap:12px;">
A lightweight, opinionated, and automatic tiling window manager for GNOME Shell
A lightweight, opinionated, and automatic tiling window manager for GNOME Shell 3.38.
</span>
<p>
<h6 align="center">
<a href="https://ztfr.eu">🏰 Website</a>
·
<a href="https://ztfr.eu/matrix">📰 Zeitfresser Matrix Community</a>
·
<a href="https://social.ztfr.eu/@dome">🐘 Mastodon</a>
·
<a href="https://look.ztfr.eu/#/#support:ztfr.eu">💬 Supportchat</a>
</h6>
<br>
[![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" />
@@ -30,7 +17,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.
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 49**.
This extension was built from the ground up to be stable and performant on **GNOME Shell 3.38**.
## Features
@@ -49,10 +36,10 @@ This extension was built from the ground up to be stable and performant on **GNO
## Requirements
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.
Please note that this extension has been developed for a very specific environment:
* **GNOME Shell Version:** **3.38 - 49**
* **Session Type:** **X11** (Wayland is still in beta but should be fine!).
* **GNOME Shell Version:** **3.38**
* **Session Type:** **X11** (Wayland is not supported).
* **Monitor Setup:** **Single monitor only.** Multi-monitor support is not yet implemented.
## Installation
@@ -63,34 +50,29 @@ Use the [GNOME Shell Extensions website](https://extensions.gnome.org/extension/
#### Manual Installation
The repository includes a Makefile that produces readytoinstall ZIP packages for the three supported GnomeShell lines (a legacy build for Gnome-Shell 3.38, an enterprise build for Gnome-Shell 40, an interim build for Gnome-Shell 41 - 44 and a modern build for Gnome-Shell 45+).
1. **Navigate to your extensions folder:**
1. **Clone the Source**
```bash
git clone https://github.com/Domoel/Simple-Tiling.git
cd Simple-Tiling
cd ~/.local/share/gnome-shell/extensions/
```
3. **Clone the repository directly into a folder named after the extension's UUID:**
2. **Install the package that matches your GNOME-Shell version**
Open the Terminal within the Simple-Tiling directory and run
```bash
make install-legacy # Installs Legacy Extension (Gnome-Shell 3.38)
make install-enterprise # Installs Enterprise Extension (Gnome-Shell 40)
make install-interim # Installs Interim Extension (Gnome-Shell 41 - 44)
make install-modern # Installs Modern Extension (Gnome-Shell 45+)
git clone https://github.com/Domoel/Simple-Tiling.git simple-tiling@domoel
```
**Note:** This command will directly install the extension in the choosen variant (legacy, interim or modern). If you want to manually create and upload the extension to your gnome extensions directory `(~/.local/share/gnome-shell/extensions)` you can just run `make build` to create all versions as .zip or `make build-legacy`, `make build-enterprise`, `make build-interim` or `make build-modern` to create them seperately as .zip. To enable them you need to unzip these archives and put them into your extensions directory.
5. **Compile the GSettings schema.** This is a mandatory step for the keyboard shortcuts to work.
4. **Reload the shell**
```bash
Press Alt + F2, type r , hit ↩ (works for X11 and Wayland)
```
5. **Clean up (optional)**
```bash
make clean # perform this command in the downloaded folder to remove builds and generated ZIPs
cd ~/.local/share/gnome-shell/extensions/simple-tiling@domoel
glib-compile-schemas schemas/
```
3. **Restart GNOME Shell.** Press `Alt` + `F2`, type `r`, and press `Enter`.
5. **Enable the extension** using the GNOME Extensions app or GNOME Tweaks.
**Note:** You have to use "simple-tiling@domoel" as your extension folder / directory. Put all necessary files into this directory. Otherwise the extension will not show up in extension manager.
## Configuration
#### Keyboard Shortcuts
@@ -103,13 +85,13 @@ All keyboard shortcuts can be configured through the Settings panel of Simple Ti
#### Ignoring Applications (`exceptions.txt`)
To prevent an application from being tiled, you can add its `WM_CLASS` (x11) or `App ID` (Wayland) to the `exceptions.txt` file in the extension's directory.
To prevent an application from being tiled, you can add its `WM_CLASS` to the `exceptions.txt` file in the extension's directory.
* Each application's `WM_CLASS` or `App ID` should be on a new line.
* Each application's `WM_CLASS` should be on a new line.
* Lines starting with `#` are treated as comments and are ignored.
* The check is case-insensitive.
To find an application's `WM_CLASS`, open a terminal and run the command `xprop WM_CLASS`. Your cursor will turn into a crosshair. Click on the window of the application you want to exclude. To find the `App ID`, Press Alt + F2, type 'lg', and press Enter. In the Looking Glass window, click the "Windows" tab. Click on the desired window to see its details. Find the value for "app id" and add it to a new line below.
To find an application's `WM_CLASS`, open a terminal and run the command `xprop WM_CLASS`. Your cursor will turn into a crosshair. Click on the window of the application you want to exclude.
An Example of an exceptions.txt can be found in the repo.
@@ -131,13 +113,11 @@ 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:
* Multi-monitor support.
* Support for newer Gnome shells
* Additional layout algorithms.
* A more detailed settings panel to configure other options via a GUI.
## Development & Support
If you need to get support or want to participate in the active development of this software, you can <a href="https://ztfr.eu/matrix">join our Zeitfresser Matrix Community</a> or the <a href="https://look.ztfr.eu/#/#support:ztfr.eu">Development & Support Channel</a> on Matrix.
## License
This project is licensed under the MIT License - see the `LICENSE` file for details.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

-604
View File
@@ -1,604 +0,0 @@
////////////////////////////////////////////////////////////////
// Simple-Tiling ENTERPRISE (GNOME Shell 40 non-ESM) //
// © 2025 domoel MIT //
////////////////////////////////////////////////////////////////
// ── GLOBAL IMPORTS ───────────────────────────
const { Meta, Shell, Gio, GLib, Clutter } = imports.gi;
const Main = imports.ui.main;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
// ── 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'),
};
// ── 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 {
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) {
Main.wm.addKeybinding(
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) Main.wm.removeKeybinding(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);
}
}
// ── EXTENSIONWRAPPER ──────────────────────────
let tiler;
function enable() {
tiler = new Tiler(Me);
tiler.enable();
}
function disable() {
if (tiler) {
tiler.disable();
tiler = null;
}
}
-9
View File
@@ -24,12 +24,3 @@
# --- Start of the Exception List ---
ulauncher
steam
element
totem
extension-manager
timeshift-gtk
gnome-screenshot
org.gnome.NautilusPreviewer
org.gnome.Shell.Extensions
evolution-alarm-notify
+41
View File
@@ -0,0 +1,41 @@
/////////////////////////////////////////////////////////////
// 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;
}
}
-607
View File
@@ -1,607 +0,0 @@
///////////////////////////////////////////////////////////////
// SimpleTiling  INTERIM (GNOME Shell 41 - 44) //
// © 2025domoel  MIT //
/////////////////////////////////////////////////////////////
// ── GLOBAL IMPORTS ────────────────────────────────────────
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';
import { Extension } from 'resource:///org/gnome/shell/extensions/js/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
// ── 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'),
};
// ── 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 {
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) {
Main.wm.addKeybinding(
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) Main.wm.removeKeybinding(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);
}
}
// ── EXTENSIONWRAPPER ───────────────────────────────────
export default class InterimExtension extends Extension {
enable() {
this.tiler = new Tiler(this);
this.tiler.enable();
}
disable() {
if (this.tiler) {
this.tiler.disable();
this.tiler = null;
}
}
}
+21 -20
View File
@@ -1,8 +1,10 @@
/////////////////////////////////////////////////////////////
// Simple-Tiling LEGACY (for GNOME Shell 3.38) //
// © 2025 domoel MIT //
/////////////////////////////////////////////////////////////
// Simple-Tiling LEGACY (for GNOME Shell 3.38) //
// © 2025 domoel MIT //
//////////////////////////////////////////////////////////
// --- GLOBAL IMPORTS ---
'use strict';
const Main = imports.ui.main;
@@ -17,19 +19,19 @@ const Me = ExtensionUtils.getCurrentExtension();
const SCHEMA_NAME = "org.gnome.shell.extensions.simple-tiling.domoel";
const WM_SCHEMA = "org.gnome.desktop.wm.keybindings";
const TILING_DELAY_MS = 20; // Change Tiling Window Delay
const 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'),
"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 ---
@@ -124,7 +126,10 @@ class InteractionHandler {
if (keys.length) {
this._wmKeysToDisable = keys;
keys.forEach(
(key) => (this._savedWmShortcuts[key] = this._wmSettings.get_value(key))
(key) =>
(this._savedWmShortcuts[
key
] = this._wmSettings.get_value(key))
);
}
}
@@ -659,9 +664,9 @@ class Tiler {
}
}
// --- EXTENSION-WRAPPER (for legacy loader) ---
// --- EXTENSION WRAPPER ---
var LegacyExtension = class {
constructor(metadata) {
constructor() {
this.tiler = null;
}
enable() {
@@ -675,7 +680,3 @@ var LegacyExtension = class {
}
}
};
function init(metadata) {
return new LegacyExtension(metadata);
}
+5 -5
View File
@@ -1,17 +1,17 @@
{
"uuid": "__UUID__",
"uuid": "simple-tiling@domoel",
"name": "Simple Tiling",
"description": "A Simple Tiling Extension for Gnome Shell.",
"version": __VERSION__,
"version": 6,
"shell-version": [
"3.38",
"45",
"46",
"47",
"48",
"49"
"48"
],
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
"preferences_ui": "prefs.js",
"url": "https://github.com/Domoel/Simple-Tiling",
"gettext-domain": "__UUID__"
"gettext-domain": "simple-tiling-domoel"
}
-13
View File
@@ -1,13 +0,0 @@
{
"uuid": "__UUID__",
"name": "Simple Tiling",
"description": "A Simple Tiling Extension for Gnome Shell.",
"version": __VERSION__,
"shell-version": [
"40"
],
"settings-schema": "org.gnome.shell.extensions.simple-tiling.domoel",
"preferences_ui": "prefs.js",
"url": "https://github.com/Domoel/Simple-Tiling",
"gettext-domain": "__UUID__"
}
-16
View File
@@ -1,16 +0,0 @@
{
"uuid": "__UUID__",
"name": "Simple Tiling",
"description": "A Simple Tiling Extension for Gnome Shell.",
"version": __VERSION__,
"shell-version": [
"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__"
}
-13
View File
@@ -1,13 +0,0 @@
{
"uuid": "__UUID__",
"name": "Simple Tiling",
"description": "A Simple Tiling Extension for Gnome Shell.",
"version": __VERSION__,
"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": "__UUID__"
}
+300 -232
View File
@@ -1,129 +1,109 @@
/////////////////////////////////////////////////////////////
// SimpleTiling  MODERN (GNOME Shell 45+) //
// ©2025domoel  MIT //
/////////////////////////////////////////////////////////////
// Simple-Tiling MODERN (for GNOME Shell 40+) //
// © 2025 domoel MIT //
//////////////////////////////////////////////////////////
// ── GLOBAL IMPORTS ────────────────────────────────────────
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';
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
// --- GLOBAL IMPORTS ---
import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import Meta from "gi://Meta";
import Shell from "gi://Shell";
import Gio from "gi://Gio";
import GLib from "gi://GLib";
// ── CONST ────────────────────────────────────────────
const WM_SCHEMA = 'org.gnome.desktop.wm.keybindings';
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 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'),
"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"),
};
// ── VERSION CHECK ────────────────────────────────────────────
let shellVersion;
if (Shell.get_session) {
shellVersion = Shell.get_session().get_shell_version();
} else if (Config.PACKAGE_VERSION) {
shellVersion = Config.PACKAGE_VERSION;
} else {
shellVersion = global.shell_version;
}
const SHELL_MAJOR = parseInt(shellVersion.split('.')[0]);
// ── 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 ───────────────────────────────────
// --- 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.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', [])));
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',
(_, __, win) => { if (this.tiler.windows.includes(win))
this.tiler.grabbedWindow = win; })
this._settingsChangedId = this._settings.connect("changed", () =>
this._onSettingsChanged()
);
this._grabOpIds.push(
global.display.connect('grab-op-end', () => this._onGrabEnd())
global.display.connect(
"grab-op-begin",
(display, screen, window) => {
if (this.tiler.windows.includes(window)) {
this.tiler.grabbedWindow = window;
}
}
)
);
this._grabOpIds.push(
global.display.connect("grab-op-end", () => this._onGrabEnd())
);
}
disable() {
if (this._wmKeysToDisable.length)
this._wmKeysToDisable.forEach(k =>
this._wmSettings.set_value(k, this._savedWmShortcuts[k]));
if (this._wmKeysToDisable.length) {
this._wmKeysToDisable.forEach((key) =>
this._wmSettings.set_value(key, this._savedWmShortcuts[key])
);
}
this._unbindAllShortcuts();
if (this._settingsChangedId) {
this._settings.disconnect(this._settingsChangedId);
this._settingsChangedId = null;
}
this._grabOpIds.forEach(id => global.display.disconnect(id));
this._grabOpIds.forEach((id) => global.display.disconnect(id));
this._grabOpIds = [];
}
_bind(key, handler) {
Main.wm.addKeybinding(
_bind(key, callback) {
global.display.add_keybinding(
key,
this._settings,
Meta.KeyBindingFlags.NONE,
Shell.ActionMode.NORMAL,
(..._args) => handler(this)
callback
);
}
_bindAllShortcuts() { for (const [k,h] of Object.entries(KEYBINDINGS)) this._bind(k, h); }
_unbindAllShortcuts(){ for (const k in KEYBINDINGS) Main.wm.removeKeybinding(k); }
_bindAllShortcuts() {
for (const [key, handler] of Object.entries(KEYBINDINGS)) {
this._bind(key, () => handler(this));
}
}
_unbindAllShortcuts() {
for (const key in KEYBINDINGS) {
global.display.remove_keybinding(key);
}
}
_onSettingsChanged() {
this._unbindAllShortcuts();
@@ -131,168 +111,253 @@ class InteractionHandler {
}
_prepareWmShortcuts() {
const schema = this._wmSettings.settings_schema;
const schema = this._wmSettings
.get_schema_source()
.lookup(WM_SCHEMA, true);
if (!schema) return;
const addKeyIfExists = (keys, key) => {
if (schema.has_key(key)) keys.push(key);
};
const keys = [];
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-tiled-left")) {
keys.push("toggle-tiled-left", "toggle-tiled-right");
} else {
addKeyIfExists(keys, "tile-left");
addKeyIfExists(keys, "tile-right");
}
if (schema.has_key('toggle-maximized'))
keys.push('toggle-maximized');
else {
add('maximize'); add('unmaximize');
if (schema.has_key("toggle-maximized")) {
keys.push("toggle-maximized");
} else {
addKeyIfExists(keys, "maximize");
addKeyIfExists(keys, "unmaximize");
}
if (keys.length) {
this._wmKeysToDisable = keys;
keys.forEach(k => this._savedWmShortcuts[k] =
this._wmSettings.get_value(k));
keys.forEach(
(key) =>
(this._savedWmShortcuts[key] = this._wmSettings.get_value(
key
))
);
}
}
_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());
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 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]];
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();
w[0]?.activate(global.get_current_time());
if (windows.length > 0) windows[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]];
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();
src.activate(global.get_current_time());
sourceWindow.activate(global.get_current_time());
}
_findTargetInDirection(src, dir) {
const sRect = src.get_frame_rect(), cand=[];
_findTargetInDirection(source, direction) {
const sourceRect = source.get_frame_rect();
let candidates = [];
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 (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 (!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;
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 best;
return bestTarget;
}
_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]];
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(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;}
_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];
}
return best;
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 ────────────────────────────────────────────────
// --- TILER ---
class Tiler {
constructor(extension) {
this._extension = extension;
this.settings = this._extension.getSettings();
this._extension = extension;
this.settings = this._extension.getSettings();
this.windows = [];
this.grabbedWindow = null;
this._signalIds = new Map();
this._tileInProgress = false;
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._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._tilingDelay = TILING_DELAY_MS;
this._centeringDelay = CENTERING_DELAY_MS;
this._exceptions = [];
this._exceptions = [];
this._interactionHandler = new InteractionHandler(this);
this._tileTimeoutId = null;
this._centerTimeoutIds= [];
this._tileTimeoutId = null;
this._centerTimeoutIds = [];
}
enable() {
this._loadExceptions();
this._workspaceManager = global.workspace_manager;
this._signalIds.set('workspace-changed', {
this._signalIds.set("workspace-changed", {
object: this._workspaceManager,
id: this._workspaceManager.connect('active-workspace-changed',
()=>this._onActiveWorkspaceChanged())
id: this._workspaceManager.connect("active-workspace-changed", () =>
this._onActiveWorkspaceChanged()
),
});
this._connectToWorkspace();
this._interactionHandler.enable();
this._signalIds.set('settings-changed', {
this._signalIds.set("settings-changed", {
object: this.settings,
id: this.settings.connect('changed', ()=>this._onSettingsChanged())
id: this.settings.connect("changed", () =>
this._onSettingsChanged()
),
});
}
@@ -301,38 +366,47 @@ class Tiler {
GLib.source_remove(this._tileTimeoutId);
this._tileTimeoutId = null;
}
this._centerTimeoutIds.forEach(id=>GLib.source_remove(id));
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 {}
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._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());
const file = Gio.File.new_for_path(
this._extension.path + "/exceptions.txt"
);
if (!file.query_exists(null)) {
this._exceptions = [];
return;
}
const [ok, data] = file.load_contents(null);
if (ok) {
this._exceptions = GLib.locale_from_utf8(data)
.split("\n")
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#"))
.map((l) => l.toLowerCase());
} else {
this._exceptions = [];
}
}
_isException(win) {
@@ -360,15 +434,8 @@ class Tiler {
if (index > -1) this._centerTimeoutIds.splice(index, 1);
if (!win || !win.get_display()) return GLib.SOURCE_REMOVE;
if (SHELL_MAJOR < 49) {
if (win.get_maximized()) {
win.unmaximize(Meta.MaximizeFlags.BOTH);
}
} else {
if (win.is_maximized()) {
win.unmaximize();
}
}
if (win.get_maximized())
win.unmaximize(Meta.MaximizeFlags.BOTH);
const monitorIndex = win.get_monitor();
const workspace = this._workspaceManager.get_active_workspace();
@@ -578,15 +645,7 @@ class Tiler {
height: workArea.height - 2 * this._outerGapVertical,
};
windowsToTile.forEach((win) => {
if (SHELL_MAJOR < 49) {
if (win.get_maximized()) {
win.unmaximize(Meta.MaximizeFlags.BOTH);
}
} else {
if (win.is_maximized()) {
win.unmaximize();
}
}
if (win.get_maximized()) win.unmaximize(Meta.MaximizeFlags.BOTH);
});
if (windowsToTile.length === 1) {
windowsToTile[0].move_resize_frame(
@@ -618,8 +677,17 @@ class Tiler {
}
}
// ── EXTENSIONWRAPPER ───────────────────────────────────
// --- MODERN EXTENSION WRAPPER ---
export default class ModernExtension extends Extension {
enable() { this.tiler = new Tiler(this); this.tiler.enable(); }
disable() { this.tiler?.disable(); this.tiler = null; }
enable() {
this.tiler = new Tiler(this);
this.tiler.enable();
}
disable() {
if (this.tiler) {
this.tiler.disable();
this.tiler = null;
}
}
}
+221
View File
@@ -0,0 +1,221 @@
///////////////////////////////////////////////////////
// --- 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;
}
-91
View File
@@ -1,91 +0,0 @@
//////////////////////////////////////////////////////////////////
// Simple-Tiling ENTERPRISE MENU (GNOME Shell 40 non-ESM) //
// © 2025 domoel MIT //
//////////////////////////////////////////////////////////////////
'use strict';
// ── GLOBAL IMPORTS ────────────────────────────────────────
const { Adw, Gio, Gtk, GLib } = imports.gi;
const ExtensionUtils = imports.misc.extensionUtils;
const _ = ExtensionUtils.gettext;
function init() {
}
function buildPrefsWidget() {
const settings = ExtensionUtils.getSettings();
const page = new Adw.PreferencesPage();
// ── 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 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);
const currentBehavior = settings.get_string('new-window-behavior');
rowNewWindow.selected = currentBehavior === '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);
return page;
}
-90
View File
@@ -1,90 +0,0 @@
///////////////////////////////////////////////////////////////
// Simple-Tiling MODERN MENU (GNOME Shell 41-44) //
// © 2025 domoel MIT //
///////////////////////////////////////////////////////////////
'use strict';
import Adw from 'gi://Adw';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk';
import GLib from 'gi://GLib';
import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
export default class SimpleTilingPrefs extends ExtensionPreferences {
fillPreferencesWindow(window) {
const settings = this.getSettings();
const page = new Adw.PreferencesPage();
window.add(page);
// --- Group for 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 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);
// --- Group for 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);
const currentBehavior = settings.get_string('new-window-behavior');
rowNewWindow.selected = currentBehavior === 'master' ? 1 : 0;
rowNewWindow.connect('notify::selected', () => {
const newVal = rowNewWindow.selected === 1 ? 'master' : 'stack';
settings.set_string('new-window-behavior', newVal);
});
// --- Group for 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);
}
}
-227
View File
@@ -1,227 +0,0 @@
///////////////////////////////////////////////////////////////
// SimpleTiling  LEGACY MENU (GNOME Shell 3.38) //
// © 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);
}
-91
View File
@@ -1,91 +0,0 @@
///////////////////////////////////////////////////////////////
// Simple-Tiling MODERN MENU (GNOME Shell 45+) //
// © 2025 domoel MIT //
///////////////////////////////////////////////////////////////
// ── GLOBAL IMPORTS ────────────────────────────────────────
import Adw from 'gi://Adw';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk';
import GLib from 'gi://GLib';
import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
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);
const currentBehavior = settings.get_string('new-window-behavior');
rowNewWindow.selected = currentBehavior === '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);
}
}