%PDF- %PDF-
Direktori : /usr/share/gnome-shell/extensions/ubuntu-dock@ubuntu.com/ |
Current File : //usr/share/gnome-shell/extensions/ubuntu-dock@ubuntu.com/windowPreview.js |
/* * 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)); Main.uiGroup.add_child(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'); this.actor.add_child(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, }); this.closeButton.set_child(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, }); 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 trasient 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() { this._getTopMenu().close(); Main.activateWindow(this._window); } _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; } } });