diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index b5348ddcb53dd0744011cc05fdb96fa3b881706a..aec3427e0bb5e93ff60c9b1fbd3d32ed9f1252d3 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/appDisplay.js b/js/ui/appDisplay.js index 0156877e8fbb7edfcb10a1aafb100359db79acce..da8f56219599a88b49251605db3b0256817a44ac 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) { diff --git a/js/ui/swipeTracker.js b/js/ui/swipeTracker.js new file mode 100644 index 0000000000000000000000000000000000000000..65c3d0d649999703e9bacf01d3168e9da0d88441 --- /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; + } +}); diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js index 7716bab7414707dfcfb6b96399c198bc95283a31..f85298cd53b74dbae53e5ee48151dfecd29628fa 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(); diff --git a/js/ui/workspacesView.js b/js/ui/workspacesView.js index aa4dd65fcf92bf644089b77f65c8ecf3a5adbe04..975dd076042741dd5febd3208313b8fe6e2fc8b1 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;