diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:54:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:54:43 +0000 |
commit | e4283f6d48b98e764b988b43bbc86b9d52e6ec94 (patch) | |
tree | c8f7f7a6c2f5faa2942d27cefc6fd46cca492656 /js/ui/overview.js | |
parent | Initial commit. (diff) | |
download | gnome-shell-e4283f6d48b98e764b988b43bbc86b9d52e6ec94.tar.xz gnome-shell-e4283f6d48b98e764b988b43bbc86b9d52e6ec94.zip |
Adding upstream version 43.9.upstream/43.9upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/ui/overview.js')
-rw-r--r-- | js/ui/overview.js | 715 |
1 files changed, 715 insertions, 0 deletions
diff --git a/js/ui/overview.js b/js/ui/overview.js new file mode 100644 index 0000000..757a8e4 --- /dev/null +++ b/js/ui/overview.js @@ -0,0 +1,715 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported Overview, ANIMATION_TIME */ + +const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi; +const Signals = imports.misc.signals; + +// Time for initial animation going into Overview mode; +// this is defined here to make it available in imports. +var ANIMATION_TIME = 250; + +const DND = imports.ui.dnd; +const LayoutManager = imports.ui.layout; +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; +const OverviewControls = imports.ui.overviewControls; +const Params = imports.misc.params; +const SwipeTracker = imports.ui.swipeTracker; +const WindowManager = imports.ui.windowManager; +const WorkspaceThumbnail = imports.ui.workspaceThumbnail; + +var DND_WINDOW_SWITCH_TIMEOUT = 750; + +var OVERVIEW_ACTIVATION_TIMEOUT = 0.5; + +var ShellInfo = class { + constructor() { + this._source = null; + } + + setMessage(text, options) { + options = Params.parse(options, { + undoCallback: null, + forFeedback: false, + }); + + let undoCallback = options.undoCallback; + let forFeedback = options.forFeedback; + + if (this._source == null) { + this._source = new MessageTray.SystemNotificationSource(); + this._source.connect('destroy', () => { + this._source = null; + }); + Main.messageTray.add(this._source); + } + + let notification = null; + if (this._source.notifications.length == 0) { + notification = new MessageTray.Notification(this._source, text, null); + notification.setTransient(true); + notification.setForFeedback(forFeedback); + } else { + notification = this._source.notifications[0]; + notification.update(text, null, { clear: true }); + } + + if (undoCallback) + notification.addAction(_('Undo'), () => undoCallback()); + + this._source.showNotification(notification); + } +}; + +var OverviewActor = GObject.registerClass( +class OverviewActor extends St.BoxLayout { + _init() { + super._init({ + name: 'overview', + /* Translators: This is the main view to select + activities. See also note for "Activities" string. */ + accessible_name: _("Overview"), + vertical: true, + }); + + this.add_constraint(new LayoutManager.MonitorConstraint({ primary: true })); + + this._controls = new OverviewControls.ControlsManager(); + this.add_child(this._controls); + } + + prepareToEnterOverview() { + this._controls.prepareToEnterOverview(); + } + + prepareToLeaveOverview() { + this._controls.prepareToLeaveOverview(); + } + + animateToOverview(state, callback) { + this._controls.animateToOverview(state, callback); + } + + animateFromOverview(callback) { + this._controls.animateFromOverview(callback); + } + + runStartupAnimation(callback) { + this._controls.runStartupAnimation(callback); + } + + get dash() { + return this._controls.dash; + } + + get searchEntry() { + return this._controls.searchEntry; + } + + get controls() { + return this._controls; + } +}); + +const OverviewShownState = { + HIDDEN: 'HIDDEN', + HIDING: 'HIDING', + SHOWING: 'SHOWING', + SHOWN: 'SHOWN', +}; + +const OVERVIEW_SHOWN_TRANSITIONS = { + [OverviewShownState.HIDDEN]: { + signal: 'hidden', + allowedTransitions: [OverviewShownState.SHOWING], + }, + [OverviewShownState.HIDING]: { + signal: 'hiding', + allowedTransitions: + [OverviewShownState.HIDDEN, OverviewShownState.SHOWING], + }, + [OverviewShownState.SHOWING]: { + signal: 'showing', + allowedTransitions: + [OverviewShownState.SHOWN, OverviewShownState.HIDING], + }, + [OverviewShownState.SHOWN]: { + signal: 'shown', + allowedTransitions: [OverviewShownState.HIDING], + }, +}; + +var Overview = class extends Signals.EventEmitter { + constructor() { + super(); + + this._initCalled = false; + this._visible = false; + + Main.sessionMode.connect('updated', this._sessionUpdated.bind(this)); + this._sessionUpdated(); + } + + get dash() { + return this._overview.dash; + } + + get dashIconSize() { + logError(new Error('Usage of Overview.\'dashIconSize\' is deprecated, ' + + 'use \'dash.iconSize\' property instead')); + return this.dash.iconSize; + } + + get animationInProgress() { + return this._animationInProgress; + } + + get visible() { + return this._visible; + } + + get visibleTarget() { + return this._visibleTarget; + } + + get closing() { + return this._animationInProgress && !this._visibleTarget; + } + + _createOverview() { + if (this._overview) + return; + + if (this.isDummy) + return; + + this._activationTime = 0; + + this._visible = false; // animating to overview, in overview, animating out + this._shown = false; // show() and not hide() + this._modal = false; // have a modal grab + this._animationInProgress = false; + this._visibleTarget = false; + this._shownState = OverviewShownState.HIDDEN; + + // During transitions, we raise this to the top to avoid having the overview + // area be reactive; it causes too many issues such as double clicks on + // Dash elements, or mouseover handlers in the workspaces. + this._coverPane = new Clutter.Actor({ + opacity: 0, + reactive: true, + }); + Main.layoutManager.overviewGroup.add_child(this._coverPane); + this._coverPane.connect('event', (_actor, event) => { + return event.type() === Clutter.EventType.ENTER || + event.type() === Clutter.EventType.LEAVE + ? Clutter.EVENT_PROPAGATE : Clutter.EVENT_STOP; + }); + this._coverPane.hide(); + + // XDND + this._dragMonitor = { + dragMotion: this._onDragMotion.bind(this), + }; + + + Main.layoutManager.overviewGroup.connect('scroll-event', + this._onScrollEvent.bind(this)); + Main.xdndHandler.connect('drag-begin', this._onDragBegin.bind(this)); + Main.xdndHandler.connect('drag-end', this._onDragEnd.bind(this)); + + global.display.connect('restacked', this._onRestacked.bind(this)); + + this._windowSwitchTimeoutId = 0; + this._windowSwitchTimestamp = 0; + this._lastActiveWorkspaceIndex = -1; + this._lastHoveredWindow = null; + + if (this._initCalled) + this.init(); + } + + _sessionUpdated() { + const { hasOverview } = Main.sessionMode; + if (!hasOverview) + this.hide(); + + this.isDummy = !hasOverview; + this._createOverview(); + } + + // The members we construct that are implemented in JS might + // want to access the overview as Main.overview to connect + // signal handlers and so forth. So we create them after + // construction in this init() method. + init() { + this._initCalled = true; + + if (this.isDummy) + return; + + this._overview = new OverviewActor(); + this._overview._delegate = this; + Main.layoutManager.overviewGroup.add_child(this._overview); + + this._shellInfo = new ShellInfo(); + + Main.layoutManager.connect('monitors-changed', this._relayout.bind(this)); + this._relayout(); + + Main.wm.addKeybinding( + 'toggle-overview', + new Gio.Settings({ schema_id: WindowManager.SHELL_KEYBINDINGS_SCHEMA }), + Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, + this.toggle.bind(this)); + + const swipeTracker = new SwipeTracker.SwipeTracker(global.stage, + Clutter.Orientation.VERTICAL, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, + { allowDrag: false, allowScroll: false }); + swipeTracker.orientation = Clutter.Orientation.VERTICAL; + swipeTracker.connect('begin', this._gestureBegin.bind(this)); + swipeTracker.connect('update', this._gestureUpdate.bind(this)); + swipeTracker.connect('end', this._gestureEnd.bind(this)); + this._swipeTracker = swipeTracker; + } + + // + // options: + // - undoCallback (function): the callback to be called if undo support is needed + // - forFeedback (boolean): whether the message is for direct feedback of a user action + // + setMessage(text, options) { + if (this.isDummy) + return; + + this._shellInfo.setMessage(text, options); + } + + _changeShownState(state) { + const {allowedTransitions} = + OVERVIEW_SHOWN_TRANSITIONS[this._shownState]; + + if (!allowedTransitions.includes(state)) { + throw new Error('Invalid overview shown transition from ' + + `${this._shownState} to ${state}`); + } + + this._shownState = state; + this.emit(OVERVIEW_SHOWN_TRANSITIONS[state].signal); + } + + _onDragBegin() { + this._inXdndDrag = true; + + DND.addDragMonitor(this._dragMonitor); + // Remember the workspace we started from + let workspaceManager = global.workspace_manager; + this._lastActiveWorkspaceIndex = workspaceManager.get_active_workspace_index(); + } + + _onDragEnd() { + this._inXdndDrag = false; + + // In case the drag was canceled while in the overview + // we have to go back to where we started and hide + // the overview + if (this._shown) { + let workspaceManager = global.workspace_manager; + workspaceManager.get_workspace_by_index(this._lastActiveWorkspaceIndex) + .activate(global.get_current_time()); + this.hide(); + } + this._resetWindowSwitchTimeout(); + this._lastHoveredWindow = null; + DND.removeDragMonitor(this._dragMonitor); + this.endItemDrag(); + } + + _resetWindowSwitchTimeout() { + if (this._windowSwitchTimeoutId != 0) { + GLib.source_remove(this._windowSwitchTimeoutId); + this._windowSwitchTimeoutId = 0; + } + } + + _onDragMotion(dragEvent) { + let targetIsWindow = dragEvent.targetActor && + dragEvent.targetActor._delegate && + dragEvent.targetActor._delegate.metaWindow && + !(dragEvent.targetActor._delegate instanceof WorkspaceThumbnail.WindowClone); + + this._windowSwitchTimestamp = global.get_current_time(); + + if (targetIsWindow && + dragEvent.targetActor._delegate.metaWindow == this._lastHoveredWindow) + return DND.DragMotionResult.CONTINUE; + + this._lastHoveredWindow = null; + + this._resetWindowSwitchTimeout(); + + if (targetIsWindow) { + this._lastHoveredWindow = dragEvent.targetActor._delegate.metaWindow; + this._windowSwitchTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + DND_WINDOW_SWITCH_TIMEOUT, + () => { + this._windowSwitchTimeoutId = 0; + Main.activateWindow(dragEvent.targetActor._delegate.metaWindow, + this._windowSwitchTimestamp); + this.hide(); + this._lastHoveredWindow = null; + return GLib.SOURCE_REMOVE; + }); + GLib.Source.set_name_by_id(this._windowSwitchTimeoutId, '[gnome-shell] Main.activateWindow'); + } + + return DND.DragMotionResult.CONTINUE; + } + + _onScrollEvent(actor, event) { + this.emit('scroll-event', event); + return Clutter.EVENT_PROPAGATE; + } + + _relayout() { + // To avoid updating the position and size of the workspaces + // we just hide the overview. The positions will be updated + // when it is next shown. + this.hide(); + + this._coverPane.set_position(0, 0); + this._coverPane.set_size(global.screen_width, global.screen_height); + } + + _onRestacked() { + let stack = global.get_window_actors(); + let stackIndices = {}; + + for (let i = 0; i < stack.length; i++) { + // Use the stable sequence for an integer to use as a hash key + stackIndices[stack[i].get_meta_window().get_stable_sequence()] = i; + } + + this.emit('windows-restacked', stackIndices); + } + + _gestureBegin(tracker) { + this._overview.controls.gestureBegin(tracker); + } + + _gestureUpdate(tracker, progress) { + if (!this._shown) { + Meta.disable_unredirect_for_display(global.display); + + this._shown = true; + this._visible = true; + this._visibleTarget = true; + this._animationInProgress = true; + + Main.layoutManager.overviewGroup.set_child_above_sibling( + this._coverPane, null); + this._coverPane.show(); + this._changeShownState(OverviewShownState.SHOWING); + + Main.layoutManager.showOverview(); + this._syncGrab(); + } + + this._overview.controls.gestureProgress(progress); + } + + _gestureEnd(tracker, duration, endProgress) { + let onComplete; + if (endProgress === 0) { + this._shown = false; + this._visibleTarget = false; + this._changeShownState(OverviewShownState.HIDING); + Main.panel.style = `transition-duration: ${duration}ms;`; + onComplete = () => this._hideDone(); + } else { + onComplete = () => this._showDone(); + } + + this._overview.controls.gestureEnd(endProgress, duration, onComplete); + } + + beginItemDrag(source) { + this.emit('item-drag-begin', source); + this._inItemDrag = true; + } + + cancelledItemDrag(source) { + this.emit('item-drag-cancelled', source); + } + + endItemDrag(source) { + if (!this._inItemDrag) + return; + this.emit('item-drag-end', source); + this._inItemDrag = false; + } + + beginWindowDrag(window) { + this.emit('window-drag-begin', window); + this._inWindowDrag = true; + } + + cancelledWindowDrag(window) { + this.emit('window-drag-cancelled', window); + } + + endWindowDrag(window) { + if (!this._inWindowDrag) + return; + this.emit('window-drag-end', window); + this._inWindowDrag = false; + } + + focusSearch() { + this.show(); + this._overview.searchEntry.grab_key_focus(); + } + + // Checks if the Activities button is currently sensitive to + // clicks. The first call to this function within the + // OVERVIEW_ACTIVATION_TIMEOUT time of the hot corner being + // triggered will return false. This avoids opening and closing + // the overview if the user both triggered the hot corner and + // clicked the Activities button. + shouldToggleByCornerOrButton() { + if (this._animationInProgress) + return false; + if (this._inItemDrag || this._inWindowDrag) + return false; + if (!this._activationTime || + GLib.get_monotonic_time() / GLib.USEC_PER_SEC - this._activationTime > OVERVIEW_ACTIVATION_TIMEOUT) + return true; + return false; + } + + _syncGrab() { + // We delay grab changes during animation so that when removing the + // overview we don't have a problem with the release of a press/release + // going to an application. + if (this._animationInProgress) + return true; + + if (this._shown) { + let shouldBeModal = !this._inXdndDrag; + if (shouldBeModal && !this._modal) { + if (global.display.get_grab_op() !== Meta.GrabOp.NONE && + global.display.get_grab_op() !== Meta.GrabOp.WAYLAND_POPUP) { + this.hide(); + return false; + } + + const grab = Main.pushModal(global.stage, { + actionMode: Shell.ActionMode.OVERVIEW, + }); + if (grab.get_seat_state() !== Clutter.GrabState.ALL) { + Main.popModal(grab); + this.hide(); + return false; + } + + this._grab = grab; + this._modal = true; + } + } else { + // eslint-disable-next-line no-lonely-if + if (this._modal) { + Main.popModal(this._grab); + this._grab = false; + this._modal = false; + } + } + return true; + } + + // show: + // + // Animates the overview visible and grabs mouse and keyboard input + show(state = OverviewControls.ControlsState.WINDOW_PICKER) { + if (state === OverviewControls.ControlsState.HIDDEN) + throw new Error('Invalid state, use hide() to hide'); + + if (this.isDummy) + return; + if (this._shown) + return; + this._shown = true; + + if (!this._syncGrab()) + return; + + Main.layoutManager.showOverview(); + this._animateVisible(state); + } + + + _animateVisible(state) { + if (this._visible || this._animationInProgress) + return; + + this._visible = true; + this._animationInProgress = true; + this._visibleTarget = true; + this._activationTime = GLib.get_monotonic_time() / GLib.USEC_PER_SEC; + + Meta.disable_unredirect_for_display(global.display); + + Main.layoutManager.overviewGroup.set_child_above_sibling( + this._coverPane, null); + this._coverPane.show(); + + this._overview.prepareToEnterOverview(); + this._changeShownState(OverviewShownState.SHOWING); + this._overview.animateToOverview(state, () => this._showDone()); + } + + _showDone() { + this._animationInProgress = false; + this._coverPane.hide(); + + if (this._shownState !== OverviewShownState.SHOWN) + this._changeShownState(OverviewShownState.SHOWN); + + // Handle any calls to hide* while we were showing + if (!this._shown) + this._animateNotVisible(); + + this._syncGrab(); + } + + // hide: + // + // Reverses the effect of show() + hide() { + if (this.isDummy) + return; + + if (!this._shown) + return; + + let event = Clutter.get_current_event(); + if (event) { + let type = event.type(); + let button = type == Clutter.EventType.BUTTON_PRESS || + type == Clutter.EventType.BUTTON_RELEASE; + let ctrl = (event.get_state() & Clutter.ModifierType.CONTROL_MASK) != 0; + if (button && ctrl) + return; + } + + this._shown = false; + + this._animateNotVisible(); + this._syncGrab(); + } + + _animateNotVisible() { + if (!this._visible || this._animationInProgress) + return; + + this._animationInProgress = true; + this._visibleTarget = false; + + Main.layoutManager.overviewGroup.set_child_above_sibling( + this._coverPane, null); + this._coverPane.show(); + + this._overview.prepareToLeaveOverview(); + this._changeShownState(OverviewShownState.HIDING); + this._overview.animateFromOverview(() => this._hideDone()); + } + + _hideDone() { + // Re-enable unredirection + Meta.enable_unredirect_for_display(global.display); + + this._coverPane.hide(); + + this._visible = false; + this._animationInProgress = false; + + // Handle any calls to show* while we were hiding + if (this._shown) { + this._changeShownState(OverviewShownState.HIDDEN); + this._animateVisible(OverviewControls.ControlsState.WINDOW_PICKER); + } else { + Main.layoutManager.hideOverview(); + this._changeShownState(OverviewShownState.HIDDEN); + } + + Main.panel.style = null; + + this._syncGrab(); + } + + toggle() { + if (this.isDummy) + return; + + if (this._visible) + this.hide(); + else + this.show(); + } + + showApps() { + this.show(OverviewControls.ControlsState.APP_GRID); + } + + selectApp(id) { + this.showApps(); + this._overview.controls.appDisplay.selectApp(id); + } + + runStartupAnimation(callback) { + Main.panel.style = 'transition-duration: 0ms;'; + + this._shown = true; + this._visible = true; + this._visibleTarget = true; + Main.layoutManager.showOverview(); + // We should call this._syncGrab() here, but moved it to happen after + // the animation because of a race in the xserver where the grab + // fails when requested very early during startup. + + Meta.disable_unredirect_for_display(global.display); + + this._changeShownState(OverviewShownState.SHOWING); + + this._overview.runStartupAnimation(() => { + // Overview got hidden during startup animation + if (this._shownState !== OverviewShownState.SHOWING) { + callback(); + return; + } + + if (!this._syncGrab()) { + callback(); + this.hide(); + return; + } + + Main.panel.style = null; + this._changeShownState(OverviewShownState.SHOWN); + callback(); + }); + } + + getShowAppsButton() { + logError(new Error('Usage of Overview.\'getShowAppsButton\' is deprecated, ' + + 'use \'dash.showAppsButton\' property instead')); + + return this.dash.showAppsButton; + } + + get searchEntry() { + return this._overview.searchEntry; + } +}; |