/* * This file is part of the Dash-To-Panel extension for Gnome 3 * * 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 . */ import GObject from 'gi://GObject' import Clutter from 'gi://Clutter' import GLib from 'gi://GLib' import Graphene from 'gi://Graphene' import * as Main from 'resource:///org/gnome/shell/ui/main.js' import Meta from 'gi://Meta' import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js' import St from 'gi://St' import * as Taskbar from './taskbar.js' import * as Utils from './utils.js' import { SETTINGS, DESKTOPSETTINGS } from './extension.js' import { gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js' //timeout intervals const ENSURE_VISIBLE_MS = 200 //timeout names const T1 = 'openMenuTimeout' const T2 = 'closeMenuTimeout' const T3 = 'peekTimeout' const T4 = 'ensureVisibleTimeout' const MAX_TRANSLATION = 40 const HEADER_HEIGHT = 38 const MAX_CLOSE_BUTTON_SIZE = 30 const MIN_DIMENSION = 100 const FOCUSED_COLOR_OFFSET = 24 const HEADER_COLOR_OFFSET = -12 const FADE_SIZE = 36 const PEEK_INDEX_PROP = '_dtpPeekInitialIndex' let headerHeight = 0 let alphaBg = 0 let isLeftButtons = false let isTopHeader = true let isManualStyling = false let scaleFactor = 1 let animationTime = 0 let aspectRatio = {} export const PreviewMenu = GObject.registerClass( { Signals: { 'open-state-changed': {} }, }, class PreviewMenu extends St.Widget { _init(panel) { super._init({ layout_manager: new Clutter.BinLayout() }) let geom = panel.geom this.panel = panel this.currentAppIcon = null this._focusedPreview = null this._peekedWindow = null this.allowCloseWindow = true this.peekInitialWorkspaceIndex = -1 this.opened = false this.isVertical = geom.position == St.Side.LEFT || geom.position == St.Side.RIGHT this._translationProp = 'translation_' + (this.isVertical ? 'x' : 'y') this._translationDirection = geom.position == St.Side.TOP || geom.position == St.Side.LEFT ? -1 : 1 this._translationOffset = Math.min(panel.dtpSize, MAX_TRANSLATION) * this._translationDirection this.menu = new St.Widget({ name: 'preview-menu', layout_manager: new Clutter.BinLayout(), reactive: true, track_hover: true, x_expand: true, y_expand: true, x_align: Clutter.ActorAlign[geom.position != St.Side.RIGHT ? 'START' : 'END'], y_align: Clutter.ActorAlign[geom.position != St.Side.BOTTOM ? 'START' : 'END'], }) this._box = new St.BoxLayout({ vertical: this.isVertical }) this._scrollView = new St.ScrollView({ name: 'dashtopanelPreviewScrollview', hscrollbar_policy: St.PolicyType.NEVER, vscrollbar_policy: St.PolicyType.NEVER, enable_mouse_scrolling: true, y_expand: !this.isVertical, }) this._scrollView.add_child(this._box) this.menu.add_child(this._scrollView) this.add_child(this.menu) } enable() { this._timeoutsHandler = new Utils.TimeoutsHandler() this._signalsHandler = new Utils.GlobalSignalsHandler() Main.layoutManager.addChrome(this, { affectsInputRegion: false }) Main.layoutManager.trackChrome(this.menu, { affectsInputRegion: true }) this._resetHiddenState() this._refreshGlobals() this._updateClip() this.menu.set_position(1, 1) this._signalsHandler.add( [this.menu, 'notify::hover', () => this._onHoverChanged()], [this._scrollView, 'scroll-event', this._onScrollEvent.bind(this)], [this.panel.panelBox, 'style-changed', () => this._updateClip()], [ Utils.DisplayWrapper.getScreen(), 'in-fullscreen-changed', () => { if ( global.display.focus_window && global.display.focus_window.is_fullscreen() ) { this.close(true) } }, ], [ SETTINGS, [ 'changed::panel-sizes', 'changed::window-preview-size', 'changed::window-preview-padding', 'changed::window-preview-show-title', ], () => { this._refreshGlobals() this._updateClip() }, ], ) } disable() { this._timeoutsHandler.destroy() this._signalsHandler.destroy() this.close(true) Main.layoutManager.untrackChrome(this.menu) Main.layoutManager.removeChrome(this) } requestOpen(appIcon) { let timeout = SETTINGS.get_int('show-window-previews-timeout') if (this.opened) { timeout = Math.min(100, timeout) } this._endOpenCloseTimeouts() this._timeoutsHandler.add([T1, timeout, () => this.open(appIcon)]) } requestClose() { this._endOpenCloseTimeouts() this._addCloseTimeout() } open(appIcon, preventCloseWindow) { if (this.currentAppIcon != appIcon) { this.currentAppIcon = appIcon this.allowCloseWindow = !preventCloseWindow if (!this.opened) { this._refreshGlobals() this.set_height(this.clipHeight) this.show() setStyle( this.menu, 'background: ' + Utils.getrgbaColor( this.panel.dynamicTransparency.backgroundColorRgb, alphaBg, ), ) } this._mergeWindows(appIcon) this._updatePosition() this._animateOpenOrClose(true) this._setReactive(true) this._setOpenedState(true) } } close(immediate) { this._endOpenCloseTimeouts() this._removeFocus() this._endPeek() if (immediate) { Utils.stopAnimations(this.menu) this._resetHiddenState() } else { this._animateOpenOrClose(false, () => this._resetHiddenState()) } this._setReactive(false) this.currentAppIcon = null } update(appIcon, windows) { if (this.currentAppIcon == appIcon) { if (windows && !windows.length) { this.close() } else { this._addAndRemoveWindows(windows) this._updatePosition() } } } updatePosition() { this._updatePosition() } focusNext() { let previews = this._box.get_children() let currentIndex = this._focusedPreview ? previews.indexOf(this._focusedPreview) : -1 let nextIndex = currentIndex + 1 nextIndex = previews[nextIndex] ? nextIndex : 0 if (previews[nextIndex]) { this._removeFocus() previews[nextIndex].setFocus(true) this._focusedPreview = previews[nextIndex] } return nextIndex } activateFocused() { if (this.opened && this._focusedPreview) { this._focusedPreview.activate() } } requestPeek(window) { this._timeoutsHandler.remove(T3) if (SETTINGS.get_boolean('peek-mode')) { if (this.peekInitialWorkspaceIndex < 0) { this._timeoutsHandler.add([ T3, SETTINGS.get_int('enter-peek-mode-timeout'), () => this._peek(window), ]) } else { this._peek(window) } } } endPeekHere() { this._endPeek(true) } ensureVisible(preview) { let [, upper, pageSize] = this._getScrollAdjustmentValues() if (upper > pageSize) { this._timeoutsHandler.add([ T4, ENSURE_VISIBLE_MS, () => Utils.ensureActorVisibleInScrollView( this._scrollView, preview, MIN_DIMENSION, () => this._updateScrollFade(), ), ]) } } getCurrentAppIcon() { return this.currentAppIcon } _setReactive(reactive) { this._box.get_children().forEach((c) => (c.reactive = reactive)) this.menu.reactive = reactive } _setOpenedState(opened) { this.opened = opened this.emit('open-state-changed') } _resetHiddenState() { this.hide() this.set_height(0) this._setOpenedState(false) this.menu.opacity = 0 this.menu[this._translationProp] = this._translationOffset this._box.get_children().forEach((c) => c.destroy()) } _removeFocus() { if (this._focusedPreview) { this._focusedPreview.setFocus(false) this._focusedPreview = null } } _mergeWindows(appIcon, windows) { windows = windows || (appIcon.window ? [appIcon.window] : appIcon.getAppIconInterestingWindows()) windows.sort(Taskbar.sortWindowsCompareFunction) let currentPreviews = this._box.get_children() let l = Math.max(windows.length, currentPreviews.length) for (let i = 0; i < l; ++i) { if (currentPreviews[i] && windows[i]) { currentPreviews[i].assignWindow(windows[i], this.opened) } else if (!currentPreviews[i]) { this._addNewPreview(windows[i]) } else if (!windows[i]) { currentPreviews[i][!this.opened ? 'destroy' : 'animateOut']() } } } _addAndRemoveWindows(windows) { let currentPreviews = this._box.get_children() windows.sort(Taskbar.sortWindowsCompareFunction) for (let i = 0, l = windows.length; i < l; ++i) { let currentIndex = Utils.findIndex( currentPreviews, (c) => c.window == windows[i], ) if (currentIndex < 0) { this._addNewPreview(windows[i]) } else { currentPreviews[currentIndex].assignWindow(windows[i]) currentPreviews.splice(currentIndex, 1) if (this._peekedWindow && this._peekedWindow == windows[i]) { this.requestPeek(windows[i]) } } } currentPreviews.forEach((c) => c.animateOut()) } _addNewPreview(window) { let preview = new Preview(this) this._box.add_child(preview) preview.adjustOnStage() preview.assignWindow(window, this.opened) } _addCloseTimeout() { this._timeoutsHandler.add([ T2, SETTINGS.get_int('leave-timeout'), () => this.close(), ]) } _onHoverChanged() { this._endOpenCloseTimeouts() if (this.currentAppIcon && !this.menu.hover) { this._addCloseTimeout() this._endPeek() } } _onScrollEvent(actor, event) { if (!event.is_pointer_emulated()) { let vOrh = this.isVertical ? 'v' : 'h' let adjustment = this._scrollView['get_' + vOrh + 'scroll_bar']().get_adjustment() let increment = adjustment.step_increment let delta = increment switch (event.get_scroll_direction()) { case Clutter.ScrollDirection.UP: delta = -increment break case Clutter.ScrollDirection.SMOOTH: let [dx, dy] = event.get_scroll_delta() delta = dy * increment delta += dx * increment break } adjustment.set_value(adjustment.get_value() + delta) this._updateScrollFade() } return Clutter.EVENT_STOP } _endOpenCloseTimeouts() { this._timeoutsHandler.remove(T1) this._timeoutsHandler.remove(T2) this._timeoutsHandler.remove(T4) } _refreshGlobals() { isLeftButtons = Meta.prefs_get_button_layout().left_buttons.indexOf( Meta.ButtonFunction.CLOSE, ) >= 0 isTopHeader = SETTINGS.get_string('window-preview-title-position') == 'TOP' isManualStyling = SETTINGS.get_boolean('window-preview-manual-styling') scaleFactor = Utils.getScaleFactor() headerHeight = SETTINGS.get_boolean('window-preview-show-title') ? HEADER_HEIGHT * scaleFactor : 0 animationTime = SETTINGS.get_int('window-preview-animation-time') * 0.001 aspectRatio.x = { size: SETTINGS.get_int('window-preview-aspect-ratio-x'), fixed: SETTINGS.get_boolean('window-preview-fixed-x'), } aspectRatio.y = { size: SETTINGS.get_int('window-preview-aspect-ratio-y'), fixed: SETTINGS.get_boolean('window-preview-fixed-y'), } alphaBg = SETTINGS.get_boolean('preview-use-custom-opacity') ? SETTINGS.get_int('preview-custom-opacity') * 0.01 : this.panel.dynamicTransparency.alpha } _updateClip() { let x, y, w let geom = this.panel.getGeometry() let panelBoxTheme = this.panel.panelBox.get_theme_node() let previewSize = (SETTINGS.get_int('window-preview-size') + SETTINGS.get_int('window-preview-padding') * 2) * scaleFactor if (this.isVertical) { w = previewSize this.clipHeight = this.panel.monitor.height y = this.panel.monitor.y } else { w = this.panel.monitor.width this.clipHeight = previewSize + headerHeight x = this.panel.monitor.x } if (geom.position == St.Side.LEFT) { x = this.panel.monitor.x + this.panel.dtpSize + panelBoxTheme.get_padding(St.Side.LEFT) } else if (geom.position == St.Side.RIGHT) { x = this.panel.monitor.x + this.panel.monitor.width - (this.panel.dtpSize + previewSize) - panelBoxTheme.get_padding(St.Side.RIGHT) } else if (geom.position == St.Side.TOP) { y = this.panel.monitor.y + this.panel.dtpSize + panelBoxTheme.get_padding(St.Side.TOP) } else { //St.Side.BOTTOM y = this.panel.monitor.y + this.panel.monitor.height - (this.panel.dtpSize + panelBoxTheme.get_padding(St.Side.BOTTOM) + previewSize + headerHeight) } Utils.setClip(this, x, y, w, this.clipHeight) } _updatePosition() { let sourceNode = this.currentAppIcon.get_theme_node() let sourceContentBox = sourceNode.get_content_box( this.currentAppIcon.get_allocation_box(), ) let sourceAllocation = Utils.getTransformedAllocation(this.currentAppIcon) let [previewsWidth, previewsHeight] = this._getPreviewsSize() let appIconMargin = SETTINGS.get_int('appicon-margin') / scaleFactor let x = 0, y = 0 previewsWidth = Math.min(previewsWidth, this.panel.monitor.width) previewsHeight = Math.min(previewsHeight, this.panel.monitor.height) this._updateScrollFade( previewsWidth < this.panel.monitor.width && previewsHeight < this.panel.monitor.height, ) if (this.isVertical) { y = sourceAllocation.y1 + appIconMargin - this.panel.monitor.y + (sourceContentBox.y2 - sourceContentBox.y1 - previewsHeight) * 0.5 y = Math.max(y, 0) y = Math.min(y, this.panel.monitor.height - previewsHeight) } else { x = sourceAllocation.x1 + appIconMargin - this.panel.monitor.x + (sourceContentBox.x2 - sourceContentBox.x1 - previewsWidth) * 0.5 x = Math.max(x, 0) x = Math.min(x, this.panel.monitor.width - previewsWidth) } if (!this.opened) { this.menu.set_position(x, y) this.menu.set_size(previewsWidth, previewsHeight) } else { Utils.animate( this.menu, getTweenOpts({ x: x, y: y, width: previewsWidth, height: previewsHeight, }), ) } } _updateScrollFade(remove) { let [value, upper, pageSize] = this._getScrollAdjustmentValues() let needsFade = Math.round(upper) > Math.round(pageSize) let fadeWidgets = this.menu .get_children() .filter((c) => c != this._scrollView) if (!remove && needsFade) { if (!fadeWidgets.length) { fadeWidgets.push(this._getFadeWidget()) fadeWidgets.push(this._getFadeWidget(true)) this.menu.add_child(fadeWidgets[0]) this.menu.add_child(fadeWidgets[1]) } fadeWidgets[0].visible = value > 0 fadeWidgets[1].visible = value + pageSize < upper } else if (remove || (!needsFade && fadeWidgets.length)) { fadeWidgets.forEach((fw) => fw.destroy()) } } _getScrollAdjustmentValues() { let [value, , upper, , , pageSize] = this._scrollView[ (this.isVertical ? 'v' : 'h') + 'adjustment' ].get_values() return [value, upper, pageSize] } _getFadeWidget(end) { let x = 0, y = 0 let startBg = Utils.getrgbaColor( this.panel.dynamicTransparency.backgroundColorRgb, Math.min(alphaBg + 0.1, 1), ) let endBg = Utils.getrgbaColor( this.panel.dynamicTransparency.backgroundColorRgb, 0, ) let fadeStyle = 'background-gradient-start:' + startBg + 'background-gradient-end:' + endBg + 'background-gradient-direction:' + this.panel.getOrientation() if (this.isVertical) { y = end ? this.panel.monitor.height - FADE_SIZE : 0 } else { x = end ? this.panel.monitor.width - FADE_SIZE : 0 } let fadeWidget = new St.Widget({ reactive: false, pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }), rotation_angle_z: end ? 180 : 0, style: fadeStyle, x: x, y: y, width: this.isVertical ? this.width : FADE_SIZE, height: this.isVertical ? FADE_SIZE : this.height, }) return fadeWidget } _getPreviewsSize() { let previewsWidth = 0 let previewsHeight = 0 this._box.get_children().forEach((c) => { if (!c.animatingOut) { let [width, height] = c.getSize() if (this.isVertical) { previewsWidth = Math.max(width, previewsWidth) previewsHeight += height } else { previewsWidth += width previewsHeight = Math.max(height, previewsHeight) } } }) return [previewsWidth, previewsHeight] } _animateOpenOrClose(show, onComplete) { let isTranslationAnimation = this.menu[this._translationProp] != 0 let tweenOpts = { opacity: show ? 255 : 0, transition: show ? 'easeInOutQuad' : 'easeInCubic', onComplete: () => { if (isTranslationAnimation) { Main.layoutManager._queueUpdateRegions() } ;(onComplete || (() => {}))() }, } tweenOpts[this._translationProp] = show ? this._translationDirection : this._translationOffset Utils.animate(this.menu, getTweenOpts(tweenOpts)) } _peek(window) { let currentWorkspace = Utils.getCurrentWorkspace() let windowWorkspace = window.get_workspace() let focusWindow = () => this._focusMetaWindow(SETTINGS.get_int('peek-mode-opacity'), window) this._restorePeekedWindowStack() if (this._peekedWindow && windowWorkspace != currentWorkspace) { currentWorkspace .list_windows() .forEach((mw) => this.animateWindowOpacity(mw, null, 255)) } this._peekedWindow = window if (currentWorkspace != windowWorkspace) { this._switchToWorkspaceImmediate(windowWorkspace.index()) this._timeoutsHandler.add([T3, 100, focusWindow]) } else { focusWindow() } if (this.peekInitialWorkspaceIndex < 0) { this.peekInitialWorkspaceIndex = currentWorkspace.index() } } _endPeek(stayHere) { this._timeoutsHandler.remove(T3) if (this._peekedWindow) { let immediate = !stayHere && this.peekInitialWorkspaceIndex != Utils.getCurrentWorkspace().index() this._restorePeekedWindowStack() this._focusMetaWindow(255, this._peekedWindow, immediate, true) this._peekedWindow = null if (!stayHere) { this._switchToWorkspaceImmediate(this.peekInitialWorkspaceIndex) } this.peekInitialWorkspaceIndex = -1 } } _switchToWorkspaceImmediate(workspaceIndex) { let workspace = Utils.getWorkspaceByIndex(workspaceIndex) let shouldAnimate = Main.wm._shouldAnimate if ( !workspace || (!workspace.list_windows().length && workspaceIndex < Utils.getWorkspaceCount() - 1) ) { workspace = Utils.getCurrentWorkspace() } Main.wm._shouldAnimate = () => false workspace.activate(global.display.get_current_time_roundtrip()) Main.wm._shouldAnimate = shouldAnimate } _focusMetaWindow(dimOpacity, window, immediate, ignoreFocus) { window .get_workspace() .list_windows() .forEach((mw) => { let wa = mw.get_compositor_private() let isFocused = !ignoreFocus && mw == window if (wa) { if (isFocused) { mw[PEEK_INDEX_PROP] = wa.get_parent().get_children().indexOf(wa) wa.get_parent().set_child_above_sibling(wa, null) } if (isFocused && mw.minimized) { wa.show() } this.animateWindowOpacity( mw, wa, isFocused ? 255 : dimOpacity, immediate, ) } }) } animateWindowOpacity(metaWindow, windowActor, opacity, immediate) { windowActor = windowActor || metaWindow.get_compositor_private() if (windowActor && !metaWindow.minimized) { let tweenOpts = getTweenOpts({ opacity }) if (immediate && !metaWindow.is_on_all_workspaces()) { tweenOpts.time = 0 } Utils.animateWindowOpacity(windowActor, tweenOpts) } } _restorePeekedWindowStack() { let windowActor = this._peekedWindow ? this._peekedWindow.get_compositor_private() : null if (windowActor) { if (this._peekedWindow.hasOwnProperty(PEEK_INDEX_PROP)) { windowActor .get_parent() .set_child_at_index( windowActor, this._peekedWindow[PEEK_INDEX_PROP], ) delete this._peekedWindow[PEEK_INDEX_PROP] } if (this._peekedWindow.minimized) { windowActor.hide() } } } }, ) export const Preview = GObject.registerClass( {}, class Preview extends St.Widget { _init(previewMenu) { super._init({ style_class: 'preview-container', reactive: true, track_hover: true, layout_manager: new Clutter.BinLayout(), }) this.window = null this._waitWindowId = 0 this._needsCloseButton = true this.cloneWidth = this.cloneHeight = 0 this._previewMenu = previewMenu this._padding = SETTINGS.get_int('window-preview-padding') * scaleFactor this._previewDimensions = this._getPreviewDimensions() this.animatingOut = false let box = new St.Widget({ layout_manager: new Clutter.BoxLayout({ orientation: Clutter.Orientation.VERTICAL, }), y_expand: true, }) let [previewBinWidth, previewBinHeight] = this._getBinSize() let closeButton = new St.Button({ style_class: 'window-close', accessible_name: 'Close window', }) closeButton.add_child(new St.Icon({ icon_name: 'window-close-symbolic' })) this._closeButtonBin = new St.Widget({ style_class: 'preview-close-btn-container', layout_manager: new Clutter.BinLayout(), opacity: 0, x_expand: true, y_expand: true, x_align: Clutter.ActorAlign[isLeftButtons ? 'START' : 'END'], y_align: Clutter.ActorAlign[isTopHeader ? 'START' : 'END'], }) this._closeButtonBin.add_child(closeButton) this._previewBin = new St.Widget({ layout_manager: new Clutter.BinLayout(), x_expand: true, y_expand: true, style: 'padding: ' + this._padding / scaleFactor + 'px;', }) this._previewBin.set_size(previewBinWidth, previewBinHeight) box.add_child(this._previewBin) if (headerHeight) { let headerBox = new St.Widget({ style_class: 'preview-header-box', layout_manager: new Clutter.BoxLayout(), x_expand: true, y_align: Clutter.ActorAlign[isTopHeader ? 'START' : 'END'], }) setStyle(headerBox, this._getBackgroundColor(HEADER_COLOR_OFFSET, 1)) this._workspaceIndicator = new St.Label({ y_align: Clutter.ActorAlign.CENTER, }) this._windowTitle = new St.Label({ y_align: Clutter.ActorAlign.CENTER, x_expand: true, }) this._iconBin = new St.Widget({ layout_manager: new Clutter.BinLayout(), }) this._iconBin.set_size(headerHeight, headerHeight) headerBox.add_child(this._iconBin) headerBox.insert_child_at_index( this._workspaceIndicator, isLeftButtons ? 0 : 1, ) headerBox.insert_child_at_index( this._windowTitle, isLeftButtons ? 1 : 2, ) box.insert_child_at_index(headerBox, isTopHeader ? 0 : 1) } this.add_child(box) this.add_child(this._closeButtonBin) closeButton.connect('clicked', () => this._onCloseBtnClick()) this.connect('notify::hover', () => this._onHoverChanged()) this.connect('button-release-event', (actor, e) => this._onButtonReleaseEvent(e), ) this.connect('destroy', () => this._onDestroy()) } adjustOnStage() { let closeButton = this._closeButtonBin.get_first_child() let closeButtonHeight = closeButton.height let maxCloseButtonSize = MAX_CLOSE_BUTTON_SIZE * scaleFactor let closeButtonBorderRadius = '' if (closeButtonHeight > maxCloseButtonSize) { closeButtonHeight = maxCloseButtonSize closeButton.set_size(closeButtonHeight, closeButtonHeight) } if (!headerHeight) { closeButtonBorderRadius = 'border-radius: ' if (isTopHeader) { closeButtonBorderRadius += isLeftButtons ? '0 0 4px 0;' : '0 0 0 4px;' } else { closeButtonBorderRadius += isLeftButtons ? '0 4px 0 0;' : '4px 0 0 0;' } } setStyle( this._closeButtonBin, 'padding: ' + (headerHeight ? Math.round( ((headerHeight - closeButtonHeight) * 0.5) / scaleFactor, ) : 4) + 'px;' + this._getBackgroundColor( HEADER_COLOR_OFFSET, headerHeight ? 1 : 0.6, ) + closeButtonBorderRadius, ) } assignWindow(window, animateSize) { if (this.window != window) { let _assignWindowClone = () => { if (window.get_compositor_private()) { let cloneBin = this._getWindowCloneBin(window) this._resizeClone(cloneBin, window) this._addClone(cloneBin, animateSize) this._previewMenu.updatePosition() } else if (!this._waitWindowId) { this._waitWindowId = GLib.idle_add( GLib.PRIORITY_DEFAULT_IDLE, () => { this._waitWindowId = 0 if (this._previewMenu.opened) { _assignWindowClone() } return GLib.SOURCE_REMOVE }, ) } } _assignWindowClone() } this._cancelAnimateOut() this._removeWindowSignals() this.window = window this._needsCloseButton = this._previewMenu.allowCloseWindow && window.can_close() && !Utils.checkIfWindowHasTransient(window) this._updateHeader() } animateOut() { if (!this.animatingOut) { let tweenOpts = getTweenOpts({ opacity: 0, width: 0, height: 0, onComplete: () => this.destroy(), }) this.animatingOut = true Utils.stopAnimations(this) Utils.animate(this, tweenOpts) } } getSize() { let [binWidth, binHeight] = this._getBinSize() binWidth = Math.max(binWidth, this.cloneWidth + this._padding * 2) binHeight = Math.max(binHeight, this.cloneHeight + this._padding * 2) + headerHeight return [binWidth, binHeight] } setFocus(focused) { this._hideOrShowCloseButton(!focused) setStyle( this, this._getBackgroundColor(FOCUSED_COLOR_OFFSET, focused ? '-' : 0), ) if (focused) { this._previewMenu.ensureVisible(this) this._previewMenu.requestPeek(this.window) } } activate() { this._previewMenu.endPeekHere() this._previewMenu.close() Main.activateWindow(this.window) } _onDestroy() { if (this._waitWindowId) { GLib.source_remove(this._waitWindowId) this._waitWindowId = 0 } this._removeWindowSignals() } _onHoverChanged() { this.setFocus(this.hover) } _onCloseBtnClick() { this._hideOrShowCloseButton(true) this.reactive = false if (!SETTINGS.get_boolean('group-apps')) { this._previewMenu.close() } else { this._previewMenu.endPeekHere() } this.window.delete(global.get_current_time()) } _onButtonReleaseEvent(e) { switch (e.get_button()) { case 1: // Left click this.activate() break case 2: // Middle click if (SETTINGS.get_boolean('preview-middle-click-close')) { this._onCloseBtnClick() } break case 3: // Right click this._showContextMenu(e) break } return Clutter.EVENT_STOP } _cancelAnimateOut() { if (this.animatingOut) { this.animatingOut = false Utils.stopAnimations(this) Utils.animate( this, getTweenOpts({ opacity: 255, width: this.cloneWidth, height: this.cloneHeight, }), ) } } _showContextMenu(e) { let coords = e.get_coords() let currentWorkspace = this._previewMenu.peekInitialWorkspaceIndex < 0 ? Utils.getCurrentWorkspace() : Utils.getWorkspaceByIndex( this._previewMenu.peekInitialWorkspaceIndex, ) Main.wm._showWindowMenu(null, this.window, Meta.WindowMenuType.WM, { x: coords[0], y: coords[1], width: 0, height: 0, }) let menu = Main.wm._windowMenuManager._manager._menus[0] menu.connect('open-state-changed', () => this._previewMenu.menu.sync_hover(), ) this._previewMenu.menu.sync_hover() if (this.window.get_workspace() != currentWorkspace) { let menuItem = new PopupMenu.PopupMenuItem( _('Move to current Workspace') + ' [' + (currentWorkspace.index() + 1) + ']', ) let menuItems = menu.box.get_children() let insertIndex = Utils.findIndex( menuItems, (c) => c._delegate instanceof PopupMenu.PopupSeparatorMenuItem, ) insertIndex = insertIndex >= 0 ? insertIndex : menuItems.length - 1 menu.addMenuItem(menuItem, insertIndex) menuItem.connect('activate', () => this.window.change_workspace(currentWorkspace), ) } } _removeWindowSignals() { if (this._titleWindowChangeId) { this.window.disconnect(this._titleWindowChangeId) this._titleWindowChangeId = 0 } } _updateHeader() { if (headerHeight) { let iconTextureSize = SETTINGS.get_boolean( 'window-preview-use-custom-icon-size', ) ? SETTINGS.get_int('window-preview-custom-icon-size') : (headerHeight / scaleFactor) * 0.6 let icon = this._previewMenu .getCurrentAppIcon() .app.create_icon_texture(iconTextureSize) let workspaceIndex = '' let workspaceStyle = null let fontScale = DESKTOPSETTINGS.get_double('text-scaling-factor') let commonTitleStyles = 'color: ' + SETTINGS.get_string('window-preview-title-font-color') + ';' + 'font-size: ' + SETTINGS.get_int('window-preview-title-font-size') * fontScale + 'px;' + 'font-weight: ' + SETTINGS.get_string('window-preview-title-font-weight') + ';' this._iconBin.destroy_all_children() this._iconBin.add_child(icon) if (!SETTINGS.get_boolean('isolate-workspaces')) { workspaceIndex = (this.window.get_workspace().index() + 1).toString() workspaceStyle = 'margin: 0 4px 0 ' + (isLeftButtons ? Math.round((headerHeight - icon.width) * 0.5) + 'px' : '0') + '; padding: 0 4px;' + 'border: 2px solid ' + this._getRgbaColor(FOCUSED_COLOR_OFFSET, 0.8) + 'border-radius: 2px;' + commonTitleStyles } this._workspaceIndicator.text = workspaceIndex setStyle(this._workspaceIndicator, workspaceStyle) this._titleWindowChangeId = this.window.connect('notify::title', () => this._updateWindowTitle(), ) setStyle( this._windowTitle, 'max-width: 0px; padding-right: 4px;' + commonTitleStyles, ) this._updateWindowTitle() } } _updateWindowTitle() { this._windowTitle.text = this.window.title } _hideOrShowCloseButton(hide) { if (this._needsCloseButton) { Utils.animate( this._closeButtonBin, getTweenOpts({ opacity: hide ? 0 : 255 }), ) } } _getBackgroundColor(offset, alpha) { return ( 'background-color: ' + this._getRgbaColor(offset, alpha) + 'transition-duration:' + this._previewMenu.panel.dynamicTransparency.animationDuration ) } _getRgbaColor(offset, alpha) { alpha = Math.abs(alpha) if (isNaN(alpha)) { alpha = alphaBg } return Utils.getrgbaColor( this._previewMenu.panel.dynamicTransparency.backgroundColorRgb, alpha, offset, ) } _addClone(newCloneBin, animateSize) { let currentClones = this._previewBin.get_children() let newCloneOpts = getTweenOpts({ opacity: 255 }) this._previewBin.add_child(newCloneBin) if (currentClones.length) { let currentCloneBin = currentClones.pop() let currentCloneOpts = getTweenOpts({ opacity: 0, onComplete: () => currentCloneBin.destroy(), }) if (newCloneBin.width > currentCloneBin.width) { newCloneOpts.width = newCloneBin.width newCloneBin.width = currentCloneBin.width } else { currentCloneOpts.width = newCloneBin.width } if (newCloneBin.height > currentCloneBin.height) { newCloneOpts.height = newCloneBin.height newCloneBin.height = currentCloneBin.height } else { currentCloneOpts.height = newCloneBin.height } currentClones.forEach((c) => c.destroy()) Utils.animate(currentCloneBin, currentCloneOpts) } else if (animateSize) { newCloneBin.width = 0 newCloneBin.height = 0 newCloneOpts.width = this.cloneWidth newCloneOpts.height = this.cloneHeight } Utils.animate(newCloneBin, newCloneOpts) } _getWindowCloneBin(window) { let frameRect = window.get_frame_rect() let bufferRect = window.get_buffer_rect() let clone = new Clutter.Clone({ source: window.get_compositor_private() }) let cloneBin = new St.Widget({ opacity: 0, layout_manager: frameRect.width != bufferRect.width || frameRect.height != bufferRect.height ? new WindowCloneLayout(frameRect, bufferRect) : new Clutter.BinLayout(), }) cloneBin.add_child(clone) return cloneBin } _getBinSize() { let [fixedWidth, fixedHeight] = this._previewDimensions return [ aspectRatio.x.fixed ? fixedWidth + this._padding * 2 : -1, aspectRatio.y.fixed ? fixedHeight + this._padding * 2 : -1, ] } _resizeClone(cloneBin, window) { let frameRect = cloneBin.layout_manager.frameRect || window.get_frame_rect() let [fixedWidth, fixedHeight] = this._previewDimensions let ratio = Math.min( fixedWidth / frameRect.width, fixedHeight / frameRect.height, 1, ) let cloneWidth = frameRect.width * ratio let cloneHeight = frameRect.height * ratio let clonePaddingTB = cloneHeight < MIN_DIMENSION ? MIN_DIMENSION - cloneHeight : 0 let clonePaddingLR = cloneWidth < MIN_DIMENSION ? MIN_DIMENSION - cloneWidth : 0 let clonePaddingTop = clonePaddingTB * 0.5 let clonePaddingLeft = clonePaddingLR * 0.5 this.cloneWidth = cloneWidth + clonePaddingLR * scaleFactor this.cloneHeight = cloneHeight + clonePaddingTB * scaleFactor cloneBin.set_style( 'padding: ' + clonePaddingTop + 'px ' + clonePaddingLeft + 'px;', ) cloneBin.layout_manager.ratio = ratio cloneBin.layout_manager.padding = [ clonePaddingLeft * scaleFactor, clonePaddingTop * scaleFactor, ] cloneBin.get_first_child().set_size(cloneWidth, cloneHeight) } _getPreviewDimensions() { let size = SETTINGS.get_int('window-preview-size') * scaleFactor let w, h if (this._previewMenu.isVertical) { w = size h = (w * aspectRatio.y.size) / aspectRatio.x.size } else { h = size w = (h * aspectRatio.x.size) / aspectRatio.y.size } return [w, h] } }, ) export const WindowCloneLayout = GObject.registerClass( {}, class WindowCloneLayout extends Clutter.BinLayout { _init(frameRect, bufferRect) { super._init() //the buffer_rect contains the transparent padding that must be removed this.frameRect = frameRect this.bufferRect = bufferRect } vfunc_allocate(actor, box) { let [width, height] = box.get_size() box.set_origin( (this.bufferRect.x - this.frameRect.x) * this.ratio + this.padding[0], (this.bufferRect.y - this.frameRect.y) * this.ratio + this.padding[1], ) box.set_size( width + (this.bufferRect.width - this.frameRect.width) * this.ratio, height + (this.bufferRect.height - this.frameRect.height) * this.ratio, ) actor.get_first_child().allocate(box) } }, ) export function setStyle(actor, style) { if (!isManualStyling) { actor.set_style(style) } } export function getTweenOpts(opts) { let defaults = { time: animationTime, transition: 'easeInOutQuad', } return Utils.mergeObjects(opts || {}, defaults) }