From a0c0e52229691a379a1356e28811d55ec2595fe0 Mon Sep 17 00:00:00 2001 From: Alexander Mikhaylenko Date: Sun, 30 Jun 2019 17:11:27 +0500 Subject: [PATCH 1/4] swipeTracker: Introduce swipe tracker Add a unified swipe tracker supporting dragging, four-finger swipe on both touchscreen and touchpad, and touchpad scrolling. The shared logic is largely same as the one in WebKit and libhandy. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/826 --- js/js-resources.gresource.xml | 1 + js/ui/swipeTracker.js | 648 ++++++++++++++++++++++++++++++++++ 2 files changed, 649 insertions(+) create mode 100644 js/ui/swipeTracker.js diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index b5348ddcb5..aec3427e0b 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -98,6 +98,7 @@ ui/shellEntry.js ui/shellMountOperation.js ui/slider.js + ui/swipeTracker.js ui/switcherPopup.js ui/switchMonitor.js ui/tweener.js diff --git a/js/ui/swipeTracker.js b/js/ui/swipeTracker.js new file mode 100644 index 0000000000..65c3d0d649 --- /dev/null +++ b/js/ui/swipeTracker.js @@ -0,0 +1,648 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported SwipeTracker */ + +const { Clutter, Gio, GObject, Meta } = imports.gi; + +const Main = imports.ui.main; +const Params = imports.misc.params; + +// FIXME: ideally these values matches physical touchpad size. We can get the +// correct values for gnome-shell specifically, since mutter uses libinput +// directly, but GTK apps cannot get it, so use an arbitrary value so that +// it's consistent with apps. +const TOUCHPAD_BASE_HEIGHT = 300; +const TOUCHPAD_BASE_WIDTH = 400; + +const SCROLL_MULTIPLIER = 10; +const SWIPE_MULTIPLIER = 0.5; + +const MIN_ANIMATION_DURATION = 100; +const MAX_ANIMATION_DURATION = 400; +const VELOCITY_THRESHOLD = 0.4; +// Derivative of easeOutCubic at t=0 +const DURATION_MULTIPLIER = 3; +const ANIMATION_BASE_VELOCITY = 0.002; + +const State = { + NONE: 0, + SCROLLING: 1, +}; + +function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); +} + +const TouchpadSwipeGesture = GObject.registerClass({ + Properties: { + 'enabled': GObject.ParamSpec.boolean( + 'enabled', 'enabled', 'enabled', + GObject.ParamFlags.READWRITE, + true), + 'orientation': GObject.ParamSpec.enum( + 'orientation', 'orientation', 'orientation', + GObject.ParamFlags.READWRITE, + Clutter.Orientation, Clutter.Orientation.VERTICAL), + }, + Signals: { + 'begin': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] }, + 'update': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE] }, + 'end': { param_types: [GObject.TYPE_UINT] }, + }, +}, class TouchpadSwipeGesture extends GObject.Object { + _init(allowedModes) { + super._init(); + this._allowedModes = allowedModes; + this._touchpadSettings = new Gio.Settings({ + schema_id: 'org.gnome.desktop.peripherals.touchpad', + }); + this._orientation = Clutter.Orientation.VERTICAL; + this._enabled = true; + + global.stage.connect('captured-event', this._handleEvent.bind(this)); + } + + get enabled() { + return this._enabled; + } + + set enabled(enabled) { + if (this._enabled === enabled) + return; + + this._enabled = enabled; + this.notify('enabled'); + } + + get orientation() { + return this._orientation; + } + + set orientation(orientation) { + if (this._orientation === orientation) + return; + + this._orientation = orientation; + this.notify('orientation'); + } + + _handleEvent(actor, event) { + if (event.type() !== Clutter.EventType.TOUCHPAD_SWIPE) + return Clutter.EVENT_PROPAGATE; + + if (event.get_touchpad_gesture_finger_count() !== 4) + return Clutter.EVENT_PROPAGATE; + + if ((this._allowedModes & Main.actionMode) === 0) + return Clutter.EVENT_PROPAGATE; + + if (!this.enabled) + return Clutter.EVENT_PROPAGATE; + + let time = event.get_time(); + + let [x, y] = event.get_coords(); + let [dx, dy] = event.get_gesture_motion_delta(); + + let delta; + if (this._orientation === Clutter.Orientation.VERTICAL) + delta = dy / TOUCHPAD_BASE_HEIGHT; + else + delta = dx / TOUCHPAD_BASE_WIDTH; + + switch (event.get_gesture_phase()) { + case Clutter.TouchpadGesturePhase.BEGIN: + this.emit('begin', time, x, y); + break; + + case Clutter.TouchpadGesturePhase.UPDATE: + if (this._touchpadSettings.get_boolean('natural-scroll')) + delta = -delta; + + this.emit('update', time, delta * SWIPE_MULTIPLIER); + break; + + case Clutter.TouchpadGesturePhase.END: + case Clutter.TouchpadGesturePhase.CANCEL: + this.emit('end', time); + break; + } + + return Clutter.EVENT_STOP; + } +}); + +const TouchSwipeGesture = GObject.registerClass({ + Properties: { + 'distance': GObject.ParamSpec.double( + 'distance', 'distance', 'distance', + GObject.ParamFlags.READWRITE, + 0, Infinity, 0), + 'orientation': GObject.ParamSpec.enum( + 'orientation', 'orientation', 'orientation', + GObject.ParamFlags.READWRITE, + Clutter.Orientation, Clutter.Orientation.VERTICAL), + }, + Signals: { + 'begin': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] }, + 'update': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE] }, + 'end': { param_types: [GObject.TYPE_UINT] }, + 'cancel': { param_types: [GObject.TYPE_UINT] }, + }, +}, class TouchSwipeGesture extends Clutter.GestureAction { + _init(allowedModes, nTouchPoints, thresholdTriggerEdge) { + super._init(); + this.set_n_touch_points(nTouchPoints); + this.set_threshold_trigger_edge(thresholdTriggerEdge); + + this._allowedModes = allowedModes; + this._distance = global.screen_height; + this._orientation = Clutter.Orientation.VERTICAL; + + global.display.connect('grab-op-begin', () => { + this.cancel(); + }); + + this._lastPosition = 0; + } + + get distance() { + return this._distance; + } + + set distance(distance) { + if (this._distance === distance) + return; + + this._distance = distance; + this.notify('distance'); + } + + get orientation() { + return this._orientation; + } + + set orientation(orientation) { + if (this._orientation === orientation) + return; + + this._orientation = orientation; + this.notify('orientation'); + } + + vfunc_gesture_prepare(actor) { + if (!super.vfunc_gesture_prepare(actor)) + return false; + + if ((this._allowedModes & Main.actionMode) === 0) + return false; + + let time = this.get_last_event(0).get_time(); + let [xPress, yPress] = this.get_press_coords(0); + let [x, y] = this.get_motion_coords(0); + + this._lastPosition = + this._orientation === Clutter.Orientation.VERTICAL ? y : x; + + this.emit('begin', time, xPress, yPress); + return true; + } + + vfunc_gesture_progress(_actor) { + let [x, y] = this.get_motion_coords(0); + let pos = this._orientation === Clutter.Orientation.VERTICAL ? y : x; + + let delta = pos - this._lastPosition; + this._lastPosition = pos; + + let time = this.get_last_event(0).get_time(); + + this.emit('update', time, -delta / this._distance); + + return true; + } + + vfunc_gesture_end(_actor) { + let time = this.get_last_event(0).get_time(); + + this.emit('end', time); + } + + vfunc_gesture_cancel(_actor) { + let time = Clutter.get_current_event_time(); + + this.emit('cancel', time); + } +}); + +const ScrollGesture = GObject.registerClass({ + Properties: { + 'enabled': GObject.ParamSpec.boolean( + 'enabled', 'enabled', 'enabled', + GObject.ParamFlags.READWRITE, + true), + 'orientation': GObject.ParamSpec.enum( + 'orientation', 'orientation', 'orientation', + GObject.ParamFlags.READWRITE, + Clutter.Orientation, Clutter.Orientation.VERTICAL), + }, + Signals: { + 'begin': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] }, + 'update': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE] }, + 'end': { param_types: [GObject.TYPE_UINT] }, + }, +}, class ScrollGesture extends GObject.Object { + _init(actor, allowedModes) { + super._init(); + this._allowedModes = allowedModes; + this._began = false; + this._enabled = true; + this._orientation = Clutter.Orientation.VERTICAL; + + actor.connect('scroll-event', this._handleEvent.bind(this)); + } + + get enabled() { + return this._enabled; + } + + set enabled(enabled) { + if (this._enabled === enabled) + return; + + this._enabled = enabled; + this.notify('enabled'); + } + + get orientation() { + return this._orientation; + } + + set orientation(orientation) { + if (this._orientation === orientation) + return; + + this._orientation = orientation; + this.notify('orientation'); + } + + canHandleEvent(event) { + if (event.type() !== Clutter.EventType.SCROLL) + return false; + + if (event.get_scroll_source() !== Clutter.ScrollSource.FINGER && + event.get_source_device().get_device_type() !== Clutter.InputDeviceType.TOUCHPAD_DEVICE) + return false; + + if (!this.enabled) + return false; + + if ((this._allowedModes & Main.actionMode) === 0) + return false; + + return true; + } + + _handleEvent(actor, event) { + if (!this.canHandleEvent(event)) + return Clutter.EVENT_PROPAGATE; + + if (event.get_scroll_direction() !== Clutter.ScrollDirection.SMOOTH) + return Clutter.EVENT_PROPAGATE; + + let time = event.get_time(); + let [dx, dy] = event.get_scroll_delta(); + if (dx === 0 && dy === 0) { + this.emit('end', time); + this._began = false; + return Clutter.EVENT_STOP; + } + + if (!this._began) { + let [x, y] = event.get_coords(); + this.emit('begin', time, x, y); + this._began = true; + } + + let delta; + if (this._orientation === Clutter.Orientation.VERTICAL) + delta = dy / TOUCHPAD_BASE_HEIGHT; + else + delta = dx / TOUCHPAD_BASE_WIDTH; + + this.emit('update', time, delta * SCROLL_MULTIPLIER); + + return Clutter.EVENT_STOP; + } +}); + +// USAGE: +// +// To correctly implement the gesture, there must be handlers for the following +// signals: +// +// begin(tracker, monitor) +// The handler should check whether a deceleration animation is currently +// running. If it is, it should stop the animation (without resetting +// progress). Then it should call: +// tracker.confirmSwipe(distance, snapPoints, currentProgress, cancelProgress) +// If it's not called, the swipe would be ignored. +// The parameters are: +// * distance: the page size; +// * snapPoints: an (sorted with ascending order) array of snap points; +// * currentProgress: the current progress; +// * cancelprogress: a non-transient value that would be used if the gesture +// is cancelled. +// If no animation was running, currentProgress and cancelProgress should be +// same. The handler may set 'orientation' property here. +// +// update(tracker, progress) +// The handler should set the progress to the given value. +// +// end(tracker, duration, endProgress) +// The handler should animate the progress to endProgress. If endProgress is +// 0, it should do nothing after the animation, otherwise it should change the +// state, e.g. change the current page or switch workspace. +// NOTE: duration can be 0 in some cases, in this case it should finish +// instantly. + +/** A class for handling swipe gestures */ +var SwipeTracker = GObject.registerClass({ + Properties: { + 'enabled': GObject.ParamSpec.boolean( + 'enabled', 'enabled', 'enabled', + GObject.ParamFlags.READWRITE, + true), + 'orientation': GObject.ParamSpec.enum( + 'orientation', 'orientation', 'orientation', + GObject.ParamFlags.READWRITE, + Clutter.Orientation, Clutter.Orientation.VERTICAL), + 'distance': GObject.ParamSpec.double( + 'distance', 'distance', 'distance', + GObject.ParamFlags.READWRITE, + 0, Infinity, 0), + }, + Signals: { + 'begin': { param_types: [GObject.TYPE_UINT] }, + 'update': { param_types: [GObject.TYPE_DOUBLE] }, + 'end': { param_types: [GObject.TYPE_UINT64, GObject.TYPE_DOUBLE] }, + }, +}, class SwipeTracker extends GObject.Object { + _init(actor, allowedModes, params) { + super._init(); + params = Params.parse(params, { allowDrag: true, allowScroll: true }); + + this._allowedModes = allowedModes; + this._enabled = true; + this._orientation = Clutter.Orientation.VERTICAL; + this._distance = global.screen_height; + + this._reset(); + + this._touchpadGesture = new TouchpadSwipeGesture(allowedModes); + this._touchpadGesture.connect('begin', this._beginGesture.bind(this)); + this._touchpadGesture.connect('update', this._updateGesture.bind(this)); + this._touchpadGesture.connect('end', this._endGesture.bind(this)); + this.bind_property('enabled', this._touchpadGesture, 'enabled', 0); + this.bind_property('orientation', this._touchpadGesture, 'orientation', 0); + + this._touchGesture = new TouchSwipeGesture(allowedModes, 4, + Clutter.GestureTriggerEdge.NONE); + this._touchGesture.connect('begin', this._beginTouchSwipe.bind(this)); + this._touchGesture.connect('update', this._updateGesture.bind(this)); + this._touchGesture.connect('end', this._endGesture.bind(this)); + this._touchGesture.connect('cancel', this._cancelGesture.bind(this)); + this.bind_property('enabled', this._touchGesture, 'enabled', 0); + this.bind_property('orientation', this._touchGesture, 'orientation', 0); + this.bind_property('distance', this._touchGesture, 'distance', 0); + global.stage.add_action(this._touchGesture); + + if (params.allowDrag) { + this._dragGesture = new TouchSwipeGesture(allowedModes, 1, + Clutter.GestureTriggerEdge.AFTER); + this._dragGesture.connect('begin', this._beginGesture.bind(this)); + this._dragGesture.connect('update', this._updateGesture.bind(this)); + this._dragGesture.connect('end', this._endGesture.bind(this)); + this._dragGesture.connect('cancel', this._cancelGesture.bind(this)); + this.bind_property('enabled', this._dragGesture, 'enabled', 0); + this.bind_property('orientation', this._dragGesture, 'orientation', 0); + this.bind_property('distance', this._dragGesture, 'distance', 0); + actor.add_action(this._dragGesture); + } else { + this._dragGesture = null; + } + + if (params.allowScroll) { + this._scrollGesture = new ScrollGesture(actor, allowedModes); + this._scrollGesture.connect('begin', this._beginGesture.bind(this)); + this._scrollGesture.connect('update', this._updateGesture.bind(this)); + this._scrollGesture.connect('end', this._endGesture.bind(this)); + this.bind_property('enabled', this._scrollGesture, 'enabled', 0); + this.bind_property('orientation', this._scrollGesture, 'orientation', 0); + } else { + this._scrollGesture = null; + } + } + + /** + * canHandleScrollEvent: + * @param {Clutter.Event} scrollEvent: an event to check + * @returns {bool} whether the event can be handled by the tracker + * + * This function can be used to combine swipe gesture and mouse + * scrolling. + */ + canHandleScrollEvent(scrollEvent) { + if (!this.enabled || this._scrollGesture === null) + return false; + + return this._scrollGesture.canHandleEvent(scrollEvent); + } + + get enabled() { + return this._enabled; + } + + set enabled(enabled) { + if (this._enabled === enabled) + return; + + this._enabled = enabled; + if (!enabled && this._state === State.SCROLLING) + this._interrupt(); + this.notify('enabled'); + } + + get orientation() { + return this._orientation; + } + + set orientation(orientation) { + if (this._orientation === orientation) + return; + + this._orientation = orientation; + this.notify('orientation'); + } + + get distance() { + return this._distance; + } + + set distance(distance) { + if (this._distance === distance) + return; + + this._distance = distance; + this.notify('distance'); + } + + _reset() { + this._state = State.NONE; + + this._snapPoints = []; + this._initialProgress = 0; + this._cancelProgress = 0; + + this._prevOffset = 0; + this._progress = 0; + + this._prevTime = 0; + this._velocity = 0; + + this._cancelled = false; + } + + _interrupt() { + this.emit('end', 0, this._cancelProgress); + this._reset(); + } + + _beginTouchSwipe(gesture, time, x, y) { + if (this._dragGesture) + this._dragGesture.cancel(); + + this._beginGesture(gesture, time, x, y); + } + + _beginGesture(gesture, time, x, y) { + if (this._state === State.SCROLLING) + return; + + this._prevTime = time; + + let rect = new Meta.Rectangle({ x, y, width: 1, height: 1 }); + let monitor = global.display.get_monitor_index_for_rect(rect); + + this.emit('begin', monitor); + } + + _updateGesture(gesture, time, delta) { + if (this._state !== State.SCROLLING) + return; + + if ((this._allowedModes & Main.actionMode) === 0 || !this.enabled) { + this._interrupt(); + return; + } + + if (this.orientation === Clutter.Orientation.HORIZONTAL && + Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) + delta = -delta; + + this._progress += delta; + + if (time !== this._prevTime) + this._velocity = delta / (time - this._prevTime); + + let firstPoint = this._snapPoints[0]; + let lastPoint = this._snapPoints[this._snapPoints.length - 1]; + this._progress = clamp(this._progress, firstPoint, lastPoint); + this._progress = clamp(this._progress, + this._initialProgress - 1, this._initialProgress + 1); + + this.emit('update', this._progress); + + this._prevTime = time; + } + + _getClosestSnapPoints() { + let upper = this._snapPoints.find(p => p >= this._progress); + let lower = this._snapPoints.slice().reverse().find(p => p <= this._progress); + return [lower, upper]; + } + + _getEndProgress() { + if (this._cancelled) + return this._cancelProgress; + + let [lower, upper] = this._getClosestSnapPoints(); + let middle = (upper + lower) / 2; + + if (this._progress > middle) { + let thresholdMet = this._velocity * this._distance > -VELOCITY_THRESHOLD; + return thresholdMet || this._initialProgress > upper ? upper : lower; + } else { + let thresholdMet = this._velocity * this._distance < VELOCITY_THRESHOLD; + return thresholdMet || this._initialProgress < lower ? lower : upper; + } + } + + _endGesture(_gesture, _time) { + if (this._state !== State.SCROLLING) + return; + + if ((this._allowedModes & Main.actionMode) === 0 || !this.enabled) { + this._interrupt(); + return; + } + + let endProgress = this._getEndProgress(); + + let velocity = ANIMATION_BASE_VELOCITY; + if ((endProgress - this._progress) * this._velocity > 0) + velocity = this._velocity; + + let duration = Math.abs((this._progress - endProgress) / velocity * DURATION_MULTIPLIER); + if (duration > 0) { + duration = clamp(duration, + MIN_ANIMATION_DURATION, MAX_ANIMATION_DURATION); + } + + this.emit('end', duration, endProgress); + this._reset(); + } + + _cancelGesture(gesture, time) { + if (this._state !== State.SCROLLING) + return; + + this._cancelled = true; + this._endGesture(gesture, time); + } + + /** + * confirmSwipe: + * @param {number} distance: swipe distance in pixels + * @param {number[]} snapPoints: + * An array of snap points, sorted in ascending order + * @param {number} currentProgress: initial progress value + * @param {number} cancelProgress: the value to be used on cancelling + * + * Confirms a swipe. User has to call this in 'begin' signal handler, + * otherwise the swipe wouldn't start. If there's an animation running, + * it should be stopped first. + * + * @cancel_progress must always be a snap point, or a value matching + * some other non-transient state. + */ + confirmSwipe(distance, snapPoints, currentProgress, cancelProgress) { + this.distance = distance; + this._snapPoints = snapPoints; + this._initialProgress = currentProgress; + this._progress = currentProgress; + this._cancelProgress = cancelProgress; + + this._velocity = 0; + this._state = State.SCROLLING; + } +}); -- GitLab From a11f417cd0f9876b6e00731a6e39665d0103336f Mon Sep 17 00:00:00 2001 From: Alexander Mikhaylenko Date: Mon, 8 Jul 2019 13:47:04 +0500 Subject: [PATCH 2/4] workspacesView: Use SwipeTracker Replace existing panning, touchpad scrolling and four-finger gesture by SwipeTracker. Change programmatic workspace animation to use easeOutCubic interpolator to match the gesture. Also change the dragging distance to always match the current monitor. Fixes touchpad parts of https://gitlab.gnome.org/GNOME/gnome-shell/issues/1338 https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/826 --- js/ui/workspacesView.js | 185 ++++++++++++++++------------------------ 1 file changed, 75 insertions(+), 110 deletions(-) diff --git a/js/ui/workspacesView.js b/js/ui/workspacesView.js index aa4dd65fcf..975dd07604 100644 --- a/js/ui/workspacesView.js +++ b/js/ui/workspacesView.js @@ -4,7 +4,7 @@ const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi; const Main = imports.ui.main; -const WindowManager = imports.ui.windowManager; +const SwipeTracker = imports.ui.swipeTracker; const Workspace = imports.ui.workspace; var WORKSPACE_SWITCH_TIME = 250; @@ -79,7 +79,6 @@ class WorkspacesView extends WorkspacesViewBase { super._init(monitorIndex); this._animating = false; // tweening - this._scrolling = false; // swipe-scrolling this._gestureActive = false; // touch(pad) gestures this._scrollAdjustment = scrollAdjustment; @@ -183,7 +182,7 @@ class WorkspacesView extends WorkspacesViewBase { if (showAnimation) { let easeParams = Object.assign(params, { duration: WORKSPACE_SWITCH_TIME, - mode: Clutter.AnimationMode.EASE_OUT_QUAD, + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, }); // we have to call _updateVisibility() once before the // animation and once afterwards - it does not really @@ -211,7 +210,7 @@ class WorkspacesView extends WorkspacesViewBase { for (let w = 0; w < this._workspaces.length; w++) { let workspace = this._workspaces[w]; - if (this._animating || this._scrolling || this._gestureActive) + if (this._animating || this._gestureActive) workspace.show(); else if (this._inDrag) workspace.visible = Math.abs(w - active) <= 1; @@ -268,18 +267,6 @@ class WorkspacesView extends WorkspacesViewBase { workspaceManager.disconnect(this._reorderWorkspacesId); } - startSwipeScroll() { - this._scrolling = true; - } - - endSwipeScroll() { - this._scrolling = false; - - // Make sure title captions etc are shown as necessary - this._updateWorkspaceActors(true); - this._updateVisibility(); - } - startTouchGesture() { this._gestureActive = true; } @@ -395,12 +382,6 @@ class ExtraWorkspaceView extends WorkspacesViewBase { this._workspace.syncStacking(stackIndices); } - startSwipeScroll() { - } - - endSwipeScroll() { - } - startTouchGesture() { } @@ -438,42 +419,14 @@ class WorkspacesDisplay extends St.Widget { }); Main.overview.addAction(clickAction); this.bind_property('mapped', clickAction, 'enabled', GObject.BindingFlags.SYNC_CREATE); + this._clickAction = clickAction; - let panAction = new Clutter.PanAction({ threshold_trigger_edge: Clutter.GestureTriggerEdge.AFTER }); - panAction.connect('pan', this._onPan.bind(this)); - panAction.connect('gesture-begin', () => { - if (this._workspacesOnlyOnPrimary) { - let event = Clutter.get_current_event(); - if (this._getMonitorIndexForEvent(event) != this._primaryIndex) - return false; - } - - this._startSwipeScroll(); - return true; - }); - panAction.connect('gesture-cancel', () => { - clickAction.release(); - this._endSwipeScroll(); - }); - panAction.connect('gesture-end', () => { - clickAction.release(); - this._endSwipeScroll(); - }); - Main.overview.addAction(panAction); - this.bind_property('mapped', panAction, 'enabled', GObject.BindingFlags.SYNC_CREATE); - - let allowedModes = Shell.ActionMode.OVERVIEW; - let switchGesture = new WindowManager.WorkspaceSwitchAction(allowedModes); - switchGesture.connect('motion', this._onSwitchWorkspaceMotion.bind(this)); - switchGesture.connect('activated', this._onSwitchWorkspaceActivated.bind(this)); - switchGesture.connect('cancel', this._endTouchGesture.bind(this)); - Main.overview.addAction(switchGesture); - this.bind_property('mapped', switchGesture, 'enabled', GObject.BindingFlags.SYNC_CREATE); - - switchGesture = new WindowManager.TouchpadWorkspaceSwitchAction(global.stage, allowedModes); - switchGesture.connect('motion', this._onSwitchWorkspaceMotion.bind(this)); - switchGesture.connect('activated', this._onSwitchWorkspaceActivated.bind(this)); - switchGesture.connect('cancel', this._endTouchGesture.bind(this)); + this._swipeTracker = new SwipeTracker.SwipeTracker( + Main.layoutManager.overviewGroup, Shell.ActionMode.OVERVIEW); + this._swipeTracker.connect('begin', this._switchWorkspaceBegin.bind(this)); + this._swipeTracker.connect('update', this._switchWorkspaceUpdate.bind(this)); + this._swipeTracker.connect('end', this._switchWorkspaceEnd.bind(this)); + this.bind_property('mapped', this._swipeTracker, 'enabled', GObject.BindingFlags.SYNC_CREATE); this._primaryIndex = Main.layoutManager.primaryIndex; this._workspacesViews = []; @@ -491,7 +444,6 @@ class WorkspacesDisplay extends St.Widget { this._fullGeometry = null; - this._scrolling = false; // swipe-scrolling this._gestureActive = false; // touch(pad) gestures this._canScroll = true; // limiting scrolling speed @@ -528,7 +480,7 @@ class WorkspacesDisplay extends St.Widget { } _activeWorkspaceChanged(_wm, _from, _to, _direction) { - if (this._scrolling) + if (this._gestureActive) return; this._scrollToActive(); @@ -542,77 +494,89 @@ class WorkspacesDisplay extends St.Widget { } _updateScrollAdjustment(index) { - if (this._scrolling || this._gestureActive) + if (this._gestureActive) return; this._scrollAdjustment.ease(index, { - mode: Clutter.AnimationMode.EASE_OUT_QUAD, + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, duration: WORKSPACE_SWITCH_TIME, }); } - _onPan(action) { - let [dist_, dx, dy] = action.get_motion_delta(0); - let adjustment = this._scrollAdjustment; - if (global.workspace_manager.layout_rows == -1) - adjustment.value -= (dy / this.height) * adjustment.page_size; - else if (this.text_direction == Clutter.TextDirection.RTL) - adjustment.value += (dx / this.width) * adjustment.page_size; - else - adjustment.value -= (dx / this.width) * adjustment.page_size; - return false; + _directionForProgress(progress) { + if (global.workspace_manager.layout_rows === -1) { + return progress > 0 + ? Meta.MotionDirection.DOWN + : Meta.MotionDirection.UP; + } else if (this.text_direction === Clutter.TextDirection.RTL) { + return progress > 0 + ? Meta.MotionDirection.LEFT + : Meta.MotionDirection.RIGHT; + } else { + return progress > 0 + ? Meta.MotionDirection.RIGHT + : Meta.MotionDirection.LEFT; + } } - _startSwipeScroll() { - for (let i = 0; i < this._workspacesViews.length; i++) - this._workspacesViews[i].startSwipeScroll(); - this._scrolling = true; - } + _switchWorkspaceBegin(tracker, monitor) { + if (this._workspacesOnlyOnPrimary && monitor !== this._primaryIndex) + return; - _endSwipeScroll() { - for (let i = 0; i < this._workspacesViews.length; i++) - this._workspacesViews[i].endSwipeScroll(); - this._scrolling = false; - this._scrollToActive(); - } + let workspaceManager = global.workspace_manager; + let adjustment = this._scrollAdjustment; + if (this._gestureActive) + adjustment.remove_transition('value'); + + tracker.orientation = workspaceManager.layout_rows !== -1 + ? Clutter.Orientation.HORIZONTAL + : Clutter.Orientation.VERTICAL; - _startTouchGesture() { for (let i = 0; i < this._workspacesViews.length; i++) this._workspacesViews[i].startTouchGesture(); - this._gestureActive = true; - } - _endTouchGesture() { - for (let i = 0; i < this._workspacesViews.length; i++) - this._workspacesViews[i].endTouchGesture(); - this._gestureActive = false; - this._scrollToActive(); - } + let monitors = Main.layoutManager.monitors; + let geometry = monitor === this._primaryIndex + ? this._fullGeometry : monitors[monitor]; + let distance = global.workspace_manager.layout_rows === -1 + ? geometry.height : geometry.width; - _onSwitchWorkspaceMotion(action, xRel, yRel) { - // We don't have a way to hook into start of touchpad actions, - // luckily this is safe to call repeatedly. - this._startTouchGesture(); + let progress = adjustment.value / adjustment.page_size; + let points = Array.from( + { length: workspaceManager.n_workspaces }, (v, i) => i); - let workspaceManager = global.workspace_manager; - let active = workspaceManager.get_active_workspace_index(); + tracker.confirmSwipe(distance, points, progress, Math.round(progress)); + + this._gestureActive = true; + } + + _switchWorkspaceUpdate(tracker, progress) { let adjustment = this._scrollAdjustment; - if (workspaceManager.layout_rows == -1) - adjustment.value = (active - yRel / this.height) * adjustment.page_size; - else if (this.text_direction == Clutter.TextDirection.RTL) - adjustment.value = (active + xRel / this.width) * adjustment.page_size; - else - adjustment.value = (active - xRel / this.width) * adjustment.page_size; + adjustment.value = progress * adjustment.page_size; } - _onSwitchWorkspaceActivated(action, direction) { + _switchWorkspaceEnd(tracker, duration, endProgress) { + this._clickAction.release(); + let workspaceManager = global.workspace_manager; let activeWorkspace = workspaceManager.get_active_workspace(); - let newWs = activeWorkspace.get_neighbor(direction); - if (newWs != activeWorkspace) - newWs.activate(global.get_current_time()); + let newWs = workspaceManager.get_workspace_by_index(endProgress); + + this._scrollAdjustment.ease(endProgress, { + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, + duration, + onComplete: () => { + if (newWs !== activeWorkspace) + newWs.activate(global.get_current_time()); + this._endTouchGesture(); + }, + }); + } - this._endTouchGesture(); + _endTouchGesture() { + for (let i = 0; i < this._workspacesViews.length; i++) + this._workspacesViews[i].endTouchGesture(); + this._gestureActive = false; } vfunc_navigate_focus(from, direction) { @@ -692,8 +656,6 @@ class WorkspacesDisplay extends St.Widget { else view = new WorkspacesView(i, this._scrollAdjustment); - view.connect('scroll-event', this._onScrollEvent.bind(this)); - // HACK: Avoid spurious allocation changes while updating views view.hide(); @@ -793,6 +755,9 @@ class WorkspacesDisplay extends St.Widget { } _onScrollEvent(actor, event) { + if (this._swipeTracker.canHandleScrollEvent(event)) + return Clutter.EVENT_PROPAGATE; + if (!this.mapped) return Clutter.EVENT_PROPAGATE; -- GitLab From 3e6bcbb486e38877b672627ad33923b2e6aef277 Mon Sep 17 00:00:00 2001 From: Alexander Mikhaylenko Date: Sun, 30 Jun 2019 17:15:37 +0500 Subject: [PATCH 3/4] appDisplay: Use SwipeTracker Replace existing panning and touchpad scrolling by SwipeTracker. Since SwipeTracker only references one actor, redirect scroll events from page indicators to the main scroll view. Change programmatic scroll animation to use easeOutCubic interpolator to match the gesture. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/826 --- js/ui/appDisplay.js | 111 +++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 64 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 0156877e8f..da8f562195 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -13,6 +13,7 @@ const Main = imports.ui.main; const PageIndicators = imports.ui.pageIndicators; const PopupMenu = imports.ui.popupMenu; const Search = imports.ui.search; +const SwipeTracker = imports.ui.swipeTracker; const Params = imports.misc.params; const Util = imports.misc.util; const SystemActions = imports.misc.systemActions; @@ -327,7 +328,9 @@ var AllView = GObject.registerClass({ (indicators, pageIndex) => { this.goToPage(pageIndex); }); - this._pageIndicators.connect('scroll-event', this._onScroll.bind(this)); + this._pageIndicators.connect('scroll-event', (actor, event) => { + this._scrollView.event(event, false); + }); this.add_actor(this._pageIndicators); this._folderIcons = []; @@ -353,13 +356,12 @@ var AllView = GObject.registerClass({ this._scrollView.connect('scroll-event', this._onScroll.bind(this)); - let panAction = new Clutter.PanAction({ interpolate: false }); - panAction.connect('pan', this._onPan.bind(this)); - panAction.connect('gesture-cancel', this._onPanEnd.bind(this)); - panAction.connect('gesture-end', this._onPanEnd.bind(this)); - this._panAction = panAction; - this._scrollView.add_action(panAction); - this._panning = false; + this._swipeTracker = new SwipeTracker.SwipeTracker( + this._scrollView, Shell.ActionMode.OVERVIEW); + this._swipeTracker.connect('begin', this._swipeBegin.bind(this)); + this._swipeTracker.connect('update', this._swipeUpdate.bind(this)); + this._swipeTracker.connect('end', this._swipeEnd.bind(this)); + this._clickAction = new Clutter.ClickAction(); this._clickAction.connect('clicked', () => { if (!this._currentPopup) @@ -424,6 +426,7 @@ var AllView = GObject.registerClass({ this._keyPressEventId = global.stage.connect('key-press-event', this._onKeyPressEvent.bind(this)); + this._swipeTracker.enabled = true; super.vfunc_map(); } @@ -432,6 +435,7 @@ var AllView = GObject.registerClass({ global.stage.disconnect(this._keyPressEventId); this._keyPressEventId = 0; } + this._swipeTracker.enabled = false; super.vfunc_unmap(); } @@ -577,7 +581,7 @@ var AllView = GObject.registerClass({ return this._grid.getPageY(this._grid.currentPage); } - goToPage(pageNumber) { + goToPage(pageNumber, animate = true) { pageNumber = clamp(pageNumber, 0, this._grid.nPages() - 1); if (this._grid.currentPage == pageNumber && this._displayingPopup && this._currentPopup) @@ -592,42 +596,16 @@ var AllView = GObject.registerClass({ return; } - let velocity; - if (!this._panning) - velocity = 0; - else - velocity = Math.abs(this._panAction.get_velocity(0)[2]); - // Tween the change between pages. - // If velocity is not specified (i.e. scrolling with mouse wheel), - // use the same speed regardless of original position - // if velocity is specified, it's in pixels per milliseconds - let diffToPage = this._diffToPage(pageNumber); - let childBox = this._scrollView.get_allocation_box(); - let totalHeight = childBox.y2 - childBox.y1; - let time; - // Only take the velocity into account on page changes, otherwise - // return smoothly to the current page using the default velocity - if (this._grid.currentPage != pageNumber) { - let minVelocity = totalHeight / PAGE_SWITCH_TIME; - velocity = Math.max(minVelocity, velocity); - time = diffToPage / velocity; - } else { - time = PAGE_SWITCH_TIME * diffToPage / totalHeight; - } - // When changing more than one page, make sure to not take - // longer than PAGE_SWITCH_TIME - time = Math.min(time, PAGE_SWITCH_TIME); + if (this._grid.currentPage === pageNumber) + return; this._grid.currentPage = pageNumber; - this._adjustment.ease(this._grid.getPageY(pageNumber), { - mode: Clutter.AnimationMode.EASE_OUT_QUAD, - duration: time, - }); - } - _diffToPage(pageNumber) { - let currentScrollPosition = this._adjustment.value; - return Math.abs(currentScrollPosition - this._grid.getPageY(pageNumber)); + // Tween the change between pages. + this._adjustment.ease(this._grid.getPageY(this._grid.currentPage), { + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, + duration: animate ? PAGE_SWITCH_TIME : 0, + }); } openSpaceForPopup(item, side, nRows) { @@ -650,6 +628,9 @@ var AllView = GObject.registerClass({ if (this._displayingPopup || !this._scrollView.reactive) return Clutter.EVENT_STOP; + if (this._swipeTracker.canHandleScrollEvent(event)) + return Clutter.EVENT_PROPAGATE; + if (!this._canScroll) return Clutter.EVENT_STOP; @@ -673,34 +654,36 @@ var AllView = GObject.registerClass({ return Clutter.EVENT_STOP; } - _onPan(action) { - if (this._displayingPopup) - return false; - this._panning = true; - this._clickAction.release(); - let [dist_, dx_, dy] = action.get_motion_delta(0); - let adjustment = this._adjustment; - adjustment.value -= (dy / this._scrollView.height) * adjustment.page_size; - return false; - } - - _onPanEnd(action) { - if (this._displayingPopup) + _swipeBegin(tracker, monitor) { + if (monitor !== Main.layoutManager.primaryIndex) return; - let pageHeight = this._grid.getPageHeight(); + let adjustment = this._adjustment; + adjustment.remove_transition('value'); - // Calculate the scroll value we'd be at, which is our current - // scroll plus any velocity the user had when they released - // their finger. + let progress = adjustment.value / adjustment.page_size; + let points = Array.from({ length: this._grid.nPages() }, (v, i) => i); - let velocity = -action.get_velocity(0)[2]; - let endPanValue = this._adjustment.value + velocity; + tracker.confirmSwipe(this._scrollView.height, + points, progress, Math.round(progress)); + } - let closestPage = Math.round(endPanValue / pageHeight); - this.goToPage(closestPage); + _swipeUpdate(tracker, progress) { + let adjustment = this._adjustment; + adjustment.value = progress * adjustment.page_size; + } - this._panning = false; + _swipeEnd(tracker, duration, endProgress) { + let adjustment = this._adjustment; + let value = endProgress * adjustment.page_size; + + adjustment.ease(value, { + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, + duration, + onComplete: () => { + this.goToPage(endProgress, false); + }, + }); } _onKeyPressEvent(actor, event) { -- GitLab From a8dcfa4656e01764380d081d74365092f08c114e Mon Sep 17 00:00:00 2001 From: Alexander Mikhaylenko Date: Sun, 30 Jun 2019 17:15:44 +0500 Subject: [PATCH 4/4] windowManager: Use SwipeTracker Replace existing four-finger gestures with SwipeTracker. Since TouchpadWorkspaceSwitchAction and WorkspaceSwitchAction are now unused, remove them. Change programmatic workspace transition to use easeOutCubic interpolator to match the gesture. Fixes https://gitlab.gnome.org/GNOME/gnome-shell/issues/756 https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/826 --- js/ui/windowManager.js | 388 +++++++++++++++++++---------------------- 1 file changed, 177 insertions(+), 211 deletions(-) diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js index 7716bab741..f85298cd53 100644 --- a/js/ui/windowManager.js +++ b/js/ui/windowManager.js @@ -2,7 +2,6 @@ /* exported WindowManager */ const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi; -const Signals = imports.signals; const AltTab = imports.ui.altTab; const AppFavorites = imports.ui.appFavorites; @@ -15,6 +14,7 @@ const WindowMenu = imports.ui.windowMenu; const PadOsd = imports.ui.padOsd; const EdgeDragAction = imports.ui.edgeDragAction; const CloseDialog = imports.ui.closeDialog; +const SwipeTracker = imports.ui.swipeTracker; const SwitchMonitor = imports.ui.switchMonitor; const IBusManager = imports.misc.ibusManager; @@ -30,7 +30,6 @@ var WINDOW_ANIMATION_TIME = 250; var DIM_BRIGHTNESS = -0.3; var DIM_TIME = 500; var UNDIM_TIME = 250; -var WS_MOTION_THRESHOLD = 100; var APP_MOTION_THRESHOLD = 30; var ONE_SECOND = 1000; // in ms @@ -468,147 +467,6 @@ class TilePreview extends St.Widget { } }); -var TouchpadWorkspaceSwitchAction = class { - constructor(actor, allowedModes) { - this._allowedModes = allowedModes; - this._dx = 0; - this._dy = 0; - this._enabled = true; - actor.connect('captured-event', this._handleEvent.bind(this)); - this._touchpadSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.peripherals.touchpad' }); - } - - get enabled() { - return this._enabled; - } - - set enabled(enabled) { - if (this._enabled == enabled) - return; - - this._enabled = enabled; - if (!enabled) - this.emit('cancel'); - } - - _checkActivated() { - let dir; - - if (this._dy < -WS_MOTION_THRESHOLD) - dir = Meta.MotionDirection.DOWN; - else if (this._dy > WS_MOTION_THRESHOLD) - dir = Meta.MotionDirection.UP; - else if (this._dx < -WS_MOTION_THRESHOLD) - dir = Meta.MotionDirection.RIGHT; - else if (this._dx > WS_MOTION_THRESHOLD) - dir = Meta.MotionDirection.LEFT; - else - return false; - - this.emit('activated', dir); - return true; - } - - _handleEvent(actor, event) { - if (event.type() != Clutter.EventType.TOUCHPAD_SWIPE) - return Clutter.EVENT_PROPAGATE; - - if (event.get_touchpad_gesture_finger_count() != 4) - return Clutter.EVENT_PROPAGATE; - - if ((this._allowedModes & Main.actionMode) == 0) - return Clutter.EVENT_PROPAGATE; - - if (!this._enabled) - return Clutter.EVENT_PROPAGATE; - - if (event.get_gesture_phase() == Clutter.TouchpadGesturePhase.UPDATE) { - let [dx, dy] = event.get_gesture_motion_delta(); - - // Scale deltas up a bit to make it feel snappier - this._dx += dx * 2; - if (!this._touchpadSettings.get_boolean('natural-scroll')) - this._dy -= dy * 2; - else - this._dy += dy * 2; - - this.emit('motion', this._dx, this._dy); - } else { - if ((event.get_gesture_phase() == Clutter.TouchpadGesturePhase.END && !this._checkActivated()) || - event.get_gesture_phase() == Clutter.TouchpadGesturePhase.CANCEL) - this.emit('cancel'); - - this._dx = 0; - this._dy = 0; - } - - return Clutter.EVENT_STOP; - } -}; -Signals.addSignalMethods(TouchpadWorkspaceSwitchAction.prototype); - -var WorkspaceSwitchAction = GObject.registerClass({ - Signals: { 'activated': { param_types: [Meta.MotionDirection.$gtype] }, - 'motion': { param_types: [GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] }, - 'cancel': { param_types: [] } }, -}, class WorkspaceSwitchAction extends Clutter.SwipeAction { - _init(allowedModes) { - super._init(); - this.set_n_touch_points(4); - this._swept = false; - this._allowedModes = allowedModes; - - global.display.connect('grab-op-begin', () => { - this.cancel(); - }); - } - - vfunc_gesture_prepare(actor) { - this._swept = false; - - if (!super.vfunc_gesture_prepare(actor)) - return false; - - return this._allowedModes & Main.actionMode; - } - - vfunc_gesture_progress(_actor) { - let [x, y] = this.get_motion_coords(0); - let [xPress, yPress] = this.get_press_coords(0); - this.emit('motion', x - xPress, y - yPress); - return true; - } - - vfunc_gesture_cancel(_actor) { - if (!this._swept) - this.emit('cancel'); - } - - vfunc_swipe(actor, direction) { - let [x, y] = this.get_motion_coords(0); - let [xPress, yPress] = this.get_press_coords(0); - if (Math.abs(x - xPress) < WS_MOTION_THRESHOLD && - Math.abs(y - yPress) < WS_MOTION_THRESHOLD) { - this.emit('cancel'); - return; - } - - let dir; - - if (direction & Clutter.SwipeDirection.UP) - dir = Meta.MotionDirection.DOWN; - else if (direction & Clutter.SwipeDirection.DOWN) - dir = Meta.MotionDirection.UP; - else if (direction & Clutter.SwipeDirection.LEFT) - dir = Meta.MotionDirection.RIGHT; - else if (direction & Clutter.SwipeDirection.RIGHT) - dir = Meta.MotionDirection.LEFT; - - this._swept = true; - this.emit('activated', dir); - } -}); - var AppSwitchAction = GObject.registerClass({ Signals: { 'activated': {} }, }, class AppSwitchAction extends Clutter.GestureAction { @@ -1054,10 +912,17 @@ var WindowManager = class { Main.overview.connect('showing', () => { for (let i = 0; i < this._dimmedWindows.length; i++) this._undimWindow(this._dimmedWindows[i]); + + if (this._switchData) { + if (this._switchData.gestureActivated) + this._switchWorkspaceStop(); + this._swipeTracker.enabled = false; + } }); Main.overview.connect('hiding', () => { for (let i = 0; i < this._dimmedWindows.length; i++) this._dimWindow(this._dimmedWindows[i]); + this._swipeTracker.enabled = true; }); this._windowMenuManager = new WindowMenu.WindowMenuManager(); @@ -1068,18 +933,12 @@ var WindowManager = class { global.workspace_manager.override_workspace_layout(Meta.DisplayCorner.TOPLEFT, false, -1, 1); - let allowedModes = Shell.ActionMode.NORMAL; - let workspaceSwitchAction = new WorkspaceSwitchAction(allowedModes); - workspaceSwitchAction.connect('motion', this._switchWorkspaceMotion.bind(this)); - workspaceSwitchAction.connect('activated', this._actionSwitchWorkspace.bind(this)); - workspaceSwitchAction.connect('cancel', this._switchWorkspaceCancel.bind(this)); - global.stage.add_action(workspaceSwitchAction); - - // This is not a normal Clutter.GestureAction, doesn't need add_action() - let touchpadSwitchAction = new TouchpadWorkspaceSwitchAction(global.stage, allowedModes); - touchpadSwitchAction.connect('motion', this._switchWorkspaceMotion.bind(this)); - touchpadSwitchAction.connect('activated', this._actionSwitchWorkspace.bind(this)); - touchpadSwitchAction.connect('cancel', this._switchWorkspaceCancel.bind(this)); + let swipeTracker = new SwipeTracker.SwipeTracker(global.stage, + Shell.ActionMode.NORMAL, { allowDrag: false, allowScroll: false }); + swipeTracker.connect('begin', this._switchWorkspaceBegin.bind(this)); + swipeTracker.connect('update', this._switchWorkspaceUpdate.bind(this)); + swipeTracker.connect('end', this._switchWorkspaceEnd.bind(this)); + this._swipeTracker = swipeTracker; let appSwitchAction = new AppSwitchAction(); appSwitchAction.connect('activated', this._switchApp.bind(this)); @@ -1121,52 +980,6 @@ var WindowManager = class { return this._currentPadOsd; } - _switchWorkspaceMotion(action, xRel, yRel) { - let workspaceManager = global.workspace_manager; - let activeWorkspace = workspaceManager.get_active_workspace(); - - if (!this._switchData) - this._prepareWorkspaceSwitch(activeWorkspace.index(), -1); - - if (yRel < 0 && !this._switchData.surroundings[Meta.MotionDirection.DOWN]) - yRel = 0; - if (yRel > 0 && !this._switchData.surroundings[Meta.MotionDirection.UP]) - yRel = 0; - if (xRel < 0 && !this._switchData.surroundings[Meta.MotionDirection.RIGHT]) - xRel = 0; - if (xRel > 0 && !this._switchData.surroundings[Meta.MotionDirection.LEFT]) - xRel = 0; - - this._switchData.container.set_position(xRel, yRel); - } - - _switchWorkspaceCancel() { - if (!this._switchData || this._switchData.inProgress) - return; - let switchData = this._switchData; - this._switchData = null; - switchData.container.ease({ - x: 0, - y: 0, - duration: WINDOW_ANIMATION_TIME, - mode: Clutter.AnimationMode.EASE_OUT_QUAD, - onComplete: () => this._finishWorkspaceSwitch(switchData), - }); - } - - _actionSwitchWorkspace(action, direction) { - let workspaceManager = global.workspace_manager; - let activeWorkspace = workspaceManager.get_active_workspace(); - let newWs = activeWorkspace.get_neighbor(direction); - - if (newWs == activeWorkspace) { - this._switchWorkspaceCancel(); - } else { - this._switchData.gestureActivated = true; - this.actionMoveWorkspace(newWs); - } - } - _lookupIndex(windows, metaWindow) { for (let i = 0; i < windows.length; i++) { if (windows[i].metaWindow == metaWindow) @@ -1280,7 +1093,8 @@ var WindowManager = class { } _shouldAnimate() { - return !Main.overview.visible; + return !(Main.overview.visible || + (this._switchData && this._switchData.gestureActivated)); } _shouldAnimateActor(actor, types) { @@ -1860,13 +1674,17 @@ var WindowManager = class { continue; } - let info = { index: ws.index(), - actor: new Clutter.Actor() }; + let [x, y] = this._getPositionForDirection(dir, curWs, ws); + let info = { + index: ws.index(), + actor: new Clutter.Actor(), + xDest: x, + yDest: y, + }; switchData.surroundings[dir] = info; switchData.container.add_actor(info.actor); switchData.container.set_child_above_sibling(info.actor, null); - let [x, y] = this._getPositionForDirection(dir, curWs, ws); info.actor.set_position(x, y); } @@ -1948,11 +1766,7 @@ var WindowManager = class { return; } - // If we come from a gesture, switchData will already be set, - // and we don't want to overwrite it. - if (!this._switchData) - this._prepareWorkspaceSwitch(from, to, direction); - + this._prepareWorkspaceSwitch(from, to, direction); this._switchData.inProgress = true; let workspaceManager = global.workspace_manager; @@ -1972,7 +1786,7 @@ var WindowManager = class { x: xDest, y: yDest, duration: WINDOW_ANIMATION_TIME, - mode: Clutter.AnimationMode.EASE_OUT_QUAD, + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, onComplete: () => this._switchWorkspaceDone(shellwm), }); } @@ -1982,6 +1796,158 @@ var WindowManager = class { shellwm.completed_switch_workspace(); } + _directionForProgress(progress) { + if (global.workspace_manager.layout_rows === -1) { + return progress > 0 + ? Meta.MotionDirection.DOWN + : Meta.MotionDirection.UP; + } else if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) { + return progress > 0 + ? Meta.MotionDirection.LEFT + : Meta.MotionDirection.RIGHT; + } else { + return progress > 0 + ? Meta.MotionDirection.RIGHT + : Meta.MotionDirection.LEFT; + } + } + + _getProgressRange() { + if (!this._switchData) + return [0, 0]; + + let lower = 0; + let upper = 0; + + let horiz = global.workspace_manager.layout_rows !== -1; + let baseDistance; + if (horiz) + baseDistance = global.screen_width; + else + baseDistance = global.screen_height; + + let direction = this._directionForProgress(-1); + let info = this._switchData.surroundings[direction]; + if (info !== null) { + let distance = horiz ? info.xDest : info.yDest; + lower = -Math.abs(distance) / baseDistance; + } + + direction = this._directionForProgress(1); + info = this._switchData.surroundings[direction]; + if (info !== null) { + let distance = horiz ? info.xDest : info.yDest; + upper = Math.abs(distance) / baseDistance; + } + + return [lower, upper]; + } + + _switchWorkspaceBegin(tracker, monitor) { + if (Meta.prefs_get_workspaces_only_on_primary() && + monitor !== Main.layoutManager.primaryIndex) + return; + + let workspaceManager = global.workspace_manager; + let horiz = workspaceManager.layout_rows !== -1; + tracker.orientation = horiz + ? Clutter.Orientation.HORIZONTAL + : Clutter.Orientation.VERTICAL; + + let activeWorkspace = workspaceManager.get_active_workspace(); + + let baseDistance; + if (horiz) + baseDistance = global.screen_width; + else + baseDistance = global.screen_height; + + let progress; + if (this._switchData && this._switchData.gestureActivated) { + this._switchData.container.remove_all_transitions(); + if (!horiz) + progress = -this._switchData.container.y / baseDistance; + else if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) + progress = this._switchData.container.x / baseDistance; + else + progress = -this._switchData.container.x / baseDistance; + } else { + this._prepareWorkspaceSwitch(activeWorkspace.index(), -1); + progress = 0; + } + + let points = []; + let [lower, upper] = this._getProgressRange(); + + if (lower !== 0) + points.push(lower); + + points.push(0); + + if (upper !== 0) + points.push(upper); + + tracker.confirmSwipe(baseDistance, points, progress, 0); + } + + _switchWorkspaceUpdate(tracker, progress) { + if (!this._switchData) + return; + + let direction = this._directionForProgress(progress); + let info = this._switchData.surroundings[direction]; + let xPos = 0; + let yPos = 0; + if (info) { + if (global.workspace_manager.layout_rows === -1) + yPos = -Math.round(progress * global.screen_height); + else if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) + xPos = Math.round(progress * global.screen_width); + else + xPos = -Math.round(progress * global.screen_width); + } + + this._switchData.container.set_position(xPos, yPos); + } + + _switchWorkspaceEnd(tracker, duration, endProgress) { + if (!this._switchData) + return; + + let workspaceManager = global.workspace_manager; + let activeWorkspace = workspaceManager.get_active_workspace(); + let newWs = activeWorkspace; + let xDest = 0; + let yDest = 0; + if (endProgress !== 0) { + let direction = this._directionForProgress(endProgress); + newWs = activeWorkspace.get_neighbor(direction); + xDest = -this._switchData.surroundings[direction].xDest; + yDest = -this._switchData.surroundings[direction].yDest; + } + + let switchData = this._switchData; + switchData.gestureActivated = true; + + this._switchData.container.ease({ + x: xDest, + y: yDest, + duration, + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, + onComplete: () => { + if (newWs !== activeWorkspace) + this.actionMoveWorkspace(newWs); + this._finishWorkspaceSwitch(switchData); + }, + }); + } + + _switchWorkspaceStop() { + this._switchData.container.x = 0; + this._switchData.container.y = 0; + this._finishWorkspaceSwitch(this._switchData); + } + _showTilePreview(shellwm, window, tileRect, monitorIndex) { if (!this._tilePreview) this._tilePreview = new TilePreview(); -- GitLab