__  __    __   __  _____      _            _          _____ _          _ _ 
 |  \/  |   \ \ / / |  __ \    (_)          | |        / ____| |        | | |
 | \  / |_ __\ V /  | |__) | __ ___   ____ _| |_ ___  | (___ | |__   ___| | |
 | |\/| | '__|> <   |  ___/ '__| \ \ / / _` | __/ _ \  \___ \| '_ \ / _ \ | |
 | |  | | |_ / . \  | |   | |  | |\ V / (_| | ||  __/  ____) | | | |  __/ | |
 |_|  |_|_(_)_/ \_\ |_|   |_|  |_| \_/ \__,_|\__\___| |_____/|_| |_|\___V 2.1
 if you need WebShell for Seo everyday contact me on Telegram
 Telegram Address : @jackleet
        
        
For_More_Tools: Telegram: @jackleet | Bulk Smtp support mail sender | Business Mail Collector | Mail Bouncer All Mail | Bulk Office Mail Validator | Html Letter private



Upload:

Command:

[email protected]: ~ $
import { GLib, Meta, Mtk, Shell } from '../dependencies/gi.js';
import { Main } from '../dependencies/shell.js';
import { getWindows } from '../dependencies/unexported/altTab.js';

import { Orientation, Settings } 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();

        /**
         * @type {Map<number, number[]>} - {
         *      windowId1: [windowIdX, windowIdY, ...],
         *      windowId2: [...],
         * }
         */
        this._tileGroups = new Map();

        /**
         * {windowId: {isTiled: boolean, tiledRect: {}, untiledRect: {}}}
         */
        this._tileStates = new Map();

        const assertExistenceFor = window => {
            window.assertExistence = () => {};

            window.connectObject(
                'unmanaging',
                () => {
                    window.assertExistence = () => {
                        throw new Error(
                            'Trying to operate on an unmanaging window!'
                        );
                    };
                },
                this
            );
        };

        global.display.list_all_windows().forEach(w => assertExistenceFor(w));
        global.display.connectObject(
            'window-created',
            (_, window) => assertExistenceFor(window),
            this
        );

        global.workspace_manager.connectObject(
            'workspace-added',
            this._onWorkspaceAdded.bind(this),
            this
        );
        global.workspace_manager.connectObject(
            'workspace-removed',
            this._onWorkspaceRemoved.bind(this),
            this
        );
    }

    static destroy() {
        this._signals.destroy();
        this._signals = null;

        global.workspace_manager.disconnectObject(this);
        global.display.disconnectObject(this);

        global.display.list_all_windows().forEach(w => {
            w.disconnectObject(this);

            delete w.assertExistence;
        });

        this._tileGroups.clear();
        this._tileStates.clear();

        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;
        }
    }

    /**
     * Gets windows, which can be tiled
     *
     * @param {boolean} [allWorkspaces=false] determines whether we only want
     *      the windows from the current workspace.
     * @returns {Meta.Window[]} 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 {Mtk.Rectangle|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 {object} [param] the Rect the `window` will be tiled to.
     * @param {boolean} [param.openTilingPopup=true] decides, if we open a Tiling
     *      Popup after the window is tiled and there is unambiguous free
     *      screen space.
     * @param {boolean} [param.ignoreTA=false]
     * @param {number} [param.monitorNr=null]
     * @param {boolean} [param.skipAnim=false] decides, if we skip the tile animation.
     * @param {boolean} [param.tileGroup=null] forces the creation of this tile group.
     * @param {boolean} [param.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 wasTiled = window.isTiled;
        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('maximize-with-gap')) {
            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('enable-tile-animations') &&
            wActor &&
            !wasTiled &&
            !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);
            this.saveTileState(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('disable-tile-groups') || ignoreTA) {
                this.updateTileGroup([window]);
                this.saveTileState(window);
                return;
            }

            // Setup the (new) tileGroup to raise tiled windows as a group
            const topTileGroup = this._getWindowsForBuildingTileGroup(monitor);
            this.updateTileGroup(topTileGroup);
            this.saveTileState(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 {object} [params]
     * @param {boolean} [params.restoreFullPos=true] decides, if we restore the
     *      pre-tile position or whether the size while keeping the titlebar
     *      at the relative same position.
     * @param {boolean} [params.skipAnim=false] decides, if we skip the until animation.
     * @param {boolean} [params.clampToWorkspace=false]
     */
    static untile(window, { restoreFullPos = true, 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('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());

            window.move_resize_frame(userOp, currWindowFrame.x, currWindowFrame.y, oldRect.width, oldRect.height);
        }

        this.clearTilingProps(window.get_id());
        window.isTiled = false;
        window.tiledRect = null;
        window.untiledRect = null;

        this.deleteTilingState(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);
    }

    static getTileStates() {
        return this._tileStates;
    }

    /**
     * @param {Map<number, object>} states -
     */
    static setTileStates(states) {
        this._tileStates = states;
    }

    /**
     * @returns {Map<number,number[]>}
     */
    static getTileGroups() {
        return this._tileGroups;
    }

    /**
     * @param {Map<number, number[]>} tileGroups
     */
    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.Window[]} 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', () => {
                this.clearTilingProps(windowId);
            });
            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('enable-raise-tile-group')) {
                    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 {object} [params]
     * @param {boolean} [params.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} [params.monitor=null] get the group for the monitor number.
     *
     * @returns {Meta.Window[]} 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('enable-raise-tile-group') ||
            Settings.getBoolean('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 {object} [params]
     * @param {Rect} [params.currRect=null] a Rect, which may be expanded.
     * @param {Orientation} [params.orientation=null] The orientation we want to expand
     *      `currRect` into. If `null`, expand in both orientations.
     * @param {number} [params.monitorNr=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.Window} currWindow the Meta.Window that the search starts
     *      from.
     * @param {Meta.Window[]} 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: 'tile-left-half' gets the left-most rectangle with the height
     * of the workArea. 'tile-bottomleft-quarter' 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 {string} 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('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('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 'tile-maximize': {
                return workArea.copy();
            } case 'tile-left-half': {
                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 'tile-right-half': {
                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 'tile-top-half': {
                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 'tile-bottom-half': {
                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 'tile-topleft-quarter': {
                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 'tile-topright-quarter': {
                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 'tile-bottomleft-quarter': {
                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 'tile-bottomright-quarter': {
                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 {string} shortcut determines, which half/quarter to get the tile for
     * @param {Rect} workArea
     * @returns
     */
    static getDefaultTileFor(shortcut, workArea) {
        switch (shortcut) {
            case 'tile-maximize':
                return workArea.copy();
            case 'tile-left-half':
            case 'tile-left-half-ignore-ta':
                return workArea.getUnitAt(0, workArea.width / 2, Orientation.V);
            case 'tile-right-half':
            case 'tile-right-half-ignore-ta':
                return workArea.getUnitAt(1, workArea.width / 2, Orientation.V);
            case 'tile-top-half':
            case 'tile-top-half-ignore-ta':
                return workArea.getUnitAt(0, workArea.height / 2, Orientation.H);
            case 'tile-bottom-half':
            case 'tile-bottom-half-ignore-ta':
                return workArea.getUnitAt(1, workArea.height / 2, Orientation.H);
            case 'tile-topleft-quarter':
            case 'tile-topleft-quarter-ignore-ta':
                return workArea.getUnitAt(0, workArea.width / 2, Orientation.V).getUnitAt(0, workArea.height / 2, Orientation.H);
            case 'tile-topright-quarter':
            case 'tile-topright-quarter-ignore-ta':
                return workArea.getUnitAt(1, workArea.width / 2, Orientation.V).getUnitAt(0, workArea.height / 2, Orientation.H);
            case 'tile-bottomleft-quarter':
            case 'tile-bottomleft-quarter-ignore-ta':
                return workArea.getUnitAt(0, workArea.width / 2, Orientation.V).getUnitAt(1, workArea.height / 2, Orientation.H);
            case 'tile-bottomright-quarter':
            case 'tile-bottomright-quarter-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('enable-tiling-popup'))
            return;

        const allWs = Settings.getBoolean('tiling-popup-all-workspace');
        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);
    }

    static saveTileState(window) {
        const windowState = this._tileStates.get(window.get_id());
        const rectToJsObject = rect => {
            return rect
                ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
                : undefined;
        };

        if (windowState) {
            windowState.isTiled = window.isTiled;
            windowState.tiledRect = rectToJsObject(window.tiledRect);
            windowState.untiledRect = rectToJsObject(window.untiledRect);
        } else {
            this._tileStates.set(
                window.get_id(),
                {
                    isTiled: window.isTiled,
                    tiledRect: rectToJsObject(window.tiledRect),
                    untiledRect: rectToJsObject(window.untiledRect)
                }
            );
        }
    }

    static deleteTilingState(window) {
        this._tileStates.delete(window.get_id());
    }

    /**
     * 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 {object} [param]
     * @param {boolean} [param.skipTopWindow=false]
     * @param {number} [param.monitor=null]
     */
    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', () => {
            this.clearTilingProps(window.get_id());
        });
        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.
        try {
            window.assertExistence();
        } catch {
            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.
 */
class TilingSignals {
    // 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';

    constructor() {
        // { 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));
        });
    }

    /**
     * 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;
    }
};

Filemanager

Name Type Size Permission Actions
altTab.js File 16.9 KB 0644
focusHint.js File 26.13 KB 0644
keybindingHandler.js File 23.24 KB 0644
layoutsManager.js File 20.88 KB 0644
moveHandler.js File 38.3 KB 0644
resizeHandler.js File 23.56 KB 0644
tileEditingMode.js File 26.93 KB 0644
tilingPopup.js File 13.64 KB 0644
tilingWindowManager.js File 60.7 KB 0644
utility.js File 26.39 KB 0644
Filemanager