diff options
Diffstat (limited to 'raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/windowPreview.js')
| -rw-r--r-- | raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/windowPreview.js | 678 |
1 files changed, 678 insertions, 0 deletions
diff --git a/raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/windowPreview.js b/raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/windowPreview.js new file mode 100644 index 0000000..337d5f1 --- /dev/null +++ b/raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/windowPreview.js @@ -0,0 +1,678 @@ +/* + * Credits: + * This file is based on code from the Dash to Panel extension by Jason DeRose + * and code from the Taskbar extension by Zorin OS + * Some code was also adapted from the upstream Gnome Shell source code. + */ + +import { + Clutter, + GLib, + GObject, + Meta, + St, +} from './dependencies/gi.js'; + +import { + BoxPointer, + Main, + PopupMenu, + Workspace, +} from './dependencies/shell/ui.js'; + +import { + Docking, + Theming, + Utils, +} from './imports.js'; + +const PREVIEW_MAX_WIDTH = 250; +const PREVIEW_MAX_HEIGHT = 150; + +const PREVIEW_ANIMATION_DURATION = 250; +const MAX_PREVIEW_GENERATION_ATTEMPTS = 15; + +const MENU_MARGINS = 10; + +export class WindowPreviewMenu extends PopupMenu.PopupMenu { + constructor(source) { + super(source, 0.5, Utils.getPosition()); + + // We want to keep the item hovered while the menu is up + this.blockSourceEvents = true; + + this._source = source; + this._app = this._source.app; + const workArea = Main.layoutManager.getWorkAreaForMonitor( + this._source.monitorIndex); + const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); + + this.actor.add_style_class_name('app-menu'); + this.actor.set_style( + `max-width: ${Math.round(workArea.width / scaleFactor) - MENU_MARGINS}px; ` + + `max-height: ${Math.round(workArea.height / scaleFactor) - MENU_MARGINS}px;`); + this.actor.hide(); + + // Chain our visibility and lifecycle to that of the source + this._mappedId = this._source.connect('notify::mapped', () => { + if (!this._source.mapped) + this.close(); + }); + this._destroyId = this._source.connect('destroy', this.destroy.bind(this)); + + Utils.addActor(Main.uiGroup, this.actor); + + this.connect('destroy', this._onDestroy.bind(this)); + } + + _redisplay() { + if (this._previewBox) + this._previewBox.destroy(); + this._previewBox = new WindowPreviewList(this._source); + this.addMenuItem(this._previewBox); + this._previewBox._redisplay(); + } + + popup() { + const windows = this._source.getInterestingWindows(); + if (windows.length > 0) { + this._redisplay(); + this.open(BoxPointer.PopupAnimation.FULL); + this.actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false); + this._source.emit('sync-tooltip'); + } + } + + _onDestroy() { + if (this._mappedId) + this._source.disconnect(this._mappedId); + + if (this._destroyId) + this._source.disconnect(this._destroyId); + } +} + +class WindowPreviewList extends PopupMenu.PopupMenuSection { + constructor(source) { + super(); + this.actor = new St.ScrollView({ + name: 'dashtodockWindowScrollview', + hscrollbar_policy: St.PolicyType.NEVER, + vscrollbar_policy: St.PolicyType.NEVER, + overlay_scrollbars: true, + enable_mouse_scrolling: true, + }); + + this.actor.connect('scroll-event', this._onScrollEvent.bind(this)); + + const position = Utils.getPosition(); + this.isHorizontal = position === St.Side.BOTTOM || position === St.Side.TOP; + this.box.set_vertical(!this.isHorizontal); + this.box.set_name('dashtodockWindowList'); + Utils.addActor(this.actor, this.box); + this.actor._delegate = this; + + this._shownInitially = false; + + this._source = source; + this.app = source.app; + + this._redisplayId = Main.initializeDeferredWork(this.actor, this._redisplay.bind(this)); + + this.actor.connect('destroy', this._onDestroy.bind(this)); + this._stateChangedId = this.app.connect('windows-changed', + this._queueRedisplay.bind(this)); + } + + _queueRedisplay() { + Main.queueDeferredWork(this._redisplayId); + } + + _onScrollEvent(actor, event) { + // Event coordinates are relative to the stage but can be transformed + // as the actor will only receive events within his bounds. + const [stageX, stageY] = event.get_coords(); + const [,, eventY] = actor.transform_stage_point(stageX, stageY); + const [, actorH] = actor.get_size(); + + // If the scroll event is within a 1px margin from + // the relevant edge of the actor, let the event propagate. + if (eventY >= actorH - 2) + return Clutter.EVENT_PROPAGATE; + + // Skip to avoid double events mouse + if (event.is_pointer_emulated()) + return Clutter.EVENT_STOP; + + let adjustment, delta; + + if (this.isHorizontal) + adjustment = this.actor.get_hscroll_bar().get_adjustment(); + else + adjustment = this.actor.get_vscroll_bar().get_adjustment(); + + const increment = adjustment.step_increment; + + switch (event.get_scroll_direction()) { + case Clutter.ScrollDirection.UP: + delta = -increment; + break; + case Clutter.ScrollDirection.DOWN: + delta = Number(increment); + break; + case Clutter.ScrollDirection.SMOOTH: { + const [dx, dy] = event.get_scroll_delta(); + delta = dy * increment; + delta += dx * increment; + break; + } + } + + adjustment.set_value(adjustment.get_value() + delta); + + return Clutter.EVENT_STOP; + } + + _onDestroy() { + this.app.disconnect(this._stateChangedId); + this._stateChangedId = 0; + } + + _createPreviewItem(window) { + const preview = new WindowPreviewMenuItem(window, Utils.getPosition()); + return preview; + } + + _redisplay() { + const children = this._getMenuItems().filter(actor => { + return actor._window; + }); + + // Windows currently on the menu + const oldWin = children.map(actor => { + return actor._window; + }); + + // All app windows with a static order + const newWin = this._source.getInterestingWindows().sort((a, b) => + a.get_stable_sequence() > b.get_stable_sequence()); + + const addedItems = []; + const removedActors = []; + + let newIndex = 0; + let oldIndex = 0; + + while (newIndex < newWin.length || oldIndex < oldWin.length) { + const currentOldWin = oldWin[oldIndex]; + const currentNewWin = newWin[newIndex]; + + // No change at oldIndex/newIndex + if (currentOldWin === currentNewWin) { + oldIndex++; + newIndex++; + continue; + } + + // Window removed at oldIndex + if (currentOldWin && !newWin.includes(currentOldWin)) { + removedActors.push(children[oldIndex]); + oldIndex++; + continue; + } + + // Window added at newIndex + if (currentNewWin && !oldWin.includes(currentNewWin)) { + addedItems.push({ + item: this._createPreviewItem(currentNewWin), + pos: newIndex, + }); + newIndex++; + continue; + } + + // Window moved + const insertHere = newWin[newIndex + 1] && + newWin[newIndex + 1] === currentOldWin; + const alreadyRemoved = removedActors.reduce((result, actor) => + result || actor._window === currentNewWin, false); + + if (insertHere || alreadyRemoved) { + addedItems.push({ + item: this._createPreviewItem(currentNewWin), + pos: newIndex + removedActors.length, + }); + newIndex++; + } else { + removedActors.push(children[oldIndex]); + oldIndex++; + } + } + + for (let i = 0; i < addedItems.length; i++) { + this.addMenuItem(addedItems[i].item, + addedItems[i].pos); + } + + for (let i = 0; i < removedActors.length; i++) { + const item = removedActors[i]; + if (this._shownInitially) + item._animateOutAndDestroy(); + else + item.actor.destroy(); + } + + // Skip animations on first run when adding the initial set + // of items, to avoid all items zooming in at once + const animate = this._shownInitially; + + if (!this._shownInitially) + this._shownInitially = true; + + for (let i = 0; i < addedItems.length; i++) + addedItems[i].item.show(animate); + + // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744 + // Without it, StBoxLayout may use a stale size cache + this.box.queue_relayout(); + + if (newWin.length < 1) + this._getTopMenu().close(~0); + + // As for upstream: + // St.ScrollView always requests space horizontally for a possible vertical + // scrollbar if in AUTOMATIC mode. Doing better would require implementation + // of width-for-height in St.BoxLayout and St.ScrollView. This looks bad + // when we *don't* need it, so turn off the scrollbar when that's true. + // Dynamic changes in whether we need it aren't handled properly. + const needsScrollbar = this._needsScrollbar(); + const scrollbarPolicy = needsScrollbar + ? St.PolicyType.AUTOMATIC : St.PolicyType.NEVER; + if (this.isHorizontal) + this.actor.hscrollbarPolicy = scrollbarPolicy; + else + this.actor.vscrollbarPolicy = scrollbarPolicy; + + if (needsScrollbar) + this.actor.add_style_pseudo_class('scrolled'); + else + this.actor.remove_style_pseudo_class('scrolled'); + } + + _needsScrollbar() { + const topMenu = this._getTopMenu(); + const topThemeNode = topMenu.actor.get_theme_node(); + if (this.isHorizontal) { + const [topMinWidth_, topNaturalWidth] = + topMenu.actor.get_preferred_width(-1); + const topMaxWidth = topThemeNode.get_max_width(); + return topMaxWidth >= 0 && topNaturalWidth >= topMaxWidth; + } else { + const [topMinHeight_, topNaturalHeight] = + topMenu.actor.get_preferred_height(-1); + const topMaxHeight = topThemeNode.get_max_height(); + return topMaxHeight >= 0 && topNaturalHeight >= topMaxHeight; + } + } + + isAnimatingOut() { + return this.actor.get_children().reduce((result, actor) => { + return result || actor.animatingOut; + }, false); + } +} + +export const WindowPreviewMenuItem = GObject.registerClass( +class WindowPreviewMenuItem extends PopupMenu.PopupBaseMenuItem { + _init(window, position, params) { + super._init(params); + + this._window = window; + this._destroyId = 0; + this._windowAddedId = 0; + + // We don't want this: it adds spacing on the left of the item. + this.remove_child(this._ornamentIcon); + this.add_style_class_name('dashtodock-app-well-preview-menu-item'); + this.add_style_class_name(Theming.PositionStyleClass[position]); + if (Docking.DockManager.settings.customThemeShrink) + this.add_style_class_name('shrink'); + + // Now we don't have to set PREVIEW_MAX_WIDTH and PREVIEW_MAX_HEIGHT as + // preview size - that made all kinds of windows either stretched or + // squished (aspect ratio problem) + this._cloneBin = new St.Bin(); + + this._updateWindowPreviewSize(); + + // TODO: improve the way the closebutton is layout. Just use some padding + // for the moment. + this._cloneBin.set_style('padding-bottom: 0.5em'); + + const buttonLayout = Meta.prefs_get_button_layout(); + this.closeButton = new St.Button({ + style_class: 'window-close', + opacity: 0, + x_expand: true, + y_expand: true, + x_align: buttonLayout.left_buttons.includes(Meta.ButtonFunction.CLOSE) + ? Clutter.ActorAlign.START : Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.START, + }); + Utils.addActor(this.closeButton, new St.Icon({icon_name: 'window-close-symbolic'})); + this.closeButton.connect('clicked', () => this._closeWindow()); + + const overlayGroup = new Clutter.Actor({ + layout_manager: new Clutter.BinLayout(), + y_expand: true, + }); + + overlayGroup.add_child(this._cloneBin); + overlayGroup.add_child(this.closeButton); + + const label = new St.Label({text: window.get_title()}); + label.set_style(`max-width: ${PREVIEW_MAX_WIDTH}px`); + const labelBin = new St.Bin({ + child: label, + x_align: Clutter.ActorAlign.CENTER, + }); + + this._windowTitleId = this._window.connect('notify::title', () => { + label.set_text(this._window.get_title()); + }); + + const box = new St.BoxLayout({ + vertical: true, + reactive: true, + x_expand: true, + }); + + if (box.add) { + box.add(overlayGroup); + box.add(labelBin); + } else { + box.add_child(overlayGroup); + box.add_child(labelBin); + } + this._box = box; + this.add_child(box); + + this._cloneTexture(window); + + this.connect('destroy', this._onDestroy.bind(this)); + } + + vfunc_style_changed() { + super.vfunc_style_changed(); + + // For some crazy clutter / St reason we can't just have this handled + // automatically or here via vfunc_allocate + vfunc_get_preferred_* + // because if we do so, the St paddings on first / last child are lost + const themeNode = this.get_theme_node(); + let [minWidth, naturalWidth] = this._box.get_preferred_width(-1); + let [minHeight, naturalHeight] = this._box.get_preferred_height(naturalWidth); + [minWidth, naturalWidth] = themeNode.adjust_preferred_width(minWidth, naturalWidth); + [minHeight, naturalHeight] = themeNode.adjust_preferred_height(minHeight, naturalHeight); + this.set({minWidth, naturalWidth, minHeight, naturalHeight}); + } + + _getWindowPreviewSize() { + const emptySize = [0, 0, 0]; + + const mutterWindow = this._window.get_compositor_private(); + if (!mutterWindow?.get_texture()) + return emptySize; + + const [width, height] = mutterWindow.get_size(); + if (!width || !height) + return emptySize; + + let {previewSizeScale: scale} = Docking.DockManager.settings; + if (!scale) { + // a simple example with 1680x1050: + // * 250/1680 = 0,1488 + // * 150/1050 = 0,1429 + // => scale is 0,1429 + scale = Math.min(1.0, PREVIEW_MAX_WIDTH / width, PREVIEW_MAX_HEIGHT / height); + } + + scale *= St.ThemeContext.get_for_stage(global.stage).scaleFactor; + + // width and height that we wanna multiply by scale + return [width, height, scale]; + } + + _updateWindowPreviewSize() { + // This gets the actual windows size for the preview + [this._width, this._height, this._scale] = this._getWindowPreviewSize(); + this._cloneBin.set_size(this._width * this._scale, this._height * this._scale); + } + + _cloneTexture(metaWin) { + // Newly-created windows are added to a workspace before + // the compositor finds out about them... + if (!this._width || !this._height) { + this._cloneTextureLater = Utils.laterAdd(Meta.LaterType.BEFORE_REDRAW, () => { + // Check if there's still a point in getting the texture, + // otherwise this could go on indefinitely + this._updateWindowPreviewSize(); + + if (this._width && this._height) { + this._cloneTexture(metaWin); + } else { + this._cloneAttempt = (this._cloneAttempt || 0) + 1; + if (this._cloneAttempt < MAX_PREVIEW_GENERATION_ATTEMPTS) + return GLib.SOURCE_CONTINUE; + } + delete this._cloneTextureLater; + return GLib.SOURCE_REMOVE; + }); + return; + } + + const mutterWindow = metaWin.get_compositor_private(); + const clone = new Clutter.Clone({ + source: mutterWindow, + reactive: true, + width: this._width * this._scale, + height: this._height * this._scale, + }); + + // when the source actor is destroyed, i.e. the window closed, first destroy the clone + // and then destroy the menu item (do this animating out) + this._destroyId = mutterWindow.connect('destroy', () => { + clone.destroy(); + this._destroyId = 0; // avoid to try to disconnect this signal from mutterWindow in _onDestroy(), + // as the object was just destroyed + this._animateOutAndDestroy(); + }); + + this._clone = clone; + this._mutterWindow = mutterWindow; + this._cloneBin.set_child(this._clone); + + this._clone.connect('destroy', () => { + if (this._destroyId) { + mutterWindow.disconnect(this._destroyId); + this._destroyId = 0; + } + this._clone = null; + }); + } + + _windowCanClose() { + return this._window.can_close() && + !this._hasAttachedDialogs(); + } + + _closeWindow() { + this._workspace = this._window.get_workspace(); + + // This mechanism is copied from the workspace.js upstream code + // It forces window activation if the windows don't get closed, + // for instance because asking user confirmation, by monitoring the opening of + // such additional confirmation window + this._windowAddedId = this._workspace.connect('window-added', + this._onWindowAdded.bind(this)); + + this.deleteAllWindows(); + } + + deleteAllWindows() { + // Delete all windows, starting from the bottom-most (most-modal) one + // let windows = this._window.get_compositor_private().get_children(); + const windows = this._clone.get_children(); + for (let i = windows.length - 1; i >= 1; i--) { + const realWindow = windows[i].source; + const metaWindow = realWindow.meta_window; + + metaWindow.delete(global.get_current_time()); + } + + this._window.delete(global.get_current_time()); + } + + _onWindowAdded(workspace, win) { + const metaWindow = this._window; + + if (win.get_transient_for() === metaWindow) { + workspace.disconnect(this._windowAddedId); + this._windowAddedId = 0; + + // use an idle handler to avoid mapping problems - + // see comment in Workspace._windowAdded + const activationEvent = Clutter.get_current_event(); + this._windowAddedLater = Utils.laterAdd(Meta.LaterType.BEFORE_REDRAW, () => { + delete this._windowAddedLater; + this.emit('activate', activationEvent); + return GLib.SOURCE_REMOVE; + }); + } + } + + _hasAttachedDialogs() { + // count transient windows + let n = 0; + this._window.foreach_transient(() => { + n++; + }); + return n > 0; + } + + vfunc_key_focus_in() { + super.vfunc_key_focus_in(); + this._showCloseButton(); + } + + vfunc_key_focus_out() { + super.vfunc_key_focus_out(); + this._hideCloseButton(); + } + + vfunc_enter_event(crossingEvent) { + this._showCloseButton(); + return super.vfunc_enter_event(crossingEvent); + } + + vfunc_leave_event(crossingEvent) { + this._hideCloseButton(); + return super.vfunc_leave_event(crossingEvent); + } + + _idleToggleCloseButton() { + this._idleToggleCloseId = 0; + + this._hideCloseButton(); + + return GLib.SOURCE_REMOVE; + } + + _showCloseButton() { + if (this._windowCanClose()) { + this.closeButton.show(); + this.closeButton.remove_all_transitions(); + this.closeButton.ease({ + opacity: 255, + duration: Workspace.WINDOW_OVERLAY_FADE_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + } + + _hideCloseButton() { + if (this.closeButton.has_pointer || + this.get_children().some(a => a.has_pointer)) + return; + + this.closeButton.remove_all_transitions(); + this.closeButton.ease({ + opacity: 0, + duration: Workspace.WINDOW_OVERLAY_FADE_TIME, + mode: Clutter.AnimationMode.EASE_IN_QUAD, + }); + } + + show(animate) { + const fullWidth = this.get_width(); + + this.opacity = 0; + this.set_width(0); + + const time = animate ? PREVIEW_ANIMATION_DURATION : 0; + this.remove_all_transitions(); + this.ease({ + opacity: 255, + width: fullWidth, + duration: time, + mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD, + }); + } + + _animateOutAndDestroy() { + this.remove_all_transitions(); + this.ease({ + opacity: 0, + duration: PREVIEW_ANIMATION_DURATION, + }); + + this.ease({ + width: 0, + height: 0, + duration: PREVIEW_ANIMATION_DURATION, + delay: PREVIEW_ANIMATION_DURATION, + onComplete: () => this.destroy(), + }); + } + + activate() { + Main.activateWindow(this._window); + this._getTopMenu().close(); + } + + _onDestroy() { + if (this._cloneTextureLater) { + Utils.laterRemove(this._cloneTextureLater); + delete this._cloneTextureLater; + } + + if (this._windowAddedLater) { + Utils.laterRemove(this._windowAddedLater); + delete this._windowAddedLater; + } + + if (this._windowAddedId > 0) { + this._workspace.disconnect(this._windowAddedId); + this._windowAddedId = 0; + } + + if (this._destroyId > 0) { + this._mutterWindow.disconnect(this._destroyId); + this._destroyId = 0; + } + + if (this._windowTitleId > 0) { + this._window.disconnect(this._windowTitleId); + this._windowTitleId = 0; + } + } +}); |