[gnome-shell] overview: Add a facility for swipe-scrolling



commit a6da22fa70e1307359af203c3140edf61739df1e
Author: Florian Müllner <fmuellner gnome org>
Date:   Fri Jan 21 14:08:42 2011 +0100

    overview: Add a facility for swipe-scrolling
    
    The workspaces view allows to drag the active workspace to swipe-scroll
    to the next or previous workspace. While this behavior can come in handy
    in general, there are good reasons to move the functionality to the
    overview:
    
     - Finding a spot on a workspace to start a drag can be hard,
       especially when the workspace contains a single window
     - With the new layout, workspaces have no visible border, making
       it hard to predict where a drag can be initiated
     - The same behavior is equally useful for other elements
    
    So add setScrollAdjustment() to the overview, which allows setting
    an adjustment controlled with swipe-scrolling (either horizontally
    or vertically); only a single adjustment can be controlled at a
    time. A swipe-scroll can be initiated on any part of the background that
    is not occupied by a reactive actor. For cases where further control
    is needed, 'swipe-scroll-start' and 'swipe-scroll-end' signals are
    emitted.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=635034

 js/ui/overview.js |  179 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 175 insertions(+), 4 deletions(-)
---
diff --git a/js/ui/overview.js b/js/ui/overview.js
index 35c1b05..8f4e830 100644
--- a/js/ui/overview.js
+++ b/js/ui/overview.js
@@ -1,6 +1,7 @@
 /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
 
 const Clutter = imports.gi.Clutter;
+const Gtk = imports.gi.Gtk;
 const Meta = imports.gi.Meta;
 const Mainloop = imports.mainloop;
 const Signals = imports.signals;
@@ -32,6 +33,18 @@ const DASH_SPLIT_FRACTION = 0.1;
 
 const DND_WINDOW_SWITCH_TIMEOUT = 1250;
 
+const SwipeScrollDirection = {
+    NONE: 0,
+    HORIZONTAL: 1,
+    VERTICAL: 2
+};
+
+const SwipeScrollResult = {
+    CANCEL: 0,
+    SWIPE: 1,
+    CLICK: 2
+};
+
 function ShellInfo() {
     this._init();
 }
@@ -98,7 +111,8 @@ Overview.prototype = {
 
         this._spacing = 0;
 
-        this._group = new St.Group({ name: 'overview' });
+        this._group = new St.Group({ name: 'overview',
+                                     reactive: true });
         this._group._delegate = this;
         this._group.connect('style-changed',
             Lang.bind(this, function() {
@@ -110,6 +124,11 @@ Overview.prototype = {
                 }
             }));
 
+        this._scrollDirection = SwipeScrollDirection.NONE;
+        this._scrollAdjustment = null;
+        this._capturedEventId = 0;
+        this._buttonPressId = 0;
+
         this.shellInfo = new ShellInfo();
 
         this._workspacesDisplay = null;
@@ -154,7 +173,7 @@ Overview.prototype = {
         this._dash.actor.add_constraint(this.viewSelector.constrainY);
         this._dash.actor.add_constraint(this.viewSelector.constrainHeight);
 
-        this._coverPane.lower_bottom();
+        this._coverPane.hide();
 
         // XDND
         this._dragMonitor = {
@@ -233,6 +252,149 @@ Overview.prototype = {
         return DND.DragMotionResult.CONTINUE;
     },
 
+    setScrollAdjustment: function(adjustment, direction) {
+        this._scrollAdjustment = adjustment;
+        if (this._scrollAdjustment == null)
+            this._scrollDirection = SwipeScrollDirection.NONE;
+        else
+            this._scrollDirection = direction;
+    },
+
+    _onButtonPress: function(actor, event) {
+        if (this._scrollDirection == SwipeScrollDirection.NONE)
+            return;
+
+        let [stageX, stageY] = event.get_coords();
+        this._dragStartX = this._dragX = stageX;
+        this._dragStartY = this._dragY = stageY;
+        this._dragStartValue = this._scrollAdjustment.value;
+        this._lastMotionTime = -1; // used to track "stopping" while swipe-scrolling
+        this._capturedEventId = global.stage.connect('captured-event',
+            Lang.bind(this, this._onCapturedEvent));
+        this.emit('swipe-scroll-begin');
+    },
+
+    _onCapturedEvent: function(actor, event) {
+        let stageX, stageY;
+        switch(event.type()) {
+            case Clutter.EventType.BUTTON_RELEASE:
+                [stageX, stageY] = event.get_coords();
+
+                // default to snapping back to the original value
+                let newValue = this._dragStartValue;
+
+                let minValue = this._scrollAdjustment.lower;
+                let maxValue = this._scrollAdjustment.upper - this._scrollAdjustment.page_size;
+
+                let direction;
+                if (this._scrollDirection == SwipeScrollDirection.HORIZONTAL) {
+                    direction = stageX > this._dragStartX ? -1 : 1;
+                    if (St.Widget.get_default_direction() == St.TextDirection.RTL)
+                        direction *= -1;
+                } else {
+                    direction = stageY > this._dragStartY ? -1 : 1;
+                }
+
+                // We default to scroll a full page size; both the first
+                // and the last page may be smaller though, so we need to
+                // adjust difference in those cases.
+                let difference = direction * this._scrollAdjustment.page_size;
+                if (this._dragStartValue + difference > maxValue)
+                    difference = maxValue - this._dragStartValue;
+                else if (this._dragStartValue + difference < minValue)
+                    difference = minValue - this._dragStartValue;
+
+                // If the user has moved more than half the scroll
+                // difference, we want to "settle" to the new value
+                // even if the user stops dragging rather "throws" by
+                // releasing during the drag.
+                let distance = this._dragStartValue - this._scrollAdjustment.value;
+                let noStop = Math.abs(distance / difference) > 0.5;
+
+                // We detect if the user is stopped by comparing the
+                // timestamp of the button release with the timestamp of
+                // the last motion. Experimentally, a difference of 0 or 1
+                // millisecond indicates that the mouse is in motion, a
+                // larger difference indicates that the mouse is stopped.
+                if ((this._lastMotionTime > 0 &&
+                     this._lastMotionTime > event.get_time() - 2) ||
+                    noStop) {
+                    if (this._dragStartValue + difference >= minValue &&
+                        this._dragStartValue + difference <= maxValue)
+                        newValue += difference;
+                }
+
+                // See if the user has moved the mouse enough to trigger
+                // a drag
+                let threshold = Gtk.Settings.get_default().gtk_dnd_drag_threshold;
+                if (Math.abs(stageX - this._dragStartX) < threshold &&
+                    Math.abs(stageY - this._dragStartY) < threshold) {
+                    // no motion? It's a click!
+                    this.emit('swipe-scroll-end', SwipeScrollResult.CLICK);
+                } else {
+                    let result;
+
+                    if (newValue == this._dragStartValue)
+                        result = SwipeScrollResult.CANCEL;
+                    else
+                        result = SwipeScrollResult.SWIPE;
+
+                    // The event capture handler is disconnected
+                    // while scrolling to the final position, so
+                    // to avoid undesired prelights we raise
+                    // the cover pane.
+                    this._coverPane.raise_top();
+                    this._coverPane.show();
+
+                    Tweener.addTween(this._scrollAdjustment,
+                                     { value: newValue,
+                                       time: ANIMATION_TIME,
+                                       transition: 'easeOutQuad',
+                                       onCompleteScope: this,
+                                       onComplete: function() {
+                                          this._coverPane.hide();
+                                          this.emit('swipe-scroll-end',
+                                                    result);
+                                       }
+                                     });
+                }
+
+                global.stage.disconnect(this._capturedEventId);
+                this._capturedEventId = 0;
+
+                return true;
+
+            case Clutter.EventType.MOTION:
+                [stageX, stageY] = event.get_coords();
+                let dx = this._dragX - stageX;
+                let dy = this._dragY - stageY;
+                let primary = global.get_primary_monitor();
+
+                if (this._scrollDirection == SwipeScrollDirection.HORIZONTAL) {
+                    if (St.Widget.get_default_direction() == St.TextDirection.RTL)
+                        this._scrollAdjustment.value -= (dx / primary.width) * this._scrollAdjustment.page_size;
+                    else
+                        this._scrollAdjustment.value += (dx / primary.width) * this._scrollAdjustment.page_size;
+                } else {
+                    this._scrollAdjustment.value += (dy / primary.height) * this._scrollAdjustment.page_size;
+                }
+
+                this._dragX = stageX;
+                this._dragY = stageY;
+                this._lastMotionTime = event.get_time();
+
+                return true;
+
+            // Block enter/leave events to avoid prelights
+            // during swipe-scroll
+            case Clutter.EventType.ENTER:
+            case Clutter.EventType.LEAVE:
+                return true;
+        }
+
+        return false;
+    },
+
     _getDesktopClone: function() {
         let windows = global.get_window_actors().filter(function(w) {
             return w.meta_window.get_window_type() == Meta.WindowType.DESKTOP;
@@ -335,6 +497,9 @@ Overview.prototype = {
         this._modal = true;
         this._animateVisible();
         this._shown = true;
+
+        this._buttonPressId = this._group.connect('button-press-event',
+            Lang.bind(this, this._onButtonPress));
     },
 
     _animateVisible: function() {
@@ -402,6 +567,7 @@ Overview.prototype = {
                          });
 
         this._coverPane.raise_top();
+        this._coverPane.show();
         this.emit('showing');
     },
 
@@ -432,6 +598,10 @@ Overview.prototype = {
 
         this._shown = false;
         this._syncInputMode();
+
+        if (this._buttonPressId > 0)
+            this._group.disconnect(this._buttonPressId);
+        this._buttonPressId = 0;
     },
 
     // hideTemporarily:
@@ -541,13 +711,14 @@ Overview.prototype = {
                          });
 
         this._coverPane.raise_top();
+        this._coverPane.show();
         this.emit('hiding');
     },
 
     _showDone: function() {
         this.animationInProgress = false;
         this._desktopFade.hide();
-        this._coverPane.lower_bottom();
+        this._coverPane.hide();
 
         this.emit('shown');
         // Handle any calls to hide* while we were showing
@@ -575,7 +746,7 @@ Overview.prototype = {
         this.animationInProgress = false;
         this._hideInProgress = false;
 
-        this._coverPane.lower_bottom();
+        this._coverPane.hide();
 
         this.emit('hidden');
         // Handle any calls to show* while we were hiding



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]