Source code for pyspread.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
from typing import Generator, List, Tuple


try:
    from pyspread.lib.pattern import pattern, pattern2d
except ImportError:
    from lib.pattern import pattern, pattern2d


[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]) -> bool: """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 ValueError("Cell must be a 2-tuple.") # Cell selections if cell in self.cells: return True cell_row, cell_col = cell # Block selections for (top, left), (bottom, right) in zip(self.block_tl, self.block_br): top = 0 if top is None else top left = 0 if left is None else left bottom = cell_row if bottom is None else bottom right = cell_col if right is None else right 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 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 selection parameters. :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, _ = shape crow, ccolumn, _ = 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 clipped(self, top: int, bottom: int, left: int, right: int): """Get a clipped selection :param top: Top cell of clip region :param bottom: Bottom cell of clip region :param left: Leftmost cell of clip region :param right: Rightmost cell of clip region :return: New selection that is clipped :rtype: Selection """ clipped_block_tl = [(max(top, row), max(left, column)) for row, column in self.block_tl] clipped_block_br = [(min(bottom, row), min(right, column)) for row, column in self.block_br] clipped_rows = [row for row in self.rows if top <= row <= bottom] clipped_columns = [col for col in self.columns if left <= col <= right] clipped_cells = [(row, col) for row, col in self.cells if top <= row <= bottom and left <= col <= right] return Selection(clipped_block_tl, clipped_block_br, clipped_rows, clipped_columns, clipped_cells)
[docs] def grown(self, pattern_bbox: Tuple[int], bottom: int, right: int): """Get a selection that is grown to bottom, right repeating itself :param pattern_bbox: Bounding box of repetition pattern :param bottom: Bottom cell for grown selection :param bottom: Rightmost cell for grown selection :return: New selection that is grown :rtype: Selection """ (bb_top, bb_left), (bb_bottom, bb_right) = self.get_bbox() (p_top, p_left), (p_bottom, p_right) = pattern_bbox height = bb_bottom - bb_top width = bb_right - bb_left p_height = p_bottom - p_top p_width = p_right - p_left offset = p_height + 1, p_width + 1 if not all(offset): # Zero growth return self # start = top, left stop_height = bottom + 1 stop_width = right + 1 stop = stop_height, stop_width tl_log = [] grown_block_tl = list(pattern2d(self.block_tl, offset, stop, cycle_log=tl_log)) grown_block_br = list(pattern2d(self.block_br, offset, stop=(stop_height+height, stop_width+width), limit=(stop_height, stop_width), only=tl_log)) grown_rows = list(pattern(self.rows, offset=p_height+1, stop=stop_height)) grown_columns = list(pattern(self.columns, offset=p_width+1, stop=stop_width)) grown_cells = list(pattern2d(self.cells, offset, stop)) return Selection(grown_block_tl, grown_block_br, grown_rows, grown_columns, grown_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 :param shape: Grid shape :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