Source code for PDFReport.tableframe

from typing import List

from .enums import RowType
from .border import Border
from .pen import Pen
from .rect import Rect
from .reportframe import get_rect_with_size_and_align
from .renderer import Renderer
from .simpleframe import SimpleFrame
from .containerframe import ContainerFrame
from .size import Size
from .sizestate import SizeState
from .tablecolumn import TableColumn
from .tablerow import TableRow
from .textstyle import TextStyle
from .textstyles import TextStyles


[docs] class TableFrame(SimpleFrame): """ Class for a table in a report. It is a simple frame with no sub-frames in it. The table has a list of columns and data rows. """ HEADER_ROW_INDEX = -1
[docs] def __init__(self, parent: ContainerFrame, frame_id: str = ""): """ Creates a new TableFrame :param parent: Parent frame to which this frame will be added :param frame_id: frame id (optional) """ super().__init__(parent, frame_id) self._column_lines = False self._min_header_row_height = 0.0 self._min_detail_row_height = 0.0 self._max_header_row_height = 100.0 self._max_detail_row_height = 100.0 self._margin_bottom_subtotal = 1.0 self._inter_row_space = 0.0 self._header_text_style = TextStyle("", base_style=TextStyle.TABLE_HEADER) self._detail_row_text_style = TextStyle("", base_style=TextStyle.TABLE_ROW) self._alternating_row_text_style = None self._sub_total_row_text_style = TextStyle("", base_style=TextStyle.TABLE_SUBTOTAL) self._total_row_text_style = TextStyle("", base_style=TextStyle.TABLE_TOTAL) self._border = Border() self._repeat_header_row = True self._suppress_header_row = False self._inner_pen_header_bottom = Pen(0.2) self._inner_pen_total_top = Pen(0.2) self._inner_pen_row = Pen() self._header_row_height = 0.0 self._table_height_for_page = 0.0 self._header_size_init = False self._data_rows_fit = 0 self._min_data_rows_fit = 1 self._row_heights = [] self._table_data: List[TableRow] = [] self._width = 0.0 self._num_sub_rows = 1 self._sub_row_height_list = {} self._columns: List[TableColumn] = [] self._row_index = 0 self.__save_y = -1
@property def margin_bottom_subtotal(self) -> float: """ Margin below a subtotal row :getter: Returns the margin :setter: Sets the margin """ return self._margin_bottom_subtotal @margin_bottom_subtotal.setter def margin_bottom_subtotal(self, margin_bottom_subtotal: float): self._margin_bottom_subtotal = margin_bottom_subtotal @property def repeat_header_row(self) -> bool: """ Flag if the header row will be repeated after a page break :getter: Returns the flag :setter: Sets the flag """ return self._repeat_header_row @repeat_header_row.setter def repeat_header_row(self, repeat_header_row: bool): self._repeat_header_row = repeat_header_row @property def suppress_header_row(self) -> bool: """ Flag if the header row will not be repeated at all :getter: Returns the flag :setter: Sets the flag """ return self._suppress_header_row @suppress_header_row.setter def suppress_header_row(self, suppress_header_row: bool): self._suppress_header_row = suppress_header_row @property def min_header_row_height(self) -> float: """ Minimal height of the header row :getter: Returns the min header height :setter: Sets the min header height """ return self._min_header_row_height @min_header_row_height.setter def min_header_row_height(self, min_header_row_height: float): self._min_header_row_height = abs(min_header_row_height) @property def min_detail_row_height(self) -> float: """ Minimal height of a row :getter: Returns the min height :setter: Sets the min height """ return self._min_detail_row_height @min_detail_row_height.setter def min_detail_row_height(self, min_detail_row_height: float): self._min_detail_row_height = abs(min_detail_row_height) @property def max_header_row_height(self) -> float: """ Maximal height of the header row :getter: Returns the max header height :setter: Sets the max header height """ return self._max_header_row_height @max_header_row_height.setter def max_header_row_height(self, max_header_row_height: float): self._max_header_row_height = abs(max_header_row_height) @property def max_detail_row_height(self) -> float: """ Maximal height of a row :getter: Returns the max height :setter: Sets the max height """ return self._max_detail_row_height @max_detail_row_height.setter def max_detail_row_height(self, max_detail_row_height: float): self._max_detail_row_height = abs(max_detail_row_height) @property def header_text_style(self) -> TextStyle: """ Text style for the header row :getter: Returns the text style :setter: Sets the text style """ return self._header_text_style @header_text_style.setter def header_text_style(self, header_text_style: TextStyle or str): if isinstance(header_text_style, TextStyle): self._header_text_style = TextStyle("", base_style=header_text_style.name) elif header_text_style != "": self._header_text_style = TextStyle("", base_style=header_text_style) else: self._header_text_style = TextStyle("", base_style=TextStyle.TABLE_HEADER) @property def detail_row_text_style(self) -> TextStyle: """ Text style for a row :getter: Returns the text style :setter: Sets the text style """ return self._detail_row_text_style @detail_row_text_style.setter def detail_row_text_style(self, detail_row_text_style: TextStyle or str): if isinstance(detail_row_text_style, TextStyle): self._detail_row_text_style = TextStyle("", base_style=detail_row_text_style.name) elif detail_row_text_style != "": self._detail_row_text_style = TextStyle("", base_style=detail_row_text_style) else: self._detail_row_text_style = TextStyle("", base_style=TextStyle.TABLE_ROW) @property def sub_total_row_text_style(self) -> TextStyle: """ Text style for a sub-total row :getter: Returns the text style :setter: Sets the text style """ return self._sub_total_row_text_style @sub_total_row_text_style.setter def sub_total_row_text_style(self, sub_total_row_text_style: TextStyle or str): if isinstance(sub_total_row_text_style, TextStyle): self._sub_total_row_text_style = TextStyle("", base_style=sub_total_row_text_style.name) elif sub_total_row_text_style != "": self._sub_total_row_text_style = TextStyle("", base_style=sub_total_row_text_style) else: self._sub_total_row_text_style = TextStyle("", base_style=TextStyle.TABLE_SUBTOTAL) @property def total_row_text_style(self) -> TextStyle: """ Text style for a total row :getter: Returns the text style :setter: Sets the text style """ return self._total_row_text_style @total_row_text_style.setter def total_row_text_style(self, total_row_text_style: TextStyle or str): if isinstance(total_row_text_style, TextStyle): self._total_row_text_style = TextStyle("", base_style=total_row_text_style.name) elif total_row_text_style != "": self._total_row_text_style = TextStyle("", base_style=total_row_text_style) else: self._total_row_text_style = TextStyle("", base_style=TextStyle.TABLE_TOTAL) @property def alternating_row_text_style(self) -> TextStyle: """ Text style for the alternating rows :getter: Returns the text style :setter: Sets the text style """ return self._alternating_row_text_style @alternating_row_text_style.setter def alternating_row_text_style(self, alternating_row_text_style: TextStyle or str): if isinstance(alternating_row_text_style, TextStyle): self._alternating_row_text_style = TextStyle("", base_style=alternating_row_text_style.name) elif alternating_row_text_style != "": self._alternating_row_text_style = TextStyle("", base_style=alternating_row_text_style) else: self._alternating_row_text_style = None @property def border(self) -> Border: """ Border of the table :getter: Returns the border :setter: Sets the border """ return self._border @border.setter def border(self, border: Border): self._border = border @property def inner_pen_header_bottom(self) -> Pen: """ Pen for a line below the header row :getter: Returns the pen :setter: Sets the pen """ return self._inner_pen_header_bottom @inner_pen_header_bottom.setter def inner_pen_header_bottom(self, inner_pen_header_bottom: Pen): self._inner_pen_header_bottom = inner_pen_header_bottom @property def inner_pen_total_top(self) -> Pen: """ Pen for a line above the total row :getter: Returns the pen :setter: Sets the pen """ return self._inner_pen_total_top @inner_pen_total_top.setter def inner_pen_total_top(self, inner_pen_total_top: Pen): self._inner_pen_total_top = inner_pen_total_top @property def inner_pen_row(self) -> Pen: """ Pen for a line between rows :getter: Returns the pen :setter: Sets the pen """ return self._inner_pen_row @inner_pen_row.setter def inner_pen_row(self, inner_pen_row: Pen): self._inner_pen_row = inner_pen_row @property def inter_row_space(self) -> float: """ Space between rows in millimeters :getter: Returns the space :setter: Sets the space """ return self._inter_row_space @inter_row_space.setter def inter_row_space(self, inter_row_space: float): self._inter_row_space = abs(inter_row_space) @property def column_lines(self) -> bool: """ Flag if there are vertical lines between the cells :getter: Returns the flag :setter: Sets the flag """ return self._column_lines @column_lines.setter def column_lines(self, column_lines: bool): self._column_lines = column_lines def reset(self): super().reset() self._header_size_init = False def add_row(self, row: TableRow) -> int: self._table_data.append(row) return len(self._table_data) - 1 def add_column(self, tc: TableColumn) -> int: tc.header_text_style = self._header_text_style tc.detail_row_text_style = self._detail_row_text_style tc.sub_total_row_text_style = self._sub_total_row_text_style tc.total_row_text_style = self._total_row_text_style if self._alternating_row_text_style is not None: tc.alternating_row_text_style = self._alternating_row_text_style self._columns.append(tc) return len(self._columns) - 1 def _get_text(self, header_row: bool, row: TableRow or None, col_number: int) -> str: if header_row: return self._columns[col_number].title if row is not None: return row.get_text(col_number) return "" def _get_text_style(self, row: TableRow or None, col_number: int, header_row: bool, alternating_row: bool) -> TextStyle: if row is not None: ts = row.get_text_style(col_number) if ts is not None: return ts if row is not None and row.row_type != RowType.DETAIL: if row.row_type == RowType.HEADER: style = self._header_text_style elif row.row_type == RowType.SUBTOTAL: style = self._sub_total_row_text_style elif row.row_type == RowType.TOTAL: style = self._total_row_text_style else: style = self._detail_row_text_style else: if header_row: style = self._header_text_style else: if alternating_row: if self._alternating_row_text_style is not None: style = self._alternating_row_text_style else: style = self._detail_row_text_style else: style = self._detail_row_text_style return style def _calc_header_size(self, r: Renderer, rect: Rect) -> Size: if not self._header_size_init: width = rect.get_width() self._resize_columns(width) self._header_row_height = self._size_print_row(r, TableFrame.HEADER_ROW_INDEX, rect.left, rect.top, self._max_detail_row_height, True, True) self._header_size_init = True return Size(self._width, self._header_row_height) def _get_table_bounds(self, for_rect: Rect, size: Size = None) -> Rect: if size is None: size = self._border.add_border_size(self._get_header_size()) rect = get_rect_with_size_and_align(for_rect, size, self._h_align, self._v_align) return Rect(rect.left, for_rect.top, rect.right, for_rect.bottom) rect = get_rect_with_size_and_align(for_rect, size, self._h_align, self._v_align) return Rect(rect.left, rect.top, rect.right, rect.bottom) def _get_header_size(self) -> Size: return Size(self._width, self._header_row_height) def _size_print_header(self, r: Renderer, in_rect: Rect, size_only: bool) -> bool: header_fits = True if not self._suppress_header_row and self._repeat_header_row: if in_rect.size_fits(self._get_header_size()): if not size_only: self._size_print_row(r, TableFrame.HEADER_ROW_INDEX, in_rect.left, in_rect.top, self._header_row_height, False, False) in_rect.top += self._header_row_height else: header_fits = False return header_fits def _find_data_rows_fit(self, r: Renderer, in_rect: Rect) -> int: rows_that_fit = 0 index = self._row_index self._row_heights = [] while index < self._get_total_rows(): include_row_line = index < self._get_total_rows() - 1 row_height = self._size_print_row(r, index, in_rect.left, in_rect.top, self._max_detail_row_height, True, include_row_line) if in_rect.size_fits(Size(self._width, row_height)): self._row_heights.append(row_height) in_rect.top += row_height index += 1 rows_that_fit += 1 else: if len(self._row_heights) > 0: row_height = self._size_print_row(r, index - 1, in_rect.left, in_rect.top, self._max_detail_row_height, True, False) in_rect.top -= (self._row_heights[len(self._row_heights) - 1]) in_rect.top += row_height self._row_heights[len(self._row_heights) - 1] = row_height else: if self.__save_y < 40 and self.__save_y == in_rect.top: self._row_heights.append(in_rect.get_height()) in_rect.top += in_rect.get_height() index += 1 rows_that_fit += 1 else: self.__save_y = in_rect.top break if self._min_data_rows_fit != 0 and index < self._get_total_rows(): if rows_that_fit < self._min_data_rows_fit: rows_that_fit = 0 else: rows_left = self._get_total_rows() - index if rows_left + rows_that_fit < (2 * self._min_data_rows_fit): rows_that_fit = 0 elif self._min_data_rows_fit > rows_left: rows_that_fit -= (self._min_data_rows_fit - rows_left) return rows_that_fit def _get_total_rows(self) -> int: return len(self._table_data) def _print_rows(self, r: Renderer, in_rect: Rect): for rowCount in range(0, self._data_rows_fit): height = self._row_heights[rowCount] self._size_print_row(r, self._row_index, in_rect.left, in_rect.top, height, False, False) in_rect.top += height self._row_index += 1 def _check_for_sub_rows(self, width: float): cols_width = 0.0 for col_number, column in enumerate(self._columns): column.calc_width(width) cols_width += column.width_to_use next_col_number = col_number + 1 if next_col_number < len(self._columns): next_column = self._columns[next_col_number] next_column.calc_width(width) if cols_width + next_column.width_to_use > width: column.line_break = True self._num_sub_rows += 1 if cols_width > self._width: self._width = cols_width cols_width = 0.0 if cols_width > self._width: self._width = cols_width def _resize_columns(self, width: float): self._check_for_sub_rows(width) if self.use_full_width: self._adjust_columns_to_width(width) def _adjust_columns_to_width(self, max_width: float): if len(self._columns) == 0 or max_width <= 0: return max_width -= self._margin_left max_width -= self._margin_right curr_width = 0.0 first_col = 0 for col_number, column in enumerate(self._columns): curr_width += column.width_to_use if column.line_break and curr_width > 0.0: d_delta = max_width / curr_width * 10000 delta = int(d_delta) d_delta = delta / 10000.0 for col_nr in range(first_col, col_number + 1): column = self._columns[col_nr] column.width_to_use = column.width_to_use * d_delta first_col = col_number + 1 curr_width = 0.0 if curr_width > 0.0: d_delta = max_width / curr_width * 10000 delta = int(d_delta) d_delta = delta / 10000.0 for col_nr in range(first_col, len(self._columns)): column = self._columns[col_nr] column.width_to_use = column.width_to_use * d_delta self._width = max_width def _get_valid_height(self, height: float, is_header: bool) -> float: if is_header: min_h = self._min_header_row_height max_h = self._max_header_row_height else: min_h = self._min_detail_row_height max_h = self._max_detail_row_height if height < min_h: return min_h elif height > max_h: return max_h else: return height def _size_print_row(self, r: Renderer, row_index: int, x: float, y: float, max_height: float, size_only: bool, show_line: bool) -> float: is_header = (row_index == TableFrame.HEADER_ROW_INDEX) alt_row = ((row_index % 2) != 0) row_height = 0.0 curr_row_height = 0.0 x_pos = x y_pos = y row = None if not is_header: row = self._table_data[row_index] cur_sub_row = 0 if not size_only and self._num_sub_rows > 1: max_height = self._get_valid_height(self._get_sub_row_height(row_index + 1, cur_sub_row), is_header) for col_number, column in enumerate(self._columns): col_w = column.width_to_use if row is not None: if row.join_start >= 0 and row.join_end >= 0: if col_number == row.join_start: col_to_start = self._columns[col_number] col_w = col_to_start.width # Add additional column width get the total width of the joined column for join_col in range(row.join_start + 1, row.join_end + 1): if join_col >= len(self._columns): continue col_in_join = self._columns[join_col] col_w += col_in_join.width # Remove text from columns in the join row.set_text(join_col, "") text = self._get_text(is_header, row, col_number) text_style = self._get_text_style(row, col_number, is_header, alt_row) size = column.size_paint_cell(r, text, text_style, x_pos, y_pos, col_w, max_height, size_only) curr_row_height = max(curr_row_height, self._get_valid_height(size.height, is_header)) x_pos += column.width_to_use if column.line_break: if size_only: self._set_sub_row_height(row_index + 1, cur_sub_row, curr_row_height) cur_sub_row += 1 else: cur_sub_row += 1 max_height = self._get_valid_height(self._get_sub_row_height(row_index + 1, cur_sub_row), is_header) row_height += curr_row_height y_pos += curr_row_height x_pos = x curr_row_height = 0.0 row_height += curr_row_height if size_only and self._num_sub_rows > 1: self._set_sub_row_height(row_index + 1, cur_sub_row, curr_row_height) else: row_height = curr_row_height if show_line: row_height += self._row_line(r, x, y_pos + row_height, self._width, is_header, False, size_only) if not is_header: row_height += self._inter_row_space if row is not None and row.row_type == RowType.SUBTOTAL: row_height += self._row_line(r, x, y, self._width, False, True, size_only) row_height += self._margin_bottom_subtotal if row is not None and row.row_type == RowType.TOTAL: row_height += self._row_line(r, x, y, self._width, False, True, size_only) return row_height def _row_line(self, r: Renderer, x: float, y: float, length: float, is_header: bool, is_total: bool, size_only: bool) -> float: height = 0 if is_header: pen = self._inner_pen_header_bottom elif is_total: pen = self._inner_pen_total_top else: pen = self._inner_pen_row if pen.extent != 0.0: if not size_only: y -= pen.extent / 2.0 r.add_line(x, y, x + length, y, pen.extent, pen.line_style, pen.color) height = pen.extent return height def _print_all_row_lines(self, r: Renderer, rect: Rect, include_header: bool): x = rect.left y = rect.top row_width = rect.get_width() if include_header: self._row_line(r, x, y + self._header_row_height, row_width, True, False, False) y += self._header_row_height for rowCount in range(0, self._data_rows_fit - 1): height = self._row_heights[rowCount] self._row_line(r, x, y + height, row_width, False, False, False) y += height def _print_all_column_lines(self, r: Renderer, rect: Rect): if self._num_sub_rows > 1 or not self._column_lines: return x = rect.left y = rect.top for colNumber in range(0, len(self._columns)): # @var TableColumn column */ column = self._columns[colNumber] x += column.width_to_use column.draw_right_line(r, x, y, rect.get_height()) def _get_sub_row_height(self, row: int, sub_row: int) -> float: height = 0.0 if row in self._sub_row_height_list: if sub_row in self._sub_row_height_list[row]: height = self._sub_row_height_list[row][sub_row] return height def _set_sub_row_height(self, row: int, sub_row: int, height: float): if row in self._sub_row_height_list: self._sub_row_height_list[row][sub_row] = height else: self._sub_row_height_list[row] = {} self._sub_row_height_list[row][sub_row] = height def _do_calc_size(self, r: Renderer, for_rect: Rect) -> SizeState: size_states = SizeState() inside_border = self._border.get_inner_rect(for_rect) self._calc_header_size(r, inside_border) table_bounds = self._get_table_bounds(inside_border) original_position_y = table_bounds.top if self._size_print_header(r, table_bounds, True): self._data_rows_fit = self._find_data_rows_fit(r, table_bounds) self._table_height_for_page = table_bounds.top - original_position_y if self._get_total_rows() == 0: size_states.fits = True elif self._data_rows_fit > 0: size_states.fits = True if self._row_index + self._data_rows_fit < self._get_total_rows(): size_states.continued = True else: size_states.continued = True if self._data_rows_fit < self._min_data_rows_fit: size_states.fits = False else: size_states.fits = False size_states.continued = True size_states.required_size = self._border.add_border_size(Size(self._width, self._table_height_for_page)) return size_states def _do_print(self, r: Renderer, in_rect: Rect): table_bounds = self._get_table_bounds(in_rect, self._required_size) inside_borders = self._border.get_inner_rect(table_bounds) printing_bounds = Rect(other=inside_borders) self._size_print_header(r, printing_bounds, False) self._print_rows(r, printing_bounds) self._print_all_row_lines(r, inside_borders, (not self._suppress_header_row and self._repeat_header_row)) self._print_all_column_lines(r, inside_borders) self._border.draw_border(r, get_rect_with_size_and_align(table_bounds)) def _do_begin_print(self, r: Renderer): # If the header should not be printed, start with the first row if self._suppress_header_row or self._repeat_header_row: self._row_index = 0 else: self._row_index = TableFrame.HEADER_ROW_INDEX self._data_rows_fit = 0 if r.data is not None: r.data.on_table_data(self) def to_dict(self, data: dict, frame: dict): frame["class"] = "TableFrame" if self.column_lines: frame["column_lines"] = self.column_lines if self.min_header_row_height > 0.0: frame["min_header_row_height"] = self.min_header_row_height if self.min_detail_row_height > 0.0: frame["min_detail_row_height"] = self.min_detail_row_height if self.max_header_row_height != 100.0: frame["max_header_row_height"] = self.max_header_row_height if self.max_detail_row_height != 100.0: frame["max_detail_row_height"] = self.max_detail_row_height if self.margin_bottom_subtotal != 1.0: frame["margin_bottom_subtotal"] = self.margin_bottom_subtotal if self.inter_row_space > 0.0: frame["inter_row_space"] = self.inter_row_space ts = self.header_text_style.to_dict() if len(ts) > 0: frame["header_text_style"] = ts ts = self.detail_row_text_style.to_dict() if len(ts) > 0: frame["detail_row_text_style"] = ts if self.alternating_row_text_style is not None: ts = self.alternating_row_text_style.to_dict() if len(ts) > 0: frame["alternating_row_text_style"] = ts ts = self.sub_total_row_text_style.to_dict() if len(ts) > 0: frame["sub_total_row_text_style"] = ts ts = self.total_row_text_style.to_dict() if len(ts) > 0: frame["total_row_text_style"] = ts b = self.border.to_dict() if len(b) > 0: frame["border"] = b if not self.repeat_header_row: frame["repeat_header_row"] = self.repeat_header_row if self.suppress_header_row: frame["suppress_header_row"] = self.suppress_header_row p = self.inner_pen_row.to_dict() if len(p) > 0: frame["inner_pen_row"] = p if self._inner_pen_header_bottom != Pen(0.2): frame["inner_pen_header_bottom"] = self.inner_pen_header_bottom.to_dict() if self._inner_pen_total_top != Pen(0.2): frame["inner_pen_total_top"] = self.inner_pen_total_top.to_dict() data[self.frame_id] = frame super().to_dict(data, frame) for column in self._columns: f = {} column.to_dict(data, f) for row in self._table_data: f = {} row.to_dict(data, f) super().to_dict(data, frame) def from_dict(self, frame: dict): super().from_dict(frame) if "column_lines" in frame: self.column_lines = frame["column_lines"] if "min_header_row_height" in frame: self.min_header_row_height = frame["min_header_row_height"] if "min_detail_row_height" in frame: self.min_detail_row_height = frame["min_detail_row_height"] if "max_header_row_height" in frame: self.max_header_row_height = frame["max_header_row_height"] if "max_detail_row_height" in frame: self.max_detail_row_height = frame["max_detail_row_height"] if "margin_bottom_subtotal" in frame: self.margin_bottom_subtotal = frame["margin_bottom_subtotal"] if "inter_row_space" in frame: self.inter_row_space = frame["inter_row_space"] if "header_text_style" in frame: ts = TextStyle("") ts.from_dict(frame["header_text_style"]) self.header_text_style = ts if "detail_row_text_style" in frame: ts = TextStyle("") ts.from_dict(frame["detail_row_text_style"]) self.detail_row_text_style = ts if "alternating_row_text_style" in frame: ts = TextStyle("") ts.from_dict(frame["alternating_row_text_style"]) self.alternating_row_text_style = ts if "sub_total_row_text_style" in frame: ts = TextStyle("") ts.from_dict(frame["sub_total_row_text_style"]) self.sub_total_row_text_style = ts if "total_row_text_style" in frame: ts = TextStyle("") ts.from_dict(frame["total_row_text_style"]) self.total_row_text_style = ts if "border" in frame: border = Border() border.from_dict(frame["border"]) self.border = border if "repeat_header_row" in frame: self.repeat_header_row = frame["repeat_header_row"] if "suppress_header_row" in frame: self.suppress_header_row = frame["suppress_header_row"] if "inner_pen_header_bottom" in frame: pen = Pen() pen.from_dict(frame["inner_pen_header_bottom"]) self.inner_pen_header_bottom = pen if "inner_pen_total_top" in frame: pen = Pen() pen.from_dict(frame["inner_pen_total_top"]) self.inner_pen_total_top = pen if "inner_pen_row" in frame: pen = Pen() pen.from_dict(frame["inner_pen_row"]) self.inner_pen_row = pen