%PDF- %PDF-
| Direktori : /usr/share/gnome-shell/extensions/tiling-assistant@ubuntu.com/src/extension/ |
| Current File : //usr/share/gnome-shell/extensions/tiling-assistant@ubuntu.com/src/extension/tilingWindowManager.js |
import { Clutter, GLib, GObject, Meta, Mtk, Shell } from '../dependencies/gi.js';
import { Main } from '../dependencies/shell.js';
import { getWindows } from '../dependencies/unexported/altTab.js';
import { Orientation, Settings, Shortcuts } from '../common.js';
import { Rect, Util } from './utility.js';
/**
* Singleton responsible for tiling. Implement the signals in a separate Clutter
* class so this doesn't need to be instanced.
*/
export class TilingWindowManager {
static initialize() {
this._signals = new TilingSignals();
// { windowId1: [windowIdX, windowIdY, ...], windowId2: [...], ... }
this._tileGroups = new Map();
// [windowIds]
this._unmanagingWindows = [];
this._wsAddedId = global.workspace_manager.connect('workspace-added', this._onWorkspaceAdded.bind(this));
this._wsRemovedId = global.workspace_manager.connect('workspace-removed', this._onWorkspaceRemoved.bind(this));
}
static destroy() {
this._signals.destroy();
this._signals = null;
global.workspace_manager.disconnect(this._wsAddedId);
global.workspace_manager.disconnect(this._wsRemovedId);
this._tileGroups.clear();
this._unmanagingWindows = [];
if (this._openAppTiledTimerId) {
GLib.Source.remove(this._openAppTiledTimerId);
this._openAppTiledTimerId = null;
}
if (this._wsAddedTimer) {
GLib.Source.remove(this._wsAddedTimer);
this._wsAddedTimer = null;
}
if (this._wsRemovedTimer) {
GLib.Source.remove(this._wsRemovedTimer);
this._wsRemovedTimer = null;
}
}
static connect(signal, func) {
return this._signals.connect(signal, func);
}
static disconnect(id) {
this._signals.disconnect(id);
}
static emit(...params) {
this._signals.emit(...params);
}
/**
* Gets windows, which can be tiled
*
* @param {boolean} [allWorkspaces=false] determines whether we only want
* the windows from the current workspace.
* @returns {Meta.Windows[]} an array of of the open Meta.Windows in
* stacking order.
*/
static getWindows(allWorkspaces = false) {
const activeWs = global.workspace_manager.get_active_workspace();
const openWindows = getWindows(allWorkspaces ? null : activeWs);
// The open windows are not sorted properly when tiling with the Tiling
// Popup because altTab sorts by focus.
const sorted = global.display.sort_windows_by_stacking(openWindows);
return sorted.reverse().filter(w => {
// I don't think this should normally happen but if it does, this
// extension can crash GNOME Shell.. so guard against it. A way to
// have a window's monitor be -1, for example, is explained here:
// https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/4713
if (w.get_monitor() === -1)
return false;
// Assumption: a maximized window can also resize (once unmaximized)
const canResize = w.allows_move() && w.allows_resize() || this.isMaximized(w);
return canResize;
});
}
/**
* @param {Meta.Window} window a Meta.Window.
* @param {Meta.WorkArea|Rect|null} workArea useful for the grace period
* @returns whether the window is maximized. Be it using GNOME's native
* maximization or the maximization by this extension when using gaps.
*/
static isMaximized(window, workArea = null) {
const area = workArea ?? window.get_work_area_current_monitor();
return window.get_maximized() === Meta.MaximizeFlags.BOTH ||
window.tiledRect?.equal(area);
}
/**
* Tiles a window to a specific spot and setup all tiling properties.
*
* @param {Meta.Window} window a Meta.Window to tile.
* @param {Rect} newRect the Rect the `window` will be tiled to.
* @param {boolean} [openTilingPopup=true] decides, if we open a Tiling
* Popup after the window is tiled and there is unambiguous free
* screen space.
* @param {number} [number=null] is used to get the workArea in which the
* window tiles on. It's used for gap calculation. We can't always rely on
* window.get_monitor with its monitor or global.display.get_current_monitor
* (the pointer monitor) because of the 'grace period' during a quick dnd
* towards a screen border since the pointer and the window will be on the
* 'wrong' monitor.
* @param {boolean} [skipAnim=false] decides, if we skip the tile animation.
* @param {boolean} [tileGroup=null] forces the creation of this tile group.
* @param {boolean} [fakeTile=false] don't create a new tile group, don't
* emit 'tiled' signal or open the Tiling Popup
*/
static async tile(window, newRect, {
openTilingPopup = true,
ignoreTA = false,
monitorNr = null,
skipAnim = false,
fakeTile = false
} = {}) {
if (!window || window.is_skip_taskbar())
return;
const wasMaximized = window.get_maximized();
if (wasMaximized)
window.unmaximize(wasMaximized);
window.unmake_fullscreen();
if (!window.allows_resize() || !window.allows_move())
return;
// Remove window from the other windows' tileGroups so it
// doesn't falsely get raised with them.
this.clearTilingProps(window.get_id());
window.unmake_above();
window.unminimize();
// Raise window since tiling with the popup means that
// the window can be below others.
if (window.raise_and_make_recent_on_workspace)
window.raise_and_make_recent_on_workspace(global.workspace_manager.get_active_workspace());
else
window.raise_and_make_recent();
const oldRect = new Rect(window.get_frame_rect());
const monitor = monitorNr ?? window.get_monitor();
const workArea = new Rect(window.get_work_area_for_monitor(monitor));
const maximize = newRect.equal(workArea);
window.isTiled = !maximize;
if (!window.untiledRect)
window.untiledRect = oldRect;
if (maximize && !Settings.getBoolean(Settings.MAXIMIZE_WITH_GAPS)) {
window.tiledRect = null;
// It's possible for a window to maximize() to the wrong monitor.
// This is very easy to reproduce when dragging a window on the
// lower half with Super + LMB.
window.move_to_monitor(monitor);
window.maximize(Meta.MaximizeFlags.BOTH);
return;
}
// Save the intended tiledRect for accurate operations later.
// Workaround for windows which can't be resized freely...
// For ex. which only resize in full rows/columns like gnome-terminal
window.tiledRect = newRect.copy();
const { x, y, width, height } = newRect.addGaps(workArea, monitor);
// Animations
const wActor = window.get_compositor_private();
if (Settings.getBoolean(Settings.ENABLE_TILE_ANIMATIONS) && wActor && !skipAnim) {
wActor.remove_all_transitions();
// HACK => journalctl: 'error in size change accounting'...?
// TODO: no animation if going from maximized -> tiled and back to back multiple times?
Main.wm._prepareAnimationInfo(
global.window_manager,
wActor,
oldRect.meta,
Meta.SizeChange.MAXIMIZE
);
}
if (!maximize && window.override_constraints) {
const leftConstraint = newRect.x === workArea.x ?
Meta.WindowConstraint.MONITOR : Meta.WindowConstraint.WINDOW;
const rightConstraint = newRect.x2 === workArea.x2 ?
Meta.WindowConstraint.MONITOR : Meta.WindowConstraint.WINDOW;
const topConstraint = newRect.y === workArea.y ?
Meta.WindowConstraint.MONITOR : Meta.WindowConstraint.WINDOW;
const bottomConstraint = newRect.y2 === workArea.y2 ?
Meta.WindowConstraint.MONITOR : Meta.WindowConstraint.WINDOW;
window.override_constraints(topConstraint, leftConstraint,
rightConstraint, bottomConstraint);
}
// See issue #137.
// Under some circumstances it's possible that windows will tile to the wrong
// monitor. I can't reproduce it but I suspect that it's because of passing
// false as the user_op to move_resize_frame. user_op is meant to determine if
// the window should be clamped to the monitor. A user operation (user_op = true)
// won't be clamped. So I think there is something unexpected happening.
// Someone in the issue mentioned that passing true as the user_op fixes the
// multi-monitor bug.
//
// The reason why I set user_op as false originally is that GNOME Terminal (and
// some other Terminals) will only resize but not move with user_op as true. Try
// to workaround that by first only moving the window and then resizing it. That
// workaround was already necessary under Wayland because of some apps. E. g.
// first tiling Nautilus and then Firefox using the Tiling Popup.
window.move_to_monitor(monitor);
window.move_frame(true, x, y);
window.move_resize_frame(true, x, y, width, height);
// Maximized with gaps
if (maximize) {
this._updateGappedMaxWindowSignals(window);
// Tiled window
} else if (!fakeTile) {
// Make the tile group only consist of the window itself to stop
// resizing or raising together. Also don't call the Tiling Popup.
if (Settings.getBoolean(Settings.DISABLE_TILE_GROUPS) || ignoreTA) {
this.updateTileGroup([window]);
return;
}
// Setup the (new) tileGroup to raise tiled windows as a group
const topTileGroup = this._getWindowsForBuildingTileGroup(monitor);
this.updateTileGroup(topTileGroup);
this.emit('window-tiled', window);
if (openTilingPopup)
await this.tryOpeningTilingPopup();
}
}
/**
* Untiles a tiled window and delete all tiling properties.
*
* @param {Meta.Window} window a Meta.Window to untile.
* @param {boolean} [restoreFullPos=true] decides, if we restore the
* pre-tile position or whether the size while keeping the titlebar
* at the relative same position.
* @param {number} [xAnchor=undefined] used when wanting to restore the
* size while keeping titlebar at the relative x position. By default,
* we use the pointer position.
* @param {boolean} [skipAnim=false] decides, if we skip the until animation.
*/
static untile(window, { restoreFullPos = true, xAnchor = undefined, skipAnim = false, clampToWorkspace = false } = {}) {
const wasMaximized = window.get_maximized();
if (wasMaximized)
window.unmaximize(wasMaximized);
if (!window.untiledRect || !window.allows_resize() || !window.allows_move())
return;
// If you tiled a window and then used the popup to tile more
// windows, the consecutive windows will be raised above the first
// one. So untiling the initial window after tiling more windows with
// the popup (without re-focusing the initial window), means the
// untiled window will be below the others.
if (window.raise_and_make_recent_on_workspace)
window.raise_and_make_recent_on_workspace(global.workspace_manager.get_active_workspace());
else
window.raise_and_make_recent();
// Animation
const untileAnim = Settings.getBoolean(Settings.ENABLE_UNTILE_ANIMATIONS);
const wActor = window.get_compositor_private();
if (untileAnim && !wasMaximized && wActor && !skipAnim) {
wActor.remove_all_transitions();
Main.wm._prepareAnimationInfo(
global.window_manager,
wActor,
window.get_frame_rect(),
Meta.SizeChange.UNMAXIMIZE
);
}
if (window.override_constraints) {
window.override_constraints(Meta.WindowConstraint.NONE,
Meta.WindowConstraint.NONE, Meta.WindowConstraint.NONE,
Meta.WindowConstraint.NONE);
}
// userOp means that the window won't clamp to the workspace. For DND
// we don't want to clamp to the workspace, so it's false by default.
const userOp = !clampToWorkspace;
const oldRect = window.untiledRect;
if (restoreFullPos) {
window.move_resize_frame(userOp, oldRect.x, oldRect.y, oldRect.width, oldRect.height);
} else {
// Resize the window while keeping the relative x pos (of the pointer)
const currWindowFrame = new Rect(window.get_frame_rect());
xAnchor = xAnchor ?? global.get_pointer()[0];
const relativeMouseX = (xAnchor - currWindowFrame.x) / currWindowFrame.width;
const newPosX = xAnchor - oldRect.width * relativeMouseX;
// Wayland workaround for DND / restore position
Meta.is_wayland_compositor() && window.move_frame(true, newPosX, currWindowFrame.y);
window.move_resize_frame(userOp, newPosX, currWindowFrame.y, oldRect.width, oldRect.height);
}
this.clearTilingProps(window.get_id());
window.isTiled = false;
window.tiledRect = null;
window.untiledRect = null;
this.emit('window-untiled', window);
}
/**
* Moves the tile group to a different workspace
*
* @param {Meta.Window[]} tileGroup
* @param {Meta.Workspace} workspace
*/
static moveGroupToWorkspace(tileGroup, workspace) {
tileGroup.forEach(w => {
this._blockTilingSignalsFor(w);
w.change_workspace(workspace);
this._unblockTilingSignalsFor(w);
});
}
/**
* Moves the tile group to a different monitor
*
* @param {Meta.Window[]} tileGroup
* @param {number} oldMon
* @param {number} newMon
*/
static moveGroupToMonitor(tileGroup, oldMon, newMon) {
const activeWs = global.workspace_manager.get_active_workspace();
const oldWorkArea = new Rect(activeWs.get_work_area_for_monitor(oldMon));
const newWorkArea = new Rect(activeWs.get_work_area_for_monitor(newMon));
const hScale = oldWorkArea.width / newWorkArea.width;
const vScale = oldWorkArea.height / newWorkArea.height;
tileGroup.forEach((w, idx) => {
const newTile = w.tiledRect.copy();
newTile.x = newWorkArea.x + Math.floor(newWorkArea.width * ((w.tiledRect.x - oldWorkArea.x) / oldWorkArea.width));
newTile.y = newWorkArea.y + Math.floor(newWorkArea.height * ((w.tiledRect.y - oldWorkArea.y) / oldWorkArea.height));
newTile.width = Math.floor(w.tiledRect.width * (1 / hScale));
newTile.height = Math.floor(w.tiledRect.height * (1 / vScale));
// Try to align with all previously scaled tiles and the workspace to prevent gaps
for (let i = 0; i < idx; i++)
newTile.tryAlignWith(tileGroup[i].tiledRect);
newTile.tryAlignWith(newWorkArea, 10);
this.tile(w, newTile, {
skipAnim: true,
fakeTile: true
});
});
// The tiling signals got disconnected during the tile() call but not
// (re-)connected with it since it may have been possible that wrong tile
// groups would have been created when moving one window after the other
// to the new monitor. So update the tileGroup now with the full/old group.
this.updateTileGroup(tileGroup);
}
/**
* @returns {Map<number,number>}
* For ex: { windowId1: [windowIdX, windowIdY, ...], windowId2: ... }
*/
static getTileGroups() {
return this._tileGroups;
}
/**
* @param {Map<number, number>} tileGroups
* For ex: { windowId1: [windowIdX, windowIdY, ...], windowId2: ... }
*/
static setTileGroups(tileGroups) {
this._tileGroups = tileGroups;
}
/**
* Creates a tile group of windows to raise them together, if one of them
* is raised by (re)connecting signals. Usually, this is done automatically
* by calling tile() and thus shouldn't be done manually. tile() only allows
* unique/non-overlapping tile groups, so 1 window can't be part of multiple
* tile groups. But we specifically allow the user to do that sometimes
* (i. e. ctrl-drag or tile editing mode+space). So manually create the
* tile group in those cases.
*
* @param {Meta.Windows[]} tileGroup an array of Meta.Windows to group
* together.
*/
static updateTileGroup(tileGroup) {
tileGroup.forEach(window => {
const windowId = window.get_id();
const signals = this._signals.getSignalsFor(windowId);
this._tileGroups.set(windowId, tileGroup.map(w => w.get_id()));
/**
* clearTilingProps may have been called before this function,
* so we need to reconnect all the signals on the tileGroup.
* Just in case, also try to disconnect old signals...
*/
// Reconnect unmanaging signal
const unmanagingSignal = signals.get(TilingSignals.UNMANAGING);
unmanagingSignal && window.disconnect(unmanagingSignal);
const umId = window.connect('unmanaging', w => {
this.clearTilingProps(windowId);
this._unmanagingWindows.push(w.get_stable_sequence());
});
signals.set(TilingSignals.UNMANAGING, umId);
// Reconnect ws-changed signal
const wsChangeSignal = signals.get(TilingSignals.WS_CHANGED);
wsChangeSignal && window.disconnect(wsChangeSignal);
const wsId = window.connect('workspace-changed', () => this._onWindowWorkspaceChanged(window));
signals.set(TilingSignals.WS_CHANGED, wsId);
// Reconnect raise signal
const raiseSignal = signals.get(TilingSignals.RAISE);
raiseSignal && window.disconnect(raiseSignal);
const raiseId = window.connect('raised', raisedWindow => {
const raisedWindowId = raisedWindow.get_id();
if (Settings.getBoolean(Settings.RAISE_TILE_GROUPS)) {
const raisedWindowsTileGroup = this._tileGroups.get(raisedWindowId);
raisedWindowsTileGroup.forEach(wId => {
const w = this._getWindow(wId);
const otherRaiseId = this._signals.getSignalsFor(wId).get(TilingSignals.RAISE);
// May be undefined, if w was just closed. This would
// automatically call clearTilingProps() with the signal
// but in case I missed / don't know about other cases where
// w may be nullish, dissolve the tileGroups anyway.
if (!w || !otherRaiseId) {
this.clearTilingProps(wId);
return;
}
// Prevent an infinite loop of windows raising each other
w.block_signal_handler(otherRaiseId);
if (w.raise_and_make_recent_on_workspace)
w.raise_and_make_recent_on_workspace(global.workspace_manager.get_active_workspace());
else
w.raise_and_make_recent();
w.unblock_signal_handler(otherRaiseId);
});
// Re-raise the just raised window so it may not be below
// other tiled windows otherwise when untiling via keyboard
// it may be below other tiled windows.
const signalId = this._signals.getSignalsFor(raisedWindowId).get(TilingSignals.RAISE);
raisedWindow.block_signal_handler(signalId);
if (raisedWindow.raise_and_make_recent_on_workspace)
raisedWindow.raise_and_make_recent_on_workspace(global.workspace_manager.get_active_workspace());
else
raisedWindow.raise_and_make_recent();
raisedWindow.unblock_signal_handler(signalId);
}
// Update the tileGroup (and reconnect the raised signals) to allow windows
// to be part of multiple tileGroups: for ex.: tiling a window over another
// tiled window with ctrl-drag will replace the overlapped window in the old
// tileGroup but the overlapped window will remember its old tile group to
// raise them as well, if it is raised.
const raisedTileGroup = this.getTileGroupFor(raisedWindow);
this.updateTileGroup(raisedTileGroup);
});
signals.set(TilingSignals.RAISE, raiseId);
});
}
/**
* Deletes the tile group of a window and remove that window from other
* tiled windows' tile groups. Also disconnects the signals for windows
* which are maximized-with-gaps.
*
* @param {number} windowId the id of a Meta.Window.
*/
static clearTilingProps(windowId) {
const window = this._getWindow(windowId);
const signals = this._signals.getSignalsFor(windowId);
if (signals.get(TilingSignals.RAISE)) {
window && window.disconnect(signals.get(TilingSignals.RAISE));
signals.set(TilingSignals.RAISE, 0);
}
if (signals.get(TilingSignals.WS_CHANGED)) {
window && window.disconnect(signals.get(TilingSignals.WS_CHANGED));
signals.set(TilingSignals.WS_CHANGED, 0);
}
if (signals.get(TilingSignals.UNMANAGING)) {
window && window.disconnect(signals.get(TilingSignals.UNMANAGING));
signals.set(TilingSignals.UNMANAGING, 0);
}
if (!this._tileGroups.has(windowId))
return;
// Delete window's tileGroup
this._tileGroups.delete(windowId);
// Delete window from other windows' tileGroup
this._tileGroups.forEach(tileGroup => {
const idx = tileGroup.indexOf(windowId);
idx !== -1 && tileGroup.splice(idx, 1);
});
}
/**
* @param {Meta.Window} window a Meta.Window.
* @returns {Meta.Window[]} an array of Meta.Windows, which are in `window`'s
* tile group (including the `window` itself).
*/
static getTileGroupFor(window) {
const tileGroup = this._tileGroups.get(window.get_id());
if (!tileGroup)
return [];
return this._getAllWindows().filter(w => tileGroup.includes(w.get_id()));
}
/**
* Gets the top most tiled window group; that means they complement each
* other and don't intersect. This may differ from the TileGroupManager's
* *tracked* tile groups since floating windows may overlap some tiled
* windows *at the moment* when this function is called.
*
* @param {boolean} [skipTopWindow=true] whether we ignore the focused window
* in the active search for the top tile group. The focused window may
* still be part of the returned array if it is part of another high-
* stacked window's tile group. This is mainly only useful, if the
* focused window isn't tiled (for example when dnd-ing a window).
* @param {number} [monitor=null] get the group for the monitor number.
* @returns {Meta.Windows[]} an array of tiled Meta.Windows.
*/
static getTopTileGroup({ skipTopWindow = false, monitor = null } = {}) {
// 'Raise Tile Group' setting is enabled so we just return the tracked
// tile group. Same thing for the setting 'Disable Tile Groups' because
// it's implemented by just making the tile groups consist of single
// windows (the tiled window itself).
if (Settings.getBoolean(Settings.RAISE_TILE_GROUPS) ||
Settings.getBoolean(Settings.DISABLE_TILE_GROUPS)
) {
const openWindows = this.getWindows();
if (!openWindows.length)
return [];
if (skipTopWindow) {
// the focused window isn't necessarily the top window due to always
// on top windows.
const idx = openWindows.indexOf(global.display.focus_window);
idx !== -1 && openWindows.splice(idx, 1);
}
const ignoredWindows = [];
const mon = monitor ??
global.display.focus_window?.get_monitor() ??
openWindows[0].get_monitor();
for (const window of openWindows) {
if (window.get_monitor() !== mon)
continue;
// Ignore non-tiled windows, which are always-on-top, for the
// calculation since they are probably some utility apps etc.
if (window.is_above() && !window.isTiled)
continue;
// Find the first not overlapped tile group, if it exists
if (window.isTiled) {
const overlapsIgnoredWindow = ignoredWindows.some(w => {
const rect = w.tiledRect ?? new Rect(w.get_frame_rect());
return rect.overlap(window.tiledRect);
});
if (overlapsIgnoredWindow)
ignoredWindows.push(window);
else
return this.getTileGroupFor(window);
} else {
ignoredWindows.push(window);
}
}
return [];
// 'Raise Tile Group' setting is disabled so we get thetop most
// non-overlapped/ing tiled windows ignoring the tile groups.
} else {
return this._getTopTiledWindows({ skipTopWindow, monitor });
}
}
/**
* Gets the free screen space (1 big Rect). If the free screen space
* is ambiguous that means it consists of multiple (unaligned) rectangles
* (for ex.: 2 diagonally opposing quarters). In that case we return null.
*
* @param {Rect[]} rectList an array of Rects, which occupy the screen.
* @param {number|null} [monitorNr] useful for the grace period during dnd.
* Defaults to pointer monitor.
* @returns {Rect|null} a Rect, which represent the free screen space.
*/
static getFreeScreen(rectList, monitorNr = null) {
const activeWs = global.workspace_manager.get_active_workspace();
const monitor = monitorNr ?? global.display.get_current_monitor();
const workArea = new Rect(activeWs.get_work_area_for_monitor(monitor));
const freeScreenRects = workArea.minus(rectList);
if (!freeScreenRects.length)
return null;
// Create the union of all freeScreenRects and calculate the sum
// of their areas. If the area of the union-rect equals the area
// of the individual rects, the individual rects align properly.
const startRect = new Rect(freeScreenRects[0].x, freeScreenRects[0].y, 0, 0);
const { checkSum, combinedRect } = freeScreenRects.reduce((result, rect) => {
result.checkSum += rect.area;
result.combinedRect = result.combinedRect.union(rect);
return result;
}, { checkSum: 0, combinedRect: startRect });
if (combinedRect.area !== checkSum)
return null;
// Random min. size requirement
if (combinedRect.width < 250 || combinedRect.height < 250)
return null;
return combinedRect;
}
/**
* Gets the best available free screen rect. If a `currRect` is passed,
* instead this will return an expanded copy of that rect filling all
* the available space around it.
*
* @param {Rect[]} rectList an array of Rects, which occupy the screen.
* Like usual, they shouldn't overlap each other.
* @param {Rect} [currRect=null] a Rect, which may be expanded.
* @param {Orientation} [orientation=null] The orientation we want to expand
* `currRect` into. If `null`, expand in both orientations.
* @param {Rect} [monitor=null] defaults to pointer monitor.
* @returns {Rect} a new Rect.
*/
static getBestFreeRect(rectList, { currRect = null, orientation = null, monitorNr = null } = {}) {
const activeWs = global.workspace_manager.get_active_workspace();
const monitor = monitorNr ?? global.display.get_current_monitor();
const workArea = new Rect(activeWs.get_work_area_for_monitor(monitor));
const freeRects = workArea.minus(rectList);
if (!freeRects.length)
return currRect ?? new Rect(workArea);
// Try to expand the currRect to fill the rest of the space
// that is available around it.
if (currRect) {
const isVert = (orientation ?? Orientation.V) === Orientation.V;
const [xpndPos1, xpndPos2] = isVert ? ['y', 'y2'] : ['x', 'x2'];
const [unxpndPos1, unxpndPos2] = isVert ? ['x', 'x2'] : ['y', 'y2'];
// Filter the rects to only keep the ones directly bordering the
// currRect and sort the array so that the free rects are ordered
// from the left to the right or from the top to the bottom. See
// below for the reasoning.
const borderingRects = freeRects.filter(r => {
const axis1 = currRect[xpndPos1] === r[xpndPos2] || currRect[xpndPos2] === r[xpndPos1];
const axis2 = isVert ? currRect.horizOverlap(r) : currRect.vertOverlap(r);
return axis1 && axis2;
}).sort((a, b) => a[unxpndPos1] - b[unxpndPos1]);
// Separate the rects into the ones that come before (left / top)
// or after (right / bottom) the current rect.
const { before, after } = borderingRects.reduce((result, r) => {
if (currRect[xpndPos1] === r[xpndPos2])
result.before.push(r);
else if (currRect[xpndPos2] === r[xpndPos1])
result.after.push(r);
return result;
}, { before: [], after: [] });
// If we want to check whether the current rect can expand on a certain
// side (let's say we expand the height), we need to check the *other*
// (unexpanded) side. So whether the current rect is bordering the free
// screen rects along its *entire width*. We do this by 'union-ing' the
// free screen rects along the relevant side (our ex.: width). For this
// reason we needed to sort the free rects in ascending order before
// to make sure they overlap before trying to 'union' them. After the
// union-ing, we just check, if the union-ed rect contains the current
// rects unexpanded side.
// Orientation doesn't matter here since we are always comparing sides
// of the same orientation. So just make the side always horizontal.
const makeSide = (startPoint, endPoint) => new Mtk.Rectangle({
x: startPoint,
width: endPoint - startPoint,
height: 1
});
const freeRectsContainCurrRectSide = rects => {
const currRectSide = makeSide(currRect[unxpndPos1], currRect[unxpndPos2]);
const linkedSides = rects.reduce((linked, r) => {
const side = makeSide(r[unxpndPos1], r[unxpndPos2]);
return linked.overlap(side) ? linked.union(side) : linked;
}, makeSide(rects[0][unxpndPos1], rects[0][unxpndPos2]));
return linkedSides.contains_rect(currRectSide);
};
const newRect = currRect.copy();
// Expand to the left / top.
if (before.length) {
if (freeRectsContainCurrRectSide(before)) {
const expandStartTo = before.reduce((currSize, rect) => {
return Math.max(currSize, rect[xpndPos1]);
}, before[0][xpndPos1]);
newRect[xpndPos2] += newRect[xpndPos1] - expandStartTo;
newRect[xpndPos1] = expandStartTo;
}
}
// Expand to the right / bottom.
if (after.length) {
if (freeRectsContainCurrRectSide(after)) {
const expandEndTo = after.reduce((currSize, rect) => {
return Math.min(currSize, rect[xpndPos2]);
}, after[0][xpndPos2]);
newRect[xpndPos2] = expandEndTo;
}
}
if (!orientation) {
// if orientation is null, we expanded vertically. Now we want
// to expand horizontally as well.
rectList = [...rectList];
const currRectIdx = rectList.findIndex(r => r.equal(currRect));
rectList.splice(currRectIdx, 1);
rectList.push(newRect);
return newRect.union(
this.getBestFreeRect(rectList, {
currRect: newRect,
orientation: Orientation.H,
monitorNr: monitor
}));
} else {
return newRect;
}
// No currRect was passed, so we just choose the single biggest free rect
// and expand it using this function. This is a naive approach and doesn't
// guarantee that we get the best combination of free screen rects... but
// it should be good enough.
} else {
const biggestSingle = freeRects.reduce((currBiggest, rect) => {
return currBiggest.area >= rect.area ? currBiggest : rect;
});
rectList.push(biggestSingle);
return this.getBestFreeRect(rectList, { currRect: biggestSingle });
}
}
/**
* Gets the nearest Meta.Window in the direction of `dir`.
*
* @param {Meta.Windows} currWindow the Meta.Window that the search starts
* from.
* @param {Meta.Windows[]} windows an array of the available Meta.Windows.
* It may contain the current window itself. The windows shouldn't
* overlap each other.
* @param {Direction} dir the direction that is look into.
* @param {boolean} [wrap=true] whether we wrap around,
* if there is no Meta.Window in the direction of `dir`.
* @returns {Meta.Window|null} the nearest Meta.Window.
*/
static getNearestWindow(currWindow, windows, dir, wrap = true) {
const getRect = w => w.tiledRect ?? new Rect(w.get_frame_rect());
const rects = windows.map(w => getRect(w));
const nearestRect = getRect(currWindow).getNeighbor(dir, rects, wrap);
if (!nearestRect)
return null;
return windows.find(w => getRect(w).equal(nearestRect));
}
/**
* Gets the rectangle for special positions adapted to the surrounding
* rectangles. The position is determined by `shortcut` but this function
* isn't limited to just keyboard shortcuts. This is also used when
* dnd-ing a window.
*
* Examples: Shortcuts.LEFT gets the left-most rectangle with the height
* of the workArea. Shortcuts.BOTTOM_LEFT gets the rectangle touching the
* bottom left screen corner etc... If there is no other rect to adapt to
* we default to half the workArea.
*
* @param {Shortcuts} shortcut the side / quarter to get the tile rect for.
* @param {Rect} workArea the workArea.
* @param {number} [monitor=null] the monitor number we want to get the
* rect for. This may not always be the current monitor. It is only
* used to implement the 'grace period' to enable quickly tiling a
* window using the screen edges even if there is another monitor
* at that edge.
* @returns a Rect.
*/
static getTileFor(shortcut, workArea, monitor = null) {
// Don't try to adapt a tile rect
if (Settings.getBoolean(Settings.DISABLE_TILE_GROUPS))
return this.getDefaultTileFor(shortcut, workArea);
const topTileGroup = this.getTopTileGroup({ skipTopWindow: true, monitor });
// getTileFor is used to get the adaptive tiles for dnd & tiling keyboard
// shortcuts. That's why the top most window needs to be ignored when
// calculating the new tile rect. The top most window is already ignored
// for dnd in the getTopTileGroup() call. While the top most window will
// be ignored for the active search in getTopTileGroup, it may still be
// part of the returned array if it's part of another high-stackeing
// window's tile group.
const idx = topTileGroup.indexOf(global.display.focus_window);
idx !== -1 && topTileGroup.splice(idx, 1);
const favLayout = Util.getFavoriteLayout(monitor);
const useFavLayout = favLayout.length && Settings.getBoolean(Settings.ADAPT_EDGE_TILING_TO_FAVORITE_LAYOUT);
const twRects = useFavLayout && favLayout || topTileGroup.map(w => w.tiledRect);
if (!twRects.length)
return this.getDefaultTileFor(shortcut, workArea);
// Return the adapted rect only if it doesn't overlap an existing tile.
// Ignore an overlap, if a fav layout is used since we always prefer the
// user set layout in that case.
const getTile = rect => {
if (useFavLayout)
return rect;
const overlapsTiles = twRects.some(r => r.overlap(rect));
return overlapsTiles ? this.getDefaultTileFor(shortcut, workArea) : rect;
};
const screenRects = twRects.concat(workArea.minus(twRects));
switch (shortcut) {
case Shortcuts.MAXIMIZE: {
return workArea.copy();
} case Shortcuts.LEFT: {
const left = screenRects.find(r => r.x === workArea.x && r.width !== workArea.width);
const { width } = left ?? workArea.getUnitAt(0, workArea.width / 2, Orientation.V);
const result = new Rect(workArea.x, workArea.y, width, workArea.height);
return getTile(result);
} case Shortcuts.RIGHT: {
const right = screenRects.find(r => r.x2 === workArea.x2 && r.width !== workArea.width);
const { width } = right ?? workArea.getUnitAt(1, workArea.width / 2, Orientation.V);
const result = new Rect(workArea.x2 - width, workArea.y, width, workArea.height);
return getTile(result);
} case Shortcuts.TOP: {
const top = screenRects.find(r => r.y === workArea.y && r.height !== workArea.height);
const { height } = top ?? workArea.getUnitAt(0, workArea.height / 2, Orientation.H);
const result = new Rect(workArea.x, workArea.y, workArea.width, height);
return getTile(result);
} case Shortcuts.BOTTOM: {
const bottom = screenRects.find(r => r.y2 === workArea.y2 && r.height !== workArea.height);
const { height } = bottom ?? workArea.getUnitAt(1, workArea.height / 2, Orientation.H);
const result = new Rect(workArea.x, workArea.y2 - height, workArea.width, height);
return getTile(result);
} case Shortcuts.TOP_LEFT: {
const left = screenRects.find(r => r.x === workArea.x && r.width !== workArea.width);
const { width } = left ?? workArea.getUnitAt(0, workArea.width / 2, Orientation.V);
const top = screenRects.find(r => r.y === workArea.y && r.height !== workArea.height);
const { height } = top ?? workArea.getUnitAt(0, workArea.height / 2, Orientation.H);
const result = new Rect(workArea.x, workArea.y, width, height);
return getTile(result);
} case Shortcuts.TOP_RIGHT: {
const right = screenRects.find(r => r.x2 === workArea.x2 && r.width !== workArea.width);
const { width } = right ?? workArea.getUnitAt(1, workArea.width / 2, Orientation.V);
const top = screenRects.find(r => r.y === workArea.y && r.height !== workArea.height);
const { height } = top ?? workArea.getUnitAt(0, workArea.height / 2, Orientation.H);
const result = new Rect(workArea.x2 - width, workArea.y, width, height);
return getTile(result);
} case Shortcuts.BOTTOM_LEFT: {
const left = screenRects.find(r => r.x === workArea.x && r.width !== workArea.width);
const { width } = left ?? workArea.getUnitAt(0, workArea.width / 2, Orientation.V);
const bottom = screenRects.find(r => r.y2 === workArea.y2 && r.height !== workArea.height);
const { height } = bottom ?? workArea.getUnitAt(1, workArea.height / 2, Orientation.H);
const result = new Rect(workArea.x, workArea.y2 - height, width, height);
return getTile(result);
} case Shortcuts.BOTTOM_RIGHT: {
const right = screenRects.find(r => r.x2 === workArea.x2 && r.width !== workArea.width);
const { width } = right ?? workArea.getUnitAt(1, workArea.width / 2, Orientation.V);
const bottom = screenRects.find(r => r.y2 === workArea.y2 && r.height !== workArea.height);
const { height } = bottom ?? workArea.getUnitAt(1, workArea.height / 2, Orientation.H);
const result = new Rect(workArea.x2 - width, workArea.y2 - height, width, height);
return getTile(result);
}
}
}
/**
* @param {Shortcuts} shortcut determines, which half/quarter to get the tile for
* @param {Rect} workArea
* @returns
*/
static getDefaultTileFor(shortcut, workArea) {
switch (shortcut) {
case Shortcuts.MAXIMIZE:
return workArea.copy();
case Shortcuts.LEFT:
case Shortcuts.LEFT_IGNORE_TA:
return workArea.getUnitAt(0, workArea.width / 2, Orientation.V);
case Shortcuts.RIGHT:
case Shortcuts.RIGHT_IGNORE_TA:
return workArea.getUnitAt(1, workArea.width / 2, Orientation.V);
case Shortcuts.TOP:
case Shortcuts.TOP_IGNORE_TA:
return workArea.getUnitAt(0, workArea.height / 2, Orientation.H);
case Shortcuts.BOTTOM:
case Shortcuts.BOTTOM_IGNORE_TA:
return workArea.getUnitAt(1, workArea.height / 2, Orientation.H);
case Shortcuts.TOP_LEFT:
case Shortcuts.TOP_LEFT_IGNORE_TA:
return workArea.getUnitAt(0, workArea.width / 2, Orientation.V).getUnitAt(0, workArea.height / 2, Orientation.H);
case Shortcuts.TOP_RIGHT:
case Shortcuts.TOP_RIGHT_IGNORE_TA:
return workArea.getUnitAt(1, workArea.width / 2, Orientation.V).getUnitAt(0, workArea.height / 2, Orientation.H);
case Shortcuts.BOTTOM_LEFT:
case Shortcuts.BOTTOM_LEFT_IGNORE_TA:
return workArea.getUnitAt(0, workArea.width / 2, Orientation.V).getUnitAt(1, workArea.height / 2, Orientation.H);
case Shortcuts.BOTTOM_RIGHT:
case Shortcuts.BOTTOM_RIGHT_IGNORE_TA:
return workArea.getUnitAt(1, workArea.width / 2, Orientation.V).getUnitAt(1, workArea.height / 2, Orientation.H);
}
}
/**
* Opens the Tiling Popup, if there is unambiguous free screen space,
* and offer to tile an open window to that spot.
*/
static async tryOpeningTilingPopup() {
if (!Settings.getBoolean(Settings.ENABLE_TILING_POPUP))
return;
const allWs = Settings.getBoolean(Settings.POPUP_ALL_WORKSPACES);
const openWindows = this.getWindows(allWs);
const topTileGroup = this.getTopTileGroup();
topTileGroup.forEach(w => openWindows.splice(openWindows.indexOf(w), 1));
if (!openWindows.length)
return;
const tRects = topTileGroup.map(w => w.tiledRect);
const monitor = topTileGroup[0]?.get_monitor(); // for the grace period
const freeSpace = this.getFreeScreen(tRects, monitor);
if (!freeSpace)
return;
const TilingPopup = await import('./tilingPopup.js');
const popup = new TilingPopup.TilingSwitcherPopup(openWindows, freeSpace);
if (!popup.show(topTileGroup))
popup.destroy();
}
/**
* Tiles or untiles a window based on its current tiling state.
*
* @param {Meta.Window} window a Meta.Window.
* @param {Rect} rect the Rect the `window` tiles to or untiles from.
*/
static toggleTiling(window, rect, params = {}) {
const workArea = window.get_work_area_current_monitor();
const equalsWA = rect.equal(workArea);
const equalsTile = window.tiledRect && rect.equal(window.tiledRect);
if (window.isTiled && equalsTile || this.isMaximized(window) && equalsWA)
this.untile(window, params);
else
this.tile(window, rect, params);
}
/**
* Tries to open an app on a tiling state (in a very dumb way...).
*
* @param {Shell.App} app the Shell.App to open and tile.
* @param {Rect} rect the Rect to tile to.
* @param {boolean} [openTilingPopup=false] allow the Tiling Popup to
* appear, if there is free screen space after the `app` was tiled.
*/
static openAppTiled(app, rect, openTilingPopup = false) {
if (!app?.can_open_new_window())
return;
let createId = global.display.connect('window-created', (src, window) => {
const wActor = window.get_compositor_private();
let firstFrameId = wActor?.connect('first-frame', () => {
wActor.disconnect(firstFrameId);
firstFrameId = 0;
const winTracker = Shell.WindowTracker.get_default();
const openedWindowApp = winTracker.get_window_app(window);
// Check, if the created window is from the app and if it allows
// to be moved and resized because, for example, Steam uses a
// WindowType.Normal window for their loading screen, which we
// don't want to trigger the tiling for.
if (createId && openedWindowApp && openedWindowApp === app &&
(window.allows_resize() && window.allows_move() || window.get_maximized())
) {
global.display.disconnect(createId);
createId = 0;
this.tile(window, rect, { openTilingPopup, skipAnim: true });
}
});
// Don't immediately disconnect the signal in case the launched
// window doesn't match the original app. It may be a loading screen
// or the user started an app in between etc... but in case the checks/
// signals above fail disconnect the signals after 1 min at the latest
this._openAppTiledTimerId && GLib.Source.remove(this._openAppTiledTimerId);
this._openAppTiledTimerId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 60000, () => {
createId && global.display.disconnect(createId);
createId = 0;
firstFrameId && wActor.disconnect(firstFrameId);
firstFrameId = 0;
this._openAppTiledTimerId = null;
return GLib.SOURCE_REMOVE;
});
});
app.open_new_window(-1);
}
/**
* Gets the top windows, which are supposed to be in a tile group. That
* means windows, which are tiled, and don't overlap each other.
*/
static _getWindowsForBuildingTileGroup(monitor = null) {
const openWindows = this.getWindows();
if (!openWindows.length)
return [];
const ignoredWindows = [];
const result = [];
const mon = monitor ??
global.display.focus_window?.get_monitor() ??
openWindows[0].get_monitor();
for (const window of openWindows) {
if (window.get_monitor() !== mon)
continue;
if (window.is_above() && !window.isTiled)
continue;
if (window.isTiled) {
// Window was already checked as part of another's tileGroup.
if (ignoredWindows.includes(window) || result.includes(window))
continue;
// Check for the other windows in the tile group as well regardless
// of the 'raise tile group' setting so that once the setting is
// enabled the tile groups are already set properly.
const tileGroup = this.getTileGroupFor(window);
// This means `window` is the window that was just tiled and
// thus has no tileGroup set at this point yet.
if (!tileGroup.length) {
result.push(window);
continue;
}
const tileGroupOverlaps = tileGroup.some(w =>
result.some(r => r.tiledRect.overlap(w.tiledRect)) ||
ignoredWindows.some(r => (r.tiledRect ?? new Rect(r.get_frame_rect())).overlap(w.tiledRect)));
tileGroupOverlaps
? tileGroup.forEach(w => ignoredWindows.push(w))
: tileGroup.forEach(w => result.push(w));
} else {
// The window is maximized, so all windows below it can't belong
// to this group anymore.
if (this.isMaximized(window))
break;
ignoredWindows.push(window);
}
}
return result;
}
/**
* Gets the top most non-overlapped/ing tiled windows ignoring
* the stacking order and tile groups.
*
* @param {{boolean, number}} param1
*/
static _getTopTiledWindows({ skipTopWindow = false, monitor = null } = {}) {
const openWindows = this.getWindows();
if (!openWindows.length)
return [];
if (skipTopWindow) {
// the focused window isn't necessarily the top window due to always
// on top windows.
const idx = openWindows.indexOf(global.display.focus_window);
idx !== -1 && openWindows.splice(idx, 1);
}
const topTiledWindows = [];
const ignoredWindows = [];
const mon = monitor ??
global.display.focus_window?.get_monitor() ??
openWindows[0].get_monitor();
for (const window of openWindows) {
if (window.get_monitor() !== mon)
continue;
if (window.is_above() && !window.isTiled)
continue;
if (window.isTiled) {
const wRect = window.tiledRect;
// If a ignored window in a higher stack order overlaps the
// currently tested tiled window, the currently tested tiled
// window isn't part of the top tile group.
const overlapsIgnoredWindow = ignoredWindows.some(w => {
const rect = w.tiledRect ?? new Rect(w.get_frame_rect());
return rect.overlap(wRect);
});
// Same applies for already grouped windows
const overlapsTopTiledWindows = topTiledWindows.some(w => w.tiledRect.overlap(wRect));
overlapsIgnoredWindow || overlapsTopTiledWindows
? ignoredWindows.push(window)
: topTiledWindows.push(window);
} else {
// The window is maximized, so all windows below it can't belong
// to this group anymore.
if (this.isMaximized(window))
break;
ignoredWindows.push(window);
}
}
return topTiledWindows;
}
/**
* Blocks all tiling signals for a window.
*
* @param {Meta.Window} window
*/
static _blockTilingSignalsFor(window) {
const signals = this._signals.getSignalsFor(window.get_id());
const blockedSignals = [TilingSignals.RAISE, TilingSignals.WS_CHANGED, TilingSignals.UNMANAGING];
blockedSignals.forEach(s => {
const id = signals.get(s);
id && window.block_signal_handler(id);
});
}
/**
* Unblocks all tiling signals for a window.
* Should only be called after _blockTilingSignalsFor().
*
* @param {Meta.Window} window
*/
static _unblockTilingSignalsFor(window) {
const signals = this._signals.getSignalsFor(window.get_id());
const blockedSignals = [TilingSignals.RAISE, TilingSignals.WS_CHANGED, TilingSignals.UNMANAGING];
blockedSignals.forEach(s => {
const id = signals.get(s);
id && window.unblock_signal_handler(id);
});
}
/**
* Updates the signals after maximizing a window with gaps.
*
* @param {Meta.Window} window
*/
static _updateGappedMaxWindowSignals(window) {
const wId = window.get_id();
const signals = this._signals.getSignalsFor(wId);
// Refresh 'unmanaging' signal
const unmanagingSignal = signals.get(TilingSignals.UNMANAGING);
unmanagingSignal && window.disconnect(unmanagingSignal);
const umId = window.connect('unmanaging', w => {
this.clearTilingProps(window.get_id());
this._unmanagingWindows.push(w.get_stable_sequence());
});
signals.set(TilingSignals.UNMANAGING, umId);
// Refresh 'workspace-changed' signal
const wsId = window.connect('workspace-changed', () => this._onWindowWorkspaceChanged(window));
this._signals.getSignalsFor(wId).set(TilingSignals.WS_CHANGED, wsId);
}
/**
* @returns {Meta.Window[]} an array of *all* windows
* (and not just the ones relevant to altTab)
*/
static _getAllWindows() {
return global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null);
}
/**
* Gets the window matching a window id
*
* @param {number} id
* @returns {Meta.Window}
*/
static _getWindow(id) {
return this._getAllWindows().find(w => w.get_id() === id);
}
/**
* A window's workspace-changed signal is used to untile it when the user
* changes its workspace. However, dynamic workspaces *may* also trigger a
* ws-changed signal. So listen to the workspace-added/removed signals and
* 'ignore' the next ws-changed signal. A ws addition/removal doesn't guarantuee
* a ws-changed signal (e. g. the workspace is at the end), so reset after
* a short timer.
*/
static _onWorkspaceAdded() {
this._ignoreWsChange = true;
this._wsAddedTimer && GLib.Source.remove(this._wsAddedTimer);
this._wsAddedTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => {
this._ignoreWsChange = false;
this._wsAddedTimer = null;
return GLib.SOURCE_REMOVE;
});
}
/**
* A window's workspace-changed signal is used to untile it when the user
* changes its workspace. However, dynamic workspaces *may* also trigger a
* ws-changed signal. So listen to the workspace-added/removed signals and
* 'ignore' the next ws-changed signal. A ws addition/removal doesn't guarantuee
* a ws-changed signal (e. g. the workspace is at the end), so reset after
* a short timer.
*/
static _onWorkspaceRemoved() {
this._ignoreWsChange = true;
this._wsRemovedTimer && GLib.Source.remove(this._wsRemovedTimer);
this._wsRemovedTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => {
this._ignoreWsChange = false;
this._wsRemovedTimer = null;
return GLib.SOURCE_REMOVE;
});
}
/**
* This is only called for tiled and maximized (with gaps) windows.
* Untile tiled windows. Re-tile maximized windows to fit the whole workArea
* since a monitor change will also trigger a workspace-change signal.
* Previously, we tried to adapt the tiled window's size to the new monitor
* but that is probably too unpredictable. First, it may introduce rounding
* errors when moving multiple windows of the same tileGroup and second (and
* more importantly) the behavior with regards to tileGroups isn't clear...
* Should the entire tileGroup move, if 1 tiled window is moved? If not,
* there should probably be a way to just detach 1 window from a group. What
* happens on the new monitor, if 1 window is moved? Should it create a new
* tileGroup? Should it try to integrate into existing tileGroups on that
* monitor etc... there are too many open questions. Instead just untile
* and leave it up to the user to re-tile a window.
*
* @param {Meta.Window} window
*/
static _onWindowWorkspaceChanged(window) {
// Closing a window triggers a ws-changed signal, which may lead to a
// crash, if we try to operate on it any further. So we listen to the
// 'unmanaging'-signal to see, if there is a 'true workspace change'
// or whether the window was just closed
if (this._unmanagingWindows.includes(window.get_stable_sequence()))
return;
if (this._ignoreWsChange)
return;
if (this.isMaximized(window)) {
const wA = window.get_work_area_for_monitor(window.get_monitor());
const workArea = new Rect(wA);
if (workArea.equal(window.tiledRect))
return;
this.tile(window, workArea, { openTilingPopup: false, skipAnim: true });
} else if (window.isTiled) {
this.untile(window, { restoreFullPos: false, clampToWorkspace: true, skipAnim: Main.overview.visible });
}
}
}
/**
* This is instanced by the 'TilingWindowManager'. It implements the tiling
* signals and tracks the signal( id)s, which are relevant for tiling:
* Raise: for group raising.
* Ws-changed: for untiling a tiled window after its ws changed.
* Unmanaging: to remove unmanaging tiled windows from the other tileGroups.
*/
const TilingSignals = GObject.registerClass({
Signals: {
'window-tiled': { param_types: [Meta.Window.$gtype] },
'window-untiled': { param_types: [Meta.Window.$gtype] }
}
}, class TilingSignals extends Clutter.Actor {
// Relevant 'signal types' (sorta used as an enum / key for the signal map).
// Tiled windows use all 3 signals; maximized-with-gaps windows only use the
// workspace-changed and unmanaging signal.
static RAISE = 'RAISE';
static WS_CHANGED = 'WS_CHANGED';
static UNMANAGING = 'UNMANAGING';
_init() {
super._init();
// { windowId1: { RAISE: signalId1, WS_CHANGED: signalId2, UNMANAGING: signalId3 }, ... }
this._ids = new Map();
}
destroy() {
// Disconnect remaining signals
const allWindows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null);
this._ids.forEach((signals, windowId) => {
const window = allWindows.find(w => w.get_id() === windowId);
window && signals.forEach(s => s && window.disconnect(s));
});
super.destroy();
}
/**
* Gets the signal ids for the raise, ws-changed and unmanaging signals
* for a specific window
*
* @param {number} windowId Meta.Window's id
* @returns {Map<string, number>} the tiling signal ids for the window (id)
* with a 'signal type' as the keys
*/
getSignalsFor(windowId) {
let ret = this._ids.get(windowId);
if (!ret) {
ret = new Map();
this._ids.set(windowId, ret);
}
return ret;
}
});