diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 807c43a53..eadbe9bb4 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -404,6 +404,58 @@ var BaseAppView = GObject.registerClass({ } } + _getDropTarget(x, y, source) { + const { currentPage } = this._grid; + + let [item, dragLocation] = this._grid.getDropTarget(x, y); + + const [sourcePage, sourcePosition] = this._grid.getItemPosition(source); + const targetPage = currentPage; + let targetPosition = item + ? this._grid.getItemPosition(item)[1] : -1; + + // In case we're hovering over the edge of an item but the + // reflow will happen in the opposite direction (the drag + // can't "naturally push the item away"), we instead set the + // drop target to the adjacent item that can be pushed away + // in the reflow-direction. + // + // We must avoid doing that if we're hovering over the first + // or last column though, in that case there is no adjacent + // icon we could push away. + if (dragLocation === IconGrid.DragLocation.START_EDGE && + targetPosition > sourcePosition && + targetPage === sourcePage) { + const nColumns = this._grid.layout_manager.columns_per_page; + const targetColumn = targetPosition % nColumns; + + if (targetColumn > 0) { + targetPosition -= 1; + dragLocation = IconGrid.DragLocation.END_EDGE; + } + } else if (dragLocation === IconGrid.DragLocation.END_EDGE && + (targetPosition < sourcePosition || + targetPage !== sourcePage)) { + const nColumns = this._grid.layout_manager.columns_per_page; + const targetColumn = targetPosition % nColumns; + + if (targetColumn < nColumns - 1) { + targetPosition += 1; + dragLocation = IconGrid.DragLocation.START_EDGE; + } + } + + // Append to the page if dragging over empty area + if (dragLocation === IconGrid.DragLocation.EMPTY_SPACE) { + const pageItems = + this._grid.getItemsAtPage(currentPage).filter(c => c.visible); + + targetPosition = pageItems.length; + } + + return [targetPage, targetPosition, dragLocation]; + } + vfunc_allocate(box) { const width = box.get_width(); const height = box.get_height(); diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js index ecbbcb3cc..2d3a17a62 100644 --- a/js/ui/iconGrid.js +++ b/js/ui/iconGrid.js @@ -52,6 +52,17 @@ const defaultGridModes = [ }, ]; +var LEFT_DIVIDER_LEEWAY = 20; +var RIGHT_DIVIDER_LEEWAY = 20; + +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) { @@ -946,6 +957,99 @@ var IconGridLayout = GObject.registerClass({ } } + /** + * getDropTarget: + * @param {int} x: position of the horizontal axis + * @param {int} y: position of the vertical axis + * + * Retrieves the item located at (@x, @y), as well as the drag location. + * Both @x and @y are relative to the grid. + * + * @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); + + // Out of bounds + if (page >= this._pages.length) + return [null, DragLocation.INVALID]; + + 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 (const item of visibleItems) { + const childBox = item.allocation.copy(); + + // Page offset + switch (this._orientation) { + case Clutter.Orientation.HORIZONTAL: + childBox.set_origin(childBox.x1 % this._pageWidth, childBox.y1); + break; + case Clutter.Orientation.VERTICAL: + childBox.set_origin(childBox.x1, childBox.y1 % 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; @@ -1559,6 +1663,11 @@ var IconGrid = GObject.registerClass({ this.queue_relayout(); } + 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;