%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /usr/share/gnome-shell/extensions/tiling-assistant@ubuntu.com/src/extension/
Upload File :
Create Path :
Current File : //usr/share/gnome-shell/extensions/tiling-assistant@ubuntu.com/src/extension/tileEditingMode.js

import { Clutter, GObject, Meta, St } from '../dependencies/gi.js';
import { _, Main } from '../dependencies/shell.js';

import { Direction, Orientation, Settings } from '../common.js';
import { Rect, Util } from './utility.js';
import { TilingWindowManager as Twm } from './tilingWindowManager.js';

const SCALE_SIZE = 100;
const Modes = {
    DEFAULT: 1,
    SWAP: 2,
    RESIZE: 4,
    MOVE: 8,
    CLOSE: 16
};

/**
 * Classes for the 'Tile Editing Mode'. A mode to manage your tiled windows
 * with your keyboard (and only the keyboard). The Tile Editor gets instanced
 * as soon as the keyboard shortcut is activated. The Handler classes are
 * basically modes / states for the Tile Editor each with a 'on key press' and
 * 'on key released' function.
 */

export const TileEditor = GObject.registerClass(
class TileEditingMode extends St.Widget {
    _init() {
        super._init({ reactive: true });

        this._haveModal = false;
        // The windows managed by the Tile Editor, that means the tiled windows
        // that aren't overlapped by other windows; in other words: the top tile Group
        this._windows = [];
        // Indicate the active selection by the user. Added to `this`.
        this._selectIndicator = null;
        this._mode = Modes.DEFAULT;
        // Handler of keyboard events depending on the mode.
        this._keyHandler = null;

        Main.uiGroup.add_child(this);

        this.connect('key-press-event', (__, event) =>
            this._onKeyPressEvent(event));
    }

    open() {
        this._windows = Twm.getTopTileGroup();

        const grab = Main.pushModal(this);
        // We expect at least a keyboard grab here
        if ((grab.get_seat_state() & Clutter.GrabState.KEYBOARD) === 0) {
            Main.popModal(grab);
            return false;
        }

        this._grab = grab;
        this._haveModal = true;

        const openWindows = Twm.getWindows();
        if (!openWindows.length || !this._windows.length) {
            const msg = _("Can't enter 'Tile Editing Mode', if no tiled window is visible.");
            Main.notify('Tiling Assistant', msg);
            this.close();
            return;
        }

        this.monitor = this._windows[0].get_monitor();
        const display = global.display.get_monitor_geometry(this.monitor);
        this.set_position(display.x, display.y);
        this.set_size(display.width, display.height);

        // Enter initial state.
        this._mode = Modes.DEFAULT;
        this._keyHandler = new DefaultKeyHandler(this);

        // The windows may not be at the foreground. They just weren't
        // overlapping other windows. So raise the entire tile group.
        this._windows.forEach(w => {
            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();
        });

        // Create the active selection indicator.
        const window = this._windows[0];
        const params = { style_class: 'tile-preview' };
        this._selectIndicator = new Indicator(window.tiledRect, this.monitor, params);
        this._selectIndicator.focus(window.tiledRect, window);
        this.add_child(this._selectIndicator);
    }

    close() {
        if (this._haveModal) {
            Main.popModal(this._grab);
            this._haveModal = false;
        }

        this._windows = [];
        this._keyHandler = null;

        // this._selectIndicator may be undefined, if Tile Editing Mode is
        // left as soon as it's entered (e. g. when there's no tile group).
        this._selectIndicator?.window?.activate(global.get_current_time());
        this._selectIndicator?.ease({
            x: this._selectIndicator.x + SCALE_SIZE / 2,
            y: this._selectIndicator.y + SCALE_SIZE / 2,
            width: this._selectIndicator.width - SCALE_SIZE,
            height: this._selectIndicator.height - SCALE_SIZE,
            opacity: 0,
            duration: 150,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this.destroy()
        }) ?? this.destroy();
    }

    vfunc_button_press_event() {
        this._keyHandler.prepareLeave();
        this.close();
    }

    async _onKeyPressEvent(keyEvent) {
        const mods = keyEvent.get_state();
        let newMode;

        // Swap windows
        if (mods & Clutter.ModifierType.CONTROL_MASK)
            newMode = Modes.SWAP;
        // Move group to different workspace / monitor
        else if (mods & Clutter.ModifierType.SHIFT_MASK)
            newMode = Modes.MOVE;
        // Resize windows
        else if (mods & Clutter.ModifierType.MOD4_MASK)
            newMode = Modes.RESIZE;
        // Default keys
        else
            newMode = Modes.DEFAULT;

        // First switch mode, if a new mod is pressed.
        if (newMode !== this._mode)
            this._switchMode(newMode);

        // Handle the key press and get mode depending on that.
        newMode = await this._keyHandler.handleKeyPress(keyEvent);

        if (newMode && newMode !== this._mode)
            this._switchMode(newMode);
    }

    vfunc_key_release_event(keyEvent) {
        const newMode = this._keyHandler.handleKeyRelease(keyEvent);
        if (newMode && newMode !== this._mode)
            this._switchMode(newMode);
    }

    _switchMode(newMode) {
        if (!newMode)
            return;

        this._mode = newMode;
        this._keyHandler.prepareLeave();

        switch (newMode) {
            case Modes.DEFAULT:
                this._keyHandler = new DefaultKeyHandler(this);
                break;
            case Modes.SWAP:
                this._keyHandler = new SwapKeyHandler(this);
                break;
            case Modes.MOVE:
                this._keyHandler = new MoveKeyHandler(this);
                break;
            case Modes.RESIZE:
                this._keyHandler = new ResizeKeyHandler(this);
                break;
            case Modes.CLOSE:
                this.close();
        }
    }
});

/**
 * Indicate the user selection or other stuff.
 */
const Indicator = GObject.registerClass(class TileEditingModeIndicator extends St.Widget {
    /**
     * @param {string} widgetParams
     * @param {Rect} rect the final rect / pos of the indicator
     * @param {number} monitor
     */
    _init(rect, monitor, widgetParams = {}) {
        // Start from a scaled down position.
        super._init({
            ...widgetParams,
            x: rect.x + SCALE_SIZE / 2,
            y: rect.y + SCALE_SIZE / 2,
            width: rect.width - SCALE_SIZE,
            height: rect.height - SCALE_SIZE,
            opacity: 0
        });

        this.rect = null;
        this.window = null;
        this._monitor = monitor;
    }

    /**
     * Animate the indicator to a specific position.
     *
     * @param {Rect} rect the position the indicator will animate to.
     * @param {Meta.Window|null} window the window at `rect`'s position.
     */
    focus(rect, window = null) {
        const display = global.display.get_monitor_geometry(this._monitor);
        const activeWs = global.workspace_manager.get_active_workspace();
        const workArea = new Rect(activeWs.get_work_area_for_monitor(this._monitor));

        // Adjusted for window / screen gaps
        const { x, y, width, height } = rect.addGaps(workArea);

        this.ease({
            x: x - display.x,
            y: y - display.y,
            width,
            height,
            opacity: 255,
            duration: 150,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD
        });

        this.rect = rect;
        this.window = window;
    }
});

/**
 * Base class for other keyboard handlers and the default handler itself.
 *
 * @param {TileEditingMode} tileEditor
 */
const DefaultKeyHandler = class DefaultKeyHandler {
    constructor(tileEditor) {
        this._tileEditor = tileEditor;
    }

    /**
     * Automatically called when leaving a mode.
     */
    prepareLeave() {
    }

    /**
     * Automatically called on a keyEvent.
     *
     * @param {number} keyEvent
     * @returns {Modes} The mode to enter after the event was handled.
     */
    async handleKeyPress(keyEvent) {
        const keyVal = keyEvent.get_key_symbol();

        // [Directions] to move focus with WASD, hjkl or arrow keys
        const dir = Util.getDirection(keyVal);
        if (dir) {
            this._focusInDir(dir);

        // [E]xpand to fill the available space
        } else if (keyVal === Clutter.KEY_e || keyVal === Clutter.KEY_E) {
            const window = this._selectIndicator.window;
            if (!window)
                return Modes.DEFAULT;

            const tiledRect = this._windows.map(w => w.tiledRect);
            const tileRect = Twm.getBestFreeRect(tiledRect, { currRect: window.tiledRect });
            if (window.tiledRect.equal(tileRect))
                return Modes.DEFAULT;

            const workArea = window.get_work_area_current_monitor();
            const maximize = tileRect.equal(workArea);
            if (maximize && this._windows.length > 1)
                return Modes.DEFAULT;

            Twm.tile(window, tileRect, { openTilingPopup: false });

            if (maximize)
                return Modes.CLOSE;

            this._selectIndicator.focus(window.tiledRect, window);

        // [C]ycle through halves of the available space around the window
        } else if (keyVal === Clutter.KEY_c || keyVal === Clutter.KEY_C) {
            const window = this._selectIndicator.window;
            if (!window)
                return Modes.DEFAULT;

            const tiledRects = this._windows.map(w => w.tiledRect);
            const fullRect = Twm.getBestFreeRect(tiledRects, { currRect: window.tiledRect });
            const topHalf = fullRect.getUnitAt(0, fullRect.height / 2, Orientation.H);
            const rightHalf = fullRect.getUnitAt(1, fullRect.width / 2, Orientation.V);
            const bottomHalf = fullRect.getUnitAt(1, fullRect.height / 2, Orientation.H);
            const leftHalf = fullRect.getUnitAt(0, fullRect.width / 2, Orientation.V);
            const rects = [topHalf, rightHalf, bottomHalf, leftHalf];
            const currIdx = rects.findIndex(r => r.equal(window.tiledRect));
            const newIndex = (currIdx + 1) % 4;

            Twm.tile(window, rects[newIndex], { openTilingPopup: false });
            this._selectIndicator.focus(window.tiledRect, window);

        // [Q]uit a window
        } else if (keyVal === Clutter.KEY_q || keyVal === Clutter.KEY_Q) {
            const window = this._selectIndicator.window;
            if (!window)
                return Modes.DEFAULT;

            this._windows.splice(this._windows.indexOf(window), 1);
            window.delete(global.get_current_time());
            const newWindow = this._windows[0];
            if (!newWindow)
                return Modes.CLOSE;

            this._selectIndicator.focus(newWindow.tiledRect, newWindow);

        // [R]estore a window's size
        } else if (keyVal === Clutter.KEY_r || keyVal === Clutter.KEY_R) {
            const window = this._selectIndicator.window;
            if (!window)
                return Modes.DEFAULT;

            const selectedRect = window.tiledRect.copy();
            this._windows.splice(this._windows.indexOf(window), 1);
            Twm.untile(window);
            if (!this._windows.length)
                return Modes.CLOSE;

            // Re-raise tile group, so it isn't below the just-untiled window
            if (this._windows[0].raise_and_make_recent_on_workspace)
                this._windows[0].raise_and_make_recent_on_workspace(global.workspace_manager.get_active_workspace());
            else
                this._windows[0].raise_and_make_recent();
            this._selectIndicator.focus(selectedRect, null);

        // [Enter] / [Esc]ape Tile Editing Mode
        } else if (keyVal === Clutter.KEY_Escape || keyVal === Clutter.KEY_Return) {
            return Modes.CLOSE;

        // [Space] to activate the Tiling Popup
        } else if (keyVal === Clutter.KEY_space) {
            const allWs = Settings.getBoolean(Settings.POPUP_ALL_WORKSPACES);
            const openWindows = Twm.getWindows(allWs).filter(w => !this._windows.includes(w));
            const TilingPopup = await import('./tilingPopup.js');
            const tilingPopup = new TilingPopup.TilingSwitcherPopup(
                openWindows,
                this._selectIndicator.rect,
                false
            );

            if (!tilingPopup.show(this._windows)) {
                tilingPopup.destroy();
                return Modes.DEFAULT;
            }

            tilingPopup.connect('closed', (popup, canceled) => {
                if (canceled)
                    return;

                const { tiledWindow } = popup;
                const replaced = this._windows.findIndex(w => w.tiledRect.equal(tiledWindow.tiledRect));
                replaced !== -1 && this._windows.splice(replaced, 1);

                // Create the new tile group to allow 1 window to be part of multiple tile groups
                Twm.updateTileGroup([tiledWindow, ...this._windows]);

                this._windows.unshift(tiledWindow);
                this._selectIndicator.focus(tiledWindow.tiledRect, tiledWindow);
            });
        }

        return Modes.DEFAULT;
    }

    /**
     * Automatically called on a keyEvent.
     *
     * @param {number} keyEvent
     * @returns {Modes|undefined} The mode to enter after the event was handled.
     */
    handleKeyRelease() {
        return undefined;
    }

    /**
     * Move the the selection indicator towards direction of `dir`.
     *
     * @param {Direction} dir
     */
    _focusInDir(dir) {
        const activeWs = global.workspace_manager.get_active_workspace();
        const workArea = new Rect(activeWs.get_work_area_for_monitor(this._tileEditor.monitor));
        const tiledRects = this._windows.map(w => w.tiledRect);
        const screenRects = tiledRects.concat(workArea.minus(tiledRects));
        const nearestRect = this._selectIndicator.rect.getNeighbor(dir, screenRects);
        if (!nearestRect)
            return;

        const newWindow = this._windows.find(w => w.tiledRect.equal(nearestRect));
        this._selectIndicator.focus(newWindow?.tiledRect ?? nearestRect, newWindow);
    }

    get _windows() {
        return this._tileEditor._windows;
    }

    get _selectIndicator() {
        return this._tileEditor._selectIndicator;
    }
};

/**
 * Move the selected window to a different position. If there is a window at
 * the new position, the 2 windows will swap their positions.
 *
 * @param {TileEditingMode} tileEditor
 */
const SwapKeyHandler = class SwapKeyHandler extends DefaultKeyHandler {
    constructor(tileEditor) {
        super(tileEditor);

        // Create an 'anchor indicator' to indicate the window that will be swapped
        const color = this._selectIndicator.get_theme_node().get_background_color();
        const { red, green, blue, alpha } = color;
        this._anchorIndicator = new Indicator(this._selectIndicator.rect, tileEditor.monitor, {
            style: `background-color: rgba(${red}, ${green}, ${blue}, ${alpha / 255})`
        });
        this._anchorIndicator.focus(this._selectIndicator.rect, this._selectIndicator.window);
        this._tileEditor.add_child(this._anchorIndicator);
    }

    prepareLeave() {
        this._anchorIndicator.destroy();
    }

    handleKeyPress(keyEvent) {
        const direction = Util.getDirection(keyEvent.get_key_symbol());

        // [Directions] to choose a window to swap with WASD, hjkl or arrow keys
        if (direction)
            this._focusInDir(direction);

        // [Esc]ape Tile Editing Mode
        else if (keyEvent.get_key_symbol() === Clutter.KEY_Escape)
            return Modes.DEFAULT;

        return Modes.SWAP;
    }

    handleKeyRelease(keyEvent) {
        const keyVal = keyEvent.get_key_symbol();
        const ctrlKeys = [Clutter.KEY_Control_L, Clutter.KEY_Control_R];

        if (ctrlKeys.includes(keyVal)) {
            this._swap();
            return Modes.DEFAULT;
        }

        return Modes.SWAP;
    }

    _swap() {
        if (this._anchorIndicator.window)
        { Twm.tile(this._anchorIndicator.window, this._selectIndicator.rect, {
            openTilingPopup: false
        }); }

        if (this._selectIndicator.window)
        { Twm.tile(this._selectIndicator.window, this._anchorIndicator.rect, {
            openTilingPopup: false
        }); }

        this._selectIndicator.focus(this._selectIndicator.rect,
            this._anchorIndicator.window);
    }
};

/**
 * Move the tile group to a different workspace / monitor.
 *
 * @param {TileEditingMode} tileEditor
 */
const MoveKeyHandler = class MoveKeyHandler extends DefaultKeyHandler {
    handleKeyPress(keyEvent) {
        const direction = Util.getDirection(keyEvent.get_key_symbol());
        const moveWorkspace = keyEvent.get_state() & Clutter.ModifierType.MOD1_MASK;

        // [Directions] to move the tile group
        if (direction) {
            // To new workspace
            if (moveWorkspace) {
                let metaDir = Meta.MotionDirection.UP;
                if (direction === Direction.N)
                    metaDir = Meta.MotionDirection.UP;
                else if (direction === Direction.S)
                    metaDir = Meta.MotionDirection.DOWN;
                else if (direction === Direction.W)
                    metaDir = Meta.MotionDirection.LEFT;
                else if (direction === Direction.E)
                    metaDir = Meta.MotionDirection.RIGHT;

                const activeWs = global.workspace_manager.get_active_workspace();
                const newWs = activeWs.get_neighbor(metaDir);
                if (activeWs === newWs)
                    return Modes.MOVE;

                Twm.moveGroupToWorkspace(this._tileEditor._windows, newWs);

            // To new monitor
            } else {
                let metaDir = Meta.DisplayDirection.UP;
                if (direction === Direction.N)
                    metaDir = Meta.DisplayDirection.UP;
                else if (direction === Direction.S)
                    metaDir = Meta.DisplayDirection.DOWN;
                else if (direction === Direction.W)
                    metaDir = Meta.DisplayDirection.LEFT;
                else if (direction === Direction.E)
                    metaDir = Meta.DisplayDirection.RIGHT;

                // get_current_monitor isn't accurate for our case
                const currMonitor = this._tileEditor.monitor;
                const newMonitor = global.display.get_monitor_neighbor_index(currMonitor, metaDir);
                if (newMonitor === -1)
                    return Modes.MOVE;

                Twm.moveGroupToMonitor(this._tileEditor._windows, currMonitor, newMonitor);
            }

            return Modes.CLOSE;

        // [Esc] to return to default mode
        } else if (keyEvent.get_key_symbol() === Clutter.KEY_Escape) {
            return Modes.DEFAULT;
        }

        return Modes.MOVE;
    }
};

/**
 * Handler to resize the highlighted window.
 *
 * @param {TileEditor} tileEditor
 */
const ResizeKeyHandler = class ResizeKeyHandler extends DefaultKeyHandler {
    constructor(tileEditor) {
        super(tileEditor);

        // The edge that is currently being resized.
        this._currEdge = null;
        this._resizeSideIndicator = null;
    }

    prepareLeave() {
        this._resizeSideIndicator?.destroy();
    }

    handleKeyPress(keyEvent) {
        // [Directions] to resize with WASD, hjkl or arrow keys
        const direction = Util.getDirection(keyEvent.get_key_symbol());
        if (direction) {
            const window = this._selectIndicator.window;
            if (!window)
                return Modes.DEFAULT;

            // First call: Go to an edge.
            if (!this._currEdge) {
                this._currEdge = direction;
                this._createResizeIndicator();
                return Modes.RESIZE;

            // Change resize orientation from H to V
            } else if ([Direction.N, Direction.S].includes(this._currEdge)) {
                if ([Direction.W, Direction.E].includes(direction)) {
                    this._currEdge = direction;
                    this._createResizeIndicator();
                    return Modes.RESIZE;
                }

            // Change resize orientation from V to H
            } else if ([Direction.W, Direction.E].includes(this._currEdge)) {
                if ([Direction.N, Direction.S].includes(direction)) {
                    this._currEdge = direction;
                    this._createResizeIndicator();
                    return Modes.RESIZE;
                }
            }

            this._resize(window, direction);

            // Update the selection indicator.
            this._selectIndicator.focus(window.tiledRect, window);

            // Update resize side indicator
            this._resizeSideIndicator.updatePos(window.tiledRect);

        // [Esc]ape Tile Editing Mode
        } else if (keyEvent.get_key_symbol() === Clutter.KEY_Escape) {
            return Modes.CLOSE;
        }

        return Modes.RESIZE;
    }

    handleKeyRelease(keyEvent) {
        const keyVal = keyEvent.get_key_symbol();
        const superKeys = [Clutter.KEY_Super_L, Clutter.KEY_Super_R];
        return superKeys.includes(keyVal) ? Modes.DEFAULT : Modes.RESIZE;
    }

    _resize(window, keyDir) {
        // Rect, which is being resized by the user. But it still has
        // its original / pre-resize dimensions
        const resizedRect = window.tiledRect;
        const workArea = new Rect(window.get_work_area_current_monitor());
        let resizeAmount = 50;

        // Limit resizeAmount to the workArea
        if (this._currEdge === Direction.N && keyDir === Direction.N)
            resizeAmount = Math.min(resizeAmount, resizedRect.y - workArea.y);
        else if (this._currEdge === Direction.S && keyDir === Direction.S)
            resizeAmount = Math.min(resizeAmount, workArea.y2 - resizedRect.y2);
        else if (this._currEdge === Direction.W && keyDir === Direction.W)
            resizeAmount = Math.min(resizeAmount, resizedRect.x - workArea.x);
        else if (this._currEdge === Direction.E && keyDir === Direction.E)
            resizeAmount = Math.min(resizeAmount, workArea.x2 - resizedRect.x2);

        if (resizeAmount <= 0)
            return;

        // Function to update the passed rect by the resizeAmount depending on
        // the edge that is resized. Some windows will resize on the same edge
        // as the one the user is resizing. Other windows will resize on the
        // opposite edge.
        const updateRectSize = (rect, resizeOnEdge) => {
            const growDir = keyDir === resizeOnEdge ? 1 : -1;
            switch (resizeOnEdge) {
                case Direction.N:
                    rect.y -= resizeAmount * growDir;
                    // falls through
                case Direction.S:
                    rect.height += resizeAmount * growDir;
                    break;

                case Direction.W:
                    rect.x -= resizeAmount * growDir;
                    // falls through
                case Direction.E:
                    rect.width += resizeAmount * growDir;
            }
        };

        // Actually resize the windows here.
        this._windows.forEach(w => {
            // The window, which is resized by the user, is included in this.
            if (this._isSameSide(resizedRect, w.tiledRect)) {
                const newRect = w.tiledRect.copy();
                updateRectSize(newRect, this._currEdge);
                Twm.tile(w, newRect, { openTilingPopup: false });
            } else if (this._isOppositeSide(resizedRect, w.tiledRect)) {
                const newRect = w.tiledRect.copy();
                updateRectSize(newRect, Direction.opposite(this._currEdge));
                Twm.tile(w, newRect, { openTilingPopup: false });
            }
        });
    }

    _isOppositeSide(rect1, rect2) {
        switch (this._currEdge) {
            case Direction.N:
                return rect1.y === rect2.y2;
            case Direction.S:
                return rect1.y2 === rect2.y;
            case Direction.W:
                return rect1.x === rect2.x2;
            case Direction.E:
                return rect1.x2 === rect2.x;
        }

        return false;
    }

    _isSameSide(rect1, rect2) {
        switch (this._currEdge) {
            case Direction.N:
                return rect1.y === rect2.y;
            case Direction.S:
                return rect1.y2 === rect2.y2;
            case Direction.W:
                return rect1.x === rect2.x;
            case Direction.E:
                return rect1.x2 === rect2.x2;
        }

        return false;
    }

    _createResizeIndicator() {
        this._resizeSideIndicator?.destroy();
        this._resizeSideIndicator = new ResizeSideIndicator(
            this._currEdge, this._selectIndicator.rect);
        Main.uiGroup.add_child(this._resizeSideIndicator);
    }
};

const ResizeSideIndicator = GObject.registerClass(
class ResizeSideIndicator extends St.Widget {
    _init(edge, activeRect) {
        const [width, height] = [Direction.N, Direction.S].includes(edge)
            ? [200, 20]
            : [20, 200];

        super._init({
            width,
            height,
            opacity: 0,
            style: 'background-color: black;\
                    border-radius: 999px;'
        });

        this._edge = edge;
        this._moveDist = 100;

        this.updatePos(activeRect);

        // Inner pill
        const innerWidth = this.width < this.height ? 4 : 75;
        const innerHeight = this.width < this.height ? 75 : 4;
        this.add_child(new St.Widget({
            x: this.width / 2 - innerWidth / 2,
            y: this.height / 2 - innerHeight / 2,
            width: innerWidth,
            height: innerHeight,
            style: 'background-color: #ebebeb;\
                    border-radius: 999px;'
        }));
    }

    destroy() {
        this.ease({
            opacity: 0,
            duration: 100,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => super.destroy()
        });
    }

    updatePos(rect) {
        let x, y;
        switch (this._edge) {
            case Direction.N:
                x = rect.center.x - this.width / 2;
                y = rect.y - this.height / 2;
                break;
            case Direction.S:
                x = rect.center.x - this.width / 2;
                y = rect.y2 - this.height / 2;
                break;
            case Direction.W:
                x = rect.x - this.width / 2;
                y = rect.center.y - this.height / 2;
                break;
            case Direction.E:
                x = rect.x2 - this.width / 2;
                y = rect.center.y - this.height / 2;
        }

        this.ease({
            x,
            y,
            opacity: 255,
            duration: 150,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD
        });
    }
});

Zerion Mini Shell 1.0