[gnome-shell/T27795: 132/138] iconGrid: Add item nudge API



commit 13540ec5289356fb8e892b4314d0de4cee65797b
Author: Georges Basile Stavracas Neto <georges stavracas gmail com>
Date:   Tue Jul 2 17:39:59 2019 -0300

    iconGrid: Add item nudge API
    
    Nudging an item can be done via one side, or both. This is
    essentially a set of helpers for Drag n' Drop to use. The
    x and y values are relative to the icon grid itself.
    
    https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/603

 js/ui/appDisplay.js |  12 +++
 js/ui/iconGrid.js   | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 265 insertions(+)
---
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index a9a7a193e7..20e07659e9 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -302,6 +302,18 @@ class BaseAppView {
     get gridActor() {
         return this._grid;
     }
+
+    canDropAt(x, y) {
+        return this._grid.canDropAt(x, y);
+    }
+
+    nudgeItemsAtIndex(index, dragLocation) {
+        this._grid.nudgeItemsAtIndex(index, dragLocation);
+    }
+
+    removeNudges() {
+        this._grid.removeNudges();
+    }
 }
 Signals.addSignalMethods(BaseAppView.prototype);
 
diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js
index d671593c5e..885d43e282 100644
--- a/js/ui/iconGrid.js
+++ b/js/ui/iconGrid.js
@@ -28,6 +28,25 @@ var AnimationDirection = {
 var APPICON_ANIMATION_OUT_SCALE = 3;
 var APPICON_ANIMATION_OUT_TIME = 250;
 
+const LEFT_DIVIDER_LEEWAY = 30;
+const RIGHT_DIVIDER_LEEWAY = 30;
+
+const NUDGE_ANIMATION_TYPE = Clutter.AnimationMode.EASE_OUT_ELASTIC;
+const NUDGE_DURATION = 800;
+
+const NUDGE_RETURN_ANIMATION_TYPE = Clutter.AnimationMode.EASE_OUT_QUINT;
+const NUDGE_RETURN_DURATION = 300;
+
+const NUDGE_FACTOR = 0.33;
+
+var DragLocation = {
+    DEFAULT: 0,
+    ON_ICON: 1,
+    START_EDGE: 2,
+    END_EDGE: 3,
+    EMPTY_AREA: 4,
+}
+
 var BaseIcon = GObject.registerClass(
 class BaseIcon extends St.Bin {
     _init(label, params) {
@@ -695,6 +714,223 @@ var IconGrid = GObject.registerClass({
         this._fixedHItemSize = this._hItemSize;
         this._fixedVItemSize = this._vItemSize;
     }
+
+    // Drag n' Drop methods
+
+    nudgeItemsAtIndex(index, dragLocation) {
+        // No nudging when the cursor is in an empty area
+        if (dragLocation == DragLocation.EMPTY_AREA || dragLocation == DragLocation.ON_ICON)
+            return;
+
+        let children = this.get_children().filter(c => c.is_visible());
+        let nudgeIndex = index;
+        let rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL);
+
+        if (dragLocation != DragLocation.START_EDGE) {
+            let leftItem = children[nudgeIndex - 1];
+            let offset = rtl ? Math.floor(this._hItemSize * NUDGE_FACTOR) : Math.floor(-this._hItemSize * 
NUDGE_FACTOR);
+            this._animateNudge(leftItem, NUDGE_ANIMATION_TYPE, NUDGE_DURATION, offset);
+        }
+
+        // Nudge the icon to the right if we are the first item or not at the
+        // end of row
+        if (dragLocation != DragLocation.END_EDGE) {
+            let rightItem = children[nudgeIndex];
+            let offset = rtl ? Math.floor(-this._hItemSize * NUDGE_FACTOR) : Math.floor(this._hItemSize * 
NUDGE_FACTOR);
+            this._animateNudge(rightItem, NUDGE_ANIMATION_TYPE, NUDGE_DURATION, offset);
+        }
+    }
+
+    removeNudges() {
+        let children = this.get_children().filter(c => c.is_visible());
+        for (let index = 0; index < children.length; index++) {
+            this._animateNudge(children[index],
+                               NUDGE_RETURN_ANIMATION_TYPE,
+                               NUDGE_RETURN_DURATION,
+                               0);
+        }
+    }
+
+    _animateNudge(item, animationType, duration, offset) {
+        if (!item)
+            return;
+
+        item.save_easing_state();
+        item.set_easing_mode(animationType);
+        item.set_easing_duration(duration);
+        item.translation_x = offset;
+        item.restore_easing_state();
+    }
+
+    // This function is overriden by the PaginatedIconGrid subclass so we can
+    // take into account the extra space when dragging from a folder
+    _calculateDndRow(y) {
+        let rowHeight = this._getVItemSize() + this._getSpacing();
+        return Math.floor(y / rowHeight);
+    }
+
+    // Returns the drop point index or -1 if we can't drop there
+    canDropAt(x, y) {
+        // This is an complex calculation, but in essence, we divide the grid
+        // as:
+        //
+        //  left empty space
+        //      |   left padding                          right padding
+        //      |     |        width without padding               |
+        // +--------+---+---------------------------------------+-----+
+        // |        |   |        |           |          |       |     |
+        // |        |   |        |           |          |       |     |
+        // |        |   |--------+-----------+----------+-------|     |
+        // |        |   |        |           |          |       |     |
+        // |        |   |        |           |          |       |     |
+        // |        |   |--------+-----------+----------+-------|     |
+        // |        |   |        |           |          |       |     |
+        // |        |   |        |           |          |       |     |
+        // |        |   |--------+-----------+----------+-------|     |
+        // |        |   |        |           |          |       |     |
+        // |        |   |        |           |          |       |     |
+        // +--------+---+---------------------------------------+-----+
+        //
+        // The left empty space is immediately discarded, and ignored in all
+        // calculations.
+        //
+        // The width (with paddings) is used to determine if we're dragging
+        // over the left or right padding, and which column is being dragged
+        // on.
+        //
+        // Finally, the width without padding is used to figure out where in
+        // the icon (start edge, end edge, on it, etc) the cursor is.
+
+        let [nColumns, usedWidth] = this._computeLayout(this.width);
+
+        let leftEmptySpace;
+        switch (this._xAlign) {
+        case St.Align.START:
+            leftEmptySpace = 0;
+            break;
+        case St.Align.MIDDLE:
+            leftEmptySpace = Math.floor((this.width - usedWidth) / 2);
+            break;
+        case St.Align.END:
+            leftEmptySpace = availWidth - usedWidth;
+        }
+
+        x -= leftEmptySpace;
+        y -= this.topPadding;
+
+        let row = this._calculateDndRow(y);
+
+        // Correct sx to handle the left padding to correctly calculate
+        // the column
+        let rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL);
+        let gridX = x - this.leftPadding;
+
+        let widthWithoutPadding = usedWidth - this.leftPadding - this.rightPadding;
+        let columnWidth = widthWithoutPadding / nColumns;
+
+        let column;
+        if (x < this.leftPadding)
+            column = 0;
+        else if (x > usedWidth - this.rightPadding)
+            column = nColumns - 1;
+        else
+            column = Math.floor(gridX / columnWidth);
+
+        let isFirstIcon = column == 0;
+        let isLastIcon = column == nColumns - 1;
+
+        // If we're outside of the grid, we are in an invalid drop location
+        if (x < 0 || x > usedWidth)
+            return [-1, DragLocation.DEFAULT];
+
+        let children = this.get_children().filter(c => c.is_visible());
+        let childIndex = Math.min((row * nColumns) + column, children.length);
+
+        // If we're above the grid vertically, we are in an invalid
+        // drop location
+        if (childIndex < 0)
+            return [-1, DragLocation.DEFAULT];
+
+        // If we're past the last visible element in the grid,
+        // we might be allowed to drop there.
+        if (childIndex >= children.length)
+            return [children.length, DragLocation.EMPTY_AREA];
+
+        let child = children[childIndex];
+        let [childMinWidth, childMinHeight, childNaturalWidth, childNaturalHeight] = 
child.get_preferred_size();
+
+        // This is the width of the cell that contains the icon
+        // (excluding spacing between cells)
+        let childIconWidth = Math.max(this._getHItemSize(), childNaturalWidth);
+
+        // Calculate the original position of the child icon (prior to nudging)
+        let childX;
+        if (rtl)
+            childX = widthWithoutPadding - (column * columnWidth) - childIconWidth;
+        else
+            childX = column * columnWidth;
+
+        let iconLeftX = childX + LEFT_DIVIDER_LEEWAY;
+        let iconRightX = childX + childIconWidth - RIGHT_DIVIDER_LEEWAY
+
+        let dropIndex;
+        let dragLocation;
+
+        x -= this.leftPadding;
+
+        if (x < iconLeftX) {
+            // We are to the left of the icon target
+            if (isFirstIcon || x < 0) {
+                // We are before the leftmost icon on the grid
+                if (rtl) {
+                    dropIndex = childIndex + 1;
+                    dragLocation = DragLocation.END_EDGE;
+                } else {
+                    dropIndex = childIndex;
+                    dragLocation = DragLocation.START_EDGE;
+                }
+            } else {
+                // We are between the previous icon (next in RTL) and this one
+                if (rtl)
+                    dropIndex = childIndex + 1;
+                else
+                    dropIndex = childIndex;
+
+                dragLocation = DragLocation.DEFAULT;
+            }
+        } else if (x >= iconRightX) {
+            // We are to the right of the icon target
+            if (childIndex >= children.length) {
+                // We are beyond the last valid icon
+                // (to the right of the app store / trash can, if present)
+                dropIndex = -1;
+                dragLocation = DragLocation.DEFAULT;
+            } else if (isLastIcon || x >= widthWithoutPadding) {
+                // We are beyond the rightmost icon on the grid
+                if (rtl) {
+                    dropIndex = childIndex;
+                    dragLocation = DragLocation.START_EDGE;
+                } else {
+                    dropIndex = childIndex + 1;
+                    dragLocation = DragLocation.END_EDGE;
+                }
+            } else {
+                // We are between this icon and the next one (previous in RTL)
+                if (rtl)
+                    dropIndex = childIndex;
+                else
+                    dropIndex = childIndex + 1;
+
+                dragLocation = DragLocation.DEFAULT;
+            }
+        } else {
+            // We are over the icon target area
+            dropIndex = childIndex;
+            dragLocation = DragLocation.ON_ICON;
+        }
+
+        return [dropIndex, dragLocation];
+    }
 });
 
 var PaginatedIconGrid = GObject.registerClass({
@@ -770,6 +1006,23 @@ var PaginatedIconGrid = GObject.registerClass({
     }
 
     // Overridden from IconGrid
+    _calculateDndRow(y) {
+        let row = super._calculateDndRow(y);
+
+        // If there's no extra space, just return the current value and maintain
+        // the same behavior when without a folder opened.
+        if (!this._extraSpaceData)
+            return row;
+
+        let [ baseRow, nRowsUp, nRowsDown ] = this._extraSpaceData;
+        let newRow = row + nRowsUp;
+
+        if (row > baseRow)
+            newRow -= nRowsDown;
+
+        return newRow;
+    }
+
     _getChildrenToAnimate() {
         let children = super._getChildrenToAnimate();
         let firstIndex = this._childrenPerPage * this.currentPage;


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