/* extension.js * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * SPDX-License-Identifier: GPL-2.0-or-later */ import Clutter from 'gi://Clutter'; import Meta from 'gi://Meta'; import St from 'gi://St'; import Shell from 'gi://Shell'; import Gio from 'gi://Gio'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js'; // Window edge action const WindowEdgeAction = { NONE: 0, // No action WAIT_GESTURE: 0x01, // Wait for gesture flag MOVE: 0x02, // Move flag RESIZE: 0x04, // Resize flag GESTURE_LEFT: 0x10, // Gesture Left GESTURE_RIGHT: 0x20, // Gesture Right GESTURE_UP: 0x40, // Gesture Up GESTURE_DOWN: 0x80, // Gesture Down GESTURE_UP_LEFT: 0x100, // Gesture Up Left GESTURE_UP_RIGHT: 0x200, // Gesture Up Right GESTURE_HORIZONTAL: 0x400, // Non-Window Gestures GESTURE_VERTICAL: 0x800, RESIZE_LEFT: 0x1000, // Resize Flags RESIZE_RIGHT: 0x2000, RESIZE_TOP: 0x4000, RESIZE_BOTTOM: 0x8000, MOVE_SNAP_TOP: 0x10000, // Snap Flags MOVE_SNAP_LEFT: 0x20000, MOVE_SNAP_RIGHT: 0x40000 }; // Window Blacklist Classes const WindowClassBlacklist = [ "gjs" ]; // Manager Class class Manager { // Init Extension constructor(ext) { // Get settings this._settings = ext.getSettings(); // Gestures are hooked by WGS this._hooked = false; // Create virtual devices const seat = Clutter.get_default_backend().get_default_seat(); this._virtualTouchpad = seat.create_virtual_device( Clutter.InputDeviceType.POINTER_DEVICE ); this._virtualKeyboard = seat.create_virtual_device( Clutter.InputDeviceType.KEYBOARD_DEVICE ); // Init variables - keep enable() clean this._clearVars(); // Capture Touchpad Event this._gestureCallbackID = global.stage.connect( 'captured-event::touchpad', this._touchpadEvent.bind(this) ); // init 3 or 4 fingers config support this._initFingerCountFlip(); // action widget holder this._actionWidgets = {}; // Desktop Theme Setting this._isettings = new Gio.Settings({ schema: 'org.gnome.desktop.interface' }); } // Clear potentially running timeout/interval destroyTimers() { if (this._holdTo) { // hold waiter clearTimeout(this._holdTo); this._holdTo = null; } if (this._actionWidgets.resetWinlist) { // Alt+Tab timeout clearTimeout(this._actionWidgets.resetWinlist); this._actionWidgets.resetWinlist = null; } if (this._keyRepeatInterval) { // Key repeat interval clearInterval(this._keyRepeatInterval); this._keyRepeatInterval = null; } if (this._flingInterval) { // Kinetic interval clearInterval(this._flingInterval); this._flingInterval = null; } if (this._actionWidgets.tilerHider) { clearTimeout(this._actionWidgets.tilerHider); this._actionWidgets.tilerHider = null; } } // Cleanup Extension destroy() { // clear timers this.destroyTimers(); // restore default GNOME 3 fingers gesture this._restoreFingerCountFlip(); // Release Touchpad Event Capture global.stage.disconnect(this._gestureCallbackID); // Cleanup virtual devices this._virtualTouchpad = null; this._virtualKeyboard = null; // Cleanup all variables this._clearVars(); this._isettings = null; this._settings = null; this._ShaderClass = null; } // Initialize variables _clearVars() { // Target window to manage this._targetWindow = null; // Mouse start position this._startPos = { x: 0, y: 0 }; // Mouse start position this._movePos = { x: 0, y: 0 }; // Monitor Id this._monitorId = 0; // Monitor Workarea this._monitorArea = null; // Starting Window Area this._startWinArea = null; // Edge Action this._edgeAction = WindowEdgeAction.NONE; this._edgeGestured = 0; this._swipeIsWin = false; this._isActiveWin = false; this._tapHold = 0; this._tapHoldWin = null; this._tapHoldTick = 0; // Pinch this._gesture = { begin: false, fingers: 0, progress: 0, velocity: null, action: 0, action_id: 0, action_cmp: 0 }; // Clear window tile preview // this._hidePreview(); } // Init 3 or 4 finger count switch mode _initFingerCountFlip() { // Move 3-4 Finger Gesture /* * Original Hook Logic From (swap-finger-gestures): * https://github.com/icedman/swap-finger-gestures-3-4 * */ this._swipeMods = [ Main.overview._swipeTracker._touchpadGesture, Main.wm._workspaceAnimation._swipeTracker._touchpadGesture, Main.overview._overview._controls ._workspacesDisplay._swipeTracker._touchpadGesture, Main.overview._overview._controls ._appDisplay._swipeTracker._touchpadGesture ]; let me = this; this._swipeMods.forEach((g) => { g._newHandleEvent = (actor, event) => { event._get_touchpad_gesture_finger_count = event.get_touchpad_gesture_finger_count; event.get_touchpad_gesture_finger_count = () => { let real_count = event._get_touchpad_gesture_finger_count(); if (me._hooked || (real_count == me._gestureNumFinger())) { return 0; } else if (real_count >= 3) { return 3; } return 0; }; if (me._hooked) { return Clutter.EVENT_STOP; } return g._handleEvent(actor, event); }; global.stage.disconnectObject(g); global.stage.connectObject( 'captured-event::touchpad', g._newHandleEvent.bind(g), g ); }); } // Restore 3 or 4 finger count switch mode _restoreFingerCountFlip() { // Restore 3 finger gesture this._swipeMods.forEach((g) => { global.stage.disconnectObject(g); global.stage.connectObject( 'captured-event::touchpad', g._handleEvent.bind(g), g ); }); this._swipeMods = []; } // Is dark theme? _isDarkTheme() { let uit = this._settings.get_int('ui-theme'); if (uit == 0) { return (this._isettings .get_string('color-scheme') == 'prefer-dark'); } return (uit == 2); } // Create UI Indicator _createUi(ui_class, x, y, w, h, icon, parent) { let ui = new St.Widget({ style_class: ui_class }); if (this._isDarkTheme()) { ui.add_style_class_name("wgs-dark"); } // ui.set_clip_to_allocation(true); ui._icon = null; ui._parent = parent ? parent : Main.layoutManager.uiGroup; if (icon) { ui._icon = new St.Icon({ icon_name: icon, style_class: 'wgs-widget-icon' }); ui.add_child(ui._icon); } ui.set_position(x, y); ui.set_size(w, h); ui.set_pivot_point(0.5, 0.5); ui.viewShow = (prop, duration) => { ui.show(); prop.mode = Clutter.AnimationMode.EASE_OUT_QUAD; prop.duration = duration; ui.ease(prop); }; ui.viewHide = (prop) => { prop.mode = Clutter.AnimationMode.EASE_OUT_QUAD; prop.duration = duration; prop.onStopped = () => { ui.hide(); }; ui.ease(prop); }; ui.aniRelease = (progress) => { if (!progress) { progress = 1.0; } if (progress > 0.2) { ui.ease({ opacity: 0, scale_x: 0, scale_y: 0, duration: Math.round(250 * progress), mode: Clutter.AnimationMode.EASE_OUT_QUAD, onStopped: () => { ui.release(); } }); } else { ui.release(); } }; ui.release = () => { // Cleanup ui.hide(); ui._parent.remove_child(ui); if (ui._icon) { ui.remove_child(ui._icon); ui._icon.destroy(); ui._icon = null; } ui.destroy(); ui = null; }; ui._parent.add_child(ui); return ui; } // Create Shader Effect _createShader(type, actor, name) { let fx = new Clutter.ShaderEffect( { shader_type: Clutter.ShaderType.FRAGMENT_SHADER } ); let shader = ''; if (type == 'close') { shader = 'color.g *= 1-(0.3*value); ' + 'color.b *= 1-(0.34*value); '; } else { shader = 'color.rgb *= 1.0-value; '; } fx.set_shader_source( 'uniform sampler2D tex; ' + 'uniform float value; ' + 'void main() { ' + 'vec4 color=texture2D(tex,cogl_tex_coord_in[0].st);' + shader + 'cogl_color_out = color * cogl_color_in;}' ); fx.set_uniform_value('tex', 0); fx.setValue = function (v) { fx.set_uniform_value('value', v); } if (actor) { fx._fxname = name; actor.fx = fx; if (actor.get_effect(name)) { actor.remove_effect_by_name(name); } actor.add_effect_with_name(name, fx); } fx.release = () => { if (actor) { actor.remove_effect_by_name(actor.fx._fxname); actor.fx = null; actor = null; } fx = null; }; return fx; } // Velocity functions _velocityInit() { let vel = { data: [], prev: 0 }; return vel; } _velocityTrim(vel) { const thresholdTime = this._tick() - 150; const index = vel.data.findIndex(r => r.time >= thresholdTime); vel.data.splice(0, index); } _velocityAppend(vel, v) { this._velocityTrim(vel); let vb = Math.abs(v); let d = vb - vel.prev; vel.prev = vb; vel.data.push({ time: this._tick(), delta: d }); } _velocityCalc(vel) { this._velocityTrim(vel); if (vel.data.length < 2) return 0; const firstTime = vel.data[0].time; const lastTime = vel.data[vel.data.length - 1].time; if (firstTime === lastTime) return 0; const totalDelta = vel.data.slice(1).map( a => a.delta).reduce((a, b) => a + b); const period = lastTime - firstTime; return totalDelta / period; } _velocityFlingHandler(me) { if (me._velocityFlingQueue.length == 0) { clearInterval(me._flingInterval); me._flingInterval = 0; return; } let now = me._velocityFlingQueue[0]; let clearIt = false; now.target += now.v * 2; now.v *= 0.98; now.n++; if (me._velocityFlingQueue.length != 1) { // Another fling called now.cb(1, target); clearIt = true; } else if (now.target >= now.max || now.n >= now.maxframe) { if (now.target >= now.max) { now.target = now.max; } now.cb(1, now.target); clearIt = true; } else { now.cb(0, now.target); } if (clearIt) { me._velocityFlingQueue.splice(0, 1); } } _velocityFling(vel, curr, max, maxframe, cb) { if (!this._velocityFlingQueue) { this._velocityFlingQueue = []; } this._velocityFlingQueue.push({ target: curr, v: vel, max: max, maxframe: maxframe, cb: cb, n: 0 }); if (!this._flingInterval) { this._flingInterval = setInterval( this._velocityFlingHandler, 4, this ); } } _tick() { return new Date().getTime(); } _isWindowBlacklist(win) { if (win) { if (WindowClassBlacklist.indexOf(win.get_wm_class()) == -1 || win.get_window_type() === Meta.WindowType.DESKTOP) { return false; } } return true; } // Get padding edge size _edgeSize() { return this._settings.get_int('edge-size'); } // Get top padding edge size _topEdgeSize() { return this._settings.get_int('top-edge-size'); } // Get gesture threshold _gestureThreshold() { return this._settings.get_int('gesture-threshold'); } // Get acceleration _getAcceleration() { return (this._settings.get_int('gesture-acceleration') * 0.1); } // Get gesture threshold _gestureCancelThreshold() { return this._settings.get_int('gesture-cancel-threshold'); } // Is 3 Finger _gestureNumFinger() { return this._settings.get_boolean("three-finger") ? 3 : 4; } // Functions Settings _getUseActiveWindow() { return this._settings.get_boolean("use-active-window"); } _getEnableResize() { return this._settings.get_boolean("fn-resize"); } _getEnableMove() { return this._settings.get_boolean("fn-move"); } _getEnableMaxSnap() { return this._settings.get_boolean("fn-maximized-snap"); } _getEnableMoveSnap() { return this._settings.get_boolean("fn-move-snap"); } _getEnableFullscreen() { return this._settings.get_boolean("fn-fullscreen"); } _getPinchInScale() { return this._settings.get_int('pinch-in-scale'); } _getPinchOutScale() { return this._settings.get_int('pinch-out-scale'); } _getPinchEnabled() { return this._settings.get_boolean("pinch-enable"); } _getTapHoldMove() { return this._settings.get_boolean("taphold-move"); } // Is On Overview _isOnOverview() { return Main.overview._shown; } // Check edge flags _isEdge(edge) { return ((this._edgeAction & edge) == edge); } // Show Preview _showPreview(rx, ry, rw, rh) { if (global.display.get_focus_window() == null) { return; } global.window_manager.emit("show-tile-preview", global.display.get_focus_window(), new Meta.Rectangle( { x: rx, y: ry, width: rw, height: rh } ) , this._monitorId ); } // Find Target Window _findPointerWindow() { let target = null; let [pointerX, pointerY, pointerZ] = global.get_pointer(); let currActor = global.stage.get_actor_at_pos( Clutter.PickMode.REACTIVE, pointerX, pointerY ); if (currActor) { // Find root window for current actor let currWindow = currActor.get_parent(); let i = 0; while (currWindow && !currWindow.get_meta_window) { currWindow = currWindow.get_parent(); if (!currWindow || (++i > 10)) { currWindow = null; break; } } // Set meta window as target window to manage target = currWindow?.get_meta_window(); } return target; } // Get Alt Tabs List _getWindowTabList() { let wm = global.workspace_manager; let workspace = wm.get_active_workspace(); let windows = global.display.get_tab_list( Meta.TabList.NORMAL_ALL, workspace ); return windows.map(w => { return w.is_attached_dialog() ? w.get_transient_for() : w; }).filter((w, i, a) => !w.skip_taskbar && a.indexOf(w) === i); } // Hide Preview _hidePreview() { global.window_manager.emit("hide-tile-preview"); } // Simulate keypress (up -> down) _sendKeyPress(combination) { combination.forEach(key => this._virtualKeyboard.notify_keyval( Clutter.get_current_event_time(), key, Clutter.KeyState.PRESSED) ); combination.reverse().forEach(key => this._virtualKeyboard.notify_keyval( Clutter.get_current_event_time(), key, Clutter.KeyState.RELEASED )); } // Move Mouse Pointer _movePointer(x, y) { if (!this._isActiveWin) { // Move only if not use active window this._virtualTouchpad.notify_relative_motion( Meta.CURRENT_TIME, x, y ); } } // window management _winmanWinApp(win) { return Shell.WindowTracker.get_default() .get_window_app(win); // App.create_icon_texture; } // Snap Window _setSnapWindow(snapRight) { if (this._targetWindow == null) { return; } // TODO: Using non key shortcut for snap left/right if (snapRight) { this._sendKeyPress([Clutter.KEY_Super_L, Clutter.KEY_Right]); } else { this._sendKeyPress([Clutter.KEY_Super_L, Clutter.KEY_Left]); } } // Move to Workspace _moveWindowWorkspace(moveRight) { if ((this._targetWindow == null) || !Meta.prefs_get_dynamic_workspaces()) { return; } let wsid = this._targetWindow.get_workspace().index(); if (wsid == 0 && !moveRight) { this._insertWorkspace(0, this._targetWindow); return; } let tw = this._targetWindow.get_workspace() .get_neighbor(moveRight ? Meta.MotionDirection.RIGHT : Meta.MotionDirection.LEFT); this._targetWindow.change_workspace(tw); this._targetWindow.activate(Meta.CURRENT_TIME); } // Insert new workspace _insertWorkspace(pos, chkwin) { // Main.wm.insertWorkspace(0); let wm = global.workspace_manager; if (!Meta.prefs_get_dynamic_workspaces()) { return -1; } wm.append_new_workspace(false, Meta.CURRENT_TIME); let windows = global.get_window_actors().map(a => a.meta_window); windows.forEach(window => { if (chkwin && chkwin == window) return -1; if (window.get_transient_for() != null) return -1; if (window.is_override_redirect()) return -1; if (window.on_all_workspaces) return -1; let index = window.get_workspace().index(); if (index < pos) return -1; window.change_workspace_by_index(index + 1, true); }); if (chkwin) { wm.get_workspace_by_index(pos + 1).activate( Meta.CURRENT_TIME ); chkwin.change_workspace_by_index(pos, true); this._targetWindow.activate(Meta.CURRENT_TIME); } return pos; } _isDynamicWorkspace() { return Meta.prefs_get_dynamic_workspaces(); } // Have Left Workspace _workspaceHavePrev() { if (this._targetWindow == null) { return false; } return (this._targetWindow.get_workspace().index() > 0); } _resetWinPos() { if (this._targetWindow == null) { return; } // Reset this._targetWindow.move_frame( true, this._startWinArea.x, this._startWinArea.y ); } // Activate Target Window _activateWindow() { if (this._targetWindow == null) { return; } // Activate window if not focused yet if (!this._targetWindow.has_focus()) { this._targetWindow.activate( Meta.CURRENT_TIME ); } } // Swipe window move handler _swipeUpdateMove() { this._activateWindow(); // gnome-shell-extension-tiling-assistant support if (this._targetWindow.isTiled) { let urct = this._targetWindow.untiledRect; if (urct) { let r = urct._rect; this._startWinArea.x = r.x; this._startWinArea.y = r.y; this._startWinArea.width = r.width; this._startWinArea.height = r.height; // this._targetWindow.move_resize_frame( // true, // r.x, r.y, r.width, r.height // ); this._sendKeyPress([Clutter.KEY_Super_L, Clutter.KEY_Down]); } } let allowMoveSnap = this._getEnableMoveSnap(); // Move calculation let mX = this._monitorArea.x; let mY = this._monitorArea.y; let mW = this._monitorArea.width; let mR = mX + mW; let winX = this._startWinArea.x + this._movePos.x; let winY = this._startWinArea.y + this._movePos.y; let winR = winX + this._startWinArea.width; // Move action this._targetWindow.move_frame( true, winX, winY ); if (allowMoveSnap && winX < mX) { this._showPreview( this._monitorArea.x, this._monitorArea.y, this._monitorArea.width / 2, this._monitorArea.height ); this._edgeAction = WindowEdgeAction.MOVE | WindowEdgeAction.MOVE_SNAP_LEFT; } else if (allowMoveSnap && winR > mR) { this._showPreview( this._monitorArea.x + (this._monitorArea.width / 2), this._monitorArea.y, this._monitorArea.width / 2, this._monitorArea.height ); this._edgeAction = WindowEdgeAction.MOVE | WindowEdgeAction.MOVE_SNAP_RIGHT; } else if (allowMoveSnap && winY < mY) { this._showPreview( this._monitorArea.x, this._monitorArea.y, this._monitorArea.width, this._monitorArea.height ); this._edgeAction = WindowEdgeAction.MOVE | WindowEdgeAction.MOVE_SNAP_TOP; } else { this._edgeAction = WindowEdgeAction.MOVE; this._hidePreview(); } return Clutter.EVENT_STOP; } // Swipe window resize handler _swipeUpdateResize(dx, dy) { this._activateWindow(); // Move cursor pointer this._movePointer( (this._isEdge(WindowEdgeAction.RESIZE_LEFT) || this._isEdge(WindowEdgeAction.RESIZE_RIGHT)) ? dx : 0, (this._isEdge(WindowEdgeAction.RESIZE_TOP) || this._isEdge(WindowEdgeAction.RESIZE_BOTTOM)) ? dy : 0 ); // Resize actions let tX = this._startWinArea.x; let tY = this._startWinArea.y; let tW = this._startWinArea.width; let tH = this._startWinArea.height; if (this._isEdge(WindowEdgeAction.RESIZE_BOTTOM)) { tH += this._movePos.y; } else if (this._isEdge(WindowEdgeAction.RESIZE_TOP)) { tY += this._movePos.y; tH -= this._movePos.y; } if (this._isEdge(WindowEdgeAction.RESIZE_RIGHT)) { tW += this._movePos.x; } else if (this._isEdge(WindowEdgeAction.RESIZE_LEFT)) { tX += this._movePos.x; tW -= this._movePos.x; } let tR = tX + tW; let tB = tY + tH; let mX = this._monitorArea.x; let mY = this._monitorArea.y; let mW = this._monitorArea.width; let mH = this._monitorArea.height; let mR = mX + mW; let mB = mY + mH; // edge if (tX < mX) { tX = mX; tW = tR - tX; } if (tY < mY) { tY = mY; tH = tB - tY; } if (tR > mR) { tW = mR - tX; } if (tB > mB) { tH = mB - tY; } // Resize Window this._targetWindow.move_resize_frame( true, tX, tY, tW, tH ); return Clutter.EVENT_STOP; } // Begin swipe gesture _swipeBegin(numfingers) { // Swipe Variables this._swipeIsWin = (numfingers == this._gestureNumFinger()); // Workspace gesture only not on overview if (this._isOnOverview() && !this._swipeIsWin) { return Clutter.EVENT_PROPAGATE; } // Init gesture variables this._gesture.begin = true; this._gesture.fingers = numfingers; this._gesture.progress = 0; // Action Variables this._gesture.action = 0; this._gesture.action_cmp = 0; this._gesture.action_id = 0; // Velocity Variables this._gesture.velocity = this._velocityInit(); this._velocityAppend(this._gesture.velocity, 0); // Get current mouse position let [pointerX, pointerY, pointerZ] = global.get_pointer(); this._startPos.x = pointerX; this._startPos.y = pointerY; this._movePos.x = this._movePos.y = 0; // Configs let allowResize = this._getEnableResize(); let allowMove = this._getEnableMove(); this._isActiveWin = false; this._targetWindow = null; let isTapHoldAction = false; // Clear unswipe tap-hold if ((this._tapHoldTick != 1) && (this._tapHoldTick < this._tick())) { this._tapHold = this._tapHoldTick = 0; this._tapHoldWin = null; } if (this._tapHoldWin) { this._targetWindow = this._tapHoldWin; this._tapHoldWin = null; isTapHoldAction = true; } else if (!this._getUseActiveWindow() && !this._getTapHoldMove()) { this._targetWindow = this._findPointerWindow(); } if (!this._targetWindow) { // Get Active Window this._targetWindow = global.display.get_focus_window(); if (!this._targetWindow) { return this._swipeIsWin ? Clutter.EVENT_STOP : Clutter.EVENT_PROPAGATE; } allowResize = false; this._isActiveWin = true; } // Set opener window as target if it was dialog if (this._targetWindow.is_attached_dialog()) { this._targetWindow = this._targetWindow.get_transient_for(); } // Check blacklist window if (this._isWindowBlacklist(this._targetWindow)) { this._targetWindow = null; return this._swipeIsWin ? Clutter.EVENT_STOP : Clutter.EVENT_PROPAGATE; } // Get monitor area this._monitorArea = this._targetWindow.get_work_area_current_monitor(); // Get monitor id this._monitorId = global.display.get_monitor_index_for_rect( this._monitorArea ); // Get start frame rectangle this._startWinArea = this._targetWindow.get_frame_rect(); // Window area as local value let wLeft = this._startWinArea.x; let wTop = this._startWinArea.y; let wRight = wLeft + this._startWinArea.width; let wBottom = wTop + this._startWinArea.height; let wThirdX = wLeft + (this._startWinArea.width / 3); let wThirdY = wTop + (this._startWinArea.height / 3); let w34X = wLeft + ((this._startWinArea.width / 3) * 2); let w34Y = wTop + ((this._startWinArea.height / 3) * 2); // Detect window edge let edge = this._edgeSize(); let topEdge = this._topEdgeSize(); // Default edge: need move event for more actions this._edgeAction = WindowEdgeAction.NONE; this._edgeGestured = 0; // Check allow resize if (this._swipeIsWin && !this._isActiveWin && allowResize && this._targetWindow.allows_resize() && this._targetWindow.allows_move() && !this._targetWindow.isTiled && !isTapHoldAction) { // Edge cursor position detection if (this._startPos.y >= wBottom - edge) { if (this._startPos.y <= wBottom) { // Cursor on bottom of window this._edgeAction = WindowEdgeAction.RESIZE | WindowEdgeAction.RESIZE_BOTTOM; // 1/3 from left|right if (this._startPos.x <= wThirdX) { this._edgeAction |= WindowEdgeAction.RESIZE_LEFT; } else if (this._startPos.x >= w34X) { this._edgeAction |= WindowEdgeAction.RESIZE_RIGHT; } } } else { if (this._startPos.x >= wLeft && this._startPos.x <= wRight) { if (this._startPos.x <= wLeft + edge) { // Cursor on left side of window this._edgeAction = WindowEdgeAction.RESIZE | WindowEdgeAction.RESIZE_LEFT; } else if (this._startPos.x >= wRight - edge) { // Cursor on right side of window this._edgeAction = WindowEdgeAction.RESIZE | WindowEdgeAction.RESIZE_RIGHT; } if (this._isEdge(WindowEdgeAction.RESIZE)) { // 1/3 from top|bottom if (this._startPos.y <= wThirdY) { this._edgeAction |= WindowEdgeAction.RESIZE_TOP; } else if (this._startPos.y >= w34Y) { this._edgeAction |= WindowEdgeAction.RESIZE_BOTTOM; } } } } } if (this._swipeIsWin && !this._isEdge(WindowEdgeAction.RESIZE)) { let setmove = false; if (this._getTapHoldMove()) { if (this._tapHold == this._gestureNumFinger()) { // Tap and hold setmove = true; } } else if (allowMove && this._startPos.x <= wRight && this._startPos.x >= wLeft && this._startPos.y >= wTop && this._startPos.y <= wTop + topEdge) { // Mouse in top side of window setmove = true; } if (setmove) { if (this._targetWindow.allows_move() && !this._targetWindow.get_maximized()) { this._edgeAction = WindowEdgeAction.MOVE; } } } else if (this._tapHold > 2 && this._tapHold != this._gestureNumFinger()) { this._edgeAction = WindowEdgeAction.GESTURE_DOWN; this._movePos.y = (this._gestureThreshold() / 4) + 1; } return this._swipeIsWin ? Clutter.EVENT_STOP : Clutter.EVENT_PROPAGATE; } _swipeUpdate(dx, dy) { if (!this._gesture.begin) { return Clutter.EVENT_PROPAGATE; } // Moving coordinate this._movePos.x += dx; this._movePos.y += dy; // Move & Resize Handler if (this._isEdge(WindowEdgeAction.MOVE)) { return this._swipeUpdateMove(); } else if (this._isEdge(WindowEdgeAction.RESIZE)) { return this._swipeUpdateResize(dx, dy); } // Get threshold setting let threshold = this._gestureThreshold(); let combineTrigger = this._gestureThreshold() * 2; let trigger = (threshold / 4) + 1; let target = 1.00 * (trigger + (threshold * 10)); let absX = Math.abs(this._movePos.x); let absY = Math.abs(this._movePos.y); // Find gesture directions if (this._edgeAction == WindowEdgeAction.NONE) { if (absX >= trigger || absY >= trigger) { if (absX > absY) { if (this._movePos.x <= 0 - trigger) { this._edgeAction = WindowEdgeAction.GESTURE_LEFT; } else if (this._movePos.x >= trigger) { this._edgeAction = WindowEdgeAction.GESTURE_RIGHT; } this._movePos.y = 0; } else { if (this._movePos.y <= 0 - trigger) { this._edgeAction = WindowEdgeAction.GESTURE_UP; } else if (this._movePos.y >= trigger) { this._edgeAction = WindowEdgeAction.GESTURE_DOWN; if (this._swipeIsWin) { let allowMove = this._getEnableMove(); let holdMove = this._getTapHoldMove(); if (!allowMove || holdMove) { if (!this._targetWindow.get_maximized() && !this._targetWindow.isTiled) { this._edgeGestured = 1; } else { this._edgeGestured = 0; } } else if ( !this._edgeGestured && !this._targetWindow.is_fullscreen() && !this._targetWindow.get_maximized() && this._targetWindow.allows_move()) { this._edgeAction = WindowEdgeAction.MOVE; return this._swipeUpdateMove(); } } } this._movePos.x = 0; } this._edgeGestured = this._edgeGestured ? 2 : 1; this._gesture.velocity = this._velocityInit(); this._velocityAppend(this._gesture.velocity, 0); } } let prog = 0; let vert = 0; let horiz = 0; if (this._isEdge(WindowEdgeAction.GESTURE_LEFT)) { if (!this._swipeIsWin && !this._hooked) { return Clutter.EVENT_PROPAGATE; } let xmove = this._movePos.x + trigger; if (this._isEdge(WindowEdgeAction.GESTURE_UP) || this._isEdge(WindowEdgeAction.GESTURE_DOWN)) { xmove = this._movePos.x + combineTrigger; } else if (!this._swipeIsWin) { return Clutter.EVENT_PROPAGATE; } if (xmove < 0) { prog = Math.abs(xmove) / target; } horiz = 1; } else if (this._isEdge(WindowEdgeAction.GESTURE_RIGHT)) { if (!this._swipeIsWin && !this._hooked) { return Clutter.EVENT_PROPAGATE; } let xmove = this._movePos.x - trigger; if (this._isEdge(WindowEdgeAction.GESTURE_UP) || this._isEdge(WindowEdgeAction.GESTURE_DOWN)) { xmove = this._movePos.x - combineTrigger; } else if (!this._swipeIsWin) { return Clutter.EVENT_PROPAGATE; } if (xmove > 0) { prog = xmove / target; } horiz = 2; } else if (this._isEdge(WindowEdgeAction.GESTURE_UP)) { if (!this._swipeIsWin && !this._hooked) { return Clutter.EVENT_PROPAGATE; } if (this._movePos.y < 0) { prog = Math.abs(this._movePos.y) / target; } vert = 1; } else if (this._isEdge(WindowEdgeAction.GESTURE_DOWN)) { if (!this._swipeIsWin && this._isOnOverview()) { return Clutter.EVENT_PROPAGATE; } if (!this._swipeIsWin) { this._hooked = true; } if (this._movePos.y > 0) { let movey = this._movePos.y - (this._swipeIsWin ? threshold : (threshold * 6)); if (movey < 0) { movey = 0; } prog = movey / target; } vert = 2; } if (vert) { if (absX >= trigger && ((vert == 1 && this._swipeIsWin) || (vert == 2 && !this._swipeIsWin))) { if (vert == 1 || prog == 0) { // Combination gesture (Down + Left/Right) if (this._movePos.x <= 0 - combineTrigger) { this._edgeAction |= WindowEdgeAction.GESTURE_LEFT; this._gesture.velocity = this._velocityInit(); this._velocityAppend(this._gesture.velocity, 0); this._movePos.x = 0 - combineTrigger; return this._swipeUpdate(0, 0); } else if (this._movePos.x >= combineTrigger) { this._edgeAction |= WindowEdgeAction.GESTURE_RIGHT; this._gesture.velocity = this._velocityInit(); this._velocityAppend(this._gesture.velocity, 0); this._movePos.x = combineTrigger; return this._swipeUpdate(0, 0); } } } } else if (horiz) { if (this._isEdge(WindowEdgeAction.GESTURE_UP)) { vert = 1; // Reset to single gesture if (Math.abs(this._movePos.x) < combineTrigger) { this._edgeAction = WindowEdgeAction.GESTURE_UP; this._gesture.velocity = this._velocityInit(); this._velocityAppend(this._gesture.velocity, 0); this._movePos.y = 0 - combineTrigger; return this._swipeUpdate(0, 0); } } else if (this._isEdge(WindowEdgeAction.GESTURE_DOWN)) { vert = 2; // Reset to single gesture if (Math.abs(this._movePos.x) < combineTrigger) { this._edgeAction = WindowEdgeAction.GESTURE_DOWN; this._gesture.velocity = this._velocityInit(); this._velocityAppend(this._gesture.velocity, 0); this._movePos.y = combineTrigger; return this._swipeUpdate(0, 0); } } } // Switch gestures direction if ( (vert == 1 && (this._movePos.y > 0 - trigger)) || (vert == 2 && (this._movePos.y < 0)) || (horiz == 1 && (this._movePos.x > 0 - trigger)) || (horiz == 2 && (this._movePos.x < 0)) ) { if (!(this._isEdge(WindowEdgeAction.GESTURE_DOWN) && horiz)) { if (vert) { this._movePos.x = 0; } if (horiz) { this._movePos.y = 0; } this._edgeAction = WindowEdgeAction.NONE; this._gesture.velocity = this._velocityInit(); this._velocityAppend(this._gesture.velocity, 0); return this._swipeUpdate(0, 0); } } // Limit progress let oprog = prog; if (prog >= 1) { prog = 1.0; } this._gesture.action = 0; // Set action if (vert == 0) { this._gesture.action = horiz; // left=1, right=2 } else { if (!this._swipeIsWin) { if (horiz && vert == 2) { // down + horiz this._gesture.action = horiz + 4; // left=5, right=6 } else { // vert this._gesture.action = (vert == 2) ? 4 : 7; // d=4, u=7 } } else { if (horiz && vert == 1) { // up + horiz this._gesture.action = horiz + 50; // left=51, right=52 } else { // vert this._gesture.action = (vert == 2) ? ((this._edgeGestured == 1) ? 53 : 3) : 50; // up=50, dn=53, up+dn=3 } } } // Cancel action if (this._gesture.action_cmp && (this._gesture.action != this._gesture.action_cmp)) { // Send Cancel State let aid = this._actionIdGet(this._gesture.action_cmp); if (aid) { this._runAction(aid, 1, 0); } } // Update progress & velocity this._gesture.progress = prog; this._velocityAppend(this._gesture.velocity, this._gesture.progress); if (this._gesture.action) { let aid = this._actionIdGet(this._gesture.action); if (aid) { if (aid >= 50) { this._activateWindow(); } this._runAction(aid, 0, this._gesture.progress, oprog); } this._gesture.action_cmp = this._gesture.action; } return Clutter.EVENT_STOP; } _swipeEnd() { // Check move snap if (this._isEdge(WindowEdgeAction.MOVE)) { this._hidePreview(); if (this._isEdge(WindowEdgeAction.MOVE_SNAP_TOP)) { if (this._targetWindow.can_maximize()) { this._resetWinPos(); this._targetWindow.maximize(Meta.MaximizeFlags.BOTH); } } else if (this._isEdge(WindowEdgeAction.MOVE_SNAP_LEFT)) { this._resetWinPos(); this._setSnapWindow(0); } else if (this._isEdge(WindowEdgeAction.MOVE_SNAP_RIGHT)) { this._resetWinPos(); this._setSnapWindow(1); } this._hooked = false; this._clearVars(); return Clutter.EVENT_STOP; } // Gesture Release let retval = (!this._swipeIsWin && !this._hooked) ? Clutter.EVENT_PROPAGATE : Clutter.EVENT_STOP; this._hooked = false; if (this._gesture.action) { let aid = this._actionIdGet(this._gesture.action); if (aid) { let issnapaction = (aid == 51 || aid == 52); if ((this._gesture.progress < 1.0) && !issnapaction) { // Fling Velocity let vel = this._velocityCalc(this._gesture.velocity); if (vel > 0.001) { let me = this; this._velocityFling(vel, this._gesture.progress, 1.0, 30, function (state, prog) { me._runAction(aid, state, prog); } ); this._clearVars(); return retval; } } this._runAction(aid, 1, this._gesture.progress); } } this._clearVars(); return retval; } // Swipe Handler _swipeEventHandler(actor, event) { let numfingers = event.get_touchpad_gesture_finger_count(); if (numfingers != 3 && numfingers != 4) { return Clutter.EVENT_PROPAGATE; } // Process gestures state switch (event.get_gesture_phase()) { case Clutter.TouchpadGesturePhase.BEGIN: // Begin event return this._swipeBegin(numfingers); case Clutter.TouchpadGesturePhase.UPDATE: // Update / move event let [dx, dy] = event.get_gesture_motion_delta(); return this._swipeUpdate( dx * this._getAcceleration(), dy * this._getAcceleration() ); } return this._swipeEnd(); } // Get Current Action Id _pinchGetCurrentActionId() { if (this._gesture.begin && this._gesture.action != 0) { if (this._gesture.action != this._gesture.action_cmp) { this._gesture.action_id = this._actionIdGet( (this._gesture.fingers == 3) ? this._gesture.action + 7 : this._gesture.action + 9 ); this._gesture.action_cmp = this._gesture.action; } return this._gesture.action_id; } return 0; } // Update Pinch _pinchUpdate(pinch_scale) { if (this._gesture.begin) { let pIn = (this._getPinchInScale() / 100.0); let pOut = (this._getPinchOutScale() / 100.0); // Get prediction action & current progress position if (pinch_scale < 1.0) { if (pinch_scale < pIn) { pinch_scale = pIn; } this._gesture.action = 1; this._gesture.progress = (1.0 - pinch_scale) / (1.0 - pIn); } else if (pinch_scale > 1.0) { if (pinch_scale > pOut) { pinch_scale = pOut; } this._gesture.action = 2; this._gesture.progress = (pinch_scale - 1.0) / (pOut - 1.0); } else { this._gesture.action = 0; this._gesture.progress = 0; } if (this._gesture.action_cmp && (this._gesture.action != this._gesture.action_cmp)) { // Send Cancel State let aid = this._actionIdGet( (this._gesture.fingers == 3) ? this._gesture.action_cmp + 7 : this._gesture.action_cmp + 9 ); if (aid) { this._runAction(aid, 1, 0); } // Reset velocity this._gesture.velocity = this._velocityInit(); this._velocityAppend(this._gesture.velocity, 0); } this._velocityAppend(this._gesture.velocity, this._gesture.progress); let action_id = this._pinchGetCurrentActionId(); if (action_id) { this._runAction(action_id, 0, this._gesture.progress ); } } return Clutter.EVENT_STOP; } // Pinch Begin _pinchBegin(numfingers) { // Pinch Variables this._gesture.begin = true; this._gesture.fingers = numfingers; this._gesture.progress = 0; // Action Variables this._gesture.action = 0; this._gesture.action_cmp = 0; this._gesture.action_id = 0; // Velocity Variables this._gesture.velocity = this._velocityInit(); this._velocityAppend(this._gesture.velocity, 0); return Clutter.EVENT_STOP; } // End Pinch _pinchEnd() { let action_id = this._pinchGetCurrentActionId(); if (action_id) { if (this._gesture.progress < 1.0) { // Fling Velocity let vel = this._velocityCalc(this._gesture.velocity); if (vel > 0.001) { let me = this; this._velocityFling(vel, this._gesture.progress, 1.0, 30, function (state, prog) { me._runAction(action_id, state, prog); } ); this._clearVars(); return Clutter.EVENT_STOP; } } this._runAction(action_id, 1, this._gesture.progress); } this._clearVars(); return Clutter.EVENT_STOP; } // Pinch Handler _pinchEventHandler(actor, event) { if (!this._getPinchEnabled()) { return Clutter.EVENT_PROPAGATE; } let numfingers = event.get_touchpad_gesture_finger_count(); if (numfingers != 3 && numfingers != 4) { return Clutter.EVENT_PROPAGATE; } const pinch_scale = event.get_gesture_pinch_scale(); // Process gestures state switch (event.get_gesture_phase()) { case Clutter.TouchpadGesturePhase.BEGIN: return this._pinchBegin(numfingers); case Clutter.TouchpadGesturePhase.UPDATE: return this._pinchUpdate(pinch_scale); } return this._pinchEnd(); } _tapHoldGesture(state, numfingers) { let isWin = (numfingers == this._gestureNumFinger()); if (!isWin || this._getTapHoldMove()) { if (state) { let me = this; this._holdTo = setTimeout(function () { me._tapHold = 0; let activeWin = null; if (!me._getUseActiveWindow()) { activeWin = me._findPointerWindow(); } if (!activeWin) { activeWin = global.display.get_focus_window(); } if (activeWin && ((activeWin.allows_move() && !activeWin.get_maximized()) || !isWin)) { activeWin.activate( Meta.CURRENT_TIME ); me._tapHold = numfingers; me._tapHoldWin = activeWin; me._tapHoldTick = 1; activeWin.get_compositor_private() .set_pivot_point(0.5, 0.5); activeWin.get_compositor_private().ease({ duration: 100, mode: Clutter.AnimationMode.EASE_OUT_QUAD, scale_y: 1.05, scale_x: 1.05, onStopped: () => { activeWin?.get_compositor_private().ease({ duration: 100, mode: Clutter.AnimationMode.EASE_OUT_QUAD, scale_y: 1, scale_x: 1, onStopped: () => { activeWin?.get_compositor_private() .set_pivot_point(0, 0); } }); } }); } }, 100); } else { if (this._holdTo) { clearTimeout(this._holdTo); } this._tapHoldTick = this._tick() + 100; this._holdTo = 0; } } return Clutter.EVENT_PROPAGATE; } _tapHoldHandler(actor, event) { // Process gestures state let numfingers = event.get_touchpad_gesture_finger_count(); if (numfingers != 3 && numfingers != 4) { return Clutter.EVENT_PROPAGATE; } if (event.get_gesture_phase() == Clutter.TouchpadGesturePhase.BEGIN) { return this._tapHoldGesture(1, numfingers); } return this._tapHoldGesture(0, numfingers); } // Touch Event Handler _touchpadEvent(actor, event) { // log("_touchpadEvent = " + event.type()); // Process pinch if (event.type() == Clutter.EventType.TOUCHPAD_PINCH) return this._pinchEventHandler(actor, event); // Process swipe if (event.type() == Clutter.EventType.TOUCHPAD_SWIPE) return this._swipeEventHandler(actor, event); // Process tap hold if (event.type() == Clutter.EventType.TOUCHPAD_HOLD) return this._tapHoldHandler(actor, event); return Clutter.EVENT_PROPAGATE; } // Get Action Id _actionIdGet(type) { let cfg_name = ""; switch (type) { case 1: cfg_name = "swipe4-left"; break; case 2: cfg_name = "swipe4-right"; break; case 3: cfg_name = "swipe4-updown"; break; case 4: cfg_name = "swipe3-down"; break; case 5: cfg_name = "swipe3-left"; break; case 6: cfg_name = "swipe3-right"; break; case 7: cfg_name = "swipe3-downup"; break; case 8: cfg_name = "pinch3-in"; break; case 9: cfg_name = "pinch3-out"; break; case 10: cfg_name = "pinch4-in"; break; case 11: cfg_name = "pinch4-out"; break; default: if (type >= 50 && type <= 53) { // Max & Snap return type; } return 0; } return this._settings.get_int(cfg_name); } // Run Action _runAction(id, state, progress, oprog) { const _LCASE = 32; if (id == 1) { // // MINIMIZE ACTION // if (this._isOnOverview()) { return; // Ignore on overview } let activeWin = null; let ui = this._actionWidgets.minimize; // Init indicator if (!ui) { activeWin = global.display.get_focus_window(); if (this._isWindowBlacklist(activeWin)) { activeWin = null; } if (activeWin && activeWin.can_minimize()) { ui = activeWin.get_compositor_private(); this._actionWidgets.minimize = ui; if (ui) { ui.set_pivot_point(0.5, 1); } } else { this._actionWidgets.minimize = ui = -1; } } // Execute Progress if (ui && ui != -1) { if (!state) { ui.set_pivot_point(0.5, 1); ui.opacity = 255 - Math.round(100 * progress); ui.scale_x = 1.0 - (0.2 * progress); ui.scale_y = 1.0 - (0.2 * progress); } else { // Action is executed activeWin = null; if (progress >= 1.0) { activeWin = global.display.get_focus_window(); if (activeWin) { if (!activeWin.can_minimize()) { activeWin = null; } } } // Restore ui.ease({ duration: Math.round(250 * progress), mode: Clutter.AnimationMode.EASE_OUT_QUAD, opacity: activeWin ? 0 : 255, scale_x: activeWin ? 0 : 1, scale_y: activeWin ? 0 : 1, onStopped: () => { ui.set_pivot_point(0, 0); if (activeWin) { activeWin.minimize(); ui.opacity = 0; ui.ease({ duration: 800, opacity: 0, onStopped: () => { ui.opacity = 255; ui.scale_x = 1; ui.scale_y = 1; ui = null; } }); } else { ui = null; } } }); this._actionWidgets.minimize = null; } } else if (state) { this._actionWidgets.minimize = ui = null; } } else if (id == 2) { // // CLOSE WINDOW ACTION // if (this._isOnOverview()) { return; // Ignore on overview } let activeWin = null; let ui = this._actionWidgets.close; // Init indicator if (!ui) { activeWin = global.display.get_focus_window(); if (this._isWindowBlacklist(activeWin)) { activeWin = null; } if (activeWin) { ui = activeWin.get_compositor_private(); this._actionWidgets.close = ui; if (ui) { ui.set_pivot_point(0.5, 0.5); this._createShader('close', ui, 'closeindicator'); ui.fx?.setValue(0); } } else { this._actionWidgets.close = ui = -1; } } // Execute Progress if (ui && ui != -1) { if (!state) { ui.set_pivot_point(0.5, 0.5); ui.opacity = 255 - Math.round(40 * progress); ui.scale_x = 1.0 - (progress * 0.08); ui.scale_y = 1.0 - (progress * 0.08); ui.fx?.setValue(progress * 0.99); } else { activeWin = null; // Action is executed if (progress >= 1.0) { activeWin = global.display.get_focus_window(); } ui.fx?.release(); ui.set_pivot_point(0, 0); ui.opacity = 255; ui.scale_x = 1.0; ui.scale_y = 1.0; if (activeWin) { activeWin.delete( Meta.CURRENT_TIME ); activeWin = null; } ui = null; this._actionWidgets.close = null; } } else if (state) { this._actionWidgets.close = ui = null; } } else if (id == 3) { // // SHOW DESKTOP // if (this._isOnOverview()) { // Ignore on overview return; } let ui = this._actionWidgets.show_desktop; // Init indicator if (!ui) { let monitorArea = global.display.list_all_windows(); if (monitorArea.length > 0) { ui = []; for (var i = 0; i < monitorArea.length; i++) { if (!this._isWindowBlacklist(monitorArea[i])) { let aui = monitorArea[i].get_compositor_private(); if (aui) { ui.push(aui); let mrect = monitorArea[i] .get_work_area_current_monitor(); let wrect = monitorArea[i].get_frame_rect(); // black magic calc ;) let wl = (mrect.width / 32); aui._t_targetx = (mrect.width - wl) - wrect.x; var nx = (0 - (wrect.width - wl)) - wrect.x; aui.set_pivot_point(1.0, 0.5); if (Math.abs(nx) < Math.abs(aui._t_targetx)) { aui._t_targetx = nx; aui.set_pivot_point(1, (i % 5) * 0.25); } else { aui.set_pivot_point(0, (i % 5) * 0.25); } } } } this._actionWidgets.show_desktop = ui; } else { this._actionWidgets.show_desktop = ui = -1; } } // Execute Progress if (ui && ui != -1) { if (!state) { ui.forEach((aui) => { aui.opacity = 255 - Math.round(180 * progress); aui.scale_y = aui.scale_x = 1.0 - (progress * 0.6); aui.translation_x = ( (progress * progress) * aui._t_targetx ); }); } else { // Action is executed if (progress >= 1.0) { // Show Desktop (Super+D) Clutter.KEY_D this._sendKeyPress( [Clutter.KEY_Super_L, Clutter.KEY_D + _LCASE] ); } ui.forEach((aui) => { aui.ease({ duration: Math.round(250 * progress), mode: Clutter.AnimationMode.EASE_OUT_QUAD, opacity: 255, translation_x: 0, scale_x: 1, scale_y: 1, onStopped: () => { aui.set_pivot_point(0, 0); delete aui._t_targetx; delete aui._t_targety; } }); }); this._actionWidgets.show_desktop = ui = null; } } else if (state) { this._actionWidgets.show_desktop = ui = null; } } else if ((id == 4) || (id == 5)) { // // NEXT & PREVIOUS WINDOW SWITCHING // if (this._isOnOverview()) { return; // Ignore on overview } let prv = (id == 5); let wid = prv ? "switchwin_prev" : "switchwin_next"; let ui = this._actionWidgets[wid]; // Init indicator if (!ui) { ui = -1; let wins = null; let listActor = null; // Cancel cache win timeout if (this._actionWidgets.resetWinlist) { clearTimeout(this._actionWidgets.resetWinlist); this._actionWidgets.resetWinlist = 0; } // Get cached window list if (this._actionWidgets.cacheWinTabList) { wins = this._actionWidgets.cacheWinTabList?.wins; listActor = this._actionWidgets.cacheWinTabList?.actor; } if (!wins) { // No last cached wins = this._getWindowTabList(); if (wins.length > 1) { // Create UI let gsize = Main.layoutManager.uiGroup.get_size(); let pad = 8; let posCfg = this._settings.get_int( 'winswitch-position' ); let lW = (pad * 2) + (wins.length * 48); let lH = (pad * 2) + 32; let lX = (gsize[0] - lW) / 2; let lY = 64; // Top let pivY = 0; if (posCfg == 1) { // Center lY = (gsize[1] - lH) / 2; pivY = 0.5; } else if (posCfg == 2) { // Bottom lY = gsize[1] - (lH + 64); pivY = 1; } listActor = this._createUi( "wgs-winswitch", lX, lY, lW, lH ); listActor.set_pivot_point(0.5, pivY); listActor.opacity = 0; listActor.scale_x = 0.5; listActor.scale_y = 0.5; listActor._data = []; for (var i = 0; i < wins.length; i++) { let win = wins[i]; let app = this._winmanWinApp(win); let ico = app.create_icon_texture(32); ico.add_style_class_name("wgs-winswitch-ico"); // remove_style_class_name listActor.add_child(ico); ico.set_size(32, 32); ico.set_position((pad * 2) + (48 * i), pad); ico.set_pivot_point(0.5, 0.5); listActor._data.push( { app: app, win: win, ico: ico } ) } listActor.viewShow({ opacity: 255, scale_x: 1, scale_y: 1 }, 200); // Reorder for next (below 1s) calls this._actionWidgets.cacheWinTabList = { sel: 0, first: wins[0], wins: wins, actor: listActor }; } else { this._actionWidgets.cacheWinTabList = null; } } if (wins.length > 1) { ui = { from: wins[0] }; if (prv) { ui.into = wins[wins.length - 1]; ui.nsel = -1; } else { ui.into = wins[1]; ui.nsel = 1; } ui.lstate = 0; ui.from_actor = ui.from.get_compositor_private(); ui.into_actor = ui.into.get_compositor_private(); ui.from_actor.set_pivot_point(0.5, 1); ui.into_actor.set_pivot_point(0.5, 1); // Set selected target for (var i = 0; i < listActor._data.length; i++) { let d = listActor._data[i]; if (d.win == ui.from) { d.ico.add_style_class_name("selected"); ui.from_ico = d.ico; } else if (d.win == ui.into) { d.ico.remove_style_class_name("selected"); ui.into_ico = d.ico; } else { d.ico.remove_style_class_name("selected"); } } } this._actionWidgets[wid] = ui; } if (ui && ui != -1) { if (!state) { try { ui.from_actor.opacity = 255 - Math.round(80 * progress); ui.from_actor.scale_y = ui.from_actor.scale_x = 1.0 - (0.05 * progress); ui.into_actor.scale_y = ui.into_actor.scale_x = 1.0 + (0.05 * progress); } catch (e) { } if (progress > 0.8) { if (!ui.lstate) { try { ui.into.raise(); } catch (e) { } ui.lstate = 1; ui.from_ico?.remove_style_class_name("selected"); ui.into_ico?.add_style_class_name("selected"); } } else if (ui.lstate) { try { ui.from.raise(); } catch (e) { } ui.lstate = 0; ui.from_ico?.add_style_class_name("selected"); ui.into_ico?.remove_style_class_name("selected"); } } else { if ((progress > 0.8) || ui.lstate) { try { try { this._actionWidgets .cacheWinTabList.first.activate( Meta.CURRENT_TIME ); } catch (e) { } try { ui.from.raise(); } catch (e) { } ui.into.activate( Meta.CURRENT_TIME ); } catch (e) { } if (prv) { this._actionWidgets.cacheWinTabList.wins.unshift( this._actionWidgets.cacheWinTabList.wins.pop() ); } else { this._actionWidgets.cacheWinTabList.wins.push( this._actionWidgets.cacheWinTabList.wins.shift() ); } ui.from_ico?.remove_style_class_name("selected"); ui.into_ico?.add_style_class_name("selected"); } else { ui.from_ico?.add_style_class_name("selected"); ui.into_ico?.remove_style_class_name("selected"); } ui.nclose = 0; // Ease Restore try { ui.from_actor.ease({ duration: Math.round(200 * progress), mode: Clutter.AnimationMode.EASE_OUT_QUAD, opacity: 255, scale_x: 1, scale_y: 1, onStopped: () => { ui.from_actor.set_pivot_point(0, 0); ui.from_actor = null; ui.from = null; if (++ui.nclose == 2) { ui = null; } } }); } catch (e) { } try { ui.into_actor.ease({ duration: Math.round(200 * progress), mode: Clutter.AnimationMode.EASE_OUT_QUAD, opacity: 255, scale_x: 1, scale_y: 1, onStopped: () => { ui.into_actor.set_pivot_point(0, 0); ui.into_actor = null; ui.into = null; if (++ui.nclose == 2) { ui = null; } } }); } catch (e) { } this._actionWidgets[wid] = null; // Clear cache after timeout let me = this; this._actionWidgets.resetWinlist = setTimeout( function () { me._actionWidgets .cacheWinTabList.actor.aniRelease(); me._actionWidgets.cacheWinTabList = null; clearTimeout(me._actionWidgets.resetWinlist); me._actionWidgets.resetWinlist = 0; }, 500 ); } } else if (state) { this._actionWidgets[wid] = ui = null; } } else if ((id == 6) || (id == 7)) { // // SEND WINDOW LEFT/RIGHT WORKSPACE // if (this._isOnOverview()) { return; // Ignore on overview } let prv = (id == 6); let wid = prv ? "movewin_left" : "movewin_right"; let ui = this._actionWidgets[wid]; let activeWin = null; // Init indicator if (!ui) { ui = -1; let isDynamic = this._isDynamicWorkspace(); activeWin = global.display.get_focus_window(); let inserted = 0; if (activeWin) { let wsid = activeWin.get_workspace().index(); let tsid = wsid; if (prv) { if (isDynamic) { if (wsid == 0) { let wpl = activeWin.get_workspace().list_windows(); inserted = wpl.length; if (inserted > 1) { // Check for blacklisted windows for (var i = 0; i < inserted; i++) { if (this._isWindowBlacklist(wpl[i])) { inserted--; } } } wsid = tsid = 1; } } else { if (wsid == 0) { inserted = 1; } } tsid--; } else { if (!isDynamic && (wsid >= global.workspace_manager.get_n_workspaces() - 1)) { inserted = 1; } tsid++; } // Make sure move left from left-most workspace only // triggered if other window is available in // current workspace if (inserted !== 1) { activeWin.stick(); if (inserted) { this._insertWorkspace(0); } ui = { confirmSwipe: () => { }, wm: Main.wm._workspaceAnimation, win: activeWin, wid: tsid, sid: wsid, ins: inserted }; ui.wm._switchWorkspaceBegin( ui, global.display.get_primary_monitor() ); } } this._actionWidgets[wid] = ui; } if (ui && ui != -1) { if (!state) { ui.wm._switchWorkspaceUpdate( ui, ui.sid + ((prv) ? 0 - progress : progress) ) } else { ui.win.unstick(); ui.wm._switchWorkspaceEnd( ui, 350, ui.sid + ((prv) ? 0 - progress : progress) ); if (progress > 0.5) { ui.win.change_workspace_by_index( ui.wid, true ); global.workspace_manager.get_workspace_by_index(ui.wid) .activate( Meta.CURRENT_TIME ); } else if (ui.ins) { // list_windows if (ui.ins > 1) { ui.win.change_workspace_by_index( ui.sid, true ); } else { ui.win.change_workspace_by_index( ui.wid, true ); } } ui.win.activate(Meta.CURRENT_TIME); this._actionWidgets[wid] = ui = null; } } else if (state) { this._actionWidgets[wid] = ui = null; } } else if (id >= 8 && id <= 9) { // // BACK / FORWARD // if (this._isOnOverview()) { return; // Ignore on overview } const keyList = [ Clutter.KEY_Back, Clutter.KEY_Forward ]; let kid = id - 8; let activeWin = null; let kidw = kid ? 'btnforward' : 'btnback'; let ui = this._actionWidgets[kidw]; // Init indicator if (!ui) { activeWin = global.display.get_focus_window(); if (this._isWindowBlacklist(activeWin)) { activeWin = null; } if (activeWin) { let uipar = activeWin.get_compositor_private(); if (uipar) { let wrect = activeWin.get_frame_rect(); let uix = ((uipar.get_width() - wrect.width) / 2); let uiy = (uipar.get_height() / 2) - 32; if (kid) { uix += wrect.width - 128; } else { uix += 64; } ui = this._createUi( 'wgs-indicator-backforward', uix, uiy, 64, 64, kid ? 'pan-end-symbolic.symbolic' : 'pan-start-symbolic.symbolic', uipar ); if (ui) { ui.translation_x = kid ? -32 : 32; ui.opacity = 0; } else { ui = -1; } this._actionWidgets[kidw] = ui; } else { this._actionWidgets[kidw] = -1; } } else { this._actionWidgets[kidw] = -1; } } // Execute Progress if (ui && ui != -1) { if (!state) { if (kid) { ui.translation_x = 32 - (32 * progress); } else { ui.translation_x = (32 * progress) - 32; } ui.opacity = Math.round(255 * progress); } else { // Action is executed if (progress >= 1.0) { this._sendKeyPress([keyList[kid]]); } ui.release(); this._actionWidgets[kidw] = ui = null; } } else if (state) { this._actionWidgets[kidw] = ui = null; } } else if (id >= 10 && id <= 17) { // // MEDIA & BRIGHTNESS // const keyList = [ Clutter.KEY_MonBrightnessUp, Clutter.KEY_MonBrightnessDown, Clutter.KEY_AudioRaiseVolume, Clutter.KEY_AudioLowerVolume, Clutter.KEY_AudioMute, Clutter.KEY_AudioPlay, Clutter.KEY_AudioNext, Clutter.KEY_AudioPrev ]; let wid = 'keys_' + id; let cid = 'keysn_' + id; let keyId = keyList[id - 10]; let isRepeat = (id < 14); let kidw = 'keyaction_' + id; let ui = this._actionWidgets[kidw]; if (isRepeat) { if (!state && (progress >= 1)) { if (!this._keyRepeatInterval) { this._actionWidgets[cid] = 0; this._sendKeyPress([keyId]); this._keyRepeatInterval = setInterval( function (me, cid, keyId) { if (me._actionWidgets[cid] >= 5) { me._sendKeyPress([keyId]); } else { me._actionWidgets[cid]++; } }, 100, this, cid, keyId ); } } if (state) { if (this._keyRepeatInterval) { clearInterval(this._keyRepeatInterval); } this._keyRepeatInterval = 0; this._actionWidgets[cid] = 0; } } else { if (!ui) { const iconlist = [ 'audio-volume-muted-symbolic', 'media-playback-start-symbolic', 'media-skip-forward-symbolic', 'media-skip-backward-symbolic' ]; let display = global.get_display(); let mrect = display.get_monitor_geometry( display.get_primary_monitor() ); ui = this._createUi( 'wgs-indicator-keys', mrect.x + (mrect.width / 2) - 64, mrect.y + (mrect.height / 2) - 64, 128, 128, iconlist[id - 14], null ); ui.opacity = 0; ui.scale_x = ui.scale_y = 0; this._actionWidgets[kidw] = ui; } // Execute Progress if (ui && ui != -1) { if (!state) { ui.scale_x = ui.scale_y = progress; ui.opacity = Math.round(255 * progress); } else { // Action is executed if (progress >= 1.0) { this._sendKeyPress([keyId]); } ui.aniRelease(progress); this._actionWidgets[kidw] = ui = null; } } else if (state) { this._actionWidgets[kidw] = ui = null; } } } // // Non animated actions // else if (id == 18) { if (this._isOnOverview()) { // Ignore on overview return; } if (!state || progress < 1.0) { // Ignore if non end return; } // ALT+TAB this._sendKeyPress([Clutter.KEY_Alt_L, Clutter.KEY_Tab]); } else if (id == 19 || id == 20) { if (!state || progress < 1.0) { // Ignore if non end return; } if (id == 19) { Main.overview.show(); } else { Main.overview.showApps(); } } else if (id == 21) { if (!state || progress < 1.0) { // Ignore if non end return; } // Quick Settings (Super+S) Main.wm._toggleQuickSettings(); // this._sendKeyPress([Clutter.KEY_Super_L, Clutter.KEY_S + _LCASE]); } else if (id == 22) { if (!state || progress < 1.0) { // Ignore if non end return; } // Notification (Super+V) Main.wm._toggleCalendar(); // this._sendKeyPress([Clutter.KEY_Super_L, Clutter.KEY_V + _LCASE]); } else if (id == 23) { if (!state || progress < 1.0) { // Ignore if non end return; } // Run (Alt+F2) this._sendKeyPress([Clutter.KEY_Alt_L, Clutter.KEY_F2]); } else if (id >= 50 && id <= 53) { // // Maximized, Fullscreen, Spap Etc. // let activeWin = global.display.get_focus_window(); if (!activeWin || this._isOnOverview()) { return; // Ignore on overview & no active window } if (this._isWindowBlacklist(activeWin)) { return; // Ignore blacklisted } let winCanMax = activeWin.allows_move() && activeWin.can_maximize(); let winIsMaximized = activeWin.get_maximized(); let winMaxed = Meta.MaximizeFlags.BOTH == winIsMaximized; if (activeWin.isTiled) { winIsMaximized = Meta.MaximizeFlags.VERTICAL; } let winIsFullscreen = activeWin.is_fullscreen(); let allowFullscreen = this._getEnableFullscreen(); let ui = 0; // find action id if (id < 53) { // Maximize if (winMaxed) { if (winIsFullscreen) { ui = 5; // un-fullscreen } else if (allowFullscreen) { ui = 4; // fullscreen } else { ui = 6; // if not allow fullscreen (restore) } } else if (winCanMax) { // 1. Max, 2. Snap Left, 3. Snap Right ui = id - 49; } } else if (winIsFullscreen) { ui = 5; // un-fullscreen } else if (winIsMaximized) { ui = 6; // restore } let wid = "wmax_state" + ui; if (ui) { if (!state) { if (ui == 4) { // fullsccreen activeWin?.get_compositor_private() .set_pivot_point(0.5, 1); activeWin.get_compositor_private().scale_y = 1.0 + (progress * 0.025); } else if (ui >= 5) { // un-fullsccreen / restore activeWin?.get_compositor_private() .set_pivot_point(0.5, 1); if (ui == 6) { activeWin.get_compositor_private().scale_y = activeWin.get_compositor_private().scale_x = 1.0 - (progress * 0.04); } else { activeWin.get_compositor_private().scale_y = 1.0 - (progress * 0.025); } } else if (ui <= 3) { if (!this._actionWidgets[wid]) { if (this._actionWidgets.tilerHider) { clearTimeout(this._actionWidgets.tilerHider); this._actionWidgets.tilerHider = null; } let moarea = activeWin .get_work_area_current_monitor(); if (ui == 1) { // max this._showPreview( moarea.x, moarea.y, moarea.width, moarea.height ); } else if (ui == 2) { // snap left this._showPreview( moarea.x, moarea.y, moarea.width / 2, moarea.height ); } else if (ui == 3) { // snap right this._showPreview( moarea.x + (moarea.width / 2), moarea.y, moarea.width / 2, moarea.height ); } this._actionWidgets[wid] = 1; } } } else { if (ui <= 3) { // max, snap if (this._actionWidgets.tilerHider) { clearTimeout(this._actionWidgets.tilerHider); this._actionWidgets.tilerHider = null; } if (this._actionWidgets[wid] && (progress > 0)) { if (ui == 1) { activeWin.maximize(Meta.MaximizeFlags.BOTH); } else if (ui == 2) { this._setSnapWindow(0); } else if (ui == 3) { this._setSnapWindow(1); } } this._actionWidgets.tilerHider = setTimeout( function (me) { me._hidePreview(); me._actionWidgets.tilerHider = null; }, 100, this); this._actionWidgets[wid] = 0; } else { // resize back activeWin?.get_compositor_private().ease({ duration: Math.round(200 * progress), mode: Clutter.AnimationMode.EASE_OUT_QUAD, scale_y: 1, scale_x: 1, onStopped: () => { activeWin?.get_compositor_private() .set_pivot_point(0, 0); } }); if (progress >= 0.5) { if (ui == 4) { // fullscreen activeWin.make_fullscreen(); } else if (ui == 5) { // un-fullscreen activeWin.unmake_fullscreen(); } else if (ui == 6) { // restore activeWin.unmaximize( Meta.MaximizeFlags.BOTH ); if (activeWin.isTiled) { this._sendKeyPress([ Clutter.KEY_Super_L, Clutter.KEY_Down ]); } } } } } } } // // End Of Actions // } } // Export Extension export default class WindowGesturesExtension extends Extension { // Enable Extension enable() { this.manager = new Manager(this); } // Disable Extension disable() { this.manager.destroy(); this.manager = null; } }