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;