Source code for lib.selection

# -*- 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/>.
# --------------------------------------------------------------------

"""

Grid selection representation


**Provides**

* :class:`Selection`: Represents grid selection independently from PyQt

"""

from builtins import zip, range, object
from typing import Generator, List, Tuple


[docs] class Selection: """Represents grid selection""" def __init__(self, block_top_left: List[Tuple[int, int]], block_bottom_right: List[Tuple[int, int]], rows: List[int], columns: List[int], cells: List[Tuple[int, int]]): """ :param block_top_left: Top left edges of all selection rectangles :param block_bottom_right: Top left edges of all selection rectangles :param rows: Selected rows :param columns: Selected columns :param cells: Individually selected cells as list of (row, column) """ self.block_tl = block_top_left self.block_br = block_bottom_right self.rows = rows self.columns = columns self.cells = cells def __bool__(self) -> bool: """ :return: True iif any attribute is non-empty """ return any(self.parameters) def __repr__(self) -> str: """ :return: String output for printing selection """ return f"Selection{self.parameters}" def __eq__(self, other): """Eqality check Selections are equal iif the order of each attribute is equal because order precedence may change the selection outcome in the grid. :param other: Other selection for equality comparison :type other: Selection :return: True if self and other selection are equal """ attrs = "block_tl", "block_br", "rows", "columns", "cells" return all(getattr(self, at) == getattr(other, at) for at in attrs) def __contains__(self, cell: Tuple[int, int]): """Check if cell is included in self :param cell: Index of cell to be checked :return: True iif cell is in selection """ if len(cell) != 2: raise Warning("Key length is not 2. Returning None.") return cell_row, cell_col = cell # Block selections for top_left, bottom_right in zip(self.block_tl, self.block_br): top, left = top_left bottom, right = bottom_right if top is None: top = 0 if left is None: left = 0 if bottom is None: bottom = cell_row if right is None: right = cell_col if top <= cell_row <= bottom and left <= cell_col <= right: return True # Row and column selections if cell_row in self.rows or cell_col in self.columns: return True # Cell selections if cell in self.cells: return True return False def __add__(self, value: Tuple[int, int]): """Shifts selection down and / or right :param value: Number of rows / columns to be shifted down / right :return: Shifted selection :rtype: Selection """ def shifted_block(block0: int, block1: int, delta_row: int, delta_col: int) -> Tuple[int, int]: """Returns shifted block""" try: row = block0 + delta_row except TypeError: row = block0 try: col = block1 + delta_col except TypeError: col = block1 return row, col delta_row, delta_col = value block_tl = [shifted_block(top, left, delta_row, delta_col) for top, left in self.block_tl] block_br = [shifted_block(bottom, right, delta_row, delta_col) for bottom, right in self.block_br] rows = [row + delta_row for row in self.rows] columns = [col + delta_col for col in self.columns] cells = [(r + delta_row, c + delta_col) for r, c in self.cells] return Selection(block_tl, block_br, rows, columns, cells) def __and__(self, other): """Returns intersection selection of self and other :param other: Other selection for intersecting :type other: Selection :return: Intersection selection :rtype: Selection """ block_tl = [] block_br = [] rows = [] columns = [] cells = [] # Blocks # Check cells in block: If all are in other, add block else add cells for block in zip(self.block_tl, self.block_br): if block[0] in other.block_tl and block[1] in other.block_br: block_tl.append(block[0]) block_br.append(block[1]) else: block_cells = [] for row in range(block[0][0], block[1][0] + 1): for col in range(block[0][1], block[1][1] + 1): cell = row, col if cell in other: block_cells.append(cell) if len(block_cells) == (block[1][0] + 1 - block[0][0]) * \ (block[1][1] + 1 - block[0][1]): block_tl.append(block[0]) block_br.append(block[1]) else: cells.extend(block_cells) # Rows # If a row/col is selected in self and other then add it. # Otherwise, add all cells in the respective row/col that are in other. for row in self.rows: if row in other.rows: rows.append(row) else: for block in zip(other.block_tl, other.block_br): if block[0][0] <= row <= block[1][0]: block_tl.append((row, block[0][1])) block_br.append((row, block[1][1])) for cell in other.cells: if cell[0] == row and cell not in cells: cells.append(cell) # Columns for col in self.columns: if col in other.columns: columns.append(col) else: for block in zip(other.block_tl, other.block_br): if block[0][1] <= col <= block[1][1]: block_tl.append((block[0][0], col)) block_br.append((block[1][0], col)) for cell in other.cells: if cell[1] == col and cell not in cells: cells.append(cell) # Cells for cell in self.cells: if cell in other and cell not in cells: cells.append(cell) cells = list(set(cells)) return Selection(block_tl, block_br, rows, columns, cells) # Parameter access @property def parameters(self) -> Tuple[List[Tuple[int, int]], List[Tuple[int, int]], List[int], List[int], List[Tuple[int, int]]]: """ :return: Tuple of selection parameters of self (self.block_tl, self.block_br, self.rows, self.columns, self.cells) """ return (self.block_tl, self.block_br, self.rows, self.columns, self.cells)
[docs] def insert(self, point: int, number: int, axis: int): """Inserts number of rows/columns/tables into selection Insertion takes place at point on axis. :param point: At this point the rows/columns are inserted or deleted :param number: Number of rows/columns to be inserted or deleted if negative :param axis: If 0, rows are affected, if 1, columns, axis in 0, 1 """ def build_tuple_list(source_list, point, number, axis): """Returns adjusted tuple list for single cells""" target_list = [] for tl in source_list: tl_list = list(tl) if tl[axis] >= point: tl_list[axis] += number target_list.append(tuple(tl_list)) return target_list if number == 0: return self.block_tl = build_tuple_list(self.block_tl, point, number, axis) self.block_br = build_tuple_list(self.block_br, point, number, axis) if axis == 0: self.rows = [row + number if row >= point else row for row in self.rows] elif axis == 1: self.columns = [column + number if column >= point else column for column in self.columns] else: raise ValueError("Axis not in [0, 1]") self.cells = build_tuple_list(self.cells, point, number, axis)
[docs] def get_bbox(self) -> Tuple[Tuple[int, int], Tuple[int, int]]: """Returns bounding box A bounding box is the smallest rectangle that contains all selections. Non-specified boundaries are None. :return: ((top, left), (bottom, right)) of bounding box """ bb_top, bb_left, bb_bottom, bb_right = [None] * 4 # Block selections for top_left, bottom_right in zip(self.block_tl, self.block_br): top, left = top_left bottom, right = bottom_right if bb_top is None or bb_top > top: bb_top = top if bb_left is None or bb_left > left: bb_left = left if bb_bottom is None or bb_bottom < bottom: bb_bottom = bottom if bb_right is None or bb_right < right: bb_right = right # Row and column selections for row in self.rows: if bb_top is None or bb_top > row: bb_top = row if bb_bottom is None or bb_bottom < row: bb_bottom = row for col in self.columns: if bb_left is None or bb_left > col: bb_left = col if bb_right is None or bb_right < col: bb_right = col # Cell selections for cell in self.cells: cell_row, cell_col = cell if bb_top is None or bb_top > cell_row: bb_top = cell_row if bb_left is None or bb_left > cell_col: bb_left = cell_col if bb_bottom is None or bb_bottom < cell_row: bb_bottom = cell_row if bb_right is None or bb_right < cell_col: bb_right = cell_col if self.rows: bb_left = bb_right = None if self.columns: bb_top = bb_bottom = None return ((bb_top, bb_left), (bb_bottom, bb_right))
[docs] def get_grid_bbox(self, shape: Tuple[int, int, int] ) -> Tuple[Tuple[int, int], Tuple[int, int]]: """Returns bounding box within grid shape limits A bounding box is the smallest rectangle that contains all selections. Non-specified boundaries are filled i from size. :param shape: Grid shape :return: ((top, left), (bottom, right)) of bounding box """ (bb_top, bb_left), (bb_bottom, bb_right) = self.get_bbox() if bb_top is None: bb_top = 0 if bb_left is None: bb_left = 0 if bb_bottom is None: bb_bottom = shape[0] if bb_right is None: bb_right = shape[1] return ((bb_top, bb_left), (bb_bottom, bb_right))
[docs] def get_absolute_access_string(self, shape: Tuple[int, int, int], table: int) -> str: """Get access string for absolute addressing :param shape: Grid shape, for which the generated keys are valid :param table: Table for all returned keys. Must be valid table in shape :return: String, with which the selection can be accessed """ rows, columns, _ = shape strings = [] # Block selections for (top, left), (bottom, right) in zip(self.block_tl, self.block_br): strings += [f"[(r, c, {table})" f" for r in range({top}, {bottom + 1})" f" for c in range({left}, {right + 1})]"] # Fully selected rows for row in self.rows: strings += [f"[({row}, c, {table}) for c in range({columns})]"] # Fully selected columns for column in self.columns: strings += [f"[(r, {column}, {table}) for r in range({rows})]"] # Single cells for row, column in self.cells: strings += [f"[({row}, {column}, {table})]"] if not strings: return "" if len(self.cells) == 1 and len(strings) == 1: return f"S[{strings[0][2:-2]}]" key_string = " + ".join(strings) return f"[S[key] for key in {key_string} if S[key] is not None]"
[docs] def get_relative_access_string(self, shape: Tuple[int, int, int], current: Tuple[int, int, int]) -> str: """Get access string relative to current cell It is assumed that the selected cells are in the same table as the current cell. :param shape: Grid shape, for which the generated keys are valid :param current: Current cell for relative addressing :return: String, with which the selection can be accessed """ rows, columns, tables = shape crow, ccolumn, ctable = current strings = [] # Block selections for (top, left), (bottom, right) in zip(self.block_tl, self.block_br): strings += [ "[(X + dr, Y + dc, Z)" + f" for dr in range({top - crow}, {bottom - crow + 1})" f" for dc in range({left - ccolumn}, {right - ccolumn + 1})]"] # Fully selected rows for row in self.rows: strings += [ f"[(X + {row - crow}, c, Z) for c in range({columns})]"] # Fully selected columns for column in self.columns: strings += [f"[(r, {column - ccolumn}, Z) for r in range({rows})]"] # Single cells for row, column in self.cells: strings += [f"[(X + {row-crow}, Y + {column-ccolumn}, Z)]"] key_string = " + ".join(strings) if not strings: return "" if len(self.cells) == 1 and len(strings) == 1: return f"S[{strings[0][2:-2]}]" return f"[S[key] for key in {key_string} if S[key] is not None]"
[docs] def shifted(self, rows: int, columns: int): """Get a shifted selection Negative values for rows and columns may result in a selection that addresses negative cells. :param rows: Number of rows that the selection is shifted down :param columns: Number of columns that the selection is shifted right :return: New selection that is shifted by rows and columns :rtype: Selection """ shifted_block_tl = [(row + rows, col + columns) for row, col in self.block_tl] shifted_block_br = [(row + rows, col + columns) for row, col in self.block_br] shifted_rows = [row + rows for row in self.rows] shifted_columns = [col + columns for col in self.columns] shifted_cells = [(row + rows, col + columns) for row, col in self.cells] return Selection(shifted_block_tl, shifted_block_br, shifted_rows, shifted_columns, shifted_cells)
[docs] def get_right_borders_selection(self, border_choice: str, shape: Tuple[int, int, int]): """Get selection of cells, for which the right border attributes need to be adjusted on border line and border color changes. border_choice names are: * "All borders" * "Top border" * "Bottom border" * "Left border" * "Right border" * "Outer borders" * "Inner borders" * "Top and bottom borders" :param border_choice: Border choice name :return: Selection of cells that need to be adjusted on border change :rtype: Selection """ (top, left), (bottom, right) = self.get_grid_bbox(shape) if border_choice == "All borders": return Selection([(top, left-1)], [(bottom, right)], [], [], []) if border_choice in ("Top border", "Bottom border", "Top and bottom borders"): return Selection([], [], [], [], []) if border_choice == "Left border": return Selection([(top, left-1)], [(bottom, left-1)], [], [], []) if border_choice == "Right border": return Selection([(top, right)], [(bottom, right)], [], [], []) if border_choice == "Outer borders": return Selection([(top, right), (top, left-1)], [(bottom, right), (bottom, left-1)], [], [], []) if border_choice == "Inner borders": return Selection([(top, left)], [(bottom, right-1)], [], [], []) raise ValueError(f"border_choice {border_choice} unknown.")
[docs] def get_bottom_borders_selection(self, border_choice: str, shape: Tuple[int, int, int]): """Get selection of cells, for which the bottom border attributes need to be adjusted on border line and border color changes. border_choice names are: * "All borders" * "Top border" * "Bottom border" * "Left border" * "Right border" * "Outer borders" * "Inner borders" * "Top and bottom borders" :param border_choice: Border choice name :return: Selection of cells that need to be adjusted on border change :rtype: Selection """ (top, left), (bottom, right) = self.get_grid_bbox(shape) if border_choice == "All borders": return Selection([(top-1, left)], [(bottom, right)], [], [], []) if border_choice == "Top border": return Selection([(top-1, left)], [(top-1, right)], [], [], []) if border_choice == "Bottom border": return Selection([(bottom, left)], [(bottom, right)], [], [], []) if border_choice in ("Left border", "Right border"): return Selection([], [], [], [], []) if border_choice == "Outer borders": return Selection([(top-1, left), (bottom, left)], [(top-1, right), (bottom, right)], [], [], []) if border_choice == "Inner borders": return Selection([(top, left)], [(bottom-1, right)], [], [], []) if border_choice == "Top and bottom borders": return Selection([(top-1, left), (bottom, left)], [(top-1, right), (bottom, right)], [], [], []) raise ValueError(f"border_choice {border_choice} unknown.")
[docs] def single_cell_selected(self) -> bool: """ :return: True iif a single cell is selected via self.cells """ return len(self.cells) == 1 and not any((self.block_tl, self.block_br, self.rows, self.columns))
[docs] def cell_generator(self, shape, table=None) -> Generator: """Returns a generator of cell key tuples :param shape: Grid shape :param table: Third component of each returned key If table is None 2-tuples (row, column) are yielded else 3-tuples """ rows, columns, tables = shape (top, left), (bottom, right) = self.get_grid_bbox(shape) bottom = min(bottom, rows - 1) right = min(right, columns - 1) for row in range(top, bottom + 1): for column in range(left, right + 1): if (row, column) in self: if table is None: yield row, column elif table < tables: yield row, column, table