All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.dua3.meja.ui.SheetPainterBase Maven / Gradle / Ivy

/*
 * Copyright 2016 Axel Howind.
 *
 * 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 com.dua3.meja.ui;

import java.util.concurrent.locks.Lock;

import com.dua3.cabe.annotations.Nullable;
import com.dua3.meja.model.BorderStyle;
import com.dua3.meja.model.Cell;
import com.dua3.meja.model.CellStyle;
import com.dua3.meja.model.Direction;
import com.dua3.meja.model.FillPattern;
import com.dua3.meja.model.Row;
import com.dua3.meja.model.Sheet;
import com.dua3.utility.data.Color;

/**
 * A helper class that implements the actual drawing algorithm.
 *
 * @param  the concrete class implementing SheetView
 * @param  the concrete class implementing GraphicsContext
 */
public abstract class SheetPainterBase {

    public static final String MEJA_USE_XOR_DRAWING = "MEJA_USE_XOR_DRAWING";

    private final boolean useXorDrawing = System.getProperty(MEJA_USE_XOR_DRAWING, "true").equalsIgnoreCase("true");

    enum CellDrawMode {
        /**
         *
         */
        DRAW_CELL_BACKGROUND,
        /**
         *
         */
        DRAW_CELL_BORDER,
        /**
         *
         */
        DRAW_CELL_FOREGROUND
    }

    /**
     * Horizontal padding.
     */
    protected static final int PADDING_X = 2;

    /**
     * Vertical padding.
     */
    protected static final int PADDING_Y = 1;

    /**
     * Color used to draw the selection rectangle.
     */
    protected static final Color SELECTION_COLOR = Color.BLACK;

    /**
     * Width of the selection rectangle borders.
     */
    protected static final int SELECTION_STROKE_WIDTH = 4;

    /**
     * Test whether style uses text wrapping. While there is a property for text
     * wrapping, the alignment settings have to be taken into account too.
     *
     * @param style style
     * @return true if cell content should be displayed with text wrapping
     */
    static boolean isWrapping(CellStyle style) {
        return style.isWrap() || style.getHAlign().isWrap() || style.getVAlign().isWrap();
    }

    protected final SV sheetView;

    /**
     * Reference to the sheet.
     */
    private Sheet sheet = null;

    /**
     * Array with column positions (x-axis) in pixels.
     */
    private double[] columnPos = { 0 };

    /**
     * Array with column positions (y-axis) in pixels.
     */
    private double[] rowPos = { 0 };
    private double sheetHeightInPoints = 0;

    private double sheetWidthInPoints = 0;

    protected SheetPainterBase(SV sheetView) {
        this.sheetView = sheetView;
    }

    public void drawSheet(GC gc) {
        if (sheet == null) {
            return;
        }

        Lock readLock = sheet.readLock();
        readLock.lock();
        try {
            beginDraw(gc);

            drawBackground(gc);

            drawLabels(gc);

            drawCells(gc, CellDrawMode.DRAW_CELL_BACKGROUND);
            drawCells(gc, CellDrawMode.DRAW_CELL_BORDER);
            drawCells(gc, CellDrawMode.DRAW_CELL_FOREGROUND);
            drawSelection(gc);

            endDraw(gc);
        } finally {
            readLock.unlock();
        }
    }

    /**
     * Calculate the rectangle the cell occupies on screen.
     *
     * @param cell the cell whose area is requested
     * @return the rectangle the cell takes up in screen coordinates
     */
    public Rectangle getCellRect(Cell cell) {
        final int i = cell.getRowNumber();
        final int j = cell.getColumnNumber();

        final double x = getColumnPos(j);
        final double w = getColumnPos(j + cell.getHorizontalSpan()) - x;
        final double y = getRowPos(i);
        final double h = getRowPos(i + cell.getVerticalSpan()) - y;

        return new Rectangle(x, y, w, h);
    }

    /**
     * Get number of columns for the currently loaded sheet.
     *
     * @return number of columns
     */
    public int getColumnCount() {
        return columnPos.length - 1;
    }

    /**
     * Get the column number that the given x-coordinate belongs to.
     *
     * @param x x-coordinate
     *
     * @return
     *         
    *
  • -1, if the first column is displayed to the right of the given * coordinate *
  • number of columns, if the right edge of the last column is * displayed to the left of the given coordinate *
  • the number of the column that belongs to the given coordinate *
*/ public int getColumnNumberFromX(double x) { if (columnPos.length == 0) { return 0; } // guess position int j = (int) (columnPos.length * x / sheetWidthInPoints); if (j < 0) { j = 0; } else if (j >= columnPos.length) { j = columnPos.length - 1; } // linear search from here if (getColumnPos(j) > x) { while (j > 0 && getColumnPos(j - 1) > x) { j--; } } else { while (j < columnPos.length && getColumnPos(j) <= x) { j++; } } return j - 1; } /** * @param j the column number * @return the columnPos */ public double getColumnPos(int j) { return columnPos[Math.min(columnPos.length - 1, j)]; } /** * Get number of rows for the currently loaded sheet. * * @return number of rows */ public int getRowCount() { return rowPos.length - 1; } /** * Get the row number that the given y-coordinate belongs to. * * @param y y-coordinate * * @return *
    *
  • -1, if the first row is displayed below the given coordinate *
  • number of rows, if the lower edge of the last row is displayed * above the given coordinate *
  • the number of the row that belongs to the given coordinate *
*/ public int getRowNumberFromY(double y) { if (rowPos.length == 0) { return 0; } // guess position int i = (int) (rowPos.length * y / sheetHeightInPoints); if (i < 0) { i = 0; } else if (i >= rowPos.length) { i = rowPos.length - 1; } // linear search from here if (getRowPos(i) > y) { while (i > 0 && getRowPos(i - 1) > y) { i--; } } else { while (i < rowPos.length && getRowPos(i) <= y) { i++; } } return i - 1; } /** * @param i the row number * @return the rowPos */ public double getRowPos(int i) { return rowPos[Math.min(rowPos.length - 1, i)]; } /** * Get display coordinates of selection rectangle. * * @param cell the selected cell * @return selection rectangle in display coordinates */ public Rectangle getSelectionRect(Cell cell) { Rectangle cellRect = getCellRect(cell.getLogicalCell()); double extra = (getSelectionStrokeWidth() + 1) / 2; return new Rectangle(cellRect.getX() - extra, cellRect.getY() - extra, cellRect.getW() + 2 * extra, cellRect.getH() + 2 * extra); } public double getSheetHeightInPoints() { return sheetHeightInPoints; } public double getSheetWidthInPoints() { return sheetWidthInPoints; } public double getSplitX() { return getColumnPos(sheet.getSplitColumn()); } public double getSplitY() { return getRowPos(sheet.getSplitRow()); } public void update(@Nullable Sheet sheet) { //noinspection ObjectEquality if (sheet != this.sheet) { this.sheet = sheet; } // determine sheet dimensions if (sheet == null) { sheetWidthInPoints = 0; sheetHeightInPoints = 0; rowPos = new double[] { 0 }; columnPos = new double[] { 0 }; return; } Lock readLock = sheet.readLock(); readLock.lock(); try { sheetHeightInPoints = 0; rowPos = new double[2 + sheet.getLastRowNum()]; rowPos[0] = 0; for (int i = 1; i < rowPos.length; i++) { sheetHeightInPoints += sheet.getRowHeight(i - 1); rowPos[i] = sheetHeightInPoints; } sheetWidthInPoints = 0; columnPos = new double[2 + sheet.getLastColNum()]; columnPos[0] = 0; for (int j = 1; j < columnPos.length; j++) { sheetWidthInPoints += sheet.getColumnWidth(j - 1); columnPos[j] = sheetWidthInPoints; } } finally { readLock.unlock(); } } /** * Draw cell background. * * @param g the graphics context to use * @param cell cell to draw */ private void drawCellBackground(GC g, Cell cell) { Rectangle cr = getCellRect(cell); // draw grid lines g.setColor(getGridColor()); g.drawRect(cr.getX(), cr.getY(), cr.getW(), cr.getH()); CellStyle style = cell.getCellStyle(); FillPattern pattern = style.getFillPattern(); if (pattern == FillPattern.NONE) { return; } Color fillFgColor = style.getFillFgColor(); if (fillFgColor != null) { g.setColor(fillFgColor); g.fillRect(cr.getX(), cr.getY(), cr.getW(), cr.getH()); } if (pattern != FillPattern.SOLID) { Color fillBgColor = style.getFillBgColor(); if (fillBgColor != null) { g.setColor(fillBgColor); g.fillRect(cr.getX(), cr.getY(), cr.getW(), cr.getH()); } } } /** * Draw cell border. * * @param g the graphics context to use * @param cell cell to draw */ private void drawCellBorder(GC g, Cell cell) { CellStyle styleTopLeft = cell.getCellStyle(); Cell cellBottomRight = sheet.getRow(cell.getRowNumber() + cell.getVerticalSpan() - 1) .getCell(cell.getColumnNumber() + cell.getHorizontalSpan() - 1); CellStyle styleBottomRight = cellBottomRight.getCellStyle(); Rectangle cr = getCellRect(cell); // draw border for (Direction d : Direction.values()) { boolean isTopLeft = d == Direction.NORTH || d == Direction.WEST; CellStyle style = isTopLeft ? styleTopLeft : styleBottomRight; BorderStyle b = style.getBorderStyle(d); if (b.width() == 0) { continue; } Color color = b.color(); if (color == null) { color = Color.BLACK; } g.setStroke(color, b.width()); switch (d) { case NORTH -> g.drawLine(cr.getLeft(), cr.getTop(), cr.getRight(), cr.getTop()); case EAST -> g.drawLine(cr.getRight(), cr.getTop(), cr.getRight(), cr.getBottom()); case SOUTH -> g.drawLine(cr.getLeft(), cr.getBottom(), cr.getRight(), cr.getBottom()); case WEST -> g.drawLine(cr.getLeft(), cr.getTop(), cr.getLeft(), cr.getBottom()); } } } /** * Draw cell foreground. * * @param g the graphics context to use * @param cell cell to draw */ private void drawCellForeground(GC g, Cell cell) { if (cell.isEmpty()) { return; } double paddingX = getPaddingX(); double paddingY = getPaddingY(); // the rectangle used for positioning the text Rectangle textRect = getCellRect(cell); textRect = new Rectangle(textRect.getX() + paddingX, textRect.getY() + paddingY, textRect.getW() - 2 * paddingX, textRect.getH() - 2 * paddingY); // the clipping rectangle final Rectangle clipRect; final CellStyle style = cell.getCellStyle(); if (isWrapping(style)) { clipRect = textRect; } else { Row row = cell.getRow(); double clipXMin = textRect.getX(); for (int j = cell.getColumnNumber() - 1; j > 0; j--) { if (!row.getCell(j).isEmpty()) { break; } clipXMin = getColumnPos(j) + paddingX; } double clipXMax = textRect.getRight(); for (int j = cell.getColumnNumber() + 1; j < getColumnCount(); j++) { if (!row.getCell(j).isEmpty()) { break; } clipXMax = getColumnPos(j + 1) - paddingX; } clipRect = new Rectangle(clipXMin, textRect.getY(), clipXMax - clipXMin, textRect.getH()); } render(g, cell, textRect, clipRect); } /** * Draw frame around current selection. * * @param gc graphics object used for drawing */ private void drawSelection(GC gc) { // no sheet, no drawing if (sheet == null) { return; } Cell logicalCell = sheet.getCurrentCell().getLogicalCell(); Rectangle rect = getCellRect(logicalCell); gc.setXOR(useXorDrawing); gc.setStroke(SELECTION_COLOR, getSelectionStrokeWidth()); gc.drawRect(rect.getX(), rect.getY(), rect.getW(), rect.getH()); gc.setXOR(false); } private String getColumnName(int j) { return sheetView.getColumnName(j); } private String getRowName(int i) { return sheetView.getRowName(i); } protected void beginDraw(GC gc) { // nop } protected abstract void drawBackground(GC gc); protected abstract void drawLabel(GC gc, Rectangle rect, String text); protected void drawLabels(GC gc) { // determine visible rows and columns Rectangle clipBounds = gc.getClipBounds(); int startRow = Math.max(0, getRowNumberFromY(clipBounds.getTop())); int endRow = Math.min(getRowCount(), 1 + getRowNumberFromY(clipBounds.getBottom())); int startColumn = Math.max(0, getColumnNumberFromX(clipBounds.getLeft())); int endColumn = Math.min(getColumnCount(), 1 + getColumnNumberFromX(clipBounds.getRight())); // draw row labels Rectangle r = new Rectangle(-getRowLabelWidth(), 0, getRowLabelWidth(), 0); for (int i = startRow; i < endRow; i++) { r.setY(getRowPos(i)); r.setH(getRowPos(i + 1) - r.getY()); String text = getRowName(i); drawLabel(gc, r, text); } // draw column labels r = new Rectangle(0, -getColumnLabelHeight(), 0, getColumnLabelHeight()); for (int j = startColumn; j < endColumn; j++) { r.setX(getColumnPos(j)); r.setW(getColumnPos(j + 1) - r.getX()); String text = getColumnName(j); drawLabel(gc, r, text); } } protected void endDraw(GC gc) { // nop } protected abstract double getColumnLabelHeight(); protected Color getGridColor() { return sheetView.getGridColor(); } protected double getPaddingX() { return PADDING_X; } protected double getPaddingY() { return PADDING_Y; } protected abstract double getRowLabelWidth(); protected Color getSelectionColor() { return SELECTION_COLOR; } protected double getSelectionStrokeWidth() { return SELECTION_STROKE_WIDTH; } protected abstract void render(GC g, Cell cell, Rectangle textRect, Rectangle clipRect); /** * Draw cells. *

* Since borders can be draw over by the background of adjacent cells and text * can overlap, drawing is done in three steps: *

    *
  • draw background for all cells *
  • draw borders for all cells *
  • draw foreground all cells *
* This is controlled by {@code cellDrawMode}. * * @param g the graphics object to use * @param cellDrawMode the draw mode to use */ void drawCells(GC g, CellDrawMode cellDrawMode) { // no sheet, no drawing if (sheet == null) { return; } double maxWidth = SheetView.MAX_COLUMN_WIDTH; Rectangle clipBounds = g.getClipBounds(); // determine visible rows and columns int startRow = Math.max(0, getRowNumberFromY(clipBounds.getTop())); int endRow = Math.min(getRowCount(), 1 + getRowNumberFromY(clipBounds.getBottom())); int startColumn = Math.max(0, getColumnNumberFromX(clipBounds.getLeft())); int endColumn = Math.min(getColumnCount(), 1 + getColumnNumberFromX(clipBounds.getRight())); // Collect cells to be drawn for (int i = startRow; i < endRow; i++) { Row row = sheet.getRow(i); if (row == null) { continue; } // if first/last displayed cell of row is empty, start drawing at // the first non-empty cell to the left/right to make sure // overflowing text is visible. int first = startColumn; while (first > 0 && getColumnPos(first) + maxWidth > clipBounds.getLeft() && row.getCell(first).isEmpty()) { first--; } int end = endColumn; while (end < getColumnCount() && getColumnPos(end) - maxWidth < clipBounds.getRight() && (end <= 0 || row.getCell(end - 1).isEmpty())) { end++; } for (int j = first; j < end; j++) { Cell cell = row.getCell(j); Cell logicalCell = cell.getLogicalCell(); final boolean visible; //noinspection ObjectEquality if (cell == logicalCell) { // if cell is not merged or the top left cell of the // merged region, then it is visible visible = true; } else { // otherwise calculate row and column numbers of the // first visible cell of the merged region int iCell = Math.max(startRow, logicalCell.getRowNumber()); int jCell = Math.max(first, logicalCell.getColumnNumber()); visible = i == iCell && j == jCell; // skip the other cells of this row that belong to the same // merged region j = logicalCell.getColumnNumber() + logicalCell.getHorizontalSpan() - 1; // filter out cells that cannot overflow into the visible // region if (j < startColumn && isWrapping(cell.getCellStyle())) { continue; } } // draw cell if (visible) { switch (cellDrawMode) { case DRAW_CELL_BACKGROUND -> drawCellBackground(g, logicalCell); case DRAW_CELL_BORDER -> drawCellBorder(g, logicalCell); case DRAW_CELL_FOREGROUND -> drawCellForeground(g, logicalCell); } } } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy