diff options
Diffstat (limited to 'extensions/45/vertical-workspaces/lib/windowPreview.js')
-rw-r--r-- | extensions/45/vertical-workspaces/lib/windowPreview.js | 619 |
1 files changed, 619 insertions, 0 deletions
diff --git a/extensions/45/vertical-workspaces/lib/windowPreview.js b/extensions/45/vertical-workspaces/lib/windowPreview.js new file mode 100644 index 0000000..c775d37 --- /dev/null +++ b/extensions/45/vertical-workspaces/lib/windowPreview.js @@ -0,0 +1,619 @@ +/** + * V-Shell (Vertical Workspaces) + * windowPreview.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2023 + * @license GPL-3.0 + * + */ + +'use strict'; + +import GLib from 'gi://GLib'; +import Clutter from 'gi://Clutter'; +import St from 'gi://St'; +import Meta from 'gi://Meta'; +import Shell from 'gi://Shell'; +import Pango from 'gi://Pango'; +import Graphene from 'gi://Graphene'; +import Atk from 'gi://Atk'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as DND from 'resource:///org/gnome/shell/ui/dnd.js'; +import * as OverviewControls from 'resource:///org/gnome/shell/ui/overviewControls.js'; +import * as WindowPreview from 'resource:///org/gnome/shell/ui/windowPreview.js'; + +let Me; +let opt; + +const WINDOW_SCALE_TIME = 200; +const WINDOW_ACTIVE_SIZE_INC = 5; +const WINDOW_OVERLAY_FADE_TIME = 200; +const WINDOW_DND_SIZE = 256; +const DRAGGING_WINDOW_OPACITY = 100; + +const ControlsState = OverviewControls.ControlsState; + +export const WindowPreviewModule = class { + constructor(me) { + Me = me; + opt = Me.opt; + + this._firstActivation = true; + this.moduleEnabled = false; + this._overrides = null; + } + + cleanGlobals() { + Me = null; + opt = null; + } + + update(reset) { + this.moduleEnabled = opt.get('windowPreviewModule'); + const conflict = false; + + reset = reset || !this.moduleEnabled || conflict; + + // don't touch the original code if module disabled + if (reset && !this._firstActivation) { + this._disableModule(); + } else if (!reset) { + this._firstActivation = false; + this._activateModule(); + } + if (reset && this._firstActivation) + console.debug(' WindowPreviewModule - Keeping untouched'); + } + + _activateModule() { + if (!this._overrides) + this._overrides = new Me.Util.Overrides(); + + this._overrides.addOverride('WindowPreview', WindowPreview.WindowPreview.prototype, WindowPreviewCommon); + // A shorter timeout allows user to quickly cancel the selection by leaving the preview with the mouse pointer + // if (opt.ALWAYS_ACTIVATE_SELECTED_WINDOW) + // WindowPreview.WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT = 150; // incompatible + console.debug(' WindowPreviewModule - Activated'); + } + + _disableModule() { + // If WindowPreview._init was injected by another extension (like Burn My Windows) + // which enables/disables before V-Shell + // don't restore the original if it's not injected, + // because it would restore injected _init and recursion would freeze GS when extensions are enabled again. + // This can happen when all extension re-enabled, not only when screen is locked/unlocked + // If _init doesn't include "fn.apply(this, args)" when reset === true, some extension already restored the original + const skipReset = WindowPreview.WindowPreview.prototype._init.toString().includes('fn.apply(this, args)'); + if (this._overrides && skipReset) { + // skip restoring original _init() + this._overrides['_init'] = null; + } + + if (this._overrides) + this._overrides.removeAll(); + + this._overrides = null; + + console.debug(' WindowPreviewModule - Disabled'); + } +}; + +const WindowPreviewCommon = { + _init(metaWindow, workspace, overviewAdjustment) { + this.metaWindow = metaWindow; + this.metaWindow._delegate = this; + this._windowActor = metaWindow.get_compositor_private(); + this._workspace = workspace; + this._overviewAdjustment = overviewAdjustment; + + const ICON_SIZE = opt.WIN_PREVIEW_ICON_SIZE; + const ICON_OVERLAP = 0.7; + + Shell.WindowPreview.prototype._init.bind(this)({ + reactive: true, + can_focus: true, + accessible_role: Atk.Role.PUSH_BUTTON, + offscreen_redirect: Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY, + }); + + const windowContainer = new Clutter.Actor({ + pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }), + }); + this.window_container = windowContainer; + + windowContainer.connect('notify::scale-x', + () => this._adjustOverlayOffsets()); + // gjs currently can't handle setting an actors layout manager during + // the initialization of the actor if that layout manager keeps track + // of its container, so set the layout manager after creating the + // container + windowContainer.layout_manager = new Shell.WindowPreviewLayout(); + this.add_child(windowContainer); + + this._addWindow(metaWindow); + + this._delegate = this; + + this._stackAbove = null; + + this._cachedBoundingBox = { + x: windowContainer.layout_manager.bounding_box.x1, + y: windowContainer.layout_manager.bounding_box.y1, + width: windowContainer.layout_manager.bounding_box.get_width(), + height: windowContainer.layout_manager.bounding_box.get_height(), + }; + + windowContainer.layout_manager.connect( + 'notify::bounding-box', layout => { + this._cachedBoundingBox = { + x: layout.bounding_box.x1, + y: layout.bounding_box.y1, + width: layout.bounding_box.get_width(), + height: layout.bounding_box.get_height(), + }; + + // A bounding box of 0x0 means all windows were removed + if (layout.bounding_box.get_area() > 0) + this.emit('size-changed'); + }); + + this._windowActor.connectObject('destroy', () => this.destroy(), this); + + this._updateAttachedDialogs(); + + let clickAction = new Clutter.ClickAction(); + clickAction.connect('clicked', act => { + const button = act.get_button(); + if (button === Clutter.BUTTON_SECONDARY) { + if (opt.WIN_PREVIEW_SEC_BTN_ACTION === 1) { + this._closeWinAction(); + return Clutter.EVENT_STOP; + } else if (opt.WIN_PREVIEW_SEC_BTN_ACTION === 2) { + this._searchAppWindowsAction(); + return Clutter.EVENT_STOP; + } else if (opt.WIN_PREVIEW_SEC_BTN_ACTION === 3 && opt.WINDOW_THUMBNAIL_ENABLED) { + this._removeLaters(); + Me.Modules.winTmbModule.createThumbnail(metaWindow); + return Clutter.EVENT_STOP; + } + } else if (button === Clutter.BUTTON_MIDDLE) { + if (opt.WIN_PREVIEW_MID_BTN_ACTION === 1) { + this._closeWinAction(); + return Clutter.EVENT_STOP; + } else if (opt.WIN_PREVIEW_MID_BTN_ACTION === 2) { + this._searchAppWindowsAction(); + return Clutter.EVENT_STOP; + } else if (opt.WIN_PREVIEW_SEC_BTN_ACTION === 3 && opt.WINDOW_THUMBNAIL_ENABLED) { + this._removeLaters(); + Me.Modules.winTmbModule.createThumbnail(metaWindow); + return Clutter.EVENT_STOP; + } + } + return this._activate(); + }); + + + if (this._onLongPress) { + clickAction.connect('long-press', this._onLongPress.bind(this)); + } else { + clickAction.connect('long-press', (action, actor, state) => { + if (state === Clutter.LongPressState.ACTIVATE) + this.showOverlay(true); + return true; + }); + } + + this.connect('destroy', this._onDestroy.bind(this)); + + this._draggable = DND.makeDraggable(this, { + restoreOnSuccess: true, + manualMode: !!this._onLongPress, + dragActorMaxSize: WINDOW_DND_SIZE, + dragActorOpacity: DRAGGING_WINDOW_OPACITY, + }); + + // _draggable.addClickAction is new in GS45 + if (this._draggable.addClickAction) + this._draggable.addClickAction(clickAction); + else + this.add_action(clickAction); + + this._draggable.connect('drag-begin', this._onDragBegin.bind(this)); + this._draggable.connect('drag-cancelled', this._onDragCancelled.bind(this)); + this._draggable.connect('drag-end', this._onDragEnd.bind(this)); + this.inDrag = false; + + this._selected = false; + this._overlayEnabled = true; + this._overlayShown = false; + this._closeRequested = false; + this._idleHideOverlayId = 0; + + const tracker = Shell.WindowTracker.get_default(); + const app = tracker.get_window_app(this.metaWindow); + this._icon = app.create_icon_texture(ICON_SIZE); + this._icon.add_style_class_name('icon-dropshadow'); + this._icon.set({ + reactive: true, + pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }), + }); + this._icon.add_constraint(new Clutter.BindConstraint({ + source: windowContainer, + coordinate: Clutter.BindCoordinate.POSITION, + })); + this._icon.add_constraint(new Clutter.AlignConstraint({ + source: windowContainer, + align_axis: Clutter.AlignAxis.X_AXIS, + factor: 0.5, + })); + this._icon.add_constraint(new Clutter.AlignConstraint({ + source: windowContainer, + align_axis: Clutter.AlignAxis.Y_AXIS, + pivot_point: new Graphene.Point({ x: -1, y: ICON_OVERLAP }), + factor: 1, + })); + + if (opt.WINDOW_ICON_CLICK_ACTION) { + const iconClickAction = new Clutter.ClickAction(); + iconClickAction.connect('clicked', act => { + if (act.get_button() === Clutter.BUTTON_PRIMARY) { + if (opt.WINDOW_ICON_CLICK_ACTION === 1) { + this._searchAppWindowsAction(); + return Clutter.EVENT_STOP; + } else if (opt.WINDOW_ICON_CLICK_ACTION === 2 && opt.WINDOW_THUMBNAIL_ENABLED) { + this._removeLaters(); + Me.Modules.winTmbModule.createThumbnail(metaWindow); + return Clutter.EVENT_STOP; + } + } /* else if (act.get_button() === Clutter.BUTTON_SECONDARY) { + return Clutter.EVENT_STOP; + }*/ + return Clutter.EVENT_PROPAGATE; + }); + this._icon.add_action(iconClickAction); + } + const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); + this._title = new St.Label({ + visible: false, + style_class: 'window-caption', + text: this._getCaption(), + reactive: true, + }); + this._title.clutter_text.single_line_mode = true; + this._title.add_constraint(new Clutter.BindConstraint({ + source: windowContainer, + coordinate: Clutter.BindCoordinate.X, + })); + + let offset; + if (opt.WIN_TITLES_POSITION < 2) { + // we cannot get proper title height before it gets to the stage, so 35 is estimated height + spacing + offset = -scaleFactor * (ICON_SIZE * ICON_OVERLAP + 35); + } else { + offset = scaleFactor * (ICON_SIZE * (1 - ICON_OVERLAP) + 4); + } + this._title.add_constraint(new Clutter.BindConstraint({ + source: windowContainer, + coordinate: Clutter.BindCoordinate.Y, + offset, + })); + this._title.add_constraint(new Clutter.AlignConstraint({ + source: windowContainer, + align_axis: Clutter.AlignAxis.X_AXIS, + factor: 0.5, + })); + this._title.add_constraint(new Clutter.AlignConstraint({ + source: windowContainer, + align_axis: Clutter.AlignAxis.Y_AXIS, + pivot_point: new Graphene.Point({ x: -1, y: 0 }), + factor: 1, + })); + this._title.clutter_text.ellipsize = Pango.EllipsizeMode.END; + this.label_actor = this._title; + this.metaWindow.connectObject( + 'notify::title', () => (this._title.text = this._getCaption()), + this); + + const layout = Meta.prefs_get_button_layout(); + this._closeButtonSide = + layout.left_buttons.includes(Meta.ButtonFunction.CLOSE) + ? St.Side.LEFT : St.Side.RIGHT; + this._closeButton = new St.Button({ + visible: false, + style_class: 'window-close', + icon_name: 'preview-close-symbolic', + }); + this._closeButton.add_constraint(new Clutter.BindConstraint({ + source: windowContainer, + coordinate: Clutter.BindCoordinate.POSITION, + })); + this._closeButton.add_constraint(new Clutter.AlignConstraint({ + source: windowContainer, + align_axis: Clutter.AlignAxis.X_AXIS, + pivot_point: new Graphene.Point({ x: 0.5, y: -1 }), + factor: this._closeButtonSide === St.Side.LEFT ? 0 : 1, + })); + this._closeButton.add_constraint(new Clutter.AlignConstraint({ + source: windowContainer, + align_axis: Clutter.AlignAxis.Y_AXIS, + pivot_point: new Graphene.Point({ x: -1, y: 0.5 }), + factor: 0, + })); + this._closeButton.connect('clicked', () => this._deleteAll()); + + this.add_child(this._title); + this.add_child(this._icon); + this.add_child(this._closeButton); + + this._overviewAdjustment.connectObject( + 'notify::value', () => this._updateIconScale(), this); + this._updateIconScale(); + + this.connect('notify::realized', () => { + if (!this.realized) + return; + + this._title.ensure_style(); + this._icon.ensure_style(); + }); + + if (ICON_SIZE < 22) { + // disable app icon + this._icon.hide(); + } else { + this._updateIconScale(); + } + + + + // if window is created while the overview is shown, icon and title should be visible immediately + if (Main.overview._overview._controls._stateAdjustment.value < 1) { + this._icon.scale_x = 0; + this._icon.scale_y = 0; + this._title.opacity = 0; + } + + if (opt.ALWAYS_SHOW_WIN_TITLES) + this._title.show(); + + if (opt.OVERVIEW_MODE === 1) { + // spread windows on hover + this._wsStateConId = this.connect('enter-event', () => { + // don't spread windows if user don't use pointer device at this moment + if (global.get_pointer()[0] === opt.showingPointerX || Main.overview._overview._controls._stateAdjustment.value < 1) + return; + + opt.WORKSPACE_MODE = 1; + const view = this._workspace.get_parent(); + view.exposeWindows(this._workspace.metaWorkspace.index()); + this.disconnect(this._wsStateConId); + }); + } + + if (opt.OVERVIEW_MODE) { + // show window icon and title on ws windows spread + this._stateAdjustmentSigId = this._workspace.stateAdjustment.connect('notify::value', this._updateIconScale.bind(this)); + } + + const metaWin = this.metaWindow; + if (opt.DASH_ISOLATE_WS && !metaWin._wsChangedConId) { + metaWin._wsChangedConId = metaWin.connect('workspace-changed', + () => Main.overview.dash._queueRedisplay()); + } else if (!opt.DASH_ISOLATE_WS && metaWin._wsChangedConId) { + metaWin.disconnect(metaWin._wsChangedConId); + } + }, + + _closeWinAction() { + this.hide(); + this._deleteAll(); + }, + + _removeLaters() { + if (this._longPressLater) { + const laters = global.compositor.get_laters(); + laters.remove(this._longPressLater); + delete this._longPressLater; + } + }, + + _searchAppWindowsAction() { + // this action cancels long-press event and the 'long-press-cancel' event is used by the Shell to actually initiate DnD + // so the dnd initiation needs to be removed + this._removeLaters(); + const tracker = Shell.WindowTracker.get_default(); + const appName = tracker.get_window_app(this.metaWindow).get_name(); + Me.Util.activateSearchProvider(`${Me.WSP_PREFIX} ${appName}`); + }, + + _updateIconScale() { + let { currentState, initialState, finalState } = + this._overviewAdjustment.getStateTransitionParams(); + + // Current state - 0 - HIDDEN, 1 - WINDOW_PICKER, 2 - APP_GRID + const primaryMonitor = this.metaWindow.get_monitor() === global.display.get_primary_monitor(); + + const visible = + (initialState > ControlsState.HIDDEN || finalState > ControlsState.HIDDEN) && + !(finalState === ControlsState.APP_GRID && opt.WS_ANIMATION && primaryMonitor); + + let scale = 0; + if (visible) + scale = currentState >= 1 ? 1 : currentState % 1; + + if (!primaryMonitor && opt.WORKSPACE_MODE && + ((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) || + (initialState === ControlsState.APP_GRID && finalState === ControlsState.WINDOW_PICKER)) + ) + scale = 1; + else if (!primaryMonitor && opt.OVERVIEW_MODE && !opt.WORKSPACE_MODE) + scale = 0; + /* } else if (primaryMonitor && ((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) || + initialState === ControlsState.APP_GRID && finalState === ControlsState.HIDDEN)) {*/ + else if (primaryMonitor && currentState > ControlsState.WINDOW_PICKER) + scale = 0; + + // in static workspace mode show icon and title on windows expose + if (opt.OVERVIEW_MODE) { + if (currentState === 1) + scale = opt.WORKSPACE_MODE; + else if (finalState === 1 || (finalState === 0 && !opt.WORKSPACE_MODE)) + return; + } + + if (!opt.WS_ANIMATION && (Main.overview._overview.controls._searchController.searchActive || + ((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) || + (initialState === ControlsState.APP_GRID && finalState === ControlsState.WINDOW_PICKER))) + ) + return; + + // if titles are in 'always show' mode, we need to add transition between visible/invisible state + // but the transition is quite expensive, + // showing the titles at the end of the transition is good enough and workspace preview transition is much smoother + if (scale === 1) { + this._icon.set({ + scale_x: 1, + scale_y: 1, + }); + this._title.ease({ + duration: 100, + opacity: 255, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } else { + this._title.opacity = 0; + this._icon.set({ + scale_x: scale, + scale_y: scale, + }); + } + }, + + showOverlay(animate) { + if (!this._overlayEnabled) + return; + + if (this._overlayShown) + return; + + this._overlayShown = true; + if (opt.WIN_TITLES_POSITION === 2) + this._restack(); + + // If we're supposed to animate and an animation in our direction + // is already happening, let that one continue + const ongoingTransition = this._title.get_transition('opacity'); + if (animate && + ongoingTransition && + ongoingTransition.get_interval().peek_final_value() === 255) + return; + + const toShow = this._windowCanClose() && opt.SHOW_CLOSE_BUTTON + ? [this._closeButton] + : []; + + if (!opt.ALWAYS_SHOW_WIN_TITLES) + toShow.push(this._title); + + + toShow.forEach(a => { + a.opacity = 0; + a.show(); + a.ease({ + opacity: 255, + duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + }); + + const [width, height] = this.window_container.get_size(); + const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); + const activeExtraSize = WINDOW_ACTIVE_SIZE_INC * 2 * scaleFactor; + const origSize = Math.max(width, height); + const scale = (origSize + activeExtraSize) / origSize; + + this.window_container.ease({ + scale_x: scale, + scale_y: scale, + duration: animate ? WINDOW_SCALE_TIME : 0, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + + this.emit('show-chrome'); + }, + + hideOverlay(animate) { + if (!this._overlayShown) + return; + this._overlayShown = false; + + if (opt.ALWAYS_ACTIVATE_SELECTED_WINDOW && Main.overview._overview.controls._stateAdjustment.value < 1) + this._activateSelected = true; + + + if (opt.WIN_TITLES_POSITION === 2) + this._restack(); + + // If we're supposed to animate and an animation in our direction + // is already happening, let that one continue + const ongoingTransition = this._title.get_transition('opacity'); + if (animate && + ongoingTransition && + ongoingTransition.get_interval().peek_final_value() === 0) + return; + + const toHide = [this._closeButton]; + + if (!opt.ALWAYS_SHOW_WIN_TITLES) + toHide.push(this._title); + + toHide.forEach(a => { + a.opacity = 255; + a.ease({ + opacity: 0, + duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => a.hide(), + }); + }); + + if (this.window_container) { + this.window_container.ease({ + scale_x: 1, + scale_y: 1, + duration: animate ? WINDOW_SCALE_TIME : 0, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + }, + + _onDestroy() { + if (this._activateSelected) + this._activate(); + + this.metaWindow._delegate = null; + this._delegate = null; + this._destroyed = true; + + if (this._longPressLater) { + const laters = global.compositor.get_laters(); + laters.remove(this._longPressLater); + delete this._longPressLater; + } + + if (this._idleHideOverlayId > 0) { + GLib.source_remove(this._idleHideOverlayId); + this._idleHideOverlayId = 0; + } + + if (this.inDrag) { + this.emit('drag-end'); + this.inDrag = false; + } + + if (this._stateAdjustmentSigId) + this._workspace.stateAdjustment.disconnect(this._stateAdjustmentSigId); + }, +}; |