/* * Copyright 2015 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.uberfire.ext.wires.core.grids.client.widget.dnd; import java.util.ArrayList; import java.util.List; import com.ait.lienzo.client.core.event.NodeMouseMoveEvent; import com.ait.lienzo.client.core.event.NodeMouseMoveHandler; import com.ait.lienzo.client.core.mediator.IMediator; import com.ait.lienzo.client.core.shape.Group; import com.ait.lienzo.client.core.types.Point2D; import com.google.gwt.dom.client.Style; import org.uberfire.ext.wires.core.grids.client.model.GridColumn; import org.uberfire.ext.wires.core.grids.client.model.GridData; import org.uberfire.ext.wires.core.grids.client.model.GridRow; import org.uberfire.ext.wires.core.grids.client.util.CoordinateUtilities; import org.uberfire.ext.wires.core.grids.client.widget.dom.HasDOMElementResources; import org.uberfire.ext.wires.core.grids.client.widget.grid.GridWidget; import org.uberfire.ext.wires.core.grids.client.widget.grid.renderers.grids.GridRenderer; import org.uberfire.ext.wires.core.grids.client.widget.grid.renderers.grids.impl.BaseGridRendererHelper; import org.uberfire.ext.wires.core.grids.client.widget.layer.GridLayer; import org.uberfire.ext.wires.core.grids.client.widget.layer.pinning.impl.RestrictedMousePanMediator; /** * MouseMoveHandler to handle potential drag operations and handle the drag itself; if required. */ public class GridWidgetDnDMouseMoveHandler implements NodeMouseMoveHandler { // How close the mouse pointer needs to be to the column separator to initiate a resize operation. private static final int COLUMN_RESIZE_HANDLE_SENSITIVITY = 5; private final GridLayer layer; private final GridWidgetDnDHandlersState state; public GridWidgetDnDMouseMoveHandler(final GridLayer layer, final GridWidgetDnDHandlersState state) { this.layer = layer; this.state = state; } @Override @SuppressWarnings("unchecked") public void onNodeMouseMove(final NodeMouseMoveEvent event) { switch (state.getOperation()) { case GRID_MOVE: //The grid is draggable. This is handled by Lienzo. break; case COLUMN_RESIZE: //If we're currently resizing a column we don't need to find a column handleColumnResize(event); break; case COLUMN_MOVE: //If we're currently moving a column we don't need to find a column handleColumnMove(event); break; case ROW_MOVE: //If we're currently moving a row we don't need to find a row handleRowMove(event); break; default: //Otherwise try to find a Grid and GridColumn(s) findGridColumn(event); } } void findGridColumn(final NodeMouseMoveEvent event) { state.reset(); setCursor(state.getCursor()); for (GridWidget gridWidget : layer.getGridWidgets()) { gridWidget.setDraggable(false); if (!gridWidget.isVisible()) { continue; } if (!gridWidget.getModel().isColumnDraggingEnabled()) { continue; } final Group header = gridWidget.getHeader(); final GridRenderer renderer = gridWidget.getRenderer(); final double headerHeight = renderer.getHeaderHeight(); final double headerRowsYOffset = getHeaderRowsYOffset(gridWidget, renderer); final double headerMinY = (header == null ? headerRowsYOffset : header.getY() + headerRowsYOffset); final double headerMaxY = (header == null ? headerHeight : headerHeight + header.getY()); final Point2D ap = CoordinateUtilities.convertDOMToGridCoordinate(gridWidget, new Point2D(event.getX(), event.getY())); final double cx = ap.getX(); final double cy = ap.getY(); //If over Grid's drag handle prime for dragging if (gridWidget.onDragHandle(event)) { state.setActiveGridWidget(gridWidget); state.setOperation(GridWidgetDnDHandlersState.GridWidgetHandlersOperation.GRID_MOVE_PENDING); continue; } //Check bounds if (cx < 0 || cx > gridWidget.getWidth()) { continue; } if (cy < headerMinY || cy > gridWidget.getHeight()) { continue; } if (cy < headerMaxY) { //Check for column moving findMovableColumns(gridWidget, headerHeight - headerRowsYOffset, headerMinY, cx, cy); } else { //Check for movable rows findMovableRows(gridWidget, cx, cy); //Check for column resizing findResizableColumn(gridWidget, cx); } //If over Grid but no operation has been identified default to Grid dragging if (state.getActiveGridWidget() == null) { state.setActiveGridWidget(gridWidget); state.setOperation(GridWidgetDnDHandlersState.GridWidgetHandlersOperation.GRID_MOVE_PENDING); } } for (IMediator mediator : layer.getViewport().getMediators()) { mediator.setEnabled(state.getActiveGridWidget() == null); } } private double getHeaderRowsYOffset(final GridWidget gridWidget, final GridRenderer renderer) { final GridData model = gridWidget.getModel(); final int headerRowCount = model.getHeaderRowCount(); final double headerHeight = renderer.getHeaderHeight(); final double headerRowHeight = renderer.getHeaderRowHeight(); final double headerRowsHeight = headerRowHeight * headerRowCount; final double headerRowsYOffset = headerHeight - headerRowsHeight; return headerRowsYOffset; } private void setCursor(final Style.Cursor cursor) { for (IMediator mediator : layer.getViewport().getMediators()) { if (mediator instanceof RestrictedMousePanMediator) { if (((RestrictedMousePanMediator) mediator).isDragging()) { return; } } } layer.getViewport().getElement().getStyle().setCursor(cursor); state.setCursor(cursor); } void findResizableColumn(final GridWidget view, final double cx) { //Gather information on columns final BaseGridRendererHelper rendererHelper = view.getRendererHelper(); final BaseGridRendererHelper.RenderingInformation renderingInformation = rendererHelper.getRenderingInformation(); if (renderingInformation == null) { return; } final BaseGridRendererHelper.RenderingBlockInformation bodyBlockInformation = renderingInformation.getBodyBlockInformation(); final BaseGridRendererHelper.RenderingBlockInformation floatingBlockInformation = renderingInformation.getFloatingBlockInformation(); final List<GridColumn<?>> bodyColumns = bodyBlockInformation.getColumns(); final List<GridColumn<?>> floatingColumns = floatingBlockInformation.getColumns(); final double bodyX = bodyBlockInformation.getX(); final double floatingX = floatingBlockInformation.getX(); final double floatingWidth = floatingBlockInformation.getWidth(); //Check floating columns double offsetX = floatingX; GridColumn<?> column = null; for (GridColumn<?> gridColumn : floatingColumns) { if (gridColumn.isVisible()) { if (gridColumn.isResizable()) { final double columnWidth = gridColumn.getWidth(); if (cx > columnWidth + offsetX - COLUMN_RESIZE_HANDLE_SENSITIVITY && cx < columnWidth + offsetX + COLUMN_RESIZE_HANDLE_SENSITIVITY) { column = gridColumn; break; } } offsetX = offsetX + gridColumn.getWidth(); } } //Check all other columns if (column == null) { offsetX = bodyX; for (GridColumn<?> gridColumn : bodyColumns) { if (gridColumn.isVisible()) { if (gridColumn.isResizable()) { final double columnWidth = gridColumn.getWidth(); if (offsetX + gridColumn.getWidth() > floatingX + floatingWidth) { if (cx > columnWidth + offsetX - COLUMN_RESIZE_HANDLE_SENSITIVITY && cx < columnWidth + offsetX + COLUMN_RESIZE_HANDLE_SENSITIVITY) { column = gridColumn; break; } } } offsetX = offsetX + gridColumn.getWidth(); } } } if (column != null) { final List<GridColumn<?>> activeColumns = new ArrayList<>(); activeColumns.add(column); state.setActiveGridWidget(view); state.setActiveGridColumns(activeColumns); state.setOperation(GridWidgetDnDHandlersState.GridWidgetHandlersOperation.COLUMN_RESIZE_PENDING); setCursor(Style.Cursor.COL_RESIZE); } } void findMovableColumns(final GridWidget view, final double headerRowsHeight, final double headerMinY, final double cx, final double cy) { //Gather information on columns final BaseGridRendererHelper rendererHelper = view.getRendererHelper(); final BaseGridRendererHelper.RenderingInformation renderingInformation = rendererHelper.getRenderingInformation(); if (renderingInformation == null) { return; } final BaseGridRendererHelper.RenderingBlockInformation bodyBlockInformation = renderingInformation.getBodyBlockInformation(); final BaseGridRendererHelper.RenderingBlockInformation floatingBlockInformation = renderingInformation.getFloatingBlockInformation(); final List<GridColumn<?>> allColumns = view.getModel().getColumns(); final List<GridColumn<?>> bodyColumns = bodyBlockInformation.getColumns(); final List<GridColumn<?>> floatingColumns = floatingBlockInformation.getColumns(); final double bodyX = bodyBlockInformation.getX(); final double floatingX = floatingBlockInformation.getX(); final double floatingWidth = floatingBlockInformation.getWidth(); //Check all other columns. Floating columns cannot be moved. double offsetX = bodyX; for (int headerColumnIndex = 0; headerColumnIndex < bodyColumns.size(); headerColumnIndex++) { final GridColumn<?> gridColumn = bodyColumns.get(headerColumnIndex); final double columnWidth = gridColumn.getWidth(); if (gridColumn.isVisible()) { final List<GridColumn.HeaderMetaData> headerMetaData = gridColumn.getHeaderMetaData(); final double headerRowHeight = headerRowsHeight / headerMetaData.size(); for (int headerRowIndex = 0; headerRowIndex < headerMetaData.size(); headerRowIndex++) { final GridColumn.HeaderMetaData md = headerMetaData.get(headerRowIndex); if (gridColumn.isMovable()) { if (cy < (headerRowIndex + 1) * headerRowHeight + headerMinY) { if (cx > floatingX + floatingWidth) { if (cx > offsetX && cx < offsetX + columnWidth) { //Get the block of columns to be moved. final List<GridColumn<?>> blockColumns = getBlockColumns(allColumns, headerMetaData, headerRowIndex, allColumns.indexOf(gridColumn)); //If the columns to move are split between body and floating we cannot move them. for (GridColumn<?> blockColumn : blockColumns) { if (floatingColumns.contains(blockColumn)) { return; } } state.setActiveGridWidget(view); state.setActiveGridColumns(blockColumns); state.setActiveHeaderMetaData(md); state.setOperation(GridWidgetDnDHandlersState.GridWidgetHandlersOperation.COLUMN_MOVE_PENDING); setCursor(Style.Cursor.MOVE); return; } } } } } offsetX = offsetX + columnWidth; } } } private List<GridColumn<?>> getBlockColumns(final List<GridColumn<?>> allColumns, final List<GridColumn.HeaderMetaData> headerMetaData, final int headerRowIndex, final int headerColumnIndex) { final int blockStartColumnIndex = getBlockStartColumnIndex(allColumns, headerMetaData.get(headerRowIndex), headerRowIndex, headerColumnIndex); final int blockEndColumnIndex = getBlockEndColumnIndex(allColumns, headerMetaData.get(headerRowIndex), headerRowIndex, headerColumnIndex); final List<GridColumn<?>> columns = new ArrayList<GridColumn<?>>(); columns.addAll(allColumns.subList(blockStartColumnIndex, blockEndColumnIndex + 1)); return columns; } @SuppressWarnings("unchecked") private int getBlockStartColumnIndex(final List<? extends GridColumn<?>> allColumns, final GridColumn.HeaderMetaData headerMetaData, final int headerRowIndex, final int headerColumnIndex) { //Back-track adding width of proceeding columns sharing header MetaData int candidateHeaderColumnIndex = headerColumnIndex; if (candidateHeaderColumnIndex == 0) { return candidateHeaderColumnIndex; } while (candidateHeaderColumnIndex > 0) { final GridColumn candidateColumn = allColumns.get(candidateHeaderColumnIndex - 1); final List<GridColumn.HeaderMetaData> candidateHeaderMetaData = candidateColumn.getHeaderMetaData(); if (candidateHeaderMetaData.size() - 1 < headerRowIndex) { break; } if (!candidateHeaderMetaData.get(headerRowIndex).equals(headerMetaData)) { break; } candidateHeaderColumnIndex--; } return candidateHeaderColumnIndex; } @SuppressWarnings("unchecked") private int getBlockEndColumnIndex(final List<? extends GridColumn<?>> allColumns, final GridColumn.HeaderMetaData headerMetaData, final int headerRowIndex, final int headerColumnIndex) { //Forward-track adding width of following columns sharing header MetaData int candidateHeaderColumnIndex = headerColumnIndex; if (candidateHeaderColumnIndex == allColumns.size() - 1) { return candidateHeaderColumnIndex; } while (candidateHeaderColumnIndex < allColumns.size() - 1) { final GridColumn candidateColumn = allColumns.get(candidateHeaderColumnIndex + 1); final List<GridColumn.HeaderMetaData> candidateHeaderMetaData = candidateColumn.getHeaderMetaData(); if (candidateHeaderMetaData.size() - 1 < headerRowIndex) { break; } if (!candidateHeaderMetaData.get(headerRowIndex).equals(headerMetaData)) { break; } candidateHeaderColumnIndex++; } return candidateHeaderColumnIndex; } void findMovableRows(final GridWidget view, final double cx, final double cy) { if (!isOverRowDragHandleColumn(view, cx)) { return; } final GridData gridModel = view.getModel(); final GridRenderer renderer = view.getRenderer(); //Get row index if (gridModel.getRowCount() == 0) { return; } GridRow row; int uiRowIndex = 0; double offsetY = cy - renderer.getHeaderHeight(); while ((row = gridModel.getRow(uiRowIndex)).getHeight() < offsetY) { offsetY = offsetY - row.getHeight(); uiRowIndex++; } if (uiRowIndex < 0 || uiRowIndex > gridModel.getRowCount() - 1) { return; } //Add row over which MouseEvent occurred final List<GridRow> rows = new ArrayList<GridRow>(); rows.add(gridModel.getRow(uiRowIndex)); //Add any other collapsed rows GridRow collapsedRow; while (uiRowIndex + 1 < gridModel.getRowCount() && (collapsedRow = gridModel.getRow(uiRowIndex + 1)).isCollapsed()) { rows.add(collapsedRow); uiRowIndex++; } state.setActiveGridWidget(view); state.setActiveGridRows(rows); state.setOperation(GridWidgetDnDHandlersState.GridWidgetHandlersOperation.ROW_MOVE_PENDING); setCursor(Style.Cursor.MOVE); } private boolean isOverRowDragHandleColumn(final GridWidget view, final double cx) { //Gather information on columns final BaseGridRendererHelper rendererHelper = view.getRendererHelper(); final BaseGridRendererHelper.RenderingInformation renderingInformation = rendererHelper.getRenderingInformation(); if (renderingInformation == null) { return false; } final BaseGridRendererHelper.RenderingBlockInformation bodyBlockInformation = renderingInformation.getBodyBlockInformation(); final BaseGridRendererHelper.RenderingBlockInformation floatingBlockInformation = renderingInformation.getFloatingBlockInformation(); final List<GridColumn<?>> bodyColumns = bodyBlockInformation.getColumns(); final List<GridColumn<?>> floatingColumns = floatingBlockInformation.getColumns(); final double bodyX = bodyBlockInformation.getX(); final double floatingX = floatingBlockInformation.getX(); //Check floating columns if (findRowDragHandleColumn(floatingColumns, floatingX, cx) != null) { return true; } //Check all other columns return findRowDragHandleColumn(bodyColumns, bodyX, cx) != null; } private GridColumn<?> findRowDragHandleColumn(final List<GridColumn<?>> columns, final double offsetX, final double cx) { double _offsetX = offsetX; for (GridColumn<?> gridColumn : columns) { if (gridColumn.isVisible()) { if (gridColumn instanceof IsRowDragHandle) { final double columnWidth = gridColumn.getWidth(); if (cx > _offsetX && cx < _offsetX + columnWidth) { return gridColumn; } } _offsetX = _offsetX + gridColumn.getWidth(); } } return null; } void handleColumnResize(final NodeMouseMoveEvent event) { final GridWidget activeGridWidget = state.getActiveGridWidget(); final List<GridColumn<?>> activeGridColumns = state.getActiveGridColumns(); if (activeGridColumns.size() > 1) { return; } final GridColumn<?> activeGridColumn = activeGridColumns.get(0); final GridData activeGridModel = activeGridWidget.getModel(); final List<GridColumn<?>> allGridColumns = activeGridModel.getColumns(); final Point2D ap = CoordinateUtilities.convertDOMToGridCoordinate(activeGridWidget, new Point2D(event.getX(), event.getY())); final double deltaX = ap.getX() - state.getEventInitialX(); final Double columnMinimumWidth = activeGridColumn.getMinimumWidth(); final Double columnMaximumWidth = activeGridColumn.getMaximumWidth(); double columnNewWidth = state.getEventInitialColumnWidth() + deltaX; if (columnMinimumWidth != null) { if (columnNewWidth < columnMinimumWidth) { columnNewWidth = columnMinimumWidth; } } if (columnMaximumWidth != null) { if (columnNewWidth > columnMaximumWidth) { columnNewWidth = columnMaximumWidth; } } destroyColumns(allGridColumns); activeGridColumn.setWidth(columnNewWidth); layer.batch(); } @SuppressWarnings("unchecked") void handleColumnMove(final NodeMouseMoveEvent event) { final GridWidget activeGridWidget = state.getActiveGridWidget(); final List<GridColumn<?>> activeGridColumns = state.getActiveGridColumns(); final GridColumn.HeaderMetaData activeHeaderMetaData = state.getActiveHeaderMetaData(); final GridData activeGridModel = activeGridWidget.getModel(); final List<GridColumn<?>> allGridColumns = activeGridModel.getColumns(); final BaseGridRendererHelper rendererHelper = activeGridWidget.getRendererHelper(); final BaseGridRendererHelper.RenderingInformation renderingInformation = rendererHelper.getRenderingInformation(); if (renderingInformation == null) { return; } final BaseGridRendererHelper.RenderingBlockInformation floatingBlockInformation = renderingInformation.getFloatingBlockInformation(); final double floatingX = floatingBlockInformation.getX(); final double floatingWidth = floatingBlockInformation.getWidth(); final Point2D ap = CoordinateUtilities.convertDOMToGridCoordinate(activeGridWidget, new Point2D(event.getX(), event.getY())); final double cx = ap.getX(); if (cx < floatingX + floatingWidth) { return; } final double activeBlockWidth = getBlockWidth(allGridColumns, allGridColumns.indexOf(activeGridColumns.get(0)), allGridColumns.indexOf(activeGridColumns.get(activeGridColumns.size() - 1))); for (int headerColumnIndex = 0; headerColumnIndex < allGridColumns.size(); headerColumnIndex++) { final GridColumn<?> candidateGridColumn = allGridColumns.get(headerColumnIndex); if (candidateGridColumn.isVisible()) { if (!activeGridColumns.contains(candidateGridColumn)) { for (int headerRowIndex = 0; headerRowIndex < candidateGridColumn.getHeaderMetaData().size(); headerRowIndex++) { final GridColumn.HeaderMetaData candidateHeaderMetaData = candidateGridColumn.getHeaderMetaData().get(headerRowIndex); if (candidateHeaderMetaData.getColumnGroup().equals(activeHeaderMetaData.getColumnGroup())) { final int candidateBlockStartColumnIndex = getBlockStartColumnIndex(allGridColumns, candidateHeaderMetaData, headerRowIndex, headerColumnIndex); final int candidateBlockEndColumnIndex = getBlockEndColumnIndex(allGridColumns, candidateHeaderMetaData, headerRowIndex, headerColumnIndex); final double candidateBlockOffset = rendererHelper.getColumnOffset(candidateBlockStartColumnIndex); final double candidateBlockWidth = getBlockWidth(allGridColumns, candidateBlockStartColumnIndex, candidateBlockEndColumnIndex); final double minColX = Math.max(candidateBlockOffset, candidateBlockOffset + (candidateBlockWidth - activeBlockWidth) / 2); final double maxColX = Math.min(candidateBlockOffset + candidateBlockWidth, candidateBlockOffset + (candidateBlockWidth + activeBlockWidth) / 2); final double midColX = candidateBlockOffset + candidateBlockWidth / 2; if (cx > minColX && cx < maxColX) { if (cx < midColX) { destroyColumns(allGridColumns); activeGridModel.moveColumnsTo(candidateBlockEndColumnIndex, activeGridColumns); state.getEventColumnHighlight().setX(activeGridWidget.getX() + rendererHelper.getColumnOffset(activeGridColumns.get(0))); layer.batch(); return; } else { destroyColumns(allGridColumns); activeGridModel.moveColumnsTo(candidateBlockStartColumnIndex, activeGridColumns); state.getEventColumnHighlight().setX(activeGridWidget.getX() + rendererHelper.getColumnOffset(activeGridColumns.get(0))); layer.batch(); return; } } } } } } } } private double getBlockWidth(final List<? extends GridColumn> columns, final int blockStartColumnIndex, final int blockEndColumnIndex) { double blockWidth = 0; for (int blockColumnIndex = blockStartColumnIndex; blockColumnIndex <= blockEndColumnIndex; blockColumnIndex++) { final GridColumn column = columns.get(blockColumnIndex); if (column.isVisible()) { blockWidth = blockWidth + column.getWidth(); } } return blockWidth; } void handleRowMove(final NodeMouseMoveEvent event) { final GridWidget activeGridWidget = state.getActiveGridWidget(); final List<GridRow> activeGridRows = state.getActiveGridRows(); final GridData activeGridModel = activeGridWidget.getModel(); final List<GridColumn<?>> allGridColumns = activeGridModel.getColumns(); final BaseGridRendererHelper rendererHelper = activeGridWidget.getRendererHelper(); final GridRenderer renderer = activeGridWidget.getRenderer(); final double headerHeight = renderer.getHeaderHeight(); final GridRow leadRow = activeGridRows.get(0); final int leadRowIndex = activeGridModel.getRows().indexOf(leadRow); final Point2D ap = CoordinateUtilities.convertDOMToGridCoordinate(activeGridWidget, new Point2D(event.getX(), event.getY())); final double cy = ap.getY(); if (cy < headerHeight || cy > activeGridWidget.getHeight()) { return; } //Find new row index GridRow row; int uiRowIndex = 0; double offsetY = cy - headerHeight; while ((row = activeGridModel.getRow(uiRowIndex)).getHeight() < offsetY) { offsetY = offsetY - row.getHeight(); uiRowIndex++; } if (uiRowIndex < 0 || uiRowIndex > activeGridModel.getRowCount() - 1) { return; } if (uiRowIndex == leadRowIndex) { //Don't move if the new rowIndex equals the index of the row(s) being moved return; } else if (uiRowIndex < activeGridModel.getRows().indexOf(leadRow)) { //Don't move up if the pointer is in the bottom half of the target row. if (offsetY > activeGridModel.getRow(uiRowIndex).getHeight() / 2) { return; } } else if (uiRowIndex > activeGridModel.getRows().indexOf(leadRow)) { //Don't move down if the pointer is in the top half of the target row. if (offsetY < activeGridModel.getRow(uiRowIndex).getHeight() / 2) { return; } } //Move row(s) and update highlight destroyColumns(allGridColumns); activeGridModel.moveRowsTo(uiRowIndex, activeGridRows); final double rowOffsetY = rendererHelper.getRowOffset(leadRow) + headerHeight; state.getEventColumnHighlight().setY(activeGridWidget.getY() + rowOffsetY); layer.batch(); } //Destroy all DOMElement based columns as their creation stores the GridBodyCellRenderContext //which is used to write updated GridCellValue(s) to the underlying GridData at the coordinate //at which the DOMElement was created. Moving Rows and Columns changes these coordinates and //hence the reference held in the DOMElement becomes out of date. private void destroyColumns(final List<GridColumn<?>> columns) { for (GridColumn<?> column : columns) { if (column instanceof HasDOMElementResources) { ((HasDOMElementResources) column).destroyResources(); } } } }