%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/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 }); } });