Source code for grid_renderer

# -*- coding: utf-8 -*-

# Copyright Martin Manns
# Distributed under the terms of the GNU General Public License

# --------------------------------------------------------------------
# pyspread is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyspread is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyspread.  If not, see <http://www.gnu.org/licenses/>.
# --------------------------------------------------------------------

"""

**Provides**

 * :func: `painter_save`: Context manager saving and restoring painter state
 * :func: `painter_zoom`: Context manager scaling and restoring the painter
 * :func: `painter_rotate`: Context manager rotating and restoring the painter
 * :class:`GridCellNavigator`: Find neighbors of a cell
 * :class:`EdgeBorders`: Dataclass for edge properties
 * :class:`CellEdgeRenderer`: Paints cell edges
 * :class:`QColorCache`: QColor cache
 * :class:`CellRenderer`: Paints cells

"""

from contextlib import contextmanager
try:
    from dataclasses import dataclass
except ImportError:
    from pyspread.lib.dataclasses import dataclass  # Python 3.6 compatibility
from typing import List, Tuple

from PyQt5.QtCore import Qt, QModelIndex, QRectF, QLineF, QPointF
from PyQt5.QtGui import QBrush, QColor, QPainter, QPalette, QPen
from PyQt5.QtWidgets import QTableView, QStyleOptionViewItem


[docs]@contextmanager def painter_save(painter: QPainter): """Context manager saving and restoring painter state :param painter: Painter, for which the state is preserved """ painter.save() yield painter.restore()
[docs]@contextmanager def painter_zoom(painter: QPainter, zoom: float, rect: QRectF) -> QRectF: """Context manager scaling and restoring the painter (rect.x(), rect.y()) is invariant :param painter: Painter, for which the state is preserved :param zoom: Zoom factor :param rect: Rect for setting zoom invariant point (rect.x(), rect.y()) """ with painter_save(painter): painter.translate(rect.x(), rect.y()) painter.scale(zoom, zoom) painter.translate(-rect.x() * zoom, -rect.y() * zoom) yield QRectF(rect.x() * zoom, rect.y() * zoom, rect.width() / zoom, rect.height() / zoom)
[docs]@contextmanager def painter_rotate(painter: QPainter, rect: QRectF, angle: int = 0) -> QRectF: """Context manager rotating and restoring the painter :param painter: Painter, which is rotated :param rect: Rect to be painted in :param angle: Rotataion angle must be in (0, 90, 180, 270) """ supported_angles = 0, 90, 180, 270 angle = int(angle) if angle not in supported_angles: msg = "Rotation angle {} not in {}".format(angle, supported_angles) raise Warning(msg) return center_x, center_y = rect.center().x(), rect.center().y() with painter_save(painter): painter.translate(center_x, center_y) painter.rotate(angle) if angle in (0, 180): painter.translate(-center_x, -center_y) elif angle in (90, 270): painter.translate(-center_y, -center_x) rect = QRectF(rect.y(), rect.x(), rect.height(), rect.width()) yield rect
[docs]class GridCellNavigator: """Find neighbors of a cell""" def __init__(self, grid: QTableView, key: Tuple[int, int, int]): """ :param grid: The main grid widget :param key: Key of cell fow which neighbors are identified """ self.grid = grid self.code_array = grid.model.code_array self.row, self.column, self.table = self.key = key @property def borderwidth_bottom(self) -> float: """Width of bottom border line""" return self.code_array.cell_attributes[self.key].borderwidth_bottom @property def borderwidth_right(self) -> float: """Width of right border line""" return self.code_array.cell_attributes[self.key].borderwidth_right @property def border_color_bottom(self) -> QColor: """Color of bottom border line""" return self.code_array.cell_attributes[self.key].bordercolor_bottom @property def border_color_right(self) -> QColor: """Color of right border line""" return self.code_array.cell_attributes[self.key].bordercolor_right @property def merge_area(self) -> Tuple[int, int, int, int]: """Merge area of the key cell""" return self.code_array.cell_attributes[self.key].merge_area
[docs] def _merging_key(self, key: Tuple[int, int, int]) -> Tuple[int, int, int]: """Merging cell if cell is merged else cell key :param key: Key of cell that is checked for being merged """ merging_key = self.code_array.cell_attributes.get_merging_cell(key) return key if merging_key is None else merging_key
[docs] def above_keys(self) -> List[Tuple[int, int, int]]: """Key list of neighboring cells above the key cell""" merge_area = self.merge_area if merge_area is None: return [self._merging_key((self.row - 1, self.column, self.table))] _, left, _, right = merge_area return [self._merging_key((self.row - 1, col, self.table)) for col in range(left, right + 1)]
[docs] def below_keys(self) -> List[Tuple[int, int, int]]: """Key list of neighboring cells below the key cell""" merge_area = self.merge_area if merge_area is None: return [self._merging_key((self.row + 1, self.column, self.table))] _, left, _, right = merge_area return [self._merging_key((self.row + 1, col, self.table)) for col in range(left, right + 1)]
[docs] def left_keys(self) -> List[Tuple[int, int, int]]: """Key list of neighboring cells left of the key cell""" merge_area = self.merge_area if merge_area is None: return [self._merging_key((self.row, self.column - 1, self.table))] top, _, bottom, _ = merge_area return [self._merging_key((row, self.column - 1, self.table)) for row in range(top, bottom + 1)]
[docs] def right_keys(self) -> List[Tuple[int, int, int]]: """Key list of neighboring cells right of the key cell""" merge_area = self.merge_area if merge_area is None: return [self._merging_key((self.row, self.column + 1, self.table))] top, _, bottom, _ = merge_area return [self._merging_key((row, self.column + 1, self.table)) for row in range(top, bottom + 1)]
[docs] def above_left_key(self) -> Tuple[int, int, int]: """Key of neighboring cell above left of the key cell""" return self._merging_key((self.row - 1, self.column - 1, self.table))
[docs] def above_right_key(self) -> Tuple[int, int, int]: """Key of neighboring cell above right of the key cell""" return self._merging_key((self.row - 1, self.column + 1, self.table))
[docs] def below_left_key(self) -> Tuple[int, int, int]: """Key of neighboring cell below left of the key cell""" return self._merging_key((self.row + 1, self.column - 1, self.table))
[docs] def below_right_key(self) -> Tuple[int, int, int]: """Key of neighboring cell below right of the key cell""" return self._merging_key((self.row + 1, self.column + 1, self.table))
[docs]@dataclass class EdgeBorders: """Holds border data for an edge""" left_width: float right_width: float top_width: float bottom_width: float left_color: QColor right_color: QColor top_color: QColor bottom_color: QColor left_x: float right_x: float top_y: float bottom_y: float @property def widths(self) -> Tuple[float, float, float, float]: """Tuple of border widths in order left, right, top, bottom""" return (self.left_width, self.right_width, self.top_width, self.bottom_width) @property def colors(self) -> Tuple[QColor, QColor, QColor, QColor]: """Tuple of border colors in order left, right, top, bottom""" return (self.left_color, self.right_color, self.top_color, self.bottom_color)
[docs]class CellEdgeRenderer: """Paints cell edges""" def __init__(self, painter: QPainter, center: QPointF, borders: EdgeBorders): """ Borders are provided by EdgeBorders in order: left, right, top, bottom :param painter: Painter with which edge is drawn :param center: Edge center :param borders: Border widths and colors """ self.painter = painter lines = [QLineF(center.x(), center.y(), borders.left_x, center.y()), QLineF(center.x(), center.y(), borders.right_x, center.y()), QLineF(center.x(), center.y(), center.x(), borders.top_y), QLineF(center.x(), center.y(), center.x(), borders.bottom_y)] self.edge_data = list(zip(borders.widths, borders.colors, lines)) self.edge_data.sort(key=lambda edge: (-edge[1].lightnessF(), edge[0]))
[docs] def paint(self): """Paints the edge""" for width, color, line in self.edge_data: self.painter.setPen(QPen(QBrush(color), width, Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin)) self.painter.drawLine(line)
[docs]class QColorCache(dict): """QColor cache that returns default color for None""" def __init__(self, grid, *args, **kwargs): self.grid = grid super().__init__(*args, **kwargs) def __missing__(self, key): if key is None: self[key] = qcolor = self.grid.palette().color(QPalette.Mid) else: self[key] = qcolor = QColor(*key) return qcolor
[docs]class CellRenderer: """Paints cells Cell rendering governs the area of a cell inside its borders. It is done in a vector oriented way. Therefore, the following conventions shall apply: * Cell borders of width 1 shall be painted so that they appear only in the bottom and right edge of the cell. * Cell borders of all widths have the same center line. * Cell borders that are thicker than 1 are painted on all borders. * At the edges, borders are painted in the following order: 1. Thin borders are painted first 2. If border width is equal, lighter colors are painted first """ def __init__(self, grid: QTableView, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex): """ :param grid: The main grid widget :param painter: Painter with which borders are drawn :param option: Style option for rendering :param index: Index of cell to be rendered """ self.grid = grid self.painter = painter self.option = option self.index = index self.cell_attributes = grid.model.code_array.cell_attributes self.key = index.row(), index.column(), self.grid.table self.cell_nav = GridCellNavigator(grid, self.key) self.qcolor_cache = self.grid.qcolor_cache
[docs] def inner_rect(self, rect: QRectF) -> QRectF: """Returns inner rect that is shrunk by border widths For merged cells, minimum top/left border widths are taken into account :param rect: Cell rect to be shrunk """ above_keys = self.cell_nav.above_keys() left_keys = self.cell_nav.left_keys() width_top = min(self.cell_attributes[above_key].borderwidth_bottom for above_key in above_keys) width_left = min(self.cell_attributes[left_key].borderwidth_right for left_key in left_keys) width_bottom = self.cell_nav.borderwidth_bottom width_right = self.cell_nav.borderwidth_right width_top *= self.grid.zoom width_left *= self.grid.zoom width_bottom *= self.grid.zoom width_right *= self.grid.zoom rect_x = rect.x() + width_left / 2 rect_y = rect.y() + width_top / 2 rect_width = rect.width() - width_left / 2 - width_right / 2 rect_height = rect.height() - width_top / 2 - width_bottom / 2 return QRectF(rect_x, rect_y, rect_width, rect_height)
[docs] def paint_content(self, rect: QRectF): """ :param rect: Cell rect of the cell to be painted """ with painter_zoom(self.painter, self.grid.zoom, rect) as zrect: self.grid.delegate.paint_(self.painter, zrect, self.option, self.index)
[docs] def paint_bottom_border(self, rect: QRectF): """Paint bottom border of cell :param rect: Cell rect of the cell to be painted """ if not self.cell_nav.borderwidth_bottom: return line_color = self.qcolor_cache[self.cell_nav.border_color_bottom] line_width = self.cell_nav.borderwidth_bottom * self.grid.zoom self.painter.setPen(QPen(QBrush(line_color), line_width, Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin)) bottom_border_line = QLineF(rect.x(), rect.y() + rect.height(), rect.x() + rect.width(), rect.y() + rect.height()) self.painter.drawLine(bottom_border_line)
[docs] def paint_right_border(self, rect: QRectF): """Paint right border of cell :param rect: Cell rect of the cell to be painted """ if not self.cell_nav.borderwidth_right: return line_color = self.qcolor_cache[self.cell_nav.border_color_right] line_width = self.cell_nav.borderwidth_right * self.grid.zoom self.painter.setPen(QPen(QBrush(line_color), line_width, Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin)) right_border_line = QLineF(rect.x() + rect.width(), rect.y(), rect.x() + rect.width(), rect.y() + rect.height()) self.painter.drawLine(right_border_line)
[docs] def paint_above_borders(self, rect: QRectF): """Paint lower borders of all above cells :param rect: Cell rect of below cell, in which the borders are painted """ for above_key in self.cell_nav.above_keys(): above_cell_nav = GridCellNavigator(self.grid, above_key) merge_area = above_cell_nav.merge_area if merge_area is None: columns = [above_key[1]] else: _, left, _, right = merge_area columns = list(range(left, right + 1)) above_rect_x = self.grid.columnViewportPosition(columns[0]) above_rect_width = sum(self.grid.columnWidth(column) for column in columns) line_color = self.qcolor_cache[above_cell_nav.border_color_bottom] line_width = above_cell_nav.borderwidth_bottom * self.grid.zoom self.painter.setPen(QPen(QBrush(line_color), line_width, Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin)) above_border_line = QLineF(above_rect_x, rect.y(), above_rect_x + above_rect_width, rect.y()) self.painter.drawLine(above_border_line)
[docs] def paint_left_borders(self, rect: QRectF): """Paint right borders of all left cells :param rect: Cell rect of right cell, in which the borders are painted """ for left_key in self.cell_nav.left_keys(): left_cell_nav = GridCellNavigator(self.grid, left_key) merge_area = left_cell_nav.merge_area if merge_area is None: rows = [left_key[0]] else: top, _, bottom, _ = merge_area rows = list(range(top, bottom + 1)) left_rect_y = self.grid.rowViewportPosition(rows[0]) left_rect_height = sum(self.grid.rowHeight(row) for row in rows) line_color = self.qcolor_cache[left_cell_nav.border_color_right] line_width = left_cell_nav.borderwidth_right * self.grid.zoom self.painter.setPen(QPen(QBrush(line_color), line_width, Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin)) above_border_line = QLineF(rect.x(), left_rect_y, rect.x(), left_rect_y + left_rect_height) self.painter.drawLine(above_border_line)
[docs] def paint_top_left_edge(self, rect: QRectF): """Paints top left edge of the cell :param rect: Cell rect of cell, for which the edge is painted top TL | T left ------------ right L | C bottom """ center = QPointF(rect.x(), rect.y()) top_left_key = self.cell_nav.above_left_key() left_key = self.cell_nav.left_keys()[0] top_key = self.cell_nav.above_keys()[0] top_left_cell_nav = GridCellNavigator(self.grid, top_left_key) left_cell_nav = GridCellNavigator(self.grid, left_key) top_cell_nav = GridCellNavigator(self.grid, top_key) left_width = top_left_cell_nav.borderwidth_bottom * self.grid.zoom right_width = top_cell_nav.borderwidth_bottom * self.grid.zoom top_width = top_left_cell_nav.borderwidth_right * self.grid.zoom bottom_width = left_cell_nav.borderwidth_right * self.grid.zoom left_color = self.qcolor_cache[top_left_cell_nav.border_color_bottom] right_color = self.qcolor_cache[top_cell_nav.border_color_bottom] top_color = self.qcolor_cache[top_left_cell_nav.border_color_right] bottom_color = self.qcolor_cache[left_cell_nav.border_color_right] left_x = rect.x() - rect.width() right_x = rect.x() top_y = rect.y() - rect.height() bottom_y = rect.y() borders = EdgeBorders(left_width, right_width, top_width, bottom_width, left_color, right_color, top_color, bottom_color, left_x, right_x, top_y, bottom_y) renderer = CellEdgeRenderer(self.painter, center, borders) renderer.paint()
[docs] def paint_top_right_edge(self, rect: QRectF): """Paints top right edge of the cell :param rect: Cell rect of cell, for which the edge is painted top T | TR left ------------ right C | R bottom """ center = QPointF(rect.x() + rect.width(), rect.y()) top_key = self.cell_nav.above_keys()[-1] top_right_key = self.cell_nav.above_right_key() top_cell_nav = GridCellNavigator(self.grid, top_key) top_right_cell_nav = GridCellNavigator(self.grid, top_right_key) left_width = top_cell_nav.borderwidth_bottom * self.grid.zoom right_width = top_right_cell_nav.borderwidth_bottom * self.grid.zoom top_width = top_cell_nav.borderwidth_right * self.grid.zoom bottom_width = self.cell_nav.borderwidth_right * self.grid.zoom left_color = self.qcolor_cache[top_cell_nav.border_color_bottom] right_color = self.qcolor_cache[top_right_cell_nav.border_color_bottom] top_color = self.qcolor_cache[top_cell_nav.border_color_right] bottom_color = self.qcolor_cache[self.cell_nav.border_color_right] left_x = rect.x() + rect.width() right_x = rect.x() + 2 * rect.width() top_y = rect.y() - rect.height() bottom_y = rect.y() borders = EdgeBorders(left_width, right_width, top_width, bottom_width, left_color, right_color, top_color, bottom_color, left_x, right_x, top_y, bottom_y) renderer = CellEdgeRenderer(self.painter, center, borders) renderer.paint()
[docs] def paint_bottom_left_edge(self, rect: QRectF): """Paints bottom left edge of the cell :param rect: Cell rect of cell, for which the edge is painted top L | C left ------------ right BL | B bottom """ center = QPointF(rect.x(), rect.y() + rect.height()) left_key = self.cell_nav.left_keys()[-1] bottom_left_key = self.cell_nav.below_left_key() left_cell_nav = GridCellNavigator(self.grid, left_key) bottom_left_cell_nav = GridCellNavigator(self.grid, bottom_left_key) left_width = left_cell_nav.borderwidth_bottom * self.grid.zoom right_width = self.cell_nav.borderwidth_bottom * self.grid.zoom top_width = left_cell_nav.borderwidth_right * self.grid.zoom bottom_width = bottom_left_cell_nav.borderwidth_right * self.grid.zoom left_color = self.qcolor_cache[left_cell_nav.border_color_bottom] right_color = self.qcolor_cache[self.cell_nav.border_color_bottom] top_color = self.qcolor_cache[left_cell_nav.border_color_right] bottom_color = \ self.qcolor_cache[bottom_left_cell_nav.border_color_right] left_x = rect.x() - rect.width() right_x = rect.x() top_y = rect.y() + rect.height() bottom_y = rect.y() + 2 * rect.height() borders = EdgeBorders(left_width, right_width, top_width, bottom_width, left_color, right_color, top_color, bottom_color, left_x, right_x, top_y, bottom_y) renderer = CellEdgeRenderer(self.painter, center, borders) renderer.paint()
[docs] def paint_bottom_right_edge(self, rect: QRectF): """Paints bottom right edge of the cell :param rect: Cell rect of cell, for which the edge is painted top C | R left ----------- right B | BR bottom """ center = QPointF(rect.x() + rect.width(), rect.y() + rect.height()) right_key = self.cell_nav.right_keys()[-1] bottom_key = self.cell_nav.below_keys()[-1] right_cell_nav = GridCellNavigator(self.grid, right_key) bottom_cell_nav = GridCellNavigator(self.grid, bottom_key) left_width = self.cell_nav.borderwidth_bottom * self.grid.zoom right_width = right_cell_nav.borderwidth_bottom * self.grid.zoom top_width = self.cell_nav.borderwidth_right * self.grid.zoom bottom_width = bottom_cell_nav.borderwidth_right * self.grid.zoom left_color = self.qcolor_cache[self.cell_nav.border_color_bottom] right_color = self.qcolor_cache[right_cell_nav.border_color_bottom] top_color = self.qcolor_cache[self.cell_nav.border_color_right] bottom_color = self.qcolor_cache[bottom_cell_nav.border_color_right] left_x = rect.x() + rect.width() right_x = rect.x() + 2 * rect.width() top_y = rect.y() + rect.height() bottom_y = rect.y() + 2 * rect.height() borders = EdgeBorders(left_width, right_width, top_width, bottom_width, left_color, right_color, top_color, bottom_color, left_x, right_x, top_y, bottom_y) renderer = CellEdgeRenderer(self.painter, center, borders) renderer.paint()
[docs] def paint_borders(self, rect): """Paint cell borders""" self.paint_bottom_border(rect) self.paint_right_border(rect) self.paint_above_borders(rect) self.paint_left_borders(rect) self.paint_top_left_edge(rect) self.paint_top_right_edge(rect) self.paint_bottom_left_edge(rect) self.paint_bottom_right_edge(rect)
[docs] def paint(self): """Paints the cell""" rect = QRectF(self.option.rect) with painter_save(self.painter): self.painter.setClipRect(self.option.rect) angle = self.cell_attributes[self.key].angle inner_rect = self.inner_rect(rect) with painter_rotate(self.painter, inner_rect, angle) as rrect: self.paint_content(rrect) self.paint_borders(rect)