| /******************************************************************************* |
| * Copyright (c) 2012, 2023 Original authors and others. |
| * |
| * This program and the accompanying materials are made |
| * available under the terms of the Eclipse Public License 2.0 |
| * which is available at https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Original authors and others - initial API and implementation |
| * neal zhang <nujiah001@126.com> - change some methods and fields visibility |
| * Loris Securo <lorissek@gmail.com> - Bug 500750 |
| ******************************************************************************/ |
| package org.eclipse.nebula.widgets.nattable.selection; |
| |
| import java.util.function.Function; |
| |
| import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes; |
| import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry; |
| import org.eclipse.nebula.widgets.nattable.layer.ILayer; |
| import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell; |
| import org.eclipse.nebula.widgets.nattable.painter.cell.BorderPainter; |
| import org.eclipse.nebula.widgets.nattable.painter.cell.BorderPainter.BorderCell; |
| import org.eclipse.nebula.widgets.nattable.painter.cell.BorderPainter.PaintModeEnum; |
| import org.eclipse.nebula.widgets.nattable.painter.layer.GridLineCellLayerPainter; |
| import org.eclipse.nebula.widgets.nattable.style.BorderStyle; |
| import org.eclipse.nebula.widgets.nattable.style.BorderStyle.BorderModeEnum; |
| import org.eclipse.nebula.widgets.nattable.style.BorderStyle.LineStyleEnum; |
| import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes; |
| import org.eclipse.nebula.widgets.nattable.style.DisplayMode; |
| import org.eclipse.nebula.widgets.nattable.style.IStyle; |
| import org.eclipse.nebula.widgets.nattable.style.SelectionStyleLabels; |
| import org.eclipse.nebula.widgets.nattable.util.GUIHelper; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.GC; |
| import org.eclipse.swt.graphics.Rectangle; |
| |
| /** |
| * Specialised GridLineCellLayerPainter that renders an additional border around |
| * selected cells. By default the additional selection anchor border style is |
| * black dotted one pixel sized line. This style can be configured via |
| * ConfigRegistry. |
| * |
| * @see SelectionStyleLabels#SELECTION_ANCHOR_GRID_LINE_STYLE |
| */ |
| public class SelectionLayerPainter extends GridLineCellLayerPainter { |
| |
| /** |
| * Create a SelectionLayerPainter that renders grid lines in the specified |
| * color and uses the default clipping behaviour. |
| * |
| * @param gridColor |
| * The color that should be used to render the grid lines. |
| */ |
| public SelectionLayerPainter(final Color gridColor) { |
| super(gridColor); |
| } |
| |
| /** |
| * Create a SelectionLayerPainter that renders gray grid lines and uses the |
| * default clipping behaviour. |
| */ |
| public SelectionLayerPainter() { |
| super(); |
| } |
| |
| /** |
| * Create a SelectionLayerPainter that renders grid lines in the specified |
| * color and uses the specified clipping behaviour. |
| * |
| * @param gridColor |
| * The color that should be used to render the grid lines. |
| * @param clipLeft |
| * Configure the rendering behaviour when cells overlap. If set |
| * to <code>true</code> the left cell will be clipped, if set to |
| * <code>false</code> the right cell will be clipped. The default |
| * value is <code>false</code>. |
| * @param clipTop |
| * Configure the rendering behaviour when cells overlap. If set |
| * to <code>true</code> the top cell will be clipped, if set to |
| * <code>false</code> the bottom cell will be clipped. The |
| * default value is <code>false</code>. |
| */ |
| public SelectionLayerPainter(final Color gridColor, boolean clipLeft, boolean clipTop) { |
| super(gridColor, clipLeft, clipTop); |
| } |
| |
| /** |
| * Create a SelectionLayerPainter that renders gray grid lines and uses the |
| * specified clipping behaviour. |
| * |
| * @param clipLeft |
| * Configure the rendering behaviour when cells overlap. If set |
| * to <code>true</code> the left cell will be clipped, if set to |
| * <code>false</code> the right cell will be clipped. The default |
| * value is <code>false</code>. |
| * @param clipTop |
| * Configure the rendering behaviour when cells overlap. If set |
| * to <code>true</code> the top cell will be clipped, if set to |
| * <code>false</code> the bottom cell will be clipped. The |
| * default value is <code>false</code>. |
| */ |
| public SelectionLayerPainter(boolean clipLeft, boolean clipTop) { |
| this(GUIHelper.COLOR_GRAY, clipLeft, clipTop); |
| } |
| |
| @Override |
| public void paintLayer( |
| ILayer natLayer, GC gc, |
| int xOffset, int yOffset, Rectangle pixelRectangle, |
| IConfigRegistry configRegistry) { |
| |
| super.paintLayer(natLayer, gc, xOffset, yOffset, pixelRectangle, configRegistry); |
| |
| Rectangle positionRectangle = getPositionRectangleFromPixelRectangle(natLayer, pixelRectangle); |
| |
| // nothing to draw, we exit |
| if (positionRectangle.width <= 0 || positionRectangle.height <= 0) { |
| return; |
| } |
| |
| final int columnOffset = natLayer.getColumnPositionByX(xOffset); |
| final int rowOffset = natLayer.getRowPositionByY(yOffset); |
| BorderCell[][] borderCells = getBorderCells(natLayer, xOffset, yOffset, positionRectangle, cell -> (cell.getColumnPosition() >= columnOffset |
| && cell.getRowPosition() >= rowOffset) |
| && (DisplayMode.SELECT.equals(cell.getDisplayMode()) |
| || DisplayMode.SELECT_HOVER.equals(cell.getDisplayMode()))); |
| |
| if (borderCells != null) { |
| // Save gc settings |
| int originalLineStyle = gc.getLineStyle(); |
| int originalLineWidth = gc.getLineWidth(); |
| Color originalForeground = gc.getForeground(); |
| |
| BorderStyle borderStyle = getBorderStyle(configRegistry); |
| |
| // on a single cell update we only need to repaint the internal |
| // borders of the |
| // external cells |
| PaintModeEnum paintMode = (positionRectangle.width <= 2 && positionRectangle.height <= 2) |
| ? PaintModeEnum.NO_EXTERNAL_BORDERS |
| : PaintModeEnum.ALL; |
| |
| BorderPainter borderPainter = new BorderPainter(borderCells, borderStyle, paintMode); |
| borderPainter.paintBorder(gc); |
| |
| // Restore original gc settings |
| gc.setLineStyle(originalLineStyle); |
| gc.setLineWidth(originalLineWidth); |
| gc.setForeground(originalForeground); |
| } |
| } |
| |
| /** |
| * Calculate the cells around which borders should be painted. |
| * |
| * @param natLayer |
| * The layer that is painted. |
| * @param xOffset |
| * of the layer from the origin of the table |
| * @param yOffset |
| * of the layer from the origin of the table |
| * @param positionRectangle |
| * The calculated position rectangle for the pixel rectangle that |
| * should be painted. |
| * @param function |
| * The function that is used to determine if a border should be |
| * applied to a cell or not. |
| * @return The {@link BorderCell}s around which the border should be painted |
| * or <code>null</code> if no border rendering is necessary. |
| * |
| * @since 2.0 |
| */ |
| protected BorderCell[][] getBorderCells(ILayer natLayer, int xOffset, int yOffset, Rectangle positionRectangle, Function<ILayerCell, Boolean> function) { |
| |
| BorderCell[][] borderCells; |
| boolean atLeastOne = false; |
| |
| int columnPositionOffset = positionRectangle.x; |
| int columnPositionEnd = columnPositionOffset + positionRectangle.width; |
| int rectangleWidth = positionRectangle.width; |
| if ((positionRectangle.width + positionRectangle.x) <= natLayer.getColumnCount()) { |
| columnPositionOffset--; |
| columnPositionEnd++; |
| rectangleWidth += 2; |
| } |
| // we are going to read also adjacent cells in the extremities to ensure |
| // that external borders are rendered correctly for single cell updates |
| int rowPositionOffset = positionRectangle.y; |
| int rowPositionEnd = rowPositionOffset + positionRectangle.height; |
| int rectangleHeight = positionRectangle.height; |
| if ((positionRectangle.height + positionRectangle.y) <= natLayer.getRowCount()) { |
| rowPositionOffset--; |
| rowPositionEnd++; |
| rectangleHeight += 2; |
| } |
| |
| borderCells = new BorderCell[rectangleHeight][rectangleWidth]; |
| |
| for (int columnPosition = columnPositionOffset, ix = 0; columnPosition < columnPositionEnd; columnPosition++, ix++) { |
| for (int rowPosition = rowPositionOffset, iy = 0; rowPosition < rowPositionEnd; rowPosition++, iy++) { |
| |
| boolean insideBorder = false; |
| Rectangle cellBounds = null; |
| |
| ILayerCell currentCell = natLayer.getCellByPosition(columnPosition, rowPosition); |
| if (currentCell != null) { |
| |
| // In case of spanned cells the border painter needs to |
| // know the bounds of adjacent cells even if they are |
| // not selected. This is the reason why we get the |
| // bounds also for non selected cells. |
| |
| cellBounds = currentCell.getBounds(); |
| |
| // the cell should be considered only if it is in our |
| // layer |
| boolean toBeConsidered = isInCurrentLayer(ix, iy, xOffset, yOffset, cellBounds, borderCells); |
| |
| if (toBeConsidered && function.apply(currentCell)) { |
| insideBorder = true; |
| atLeastOne = true; |
| } |
| } |
| |
| Rectangle fixedBounds = fixBoundsInGridLines(cellBounds, xOffset, yOffset); |
| BorderCell borderCell = new BorderCell(fixedBounds, insideBorder); |
| borderCells[iy][ix] = borderCell; |
| } |
| } |
| |
| return atLeastOne ? borderCells : null; |
| } |
| |
| /** |
| * Returns a rectangle that will cover the left and top grid lines, if they |
| * are present. |
| * |
| * @param cellBounds |
| * the rectangle that needs to be considered |
| * @param xOffset |
| * the starting x coordinate of the area we can draw on. The fix |
| * will not be applied if the <code>cellBounds</code> are placed |
| * on this limit. |
| * @param yOffset |
| * the starting y coordinate of the area we can draw on. The fix |
| * will not be applied if the <code>cellBounds</code> are placed |
| * on this limit. |
| * @return Updated rectangle. |
| * |
| * @since 1.5 |
| */ |
| protected Rectangle fixBoundsInGridLines(Rectangle cellBounds, int xOffset, int yOffset) { |
| |
| if (cellBounds == null) { |
| return null; |
| } |
| |
| Rectangle fixedBounds = new Rectangle(cellBounds.x, cellBounds.y, cellBounds.width, cellBounds.height); |
| |
| // when grid lines are rendered we want the border |
| // to cover them, otherwise we remain inside the |
| // cell |
| if (this.renderGridLines && fixedBounds.x != 0 && fixedBounds.x != xOffset) { |
| fixedBounds.x--; |
| fixedBounds.width++; |
| } |
| if (this.renderGridLines && fixedBounds.y != 0 && fixedBounds.y != yOffset) { |
| fixedBounds.y--; |
| fixedBounds.height++; |
| } |
| |
| return fixedBounds; |
| } |
| |
| /** |
| * Tries to detect if the cell is part of the current layer. It does so |
| * using xOffset/yOffset (which are not affected by single cell updates) and |
| * detecting overlapping of cells, which should not be possible in the same |
| * layer. It's not perfect, there might be false positives. |
| * |
| * @param ix |
| * column position |
| * @param iy |
| * row position |
| * @param xOffset |
| * column offset |
| * @param yOffset |
| * row offset |
| * @param cellBounds |
| * cell bounds |
| * @param borderCells |
| * available border cells |
| * @return <code>true</code> if the cell is part of the current layer. |
| * |
| * @since 1.5 |
| */ |
| protected boolean isInCurrentLayer(int ix, int iy, int xOffset, int yOffset, Rectangle cellBounds, BorderCell[][] borderCells) { |
| |
| // If the cell bounds are not inside the x/y offset, we consider it part |
| // of another layer |
| if (ix == 0) { |
| if (cellBounds.x + cellBounds.width <= xOffset) { |
| return false; |
| } |
| } |
| if (iy == 0) { |
| if (cellBounds.y + cellBounds.height <= yOffset) { |
| return false; |
| } |
| } |
| |
| // if the previous cell is overlapping the current cell we consider it |
| // part of another layer |
| if (ix == 1) { |
| if (borderCells[iy][ix - 1].isInsideBorder) { |
| Rectangle prevCellBounds = borderCells[iy][ix - 1].bounds; |
| if ((prevCellBounds != null) && (prevCellBounds.x + prevCellBounds.width > cellBounds.x)) { |
| borderCells[iy][ix - 1].isInsideBorder = false; |
| } |
| } |
| } |
| if (iy == 1) { |
| if (borderCells[iy - 1][ix].isInsideBorder) { |
| Rectangle prevCellBounds = borderCells[iy - 1][ix].bounds; |
| if ((prevCellBounds != null) && (prevCellBounds.y + prevCellBounds.height > cellBounds.y)) { |
| borderCells[iy - 1][ix].isInsideBorder = false; |
| } |
| } |
| } |
| |
| // it's an external cell and it's getting overlapped by the previous |
| // cell, we consider it part of another layer |
| if (ix == borderCells[iy].length - 1) { |
| Rectangle prevCellBounds = borderCells[iy][ix - 1].bounds; |
| if ((prevCellBounds != null) && (prevCellBounds.x + prevCellBounds.width > cellBounds.x)) { |
| return false; |
| } |
| } |
| if (iy == borderCells.length - 1) { |
| Rectangle prevCellBounds = borderCells[iy - 1][ix].bounds; |
| if ((prevCellBounds != null) && (prevCellBounds.y + prevCellBounds.height > cellBounds.y)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Get the border style that should be used to render the border for cells |
| * that are currently selected. Checks the {@link IConfigRegistry} for a |
| * registered {@link IStyle} for the |
| * {@link SelectionConfigAttributes#SELECTION_GRID_LINE_STYLE} label or the |
| * {@link SelectionStyleLabels#SELECTION_ANCHOR_GRID_LINE_STYLE} label. If |
| * none is registered, a default line style will be returned. |
| * |
| * @param configRegistry |
| * The {@link IConfigRegistry} to retrieve the style information |
| * from. |
| * |
| * @return The border style that should be used |
| * |
| * @since 1.5 |
| */ |
| protected BorderStyle getBorderStyle(IConfigRegistry configRegistry) { |
| BorderStyle borderStyle = configRegistry.getConfigAttribute( |
| SelectionConfigAttributes.SELECTION_GRID_LINE_STYLE, |
| DisplayMode.SELECT); |
| |
| // check for backwards compatibility style configuration |
| if (borderStyle == null) { |
| // Note: If there is no style configured for the |
| // SelectionStyleLabels.SELECTION_ANCHOR_GRID_LINE_STYLE |
| // label, the style configured for DisplayMode.SELECT will be |
| // retrieved by this call. |
| // Ensure that the selection style configuration does not contain a |
| // border style configuration to avoid strange rendering behavior. |
| // By default there is no border configuration added, so there |
| // shouldn't be issues with backwards compatibility. And if there |
| // are some, they can be solved easily by adding the necessary |
| // border style configuration. |
| IStyle cellStyle = configRegistry.getConfigAttribute( |
| CellConfigAttributes.CELL_STYLE, |
| DisplayMode.SELECT, |
| SelectionStyleLabels.SELECTION_ANCHOR_GRID_LINE_STYLE); |
| borderStyle = cellStyle != null ? cellStyle.getAttributeValue(CellStyleAttributes.BORDER_STYLE) : null; |
| } |
| |
| // if there is no border style configured, use the default |
| if (borderStyle == null) { |
| borderStyle = new BorderStyle(1, GUIHelper.COLOR_BLACK, LineStyleEnum.DOTTED, BorderModeEnum.CENTERED); |
| } |
| |
| return borderStyle; |
| } |
| } |