363 lines
13 KiB
JavaScript
363 lines
13 KiB
JavaScript
/* extension.js
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
import { Gio, GLib, Meta } from './src/dependencies/gi.js';
|
|
import { Extension, Main } from './src/dependencies/shell.js';
|
|
|
|
import MoveHandler from './src/extension/moveHandler.js';
|
|
import ResizeHandler from './src/extension/resizeHandler.js';
|
|
import KeybindingHandler from './src/extension/keybindingHandler.js';
|
|
import LayoutsManager from './src/extension/layoutsManager.js';
|
|
import ActiveWindowHint from './src/extension/activeWindowHint.js';
|
|
import AltTabOverride from './src/extension/altTab.js';
|
|
import { Rect } from './src/extension/utility.js';
|
|
|
|
/**
|
|
* 2 entry points:
|
|
* 1. keyboard shortcuts:
|
|
* => keybindingHandler.js
|
|
* 2. Grabbing a window:
|
|
* => moveHandler.js (when moving a window)
|
|
* => resizeHandler.js (when resizing a window)
|
|
*/
|
|
|
|
class SettingsOverrider {
|
|
constructor(settingsSingleton) {
|
|
this._settings = settingsSingleton;
|
|
this._overrides = new Map();
|
|
this._originalSettings = new Map();
|
|
this._maybeNullValue = GLib.Variant.new_maybe(
|
|
new GLib.VariantType('b'), null);
|
|
|
|
const savedSettings = this._settings.getUserValue('overridden-settings');
|
|
this._wasOverridden = savedSettings !== null;
|
|
}
|
|
|
|
_maybeUpdateOverriden(schemaId, key, value) {
|
|
if (this._wasOverridden)
|
|
return undefined;
|
|
|
|
const savedSettings = this._settings.getValue(
|
|
'overridden-settings').deepUnpack();
|
|
const prefKey = `${schemaId}.${key}`;
|
|
const oldValue = savedSettings[prefKey];
|
|
|
|
if (value !== undefined)
|
|
savedSettings[prefKey] = value ?? this._maybeNullValue;
|
|
else
|
|
delete savedSettings[prefKey];
|
|
|
|
this._settings.setValue('overridden-settings',
|
|
new GLib.Variant('a{sv}', savedSettings));
|
|
|
|
return oldValue;
|
|
}
|
|
|
|
add(settings, key, value) {
|
|
this._originalSettings.set(settings.schemaId, settings);
|
|
const userValue = settings.get_user_value(key);
|
|
|
|
const values = this._overrides.get(settings.schemaId) ?? new Map();
|
|
if (!values.size)
|
|
this._overrides.set(settings.schemaId, values);
|
|
values.set(key, userValue);
|
|
|
|
settings.set_value(key, value);
|
|
|
|
this._maybeUpdateOverriden(settings.schemaId, key,
|
|
userValue ?? this._maybeNullValue);
|
|
}
|
|
|
|
remove(schema, key) {
|
|
const settings = this._originalSettings.get(schema);
|
|
if (!settings)
|
|
return;
|
|
|
|
const values = this._overrides.get(settings.schemaId);
|
|
const value = values?.get(key);
|
|
|
|
if (value === undefined)
|
|
return;
|
|
|
|
if (value)
|
|
settings.set_value(key, value);
|
|
else
|
|
settings.reset(key);
|
|
|
|
values.delete(key);
|
|
this._maybeUpdateOverriden(settings.schemaId, key, undefined);
|
|
}
|
|
|
|
_clear() {
|
|
if (this._wasOverridden) {
|
|
const savedSettings = this._settings.getValue(
|
|
'overridden-settings').unpack();
|
|
|
|
Object.entries(savedSettings).forEach(([path, value]) => {
|
|
const splits = path.split('.');
|
|
const key = splits.at(-1);
|
|
const schemaId = splits.slice(0, -1).join('.');
|
|
|
|
const settings = this._originalSettings.get(schemaId) ??
|
|
new Gio.Settings({ schema_id: schemaId });
|
|
|
|
value = value.get_variant();
|
|
if (value.equal(this._maybeNullValue))
|
|
settings.reset(key);
|
|
else
|
|
settings.set_value(key, value);
|
|
});
|
|
} else {
|
|
this._originalSettings.forEach(settings => {
|
|
this._overrides.get(settings.schemaId).forEach((value, key) => {
|
|
if (value)
|
|
settings.set_value(key, value);
|
|
else
|
|
settings.reset(key);
|
|
});
|
|
});
|
|
}
|
|
|
|
this._settings.reset('overridden-settings');
|
|
}
|
|
|
|
destroy() {
|
|
this._clear();
|
|
this._maybeNullValue = null;
|
|
this._originalSettings = null;
|
|
this._overrides = null;
|
|
this._settings = null;
|
|
}
|
|
}
|
|
|
|
export default class TilingAssistantExtension extends Extension {
|
|
async enable() {
|
|
this.settings = (await import('./src/common.js')).Settings;
|
|
this.settings.initialize(this.getSettings());
|
|
this._settingsOverrider = new SettingsOverrider(this.settings);
|
|
|
|
const twmModule = await import('./src/extension/tilingWindowManager.js');
|
|
|
|
this._twm = twmModule.TilingWindowManager;
|
|
this._twm.initialize();
|
|
|
|
this._moveHandler = new MoveHandler();
|
|
this._resizeHandler = new ResizeHandler();
|
|
this._keybindingHandler = new KeybindingHandler();
|
|
this._layoutsManager = new LayoutsManager();
|
|
this._activeWindowHintHandler = new ActiveWindowHint();
|
|
this._altTabOverride = new AltTabOverride();
|
|
|
|
// Disable native tiling.
|
|
this._settingsOverrider.add(new Gio.Settings({
|
|
schema_id: 'org.gnome.mutter'
|
|
}), 'edge-tiling', new GLib.Variant('b', false));
|
|
|
|
// Disable native keybindings for Super+Up/Down/Left/Right
|
|
const gnomeMutterKeybindings = new Gio.Settings({
|
|
schema_id: 'org.gnome.mutter.keybindings'
|
|
});
|
|
const gnomeDesktopKeybindings = new Gio.Settings({
|
|
schema_id: 'org.gnome.desktop.wm.keybindings'
|
|
});
|
|
const sc = (await import('./src/common.js')).Shortcuts;
|
|
const emptyStrvVariant = new GLib.Variant('as', []);
|
|
|
|
if (gnomeDesktopKeybindings.get_strv('maximize').includes('<Super>Up') &&
|
|
this.settings.getStrv(sc.MAXIMIZE).includes('<Super>Up')) {
|
|
this._settingsOverrider.add(gnomeDesktopKeybindings,
|
|
'maximize', emptyStrvVariant);
|
|
}
|
|
if (gnomeDesktopKeybindings.get_strv('unmaximize').includes('<Super>Down') &&
|
|
this.settings.getStrv(sc.RESTORE_WINDOW).includes('<Super>Down')) {
|
|
this._settingsOverrider.add(gnomeDesktopKeybindings,
|
|
'unmaximize', emptyStrvVariant);
|
|
}
|
|
if (gnomeMutterKeybindings.get_strv('toggle-tiled-left').includes('<Super>Left') &&
|
|
this.settings.getStrv(sc.LEFT).includes('<Super>Left')) {
|
|
this._settingsOverrider.add(gnomeMutterKeybindings,
|
|
'toggle-tiled-left', emptyStrvVariant);
|
|
}
|
|
if (gnomeMutterKeybindings.get_strv('toggle-tiled-right').includes('<Super>Right') &&
|
|
this.settings.getStrv(sc.RIGHT).includes('<Super>Right')) {
|
|
this._settingsOverrider.add(gnomeMutterKeybindings,
|
|
'toggle-tiled-right', emptyStrvVariant);
|
|
}
|
|
|
|
// Include tiled windows when dragging from the top panel.
|
|
this._getDraggableWindowForPosition = Main.panel._getDraggableWindowForPosition;
|
|
Main.panel._getDraggableWindowForPosition = function (stageX) {
|
|
const workspaceManager = global.workspace_manager;
|
|
const windows = workspaceManager.get_active_workspace().list_windows();
|
|
const allWindowsByStacking = global.display.sort_windows_by_stacking(windows).reverse();
|
|
|
|
return allWindowsByStacking.find(w => {
|
|
const rect = w.get_frame_rect();
|
|
const workArea = w.get_work_area_current_monitor();
|
|
return w.is_on_primary_monitor() &&
|
|
w.showing_on_its_workspace() &&
|
|
w.get_window_type() !== Meta.WindowType.DESKTOP &&
|
|
(w.maximized_vertically || w.tiledRect?.y === workArea.y) &&
|
|
stageX > rect.x && stageX < rect.x + rect.width;
|
|
});
|
|
};
|
|
|
|
// Restore tiled window properties after session was unlocked.
|
|
this._loadAfterSessionLock();
|
|
|
|
// Setting used for detection of a fresh install and do compatibility
|
|
// changes if necessary...
|
|
this.settings.setInt('last-version-installed', this.metadata.version);
|
|
}
|
|
|
|
disable() {
|
|
// Save tiled window properties, if the session was locked to restore
|
|
// them after the session is unlocked again.
|
|
this._saveBeforeSessionLock();
|
|
|
|
this._settingsOverrider.destroy();
|
|
this._settingsOverrider = null;
|
|
this._moveHandler.destroy();
|
|
this._moveHandler = null;
|
|
this._resizeHandler.destroy();
|
|
this._resizeHandler = null;
|
|
this._keybindingHandler.destroy();
|
|
this._keybindingHandler = null;
|
|
this._layoutsManager.destroy();
|
|
this._layoutsManager = null;
|
|
this._activeWindowHintHandler.destroy();
|
|
this._activeWindowHintHandler = null;
|
|
|
|
this._altTabOverride.destroy();
|
|
this._altTabOverride = null;
|
|
|
|
this._twm.destroy();
|
|
this._twm = null;
|
|
|
|
this.settings.destroy();
|
|
this.settings = null;
|
|
|
|
// Restore old functions.
|
|
Main.panel._getDraggableWindowForPosition = this._getDraggableWindowForPosition;
|
|
this._getDraggableWindowForPosition = null;
|
|
|
|
// Delete custom tiling properties.
|
|
const openWindows = global.display.get_tab_list(Meta.TabList.NORMAL, null);
|
|
openWindows.forEach(w => {
|
|
delete w.isTiled;
|
|
delete w.tiledRect;
|
|
delete w.untiledRect;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Extensions are disabled when the screen is locked. So save the custom tiling
|
|
* properties of windows before locking the screen.
|
|
*/
|
|
_saveBeforeSessionLock() {
|
|
if (!Main.sessionMode.isLocked)
|
|
return;
|
|
|
|
this._wasLocked = true;
|
|
|
|
const rectToJsObj = rect => rect && {
|
|
x: rect.x,
|
|
y: rect.y,
|
|
width: rect.width,
|
|
height: rect.height
|
|
};
|
|
|
|
// can't just check for isTiled because maximized windows may
|
|
// have an untiledRect as well in case window gaps are used
|
|
const openWindows = this._twm.getWindows(true);
|
|
const savedWindows = openWindows.filter(w => w.untiledRect).map(w => {
|
|
return {
|
|
windowId: w.get_stable_sequence(),
|
|
isTiled: w.isTiled,
|
|
tiledRect: rectToJsObj(w.tiledRect),
|
|
untiledRect: rectToJsObj(w.untiledRect)
|
|
};
|
|
});
|
|
|
|
const saveObj = {
|
|
'windows': savedWindows,
|
|
'tileGroups': Array.from(this._twm.getTileGroups())
|
|
};
|
|
|
|
const userPath = GLib.get_user_config_dir();
|
|
const parentPath = GLib.build_filenamev([userPath, '/tiling-assistant']);
|
|
const parent = Gio.File.new_for_path(parentPath);
|
|
try { parent.make_directory_with_parents(null); } catch (e) {}
|
|
const path = GLib.build_filenamev([parentPath, '/tiledSessionRestore.json']);
|
|
const file = Gio.File.new_for_path(path);
|
|
try { file.create(Gio.FileCreateFlags.NONE, null); } catch (e) {}
|
|
file.replace_contents(JSON.stringify(saveObj), null, false,
|
|
Gio.FileCreateFlags.REPLACE_DESTINATION, null);
|
|
}
|
|
|
|
/**
|
|
* Extensions are disabled when the screen is locked. After having saved them,
|
|
* reload them here.
|
|
*/
|
|
_loadAfterSessionLock() {
|
|
if (!this._wasLocked)
|
|
return;
|
|
|
|
this._wasLocked = false;
|
|
|
|
const userPath = GLib.get_user_config_dir();
|
|
const path = GLib.build_filenamev([userPath, '/tiling-assistant/tiledSessionRestore.json']);
|
|
const file = Gio.File.new_for_path(path);
|
|
if (!file.query_exists(null))
|
|
return;
|
|
|
|
try { file.create(Gio.FileCreateFlags.NONE, null); } catch (e) {}
|
|
const [success, contents] = file.load_contents(null);
|
|
if (!success || !contents.length)
|
|
return;
|
|
|
|
const openWindows = this._twm.getWindows(true);
|
|
const saveObj = JSON.parse(new TextDecoder().decode(contents));
|
|
|
|
const windowObjects = saveObj['windows'];
|
|
windowObjects.forEach(wObj => {
|
|
const { windowId, isTiled, tiledRect, untiledRect } = wObj;
|
|
const window = openWindows.find(w => w.get_stable_sequence() === windowId);
|
|
if (!window)
|
|
return;
|
|
|
|
const jsToRect = jsRect => jsRect && new Rect(
|
|
jsRect.x, jsRect.y, jsRect.width, jsRect.height
|
|
);
|
|
|
|
window.isTiled = isTiled;
|
|
window.tiledRect = jsToRect(tiledRect);
|
|
window.untiledRect = jsToRect(untiledRect);
|
|
});
|
|
|
|
const tileGroups = new Map(saveObj['tileGroups']);
|
|
this._twm.setTileGroups(tileGroups);
|
|
openWindows.forEach(w => {
|
|
if (tileGroups.has(w.get_id())) {
|
|
const group = this._twm.getTileGroupFor(w);
|
|
this._twm.updateTileGroup(group);
|
|
}
|
|
});
|
|
}
|
|
}
|