blob: 945c33005bf4b71a59134d8a8461d5ce6aedc5e5 [file] [log] [blame]
/*******************************************************************************
* 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;
}
}