[gnome-shell/gbsneto/custom-icon-positions: 22/40] iconGrid: Add item nudge API



commit a856bfcfe54fdf59867de270f36b2c216f82e86e
Author: Georges Basile Stavracas Neto <georges stavracas gmail com>
Date:   Mon May 25 15:58:39 2020 -0300

    iconGrid: Add item nudge API
    
    Add a new item nudging API, and the acompanying drop target
    API. The bulk of it is implemented by IconGridLayout, since
    it's this layout manager that knows where each icon is placed
    at.
    
    https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1284

 js/ui/appDisplay.js |  85 ++++++++++++++++++++++++++
 js/ui/iconGrid.js   | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 252 insertions(+)
---
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index 20cbd480b7..b004493710 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -180,6 +180,8 @@ var BaseAppView = GObject.registerClass({
 
         this._availWidth = 0;
         this._availHeight = 0;
+        this._nudgedItem = null;
+        this._nudgedLocation = -1;
 
         this._items = new Map();
         this._orderedItems = [];
@@ -350,6 +352,47 @@ var BaseAppView = GObject.registerClass({
         this._grid.opacity = 255;
     }
 
+    _getRealDropPosition(item, dragLocation) {
+        const itemsPerPage = this._grid.itemsPerPage;
+        let [page, position] = this._grid.getItemPosition(item);
+
+        if (dragLocation === IconGrid.DragLocation.END_EDGE)
+            position += 1;
+
+        if (position >= itemsPerPage) {
+            position %= itemsPerPage;
+            page++;
+        } else if (position < 0) {
+            page--;
+            if (page >= 0) {
+                const pageItems =
+                    this._grid.getItemsAtPage(page).filter(c => c.visible);
+                position = pageItems.length - 1;
+            } else {
+                position = 0;
+                page = 0;
+            }
+        }
+
+        return [page, position];
+    }
+
+    _nudgedItemReallyChanged(item, dragLocation) {
+        if (!this._nudgedItem)
+            return true;
+
+        if (dragLocation === IconGrid.DragLocation.ON_ICON ||
+            this._nudgedLocation === IconGrid.DragLocation.ON_ICON)
+            return true;
+
+        const [realPage, realPosition] =
+            this._getRealDropPosition(item, dragLocation);
+        const [oldPage, oldPosition] =
+            this._getRealDropPosition(this._nudgedItem, this._nudgedLocation);
+
+        return realPage !== oldPage || realPosition !== oldPosition;
+    }
+
     animate(animationDirection, onComplete) {
         if (onComplete) {
             let animationDoneId = this._grid.connect('animation-done', () => {
@@ -435,6 +478,48 @@ var BaseAppView = GObject.registerClass({
         this._grid.goToPage(pageNumber, animate);
     }
 
+    getDropTarget(x, y) {
+        let [item, dragLocation] = this._grid.getDropTarget(x, y);
+
+        // Append to the page if dragging over empty area
+        if (dragLocation === IconGrid.DragLocation.EMPTY_SPACE) {
+            const currentPage = this._grid.currentPage;
+            const pageItems =
+                this._grid.getItemsAtPage(currentPage).filter(c => c.visible);
+
+            item = pageItems[pageItems.length - 1];
+            dragLocation = IconGrid.DragLocation.END_EDGE;
+        }
+
+        return [item, dragLocation];
+    }
+
+    nudgeItem(item, dragLocation) {
+        if (this._nudgedItem === item && this._nudgedLocation === dragLocation)
+            return;
+
+        // Did the nudged item actually change?
+        if (!this._nudgedItemReallyChanged(item, dragLocation))
+            return;
+
+        this.removeNudges();
+
+        this._nudgedItem = item;
+        this._nudgedLocation = dragLocation;
+
+        this._grid.nudgeItem(item, dragLocation);
+    }
+
+    removeNudges() {
+        if (!this._nudgedItem)
+            return;
+
+        this._nudgedItem = null;
+        this._nudgedLocation = -1;
+
+        this._grid.removeNudges();
+    }
+
     adaptToSize(width, height) {
         let box = new Clutter.ActorBox();
         box.x1 = 0;
diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js
index 4347cafc64..f0da69b508 100644
--- a/js/ui/iconGrid.js
+++ b/js/ui/iconGrid.js
@@ -52,6 +52,25 @@ const gridModes = [
     },
 ];
 
+var LEFT_DIVIDER_LEEWAY = 20;
+var RIGHT_DIVIDER_LEEWAY = 20;
+
+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 = {
+    INVALID: 0,
+    START_EDGE: 1,
+    ON_ICON: 2,
+    END_EDGE: 3,
+    EMPTY_SPACE: 4,
+};
+
 var BaseIcon = GObject.registerClass(
 class BaseIcon extends St.Bin {
     _init(label, params) {
@@ -899,6 +918,97 @@ var IconGridLayout = GObject.registerClass({
         return itemData.pageIndex;
     }
 
+    /**
+     * getDropTarget:
+     * @param {int} x: position of the horizontal axis
+     * @param {int} y: position of the vertical axis
+     * @param {int} page: page, or -1 for the current page
+     *
+     * Retrieves the item located at (@x, @y), as well as the position.
+     * Both @x and @y are relative to the current page.
+     *
+     * @returns {[Clutter.Actor, DragLocation]} the item and drag location
+     * under (@x, @y)
+     */
+    getDropTarget(x, y) {
+        const childSize = this._getChildrenMaxSize();
+        const [leftEmptySpace, topEmptySpace, hSpacing, vSpacing] =
+            this._calculateSpacing(childSize);
+
+        const isRtl =
+            Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
+
+        let page = this._orientation === Clutter.Orientation.VERTICAL
+            ? Math.floor(y / this._pageHeight)
+            : Math.floor(x / this._pageWidth);
+
+        if (isRtl && this._orientation === Clutter.Orientation.HORIZONTAL)
+            page = swap(page, this._pages.length);
+
+        // Page-relative coordinates from now on
+        x %= this._pageWidth;
+        y %= this._pageHeight;
+
+        if (x < leftEmptySpace || y < topEmptySpace)
+            return [null, DragLocation.INVALID];
+
+        const gridWidth =
+            childSize * this._columnsPerPage +
+            hSpacing * (this._columnsPerPage - 1);
+        const gridHeight =
+            childSize * this._rowsPerPage +
+            vSpacing * (this._rowsPerPage - 1);
+
+        if (x > leftEmptySpace + gridWidth || y > topEmptySpace + gridHeight)
+            return [null, DragLocation.INVALID];
+
+        const halfHSpacing = hSpacing / 2;
+        const halfVSpacing = vSpacing / 2;
+        const visibleItems = this._getVisibleChildrenForPage(page);
+
+        for (let itemIndex in visibleItems) {
+            const item = visibleItems[itemIndex];
+            const childBox = item.get_allocation_box().copy();
+
+            // Page offset
+            switch (this._orientation) {
+            case Clutter.Orientation.HORIZONTAL:
+                childBox.set_origin(childBox.x1 - page * this._pageWidth, childBox.y1);
+                break;
+            case Clutter.Orientation.VERTICAL:
+                childBox.set_origin(childBox.x1, childBox.y1 - page * this._pageHeight);
+                break;
+            }
+
+            // Outside the icon boundaries
+            if (x < childBox.x1 - halfHSpacing ||
+                x > childBox.x2 + halfHSpacing ||
+                y < childBox.y1 - halfVSpacing ||
+                y > childBox.y2 + halfVSpacing)
+                continue;
+
+            let dragLocation;
+
+            if (x < childBox.x1 + LEFT_DIVIDER_LEEWAY)
+                dragLocation = DragLocation.START_EDGE;
+            else if (x > childBox.x2 - RIGHT_DIVIDER_LEEWAY)
+                dragLocation = DragLocation.END_EDGE;
+            else
+                dragLocation = DragLocation.ON_ICON;
+
+            if (isRtl) {
+                if (dragLocation === DragLocation.START_EDGE)
+                    dragLocation = DragLocation.END_EDGE;
+                else if (dragLocation === DragLocation.END_EDGE)
+                    dragLocation = DragLocation.START_EDGE;
+            }
+
+            return [item, dragLocation];
+        }
+
+        return [null, DragLocation.EMPTY_SPACE];
+    }
+
     // eslint-disable-next-line camelcase
     get allow_incomplete_pages() {
         return this._allowIncompletePages;
@@ -1525,6 +1635,63 @@ var IconGrid = GObject.registerClass({
         });
     }
 
+    nudgeItem(item, dragLocation) {
+        if (dragLocation === DragLocation.INVALID ||
+            dragLocation === DragLocation.EMPTY_AREA ||
+            dragLocation === DragLocation.ON_ICON)
+            return;
+
+        const layoutManager = this.layout_manager;
+        const columnsPerPage = layoutManager.columns_per_page;
+        const itemPage = layoutManager.getItemPage(item);
+        const children = layoutManager.getChildrenAtPage(itemPage).filter(c => c.visible);
+        const nudgeIndex = children.indexOf(item);
+        const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
+
+        if (dragLocation === DragLocation.START_EDGE) {
+            const offset = rtl
+                ? Math.floor(item.width * NUDGE_FACTOR)
+                : Math.floor(-item.width * NUDGE_FACTOR);
+
+            const leftItem = children[nudgeIndex - 1];
+            if (nudgeIndex % columnsPerPage > 0)
+                this._animateNudge(leftItem, NUDGE_ANIMATION_TYPE, NUDGE_DURATION, offset);
+
+            this._animateNudge(item, NUDGE_ANIMATION_TYPE, NUDGE_DURATION, -offset);
+        }
+
+        if (dragLocation === DragLocation.END_EDGE) {
+            const offset = rtl
+                ? Math.floor(-item.width * NUDGE_FACTOR)
+                : Math.floor(item.width * NUDGE_FACTOR);
+
+            this._animateNudge(item, NUDGE_ANIMATION_TYPE, NUDGE_DURATION, -offset);
+
+            const rightItem = children[nudgeIndex + 1];
+            if (nudgeIndex < children.length - 1 &&
+                nudgeIndex % columnsPerPage < columnsPerPage - 1)
+                this._animateNudge(rightItem, NUDGE_ANIMATION_TYPE, NUDGE_DURATION, offset);
+        }
+    }
+
+    removeNudges() {
+        const children = this.get_children().filter(c => c.visible);
+        for (let child of children)
+            this._animateNudge(child, NUDGE_RETURN_ANIMATION_TYPE, NUDGE_RETURN_DURATION, 0);
+    }
+
+    _animateNudge(item, animationType, duration, offset) {
+        item.ease({
+            translation_x: offset,
+            mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD,
+        });
+    }
+
+    getDropTarget(x, y) {
+        const layoutManager = this.layout_manager;
+        return layoutManager.getDropTarget(x, y, this._currentPage);
+    }
+
     get itemsPerPage() {
         const layoutManager = this.layout_manager;
         return layoutManager.rows_per_page * layoutManager.columns_per_page;


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