pax_global_header00006660000000000000000000000064146343207330014517gustar00rootroot0000000000000052 comment=1a6a39c2864d1492e980682362d7e0f93cec22ca tksheet-7.2.12/000077500000000000000000000000001463432073300132575ustar00rootroot00000000000000tksheet-7.2.12/.editorconfig000066400000000000000000000004101463432073300157270ustar00rootroot00000000000000# EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true [*] charset = utf-8 end_of_line = crlf indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.yaml] indent_size = 2 [*.py] indent_size = 4tksheet-7.2.12/.gitignore000066400000000000000000000061141463432073300152510ustar00rootroot00000000000000# tksheet test*.py* MOD_VERSION.py *.xlsx *.csv *.tsv *.json # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .vscode .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ tksheet-7.2.12/.pre-commit-config.yaml000066400000000000000000000006431463432073300175430ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: check-added-large-files - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black language_version: python3.11 - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - id: isort tksheet-7.2.12/LICENSE.txt000066400000000000000000000020721463432073300151030ustar00rootroot00000000000000Copyright (c) 2019 ragardner and open source contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tksheet-7.2.12/README.md000066400000000000000000000100251463432073300145340ustar00rootroot00000000000000

#
tksheet - python tkinter table widget
[![PyPI version shields.io](https://img.shields.io/pypi/v/tksheet.svg)](https://pypi.python.org/pypi/tksheet/) ![python](https://img.shields.io/badge/python-3.8|3.9|3.10|3.11|3.12-blue) [![License: MIT](https://img.shields.io/badge/License-MIT%20-blue.svg)](https://github.com/ragardner/tksheet/blob/master/LICENSE.txt) [![GitHub Release Date](https://img.shields.io/github/release-date-pre/ragardner/tksheet.svg)](https://github.com/ragardner/tksheet/releases) [![Downloads](https://img.shields.io/pypi/dm/tksheet.svg)](https://pypi.org/project/tksheet/) | **Help** | | |-------------------|------------------------------------------------------------------| | Versions 6.x.x -> | [Documentation Wiki](https://github.com/ragardner/tksheet/wiki/Version-6) | | | Versions 7.x.x -> | [Documentation Wiki](https://github.com/ragardner/tksheet/wiki/Version-7) | | | [Changelog](https://github.com/ragardner/tksheet/blob/master/docs/CHANGELOG.md) | | | [Questions](https://github.com/ragardner/tksheet/wiki/Version-7#asking-questions) | | | [Issues](https://github.com/ragardner/tksheet/wiki/Version-7#issues) | | | [Suggestions](https://github.com/ragardner/tksheet/wiki/Version-7#enhancements-or-suggestions) | | This library is maintained with the help of **[others](https://github.com/ragardner/tksheet/graphs/contributors)**. If you would like to contribute please read this [help section](https://github.com/ragardner/tksheet/wiki/Version-7#contributing). ## **Changes for versions `7`+:** - **ALL** `extra_bindings()` event objects have changed, information [here](https://github.com/ragardner/tksheet/wiki/Version-7#bind-specific-table-functionality). - The bound function for `extra_bindings()` with `"edit_cell"`/`"end_edit_cell"` **no longer** requires a return value and no longer sets the cell to the return value. Use [this](https://github.com/ragardner/tksheet/wiki/Version-7#validate-user-cell-edits) instead. - `edit_cell_validation` has been removed and replaced with the function `edit_validation()`, information [here](https://github.com/ragardner/tksheet/wiki/Version-7#validate-user-cell-edits). - Only Python versions >= `3.8` are supported. - `tksheet` file names have been changed. - Many other smaller changes, see the [changelog](https://github.com/ragardner/tksheet/blob/master/docs/CHANGELOG.md) for more information. ## **Features** - Display and modify tabular data - Stores its display data as a Python list of lists, sublists being rows - Runs smoothly even with millions of rows/columns - Edit cells directly - Cell values can potentially be [any class](https://github.com/ragardner/tksheet/wiki/Version-7#data-formatting), the default is any class with a `__str__` method - Drag and drop columns and rows - Multiple line header and index cells - Expand row heights and column widths - Change fonts and font size (not for individual cells) - Change any colors in the sheet - Dropdowns, check boxes, progress bars - [Hide rows and/or columns](https://github.com/ragardner/tksheet/wiki/Version-7#example-header-dropdown-boxes-and-row-filtering) - Left `"w"`, Center `"center"` or Right `"e"` text alignment for any cell/row/column ```python """ Versions 7+ have succinct and easy to read syntax: """ # set data sheet["A1"].data = "edited cell A1" # get data column_b = sheet["B"].data # add 2 empty columns and add the change to undo stack sheet.insert_columns(columns=2, idx=4, undo=True) # delete columns 0 and 3 and add the change to undo stack sheet.delete_columns(columns=[0, 3], undo=True) ``` ### **light green theme** ![tksheet light green theme](https://github.com/ragardner/tksheet/assets/26602401/790ee9bd-b4de-48df-8c44-33f303061d84) ### **dark theme** ![tksheet dark theme](https://github.com/ragardner/tksheet/assets/26602401/fc8a0407-1486-46cf-b852-9bcff23160e5) tksheet-7.2.12/docs/000077500000000000000000000000001463432073300142075ustar00rootroot00000000000000tksheet-7.2.12/docs/CHANGELOG.md000066400000000000000000002366601463432073300160350ustar00rootroot00000000000000### Version 7.2.12 #### Pull Requests: - [237](https://github.com/ragardner/tksheet/pull/237) ### Version 7.2.11 #### Fixed: - [235](https://github.com/ragardner/tksheet/issues/235) - Error with `show_columns()` #### Pull Requests: - [236](https://github.com/ragardner/tksheet/pull/236) #### Changed: - Resizing rows/columns with mouse button motion now does so during the motion also ### Version 7.2.10 #### Fixed: - Fix index width resizing regression - Sheet no longer steals focus when using `set_sheet_data()` and similar functions, issue [233](https://github.com/ragardner/tksheet/issues/233) #### Improved: - Don't show row index resize cursor when unnecessary ### Version 7.2.9 #### Fixed: - [232](https://github.com/ragardner/tksheet/issues/232) #### Addressed: - [231](https://github.com/ragardner/tksheet/issues/231) #### Changed: - Dropdown box arrows changed back to lines as polygon triangles had issues with outlines - Possible slight changes to logic of `display_rows()`/`display_columns()` - `displayed_rows.setter`/`displayed_columns.setter` now reset row/column positions ### Version 7.2.8 #### Fixed: - Minor flicker when selecting a row or column - Top left rectangle sometimes not updating properly on change of dimensions #### Improved: - Dropdown triangle visuals #### Changed: - Top left rectangle colors - Dropdown box outline in index/header ### Version 7.2.7 #### Fixed: - [230](https://github.com/ragardner/tksheet/issues/230) - Incorrect rows/columns after move with hidden rows/columns #### Changed: - Event data for moving rows/columns with hidden rows/columns e.g. under `event.moved.rows.data` now returns not just the moved rows/columns but all new row/column indexes for the displayed rows/columns. e.g. Use `event.moved.rows.displayed` with `Sheet.data_r()`/`Sheet.data_c()` to find only the originally moved rows/columns - Moving rows/columns with hidden rows/columns will only modify these indexes if `move_data` parameter is `True`, default is `True` ### Version 7.2.6 #### Fixed: - Drag and drop incorrect drop index - `set_all_cell_sizes_to_text()` incorrect widths #### Changed: - Functions that move rows/columns such as `move_rows()`/`move_columns()` have had their move to indexes changed slightly, you may need to check yours still work as intended if using these functions ### Version 7.2.5 #### Fixed: - `StopIteration` on drag and drop #### Added: - `gen_selected_cells()` function - `is_contiguous` to `tksheet` namespace ### Version 7.2.4 #### Added: - Progress bars #### Fixed: - Fix resizing cursor appearing at start of header/index and causing issues if clicked ### Version 7.2.3 #### Fixed: - Fix add columns/add rows not inserting at correct index when index is larger than row or data len - Fix add columns/add rows not undoing/redoing added heights/widths respectively - Fix add columns/add rows regression ### Version 7.2.2 #### Added: - Add functions to address [227](https://github.com/ragardner/tksheet/issues/227): - `get_column_text_width` - `get_row_text_height` - `visible_columns` - `@property` - `visible_rows` - `@property` #### Changed: - Internal parameter names: - `only_set_if_too_small` -> ` only_if_too_small` - Internal functions: - `get_visible_rows`, `get_visible_columns` -> `visible_text_rows`, `visible_text_columns`, also changed to properties `@property` - Internal function parameters: - `set_col_width` - `set_row_height` ### Version 7.2.1 #### Fixed: - Regression since `7.2.0`: selection box borders not showing for rows/columns #### Changed: - Slightly reduced minimum row height since `7.2.0` change ### Version 7.2.0 #### Fixed: - Cells in index/header not having correct colors when columns/rows were selected - [226](https://github.com/ragardner/tksheet/issues/226) #### Changed: - A somewhat major change which warranted a significant version bump - **the minimum row height has increased slightly** to better accodomate the pipe character `|` ### Version 7.1.24 #### Fixed: - Error on paste into empty sheet with `expand_sheet_if_paste_too_big` - Data shape not being correct upon inserting a single column or row into an empty sheet - Potential for `insert_columns()` to cause issues if provided columns are longer than current number of sheet rows - [225](https://github.com/ragardner/tksheet/issues/225) Insert columns and insert rows not inserting blank cells into header/row index, issue seen with either: - Right clicking and using the in-built insert functionality - Using the `Sheet()` functions with an `int` for first parameter - Wrong sheet dimensions caused by a paste which expands the sheet - Wrong new selection box dimensions after paste which expands the sheet - `set_options()` with `table_font=` not working #### Changed: - `expand_sheet_if_paste_too_big` replaced by `paste_can_expand_x` and `paste_can_expand_y`. To avoid compatibility issues using `expand_sheet_if_paste_too_big` will set both new options #### Improved: - Additional protection against issue [224](https://github.com/ragardner/tksheet/issues/224) but with `auto_resize_row_index="empty"` which is the default setting ### Version 7.1.23 #### Fixed: - Redrawing loop with `auto_resize_row_index=True` [224](https://github.com/ragardner/tksheet/issues/224) #### Changed: - Unfortunately in order to fix [224](https://github.com/ragardner/tksheet/issues/224) while using `auto_resize_row_index=True` the x scroll bar will no longer be hidden if it is not needed ### Version 7.1.22 #### Addressed: - Issue [222](https://github.com/ragardner/tksheet/issues/222) - Issue [223](https://github.com/ragardner/tksheet/issues/223) #### Fixed: - Massive lag when pasting large amounts of data, caused by using Python `csv.Sniffer().sniff()` without setting sample size, now only samples 5000 characters max #### Changed: - Added `widget` key to emitted event `dict`s which can be either the header, index or table canvas. If you're using `pickle` on tksheet event `dict`s then you may need to delete the `widget` key beforehand - Added `Highlight`, `add_highlight`, `new_tk_event` and `get_csv_str_dialect` to tksheet namespace - Some old functions which used to return `None` now return `self` (`Sheet`) - Re-add old highlighting functions to docs - Event data from moving rows/columns while rows/columns are hidden no longer returns all displayed row/column indexes in the `dict` keys `["moved"]["rows"]["data"]`/`["moved"]["columns"]["data"]` but instead returns only the rows/columns which were originally moved. - Add `Shift-Return` to text editor newline bindings - Internal `close_text_editor()` function parameters - Function `close_text_editor()` now closes all text editors that are open #### Added: - New parameters to `dropdown()`: - `edit_data` to disable editing of data when creating the dropdowns - `set_values` so a `dict` of cell/column/row coordinates and values can be provided instead of using `set_value` for every cell in the span - New emitted events - issue [223](https://github.com/ragardner/tksheet/issues/223): - `"<>"` - `"<>"` - `"<>"` - `"<>"` - `"<>"` - `"<>"` - `"<>"` #### Improved: - Dropdown box scrollbars will only display when necessary - Some themes colors - Old function `highlight_cells()` slight performance boost when calling it thousands of times - Added parameters back to the functions `dropdown()`/`checkbox()` for help when writing code ### Version 7.1.21 #### Fixed: - Error with `equalize_data_row_lengths()` while displaying a row as the header #### Changed: - Dropdown and treeview arrow appearance to triangles ### Version 7.1.20 #### Fixed: - External right click popup menu being overwritten by internal when right clicking in empty table space - Copying and pasting values from a single column not working correctly - Mousewheel in header not working on `Linux` - `get_cell_options()`/`get_row_options()`/`get_column_options()`/`get_index_options()`/`get_header_options()` values not being requested option. These functions now return a `dict` where the values are the requested option, e.g. for `"highlight"` the values will be `namedtuple`s #### Changed: - Functions `cut()`/`copy()`/`paste()`/`delete()`/`undo()`/`redo()` now return `EventDataDict` and not `self`/`Sheet` - `get_cell_options()`/`get_row_options()`/`get_column_options()`/`get_index_options()`/`get_header_options()`, see above fix - Clipboard operations where there's a single cell which contains newlines will now be clipboarded surrounded by quotechars #### Added: - Functions `del_row_positions()`/`del_column_positions()` - `validation` parameter to `cut()`/`paste()`/`delete()`, `False` disables any bound `edit_validation()` function from running - `row` and `column` keys to `cut()`/`paste()`/`delete()` events bound with `edit_validation()` ### Version 7.1.12 #### Fixed: - Incorrect dropdown box opening coordinates following the opening of dropdown boxes on different sized rows - Calling `grid_forget()` on a parent widget after opening a dropdown box would cause opened dropdown boxes to be hidden - `disable_bindings()` with right click menu options such as insert_columns would disable the whole right click menu - Removed old deprecated dropdown box `dict` values #### Changed: - To disable the whole right click menu using `disable_bindings()` it must be called with the specific string `"right_click_popup_menu"` - themes `dict`s now `DotDict`s ### Version 7.1.11 #### Fixed: - Rare error when selecting header cell with an empty table and `auto_resize_columns` enabled #### Addressed: - Issue [221](https://github.com/ragardner/tksheet/issues/221) ### Version 7.1.10 #### Fixed: - Flickering when scrolling by using mouse to drag scroll bars - Custom bindings overwritten when using `enable_bindings()` - Rare situation where header/index is temporarily out of sync with table after setting xview/yview ### Version 7.1.9 #### Fixed: - Potential error caused by moving rows/columns where: - There exists a header but data is empty - Data and index/header are all empty but displayed row/column lines exist #### Changed: - Function `equalize_data_row_lengths` parameter `include_header` now `True` by default - Right clicking with right click select enabled in empty space in the table will now deselect all - Right clicking on a selected area in the main table will no longer select the cell under the pointer if popup menus are not enabled. Use mouse button 1 to select a cell within a selected area #### Added: - Functions: - `has_focus()` - `ctrl_boxes` - `canvas_boxes` - `full_move_columns_idxs()` - `full_move_rows_idxs()` ### Version 7.1.8 #### Fixed: - Issue with setting sheet xview/yview immediately after initialization - Visual selection box issue with boxes with 0 length or width #### Addressed: - Issue [220](https://github.com/ragardner/tksheet/issues/220) #### Added: - Functions: - `boxes` setter for use with `Sheet.get_all_selection_boxes_with_types()` or `Sheet.boxes` property - `selected` setter - `all_rows` property to get `all_rows_displayed` `bool` - `all_columns` property to get `all_columns_displayed` `bool` - `all_rows` setter to set `all_rows_displayed` `bool` - `all_columns` setter to set `all_columns_displayed` `bool` - `displayed_rows` setter uses function `Sheet.display_rows()` - `displayed_columns` setter uses function `Sheet.display_columns()` #### Removed: - All parameters from function `Sheet.after_redraw()` - Warnings about version changes from `Sheet()` initialization - `within_range` parameters from internal `get_selected_cells`/`get_selected_rows`/`get_selected_columns` functions ### Version 7.1.7 #### Fixed: - Using a cell text editor followed by setting a dropdown box with a text editor would set the previously open cell to the dropdown value #### Changed: - Selection boxes now have rounded corners - Function `get_checkbox_points` renamed `rounded_box_coords` #### Added: - `rounded_boxes` Sheet option - WIP `ListBox` class ### Version 7.1.6 #### Fixed: - Undo error #### Added: - Function `deselect_any()` for non specific selection box type deselections ### Version 7.1.5 #### Fixed: - `set_all_cell_sizes_to_text()` not working correctly if table font is different to index font, resulting in dropdown box values not showing properly - Dropdown box colors and options not being updated after sheet color change - Text editor alignments not working #### Improved: - Dropdown box alignment now uses cell alignment - Minor changes to arrow key cell selection #### Changed: - When using `show_selected_cells_border=False` the colors for `table_selected_box_cells_fg`/`table_selected_box_rows_fg`/`table_selected_box_columns_fg` will be used for the currently selected cells background ### Version 7.1.4 #### Fixed: - Fixed shift mouse click select rows/columns selecting cells instead of rows/columns - Fixed `"<>"` event not being emitted after row/column select events #### Added: - Add new parameters to `cell_selected()`, `row_selected()`, `column_selected()`, no default behaviour change - Functions: - `event_widget_is_sheet()` - `@property` function `boxes`, the same as `get_all_selection_boxes()` - `drow()`, `dcol()` functions the same as `displayed_row_to_data()`/`displayed_column_to_data()` ### Version 7.1.3 #### Fixed: - Error with `get_all_selection_boxes_with_types()` ### Version 7.1.2 #### Fixed: - Column selected detection bug - Tagged cells/rows/columns not taken into account in max index detection, relevant for moving columns/rows ### Version 7.1.1 #### Fixed: - Select all error - Span widget attribute lost on delete rows/columns and undo - Tagged cells/rows/columns lost on delete rows/columns and undo ### Version 7.1.0 #### Changed: - Event data key `"selected"` and function `get_currently_selected()` values have changed: - `type_` attribute has been changed from either `"cell"`/`"row"`/`"column"` to `"cells"`/`"rows"`/`"columns"` - The attributes in the latter indexes have also changed - See the documentation for `get_currently_selected` for more information --- - Rename class `TextEditor_` to `TextEditorTkText` - Rename `TextEditor` attribute `textedit` to `tktext` - Rename `namedtuple` `CurrentlySelectedClass` to `Selected` --- - Overhaul how selection boxes are handled internally. `Sheet` functions dealing with selection boxes should behave the same - Changed order of `Sheet()` init parameters --- - `auto_resize_row_index` now has a different default value for its old behaviour: - `auto_resize_row_index: bool | Literal["empty"] = "empty"` - With `"empty"` it will only automatically resize if the row index is empty - With `True` it will always automatically resize - `False` it will never automatically resize --- - Scrollbar appearance - `hide_rows()`/`hide_columns()` functions now endeavour to save the row heights/column widths so that they may be reinserted when using new functions `show_rows()`/`show_columns()` - Internal Dropdown Box information `dict`s no longer have the keys `"window"` and `"canvas_id"` --- Span objects now have an additional two functions which link to the `Sheet` functions of the same names: - `span.tag()` - `span.untag()` #### Removed: - **Parameters**: - `set_text_editor_value()` parameters `r` and `c` #### Added: - **Functions**: - `show_rows()`, `show_columns()` which are designed to work alongside their `hide_rows()`/`hide_columns()` counterparts - `set_index_text_editor_value()` and `set_header_text_editor_value()` - `xview()`, `yview()`, `xview_moveto()`, `yview_moveto()` - **Parameters**: - `data_indexes` `bool` parameters to functions: `hide_rows`, `hide_columns`, default value is `False` meaning there is no behavior change - `create_selections` `bool` parameters to functions: `insert_rows`, `insert_columns` default value is `True` meaning there is no behavior change - **New tksheet functionality**: - Treeview mode (still a work in progress - functions are inside `sheet.py` under # Treeview Mode) - Cell, row and column tagging functions, also added to `Span`s - Ability to change the appearance of both scroll bars - New binding `"<>"` which encompasses all select events #### Fixed: - `mapping_move_rows()` error - Potential issue with using `insert_rows` while also using an `int` as the row index to display a specific column in the index - Potential error if a selection box ends up outside of rows/columns - Pull request [#214](https://github.com/ragardner/tksheet/pull/214) - Issue [215](https://github.com/ragardner/tksheet/issues/215) #### Improved: - Ctrl select now allows overlapping boxes which begin from within another box - Ctrl click deselection - The currently selected cell will no longer change after edits to individual cells in the main table which are not valid with a different value ### Version 7.0.6 #### Changed: - The following `MainTable` attributes are now simply `int`s or `str`s which represent either pixels or number of lines, instead of `tuple`s: - `default_header_height` - `default_row_height` - `Sheet()` init keyword argument `default_row_index_width` now only accepts `int`s - Simplify internal use of `default_header_height`, `default_row_height`, `default_column_width`, `default_row_index_width` - Move the following attribute locations from `MainTable` to `Sheet.ops`: - `default_header_height` - `default_row_height` - `default_column_width` - `default_row_index_width` - Removed some protections for setting default row heights, default column widths smaller than minimum heights/widths #### Added: - Functions to address issue [#212](https://github.com/ragardner/tksheet/issues/212): - `get_text_editor_value` - `close_text_editor` ### Version 7.0.5 #### Fixed: - Issue #210 ### Version 7.0.4 #### Fixed: - Additional header cells being created when using `set_data()` or data setting using spans under certain circumstances - Additional index cells being created when using `set_data()` or data setting using spans under certain circumstances #### Added: - `Sheet.reset()` function ### Version 7.0.3 #### Fixed: - Some classifiers in `pyproject.toml` #### Added: - Parameter `emit_event` to the functions: - `span` - `set_data` - `clear` - `insert_row` - `insert_column` - `insert_rows` - `insert_columns` - `del_row` - `del_column` - `del_rows` - `del_columns` - `move_rows` - `move_columns` - `mapping_move_rows` - `mapping_move_columns` - Attribute `emit_event` to Span objects, default value is `False` - Functions `mapping_move_rows` and `mapping_move_columns` to docs #### Changed: - Functions renamed: - `move_rows_using_mapping` -> `mapping_move_rows` - `move_columns_using_mapping` -> `mapping_move_columns` - Order of parameters for functions: - `mapping_move_rows` - `mapping_move_columns` ### Version 7.0.2 #### Changed: - `Sheet()` initialization parameter `row_index_width` renamed `default_row_index_width` - `set_options()` keyword argument `row_index_width` renamed `default_row_index_width` - Move doc files to new docs folder - Delete `version.py` file, move `__version__` variable to `__init__.py` - Add backwards compatibility for `Sheet()` initialization parameters: - `column_width` - `header_height` - `row_height` - `row_index_width` ### Version 7.0.1 #### Removed: - Function `cell_edit_binding()` use `enable_bindings()` instead - Function `edit_bindings()` use `enable_bindings()` instead #### Fixed: - Error when closing a dropdown in the row index - Menus modified for every binding for one call of enable/disable bindings with more than one binding - Editing the index/header cells being allowed when using `enable_bindings("all")`/`enable_bindings()` when it's not supposed to #### Changed: - `Sheet()` initialization parameter `column_width` renamed `default_column_width` - `set_options()` keyword argument `column_width` renamed `default_column_width` - `Sheet()` initialization parameter `header_height` renamed `default_header_height` - `set_options()` keyword argument `header_height` renamed `default_header_height` - `Sheet()` initialization parameter `row_height` renamed `default_row_height` - `set_options()` keyword argument `row_height` renamed `default_row_height` - `Sheet()` initialization parameter `auto_resize_default_row_index` renamed `auto_resize_row_index` - `set_options()` keyword argument `auto_resize_default_row_index` renamed `auto_resize_row_index` - `Sheet()` initialization parameter `enable_edit_cell_auto_resize` renamed `cell_auto_resize_enabled` - `set_options()` keyword argument `enable_edit_cell_auto_resize` renamed `cell_auto_resize_enabled` - Moved most changable sheet attributes to a dict variable named `ops` (which has dot notation access) accessible from the `Sheet` object - The order of parameters has been changed for `Sheet()` initialization - `Sheet` attribute `self.C` renamed `self.PAR` - Various widget attributes named `self.parentframe` have been renamed `self.PAR` - Return key on cell editor will move to the next cell regardless of whether the cell was edited - Parameters for classes `TextEditor`, `TextEditor_`, `Dropdown - Bindings for cut, copy, paste, delete, undo, redo no longer use both `Command` and `Control` but either one depending on the users operating system - Internal arrow key binding methodology, if you were previously using this to change the arrow key bindings see [here](https://github.com/ragardner/tksheet/wiki/Version-7#changing-key-bindings) for info on adding arrow key bindings #### Added: - A way to change the in-built popup menu labels, info [here](https://github.com/ragardner/tksheet/wiki/Version-7#sheet-languages-and-bindings) - A way to change the in-built bindings for cut, copy, paste, delete, undo, redo, select all, all the arrowkey bindings and page up/down, info [here](https://github.com/ragardner/tksheet/wiki/Version-7#changing-key-bindings) ### Version 7.0.0 #### Removed: - `edit_cell_validation` from `Sheet()` initialization, `set_options()` and everywhere else due to confusion. Use the new function `edit_validation()` instead. - Functions (use spans instead for the same purpose): - `header_checkbox` - `index_checkbox` - `format_sheet` - `delete_sheet_format` - `dropdown_sheet` - `delete_sheet_dropdown` - `checkbox_sheet` - `delete_sheet_checkbox` - Parameters: - `verify` from functions `set_column_widths` and `set_row_heights` - Old unused and deprecated parameters for: - `get_cell_data()`, `get_sheet_data()`, `get_row_data()`, `get_column_data()`, `yield_sheet_rows()` - All checkbox and dropdown creation functions #### Fixed: - Grid lines now properly raised above highlighted rows/columns - Deselect events firing when unnecessary #### Changed: - Using `extra_bindings()` with `"end_edit_cell"`/`"edit_cell"` no longer requires a return value in your bound function to set the cell value to. For end user cell edit validation use the new function `edit_validation()` instead - Changed functions: - Parameters and behavior: - `checkbox` now is used to create checkboxes and utilises spans - `delete_header_dropdown` default argument changed to required argument and can no longer delete a header wide dropdown - `delete_index_dropdown` default argument changed to required argument and can no longer delete an index wide dropdown - Parameters only: - `move_columns` most parameters changed - `move_rows` most parameters changed - `click_checkbox` most parameters changed - `insert_row` parameter `idx` default argument changed to `None` - `insert_column` parameter `idx` default argument changed to `None` - `insert_rows` parameter `idx` default argument changed to `None` - `insert_columns` parameter `idx` default argument changed to `None` - Renamed: - `align` -> `table_align` - Reorganised order of functions in `sheet.py` to match documentation #### Added: - Method `edit_validation(func: Callable | None = None) -> None` to replace `edit_cell_validation` - New methods for getting and setting data - `bind` now also accepts `"<>"` and `"<>"` arguments - Redo, which is enabled when undo is enabled, use by pressing ctrl/cmd + shift + z - Named spans for sheet options such as highlight, format - Ctrl/cmd click deselect - Ability to make currently selected box border different color to selection box border #### Other Changes: - Overhaul and totally change event data sent to functions bound by `extra_bindings()` - Deselect events are now labelled as "select" events, see the docs on `"selection_boxes"` for more information - Overhaul internal selection box workings - Rename tksheet files - Pressing escape on text editors no longer generates an edit cell/header/index event ### Version 6.3.5 #### Fixed: - Error with function `set_currently_selected()` #### Addressed: - [207](https://github.com/ragardner/tksheet/issues/207) ### Version 6.3.4 #### Fixed: - [#205](https://github.com/ragardner/tksheet/issues/205) ### Version 6.3.3 #### Improved: - Term searching improved when typing in a normal state dropdown box #### Removed: - `bind_event()` function, `bind()` to be used instead ### Version 6.3.2 #### Changed: - tksheet no longer supports Python 3.6, only versions 3.7+ #### Removed: Due to errors caused when using Python versions < 3.8 the following functions have been removed: - __bool__ - __len__ #### Fixed: - Sheet init error since version `6.3.0` when running Python 3.7 ### Version 6.3.1 #### Fixed: - Two `EditCellEvent`s being emitted, removed the one with `None` as `text` attribute - Selection box and currently selected box in different places when tab key pressed with single cell selected - Return key not working in dropdown box when mouse pointer is outside of dropdown - Visual issue: Dropdown arrow staying up when clicking on same cell with readonly dropdown state - Visual issue: Dropdown row/column all arrows in up position when one box is open #### Removed: - Many parameters from internal functions dealing with text editor to simplify code #### Changed: - Cell selection doesn't move on Return key when cell edit using text editor was invalid - Events for `extra_bindings()` end cut, delete and paste are no longer emitted if no changes were made #### Improved: - Term searching improved when typing in a normal state dropdown box ### Version 6.3.0 #### Fixed: - Cell editor right click not working - Cell editor select all not working #### Added: - Some methods to `Sheet` objects, see [documentation](https://github.com/ragardner/tksheet/wiki/Version-6#sheet-methods) for more information. - __bool__ - __len__ - __iter__ - __reversed__ - __contains__ ### Version 6.2.9 #### Fixed: - Incorrect row being targeted with hidden rows and text editor newline binding (potential error) ### Version 6.2.8 #### Fixed: - [#203](https://github.com/ragardner/tksheet/issues/203) ### Version 6.2.7 #### Fixed: - [#202](https://github.com/ragardner/tksheet/issues/202) ### Version 6.2.6 #### Fixed: - [#201](https://github.com/ragardner/tksheet/issues/201) - The ends of grid lines were incorrectly displaying connections with one another when only showing horizontal or vertical grid - When a cell dropdown and a row checkbox were in the same cell both would be drawn but only one would function, this has been changed to give dropdown boxes priority - Index text overlapping checkbox when alignment is `"right"`/`"e"`/`"east"` and index is not wide enough #### Added: - [#200](https://github.com/ragardner/tksheet/issues/200) ### Version 6.2.5 #### Fixed: - Error when zooming or using `see()` with empty table - Add initialization option `zoom` which is an `int` representing a percentage of the font size, `100` being no change #### Removed: - Some unnecessary internal variables ### Version 6.2.4 #### Added: - Zoom in/out bindings control + mousewheel - Zoom in bindings control + equals, control + plus - Zoom out binding control + minus ### Version 6.2.3 #### Fixed: - Bug with `format_row` using "all" ### Version 6.2.2 #### Fixed: - 2 pixel misalignment of index/header and table when scrolling - Undo cell edit not scrolling window #### Added: - Functionality to scroll multiple Sheets when scrolling one particular Sheet ### Version 6.2.1 #### Fixed: - Editing header and overflowing text editor so text wraps while using `auto_resize_columns` causes text editor to be out of position - `insert_columns()` when using an `int` for number of blank columns creates incorrect list layout ### Version 6.2.0 #### Fixed: - Removed some type hinting that was only available to python versions >= 3.9 ### Version 6.1.9 #### Fixed: - Issues that follow selection boxes being recreated such as resizing columns/rows ### Version 6.1.8 #### Added: - [#180](https://github.com/ragardner/tksheet/issues/180) ### Version 6.1.7 #### Fixed: - [#183](https://github.com/ragardner/tksheet/issues/183) - [#184](https://github.com/ragardner/tksheet/issues/184) ### Version 6.1.5 #### Fixed: - `extra_bindings()` not binding functions - [#181](https://github.com/ragardner/tksheet/issues/181) ### Version 6.1.4 #### Fixed: - Error with setting/getting header font - Setting fonts with `set_options()` not working - Setting fonts after table initialization didn't refresh selection boxes or top left rectangle dimensions #### Changed: - Replaced wildcard imports - Format code with 120 line length ### Version 6.1.3 #### Fixed: - Missing imports - Bug with `delete_rows()` - Bug with hidden columns, cell options and deleting columns with the inbuilt right click menu - Bug with a paste that expands the sheet where row lengths are uneven - Poor box selection for cut and copy with multiple selected boxes - Bug with `insert_columns()` with uneven row lengths - Bug with `insert_rows()` if new data contains row lengths that are longer than sheet data - Errors that occur when dragging and dropping rows/columns beyond the window ### Version 6.1.2 #### Fixed: - Further potential issues with moving columns where row lengths are uneven - Potential issues with using `move_rows()` where the provided index is larger than the number of rows - `dropdown_sheet()` causing error ### Version 6.1.1 #### Fixed: - [#177](https://github.com/ragardner/tksheet/issues/177) ### Version 6.1.0 #### Fixed: - Error with using `None` to create a dropdown for the entire index when index is not `int` ### Version 6.0.5 #### Fixed: - Bugs with using `None` to create dropdowns/checkboxes for entire header/index - Bug with creating dropdowns in row index - Bug with opening dropdowns in row index - Bug with `insert_rows()` if `rows` had more columns than existing sheet - Bug with `delete_column_dropdown()` - Bug with using external move rows/columns functions where `index_type` wasn't `"displayed"` - Bug with hidden rows and cutting/copying #### Changed: - Formatted code using `Black` ### Version 6.0.4 #### Fixed: - Bug with setting header height when header is set to `int` - Bug with clicking top left rectangle bars to reset header height / index width - Wrong colors showing when having rows and columns selected at the same time - Wrong colors showing for particular selection scenarios - Control select enabled when using `enable_bindings("all")` when it should only be enabled if using `enable_bindings("ctrl_select")` - When control select was disabled holding control/command would disable basic sheet bindings #### Added: - Add additional binding for `sheet.bind_event()`. `"<>"` and `"<>"` are the two existing event options but currently only an empty dict is the associated data ### Version 6.0.3 #### Fixed: - `disable_bindings()` disable all bindings not disabling edit header/index capability - Undo not recreating all former selection boxes with cell edits - Undoing paste where sheet was expanded and rows were hidden didn't remove added rows - Bug with hidden rows and index highlights - Bug with hidden rows and text editor newline binding in main table - Bug with hidden rows and drag and drop rows - Issue where columns/rows that where selected already would not open dropdown boxes or toggle checkboxes #### Removed: - External function `edit_cell()`, use `open_cell()` and `set_currently_selected()` as alternative #### Added: - Control / Command selecting multiple non-consecutive boxes - Functions `checkbox_cell`, `checkbox_row`, `checkbox_column`, `checkbox_sheet`, `dropdown_cell`, `dropdown_row`, `dropdown_column`, `dropdown_sheet` and their related deletion functions #### Changed: - `extra_bindings()` undo event type `"insert_row"` renamed `"insert_rows"`, `"insert_col"` renamed `"insert_cols"` - `extra_bindings()` undo event `.storeddata` has changed for row/column drag drop undo events to `("move_cols" or "move_rows", original_indexes, new_indexes)` - Initialization argument `max_rh` renamed `max_row_height` - Initialization argument `max_colwidth` renamed `max_column_width` - Initialization argument `max_row_width` renamed `max_index_width` - `header_dropdown_functions`/`index_dropdown_functions`/`dropdown_functions` now return dict - Rename internal function `create_text_editor()` - Select all now maintains existing currently selected cell when using Control/Command - a - Renamed internal functions `check_views()` `check_xview()` `check_yview()` - Default argument `redraw` changed to `True` for dropdown/checkbox creation functions - Internal function `open_text_editor()` keyword argument `set_data_on_close` defaulted to `True` - Merge internal functions `edit_cell_()` and `open_text_editor()`, remove function `edit_cell_()` - Better graphical indicators for which columns/rows are being moved when dragging and dropping - Better retainment of selection boxes after undo cell edits ### Version 6.0.2 #### Fixed: - Right click delete rows bug [PR#171](https://github.com/ragardner/tksheet/pull/171) ### Version 6.0.1 #### Fixed: - Data getting with `get_displayed = True` not returning checkbox text and also in the main table dropdown box text - None being displayed on table instead of empty string - `insert_columns()`/`insert_rows()` with hidden columns/rows not working quite right #### Added: - Hidden rows capability use `display_rows()`/`hide_rows()` and read the documentation for more info #### Changed: - Some functions have had their `redraw` keyword argument defaulted to `True` instead of `False` such as `highlight_cells()` - `hide_rows()`/`hide_columns()` `refresh` arg changed to `redraw` - `hide_rows()`/`hide_columns()` now uses displayed indexes not data ### Version 6.0.0 #### Fixed: - Undo added to stack when no changes made with cut, paste, delete - Using generator with `set_column_widths()`/`set_row_heights()` would result in lost first width/height - Header/Index dropdown `modified_function` not sending modified event - Escape out of dropdown box doesn't reset arrow orientation #### Added: - Cell formatters, thanks to [PR#158](https://github.com/ragardner/tksheet/pull/158) - `format_cell()`, `format_row()`, `format_column()`, `format_sheet()` - Options for changing output to clipboard delimiter, quotechar, lineterminator and option for setting paste delimiter detection - Bindings `"up" "down" "left" "right" "prior" "next"` to enable arrowkey bindings individually use with `enable_bindings()`/`disable_bindings()` = Dropdowns now have a search feature which searches their values after the entry box is modified (if dropdown is state `normal`) - Dropdown kwargs `search_function`, `validate_input` and `text` #### Removed: - Startup arg `ctrl_keys_over_dropdowns_enabled`, also removed in `set_options()` - `set_copy` arg in `set_cell_data()` - `return_copy` arg in data getting functions but will not generate error if used as keyword arg #### Changed: - `index_border_fg` and `header_border_fg` no longer work, they now use the relevant grid foreground options - `"dark"`/`"black"` themes - Dropdowns now default to state `"normal"` and validate input by default - `set_dropdown_values()`/`set_index_dropdown_values()`/`set_header_dropdown_values()` keyword argument `displayed` changed to `set_value` for clarity - Checkbox click extra binding and edit cell extra binding (when associated with a checkbox click) return `bool` now, not `str` - `get_cell_data()`/`get_row_data()`/`get_column_data()` have had an overhaul and have different keyword arguments, see documentation for more information. They also now return empty string/s if index is out of bounds (instead of `None`s) - Rename some internal functions for consistency - Extra bindings delete key now returns dict instead of list for boxes ### Version 5.6.8 #### Fixed: - [#159](https://github.com/ragardner/tksheet/issues/159) #### Changed: - `"dark"` theme now looks more appropriate for MacOS dark theme ### Version 5.6.7 Fixed: - [#157](https://github.com/ragardner/tksheet/issues/157) - Double click row height resize not taking into account index checkbox text Changed: - Some internal variables local to redrawing functions for clarity ### Version 5.6.6 Fixed: - `delete_rows()`/`delete_columns()` incorrect row heights/column widths after use - Tab key not seeing cell if out of sight - Row height / column width resizing with mouse incorrect by 1 pixel - Row index not extending if too short when changing a specific index - Selected rows/columns border fg not displaying in cell border - Various minor text placements - Edit index/header prematurely resizing height/width Improved: - Significant performance improvements in redrawing table, especially when simply selecting cells - All themes - Can now drag and drop columns and rows with or without shift being held down, mouse cursor changes to hand when over selected Changed: - Dropdown box colors now use popup menu colors - Checkboxes no longer have X inside, instead simply a smaller more distinct rectangle to improve redrawing performance ### Version 5.6.5 Fixed: - [#152](https://github.com/ragardner/tksheet/issues/152) ### Version 5.6.4 Fixed: - `set_row_heights()`/`set_column_widths()` failing to set if iterable was empty - `delete_rows()`/`delete_columns()` failing to delete row heights, column widths if arg is empty - Edges of grid lines appearing when not meant to Changed: - Add `redraw` option for `change_theme()` ### Version 5.6.3 Fixed: - Dropdown boxes in main table not opening in certain circumstances - Scrollbars not having correct column/rowspan - Error when moving header height - Resizing arrows still showing up when hiding header/index if height/width resizing enabled Added: - `header` startup argument, same as `headers` Changed: - Increase default maximum undos from 20 to 30 - Removed dropdown box border, to get them back use `show_dropdown_borders = True` on startup or with `set_options()` - Removed unnecessary variables and tidied `__init__` code ### Version 5.6.2 Fixed: - [#154](https://github.com/ragardner/tksheet/issues/154) - `delete_column()` not working with hidden columns - `insert_column()`/`insert_columns()` not working correctly with hidden columns Added: - `delete_columns()`, `delete_rows()` Changed: - `delete_column()`/`delete_row()` now use `delete_columns()`/`delete_rows()` internally - `insert_column()` now uses `insert_columns()` internally ### Version 5.6.1 Fixed: - [#153](https://github.com/ragardner/tksheet/issues/153) Changed: - Adjust dropdown arrow sizes for more consistency for varying fonts ### Version 5.6.0 Fixed: - Issues and possible errors with dropdowns/checkboxes/cell edits - `delete_dropdown()`/`delete_checkbox()` issues Changed: - Deprecated external functions `create_text_editor()`/`get_text_editor_value()`/`bind_text_editor_set()` as they no longer worked both externally and internally, use `open_cell()` instead - Renamed internal function `get_text_editor_value()` to `close_text_editor()` Improved: - Slightly boost performance if there are many cells onscreen and gridlines are showing - You can now use the scroll wheel in the header to vertically scroll if there are multiple lines in the column headers - Improvements to text editor - Text now draws slightly closer to cell edges in certain scenarios - Improved visibility of dropdown box against sheet background - Improved dropdown window height - black theme selected cells border color - light green theme selected cells background color ### Version 5.5.3 Fixed: - Error on start ### Version 5.5.2 Fixed: - Row index missing itertools repeat - Header checkbox, index checkbox bugs - Row index editing errors - `move_columns()`/`move_rows()` redrawing when not supposed to - `move_row()`/`move_column()` error - `delete_out_of_bounds_options()` error - `dehighlight_cells()` incorrectly wiping all cell options when `"all"` is used Changed: - `set_column_widths()`/`set_row_heights()` can now receive any iterable if `canvas_positions` is `False` - Tidied internal row/column moving code ### Version 5.5.1 Fixed: - `display_columns()` no longer redraws if `deselect_all` is `True` even when `redraw` is `False` - `extra_bindings()` cell editors carrying out cell edits even if validation function returns `None` - Index and header alignments wrongly associated with column and row alignments if `align_header`/`align_index` were `False` - Undo drag and drop wrong position if columns move back to higher index - Error when shift b1 press in headers/index while using extra bindings Changed: - Internal variable name `data_ref` to `data` - `get_currently_selected()` and `currently_selected()` see documentation for more information - The way `extra_bindings()` + `begin_edit_cell` works, now if `None` is returned then the cell editor will not be opened - Paste repeats when selection box is larger than pasted items and is a multiple of pasted box - `move_row()`/`move_column()` now internally use `move_rows()`/`move_columns()` - `black` theme text to be a lot brighter Added: - Spacebar to edit cell keys - Function `open_cell()` which uses currently selected box and mouse event - Function `data` see the documentation for more info - Functions `get_cell_alignments()`, `get_row_alignments()`, `get_column_alignments()`, `reset_all_options()`, `delete_out_of_bounds_options()` - Functions `move_columns()`, `move_rows()` ### Version 5.5.0 - Deprecate functions `display_subset_of_columns()`, `displayed_columns()` - Change `get_text_editor_value()` function argument `destroy_tup` to `editor_info` - Change `display_columns()` function argument `indexes` to `columns` - Change `display_columns()` function argument `enable` to `all_columns_displayed` - Change some internal variable names - Remove all calls to `update()` from `tksheet` - Fix row index `"e"` text alignment bug - Fix checkbox text not showing in main table when not using west alignment - Fix `total_rows()` bug - Fix dropdown arrow being asymmetrical with different font sizes - Fix incorrect default headers while using hidden columns - Fix issue with `get_sheet_data()` if including index and header not putting a corner cell in the top left - Modify redrawing code, slightly improve redrawing efficiency in some scenarios - Add dropdown boxes, check boxes and cell editing to index - Add options `edit_cell_validation` which is an option for `extra_bindings`, see the documentation for more info - Add function `yield_sheet_rows()` which includes default index and header values if using them - Add function `hide_columns()` which allows input of columns to hide, instead of columns to display - Add functions related to index editing, checkboxes and dropdown boxes - Add default argument `show_default_index_for_empty` - Add `Edit header` option to in-built right click menu if both are enabled - Add `Edit index` option to in-built right click menu if both are enabled - Add `Edit cell` option to in-built right click menu if both are enabled - Documentation updates ### Version 5.4.1 - Fix bugs with functions `readonly_header()`, `checkbox()` and `header_checkbox()` - Clarify table colors documentation ### Version 5.4.0 - Improve documentation a bit ### Version 5.3.9 - Fix bug with paste without anything selected - Minor fix to highlight functions which could cause an error using certain args - Fix `ctrl_keys_over_dropdowns_enabled` initialization arg not setting ### Version 5.3.8 - Fix focus out of table cell editor by clicking on header not setting cell ### Version 5.3.7 - Fix issue with `enable_bindings()`/`disable_bindings()` no longer accept a tuple - Fix drag and drop bugs introduced in 5.3.6 - Fix bugs with header set to integer introduced in 5.3.6 ### Version 5.3.6 - Editable and readonly dropdown boxes and checkboxes in the header - Add readonly functions to header - Editable header using `enable_bindings("edit_header") - Display text next to checkboxes in cells using `text` argument (is not considered data just for display, the data is either `True` or `False` in a checkbox) - Move position of checkboxes and dropdown boxes to top left of cells - Enable dragging and dropping columns when there are hidden columns, including undo - Checkboxes no longer editable using `Delete`, `Paste` or `Cut` - Dropdown boxes no longer editable using `Delete`, `Paste` or `Cut` unless dropdown box has a value which matches, override this behaviour with new option `ctrl_keys_over_dropdowns_enabled` - More logical behaviour around checkboxes and dropdown boxes and bindings - Many improvements and bug fixes ### Version 5.3.5 - Fix control commands not working when top left has focus - Fix bug when undoing paste where extra rows/columns were added during the paste (disabled by default) ### Version 5.3.4 - Unnecessary folder deletion - Add new theme `dark` ### Version 5.3.3 - Add `add` parameter to `.bind()` function (only works for bindings which are not the following: `""`, `""`, `""`, `""`, `""` and lastly whichever is your operating systems right mouse click button) ### Version 5.3.2 - Fix Backspace binding in main table ### Version 5.3.1 - Fix minor issues with `startup_select` argument - Fix extra menus being created in memory ### Version 5.3.0 - Fix shift select error in main table when nothing else is selected ### Version 5.2.9 - Fix text editor not opening in highlighted cells - Linux return key in cell editor fix - Fix potential error with check box / dropdown ### Version 5.2.8 - Make dropdown box border consistent color - Fix error with modifier key press on editable dropdown boxes ### Version 5.2.7 - Improve looks of dropdown arrows and fix slight overlapping with text - Check boxes no longer show `Tr` and `Fa` when cell is too small for check box for large enough for text ### Version 5.2.6 - Fix issues with editable (`"normal"`) dropdown boxes and focus out bindings - Editable dropdown boxes now respond normally to character, backspace etc. key presses ### Version 5.2.5 - Fix bug with highlighted cells being readonly ### Version 5.2.4 - Fix user bound right click event not firing due to right click context menu - Fix issues with hidden columns and right click insert columns ### Version 5.2.3 - Fix error on deleting row with dropdown box - Fix issues with deleting columns with check boxes, dropdown boxes ### Version 5.2.2 - Improve looks for dropdown arrows - Fix grid lines in certain drop down boxes - Fix dropdown scroll bar showing up when not needed - After resizing rows/columns if mouse is in same position cursor reacts accordingly again ### Version 5.2.1 - Remove `see` argument from `create_checkbox()` and `create_dropdown()` because they rely on data indexes whereas `see()` relies on displayed indexes - Fix undo not working with check box toggle - Fix error on clicking sheet empty space - Improve check box looks - Fix bugs related to hidden columns and dropdowns/checkboxes - Disable align from working with `create_dropdown()` because it was broken with anything except left alignment - Code cleanup ### Version 5.2.0 - Adjust dropdown box heights slightly - Fix extra bindings begin edit cell making cell edit delete contents - Make many events namedtuples for better clarity - Add checkbox functionality ### Version 5.1.7 - Fix double button-1 not editing cell - Fix error with edit cell extra bindings ### Version 5.1.6 - Fix dropdown box and checkbox triggering off of non-Return keypresses - Fix issues with readonly cells and rows and control actions such as cut, delete, paste - Begin to add checkbox code - Some code cleanup ### Version 5.1.5 - Fix center, e text alignment with dropdowns - Fix potential error when closing root Tk() window ### Version 5.1.4 - Fix error when hiding columns and creating dropdown boxes - Fix dropdown box sizes ### Version 5.1.3 - Major fixes for dropdown boxes and possibly cell editor - Fix mouse click outside cell edit window not setting cell value - International characters should work to edit a cell by default - Minor improvements ### Version 5.1.2 - Bugfixes and improvements related to `5.1.0` ### Version 5.1.1 - Bugfixes and improvements related to `5.1.0` ### Version 5.1.0 - Overhaul dropdowns - Replace ttk dropdown widget ### Version 5.0.34 - Fix `None` returned with `get_row_data()` when `get_copy` is `False` - Add bindings for column width resize and row height resize - Fix error with function `get_dropdowns()` ### Version 5.0.33 - Add default argument `selection_function` to `create_dropdown()` ### Version 5.0.32 - Add sheet refreshing to delete_row(), delete_column() ### Version 5.0.31 - Fixed insert rows/columns below/left working incorrectly ### Version 5.0.30 - Fixed issue with cell editor stripping whitespace from the end of value ### Version 5.0.29 - Update documentation - `insert_row` argument `add_columns` now `False` by default for better performance ### Version 5.0.28 - Various minor improvements ### Version 5.0.27 - Internally set `all_columns_displayed` to `True` when user chooses full list of columns as argument for `display_columns()`/`display_subset_of_columns()` ### Version 5.0.26 - Add option `expand_sheet_if_paste_too_big` in initialization and `set_options()` which adds rows/columns if paste is too large for sheet, disabled by default ### Version 5.0.25 - Select all now needs to be enabled separately from `"drag_select"` using `"select_all"` - Separate Control / Command bindings based on OS ### Version 5.0.24 - Fix documentation link ### Version 5.0.23 - Fix page up/down cell select event not being created if user has cell selected ### Version 5.0.22 - Put all begin extra functions inside try/except, returns on exception ### Version 5.0.21 - Attempt to fix PyPi version issue ### Version 5.0.2 - `set_sheet_data()` no longer verifies by default that data is list of lists (inbuilt functionality such as cell editing still requires list of lists though) - Initialization argument `data` allows tuple or list ### Version 5.0.19 - Scrolling with arrowkeys improvements ### Version 5.0.18 - Fix bug with hiding scrollbars not working ### Version 5.0.17 - Cell highlight tkinter colors no longer case sensitive - Add additional items to `end_edit_cell` event responses ### Version 5.0.16 - Add function `default_column_width()` and add `column_width` to function `set_options()` - Add functions `get_dropdown_values()` and `set_dropdown_values()` - Add row and column arguments to `get_dropdown_value()` to get a specific dropdown boxes value - `create_dropdown()` argument `see` is now `False` by default ### Version 5.0.15 - Add functions `popup_menu_add_command()` and `popup_menu_del_command()` for extra commands on in-built right click popup menu - Update documentation ### Version 5.0.14 - Remove some unnecessary code (`preserve_other_selections` arguments in some functions) - Add documentation file, update readme file ### Version 5.0.13 - Add extra variable to undo event - Edit cell no longer creates undo if cell is left unchanged ### Version 5.0.12 - Remove/add scrollbars depending on if window can be scrolled - Fix bug with delete key ### Version 5.0.11 - Add default height and width if a height is used without a width or vice versa ### Version 5.0.1 - Add right (`e`) cell alignment for index and header ### Version 5.0.0 - Fix errors with `insert_column()` and `insert_columns()` ### Version 4.9.99 - Fixed bugs with row copying where `list(repeat(list(repeat(` was used in code to create empty list of lists - Made cell resize to text (width only) take dropdown boxes into account ### Version 4.9.98 - Fix error with dropdown box close while showing all columns ### Version 4.9.97 - Fix bug with `delete_row()` and `delete_column()` functions when used with default arguments ### Version 4.9.96 - Fix bug with dragging scrollbar when columns are shorter than window - Add startup argument `after_redraw_time_ms` default is `100` ### Version 4.9.95 - Fix bug with `insert_rows()` - Add `redraw` default argument to many functions, default is `False` ### Version 4.9.92 - 4.9.94 - Hopefully fix Linux mousewheel ### Version 4.9.91 - Fix auto resize issue ### Version 4.9.9 - Attempt to fix Linux mousewheel scrolling - Add `enable_edit_cell_auto_resize` option to startup and `set_options` default is `True` ### Version 4.9.8 - Fix potential issue with undo and dictionary copying - Fix potential errors when moving/inserting/deleting rows/columns - Add drop down position refresh to delete columns/rows on right click menu ### Version 4.9.7 - Various bug fixes and improvements - Add readonly cells/columns/rows ### Version 4.9.6 - Fix bugs with `font()` functions - Fix edit cell bug when hiding columns ### Version 4.9.5 - Attempt to fix scrolling issues ### Version 4.9.4 - Make `display_subset_of_columns()` and other names of the same function always sort the showing columns - Make right click insert columns left shift data columns along - Fix issues with `insert_column()`/`insert_columns()` when hiding columns - Add default arguments `mod_column_positions` to functions `insert_column()`/`insert_columns()` which when set to `False` only changes data, not number of showing columns - Add `"e"` aka right hand side text alignment for main table, have not added to header or index yet - Add functions `align_cells()`, `align_rows()`, `align_columns()` ### Version 4.9.3 - Fix paste bug - Fix mac os vertical scroll code ### Version 4.9.2 - Add mac OS command c, x, v, z bindings - Make shift - mousewheel horizontal scroll ### Version 4.9.1 - Make alt tab windows maintain open cell edit box - Fix bug in `insert_columns()` ### Version 4.8.9 - 4.9.0 - Make functions `insert_row()`, `insert_column()`, `delete_row()`, `delete_column()` adjust highlighted cells/rows/columns to maintain correct highlight indexes - Built in right click functions now also auto-update highlighted cells/rows/columns - Add argument `reset_highlights` to `set_sheet_data()` default is `False` - Add function `dehighlight_all()` - Various bug fixes - Right click insert column when hiding columns has been changed to always insert fresh new columns ### Version 4.8.8 - Fix bug with `highlight` functions where `fg` is set but not `bg` ### Version 4.8.7 - Fix delete key bug ### Version 4.8.6 - Change many values sent to functions set by `extra_bindings()` - begin is before action is taken, end is after | Binding | Response | |--------------------------------|------------------------------------------------------------------------:| |"begin_ctrl_c" |("begin_ctrl_c", boxes, currently_selected) | |"end_ctrl_c" |("end_ctrl_c", boxes, currently_selected, rows) | |"begin_ctrl_x" |("begin_ctrl_x", boxes, currently_selected) | |"end_ctrl_x" |("end_ctrl_x", boxes, currently_selected, rows) | |"begin_ctrl_v" |("begin_ctrl_v", currently_selected, rows) | |"end_ctrl_v" |("end_ctrl_v", currently_selected, rows) | |"begin_delete_key" |("begin_delete_key", boxes, currently_selected) | |"end_delete_key" |("end_delete_key", boxes, currently_selected) | |"begin_ctrl_z" |("begin_ctrl_z", change type) | |"end_ctrl_z" |("end_ctrl_z", change type) | |"begin_insert_columns" |("begin_insert_columns", stidx, posidx, numcols) | |"end_insert_columns" |("end_insert_columns", stidx, posidx, numcols) | |"begin_insert_rows" |("begin_insert_rows", stidx, posidx, numrows) | |"end_insert_rows" |("end_insert_rows", stidx, posidx, numrows) | |"begin_row_index_drag_drop" |("begin_row_index_drag_drop", orig_selected_rows, r) | |"end_row_index_drag_drop" |("end_row_index_drag_drop", orig_selected_rows, new_selected, r) | |"begin_column_header_drag_drop" |("begin_column_header_drag_drop", orig_selected_cols, c) | |"end_column_header_drag_drop" |("end_column_header_drag_drop", orig_selected_cols, new_selected, c) | `boxes` are all selection box coordinates in the table, currently selected is cell coordinates `rows` (in ctrl_c, ctrl_x and ctrl_v) is a list of lists which represents the data which was worked on - Right click insert columns/rows now inserts as many as selected or one if none selected - Some minor improvements ### Version 4.8.5 - Fix line creation when click drag resizing index - Improve looks of top left rectangle resizers when one is disabled ### Version 4.8.4 - Fix bug in `insert_row_positions()` ### Version 4.8.3 - Make `enable_bindings()` and `disable_bindings()` without arguments do all bindings - Fix top left resizers not showing up if enabling/disabling all bindings - Improve look and feel of drag and drop - Fix bug in certain circumstances with `insert_row_positions()` - Fix `highlight_rows()` bug ### Version 4.8.2 - Fix old color name, credit `ministiy` ### Version 4.8.1 - Make drag selection only redraw table when rectangle dimensions change instead of mouse moves ### Version 4.8.0 - Adjust light green theme colors slightly - Changed all color option argument names for better clarity | Old Name | New Name | |------------------------------|----------------------------------:| |frame_background |frame_bg | |grid_color |table_grid_fg | |table_background |table_bg | |text_color |table_fg | |selected_cells_border_color |table_selected_cells_border_fg | |selected_cells_background |table_selected_cells_bg | |selected_cells_foreground |table_selected_cells_fg | |selected_rows_border_color |table_selected_rows_border_fg | |selected_rows_background |table_selected_rows_bg | |selected_rows_foreground |table_selected_rows_fg | |selected_columns_border_color |table_selected_columns_border_fg | |selected_columns_background |table_selected_columns_bg | |selected_columns_foreground |table_selected_columns_fg | |resizing_line_color |resizing_line_fg | |drag_and_drop_color |drag_and_drop_bg | |row_index_background |index_bg | |row_index_foreground |index_fg | |row_index_border_color |index_border_fg | |row_index_grid_color |index_grid_fg | |row_index_select_background |index_selected_cells_bg | |row_index_select_foreground |index_selected_cells_fg | |row_index_select_row_bg |index_selected_rows_bg | |row_index_select_row_fg |index_selected_rows_fg | |header_background |header_bg | |header_foreground |header_fg | |header_border_color |header_border_fg | |header_grid_color |header_grid_fg | |header_select_background |header_selected_cells_bg | |header_select_foreground |header_selected_cells_fg | |header_select_column_bg |header_selected_columns_bg | |header_select_column_fg |header_selected_columns_fg | |top_left_background |top_left_bg | |top_left_foreground |top_left_fg | |top_left_foreground_highlight |top_left_fg_highlight | ### Version 4.7.9 - Add option to use integer in `insert_row_positions()` and `insert_column_positions()` for how many columns to add - Fix error occurring in above functions when using python 3.6-3.7 due to itertools argument added in python 3.8 ### Version 4.7.8 - Fix show_horizontal and show_vertical grid ### Version 4.7.7 - Reduce column minimum size even further - Fix bug with highlights introduced in 4.7.6 ### Version 4.7.6 - Make `startup_select` also see the chosen cell - Deprecated functions `is_cell_selected()`, `is_row_selected()`, `is_column_selected()` use the same functions but without the `is_` in the name - Add functions `highlight_columns()`, `highlight_rows()`, make `highlight_cells()` argument `cells` update dictionary rather than overwrite - Remove double click resizing of index - Reduce minimum column width to 1 character ### Version 4.7.5 - Make arrowkey up and left not select column/row when at end, will add more shortcuts in a later update - Adjust width of auto-resized index - Hide header height/index width reset bars in top left if relevant options are disabled ### Version 4.7.4 - Add option `page_up_down_select_row` will select row when using page up/down, default is `True` - Overhaul text, grid and highlight canvas item management, no longer deletes and redraws, keeps items using `"hidden"` and reuses them to prevent canvas item number getting high quickly - Edit cell resizing now only uses displayed rows/columns to fit to cell text - Fix `see()` not being correct when using non-default `empty_vertical` and `empty_horizontal` - Fix flicker when row index auto resizes - Rename `auto_resize_numerical_row_index` to `auto_resize_default_row_index` - Add option `startup_focus` to give `Sheet()` main table focus on initialization - Add startup argument `startup_select` use like so: To create a cell selection box where cells 0,0 up to but not including 3,3 are selected: ```python startup_select = (0, 0, 3, 3, "cells"), ``` To create a row selection box where rows 0 up to but not including 3 are selected: ```python startup_select = (0, 3, "rows"), ``` To create a column selection box where columns 0 up to but not including 3 are selected: ```python startup_select = (0, 3, "columns"), ``` ### Version 4.7.3 - Make `"begin_edit_cell"` and `"end_edit_cell"` the only two edit cell bindings for use with `extra_bindings()` function (although `"edit_cell"` still works and is the equivalent of `"end_edit_cell"`) - Make it so if `"begin_edit_cell"` binding returns anything other than `None` the text in the cell edit window will be that return value otherwise it will be the relevant keypress - Prevent auto resize from firing two redraw commands in one go - Fixed issue where clicking outside of cell edit window and Sheet would return focus to the Sheet widget anyway - Fixed issues where row/column select highlight would be lower in canvas than cell selections - Hopefully simplified cell edit code a bit - Fix undo delete columns error for python versions < 3.8 ### Version 4.7.2 - Adjustment to dark theme colors - Clean up repetitions of code which had virtually no performance gain anyway ### Version 4.7.1 - Rename themes to "light blue", "light green", "dark blue", "dark green" - Clean up theme and color settings code ### Version 4.7.0 - Add green theme - Change font to calibri ### Version 4.6.9 - Minor fixes when using in-built ctrl functions ### Version 4.6.8 - Fix button-1-motion scrolling issues - Many minor improvements ### Version 4.6.7 - Some minor improvements - Fix minor display issue where sheet is empty and user clicks and drags upwards ### Version 4.6.6 - Added multiple new functions - Many minor improvements - Decreased size of area that resize cursors will show - Much improved dark theme - Resizing cells will now resize dropdown boxes, still have to add support for moving rows/columns ### Version 4.6.5 - Begin process of adding code to maintain multiple persistent dropdown boxes - Add function `delete_dropdown()` (use argument `"all"` for all dropdown boxes) - Add function `get_dropdowns()` to return locations and info for all boxes ### Version 4.6.4 - Fix `create_dropdown()` issues ### Version 4.6.3 - Fix github release ### Version 4.6.2 - Add startup and `set_options()` arguments: ``` empty_horizontal #empty space on the x axis in pixels empty_vertical #empty space on the y axis in pixels show_horizontal_grid #grid lines for main table show_vertical_grid #grid lines for main table ``` - Fix scrollbar issues if hiding index or header - Change `C` in startup arguments to `parent` for clarity - Add Listbox using `Sheet()` recipe to tests ### Version 4.6.1 - Clean up menus - Improve Dark theme ### Version 4.6.0 - Fix bug with column drag and drop and row index drag and drop ### Version 4.5.9 - Attempt made to remove cell editor window internal border, showing up on some platforms ### Version 4.5.8 - Add function `set_text_editor_value()` - Moved internal `begin_cell_edit` code slightly - Make Alt-Return on text editor only increase text window height if too small ### Version 4.5.7 - Fix download url in `setup.py` ### Version 4.5.6 - Add `"begin_edit_cell"` to function `extra_bindings()` ### Version 4.5.5 - Fix mouse motion binding in top left code not returning event object - Add some extra demonstration code to `test_tksheet.py` ### Version 4.5.4 - Fix typo in row index code leading to text not slicing properly with center alignment ### Version 4.5.3 - Improve text redraw slicing and positioning when cell size too small, especially for center alignments, very slight performance cost - If a number of lines has been set by using a string e.g. "1" for default row or header heights then the heights will be updated when changing fonts ### Version 4.5.2 - Fix resize not taking into account header if header is set to a row in the sheet - Fix bugs with `display_subset_of_columns()`, argument `set_col_positions` does nothing until reworked or removed ### Version 4.5.1 - Improve center alignment and auto resize widths issues ### Version 4.5.0 - Make `grid_propagate()` only occur internally if both `height` and `width` arguments are used, instead of just one - Fix bug in `get_sheet_data()` - Fix mismatch with scrollbars in `show` scrollbar options ### Version 4.4.9 - Add `header_height` argument to `set_options()` - Make `header_height` arguments accept integer representing pixels as well as string representing number of lines - Make top left header height resize bar use `header_height` and not minimum height ### Version 4.4.8 - Fix readme python versions ### Version 4.4.7 - Fix readme python versions ### Version 4.4.6 - Fix Python version required ### Version 4.4.5 - Fix typo in header code ### Version 4.4.4 - Fix some issues with header and index not showing when values aren't string - Fix row index width when starting with default index and then switching later - Fix very minor issue with header text limit ### Version 4.4.3 - Fix some issues with right click insert column/row - Fix error that occurs if row is too short on edit cell or not enough rows - Add function `recreate_all_selection_boxes()` - Add function `bind_text_editor_set()` - Add function `get_text_editor_value()` ### Version 4.4.2 - Removed internal `total_rows` and `total_cols` variables, replaced with functions for better maintainability but at the cost of performance in some cases - Added `fix_data` argument to function `total_columns()` to even up all row lengths in sheet data - Removed `total_rows` and `total_cols` arguments from functions `data_reference()` and `set_sheet_data()` - Fix issues with function `total_columns()` - Redo functions `insert_column()` and `insert_row()`, hopefully they work better - Add argument `equalize_data_row_lengths` to function `insert_column()` default is `True` - Add arguments `get_index` and `get_header` to function `get_sheet_data()` defaults are `False` - Change some argument names for functions involving inserting/setting rows and columns - `display_subset_of_columns()` now tries to maintain some existing column widths - Add functions `sheet_data_dimensions()`, `sheet_display_dimensions()`, `set_sheet_data_and_display_dimensions()` - Fix bug with undo edit cell if displaying subset of columns - Fix max undos issue - `display_subset_of_columns()` now resets undo storage ### Version 4.4.1 - Potential fix for functions `headers()` and `row_index()` when `show...if_not_sheet` arguments are utilized ### Version 4.4.0 - Add argument `reset_col_positions` to function `headers()` default is `False` - Add argument `reset_row_positions` to function `row_index()` default is `False` - Add argument `show_headers_if_not_sheet` to function `headers()` default is `True` - Add argument `show_index_if_not_sheet` to function `row_index()` default is `True` - Adjust most `center` text draw positions slightly, `center` alignment should now actually be center - Improve header resizing accuracy for set to text size ### Version 4.3.9 - Add `rc_insert_column` and `rc_insert_row` strings to extra bindings - Fix errors with insert column/row that occur if data is empty list - Fix resizing row index width/header height sometimes not working - Fix select all creating event when sheet is empty - Fix `create_current()` internal error if there are columns but no rows or rows but no columns - Add argument `get_cells_as_rows` to function `get_selected_rows()` to return selected cells as if they were selected rows - Add argument `get_cells_as_columns` to function `get_selected_columns()` to return selected cells as if they were selected columns - Add argument `return_tuple` to functions `get_selected_rows()`, `get_selected_columns()`, `get_selected_cells()` ### Version 4.3.8 - Add function `set_currently_selected()` - Make `get_selected_min_max()` return `(None, None, None, None)` if nothing selected to prevent error with tuple unpacking e.g `r1, c1, r2, c2 = get_selected...` - Fix potential issue with internal function `set_col_width()` if `only_set...` argument is `True` - Fix typo in function `create_selection_box()` - Add `right_click_select` and `rc_select` to function `enable_bindings()`, is enabled as well if other right click functionality is enabled - Add `"all_select_events"` as option in function `extra_bindings()` to quickly bind all selection events in the table, including deselection to a single function ### Version 4.3.7 - Make row height resize to text include row index values - Fix bug with function `.set_all_cell_sizes_to_text()` where empty cells would result in minimum sizes - Fix typo error in function `.set_all_cell_sizes_to_text()` ### Version 4.3.6 - Add function `get_currently_selected()` - Add functions `self.sheet_demo.set_all_column_widths()`, `self.sheet_demo.set_all_row_heights()`, `self.sheet_demo.set_all_cell_sizes_to_text()` - Add argument `set_all_heights_and_widths` to startup, use `True` or `False` - Improve performance of all resizing functions - Make resizing detect text dimensions for full row/column, not just what is displayed ### Version 4.3.5 - Fix bug with `row_index()` function - Fix `headers` set to `int` at start up not working if `headers` is `0` ### Version 4.3.4 - Fix bug if `row_drag_and_drop_perform` or `column_drag_and_drop_perform` is False - Fix display issue with undo drag and drop rows/columns - Add `"unbind_all"` as possible argument for function `extra_bindings()` - Add `"enable_all"` as possible argument for function `enable_bindings()` - Add `"disable_all"` as possible argument for function `disable_bindings()` ### Version 4.3.3 - Add function `set_sheet_data()` use argument `verify = False` to prevent verification of types - Add startup arguments `row_drag_and_drop_perform` and `column_drag_and_drop_perform` and add to `set_options()` - Fix double selection with drag and drop rows - Add `data` argument to startup arguments ### Version 4.3.2 - Fix some highlighted cells not being reset when displaying subset of columns ### Version 4.3.1 - Fix highlighted cells not blending with drag selection ### Version 4.3.0 - Fix basic bindings bug ### Version 4.2.9 - Fix typo in popup menu ### Version 4.2.8 - Improve performance of drag selection - Improve `row_index()` and `headers()` functions, add examples - Add `"single_select"` and `"toggle_select"` to `enable_bindings()` ### Version 4.2.7 - Fix bug with undo deletion ### Version 4.2.6 - Fix bug with insert row - Deprecate `change_color()` use `set_options()` instead - Fix display bug where resizing row index width or header height would result in small selection boxes - Rework popup menus, use `enable_bindings()` with `"right_click_popup_menu"` to make them work again - Allow additional binding of right click to a function alongside `Sheet()` popup menus - Add function `get_sheet_data()` - Add `x_scrollbar` and `y_scrollbar` to `show()`, `hide()` and `Sheet()` startup - Removed `show` argument from startup, added `show_table`, default is `True` - Add option to automatically resize row index width when user has not set new indexes, default is `True` - Rework the top left rectangle and add select all on left click if drag selection is enabled - Change appearance of popup menus - Change popup menu color for dark theme - Fix wrong popup menu on right click with selected rows/columns ### Version 4.2.5 - Fix bug where highlighted background or foreground might not be in the correct column when displaying a subset of columns - Change colors of selected rows and columns - Add color options: ```python header_select_column_bg header_select_column_fg row_index_select_row_bg row_index_select_row_fg selected_rows_border_color selected_rows_background selected_rows_foreground selected_columns_border_color selected_columns_background selected_columns_foreground ``` - Fix various issues with displaying correct colors in certain circumstances - Change dark theme colors slightly ### Version 4.2.4 - Fix PyPi release version ### Version 4.2.3 - Fix bug with right click delete columns and undo - Right click in main table when over selected columns/rows now brings up column/row menu - Add internal cut, copy, paste, delete and undo to usable `Sheet()` functions ### Version 4.2.2 - Fix bug with center alignment and display subset of columns - Fix bug with highlighted cells showing as selected when they're not ### Version 4.2.1 - Fix resize row height bug - Fix deselect bug - Improve and fix many bugs with toggle select mode ### Version 4.2.0 - Fix extra selection box drawing in certain circumstances - Fix bug in Paste - Remove some unnessecary code ### Version 4.1.9 - Fix bugs introduced with 4.1.8 ### Version 4.1.8 - Overhaul internal selections variables and workings - Replace functions `get_min`/`get_max` `selected` `x` and `y` with `get_selected_min_max()` - Fix bugs with functions `total_columns()` and `total_rows()` and set default argument `mod_data` to `True` - Change drag and drop so that it modifies data with or without an extra binding set - Add functions `set_cell_data()`, `set_row_data()`, `set_column_data()` - Remove bloat in `get_highlighted_cells()` - Prepare many functions for implementation of control + click ### Version 4.1.7 - Fix typo in `get_cell_data()` - Add function `height_and_width()` - Fix height and width in widget startup ### Version 4.1.6 - Fix cell selection after editing cell ending with mouse click ### Version 4.1.5 - Separate drag selection `extra_bindings()` into cells, rows and columns - Add shift select and separate it from left click and right click in `extra_bindings()` - Changed and hopefully made better all responses to `extra_bindings()` functions - Added selection box to selected rows/columns ### Version 4.1.4 - Fix bugs with drag and drop rows/columns - Clean up drag and drop code ### Version 4.1.3 - Improve speed of `get_selected_cells()` - Add function `get_selection_boxes()` - Remove more unnessecary loops - Added argument `show_selected_cells_border` to start up - Added function `set_options()` - Added undo to drag and drop rows/columns - Fixed bug with drag and drop columns when displaying a subset of columns ### Version 4.1.2 - Move text up slightly inside cells - Fix selected cells border not showing sometimes - Replace some unnessecary loops ### Version 4.1.1 - Improve select all speed by 20x - Shrink cell size slightly - Added options to hide row index and header at start up - Change looks of ctrl x border - Fix minor issue where two borders would draw in the same place ### Version 4.1.0 - Added undo to insert row/column and delete rows/columns - Changed the default looks - Deprecated function `select()` and added `toggle_select_cell()`, `toggle_select_row()` and `toggle_select_column()` - Added functions `is_cell_selected()`, `is_row_selected()`, `is_column_selected()`, `get_cell_data()`, `get_row_data()` and `get_column_data()` - Changed behavior of `deselect()` function slightly to now accept a cell from arguments `row = ` and `column = ` as well as `cell = ` - Added cell selection borders and start up option `selected_cells_border_color` as well as to the function `change_color()` - Removed the main table variable `selected_cells` and reduced memory use - Fixed a bug with editing a cell while displaying a subset of columns - Renamed `RowIndexes` class to `RowIndex` - Added separate test file ### Version 3.1 - 4.0.1 - Fixed import errors ### Version 3.1: - Added `frame_background` parameter to `Sheet()` startup and `change_color()` function - Added more keys to edit cell binding - Separated `edit_bindings` for the function `enable_bindings()` into the following: - `"cut"` - `"copy"` - `"paste"` - `"delete"` - `"edit_cell"` - `"undo"` Note that the argument `"edit_bindings"` still works - Separated the tksheet classes and variables into different files ### Version 3.0: - Fixed bug with function `display_subset_of_columns()` - Fixed issues with row heights and column widths adjusting too small - Improved dark theme - Added top left rectangle to hide - Added insert and delete row and column to right click menu - Some general code improvements ### Version 2.9: - The function `display_columns()` has been changed to `display_subset_of_columns()` and some paramaters have been changed - The variables `total_rows` and `total_cols` have been reworked - Sheets can now be created with the parameters `total_rows` and `total_columns` to create blank sheets of certain dimensions - The functions `total_rows()` and `total_columns()` have been added to set or get the dimensions of the sheet - `"edit_bindings"` has been added as an input for the function `enable_bindings()` - `show()` and `hide()` have been reworked to allow hiding of the header and/or row index ### Version 2.6 - 2.9: - delete_row() is now delete_row_position() - insert_row() is now insert_row_position() - move_row() is now move_row_position() - delete_column() is now delete_column_position() - insert_column() is now insert_column_position() - move_column() is now move_column_position() - The original functions have been changed to behave as they read and actually modify the table data tksheet-7.2.12/docs/DOCUMENTATION.md000066400000000000000000007272671463432073300165670ustar00rootroot00000000000000# Table of Contents - [About tksheet](https://github.com/ragardner/tksheet/wiki/Version-7#about-tksheet) - [Installation and Requirements](https://github.com/ragardner/tksheet/wiki/Version-7#installation-and-requirements) - [Basic Initialization](https://github.com/ragardner/tksheet/wiki/Version-7#basic-initialization) - [Usage Examples](https://github.com/ragardner/tksheet/wiki/Version-7#usage-examples) - [Initialization Options](https://github.com/ragardner/tksheet/wiki/Version-7#initialization-options) --- - [Sheet Appearance](https://github.com/ragardner/tksheet/wiki/Version-7#sheet-appearance) - [Header and Index](https://github.com/ragardner/tksheet/wiki/Version-7#header-and-index) --- - [Bindings and Functionality](https://github.com/ragardner/tksheet/wiki/Version-7#bindings-and-functionality) - [tkinter and tksheet Events](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) - [Sheet Languages and Bindings](https://github.com/ragardner/tksheet/wiki/Version-7#sheet-languages-and-bindings) --- - [Span Objects](https://github.com/ragardner/tksheet/wiki/Version-7#span-objects) - [Named Spans](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans) --- - [Getting Sheet Data](https://github.com/ragardner/tksheet/wiki/Version-7#getting-sheet-data) - [Setting Sheet Data](https://github.com/ragardner/tksheet/wiki/Version-7#setting-sheet-data) --- - [Highlighting Cells](https://github.com/ragardner/tksheet/wiki/Version-7#highlighting-cells) - [Dropdown Boxes](https://github.com/ragardner/tksheet/wiki/Version-7#dropdown-boxes) - [Check Boxes](https://github.com/ragardner/tksheet/wiki/Version-7#check-boxes) - [Data Formatting](https://github.com/ragardner/tksheet/wiki/Version-7#data-formatting) - [Readonly Cells](https://github.com/ragardner/tksheet/wiki/Version-7#readonly-cells) - [Text Font and Alignment](https://github.com/ragardner/tksheet/wiki/Version-7#text-font-and-alignment) --- - [Getting Selected Cells](https://github.com/ragardner/tksheet/wiki/Version-7#getting-selected-cells) - [Modifying Selected Cells](https://github.com/ragardner/tksheet/wiki/Version-7#modifying-selected-cells) - [Row Heights and Column Widths](https://github.com/ragardner/tksheet/wiki/Version-7#row-heights-and-column-widths) - [Identifying Bound Event Mouse Position](https://github.com/ragardner/tksheet/wiki/Version-7#identifying-bound-event-mouse-position) - [Scroll Positions and Cell Visibility](https://github.com/ragardner/tksheet/wiki/Version-7#scroll-positions-and-cell-visibility) --- - [Hiding Columns](https://github.com/ragardner/tksheet/wiki/Version-7#hiding-columns) - [Hiding Rows](https://github.com/ragardner/tksheet/wiki/Version-7#hiding-rows) - [Hiding Sheet Elements](https://github.com/ragardner/tksheet/wiki/Version-7#hiding-sheet-elements) - [Sheet Height and Width](https://github.com/ragardner/tksheet/wiki/Version-7#sheet-height-and-width) --- - [Cell Text Editor](https://github.com/ragardner/tksheet/wiki/Version-7#cell-text-editor) - [Sheet Options and Other Functions](https://github.com/ragardner/tksheet/wiki/Version-7#sheet-options-and-other-functions) --- - [Treeview Mode](https://github.com/ragardner/tksheet/wiki/Version-7#treeview-mode) - [Progress Bars](https://github.com/ragardner/tksheet/wiki/Version-7#progress-bars) - [Tags](https://github.com/ragardner/tksheet/wiki/Version-7#tags) --- - [Example Loading Data from Excel](https://github.com/ragardner/tksheet/wiki/Version-7#example-loading-data-from-excel) - [Example Custom Right Click and Text Editor Validation](https://github.com/ragardner/tksheet/wiki/Version-7#example-custom-right-click-and-text-editor-validation) - [Example Displaying Selections](https://github.com/ragardner/tksheet/wiki/Version-7#example-displaying-selections) - [Example List Box](https://github.com/ragardner/tksheet/wiki/Version-7#example-list-box) - [Example Header Dropdown Boxes and Row Filtering](https://github.com/ragardner/tksheet/wiki/Version-7#example-header-dropdown-boxes-and-row-filtering) - [Example ReadMe Screenshot Code](https://github.com/ragardner/tksheet/wiki/Version-7#example-readme-screenshot-code) - [Example Saving tksheet as a csv File](https://github.com/ragardner/tksheet/wiki/Version-7#example-saving-tksheet-as-a-csv-file) - [Example Using and Creating Formatters](https://github.com/ragardner/tksheet/wiki/Version-7#example-using-and-creating-formatters) - [Contributing](https://github.com/ragardner/tksheet/wiki/Version-7#contributing) --- # **About tksheet** - `tksheet` is a Python tkinter table widget written in pure python. - It is licensed under the [MIT license](https://github.com/ragardner/tksheet/blob/master/LICENSE.txt). - It works by using tkinter canvases and moving lines, text and rectangles around for only the visible portion of the table. - If you are using a version of tksheet that is older than `7.0.0` then you will need the documentation [here](https://github.com/ragardner/tksheet/wiki/Version-6) instead. - In tksheet versions >= `7.0.2` the current version will be at the top of the file `__init__.py`. ### **Limitations** Some examples of things that are not possible with tksheet: - Cell merging - Cell text wrap - Changing font for individual cells - Different fonts for index and table - Mouse drag copy cells - Cell highlight borders - At the present time the type hinting in tksheet is only meant to serve as a guide and not to be used with type checkers. --- # **Installation and Requirements** `tksheet` is available through PyPi (Python package index) and can be installed by using Pip through the command line `pip install tksheet` ```python #To install using pip pip install tksheet #To update using pip pip install tksheet --upgrade ``` Alternatively you can download the source code and inside the tksheet directory where the `pyproject.toml` file is located use the command line `pip install -e .` - Versions **<** `7.0.0` require a Python version of **`3.7`** or higher. - Versions **>=** `7.0.0` require a Python version of **`3.8`** or higher. --- # **Basic Initialization** Like other tkinter widgets you need only the `Sheet()`s parent as an argument to initialize a `Sheet()` e.g. ```python sheet = Sheet(my_frame_widget) ``` - `my_frame_widget` would be replaced by whatever widget is your `Sheet()`s parent. ___ As an example, this is a tkinter program involving a `Sheet()` widget ```python from tksheet import Sheet import tkinter as tk class demo(tk.Tk): def __init__(self): tk.Tk.__init__(self) self.grid_columnconfigure(0, weight = 1) self.grid_rowconfigure(0, weight = 1) self.frame = tk.Frame(self) self.frame.grid_columnconfigure(0, weight = 1) self.frame.grid_rowconfigure(0, weight = 1) self.sheet = Sheet(self.frame, data = [[f"Row {r}, Column {c}\nnewline1\nnewline2" for c in range(50)] for r in range(500)]) self.sheet.enable_bindings() self.frame.grid(row = 0, column = 0, sticky = "nswe") self.sheet.grid(row = 0, column = 0, sticky = "nswe") app = demo() app.mainloop() ``` --- # **Usage Examples** This is to demonstrate some of tksheets functionality. - The functions which return the Sheet itself (have `-> Sheet`) can be chained with other Sheet functions. - The functions which return a Span (have `-> Span`) can be chained with other Span functions. ```python from tksheet import Sheet, num2alpha import tkinter as tk class demo(tk.Tk): def __init__(self): tk.Tk.__init__(self) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.frame = tk.Frame(self) self.frame.grid_columnconfigure(0, weight=1) self.frame.grid_rowconfigure(0, weight=1) # create an instance of Sheet() self.sheet = Sheet( # set the Sheets parent widget self.frame, # optional: set the Sheets data at initialization data=[[f"Row {r}, Column {c}\nnewline1\nnewline2" for c in range(20)] for r in range(100)], theme="light green", height=520, width=1000, ) # enable various bindings self.sheet.enable_bindings("all", "edit_index", "edit_header") # set a user edit validation function # AND bind all sheet modification events to a function # chained as two functions # more information at: # https://github.com/ragardner/tksheet/wiki/Version-7#validate-user-cell-edits self.sheet.edit_validation(self.validate_edits).bind("<>", self.sheet_modified) # add some new commands to the in-built right click menu # setting data self.sheet.popup_menu_add_command( "Say Hello", self.say_hello, index_menu=False, header_menu=False, empty_space_menu=False, ) # getting data self.sheet.popup_menu_add_command( "Print some data", self.print_data, empty_space_menu=False, ) # overwrite Sheet data self.sheet.popup_menu_add_command("Reset Sheet data", self.reset) # set the header self.sheet.popup_menu_add_command( "Set header data", self.set_header, table_menu=False, index_menu=False, empty_space_menu=False, ) # set the index self.sheet.popup_menu_add_command( "Set index data", self.set_index, table_menu=False, header_menu=False, empty_space_menu=False, ) self.frame.grid(row=0, column=0, sticky="nswe") self.sheet.grid(row=0, column=0, sticky="nswe") def validate_edits(self, event): # print (event) if event.eventname.endswith("header"): return event.value + " edited header" elif event.eventname.endswith("index"): return event.value + " edited index" else: if not event.value: return "EMPTY" return event.value[:3] def say_hello(self): current_selection = self.sheet.get_currently_selected() if current_selection: box = (current_selection.row, current_selection.column) # set cell data, end user Undo enabled # more information at: # https://github.com/ragardner/tksheet/wiki/Version-7#setting-sheet-data self.sheet[box].options(undo=True).data = "Hello World!" # highlight the cell for 2 seconds self.highlight_area(box) def print_data(self): for box in self.sheet.get_all_selection_boxes(): # get user selected area sheet data # more information at: # https://github.com/ragardner/tksheet/wiki/Version-7#getting-sheet-data data = self.sheet[box].data for row in data: print(row) def reset(self): # overwrites sheet data, more information at: # https://github.com/ragardner/tksheet/wiki/Version-7#setting-sheet-data self.sheet.set_sheet_data([[f"Row {r}, Column {c}\nnewline1\nnewline2" for c in range(20)] for r in range(100)]) # reset header and index self.sheet.headers([]) self.sheet.index([]) def set_header(self): self.sheet.headers( [f"Header {(letter := num2alpha(i))} - {i + 1}\nHeader {letter} 2nd line!" for i in range(20)] ) def set_index(self): self.sheet.set_index_width() self.sheet.row_index( [f"Index {(letter := num2alpha(i))} - {i + 1}\nIndex {letter} 2nd line!" for i in range(100)] ) def sheet_modified(self, event): # uncomment below if you want to take a look at the event object # print ("The sheet was modified! Event object:") # for k, v in event.items(): # print (k, ":", v) # print ("\n") # otherwise more information at: # https://github.com/ragardner/tksheet/wiki/Version-7#event-data # highlight the modified cells briefly if event.eventname.startswith("move"): for box in self.sheet.get_all_selection_boxes(): self.highlight_area(box) else: for box in event.selection_boxes: self.highlight_area(box) def highlight_area(self, box, time=800): # highlighting an area of the sheet # more information at: # https://github.com/ragardner/tksheet/wiki/Version-7#highlighting-cells self.sheet[box].bg = "indianred1" self.after(time, lambda: self.clear_highlight(box)) def clear_highlight(self, box): self.sheet[box].dehighlight() app = demo() app.mainloop() ``` --- # **Initialization Options** These are all the initialization parameters, the only required argument is the sheets `parent`, every other parameter has default arguments. ```python def __init__( parent: tk.Misc, name: str = "!sheet", show_table: bool = True, show_top_left: bool = True, show_row_index: bool = True, show_header: bool = True, show_x_scrollbar: bool = True, show_y_scrollbar: bool = True, width: int | None = None, height: int | None = None, headers: None | list[object] = None, header: None | list[object] = None, row_index: None | list[object] = None, index: None | list[object] = None, default_header: Literal["letters", "numbers", "both"] = "letters", default_row_index: Literal["letters", "numbers", "both"] = "numbers", data_reference: None | Sequence[Sequence[object]] = None, data: None | Sequence[Sequence[object]] = None, # either (start row, end row, "rows"), (start column, end column, "rows") or # (cells start row, cells start column, cells end row, cells end column, "cells") # noqa: E501 startup_select: tuple[int, int, str] | tuple[int, int, int, int, str] = None, startup_focus: bool = True, total_columns: int | None = None, total_rows: int | None = None, default_column_width: int = 120, default_header_height: str | int = "1", default_row_index_width: int = 70, default_row_height: str | int = "1", max_column_width: Literal["inf"] | float = "inf", max_row_height: Literal["inf"] | float = "inf", max_header_height: Literal["inf"] | float = "inf", max_index_width: Literal["inf"] | float = "inf", after_redraw_time_ms: int = 20, set_all_heights_and_widths: bool = False, zoom: int = 100, align: str = "w", header_align: str = "center", row_index_align: str | None = None, index_align: str = "center", displayed_columns: list[int] = [], all_columns_displayed: bool = True, displayed_rows: list[int] = [], all_rows_displayed: bool = True, to_clipboard_delimiter: str = "\t", to_clipboard_quotechar: str = '"', to_clipboard_lineterminator: str = "\n", from_clipboard_delimiters: list[str] | str = ["\t"], show_default_header_for_empty: bool = True, show_default_index_for_empty: bool = True, page_up_down_select_row: bool = True, paste_can_expand_x: bool = False, paste_can_expand_y: bool = False, paste_insert_column_limit: int | None = None, paste_insert_row_limit: int | None = None, show_dropdown_borders: bool = False, arrow_key_down_right_scroll_page: bool = False, cell_auto_resize_enabled: bool = True, auto_resize_row_index: bool | Literal["empty"] = "empty", auto_resize_columns: int | None = None, auto_resize_rows: int | None = None, set_cell_sizes_on_zoom: bool = False, font: tuple[str, int, str] = FontTuple( "Calibri", 13 if USER_OS == "darwin" else 11, "normal", ), header_font: tuple[str, int, str] = FontTuple( "Calibri", 13 if USER_OS == "darwin" else 11, "normal", ), index_font: tuple[str, int, str] = FontTuple( "Calibri", 13 if USER_OS == "darwin" else 11, "normal", ), # currently has no effect popup_menu_font: tuple[str, int, str] = FontTuple( "Calibri", 13 if USER_OS == "darwin" else 11, "normal", ), max_undos: int = 30, column_drag_and_drop_perform: bool = True, row_drag_and_drop_perform: bool = True, empty_horizontal: int = 50, empty_vertical: int = 50, selected_rows_to_end_of_window: bool = False, horizontal_grid_to_end_of_window: bool = False, vertical_grid_to_end_of_window: bool = False, show_vertical_grid: bool = True, show_horizontal_grid: bool = True, display_selected_fg_over_highlights: bool = False, show_selected_cells_border: bool = True, treeview: bool = False, treeview_indent: str | int = "3", rounded_boxes: bool = True, # colors outline_thickness: int = 0, outline_color: str = theme_light_blue["outline_color"], theme: str = "light blue", frame_bg: str = theme_light_blue["table_bg"], popup_menu_fg: str = theme_light_blue["popup_menu_fg"], popup_menu_bg: str = theme_light_blue["popup_menu_bg"], popup_menu_highlight_bg: str = theme_light_blue["popup_menu_highlight_bg"], popup_menu_highlight_fg: str = theme_light_blue["popup_menu_highlight_fg"], table_grid_fg: str = theme_light_blue["table_grid_fg"], table_bg: str = theme_light_blue["table_bg"], table_fg: str = theme_light_blue["table_fg"], table_selected_box_cells_fg: str = theme_light_blue["table_selected_box_cells_fg"], table_selected_box_rows_fg: str = theme_light_blue["table_selected_box_rows_fg"], table_selected_box_columns_fg: str = theme_light_blue["table_selected_box_columns_fg"], table_selected_cells_border_fg: str = theme_light_blue["table_selected_cells_border_fg"], table_selected_cells_bg: str = theme_light_blue["table_selected_cells_bg"], table_selected_cells_fg: str = theme_light_blue["table_selected_cells_fg"], table_selected_rows_border_fg: str = theme_light_blue["table_selected_rows_border_fg"], table_selected_rows_bg: str = theme_light_blue["table_selected_rows_bg"], table_selected_rows_fg: str = theme_light_blue["table_selected_rows_fg"], table_selected_columns_border_fg: str = theme_light_blue["table_selected_columns_border_fg"], table_selected_columns_bg: str = theme_light_blue["table_selected_columns_bg"], table_selected_columns_fg: str = theme_light_blue["table_selected_columns_fg"], resizing_line_fg: str = theme_light_blue["resizing_line_fg"], drag_and_drop_bg: str = theme_light_blue["drag_and_drop_bg"], index_bg: str = theme_light_blue["index_bg"], index_border_fg: str = theme_light_blue["index_border_fg"], index_grid_fg: str = theme_light_blue["index_grid_fg"], index_fg: str = theme_light_blue["index_fg"], index_selected_cells_bg: str = theme_light_blue["index_selected_cells_bg"], index_selected_cells_fg: str = theme_light_blue["index_selected_cells_fg"], index_selected_rows_bg: str = theme_light_blue["index_selected_rows_bg"], index_selected_rows_fg: str = theme_light_blue["index_selected_rows_fg"], index_hidden_rows_expander_bg: str = theme_light_blue["index_hidden_rows_expander_bg"], header_bg: str = theme_light_blue["header_bg"], header_border_fg: str = theme_light_blue["header_border_fg"], header_grid_fg: str = theme_light_blue["header_grid_fg"], header_fg: str = theme_light_blue["header_fg"], header_selected_cells_bg: str = theme_light_blue["header_selected_cells_bg"], header_selected_cells_fg: str = theme_light_blue["header_selected_cells_fg"], header_selected_columns_bg: str = theme_light_blue["header_selected_columns_bg"], header_selected_columns_fg: str = theme_light_blue["header_selected_columns_fg"], header_hidden_columns_expander_bg: str = theme_light_blue["header_hidden_columns_expander_bg"], top_left_bg: str = theme_light_blue["top_left_bg"], top_left_fg: str = theme_light_blue["top_left_fg"], top_left_fg_highlight: str = theme_light_blue["top_left_fg_highlight"], vertical_scroll_background: str = theme_light_blue["vertical_scroll_background"], horizontal_scroll_background: str = theme_light_blue["horizontal_scroll_background"], vertical_scroll_troughcolor: str = theme_light_blue["vertical_scroll_troughcolor"], horizontal_scroll_troughcolor: str = theme_light_blue["horizontal_scroll_troughcolor"], vertical_scroll_lightcolor: str = theme_light_blue["vertical_scroll_lightcolor"], horizontal_scroll_lightcolor: str = theme_light_blue["horizontal_scroll_lightcolor"], vertical_scroll_darkcolor: str = theme_light_blue["vertical_scroll_darkcolor"], horizontal_scroll_darkcolor: str = theme_light_blue["horizontal_scroll_darkcolor"], vertical_scroll_relief: str = theme_light_blue["vertical_scroll_relief"], horizontal_scroll_relief: str = theme_light_blue["horizontal_scroll_relief"], vertical_scroll_troughrelief: str = theme_light_blue["vertical_scroll_troughrelief"], horizontal_scroll_troughrelief: str = theme_light_blue["horizontal_scroll_troughrelief"], vertical_scroll_bordercolor: str = theme_light_blue["vertical_scroll_bordercolor"], horizontal_scroll_bordercolor: str = theme_light_blue["horizontal_scroll_bordercolor"], vertical_scroll_borderwidth: int = 1, horizontal_scroll_borderwidth: int = 1, vertical_scroll_gripcount: int = 0, horizontal_scroll_gripcount: int = 0, vertical_scroll_active_bg: str = theme_light_blue["vertical_scroll_active_bg"], horizontal_scroll_active_bg: str = theme_light_blue["horizontal_scroll_active_bg"], vertical_scroll_not_active_bg: str = theme_light_blue["vertical_scroll_not_active_bg"], horizontal_scroll_not_active_bg: str = theme_light_blue["horizontal_scroll_not_active_bg"], vertical_scroll_pressed_bg: str = theme_light_blue["vertical_scroll_pressed_bg"], horizontal_scroll_pressed_bg: str = theme_light_blue["horizontal_scroll_pressed_bg"], vertical_scroll_active_fg: str = theme_light_blue["vertical_scroll_active_fg"], horizontal_scroll_active_fg: str = theme_light_blue["horizontal_scroll_active_fg"], vertical_scroll_not_active_fg: str = theme_light_blue["vertical_scroll_not_active_fg"], horizontal_scroll_not_active_fg: str = theme_light_blue["horizontal_scroll_not_active_fg"], vertical_scroll_pressed_fg: str = theme_light_blue["vertical_scroll_pressed_fg"], horizontal_scroll_pressed_fg: str = theme_light_blue["horizontal_scroll_pressed_fg"], scrollbar_theme_inheritance: str = "default", scrollbar_show_arrows: bool = True, # changing the arrowsize (width) of the scrollbars # is not working with 'default' theme # use 'clam' theme instead if you want to change the width vertical_scroll_arrowsize: str | int = "", horizontal_scroll_arrowsize: str | int = "", ) -> None ``` - `name` setting a name for the sheet is useful when you have multiple sheets and you need to determine which one an event came from. - `auto_resize_columns` (`int`, `None`) if set as an `int` the columns will automatically resize to fit the width of the window, the `int` value being the minimum of each column in pixels. - `auto_resize_rows` (`int`, `None`) if set as an `int` the rows will automatically resize to fit the height of the window, the `int` value being the minimum height of each row in pixels. - `startup_select` selects cells, rows or columns at initialization by using a `tuple` e.g. `(0, 0, "cells")` for cell A0 or `(0, 5, "rows")` for rows 0 to 5. - `data_reference` and `data` are essentially the same. - `row_index` and `index` are the same, `index` takes priority, same as with `headers` and `header`. - `startup_select` either `(start row, end row, "rows")`, `(start column, end column, "rows")` or `(start row, start column, end row, end column, "cells")`. The start/end row/column variables need to be `int`s. - `auto_resize_row_index` either `True`, `False` or `"empty"`. - `"empty"` it will only automatically resize if the row index is empty. - `True` it will always automatically resize. - `False` it will never automatically resize. - If `show_selected_cells_border` is `False` then the colors for `table_selected_box_cells_fg`/`table_selected_box_rows_fg`/`table_selected_box_columns_fg` will be used for the currently selected cells background. You can change most of these settings after initialization using the [`set_options()` function](https://github.com/ragardner/tksheet/wiki/Version-7#sheet-options-and-other-functions). - `scrollbar_theme_inheritance` and `scrollbar_show_arrows` will only work on `Sheet()` initialization, not with `set_options()` --- # **Sheet Appearance** ### **Sheet Colors** To change the colors of individual cells, rows or columns use the functions listed under [highlighting cells](https://github.com/ragardner/tksheet/wiki/Version-7#highlighting-cells). For the colors of specific parts of the table such as gridlines and backgrounds use the function `set_options()`, keyword arguments specific to sheet colors are listed below. All the other `set_options()` arguments can be found [here](https://github.com/ragardner/tksheet/wiki/Version-7#sheet-options-and-other-functions). Use a tkinter color or a hex string e.g. ```python my_sheet_widget.set_options(table_bg="black") my_sheet_widget.set_options(table_bg="#000000") my_sheet_widget.set_options(horizontal_scroll_pressed_bg="red") ``` #### **Set options** ```python set_options( top_left_bg top_left_fg top_left_fg_highlight table_bg table_grid_fg table_fg table_selected_box_cells_fg table_selected_box_rows_fg table_selected_box_columns_fg table_selected_cells_border_fg table_selected_cells_bg table_selected_cells_fg table_selected_rows_border_fg table_selected_rows_bg table_selected_rows_fg table_selected_columns_border_fg table_selected_columns_bg table_selected_columns_fg header_bg header_border_fg header_grid_fg header_fg header_selected_cells_bg header_selected_cells_fg header_selected_columns_bg header_selected_columns_fg index_bg index_border_fg index_grid_fg index_fg index_selected_cells_bg index_selected_cells_fg index_selected_rows_bg index_selected_rows_fg resizing_line_fg drag_and_drop_bg outline_thickness outline_color frame_bg popup_menu_font popup_menu_fg popup_menu_bg popup_menu_highlight_bg popup_menu_highlight_fg # scroll bars vertical_scroll_background horizontal_scroll_background vertical_scroll_troughcolor horizontal_scroll_troughcolor vertical_scroll_lightcolor horizontal_scroll_lightcolor vertical_scroll_darkcolor horizontal_scroll_darkcolor vertical_scroll_bordercolor horizontal_scroll_bordercolor vertical_scroll_active_bg horizontal_scroll_active_bg vertical_scroll_not_active_bg horizontal_scroll_not_active_bg vertical_scroll_pressed_bg horizontal_scroll_pressed_bg vertical_scroll_active_fg horizontal_scroll_active_fg vertical_scroll_not_active_fg horizontal_scroll_not_active_fg vertical_scroll_pressed_fg horizontal_scroll_pressed_fg ) ``` Otherwise you can change the theme using the below function. ```python change_theme(theme: str = "light blue", redraw: bool = True) -> Sheet ``` - `theme` (`str`) options (themes) are currently `"light blue"`, `"light green"`, `"dark"`, `"black"`, `"dark blue"` and `"dark green"`. ### **Scrollbar Appearance** **Scrollbar colors:** The above [function and keyword arguments](https://github.com/ragardner/tksheet/wiki/Version-7#set-options) can be used to change the colors of the scroll bars. **Scrollbar relief, size, arrows, etc.** Some scroll bar style options can only be changed on `Sheet()` initialization, others can be changed whenever using `set_options()`. - Options that can only be set in the `= Sheet(...)` initialization: - `scrollbar_theme_inheritance: str = "default"` - This is which tkinter theme to inherit the new style from, changing the width of the scroll bar might not work with the `"default"` theme. If this is the case try using `"clam"` instead. - `scrollbar_show_arrows: bool` - When `False` the scroll bars arrow buttons on either end will be hidden, this may effect the width of the scroll bar. - Options that can be set using `set_options()` also: - `vertical_scroll_borderwidth: int` - `horizontal_scroll_borderwidth: int` - `vertical_scroll_gripcount: int` - `horizontal_scroll_gripcount: int` - `vertical_scroll_arrowsize: str | int` - `horizontal_scroll_arrowsize: str | int` --- # **Header and Index** #### **Set the header** ```python set_header_data(value: object, c: int | None | Iterator = None, redraw: bool = True) -> Sheet ``` - `value` (`iterable`, `int`, `Any`) if `c` is left as `None` then it attempts to set the whole header as the `value` (converting a generator to a list). If `value` is `int` it sets the header to display the row with that position. - `c` (`iterable`, `int`, `None`) if both `value` and `c` are iterables it assumes `c` is an iterable of positions and `value` is an iterable of values and attempts to set each value to each position. If `c` is `int` it attempts to set the value at that position. ```python headers( newheaders: object = None, index: None | int = None, reset_col_positions: bool = False, show_headers_if_not_sheet: bool = True, redraw: bool = True, ) -> object ``` - Using an integer `int` for argument `newheaders` makes the sheet use that row as a header e.g. `headers(0)` means the first row will be used as a header (the first row will not be hidden in the sheet though), this is sort of equivalent to freezing the row. - Leaving `newheaders` as `None` and using the `index` argument returns the existing header value in that index. - Leaving all arguments as default e.g. `headers()` returns existing headers. ___ #### **Set the index** ```python set_index_data(value: object, r: int | None | Iterator = None, redraw: bool = True) -> Sheet ``` - `value` (`iterable`, `int`, `Any`) if `r` is left as `None` then it attempts to set the whole index as the `value` (converting a generator to a list). If `value` is `int` it sets the index to display the row with that position. - `r` (`iterable`, `int`, `None`) if both `value` and `r` are iterables it assumes `r` is an iterable of positions and `value` is an iterable of values and attempts to set each value to each position. If `r` is `int` it attempts to set the value at that position. ```python row_index( newindex: object = None, index: None | int = None, reset_row_positions: bool = False, show_index_if_not_sheet: bool = True, redraw: bool = True, ) -> object ``` - Using an integer `int` for argument `newindex` makes the sheet use that column as an index e.g. `row_index(0)` means the first column will be used as an index (the first column will not be hidden in the sheet though), this is sort of equivalent to freezing the column. - Leaving `newindex` as `None` and using the `index` argument returns the existing row index value in that index. - Leaving all arguments as default e.g. `row_index()` returns the existing row index. --- # **Bindings and Functionality** #### **Enable table functionality and bindings** ```python enable_bindings(*bindings) ``` - `bindings` (`str`) options are (rc stands for right click): - `"all"` - `"single_select"` - `"toggle_select"` - `"drag_select"` - `"select_all"` - `"column_drag_and_drop"` / `"move_columns"` - `"row_drag_and_drop"` / `"move_rows"` - `"column_select"` - `"row_select"` - `"column_width_resize"` - `"double_click_column_resize"` - `"row_width_resize"` - `"column_height_resize"` - `"arrowkeys"` # all arrowkeys including page up and down - `"up"` - `"down"` - `"left"` - `"right"` - `"prior"` # page up - `"next"` # page down - `"row_height_resize"` - `"double_click_row_resize"` - `"right_click_popup_menu"` - `"rc_select"` - `"rc_insert_column"` - `"rc_delete_column"` - `"rc_insert_row"` - `"rc_delete_row"` - `"ctrl_click_select"` / `"ctrl_select"` - `"copy"` - `"cut"` - `"paste"` - `"delete"` - `"undo"` - `"edit_cell"` - `"edit_header"` - `"edit_index"` Notes: - You can change the Sheets key bindings for functionality such as copy, paste, up, down etc. Instructions can be found [here](https://github.com/ragardner/tksheet/wiki/Version-7#changing-key-bindings). - Control selection is **NOT** enabled with `"all"` and has to be specifically enabled. - Header cell editing is **NOT** enabled with `"all"` and has to be specifically enabled. - Index cell editing is **NOT** enabled with `"all"` and has to be specifically enabled. - To allow table expansion when pasting data which doesn't fit in the table use either: - `paste_can_expand_x=True`, `paste_can_expand_y=True` in sheet initialization arguments or the same keyword arguments with the function `set_options()`. Example: - `sheet.enable_bindings()` to enable absolutely everything. ___ #### **Disable table functionality and bindings** ```python disable_bindings(*bindings) ``` Notes: - Uses the same arguments as `enable_bindings()`. ___ #### **Bind specific table functionality** This function allows you to bind very specific table functionality to your own functions. - If you want less specificity in event names you can also bind all sheet modifying events to a single function, [see here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events). - If you want to validate/modify user cell edits [see here](https://github.com/ragardner/tksheet/wiki/Version-7#validate-user-cell-edits). ```python extra_bindings( bindings: str | list | tuple, func: Callable | None = None, ) -> Sheet ``` Notes: - There are several ways to use this function: - `bindings` as a `str` and `func` as either `None` or a function. Using `None` as an argument for `func` will effectively unbind the function. - `extra_bindings("edit_cell", func=my_function)` - `bindings` as an `iterable` of `str`s and `func` as either `None` or a function. Using `None` as an argument for `func` will effectively unbind the function. - `extra_bindings(["all_select_events", "copy", "cut"], func=my_function)` - `bindings` as an `iterable` of `list`s or `tuple`s with length of two, e.g. - `extra_bindings([(binding, function), (binding, function), ...])` In this example you could also use `None` in the place of `function` to unbind the binding. - In this case the arg `func` is totally ignored. - For `"end_..."` events the bound function is run before the value is set. - **To unbind** a function either set `func` argument to `None` or leave it as default e.g. `extra_bindings("begin_copy")` to unbind `"begin_copy"`. Parameters: - `bindings` (`str`) options are: - `"begin_copy", "begin_ctrl_c"` - `"ctrl_c", "end_copy", "end_ctrl_c", "copy"` - `"begin_cut", "begin_ctrl_x"` - `"ctrl_x", "end_cut", "end_ctrl_x", "cut"` - `"begin_paste", "begin_ctrl_v"` - `"ctrl_v", "end_paste", "end_ctrl_v", "paste"` - `"begin_undo", "begin_ctrl_z"` - `"ctrl_z", "end_undo", "end_ctrl_z", "undo"` - `"begin_delete_key", "begin_delete"` - `"delete_key", "end_delete", "end_delete_key", "delete"` - `"begin_edit_cell", "begin_edit_table"` - `"end_edit_cell", "edit_cell", "edit_table"` - `"begin_edit_header"` - `"end_edit_header", "edit_header"` - `"begin_edit_index"` - `"end_edit_index", "edit_index"` - `"begin_row_index_drag_drop", "begin_move_rows"` - `"row_index_drag_drop", "move_rows", "end_move_rows", "end_row_index_drag_drop"` - `"begin_column_header_drag_drop", "begin_move_columns"` - `"column_header_drag_drop", "move_columns", "end_move_columns", "end_column_header_drag_drop"` - `"begin_rc_delete_row", "begin_delete_rows"` - `"rc_delete_row", "end_rc_delete_row", "end_delete_rows", "delete_rows"` - `"begin_rc_delete_column", "begin_delete_columns"` - `"rc_delete_column", "end_rc_delete_column","end_delete_columns", "delete_columns"` - `"begin_rc_insert_column", "begin_insert_column", "begin_insert_columns", "begin_add_column","begin_rc_add_column", "begin_add_columns"` - `"rc_insert_column", "end_rc_insert_column", "end_insert_column", "end_insert_columns", "rc_add_column", "end_rc_add_column", "end_add_column", "end_add_columns"` - `"begin_rc_insert_row", "begin_insert_row", "begin_insert_rows", "begin_rc_add_row", "begin_add_row", "begin_add_rows"` - `"rc_insert_row", "end_rc_insert_row", "end_insert_row", "end_insert_rows", "rc_add_row", "end_rc_add_row", "end_add_row", "end_add_rows"` - `"row_height_resize"` - `"column_width_resize"` - `"cell_select"` - `"select_all"` - `"row_select"` - `"column_select"` - `"drag_select_cells"` - `"drag_select_rows"` - `"drag_select_columns"` - `"shift_cell_select"` - `"shift_row_select"` - `"shift_column_select"` - `"deselect"` - `"all_select_events", "select", "selectevents", "select_events"` - `"all_modified_events", "sheetmodified", "sheet_modified" "modified_events", "modified"` - `"bind_all"` - `"unbind_all"` - `func` argument is the function you want to send the binding event to. - Using one of the following `"all_modified_events"`, `"sheetmodified"`, `"sheet_modified"`, `"modified_events"`, `"modified"` will make any insert, delete or cell edit including pastes and undos send an event to your function. - For events `"begin_move_columns"`/`"begin_move_rows"` the point where columns/rows will be moved to will be accessible by the key named `"value"`. - For `"begin_edit..."` events the bound function must return a value to open the cell editor with, example [here](https://github.com/ragardner/tksheet/wiki/Version-7#example-custom-right-click-and-text-editor-validation). #### **Event Data** Using `extra_bindings()` the function you bind needs to have at least one argument which will receive a `dict`. The values of which can be accessed by dot notation e.g. `event.eventname` or `event.cells.table`: ```python for (row, column), old_value in event.cells.table.items(): print (f"R{row}", f"C{column}", "Old Value:", old_value) ``` It has the following layout and keys: ```python { "eventname": "", "sheetname": "", "cells": { "table": {}, "header": {}, "index": {}, }, "moved": { "rows": {}, "columns": {}, }, "added": { "rows": {}, "columns": {}, }, "deleted": { "rows": {}, "columns": {}, "header": {}, "index": {}, "column_widths": {}, "row_heights": {}, "options": {}, "displayed_columns": None, "displayed_rows": None, }, "named_spans": {}, "selection_boxes": {}, "selected": tuple(), "being_selected": tuple(), "data": [], "key": "", "value": None, "loc": tuple(), "row": None, "column": None, "resized": { "rows": {}, "columns": {}, }, "widget": None, } ``` Keys: - Key **`["eventname"]`** will be one of the following: - `"begin_ctrl_c"` - `"end_ctrl_c"` - `"begin_ctrl_x"` - `"end_ctrl_x"` - `"begin_ctrl_v"` - `"end_ctrl_v"` - `"begin_delete"` - `"end_delete"` - `"begin_undo"` - `"end_undo"` - `"begin_add_columns"` - `"end_add_columns"` - `"begin_add_rows"` - `"end_add_rows"` - `"begin_delete_columns"` - `"end_delete_columns"` - `"begin_delete_rows"` - `"end_delete_rows"` - `"begin_edit_table"` - `"end_edit_table"` - `"begin_edit_index"` - `"end_edit_index"` - `"begin_edit_header"` - `"end_edit_header"` - `"begin_move_rows"` - `"end_move_rows"` - `"begin_move_columns"` - `"end_move_columns"` - `"select"` - `"resize"` - For events `"begin_move_columns"`/`"begin_move_rows"` the point where columns/rows will be moved to will be under the `event_data` key `"value"`. - Key **`["sheetname"]`** is the [name given to the sheet widget on initialization](https://github.com/ragardner/tksheet/wiki/Version-7#initialization-options), useful if you have multiple sheets to determine which one emitted the event. - Key **`["cells"]["table"]`** if any table cells have been modified by cut, paste, delete, cell editors, dropdown boxes, check boxes, undo or redo this will be a `dict` with `tuple` keys of `(data row index: int, data column index: int)` and the values will be the cell values at that location **prior** to the change. The `dict` will be empty if no such changes have taken place. - Key **`["cells"]["header"]`** if any header cells have been modified by cell editors, dropdown boxes, check boxes, undo or redo this will be a `dict` with keys of `int: data column index` and the values will be the cell values at that location **prior** to the change. The `dict` will be empty if no such changes have taken place. - Key **`["cells"]["index"]`** if any index cells have been modified by cell editors, dropdown boxes, check boxes, undo or redo this will be a `dict` with keys of `int: data row index` and the values will be the cell values at that location **prior** to the change. The `dict` will be empty if no such changes have taken place. - Key **`["moved"]["rows"]`** if any rows have been moved by dragging and dropping or undoing/redoing of dragging and dropping rows this will be a `dict` with the following keys: - `{"data": {old data index: new data index, ...}, "displayed": {old displayed index: new displayed index, ...}}` - `"data"` will be a `dict` where the keys are the old data indexes of the rows and the values are the data indexes they have moved to. - `"displayed"` will be a `dict` where the keys are the old displayed indexes of the rows and the values are the displayed indexes they have moved to. - If no rows have been moved the `dict` under `["moved"]["rows"]` will be empty. - Note that if there are hidden rows the values for `"data"` will include all currently displayed row indexes and their new locations. If required and available, the values under `"displayed"` include only the directly moved rows, convert to data indexes using `Sheet.data_r()`. - For events `"begin_move_rows"` the point where rows will be moved to will be under the `event_data` key `"value"`. - Key **`["moved"]["columns"]`** if any columns have been moved by dragging and dropping or undoing/redoing of dragging and dropping columns this will be a `dict` with the following keys: - `{"data": {old data index: new data index, ...}, "displayed": {old displayed index: new displayed index, ...}}` - `"data"` will be a `dict` where the keys are the old data indexes of the columns and the values are the data indexes they have moved to. - `"displayed"` will be a `dict` where the keys are the old displayed indexes of the columns and the values are the displayed indexes they have moved to. - If no columns have been moved the `dict` under `["moved"]["columns"]` will be empty. - Note that if there are hidden columns the values for `"data"` will include all currently displayed column indexes and their new locations. If required and available, the values under `"displayed"` include only the directly moved columns, convert to data indexes using `Sheet.data_c()`. - For events `"begin_move_columns"` the point where columns will be moved to will be under the `event_data` key `"value"`. - Key **`["added"]["rows"]`** if any rows have been added by the inbuilt popup menu insert rows or by a paste which expands the sheet then this will be a `dict` with the following keys: - `{"data_index": int, "displayed_index": int, "num": int, "displayed": []}` - `"data_index"` is an `int` representing the row where the rows were added in the data. - `"displayed_index"` is an `int` representing the displayed table index where the rows were added (which will be different from the data index if there are hidden rows). - `"displayed"` is simply a copied list of the `Sheet()`s displayed rows immediately prior to the change. - If no rows have been added the `dict` will be empty. - Key **`["added"]["columns"]`** if any columns have been added by the inbuilt popup menu insert columns or by a paste which expands the sheet then this will be a `dict` with the following keys: - `{"data_index": int, "displayed_index": int, "num": int, "displayed": []}` - `"data_index"` is an `int` representing the column where the columns were added in the data. - `"displayed_index"` is an `int` representing the displayed table index where the columns were added (which will be different from the data index if there are hidden columns). - `"displayed"` is simply a copied list of the `Sheet()`s displayed columns immediately prior to the change. - If no columns have been added the `dict` will be empty. - Key **`["deleted"]["columns"]`** if any columns have been deleted by the inbuilt popup menu delete columns or by undoing a paste which added columns then this will be a `dict`. This `dict` will look like the following: - `{[column data index]: {[row data index]: cell value, [row data index]: cell value}, [column data index]: {...} ...}` - If no columns have been deleted then the `dict` value for `["deleted"]["columns"]` will be empty. - Key **`["deleted"]["rows"]`** if any rows have been deleted by the inbuilt popup menu delete rows or by undoing a paste which added rows then this will be a `dict`. This `dict` will look like the following: - `{[row data index]: {[column data index]: cell value, [column data index]: cell value}, [row data index]: {...} ...}` - If no rows have been deleted then the `dict` value for `["deleted"]["rows"]` will be empty. - Key **`["deleted"]["header"]`** if any header values have been deleted by the inbuilt popup menu delete columns or by undoing a paste which added columns and header values then this will be a `dict`. This `dict` will look like the following: - `{[column data index]: header cell value, [column data index]: header cell value, ...}` - If no columns have been deleted by the mentioned methods then the `dict` value for `["deleted"]["header"]` will be empty. - Key **`["deleted"]["index"]`** if any index values have been deleted by the inbuilt popup menu delete rows or by undoing a paste which added rows and index values then this will be a `dict`. This `dict` will look like the following: - `{[row data index]: index cell value, [row data index]: index cell value, ...}` - If no index values have been deleted by the mentioned methods then the `dict` value for `["deleted"]["index"]` will be empty. - Key **`["deleted"]["column_widths"]`** if any columns have been deleted by the inbuilt popup menu delete columns or by undoing a paste which added columns then this will be a `dict`. This `dict` will look like the following: - `{[column data index]: column width, [column data index]: column width, ...}` - If no columns have been deleted then the `dict` value for `["deleted"]["column_widths"]` will be empty. - Key **`["deleted"]["row_heights"]`** if any rows have been deleted by the inbuilt popup menu delete rows or by undoing a paste which added rows then this will be a `dict`. This `dict` will look like the following: - `{[row data index]: row height, [row data index]: row height, ...}` - If no rows have been deleted then the `dict` value for `["deleted"]["row_heights"]` will be empty. - Key **`["deleted"]["displayed_columns"]`** if any columns have been deleted by the inbuilt popup menu delete columns or by undoing a paste which added columns then this will be a `list`. This `list` stores the displayed columns (the columns that are showing when others are hidden) immediately prior to the change. - Key **`["deleted"]["displayed_rows"]`** if any rows have been deleted by the inbuilt popup menu delete rows or by undoing a paste which added rows then this will be a `list`. This `list` stores the displayed rows (the rows that are showing when others are hidden) immediately prior to the change. - Key **`["named_spans"]`** This `dict` serves as storage for the `Sheet()`s named spans. Each value in the `dict` is a pickled `span` object. - Key **`["options"]`** This serves as storage for the `Sheet()`s options such as highlights, formatting, alignments, dropdown boxes, check boxes etc. It is a Python pickled `dict` where the values are the sheets internal cell/row/column options `dicts`. - Key **`["selection_boxes"]`** the value of this is all selection boxes on the sheet in the form of a `dict` as shown below: - For every event except `"select"` events the selection boxes are those immediately prior to the modification, for `"select"` events they are the current selection boxes. - The layout is always: `"selection_boxes": {(start row, start column, up to but not including row, up to but not including column): selection box type}`. - The row/column indexes are `int`s and the selection box type is a `str` either `"cells"`, `"rows"` or `"columns"`. - The `dict` will be empty if there is nothing selected. - Key **`["selected"]`** the value of this when there is something selected on the sheet is a `namedtuple`. The values of which can be found [here](https://github.com/ragardner/tksheet/wiki/Version-7#get-the-currently-selected-cell). - When nothing is selected or the event is not relevant to the currently selected box, such as a resize event it will be an empty `tuple`. - Key **`["being_selected"]`** if any selection box is in the process of being drawn by holding down mouse button 1 and dragging then this will be a tuple with the following layout: - `(start row, start column, up to but not including row, up to but not including column, selection box type)`. - The selection box type is a `str` either `"cells"`, `"rows"` or `"columns"`. - If no box is in the process of being created then this will be a an empty `tuple`. - [See here](https://github.com/ragardner/tksheet/wiki/Version-7#example-displaying-selections) for an example. - Key **`["data"]`** is primarily used for `paste` and it will contain the pasted data if any. - Key **`["key"]`** - `str` - is primarily used for cell edit events where a key press has occurred. For `"begin_edit..."` events the value is the actual key which was pressed (or `"??"` for using the mouse to open a cell). It also might be one of the following for end edit events: - `"Return"` - enter key. - `"FocusOut"` - the editor or box lost focus, perhaps by mouse clicking elsewhere. - `"Tab"` - tab key. - Key **`["value"]`** is used primarily by cell editing events. For `"begin_edit..."` events it's the value displayed in he text editor when it opens. For `"end_edit..."` events it's the value in the text editor when it was closed, for example by hitting `Return`. It also used by `"begin_move_columns"`/`"begin_move_rows"` - the point where columns/rows will be moved to will be under the `event_data` key `"value"`. - Key **`["loc"]`** is for cell editing events to show the displayed (not data) coordinates of the event. It will be **either:** - A tuple of `(int displayed row index, int displayed column index)` in the case of editing table cells. - A single `int` in the case of editing index/header cells. - Key **`["row"]`** is for cell editing events to show the displayed (not data) row number `int` of the event. If the event was not a cell editing event or a header cell was edited the value will be `None`. - Key **`["column"]`** is for cell editing events to show the displayed (not data) column number `int` of the event. If the event was not a cell editing event or an index cell was edited the value will be `None`. - Key **`["resized"]["rows"]`** is for row height resizing events, it will be a `dict` with the following layout: - `{int displayed row index: {"old_size": old_height, "new_size": new_height}}`. - If no rows have been resized then the value for `["resized"]["rows"]` will be an empty `dict`. - Key **`["resized"]["columns"]`** is for column width resizing events, it will be a `dict` with the following layout: - `{int displayed column index: {"old_size": old_width, "new_size": new_width}}`. - If no columns have been resized then the value for `["resized"]["columns"]` will be an empty `dict`. - Key **`["widget"]`** will contain the widget which emitted the event, either the `MainTable()`, `ColumnHeaders()` or `RowIndex()` which are all `tk.Canvas` widgets. ___ #### **Validate user cell edits** With this function you can validate or modify most user sheet edits, includes cut, paste, delete (including column/row clear), dropdown boxes and cell edits. ```python edit_validation(func: Callable | None = None) -> Sheet ``` Parameters: - `func` (`Callable`, `None`) must either be a function which will receive a tksheet event dict which looks like [this](https://github.com/ragardner/tksheet/wiki/Version-7#event-data) or `None` which unbinds the function. Notes: - If your bound function returns `None` then that specific cell edit will not be performed. - For examples of this function see [here](https://github.com/ragardner/tksheet/wiki/Version-7#usage-examples) and [here](https://github.com/ragardner/tksheet/wiki/Version-7#example-custom-right-click-and-text-editor-validation). ___ #### **Add commands to the in-built right click popup menu** ```python popup_menu_add_command( label: str, func: Callable, table_menu: bool = True, index_menu: bool = True, header_menu: bool = True, empty_space_menu: bool = True, ) -> Sheet ``` ___ #### **Remove commands added using popup_menu_add_command from the in-built right click popup menu** ```python popup_menu_del_command(label: str | None = None) -> Sheet ``` - If `label` is `None` then it removes all. ___ #### **Enable or disable mousewheel, left click etc** ```python basic_bindings(enable: bool = False) -> Sheet ``` ___ These functions are links to the Sheets own functionality. Functions such as `cut()` rely on whatever is currently selected on the Sheet. ```python cut(event: object = None) -> Sheet copy(event: object = None) -> Sheet paste(event: object = None) -> Sheet delete(event: object = None) -> Sheet undo(event: object = None) -> Sheet redo(event: object = None) -> Sheet zoom_in() -> Sheet zoom_out() -> Sheet ``` ___ #### **Get the last event data dict** ```python @property def event() -> EventDataDict ``` - e.g. `last_event_data = sheet.event` - Will be empty `EventDataDict` if there is no last event. ___ #### **Set focus to the Sheet** ```python focus_set( canvas: Literal[ "table", "header", "row_index", "index", "topleft", "top_left", ] = "table", ) -> Sheet ``` --- # **tkinter and tksheet Events** ### **Sheet bind** - With the `Sheet.bind()` function you can bind things in the usual way you would in tkinter and they will bind to all the `tksheet` canvases. - There are also the following special `tksheet` events you can bind: | Binding | Usable with `Sheet.event_generate()` | | -------- | ------- | | `"<>"` | - | | `"<>"` | - | | `"<>"` | - | | `"<>"` | X | | `"<>"` | X | | `"<>"` | X | | `"<>"` | X | | `"<>"` | X | | `"<>"` | X | | `"<>"` | X | ```python bind( event: str, func: Callable, add: str | None = None, ) ``` Parameters: - `add` may or may not work for various bindings depending on whether they are already in use by `tksheet`. - **Note** that while a bound event after a paste/undo/redo might have the event name `"edit_table"` it also might have added/deleted rows/columns, refer to the docs on the event data `dict` for more information. - `event` the emitted events are: - `"<>"` emitted whenever the sheet was modified by the end user by editing cells or adding or deleting rows/columns. The function you bind to this event must be able to receive a `dict` argument which will be the same as [the event data dict](https://github.com/ragardner/tksheet/wiki/Version-7#event-data) but with less specific event names. The possible event names are listed below: - `"edit_table"` when a user has cut, paste, delete or any cell edits including using dropdown boxes etc. in the table. - `"edit_index"` when a user has edited a index cell. - `"edit_header"` when a user has edited a header cell. - `"add_columns"` when a user has inserted columns. - `"add_rows"` when a user has inserted rows. - `"delete_columns"` when a user has deleted columns. - `"delete_rows"` when a user has deleted rows. - `"move_columns"` when a user has dragged and dropped columns. - `"move_rows"` when a user has dragged and dropped rows. - `"<>"` emitted whenever the sheet GUI was refreshed (redrawn). The data for this event will be different than the usual event data, it is simply: - `{"sheetname": name of your sheet, "header": bool True if the header was redrawn, "row_index": bool True if the index was redrawn, "table": bool True if the the table was redrawn}` - `"<>"` encompasses all select events and emits the same event as `"<>"` but with the event name: `"select"`. - `"<>"` emitted when a Sheet copy e.g. `` was performed and will have the `eventname` `"copy"`. - `"<>"` - `"<>"` - `"<>"` emitted when a Sheet delete key function was performed. - `"<>"` - `"<>"` - `"<>"` Example: ```python # self.sheet_was_modified is your function self.sheet.bind("<>", self.sheet_was_modified) ``` Example for `event_generate()`: ```python self.sheet.event_generate("<>") ``` - Tells the sheet to run its copy function. ___ ### **Sheet unbind** With this function you can unbind things you have bound using the `bind()` function. ```python unbind(binding: str) -> Sheet ``` --- # **Sheet Languages and Bindings** In this section are instructions to change some of tksheets in-built language and bindings: - The in-built right click menu. - The in-built functionality keybindings, such as copy, paste etc. #### **Changing right click menu labels** You can change the labels for tksheets in-built right click popup menu by using the [`set_options()` function](https://github.com/ragardner/tksheet/wiki/Version-7#sheet-options-and-other-functions) with any of the following keyword arguments: ```python edit_header_label edit_header_accelerator edit_index_label edit_index_accelerator edit_cell_label edit_cell_accelerator cut_label cut_accelerator cut_contents_label cut_contents_accelerator copy_label copy_accelerator copy_contents_label copy_contents_accelerator paste_label paste_accelerator delete_label delete_accelerator clear_contents_label clear_contents_accelerator delete_columns_label delete_columns_accelerator insert_columns_left_label insert_columns_left_accelerator insert_column_label insert_column_accelerator insert_columns_right_label insert_columns_right_accelerator delete_rows_label delete_rows_accelerator insert_rows_above_label insert_rows_above_accelerator insert_rows_below_label insert_rows_below_accelerator insert_row_label insert_row_accelerator select_all_label select_all_accelerator undo_label undo_accelerator ``` Example: ```python # changing the copy label to the spanish for Copy sheet.set_options(copy_label="Copiar") ``` #### **Changing key bindings** You can change the bindings for tksheets in-built functionality such as cut, copy, paste by using the [`set_options()` function](https://github.com/ragardner/tksheet/wiki/Version-7#sheet-options-and-other-functions) with any the following keyword arguments: ```python copy_bindings cut_bindings paste_bindings undo_bindings redo_bindings delete_bindings select_all_bindings tab_bindings up_bindings right_bindings down_bindings left_bindings prior_bindings next_bindings ``` The argument must be a `list` of **tkinter** binding `str`s. In the below example the binding for copy is changed to `""` and `""`. ```python # changing the binding for copy sheet.set_options(copy_bindings=["", ""]) ``` The default values for these bindings can be found in the tksheet file `sheet_options.py`. #### **Key bindings for other languages** There is limited support in tkinter for keybindings in languages other than english, for example tkinters `.bind()` function doesn't cooperate with cyrillic characters. There are ways around this however, see below for a limited example of how this might be achieved: ```python from __future__ import annotations import tkinter as tk from tksheet import Sheet class demo(tk.Tk): def __init__(self) -> None: tk.Tk.__init__(self) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.sheet = Sheet( parent=self, data=[[f"{r} {c}" for c in range(5)] for r in range(5)], ) self.sheet.enable_bindings() self.sheet.grid(row=0, column=0, sticky="nswe") self.bind_all("", self.any_key) def any_key(self, event: tk.Event) -> None: """ Establish that the Control key is held down """ ctrl = (event.state & 4 > 0) if not ctrl: return """ From here you can use event.keycode and event.keysym to determine which key has been pressed along with Control """ print(event.keycode) print(event.keysym) """ If the keys are the ones you want to have bound to Sheet functionality You can then call the Sheets functionality using event_generate() For example: """ # if the key is correct then: self.sheet.event_generate("<>") app = demo() app.mainloop() ``` --- # **Span Objects** In `tksheet` versions > `7` there are functions which utilise an object named `Span`. These objects are a subclass of `dict` but with various additions and dot notation attribute access. Spans basically represent an **contiguous** area of the sheet. They can be **one** of three **kinds**: - `"cell"` - `"row"` - `"column"` They can be used with some of the sheets functions such as data getting/setting and creation of things on the sheet such as dropdown boxes. Spans store: - A reference to the `Sheet()` they were created with. - Variables which represent a particular range of cells and properties for accessing these ranges. - Variables which represent options for those cells. - Methods which can modify the above variables. - Methods which can act upon the table using the above variables such as `highlight`, `format`, etc. Whether cells, rows or columns are affected will depend on the spans [`kind`](https://github.com/ragardner/tksheet/wiki/Version-7#get-a-spans-kind). ### **Creating a span** You can create a span by: - Using the `span()` function e.g. `sheet.span("A1")` represents the cell `A1` **or** - Using square brackets on a Sheet object e.g. `sheet["A1"]` represents the cell `A1` Both methods return the created span object. ```python span( *key: CreateSpanTypes, type_: str = "", name: str = "", table: bool = True, index: bool = False, header: bool = False, tdisp: bool = False, idisp: bool = True, hdisp: bool = True, transposed: bool = False, ndim: int = 0, convert: object = None, undo: bool = False, emit_event: bool = False, widget: object = None, expand: None | str = None, formatter_options: dict | None = None, **kwargs, ) -> Span """ Create a span / get an existing span by name Returns the created span """ ``` Parameters: - `key` you do not have to provide an argument for `key`, if no argument is provided then the span will be a full sheet span. Otherwise `key` can be the following types which are type hinted as `CreateSpanTypes`: - `None` - `str` e.g. `sheet.span("A1:F1")` - `int` e.g. `sheet.span(0)` - `slice` e.g. `sheet.span(slice(0, 4))` - `Sequence[int | None, int | None]` representing a cell of `row, column` e.g. `sheet.span(0, 0)` - `Sequence[Sequence[int | None, int | None], Sequence[int | None, int | None]]` representing `sheet.span(start row, start column, up to but not including row, up to but not including column)` e.g. `sheet.span(0, 0, 2, 2)` - `Span` e.g `sheet.span(another_span)` - `type_` (`str`) must be either an empty string `""` or one of the following: `"format"`, `"highlight"`, `"dropdown"`, `"checkbox"`, `"readonly"`, `"align"`. - `name` (`str`) used for named spans or for identification. If no name is provided then a name is generated for the span which is based on an internal integer ticker and then converted to a string in the same way column names are. - `table` (`bool`) when `True` will make all functions used with the span target the main table as well as the header/index if those are `True`. - `index` (`bool`) when `True` will make all functions used with the span target the index as well as the table/header if those are `True`. - `header` (`bool`) when `True` will make all functions used with the span target the header as well as the table/index if those are `True`. - `tdisp` (`bool`) is used by data getting functions that utilize spans and when `True` the function retrieves screen displayed data for the table, not underlying cell data. - `idisp` (`bool`) is used by data getting functions that utilize spans and when `True` the function retrieves screen displayed data for the index, not underlying cell data. - `hdisp` (`bool`) is used by data getting functions that utilize spans and when `True` the function retrieves screen displayed data for the header, not underlying cell data. - `transposed` (`bool`) is used by data getting and setting functions that utilize spans. When `True`: - Returned sublists from data getting functions will represent columns rather than rows. - Data setting functions will assume that a single sequence is a column rather than row and that a list of lists is a list of columns rather than a list of rows. - `ndim` (`int`) is used by data getting functions that utilize spans, it must be either `0` or `1` or `2`. - `0` is the default setting which will make the return value vary based on what it is. For example if the gathered data is only a single cell it will return a value instead of a list of lists with a single list containing a single value. A single row will be a single list. - `1` will force the return of a single list as opposed to a list of lists. - `2` will force the return of a list of lists. - `convert` (`None`, `Callable`) can be used to modify the data using a function before returning it. The data sent to the `convert` function will be as it was before normally returning (after `ndim` has potentially modified it). - `undo` (`bool`) is used by data modifying functions that utilize spans. When `True` and if undo is enabled for the sheet then the end user will be able to undo/redo the modification. - `emit_event` when `True` and when using data setting functions that utilize spans causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information on binding this event. - `widget` (`object`) is the reference to the original sheet which created the span. This can be changed to a different sheet if required e.g. `my_span.widget = new_sheet`. - `expand` (`None`, `str`) must be either `None` or: - `"table"`/`"both"` expand the span both down and right from the span start to the ends of the table. - `"right"` expand the span right to the end of the table `x` axis. - `"down"` expand the span downwards to the bottom of the table `y` axis. - `formatter_options` (`dict`, `None`) must be either `None` or `dict`. If providing a `dict` it must be the same structure as used in format functions, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#data-formatting) for more information. Used to turn the span into a format type span which: - When using `get_data()` will format the returned data. - When using `set_data()` will format the data being set but **NOT** create a new formatting rule on the sheet. - `**kwargs` you can provide additional keyword arguments to the function for example those used in `span.highlight()` or `span.dropdown()` which are used when applying a named span to a table. Notes: - To create a named span see [here](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans). #### **Span creation syntax** **When creating a span using the below methods:** - `str`s use excel syntax and the indexing rule of up to **AND** including. - `int`s use python syntax and the indexing rule of up to but **NOT** including. For example python index `0` as in `[0]` is the first whereas excel index `1` as in `"A1"` is the first. If you need to convert python indexes into column letters you can use the function `num2alpha` importable from `tksheet`: ```python from tksheet import ( Sheet, num2alpha as n2a, ) # column index five as a letter n2a(5) ``` #### **Span creation examples using square brackets** ```python """ EXAMPLES USING SQUARE BRACKETS """ span = sheet[0] # first row span = sheet["1"] # first row span = sheet[0:2] # first two rows span = sheet["1:2"] # first two rows span = sheet[:] # entire sheet span = sheet[":"] # entire sheet span = sheet[:2] # first two rows span = sheet[":2"] # first two rows """ THESE TWO HAVE DIFFERENT OUTCOMES """ span = sheet[2:] # all rows after and not inlcuding python index 1 span = sheet["2:"] # all rows after and not including python index 0 span = sheet["A"] # first column span = sheet["A:C"] # first three columns """ SOME CELL AREA EXAMPLES """ span = sheet[0, 0] # cell A1 span = sheet[(0, 0)] # cell A1 span = sheet["A1:C1"] # cells A1, B1, C1 span = sheet[0, 0, 1, 3] # cells A1, B1, C1 span = sheet[(0, 0, 1, 3)] # cells A1, B1, C1 span = sheet[(0, 0), (1, 3)] # cells A1, B1, C1 span = sheet[((0, 0), (1, 3))] # cells A1, B1, C1 span = sheet["A1:2"] span = sheet[0, 0, 2, None] """ ["A1:2"] All the cells starting from (0, 0) expanding down to include row 1 but not including cells beyond row 1 and expanding out to include all columns A B C D 1 x x x x 2 x x x x 3 4 ... """ span = sheet["A1:B"] span = sheet[0, 0, None, 2] """ ["A1:B"] All the cells starting from (0, 0) expanding out to include column 1 but not including cells beyond column 1 and expanding down to include all rows A B C D 1 x x 2 x x 3 x x 4 x x ... """ """ GETTING AN EXISTING NAMED SPAN """ # you can retrieve an existing named span quickly by surrounding its name in <> e.g. named_span_retrieval = sheet[""] ``` #### **Span creation examples using sheet.span()** ```python """ EXAMPLES USING span() """ """ USING NO ARGUMENTS """ sheet.span() # entire sheet, in this case not including header or index """ USING ONE ARGUMENT str or int or slice() """ # with one argument you can use the same string syntax used for square bracket span creation sheet.span("A1") sheet.span(0) # row at python index 0, all columns sheet.span(slice(0, 2)) # rows at python indexes 0 and 1, all columns sheet.span(":") # entire sheet """ USING TWO ARGUMENTS int | None, int | None or (int | None, int | None), (int | None, int | None) """ sheet.span(0, 0) # row 0, column 0 - the first cell sheet.span(0, None) # row 0, all columns sheet.span(None, 0) # column 0, all rows sheet.span((0, 0), (1, 1)) # row 0, column 0 - the first cell sheet.span((0, 0), (None, 2)) # rows 0 - end, columns 0 and 1 """ USING FOUR ARGUMENTS int | None, int | None, int | None, int | None """ sheet.span(0, 0, 1, 1) # row 0, column 0 - the first cell sheet.span(0, 0, None, 2) # rows 0 - end, columns 0 and 1 ``` ### **Span properties** Spans have a few `@property` functions: - `span.kind` - `span.rows` - `span.columns` #### **Get a spans kind** ```python span.kind ``` - Returns either `"cell"`, `"row"` or `"column"`. ```python span = sheet.span("A1:C4") print (span.kind) # prints "cell" span = sheet.span(":") print (span.kind) # prints "cell" span = sheet.span("1:3") print (span.kind) # prints "row" span = sheet.span("A:C") print (span.kind) # prints "column" # after importing num2alpha from tksheet print (sheet[num2alpha(0)].kind) # prints "column" ``` #### **Get span rows and columns** ```python span.rows span.columns ``` Returns a `SpanRange` object. The below examples are for `span.rows` but you can use `span.columns` for the spans columns exactly the same way. ```python # use as an iterator span = sheet.span("A1:C4") for row in span.rows: pass # use as a reversed iterator for row in reversed(span.rows): pass # check row membership span = sheet.span("A1:C4") print (2 in span.rows) # prints True # check span.rows equality, also can do not equal span = self.sheet["A1:C4"] span2 = self.sheet["1:4"] print (span.rows == span2.rows) # prints True # check len span = self.sheet["A1:C4"] print (len(span.rows)) # prints 4 ``` ### **Span methods** Spans have the following methods, all of which return the span object itself so you can chain the functions e.g. `span.options(undo=True).clear().bg = "indianred1"` #### **Modify a spans attributes** ```python span.options( type_: str | None = None, name: str | None = None, table: bool | None = None, index: bool | None = None, header: bool | None = None, tdisp: bool | None = None, idisp: bool | None = None, hdisp: bool | None = None, transposed: bool | None = None, ndim: int | None = None, convert: Callable | None = None, undo: bool | None = None, emit_event: bool | None = None, widget: object = None, expand: str | None = None, formatter_options: dict | None = None, **kwargs, ) -> Span ``` **Note:** that if `None` is used for any of the following parameters then that `Span`s attribute will be unchanged. - `type_` (`str`, `None`) if not `None` then must be either an empty string `""` or one of the following: `"format"`, `"highlight"`, `"dropdown"`, `"checkbox"`, `"readonly"`, `"align"`. - `name` (`str`, `None`) is used for named spans or for identification. - `table` (`bool`, `None`) when `True` will make all functions used with the span target the main table as well as the header/index if those are `True`. - `index` (`bool`, `None`) when `True` will make all functions used with the span target the index as well as the table/header if those are `True`. - `header` (`bool`, `None`) when `True` will make all functions used with the span target the header as well as the table/index if those are `True`. - `tdisp` (`bool`, `None`) is used by data getting functions that utilize spans and when `True` the function retrieves screen displayed data for the table, not underlying cell data. - `idisp` (`bool`, `None`) is used by data getting functions that utilize spans and when `True` the function retrieves screen displayed data for the index, not underlying cell data. - `hdisp` (`bool`, `None`) is used by data getting functions that utilize spans and when `True` the function retrieves screen displayed data for the header, not underlying cell data. - `transposed` (`bool`, `None`) is used by data getting and setting functions that utilize spans. When `True`: - Returned sublists from data getting functions will represent columns rather than rows. - Data setting functions will assume that a single sequence is a column rather than row and that a list of lists is a list of columns rather than a list of rows. - `ndim` (`int`, `None`) is used by data getting functions that utilize spans, it must be either `0` or `1` or `2`. - `0` is the default setting which will make the return value vary based on what it is. For example if the gathered data is only a single cell it will return a value instead of a list of lists with a single list containing a single value. A single row will be a single list. - `1` will force the return of a single list as opposed to a list of lists. - `2` will force the return of a list of lists. - `convert` (`Callable`, `None`) can be used to modify the data using a function before returning it. The data sent to the `convert` function will be as it was before normally returning (after `ndim` has potentially modified it). - `undo` (`bool`, `None`) is used by data modifying functions that utilize spans. When `True` and if undo is enabled for the sheet then the end user will be able to undo/redo the modification. - `emit_event` (`bool`, `None`) is used by data modifying functions that utilize spans. When `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. - `widget` (`object`) is the reference to the original sheet which created the span. This can be changed to a different sheet if required e.g. `my_span.widget = new_sheet`. - `expand` (`str`, `None`) must be either `None` or: - `"table"`/`"both"` expand the span both down and right from the span start to the ends of the table. - `"right"` expand the span right to the end of the table `x` axis. - `"down"` expand the span downwards to the bottom of the table `y` axis. - `formatter_options` (`dict`, `None`) must be either `None` or `dict`. If providing a `dict` it must be the same structure as used in format functions, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#data-formatting) for more information. Used to turn the span into a format type span which: - When using `get_data()` will format the returned data. - When using `set_data()` will format the data being set but **NOT** create a new formatting rule on the sheet. - `**kwargs` you can provide additional keyword arguments to the function for example those used in `span.highlight()` or `span.dropdown()` which are used when applying a named span to a table. - This function returns the span instance itself (`self`). ```python # entire sheet span = sheet["A1"].options(expand="both") # column A span = sheet["A1"].options(expand="down") # row 0 span = sheet["A1"].options( expand="right", ndim=1, # to return a single list when getting data ) ``` All of a spans modifiable attributes are listed here: - `from_r` (`int`) represents which row the span starts at, must be a positive `int`. - `from_c` (`int`) represents which column the span starts at, must be a positive `int`. - `upto_r` (`int`, `None`) represents which row the span ends at, must be a positive `int` or `None`. `None` means always up to and including the last row. - `upto_c` (`int`, `None`) represents which column the span ends at, must be a positive `int` or `None`. `None` means always up to and including the last column. - `type_` (`str`) must be either an empty string `""` or one of the following: `"format"`, `"highlight"`, `"dropdown"`, `"checkbox"`, `"readonly"`, `"align"`. - `name` (`str`) used for named spans or for identification. If no name is provided then a name is generated for the span which is based on an internal integer ticker and then converted to a string in the same way column names are. - `table` (`bool`) when `True` will make all functions used with the span target the main table as well as the header/index if those are `True`. - `index` (`bool`) when `True` will make all functions used with the span target the index as well as the table/header if those are `True`. - `header` (`bool`) when `True` will make all functions used with the span target the header as well as the table/index if those are `True`. - `tdisp` (`bool`) is used by data getting functions that utilize spans and when `True` the function retrieves screen displayed data for the table, not underlying cell data. - `idisp` (`bool`) is used by data getting functions that utilize spans and when `True` the function retrieves screen displayed data for the index, not underlying cell data. - `hdisp` (`bool`) is used by data getting functions that utilize spans and when `True` the function retrieves screen displayed data for the header, not underlying cell data. - `transposed` (`bool`) is used by data getting and setting functions that utilize spans. When `True`: - Returned sublists from data getting functions will represent columns rather than rows. - Data setting functions will assume that a single sequence is a column rather than row and that a list of lists is a list of columns rather than a list of rows. - `ndim` (`int`) is used by data getting functions that utilize spans, it must be either `0` or `1` or `2`. - `0` is the default setting which will make the return value vary based on what it is. For example if the gathered data is only a single cell it will return a value instead of a list of lists with a single list containing a single value. A single row will be a single list. - `1` will force the return of a single list as opposed to a list of lists. - `2` will force the return of a list of lists. - `convert` (`None`, `Callable`) can be used to modify the data using a function before returning it. The data sent to the `convert` function will be as it was before normally returning (after `ndim` has potentially modified it). - `undo` (`bool`) is used by data modifying functions that utilize spans. When `True` and if undo is enabled for the sheet then the end user will be able to undo/redo the modification. - `emit_event` (`bool`) is used by data modifying functions that utilize spans. When `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. - `widget` (`object`) is the reference to the original sheet which created the span. This can be changed to a different sheet if required e.g. `my_span.widget = new_sheet`. - `kwargs` a `dict` containing keyword arguments relevant for functions such as `span.highlight()` or `span.dropdown()` which are used when applying a named span to a table. If necessary you can also modify these attributes the same way you would an objects. e.g. ```python # span now takes in all columns, including A span = self.sheet("A") span.upto_c = None # span now adds to sheets undo stack when using data modifying functions that use spans span = self.sheet("A") span.undo = True ``` #### **Using a span to format data** Formats table data, see the help on [formatting](https://github.com/ragardner/tksheet/wiki/Version-7#data-formatting) for more information. Note that using this function also creates a format rule for the affected table cells. ```python span.format( formatter_options: dict = {}, formatter_class: object = None, redraw: bool = True, **kwargs, ) -> Span ``` Example: ```python # using square brackets sheet[:].format(int_formatter()) # or instead using sheet.span() sheet.span(":").format(int_formatter()) ``` These examples show the formatting of the entire sheet (not including header and index) as `int` and creates a format rule for all currently existing cells. [Named spans](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans) are required to create a rule for all future existing cells as well, for example those created by the end user inserting rows or columns. #### **Using a span to delete data format rules** Delete any currently existing format rules for parts of the table that are covered by the span. Should not be used where there are data formatting rules created by named spans, see [Named spans](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans) for more information. ```python span.del_format() -> Span ``` Example: ```python span1 = sheet[2:4] span1.format(float_formatter()) span1.del_format() ``` #### **Using a span to create highlights** ```python span.highlight( bg: bool | None | str = False, fg: bool | None | str = False, end: bool | None = None, overwrite: bool = False, redraw: bool = True, ) -> Span ``` There are two ways to create highlights using a span: Method 1 example using `.highlight()`: ```python # highlights column A background red, text color black sheet["A"].highlight(bg="red", fg="black") # the same but after having saved a span my_span = sheet["A"] my_span.highlight(bg="red", fg="black") ``` Method 2 example using `.bg`/`.fg`: ```python # highlights column A background red, text color black sheet["A"].bg = "red" sheet["A"].fg = "black" # the same but after having saved a span my_span = sheet["A"] my_span.bg = "red" my_span.fg = "black" ``` #### **Using a span to delete highlights** Delete any currently existing highlights for parts of the sheet that are covered by the span. Should not be used where there are highlights created by named spans, see [Named spans](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans) for more information. ```python span.dehighlight() -> Span ``` Example: ```python span1 = sheet[2:4].highlight(bg="red", fg="black") span1.dehighlight() ``` #### **Using a span to create dropdown boxes** Creates dropdown boxes for parts of the sheet that are covered by the span. For more information see [here](https://github.com/ragardner/tksheet/wiki/Version-7#dropdown-boxes). ```python span.dropdown( values: list = [], set_value: object = None, state: str = "normal", redraw: bool = True, selection_function: Callable | None = None, modified_function: Callable | None = None, search_function: Callable = dropdown_search_function, validate_input: bool = True, text: None | str = None, ) -> Span ``` Example: ```python sheet["D"].dropdown( values=["on", "off"], set_value="off", ) ``` #### **Using a span to delete dropdown boxes** Delete dropdown boxes for parts of the sheet that are covered by the span. Should not be used where there are dropdown box rules created by named spans, see [Named spans](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans) for more information. ```python span.del_dropdown() -> Span ``` Example: ```python dropdown_span = sheet["D"].dropdown(values=["on", "off"], set_value="off") dropdown_span.del_dropdown() ``` #### **Using a span to create check boxes** Create check boxes for parts of the sheet that are covered by the span. ```python span.checkbox( edit_data: bool = True, checked: bool | None = None, state: str = "normal", redraw: bool = True, check_function: Callable | None = None, text: str = "", ) -> Span ``` Parameters: - `edit_data` when `True` edits the underlying cell data to either `checked` if `checked` is a `bool` or tries to convert the existing cell data to a `bool`. - `checked` is the initial creation value to set the box to, if `None` then and `edit_data` is `True` then it will try to convert the underlying cell data to a `bool`. - `state` can be `"normal"` or `"disabled"`. If `"disabled"` then color will be same as table grid lines, else it will be the cells text color. - `check_function` can be used to trigger a function when the user clicks a checkbox. - `text` displays text next to the checkbox in the cell, but will not be used as data, data will either be `True` or `False`. Example: ```python sheet["D"].checkbox( checked=True, text="Switch", ) ``` #### **Using a span to delete check boxes** Delete check boxes for parts of the sheet that are covered by the span. Should not be used where there are check box rules created by named spans, see [Named spans](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans) for more information. ```python span.del_checkbox() -> Span ``` Example: ```python checkbox_span = sheet["D"].checkbox(checked=True, text="Switch") checkbox_span.del_checkbox() ``` #### **Using a span to set cells to read only** Create a readonly rule for parts of the table that are covered by the span. ```python span.readonly(readonly: bool = True) -> Span ``` - Using `span.readonly(False)` deletes any existing readonly rules for the span. Should not be used where there are readonly rules created by named spans, see [Named spans](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans) for more information. #### **Using a span to create text alignment rules** Create a text alignment rule for parts of the sheet that are covered by the span. ```python span.align( align: str | None, redraw: bool = True, ) -> Span ``` - `align` (`str`, `None`) must be either: - `None` - clears the alignment rule - `"c"`, `"center"`, `"centre"` - `"w"`, `"west"`, `"left"` - `"e"`, `"east"`, `"right"` Example: ```python sheet["D"].align("right") ``` There are two ways to create alignment rules using a span: Method 1 example using `.align()`: ```python # column D right text alignment sheet["D"].align("right") # the same but after having saved a span my_span = sheet["D"] my_span.align("right") ``` Method 2 example using `.align = `: ```python # column D right text alignment sheet["D"].align = "right" # the same but after having saved a span my_span = sheet["D"] my_span.align = "right" ``` #### **Using a span to delete text alignment rules** Delete text alignment rules for parts of the sheet that are covered by the span. Should not be used where there are alignment rules created by named spans, see [Named spans](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans) for more information. ```python span.del_align() -> Span ``` Example: ```python align_span = sheet["D"].align("right") align_span.del_align() ``` #### **Using a span to clear cells** Clear cell data from all cells that are covered by the span. ```python span.clear( undo: bool | None = None, emit_event: bool | None = None, redraw: bool = True, ) -> Span ``` Parameters: - `undo` (`bool`, `None`) When `True` if undo is enabled for the end user they will be able to undo the clear change. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. Example: ```python # clears column D sheet["D"].clear() ``` #### **Using a span to tag cells** Tag cells, rows or columns depending on the spans kind, more information on tags [here](https://github.com/ragardner/tksheet/wiki/Version-7#tags). ```python tag(*tags) -> Span ``` Notes: - If `span.kind` is `"cell"` then cells will be tagged, if it's a row span then rows will be and so for columns. Example: ```python # tags rows 2, 3, 4 with "hello world" sheet[2:5].tag("hello world") ``` #### **Using a span to untag cells** Remove **all** tags from cells, rows or columns depending on the spans kind, more information on tags [here](https://github.com/ragardner/tksheet/wiki/Version-7#tags). ```python untag() -> Span ``` Notes: - If `span.kind` is `"cell"` then cells will be untagged, if it's a row span then rows will be and so for columns. Example: ```python # tags rows 2, 3, 4 with "hello" and "bye" sheet[2:5].tag("hello", "bye") # removes both "hello" and "bye" tags from rows 2, 3, 4 sheet[2:5].untag() ``` #### **Set the spans orientation** The attribute `span.transposed` (`bool`) is used by data getting and setting functions that utilize spans. When `True`: - Returned sublists from data getting functions will represent columns rather than rows. - Data setting functions will assume that a single sequence is a column rather than row and that a list of lists is a list of columns rather than a list of rows. You can toggle the transpotition of the span by using: ```python span.transpose() -> Span ``` If the attribute is already `True` this makes it `False` and vice versa. ```python span = sheet["A:D"].transpose() # this span is now transposed print (span.transposed) # prints True span.transpose() # this span is no longer transposed print (span.transposed) # prints False ``` #### **Expand the spans area** Expand the spans area either all the way to the right (x axis) or all the way down (y axis) or both. ```python span.expand(direction: str = "both") -> Span ``` - `direction` (`None`, `str`) must be either `None` or: - `"table"`/`"both"` expand the span both down and right from the span start to the ends of the table. - `"right"` expand the span right to the end of the table x axis. - `"down"` expand the span downwards to the bottom of the table y axis. --- # **Named Spans** Named spans are like spans but with a type, some keyword arguments saved in `span.kwargs` and then created by using a `Sheet()` function. Like spans, named spans are also **contiguous** areas of the sheet. Named spans can be used to: - Create options (rules) for the sheet which will expand/contract when new cells are added/removed. For example if a user were to insert rows in the middle of some already highlighted rows: - With ordinary row highlights the newly inserted rows would **NOT** be highlighted. - With named span row highlights the newly inserted rows would also be highlighted. - Quickly delete an existing option from the table whereas an ordinary span would not keep track of where the options have been moved. **Note** that generally when a user moves rows/columns around the dimensions of the named span essentially move with either end of the span: - The new start of the span will be wherever the start row/column moves. - The new end of the span will be wherever the end row/column moves. The exceptions to this rule are when a span is expanded or has been created with `None`s or the start of `0` and no end or end of `None`. For the end user, when a span is just a single row/column (and is not expanded/unlimited) it cannot be expanded but it can be deleted if the row/column is deleted. #### **Creating a named span** For a span to become a named span it needs: - One of the following `type_`s: `"format"`, `"highlight"`, `"dropdown"`, `"checkbox"`, `"readonly"`, `"align"`. - Relevant keyword arguments e.g. if the `type_` is `"highlight"` then arguments for `sheet.highlight()` found [here](https://github.com/ragardner/tksheet/wiki/Version-7#highlighting-cells). After a span has the above items the following function has to be used to make it a named span and create the options on the sheet: ```python named_span(span: Span) """ Adds a named span to the sheet Returns the span """ ``` - `span` must be an existing span with: - a `name` (a `name` is automatically generated upon span creation if one is not provided). - a `type_` as described above. - keyword arguments as described above. Examples of creating named spans: ```python # Will highlight rows 3 up to and including 5 span1 = self.sheet.span( "3:5", type_="highlight", bg="green", fg="black", ) self.sheet.named_span(span1) # Will always keep the entire sheet formatted as `int` no matter how many rows/columns are inserted span2 = self.sheet.span( ":", # you don't have to provide a `type_` when using the `formatter_kwargs` argument formatter_options=int_formatter(), ) self.sheet.named_span(span2) ``` #### **Deleting a named span** To delete a named span you simply have to provide the name. ```python del_named_span(name: str) ``` Example, creating and deleting a span: ```python # span covers the entire sheet self.sheet.named_span( self.sheet.span( name="my highlight span", type_="highlight", bg="dark green", fg="#FFFFFF", ) ) self.sheet.del_named_span("my highlight span") # ValueError is raised if name does not exist self.sheet.del_named_span("this name doesnt exist") # ValueError: Span 'this name doesnt exist' does not exist. ``` #### **Other named span functions** Sets the `Sheet`s internal dict of named spans: ```python set_named_spans(named_spans: None | dict = None) -> Sheet ``` - Using `None` deletes all existing named spans Get an existing named span: ```python get_named_span(name: str) -> dict ``` Get all existing named spans: ```python get_named_spans() -> dict ``` --- # **Getting Sheet Data** #### **Using a span to get sheet data** A `Span` object (more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#span-objects)) is returned when using square brackets on a `Sheet` like so: ```python span = self.sheet["A1"] ``` You can also use `sheet.span()`: ```python span = self.sheet.span("A1") ``` The above spans represent the cell `A1` - row 0, column 0. A reserved span attribute named `data` can then be used to retrieve the data for cell `A1`, example below: ```python span = self.sheet["A1"] cell_a1_data = span.data ``` The data that is retrieved entirely depends on the area the span represents. You can also use `span.value` to the same effect. There are certain other span attributes which have an impact on the data returned, explained below: - `table` (`bool`) when `True` will make all functions used with the span target the main table as well as the header/index if those are `True`. - `index` (`bool`) when `True` will make all functions used with the span target the index as well as the table/header if those are `True`. - `header` (`bool`) when `True` will make all functions used with the span target the header as well as the table/index if those are `True`. - `tdisp` (`bool`) when `True` the function retrieves screen displayed data for the table, not underlying cell data. - `idisp` (`bool`) when `True` the function retrieves screen displayed data for the index, not underlying cell data. - `hdisp` (`bool`) when `True` the function retrieves screen displayed data for the header, not underlying cell data. - `transposed` (`bool`) is used by data getting and setting functions that utilize spans. When `True`: - Returned sublists from **data getting** functions will represent columns rather than rows. - Data setting functions will assume that a single sequence is a column rather than row and that a list of lists is a list of columns rather than a list of rows. - `ndim` (`int`) is used by data getting functions that utilize spans, it must be either `0` or `1` or `2`. - `0` is the default setting which will make the return value vary based on what it is. For example if the gathered data is only a single cell it will return a value instead of a list of lists with a single list containing a single value. A single row will be a single list. - `1` will force the return of a single list as opposed to a list of lists. - `2` will force the return of a list of lists. - `convert` (`None`, `Callable`) can be used to modify the data using a function before returning it. The data sent to the `convert` function will be as it was before normally returning (after `ndim` has potentially modified it). - `widget` (`object`) is the reference to the original sheet which created the span (this is the widget that data is retrieved from). This can be changed to a different sheet if required e.g. `my_span.widget = new_sheet`. Some more complex examples of data retrieval: ```python "single cell" cell_a1_data = self.sheet["A1"].data "entire sheet including headers and index" entire_sheet_data = self.sheet["A1"].expand().options(header=True, index=True).data "header data, no table or index data" # a list of displayed header cells header_data = self.sheet["A:C"].options(table=False, header=True).data # a header value header_data = self.sheet["A"].options(table=False, hdisp=False, header=True).data "index data, no table or header data" # a list of displayed index cells index_data = self.sheet[:3].options(table=False, index=True).data # or using sheet.span() a list of displayed index cells index_data = self.sheet.span(slice(None, 3), table=False, index=True).data # a row index value index_data = self.sheet[3].options(table=False, idisp=False, index=True).data "sheet data as columns instead of rows, with actual header data" sheet_data = self.sheet[:].transpose().options(hdisp=False, header=True).data # or instead using sheet.span() with only kwargs sheet_data = self.sheet.span(transposed=True, hdisp=False, header=True).data ``` There is also a `Sheet()` function for data retrieval (it is used internally by the above data getting methods): ```python sheet.get_data( *key: CreateSpanTypes, ) -> object ``` Examples: ```python data = self.sheet.get_data("A1") data = self.sheet.get_data(0, 0, 3, 3) data = self.sheet.get_data( self.sheet.span(":D", transposed=True) ) ``` ___ #### **Generate sheet rows one at a time** This function is useful if you need a lot of sheet data, and produces one row at a time (may save memory use in certain scenarios). It does not use spans. ```python yield_sheet_rows( get_displayed: bool = False, get_header: bool = False, get_index: bool = False, get_index_displayed: bool = True, get_header_displayed: bool = True, only_rows: int | Iterator[int] | None = None, only_columns: int | Iterator[int] | None = None, ) -> Iterator[list[object]] ``` Parameters: - `get_displayed` (`bool`) if `True` it will return cell values as they are displayed on the screen. If `False` it will return any underlying data, for example if the cell is formatted. - `get_header` (`bool`) if `True` it will return the header of the sheet even if there is not one. - `get_index` (`bool`) if `True` it will return the index of the sheet even if there is not one. - `get_index_displayed` (`bool`) if `True` it will return whatever index values are displayed on the screen, for example if there is a dropdown box with `text` set. - `get_header_displayed` (`bool`) if `True` it will return whatever header values are displayed on the screen, for example if there is a dropdown box with `text` set. - `only_rows` (`None`, `iterable`) with this argument you can supply an iterable of `int` row indexes in any order to be the only rows that are returned. - `only_columns` (`None`, `iterable`) with this argument you can supply an iterable of `int` column indexes in any order to be the only columns that are returned. ___ #### **Get table data, readonly** ```python @property data() ``` - e.g. `self.sheet.data` - Doesn't include header or index data. ___ #### **The name of the actual internal sheet data variable** ```python .MT.data ``` - You can use this to directly modify or retrieve the main table's data e.g. `cell_0_0 = my_sheet_name_here.MT.data[0][0]` but only do so if you know what you're doing. ___ #### **Sheet methods** `Sheet` objects also have some functions similar to lists. **Note** that these functions do **not** include the header or index. Iterate over table rows: ```python for row in self.sheet: print (row) # and in reverse for row in reversed(self.sheet): print (row) ``` Check if the table has a particular value (membership): ```python # returns True or False search_value = "the cell value I'm looking for" print (search_value in self.sheet) ``` - Can also check if a row is in the sheet if a `list` is used. ___ #### **Get the number of rows in the sheet** ```python get_total_rows(include_index: bool = False) -> int ``` ___ #### **Get the number of columns in the sheet** ```python get_total_columns(include_header: bool = False) -> int ``` ___ #### **Get a value for a particular cell if that cell was empty** ```python get_value_for_empty_cell( r: int, c: int, r_ops: bool = True, c_ops: bool = True, ) -> object ``` - `r_ops`/`c_ops` when both are `True` it will take into account whatever cell/row/column options exist. When just `r_ops` is `True` it will take into account row options only and when just `c_ops` is `True` it will take into account column options only. --- # **Setting Sheet Data** Fundamentally, there are two ways to set table data: - Overwriting the entire table and setting the table data to a new object. - Modifying the existing data. #### **Overwriting the table** ```python set_sheet_data( data: list | tuple | None = None, reset_col_positions: bool = True, reset_row_positions: bool = True, redraw: bool = True, verify: bool = False, reset_highlights: bool = False, keep_formatting: bool = True, delete_options: bool = False, ) -> object ``` Parameters: - `data` (`list`) has to be a list of lists for full functionality, for display only a list of tuples or a tuple of tuples will work. - `reset_col_positions` and `reset_row_positions` (`bool`) when `True` will reset column widths and row heights. - `redraw` (`bool`) refreshes the table after setting new data. - `verify` (`bool`) goes through `data` and checks if it is a list of lists, will raise error if not, disabled by default. - `reset_highlights` (`bool`) resets all table cell highlights. - `keep_formatting` (`bool`) when `True` re-applies any prior formatting rules to the new data, if `False` all prior formatting rules are deleted. - `delete_options` (`bool`) when `True` all table options such as dropdowns, check boxes, formatting, highlighting etc. are deleted. Notes: - This function does not impact the sheet header or index. ___ ```python @data.setter data(value: object) ``` Notes: - Acts like setting an attribute e.g. `sheet.data = [[1, 2, 3], [4, 5, 6]]` - Uses the `set_sheet_data()` function and its default arguments. ___ #### **Reset all or specific sheet elements and attributes** ```python reset( table: bool = True, header: bool = True, index: bool = True, row_heights: bool = True, column_widths: bool = True, cell_options: bool = True, undo_stack: bool = True, selections: bool = True, sheet_options: bool = False, redraw: bool = True, ) -> Sheet ``` Parameters: - `table` when `True` resets the table to an empty list. - `header` when `True` resets the header to an empty list. - `index` when `True` resets the row index to an empty list. - `row_heights` when `True` deletes all displayed row lines. - `column_widths` when `True` deletes all displayed column lines. - `cell_options` when `True` deletes all dropdowns, checkboxes, highlights, data formatting, etc. - `undo_stack` when `True` resets the sheets undo stack to empty. - `selections` when `True` deletes all selection boxes. - `sheet_options` when `True` resets all the sheets options such as colors, font, popup menu labels and many more to default, for a full list of what's reset see the file `sheet_options.py`. Notes: - This function could be useful when a whole new sheet needs to be loaded. ___ #### **Modifying sheet data** A `Span` object (more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#span-objects)) is returned when using square brackets on a `Sheet` like so: ```python span = self.sheet["A1"] ``` You can also use `sheet.span()`: ```python span = self.sheet.span("A1") ``` The above span example represents the cell `A1` - row 0, column 0. A reserved span attribute named `data` (you can also use `.value`) can then be used to modify sheet data **starting** from cell `A1`. example below: ```python span = self.sheet["A1"] span.data = "new value for cell A1" # or even shorter: self.sheet["A1"].data = "new value for cell A1" # or with sheet.span() self.sheet.span("A1").data = "new value for cell A1" ``` If you provide a list or tuple it will set more than one cell, starting from the spans start cell. In the example below three cells are set in the first row, **starting from cell B1**: ```python self.sheet["B1"].data = ["row 0, column 1 new value (B1)", "row 0, column 2 new value (C1)", "row 0, column 3 new value (D1)"] ``` You can set data in column orientation with a transposed span: ```python self.sheet["B1"].transpose().data = ["row 0, column 1 new value (B1)", "row 1, column 1 new value (B2)", "row 2, column 1 new value (B3)"] ``` When setting data only a spans start cell is taken into account, the end cell is ignored. The example below demonstrates this, the spans end - `"B1"` is ignored and 4 cells get new values: ```python self.sheet["A1:B1"].data = ["A1 new val", "B1 new val", "C1 new val", "D1 new val"] ``` These are the span attributes which have an impact on the data set: - `table` (`bool`) when `True` will make all functions used with the span target the main table as well as the header/index if those are `True`. - `index` (`bool`) when `True` will make all functions used with the span target the index as well as the table/header if those are `True`. - `header` (`bool`) when `True` will make all functions used with the span target the header as well as the table/index if those are `True`. - `transposed` (`bool`) is used by data getting and setting functions that utilize spans. When `True`: - Returned sublists from data getting functions will represent columns rather than rows. - **Data setting** functions will assume that a single sequence is a column rather than row and that a list of lists is a list of columns rather than a list of rows. - `widget` (`object`) is the reference to the original sheet which created the span (this is the widget that data is set to). This can be changed to a different sheet if required e.g. `my_span.widget = new_sheet`. Some more complex examples of setting data: ```python """ SETTING ROW DATA """ # first row gets some new values and the index gets a new value also self.sheet[0].options(index=True).data = ["index val", "row 0 col 0", "row 0 col 1", "row 0 col 2"] # or instead using sheet.span() first row gets some new values and the index gets a new value also self.sheet.span(0, index=True).data = ["index val", "row 0 col 0", "row 0 col 1", "row 0 col 2"] # first two rows get some new values, index included self.sheet[0].options(index=True).data = [["index 0", "row 0 col 0", "row 0 col 1", "row 0 col 2"], ["index 1", "row 1 col 0", "row 1 col 1", "row 1 col 2"]] """ SETTING COLUMN DATA """ # first column gets some new values and the header gets a new value also self.sheet["A"].options(transposed=True, header=True).data = ["header val", "row 0 col 0", "row 1 col 0", "row 2 col 0"] # or instead using sheet.span() first column gets some new values and the header gets a new value also self.sheet.span("A", transposed=True, header=True).data = ["header val", "row 0 col 0", "row 1 col 0", "row 2 col 0"] # first two columns get some new values, header included self.sheet["A"].options(transposed=True, header=True).data = [["header 0", "row 0 col 0", "row 1 col 0", "row 2 col 0"], ["header 1", "row 0 col 1", "row 1 col 1", "row 2 col 1"]] """ SETTING CELL AREA DATA """ # cells B2, C2, B3, C3 get new values self.sheet["B2"].data = [["B2 new val", "C2 new val"], ["B3 new val", "C3 new val"]] # or instead using sheet.span() cells B2, C2, B3, C3 get new values self.sheet.span("B2").data = [["B2 new val", "C2 new val"], ["B3 new val", "C3 new val"]] """ SETTING CELL AREA DATA INCLUDING HEADER AND INDEX """ self.sheet_span = self.sheet.span( header=True, index=True, hdisp=False, idisp=False, ) # set data for the span which was created above self.sheet_span.data = [["", "A", "B", "C"] ["1", "A1", "B1", "C1"], ["2", "A2", "B2", "C2"]] ``` #### **Sheet set data function** You can also use the `Sheet` function `set_data()`. ```python set_data( *key: CreateSpanTypes, data: object = None, undo: bool | None = None, emit_event: bool | None = None, redraw: bool = True, ) -> EventDataDict ``` Parameters: - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. Example: ```python self.sheet.set_data( "A1", [["", "A", "B", "C"] ["1", "A1", "B1", "C1"], ["2", "A2", "B2", "C2"]], ) ``` You can clear cells/rows/columns using a [spans `clear()` function](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-clear-cells) or the Sheets `clear()` function. Below is the Sheets clear function: ```python clear( *key: CreateSpanTypes, undo: bool | None = None, emit_event: bool | None = None, redraw: bool = True, ) -> EventDataDict ``` - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. #### **Insert a row into the sheet** ```python insert_row( row: list[object] | tuple[object] | None = None, idx: str | int | None = None, height: int | None = None, row_index: bool = False, fill: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> EventDataDict ``` Parameters: - Leaving `row` as `None` inserts an empty row, e.g. `insert_row()` will append an empty row to the sheet. - `height` is the new rows displayed height in pixels, leave as `None` for default. - `row_index` when `True` assumes there is a row index value at the start of the row. - `fill` when `True` any provided rows that are shorter than the Sheets longest row will be filled with empty values up to the length of the longest row. - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. ___ #### **Insert a column into the sheet** ```python insert_column( column: list[object] | tuple[object] | None = None, idx: str | int | None = None, width: int | None = None, header: bool = False, fill: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> EventDataDict ``` Parameters: - Leaving `column` as `None` inserts an empty column, e.g. `insert_column()` will append an empty column to the sheet. - `width` is the new columns displayed width in pixels, leave as `None` for default. - `header` when `True` assumes there is a header value at the start of the column. - `fill` when `True` any provided columns that are shorter than the Sheets longest column will be filled with empty values up to the length of the longest column. - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. ___ #### **Insert multiple columns into the sheet** ```python insert_columns( columns: list[tuple[object] | list[object]] | tuple[tuple[object] | list[object]] | int = 1, idx: str | int | None = None, widths: list[int] | tuple[int] | None = None, headers: bool = False, fill: bool = True, undo: bool = False, emit_event: bool = False, create_selections: bool = True, add_row_heights: bool = True, push_ops: bool = True, redraw: bool = True, ) -> EventDataDict ``` Parameters: - `columns` if `int` will insert that number of blank columns. - `idx` (`str`, `int`, `None`) either `str` e.g. `"A"` for `0`, `int` or `None` for end. - `widths` are the new columns displayed widths in pixels, leave as `None` for default. - `headers` when `True` assumes there are headers values at the start of each column. - `fill` when `True` any provided columns that are shorter than the Sheets longest column will be filled with empty values up to the length of the longest column. - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. - `create_selections` when `True` creates a selection box for the newly inserted columns. - `add_row_heights` when `True` creates rows if there are no pre-existing rows. - `push_ops` when `True` increases the indexes of all cell/column options such as dropdown boxes, highlights and data formatting. ___ #### **Insert multiple rows into the sheet** ```python insert_rows( rows: list[tuple[object] | list[object]] | tuple[tuple[object] | list[object]] | int = 1, idx: str | int | None = None, heights: list[int] | tuple[int] | None = None, row_index: bool = False, fill: bool = True, undo: bool = False, emit_event: bool = False, create_selections: bool = True, add_column_widths: bool = True, push_ops: bool = True, redraw: bool = True, ) -> EventDataDict ``` Parameters: - `rows` if `int` will insert that number of blank rows. - `idx` (`str`, `int`, `None`) either `str` e.g. `"A"` for `0`, `int` or `None` for end. - `heights` are the new rows displayed heights in pixels, leave as `None` for default. - `row_index` when `True` assumes there are row index values at the start of each row. - `fill` when `True` any provided rows that are shorter than the Sheets longest row will be filled with empty values up to the length of the longest row. - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. - `create_selections` when `True` creates a selection box for the newly inserted rows. - `add_column_widths` when `True` creates columns if there are no pre-existing columns. - `push_ops` when `True` increases the indexes of all cell/row options such as dropdown boxes, highlights and data formatting. ___ #### **Delete a row from the sheet** ```python del_row( idx: int = 0, data_indexes: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> EventDataDict ``` Parameters: - `idx` is the row to delete. - `data_indexes` only applicable when there are hidden rows. When `False` it makes the `idx` represent a displayed row and not the underlying Sheet data row. When `True` the index represent a data index. - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. ___ #### **Delete multiple rows from the sheet** ```python del_rows( rows: int | Iterator[int], data_indexes: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> EventDataDict ``` Parameters: - `rows` can be either `int` or an iterable of `int`s representing row indexes. - `data_indexes` only applicable when there are hidden rows. When `False` it makes the `rows` indexes represent displayed rows and not the underlying Sheet data rows. When `True` the indexes represent data indexes. - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. ___ #### **Delete a column from the sheet** ```python del_column( idx: int = 0, data_indexes: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> EventDataDict ``` Parameters: - `idx` is the column to delete. - `data_indexes` only applicable when there are hidden columns. When `False` it makes the `idx` represent a displayed column and not the underlying Sheet data column. When `True` the index represent a data index. - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. ___ #### **Delete multiple columns from the sheet** ```python del_columns( columns: int | Iterator[int], data_indexes: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> EventDataDict ``` Parameters: - `columns` can be either `int` or an iterable of `int`s representing column indexes. - `data_indexes` only applicable when there are hidden columns. When `False` it makes the `columns` indexes represent displayed columns and not the underlying Sheet data columns. When `True` the indexes represent data indexes. - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. ___ Expands or contracts the sheet **data** dimensions. ```python sheet_data_dimensions( total_rows: int | None = None, total_columns: int | None = None, ) -> Sheet ``` Parameters: - `total_rows` sets the Sheets number of data rows. - `total_columns` sets the Sheets number of data columns. ___ ```python set_sheet_data_and_display_dimensions( total_rows: int | None = None, total_columns: int | None = None, ) -> Sheet ``` Parameters: - `total_rows` when `int` will set the number of the Sheets data and display rows by deleting or adding rows. - `total_columns` when `int` will set the number of the Sheets data and display columns by deleting or adding columns. ___ ```python total_rows( number: int | None = None, mod_positions: bool = True, mod_data: bool = True, ) -> int | Sheet ``` Parameters: - `number` sets the Sheets number of data rows. When `None` function will return the Sheets number of data rows including the number of rows in the index. - `mod_positions` when `True` also sets the number of displayed rows. - `mod_data` when `True` also sets the number of data rows. ___ ```python total_columns( number: int | None = None, mod_positions: bool = True, mod_data: bool = True, ) -> int | Sheet ``` Parameters: - `number` sets the Sheets number of data columns. When `None` function will return the Sheets number of data columns including the number of columns in the header. - `mod_positions` when `True` also sets the number of displayed columns. - `mod_data` when `True` also sets the number of data columns. ___ #### **Move a single row to a new location** ```python move_row( row: int, moveto: int, ) -> tuple[dict, dict, dict] ``` - Note that `row` and `moveto` indexes represent displayed indexes and not data. When there are hidden rows this is an important distinction, otherwise it is not at all important. To specifically use data indexes use the function `move_rows()`. ___ #### **Move a single column to a new location** ```python move_column( column: int, moveto: int, ) -> tuple[dict, dict, dict] ``` - Note that `column` and `moveto` indexes represent displayed indexes and not data. When there are hidden columns this is an important distinction, otherwise it is not at all important. To specifically use data indexes use the function `move_columns()`. ___ #### **Move any rows to a new location** ```python move_rows( move_to: int | None = None, to_move: list[int] | None = None, move_data: bool = True, data_indexes: bool = False, create_selections: bool = True, undo: bool = False, emit_event: bool = False, move_heights: bool = True, redraw: bool = True, ) -> tuple[dict, dict, dict] ``` Parameters: - `move_to` is the new start index for the rows to be moved to. - `to_move` is a `list` of row indexes to move to that new position, they will appear in the same order provided. - `move_data` when `True` moves not just the displayed row positions but the Sheet data as well. - `data_indexes` is only applicable when there are hidden rows. When `False` it makes the `move_to` and `to_move` indexes represent displayed rows and not the underlying Sheet data rows. When `True` the indexes represent data indexes. - `create_selections` creates new selection boxes based on where the rows have moved. - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. - `move_heights` when `True` also moves the displayed row lines. Notes: - The rows in `to_move` do **not** have to be contiguous. ___ #### **Move any columns to a new location** ```python move_columns( move_to: int | None = None, to_move: list[int] | None = None, move_data: bool = True, data_indexes: bool = False, create_selections: bool = True, undo: bool = False, emit_event: bool = False, move_widths: bool = True, redraw: bool = True, ) -> tuple[dict, dict, dict] ``` Parameters: - `move_to` is the new start index for the columns to be moved to. - `to_move` is a `list` of column indexes to move to that new position, they will appear in the same order provided. - `move_data` when `True` moves not just the displayed column positions but the Sheet data as well. - `data_indexes` is only applicable when there are hidden columns. When `False` it makes the `move_to` and `to_move` indexes represent displayed columns and not the underlying Sheet data columns. When `True` the indexes represent data indexes. - `create_selections` creates new selection boxes based on where the columns have moved. - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. - `move_widths` when `True` also moves the displayed column lines. Notes: - The columns in `to_move` do **not** have to be contiguous. ___ #### **Move any columns to new locations** ```python mapping_move_columns( data_new_idxs: dict[int, int], disp_new_idxs: None | dict[int, int] = None, move_data: bool = True, create_selections: bool = True, data_indexes: bool = False, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> tuple[dict[int, int], dict[int, int], EventDataDict] ``` Parameters: - `data_new_idxs` (`dict[int, int]`) must be a `dict` where the keys are the data columns to move as `int`s and the values are their new locations as `int`s. - `disp_new_idxs` (`None | dict[int, int]`) either `None` or a `dict` where the keys are the displayed columns (basically the column widths) to move as `int`s and the values are their new locations as `int`s. If `None` then no column widths will be moved. - `move_data` when `True` moves not just the displayed column positions but the Sheet data as well. - `data_indexes` is only applicable when there are hidden columns. When `False` it makes the `move_to` and `to_move` indexes represent displayed columns and not the underlying Sheet data columns. When `True` the indexes represent data indexes. - `create_selections` creates new selection boxes based on where the columns have moved. - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. ___ Get a mapping (`dict`) of all `old: new` column indexes. ```python full_move_columns_idxs(data_idxs: dict[int, int]) -> dict[int, int] ``` - e.g. converts `{0: 1}` to `{0: 1, 1: 0}` if the maximum Sheet column number is `1`. ___ #### **Move any rows to new locations** ```python mapping_move_rows( data_new_idxs: dict[int, int], disp_new_idxs: None | dict[int, int] = None, move_data: bool = True, data_indexes: bool = False, create_selections: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> tuple[dict[int, int], dict[int, int], EventDataDict] ``` Parameters: - `data_new_idxs` (`dict[int, int]`) must be a `dict` where the keys are the data rows to move as `int`s and the values are their new locations as `int`s. - `disp_new_idxs` (`None | dict[int, int]`) either `None` or a `dict` where the keys are the displayed rows (basically the row heights) to move as `int`s and the values are their new locations as `int`s. If `None` then no row heights will be moved. - `move_data` when `True` moves not just the displayed row positions but the Sheet data as well. - `data_indexes` is only applicable when there are hidden rows. When `False` it makes the `move_to` and `to_move` indexes represent displayed rows and not the underlying Sheet data rows. When `True` the indexes represent data indexes. - `create_selections` creates new selection boxes based on where the rows have moved. - `undo` when `True` adds the change to the Sheets undo stack. - `emit_event` when `True` causes a `"<>` event to occur if it has been bound, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#tkinter-and-tksheet-events) for more information. ___ Get a mapping (`dict`) of all `old: new` row indexes. ```python full_move_rows_idxs(data_idxs: dict[int, int]) -> dict[int, int] ``` - e.g. converts `{0: 1}` to `{0: 1, 1: 0}` if the maximum Sheet row number is `1`. ___ #### **Make all data rows the same length** ```python equalize_data_row_lengths(include_header: bool = True) -> int ``` - Makes every list in the table have the same number of elements, goes by longest list. This will only affect the data variable, not visible columns. - Returns the new row length for all rows in the Sheet. --- # **Highlighting Cells** ### **Creating highlights** #### **Using spans to create highlights** `Span` objects (more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#span-objects)) can be used to highlight cells, rows, columns, the entire sheet, headers and the index. You can use either of the following methods: - Using a span method e.g. `span.highlight()` more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-create-highlights). - Using a sheet method e.g. `sheet.highlight(Span)` Or if you need user inserted row/columns in the middle of highlight areas to also be highlighted you can use named spans, more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans). Whether cells, rows or columns are highlighted depends on the [`kind`](https://github.com/ragardner/tksheet/wiki/Version-7#get-a-spans-kind) of span. ```python highlight( *key: CreateSpanTypes, bg: bool | None | str = False, fg: bool | None | str = False, end: bool | None = None, overwrite: bool = False, redraw: bool = True, ) -> Span ``` Parameters: - `key` (`CreateSpanTypes`) either a span or a type which can create a span. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#creating-a-span) for more information on the types that can create a span. - `bg` and `fg` arguments use either a tkinter color or a hex `str` color. - `end` (`bool`) is used for row highlighting where `True` makes the highlight go to the end of the Sheet window on the x axis. - `overwrite` (`bool`) when `True` overwrites the any previous highlight for that cell/row/column, whereas `False` will only impact the keyword arguments used. - Highlighting cells, rows or columns will also change the colors of dropdown boxes and check boxes. Example: ```python # highlight cell - row 3, column 5 self.sheet.highlight( (3, 5), bg="dark green", fg="white", ) # or # same cells, background red, text color black sheet[3, 5].bg = "red" sheet[3, 5].fg = "black" ``` #### **Other ways to create highlights** **Cells** ```python highlight_cells( row: int | Literal["all"] = 0, column: int | Literal["all"] = 0, cells: list[tuple[int, int]] = [], canvas: Literal["table", "index", "header"] = "table", bg: bool | None | str = False, fg: bool | None | str = False, redraw: bool = True, overwrite: bool = True, ) -> Sheet ``` **Rows** ```python highlight_rows( rows: Iterator[int] | int, bg: None | str = None, fg: None | str = None, highlight_index: bool = True, redraw: bool = True, end_of_screen: bool = False, overwrite: bool = True, ) -> Sheet ``` **Columns** ```python highlight_columns( columns: Iterator[int] | int, bg: bool | None | str = False, fg: bool | None | str = False, highlight_header: bool = True, redraw: bool = True, overwrite: bool = True, ) -> Sheet ``` ___ ### **Deleting highlights** #### **Using spans to delete highlights** If the highlights were created by a named span then the named span must be deleted, more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#deleting-a-named-span). Otherwise you can use either of the following methods to delete/remove highlights: - Using a span method e.g. `span.dehighlight()` more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-delete-highlights). - Using a sheet method e.g. `sheet.dehighlight(Span)` details below: ```python dehighlight( *key: CreateSpanTypes, redraw: bool = True, ) -> Span ``` Parameters: - `key` (`CreateSpanTypes`) either a span or a type which can create a span. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#creating-a-span) for more information on the types that can create a span. Example: ```python # highlight column B self.sheet.highlight( "B", bg="dark green", fg="white", ) # dehighlight column B self.sheet.dehighlight("B") ``` #### **Other ways to delete highlights** **Cells** ```python dehighlight_cells( row: int | Literal["all"] = 0, column: int = 0, cells: list[tuple[int, int]] = [], canvas: Literal["table", "row_index", "header"] = "table", all_: bool = False, redraw: bool = True, ) -> Sheet ``` **Rows** ```python dehighlight_rows( rows: list[int] | Literal["all"] = [], redraw: bool = True, ) -> Sheet ``` **Columns** ```python dehighlight_columns( columns: list[int] | Literal["all"] = [], redraw: bool = True, ) -> Sheet ``` **All** ```python dehighlight_all( cells: bool = True, rows: bool = True, columns: bool = True, header: bool = True, index: bool = True, redraw: bool = True, ) -> Sheet ``` --- # **Dropdown Boxes** #### **Creating dropdown boxes** `Span` objects (more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#span-objects)) can be used to create dropdown boxes for cells, rows, columns, the entire sheet, headers and the index. You can use either of the following methods: - Using a span method e.g. `span.dropdown()` more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-create-dropdown-boxes). - Using a sheet method e.g. `sheet.dropdown(Span)` Or if you need user inserted row/columns in the middle of areas with dropdown boxes to also have dropdown boxes you can use named spans, more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans). Whether dropdown boxes are created for cells, rows or columns depends on the [`kind`](https://github.com/ragardner/tksheet/wiki/Version-7#get-a-spans-kind) of span. ```python dropdown( *key: CreateSpanTypes, values: list = [], edit_data: bool = True, set_values: dict[tuple[int, int] | int, object] | None = None, set_value: object = None, state: str = "normal", redraw: bool = True, selection_function: Callable | None = None, modified_function: Callable | None = None, search_function: Callable = dropdown_search_function, validate_input: bool = True, text: None | str = None, ) -> Span ``` Notes: - `selection_function`/`modified_function` (`Callable`, `None`) parameters require either `None` or a function. The function you use needs at least one argument because tksheet will send information to your function about the triggered dropdown. - When a user selects an item from the dropdown box the sheet will set the underlying cells data to the selected item, to bind this event use either the `selection_function` argument or see the function `extra_bindings()` with binding `"end_edit_cell"` [here](https://github.com/ragardner/tksheet/wiki/Version-7#bindings-and-functionality). Parameters: - `key` (`CreateSpanTypes`) either a span or a type which can create a span. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#creating-a-span) for more information on the types that can create a span. - `values` are the values to appear in a list view type interface when the dropdown box is open. - `edit_data` when `True` makes edits in the table, header or index (depending on the span) based on `set_values`/`set_value`. - `set_values` when combined with `edit_data=True` allows a `dict` to be provided of data coordinates (`tuple[int, int]` for a cell span or `int` for a row/column span) as `key`s and values to set the cell at that coordinate to. - e.g. `set_values={(0, 0): "new value for A1"}`. - The idea behind this parameter is that an entire column or row can have individual cell values and is not set to `set_value` alone. - `set_value` when combined with `edit_data=True` sets every cell in the span to the value provided. If left as `None` and if `set_values` is also `None` then the topmost value from `values` will be used or if not `values` then `""`. - `state` determines whether or not there is also an editable text window at the top of the dropdown box when it is open. - `redraw` refreshes the sheet so the newly created box is visible. - `selection_function` can be used to trigger a specific function when an item from the dropdown box is selected, if you are using the above `extra_bindings()` as well it will also be triggered but after this function. e.g. `selection_function = my_function_name` - `modified_function` can be used to trigger a specific function when the `state` of the box is set to `"normal"` and there is an editable text window and a change of the text in that window has occurred. Note that this function occurs before the dropdown boxes search feature. - `search_function` (`None`, `callable`) sets the function that will be used to search the dropdown boxes values upon a dropdown text editor modified event when the dropdowns state is `normal`. Set to `None` to disable the search feature or use your own function with the following keyword arguments: `(search_for, data):` and make it return an row number (e.g. select and see the first value would be `0`) if positive and `None` if negative. - `validate_input` (`bool`) when `True` will not allow cut, paste, delete or cell editor to input values to cell which are not in the dropdown boxes values. - `text` (`None`, `str`) can be set to something other than `None` to always display over whatever value is in the cell, this is useful when you want to display a Header name over a dropdown box selection. Example: ```python # create dropdown boxes in column "D" self.sheet.dropdown( "D", values=[0, 1, 2, 3, 4], ) ``` ___ #### **Deleting dropdown boxes** If the dropdown boxes were created by a named span then the named span must be deleted, more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#deleting-a-named-span). Otherwise you can use either of the following methods to delete/remove dropdown boxes. - Using a span method e.g. `span.del_dropdown()` more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-delete-dropdown-boxes). - Using a sheet method e.g. `sheet.del_dropdown(Span)` details below: ```python del_dropdown( *key: CreateSpanTypes, redraw: bool = True, ) -> Span ``` Parameters: - `key` (`CreateSpanTypes`) either a span or a type which can create a span. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#creating-a-span) for more information on the types that can create a span. Example: ```python # create dropdown boxes in column "D" self.sheet.dropdown( "D", values=[0, 1, 2, 3, 4], ) # delete dropdown boxes in column "D" self.sheet.del_dropdown("D") ``` ___ #### **Get chosen dropdown boxes values** ```python get_dropdown_values(r: int = 0, c: int = 0) -> None | list ``` ```python get_header_dropdown_values(c: int = 0) -> None | list ``` ```python get_index_dropdown_values(r: int = 0) -> None | list ``` ___ #### **Set the values and cell value of a chosen dropdown box** ```python set_dropdown_values( r: int = 0, c: int = 0, set_existing_dropdown: bool = False, values: list = [], set_value: object = None, ) -> Sheet ``` ```python set_header_dropdown_values( c: int = 0, set_existing_dropdown: bool = False, values: list = [], set_value: object = None, ) -> Sheet ``` ```python set_index_dropdown_values( r: int = 0, set_existing_dropdown: bool = False, values: list = [], set_value: object = None, ) -> Sheet ``` Parameters: - `set_existing_dropdown` if `True` takes priority over `r` and `c` and sets the values of the last popped open dropdown box (if one one is popped open, if not then an `Exception` is raised). - `values` (`list`, `tuple`) - `set_value` (`str`, `None`) if not `None` will try to set the value of the chosen cell to given argument. ___ #### **Set or get bound dropdown functions** ```python dropdown_functions( r: int, c: int, selection_function: str | Callable = "", modified_function: str | Callable = "", ) -> None | dict ``` ```python header_dropdown_functions( c: int, selection_function: str | Callable = "", modified_function: str | Callable = "", ) -> None | dict ``` ```python index_dropdown_functions( r: int, selection_function: str | Callable = "", modified_function: str | Callable = "", ) -> None | dict ``` ___ #### **Get a dictionary of all dropdown boxes** ```python get_dropdowns() -> dict ``` Returns: ```python {(row int, column int): {'values': values, 'select_function': selection_function, 'modified_function': modified_function, 'state': state, 'text': text}} ``` ```python get_header_dropdowns() -> dict ``` ```python get_index_dropdowns() -> dict ``` ___ #### **Open a dropdown box** ```python open_dropdown(r: int, c: int) -> Sheet ``` ```python open_header_dropdown(c: int) -> Sheet ``` ```python open_index_dropdown(r: int) -> Sheet ``` ___ #### **Close a dropdown box** ```python close_dropdown(r: int | None = None, c: int | None = None) -> Sheet ``` ```python close_header_dropdown(c: int | None = None) -> Sheet ``` ```python close_index_dropdown(r: int | None = None) -> Sheet ``` Notes: - Also destroys any opened text editor windows. --- # **Check Boxes** #### **Creating check boxes** `Span` objects (more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#span-objects)) can be used to create check boxes for cells, rows, columns, the entire sheet, headers and the index. You can use either of the following methods: - Using a span method e.g. `span.checkbox()` more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-create-check-boxes). - Using a sheet method e.g. `sheet.checkbox(Span)` Or if you need user inserted row/columns in the middle of areas with check boxes to also have check boxes you can use named spans, more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans). Whether check boxes are created for cells, rows or columns depends on the [`kind`](https://github.com/ragardner/tksheet/wiki/Version-7#get-a-spans-kind) of span. ```python checkbox( *key: CreateSpanTypes, edit_data: bool = True, checked: bool | None = None, state: str = "normal", redraw: bool = True, check_function: Callable | None = None, text: str = "", ) -> Span ``` Notes: - `check_function` (`Callable`, `None`) requires either `None` or a function. The function you use needs at least one argument because when the checkbox is clicked it will send information to your function about the clicked checkbox. - Use `highlight_cells()` or rows or columns to change the color of the checkbox. - Check boxes are always left aligned despite any align settings. Parameters: - `key` (`CreateSpanTypes`) either a span or a type which can create a span. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#creating-a-span) for more information on the types that can create a span. - `edit_data` when `True` edits the underlying cell data to either `checked` if `checked` is a `bool` or tries to convert the existing cell data to a `bool`. - `checked` is the initial creation value to set the box to, if `None` then and `edit_data` is `True` then it will try to convert the underlying cell data to a `bool`. - `state` can be `"normal"` or `"disabled"`. If `"disabled"` then color will be same as table grid lines, else it will be the cells text color. - `check_function` can be used to trigger a function when the user clicks a checkbox. - `text` displays text next to the checkbox in the cell, but will not be used as data, data will either be `True` or `False`. Example: ```python self.sheet.checkbox( "D", checked=True, ) ``` ___ #### **Deleting check boxes** If the check boxes were created by a named span then the named span must be deleted, more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#deleting-a-named-span). Otherwise you can use either of the following methods to delete/remove check boxes: - Using a span method e.g. `span.del_checkbox()` more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-delete-check-boxes). - Using a sheet method e.g. `sheet.del_checkbox(Span)` details below: ```python del_checkbox( *key: CreateSpanTypes, redraw: bool = True, ) -> Span ``` Parameters: - `key` (`CreateSpanTypes`) either a span or a type which can create a span. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#creating-a-span) for more information on the types that can create a span. Example: ```python # creating checkboxes in column D self.sheet.checkbox( "D", checked=True, ) # deleting checkboxes in column D self.sheet.del_checkbox("D") ``` #### **Set or toggle a check box** ```python click_checkbox( *key: CreateSpanTypes, checked: bool | None = None, redraw: bool = True, ) -> Span ``` ```python click_header_checkbox(c: int, checked: bool | None = None) -> Sheet ``` ```python click_index_checkbox(r: int, checked: bool | None = None) -> Sheet ``` ___ #### **Get a dictionary of all check box dictionaries** ```python get_checkboxes() -> dict ``` ```python get_header_checkboxes() -> dict ``` ```python get_index_checkboxes() -> dict ``` --- # **Data Formatting** By default tksheet stores all user inputted data as strings and while tksheet can store and display any datatype with a `__str__()` method this has some obvious limitations. Data formatting aims to provide greater functionality when working with different datatypes and provide strict typing for the sheet. With formatting you can convert sheet data and user input to a specific datatype. Additionally, formatting also provides a function for displaying data on the table GUI (as a rounded float for example) and logic for handling invalid and missing data. tksheet has several basic built-in formatters and provides functionality for creating your own custom formats as well. A demonstration of all the built-in and custom formatters can be found [here](https://github.com/ragardner/tksheet/wiki/Version-7#example-using-and-creating-formatters). ### **Creation and deletion of data formatting rules** #### **Creating a data format rule** `Span` objects (more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#span-objects)) can be used to format data for cells, rows, columns and the entire sheet. You can use either of the following methods: - Using a span method e.g. `span.format()` more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-format-data). - Using a sheet method e.g. `sheet.format(Span)` Or if you need user inserted row/columns in the middle of areas with data formatting to also be formatted you can use named spans, more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans). Whether data is formatted for cells, rows or columns depends on the [`kind`](https://github.com/ragardner/tksheet/wiki/Version-7#get-a-spans-kind) of span. ```python format( *key: CreateSpanTypes, formatter_options: dict = {}, formatter_class: object = None, redraw: bool = True, **kwargs, ) -> Span ``` Notes: 1. When applying multiple overlapping formats with e.g. a formatted cell which overlaps a formatted row, the priority is as follows: - Cell formats first. - Row formats second. - Column formats third. 2. Data formatting will effectively override `validate_input = True` on cells with dropdown boxes. 3. When getting data take careful note of the `get_displayed` options, as these are the difference between getting the actual formatted data and what is simply displayed on the table GUI. Parameters: - `key` (`CreateSpanTypes`) either a span or a type which can create a span. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#creating-a-span) for more information on the types that can create a span. - `formatter_options` (`dict`) a dictionary of keyword options/arguements to pass to the formatter, see [here](https://github.com/ragardner/tksheet/wiki/Version-7#formatters) for information on what argument to use. - `formatter_class` (`class`) in case you want to use a custom class to store functions and information as opposed to using the built-in methods. - `**kwargs` any additional keyword options/arguements to pass to the formatter. #### **Deleting a data format rule** If the data format rule was created by a named span then the named span must be deleted, more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#deleting-a-named-span). Otherwise you can use either of the following methods to delete/remove data formatting rules: - Using a span method e.g. `span.del_format()` more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-delete-check-boxes). - Using a sheet method e.g. `sheet.del_format(Span)` details below: ```python del_format( *key: CreateSpanTypes, clear_values: bool = False, redraw: bool = True, ) -> Span ``` - `key` (`CreateSpanTypes`) either a span or a type which can create a span. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#creating-a-span) for more information on the types that can create a span. - `clear_values` (`bool`) if true, all the cells covered by the span will have their values cleared. #### **Delete all formatting** ```python del_all_formatting(clear_values: bool = False) -> Sheet ``` - `clear_values` (`bool`) if true, all the sheets cell values will be cleared. #### **Reapply formatting to entire sheet** ```python reapply_formatting() -> Sheet ``` - Useful if you have manually changed the entire sheets data using `sheet.MT.data = ` and want to reformat the sheet using any existing formatting you have set. #### **Check if a cell is formatted** ```python formatted(r: int, c: int) -> dict ``` - If the cell is formatted function returns a `dict` with all the format keyword arguments. The `dict` will be empty if the cell is not formatted. ### **Formatters** `tksheet` provides a number of in-built formatters, in addition to the base `formatter` function. These formatters are designed to provide a range of functionality for different datatypes. The following table lists the available formatters and their options. **You can use any of the below formatters as an argument for the parameter `formatter_options`. ```python formatter( datatypes: tuple[object] | object, format_function: Callable, to_str_function: Callable = to_str, invalid_value: object = "NaN", nullable: bool = True, pre_format_function: Callable | None = None, post_format_function: Callable | None = None, clipboard_function: Callable | None = None, **kwargs, ) -> dict ``` This is the generic formatter options interface. You can use this to create your own custom formatters. The following options are available. Note that all these options can also be passed to the `format_cell()` function as keyword arguments and are available as attributes for all formatters. You can provide functions of your own creation for all the below arguments which take functions if you require. - `datatypes` (`list`) a list of datatypes that the formatter will accept. For example, `datatypes = [int, float]` will accept integers and floats. - `format_function` (`function`) a function that takes a string and returns a value of the desired datatype. For example, `format_function = int` will convert a string to an integer. - `to_str_function` (`function`) a function that takes a value of the desired datatype and returns a string. This determines how the formatter displays its data on the table. For example, `to_str_function = str` will convert an integer to a string. Defaults to `tksheet.to_str`. - `invalid_value` (`any`) the value to return if the input string is invalid. For example, `invalid_value = "NA"` will return "NA" if the input string is invalid. - `nullable` (`bool`) if true, the formatter will accept `None` as a valid input. - `pre_format_function` (`function`) a function that takes a input string and returns a string. This function is called before the `format_function` and can be used to modify the input string before it is converted to the desired datatype. This can be useful if you want to strip out unwanted characters or convert a string to a different format before converting it to the desired datatype. - `post_format_function` (`function`) a function that takes a value **which might not be of the desired datatype, e.g. `None` if the cell is nullable and empty** and if successful returns a value of the desired datatype or if not successful returns the input value. This function is called after the `format_function` and can be used to modify the output value after it is converted to the desired datatype. This can be useful if you want to round a float for example. - `clipboard_function` (`function`) a function that takes a value of the desired datatype and returns a string. This function is called when the cell value is copied to the clipboard. This can be useful if you want to convert a value to a different format before it is copied to the clipboard. - `**kwargs` any additional keyword options/arguements to pass to the formatter. These keyword arguments will be passed to the `format_function`, `to_str_function`, and the `clipboard_function`. These can be useful if you want to specifiy any additional formatting options, such as the number of decimal places to round to. #### **Int Formatter** The `int_formatter` is the basic configuration for a simple interger formatter. ```python int_formatter( datatypes: tuple[object] | object = int, format_function: Callable = to_int, to_str_function: Callable = to_str, invalid_value: object = "NaN", **kwargs, ) -> dict ``` Parameters: - `format_function` (`function`) a function that takes a string and returns an `int`. By default, this is set to the in-built `tksheet.to_int`. This function will always convert float-likes to its floor, for example `"5.9"` will be converted to `5`. - `to_str_function` (`function`) By default, this is set to the in-built `tksheet.to_str`, which is a very basic function that will displace the default string representation of the value. Example: ```python sheet.format_cell(0, 0, formatter_options = tksheet.int_formatter()) ``` #### **Float Formatter** The `float_formatter` is the basic configuration for a simple float formatter. It will always round float-likes to the specified number of decimal places, for example `"5.999"` will be converted to `"6.0"` if `decimals = 1`. ```python float_formatter( datatypes: tuple[object] | object = float, format_function: Callable = to_float, to_str_function: Callable = float_to_str, invalid_value: object = "NaN", decimals: int = 2, **kwargs ) -> dict ``` Parameters: - `format_function` (`function`) a function that takes a string and returns a `float`. By default, this is set to the in-built `tksheet.to_float`. This function will always convert percentages to their decimal equivalent, for example `"5%"` will be converted to `0.05`. - `to_str_function` (`function`) By default, this is set to the in-built `tksheet.float_to_str`, which will display the float to the specified number of decimal places. - `decimals` (`int`, `None`) the number of decimal places to round to. Defaults to `2`. Example: ```python sheet.format_cell(0, 0, formatter_options = tksheet.float_formatter(decimals = None)) # A float formatter with maximum float() decimal places ``` #### **Percentage Formatter** The `percentage_formatter` is the basic configuration for a simple percentage formatter. It will always round float-likes as a percentage to the specified number of decimal places, for example `"5.999%"` will be converted to `"6.0%"` if `decimals = 1`. ```python percentage_formatter( datatypes: tuple[object] | object = float, format_function: Callable = to_float, to_str_function: Callable = percentage_to_str, invalid_value: object = "NaN", decimals: int = 0, **kwargs, ) -> dict ``` Parameters: - `format_function` (`function`) a function that takes a string and returns a `float`. By default, this is set to the in-built `tksheet.to_float`. This function will always convert percentages to their decimal equivalent, for example `"5%"` will be converted to `0.05`. - `to_str_function` (`function`) By default, this is set to the in-built `tksheet.percentage_to_str`, which will display the float as a percentage to the specified number of decimal places. For example, `0.05` will be displayed as `"5.0%"`. - `decimals` (`int`) the number of decimal places to round to. Defaults to `0`. Example: ```python sheet.format_cell(0, 0, formatter_options = tksheet.percentage_formatter(decimals = 1)) # A percentage formatter with 1 decimal place ``` #### **Bool Formatter** ```python bool_formatter( datatypes: tuple[object] | object = bool, format_function: Callable = to_bool, to_str_function: Callable = bool_to_str, invalid_value: object = "NA", truthy: set = truthy, falsy: set = falsy, **kwargs, ) -> dict ``` Parameters: - `format_function` (`function`) a function that takes a string and returns a `bool`. By default, this is set to the in-built `tksheet.to_bool`. - `to_str_function` (`function`) By default, this is set to the in-built `tksheet.bool_to_str`, which will display the boolean as `"True"` or `"False"`. - `truthy` (`set`) a set of values that will be converted to `True`. Defaults to the in-built `tksheet.truthy`. - `falsy` (`set`) a set of values that will be converted to `False`. Defaults to the in-built `tksheet.falsy`. Example: ```python # A bool formatter with custom truthy and falsy values to account for aussie and kiwi slang sheet.format_cell(0, 0, formatter_options = tksheet.bool_formatter(truthy = tksheet.truthy | {"nah yeah"}, falsy = tksheet.falsy | {"yeah nah"})) ``` ### **Datetime Formatters and Designing Your Own Custom Formatters** tksheet is at the moment a dependency free library and so doesn't include a datetime parser as is. You can however very easily make a datetime parser if you are willing to install a third-party package. Recommended are: - [dateparser](https://dateparser.readthedocs.io/en/latest/) - [dateutil](https://dateutil.readthedocs.io/en/stable/) Both of these packages have a very comprehensive datetime parser which can be used to create a custom datetime formatter for tksheet. Below is a simple example of how you might create a custom datetime formatter using the `dateutil` package. ```python from tksheet import * from datetime import datetime, date from dateutil.parser import parse def to_local_datetime(dt, **kwargs): ''' Our custom format_function, converts a string or a date to a datetime object in the local timezone. ''' if isinstance(dt, datetime): pass # Do nothing elif isinstance(dt, date): dt = datetime(dt.year, dt.month, dt.day) # Always good to account for unexpected inputs else: try: dt = parser.parse(dt) except: raise ValueError(f"Could not parse {dt} as a datetime") if dt.tzinfo is None: dt = dt.replace(tzinfo = tzlocal()) # If no timezone is specified, assume local timezone dt = dt.astimezone(tzlocal()) # Convert to local timezone return dt def datetime_to_str(dt, **kwargs): ''' Our custom to_str_function, converts a datetime object to a string with a format that can be specfied in kwargs. ''' return dt.strftime(kwargs['format']) # Now we can create our custom formatter dictionary from the generic formatter interface in tksheet datetime_formatter = formatter(datatypes = datetime, format_function = to_local_datetime, to_str_function = datetime_to_str, invalid_value = "NaT", format = "%d/%m/%Y %H:%M:%S", ) # From here we can pass our datetime_formatter into sheet.format() or span.format() just like any other formatter ``` For those wanting even more customisation of their formatters you also have the option of creating a custom formatter class. This is a more advanced topic and is not covered here, but it's recommended to create a new class which is a subclass of the `tksheet.Formatter` class and overriding the methods you would like to customise. This custom class can then be passed into the `format_cells()` `formatter_class` argument. --- # **Readonly Cells** #### **Creating a readonly rule** `Span` objects (more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#span-objects)) can be used to create readonly rules for cells, rows, columns, the entire sheet, headers and the index. You can use either of the following methods: - Using a span method e.g. `span.readonly()` more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-set-cells-to-read-only). - Using a sheet method e.g. `sheet.readonly(Span)` Or if you need user inserted row/columns in the middle of areas with a readonly rule to also have a readonly rule you can use named spans, more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans). Whether cells, rows or columns are readonly depends on the [`kind`](https://github.com/ragardner/tksheet/wiki/Version-7#get-a-spans-kind) of span. ```python readonly( *key: CreateSpanTypes, readonly: bool = True, ) -> Span ``` Parameters: - `key` (`CreateSpanTypes`) either a span or a type which can create a span. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#creating-a-span) for more information on the types that can create a span. - `readonly` (`bool`) `True` to create a rule and `False` to delete one created without the use of named spans. ___ #### **Deleting a readonly rule** If the readonly rule was created by a named span then the named span must be deleted, more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#deleting-a-named-span). Otherwise you can use either of the following methods to delete/remove readonly rules: - Using a span method e.g. `span.readonly()` with the keyword argument `readonly=False` more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-set-cells-to-read-only). - Using a sheet method e.g. `sheet.readonly(Span)` with the keyword argument `readonly=False` example below: ```python # creating a readonly rule self.sheet.readonly( self.sheet.span("A", header=True), readonly=True, ) # deleting the readonly rule self.sheet.readonly( self.sheet.span("A", header=True), readonly=False, ) ``` Parameters: - `key` (`CreateSpanTypes`) either a span or a type which can create a span. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#creating-a-span) for more information on the types that can create a span. - `readonly` (`bool`) `True` to create a rule and `False` to delete one created without the use of named spans. --- # **Text Font and Alignment** ### **Font** - Font arguments require a three tuple e.g. `("Arial", 12, "normal")` or `("Arial", 12, "bold")` or `("Arial", 12, "italic")` - The table and index currently share a font, it's not possible to change the index font separate from the table font. **Set the table and index font** ```python font( newfont: tuple[str, int, str] | None = None, reset_row_positions: bool = True, ) -> tuple[str, int, str] ``` **Set the header font** ```python header_font(newfont: tuple[str, int, str] | None = None) -> tuple[str, int, str] ``` ### **Text Alignment** There are functions to set the text alignment for specific cells/rows/columns and also functions to set the text alignment for a whole part of the sheet (table/index/header). - Alignment argument (`str`) options are: - `"w"`, `"west"`, `"left"` - `"e"`, `"east"`, `"right"` - `"c"`, `"center"`, `"centre"` Unfortunately vertical alignment is not available. #### **Whole widget text alignment** Set the text alignment for the whole of the table (doesn't include index/header). ```python table_align( align: str = None, redraw: bool = True, ) -> str | Sheet ``` Set the text alignment for the whole of the header. ```python header_align( align: str = None, redraw: bool = True, ) -> str | Sheet ``` Set the text alignment for the whole of the index. ```python row_index_align( align: str = None, redraw: bool = True, ) -> str | Sheet # can also use index_align() which behaves the same ``` #### **Creating a specific cell row or column text alignment rule** The following function is for setting text alignment for specific cells, rows or columns in the table, header and index. `Span` objects (more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#span-objects)) can be used to create text alignment rules for cells, rows, columns, the entire sheet, headers and the index. You can use either of the following methods: - Using a span method e.g. `span.align()` more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-create-text-alignment-rules). - Using a sheet method e.g. `sheet.align(Span)` Or if you need user inserted row/columns in the middle of areas with an alignment rule to also have an alignment rule you can use named spans, more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#named-spans). Whether cells, rows or columns are affected depends on the [`kind`](https://github.com/ragardner/tksheet/wiki/Version-7#get-a-spans-kind) of span. ```python align( *key: CreateSpanTypes, align: str | None = None, redraw: bool = True, ) -> Span ``` Parameters: - `key` (`CreateSpanTypes`) either a span or a type which can create a span. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#creating-a-span) for more information on the types that can create a span. - `align` (`str`, `None`) must be one of the following: - `"w"`, `"west"`, `"left"` - `"e"`, `"east"`, `"right"` - `"c"`, `"center"`, `"centre"` #### **Deleting a specific text alignment rule** If the text alignment rule was created by a named span then the named span must be deleted, more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#deleting-a-named-span). Otherwise you can use either of the following methods to delete/remove specific text alignment rules: - Using a span method e.g. `span.del_align()` more information [here](https://github.com/ragardner/tksheet/wiki/Version-7#using-a-span-to-delete-text-alignment-rules). - Using a sheet method e.g. `sheet.del_align(Span)` details below: ```python del_align( *key: CreateSpanTypes, redraw: bool = True, ) -> Span ``` Parameters: - `key` (`CreateSpanTypes`) either a span or a type which can create a span. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#creating-a-span) for more information on the types that can create a span. #### **Get existing specific text alignments** Cell text alignments: ```python get_cell_alignments() -> dict ``` Row text alignments: ```python get_row_alignments() -> dict ``` Column text alignments: ```python get_column_alignments() -> dict ``` --- # **Getting Selected Cells** #### **Get the currently selected cell** This is always a single cell, the one, for example, in which the cell text editor opens when making single edits. ```python get_currently_selected() -> tuple | Selected ``` Notes: - Returns either: - `namedtuple` of `(row, column, type_, box, iid, fill_iid)`. - `type_` can be `"rows"`, `"columns"` or `"cells"`. - `box` `tuple[int, int, int, int]` are the coordinates of the box that the currently selected box is attached to. - `(from row, from column, up to but not including row, up to but not including column)`. - `iid` is the canvas item id of the currently selected box. - `fill_iid` is the canvas item id of the box that the currently selected box is attached to. - An empty `tuple` if nothing is selected. - Can also use `sheet.selected` as shorter `@property` version of the function. Example: ```python currently_selected = self.sheet.get_currently_selected() if currently_selected: row = currently_selected.row column = currently_selected.column type_ = currently_selected.type_ if self.sheet.selected: ... ``` ___ #### **Get selected rows** ```python get_selected_rows( get_cells: bool = False, get_cells_as_rows: bool = False, return_tuple: bool = False, ) -> tuple[int] | tuple[tuple[int, int]] | set[int] | set[tuple[int, int]] ``` ___ #### **Get selected columns** ```python get_selected_columns( get_cells: bool = False, get_cells_as_columns: bool = False, return_tuple: bool = False, ) -> tuple[int] | tuple[tuple[int, int]] | set[int] | set[tuple[int, int]] ``` ___ #### **Get selected cells** ```python get_selected_cells( get_rows: bool = False, get_columns: bool = False, sort_by_row: bool = False, sort_by_column: bool = False, ) -> list[tuple[int, int]] | set[tuple[int, int]] ``` ___ #### **Generate selected cells** ```python gen_selected_cells( get_rows: bool = False, get_columns: bool = False, ) -> Generator[tuple[int, int]] ``` ___ #### **Get all selection boxes** ```python get_all_selection_boxes() -> tuple[tuple[int, int, int, int]] ``` ___ #### **Get all selection boxes and their types** ```python get_all_selection_boxes_with_types() -> list[tuple[tuple[int, int, int, int], str]] ``` Equivalent to `get_all_selection_boxes_with_types()` but shortened as `@property`. ```python @property boxes() -> list[tuple[tuple[int, int, int, int], str]] ``` ___ #### **Check if cell is selected** ```python cell_selected( r: int, c: int, rows: bool = False, columns: bool = False, ) -> bool ``` - `rows` if `True` also checks if provided cell is part of a selected row. - `columns` if `True` also checks if provided cell is part of a selected column. ___ #### **Check if row is selected** ```python row_selected(r: int, cells: bool = False) -> bool ``` - `cells` if `True` also checks if provided row is selected as part of a cell selection box. ___ #### **Check if column is selected** ```python column_selected(c: int, cells: bool = False) -> bool ``` - `cells` if `True` also checks if provided column is selected as part of a cell selection box. ___ #### **Check if any cells, rows or columns are selected, there are options for exclusions** ```python anything_selected( exclude_columns: bool = False, exclude_rows: bool = False, exclude_cells: bool = False, ) -> bool ``` ___ #### **Check if user has the entire table selected** ```python all_selected() -> bool ``` ___ ```python get_ctrl_x_c_boxes(nrows: bool = True) -> tuple[dict[tuple[int, int, int, int], str], int] ``` ___ ```python @property ctrl_boxes() -> dict[tuple[int, int, int, int], str] ``` ___ ```python get_selected_min_max() -> tuple[int, int, int, int] | tuple[None, None, None, None] ``` - returns `(min_y, min_x, max_y, max_x)` of any selections including rows/columns. --- # **Modifying Selected Cells** ```python set_currently_selected(row: int | None = None, column: int | None = None) -> Sheet ``` ___ ```python select_row(row: int, redraw: bool = True, run_binding_func: bool = True) -> Sheet ``` - `run_binding_func` is only relevant if you have `extra_bindings()` with `"row_select"` bound. ___ ```python select_column(column: int, redraw: bool = True, run_binding_func: bool = True) -> Sheet ``` - `run_binding_func` is only relevant if you have `extra_bindings()` with `"column_select"` bound. ___ ```python select_cell(row: int, column: int, redraw: bool = True, run_binding_func: bool = True) -> Sheet ``` - `run_binding_func` is only relevant if you have `extra_bindings()` with `"cell_select"` bound. ___ ```python select_all(redraw: bool = True, run_binding_func: bool = True) -> Sheet ``` - `run_binding_func` is only relevant if you have `extra_bindings()` with `"select_all"` bound. ___ ```python add_cell_selection( row: int, column: int, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ) -> Sheet ``` - `run_binding_func` is only relevant if you have `extra_bindings()` with `"cell_select"` bound. ___ ```python add_row_selection( row: int, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ) -> Sheet ``` - `run_binding_func` is only relevant if you have `extra_bindings()` with `"row_select"` bound. ___ ```python add_column_selection( column: int, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ) -> Sheet ``` - `run_binding_func` is only relevant if you have `extra_bindings()` with `"column_select"` bound. ___ ```python toggle_select_cell( row: int, column: int, add_selection: bool = True, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ) -> Sheet ``` - `run_binding_func` is only relevant if you have `extra_bindings()` with `"cell_select"` bound. ___ ```python toggle_select_row( row: int, add_selection: bool = True, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ) -> Sheet ``` - `run_binding_func` is only relevant if you have `extra_bindings()` with `"row_select"` bound. ___ ```python toggle_select_column( column: int, add_selection: bool = True, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ) -> Sheet ``` - `run_binding_func` is only relevant if you have `extra_bindings()` with `"column_select"` bound. ___ ```python create_selection_box( r1: int, c1: int, r2: int, c2: int, type_: str = "cells", ) -> int ``` - `type_` either `"cells"` or `"rows"` or `"columns"`. - Returns the canvas item id for the box. ___ ```python @boxes.setter boxes(boxes: Sequence[tuple[tuple[int, int, int, int], str]]) ``` - Can be used to set the Sheets selection boxes, deselects everything before setting. Example: ```python sheet.boxes = [ ((0, 0, 3, 3), "cells"), ((4, 0, 5, 10), "rows"), ] ``` - The above would select a cells box from cell `A1` up to and including cell `C3` and row `4` (in python index, `5` as excel index) where the sheet has 10 columns. - The `str` in the type hint should be either `"cells"` or `"rows"` or `"columns"`. ___ ```python recreate_all_selection_boxes() -> Sheet ``` ___ Deselect a specific cell, row or column. ```python deselect( row: int | None | str = None, column: int | None = None, cell: tuple | None = None, redraw: bool = True, ) -> Sheet ``` ___ Deselect any cell, row or column selection box conflicting with `rows` and/or `columns`. ```python deselect_any( rows: Iterator[int] | int | None, columns: Iterator[int] | int | None, redraw: bool = True, ) -> Sheet ``` --- # **Row Heights and Column Widths** #### **Auto resize column widths to fit the window** To enable auto resizing of columns to the Sheet window use `set_options()` with the keyword argument `auto_resize_columns`. This argument can either be an `int` or `None`. If set as an `int` the columns will automatically resize to fit the width of the window, the `int` value being the minimum of each column in pixels. If `None` it will disable the auto resizing. Example: ```python # auto resize columns, column minimum width set to 150 pixels set_options(auto_resize_columns=150) ``` ___ #### **Auto resize row heights to fit the window** To enable auto resizing of rows to the Sheet window use `set_options()` with the keyword argument `auto_resize_rows`. This argument can either be an `int` or `None`. If set as an `int` the rows will automatically resize to fit the width of the window, the `int` value being the minimum of each row in pixels. If `None` it will disable the auto resizing. Example: ```python # auto resize rows, row minimum width set to 30 pixels set_options(auto_resize_rows=30) ``` ___ #### **Set default column width in pixels** ```python default_column_width(width: int | None = None) -> int ``` - `width` (`int`, `None`) use an `int` to set the width in pixels, `None` does not set the width. ___ #### **Set default row height in pixels or lines** ```python default_row_height(height: int | str | None = None) -> int ``` - `height` (`int`, `str`, `None`) use a numerical `str` for number of lines e.g. `"3"` for a height that fits 3 lines OR `int` for pixels. ___ #### **Set default header bar height in pixels or lines** ```python default_header_height(height: int | str | None = None) -> int ``` - `height` (`int`, `str`, `None`) use a numerical `str` for number of lines e.g. `"3"` for a height that fits 3 lines or `int` for pixels. ___ #### **Set a specific cell size to its text** ```python set_cell_size_to_text( row: int, column: int, only_set_if_too_small: bool = False, redraw: bool = True, ) -> Sheet ``` ___ #### **Set all row heights and column widths to cell text sizes** ```python set_all_cell_sizes_to_text( redraw: bool = True, width: int | None = None, slim: bool = False, ) -> tuple[list[float], list[float]] ``` - Returns the Sheets row positions and column positions in that order. - `width` a minimum width for all column widths set using this function. - `slim` column widths will be set precisely to text width and not add any extra space. ___ #### **Set all column widths to a specific width in pixels** ```python set_all_column_widths( width: int | None = None, only_set_if_too_small: bool = False, redraw: bool = True, recreate_selection_boxes: bool = True, ) -> Sheet ``` - `width` (`int`, `None`) leave `None` to set to cell text sizes for each column. ___ #### **Set all row heights to a specific height in pixels** ```python set_all_row_heights( height: int | None = None, only_set_if_too_small: bool = False, redraw: bool = True, recreate_selection_boxes: bool = True, ) -> Sheet ``` - `height` (`int`, `None`) leave `None` to set to cell text sizes for each row. ___ #### **Set or get a specific column width** ```python column_width( column: int | Literal["all", "displayed"] | None = None, width: int | Literal["default", "text"] | None = None, only_set_if_too_small: bool = False, redraw: bool = True, ) -> Sheet | int ``` ___ #### **Set or get a specific row height** ```python row_height( row: int | Literal["all", "displayed"] | None = None, height: int | Literal["default", "text"] | None = None, only_set_if_too_small: bool = False, redraw: bool = True, ) -> Sheet | int ``` ___ #### **Get the sheets column widths** ```python get_column_widths(canvas_positions: bool = False) -> list[float] ``` - `canvas_positions` (`bool`) gets the actual canvas x coordinates of column lines. ___ #### **Get the sheets row heights** ```python get_row_heights(canvas_positions: bool = False) -> list[float] ``` - `canvas_positions` (`bool`) gets the actual canvas y coordinates of row lines. ___ #### **Get a rows text height** ```python get_row_text_height( row: int, visible_only: bool = False, only_if_too_small: bool = False, ) -> int ``` - Returns a height in pixels which will fit all text in the specified row. - `visible_only` if `True` only measures rows visible on the Sheet. - `only_if_too_small` if `True` will only return a new height if the current row height is too short to accomodate its text. ___ #### **Get a columns text width** ```python get_column_text_width( column: int, visible_only: bool = False, only_if_too_small: bool = False, ) -> int ``` - Returns a width in pixels which will fit all text in the specified column. - `visible_only` if `True` only measures columns visible on the Sheet. - `only_if_too_small` if `True` will only return a new width if the current column width is too thin to accomodate its text. ___ #### **Set or reset displayed column widths/lines** ```python set_column_widths( column_widths: Iterator[int, float] | None = None, canvas_positions: bool = False, reset: bool = False, ) -> Sheet ``` ___ #### **Set or reset displayed row heights/lines** ```python set_row_heights( row_heights: Iterator[int, float] | None = None, canvas_positions: bool = False, reset: bool = False, ) -> Sheet ``` ___ #### **Set the width of the index to a str or existing values** ```python set_width_of_index_to_text(text: None | str = None, *args, **kwargs) -> Sheet ``` - `text` (`str`, `None`) provide a `str` to set the width to or use `None` to set it to existing values in the index. ___ #### **Set the width of the index to a value in pixels** ```python set_index_width(pixels: int, redraw: bool = True) -> Sheet ``` - Note that it disables auto resizing of index. Use [`set_options()`](https://github.com/ragardner/tksheet/wiki/Version-7#sheet-options-and-other-functions) to restore auto resizing. ___ #### **Set the height of the header to a str or existing values** ```python set_height_of_header_to_text(text: None | str = None) -> Sheet ``` - `text` (`str`, `None`) provide a `str` to set the height to or use `None` to set it to existing values in the header. ___ #### **Set the height of the header to a value in pixels** ```python set_header_height_pixels(pixels: int, redraw: bool = True) -> Sheet ``` ___ #### **Set the height of the header to accomodate a number of lines** ```python set_header_height_lines(nlines: int, redraw: bool = True) -> Sheet ``` ___ #### **Delete displayed column lines** ```python del_column_position(idx: int, deselect_all: bool = False) -> Sheet del_column_positions(idxs: Iterator[int] | None = None) -> Sheet ``` ___ #### **Delete displayed row lines** ```python del_row_position(idx: int, deselect_all: bool = False) -> Sheet del_row_positions(idxs: Iterator[int] | None = None) -> Sheet ``` ___ #### **Insert a displayed row line** ```python insert_row_position( idx: Literal["end"] | int = "end", height: int | None = None, deselect_all: bool = False, redraw: bool = False, ) -> Sheet ``` ___ #### **Insert a displayed column line** ```python insert_column_position( idx: Literal["end"] | int = "end", width: int | None = None, deselect_all: bool = False, redraw: bool = False, ) -> Sheet ``` ___ #### **Insert multiple displayed row lines** ```python insert_row_positions( idx: Literal["end"] | int = "end", heights: Sequence[float] | int | None = None, deselect_all: bool = False, redraw: bool = False, ) -> Sheet ``` ___ #### **Insert multiple displayed column lines** ```python insert_column_positions( idx: Literal["end"] | int = "end", widths: Sequence[float] | int | None = None, deselect_all: bool = False, redraw: bool = False, ) -> Sheet ``` ___ #### **Set the number of displayed row lines and column lines** ```python sheet_display_dimensions( total_rows: int | None =None, total_columns: int | None =None, ) -> tuple[int, int] | Sheet ``` ___ #### **Move a displayed row line** ```python move_row_position(row: int, moveto: int) -> Sheet ``` ___ #### **Move a displayed column line** ```python move_column_position(column: int, moveto: int) -> Sheet ``` ___ #### **Get a list of default column width values** ```python get_example_canvas_column_widths(total_cols: int | None = None) -> list[float] ``` ___ #### **Get a list of default row height values** ```python get_example_canvas_row_heights(total_rows: int | None = None) -> list[float] ``` ___ #### **Verify a list of row heights or canvas y coordinates** ```python verify_row_heights(row_heights: list[float], canvas_positions: bool = False) -> bool ``` ___ #### **Verify a list of column widths or canvas x coordinates** ```python verify_column_widths(column_widths: list[float], canvas_positions: bool = False) -> bool ``` ___ #### **Make a row height valid** ```python valid_row_height(height: int) -> int ``` ___ #### **Make a column width valid** ```python valid_column_width(width: int) -> int ``` ___ #### **Get visible rows** ```python @property visible_rows() -> tuple[int, int] ``` - Returns start row, end row - e.g. `start_row, end_row = sheet.visible_rows` ___ #### **Get visible columns** ```python @property visible_columns() -> tuple[int, int] ``` - Returns start column, end column - e.g. `start_column, end_column = sheet.visible_columns` --- # **Identifying Bound Event Mouse Position** The below functions require a mouse click event, for example you could bind right click, example [here](https://github.com/ragardner/tksheet/wiki/Version-7#example-custom-right-click-and-text-editor-functionality), and then identify where the user has clicked. ___ Determine if a tk `event.widget` is the `Sheet`. ```python event_widget_is_sheet( event: object, table: bool = True, index: bool = True, header: bool = True, top_left: bool = True, ) -> bool ``` Notes: - Parameters set to `True` will include events that occurred within that widget. - e.g. If an event occurs in the top left corner of the sheet but the parameter `top_left` is `False` the function will return `False`. ___ Check if any Sheet widgets have focus. ```python has_focus() -> bool: ``` - Includes child widgets such as scroll bars. ___ ```python identify_region(event: object) -> Literal["table", "index", "header", "top left"] ``` ___ ```python identify_row( event: object, exclude_index: bool = False, allow_end: bool = True, ) -> int | None ``` ___ ```python identify_column( event: object, exclude_header: bool = False, allow_end: bool = True, ) -> int | None ``` ___ #### **Sheet control actions** For example: `sheet.bind("", sheet.paste)` ```python cut(event: object = None, validation: bool = True) -> None | EventDataDict paste(event: object = None, validation: bool = True) -> None | EventDataDict delete(event: object = None, validation: bool = True) -> None | EventDataDict copy(event: object = None) -> None | EventDataDict undo(event: object = None) -> None | EventDataDict redo(event: object = None) -> None | EventDataDict ``` - `validation` (`bool`) when `False` disables any bound `edit_validation()` function from running. --- # **Scroll Positions and Cell Visibility** #### **Sync scroll positions between widgets** ```python sync_scroll(widget: object) -> Sheet ``` - Sync scroll positions between `Sheet`s, may or may not work with other widgets. Uses scrollbar positions. Syncing two sheets: ```python self.sheet1.sync_scroll(self.sheet2) ``` Syncing three sheets: ```python # syncs sheet 1 and 2 between each other self.sheet1.sync_scroll(self.sheet2) # syncs sheet 1 and 3 between each other self.sheet1.sync_scroll(self.sheet3) # syncs sheet 2 and 3 between each other self.sheet2.sync_scroll(self.sheet3) ``` #### **Unsync scroll positions between widgets** ```python unsync_scroll(widget: object = None) -> Sheet ``` - Leaving `widget` as `None` unsyncs all previously synced widgets. #### **See or scroll to a specific cell on the sheet** ```python see( row: int = 0, column: int = 0, keep_yscroll: bool = False, keep_xscroll: bool = False, bottom_right_corner: bool = False, check_cell_visibility: bool = True, redraw: bool = True, ) -> Sheet ``` ___ #### **Check if a cell has any part of it visible** ```python cell_visible(r: int, c: int) -> bool ``` ___ #### **Check if a cell is totally visible** ```python cell_completely_visible(r: int, c: int, seperate_axes: bool = False) -> bool ``` - `separate_axes` returns tuple of bools e.g. `(cell y axis is visible, cell x axis is visible)` ___ ```python set_xview(position: None | float = None, option: str = "moveto") -> Sheet | tuple[float, float] ``` Notes: - If `position` is `None` then `tuple[float, float]` of main table `xview()` is returned. - `xview` and `xview_moveto` have the same behaviour. ___ ```python set_yview(position: None | float = None, option: str = "moveto") -> Sheet | tuple[float, float] ``` - If `position` is `None` then `tuple[float, float]` of main table `yview()` is returned. - `yview` and `yview_moveto` have the same behaviour. ___ ```python get_xview() -> tuple[float, float] ``` ___ ```python get_yview() -> tuple[float, float] ``` ___ ```python set_view(x_args: [str, float], y_args: [str, float]) -> Sheet ``` --- # **Hiding Columns** Note that once you have hidden columns you can use the function `displayed_column_to_data(column)` to retrieve a column data index from a displayed index. #### **Display only certain columns** ```python display_columns( columns: None | Literal["all"] | Iterator[int] = None, all_columns_displayed: None | bool = None, reset_col_positions: bool = True, refresh: bool = False, redraw: bool = False, deselect_all: bool = True, **kwargs, ) -> list[int] | None ``` Parameters: - `columns` (`int`, `iterable`, `"all"`) are the columns to be displayed, omit the columns to be hidden. - Use argument `True` with `all_columns_displayed` to display all columns, use `False` to display only the columns you've set using the `columns` arg. - You can also use the keyword argument `all_displayed` instead of `all_columns_displayed`. Examples: ```python # display all columns self.sheet.display_columns("all") # displaying specific columns only self.sheet.display_columns([2, 4, 7], all_displayed = False) ``` ___ #### **Get all columns displayed boolean** **Get the bool** ```python @property all_columns() ``` - e.g. `get_all_columns_displayed = sheet.all_columns`. **Set the bool** ```python @all_columns.setter all_columns(a: bool) ``` e.g. `sheet.all_columns = True`. ```python all_columns_displayed(a: bool | None = None) -> bool ``` - `a` (`bool`, `None`) Either set by using `bool` or get by leaving `None` e.g. `all_columns_displayed()`. ___ #### **Hide specific columns** ```python hide_columns( columns: int | set | Iterator[int] = set(), redraw: bool = True, deselect_all: bool = True, data_indexes: bool = False, ) -> Sheet ``` Parameters: - **NOTE**: `columns` (`int`) by default uses displayed column indexes, not data indexes. In other words the indexes of the columns displayed on the screen are the ones that are hidden, this is useful when used in conjunction with `get_selected_columns()`. - `data_indexes` when `False` it makes the `columns` parameter indexes represent displayed columns and not the underlying Sheet data columns. When `True` the indexes represent data indexes. Example: ```python columns_to_hide = set(sheet.data_c(c) for c in sheet.get_selected_columns()) sheet.hide_columns( columns_to_hide, data_indexes=True, ) ``` ___ #### **Unhide specific columns** ```python show_columns( columns: int | Iterator[int], redraw: bool = True, deselect_all: bool = True, ) -> Sheet ``` Parameters: - **NOTE**: `columns` (`int`) uses data column indexes, not displayed indexes. In other words the indexes of the columns which represent the underlying data are shown. Notes: - Will return if all columns are currently displayed (`Sheet.all_columns`). Example: ```python # converting displayed column indexes to data indexes using data_c(c) columns = set(sheet.data_c(c) for c in sheet.get_selected_columns()) # hiding columns sheet.hide_columns( columns, data_indexes=True, ) # showing them again sheet.show_columns(columns) ``` ___ #### **Displayed column index to data** Convert a displayed column index to a data index. If the internal `all_columns_displayed` attribute is `True` then it will simply return the provided argument. ```python displayed_column_to_data(c) data_c(c) ``` ___ #### **Get currently displayed columns** ```python @property displayed_columns() -> list[int] ``` - e.g. `columns = sheet.displayed_columns` --- # **Hiding Rows** Note that once you have hidden rows you can use the function `displayed_row_to_data(row)` to retrieve a row data index from a displayed index. #### **Display only certain rows** ```python display_rows( rows: None | Literal["all"] | Iterator[int] = None, all_rows_displayed: None | bool = None, reset_row_positions: bool = True, refresh: bool = False, redraw: bool = False, deselect_all: bool = True, **kwargs, ) -> list[int] | None ``` Parameters: - `rows` (`int`, `iterable`, `"all"`) are the rows to be displayed, omit the rows to be hidden. - Use argument `True` with `all_rows_displayed` to display all rows, use `False` to display only the rows you've set using the `rows` arg. - You can also use the keyword argument `all_displayed` instead of `all_rows_displayed`. Examples: - An example of row filtering using this function can be found [here](https://github.com/ragardner/tksheet/wiki/Version-7#example-header-dropdown-boxes-and-row-filtering). - More examples below: ```python # display all rows self.sheet.display_rows("all") # display specific rows only self.sheet.display_rows([2, 4, 7], all_displayed = False) ``` ___ #### **Hide specific rows** ```python hide_rows( rows: int | set | Iterator[int] = set(), redraw: bool = True, deselect_all: bool = True, data_indexes: bool = False, ) -> Sheet ``` Parameters: - **NOTE**: `rows` (`int`) by default uses displayed row indexes, not data indexes. In other words the indexes of the rows displayed on the screen are the ones that are hidden, this is useful when used in conjunction with `get_selected_rows()`. - `data_indexes` when `False` it makes the `rows` parameter indexes represent displayed rows and not the underlying Sheet data rows. When `True` the indexes represent data indexes. Example: ```python rows_to_hide = set(sheet.data_r(r) for r in sheet.get_selected_rows()) sheet.hide_rows( rows_to_hide, data_indexes=True, ) ``` ___ #### **Unhide specific rows** ```python show_rows( rows: int | Iterator[int], redraw: bool = True, deselect_all: bool = True, ) -> Sheet ``` Parameters: - **NOTE**: `rows` (`int`) uses data row indexes, not displayed indexes. In other words the indexes of the rows which represent the underlying data are shown. Notes: - Will return if all rows are currently displayed (`Sheet.all_rows`). Example: ```python # converting displayed row indexes to data indexes using data_r(r) rows = set(sheet.data_r(r) for r in sheet.get_selected_rows()) # hiding rows sheet.hide_rows( rows, data_indexes=True, ) # showing them again sheet.show_rows(rows) ``` ___ #### **Get or set the all rows displayed boolean** **Get the bool** ```python @property all_rows() ``` - e.g. `get_all_rows_displayed = sheet.all_rows`. **Set the bool** ```python @all_rows.setter all_rows(a: bool) ``` e.g. `sheet.all_rows = True`. ```python all_rows_displayed(a: bool | None = None) -> bool ``` - `a` (`bool`, `None`) Either set by using `bool` or get by leaving `None` e.g. `all_rows_displayed()`. ___ #### **Displayed row index to data** Convert a displayed row index to a data index. If the internal `all_rows_displayed` attribute is `True` then it will simply return the provided argument. ```python displayed_row_to_data(r) data_r(r) ``` ___ #### **Get currently displayed rows** ```python @property displayed_rows() -> list[int] ``` - e.g. `rows = sheet.displayed_rows` --- # **Hiding Sheet Elements** #### **Hide parts of the Sheet or all of it** ```python hide( canvas: Literal[ "all", "row_index", "header", "top_left", "x_scrollbar", "y_scrollbar", ] = "all", ) -> Sheet ``` - `canvas` (`str`) options are `all`, `row_index`, `header`, `top_left`, `x_scrollbar`, `y_scrollbar` - `all` hides the entire table and is the default. ___ #### **Show parts of the Sheet or all of it** ```python show( canvas: Literal[ "all", "row_index", "header", "top_left", "x_scrollbar", "y_scrollbar", ] = "all", ) -> Sheet ``` - `canvas` (`str`) options are `all`, `row_index`, `header`, `top_left`, `x_scrollbar`, `y_scrollbar` - `all` shows the entire table and is the default. --- # **Sheet Height and Width** #### **Modify widget height and width in pixels** ```python height_and_width( height: int | None = None, width: int | None = None, ) -> Sheet ``` - `height` (`int`, `None`) set a height in pixels. - `width` (`int`, `None`) set a width in pixels. If both arguments are `None` then table will reset to default tkinter canvas dimensions. ___ ```python get_frame_y(y: int) -> int ``` - Adds the height of the Sheets header to a y position. ___ ```python get_frame_x(x: int) -> int ``` - Adds the width of the Sheets index to an x position. --- # **Cell Text Editor** #### **Open the currently selected cell in the main table** ```python open_cell(ignore_existing_editor: bool = True) -> Sheet ``` - Function utilises the currently selected cell in the main table, even if a column/row is selected, to open a non selected cell first use `set_currently_selected()` to set the cell to open. ___ #### **Open the currently selected cell but in the header** ```python open_header_cell(ignore_existing_editor: bool = True) -> Sheet ``` - Also uses currently selected cell, which you can set with `set_currently_selected()`. ___ #### **Open the currently selected cell but in the index** ```python open_index_cell(ignore_existing_editor: bool = True) -> Sheet ``` - Also uses currently selected cell, which you can set with `set_currently_selected()`. ___ #### **Set the cell text editor value if it is open** **Table:** ```python set_text_editor_value( text: str = "", ) -> Sheet ``` **Index:** ```python set_index_text_editor_value( text: str = "", ) -> Sheet ``` **Header:** ```python set_header_text_editor_value( text: str = "", ) -> Sheet ``` ___ #### **Close any existing text editor** ```python close_text_editor(set_data: bool = True) -> Sheet ``` Notes: - Closes any open text editors, including header and index. - Also closes any existing `"normal"` state dropdown box. Parameters: - `set_data` (`bool`) when `True` sets the cell data to the text editor value (if it is valid). When `False` the text editor is closed without setting data. ___ #### **Get an existing text editors value** ```python get_text_editor_value() -> str | None ``` Notes: - `None` is returned if no text editor exists, a `str` of the text editors value will be returned if it does. ___ #### **Hide all text editors** ```python destroy_text_editor(event: object = None) -> Sheet ``` ___ #### **Get the table tk Text widget which acts as the text editor** ```python get_text_editor_widget(event: object = None) -> tk.Text | None ``` ___ #### **Bind additional keys to the text editor window** ```python bind_key_text_editor(key: str, function: Callable) -> Sheet ``` ___ #### **Unbind additional text editors bindings set using the above function** ```python unbind_key_text_editor(key: str) -> Sheet ``` --- # **Sheet Options and Other Functions** ```python set_options(redraw: bool = True, **kwargs) -> Sheet ``` The list of key word arguments available for `set_options()` are as follows, [see here](https://github.com/ragardner/tksheet/wiki/Version-7#initialization-options) as a guide for argument types. ```python auto_resize_columns auto_resize_rows to_clipboard_delimiter to_clipboard_quotechar to_clipboard_lineterminator from_clipboard_delimiters show_dropdown_borders show_default_header_for_empty show_default_index_for_empty selected_rows_to_end_of_window horizontal_grid_to_end_of_window vertical_grid_to_end_of_window paste_insert_column_limit paste_insert_row_limit paste_can_expand_x paste_can_expand_y arrow_key_down_right_scroll_page enable_edit_cell_auto_resize page_up_down_select_row display_selected_fg_over_highlights show_horizontal_grid show_vertical_grid empty_horizontal empty_vertical default_row_height default_column_width default_header_height default_row_index_width row_drag_and_drop_perform column_drag_and_drop_perform auto_resize_default_row_index default_header default_row_index max_column_width max_row_height max_header_height max_index_width font header_font index_font show_selected_cells_border theme top_left_bg top_left_fg top_left_fg_highlight table_bg table_grid_fg table_fg table_selected_box_cells_fg table_selected_box_rows_fg table_selected_box_columns_fg table_selected_cells_border_fg table_selected_cells_bg table_selected_cells_fg table_selected_rows_border_fg table_selected_rows_bg table_selected_rows_fg table_selected_columns_border_fg table_selected_columns_bg table_selected_columns_fg header_bg header_border_fg header_grid_fg header_fg header_selected_cells_bg header_selected_cells_fg header_selected_columns_bg header_selected_columns_fg index_bg index_border_fg index_grid_fg index_fg index_selected_cells_bg index_selected_cells_fg index_selected_rows_bg index_selected_rows_fg resizing_line_fg drag_and_drop_bg outline_thickness outline_color frame_bg popup_menu_font popup_menu_fg popup_menu_bg popup_menu_highlight_bg popup_menu_highlight_fg # scroll bars vertical_scroll_background horizontal_scroll_background vertical_scroll_troughcolor horizontal_scroll_troughcolor vertical_scroll_lightcolor horizontal_scroll_lightcolor vertical_scroll_darkcolor horizontal_scroll_darkcolor vertical_scroll_bordercolor horizontal_scroll_bordercolor vertical_scroll_active_bg horizontal_scroll_active_bg vertical_scroll_not_active_bg horizontal_scroll_not_active_bg vertical_scroll_pressed_bg horizontal_scroll_pressed_bg vertical_scroll_active_fg horizontal_scroll_active_fg vertical_scroll_not_active_fg horizontal_scroll_not_active_fg vertical_scroll_pressed_fg horizontal_scroll_pressed_fg scrollbar_theme_inheritance scrollbar_show_arrows vertical_scroll_arrowsize horizontal_scroll_arrowsize vertical_scroll_borderwidth horizontal_scroll_borderwidth vertical_scroll_gripcount horizontal_scroll_gripcount # for changing the in-built right click menus labels # use a string as an argument edit_header_label edit_header_accelerator edit_index_label edit_index_accelerator edit_cell_label edit_cell_accelerator cut_label cut_accelerator cut_contents_label cut_contents_accelerator copy_label copy_accelerator copy_contents_label copy_contents_accelerator paste_label paste_accelerator delete_label delete_accelerator clear_contents_label clear_contents_accelerator delete_columns_label delete_columns_accelerator insert_columns_left_label insert_columns_left_accelerator insert_column_label insert_column_accelerator insert_columns_right_label insert_columns_right_accelerator delete_rows_label delete_rows_accelerator insert_rows_above_label insert_rows_above_accelerator insert_rows_below_label insert_rows_below_accelerator insert_row_label insert_row_accelerator select_all_label select_all_accelerator undo_label undo_accelerator # for changing the keyboard bindings for copy, paste, etc. # use a list of strings as an argument copy_bindings cut_bindings paste_bindings undo_bindings redo_bindings delete_bindings select_all_bindings tab_bindings up_bindings right_bindings down_bindings left_bindings prior_bindings next_bindings ``` Notes: - A dictionary can be provided instead of using the keyword arguments: ```python kwargs = { "copy_bindings": [ "", "", ], "cut_bindings": [ "", "", ], } sheet.set_options(**kwargs) ``` ___ Get internal storage dictionary of highlights, readonly cells, dropdowns etc. Specifically for cell options. ```python get_cell_options(key: None | str = None, canvas: Literal["table", "row_index", "header"] = "table") -> dict ``` ___ Get internal storage dictionary of highlights, readonly rows, dropdowns etc. Specifically for row options. ```python get_row_options(key: None | str = None) -> dict ``` ___ Get internal storage dictionary of highlights, readonly columns, dropdowns etc. Specifically for column options. ```python get_column_options(key: None | str = None) -> dict ``` ___ Get internal storage dictionary of highlights, readonly header cells, dropdowns etc. Specifically for header options. ```python get_header_options(key: None | str = None) -> dict ``` ___ Get internal storage dictionary of highlights, readonly row index cells, dropdowns etc. Specifically for row index options. ```python get_index_options(key: None | str = None) -> dict ``` ___ Delete any formats, alignments, dropdown boxes, check boxes, highlights etc. that are larger than the sheets currently held data, includes row index and header in measurement of dimensions. ```python del_out_of_bounds_options() -> Sheet ``` ___ Delete all alignments, dropdown boxes, check boxes, highlights etc. ```python reset_all_options() -> Sheet ``` ___ Flash a dashed box of chosen dimensions. ```python show_ctrl_outline( canvas: Literal["table"] = "table", start_cell: tuple[int, int] = (0, 0), end_cell: tuple[int, int] = (1, 1), ) -> Sheet ``` ___ Various functions related to the Sheets internal undo and redo stacks. ```python # clears both undos and redos reset_undos() -> Sheet # get the Sheets modifiable deque variables which store changes for undo and redo get_undo_stack() -> deque get_redo_stack() -> deque # set the Sheets undo and redo stacks, returns Sheet widget set_undo_stack(stack: deque) -> Sheet set_redo_stack(stack: deque) -> Sheet ``` ___ Refresh the table. ```python refresh(redraw_header: bool = True, redraw_row_index: bool = True) -> Sheet ``` ___ Refresh the table. ```python redraw(redraw_header: bool = True, redraw_row_index: bool = True) -> Sheet ``` --- # **Treeview Mode** This is a work in progress and still being tested and changed. --- # **Progress Bars** Progress bars can be created for individual cells. They will only update when tkinter updates. #### **Create a progress bar** ```python create_progress_bar( row: int, column: int, bg: str, fg: str, name: Hashable, percent: int = 0, del_when_done: bool = False, ) -> Sheet ``` - `row` the row coordinate to create the bar at. - `column` the column coordinate to create the bar at. - `bg` the background color for the bar. - `fg` the text color for the bar. - `name` a name is required for easy referral to the bar later on. - Names can be re-used for multiple bars. - `percent` the starting progress of the bar as an `int` either `0`, `100` or a number in between. - `del_when_done` if `True` the `Sheet` will automatically delete the progress bar once it is modified with a percent of `100` or more. ___ #### **Modify progress bars** ```python progress_bar( name: Hashable | None = None, cell: tuple[int, int] | None = None, percent: int | None = None, bg: str | None = None, fg: str | None = None, ) -> Sheet ``` Either `name` or `cell` can be used to refer to existing progress bars: - `name` the name given to a progress bar, or multiple progress bars. - If this parameter is used then `cell` will not be used. - Will modify all progress bars with the given name. - `cell` (`tuple[int, int]`) a tuple of two `int`s representing the progress bars location, `(row, column)`. - Can only refer to one progress bar. Values that can be modified: - `bg` the background color for the bar, leave as `None` for no change. - `fg` the text color for the bar, leave as `None` for no change. - `percent` the progress of the bar as an `int` either `0`, `100` or a number in between, leave as `None` for no change. ___ #### **Delete progress bars** Note that this will delete the progress bars data from the Sheet as well. ```python del_progress_bar( name: Hashable | None = None, cell: tuple[int, int] | None = None, ) -> Sheet ``` Either `name` or `cell` can be used to refer to existing progress bars: - `name` the name given to a progress bar, or multiple progress bars. - Will delete all progress bars with the given name. - If this parameter is used then `cell` will not be used. - `cell` (`tuple[int, int]`) a tuple of two `int`s representing the progress bars location, `(row, column)`. - Can only refer to one progress bar. --- # **Tags** Tags can be used to keep track of specific cells, rows and columns wherever they move. Note that: - If rows/columns are deleted the the associated tags will be also. - There is no equivalent `tag_bind` functionality at this time. - All tagging functions use data indexes (not displayed indexes) - this is only relevant when there are hidden rows/columns. #### **Tag a specific cell** ```python tag_cell( cell: tuple[int, int], *tags, ) -> Sheet ``` Example: ```python sheet.tag_cell((0, 0), "tag a1", "tag a1 no.2") ``` ___ #### **Tag specific rows** ```python tag_rows( rows: int | Iterator[int], *tags, ) -> Sheet ``` ___ #### **Tag specific columns** ```python tag_columns( columns: int | Iterator[int], *tags, ) -> Sheet ``` ___ #### **Tag using a span** ```python tag( *key: CreateSpanTypes, tags: Iterator[str] | str = "", ) -> Sheet ``` ___ #### **Untag** ```python untag( cell: tuple[int, int] | None = None, rows: int | Iterator[int] | None = None, columns: int | Iterator[int] | None = None, ) -> Sheet ``` - This removes all tags from the cell, rows or columns provided. ___ #### **Delete tags** ```python tag_del( *tags, cells: bool = True, rows: bool = True, columns: bool = True, ) -> Sheet ``` - This deletes the provided tags from all `cells` if `True`, `rows` if `True` and `columns` if `True`. ___ #### **Get cells, rows or columns associated with tags** ```python tag_has( *tags, ) -> DotDict ``` Notes: - Returns all cells, rows and columns associated with **any** of the provided tags in the form of a `dict` with dot notation accessbility which has the following keys: - `"cells"` - with a value of `set[tuple[int, int]]` where the `tuple`s are cell coordinates - `(row, column)`. - `"rows"` - with a value of `set[int]` where the `int`s are rows. - `"columns"` - with a value of `set[int]` where the `int`s are columns. - Returns data indexes. - This function **updates** the `set`s with any cells/rows/columns associated with each tag, it does **not** return cells/rows/columns that have all the provided tags. Example: ```python sheet.tag_rows((0, 1), "row tag a", "row tag b") sheet.tag_rows(4, "row tag b") sheet.tag_rows(5, "row tag c") sheet.tag_rows(6, "row tag d") with_tags = sheet.tag_has("row tag b", "row tag c") print (with_tags.rows) # prints {0, 1, 4, 5} ``` --- # **Example Loading Data from Excel** Using `pandas` library, requires additional libraries: - `pandas` - `openpyxl` ```python from tksheet import Sheet import tkinter as tk import pandas as pd class demo(tk.Tk): def __init__(self): tk.Tk.__init__(self) self.grid_columnconfigure(0, weight = 1) self.grid_rowconfigure(0, weight = 1) self.frame = tk.Frame(self) self.frame.grid_columnconfigure(0, weight = 1) self.frame.grid_rowconfigure(0, weight = 1) self.sheet = Sheet(self.frame, data = pd.read_excel("excel_file.xlsx", # filepath here #sheet_name = "sheet1", # optional sheet name here engine = "openpyxl", header = None).values.tolist()) self.sheet.enable_bindings() self.frame.grid(row = 0, column = 0, sticky = "nswe") self.sheet.grid(row = 0, column = 0, sticky = "nswe") app = demo() app.mainloop() ``` --- # **Example Custom Right Click and Text Editor Validation** This is to demonstrate: - Adding your own commands to the in-built right click popup menu (or how you might start making your own right click menu functionality) - Validating text editor input; in this demonstration the validation removes spaces from user input. ```python from tksheet import Sheet import tkinter as tk class demo(tk.Tk): def __init__(self): tk.Tk.__init__(self) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.frame = tk.Frame(self) self.frame.grid_columnconfigure(0, weight=1) self.frame.grid_rowconfigure(0, weight=1) self.sheet = Sheet(self.frame, data=[[f"Row {r}, Column {c}\nnewline1\nnewline2" for c in range(50)] for r in range(500)]) self.sheet.enable_bindings( "single_select", "drag_select", "edit_cell", "paste", "cut", "copy", "delete", "select_all", "column_select", "row_select", "column_width_resize", "double_click_column_resize", "arrowkeys", "row_height_resize", "double_click_row_resize", "right_click_popup_menu", "rc_select", ) self.sheet.extra_bindings("begin_edit_cell", self.begin_edit_cell) self.sheet.edit_validation(self.validate_edits) self.sheet.popup_menu_add_command("Say Hello", self.new_right_click_button) self.frame.grid(row=0, column=0, sticky="nswe") self.sheet.grid(row=0, column=0, sticky="nswe") def new_right_click_button(self, event=None): print ("Hello World!") def begin_edit_cell(self, event=None): return event.value def validate_edits(self, event): # remove spaces from any cell edits, including paste if isinstance(event.value, str) and event.value: return event.value.replace(" ", "") app = demo() app.mainloop() ``` - If you want a totally new right click menu you can use `self.sheet.bind("<3>", )` with a `tk.Menu` of your own design (right click is `<2>` on MacOS) and don't use `"right_click_popup_menu"` with `enable_bindings()`. --- # **Example Displaying Selections** ```python from tksheet import ( Sheet, num2alpha, ) import tkinter as tk class demo(tk.Tk): def __init__(self): tk.Tk.__init__(self) self.grid_columnconfigure(0, weight = 1) self.grid_rowconfigure(0, weight = 1) self.frame = tk.Frame(self) self.frame.grid_columnconfigure(0, weight = 1) self.frame.grid_rowconfigure(0, weight = 1) self.sheet = Sheet(self.frame, data = [[f"Row {r}, Column {c}\nnewline1\nnewline2" for c in range(50)] for r in range(500)]) self.sheet.enable_bindings("all", "ctrl_select") self.sheet.extra_bindings([("all_select_events", self.sheet_select_event)]) self.show_selections = tk.Label(self) self.frame.grid(row = 0, column = 0, sticky = "nswe") self.sheet.grid(row = 0, column = 0, sticky = "nswe") self.show_selections.grid(row = 1, column = 0, sticky = "nsw") def sheet_select_event(self, event = None): if event.eventname == "select" and event.selection_boxes and event.selected: # get the most recently selected box in case there are multiple box = next(reversed(event.selection_boxes)) type_ = event.selection_boxes[box] if type_ == "cells": self.show_selections.config(text=f"{type_.capitalize()}: {box.from_r + 1},{box.from_c + 1} : {box.upto_r},{box.upto_c}") elif type_ == "rows": self.show_selections.config(text=f"{type_.capitalize()}: {box.from_r + 1} : {box.upto_r}") elif type_ == "columns": self.show_selections.config(text=f"{type_.capitalize()}: {num2alpha(box.from_c)} : {num2alpha(box.upto_c - 1)}") else: self.show_selections.config(text="") app = demo() app.mainloop() ``` --- # **Example List Box** This is to demonstrate some simple customization to make a different sort of widget (a list box). ```python from tksheet import Sheet import tkinter as tk class Sheet_Listbox(Sheet): def __init__(self, parent, values = []): Sheet.__init__(self, parent = parent, show_horizontal_grid = False, show_vertical_grid = False, show_header = False, show_row_index = False, show_top_left = False, empty_horizontal = 0, empty_vertical = 0) if values: self.values(values) def values(self, values = []): self.set_sheet_data([[v] for v in values], reset_col_positions = False, reset_row_positions = False, redraw = False, verify = False) self.set_all_cell_sizes_to_text() class demo(tk.Tk): def __init__(self): tk.Tk.__init__(self) self.grid_columnconfigure(0, weight = 1) self.grid_rowconfigure(0, weight = 1) self.listbox = Sheet_Listbox(self, values = [f"_________ Item {i} _________" for i in range(2000)]) self.listbox.grid(row = 0, column = 0, sticky = "nswe") #self.listbox.values([f"new values {i}" for i in range(50)]) set values app = demo() app.mainloop() ``` --- # **Example Header Dropdown Boxes and Row Filtering** A very simple demonstration of row filtering using header dropdown boxes. ```python from tksheet import ( Sheet, num2alpha as n2a, ) import tkinter as tk class demo(tk.Tk): def __init__(self): tk.Tk.__init__(self) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.frame = tk.Frame(self) self.frame.grid_columnconfigure(0, weight=1) self.frame.grid_rowconfigure(0, weight=1) self.data = [ ["3", "c", "z"], ["1", "a", "x"], ["1", "b", "y"], ["2", "b", "y"], ["2", "c", "z"], ] self.sheet = Sheet( self.frame, data=self.data, column_width=180, theme="dark", height=700, width=1100, ) self.sheet.enable_bindings( "copy", "rc_select", "arrowkeys", "double_click_column_resize", "column_width_resize", "column_select", "row_select", "drag_select", "single_select", "select_all", ) self.frame.grid(row=0, column=0, sticky="nswe") self.sheet.grid(row=0, column=0, sticky="nswe") self.sheet.dropdown( self.sheet.span(n2a(0), header=True, table=False), values=["all", "1", "2", "3"], set_value="all", selection_function=self.header_dropdown_selected, text="Header A Name", ) self.sheet.dropdown( self.sheet.span(n2a(1), header=True, table=False), values=["all", "a", "b", "c"], set_value="all", selection_function=self.header_dropdown_selected, text="Header B Name", ) self.sheet.dropdown( self.sheet.span(n2a(2), header=True, table=False), values=["all", "x", "y", "z"], set_value="all", selection_function=self.header_dropdown_selected, text="Header C Name", ) def header_dropdown_selected(self, event=None): hdrs = self.sheet.headers() # this function is run before header cell data is set by dropdown selection # so we have to get the new value from the event hdrs[event.loc] = event.value if all(dd == "all" for dd in hdrs): self.sheet.display_rows("all") else: rows = [ rn for rn, row in enumerate(self.data) if all(row[c] == e or e == "all" for c, e in enumerate(hdrs)) ] self.sheet.display_rows(rows=rows, all_displayed=False) self.sheet.redraw() app = demo() app.mainloop() ``` --- # **Example Readme Screenshot Code** The code used to make a screenshot for the readme file. ```python from tksheet import ( Sheet, num2alpha as n2a, ) import tkinter as tk class demo(tk.Tk): def __init__(self): tk.Tk.__init__(self) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.frame = tk.Frame(self) self.frame.grid_columnconfigure(0, weight=1) self.frame.grid_rowconfigure(0, weight=1) self.sheet = Sheet( self.frame, empty_horizontal=0, empty_vertical=0, paste_can_expand_x=True, paste_can_expand_y=True, align="w", header_align="c", data=[[f"Row {r}, Column {c}\nnewline 1\nnewline 2" for c in range(6)] for r in range(21)], headers=[ "Dropdown Column", "Checkbox Column", "Center Aligned Column", "East Aligned Column", "", "", ], theme="dark", height=520, width=930, ) self.sheet.enable_bindings("all", "edit_index", "edit_header") self.sheet.popup_menu_add_command( "Hide Rows", self.hide_rows, table_menu=False, header_menu=False, empty_space_menu=False, ) self.sheet.popup_menu_add_command( "Show All Rows", self.show_rows, table_menu=False, header_menu=False, empty_space_menu=False, ) self.sheet.popup_menu_add_command( "Hide Columns", self.hide_columns, table_menu=False, index_menu=False, empty_space_menu=False, ) self.sheet.popup_menu_add_command( "Show All Columns", self.show_columns, table_menu=False, index_menu=False, empty_space_menu=False, ) self.frame.grid(row=0, column=0, sticky="nswe") self.sheet.grid(row=0, column=0, sticky="nswe") colors = ( "#509f56", "#64a85b", "#78b160", "#8cba66", "#a0c36c", "#b4cc71", "#c8d576", "#dcde7c", "#f0e782", "#ffec87", "#ffe182", "#ffdc7d", "#ffd77b", "#ffc873", "#ffb469", "#fea05f", "#fc8c55", "#fb784b", "#fa6441", "#f85037", ) self.sheet.align_columns(columns=2, align="c") self.sheet.align_columns(columns=3, align="e") self.sheet.create_index_dropdown(r=0, values=["Dropdown"] + [f"{i}" for i in range(15)]) self.sheet.create_index_checkbox(r=3, checked=True, text="Checkbox") self.sheet.create_dropdown(r="all", c=0, values=["Dropdown"] + [f"{i}" for i in range(15)]) self.sheet.create_checkbox(r="all", c=1, checked=True, text="Checkbox") self.sheet.create_header_dropdown(c=0, values=["Header Dropdown"] + [f"{i}" for i in range(15)]) self.sheet.create_header_checkbox(c=1, checked=True, text="Header Checkbox") self.sheet.align_cells(5, 0, align="c") self.sheet.highlight_cells(5, 0, bg="gray50", fg="blue") self.sheet.highlight_cells(17, canvas="index", bg="yellow", fg="black") self.sheet.highlight_cells(12, 1, bg="gray90", fg="purple") for r in range(len(colors)): self.sheet.highlight_cells(row=r, column=3, fg=colors[r]) self.sheet.highlight_cells(row=r, column=4, bg=colors[r], fg="black") self.sheet.highlight_cells(row=r, column=5, bg=colors[r], fg="purple") self.sheet.highlight_cells(column=5, canvas="header", bg="white", fg="purple") self.sheet.align(n2a(2), align="c") self.sheet.align(n2a(3), align="e") self.sheet.dropdown( self.sheet.span("A", header=True), values=["Dropdown"] + [f"{i}" for i in range(15)], ) self.sheet.checkbox( self.sheet.span("B", header=True), checked=True, text="Checkbox", ) self.sheet.align(5, 0, align="c") self.sheet.highlight(5, 0, bg="gray50", fg="blue") self.sheet.highlight( self.sheet.span(17, index=True, table=False), bg="yellow", fg="black", ) self.sheet.highlight(12, 1, bg="gray90", fg="purple") for r in range(len(colors)): self.sheet.highlight(r, 3, fg=colors[r]) self.sheet.highlight(r, 4, bg=colors[r], fg="black") self.sheet.highlight(r, 5, bg=colors[r], fg="purple") self.sheet.highlight( self.sheet.span(n2a(5), header=True, table=False), bg="white", fg="purple", ) self.sheet.set_all_column_widths() self.sheet.extra_bindings("all", self.all_extra_bindings) def hide_rows(self, event=None): rows = self.sheet.get_selected_rows() if rows: self.sheet.hide_rows(rows) def show_rows(self, event=None): self.sheet.display_rows("all", redraw=True) def hide_columns(self, event=None): columns = self.sheet.get_selected_columns() if columns: self.sheet.hide_columns(columns) def show_columns(self, event=None): self.sheet.display_columns("all", redraw=True) def all_extra_bindings(self, event=None): return event.value app = demo() app.mainloop() ``` --- # **Example Saving tksheet as a csv File** To both load a csv file and save tksheet data as a csv file not including headers and index. ```python from tksheet import Sheet import tkinter as tk from tkinter import filedialog import csv from os.path import normpath import io class demo(tk.Tk): def __init__(self): tk.Tk.__init__(self) self.withdraw() self.title("tksheet") self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.frame = tk.Frame(self) self.frame.grid_columnconfigure(0, weight=1) self.frame.grid_rowconfigure(0, weight=1) self.sheet = Sheet(self.frame, data=[[f"Row {r}, Column {c}" for c in range(6)] for r in range(21)]) self.sheet.enable_bindings("all", "edit_header", "edit_index") self.frame.grid(row=0, column=0, sticky="nswe") self.sheet.grid(row=0, column=0, sticky="nswe") self.sheet.popup_menu_add_command("Open csv", self.open_csv) self.sheet.popup_menu_add_command("Save sheet", self.save_sheet) self.sheet.set_all_cell_sizes_to_text() self.sheet.change_theme("light green") # create a span which encompasses the table, header and index # all data values, no displayed values self.sheet_span = self.sheet.span( header=True, index=True, hdisp=False, idisp=False, ) # center the window and unhide self.update_idletasks() w = self.winfo_screenwidth() - 20 h = self.winfo_screenheight() - 70 size = (900, 500) self.geometry("%dx%d+%d+%d" % (size + ((w / 2 - size[0] / 2), h / 2 - size[1] / 2))) self.deiconify() def save_sheet(self): filepath = filedialog.asksaveasfilename( parent=self, title="Save sheet as", filetypes=[("CSV File", ".csv"), ("TSV File", ".tsv")], defaultextension=".csv", confirmoverwrite=True, ) if not filepath or not filepath.lower().endswith((".csv", ".tsv")): return try: with open(normpath(filepath), "w", newline="", encoding="utf-8") as fh: writer = csv.writer( fh, dialect=csv.excel if filepath.lower().endswith(".csv") else csv.excel_tab, lineterminator="\n", ) writer.writerows(self.sheet_span.data) except Exception as error: print(error) return def open_csv(self): filepath = filedialog.askopenfilename(parent=self, title="Select a csv file") if not filepath or not filepath.lower().endswith((".csv", ".tsv")): return try: with open(normpath(filepath), "r") as filehandle: filedata = filehandle.read() self.sheet.reset() self.sheet_span.data = [ r for r in csv.reader( io.StringIO(filedata), dialect=csv.Sniffer().sniff(filedata), skipinitialspace=False, ) ] except Exception as error: print(error) return app = demo() app.mainloop() ``` --- # **Example Using and Creating Formatters** ```python from tksheet import ( Sheet, formatter, float_formatter, int_formatter, percentage_formatter, bool_formatter, truthy, falsy, num2alpha, ) import tkinter as tk from datetime import datetime, date from dateutil import parser, tz from math import ceil import re date_replace = re.compile("|".join(["\(", "\)", "\[", "\]", "\<", "\>"])) # Custom formatter methods def round_up(x): try: # might not be a number if empty return float(ceil(x)) except Exception: return x def only_numeric(s): return "".join(n for n in f"{s}" if n.isnumeric() or n == ".") def convert_to_local_datetime(dt: str, **kwargs): if isinstance(dt, datetime): pass elif isinstance(dt, date): dt = datetime(dt.year, dt.month, dt.day) else: if isinstance(dt, str): dt = date_replace.sub("", dt) try: dt = parser.parse(dt) except Exception: raise ValueError(f"Could not parse {dt} as a datetime") if dt.tzinfo is None: dt.replace(tzinfo=tz.tzlocal()) dt = dt.astimezone(tz.tzlocal()) return dt.replace(tzinfo=None) def datetime_to_string(dt: datetime, **kwargs): return dt.strftime("%d %b, %Y, %H:%M:%S") # Custom Formatter with additional kwargs def custom_datetime_to_str(dt: datetime, **kwargs): return dt.strftime(kwargs["format"]) class demo(tk.Tk): def __init__(self): tk.Tk.__init__(self) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.frame = tk.Frame(self) self.frame.grid_columnconfigure(0, weight=1) self.frame.grid_rowconfigure(0, weight=1) self.sheet = Sheet(self.frame, empty_vertical=0, empty_horizontal=0, data=[[f"{r}"] * 11 for r in range(20)]) self.sheet.enable_bindings() self.frame.grid(row=0, column=0, sticky="nswe") self.sheet.grid(row=0, column=0, sticky="nswe") self.sheet.headers( [ "Non-Nullable Float Cell\n1 decimals places", "Float Cell", "Int Cell", "Bool Cell", "Percentage Cell\n0 decimal places", "Custom Datetime Cell", "Custom Datetime Cell\nCustom Format String", "Float Cell that\nrounds up", "Float cell that\n strips non-numeric", "Dropdown Over Nullable\nPercentage Cell", "Percentage Cell\n2 decimal places", ] ) # num2alpha converts column integer to letter # Some examples of data formatting self.sheet[num2alpha(0)].format(float_formatter(nullable=False)) self.sheet[num2alpha(1)].format(float_formatter()) self.sheet[num2alpha(2)].format(int_formatter()) self.sheet[num2alpha(3)].format(bool_formatter(truthy=truthy | {"nah yeah"}, falsy=falsy | {"yeah nah"})) self.sheet[num2alpha(4)].format(percentage_formatter()) # Custom Formatters # Custom using generic formatter interface self.sheet[num2alpha(5)].format( formatter( datatypes=datetime, format_function=convert_to_local_datetime, to_str_function=datetime_to_string, nullable=False, invalid_value="NaT", ) ) # Custom format self.sheet[num2alpha(6)].format( datatypes=datetime, format_function=convert_to_local_datetime, to_str_function=custom_datetime_to_str, nullable=True, invalid_value="NaT", format="(%Y-%m-%d) %H:%M %p", ) # Unique cell behaviour using the post_conversion_function self.sheet[num2alpha(7)].format(float_formatter(post_format_function=round_up)) self.sheet[num2alpha(8)].format(float_formatter(), pre_format_function=only_numeric) self.sheet[num2alpha(9)].dropdown(values=["", "104%", 0.24, "300%", "not a number"], set_value=1,) self.sheet[num2alpha(9)].format(percentage_formatter(), decimals=0) self.sheet[num2alpha(10)].format(percentage_formatter(decimals=5)) app = demo() app.mainloop() ``` --- # **Contributing** Welcome and thank you for your interest in `tksheet`! ### **tksheet Goals** - **Adaptable rather than comprehensive**: Prioritizes adaptability over comprehensiveness, providing essential features that can be easily extended or customized based on specific needs. This approach allows for flexibility in integrating tksheet into different projects and workflows. - **Lightweight and performant**: Aims to provide a lightweight solution for creating spreadsheet-like functionality in tkinter applications, without additional dependencies and with a focus on efficiency and performance. ### **Dependencies** tksheet is designed to only use built-in Python libraries (without third-party dependencies). Please ensure that your contributions do not introduce any new dependencies outside of Python's built-in libraries. ### **License** tksheet is released under the MIT License. You can find the full text of the license [here](https://github.com/ragardner/tksheet/blob/master/LICENSE.txt). By contributing to the tksheet project, you agree to license your contributions under the same MIT License. Please make sure to read and understand the terms and conditions of the license before contributing. ### **Contributing Code** To contribute, please follow these steps: 1. Fork the tksheet repository. 2. If you are working on a new feature, create a new branch for your contribution. Use a descriptive name for the branch that reflects the feature you're working on. 3. Make your changes in your local branch, following the code style and conventions established in the project. 4. Test your changes thoroughly to ensure they do not introduce any new bugs or issues. 5. Submit a pull request to the `main` branch of the tksheet repository, including a clear title and detailed description of your changes. Pull requests ideally should include a small but comprehensive demonstration of the feature you are adding. 6. Don't forget to update the documentation! ***Note:*** If you're submitting a bugfix, it's generally preferred to submit it directly to the relevant branch, rather than creating a separate branch. ### **Asking Questions** Got a question that hasn't been answered in the closed issues or is missing from the documentation? please follow these guidelines: - Submit your question as an issue in the [Issues tab](https://github.com/ragardner/tksheet/issues). - Provide a clear and concise description of your question, including any relevant details or examples that can help us understand your query better. ### **Issues** Please use the [Issues tab](https://github.com/ragardner/tksheet/issues) to report any issues or ask for assistance. When submitting an issue, please follow these guidelines: - Check the existing issues to see if a similar bug or question has already been reported or discussed. - If reporting a bug, provide a minimal example that can reproduce the issue, including any relevant code, error messages, and steps to reproduce. - If asking a question or seeking help, provide a clear and concise description of your question or issue, including any relevant details or examples that can help people understand your query better. - Include any relevant screenshots or gifs that can visually illustrate the issue or your question. ### **Enhancements or Suggestions** If you have an idea for a new feature, improvement or change, please follow these guidelines: - Submit your suggestion as an issue in the [Issues tab](https://github.com/ragardner/tksheet/issues). - Include a clear and concise description of your idea, including any relevant details, screenshots, or mock-ups that can help contributors understand your suggestion better. - You're also welcome to become a contributor yourself and help implement your idea! tksheet-7.2.12/pyproject.toml000066400000000000000000000020511463432073300161710ustar00rootroot00000000000000[build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [project] name = "tksheet" description = "Tkinter table / sheet widget" readme = "README.md" version = "7.2.12" authors = [{ name = "ragardner", email = "github@ragardner.simplelogin.com" }] requires-python = ">=3.8" license = {file = "LICENSE.txt"} keywords = ["tkinter", "table", "widget", "sheet", "grid", "tk"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] [project.urls] "Homepage" = "https://github.com/ragardner/tksheet" "Bug Reports" = "https://github.com/ragardner/tksheet/issues" "Funding" = "https://github.com/ragardner" [tool.black] line-length = 120 [tool.ruff] line-length = 120 tksheet-7.2.12/tksheet/000077500000000000000000000000001463432073300147265ustar00rootroot00000000000000tksheet-7.2.12/tksheet/__init__.py000066400000000000000000000037711463432073300170470ustar00rootroot00000000000000# ruff: noqa: F401 """ tksheet - A Python tkinter table widget """ __version__ = "7.2.12" from .colors import ( color_map, ) from .column_headers import ColumnHeaders from .formatters import ( Formatter, bool_formatter, data_to_str, float_formatter, float_to_str, format_data, formatter, get_clipboard_data, get_data_with_valid_check, int_formatter, is_bool_like, is_none_like, percentage_formatter, percentage_to_str, to_bool, to_float, to_int, to_str, try_to_bool, ) from .functions import ( add_highlight, alpha2idx, alpha2num, consecutive_chunks, consecutive_ranges, data_to_displayed_idxs, displayed_to_data_idxs, dropdown_search_function, event_dict, get_checkbox_dict, get_checkbox_kwargs, get_csv_str_dialect, get_data_from_clipboard, get_dropdown_dict, get_dropdown_kwargs, get_index_of_gap_in_sorted_integer_seq_forward, get_index_of_gap_in_sorted_integer_seq_reverse, get_n2a, get_new_indexes, get_seq_without_gaps_at_index, insert_items, is_contiguous, is_iterable, move_elements_by_mapping, move_elements_to, new_tk_event, num2alpha, rounded_box_coords, span_dict, tksheet_type_error, ) from .main_table import MainTable from .other_classes import ( DotDict, DraggedRowColumn, DrawnItem, EventDataDict, GeneratedMouseEvent, Highlight, Selected, Span, SpanRange, TextCfg, ) from .row_index import RowIndex from .sheet import Dropdown, Sheet from .sheet_options import new_sheet_options from .text_editor import ( TextEditor, TextEditorTkText, ) from .themes import ( theme_black, theme_dark, theme_dark_blue, theme_dark_green, theme_light_blue, theme_light_green, ) from .top_left_rectangle import TopLeftRectangle from .vars import ( USER_OS, ctrl_key, emitted_events, falsy, nonelike, rc_binding, symbols_set, truthy, ) tksheet-7.2.12/tksheet/colors.py000066400000000000000000001412351463432073300166070ustar00rootroot00000000000000from __future__ import annotations color_map: dict[str, str] = { "alice blue": "#F0F8FF", "ALICE BLUE": "#F0F8FF", "AliceBlue": "#F0F8FF", "aliceblue": "#F0F8FF", "ALICEBLUE": "#F0F8FF", "antique white": "#FAEBD7", "ANTIQUE WHITE": "#FAEBD7", "AntiqueWhite": "#FAEBD7", "antiquewhite": "#FAEBD7", "#" "ANTIQUEWHITE": "#FAEBD7", "AntiqueWhite1": "#FFEFDB", "antiquewhite1": "#FFEFDB", "ANTIQUEWHITE1": "#FFEFDB", "AntiqueWhite2": "#EEDFCC", "antiquewhite2": "#EEDFCC", "ANTIQUEWHITE2": "#EEDFCC", "AntiqueWhite3": "#CDC0B0", "antiquewhite3": "#CDC0B0", "ANTIQUEWHITE3": "#CDC0B0", "AntiqueWhite4": "#8B8378", "antiquewhite4": "#8B8378", "ANTIQUEWHITE4": "#8B8378", "aquamarine": "#7FFFD4", "AQUAMARINE": "#7FFFD4", "aquamarine1": "#7FFFD4", "AQUAMARINE1": "#7FFFD4", "aquamarine2": "#76EEC6", "AQUAMARINE2": "#76EEC6", "aquamarine3": "#66CDAA", "AQUAMARINE3": "#66CDAA", "aquamarine4": "#458B74", "AQUAMARINE4": "#458B74", "azure": "#F0FFFF", "AZURE": "#F0FFFF", "azure1": "#F0FFFF", "AZURE1": "#F0FFFF", "azure2": "#E0EEEE", "AZURE2": "#E0EEEE", "azure3": "#C1CDCD", "AZURE3": "#C1CDCD", "azure4": "#838B8B", "AZURE4": "#838B8B", "beige": "#F5F5DC", "BEIGE": "#F5F5DC", "bisque": "#FFE4C4", "BISQUE": "#FFE4C4", "bisque1": "#FFE4C4", "BISQUE1": "#FFE4C4", "bisque2": "#EED5B7", "BISQUE2": "#EED5B7", "bisque3": "#CDB79E", "BISQUE3": "#CDB79E", "bisque4": "#8B7D6B", "BISQUE4": "#8B7D6B", "black": "#000000", "BLACK": "#000000", "blanched almond": "#FFEBCD", "BLANCHED ALMOND": "#FFEBCD", "BlanchedAlmond": "#FFEBCD", "blanchedalmond": "#FFEBCD", "BLANCHEDALMOND": "#FFEBCD", "blue": "#0000FF", "BLUE": "#0000FF", "blue violet": "#8A2BE2", "BLUE VIOLET": "#8A2BE2", "blue1": "#0000FF", "BLUE1": "#0000FF", "blue2": "#0000EE", "BLUE2": "#0000EE", "blue3": "#0000CD", "BLUE3": "#0000CD", "blue4": "#00008B", "BLUE4": "#00008B", "BlueViolet": "#8A2BE2", "blueviolet": "#8A2BE2", "BLUEVIOLET": "#8A2BE2", "brown": "#A52A2A", "BROWN": "#A52A2A", "brown1": "#FF4040", "BROWN1": "#FF4040", "brown2": "#EE3B3B", "BROWN2": "#EE3B3B", "brown3": "#CD3333", "BROWN3": "#CD3333", "brown4": "#8B2323", "BROWN4": "#8B2323", "burlywood": "#DEB887", "BURLYWOOD": "#DEB887", "burlywood1": "#FFD39B", "BURLYWOOD1": "#FFD39B", "burlywood2": "#EEC591", "BURLYWOOD2": "#EEC591", "burlywood3": "#CDAA7D", "BURLYWOOD3": "#CDAA7D", "burlywood4": "#8B7355", "BURLYWOOD4": "#8B7355", "cadet blue": "#5F9EA0", "CADET BLUE": "#5F9EA0", "CadetBlue": "#5F9EA0", "cadetblue": "#5F9EA0", "CADETBLUE": "#5F9EA0", "CadetBlue1": "#98F5FF", "cadetblue1": "#98F5FF", "CADETBLUE1": "#98F5FF", "CadetBlue2": "#8EE5EE", "cadetblue2": "#8EE5EE", "CADETBLUE2": "#8EE5EE", "CadetBlue3": "#7AC5CD", "cadetblue3": "#7AC5CD", "CADETBLUE3": "#7AC5CD", "CadetBlue4": "#53868B", "cadetblue4": "#53868B", "CADETBLUE4": "#53868B", "chartreuse": "#7FFF00", "CHARTREUSE": "#7FFF00", "chartreuse1": "#7FFF00", "CHARTREUSE1": "#7FFF00", "chartreuse2": "#76EE00", "CHARTREUSE2": "#76EE00", "chartreuse3": "#66CD00", "CHARTREUSE3": "#66CD00", "chartreuse4": "#458B00", "CHARTREUSE4": "#458B00", "chocolate": "#D2691E", "CHOCOLATE": "#D2691E", "chocolate1": "#FF7F24", "CHOCOLATE1": "#FF7F24", "chocolate2": "#EE7621", "CHOCOLATE2": "#EE7621", "chocolate3": "#CD661D", "CHOCOLATE3": "#CD661D", "chocolate4": "#8B4513", "CHOCOLATE4": "#8B4513", "coral": "#FF7F50", "CORAL": "#FF7F50", "coral1": "#FF7256", "CORAL1": "#FF7256", "coral2": "#EE6A50", "CORAL2": "#EE6A50", "coral3": "#CD5B45", "CORAL3": "#CD5B45", "coral4": "#8B3E2F", "CORAL4": "#8B3E2F", "cornflower blue": "#6495ED", "CORNFLOWER BLUE": "#6495ED", "CornflowerBlue": "#6495ED", "cornflowerblue": "#6495ED", "CORNFLOWERBLUE": "#6495ED", "cornsilk": "#FFF8DC", "CORNSILK": "#FFF8DC", "cornsilk1": "#FFF8DC", "CORNSILK1": "#FFF8DC", "cornsilk2": "#EEE8CD", "CORNSILK2": "#EEE8CD", "cornsilk3": "#CDC8B1", "CORNSILK3": "#CDC8B1", "cornsilk4": "#8B8878", "CORNSILK4": "#8B8878", "cyan": "#00FFFF", "CYAN": "#00FFFF", "cyan1": "#00FFFF", "CYAN1": "#00FFFF", "cyan2": "#00EEEE", "CYAN2": "#00EEEE", "cyan3": "#00CDCD", "CYAN3": "#00CDCD", "cyan4": "#008B8B", "CYAN4": "#008B8B", "dark blue": "#00008B", "DARK BLUE": "#00008B", "dark cyan": "#008B8B", "DARK CYAN": "#008B8B", "dark goldenrod": "#B8860B", "DARK GOLDENROD": "#B8860B", "dark gray": "#A9A9A9", "DARK GRAY": "#A9A9A9", "dark green": "#006400", "DARK GREEN": "#006400", "dark grey": "#A9A9A9", "DARK GREY": "#A9A9A9", "dark khaki": "#BDB76B", "DARK KHAKI": "#BDB76B", "dark magenta": "#8B008B", "DARK MAGENTA": "#8B008B", "dark olive green": "#556B2F", "DARK OLIVE GREEN": "#556B2F", "dark orange": "#FF8C00", "DARK ORANGE": "#FF8C00", "dark orchid": "#9932CC", "DARK ORCHID": "#9932CC", "dark red": "#8B0000", "DARK RED": "#8B0000", "dark salmon": "#E9967A", "DARK SALMON": "#E9967A", "dark sea green": "#8FBC8F", "DARK SEA GREEN": "#8FBC8F", "dark slate blue": "#483D8B", "DARK SLATE BLUE": "#483D8B", "dark slate gray": "#2F4F4F", "DARK SLATE GRAY": "#2F4F4F", "dark slate grey": "#2F4F4F", "DARK SLATE GREY": "#2F4F4F", "dark turquoise": "#00CED1", "DARK TURQUOISE": "#00CED1", "dark violet": "#9400D3", "DARK VIOLET": "#9400D3", "DarkBlue": "#00008B", "darkblue": "#00008B", "DARKBLUE": "#00008B", "DarkCyan": "#008B8B", "darkcyan": "#008B8B", "DARKCYAN": "#008B8B", "DarkGoldenrod": "#B8860B", "darkgoldenrod": "#B8860B", "DARKGOLDENROD": "#B8860B", "DarkGoldenrod1": "#FFB90F", "darkgoldenrod1": "#FFB90F", "DARKGOLDENROD1": "#FFB90F", "DarkGoldenrod2": "#EEAD0E", "darkgoldenrod2": "#EEAD0E", "DARKGOLDENROD2": "#EEAD0E", "DarkGoldenrod3": "#CD950C", "darkgoldenrod3": "#CD950C", "DARKGOLDENROD3": "#CD950C", "DarkGoldenrod4": "#8B6508", "darkgoldenrod4": "#8B6508", "DARKGOLDENROD4": "#8B6508", "DarkGray": "#A9A9A9", "darkgray": "#A9A9A9", "DARKGRAY": "#A9A9A9", "DarkGreen": "#006400", "darkgreen": "#006400", "DARKGREEN": "#006400", "DarkGrey": "#A9A9A9", "darkgrey": "#A9A9A9", "DARKGREY": "#A9A9A9", "DarkKhaki": "#BDB76B", "darkkhaki": "#BDB76B", "DARKKHAKI": "#BDB76B", "DarkMagenta": "#8B008B", "darkmagenta": "#8B008B", "DARKMAGENTA": "#8B008B", "DarkOliveGreen": "#556B2F", "darkolivegreen": "#556B2F", "DARKOLIVEGREEN": "#556B2F", "DarkOliveGreen1": "#CAFF70", "darkolivegreen1": "#CAFF70", "DARKOLIVEGREEN1": "#CAFF70", "DarkOliveGreen2": "#BCEE68", "darkolivegreen2": "#BCEE68", "DARKOLIVEGREEN2": "#BCEE68", "DarkOliveGreen3": "#A2CD5A", "darkolivegreen3": "#A2CD5A", "DARKOLIVEGREEN3": "#A2CD5A", "DarkOliveGreen4": "#6E8B3D", "darkolivegreen4": "#6E8B3D", "DARKOLIVEGREEN4": "#6E8B3D", "DarkOrange": "#FF8C00", "darkorange": "#FF8C00", "DARKORANGE": "#FF8C00", "DarkOrange1": "#FF7F00", "darkorange1": "#FF7F00", "DARKORANGE1": "#FF7F00", "DarkOrange2": "#EE7600", "darkorange2": "#EE7600", "DARKORANGE2": "#EE7600", "DarkOrange3": "#CD6600", "darkorange3": "#CD6600", "DARKORANGE3": "#CD6600", "DarkOrange4": "#8B4500", "darkorange4": "#8B4500", "DARKORANGE4": "#8B4500", "DarkOrchid": "#9932CC", "darkorchid": "#9932CC", "DARKORCHID": "#9932CC", "DarkOrchid1": "#BF3EFF", "darkorchid1": "#BF3EFF", "DARKORCHID1": "#BF3EFF", "DarkOrchid2": "#B23AEE", "darkorchid2": "#B23AEE", "DARKORCHID2": "#B23AEE", "DarkOrchid3": "#9A32CD", "darkorchid3": "#9A32CD", "DARKORCHID3": "#9A32CD", "DarkOrchid4": "#68228B", "darkorchid4": "#68228B", "DARKORCHID4": "#68228B", "DarkRed": "#8B0000", "darkred": "#8B0000", "DARKRED": "#8B0000", "DarkSalmon": "#E9967A", "darksalmon": "#E9967A", "DARKSALMON": "#E9967A", "DarkSeaGreen": "#8FBC8F", "darkseagreen": "#8FBC8F", "DARKSEAGREEN": "#8FBC8F", "DarkSeaGreen1": "#C1FFC1", "darkseagreen1": "#C1FFC1", "DARKSEAGREEN1": "#C1FFC1", "DarkSeaGreen2": "#B4EEB4", "darkseagreen2": "#B4EEB4", "DARKSEAGREEN2": "#B4EEB4", "DarkSeaGreen3": "#9BCD9B", "darkseagreen3": "#9BCD9B", "DARKSEAGREEN3": "#9BCD9B", "DarkSeaGreen4": "#698B69", "darkseagreen4": "#698B69", "DARKSEAGREEN4": "#698B69", "DarkSlateBlue": "#483D8B", "darkslateblue": "#483D8B", "DARKSLATEBLUE": "#483D8B", "DarkSlateGray": "#2F4F4F", "darkslategray": "#2F4F4F", "DARKSLATEGRAY": "#2F4F4F", "DarkSlateGray1": "#97FFFF", "darkslategray1": "#97FFFF", "DARKSLATEGRAY1": "#97FFFF", "DarkSlateGray2": "#8DEEEE", "darkslategray2": "#8DEEEE", "DARKSLATEGRAY2": "#8DEEEE", "DarkSlateGray3": "#79CDCD", "darkslategray3": "#79CDCD", "DARKSLATEGRAY3": "#79CDCD", "DarkSlateGray4": "#528B8B", "darkslategray4": "#528B8B", "DARKSLATEGRAY4": "#528B8B", "DarkSlateGrey": "#2F4F4F", "darkslategrey": "#2F4F4F", "DARKSLATEGREY": "#2F4F4F", "DarkTurquoise": "#00CED1", "darkturquoise": "#00CED1", "DARKTURQUOISE": "#00CED1", "DarkViolet": "#9400D3", "darkviolet": "#9400D3", "DARKVIOLET": "#9400D3", "deep pink": "#FF1493", "DEEP PINK": "#FF1493", "deep sky blue": "#00BFFF", "DEEP SKY BLUE": "#00BFFF", "DeepPink": "#FF1493", "deeppink": "#FF1493", "DEEPPINK": "#FF1493", "DeepPink1": "#FF1493", "deeppink1": "#FF1493", "DEEPPINK1": "#FF1493", "DeepPink2": "#EE1289", "deeppink2": "#EE1289", "DEEPPINK2": "#EE1289", "DeepPink3": "#CD1076", "deeppink3": "#CD1076", "DEEPPINK3": "#CD1076", "DeepPink4": "#8B0A50", "deeppink4": "#8B0A50", "DEEPPINK4": "#8B0A50", "DeepSkyBlue": "#00BFFF", "deepskyblue": "#00BFFF", "DEEPSKYBLUE": "#00BFFF", "DeepSkyBlue1": "#00BFFF", "deepskyblue1": "#00BFFF", "DEEPSKYBLUE1": "#00BFFF", "DeepSkyBlue2": "#00B2EE", "deepskyblue2": "#00B2EE", "DEEPSKYBLUE2": "#00B2EE", "DeepSkyBlue3": "#009ACD", "deepskyblue3": "#009ACD", "DEEPSKYBLUE3": "#009ACD", "DeepSkyBlue4": "#00688B", "deepskyblue4": "#00688B", "DEEPSKYBLUE4": "#00688B", "dim gray": "#696969", "DIM GRAY": "#696969", "dim grey": "#696969", "DIM GREY": "#696969", "DimGray": "#696969", "dimgray": "#696969", "DIMGRAY": "#696969", "DimGrey": "#696969", "dimgrey": "#696969", "DIMGREY": "#696969", "dodger blue": "#1E90FF", "DODGER BLUE": "#1E90FF", "DodgerBlue": "#1E90FF", "dodgerblue": "#1E90FF", "DODGERBLUE": "#1E90FF", "DodgerBlue1": "#1E90FF", "dodgerblue1": "#1E90FF", "DODGERBLUE1": "#1E90FF", "DodgerBlue2": "#1C86EE", "dodgerblue2": "#1C86EE", "DODGERBLUE2": "#1C86EE", "DodgerBlue3": "#1874CD", "dodgerblue3": "#1874CD", "DODGERBLUE3": "#1874CD", "DodgerBlue4": "#104E8B", "dodgerblue4": "#104E8B", "DODGERBLUE4": "#104E8B", "firebrick": "#B22222", "FIREBRICK": "#B22222", "firebrick1": "#FF3030", "FIREBRICK1": "#FF3030", "firebrick2": "#EE2C2C", "FIREBRICK2": "#EE2C2C", "firebrick3": "#CD2626", "FIREBRICK3": "#CD2626", "firebrick4": "#8B1A1A", "FIREBRICK4": "#8B1A1A", "floral white": "#FFFAF0", "FLORAL WHITE": "#FFFAF0", "FloralWhite": "#FFFAF0", "floralwhite": "#FFFAF0", "FLORALWHITE": "#FFFAF0", "forest green": "#228B22", "FOREST GREEN": "#228B22", "ForestGreen": "#228B22", "forestgreen": "#228B22", "FORESTGREEN": "#228B22", "gainsboro": "#DCDCDC", "GAINSBORO": "#DCDCDC", "ghost white": "#F8F8FF", "GHOST WHITE": "#F8F8FF", "GhostWhite": "#F8F8FF", "ghostwhite": "#F8F8FF", "GHOSTWHITE": "#F8F8FF", "gold": "#FFD700", "GOLD": "#FFD700", "gold1": "#FFD700", "GOLD1": "#FFD700", "gold2": "#EEC900", "GOLD2": "#EEC900", "gold3": "#CDAD00", "GOLD3": "#CDAD00", "gold4": "#8B7500", "GOLD4": "#8B7500", "goldenrod": "#DAA520", "GOLDENROD": "#DAA520", "goldenrod1": "#FFC125", "GOLDENROD1": "#FFC125", "goldenrod2": "#EEB422", "GOLDENROD2": "#EEB422", "goldenrod3": "#CD9B1D", "GOLDENROD3": "#CD9B1D", "goldenrod4": "#8B6914", "GOLDENROD4": "#8B6914", "gray": "#BEBEBE", "GRAY": "#BEBEBE", "gray0": "#000000", "GRAY0": "#000000", "gray1": "#030303", "GRAY1": "#030303", "gray2": "#050505", "GRAY2": "#050505", "gray3": "#080808", "GRAY3": "#080808", "gray4": "#0A0A0A", "GRAY4": "#0A0A0A", "gray5": "#0D0D0D", "GRAY5": "#0D0D0D", "gray6": "#0F0F0F", "GRAY6": "#0F0F0F", "gray7": "#121212", "GRAY7": "#121212", "gray8": "#141414", "GRAY8": "#141414", "gray9": "#171717", "GRAY9": "#171717", "gray10": "#1A1A1A", "GRAY10": "#1A1A1A", "gray11": "#1C1C1C", "GRAY11": "#1C1C1C", "gray12": "#1F1F1F", "GRAY12": "#1F1F1F", "gray13": "#212121", "GRAY13": "#212121", "gray14": "#242424", "GRAY14": "#242424", "gray15": "#262626", "GRAY15": "#262626", "gray16": "#292929", "GRAY16": "#292929", "gray17": "#2B2B2B", "GRAY17": "#2B2B2B", "gray18": "#2E2E2E", "GRAY18": "#2E2E2E", "gray19": "#303030", "GRAY19": "#303030", "gray20": "#333333", "GRAY20": "#333333", "gray21": "#363636", "GRAY21": "#363636", "gray22": "#383838", "GRAY22": "#383838", "gray23": "#3B3B3B", "GRAY23": "#3B3B3B", "gray24": "#3D3D3D", "GRAY24": "#3D3D3D", "gray25": "#404040", "GRAY25": "#404040", "gray26": "#424242", "GRAY26": "#424242", "gray27": "#454545", "GRAY27": "#454545", "gray28": "#474747", "GRAY28": "#474747", "gray29": "#4A4A4A", "GRAY29": "#4A4A4A", "gray30": "#4D4D4D", "GRAY30": "#4D4D4D", "gray31": "#4F4F4F", "GRAY31": "#4F4F4F", "gray32": "#525252", "GRAY32": "#525252", "gray33": "#545454", "GRAY33": "#545454", "gray34": "#575757", "GRAY34": "#575757", "gray35": "#595959", "GRAY35": "#595959", "gray36": "#5C5C5C", "GRAY36": "#5C5C5C", "gray37": "#5E5E5E", "GRAY37": "#5E5E5E", "gray38": "#616161", "GRAY38": "#616161", "gray39": "#636363", "GRAY39": "#636363", "gray40": "#666666", "GRAY40": "#666666", "gray41": "#696969", "GRAY41": "#696969", "gray42": "#6B6B6B", "GRAY42": "#6B6B6B", "gray43": "#707070", "GRAY43": "#707070", "gray44": "#707070", "GRAY44": "#707070", "gray45": "#707070", "GRAY45": "#707070", "gray46": "#757575", "GRAY46": "#757575", "gray47": "#787878", "GRAY47": "#787878", "gray48": "#7A7A7A", "GRAY48": "#7A7A7A", "gray49": "#707070", "GRAY49": "#707070", "gray50": "#7F7F7F", "GRAY50": "#7F7F7F", "gray51": "#828282", "GRAY51": "#828282", "gray52": "#858585", "GRAY52": "#858585", "gray53": "#878787", "GRAY53": "#878787", "gray54": "#8A8A8A", "GRAY54": "#8A8A8A", "gray55": "#8C8C8C", "GRAY55": "#8C8C8C", "gray56": "#8F8F8F", "GRAY56": "#8F8F8F", "gray57": "#919191", "GRAY57": "#919191", "gray58": "#949494", "GRAY58": "#949494", "gray59": "#969696", "GRAY59": "#969696", "gray60": "#999999", "GRAY60": "#999999", "gray61": "#9C9C9C", "GRAY61": "#9C9C9C", "gray62": "#9E9E9E", "GRAY62": "#9E9E9E", "gray63": "#A1A1A1", "GRAY63": "#A1A1A1", "gray64": "#A3A3A3", "GRAY64": "#A3A3A3", "gray65": "#A6A6A6", "GRAY65": "#A6A6A6", "gray66": "#A8A8A8", "GRAY66": "#A8A8A8", "gray67": "#ABABAB", "GRAY67": "#ABABAB", "gray68": "#ADADAD", "GRAY68": "#ADADAD", "gray69": "#B0B0B0", "GRAY69": "#B0B0B0", "gray70": "#B3B3B3", "GRAY70": "#B3B3B3", "gray71": "#B5B5B5", "GRAY71": "#B5B5B5", "gray72": "#B8B8B8", "GRAY72": "#B8B8B8", "gray73": "#BABABA", "GRAY73": "#BABABA", "gray74": "#BDBDBD", "GRAY74": "#BDBDBD", "gray75": "#BFBFBF", "GRAY75": "#BFBFBF", "gray76": "#C2C2C2", "GRAY76": "#C2C2C2", "gray77": "#C4C4C4", "GRAY77": "#C4C4C4", "gray78": "#C7C7C7", "GRAY78": "#C7C7C7", "gray79": "#C9C9C9", "GRAY79": "#C9C9C9", "gray80": "#CCCCCC", "GRAY80": "#CCCCCC", "gray81": "#CFCFCF", "GRAY81": "#CFCFCF", "gray82": "#D1D1D1", "GRAY82": "#D1D1D1", "gray83": "#D4D4D4", "GRAY83": "#D4D4D4", "gray84": "#D6D6D6", "GRAY84": "#D6D6D6", "gray85": "#D9D9D9", "GRAY85": "#D9D9D9", "gray86": "#DBDBDB", "GRAY86": "#DBDBDB", "gray87": "#DEDEDE", "GRAY87": "#DEDEDE", "gray88": "#E0E0E0", "GRAY88": "#E0E0E0", "gray89": "#E3E3E3", "GRAY89": "#E3E3E3", "gray90": "#E5E5E5", "GRAY90": "#E5E5E5", "gray91": "#E8E8E8", "GRAY91": "#E8E8E8", "gray92": "#EBEBEB", "GRAY92": "#EBEBEB", "gray93": "#EDEDED", "GRAY93": "#EDEDED", "gray94": "#F0F0F0", "GRAY94": "#F0F0F0", "gray95": "#F2F2F2", "GRAY95": "#F2F2F2", "gray96": "#F5F5F5", "GRAY96": "#F5F5F5", "gray97": "#F7F7F7", "GRAY97": "#F7F7F7", "gray98": "#FAFAFA", "GRAY98": "#FAFAFA", "gray99": "#FCFCFC", "GRAY99": "#FCFCFC", "gray100": "#FFFFFF", "GRAY100": "#FFFFFF", "green": "#00FF00", "GREEN": "#00FF00", "green yellow": "#ADFF2F", "GREEN YELLOW": "#ADFF2F", "green1": "#00FF00", "GREEN1": "#00FF00", "green2": "#00EE00", "GREEN2": "#00EE00", "green3": "#00CD00", "GREEN3": "#00CD00", "green4": "#008B00", "GREEN4": "#008B00", "GreenYellow": "#ADFF2F", "greenyellow": "#ADFF2F", "GREENYELLOW": "#ADFF2F", "grey": "#BEBEBE", "GREY": "#BEBEBE", "grey0": "#000000", "GREY0": "#000000", "grey1": "#030303", "GREY1": "#030303", "grey2": "#050505", "GREY2": "#050505", "grey3": "#080808", "GREY3": "#080808", "grey4": "#0A0A0A", "GREY4": "#0A0A0A", "grey5": "#0D0D0D", "GREY5": "#0D0D0D", "grey6": "#0F0F0F", "GREY6": "#0F0F0F", "grey7": "#121212", "GREY7": "#121212", "grey8": "#141414", "GREY8": "#141414", "grey9": "#171717", "GREY9": "#171717", "grey10": "#1A1A1A", "GREY10": "#1A1A1A", "grey11": "#1C1C1C", "GREY11": "#1C1C1C", "grey12": "#1F1F1F", "GREY12": "#1F1F1F", "grey13": "#212121", "GREY13": "#212121", "grey14": "#242424", "GREY14": "#242424", "grey15": "#262626", "GREY15": "#262626", "grey16": "#292929", "GREY16": "#292929", "grey17": "#2B2B2B", "GREY17": "#2B2B2B", "grey18": "#2E2E2E", "GREY18": "#2E2E2E", "grey19": "#303030", "GREY19": "#303030", "grey20": "#333333", "GREY20": "#333333", "grey21": "#363636", "GREY21": "#363636", "grey22": "#383838", "GREY22": "#383838", "grey23": "#3B3B3B", "GREY23": "#3B3B3B", "grey24": "#3D3D3D", "GREY24": "#3D3D3D", "grey25": "#404040", "GREY25": "#404040", "grey26": "#424242", "GREY26": "#424242", "grey27": "#454545", "GREY27": "#454545", "grey28": "#474747", "GREY28": "#474747", "grey29": "#4A4A4A", "GREY29": "#4A4A4A", "grey30": "#4D4D4D", "GREY30": "#4D4D4D", "grey31": "#4F4F4F", "GREY31": "#4F4F4F", "grey32": "#525252", "GREY32": "#525252", "grey33": "#545454", "GREY33": "#545454", "grey34": "#575757", "GREY34": "#575757", "grey35": "#595959", "GREY35": "#595959", "grey36": "#5C5C5C", "GREY36": "#5C5C5C", "grey37": "#5E5E5E", "GREY37": "#5E5E5E", "grey38": "#616161", "GREY38": "#616161", "grey39": "#636363", "GREY39": "#636363", "grey40": "#666666", "GREY40": "#666666", "grey41": "#696969", "GREY41": "#696969", "grey42": "#6B6B6B", "GREY42": "#6B6B6B", "grey43": "#707070", "GREY43": "#707070", "grey44": "#707070", "GREY44": "#707070", "grey45": "#707070", "GREY45": "#707070", "grey46": "#757575", "GREY46": "#757575", "grey47": "#787878", "GREY47": "#787878", "grey48": "#7A7A7A", "GREY48": "#7A7A7A", "grey49": "#707070", "GREY49": "#707070", "grey50": "#7F7F7F", "GREY50": "#7F7F7F", "grey51": "#828282", "GREY51": "#828282", "grey52": "#858585", "GREY52": "#858585", "grey53": "#878787", "GREY53": "#878787", "grey54": "#8A8A8A", "GREY54": "#8A8A8A", "grey55": "#8C8C8C", "GREY55": "#8C8C8C", "grey56": "#8F8F8F", "GREY56": "#8F8F8F", "grey57": "#919191", "GREY57": "#919191", "grey58": "#949494", "GREY58": "#949494", "grey59": "#969696", "GREY59": "#969696", "grey60": "#999999", "GREY60": "#999999", "grey61": "#9C9C9C", "GREY61": "#9C9C9C", "grey62": "#9E9E9E", "GREY62": "#9E9E9E", "grey63": "#A1A1A1", "GREY63": "#A1A1A1", "grey64": "#A3A3A3", "GREY64": "#A3A3A3", "grey65": "#A6A6A6", "GREY65": "#A6A6A6", "grey66": "#A8A8A8", "GREY66": "#A8A8A8", "grey67": "#ABABAB", "GREY67": "#ABABAB", "grey68": "#ADADAD", "GREY68": "#ADADAD", "grey69": "#B0B0B0", "GREY69": "#B0B0B0", "grey70": "#B3B3B3", "GREY70": "#B3B3B3", "grey71": "#B5B5B5", "GREY71": "#B5B5B5", "grey72": "#B8B8B8", "GREY72": "#B8B8B8", "grey73": "#BABABA", "GREY73": "#BABABA", "grey74": "#BDBDBD", "GREY74": "#BDBDBD", "grey75": "#BFBFBF", "GREY75": "#BFBFBF", "grey76": "#C2C2C2", "GREY76": "#C2C2C2", "grey77": "#C4C4C4", "GREY77": "#C4C4C4", "grey78": "#C7C7C7", "GREY78": "#C7C7C7", "grey79": "#C9C9C9", "GREY79": "#C9C9C9", "grey80": "#CCCCCC", "GREY80": "#CCCCCC", "grey81": "#CFCFCF", "GREY81": "#CFCFCF", "grey82": "#D1D1D1", "GREY82": "#D1D1D1", "grey83": "#D4D4D4", "GREY83": "#D4D4D4", "grey84": "#D6D6D6", "GREY84": "#D6D6D6", "grey85": "#D9D9D9", "GREY85": "#D9D9D9", "grey86": "#DBDBDB", "GREY86": "#DBDBDB", "grey87": "#DEDEDE", "GREY87": "#DEDEDE", "grey88": "#E0E0E0", "GREY88": "#E0E0E0", "grey89": "#E3E3E3", "GREY89": "#E3E3E3", "grey90": "#E5E5E5", "GREY90": "#E5E5E5", "grey91": "#E8E8E8", "GREY91": "#E8E8E8", "grey92": "#EBEBEB", "GREY92": "#EBEBEB", "grey93": "#EDEDED", "GREY93": "#EDEDED", "grey94": "#F0F0F0", "GREY94": "#F0F0F0", "grey95": "#F2F2F2", "GREY95": "#F2F2F2", "grey96": "#F5F5F5", "GREY96": "#F5F5F5", "grey97": "#F7F7F7", "GREY97": "#F7F7F7", "grey98": "#FAFAFA", "GREY98": "#FAFAFA", "grey99": "#FCFCFC", "GREY99": "#FCFCFC", "grey100": "#FFFFFF", "GREY100": "#FFFFFF", "honeydew": "#F0FFF0", "HONEYDEW": "#F0FFF0", "honeydew1": "#F0FFF0", "HONEYDEW1": "#F0FFF0", "honeydew2": "#E0EEE0", "HONEYDEW2": "#E0EEE0", "honeydew3": "#C1CDC1", "HONEYDEW3": "#C1CDC1", "honeydew4": "#838B83", "HONEYDEW4": "#838B83", "hot pink": "#FF69B4", "HOT PINK": "#FF69B4", "HotPink": "#FF69B4", "hotpink": "#FF69B4", "HOTPINK": "#FF69B4", "HotPink1": "#FF6EB4", "hotpink1": "#FF6EB4", "HOTPINK1": "#FF6EB4", "HotPink2": "#EE6AA7", "hotpink2": "#EE6AA7", "HOTPINK2": "#EE6AA7", "HotPink3": "#CD6090", "hotpink3": "#CD6090", "HOTPINK3": "#CD6090", "HotPink4": "#8B3A62", "hotpink4": "#8B3A62", "HOTPINK4": "#8B3A62", "indigo": "#4b0082", "INDIGO": "#4b0082", "indian red": "#CD5C5C", "INDIAN RED": "#CD5C5C", "IndianRed": "#CD5C5C", "indianred": "#CD5C5C", "INDIANRED": "#CD5C5C", "IndianRed1": "#FF6A6A", "indianred1": "#FF6A6A", "INDIANRED1": "#FF6A6A", "IndianRed2": "#EE6363", "indianred2": "#EE6363", "INDIANRED2": "#EE6363", "IndianRed3": "#CD5555", "indianred3": "#CD5555", "INDIANRED3": "#CD5555", "IndianRed4": "#8B3A3A", "indianred4": "#8B3A3A", "INDIANRED4": "#8B3A3A", "ivory": "#FFFFF0", "IVORY": "#FFFFF0", "ivory1": "#FFFFF0", "IVORY1": "#FFFFF0", "ivory2": "#EEEEE0", "IVORY2": "#EEEEE0", "ivory3": "#CDCDC1", "IVORY3": "#CDCDC1", "ivory4": "#8B8B83", "IVORY4": "#8B8B83", "khaki": "#F0E68C", "KHAKI": "#F0E68C", "khaki1": "#FFF68F", "KHAKI1": "#FFF68F", "khaki2": "#EEE685", "KHAKI2": "#EEE685", "khaki3": "#CDC673", "KHAKI3": "#CDC673", "khaki4": "#8B864E", "KHAKI4": "#8B864E", "lavender": "#E6E6FA", "LAVENDER": "#E6E6FA", "lavender blush": "#FFF0F5", "LAVENDER BLUSH": "#FFF0F5", "LavenderBlush": "#FFF0F5", "lavenderblush": "#FFF0F5", "LAVENDERBLUSH": "#FFF0F5", "LavenderBlush1": "#FFF0F5", "lavenderblush1": "#FFF0F5", "LAVENDERBLUSH1": "#FFF0F5", "LavenderBlush2": "#EEE0E5", "lavenderblush2": "#EEE0E5", "LAVENDERBLUSH2": "#EEE0E5", "LavenderBlush3": "#CDC1C5", "lavenderblush3": "#CDC1C5", "LAVENDERBLUSH3": "#CDC1C5", "LavenderBlush4": "#8B8386", "lavenderblush4": "#8B8386", "LAVENDERBLUSH4": "#8B8386", "lawn green": "#7CFC00", "LAWN GREEN": "#7CFC00", "LawnGreen": "#7CFC00", "lawngreen": "#7CFC00", "LAWNGREEN": "#7CFC00", "lemon chiffon": "#FFFACD", "LEMON CHIFFON": "#FFFACD", "LemonChiffon": "#FFFACD", "lemonchiffon": "#FFFACD", "LEMONCHIFFON": "#FFFACD", "LemonChiffon1": "#FFFACD", "lemonchiffon1": "#FFFACD", "LEMONCHIFFON1": "#FFFACD", "LemonChiffon2": "#EEE9BF", "lemonchiffon2": "#EEE9BF", "LEMONCHIFFON2": "#EEE9BF", "LemonChiffon3": "#CDC9A5", "lemonchiffon3": "#CDC9A5", "LEMONCHIFFON3": "#CDC9A5", "LemonChiffon4": "#8B8970", "lemonchiffon4": "#8B8970", "LEMONCHIFFON4": "#8B8970", "light blue": "#ADD8E6", "LIGHT BLUE": "#ADD8E6", "light coral": "#F08080", "LIGHT CORAL": "#F08080", "light cyan": "#E0FFFF", "LIGHT CYAN": "#E0FFFF", "light goldenrod": "#EEDD82", "LIGHT GOLDENROD": "#EEDD82", "light goldenrod yellow": "#FAFAD2", "LIGHT GOLDENROD YELLOW": "#FAFAD2", "light gray": "#D3D3D3", "LIGHT GRAY": "#D3D3D3", "light green": "#90EE90", "LIGHT GREEN": "#90EE90", "light grey": "#D3D3D3", "LIGHT GREY": "#D3D3D3", "light pink": "#FFB6C1", "LIGHT PINK": "#FFB6C1", "light salmon": "#FFA07A", "LIGHT SALMON": "#FFA07A", "light sea green": "#20B2AA", "LIGHT SEA GREEN": "#20B2AA", "light sky blue": "#87CEFA", "LIGHT SKY BLUE": "#87CEFA", "light slate blue": "#8470FF", "LIGHT SLATE BLUE": "#8470FF", "light slate gray": "#778899", "LIGHT SLATE GRAY": "#778899", "light slate grey": "#778899", "LIGHT SLATE GREY": "#778899", "light steel blue": "#B0C4DE", "LIGHT STEEL BLUE": "#B0C4DE", "light yellow": "#FFFFE0", "LIGHT YELLOW": "#FFFFE0", "LightBlue": "#ADD8E6", "lightblue": "#ADD8E6", "LIGHTBLUE": "#ADD8E6", "LightBlue1": "#BFEFFF", "lightblue1": "#BFEFFF", "LIGHTBLUE1": "#BFEFFF", "LightBlue2": "#B2DFEE", "lightblue2": "#B2DFEE", "LIGHTBLUE2": "#B2DFEE", "LightBlue3": "#9AC0CD", "lightblue3": "#9AC0CD", "LIGHTBLUE3": "#9AC0CD", "LightBlue4": "#68838B", "lightblue4": "#68838B", "LIGHTBLUE4": "#68838B", "LightCoral": "#F08080", "lightcoral": "#F08080", "LIGHTCORAL": "#F08080", "LightCyan": "#E0FFFF", "lightcyan": "#E0FFFF", "LIGHTCYAN": "#E0FFFF", "LightCyan1": "#E0FFFF", "lightcyan1": "#E0FFFF", "LIGHTCYAN1": "#E0FFFF", "LightCyan2": "#D1EEEE", "lightcyan2": "#D1EEEE", "LIGHTCYAN2": "#D1EEEE", "LightCyan3": "#B4CDCD", "lightcyan3": "#B4CDCD", "LIGHTCYAN3": "#B4CDCD", "LightCyan4": "#7A8B8B", "lightcyan4": "#7A8B8B", "LIGHTCYAN4": "#7A8B8B", "LightGoldenrod": "#EEDD82", "lightgoldenrod": "#EEDD82", "LIGHTGOLDENROD": "#EEDD82", "LightGoldenrod1": "#FFEC8B", "lightgoldenrod1": "#FFEC8B", "LIGHTGOLDENROD1": "#FFEC8B", "LightGoldenrod2": "#EEDC82", "lightgoldenrod2": "#EEDC82", "LIGHTGOLDENROD2": "#EEDC82", "LightGoldenrod3": "#CDBE70", "lightgoldenrod3": "#CDBE70", "LIGHTGOLDENROD3": "#CDBE70", "LightGoldenrod4": "#8B814C", "lightgoldenrod4": "#8B814C", "LIGHTGOLDENROD4": "#8B814C", "LightGoldenrodYellow": "#FAFAD2", "lightgoldenrodyellow": "#FAFAD2", "LIGHTGOLDENRODYELLOW": "#FAFAD2", "LightGray": "#D3D3D3", "lightgray": "#D3D3D3", "LIGHTGRAY": "#D3D3D3", "LightGreen": "#90EE90", "lightgreen": "#90EE90", "LIGHTGREEN": "#90EE90", "LightGrey": "#D3D3D3", "lightgrey": "#D3D3D3", "LIGHTGREY": "#D3D3D3", "LightPink": "#FFB6C1", "lightpink": "#FFB6C1", "LIGHTPINK": "#FFB6C1", "LightPink1": "#FFAEB9", "lightpink1": "#FFAEB9", "LIGHTPINK1": "#FFAEB9", "LightPink2": "#EEA2AD", "lightpink2": "#EEA2AD", "LIGHTPINK2": "#EEA2AD", "LightPink3": "#CD8C95", "lightpink3": "#CD8C95", "LIGHTPINK3": "#CD8C95", "LightPink4": "#8B5F65", "lightpink4": "#8B5F65", "LIGHTPINK4": "#8B5F65", "LightSalmon": "#FFA07A", "lightsalmon": "#FFA07A", "LIGHTSALMON": "#FFA07A", "LightSalmon1": "#FFA07A", "lightsalmon1": "#FFA07A", "LIGHTSALMON1": "#FFA07A", "LightSalmon2": "#EE9572", "lightsalmon2": "#EE9572", "LIGHTSALMON2": "#EE9572", "LightSalmon3": "#CD8162", "lightsalmon3": "#CD8162", "LIGHTSALMON3": "#CD8162", "LightSalmon4": "#8B5742", "lightsalmon4": "#8B5742", "LIGHTSALMON4": "#8B5742", "LightSeaGreen": "#20B2AA", "lightseagreen": "#20B2AA", "LIGHTSEAGREEN": "#20B2AA", "LightSkyBlue": "#87CEFA", "lightskyblue": "#87CEFA", "LIGHTSKYBLUE": "#87CEFA", "LightSkyBlue1": "#B0E2FF", "lightskyblue1": "#B0E2FF", "LIGHTSKYBLUE1": "#B0E2FF", "LightSkyBlue2": "#A4D3EE", "lightskyblue2": "#A4D3EE", "LIGHTSKYBLUE2": "#A4D3EE", "LightSkyBlue3": "#8DB6CD", "lightskyblue3": "#8DB6CD", "LIGHTSKYBLUE3": "#8DB6CD", "LightSkyBlue4": "#607B8B", "lightskyblue4": "#607B8B", "LIGHTSKYBLUE4": "#607B8B", "LightSlateBlue": "#8470FF", "lightslateblue": "#8470FF", "LIGHTSLATEBLUE": "#8470FF", "LightSlateGray": "#778899", "lightslategray": "#778899", "LIGHTSLATEGRAY": "#778899", "LightSlateGrey": "#778899", "lightslategrey": "#778899", "LIGHTSLATEGREY": "#778899", "LightSteelBlue": "#B0C4DE", "lightsteelblue": "#B0C4DE", "LIGHTSTEELBLUE": "#B0C4DE", "LightSteelBlue1": "#CAE1FF", "lightsteelblue1": "#CAE1FF", "LIGHTSTEELBLUE1": "#CAE1FF", "LightSteelBlue2": "#BCD2EE", "lightsteelblue2": "#BCD2EE", "LIGHTSTEELBLUE2": "#BCD2EE", "LightSteelBlue3": "#A2B5CD", "lightsteelblue3": "#A2B5CD", "LIGHTSTEELBLUE3": "#A2B5CD", "LightSteelBlue4": "#6E7B8B", "lightsteelblue4": "#6E7B8B", "LIGHTSTEELBLUE4": "#6E7B8B", "LightYellow": "#FFFFE0", "lightyellow": "#FFFFE0", "LIGHTYELLOW": "#FFFFE0", "LightYellow1": "#FFFFE0", "lightyellow1": "#FFFFE0", "LIGHTYELLOW1": "#FFFFE0", "LightYellow2": "#EEEED1", "lightyellow2": "#EEEED1", "LIGHTYELLOW2": "#EEEED1", "LightYellow3": "#CDCDB4", "lightyellow3": "#CDCDB4", "LIGHTYELLOW3": "#CDCDB4", "LightYellow4": "#8B8B7A", "lightyellow4": "#8B8B7A", "LIGHTYELLOW4": "#8B8B7A", "lime green": "#32CD32", "LIME GREEN": "#32CD32", "LimeGreen": "#32CD32", "limegreen": "#32CD32", "LIMEGREEN": "#32CD32", "linen": "#FAF0E6", "LINEN": "#FAF0E6", "magenta": "#FF00FF", "MAGENTA": "#FF00FF", "magenta1": "#FF00FF", "MAGENTA1": "#FF00FF", "magenta2": "#EE00EE", "MAGENTA2": "#EE00EE", "magenta3": "#CD00CD", "MAGENTA3": "#CD00CD", "magenta4": "#8B008B", "MAGENTA4": "#8B008B", "maroon": "#B03060", "MAROON": "#B03060", "maroon1": "#FF34B3", "MAROON1": "#FF34B3", "maroon2": "#EE30A7", "MAROON2": "#EE30A7", "maroon3": "#CD2990", "MAROON3": "#CD2990", "maroon4": "#8B1C62", "MAROON4": "#8B1C62", "medium aquamarine": "#66CDAA", "MEDIUM AQUAMARINE": "#66CDAA", "medium blue": "#0000CD", "MEDIUM BLUE": "#0000CD", "medium orchid": "#BA55D3", "MEDIUM ORCHID": "#BA55D3", "medium purple": "#9370DB", "MEDIUM PURPLE": "#9370DB", "medium sea green": "#3CB371", "MEDIUM SEA GREEN": "#3CB371", "medium slate blue": "#7B68EE", "MEDIUM SLATE BLUE": "#7B68EE", "medium spring green": "#00FA9A", "MEDIUM SPRING GREEN": "#00FA9A", "medium turquoise": "#48D1CC", "MEDIUM TURQUOISE": "#48D1CC", "medium violet red": "#C71585", "MEDIUM VIOLET RED": "#C71585", "MediumAquamarine": "#66CDAA", "mediumaquamarine": "#66CDAA", "MEDIUMAQUAMARINE": "#66CDAA", "MediumBlue": "#0000CD", "mediumblue": "#0000CD", "MEDIUMBLUE": "#0000CD", "MediumOrchid": "#BA55D3", "mediumorchid": "#BA55D3", "MEDIUMORCHID": "#BA55D3", "MediumOrchid1": "#E066FF", "mediumorchid1": "#E066FF", "MEDIUMORCHID1": "#E066FF", "MediumOrchid2": "#D15FEE", "mediumorchid2": "#D15FEE", "MEDIUMORCHID2": "#D15FEE", "MediumOrchid3": "#B452CD", "mediumorchid3": "#B452CD", "MEDIUMORCHID3": "#B452CD", "MediumOrchid4": "#7A378B", "mediumorchid4": "#7A378B", "MEDIUMORCHID4": "#7A378B", "MediumPurple": "#9370DB", "mediumpurple": "#9370DB", "MEDIUMPURPLE": "#9370DB", "MediumPurple1": "#AB82FF", "mediumpurple1": "#AB82FF", "MEDIUMPURPLE1": "#AB82FF", "MediumPurple2": "#9F79EE", "mediumpurple2": "#9F79EE", "MEDIUMPURPLE2": "#9F79EE", "MediumPurple3": "#8968CD", "mediumpurple3": "#8968CD", "MEDIUMPURPLE3": "#8968CD", "MediumPurple4": "#5D478B", "mediumpurple4": "#5D478B", "MEDIUMPURPLE4": "#5D478B", "MediumSeaGreen": "#3CB371", "mediumseagreen": "#3CB371", "MEDIUMSEAGREEN": "#3CB371", "MediumSlateBlue": "#7B68EE", "mediumslateblue": "#7B68EE", "MEDIUMSLATEBLUE": "#7B68EE", "MediumSpringGreen": "#00FA9A", "mediumspringgreen": "#00FA9A", "MEDIUMSPRINGGREEN": "#00FA9A", "MediumTurquoise": "#48D1CC", "mediumturquoise": "#48D1CC", "MEDIUMTURQUOISE": "#48D1CC", "MediumVioletRed": "#C71585", "mediumvioletred": "#C71585", "MEDIUMVIOLETRED": "#C71585", "midnight blue": "#191970", "MIDNIGHT BLUE": "#191970", "MidnightBlue": "#191970", "midnightblue": "#191970", "MIDNIGHTBLUE": "#191970", "mint cream": "#F5FFFA", "MINT CREAM": "#F5FFFA", "MintCream": "#F5FFFA", "mintcream": "#F5FFFA", "MINTCREAM": "#F5FFFA", "misty rose": "#FFE4E1", "MISTY ROSE": "#FFE4E1", "MistyRose": "#FFE4E1", "mistyrose": "#FFE4E1", "MISTYROSE": "#FFE4E1", "MistyRose1": "#FFE4E1", "mistyrose1": "#FFE4E1", "MISTYROSE1": "#FFE4E1", "MistyRose2": "#EED5D2", "mistyrose2": "#EED5D2", "MISTYROSE2": "#EED5D2", "MistyRose3": "#CDB7B5", "mistyrose3": "#CDB7B5", "MISTYROSE3": "#CDB7B5", "MistyRose4": "#8B7D7B", "mistyrose4": "#8B7D7B", "MISTYROSE4": "#8B7D7B", "moccasin": "#FFE4B5", "MOCCASIN": "#FFE4B5", "navajo white": "#FFDEAD", "NAVAJO WHITE": "#FFDEAD", "NavajoWhite": "#FFDEAD", "navajowhite": "#FFDEAD", "NAVAJOWHITE": "#FFDEAD", "NavajoWhite1": "#FFDEAD", "navajowhite1": "#FFDEAD", "NAVAJOWHITE1": "#FFDEAD", "NavajoWhite2": "#EECFA1", "navajowhite2": "#EECFA1", "NAVAJOWHITE2": "#EECFA1", "NavajoWhite3": "#CDB38B", "navajowhite3": "#CDB38B", "NAVAJOWHITE3": "#CDB38B", "NavajoWhite4": "#8B795E", "navajowhite4": "#8B795E", "NAVAJOWHITE4": "#8B795E", "navy": "#000080", "NAVY": "#000080", "navy blue": "#000080", "NAVY BLUE": "#000080", "NavyBlue": "#000080", "navyblue": "#000080", "NAVYBLUE": "#000080", "old lace": "#FDF5E6", "OLD LACE": "#FDF5E6", "OldLace": "#FDF5E6", "oldlace": "#FDF5E6", "OLDLACE": "#FDF5E6", "olive drab": "#6B8E23", "OLIVE DRAB": "#6B8E23", "OliveDrab": "#6B8E23", "olivedrab": "#6B8E23", "OLIVEDRAB": "#6B8E23", "OliveDrab1": "#C0FF3E", "olivedrab1": "#C0FF3E", "OLIVEDRAB1": "#C0FF3E", "OliveDrab2": "#B3EE3A", "olivedrab2": "#B3EE3A", "OLIVEDRAB2": "#B3EE3A", "OliveDrab3": "#9ACD32", "olivedrab3": "#9ACD32", "OLIVEDRAB3": "#9ACD32", "OliveDrab4": "#698B22", "olivedrab4": "#698B22", "OLIVEDRAB4": "#698B22", "orange": "#FFA500", "ORANGE": "#FFA500", "orange red": "#FF4500", "ORANGE RED": "#FF4500", "orange1": "#FFA500", "ORANGE1": "#FFA500", "orange2": "#EE9A00", "ORANGE2": "#EE9A00", "orange3": "#CD8500", "ORANGE3": "#CD8500", "orange4": "#8B5A00", "ORANGE4": "#8B5A00", "OrangeRed": "#FF4500", "orangered": "#FF4500", "ORANGERED": "#FF4500", "OrangeRed1": "#FF4500", "orangered1": "#FF4500", "ORANGERED1": "#FF4500", "OrangeRed2": "#EE4000", "orangered2": "#EE4000", "ORANGERED2": "#EE4000", "OrangeRed3": "#CD3700", "orangered3": "#CD3700", "ORANGERED3": "#CD3700", "OrangeRed4": "#8B2500", "orangered4": "#8B2500", "ORANGERED4": "#8B2500", "orchid": "#DA70D6", "ORCHID": "#DA70D6", "orchid1": "#FF83FA", "ORCHID1": "#FF83FA", "orchid2": "#EE7AE9", "ORCHID2": "#EE7AE9", "orchid3": "#CD69C9", "ORCHID3": "#CD69C9", "orchid4": "#8B4789", "ORCHID4": "#8B4789", "pale goldenrod": "#EEE8AA", "PALE GOLDENROD": "#EEE8AA", "pale green": "#98FB98", "PALE GREEN": "#98FB98", "pale turquoise": "#AFEEEE", "PALE TURQUOISE": "#AFEEEE", "pale violet red": "#DB7093", "PALE VIOLET RED": "#DB7093", "PaleGoldenrod": "#EEE8AA", "palegoldenrod": "#EEE8AA", "PALEGOLDENROD": "#EEE8AA", "PaleGreen": "#98FB98", "palegreen": "#98FB98", "PALEGREEN": "#98FB98", "PaleGreen1": "#9AFF9A", "palegreen1": "#9AFF9A", "PALEGREEN1": "#9AFF9A", "PaleGreen2": "#90EE90", "palegreen2": "#90EE90", "PALEGREEN2": "#90EE90", "PaleGreen3": "#7CCD7C", "palegreen3": "#7CCD7C", "PALEGREEN3": "#7CCD7C", "PaleGreen4": "#548B54", "palegreen4": "#548B54", "PALEGREEN4": "#548B54", "PaleTurquoise": "#AFEEEE", "paleturquoise": "#AFEEEE", "PALETURQUOISE": "#AFEEEE", "PaleTurquoise1": "#BBFFFF", "paleturquoise1": "#BBFFFF", "PALETURQUOISE1": "#BBFFFF", "PaleTurquoise2": "#AEEEEE", "paleturquoise2": "#AEEEEE", "PALETURQUOISE2": "#AEEEEE", "PaleTurquoise3": "#96CDCD", "paleturquoise3": "#96CDCD", "PALETURQUOISE3": "#96CDCD", "PaleTurquoise4": "#668B8B", "paleturquoise4": "#668B8B", "PALETURQUOISE4": "#668B8B", "PaleVioletRed": "#DB7093", "palevioletred": "#DB7093", "PALEVIOLETRED": "#DB7093", "PaleVioletRed1": "#FF82AB", "palevioletred1": "#FF82AB", "PALEVIOLETRED1": "#FF82AB", "PaleVioletRed2": "#EE799F", "palevioletred2": "#EE799F", "PALEVIOLETRED2": "#EE799F", "PaleVioletRed3": "#CD687F", "palevioletred3": "#CD687F", "PALEVIOLETRED3": "#CD687F", "PaleVioletRed4": "#8B475D", "palevioletred4": "#8B475D", "PALEVIOLETRED4": "#8B475D", "papaya whip": "#FFEFD5", "PAPAYA WHIP": "#FFEFD5", "PapayaWhip": "#FFEFD5", "papayawhip": "#FFEFD5", "PAPAYAWHIP": "#FFEFD5", "peach puff": "#FFDAB9", "PEACH PUFF": "#FFDAB9", "PeachPuff": "#FFDAB9", "peachpuff": "#FFDAB9", "PEACHPUFF": "#FFDAB9", "PeachPuff1": "#FFDAB9", "peachpuff1": "#FFDAB9", "PEACHPUFF1": "#FFDAB9", "PeachPuff2": "#EECBAD", "peachpuff2": "#EECBAD", "PEACHPUFF2": "#EECBAD", "PeachPuff3": "#CDAF95", "peachpuff3": "#CDAF95", "PEACHPUFF3": "#CDAF95", "PeachPuff4": "#8B7765", "peachpuff4": "#8B7765", "PEACHPUFF4": "#8B7765", "peru": "#CD853F", "PERU": "#CD853F", "pink": "#FFC0CB", "PINK": "#FFC0CB", "pink1": "#FFB5C5", "PINK1": "#FFB5C5", "pink2": "#EEA9B8", "PINK2": "#EEA9B8", "pink3": "#CD919E", "PINK3": "#CD919E", "pink4": "#8B636C", "PINK4": "#8B636C", "plum": "#DDA0DD", "PLUM": "#DDA0DD", "plum1": "#FFBBFF", "PLUM1": "#FFBBFF", "plum2": "#EEAEEE", "PLUM2": "#EEAEEE", "plum3": "#CD96CD", "PLUM3": "#CD96CD", "plum4": "#8B668B", "PLUM4": "#8B668B", "powder blue": "#B0E0E6", "POWDER BLUE": "#B0E0E6", "PowderBlue": "#B0E0E6", "powderblue": "#B0E0E6", "POWDERBLUE": "#B0E0E6", "purple": "#A020F0", "PURPLE": "#A020F0", "purple1": "#9B30FF", "PURPLE1": "#9B30FF", "purple2": "#912CEE", "PURPLE2": "#912CEE", "purple3": "#7D26CD", "PURPLE3": "#7D26CD", "purple4": "#551A8B", "PURPLE4": "#551A8B", "red": "#FF0000", "RED": "#FF0000", "red1": "#FF0000", "RED1": "#FF0000", "red2": "#EE0000", "RED2": "#EE0000", "red3": "#CD0000", "RED3": "#CD0000", "red4": "#8B0000", "RED4": "#8B0000", "rosy brown": "#BC8F8F", "ROSY BROWN": "#BC8F8F", "RosyBrown": "#BC8F8F", "rosybrown": "#BC8F8F", "ROSYBROWN": "#BC8F8F", "RosyBrown1": "#FFC1C1", "rosybrown1": "#FFC1C1", "ROSYBROWN1": "#FFC1C1", "RosyBrown2": "#EEB4B4", "rosybrown2": "#EEB4B4", "ROSYBROWN2": "#EEB4B4", "RosyBrown3": "#CD9B9B", "rosybrown3": "#CD9B9B", "ROSYBROWN3": "#CD9B9B", "RosyBrown4": "#8B6969", "rosybrown4": "#8B6969", "ROSYBROWN4": "#8B6969", "royal blue": "#4169E1", "ROYAL BLUE": "#4169E1", "RoyalBlue": "#4169E1", "royalblue": "#4169E1", "ROYALBLUE": "#4169E1", "RoyalBlue1": "#4876FF", "royalblue1": "#4876FF", "ROYALBLUE1": "#4876FF", "RoyalBlue2": "#436EEE", "royalblue2": "#436EEE", "ROYALBLUE2": "#436EEE", "RoyalBlue3": "#3A5FCD", "royalblue3": "#3A5FCD", "ROYALBLUE3": "#3A5FCD", "RoyalBlue4": "#27408B", "royalblue4": "#27408B", "ROYALBLUE4": "#27408B", "saddle brown": "#8B4513", "SADDLE BROWN": "#8B4513", "SaddleBrown": "#8B4513", "saddlebrown": "#8B4513", "SADDLEBROWN": "#8B4513", "salmon": "#FA8072", "SALMON": "#FA8072", "salmon1": "#FF8C69", "SALMON1": "#FF8C69", "salmon2": "#EE8262", "SALMON2": "#EE8262", "salmon3": "#CD7054", "SALMON3": "#CD7054", "salmon4": "#8B4C39", "SALMON4": "#8B4C39", "sandy brown": "#F4A460", "SANDY BROWN": "#F4A460", "SandyBrown": "#F4A460", "sandybrown": "#F4A460", "SANDYBROWN": "#F4A460", "sea green": "#2E8B57", "SEA GREEN": "#2E8B57", "SeaGreen": "#2E8B57", "seagreen": "#2E8B57", "SEAGREEN": "#2E8B57", "SeaGreen1": "#54FF9F", "seagreen1": "#54FF9F", "SEAGREEN1": "#54FF9F", "SeaGreen2": "#4EEE94", "seagreen2": "#4EEE94", "SEAGREEN2": "#4EEE94", "SeaGreen3": "#43CD80", "seagreen3": "#43CD80", "SEAGREEN3": "#43CD80", "SeaGreen4": "#2E8B57", "seagreen4": "#2E8B57", "SEAGREEN4": "#2E8B57", "seashell": "#FFF5EE", "SEASHELL": "#FFF5EE", "seashell1": "#FFF5EE", "SEASHELL1": "#FFF5EE", "seashell2": "#EEE5DE", "SEASHELL2": "#EEE5DE", "seashell3": "#CDC5BF", "SEASHELL3": "#CDC5BF", "seashell4": "#8B8682", "SEASHELL4": "#8B8682", "sienna": "#A0522D", "SIENNA": "#A0522D", "sienna1": "#FF8247", "SIENNA1": "#FF8247", "sienna2": "#EE7942", "SIENNA2": "#EE7942", "sienna3": "#CD6839", "SIENNA3": "#CD6839", "sienna4": "#8B4726", "SIENNA4": "#8B4726", "sky blue": "#87CEEB", "SKY BLUE": "#87CEEB", "SkyBlue": "#87CEEB", "skyblue": "#87CEEB", "SKYBLUE": "#87CEEB", "SkyBlue1": "#87CEFF", "skyblue1": "#87CEFF", "SKYBLUE1": "#87CEFF", "SkyBlue2": "#7EC0EE", "skyblue2": "#7EC0EE", "SKYBLUE2": "#7EC0EE", "SkyBlue3": "#6CA6CD", "skyblue3": "#6CA6CD", "SKYBLUE3": "#6CA6CD", "SkyBlue4": "#4A708B", "skyblue4": "#4A708B", "SKYBLUE4": "#4A708B", "slate blue": "#6A5ACD", "SLATE BLUE": "#6A5ACD", "slate gray": "#708090", "SLATE GRAY": "#708090", "slate grey": "#708090", "SLATE GREY": "#708090", "SlateBlue": "#6A5ACD", "slateblue": "#6A5ACD", "SLATEBLUE": "#6A5ACD", "SlateBlue1": "#836FFF", "slateblue1": "#836FFF", "SLATEBLUE1": "#836FFF", "SlateBlue2": "#7A67EE", "slateblue2": "#7A67EE", "SLATEBLUE2": "#7A67EE", "SlateBlue3": "#6959CD", "slateblue3": "#6959CD", "SLATEBLUE3": "#6959CD", "SlateBlue4": "#473C8B", "slateblue4": "#473C8B", "SLATEBLUE4": "#473C8B", "SlateGray": "#708090", "slategray": "#708090", "SLATEGRAY": "#708090", "SlateGray1": "#C6E2FF", "slategray1": "#C6E2FF", "SLATEGRAY1": "#C6E2FF", "SlateGray2": "#B9D3EE", "slategray2": "#B9D3EE", "SLATEGRAY2": "#B9D3EE", "SlateGray3": "#9FB6CD", "slategray3": "#9FB6CD", "SLATEGRAY3": "#9FB6CD", "SlateGray4": "#6C7B8B", "slategray4": "#6C7B8B", "SLATEGRAY4": "#6C7B8B", "SlateGrey": "#708090", "slategrey": "#708090", "SLATEGREY": "#708090", "snow": "#FFFAFA", "SNOW": "#FFFAFA", "snow1": "#FFFAFA", "SNOW1": "#FFFAFA", "snow2": "#EEE9E9", "SNOW2": "#EEE9E9", "snow3": "#CDC9C9", "SNOW3": "#CDC9C9", "snow4": "#8B8989", "SNOW4": "#8B8989", "spring green": "#00FF7F", "SPRING GREEN": "#00FF7F", "SpringGreen": "#00FF7F", "springgreen": "#00FF7F", "SPRINGGREEN": "#00FF7F", "SpringGreen1": "#00FF7F", "springgreen1": "#00FF7F", "SPRINGGREEN1": "#00FF7F", "SpringGreen2": "#00EE76", "springgreen2": "#00EE76", "SPRINGGREEN2": "#00EE76", "SpringGreen3": "#00CD66", "springgreen3": "#00CD66", "SPRINGGREEN3": "#00CD66", "SpringGreen4": "#008B45", "springgreen4": "#008B45", "SPRINGGREEN4": "#008B45", "steel blue": "#4682B4", "STEEL BLUE": "#4682B4", "SteelBlue": "#4682B4", "steelblue": "#4682B4", "STEELBLUE": "#4682B4", "SteelBlue1": "#63B8FF", "steelblue1": "#63B8FF", "STEELBLUE1": "#63B8FF", "SteelBlue2": "#5CACEE", "steelblue2": "#5CACEE", "STEELBLUE2": "#5CACEE", "SteelBlue3": "#4F94CD", "steelblue3": "#4F94CD", "STEELBLUE3": "#4F94CD", "SteelBlue4": "#36648B", "steelblue4": "#36648B", "STEELBLUE4": "#36648B", "tan": "#D2B48C", "TAN": "#D2B48C", "tan1": "#FFA54F", "TAN1": "#FFA54F", "tan2": "#EE9A49", "TAN2": "#EE9A49", "tan3": "#CD853F", "TAN3": "#CD853F", "tan4": "#8B5A2B", "TAN4": "#8B5A2B", "thistle": "#D8BFD8", "THISTLE": "#D8BFD8", "thistle1": "#FFE1FF", "THISTLE1": "#FFE1FF", "thistle2": "#EED2EE", "THISTLE2": "#EED2EE", "thistle3": "#CDB5CD", "THISTLE3": "#CDB5CD", "thistle4": "#8B7B8B", "THISTLE4": "#8B7B8B", "tomato": "#FF6347", "TOMATO": "#FF6347", "tomato1": "#FF6347", "TOMATO1": "#FF6347", "tomato2": "#EE5C42", "TOMATO2": "#EE5C42", "tomato3": "#CD4F39", "TOMATO3": "#CD4F39", "tomato4": "#8B3626", "TOMATO4": "#8B3626", "turquoise": "#40E0D0", "TURQUOISE": "#40E0D0", "turquoise1": "#00F5FF", "TURQUOISE1": "#00F5FF", "turquoise2": "#00E5EE", "TURQUOISE2": "#00E5EE", "turquoise3": "#00C5CD", "TURQUOISE3": "#00C5CD", "turquoise4": "#00868B", "TURQUOISE4": "#00868B", "violet": "#EE82EE", "VIOLET": "#EE82EE", "violet red": "#D02090", "VIOLET RED": "#D02090", "VioletRed": "#D02090", "violetred": "#D02090", "VIOLETRED": "#D02090", "VioletRed1": "#FF3E96", "violetred1": "#FF3E96", "VIOLETRED1": "#FF3E96", "VioletRed2": "#EE3A8C", "violetred2": "#EE3A8C", "VIOLETRED2": "#EE3A8C", "VioletRed3": "#CD3278", "violetred3": "#CD3278", "VIOLETRED3": "#CD3278", "VioletRed4": "#8B2252", "violetred4": "#8B2252", "VIOLETRED4": "#8B2252", "wheat": "#F5DEB3", "WHEAT": "#F5DEB3", "wheat1": "#FFE7BA", "WHEAT1": "#FFE7BA", "wheat2": "#EED8AE", "WHEAT2": "#EED8AE", "wheat3": "#CDBA96", "WHEAT3": "#CDBA96", "wheat4": "#8B7E66", "WHEAT4": "#8B7E66", "white": "#FFFFFF", "WHITE": "#FFFFFF", "white smoke": "#F5F5F5", "WHITE SMOKE": "#F5F5F5", "WhiteSmoke": "#F5F5F5", "whitesmoke": "#F5F5F5", "WHITESMOKE": "#F5F5F5", "yellow": "#FFFF00", "YELLOW": "#FFFF00", "yellow green": "#9ACD32", "YELLOW GREEN": "#9ACD32", "yellow1": "#FFFF00", "YELLOW1": "#FFFF00", "yellow2": "#EEEE00", "YELLOW2": "#EEEE00", "yellow3": "#CDCD00", "YELLOW3": "#CDCD00", "yellow4": "#8B8B00", "YELLOW4": "#8B8B00", "YellowGreen": "#9ACD32", "yellowgreen": "#9ACD32", "YELLOWGREEN": "#9ACD32", } tksheet-7.2.12/tksheet/column_headers.py000066400000000000000000003032151463432073300202740ustar00rootroot00000000000000from __future__ import annotations import tkinter as tk from collections import defaultdict from collections.abc import ( Callable, Hashable, Sequence, ) from functools import ( partial, ) from itertools import ( cycle, islice, repeat, ) from math import ceil, floor from operator import ( itemgetter, ) from typing import Literal from .colors import ( color_map, ) from .formatters import is_bool_like, try_to_bool from .functions import ( consecutive_ranges, event_dict, get_n2a, is_contiguous, new_tk_event, pickled_event_dict, rounded_box_coords, try_binding, ) from .other_classes import ( DotDict, DraggedRowColumn, DropdownStorage, TextEditorStorage, ) from .text_editor import ( TextEditor, ) from .vars import ( USER_OS, rc_binding, symbols_set, text_editor_close_bindings, text_editor_newline_bindings, text_editor_to_unbind, ) class ColumnHeaders(tk.Canvas): def __init__(self, *args, **kwargs): tk.Canvas.__init__( self, kwargs["parent"], background=kwargs["parent"].ops.header_bg, highlightthickness=0, ) self.PAR = kwargs["parent"] self.current_height = None # is set from within MainTable() __init__ or from Sheet parameters self.MT = None # is set from within MainTable() __init__ self.RI = None # is set from within MainTable() __init__ self.TL = None # is set from within TopLeftRectangle() __init__ self.popup_menu_loc = None self.extra_begin_edit_cell_func = None self.extra_end_edit_cell_func = None self.centre_alignment_text_mod_indexes = (slice(1, None), slice(None, -1)) self.c_align_cyc = cycle(self.centre_alignment_text_mod_indexes) self.b1_pressed_loc = None self.closed_dropdown = None self.being_drawn_item = None self.extra_motion_func = None self.extra_b1_press_func = None self.extra_b1_motion_func = None self.extra_b1_release_func = None self.extra_double_b1_func = None self.ch_extra_begin_drag_drop_func = None self.ch_extra_end_drag_drop_func = None self.extra_rc_func = None self.selection_binding_func = None self.shift_selection_binding_func = None self.ctrl_selection_binding_func = None self.drag_selection_binding_func = None self.column_width_resize_func = None self.width_resizing_enabled = False self.height_resizing_enabled = False self.double_click_resizing_enabled = False self.col_selection_enabled = False self.drag_and_drop_enabled = False self.rc_delete_col_enabled = False self.rc_insert_col_enabled = False self.hide_columns_enabled = False self.edit_cell_enabled = False self.dragged_col = None self.visible_col_dividers = {} self.col_height_resize_bbox = tuple() self.cell_options = {} self.rsz_w = None self.rsz_h = None self.new_col_height = 0 self.lines_start_at = 0 self.currently_resizing_width = False self.currently_resizing_height = False self.ch_rc_popup_menu = None self.dropdown = DropdownStorage() self.text_editor = TextEditorStorage() self.disp_text = {} self.disp_high = {} self.disp_grid = {} self.disp_fill_sels = {} self.disp_resize_lines = {} self.disp_dropdown = {} self.disp_checkbox = {} self.disp_boxes = set() self.hidd_text = {} self.hidd_high = {} self.hidd_grid = {} self.hidd_fill_sels = {} self.hidd_resize_lines = {} self.hidd_dropdown = {} self.hidd_checkbox = {} self.hidd_boxes = set() self.default_header = kwargs["default_header"].lower() self.align = kwargs["header_align"] self.basic_bindings() def event_generate(self, *args, **kwargs) -> None: for arg in args: if self.MT and arg in self.MT.event_linker: self.MT.event_linker[arg]() else: super().event_generate(*args, **kwargs) def basic_bindings(self, enable: bool = True) -> None: if enable: self.bind("", self.mouse_motion) self.bind("", self.b1_press) self.bind("", self.b1_motion) self.bind("", self.b1_release) self.bind("", self.double_b1) self.bind(rc_binding, self.rc) self.bind("", self.mousewheel) if USER_OS == "linux": self.bind("", self.mousewheel) self.bind("", self.mousewheel) else: self.unbind("") self.unbind("") self.unbind("") self.unbind("") self.unbind("") self.unbind(rc_binding) self.unbind("") if USER_OS == "linux": self.unbind("") self.unbind("") def mousewheel(self, event: object) -> None: if isinstance(self.MT._headers, int): maxlines = max( ( len( self.MT.get_valid_cell_data_as_str(self.MT._headers, datacn, get_displayed=True) .rstrip() .split("\n") ) for datacn in range(len(self.MT.data[self.MT._headers])) ), default=0, ) elif isinstance(self.MT._headers, (list, tuple)): maxlines = max( ( len(e.rstrip().split("\n")) if isinstance(e, str) else len(f"{e}".rstrip().split("\n")) for e in self.MT._headers ), default=0, ) if maxlines == 1: maxlines = 0 if self.lines_start_at > maxlines: self.lines_start_at = maxlines if (event.delta < 0 or event.num == 5) and self.lines_start_at < maxlines: self.lines_start_at += 1 elif (event.delta >= 0 or event.num == 4) and self.lines_start_at > 0: self.lines_start_at -= 1 self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False, redraw_table=False) def set_height(self, new_height: int, set_TL: bool = False) -> None: self.current_height = new_height try: self.config(height=new_height) except Exception: return if set_TL and self.TL is not None: self.TL.set_dimensions(new_h=new_height) def rc(self, event: object) -> None: self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) self.focus_set() popup_menu = None if self.MT.identify_col(x=event.x, allow_end=False) is None: self.MT.deselect("all") c = len(self.MT.col_positions) - 1 if self.MT.rc_popup_menus_enabled: popup_menu = self.MT.empty_rc_popup_menu elif self.col_selection_enabled and not self.currently_resizing_width and not self.currently_resizing_height: c = self.MT.identify_col(x=event.x) if c < len(self.MT.col_positions) - 1: if self.MT.col_selected(c): if self.MT.rc_popup_menus_enabled: popup_menu = self.ch_rc_popup_menu else: if self.MT.single_selection_enabled and self.MT.rc_select_enabled: self.select_col(c, redraw=True) elif self.MT.toggle_selection_enabled and self.MT.rc_select_enabled: self.toggle_select_col(c, redraw=True) if self.MT.rc_popup_menus_enabled: popup_menu = self.ch_rc_popup_menu try_binding(self.extra_rc_func, event) if popup_menu is not None: self.popup_menu_loc = c popup_menu.tk_popup(event.x_root, event.y_root) def ctrl_b1_press(self, event: object) -> None: self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) if ( (self.drag_and_drop_enabled or self.col_selection_enabled) and self.MT.ctrl_select_enabled and self.rsz_h is None and self.rsz_w is None ): c = self.MT.identify_col(x=event.x) if c < len(self.MT.col_positions) - 1: c_selected = self.MT.col_selected(c) if not c_selected and self.col_selection_enabled: self.being_drawn_item = True self.being_drawn_item = self.add_selection(c, set_as_current=True, run_binding_func=False) self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) sel_event = self.MT.get_select_event(being_drawn_item=self.being_drawn_item) try_binding(self.ctrl_selection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) elif c_selected: self.MT.deselect(c=c) elif not self.MT.ctrl_select_enabled: self.b1_press(event) def ctrl_shift_b1_press(self, event: object) -> None: self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) x = event.x c = self.MT.identify_col(x=x) if ( (self.drag_and_drop_enabled or self.col_selection_enabled) and self.MT.ctrl_select_enabled and self.rsz_h is None and self.rsz_w is None ): if c < len(self.MT.col_positions) - 1: c_selected = self.MT.col_selected(c) if not c_selected and self.col_selection_enabled: if self.MT.selected and self.MT.selected.type_ == "columns": self.being_drawn_item = self.MT.recreate_selection_box( *self.get_shift_select_box(c, self.MT.selected.column), fill_iid=self.MT.selected.fill_iid, ) else: self.being_drawn_item = self.add_selection( c, run_binding_func=False, set_as_current=True, ) self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) sel_event = self.MT.get_select_event(being_drawn_item=self.being_drawn_item) try_binding(self.ctrl_selection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) elif c_selected: self.dragged_col = DraggedRowColumn( dragged=c, to_move=sorted(self.MT.get_selected_cols()), ) elif not self.MT.ctrl_select_enabled: self.shift_b1_press(event) def shift_b1_press(self, event: object) -> None: self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) x = event.x c = self.MT.identify_col(x=x) if (self.drag_and_drop_enabled or self.col_selection_enabled) and self.rsz_h is None and self.rsz_w is None: if c < len(self.MT.col_positions) - 1: c_selected = self.MT.col_selected(c) if not c_selected and self.col_selection_enabled: if self.MT.selected and self.MT.selected.type_ == "columns": r_to_sel, c_to_sel = self.MT.selected.row, self.MT.selected.column self.MT.deselect("all", redraw=False) self.being_drawn_item = self.MT.create_selection_box( *self.get_shift_select_box(c, c_to_sel), "columns" ) self.MT.set_currently_selected(r_to_sel, c_to_sel, self.being_drawn_item) else: self.being_drawn_item = self.select_col(c, run_binding_func=False) self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) sel_event = self.MT.get_select_event(being_drawn_item=self.being_drawn_item) try_binding(self.shift_selection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) elif c_selected: self.dragged_col = DraggedRowColumn( dragged=c, to_move=sorted(self.MT.get_selected_cols()), ) def get_shift_select_box(self, c: int, min_c: int) -> tuple[int, int, int, int, str]: if c > min_c: return 0, min_c, len(self.MT.row_positions) - 1, c + 1 elif c < min_c: return 0, c, len(self.MT.row_positions) - 1, min_c + 1 def create_resize_line( self, x1: int, y1: int, x2: int, y2: int, width: int, fill: str, tag: str | tuple[str], ) -> None: if self.hidd_resize_lines: t, sh = self.hidd_resize_lines.popitem() self.coords(t, x1, y1, x2, y2) if sh: self.itemconfig(t, width=width, fill=fill, tag=tag) else: self.itemconfig(t, width=width, fill=fill, tag=tag, state="normal") self.lift(t) else: t = self.create_line(x1, y1, x2, y2, width=width, fill=fill, tag=tag) self.disp_resize_lines[t] = True def delete_resize_lines(self) -> None: self.hidd_resize_lines.update(self.disp_resize_lines) self.disp_resize_lines = {} for t, sh in self.hidd_resize_lines.items(): if sh: self.itemconfig(t, tags=("",), state="hidden") self.hidd_resize_lines[t] = False def check_mouse_position_width_resizers(self, x: int, y: int) -> int | None: for c, (x1, y1, x2, y2) in self.visible_col_dividers.items(): if x >= x1 and y >= y1 and x <= x2 and y <= y2: return c def mouse_motion(self, event: object) -> None: if not self.currently_resizing_height and not self.currently_resizing_width: x = self.canvasx(event.x) y = self.canvasy(event.y) mouse_over_resize = False mouse_over_selected = False if self.width_resizing_enabled: c = self.check_mouse_position_width_resizers(x, y) if c is not None: self.rsz_w, mouse_over_resize = c, True if self.MT.current_cursor != "sb_h_double_arrow": self.config(cursor="sb_h_double_arrow") self.MT.current_cursor = "sb_h_double_arrow" else: self.rsz_w = None if self.height_resizing_enabled and not mouse_over_resize: try: x1, y1, x2, y2 = ( self.col_height_resize_bbox[0], self.col_height_resize_bbox[1], self.col_height_resize_bbox[2], self.col_height_resize_bbox[3], ) if x >= x1 and y >= y1 and x <= x2 and y <= y2: self.rsz_h, mouse_over_resize = True, True if self.MT.current_cursor != "sb_v_double_arrow": self.config(cursor="sb_v_double_arrow") self.MT.current_cursor = "sb_v_double_arrow" else: self.rsz_h = None except Exception: self.rsz_h = None if not mouse_over_resize: if self.MT.col_selected(self.MT.identify_col(event, allow_end=False)): mouse_over_selected = True if self.MT.current_cursor != "hand2": self.config(cursor="hand2") self.MT.current_cursor = "hand2" if not mouse_over_resize and not mouse_over_selected: self.MT.reset_mouse_motion_creations() try_binding(self.extra_motion_func, event) def double_b1(self, event: object) -> None: self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) self.focus_set() if ( self.double_click_resizing_enabled and self.width_resizing_enabled and self.rsz_w is not None and not self.currently_resizing_width ): col = self.rsz_w - 1 old_width = self.MT.col_positions[self.rsz_w] - self.MT.col_positions[self.rsz_w - 1] new_width = self.set_col_width(col) self.MT.allow_auto_resize_columns = False self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) if self.column_width_resize_func is not None and old_width != new_width: self.column_width_resize_func( event_dict( name="resize", sheet=self.PAR.name, resized_columns={col: {"old_size": old_width, "new_size": new_width}}, ) ) elif self.col_selection_enabled and self.rsz_h is None and self.rsz_w is None: c = self.MT.identify_col(x=event.x) if c < len(self.MT.col_positions) - 1: if self.MT.single_selection_enabled: self.select_col(c, redraw=True) elif self.MT.toggle_selection_enabled: self.toggle_select_col(c, redraw=True) datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] if ( self.get_cell_kwargs(datacn, key="dropdown") or self.get_cell_kwargs(datacn, key="checkbox") or self.edit_cell_enabled ): self.open_cell(event) self.rsz_w = None self.mouse_motion(event) try_binding(self.extra_double_b1_func, event) def b1_press(self, event: object) -> None: self.MT.unbind("") self.focus_set() self.closed_dropdown = self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) x = self.canvasx(event.x) y = self.canvasy(event.y) c = self.MT.identify_col(x=event.x) self.b1_pressed_loc = c if self.check_mouse_position_width_resizers(x, y) is None: self.rsz_w = None if self.width_resizing_enabled and self.rsz_w is not None: x1, y1, x2, y2 = self.MT.get_canvas_visible_area() self.currently_resizing_width = True x = self.MT.col_positions[self.rsz_w] line2x = self.MT.col_positions[self.rsz_w - 1] self.create_resize_line( x, 0, x, self.current_height, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rwl", ) self.MT.create_resize_line(x, y1, x, y2, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rwl") self.create_resize_line( line2x, 0, line2x, self.current_height, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rwl2", ) self.MT.create_resize_line(line2x, y1, line2x, y2, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rwl2") elif self.height_resizing_enabled and self.rsz_w is None and self.rsz_h is not None: x1, y1, x2, y2 = self.MT.get_canvas_visible_area() self.currently_resizing_height = True y = event.y if y < self.MT.min_header_height: y = int(self.MT.min_header_height) self.new_col_height = y elif self.MT.identify_col(x=event.x, allow_end=False) is None: self.MT.deselect("all") elif self.col_selection_enabled and self.rsz_w is None and self.rsz_h is None: if c < len(self.MT.col_positions) - 1: datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] if ( self.MT.col_selected(c) and not self.event_over_dropdown(c, datacn, event, x) and not self.event_over_checkbox(c, datacn, event, x) ): self.dragged_col = DraggedRowColumn( dragged=c, to_move=sorted(self.MT.get_selected_cols()), ) else: if self.MT.single_selection_enabled: self.being_drawn_item = True self.being_drawn_item = self.select_col(c, redraw=True) elif self.MT.toggle_selection_enabled: self.toggle_select_col(c, redraw=True) try_binding(self.extra_b1_press_func, event) def b1_motion(self, event: object) -> None: x1, y1, x2, y2 = self.MT.get_canvas_visible_area() if self.width_resizing_enabled and self.rsz_w is not None and self.currently_resizing_width: x = self.canvasx(event.x) size = x - self.MT.col_positions[self.rsz_w - 1] if size >= self.MT.min_column_width and size < self.MT.max_column_width: self.hide_resize_and_ctrl_lines(ctrl_lines=False) line2x = self.MT.col_positions[self.rsz_w - 1] self.create_resize_line( x, 0, x, self.current_height, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rwl", ) self.MT.create_resize_line(x, y1, x, y2, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rwl") self.create_resize_line( line2x, 0, line2x, self.current_height, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rwl2", ) self.MT.create_resize_line( line2x, y1, line2x, y2, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rwl2", ) self.drag_width_resize() elif self.height_resizing_enabled and self.rsz_h is not None and self.currently_resizing_height: evy = event.y if evy > self.current_height: y = self.MT.canvasy(evy - self.current_height) if evy > self.MT.max_header_height: evy = int(self.MT.max_header_height) y = self.MT.canvasy(evy - self.current_height) self.new_col_height = evy else: y = evy if y < self.MT.min_header_height: y = int(self.MT.min_header_height) self.new_col_height = y self.drag_height_resize() elif ( self.drag_and_drop_enabled and self.col_selection_enabled and self.MT.anything_selected(exclude_cells=True, exclude_rows=True) and self.rsz_h is None and self.rsz_w is None and self.dragged_col is not None ): x = self.canvasx(event.x) if x > 0: self.show_drag_and_drop_indicators( self.drag_and_drop_motion(event), y1, y2, self.dragged_col.to_move, ) elif ( self.MT.drag_selection_enabled and self.col_selection_enabled and self.rsz_h is None and self.rsz_w is None ): need_redraw = False end_col = self.MT.identify_col(x=event.x) if end_col < len(self.MT.col_positions) - 1 and self.MT.selected: if self.MT.selected.type_ == "columns": box = self.get_b1_motion_box(self.MT.selected.column, end_col) if ( box is not None and self.being_drawn_item is not None and self.MT.coords_and_type(self.being_drawn_item) != box ): if box[3] - box[1] != 1: self.being_drawn_item = self.MT.recreate_selection_box( *box[:-1], fill_iid=self.MT.selected.fill_iid, ) else: self.being_drawn_item = self.select_col(self.MT.selected.column, run_binding_func=False) need_redraw = True sel_event = self.MT.get_select_event(being_drawn_item=self.being_drawn_item) try_binding(self.drag_selection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) if self.scroll_if_event_offscreen(event): need_redraw = True if need_redraw: self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False) try_binding(self.extra_b1_motion_func, event) def drag_height_resize(self) -> None: self.set_height(self.new_col_height, set_TL=True) self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) def get_b1_motion_box(self, start_col: int, end_col: int) -> tuple[int, int, int, int, Literal["columns"]]: if end_col >= start_col: return 0, start_col, len(self.MT.row_positions) - 1, end_col + 1, "columns" elif end_col < start_col: return 0, end_col, len(self.MT.row_positions) - 1, start_col + 1, "columns" def ctrl_b1_motion(self, event: object) -> None: x1, y1, x2, y2 = self.MT.get_canvas_visible_area() if ( self.drag_and_drop_enabled and self.col_selection_enabled and self.MT.anything_selected(exclude_cells=True, exclude_rows=True) and self.rsz_h is None and self.rsz_w is None and self.dragged_col is not None ): x = self.canvasx(event.x) if x > 0: self.show_drag_and_drop_indicators( self.drag_and_drop_motion(event), y1, y2, self.dragged_col.to_move, ) elif ( self.MT.ctrl_select_enabled and self.MT.drag_selection_enabled and self.col_selection_enabled and self.rsz_h is None and self.rsz_w is None ): need_redraw = False end_col = self.MT.identify_col(x=event.x) if end_col < len(self.MT.col_positions) - 1 and self.MT.selected: if self.MT.selected.type_ == "columns": box = self.get_b1_motion_box(self.MT.selected.column, end_col) if ( box is not None and self.being_drawn_item is not None and self.MT.coords_and_type(self.being_drawn_item) != box ): if box[3] - box[1] != 1: self.being_drawn_item = self.MT.recreate_selection_box( *box[:-1], self.MT.selected.fill_iid, ) else: self.MT.hide_selection_box(self.MT.selected.fill_iid) self.being_drawn_item = self.add_selection(box[1], run_binding_func=False) need_redraw = True sel_event = self.MT.get_select_event(being_drawn_item=self.being_drawn_item) try_binding(self.drag_selection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) if self.scroll_if_event_offscreen(event): need_redraw = True if need_redraw: self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False) elif not self.MT.ctrl_select_enabled: self.b1_motion(event) def drag_and_drop_motion(self, event: object) -> float: x = event.x wend = self.winfo_width() xcheck = self.xview() if x >= wend - 0 and len(xcheck) > 1 and xcheck[1] < 1: if x >= wend + 15: self.MT.xview_scroll(2, "units") self.xview_scroll(2, "units") else: self.MT.xview_scroll(1, "units") self.xview_scroll(1, "units") self.fix_xview() self.MT.x_move_synced_scrolls("moveto", self.MT.xview()[0]) self.MT.main_table_redraw_grid_and_text(redraw_header=True) elif x <= 0 and len(xcheck) > 1 and xcheck[0] > 0: if x >= -15: self.MT.xview_scroll(-1, "units") self.xview_scroll(-1, "units") else: self.MT.xview_scroll(-2, "units") self.xview_scroll(-2, "units") self.fix_xview() self.MT.x_move_synced_scrolls("moveto", self.MT.xview()[0]) self.MT.main_table_redraw_grid_and_text(redraw_header=True) col = self.MT.identify_col(x=x) if col == len(self.MT.col_positions) - 1: col -= 1 if col >= self.dragged_col.to_move[0] and col <= self.dragged_col.to_move[-1]: if is_contiguous(self.dragged_col.to_move): return self.MT.col_positions[self.dragged_col.to_move[0]] return self.MT.col_positions[col] elif col > self.dragged_col.to_move[-1]: return self.MT.col_positions[col + 1] return self.MT.col_positions[col] def show_drag_and_drop_indicators( self, xpos: float, y1: float, y2: float, cols: Sequence[int], ) -> None: self.hide_resize_and_ctrl_lines() self.create_resize_line( xpos, 0, xpos, self.current_height, width=3, fill=self.PAR.ops.drag_and_drop_bg, tag="move_columns", ) self.MT.create_resize_line(xpos, y1, xpos, y2, width=3, fill=self.PAR.ops.drag_and_drop_bg, tag="move_columns") for boxst, boxend in consecutive_ranges(cols): self.MT.show_ctrl_outline( start_cell=(boxst, 0), end_cell=(boxend, len(self.MT.row_positions) - 1), dash=(), outline=self.PAR.ops.drag_and_drop_bg, delete_on_timer=False, ) def hide_resize_and_ctrl_lines(self, ctrl_lines: bool = True) -> None: self.delete_resize_lines() self.MT.delete_resize_lines() if ctrl_lines: self.MT.delete_ctrl_outlines() def scroll_if_event_offscreen(self, event: object) -> bool: xcheck = self.xview() need_redraw = False if event.x > self.winfo_width() and len(xcheck) > 1 and xcheck[1] < 1: try: self.MT.xview_scroll(1, "units") self.xview_scroll(1, "units") except Exception: pass self.fix_xview() self.MT.x_move_synced_scrolls("moveto", self.MT.xview()[0]) need_redraw = True elif event.x < 0 and self.canvasx(self.winfo_width()) > 0 and xcheck and xcheck[0] > 0: try: self.xview_scroll(-1, "units") self.MT.xview_scroll(-1, "units") except Exception: pass self.fix_xview() self.MT.x_move_synced_scrolls("moveto", self.MT.xview()[0]) need_redraw = True return need_redraw def fix_xview(self) -> None: xcheck = self.xview() if xcheck and xcheck[0] < 0: self.MT.set_xviews("moveto", 0) elif len(xcheck) > 1 and xcheck[1] > 1: self.MT.set_xviews("moveto", 1) def event_over_dropdown(self, c: int, datacn: int, event: object, canvasx: float) -> bool: if ( event.y < self.MT.header_txt_height + 5 and self.get_cell_kwargs(datacn, key="dropdown") and canvasx < self.MT.col_positions[c + 1] and canvasx > self.MT.col_positions[c + 1] - self.MT.header_txt_height - 4 ): return True return False def event_over_checkbox(self, c: int, datacn: int, event: object, canvasx: float) -> bool: if ( event.y < self.MT.header_txt_height + 5 and self.get_cell_kwargs(datacn, key="checkbox") and canvasx < self.MT.col_positions[c] + self.MT.header_txt_height + 4 ): return True return False def drag_width_resize(self) -> None: new_col_pos = int(self.coords("rwl")[0]) old_width = self.MT.col_positions[self.rsz_w] - self.MT.col_positions[self.rsz_w - 1] size = new_col_pos - self.MT.col_positions[self.rsz_w - 1] if size < self.MT.min_column_width: new_col_pos = ceil(self.MT.col_positions[self.rsz_w - 1] + self.MT.min_column_width) elif size > self.MT.max_column_width: new_col_pos = floor(self.MT.col_positions[self.rsz_w - 1] + self.MT.max_column_width) increment = new_col_pos - self.MT.col_positions[self.rsz_w] self.MT.col_positions[self.rsz_w + 1 :] = [ e + increment for e in islice(self.MT.col_positions, self.rsz_w + 1, None) ] self.MT.col_positions[self.rsz_w] = new_col_pos new_width = self.MT.col_positions[self.rsz_w] - self.MT.col_positions[self.rsz_w - 1] self.MT.allow_auto_resize_columns = False self.MT.recreate_all_selection_boxes() self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) if self.column_width_resize_func is not None and old_width != new_width: self.column_width_resize_func( event_dict( name="resize", sheet=self.PAR.name, resized_columns={self.rsz_w - 1: {"old_size": old_width, "new_size": new_width}}, ) ) def b1_release(self, event: object) -> None: if self.being_drawn_item is not None and (to_sel := self.MT.coords_and_type(self.being_drawn_item)): r_to_sel, c_to_sel = self.MT.selected.row, self.MT.selected.column self.MT.hide_selection_box(self.being_drawn_item) self.MT.set_currently_selected( r_to_sel, c_to_sel, item=self.MT.create_selection_box(*to_sel, set_current=False), ) sel_event = self.MT.get_select_event(being_drawn_item=self.being_drawn_item) try_binding(self.drag_selection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) else: self.being_drawn_item = None self.MT.bind("", self.MT.mousewheel) if self.width_resizing_enabled and self.rsz_w is not None and self.currently_resizing_width: self.drag_width_resize() self.currently_resizing_width = False self.hide_resize_and_ctrl_lines(ctrl_lines=False) elif self.height_resizing_enabled and self.rsz_h is not None and self.currently_resizing_height: self.currently_resizing_height = False self.drag_height_resize() elif ( self.drag_and_drop_enabled and self.col_selection_enabled and self.MT.anything_selected(exclude_cells=True, exclude_rows=True) and self.rsz_h is None and self.rsz_w is None and self.dragged_col is not None and self.find_withtag("move_columns") ): self.hide_resize_and_ctrl_lines() c = self.MT.identify_col(x=event.x) totalcols = len(self.dragged_col.to_move) if ( c is not None and totalcols != len(self.MT.col_positions) - 1 and not ( c >= self.dragged_col.to_move[0] and c <= self.dragged_col.to_move[-1] and is_contiguous(self.dragged_col.to_move) ) ): if c > self.dragged_col.to_move[-1]: c += 1 if c > len(self.MT.col_positions) - 1: c = len(self.MT.col_positions) - 1 event_data = event_dict( name="move_columns", sheet=self.PAR.name, widget=self, boxes=self.MT.get_boxes(), selected=self.MT.selected, value=c, ) if try_binding(self.ch_extra_begin_drag_drop_func, event_data, "begin_move_columns"): data_new_idxs, disp_new_idxs, event_data = self.MT.move_columns_adjust_options_dict( *self.MT.get_args_for_move_columns( move_to=c, to_move=self.dragged_col.to_move, ), move_data=self.PAR.ops.column_drag_and_drop_perform, move_widths=self.PAR.ops.column_drag_and_drop_perform, event_data=event_data, ) event_data["moved"]["columns"] = { "data": data_new_idxs, "displayed": disp_new_idxs, } if self.MT.undo_enabled: self.MT.undo_stack.append(pickled_event_dict(event_data)) self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) try_binding(self.ch_extra_end_drag_drop_func, event_data, "end_move_columns") self.MT.sheet_modified(event_data) elif self.b1_pressed_loc is not None and self.rsz_w is None and self.rsz_h is None: c = self.MT.identify_col(x=event.x) if ( c is not None and c < len(self.MT.col_positions) - 1 and c == self.b1_pressed_loc and self.b1_pressed_loc != self.closed_dropdown ): datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] canvasx = self.canvasx(event.x) if self.event_over_dropdown( c, datacn, event, canvasx, ) or self.event_over_checkbox( c, datacn, event, canvasx, ): self.open_cell(event) else: self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) self.b1_pressed_loc = None self.closed_dropdown = None self.dragged_col = None self.currently_resizing_width = False self.currently_resizing_height = False self.rsz_w = None self.rsz_h = None self.mouse_motion(event) try_binding(self.extra_b1_release_func, event) def toggle_select_col( self, column: int, add_selection: bool = True, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ext: bool = False, ) -> int: if add_selection: if self.MT.col_selected(column): fill_iid = self.MT.deselect(c=column, redraw=redraw) else: fill_iid = self.add_selection( c=column, redraw=redraw, run_binding_func=run_binding_func, set_as_current=set_as_current, ext=ext, ) else: if self.MT.col_selected(column): fill_iid = self.MT.deselect(c=column, redraw=redraw) else: fill_iid = self.select_col(column, redraw=redraw, ext=ext) return fill_iid def select_col( self, c: int, redraw: bool = False, run_binding_func: bool = True, ext: bool = False, ) -> int: boxes_to_hide = tuple(self.MT.selection_boxes) fill_iid = self.MT.create_selection_box(0, c, len(self.MT.row_positions) - 1, c + 1, "columns", ext=ext) for iid in boxes_to_hide: self.MT.hide_selection_box(iid) if redraw: self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) if run_binding_func: self.MT.run_selection_binding("columns") return fill_iid def add_selection( self, c: int, redraw: bool = False, run_binding_func: bool = True, set_as_current: bool = True, ext: bool = False, ) -> int: box = (0, c, len(self.MT.row_positions) - 1, c + 1, "columns") fill_iid = self.MT.create_selection_box(*box, set_current=set_as_current, ext=ext) if redraw: self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) if run_binding_func: self.MT.run_selection_binding("columns") return fill_iid def display_box( self, x1: int, y1: int, x2: int, y2: int, fill: str, outline: str, state: str, tags: str | tuple[str], iid: None | int = None, ) -> int: coords = rounded_box_coords( x1, y1, x2, y2, radius=5 if self.PAR.ops.rounded_boxes else 0, ) if isinstance(iid, int): self.coords(iid, coords) self.itemconfig(iid, fill=fill, outline=outline, state=state, tags=tags) else: if self.hidd_boxes: iid = self.hidd_boxes.pop() self.coords(iid, coords) self.itemconfig(iid, fill=fill, outline=outline, state=state, tags=tags) else: iid = self.create_polygon(coords, fill=fill, outline=outline, state=state, tags=tags, smooth=True) self.disp_boxes.add(iid) return iid def hide_box(self, item: int) -> None: if isinstance(item, int): self.disp_boxes.discard(item) self.hidd_boxes.add(item) self.itemconfig(item, state="hidden") def get_cell_dimensions(self, datacn: int) -> tuple[int, int]: txt = self.get_valid_cell_data_as_str(datacn, fix=False) if txt: self.MT.txt_measure_canvas.itemconfig( self.MT.txt_measure_canvas_text, text=txt, font=self.PAR.ops.header_font, ) b = self.MT.txt_measure_canvas.bbox(self.MT.txt_measure_canvas_text) w = b[2] - b[0] + 7 h = b[3] - b[1] + 5 else: w = self.MT.min_column_width h = self.MT.min_header_height if datacn in self.cell_options and ( self.get_cell_kwargs(datacn, key="dropdown") or self.get_cell_kwargs(datacn, key="checkbox") ): return w + self.MT.header_txt_height, h return w, h def set_height_of_header_to_text( self, text: None | str = None, only_if_too_small: bool = False, ) -> int: h = self.MT.min_header_height if (text is None and not self.MT._headers and isinstance(self.MT._headers, list)) or ( isinstance(self.MT._headers, int) and self.MT._headers >= len(self.MT.data) ): return h self.fix_header() qconf = self.MT.txt_measure_canvas.itemconfig qbbox = self.MT.txt_measure_canvas.bbox qtxtm = self.MT.txt_measure_canvas_text qfont = self.PAR.ops.header_font default_header_height = self.MT.get_default_header_height() if text is not None and text: qconf(qtxtm, text=text, font=qfont) b = qbbox(qtxtm) if (th := b[3] - b[1] + 5) > h: h = th elif text is None: if self.MT.all_columns_displayed: if isinstance(self.MT._headers, list): iterable = range(len(self.MT._headers)) else: iterable = range(len(self.MT.data[self.MT._headers])) else: iterable = self.MT.displayed_columns if ( isinstance(self.MT._headers, list) and (th := max(map(itemgetter(0), map(self.get_cell_dimensions, iterable)), default=h)) > h ): h = th elif isinstance(self.MT._headers, int): datarn = self.MT._headers for datacn in iterable: if txt := self.MT.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True): qconf(qtxtm, text=txt, font=qfont) b = qbbox(qtxtm) th = b[3] - b[1] + 5 else: th = default_header_height if th > h: h = th space_bot = self.MT.get_space_bot(0) if h > space_bot and space_bot > self.MT.min_header_height: h = space_bot if h < self.MT.min_header_height: h = int(self.MT.min_header_height) elif h > self.MT.max_header_height: h = int(self.MT.max_header_height) if not only_if_too_small or (only_if_too_small and h > self.current_height): self.set_height(h, set_TL=True) self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) return h def get_col_text_width( self, col: int, visible_only: bool = False, only_if_too_small: bool = False, ) -> int: self.fix_header() w = self.MT.min_column_width datacn = col if self.MT.all_columns_displayed else self.MT.displayed_columns[col] # header hw, hh_ = self.get_cell_dimensions(datacn) # table if self.MT.data: if self.MT.all_rows_displayed: if visible_only: iterable = range(*self.MT.visible_text_rows) else: iterable = range(0, len(self.MT.data)) else: if visible_only: start_row, end_row = self.MT.visible_text_rows else: start_row, end_row = 0, len(self.MT.displayed_rows) iterable = self.MT.displayed_rows[start_row:end_row] qconf = self.MT.txt_measure_canvas.itemconfig qbbox = self.MT.txt_measure_canvas.bbox qtxtm = self.MT.txt_measure_canvas_text qtxth = self.MT.table_txt_height qfont = self.PAR.ops.table_font for datarn in iterable: if txt := self.MT.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True): qconf(qtxtm, text=txt, font=qfont) b = qbbox(qtxtm) if ( self.MT.get_cell_kwargs(datarn, datacn, key="dropdown") or self.MT.get_cell_kwargs(datarn, datacn, key="checkbox") ) and (tw := b[2] - b[0] + qtxth + 7) > w: w = tw elif (tw := b[2] - b[0] + 7) > w: w = tw if hw > w: w = hw if only_if_too_small and w < self.MT.col_positions[col + 1] - self.MT.col_positions[col]: w = self.MT.col_positions[col + 1] - self.MT.col_positions[col] if w <= self.MT.min_column_width: w = int(self.MT.min_column_width) elif w > self.MT.max_column_width: w = int(self.MT.max_column_width) return w def set_col_width( self, col: int, width: None | int = None, only_if_too_small: bool = False, visible_only: bool = False, recreate: bool = True, ) -> int: if width is None: width = self.get_col_text_width(col=col, visible_only=visible_only) if width <= self.MT.min_column_width: width = int(self.MT.min_column_width) elif width > self.MT.max_column_width: width = int(self.MT.max_column_width) if only_if_too_small and width <= self.MT.col_positions[col + 1] - self.MT.col_positions[col]: return self.MT.col_positions[col + 1] - self.MT.col_positions[col] new_col_pos = self.MT.col_positions[col] + width increment = new_col_pos - self.MT.col_positions[col + 1] self.MT.col_positions[col + 2 :] = [ e + increment for e in islice(self.MT.col_positions, col + 2, len(self.MT.col_positions)) ] self.MT.col_positions[col + 1] = new_col_pos if recreate: self.MT.recreate_all_selection_boxes() return width def set_width_of_all_cols( self, width: None | int = None, only_if_too_small: bool = False, recreate: bool = True, ) -> None: if width is None: if self.MT.all_columns_displayed: iterable = range(self.MT.total_data_cols()) else: iterable = range(len(self.MT.displayed_columns)) self.MT.set_col_positions( itr=(self.get_col_text_width(cn, only_if_too_small=only_if_too_small) for cn in iterable) ) elif width is not None: if self.MT.all_columns_displayed: self.MT.set_col_positions(itr=repeat(width, self.MT.total_data_cols())) else: self.MT.set_col_positions(itr=repeat(width, len(self.MT.displayed_columns))) if recreate: self.MT.recreate_all_selection_boxes() def redraw_highlight_get_text_fg( self, fc: float, sc: float, c: int, c_2: str, c_3: str, selections: dict, datacn: int, ) -> tuple[str, bool]: redrawn = False kwargs = self.get_cell_kwargs(datacn, key="highlight") if kwargs: if kwargs[0] is not None: c_1 = kwargs[0] if kwargs[0].startswith("#") else color_map[kwargs[0]] if "columns" in selections and c in selections["columns"]: tf = ( self.PAR.ops.header_selected_columns_fg if kwargs[1] is None or self.PAR.ops.display_selected_fg_over_highlights else kwargs[1] ) if kwargs[0] is not None: fill = ( f"#{int((int(c_1[1:3], 16) + int(c_3[1:3], 16)) / 2):02X}" + f"{int((int(c_1[3:5], 16) + int(c_3[3:5], 16)) / 2):02X}" + f"{int((int(c_1[5:], 16) + int(c_3[5:], 16)) / 2):02X}" ) elif "cells" in selections and c in selections["cells"]: tf = ( self.PAR.ops.header_selected_cells_fg if kwargs[1] is None or self.PAR.ops.display_selected_fg_over_highlights else kwargs[1] ) if kwargs[0] is not None: fill = ( f"#{int((int(c_1[1:3], 16) + int(c_2[1:3], 16)) / 2):02X}" + f"{int((int(c_1[3:5], 16) + int(c_2[3:5], 16)) / 2):02X}" + f"{int((int(c_1[5:], 16) + int(c_2[5:], 16)) / 2):02X}" ) else: tf = self.PAR.ops.header_fg if kwargs[1] is None else kwargs[1] if kwargs[0] is not None: fill = kwargs[0] if kwargs[0] is not None: redrawn = self.redraw_highlight( fc + 1, 0, sc, self.current_height - 1, fill=fill, outline=( self.PAR.ops.header_fg if self.get_cell_kwargs(datacn, key="dropdown") and self.PAR.ops.show_dropdown_borders else "" ), tag="hi", ) elif not kwargs: if "columns" in selections and c in selections["columns"]: tf = self.PAR.ops.header_selected_columns_fg elif "cells" in selections and c in selections["cells"]: tf = self.PAR.ops.header_selected_cells_fg else: tf = self.PAR.ops.header_fg return tf, redrawn def redraw_highlight( self, x1: float, y1: float, x2: float, y2: float, fill: str, outline: str, tag: str | tuple[str], ) -> bool: coords = (x1, y1, x2, y2) if self.hidd_high: iid, showing = self.hidd_high.popitem() self.coords(iid, coords) if showing: self.itemconfig(iid, fill=fill, outline=outline) else: self.itemconfig(iid, fill=fill, outline=outline, tag=tag, state="normal") else: iid = self.create_rectangle(coords, fill=fill, outline=outline, tag=tag) self.disp_high[iid] = True return True def redraw_gridline( self, points: Sequence[float], fill: str, width: int, tag: str | tuple[str], ) -> None: if self.hidd_grid: t, sh = self.hidd_grid.popitem() self.coords(t, points) if sh: self.itemconfig(t, fill=fill, width=width, tag=tag) else: self.itemconfig(t, fill=fill, width=width, tag=tag, state="normal") self.disp_grid[t] = True else: self.disp_grid[self.create_line(points, fill=fill, width=width, tag=tag)] = True def redraw_dropdown( self, x1: float, y1: float, x2: float, y2: float, fill: str, outline: str, tag: str | tuple[str], draw_outline: bool = True, draw_arrow: bool = True, open_: bool = False, ) -> None: if draw_outline and self.PAR.ops.show_dropdown_borders: self.redraw_highlight(x1 + 1, y1 + 1, x2, y2, fill="", outline=self.PAR.ops.header_fg, tag=tag) if draw_arrow: mod = (self.MT.header_txt_height - 1) if self.MT.header_txt_height % 2 else self.MT.header_txt_height small_mod = int(mod / 5) mid_y = floor(self.MT.min_header_height / 2) if open_: # up arrow points = ( x2 - 4 - small_mod - small_mod - small_mod - small_mod, y1 + mid_y + small_mod, x2 - 4 - small_mod - small_mod, y1 + mid_y - small_mod, x2 - 4, y1 + mid_y + small_mod, ) else: # down arrow points = ( x2 - 4 - small_mod - small_mod - small_mod - small_mod, y1 + mid_y - small_mod, x2 - 4 - small_mod - small_mod, y1 + mid_y + small_mod, x2 - 4, y1 + mid_y - small_mod, ) if self.hidd_dropdown: t, sh = self.hidd_dropdown.popitem() self.coords(t, points) if sh: self.itemconfig(t, fill=fill) else: self.itemconfig(t, fill=fill, tag=tag, state="normal") self.lift(t) else: t = self.create_line( points, fill=fill, tag=tag, width=2, capstyle=tk.ROUND, joinstyle=tk.BEVEL, ) self.disp_dropdown[t] = True def redraw_checkbox( self, x1: float, y1: float, x2: float, y2: float, fill: str, outline: str, tag: str | tuple[str], draw_check: bool = False, ) -> None: points = rounded_box_coords(x1, y1, x2, y2) if self.hidd_checkbox: t, sh = self.hidd_checkbox.popitem() self.coords(t, points) if sh: self.itemconfig(t, fill=outline, outline=fill) else: self.itemconfig(t, fill=outline, outline=fill, tag=tag, state="normal") self.lift(t) else: t = self.create_polygon(points, fill=outline, outline=fill, tag=tag, smooth=True) self.disp_checkbox[t] = True if draw_check: # draw filled box x1 = x1 + 4 y1 = y1 + 4 x2 = x2 - 3 y2 = y2 - 3 points = rounded_box_coords(x1, y1, x2, y2, radius=4) if self.hidd_checkbox: t, sh = self.hidd_checkbox.popitem() self.coords(t, points) if sh: self.itemconfig(t, fill=fill, outline=outline) else: self.itemconfig(t, fill=fill, outline=outline, tag=tag, state="normal") self.lift(t) else: t = self.create_polygon(points, fill=fill, outline=outline, tag=tag, smooth=True) self.disp_checkbox[t] = True def configure_scrollregion(self, last_col_line_pos: float) -> None: self.configure( scrollregion=( 0, 0, last_col_line_pos + self.PAR.ops.empty_horizontal + 2, self.current_height, ) ) def redraw_grid_and_text( self, last_col_line_pos: float, scrollpos_left: float, x_stop: float, grid_start_col: int, grid_end_col: int, text_start_col: int, text_end_col: int, scrollpos_right: float, col_pos_exists: bool, ) -> bool: try: self.configure_scrollregion(last_col_line_pos=last_col_line_pos) except Exception: return False self.hidd_text.update(self.disp_text) self.disp_text = {} self.hidd_high.update(self.disp_high) self.disp_high = {} self.hidd_grid.update(self.disp_grid) self.disp_grid = {} self.hidd_dropdown.update(self.disp_dropdown) self.disp_dropdown = {} self.hidd_checkbox.update(self.disp_checkbox) self.disp_checkbox = {} self.visible_col_dividers = {} self.col_height_resize_bbox = ( scrollpos_left, self.current_height - 2, x_stop, self.current_height, ) draw_x = self.MT.col_positions[grid_start_col] yend = self.current_height - 5 if (self.PAR.ops.show_vertical_grid or self.width_resizing_enabled) and col_pos_exists: points = [ x_stop - 1, self.current_height - 1, scrollpos_left - 1, self.current_height - 1, scrollpos_left - 1, -1, ] for c in range(grid_start_col, grid_end_col): draw_x = self.MT.col_positions[c] if c and self.width_resizing_enabled: self.visible_col_dividers[c] = (draw_x - 2, 1, draw_x + 2, yend) points.extend( ( draw_x, -1, draw_x, self.current_height, draw_x, -1, self.MT.col_positions[c + 1] if len(self.MT.col_positions) - 1 > c else draw_x, -1, ) ) self.redraw_gridline(points=points, fill=self.PAR.ops.header_grid_fg, width=1, tag="v") top = self.canvasy(0) c_2 = ( self.PAR.ops.header_selected_cells_bg if self.PAR.ops.header_selected_cells_bg.startswith("#") else color_map[self.PAR.ops.header_selected_cells_bg] ) c_3 = ( self.PAR.ops.header_selected_columns_bg if self.PAR.ops.header_selected_columns_bg.startswith("#") else color_map[self.PAR.ops.header_selected_columns_bg] ) font = self.PAR.ops.header_font selections = self.get_redraw_selections(text_start_col, grid_end_col) dd_coords = self.dropdown.get_coords() for c in range(text_start_col, text_end_col): draw_y = self.MT.header_first_ln_ins cleftgridln = self.MT.col_positions[c] crightgridln = self.MT.col_positions[c + 1] datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] fill, dd_drawn = self.redraw_highlight_get_text_fg( cleftgridln, crightgridln, c, c_2, c_3, selections, datacn ) if datacn in self.cell_options and "align" in self.cell_options[datacn]: align = self.cell_options[datacn]["align"] else: align = self.align kwargs = self.get_cell_kwargs(datacn, key="dropdown") if align == "w": draw_x = cleftgridln + 3 if kwargs: mw = crightgridln - cleftgridln - self.MT.header_txt_height - 2 self.redraw_dropdown( cleftgridln, 0, crightgridln, self.current_height - 1, fill=fill, outline=fill, tag="dd", draw_outline=not dd_drawn, draw_arrow=mw >= 5, open_=dd_coords == c, ) else: mw = crightgridln - cleftgridln - 1 elif align == "e": if kwargs: mw = crightgridln - cleftgridln - self.MT.header_txt_height - 2 draw_x = crightgridln - 5 - self.MT.header_txt_height self.redraw_dropdown( cleftgridln, 0, crightgridln, self.current_height - 1, fill=fill, outline=fill, tag="dd", draw_outline=not dd_drawn, draw_arrow=mw >= 5, open_=dd_coords == c, ) else: mw = crightgridln - cleftgridln - 1 draw_x = crightgridln - 3 elif align == "center": if kwargs: mw = crightgridln - cleftgridln - self.MT.header_txt_height - 2 draw_x = cleftgridln + ceil((crightgridln - cleftgridln - self.MT.header_txt_height) / 2) self.redraw_dropdown( cleftgridln, 0, crightgridln, self.current_height - 1, fill=fill, outline=fill, tag="dd", draw_outline=not dd_drawn, draw_arrow=mw >= 5, open_=dd_coords == c, ) else: mw = crightgridln - cleftgridln - 1 draw_x = cleftgridln + floor((crightgridln - cleftgridln) / 2) if not kwargs: kwargs = self.get_cell_kwargs(datacn, key="checkbox") if kwargs and mw > self.MT.header_txt_height + 1: box_w = self.MT.header_txt_height + 1 if align == "w": draw_x += box_w + 3 elif align == "center": draw_x += ceil(box_w / 2) + 1 mw -= box_w + 3 try: draw_check = ( self.MT._headers[datacn] if isinstance(self.MT._headers, (list, tuple)) else self.MT.data[self.MT._headers][datacn] ) except Exception: draw_check = False self.redraw_checkbox( cleftgridln + 2, 2, cleftgridln + self.MT.header_txt_height + 3, self.MT.header_txt_height + 3, fill=fill if kwargs["state"] == "normal" else self.PAR.ops.header_grid_fg, outline="", tag="cb", draw_check=draw_check, ) lns = self.get_valid_cell_data_as_str(datacn, fix=False) if not lns: continue lns = lns.split("\n") if mw > self.MT.header_txt_width and not ( (align == "w" and draw_x > scrollpos_right) or (align == "e" and cleftgridln + 5 > scrollpos_right) or (align == "center" and cleftgridln + 5 > scrollpos_right) ): for txt in islice( lns, self.lines_start_at if self.lines_start_at < len(lns) else len(lns) - 1, None, ): if draw_y > top: if self.hidd_text: iid, showing = self.hidd_text.popitem() self.coords(iid, draw_x, draw_y) if showing: self.itemconfig( iid, text=txt, fill=fill, font=font, anchor=align, ) else: self.itemconfig( iid, text=txt, fill=fill, font=font, anchor=align, state="normal", ) self.tag_raise(iid) else: iid = self.create_text( draw_x, draw_y, text=txt, fill=fill, font=font, anchor=align, tag="t", ) self.disp_text[iid] = True wd = self.bbox(iid) wd = wd[2] - wd[0] if wd > mw: if align == "w": txt = txt[: int(len(txt) * (mw / wd))] self.itemconfig(iid, text=txt) wd = self.bbox(iid) while wd[2] - wd[0] > mw: txt = txt[:-1] self.itemconfig(iid, text=txt) wd = self.bbox(iid) elif align == "e": txt = txt[len(txt) - int(len(txt) * (mw / wd)) :] self.itemconfig(iid, text=txt) wd = self.bbox(iid) while wd[2] - wd[0] > mw: txt = txt[1:] self.itemconfig(iid, text=txt) wd = self.bbox(iid) elif align == "center": self.c_align_cyc = cycle(self.centre_alignment_text_mod_indexes) tmod = ceil((len(txt) - int(len(txt) * (mw / wd))) / 2) txt = txt[tmod - 1 : -tmod] self.itemconfig(iid, text=txt) wd = self.bbox(iid) while wd[2] - wd[0] > mw: txt = txt[next(self.c_align_cyc)] self.itemconfig(iid, text=txt) wd = self.bbox(iid) self.coords(iid, draw_x, draw_y) draw_y += self.MT.header_xtra_lines_increment if draw_y - 1 > self.current_height: break for dct in (self.hidd_text, self.hidd_high, self.hidd_grid, self.hidd_dropdown, self.hidd_checkbox): for iid, showing in dct.items(): if showing: self.itemconfig(iid, state="hidden") dct[iid] = False return True def get_redraw_selections(self, startc: int, endc: int) -> dict[str, set[int]]: d = defaultdict(set) for item, box in self.MT.get_selection_items(): r1, c1, r2, c2 = box.coords for c in range(startc, endc): if c1 <= c and c2 > c: d[box.type_ if box.type_ != "rows" else "cells"].add(c) return d def open_cell(self, event: object = None, ignore_existing_editor: bool = False) -> None: if not self.MT.anything_selected() or (not ignore_existing_editor and self.text_editor.open): return if not self.MT.selected: return c = self.MT.selected.column datacn = self.MT.datacn(c) if self.get_cell_kwargs(datacn, key="readonly"): return elif self.get_cell_kwargs(datacn, key="dropdown") or self.get_cell_kwargs(datacn, key="checkbox"): if self.MT.event_opens_dropdown_or_checkbox(event): if self.get_cell_kwargs(datacn, key="dropdown"): self.open_dropdown_window(c, event=event) elif self.get_cell_kwargs(datacn, key="checkbox"): self.click_checkbox(c, datacn) elif self.edit_cell_enabled: self.open_text_editor(event=event, c=c, dropdown=False) # displayed indexes def get_cell_align(self, c: int) -> str: datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] if datacn in self.cell_options and "align" in self.cell_options[datacn]: align = self.cell_options[datacn]["align"] else: align = self.align return align # c is displayed col def open_text_editor( self, event: object = None, c: int = 0, text: object = None, state: str = "normal", dropdown: bool = False, ) -> bool: text = None extra_func_key = "??" if event is None or self.MT.event_opens_dropdown_or_checkbox(event): if event is not None: if hasattr(event, "keysym") and event.keysym == "Return": extra_func_key = "Return" elif hasattr(event, "keysym") and event.keysym == "F2": extra_func_key = "F2" text = self.get_cell_data(self.MT.datacn(c), none_to_empty_str=True, redirect_int=True) elif event is not None and ( (hasattr(event, "keysym") and event.keysym == "BackSpace") or event.keycode in (8, 855638143) ): extra_func_key = "BackSpace" text = "" elif event is not None and ( (hasattr(event, "char") and event.char.isalpha()) or (hasattr(event, "char") and event.char.isdigit()) or (hasattr(event, "char") and event.char in symbols_set) ): extra_func_key = event.char text = event.char else: return False if self.extra_begin_edit_cell_func: try: text = self.extra_begin_edit_cell_func( event_dict( name="begin_edit_header", sheet=self.PAR.name, key=extra_func_key, value=text, loc=c, column=c, boxes=self.MT.get_boxes(), selected=self.MT.selected, ) ) except Exception: return False if text is None: return False else: text = text if isinstance(text, str) else f"{text}" text = "" if text is None else text if self.PAR.ops.cell_auto_resize_enabled: if self.height_resizing_enabled: self.set_height_of_header_to_text(text) self.set_col_width_run_binding(c) if self.text_editor.open and c == self.text_editor.column: self.text_editor.set_text(self.text_editor.get() + "" if not isinstance(text, str) else text) return self.hide_text_editor() if not self.MT.see(r=0, c=c, keep_yscroll=True, check_cell_visibility=True): self.MT.refresh() x = self.MT.col_positions[c] + 1 y = 0 w = self.MT.col_positions[c + 1] - x h = self.current_height + 1 if text is None: text = self.get_cell_data(self.MT.datacn(c), none_to_empty_str=True, redirect_int=True) bg, fg = self.PAR.ops.header_bg, self.PAR.ops.header_fg kwargs = { "menu_kwargs": DotDict( { "font": self.PAR.ops.table_font, "foreground": self.PAR.ops.popup_menu_fg, "background": self.PAR.ops.popup_menu_bg, "activebackground": self.PAR.ops.popup_menu_highlight_bg, "activeforeground": self.PAR.ops.popup_menu_highlight_fg, } ), "sheet_ops": self.PAR.ops, "border_color": self.PAR.ops.header_selected_columns_bg, "text": text, "state": state, "width": w, "height": h, "show_border": True, "bg": bg, "fg": fg, "align": self.get_cell_align(c), "c": c, } if not self.text_editor.window: self.text_editor.window = TextEditor(self, newline_binding=self.text_editor_newline_binding) self.text_editor.canvas_id = self.create_window((x, y), window=self.text_editor.window, anchor="nw") self.text_editor.window.reset(**kwargs) if not self.text_editor.open: self.itemconfig(self.text_editor.canvas_id, state="normal") self.text_editor.open = True self.coords(self.text_editor.canvas_id, x, y) for b in text_editor_newline_bindings: self.text_editor.tktext.bind(b, self.text_editor_newline_binding) for b in text_editor_close_bindings: self.text_editor.tktext.bind(b, self.close_text_editor) if not dropdown: self.text_editor.tktext.focus_set() self.text_editor.window.scroll_to_bottom() self.text_editor.tktext.bind("", self.close_text_editor) for key, func in self.MT.text_editor_user_bound_keys.items(): self.text_editor.tktext.bind(key, func) return True # displayed indexes def text_editor_has_wrapped( self, r: int = 0, c: int = 0, check_lines: None = None, # just here to receive text editor arg ) -> None: if self.width_resizing_enabled: curr_width = self.text_editor.window.winfo_width() new_width = curr_width + (self.MT.header_txt_height * 2) if new_width != curr_width: self.text_editor.window.config(width=new_width) self.set_col_width_run_binding(c, width=new_width, only_if_too_small=False) if self.dropdown.open and self.dropdown.get_coords() == c: self.itemconfig(self.dropdown.canvas_id, width=new_width) self.dropdown.window.update_idletasks() self.dropdown.window._reselect() self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False, redraw_table=True) self.coords(self.text_editor.canvas_id, self.MT.col_positions[c] + 1, 0) # displayed indexes def text_editor_newline_binding(self, event: object = None, check_lines: bool = True) -> None: if not self.height_resizing_enabled: return curr_height = self.text_editor.window.winfo_height() if curr_height < self.MT.min_header_height: return if ( not check_lines or self.MT.get_lines_cell_height( self.text_editor.window.get_num_lines() + 1, font=self.text_editor.tktext.cget("font"), ) > curr_height ): c = self.text_editor.column new_height = curr_height + self.MT.header_xtra_lines_increment space_bot = self.MT.get_space_bot(0) if new_height > space_bot: new_height = space_bot if new_height != curr_height: self.text_editor.window.config(height=new_height) self.set_height(new_height, set_TL=True) if self.dropdown.open and self.dropdown.get_coords() == c: win_h, anchor = self.get_dropdown_height_anchor(c, new_height) self.coords( self.dropdown.canvas_id, self.MT.col_positions[c], new_height - 1, ) self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) def refresh_open_window_positions(self, zoom: Literal["in", "out"]) -> None: if self.text_editor.open: c = self.text_editor.column self.text_editor.window.config( height=self.current_height, width=self.MT.col_positions[c + 1] - self.MT.col_positions[c] + 1, ) self.text_editor.tktext.config(font=self.PAR.ops.header_font) self.coords( self.text_editor.canvas_id, self.MT.col_positions[c], 0, ) if self.dropdown.open: if zoom == "in": self.dropdown.window.zoom_in() elif zoom == "out": self.dropdown.window.zoom_out() c = self.dropdown.get_coords() if self.text_editor.open: text_editor_h = self.text_editor.window.winfo_height() win_h, anchor = self.get_dropdown_height_anchor(c, text_editor_h) else: text_editor_h = self.current_height anchor = self.itemcget(self.dropdown.canvas_id, "anchor") # win_h = 0 self.dropdown.window.config(width=self.MT.col_positions[c + 1] - self.MT.col_positions[c] + 1) if anchor == "nw": self.coords( self.dropdown.canvas_id, self.MT.col_positions[c], text_editor_h - 1, ) # self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) elif anchor == "sw": self.coords( self.dropdown.canvas_id, self.MT.col_positions[c], 0, ) # self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) def hide_text_editor(self) -> None: if self.text_editor.open: for binding in text_editor_to_unbind: self.text_editor.tktext.unbind(binding) self.itemconfig(self.text_editor.canvas_id, state="hidden") self.text_editor.open = False # c is displayed col def close_text_editor(self, event: tk.Event) -> Literal["break"] | None: # checking if text editor should be closed or not # errors if __tk_filedialog is open try: focused = self.focus_get() except Exception: focused = None try: if focused == self.text_editor.tktext.rc_popup_menu: return "break" except Exception: pass if focused is None: return "break" if event.keysym == "Escape": self.hide_text_editor_and_dropdown() self.focus_set() return # setting cell data with text editor value text_editor_value = self.text_editor.get() c = self.text_editor.column datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] event_data = event_dict( name="end_edit_header", sheet=self.PAR.name, widget=self, cells_header={datacn: self.get_cell_data(datacn)}, key=event.keysym, value=text_editor_value, loc=c, column=c, boxes=self.MT.get_boxes(), selected=self.MT.selected, ) edited = False set_data = partial( self.set_cell_data_undo, c=c, datacn=datacn, check_input_valid=False, ) if self.MT.edit_validation_func: text_editor_value = self.MT.edit_validation_func(event_data) if text_editor_value is not None and self.input_valid_for_cell(datacn, text_editor_value): edited = set_data(value=text_editor_value) elif self.input_valid_for_cell(datacn, text_editor_value): edited = set_data(value=text_editor_value) if edited: try_binding(self.extra_end_edit_cell_func, event_data) self.MT.recreate_all_selection_boxes() self.hide_text_editor_and_dropdown() if event.keysym != "FocusOut": self.focus_set() return "break" def get_dropdown_height_anchor( self, c: int, text_editor_h: None | int = None, ) -> tuple[int, Literal["nw"]]: win_h = 5 datacn = self.MT.datacn(c) for i, v in enumerate(self.get_cell_kwargs(datacn, key="dropdown")["values"]): v_numlines = len(v.split("\n") if isinstance(v, str) else f"{v}".split("\n")) if v_numlines > 1: win_h += ( self.MT.header_first_ln_ins + (v_numlines * self.MT.header_xtra_lines_increment) + 5 ) # end of cell else: win_h += self.MT.min_header_height if i == 5: break if win_h > 500: win_h = 500 space_bot = self.MT.get_space_bot(0, text_editor_h) win_h2 = int(win_h) if win_h > space_bot: win_h = space_bot - 1 if win_h < self.MT.header_txt_height + 5: win_h = self.MT.header_txt_height + 5 elif win_h > win_h2: win_h = win_h2 return win_h, "nw" def dropdown_text_editor_modified( self, dd_window: object, event: dict, modified_func: Callable | None, ) -> None: if modified_func: modified_func(event) dd_window.search_and_see(event) def open_dropdown_window(self, c: int, event: object = None) -> None: self.hide_text_editor() kwargs = self.get_cell_kwargs(self.MT.datacn(c), key="dropdown") if kwargs["state"] == "normal": if not self.open_text_editor(event=event, c=c, dropdown=True): return win_h, anchor = self.get_dropdown_height_anchor(c) win_w = self.MT.col_positions[c + 1] - self.MT.col_positions[c] + 1 ypos = self.current_height - 1 reset_kwargs = { "r": 0, "c": c, "width": win_w, "height": win_h, "font": self.PAR.ops.header_font, "ops": self.PAR.ops, "outline_color": self.PAR.ops.header_selected_columns_bg, "align": self.get_cell_align(c), "values": kwargs["values"], } if self.dropdown.window: self.dropdown.window.reset(**reset_kwargs) self.itemconfig(self.dropdown.canvas_id, state="normal") self.coords(self.dropdown.canvas_id, self.MT.col_positions[c], ypos) self.dropdown.window.tkraise() else: self.dropdown.window = self.PAR.dropdown_class( self.winfo_toplevel(), **reset_kwargs, single_index="c", close_dropdown_window=self.close_dropdown_window, search_function=kwargs["search_function"], arrowkey_RIGHT=self.MT.arrowkey_RIGHT, arrowkey_LEFT=self.MT.arrowkey_LEFT, ) self.dropdown.canvas_id = self.create_window( (self.MT.col_positions[c], ypos), window=self.dropdown.window, anchor=anchor, ) if kwargs["state"] == "normal": self.text_editor.tktext.bind( "<>", lambda _x: self.dropdown_text_editor_modified( self.dropdown.window, event_dict( name="header_dropdown_modified", sheet=self.PAR.name, value=self.text_editor.get(), loc=c, column=c, boxes=self.MT.get_boxes(), selected=self.MT.selected, ), kwargs["modified_function"], ), ) self.update_idletasks() try: self.after(1, lambda: self.text_editor.tktext.focus()) self.after(2, self.text_editor.window.scroll_to_bottom()) except Exception: return redraw = False else: self.update_idletasks() self.dropdown.window.bind("", lambda _x: self.close_dropdown_window(c)) self.dropdown.window.bind("", self.close_dropdown_window) self.dropdown.window.focus_set() redraw = True self.dropdown.open = True if redraw: self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False, redraw_table=False) def close_dropdown_window( self, c: None | int = None, selection: object = None, redraw: bool = True, ) -> None: if c is not None and selection is not None: datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] kwargs = self.get_cell_kwargs(datacn, key="dropdown") pre_edit_value = self.get_cell_data(datacn) edited = False event_data = event_dict( name="end_edit_header", sheet=self.PAR.name, widget=self, cells_header={datacn: pre_edit_value}, key="??", value=selection, loc=c, column=c, boxes=self.MT.get_boxes(), selected=self.MT.selected, ) if kwargs["select_function"] is not None: kwargs["select_function"](event_data) if self.MT.edit_validation_func: selection = self.MT.edit_validation_func(event_data) if selection is not None: edited = self.set_cell_data_undo(c, datacn=datacn, value=selection, redraw=not redraw) else: edited = self.set_cell_data_undo(c, datacn=datacn, value=selection, redraw=not redraw) if edited: try_binding(self.extra_end_edit_cell_func, event_data) self.MT.recreate_all_selection_boxes() self.focus_set() self.hide_text_editor_and_dropdown(redraw=redraw) def hide_text_editor_and_dropdown(self, redraw: bool = True) -> None: self.hide_text_editor() self.hide_dropdown_window() if redraw: self.MT.refresh() def mouseclick_outside_editor_or_dropdown(self, inside: bool = False) -> int | None: closed_dd_coords = self.dropdown.get_coords() if self.text_editor.open: self.close_text_editor(new_tk_event("ButtonPress-1")) if closed_dd_coords is not None: self.hide_dropdown_window() if inside: self.MT.main_table_redraw_grid_and_text( redraw_header=True, redraw_row_index=False, redraw_table=False, ) return closed_dd_coords def mouseclick_outside_editor_or_dropdown_all_canvases(self, inside: bool = False) -> int | None: self.RI.mouseclick_outside_editor_or_dropdown() self.MT.mouseclick_outside_editor_or_dropdown() return self.mouseclick_outside_editor_or_dropdown(inside) def hide_dropdown_window(self) -> None: if self.dropdown.open: self.dropdown.window.unbind("") self.itemconfig(self.dropdown.canvas_id, state="hidden") self.dropdown.open = False # internal event use def set_cell_data_undo( self, c: int = 0, datacn: int | None = None, value: object = "", cell_resize: bool = True, undo: bool = True, redraw: bool = True, check_input_valid: bool = True, ) -> bool: if datacn is None: datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] event_data = event_dict( name="edit_header", sheet=self.PAR.name, widget=self, cells_header={datacn: self.get_cell_data(datacn)}, boxes=self.MT.get_boxes(), selected=self.MT.selected, ) edited = False if isinstance(self.MT._headers, int): disprn = self.MT.try_disprn(self.MT._headers) edited = self.MT.set_cell_data_undo( r=disprn if isinstance(disprn, int) else 0, c=c, datarn=self.MT._headers, datacn=datacn, value=value, undo=True, cell_resize=isinstance(disprn, int), ) else: self.fix_header(datacn) if not check_input_valid or self.input_valid_for_cell(datacn, value): if self.MT.undo_enabled and undo: self.MT.undo_stack.append(pickled_event_dict(event_data)) self.set_cell_data(datacn=datacn, value=value) edited = True if edited and cell_resize and self.PAR.ops.cell_auto_resize_enabled: if self.height_resizing_enabled: self.set_height_of_header_to_text(self.get_valid_cell_data_as_str(datacn, fix=False)) self.set_col_width_run_binding(c) if redraw: self.MT.refresh() if edited: self.MT.sheet_modified(event_data) return edited def set_cell_data(self, datacn: int | None = None, value: object = "") -> None: if isinstance(self.MT._headers, int): self.MT.set_cell_data(datarn=self.MT._headers, datacn=datacn, value=value) else: self.fix_header(datacn) if self.get_cell_kwargs(datacn, key="checkbox"): self.MT._headers[datacn] = try_to_bool(value) else: self.MT._headers[datacn] = value def input_valid_for_cell(self, datacn: int, value: object, check_readonly: bool = True) -> bool: if check_readonly and self.get_cell_kwargs(datacn, key="readonly"): return False if self.get_cell_kwargs(datacn, key="checkbox"): return is_bool_like(value) if self.cell_equal_to(datacn, value): return False kwargs = self.get_cell_kwargs(datacn, key="dropdown") if kwargs and kwargs["validate_input"] and value not in kwargs["values"]: return False return True def cell_equal_to(self, datacn: int, value: object) -> bool: self.fix_header(datacn) if isinstance(self.MT._headers, list): return self.MT._headers[datacn] == value elif isinstance(self.MT._headers, int): return self.MT.cell_equal_to(self.MT._headers, datacn, value) def get_cell_data( self, datacn: int, get_displayed: bool = False, none_to_empty_str: bool = False, redirect_int: bool = False, ) -> object: if get_displayed: return self.get_valid_cell_data_as_str(datacn, fix=False) if redirect_int and isinstance(self.MT._headers, int): # internal use return self.MT.get_cell_data(self.MT._headers, datacn, none_to_empty_str=True) if ( isinstance(self.MT._headers, int) or not self.MT._headers or datacn >= len(self.MT._headers) or (self.MT._headers[datacn] is None and none_to_empty_str) ): return "" return self.MT._headers[datacn] def get_valid_cell_data_as_str(self, datacn: int, fix: bool = True) -> str: kwargs = self.get_cell_kwargs(datacn, key="dropdown") if kwargs: if kwargs["text"] is not None: return f"{kwargs['text']}" else: kwargs = self.get_cell_kwargs(datacn, key="checkbox") if kwargs: return f"{kwargs['text']}" if isinstance(self.MT._headers, int): return self.MT.get_valid_cell_data_as_str(self.MT._headers, datacn, get_displayed=True) if fix: self.fix_header(datacn) try: value = "" if self.MT._headers[datacn] is None else f"{self.MT._headers[datacn]}" except Exception: value = "" if not value and self.PAR.ops.show_default_header_for_empty: value = get_n2a(datacn, self.default_header) return value def get_value_for_empty_cell(self, datacn: int, c_ops: bool = True) -> object: if self.get_cell_kwargs(datacn, key="checkbox", cell=c_ops): return False kwargs = self.get_cell_kwargs(datacn, key="dropdown", cell=c_ops) if kwargs and kwargs["validate_input"] and kwargs["values"]: return kwargs["values"][0] return "" def get_empty_header_seq(self, end: int, start: int = 0, c_ops: bool = True) -> list[object]: return [self.get_value_for_empty_cell(datacn, c_ops=c_ops) for datacn in range(start, end)] def fix_header(self, datacn: None | int = None) -> None: if isinstance(self.MT._headers, int): return if isinstance(self.MT._headers, float): self.MT._headers = int(self.MT._headers) return if not isinstance(self.MT._headers, list): try: self.MT._headers = list(self.MT._headers) except Exception: self.MT._headers = [] if isinstance(datacn, int) and datacn >= len(self.MT._headers): self.MT._headers.extend(self.get_empty_header_seq(end=datacn + 1, start=len(self.MT._headers))) # displayed indexes def set_col_width_run_binding(self, c: int, width: int | None = None, only_if_too_small: bool = True) -> None: old_width = self.MT.col_positions[c + 1] - self.MT.col_positions[c] new_width = self.set_col_width(c, width=width, only_if_too_small=only_if_too_small) if self.column_width_resize_func is not None and old_width != new_width: self.column_width_resize_func( event_dict( name="resize", sheet=self.PAR.name, resized_columns={c: {"old_size": old_width, "new_size": new_width}}, ) ) # internal event use def click_checkbox(self, c: int, datacn: int | None = None, undo: bool = True, redraw: bool = True) -> None: if datacn is None: datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] kwargs = self.get_cell_kwargs(datacn, key="checkbox") if kwargs["state"] == "normal": pre_edit_value = self.get_cell_data(datacn) if isinstance(self.MT._headers, list): value = not self.MT._headers[datacn] if isinstance(self.MT._headers[datacn], bool) else False elif isinstance(self.MT._headers, int): value = ( not self.MT.data[self.MT._headers][datacn] if isinstance(self.MT.data[self.MT._headers][datacn], bool) else False ) else: value = False self.set_cell_data_undo(c, datacn=datacn, value=value, cell_resize=False) event_data = event_dict( name="end_edit_header", sheet=self.PAR.name, widget=self, cells_header={datacn: pre_edit_value}, key="??", value=value, loc=c, column=c, boxes=self.MT.get_boxes(), selected=self.MT.selected, ) if kwargs["check_function"] is not None: kwargs["check_function"](event_data) try_binding(self.extra_end_edit_cell_func, event_data) if redraw: self.MT.refresh() def get_cell_kwargs(self, datacn: int, key: Hashable = "dropdown", cell: bool = True) -> dict: if cell and datacn in self.cell_options and key in self.cell_options[datacn]: return self.cell_options[datacn][key] return {} tksheet-7.2.12/tksheet/formatters.py000066400000000000000000000227551463432073300175010ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable from .vars import falsy, nonelike, truthy def is_none_like(o: object): if (isinstance(o, str) and o.lower().replace(" ", "") in nonelike) or o in nonelike: return True return False def to_int(o: object, **kwargs): if isinstance(o, int): return o return int(float(o)) def to_float(o: object, **kwargs): if isinstance(o, float): return o if isinstance(o, str) and o.endswith("%"): return float(o.replace("%", "")) / 100 return float(o) def to_bool(val: object, **kwargs): if isinstance(val, bool): return val if isinstance(val, str): v = val.lower() else: v = val if "truthy" in kwargs: _truthy = kwargs["truthy"] else: _truthy = truthy if "falsy" in kwargs: _falsy = kwargs["falsy"] else: _falsy = falsy if v in _truthy: return True elif v in _falsy: return False raise ValueError(f'Cannot map "{val}" to bool.') def try_to_bool(o: object, **kwargs): try: return to_bool(o) except Exception: return o def is_bool_like(o: object, **kwargs): try: to_bool(o) return True except Exception: return False def to_str(o: object, **kwargs: dict) -> str: return f"{o}" def float_to_str(v: int | float, **kwargs: dict) -> str: if isinstance(v, float): if v.is_integer(): return f"{int(v)}" if "decimals" in kwargs and isinstance(kwargs["decimals"], int): if kwargs["decimals"]: return f"{round(v, kwargs['decimals'])}" return f"{int(round(v, kwargs['decimals']))}" return f"{v}" def percentage_to_str(v: int | float, **kwargs: dict) -> str: if isinstance(v, (int, float)): x = v * 100 if isinstance(x, float): if x.is_integer(): return f"{int(x)}%" if "decimals" in kwargs and isinstance(kwargs["decimals"], int): if kwargs["decimals"]: return f"{round(x, kwargs['decimals'])}%" return f"{int(round(x, kwargs['decimals']))}%" return f"{x}%" def bool_to_str(v: object, **kwargs: dict) -> str: return f"{v}" def int_formatter( datatypes: tuple[object] | object = int, format_function: Callable = to_int, to_str_function: Callable = to_str, **kwargs, ) -> dict: return formatter( datatypes=datatypes, format_function=format_function, to_str_function=to_str_function, **kwargs, ) def float_formatter( datatypes: tuple[object] | object = float, format_function: Callable = to_float, to_str_function: Callable = float_to_str, decimals: int = 2, **kwargs, ) -> dict: return formatter( datatypes=datatypes, format_function=format_function, to_str_function=to_str_function, decimals=decimals, **kwargs, ) def percentage_formatter( datatypes: tuple[object] | object = float, format_function: Callable = to_float, to_str_function: Callable = percentage_to_str, decimals: int = 2, **kwargs, ) -> dict: return formatter( datatypes=datatypes, format_function=format_function, to_str_function=to_str_function, decimals=decimals, **kwargs, ) def bool_formatter( datatypes: tuple[object] | object = bool, format_function: Callable = to_bool, to_str_function: Callable = bool_to_str, invalid_value: object = "NA", truthy_values: set[object] = truthy, falsy_values: set[object] = falsy, **kwargs, ) -> dict: return formatter( datatypes=datatypes, format_function=format_function, to_str_function=to_str_function, invalid_value=invalid_value, truthy_values=truthy_values, falsy_values=falsy_values, **kwargs, ) def formatter( datatypes: tuple[object] | object, format_function: Callable, to_str_function: Callable = to_str, invalid_value: object = "NaN", nullable: bool = True, pre_format_function: Callable | None = None, post_format_function: Callable | None = None, clipboard_function: Callable | None = None, **kwargs, ) -> dict: return { **dict( datatypes=datatypes, format_function=format_function, to_str_function=to_str_function, invalid_value=invalid_value, nullable=nullable, pre_format_function=pre_format_function, post_format_function=post_format_function, clipboard_function=clipboard_function, ), **kwargs, } def format_data( value: object = "", datatypes: tuple[object] | object = int, nullable: bool = True, pre_format_function: Callable | None = None, format_function: Callable | None = to_int, post_format_function: Callable | None = None, **kwargs, ) -> object: if pre_format_function: value = pre_format_function(value) if nullable and is_none_like(value): value = None else: try: value = format_function(value, **kwargs) except Exception: pass if post_format_function and isinstance(value, datatypes): value = post_format_function(value) return value def data_to_str( value: object = "", datatypes: tuple[object] | object = int, nullable: bool = True, invalid_value: object = "NaN", to_str_function: Callable | None = None, **kwargs, ) -> str: if not isinstance(value, datatypes): return invalid_value if value is None and nullable: return "" return to_str_function(value, **kwargs) def get_data_with_valid_check(value="", datatypes: tuple[()] | tuple[object] | object = tuple(), invalid_value="NA"): if isinstance(value, datatypes): return value return invalid_value def get_clipboard_data(value: object = "", clipboard_function: Callable | None = None, **kwargs: dict) -> object: if clipboard_function is not None: return clipboard_function(value, **kwargs) if isinstance(value, (str, int, float, bool)): return value return data_to_str(value, **kwargs) class Formatter: def __init__( self, value: object, datatypes: tuple[object], object=int, format_function: Callable = to_int, to_str_function: Callable = to_str, nullable: bool = True, invalid_value: str = "NaN", pre_format_function: Callable | None = None, post_format_function: Callable | None = None, clipboard_function: Callable | None = None, **kwargs, ) -> None: if nullable: if isinstance(datatypes, (list, tuple)): datatypes = tuple({type_ for type_ in datatypes} | {type(None)}) else: datatypes = (datatypes, type(None)) elif isinstance(datatypes, (list, tuple)) and type(None) in datatypes: raise TypeError("Non-nullable cells cannot have NoneType as a datatype.") elif datatypes is type(None): raise TypeError("Non-nullable cells cannot have NoneType as a datatype.") self.kwargs = kwargs self.valid_datatypes = datatypes self.format_function = format_function self.to_str_function = to_str_function self.nullable = nullable self.invalid_value = invalid_value self.pre_format_function = pre_format_function self.post_format_function = post_format_function self.clipboard_function = clipboard_function try: self.value = self.format_data(value) except Exception: self.value = f"{value}" def __str__(self) -> object: if not self.valid(): return self.invalid_value if self.value is None and self.nullable: return "" return self.to_str_function(self.value, **self.kwargs) def valid(self, value: object = None) -> bool: if value is None: value = self.value if isinstance(value, self.valid_datatypes): return True return False def format_data(self, value: object) -> object: if self.pre_format_function: value = self.pre_format_function(value) value = None if (self.nullable and is_none_like(value)) else self.format_function(value, **self.kwargs) if self.post_format_function and self.valid(value): value = self.post_format_function(value) return value def get_data_with_valid_check(self) -> object: if self.valid(): return self.value return self.invalid_value def get_clipboard_data(self) -> object: if self.clipboard_function is not None: return self.clipboard_function(self.value, **self.kwargs) if isinstance(self.value, (int, float, bool)): return self.value return self.__str__() def __eq__(self, __value: object) -> bool: # in case of custom formatter class # compare the values try: if hasattr(__value, "value"): return self.value == __value.value except Exception: pass # if comparing to a string, format the string and compare if isinstance(__value, str): try: return self.value == self.format_data(__value) except Exception: pass # if comparing to anything else, compare the values return self.value == __value tksheet-7.2.12/tksheet/functions.py000066400000000000000000001155151463432073300173200ustar00rootroot00000000000000from __future__ import annotations import csv import io import pickle import re import tkinter as tk import zlib from bisect import ( bisect_left, ) from collections import deque from collections.abc import ( Callable, Generator, Iterator, Sequence, ) from functools import partial from itertools import islice, repeat from .formatters import ( to_bool, ) from .other_classes import ( Box_nt, DotDict, EventDataDict, Highlight, Loc, Span, ) compress = partial(zlib.compress, level=1) pickle_obj = partial(pickle.dumps, protocol=pickle.HIGHEST_PROTOCOL) unpickle_obj = pickle.loads def get_csv_str_dialect(s: str, delimiters: str) -> csv.Dialect: try: return csv.Sniffer().sniff(s[:5000] if len(s) > 5000 else s, delimiters=delimiters) except Exception: return csv.excel_tab def get_data_from_clipboard( widget: tk.Misc, delimiters: str, lineterminator: str = "\n", ) -> list[list[str]]: data = widget.clipboard_get() dialect = get_csv_str_dialect(data, delimiters=delimiters) if dialect.delimiter in data or lineterminator in data: return list(csv.reader(io.StringIO(data), dialect=dialect, skipinitialspace=True)) return [[data]] def pickle_compress(obj: object) -> bytes: return compress(pickle_obj(obj)) def decompress_load(b: bytes) -> object: return pickle.loads(zlib.decompress(b)) def tksheet_type_error(kwarg: str, valid_types: list[str], not_type: object) -> str: valid_types = ", ".join(f"{type_}" for type_ in valid_types) return f"Argument '{kwarg}' must be one of the following types: {valid_types}, " f"not {type(not_type)}." def new_tk_event(keysym: str) -> tk.Event: event = tk.Event() event.keysym = keysym return event def dropdown_search_function( search_for: object, data: Sequence[object], ) -> None | int: search_len = len(search_for) # search_for in data match_rn = float("inf") match_st = float("inf") match_len_diff = float("inf") # data in search_for in case no match match_data_rn = float("inf") match_data_st = float("inf") match_data_numchars = 0 for rn, row in enumerate(data): dd_val = rf"{row[0]}".lower() # checking if search text is in dropdown row st = dd_val.find(search_for) if st > -1: # priority is start index # if there's already a matching start # then compare the len difference len_diff = len(dd_val) - search_len if st < match_st or (st == match_st and len_diff < match_len_diff): match_rn = rn match_st = st match_len_diff = len_diff # fall back in case of no existing match elif match_rn == float("inf"): for numchars in range(2, search_len - 1): for from_idx in range(search_len - 1): if from_idx + numchars > search_len: break st = dd_val.find(search_for[from_idx : from_idx + numchars]) if st > -1 and ( numchars > match_data_numchars or (numchars == match_data_numchars and st < match_data_st) ): match_data_rn = rn match_data_st = st match_data_numchars = numchars if match_rn != float("inf"): return match_rn elif match_data_rn != float("inf"): return match_data_rn return None def selection_box_tup_to_dict(box: tuple) -> dict: return {Box_nt(*box[:-1]): box[-1]} def event_dict( name: str = None, sheet: object = None, widget: tk.Canvas | None = None, boxes: None | dict | tuple = None, cells_table: None | dict = None, cells_header: None | dict = None, cells_index: None | dict = None, selected: None | tuple = None, data: object = None, key: None | str = None, value: object = None, loc: None | int | tuple[int] = None, row: None | int = None, column: None | int = None, resized_rows: None | dict = None, resized_columns: None | dict = None, # resized_index: None, dict] = None, # resized_header: None, dict] = None, being_selected: None | tuple = None, named_spans: None | dict = None, **kwargs, ) -> EventDataDict: return EventDataDict( eventname="" if name is None else name, sheetname="!sheet" if sheet is None else sheet, cells=DotDict( table=DotDict() if cells_table is None else cells_table, header=DotDict() if cells_header is None else cells_header, index=DotDict() if cells_index is None else cells_index, ), moved=DotDict( rows=DotDict(), columns=DotDict(), ), added=DotDict( rows=DotDict(), columns=DotDict(), ), deleted=DotDict( rows=DotDict(), columns=DotDict(), header=DotDict(), index=DotDict(), column_widths=DotDict(), row_heights=DotDict(), displayed_rows=None, displayed_columns=None, ), named_spans=DotDict() if named_spans is None else named_spans, options=DotDict(), selection_boxes=( {} if boxes is None else selection_box_tup_to_dict(boxes) if isinstance(boxes, tuple) else boxes ), selected=tuple() if selected is None else selected, being_selected=tuple() if being_selected is None else being_selected, data=[] if data is None else data, key="" if key is None else key, value=None if value is None else value, loc=tuple() if loc is None else loc, row=row, column=column, resized=DotDict( rows=DotDict() if resized_rows is None else resized_rows, columns=DotDict() if resized_columns is None else resized_columns, # "header": DotDict() if resized_header is None else resized_header, # "index": DotDict() if resized_index is None else resized_index, ), widget=widget, ) def change_eventname(event_dict: EventDataDict, newname: str) -> EventDataDict: return EventDataDict({**event_dict, **{"eventname": newname}}) def pickled_event_dict(d: DotDict) -> DotDict: return DotDict(name=d["eventname"], data=pickle_compress(DotDict({k: v for k, v in d.items() if k != "widget"}))) def len_to_idx(n: int) -> int: if n < 1: return 0 return n - 1 def b_index(sorted_seq: Sequence[int], num_to_index: int) -> int: """ Designed to be a faster way of finding the index of an int in a sorted list of ints than list.index() """ if (idx := bisect_left(sorted_seq, num_to_index)) == len(sorted_seq) or sorted_seq[idx] != num_to_index: raise ValueError(f"{num_to_index} is not in Sequence") return idx def get_dropdown_kwargs( values: list = [], set_value: object = None, state: str = "normal", redraw: bool = True, selection_function: Callable | None = None, modified_function: Callable | None = None, search_function: Callable = dropdown_search_function, validate_input: bool = True, text: None | str = None, ) -> dict: return { "values": values, "set_value": set_value, "state": state, "redraw": redraw, "selection_function": selection_function, "modified_function": modified_function, "search_function": search_function, "validate_input": validate_input, "text": text, } def get_dropdown_dict(**kwargs) -> dict: return { "values": kwargs["values"], "select_function": kwargs["selection_function"], "modified_function": kwargs["modified_function"], "search_function": kwargs["search_function"], "validate_input": kwargs["validate_input"], "text": kwargs["text"], "state": kwargs["state"], } def get_checkbox_kwargs( checked: bool = False, state: str = "normal", redraw: bool = True, check_function: Callable | None = None, text: str = "", ) -> dict: return { "checked": checked, "state": state, "redraw": redraw, "check_function": check_function, "text": text, } def get_checkbox_dict(**kwargs) -> dict: return { "check_function": kwargs["check_function"], "state": kwargs["state"], "text": kwargs["text"], } def is_iterable(o: object) -> bool: if isinstance(o, str): return False try: iter(o) return True except Exception: return False def int_x_iter(i: Iterator[int] | int) -> Iterator[int]: if isinstance(i, int): return (i,) return i def unpack(t: tuple[object] | tuple[Iterator[object]]) -> tuple[object]: if not len(t): return t if is_iterable(t[0]) and len(t) == 1: return t[0] return t def is_type_int(o: object) -> bool: return isinstance(o, int) and not isinstance(o, bool) def force_bool(o: object) -> bool: try: return to_bool(o) except Exception: return False def str_to_coords(s: str) -> None | tuple[int]: s = s.split(":") def alpha2idx(a: str) -> int | None: try: a = a.upper() n = 0 orda = ord("A") for c in a: n = n * 26 + ord(c) - orda + 1 return n - 1 except Exception: return None def alpha2num(a: str) -> int | None: n = alpha2idx(a) return n if n is None else n + 1 def num2alpha(n: int) -> str | None: try: s = "" n += 1 while n > 0: n, r = divmod(n - 1, 26) s = chr(65 + r) + s return s except Exception: return None def idx_param_to_int(idx: str | int | None) -> int | None: if idx is None or isinstance(idx, int): return idx return alpha2idx(idx) def get_n2a(n: int = 0, _type: str = "numbers") -> str: if _type == "letters": return num2alpha(n) elif _type == "numbers": return f"{n + 1}" return f"{num2alpha(n)} {n + 1}" def get_index_of_gap_in_sorted_integer_seq_forward( seq: list[int], start: int = 0, ) -> int | None: prevn = seq[start] for idx, n in enumerate(islice(seq, start + 1, None), start + 1): if n != prevn + 1: return idx prevn = n return None def get_index_of_gap_in_sorted_integer_seq_reverse( seq: list[int], start: int = 0, ) -> int | None: prevn = seq[start] for idx, n in zip(range(start, -1, -1), reversed(seq[:start])): if n != prevn - 1: return idx prevn = n return None def get_seq_without_gaps_at_index( seq: list[int], position: int, get_st_end: bool = False, ) -> tuple[int, int] | list[int]: start_idx = bisect_left(seq, position) forward_gap = get_index_of_gap_in_sorted_integer_seq_forward(seq, start_idx) reverse_gap = get_index_of_gap_in_sorted_integer_seq_reverse(seq, start_idx) if forward_gap is not None: seq = seq[:forward_gap] if reverse_gap is not None: seq = seq[reverse_gap:] if get_st_end: return seq[0], seq[-1] return seq def consecutive_chunks(seq: list[int]) -> Generator[list[int]]: start = 0 for index, value in enumerate(seq, 1): try: if seq[index] > value + 1: yield seq[start : (start := index)] except Exception: yield seq[start : len(seq)] def consecutive_ranges(seq: Sequence[int]) -> Generator[tuple[int, int]]: start = 0 for index, value in enumerate(seq, 1): try: if seq[index] > value + 1: yield seq[start], seq[index - 1] + 1 start = index except Exception: yield seq[start], seq[-1] + 1 def is_contiguous(iterable: Iterator[int]) -> bool: itr = iter(iterable) prev = next(itr) return all(i == (prev := prev + 1) for i in itr) def get_last( it: Iterator, ) -> object: if hasattr(it, "__reversed__"): try: return next(reversed(it)) except Exception: return None else: try: return deque(it, maxlen=1)[0] except Exception: return None def index_exists(seq: Sequence[object], index: int) -> bool: try: seq[index] return True except Exception: return False def move_elements_by_mapping( seq: list[object], new_idxs: dict[int, int], old_idxs: dict[int, int] | None = None, ) -> list[object]: # move elements of a list around, displacing # other elements based on mapping # of {old index: new index, ...} if old_idxs is None: old_idxs = dict(zip(new_idxs.values(), new_idxs)) # create dummy list res = [0] * len(seq) # create generator of values yet to be put into res remaining = (e for i, e in enumerate(seq) if i not in new_idxs) # goes over res twice: # once to put elements being moved in new spots # then to fill remaining spots with remaining elements # fill new indexes in res if len(new_idxs) > int(len(seq) / 2) - 1: # if moving a lot of items better to do comprehension return [ next(remaining) if i_ not in old_idxs else e_ for i_, e_ in enumerate(seq[old_idxs[i]] if i in old_idxs else e for i, e in enumerate(res)) ] else: # if just moving a few items assignments are fine for old, new in new_idxs.items(): res[new] = seq[old] # fill remaining indexes return [next(remaining) if i not in old_idxs else e for i, e in enumerate(res)] def move_elements_to( seq: list[object], move_to: int, to_move: list[int], ) -> list[object]: return move_elements_by_mapping( seq, *get_new_indexes( move_to, to_move, get_inverse=True, ), ) def get_new_indexes( move_to: int, to_move: list[int], get_inverse: bool = False, ) -> tuple[dict]: """ returns {old idx: new idx, ...} """ offset = sum(1 for i in to_move if i < move_to) new_idxs = range(move_to - offset, move_to - offset + len(to_move)) new_idxs = {old: new for old, new in zip(to_move, new_idxs)} if get_inverse: return new_idxs, dict(zip(new_idxs.values(), new_idxs)) return new_idxs def insert_items(seq: list | tuple, to_insert: dict, seq_len_func: Callable | None = None) -> list: # inserts many items into a list using a dict of reverse sorted order of # {index: value, index: value, ...} res = [] extended = 0 for i, (idx, v) in enumerate(reversed(to_insert.items())): # need to extend seq if it's not long enough if seq_len_func and idx - i > len(seq): seq_len_func(idx - i - 1) res.extend(seq[extended : idx - i]) extended += idx - i - extended res.append(v) res.extend(seq[extended:]) seq = res return seq def data_to_displayed_idxs( to_convert: list[int], displayed: list[int], ) -> list[int]: data_indexes = set(to_convert) return [i for i, e in enumerate(displayed) if e in data_indexes] def displayed_to_data_idxs( to_convert: list[int], displayed: list[int], ) -> list[int]: return [displayed[e] for e in to_convert] def rounded_box_coords( x1: float, y1: float, x2: float, y2: float, radius: int = 5, ) -> tuple[float]: return ( x1 + radius, y1, x1 + radius, y1, x2 - radius, y1, x2 - radius, y1, x2, y1, x2, y1 + radius, x2, y1 + radius, x2, y2 - radius, x2, y2 - radius, x2, y2, x2 - radius, y2, x2 - radius, y2, x1 + radius, y2, x1 + radius, y2, x1, y2, x1, y2 - radius, x1, y2 - radius, x1, y1 + radius, x1, y1 + radius, x1, y1, ) def diff_list(seq: list[float]) -> list[int]: return [ int(b - a) for a, b in zip( seq, islice(seq, 1, None), ) ] def diff_gen(seq: list[float]) -> Generator[int]: return ( int(b - a) for a, b in zip( seq, islice(seq, 1, None), ) ) def zip_fill_2nd_value(x: Iterator, o: object) -> Generator[object, object]: return zip(x, repeat(o)) def str_to_int(s: str) -> int | None: if s.startswith(("-", "+")): if s[1:].isdigit(): return int(s) return None elif not s.startswith(("-", "+")): if s.isdigit(): return int(s) return None def gen_formatted( options: dict, formatter: object = None, ) -> Generator[tuple[int, int]] | Generator[int]: if formatter is None: return (k for k, dct in options.items() if "format" in dct) return (k for k, dct in options.items() if "format" in dct and dct["format"]["formatter"] == formatter) def options_with_key( options: dict, key: str, ) -> Generator[tuple[int, int]] | Generator[int]: return (k for k, dct in options.items() if key in dct) def try_binding( binding: None | Callable, event: dict, new_name: None | str = None, ) -> bool: if binding: try: if new_name is None: binding(event) else: binding(change_eventname(event, new_name)) except Exception: return False return True def span_dict( from_r: int | None = None, from_c: int | None = None, upto_r: int | None = None, upto_c: int | None = None, type_: str | None = None, name: str | None = None, kwargs: dict | None = None, table: bool = True, header: bool = False, index: bool = False, tdisp: bool = False, idisp: bool = True, hdisp: bool = True, transposed: bool = False, ndim: int | None = None, convert: Callable | None = None, undo: bool = False, emit_event: bool = False, widget: object = None, ) -> Span: d: Span = Span( from_r=from_r, from_c=from_c, upto_r=upto_r, upto_c=upto_c, type_="" if type_ is None else type_, name="" if name is None else name, kwargs={} if kwargs is None else kwargs, table=table, index=index, header=header, tdisp=tdisp, idisp=idisp, hdisp=hdisp, transposed=transposed, ndim=ndim, convert=convert, undo=undo, emit_event=emit_event, widget=widget, ) return d def coords_to_span( widget: object, from_r: int | None = None, from_c: int | None = None, upto_r: int | None = None, upto_c: int | None = None, ) -> Span: if not isinstance(from_r, int) and from_r is not None: from_r = None if not isinstance(from_c, int) and from_c is not None: from_c = None if not isinstance(upto_r, int) and upto_r is not None: upto_r = None if not isinstance(upto_c, int) and upto_c is not None: upto_c = None if from_r is None and from_c is None: from_r = 0 return span_dict( from_r=from_r, from_c=from_c, upto_r=upto_r, upto_c=upto_c, widget=widget, ) def key_to_span( key: ( str | int | slice | Sequence[int | None, int | None] | Sequence[int | None, int | None, int | None, int | None] | Sequence[Sequence[int | None, int | None], Sequence[int | None, int | None]] ), spans: dict[str, Span], widget: object = None, ) -> Span: if isinstance(key, Span): return key elif key is None: key = (None, None, None, None) elif not isinstance(key, (str, int, slice, list, tuple)): return f"Key type must be either str, int, list, tuple or slice, not '{type(key)}'." try: if isinstance(key, (list, tuple)): if isinstance(key[0], int) or key[0] is None: if len(key) == 2: """ (int | None, int | None) - (0, 0) - row 0, column 0 - the first cell (0, None) - row 0, all columns (None, 0) - column 0, all rows """ return span_dict( from_r=key[0] if isinstance(key[0], int) else 0, from_c=key[1] if isinstance(key[1], int) else 0, upto_r=(key[0] + 1) if isinstance(key[0], int) else None, upto_c=(key[1] + 1) if isinstance(key[1], int) else None, widget=widget, ) elif len(key) == 4: """ (int | None, int | None, int | None, int | None) - (from row, from column, up to row, up to column) """ return coords_to_span( widget=widget, from_r=key[0], from_c=key[1], upto_r=key[2], upto_c=key[3], ) # return span_dict( # from_r=key[0] if isinstance(key[0], int) else 0, # from_c=key[1] if isinstance(key[1], int) else 0, # upto_r=key[2] if isinstance(key[2], int) else None, # upto_c=key[3] if isinstance(key[3], int) else None, # widget=widget, # ) elif isinstance(key[0], (list, tuple)): """ ((int | None, int | None), (int | None, int | None)) First Sequence is start row and column Second Sequence is up to but not including row and column """ return coords_to_span( widget=widget, from_r=key[0][0], from_c=key[0][1], upto_r=key[1][0], upto_c=key[1][1], ) # return span_dict( # from_r=key[0][0] if isinstance(key[0][0], int) else 0, # from_c=key[0][1] if isinstance(key[0][1], int) else 0, # upto_r=key[1][0], # upto_c=key[1][1], # widget=widget, # ) elif isinstance(key, int): """ [int] - Whole row at that index """ return span_dict( from_r=key, from_c=None, upto_r=key + 1, upto_c=None, widget=widget, ) elif isinstance(key, slice): """ [slice] """ """ [:] - All rows """ if key.start is None and key.stop is None: """ [:] """ return span_dict( from_r=0, from_c=None, upto_r=None, upto_c=None, widget=widget, ) """ [1:3] - Rows 1, 2 [:2] - Rows up to but not including 2 [2:] - Rows starting from and including 2 """ start = 0 if key.start is None else key.start return span_dict( from_r=start, from_c=None, upto_r=key.stop, upto_c=None, widget=widget, ) elif isinstance(key, str): if not key: key = ":" if key.startswith("<") and key.endswith(">"): if (key := key[1:-1]) in spans: """ [""] - Surrounded by "<" ">" cells from a named range """ return spans[key] return f"'{key}' not in named spans." key = key.upper() if key.isdigit(): """ ["1"] - Row 0 """ return span_dict( from_r=int(key) - 1, from_c=None, upto_r=int(key), upto_c=None, widget=widget, ) if key.isalpha(): """ ["A"] - Column 0 """ return span_dict( from_r=None, from_c=alpha2idx(key), upto_r=None, upto_c=alpha2idx(key) + 1, widget=widget, ) splitk = key.split(":") if len(splitk) > 2: return f"'{key}' could not be converted to span." if len(splitk) == 1 and not splitk[0].isdigit() and not splitk[0].isalpha() and not splitk[0][0].isdigit(): """ ["A1"] - Cell (0, 0) """ keys_digits = re.search(r"\d", splitk[0]) if keys_digits: digits_start = keys_digits.start() if not digits_start: return f"'{key}' could not be converted to span." if digits_start: key_row = splitk[0][digits_start:] key_column = splitk[0][:digits_start] return span_dict( from_r=int(key_row) - 1, from_c=alpha2idx(key_column), upto_r=int(key_row), upto_c=alpha2idx(key_column) + 1, widget=widget, ) if not splitk[0] and not splitk[1]: """ [":"] - All rows """ return span_dict( from_r=0, from_c=None, upto_r=None, upto_c=None, widget=widget, ) if splitk[0].isdigit() and not splitk[1]: """ ["2:"] - Rows starting from and including 1 """ return span_dict( from_r=int(splitk[0]) - 1, from_c=None, upto_r=None, upto_c=None, widget=widget, ) if splitk[1].isdigit() and not splitk[0]: """ [":2"] - Rows up to and including 1 """ return span_dict( from_r=0, from_c=None, upto_r=int(splitk[1]), upto_c=None, widget=widget, ) if splitk[0].isdigit() and splitk[1].isdigit(): """ ["1:2"] - Rows 0, 1 """ return span_dict( from_r=int(splitk[0]) - 1, from_c=None, upto_r=int(splitk[1]), upto_c=None, widget=widget, ) if splitk[0].isalpha() and not splitk[1]: """ ["B:"] - Columns starting from and including 2 """ return span_dict( from_r=None, from_c=alpha2idx(splitk[0]), upto_r=None, upto_c=None, widget=widget, ) if splitk[1].isalpha() and not splitk[0]: """ [":B"] - Columns up to and including 2 """ return span_dict( from_r=None, from_c=0, upto_r=None, upto_c=alpha2idx(splitk[1]) + 1, widget=widget, ) if splitk[0].isalpha() and splitk[1].isalpha(): """ ["A:B"] - Columns 0, 1 """ return span_dict( from_r=None, from_c=alpha2idx(splitk[0]), upto_r=None, upto_c=alpha2idx(splitk[1]) + 1, widget=widget, ) m1 = re.search(r"\d", splitk[0]) m2 = re.search(r"\d", splitk[1]) m1start = m1.start() if m1 else None m2start = m2.start() if m2 else None if m1start and m2start: """ ["A1:B1"] - Cells (0, 0), (0, 1) """ c1 = splitk[0][:m1start] r1 = splitk[0][m1start:] c2 = splitk[1][:m2start] r2 = splitk[1][m2start:] return span_dict( from_r=int(r1) - 1, from_c=alpha2idx(c1), upto_r=int(r2), upto_c=alpha2idx(c2) + 1, widget=widget, ) if not splitk[0] and m2start: """ [":B1"] - Cells (0, 0), (0, 1) """ c2 = splitk[1][:m2start] r2 = splitk[1][m2start:] return span_dict( from_r=0, from_c=0, upto_r=int(r2), upto_c=alpha2idx(c2) + 1, widget=widget, ) if not splitk[1] and m1start: """ ["A1:"] - Cells starting from and including (0, 0) """ c1 = splitk[0][:m1start] r1 = splitk[0][m1start:] return span_dict( from_r=int(r1) - 1, from_c=alpha2idx(c1), upto_r=None, upto_c=None, widget=widget, ) if m1start and splitk[1].isalpha(): """ ["A1:B"] - All the cells starting from (0, 0) expanding out to include column 1 but not including cells beyond column 1 and expanding down to include all rows A B C D 1 x x 2 x x 3 x x 4 x x ... """ c1 = splitk[0][:m1start] r1 = splitk[0][m1start:] return span_dict( from_r=int(r1) - 1, from_c=alpha2idx(c1), upto_r=None, upto_c=alpha2idx(splitk[1]) + 1, widget=widget, ) if m1start and splitk[1].isdigit(): """ ["A1:2"] - All the cells starting from (0, 0) expanding down to include row 1 but not including cells beyond row 1 and expanding out to include all columns A B C D 1 x x x x 2 x x x x 3 4 ... """ c1 = splitk[0][:m1start] r1 = splitk[0][m1start:] return span_dict( from_r=int(r1) - 1, from_c=alpha2idx(c1), upto_r=int(splitk[1]), upto_c=None, widget=widget, ) except ValueError as error: return f"Error, '{key}' could not be converted to span: {error}" else: return f"'{key}' could not be converted to span." def span_is_cell(span: Span) -> bool: return ( isinstance(span["from_r"], int) and isinstance(span["from_c"], int) and isinstance(span["upto_r"], int) and isinstance(span["upto_c"], int) and span["upto_r"] - span["from_r"] == 1 and span["upto_c"] - span["from_c"] == 1 ) def span_to_cell(span: Span) -> tuple[int, int]: # assumed that span arg has been tested by 'span_is_cell()' return (span["from_r"], span["from_c"]) def span_ranges( span: Span, totalrows: int | Callable, totalcols: int | Callable, ) -> tuple[Generator[int], Generator[int]]: rng_from_r = 0 if span.from_r is None else span.from_r rng_from_c = 0 if span.from_c is None else span.from_c if span.upto_r is None: rng_upto_r = totalrows() if isinstance(totalrows, Callable) else totalrows else: rng_upto_r = span.upto_r if span.upto_c is None: rng_upto_c = totalcols() if isinstance(totalcols, Callable) else totalcols else: rng_upto_c = span.upto_c return range(rng_from_r, rng_upto_r), range(rng_from_c, rng_upto_c) def span_froms( span: Span, ) -> tuple[int, int]: from_r = 0 if span.from_r is None else span.from_r from_c = 0 if span.from_c is None else span.from_c return from_r, from_c def del_named_span_options(options: dict, itr: Iterator, type_: str) -> None: for k in itr: if k in options and type_ in options[k]: del options[k][type_] def del_named_span_options_nested(options: dict, itr1: Iterator, itr2: Iterator, type_: str) -> None: for k1 in itr1: for k2 in itr2: k = (k1, k2) if k in options and type_ in options[k]: del options[k][type_] def add_highlight( options: dict, key: int | tuple[int, int], bg: bool | None | str = False, fg: bool | None | str = False, end: bool | None = None, overwrite: bool = True, ) -> dict: if key not in options: options[key] = {} if overwrite or "highlight" not in options[key]: options[key]["highlight"] = Highlight( bg=None if bg is False else bg, fg=None if fg is False else fg, end=False if end is None else end, ) else: options[key]["highlight"] = Highlight( bg=options[key]["highlight"].bg if bg is False else bg, fg=options[key]["highlight"].fg if fg is False else fg, end=options[key]["highlight"].end if end is None else end, ) return options def set_readonly( options: dict, key: int | tuple[int, int], readonly: bool = True, ) -> dict: if readonly: if key not in options: options[key] = {} options[key]["readonly"] = True else: if key in options and "readonly" in options[key]: del options[key]["readonly"] return options def convert_align(align: str | None) -> str | None: if isinstance(align, str): a = align.lower() if a == "global": return None elif a in ("c", "center", "centre"): return "center" elif a in ("w", "west", "left"): return "w" elif a in ("e", "east", "right"): return "e" elif align is None: return None raise ValueError("Align must be one of the following values: c, center, w, west, left, e, east, right") def set_align( options: dict, key: int | tuple[int, int], align: str | None = None, ) -> dict: if align: if key not in options: options[key] = {} options[key]["align"] = align else: if key in options and "align" in options[key]: del options[key]["align"] def del_from_options( options: dict, key: str, coords: int | Iterator | None = None, ) -> dict: if isinstance(coords, int): if coords in options and key in options[coords]: del options[coords] elif is_iterable(coords): for coord in coords: if coord in options and key in options[coord]: del options[coord] else: for d in options.values(): if key in d: del d[key] def add_to_options( options: dict, coords: int | tuple[int, int], key: str, value: object, ) -> dict: if coords not in options: options[coords] = {} options[coords][key] = value def fix_format_kwargs(kwargs: dict) -> dict: if kwargs["formatter"] is None: if kwargs["nullable"]: if isinstance(kwargs["datatypes"], (list, tuple)): kwargs["datatypes"] = tuple(kwargs["datatypes"]) + (type(None),) else: kwargs["datatypes"] = (kwargs["datatypes"], type(None)) elif (isinstance(kwargs["datatypes"], (list, tuple)) and type(None) in kwargs["datatypes"]) or kwargs[ "datatypes" ] is type(None): raise TypeError("Non-nullable cells cannot have NoneType as a datatype.") if not isinstance(kwargs["invalid_value"], str): kwargs["invalid_value"] = f"{kwargs['invalid_value']}" return kwargs def span_idxs_post_move( new_idxs: dict[int, int], full_new_idxs: dict[int, int], total: int, span: Span, axis: str, # 'r' or 'c' ) -> tuple[int | None]: """ Calculates the position of a span after moving rows/columns """ if isinstance(span[f"upto_{axis}"], int): oldfrom, oldupto = int(span[f"from_{axis}"]), int(span[f"upto_{axis}"]) - 1 newfrom = full_new_idxs[oldfrom] newupto = full_new_idxs[oldupto] if newfrom > newupto: newfrom, newupto = newupto, newfrom newupto += 1 oldupto_colrange = int(span[f"upto_{axis}"]) newupto_colrange = newupto else: oldfrom = int(span[f"from_{axis}"]) if not oldfrom: newfrom = 0 else: newfrom = full_new_idxs[oldfrom] newupto = None oldupto_colrange = total newupto_colrange = oldupto_colrange return oldupto_colrange, newupto_colrange, newfrom, newupto def mod_span( to_set_to: Span, to_set_from: Span, from_r: int | None = None, from_c: int | None = None, upto_r: int | None = None, upto_c: int | None = None, ) -> Span: to_set_to.kwargs = to_set_from.kwargs to_set_to.type_ = to_set_from.type_ to_set_to.table = to_set_from.table to_set_to.index = to_set_from.index to_set_to.header = to_set_from.header to_set_to.from_r = from_r to_set_to.from_c = from_c to_set_to.upto_r = upto_r to_set_to.upto_c = upto_c return to_set_to def mod_span_widget(span: Span, widget: object) -> Span: span.widget = widget return span def mod_event_val( event_data: EventDataDict, val: object, loc: Loc | int, ) -> EventDataDict: event_data.value = val event_data.loc = Loc(*loc) event_data.row = loc[0] event_data.column = loc[1] return event_data def pop_positions( itr: Iterator[int], to_pop: dict[int, int], # displayed index: data index save_to: dict[int, int], ) -> Iterator[int]: for i, pos in enumerate(itr()): if i in to_pop: save_to[to_pop[i]] = pos else: yield pos tksheet-7.2.12/tksheet/main_table.py000066400000000000000000011633571463432073300174130ustar00rootroot00000000000000from __future__ import annotations import csv as csv import io import tkinter as tk from bisect import ( bisect_left, bisect_right, ) from collections import ( defaultdict, deque, ) from collections.abc import ( Callable, Generator, Hashable, Iterator, Sequence, ) from functools import ( partial, ) from itertools import ( accumulate, chain, cycle, filterfalse, islice, repeat, ) from math import ( ceil, floor, ) from operator import itemgetter from tkinter import TclError from typing import Literal from .colors import ( color_map, ) from .formatters import ( data_to_str, format_data, get_clipboard_data, get_data_with_valid_check, is_bool_like, try_to_bool, ) from .functions import ( b_index, consecutive_ranges, decompress_load, diff_gen, diff_list, event_dict, gen_formatted, get_data_from_clipboard, get_new_indexes, get_seq_without_gaps_at_index, index_exists, insert_items, int_x_iter, is_iterable, is_type_int, len_to_idx, mod_event_val, mod_span, mod_span_widget, move_elements_by_mapping, new_tk_event, pickle_obj, pickled_event_dict, rounded_box_coords, span_idxs_post_move, try_binding, unpickle_obj, ) from .other_classes import ( Box_nt, Box_st, Box_t, DotDict, DropdownStorage, EventDataDict, FontTuple, Loc, ProgressBar, Selected, SelectionBox, TextEditorStorage, ) from .text_editor import ( TextEditor, ) from .vars import ( USER_OS, bind_add_columns, bind_add_rows, bind_del_columns, bind_del_rows, ctrl_key, rc_binding, symbols_set, text_editor_close_bindings, text_editor_newline_bindings, text_editor_to_unbind, val_modifying_options, ) class MainTable(tk.Canvas): def __init__(self, *args, **kwargs): tk.Canvas.__init__( self, kwargs["parent"], background=kwargs["parent"].ops.table_bg, highlightthickness=0, ) self.PAR = kwargs["parent"] self.PAR_width = 0 self.PAR_height = 0 self.scrollregion = tuple() self.current_cursor = "" self.b1_pressed_loc = None self.closed_dropdown = None self.centre_alignment_text_mod_indexes = (slice(1, None), slice(None, -1)) self.c_align_cyc = cycle(self.centre_alignment_text_mod_indexes) self.allow_auto_resize_columns = True self.allow_auto_resize_rows = True self.span = self.PAR.span self.synced_scrolls = set() self.dropdown = DropdownStorage() self.text_editor = TextEditorStorage() self.event_linker = { "<>": self.ctrl_c, "<>": self.ctrl_x, "<>": self.ctrl_v, "<>": self.delete_key, "<>": self.undo, "<>": self.redo, "<>": self.select_all, } self.disp_ctrl_outline = {} self.disp_text = {} self.disp_high = {} self.disp_grid = {} self.disp_resize_lines = {} self.disp_dropdown = {} self.disp_checkbox = {} self.disp_boxes = set() self.hidd_ctrl_outline = {} self.hidd_text = {} self.hidd_high = {} self.hidd_grid = {} self.hidd_resize_lines = {} self.hidd_dropdown = {} self.hidd_checkbox = {} self.hidd_boxes = set() self.selection_boxes = {} self.selected = tuple() self.named_spans = {} self.reset_tags() self.cell_options = {} self.col_options = {} self.row_options = {} self.purge_undo_and_redo_stack() self.progress_bars = {} self.extra_table_rc_menu_funcs = {} self.extra_index_rc_menu_funcs = {} self.extra_header_rc_menu_funcs = {} self.extra_empty_space_rc_menu_funcs = {} self.show_index = kwargs["show_index"] self.show_header = kwargs["show_header"] self.min_row_height = 0 self.min_header_height = 0 self.being_drawn_item = None self.extra_motion_func = None self.extra_b1_press_func = None self.extra_b1_motion_func = None self.extra_b1_release_func = None self.extra_double_b1_func = None self.extra_rc_func = None self.edit_validation_func = None self.extra_begin_ctrl_c_func = None self.extra_end_ctrl_c_func = None self.extra_begin_ctrl_x_func = None self.extra_end_ctrl_x_func = None self.extra_begin_ctrl_v_func = None self.extra_end_ctrl_v_func = None self.extra_begin_ctrl_z_func = None self.extra_end_ctrl_z_func = None self.extra_begin_delete_key_func = None self.extra_end_delete_key_func = None self.extra_begin_edit_cell_func = None self.extra_end_edit_cell_func = None self.extra_begin_del_rows_rc_func = None self.extra_end_del_rows_rc_func = None self.extra_begin_del_cols_rc_func = None self.extra_end_del_cols_rc_func = None self.extra_begin_insert_cols_rc_func = None self.extra_end_insert_cols_rc_func = None self.extra_begin_insert_rows_rc_func = None self.extra_end_insert_rows_rc_func = None self.text_editor_user_bound_keys = {} self.selection_binding_func = None self.deselection_binding_func = None self.drag_selection_binding_func = None self.shift_selection_binding_func = None self.ctrl_selection_binding_func = None self.select_all_binding_func = None self.tab_enabled = False self.up_enabled = False self.right_enabled = False self.down_enabled = False self.left_enabled = False self.prior_enabled = False self.next_enabled = False self.single_selection_enabled = False # with this mode every left click adds the cell to selected cells self.toggle_selection_enabled = False self.drag_selection_enabled = False self.select_all_enabled = False self.undo_enabled = False self.cut_enabled = False self.copy_enabled = False self.paste_enabled = False self.delete_key_enabled = False self.rc_select_enabled = False self.ctrl_select_enabled = False self.rc_delete_column_enabled = False self.rc_insert_column_enabled = False self.rc_delete_row_enabled = False self.rc_insert_row_enabled = False self.rc_popup_menus_enabled = False self.edit_cell_enabled = False self.new_row_width = 0 self.new_header_height = 0 self.CH = kwargs["column_headers_canvas"] self.CH.MT = self self.CH.RI = kwargs["row_index_canvas"] self.RI = kwargs["row_index_canvas"] self.RI.MT = self self.RI.CH = kwargs["column_headers_canvas"] self.TL = None # is set from within TopLeftRectangle() __init__ self.all_columns_displayed = True self.all_rows_displayed = True self.align = kwargs["align"] self.PAR.ops.table_font = [ self.PAR.ops.table_font[0], int(self.PAR.ops.table_font[1] * kwargs["zoom"] / 100), self.PAR.ops.table_font[2], ] self.PAR.ops.index_font = [ self.PAR.ops.index_font[0], int(self.PAR.ops.index_font[1] * kwargs["zoom"] / 100), self.PAR.ops.index_font[2], ] self.PAR.ops.header_font = [ self.PAR.ops.header_font[0], int(self.PAR.ops.header_font[1] * kwargs["zoom"] / 100), self.PAR.ops.header_font[2], ] for fnt in (self.PAR.ops.table_font, self.PAR.ops.index_font, self.PAR.ops.header_font): if fnt[1] < 1: fnt[1] = 1 self.PAR.ops.table_font = FontTuple(*self.PAR.ops.table_font) self.PAR.ops.index_font = FontTuple(*self.PAR.ops.index_font) self.PAR.ops.header_font = FontTuple(*self.PAR.ops.header_font) self.txt_measure_canvas = tk.Canvas(self) self.txt_measure_canvas_text = self.txt_measure_canvas.create_text(0, 0, text="", font=self.PAR.ops.table_font) self.max_row_height = float(kwargs["max_row_height"]) self.max_index_width = float(kwargs["max_index_width"]) self.max_column_width = float(kwargs["max_column_width"]) self.max_header_height = float(kwargs["max_header_height"]) self.RI.set_width(self.PAR.ops.default_row_index_width) self.set_table_font_help() self.set_header_font_help() self.set_index_font_help() self.data = kwargs["data_reference"] if isinstance(self.data, (list, tuple)): self.data = kwargs["data_reference"] else: self.data = [] if not self.data: if ( isinstance(kwargs["total_rows"], int) and isinstance(kwargs["total_cols"], int) and kwargs["total_rows"] > 0 and kwargs["total_cols"] > 0 ): self.data = [list(repeat("", kwargs["total_cols"])) for i in range(kwargs["total_rows"])] _header = kwargs["header"] if kwargs["header"] is not None else kwargs["headers"] if isinstance(_header, int): self._headers = _header else: if _header: self._headers = _header else: self._headers = [] _row_index = kwargs["index"] if kwargs["index"] is not None else kwargs["row_index"] if isinstance(_row_index, int): self._row_index = _row_index else: if _row_index: self._row_index = _row_index else: self._row_index = [] self.saved_row_heights = {} self.saved_column_widths = {} self.displayed_columns = [] self.displayed_rows = [] self.set_col_positions(itr=[]) self.set_row_positions(itr=[]) self.display_rows( rows=kwargs["displayed_rows"], all_rows_displayed=kwargs["all_rows_displayed"], reset_row_positions=False, deselect_all=False, ) self.reset_row_positions() self.display_columns( columns=kwargs["displayed_columns"], all_columns_displayed=kwargs["all_columns_displayed"], reset_col_positions=False, deselect_all=False, ) self.reset_col_positions() self.rc_popup_menu = None self.empty_rc_popup_menu = None self.basic_bindings() self.create_rc_menus() def event_generate(self, *args, **kwargs) -> None: for arg in args: if arg in self.event_linker: self.event_linker[arg]() else: super().event_generate(*args, **kwargs) def refresh(self, event: object = None) -> None: self.main_table_redraw_grid_and_text(True, True) def window_configured(self, event: object) -> None: w = self.PAR.winfo_width() if w != self.PAR_width: self.PAR_width = w self.allow_auto_resize_columns = True h = self.PAR.winfo_height() if h != self.PAR_height: self.PAR_height = h self.allow_auto_resize_rows = True self.main_table_redraw_grid_and_text(True, True) def basic_bindings(self, enable: bool = True) -> None: bindings = ( ("", self, self.window_configured), ("", self, self.mouse_motion), ("", self, self.b1_press), ("", self, self.b1_motion), ("", self, self.b1_release), ("", self, self.double_b1), ("", self, self.mousewheel), ("", self.RI, self.mousewheel), ("", self, self.shift_b1_press), ("", self.CH, self.CH.shift_b1_press), ("", self.RI, self.RI.shift_b1_press), (rc_binding, self, self.rc), (f"<{ctrl_key}-ButtonPress-1>", self, self.ctrl_b1_press), (f"<{ctrl_key}-ButtonPress-1>", self.CH, self.CH.ctrl_b1_press), (f"<{ctrl_key}-ButtonPress-1>", self.RI, self.RI.ctrl_b1_press), (f"<{ctrl_key}-Shift-ButtonPress-1>", self, self.ctrl_shift_b1_press), (f"<{ctrl_key}-Shift-ButtonPress-1>", self.CH, self.CH.ctrl_shift_b1_press), (f"<{ctrl_key}-Shift-ButtonPress-1>", self.RI, self.RI.ctrl_shift_b1_press), (f"<{ctrl_key}-B1-Motion>", self, self.ctrl_b1_motion), (f"<{ctrl_key}-B1-Motion>", self.CH, self.CH.ctrl_b1_motion), (f"<{ctrl_key}-B1-Motion>", self.RI, self.RI.ctrl_b1_motion), ) all_canvas_bindings = ( ("", self.shift_mousewheel), ("", self.ctrl_mousewheel), ("", self.zoom_in), ("", self.zoom_in), ("", self.zoom_out), ) mt_ri_canvas_linux_bindings = { ("", self.mousewheel), ("", self.mousewheel), } all_canvas_linux_bindings = { ("", self.shift_mousewheel), ("", self.shift_mousewheel), ("", self.ctrl_mousewheel), ("", self.ctrl_mousewheel), } if enable: for b in bindings: b[1].bind(b[0], b[2]) for b in all_canvas_bindings: for canvas in (self, self.RI, self.CH): canvas.bind(b[0], b[1]) if USER_OS == "linux": for b in mt_ri_canvas_linux_bindings: for canvas in (self, self.RI): canvas.bind(b[0], b[1]) for b in all_canvas_linux_bindings: for canvas in (self, self.RI, self.CH): canvas.bind(b[0], b[1]) else: for b in bindings: b[1].unbind(b[0]) for b in all_canvas_bindings: for canvas in (self, self.RI, self.CH): canvas.unbind(b[0]) if USER_OS == "linux": for b in mt_ri_canvas_linux_bindings: for canvas in (self, self.RI): canvas.unbind(b[0]) for b in all_canvas_linux_bindings: for canvas in (self, self.RI, self.CH): canvas.unbind(b[0]) def reset_tags(self) -> None: self.tagged_cells = {} self.tagged_rows = {} self.tagged_columns = {} def show_ctrl_outline( self, canvas: Literal["table"] = "table", start_cell: tuple[int, int] = (0, 0), end_cell: tuple[int, int] = (0, 0), dash: tuple[int, int] = (20, 20), outline: str | None = None, delete_on_timer: bool = True, ) -> None: self.create_ctrl_outline( self.col_positions[start_cell[0]] + 1, self.row_positions[start_cell[1]] + 1, self.col_positions[end_cell[0]] - 1, self.row_positions[end_cell[1]] - 1, fill="", dash=dash, width=3, outline=self.PAR.ops.resizing_line_fg if outline is None else outline, tag="ctrl", ) if delete_on_timer: self.after(1500, self.delete_ctrl_outlines) def create_ctrl_outline( self, x1: int, y1: int, x2: int, y2: int, fill: str, dash: tuple[int, int], width: int, outline: str, tag: str | tuple[str, ...], ) -> None: if self.hidd_ctrl_outline: t, sh = self.hidd_ctrl_outline.popitem() self.coords(t, x1, y1, x2, y2) if sh: self.itemconfig(t, fill=fill, dash=dash, width=width, outline=outline, tag=tag) else: self.itemconfig( t, fill=fill, dash=dash, width=width, outline=outline, tag=tag, state="normal", ) self.lift(t) else: t = self.create_rectangle( x1, y1, x2, y2, fill=fill, dash=dash, width=width, outline=outline, tag=tag, ) self.disp_ctrl_outline[t] = True def delete_ctrl_outlines(self) -> None: self.hidd_ctrl_outline.update(self.disp_ctrl_outline) self.disp_ctrl_outline = {} for t, sh in self.hidd_ctrl_outline.items(): if sh: self.itemconfig(t, state="hidden") self.hidd_ctrl_outline[t] = False def get_ctrl_x_c_boxes(self) -> tuple[dict[tuple[int, int, int, int], str], int]: boxes = {} maxrows = 0 if not self.selected: return boxes, maxrows if self.selected.type_ in ("cells", "columns"): curr_box = self.selection_boxes[self.selected.fill_iid].coords maxrows = curr_box[2] - curr_box[0] for item, box in self.get_selection_items(rows=False): if maxrows >= box.coords[2] - box.coords[0]: boxes[box.coords] = box.type_ else: for item, box in self.get_selection_items(columns=False, cells=False): boxes[box.coords] = "rows" return boxes, maxrows def io_csv_writer(self) -> tuple[io.StringIO, csv.writer]: s = io.StringIO() writer = csv.writer( s, dialect=csv.excel_tab, delimiter=self.PAR.ops.to_clipboard_delimiter, quotechar=self.PAR.ops.to_clipboard_quotechar, lineterminator=self.PAR.ops.to_clipboard_lineterminator, ) return s, writer def ctrl_c(self, event=None) -> None | EventDataDict: if not self.selected: return event_data = event_dict( name="begin_ctrl_c", sheet=self.PAR.name, widget=self, selected=self.selected, ) boxes, maxrows = self.get_ctrl_x_c_boxes() event_data["selection_boxes"] = boxes s, writer = self.io_csv_writer() if not try_binding(self.extra_begin_ctrl_c_func, event_data): return if self.selected.type_ in ("cells", "columns"): for rn in range(maxrows): row = [] for r1, c1, r2, c2 in boxes: datarn = (r1 + rn) if self.all_rows_displayed else self.displayed_rows[r1 + rn] for c in range(c1, c2): datacn = self.datacn(c) v = self.get_cell_clipboard(datarn, datacn) event_data["cells"]["table"][(datarn, datacn)] = v row.append(v) writer.writerow(row) else: for r1, c1, r2, c2 in boxes: for rn in range(r2 - r1): row = [] datarn = (r1 + rn) if self.all_rows_displayed else self.displayed_rows[r1 + rn] for c in range(c1, c2): datacn = self.datacn(c) v = self.get_cell_clipboard(datarn, datacn) event_data["cells"]["table"][(datarn, datacn)] = v row.append(v) writer.writerow(row) for r1, c1, r2, c2 in boxes: self.show_ctrl_outline(canvas="table", start_cell=(c1, r1), end_cell=(c2, r2)) self.clipboard_clear() if len(event_data["cells"]["table"]) == 1 and self.PAR.ops.to_clipboard_lineterminator not in next( iter(event_data["cells"]["table"].values()) ): self.clipboard_append(next(iter(event_data["cells"]["table"].values()))) else: self.clipboard_append(s.getvalue()) self.update_idletasks() try_binding(self.extra_end_ctrl_c_func, event_data, new_name="end_ctrl_c") self.PAR.emit_event("<>", EventDataDict({**event_data, **{"eventname": "copy"}})) return event_data def ctrl_x(self, event=None, validation: bool = True) -> None | EventDataDict: if not self.selected: return event_data = event_dict( name="edit_table", sheet=self.PAR.name, widget=self, selected=self.selected, ) boxes, maxrows = self.get_ctrl_x_c_boxes() event_data["selection_boxes"] = boxes s, writer = self.io_csv_writer() if not try_binding(self.extra_begin_ctrl_x_func, event_data, "begin_ctrl_x"): return if self.selected.type_ in ("cells", "columns"): for rn in range(maxrows): row = [] for r1, c1, r2, c2 in boxes: if r2 - r1 < maxrows: continue datarn = (r1 + rn) if self.all_rows_displayed else self.displayed_rows[r1 + rn] for c in range(c1, c2): datacn = self.datacn(c) row.append(self.get_cell_clipboard(datarn, datacn)) val = self.get_value_for_empty_cell(datarn, datacn) if ( not self.edit_validation_func or not validation or ( self.edit_validation_func and (val := self.edit_validation_func(mod_event_val(event_data, val, (r1 + rn, c)))) is not None ) ): event_data = self.event_data_set_cell( datarn, datacn, val, event_data, ) writer.writerow(row) else: for r1, c1, r2, c2 in boxes: for rn in range(r2 - r1): row = [] datarn = (r1 + rn) if self.all_rows_displayed else self.displayed_rows[r1 + rn] for c in range(c1, c2): datacn = self.datacn(c) row.append(self.get_cell_clipboard(datarn, datacn)) val = self.get_value_for_empty_cell(datarn, datacn) if ( not self.edit_validation_func or not validation or ( self.edit_validation_func and (val := self.edit_validation_func(mod_event_val(event_data, val, (r1 + rn, c)))) is not None ) ): event_data = self.event_data_set_cell( datarn, datacn, val, event_data, ) writer.writerow(row) if event_data["cells"]["table"]: self.undo_stack.append(pickled_event_dict(event_data)) self.clipboard_clear() if len(event_data["cells"]["table"]) == 1 and self.PAR.ops.to_clipboard_lineterminator not in next( iter(event_data["cells"]["table"].values()) ): self.clipboard_append(next(iter(event_data["cells"]["table"].values()))) else: self.clipboard_append(s.getvalue()) self.update_idletasks() self.refresh() for r1, c1, r2, c2 in boxes: self.show_ctrl_outline(canvas="table", start_cell=(c1, r1), end_cell=(c2, r2)) if event_data["cells"]["table"]: try_binding(self.extra_end_ctrl_x_func, event_data, "end_ctrl_x") self.sheet_modified(event_data) self.PAR.emit_event("<>", event_data) return event_data def ctrl_v(self, event: object = None, validation: bool = True) -> None | EventDataDict: if not self.PAR.ops.paste_can_expand_x and len(self.col_positions) == 1: return if not self.PAR.ops.paste_can_expand_y and len(self.row_positions) == 1: return event_data = event_dict( name="edit_table", sheet=self.PAR.name, widget=self, selected=self.selected, ) if self.selected: selected_r = self.selected.box.from_r selected_c = self.selected.box.from_c curr_coords = (self.selected.row, self.selected.column) elif not self.selected and not self.PAR.ops.paste_can_expand_x and not self.PAR.ops.paste_can_expand_y: return else: if not self.data: selected_c, selected_r = 0, 0 else: if len(self.col_positions) == 1 and len(self.row_positions) > 1: selected_c, selected_r = 0, len(self.row_positions) - 1 elif len(self.row_positions) == 1 and len(self.col_positions) > 1: selected_c, selected_r = len(self.col_positions) - 1, 0 elif len(self.row_positions) > 1 and len(self.col_positions) > 1: selected_c, selected_r = 0, len(self.row_positions) - 1 curr_coords = (selected_r, selected_c) try: data = get_data_from_clipboard( widget=self, delimiters=self.PAR.ops.from_clipboard_delimiters, lineterminator=self.PAR.ops.to_clipboard_lineterminator, ) except Exception: return new_data_numcols = max(map(len, data)) new_data_numrows = len(data) for rn, r in enumerate(data): if len(r) < new_data_numcols: data[rn] += list(repeat("", new_data_numcols - len(r))) if self.selected: ( lastbox_r1, lastbox_c1, lastbox_r2, lastbox_c2, ) = self.selection_boxes[self.selected.fill_iid].coords lastbox_numrows = lastbox_r2 - lastbox_r1 lastbox_numcols = lastbox_c2 - lastbox_c1 if lastbox_numrows > new_data_numrows and not lastbox_numrows % new_data_numrows: nd = [] for _ in range(int(lastbox_numrows / new_data_numrows)): nd.extend(r.copy() for r in data) data.extend(nd) new_data_numrows *= int(lastbox_numrows / new_data_numrows) if lastbox_numcols > new_data_numcols and not lastbox_numcols % new_data_numcols: for rn, r in enumerate(data): for _ in range(int(lastbox_numcols / new_data_numcols)): data[rn].extend(r.copy()) new_data_numcols *= int(lastbox_numcols / new_data_numcols) event_data["data"] = data added_rows = 0 added_cols = 0 total_data_cols = None if self.PAR.ops.paste_can_expand_x: if selected_c + new_data_numcols > len(self.col_positions) - 1: total_data_cols = self.equalize_data_row_lengths() added_cols = selected_c + new_data_numcols - len(self.col_positions) + 1 if ( isinstance(self.PAR.ops.paste_insert_column_limit, int) and self.PAR.ops.paste_insert_column_limit < len(self.col_positions) - 1 + added_cols ): added_cols = self.PAR.ops.paste_insert_column_limit - len(self.col_positions) - 1 if self.PAR.ops.paste_can_expand_y: if selected_r + new_data_numrows > len(self.row_positions) - 1: added_rows = selected_r + new_data_numrows - len(self.row_positions) + 1 if ( isinstance(self.PAR.ops.paste_insert_row_limit, int) and self.PAR.ops.paste_insert_row_limit < len(self.row_positions) - 1 + added_rows ): added_rows = self.PAR.ops.paste_insert_row_limit - len(self.row_positions) - 1 if selected_c + new_data_numcols > len(self.col_positions) - 1: adjusted_new_data_numcols = len(self.col_positions) - 1 - selected_c else: adjusted_new_data_numcols = new_data_numcols if selected_r + new_data_numrows > len(self.row_positions) - 1: adjusted_new_data_numrows = len(self.row_positions) - 1 - selected_r else: adjusted_new_data_numrows = new_data_numrows selected_r_adjusted_new_data_numrows = selected_r + adjusted_new_data_numrows selected_c_adjusted_new_data_numcols = selected_c + adjusted_new_data_numcols endrow = selected_r_adjusted_new_data_numrows boxes = { ( selected_r, selected_c, selected_r_adjusted_new_data_numrows, selected_c_adjusted_new_data_numcols, ): "cells" } event_data["selection_boxes"] = boxes if not try_binding(self.extra_begin_ctrl_v_func, event_data, "begin_ctrl_v"): return # the order of actions here is important: # edit existing sheet (not including any added rows/columns) # then if there are any added rows/columns: # create empty rows/columns dicts for any added rows/columns # edit those dicts with so far unused cells of data from clipboard # instead of editing table using set cell data, add any new rows then columns with pasted data for ndr, r in enumerate(range(selected_r, selected_r_adjusted_new_data_numrows)): for ndc, c in enumerate(range(selected_c, selected_c_adjusted_new_data_numcols)): val = data[ndr][ndc] if ( not self.edit_validation_func or not validation or ( self.edit_validation_func and (val := self.edit_validation_func(mod_event_val(event_data, val, (r, c)))) is not None ) ): event_data = self.event_data_set_cell( datarn=self.datarn(r), datacn=self.datacn(c), value=val, event_data=event_data, ) if added_rows: ctr = 0 data_ins_row = len(self.data) displayed_ins_row = len(self.row_positions) - 1 if total_data_cols is None: total_data_cols = self.total_data_cols() rows, index, row_heights = self.get_args_for_add_rows( data_ins_row=data_ins_row, displayed_ins_row=displayed_ins_row, numrows=added_rows, total_data_cols=total_data_cols, ) for ndr, r in zip( range( adjusted_new_data_numrows, new_data_numrows, ), reversed(rows), ): for ndc, c in enumerate( range( selected_c, selected_c_adjusted_new_data_numcols, ) ): val = data[ndr][ndc] datacn = self.datacn(c) if ( not self.edit_validation_func or not validation or ( self.edit_validation_func and (val := self.edit_validation_func(mod_event_val(event_data, val, (r, c)))) is not None and self.input_valid_for_cell(r, datacn, val, ignore_empty=True) ) ): rows[r][datacn] = val ctr += 1 if ctr: event_data = self.add_rows( rows=rows, index=index, row_heights=row_heights, event_data=event_data, mod_event_boxes=False, ) if added_cols: ctr = 0 if total_data_cols is None: total_data_cols = self.total_data_cols() data_ins_col = total_data_cols displayed_ins_col = len(self.col_positions) - 1 columns, headers, column_widths = self.get_args_for_add_columns( data_ins_col=data_ins_col, displayed_ins_col=displayed_ins_col, numcols=added_cols, ) # only add the extra rows if expand_y is allowed if self.PAR.ops.paste_can_expand_x and self.PAR.ops.paste_can_expand_y: endrow = selected_r + new_data_numrows else: endrow = selected_r + adjusted_new_data_numrows for ndr, r in enumerate( range( selected_r, endrow, ) ): for ndc, c in zip( range( adjusted_new_data_numcols, new_data_numcols, ), reversed(columns), ): val = data[ndr][ndc] datarn = self.datarn(r) if ( not self.edit_validation_func or not validation or ( self.edit_validation_func and (val := self.edit_validation_func(mod_event_val(event_data, val, (r, c)))) is not None and self.input_valid_for_cell(datarn, c, val, ignore_empty=True) ) ): columns[c][datarn] = val ctr += 1 if ctr: event_data = self.add_columns( columns=columns, header=headers, column_widths=column_widths, event_data=event_data, mod_event_boxes=False, ) if added_rows: selboxr = selected_r + new_data_numrows else: selboxr = selected_r_adjusted_new_data_numrows if added_cols: selboxc = selected_c + new_data_numcols else: selboxc = selected_c_adjusted_new_data_numcols self.deselect("all", redraw=False) self.set_currently_selected( *curr_coords, item=self.create_selection_box( selected_r, selected_c, selboxr, selboxc, type_="cells", set_current=False, run_binding=True, ), ) event_data["selection_boxes"] = self.get_boxes() event_data["selected"] = self.selected if event_data["cells"]["table"] or event_data["added"]["rows"] or event_data["added"]["columns"]: self.undo_stack.append(pickled_event_dict(event_data)) self.see( r=selected_r, c=selected_c, keep_yscroll=False, keep_xscroll=False, bottom_right_corner=False, check_cell_visibility=True, redraw=False, ) self.refresh() if event_data["cells"]["table"] or event_data["added"]["rows"] or event_data["added"]["columns"]: try_binding(self.extra_end_ctrl_v_func, event_data, "end_ctrl_v") self.sheet_modified(event_data) self.PAR.emit_event("<>", event_data) return event_data def delete_key(self, event: object = None, validation: bool = True) -> None | EventDataDict: if not self.selected: return event_data = event_dict( name="edit_table", sheet=self.PAR.name, widget=self, selected=self.selected, ) boxes = self.get_boxes() event_data["selection_boxes"] = boxes if not try_binding(self.extra_begin_delete_key_func, event_data, "begin_delete"): return for r1, c1, r2, c2 in boxes: for r in range(r1, r2): for c in range(c1, c2): datarn, datacn = self.datarn(r), self.datacn(c) val = self.get_value_for_empty_cell(datarn, datacn) if ( not self.edit_validation_func or not validation or ( self.edit_validation_func and (val := self.edit_validation_func(mod_event_val(event_data, val, (r, c)))) is not None ) ): event_data = self.event_data_set_cell( datarn, datacn, val, event_data, ) if event_data["cells"]["table"]: self.undo_stack.append(pickled_event_dict(event_data)) try_binding(self.extra_end_delete_key_func, event_data, "end_delete") self.refresh() self.sheet_modified(event_data) self.PAR.emit_event("<>", event_data) return event_data def event_data_set_cell(self, datarn: int, datacn: int, value: object, event_data: dict) -> EventDataDict: if self.input_valid_for_cell(datarn, datacn, value): event_data["cells"]["table"][(datarn, datacn)] = self.get_cell_data(datarn, datacn) self.set_cell_data(datarn, datacn, value) return event_data def get_args_for_move_columns( self, move_to: int, to_move: list[int], data_indexes: bool = False, ) -> tuple[dict[int, int], dict[int, int], int, dict[int, int]]: if not data_indexes or self.all_columns_displayed: disp_new_idxs = get_new_indexes(move_to=move_to, to_move=to_move) else: disp_new_idxs = {} # at_least_cols should not be len in this case as move_to can be len fix_len = (move_to - 1) if move_to else move_to if not self.all_columns_displayed and not data_indexes: fix_len = self.datacn(fix_len) totalcols = self.equalize_data_row_lengths(at_least_cols=fix_len) data_new_idxs = get_new_indexes(move_to=move_to, to_move=to_move) if not self.all_columns_displayed and not data_indexes: data_new_idxs = dict( zip( move_elements_by_mapping( self.displayed_columns, data_new_idxs, dict(zip(data_new_idxs.values(), data_new_idxs)), ), self.displayed_columns, ), ) return data_new_idxs, dict(zip(data_new_idxs.values(), data_new_idxs)), totalcols, disp_new_idxs def move_columns_adjust_options_dict( self, data_new_idxs: dict[int, int], data_old_idxs: dict[int, int], totalcols: int | None, disp_new_idxs: None | dict[int, int] = None, move_data: bool = True, move_widths: bool = True, create_selections: bool = True, data_indexes: bool = False, event_data: EventDataDict | None = None, ) -> tuple[dict[int, int], dict[int, int], EventDataDict]: self.saved_column_widths = {} if not isinstance(totalcols, int): totalcols = max(data_new_idxs.values(), default=0) if totalcols: totalcols += 1 totalcols = self.equalize_data_row_lengths(at_least_cols=totalcols) if event_data is None: event_data = event_dict( name="move_columns", sheet=self.PAR.name, widget=self, boxes=self.get_boxes(), selected=self.selected, ) event_data["moved"]["columns"] = { "data": data_new_idxs, "displayed": {} if disp_new_idxs is None else disp_new_idxs, } event_data["options"] = self.pickle_options() event_data["named_spans"] = {k: span.pickle_self() for k, span in self.named_spans.items()} if move_widths and disp_new_idxs and (not data_indexes or self.all_columns_displayed): self.deselect("all", run_binding=False, redraw=False) self.set_col_positions( itr=move_elements_by_mapping( self.get_column_widths(), disp_new_idxs, dict( zip( disp_new_idxs.values(), disp_new_idxs, ) ), ) ) if create_selections: for boxst, boxend in consecutive_ranges(sorted(disp_new_idxs.values())): self.create_selection_box( 0, boxst, len(self.row_positions) - 1, boxend, "columns", run_binding=True, ) if move_data: self.data = list( map( move_elements_by_mapping, self.data, repeat(data_new_idxs), repeat(data_old_idxs), ), ) maxidx = len_to_idx(totalcols) self.CH.fix_header(maxidx) if isinstance(self._headers, list) and self._headers: self._headers = move_elements_by_mapping(self._headers, data_new_idxs, data_old_idxs) maxidx = self.get_max_column_idx(maxidx) full_new_idxs = self.get_full_new_idxs( max_idx=maxidx, new_idxs=data_new_idxs, old_idxs=data_old_idxs, ) full_old_idxs = dict(zip(full_new_idxs.values(), full_new_idxs)) self.tagged_cells = { tags: {(k[0], full_new_idxs[k[1]]) for k in tagged} for tags, tagged in self.tagged_cells.items() } self.cell_options = {(k[0], full_new_idxs[k[1]]): v for k, v in self.cell_options.items()} self.progress_bars = {(k[0], full_new_idxs[k[1]]): v for k, v in self.progress_bars.items()} self.col_options = {full_new_idxs[k]: v for k, v in self.col_options.items()} self.tagged_columns = { tags: {full_new_idxs[k] for k in tagged} for tags, tagged in self.tagged_columns.items() } self.CH.cell_options = {full_new_idxs[k]: v for k, v in self.CH.cell_options.items()} self.displayed_columns = sorted(full_new_idxs[k] for k in self.displayed_columns) if self.named_spans: totalrows = self.total_data_rows() new_ops = self.PAR.create_options_from_span qkspan = self.span() for span in self.named_spans.values(): # span is neither a cell options nor col options span, continue if not isinstance(span["from_c"], int): continue oldupto_colrange, newupto_colrange, newfrom, newupto = span_idxs_post_move( data_new_idxs, full_new_idxs, totalcols, span, "c", ) # add cell/col kwargs for columns that are new to the span old_span_idxs = set(full_new_idxs[k] for k in range(span["from_c"], oldupto_colrange)) for k in range(newfrom, newupto_colrange): if k not in old_span_idxs: oldidx = full_old_idxs[k] # event_data is used to preserve old cell value # in case cells are modified by # formatting, checkboxes, dropdown boxes if ( span["type_"] in val_modifying_options and span["header"] and oldidx not in event_data["cells"]["header"] ): event_data["cells"]["header"][oldidx] = self.CH.get_cell_data(k) # the span targets columns if span["from_r"] is None: if span["type_"] in val_modifying_options: for datarn in range(len(self.data)): if (datarn, oldidx) not in event_data["cells"]["table"]: event_data["cells"]["table"][(datarn, oldidx)] = self.get_cell_data( datarn, k ) # create new col options new_ops( mod_span( qkspan, span, from_c=k, upto_c=k + 1, ) ) # the span targets cells else: rng_upto_r = totalrows if span["upto_r"] is None else span["upto_r"] for datarn in range(span["from_r"], rng_upto_r): if ( span["type_"] in val_modifying_options and (datarn, oldidx) not in event_data["cells"]["table"] ): event_data["cells"]["table"][(datarn, oldidx)] = self.get_cell_data(datarn, k) # create new cell options new_ops( mod_span( qkspan, span, from_r=datarn, upto_r=datarn + 1, from_c=k, upto_c=k + 1, ) ) # remove span specific kwargs from cells/columns # that are no longer in the span, # cell options/col options keys are new idxs for k in range(span["from_c"], oldupto_colrange): # has it moved outside of new span coords if ( isinstance(newupto, int) and (full_new_idxs[k] < newfrom or full_new_idxs[k] >= newupto) ) or (newupto is None and full_new_idxs[k] < newfrom): # span includes header if ( span["header"] and full_new_idxs[k] in self.CH.cell_options and span["type_"] in self.CH.cell_options[full_new_idxs[k]] ): del self.CH.cell_options[full_new_idxs[k]][span["type_"]] # span is for col options if span["from_r"] is None: if ( full_new_idxs[k] in self.col_options and span["type_"] in self.col_options[full_new_idxs[k]] ): del self.col_options[full_new_idxs[k]][span["type_"]] # span is for cell options else: rng_upto_r = totalrows if span["upto_r"] is None else span["upto_r"] for r in range(span["from_r"], rng_upto_r): if (r, full_new_idxs[k]) in self.cell_options and span[ "type_" ] in self.cell_options[(r, full_new_idxs[k])]: del self.cell_options[(r, full_new_idxs[k])][span["type_"]] # finally, change the span coords span["from_c"], span["upto_c"] = newfrom, newupto return data_new_idxs, disp_new_idxs, event_data def get_max_column_idx(self, maxidx: int | None = None) -> int: if maxidx is None: maxidx = len_to_idx(self.total_data_cols()) maxiget = partial(max, key=itemgetter(1)) return max( max(self.cell_options, key=itemgetter(1), default=(0, maxidx))[1], max(self.col_options, default=maxidx), max(self.CH.cell_options, default=maxidx), maxiget(map(maxiget, self.tagged_cells.values()), default=(0, maxidx))[1], max(map(max, self.tagged_columns.values()), default=maxidx), max((d.from_c for d in self.named_spans.values() if isinstance(d.from_c, int)), default=maxidx), max((d.upto_c for d in self.named_spans.values() if isinstance(d.upto_c, int)), default=maxidx), self.displayed_columns[-1] if self.displayed_columns else maxidx, ) def get_args_for_move_rows( self, move_to: int, to_move: list[int], data_indexes: bool = False, ) -> tuple[dict[int, int], dict[int, int], int, dict[int, int]]: if not data_indexes or self.all_rows_displayed: disp_new_idxs = get_new_indexes(move_to=move_to, to_move=to_move) else: disp_new_idxs = {} # move_to can be len and fix_data_len() takes index so - 1 fix_len = (move_to - 1) if move_to else move_to if not self.all_rows_displayed and not data_indexes: fix_len = self.datarn(fix_len) self.fix_data_len(fix_len) totalrows = max(self.total_data_rows(), len(self.row_positions) - 1) data_new_idxs = get_new_indexes(move_to=move_to, to_move=to_move) if not self.all_rows_displayed and not data_indexes: data_new_idxs = dict( zip( move_elements_by_mapping( self.displayed_rows, data_new_idxs, dict(zip(data_new_idxs.values(), data_new_idxs)), ), self.displayed_rows, ), ) return data_new_idxs, dict(zip(data_new_idxs.values(), data_new_idxs)), totalrows, disp_new_idxs def move_rows_adjust_options_dict( self, data_new_idxs: dict[int, int], data_old_idxs: dict[int, int], totalrows: int | None, disp_new_idxs: None | dict[int, int] = None, move_data: bool = True, move_heights: bool = True, create_selections: bool = True, data_indexes: bool = False, event_data: EventDataDict | None = None, ) -> tuple[dict[int, int], dict[int, int], EventDataDict]: self.saved_row_heights = {} if not isinstance(totalrows, int): totalrows = max( self.total_data_rows(), len(self.row_positions) - 1, max(data_new_idxs.values(), default=0), ) self.fix_data_len(totalrows - 1) if event_data is None: event_data = event_dict( name="move_rows", sheet=self.PAR.name, widget=self, boxes=self.get_boxes(), selected=self.selected, ) event_data["moved"]["rows"] = { "data": data_new_idxs, "displayed": {} if disp_new_idxs is None else disp_new_idxs, } event_data["options"] = self.pickle_options() event_data["named_spans"] = {k: span.pickle_self() for k, span in self.named_spans.items()} if move_heights and disp_new_idxs and (not data_indexes or self.all_rows_displayed): self.deselect("all", run_binding=False, redraw=False) self.set_row_positions( itr=move_elements_by_mapping( self.get_row_heights(), disp_new_idxs, dict( zip( disp_new_idxs.values(), disp_new_idxs, ) ), ) ) if create_selections: for boxst, boxend in consecutive_ranges(sorted(disp_new_idxs.values())): self.create_selection_box( boxst, 0, boxend, len(self.col_positions) - 1, "rows", run_binding=True, ) if move_data: self.data = move_elements_by_mapping( self.data, data_new_idxs, data_old_idxs, ) maxidx = len_to_idx(totalrows) self.RI.fix_index(maxidx) if isinstance(self._row_index, list) and self._row_index: self._row_index = move_elements_by_mapping(self._row_index, data_new_idxs, data_old_idxs) maxidx = self.get_max_row_idx(maxidx) full_new_idxs = self.get_full_new_idxs( max_idx=maxidx, new_idxs=data_new_idxs, old_idxs=data_old_idxs, ) full_old_idxs = dict(zip(full_new_idxs.values(), full_new_idxs)) self.tagged_cells = { tags: {(full_new_idxs[k[0]], k[1]) for k in tagged} for tags, tagged in self.tagged_cells.items() } self.cell_options = {(full_new_idxs[k[0]], k[1]): v for k, v in self.cell_options.items()} self.progress_bars = {(full_new_idxs[k[0]], k[1]): v for k, v in self.progress_bars.items()} self.tagged_rows = {tags: {full_new_idxs[k] for k in tagged} for tags, tagged in self.tagged_rows.items()} self.row_options = {full_new_idxs[k]: v for k, v in self.row_options.items()} self.RI.cell_options = {full_new_idxs[k]: v for k, v in self.RI.cell_options.items()} self.RI.tree_rns = {v: full_new_idxs[k] for v, k in self.RI.tree_rns.items()} self.displayed_rows = sorted(full_new_idxs[k] for k in self.displayed_rows) if self.named_spans: totalcols = self.total_data_cols() new_ops = self.PAR.create_options_from_span qkspan = self.span() for span in self.named_spans.values(): # span is neither a cell options nor row options span, continue if not isinstance(span["from_r"], int): continue oldupto_rowrange, newupto_rowrange, newfrom, newupto = span_idxs_post_move( data_new_idxs, full_new_idxs, totalrows, span, "r", ) # add cell/row kwargs for rows that are new to the span old_span_idxs = set(full_new_idxs[k] for k in range(span["from_r"], oldupto_rowrange)) for k in range(newfrom, newupto_rowrange): if k not in old_span_idxs: oldidx = full_old_idxs[k] # event_data is used to preserve old cell value # in case cells are modified by # formatting, checkboxes, dropdown boxes if ( span["type_"] in val_modifying_options and span["index"] and oldidx not in event_data["cells"]["index"] ): event_data["cells"]["index"][oldidx] = self.RI.get_cell_data(k) # the span targets rows if span["from_c"] is None: if span["type_"] in val_modifying_options: for datacn in range(len(self.data[k])): if (oldidx, datacn) not in event_data["cells"]["table"]: event_data["cells"]["table"][(oldidx, datacn)] = self.get_cell_data( k, datacn ) # create new row options new_ops( mod_span( qkspan, span, from_r=k, upto_r=k + 1, ) ) # the span targets cells else: rng_upto_c = totalcols if span["upto_c"] is None else span["upto_c"] for datacn in range(span["from_c"], rng_upto_c): if ( span["type_"] in val_modifying_options and (oldidx, datacn) not in event_data["cells"]["table"] ): event_data["cells"]["table"][(oldidx, datacn)] = self.get_cell_data(k, datacn) # create new cell options new_ops( mod_span( qkspan, span, from_r=k, upto_r=k + 1, from_c=datacn, upto_c=datacn + 1, ) ) # remove span specific kwargs from cells/rows # that are no longer in the span, # cell options/row options keys are new idxs for k in range(span["from_r"], oldupto_rowrange): # has it moved outside of new span coords if ( isinstance(newupto, int) and (full_new_idxs[k] < newfrom or full_new_idxs[k] >= newupto) ) or (newupto is None and full_new_idxs[k] < newfrom): # span includes index if ( span["index"] and full_new_idxs[k] in self.RI.cell_options and span["type_"] in self.RI.cell_options[full_new_idxs[k]] ): del self.RI.cell_options[full_new_idxs[k]][span["type_"]] # span is for row options if span["from_c"] is None: if ( full_new_idxs[k] in self.row_options and span["type_"] in self.row_options[full_new_idxs[k]] ): del self.row_options[full_new_idxs[k]][span["type_"]] # span is for cell options else: rng_upto_c = totalcols if span["upto_c"] is None else span["upto_c"] for c in range(span["from_c"], rng_upto_c): if (full_new_idxs[k], c) in self.cell_options and span[ "type_" ] in self.cell_options[(full_new_idxs[k], c)]: del self.cell_options[(full_new_idxs[k], c)][span["type_"]] # finally, change the span coords span["from_r"], span["upto_r"] = newfrom, newupto return data_new_idxs, disp_new_idxs, event_data def get_max_row_idx(self, maxidx: int | None = None) -> int: if maxidx is None: maxidx = len_to_idx(self.total_data_rows()) maxiget = partial(max, key=itemgetter(0)) return max( max(self.cell_options, key=itemgetter(0), default=(maxidx, 0))[0], max(self.row_options, default=maxidx), max(self.RI.cell_options, default=maxidx), maxiget(map(maxiget, self.tagged_cells.values()), default=(maxidx, 0))[0], max(map(max, self.tagged_rows.values()), default=maxidx), max((d.from_r for d in self.named_spans.values() if isinstance(d.from_r, int)), default=maxidx), max((d.upto_r for d in self.named_spans.values() if isinstance(d.upto_r, int)), default=maxidx), self.displayed_rows[-1] if self.displayed_rows else maxidx, ) def get_full_new_idxs( self, max_idx: int, new_idxs: dict[int, int], old_idxs: None | dict[int, int] = None, ) -> dict[int, int]: # return a dict of all row or column indexes # old indexes and new indexes, not just the # ones that were moved e.g. # {old index: new index, ...} # all the way from 0 to max_idx if old_idxs is None: old_idxs = dict(zip(new_idxs.values(), new_idxs)) return dict( zip( move_elements_by_mapping(tuple(range(max_idx + 1)), new_idxs, old_idxs), range(max_idx + 1), ) ) def undo(self, event: object = None) -> None | EventDataDict: if not self.undo_stack: return modification = decompress_load(self.undo_stack[-1]["data"]) if not try_binding(self.extra_begin_ctrl_z_func, modification, "begin_undo"): return event_data = self.undo_modification_invert_event(modification) self.redo_stack.append(pickled_event_dict(event_data)) self.undo_stack.pop() self.sheet_modified(event_data, purge_redo=False) try_binding(self.extra_end_ctrl_z_func, event_data, "end_undo") self.PAR.emit_event("<>", event_data) return event_data def redo(self, event: object = None) -> None | EventDataDict: if not self.redo_stack: return modification = decompress_load(self.redo_stack[-1]["data"]) if not try_binding(self.extra_begin_ctrl_z_func, modification, "begin_redo"): return event_data = self.undo_modification_invert_event(modification, name="redo") self.undo_stack.append(pickled_event_dict(event_data)) self.redo_stack.pop() self.sheet_modified(event_data, purge_redo=False) try_binding(self.extra_end_ctrl_z_func, event_data, "end_redo") self.PAR.emit_event("<>", event_data) return event_data def sheet_modified(self, event_data: EventDataDict, purge_redo: bool = True) -> None: self.PAR.emit_event("<>", event_data) if purge_redo: self.purge_redo_stack() def edit_cells_using_modification(self, modification: dict, event_data: dict) -> EventDataDict: for datarn, v in modification["cells"]["index"].items(): self._row_index[datarn] = v for datacn, v in modification["cells"]["header"].items(): self._headers[datacn] = v for (datarn, datacn), v in modification["cells"]["table"].items(): self.set_cell_data(datarn, datacn, v) return event_data def save_cells_using_modification(self, modification: EventDataDict, event_data: EventDataDict) -> EventDataDict: for datarn, v in modification["cells"]["index"].items(): event_data["cells"]["index"][datarn] = self.RI.get_cell_data(datarn) for datacn, v in modification["cells"]["header"].items(): event_data["cells"]["header"][datacn] = self.CH.get_cell_data(datacn) for (datarn, datacn), v in modification["cells"]["table"].items(): event_data["cells"]["table"][(datarn, datacn)] = self.get_cell_data(datarn, datacn) return event_data def restore_options_named_spans(self, modification: EventDataDict) -> None: if not isinstance(modification["options"], dict): modification["options"] = unpickle_obj(modification["options"]) if "cell_options" in modification["options"]: self.cell_options = modification["options"]["cell_options"] if "column_options" in modification["options"]: self.col_options = modification["options"]["column_options"] if "row_options" in modification["options"]: self.row_options = modification["options"]["row_options"] if "CH_cell_options" in modification["options"]: self.CH.cell_options = modification["options"]["CH_cell_options"] if "RI_cell_options" in modification["options"]: self.RI.cell_options = modification["options"]["RI_cell_options"] if "tagged_cells" in modification["options"]: self.tagged_cells = modification["options"]["tagged_cells"] if "tagged_rows" in modification["options"]: self.tagged_rows = modification["options"]["tagged_rows"] if "tagged_columns" in modification["options"]: self.tagged_columns = modification["options"]["tagged_columns"] self.named_spans = { k: mod_span_widget(unpickle_obj(v), self.PAR) for k, v in modification["named_spans"].items() } def undo_modification_invert_event(self, modification: EventDataDict, name: str = "undo") -> EventDataDict: self.deselect("all", redraw=False) event_data = event_dict( name=modification["eventname"], sheet=self.PAR.name, widget=self, ) event_data["selection_boxes"] = modification["selection_boxes"] event_data["selected"] = modification["selected"] saved_cells = False if modification["added"]["rows"] or modification["added"]["columns"]: event_data = self.save_cells_using_modification(modification, event_data) saved_cells = True if modification["moved"]["columns"]: totalcols = max(self.equalize_data_row_lengths(), max(modification["moved"]["columns"]["data"].values())) data_new_idxs, disp_new_idxs, event_data = self.move_columns_adjust_options_dict( data_new_idxs=dict( zip( modification["moved"]["columns"]["data"].values(), modification["moved"]["columns"]["data"], ) ), data_old_idxs=modification["moved"]["columns"]["data"], totalcols=totalcols, disp_new_idxs=dict( zip( modification["moved"]["columns"]["displayed"].values(), modification["moved"]["columns"]["displayed"], ) ), event_data=event_data, ) event_data["moved"]["columns"] = { "data": data_new_idxs, "displayed": disp_new_idxs, } self.restore_options_named_spans(modification) if modification["moved"]["rows"]: totalrows = max(self.total_data_rows(), max(modification["moved"]["rows"]["data"].values())) data_new_idxs, disp_new_idxs, event_data = self.move_rows_adjust_options_dict( data_new_idxs=dict( zip( modification["moved"]["rows"]["data"].values(), modification["moved"]["rows"]["data"], ) ), data_old_idxs=modification["moved"]["rows"]["data"], totalrows=totalrows, disp_new_idxs=dict( zip( modification["moved"]["rows"]["displayed"].values(), modification["moved"]["rows"]["displayed"], ) ), event_data=event_data, ) event_data["moved"]["rows"] = { "data": data_new_idxs, "displayed": disp_new_idxs, } self.restore_options_named_spans(modification) if modification["added"]["rows"]: self.deselect("all", run_binding=False, redraw=False) event_data = self.delete_rows_data( rows=tuple(reversed(modification["added"]["rows"]["table"])), event_data=event_data, ) event_data = self.delete_rows_displayed( rows=tuple(reversed(modification["added"]["rows"]["row_heights"])), event_data=event_data, ) self.displayed_rows = modification["added"]["rows"]["displayed_rows"] if modification["added"]["columns"]: self.deselect("all", run_binding=False, redraw=False) event_data = self.delete_columns_data( cols=tuple(reversed(modification["added"]["columns"]["table"])), event_data=event_data, ) event_data = self.delete_columns_displayed( cols=tuple(reversed(modification["added"]["columns"]["column_widths"])), event_data=event_data, ) self.displayed_columns = modification["added"]["columns"]["displayed_columns"] if modification["deleted"]["rows"] or modification["deleted"]["row_heights"]: self.add_rows( rows=modification["deleted"]["rows"], index=modification["deleted"]["index"], row_heights=modification["deleted"]["row_heights"], event_data=event_data, displayed_rows=modification["deleted"]["displayed_rows"], create_ops=False, create_selections=not modification["eventname"].startswith("edit"), add_col_positions=False, ) self.restore_options_named_spans(modification) if modification["deleted"]["columns"] or modification["deleted"]["column_widths"]: self.add_columns( columns=modification["deleted"]["columns"], header=modification["deleted"]["header"], column_widths=modification["deleted"]["column_widths"], event_data=event_data, displayed_columns=modification["deleted"]["displayed_columns"], create_ops=False, create_selections=not modification["eventname"].startswith("edit"), add_row_positions=False, ) self.restore_options_named_spans(modification) if modification["eventname"].startswith(("edit", "move")): if not saved_cells: event_data = self.save_cells_using_modification(modification, event_data) event_data = self.edit_cells_using_modification(modification, event_data) elif modification["eventname"].startswith("add"): event_data["eventname"] = modification["eventname"].replace("add", "delete") elif modification["eventname"].startswith("delete"): event_data["eventname"] = modification["eventname"].replace("delete", "add") if not modification["eventname"].startswith("move") and ( len(self.row_positions) > 1 or len(self.col_positions) > 1 ): self.deselect("all", redraw=False) self.reselect_from_get_boxes( modification["selection_boxes"], modification["selected"], ) if self.selected: self.see( r=self.selected.row, c=self.selected.column, keep_yscroll=False, keep_xscroll=False, bottom_right_corner=False, check_cell_visibility=True, redraw=False, ) self.refresh() return event_data def see( self, r: int | None = None, c: int | None = None, keep_yscroll: bool = False, keep_xscroll: bool = False, bottom_right_corner: bool = False, check_cell_visibility: bool = True, redraw: bool = True, r_pc: float = 0.0, c_pc: float = 0.0, ) -> bool: need_redraw = False yvis, xvis = False, False if check_cell_visibility: yvis, xvis = self.cell_completely_visible(r=r, c=c, separate_axes=True) if not yvis and len(self.row_positions) > 1: if bottom_right_corner: if r is not None and not keep_yscroll: winfo_height = self.winfo_height() if self.row_positions[r + 1] - self.row_positions[r] > winfo_height: y = self.row_positions[r] else: y = self.row_positions[r + 1] + 1 - winfo_height args = [ "moveto", y / (self.row_positions[-1] + self.PAR.ops.empty_vertical), ] if args[1] > 1: args[1] = args[1] - 1 self.set_yviews(*args, redraw=False) need_redraw = True else: if r is not None and not keep_yscroll: y = self.row_positions[r] + ((self.row_positions[r + 1] - self.row_positions[r]) * r_pc) args = [ "moveto", y / (self.row_positions[-1] + self.PAR.ops.empty_vertical), ] if args[1] > 1: args[1] = args[1] - 1 self.set_yviews(*args, redraw=False) need_redraw = True if not xvis and len(self.col_positions) > 1: if bottom_right_corner: if c is not None and not keep_xscroll: winfo_width = self.winfo_width() if self.col_positions[c + 1] - self.col_positions[c] > winfo_width: x = self.col_positions[c] else: x = self.col_positions[c + 1] + 1 - winfo_width args = [ "moveto", x / (self.col_positions[-1] + self.PAR.ops.empty_horizontal), ] self.set_xviews(*args, redraw=False) need_redraw = True else: if c is not None and not keep_xscroll: x = self.col_positions[c] + ((self.col_positions[c + 1] - self.col_positions[c]) * c_pc) args = [ "moveto", x / (self.col_positions[-1] + self.PAR.ops.empty_horizontal), ] self.set_xviews(*args, redraw=False) need_redraw = True if redraw and need_redraw: self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) return True return False def get_cell_coords(self, r: int | None = None, c: int | None = None) -> tuple[float, float, float, float]: return ( 0 if not c else self.col_positions[c] + 1, 0 if not r else self.row_positions[r] + 1, 0 if not c else self.col_positions[c + 1], 0 if not r else self.row_positions[r + 1], ) def cell_completely_visible( self, r: int | None = 0, c: int | None = 0, separate_axes: bool = False, ) -> bool: cx1, cy1, cx2, cy2 = self.get_canvas_visible_area() x1, y1, x2, y2 = self.get_cell_coords(r, c) x_vis = True y_vis = True if cx1 > x1 or cx2 < x2: x_vis = False if cy1 > y1 or cy2 < y2: y_vis = False if separate_axes: return y_vis, x_vis if not y_vis or not x_vis: return False return True def cell_visible(self, r: int = 0, c: int = 0) -> bool: cx1, cy1, cx2, cy2 = self.get_canvas_visible_area() x1, y1, x2, y2 = self.get_cell_coords(r, c) return x1 <= cx2 or y1 <= cy2 or x2 >= cx1 or y2 >= cy1 def select_all(self, redraw: bool = True, run_binding_func: bool = True) -> None: selected = self.selected self.deselect("all", redraw=False) if len(self.row_positions) > 1 and len(self.col_positions) > 1: item = self.create_selection_box( 0, 0, len(self.row_positions) - 1, len(self.col_positions) - 1, set_current=False, ) if selected: self.set_currently_selected(selected.row, selected.column, item=item) else: self.set_currently_selected(0, 0, item=item) if redraw: self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) if run_binding_func: if self.select_all_binding_func: self.select_all_binding_func( self.get_select_event(being_drawn_item=self.being_drawn_item), ) event_data = self.get_select_event(self.being_drawn_item) self.PAR.emit_event("<>", data=event_data) self.PAR.emit_event("<>", data=event_data) def select_cell( self, r: int, c: int, redraw: bool = False, run_binding_func: bool = True, ext: bool = False, ) -> int: boxes_to_hide = tuple(self.selection_boxes) fill_iid = self.create_selection_box(r, c, r + 1, c + 1, state="hidden", ext=ext) for iid in boxes_to_hide: self.hide_selection_box(iid) if redraw: self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) if run_binding_func: self.run_selection_binding("cells") return fill_iid def add_selection( self, r: int, c: int, redraw: bool = False, run_binding_func: bool = True, set_as_current: bool = False, ext: bool = False, ) -> int: fill_iid = self.create_selection_box(r, c, r + 1, c + 1, state="hidden", set_current=set_as_current, ext=ext) if redraw: self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) if run_binding_func: self.run_selection_binding("cells") return fill_iid def toggle_select_cell( self, row: int, column: int, add_selection: bool = True, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ext: bool = False, ) -> int | None: if add_selection: if self.cell_selected(row, column, inc_rows=True, inc_cols=True): fill_iid = self.deselect(r=row, c=column, redraw=redraw) else: fill_iid = self.add_selection( r=row, c=column, redraw=redraw, run_binding_func=run_binding_func, set_as_current=set_as_current, ext=ext, ) else: if self.cell_selected(row, column, inc_rows=True, inc_cols=True): fill_iid = self.deselect(r=row, c=column, redraw=redraw) else: fill_iid = self.select_cell(row, column, redraw=redraw, ext=ext) return fill_iid def get_select_event(self, being_drawn_item: None | int = None) -> EventDataDict: return event_dict( name="select", sheet=self.PAR.name, selected=self.selected, being_selected=self.coords_and_type(being_drawn_item), boxes=self.get_boxes(), ) def deselect( self, r: int | None | str = None, c: int | None = None, cell: tuple[int, int] | None = None, redraw: bool = True, run_binding: bool = True, ) -> None: if not self.selected: return if r == "all" or (r is None and c is None and cell is None): for item, box in self.get_selection_items(): self.hide_selection_box(item) elif r in ("allrows", "allcols"): for item, box in self.get_selection_items( columns=r == "allcols", rows=r == "allrows", cells=False, ): self.hide_selection_box(item) elif isinstance(r, int) and c is None and cell is None: for item, box in self.get_selection_items(columns=False, cells=False): r1, c1, r2, c2 = box.coords if r >= r1 and r < r2: resel = self.selected.fill_iid == item to_sel = self.selected.row self.hide_selection_box(item) if r2 - r1 != 1: if r == r1: self.create_selection_box( r1 + 1, 0, r2, len(self.col_positions) - 1, "rows", set_current=resel, ) elif r == r2 - 1: self.create_selection_box( r1, 0, r2 - 1, len(self.col_positions) - 1, "rows", set_current=resel, ) else: self.create_selection_box( r1, 0, r, len(self.col_positions) - 1, "rows", set_current=resel and to_sel >= r1 and to_sel < r, ) self.create_selection_box( r + 1, 0, r2, len(self.col_positions) - 1, "rows", set_current=resel and to_sel >= r + 1 and to_sel < r2, ) elif isinstance(c, int) and r is None and cell is None: for item, box in self.get_selection_items(rows=False, cells=False): r1, c1, r2, c2 = box.coords if c >= c1 and c < c2: resel = self.selected.fill_iid == item to_sel = self.selected.column self.hide_selection_box(item) if c2 - c1 != 1: if c == c1: self.create_selection_box( 0, c1 + 1, len(self.row_positions) - 1, c2, "columns", set_current=resel, ) elif c == c2 - 1: self.create_selection_box( 0, c1, len(self.row_positions) - 1, c2 - 1, "columns", set_current=resel, ) else: self.create_selection_box( 0, c1, len(self.row_positions) - 1, c, "columns", set_current=resel and to_sel >= c1 and to_sel < c, ) self.create_selection_box( 0, c + 1, len(self.row_positions) - 1, c2, "columns", set_current=resel and to_sel >= c + 1 and to_sel < c2, ) elif (isinstance(r, int) and isinstance(c, int) and cell is None) or cell is not None: if cell is not None: r, c = cell[0], cell[1] for item, box in self.get_selection_items(reverse=True): r1, c1, r2, c2 = box.coords if r >= r1 and c >= c1 and r < r2 and c < c2: self.hide_selection_box(item) if redraw: self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) sel_event = self.get_select_event(being_drawn_item=self.being_drawn_item) if run_binding: try_binding(self.deselection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) def deselect_any( self, rows: Iterator[int] | int | None = None, columns: Iterator[int] | int | None = None, redraw: bool = True, ) -> None: rows = int_x_iter(rows) columns = int_x_iter(columns) if is_iterable(rows) and is_iterable(columns): rows = tuple(consecutive_ranges(sorted(rows))) columns = tuple(consecutive_ranges(sorted(columns))) for item, box in self.get_selection_items(reverse=True): r1, c1, r2, c2 = box.coords hidden = False for rows_st, rows_end in rows: if hidden: break for cols_st, cols_end in columns: if ((rows_end >= r1 and rows_end <= r2) or (rows_st >= r1 and rows_st < r2)) and ( (cols_end >= c1 and cols_end <= c2) or (cols_st >= c1 and cols_st < c2) ): hidden = self.hide_selection_box(item) break elif is_iterable(rows): rows = tuple(consecutive_ranges(sorted(rows))) for item, box in self.get_selection_items(reverse=True): r1, c1, r2, c2 = box.coords for rows_st, rows_end in rows: if (rows_end >= r1 and rows_end <= r2) or (rows_st >= r1 and rows_st < r2): self.hide_selection_box(item) break elif is_iterable(columns): columns = tuple(consecutive_ranges(sorted(columns))) for item, box in self.get_selection_items(reverse=True): r1, c1, r2, c2 = box.coords for cols_st, cols_end in columns: if (cols_end >= c1 and cols_end <= c2) or (cols_st >= c1 and cols_st < c2): self.hide_selection_box(item) break else: self.deselect() if redraw: self.refresh() def page_UP(self, event: object = None) -> None: height = self.winfo_height() top = self.canvasy(0) scrollto_y = top - height if scrollto_y < 0: scrollto_y = 0 if self.PAR.ops.page_up_down_select_row: r = bisect_left(self.row_positions, scrollto_y) if self.selected and self.selected.row == r: r -= 1 if r < 0: r = 0 if self.RI.row_selection_enabled and ( self.anything_selected(exclude_columns=True, exclude_cells=True) or not self.anything_selected() ): self.RI.select_row(r) self.see(r, 0, keep_xscroll=True, check_cell_visibility=False) elif (self.single_selection_enabled or self.toggle_selection_enabled) and self.anything_selected( exclude_columns=True, exclude_rows=True ): c = next(reversed(self.selection_boxes.values())).coords.from_c self.see(r, c, keep_xscroll=True, check_cell_visibility=False) self.select_cell(r, c) else: args = ("moveto", scrollto_y / (self.row_positions[-1] + 100)) self.yview(*args) self.RI.yview(*args) self.main_table_redraw_grid_and_text(redraw_row_index=True) def page_DOWN(self, event: object = None) -> None: height = self.winfo_height() top = self.canvasy(0) scrollto = top + height if self.PAR.ops.page_up_down_select_row and self.RI.row_selection_enabled: r = bisect_left(self.row_positions, scrollto) - 1 if self.selected and self.selected.row == r: r += 1 if r > len(self.row_positions) - 2: r = len(self.row_positions) - 2 if self.RI.row_selection_enabled and ( self.anything_selected(exclude_columns=True, exclude_cells=True) or not self.anything_selected() ): self.RI.select_row(r) self.see(r, 0, keep_xscroll=True, check_cell_visibility=False) elif (self.single_selection_enabled or self.toggle_selection_enabled) and self.anything_selected( exclude_columns=True, exclude_rows=True ): c = next(reversed(self.selection_boxes.values())).coords.from_c self.see(r, c, keep_xscroll=True, check_cell_visibility=False) self.select_cell(r, c) else: end = self.row_positions[-1] if scrollto > end + 100: scrollto = end args = ("moveto", scrollto / (end + 100)) self.yview(*args) self.RI.yview(*args) self.main_table_redraw_grid_and_text(redraw_row_index=True) def arrowkey_UP(self, event: object = None) -> None: if not self.selected: return if self.selected.type_ == "rows": r = self.selected.row if r and self.RI.row_selection_enabled: if self.cell_completely_visible(r=r - 1, c=0): self.RI.select_row(r - 1, redraw=True) else: self.RI.select_row(r - 1) self.see(r - 1, 0, keep_xscroll=True, check_cell_visibility=False) elif self.selected.type_ in ("cells", "columns"): r = self.selected.row c = self.selected.column if not r and self.CH.col_selection_enabled: if not self.cell_completely_visible(r=r, c=0): self.see(r, c, check_cell_visibility=False) elif r and (self.single_selection_enabled or self.toggle_selection_enabled): if self.cell_completely_visible(r=r - 1, c=c): self.select_cell(r - 1, c, redraw=True) else: self.select_cell(r - 1, c) self.see(r - 1, c, keep_xscroll=True, check_cell_visibility=False) def arrowkey_DOWN(self, event: object = None) -> None: if not self.selected: return if self.selected.type_ == "rows": r = self.selected.row if r < len(self.row_positions) - 2 and self.RI.row_selection_enabled: if self.cell_completely_visible(r=min(r + 2, len(self.row_positions) - 2), c=0): self.RI.select_row(r + 1, redraw=True) else: self.RI.select_row(r + 1) if ( r + 2 < len(self.row_positions) - 2 and (self.row_positions[r + 3] - self.row_positions[r + 2]) + (self.row_positions[r + 2] - self.row_positions[r + 1]) + 5 < self.winfo_height() ): self.see( r + 2, 0, keep_xscroll=True, bottom_right_corner=True, check_cell_visibility=False, ) elif not self.cell_completely_visible(r=r + 1, c=0): self.see( r + 1, 0, keep_xscroll=True, bottom_right_corner=False if self.PAR.ops.arrow_key_down_right_scroll_page else True, check_cell_visibility=False, ) elif self.selected.type_ == "columns": c = self.selected.column if self.single_selection_enabled or self.toggle_selection_enabled: if self.selected.row == len(self.row_positions) - 2: r = self.selected.row else: r = self.selected.row + 1 if self.cell_completely_visible(r=r, c=c): self.select_cell(r, c, redraw=True) else: self.select_cell(r, c) self.see( r, c, check_cell_visibility=False, ) else: r = self.selected.row c = self.selected.column if r < len(self.row_positions) - 2 and (self.single_selection_enabled or self.toggle_selection_enabled): if self.cell_completely_visible(r=min(r + 2, len(self.row_positions) - 2), c=c): self.select_cell(r + 1, c, redraw=True) else: self.select_cell(r + 1, c) if ( r + 2 < len(self.row_positions) - 2 and (self.row_positions[r + 3] - self.row_positions[r + 2]) + (self.row_positions[r + 2] - self.row_positions[r + 1]) + 5 < self.winfo_height() ): self.see( r + 2, c, keep_xscroll=True, bottom_right_corner=True, check_cell_visibility=False, ) elif not self.cell_completely_visible(r=r + 1, c=c): self.see( r + 1, c, keep_xscroll=True, bottom_right_corner=False if self.PAR.ops.arrow_key_down_right_scroll_page else True, check_cell_visibility=False, ) def arrowkey_LEFT(self, event: object = None) -> None: if not self.selected: return if self.selected.type_ == "columns": c = self.selected.column if c and self.CH.col_selection_enabled: if self.cell_completely_visible(r=0, c=c - 1): self.CH.select_col(c - 1, redraw=True) else: self.CH.select_col(c - 1) self.see( 0, c - 1, keep_yscroll=True, bottom_right_corner=True, check_cell_visibility=False, ) elif self.selected.type_ == "rows" and self.selected.column: self.select_cell(self.selected.row, self.selected.column - 1) self.see(self.selected.row, self.selected.column, check_cell_visibility=True) elif self.selected.type_ == "cells": r = self.selected.row c = self.selected.column if not c and not self.cell_completely_visible(r=r, c=0): self.see(r, c, keep_yscroll=True, check_cell_visibility=False) elif c and (self.single_selection_enabled or self.toggle_selection_enabled): if self.cell_completely_visible(r=r, c=c - 1): self.select_cell(r, c - 1, redraw=True) else: self.select_cell(r, c - 1) self.see(r, c - 1, keep_yscroll=True, check_cell_visibility=False) def arrowkey_RIGHT(self, event: object = None) -> None: if not self.selected: return if self.selected.type_ == "rows": r = self.selected.row if self.single_selection_enabled or self.toggle_selection_enabled: if self.selected.column == len(self.col_positions) - 2: c = self.selected.column else: c = self.selected.column + 1 if self.cell_completely_visible(r=r, c=c): self.select_cell(r, c, redraw=True) else: self.select_cell(r, c) self.see( r, c, check_cell_visibility=False, ) elif self.selected.type_ == "columns": c = self.selected.column if c < len(self.col_positions) - 2 and self.CH.col_selection_enabled: if self.cell_completely_visible(r=0, c=c + 1): self.CH.select_col(c + 1, redraw=True) else: self.CH.select_col(c + 1) self.see( 0, c + 1, keep_yscroll=True, bottom_right_corner=False if self.PAR.ops.arrow_key_down_right_scroll_page else True, check_cell_visibility=False, ) else: r = self.selected.row c = self.selected.column if c < len(self.col_positions) - 2 and (self.single_selection_enabled or self.toggle_selection_enabled): if self.cell_completely_visible(r=r, c=c + 1): self.select_cell(r, c + 1, redraw=True) else: self.select_cell(r, c + 1) self.see( r, c + 1, keep_yscroll=True, bottom_right_corner=False if self.PAR.ops.arrow_key_down_right_scroll_page else True, check_cell_visibility=False, ) def menu_add_command(self, menu: tk.Menu, **kwargs) -> None: if "label" not in kwargs: return try: index = menu.index(kwargs["label"]) menu.delete(index) except TclError: pass menu.add_command(**kwargs) def create_rc_menus(self) -> None: if not self.rc_popup_menu: self.rc_popup_menu = tk.Menu(self, tearoff=0, background=self.PAR.ops.popup_menu_bg) if not self.CH.ch_rc_popup_menu: self.CH.ch_rc_popup_menu = tk.Menu(self.CH, tearoff=0, background=self.PAR.ops.popup_menu_bg) if not self.RI.ri_rc_popup_menu: self.RI.ri_rc_popup_menu = tk.Menu(self.RI, tearoff=0, background=self.PAR.ops.popup_menu_bg) if not self.empty_rc_popup_menu: self.empty_rc_popup_menu = tk.Menu(self, tearoff=0, background=self.PAR.ops.popup_menu_bg) for menu in ( self.rc_popup_menu, self.CH.ch_rc_popup_menu, self.RI.ri_rc_popup_menu, self.empty_rc_popup_menu, ): menu.delete(0, "end") mnkwgs = { "font": self.PAR.ops.table_font, "foreground": self.PAR.ops.popup_menu_fg, "background": self.PAR.ops.popup_menu_bg, "activebackground": self.PAR.ops.popup_menu_highlight_bg, "activeforeground": self.PAR.ops.popup_menu_highlight_fg, } if self.rc_popup_menus_enabled and self.CH.edit_cell_enabled: self.menu_add_command( self.CH.ch_rc_popup_menu, label=self.PAR.ops.edit_header_label, command=lambda: self.CH.open_cell(event="rc"), **mnkwgs, ) if self.rc_popup_menus_enabled and self.RI.edit_cell_enabled: self.menu_add_command( self.RI.ri_rc_popup_menu, label=self.PAR.ops.edit_index_label, command=lambda: self.RI.open_cell(event="rc"), **mnkwgs, ) if self.rc_popup_menus_enabled and self.edit_cell_enabled: self.menu_add_command( self.rc_popup_menu, label=self.PAR.ops.edit_cell_label, command=lambda: self.open_cell(event="rc"), **mnkwgs, ) if self.cut_enabled: self.menu_add_command( self.rc_popup_menu, label=self.PAR.ops.cut_label, accelerator=self.PAR.ops.cut_accelerator, command=self.ctrl_x, **mnkwgs, ) self.menu_add_command( self.CH.ch_rc_popup_menu, label=self.PAR.ops.cut_contents_label, accelerator=self.PAR.ops.cut_contents_accelerator, command=self.ctrl_x, **mnkwgs, ) self.menu_add_command( self.RI.ri_rc_popup_menu, label=self.PAR.ops.cut_contents_label, accelerator=self.PAR.ops.cut_contents_accelerator, command=self.ctrl_x, **mnkwgs, ) if self.copy_enabled: self.menu_add_command( self.rc_popup_menu, label=self.PAR.ops.copy_label, accelerator=self.PAR.ops.copy_accelerator, command=self.ctrl_c, **mnkwgs, ) self.menu_add_command( self.CH.ch_rc_popup_menu, label=self.PAR.ops.copy_contents_label, accelerator=self.PAR.ops.copy_contents_accelerator, command=self.ctrl_c, **mnkwgs, ) self.menu_add_command( self.RI.ri_rc_popup_menu, label=self.PAR.ops.copy_contents_label, accelerator=self.PAR.ops.copy_contents_accelerator, command=self.ctrl_c, **mnkwgs, ) if self.paste_enabled: self.menu_add_command( self.rc_popup_menu, label=self.PAR.ops.paste_label, accelerator=self.PAR.ops.paste_accelerator, command=self.ctrl_v, **mnkwgs, ) self.menu_add_command( self.CH.ch_rc_popup_menu, label=self.PAR.ops.paste_label, accelerator=self.PAR.ops.paste_accelerator, command=self.ctrl_v, **mnkwgs, ) self.menu_add_command( self.RI.ri_rc_popup_menu, label=self.PAR.ops.paste_label, accelerator=self.PAR.ops.paste_accelerator, command=self.ctrl_v, **mnkwgs, ) if self.PAR.ops.paste_can_expand_x or self.PAR.ops.paste_can_expand_y: self.menu_add_command( self.empty_rc_popup_menu, label=self.PAR.ops.paste_label, accelerator=self.PAR.ops.paste_accelerator, command=self.ctrl_v, **mnkwgs, ) if self.delete_key_enabled: self.menu_add_command( self.rc_popup_menu, label=self.PAR.ops.delete_label, accelerator=self.PAR.ops.delete_accelerator, command=self.delete_key, **mnkwgs, ) self.menu_add_command( self.CH.ch_rc_popup_menu, label=self.PAR.ops.clear_contents_label, accelerator=self.PAR.ops.clear_contents_accelerator, command=self.delete_key, **mnkwgs, ) self.menu_add_command( self.RI.ri_rc_popup_menu, label=self.PAR.ops.clear_contents_label, accelerator=self.PAR.ops.clear_contents_accelerator, command=self.delete_key, **mnkwgs, ) if self.rc_delete_column_enabled: self.menu_add_command( self.CH.ch_rc_popup_menu, label=self.PAR.ops.delete_columns_label, command=self.rc_delete_columns, **mnkwgs, ) if self.rc_insert_column_enabled: self.menu_add_command( self.CH.ch_rc_popup_menu, label=self.PAR.ops.insert_columns_left_label, command=lambda: self.rc_add_columns("left"), **mnkwgs, ) self.menu_add_command( self.CH.ch_rc_popup_menu, label=self.PAR.ops.insert_columns_right_label, command=lambda: self.rc_add_columns("right"), **mnkwgs, ) self.menu_add_command( self.empty_rc_popup_menu, label=self.PAR.ops.insert_column_label, command=lambda: self.rc_add_columns("left"), **mnkwgs, ) if self.rc_delete_row_enabled: self.menu_add_command( self.RI.ri_rc_popup_menu, label=self.PAR.ops.delete_rows_label, command=self.rc_delete_rows, **mnkwgs, ) if self.rc_insert_row_enabled: self.menu_add_command( self.RI.ri_rc_popup_menu, label=self.PAR.ops.insert_rows_above_label, command=lambda: self.rc_add_rows("above"), **mnkwgs, ) self.menu_add_command( self.RI.ri_rc_popup_menu, label=self.PAR.ops.insert_rows_below_label, command=lambda: self.rc_add_rows("below"), **mnkwgs, ) self.menu_add_command( self.empty_rc_popup_menu, label=self.PAR.ops.insert_row_label, command=lambda: self.rc_add_rows("below"), **mnkwgs, ) for label, func in self.extra_table_rc_menu_funcs.items(): self.menu_add_command( self.rc_popup_menu, label=label, command=func, **mnkwgs, ) for label, func in self.extra_index_rc_menu_funcs.items(): self.menu_add_command( self.RI.ri_rc_popup_menu, label=label, command=func, **mnkwgs, ) for label, func in self.extra_header_rc_menu_funcs.items(): self.menu_add_command( self.CH.ch_rc_popup_menu, label=label, command=func, **mnkwgs, ) for label, func in self.extra_empty_space_rc_menu_funcs.items(): self.menu_add_command( self.empty_rc_popup_menu, label=label, command=func, **mnkwgs, ) def enable_bindings(self, bindings): if not bindings: self._enable_binding("all") elif isinstance(bindings, (list, tuple)): for binding in bindings: if isinstance(binding, (list, tuple)): for bind in binding: self._enable_binding(bind.lower()) elif isinstance(binding, str): self._enable_binding(binding.lower()) elif isinstance(bindings, str): self._enable_binding(bindings.lower()) self.create_rc_menus() def disable_bindings(self, bindings): if not bindings: self._disable_binding("all") elif isinstance(bindings, (list, tuple)): for binding in bindings: if isinstance(binding, (list, tuple)): for bind in binding: self._disable_binding(bind.lower()) elif isinstance(binding, str): self._disable_binding(binding.lower()) elif isinstance(bindings, str): self._disable_binding(bindings) self.create_rc_menus() def _enable_binding(self, binding): if binding == "enable_all": binding = "all" if binding in ("all", "single", "single_selection_mode", "single_select"): self.single_selection_enabled = True self.toggle_selection_enabled = False elif binding in ("toggle", "toggle_selection_mode", "toggle_select"): self.toggle_selection_enabled = True self.single_selection_enabled = False if binding in ("all", "drag_select"): self.drag_selection_enabled = True if binding in ("all", "column_width_resize"): self.CH.width_resizing_enabled = True if binding in ("all", "column_select"): self.CH.col_selection_enabled = True if binding in ("all", "column_height_resize"): self.CH.height_resizing_enabled = True self.TL.rh_state() if binding in ("all", "column_drag_and_drop", "move_columns"): self.CH.drag_and_drop_enabled = True if binding in ("all", "double_click_column_resize"): self.CH.double_click_resizing_enabled = True if binding in ("all", "row_height_resize"): self.RI.height_resizing_enabled = True if binding in ("all", "double_click_row_resize"): self.RI.double_click_resizing_enabled = True if binding in ("all", "row_width_resize"): self.RI.width_resizing_enabled = True self.TL.rw_state() if binding in ("all", "row_select"): self.RI.row_selection_enabled = True if binding in ("all", "row_drag_and_drop", "move_rows"): self.RI.drag_and_drop_enabled = True if binding in ("all", "select_all"): self.select_all_enabled = True self.TL.sa_state() self._tksheet_bind("select_all_bindings", self.select_all) if binding in ("all", "arrowkeys", "tab"): self.tab_enabled = True self._tksheet_bind("tab_bindings", self.tab_key) if binding in ("all", "arrowkeys", "up"): self.up_enabled = True self._tksheet_bind("up_bindings", self.arrowkey_UP) if binding in ("all", "arrowkeys", "right"): self.right_enabled = True self._tksheet_bind("right_bindings", self.arrowkey_RIGHT) if binding in ("all", "arrowkeys", "down"): self.down_enabled = True self._tksheet_bind("down_bindings", self.arrowkey_DOWN) if binding in ("all", "arrowkeys", "left"): self.left_enabled = True self._tksheet_bind("left_bindings", self.arrowkey_LEFT) if binding in ("all", "arrowkeys", "prior"): self.prior_enabled = True self._tksheet_bind("prior_bindings", self.page_UP) if binding in ("all", "arrowkeys", "next"): self.next_enabled = True self._tksheet_bind("next_bindings", self.page_DOWN) if binding in ("all", "copy", "edit_bindings", "edit"): self.copy_enabled = True self._tksheet_bind("copy_bindings", self.ctrl_c) if binding in ("all", "cut", "edit_bindings", "edit"): self.cut_enabled = True self._tksheet_bind("cut_bindings", self.ctrl_x) if binding in ("all", "paste", "edit_bindings", "edit"): self.paste_enabled = True self._tksheet_bind("paste_bindings", self.ctrl_v) if binding in ("all", "delete", "edit_bindings", "edit"): self.delete_key_enabled = True self._tksheet_bind("delete_bindings", self.delete_key) if binding in ("all", "undo", "redo", "edit_bindings", "edit"): self.undo_enabled = True self._tksheet_bind("undo_bindings", self.undo) self._tksheet_bind("redo_bindings", self.redo) if binding in bind_del_columns: self.rc_delete_column_enabled = True self.rc_popup_menus_enabled = True self.rc_select_enabled = True if binding in bind_del_rows: self.rc_delete_row_enabled = True self.rc_popup_menus_enabled = True self.rc_select_enabled = True if binding in bind_add_columns: self.rc_insert_column_enabled = True self.rc_popup_menus_enabled = True self.rc_select_enabled = True if binding in bind_add_rows: self.rc_insert_row_enabled = True self.rc_popup_menus_enabled = True self.rc_select_enabled = True if binding in ("all", "right_click_popup_menu", "rc_popup_menu"): self.rc_popup_menus_enabled = True self.rc_select_enabled = True if binding in ("all", "right_click_select", "rc_select"): self.rc_select_enabled = True if binding in ("all", "edit_cell", "edit_bindings", "edit"): self.edit_cell_enabled = True for w in (self, self.RI, self.CH): w.bind("", self.open_cell) if binding in ("edit_header"): self.CH.edit_cell_enabled = True if binding in ("edit_index"): self.RI.edit_cell_enabled = True # has to be specifically enabled if binding in ("ctrl_click_select", "ctrl_select"): self.ctrl_select_enabled = True def _tksheet_bind(self, bindings_key: str, func: Callable) -> None: for widget in (self, self.RI, self.CH, self.TL): for binding in self.PAR.ops[bindings_key]: widget.bind(binding, func) def _disable_binding(self, binding): if binding == "disable_all": binding = "all" if binding in ("all", "single", "single_selection_mode", "single_select"): self.single_selection_enabled = False self.toggle_selection_enabled = False elif binding in ("toggle", "toggle_selection_mode", "toggle_select"): self.toggle_selection_enabled = False self.single_selection_enabled = False if binding in ("all", "drag_select"): self.drag_selection_enabled = False if binding in ("all", "column_width_resize"): self.CH.width_resizing_enabled = False if binding in ("all", "column_select"): self.CH.col_selection_enabled = False if binding in ("all", "column_height_resize"): self.CH.height_resizing_enabled = False self.TL.rh_state("hidden") if binding in ("all", "column_drag_and_drop", "move_columns"): self.CH.drag_and_drop_enabled = False if binding in ("all", "double_click_column_resize"): self.CH.double_click_resizing_enabled = False if binding in ("all", "row_height_resize"): self.RI.height_resizing_enabled = False if binding in ("all", "double_click_row_resize"): self.RI.double_click_resizing_enabled = False if binding in ("all", "row_width_resize"): self.RI.width_resizing_enabled = False self.TL.rw_state("hidden") if binding in ("all", "row_select"): self.RI.row_selection_enabled = False if binding in ("all", "row_drag_and_drop", "move_rows"): self.RI.drag_and_drop_enabled = False if binding in bind_del_columns: self.rc_delete_column_enabled = False if binding in bind_del_rows: self.rc_delete_row_enabled = False if binding in bind_add_columns: self.rc_insert_column_enabled = False if binding in bind_add_rows: self.rc_insert_row_enabled = False if binding in ("all", "right_click_popup_menu", "rc_popup_menu"): self.rc_popup_menus_enabled = False if binding in ("all", "right_click_select", "rc_select"): self.rc_select_enabled = False if binding in ("all", "edit_cell", "edit_bindings", "edit"): self.edit_cell_enabled = False for w in (self, self.RI, self.CH): w.unbind("") if binding in ("all", "edit_header", "edit_bindings", "edit"): self.CH.edit_cell_enabled = False if binding in ("all", "edit_index", "edit_bindings", "edit"): self.RI.edit_cell_enabled = False if binding in ("all", "ctrl_click_select", "ctrl_select"): self.ctrl_select_enabled = False if binding in ("all", "select_all"): self.select_all_enabled = False self.TL.sa_state("hidden") self._tksheet_unbind("select_all_bindings") if binding in ("all", "copy", "edit_bindings", "edit"): self.copy_enabled = False self._tksheet_unbind("copy_bindings") if binding in ("all", "cut", "edit_bindings", "edit"): self.cut_enabled = False self._tksheet_unbind("cut_bindings") if binding in ("all", "paste", "edit_bindings", "edit"): self.paste_enabled = False self._tksheet_unbind("paste_bindings") if binding in ("all", "delete", "edit_bindings", "edit"): self.delete_key_enabled = False self._tksheet_unbind("delete_bindings") if binding in ("all", "arrowkeys", "tab"): self.tab_enabled = False self._tksheet_unbind("tab_bindings") if binding in ("all", "arrowkeys", "up"): self.up_enabled = False self._tksheet_unbind("up_bindings") if binding in ("all", "arrowkeys", "right"): self.right_enabled = False self._tksheet_unbind("right_bindings") if binding in ("all", "arrowkeys", "down"): self.down_enabled = False self._tksheet_unbind("down_bindings") if binding in ("all", "arrowkeys", "left"): self.left_enabled = False self._tksheet_unbind("left_bindings") if binding in ("all", "arrowkeys", "prior"): self.prior_enabled = False self._tksheet_unbind("prior_bindings") if binding in ("all", "arrowkeys", "next"): self.next_enabled = False self._tksheet_unbind("next_bindings") if binding in ("all", "undo", "redo", "edit_bindings", "edit"): self.undo_enabled = False self._tksheet_unbind("undo_bindings", "redo_bindings") def _tksheet_unbind(self, *keys) -> None: for widget in (self, self.RI, self.CH, self.TL): for bindings_key in keys: for binding in self.PAR.ops[bindings_key]: widget.unbind(binding) def reset_mouse_motion_creations(self) -> None: if self.current_cursor != "": self.config(cursor="") self.RI.config(cursor="") self.CH.config(cursor="") self.current_cursor = "" self.RI.rsz_w = None self.RI.rsz_h = None self.CH.rsz_w = None self.CH.rsz_h = None def mouse_motion(self, event: object): self.reset_mouse_motion_creations() try_binding(self.extra_motion_func, event) def not_currently_resizing(self) -> bool: return all(v is None for v in (self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w)) def rc(self, event=None): self.mouseclick_outside_editor_or_dropdown_all_canvases() self.focus_set() popup_menu = None if (self.single_selection_enabled or self.toggle_selection_enabled) and self.not_currently_resizing(): r = self.identify_row(y=event.y) c = self.identify_col(x=event.x) if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: if self.col_selected(c): if self.rc_popup_menus_enabled: popup_menu = self.CH.ch_rc_popup_menu elif self.row_selected(r): if self.rc_popup_menus_enabled: popup_menu = self.RI.ri_rc_popup_menu elif self.cell_selected(r, c): if self.rc_popup_menus_enabled: popup_menu = self.rc_popup_menu else: if self.rc_select_enabled: if self.single_selection_enabled: self.select_cell(r, c, redraw=True) elif self.toggle_selection_enabled: self.toggle_select_cell(r, c, redraw=True) if self.rc_popup_menus_enabled: popup_menu = self.rc_popup_menu else: self.deselect("all") if self.rc_popup_menus_enabled: popup_menu = self.empty_rc_popup_menu try_binding(self.extra_rc_func, event) if popup_menu: popup_menu.tk_popup(event.x_root, event.y_root) def b1_press(self, event=None): self.closed_dropdown = self.mouseclick_outside_editor_or_dropdown_all_canvases() self.focus_set() if ( self.identify_col(x=event.x, allow_end=False) is None or self.identify_row(y=event.y, allow_end=False) is None ): self.deselect("all") r = self.identify_row(y=event.y) c = self.identify_col(x=event.x) if self.single_selection_enabled and self.not_currently_resizing(): if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: self.being_drawn_item = True self.being_drawn_item = self.select_cell(r, c, redraw=True) elif self.toggle_selection_enabled and self.not_currently_resizing(): r = self.identify_row(y=event.y) c = self.identify_col(x=event.x) if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: self.toggle_select_cell(r, c, redraw=True) self.b1_pressed_loc = (r, c) try_binding(self.extra_b1_press_func, event) def create_resize_line(self, x1, y1, x2, y2, width, fill, tag): if self.hidd_resize_lines: t, sh = self.hidd_resize_lines.popitem() self.coords(t, x1, y1, x2, y2) if sh: self.itemconfig(t, width=width, fill=fill, tag=tag) else: self.itemconfig(t, width=width, fill=fill, tag=tag, state="normal") self.lift(t) else: t = self.create_line(x1, y1, x2, y2, width=width, fill=fill, tag=tag) self.disp_resize_lines[t] = True def delete_resize_lines(self): self.hidd_resize_lines.update(self.disp_resize_lines) self.disp_resize_lines = {} for t, sh in self.hidd_resize_lines.items(): if sh: self.itemconfig(t, state="hidden") self.hidd_resize_lines[t] = False def ctrl_b1_press(self, event=None): self.mouseclick_outside_editor_or_dropdown_all_canvases() self.focus_set() if self.ctrl_select_enabled and self.not_currently_resizing(): self.b1_pressed_loc = None rowsel = int(self.identify_row(y=event.y)) colsel = int(self.identify_col(x=event.x)) if rowsel < len(self.row_positions) - 1 and colsel < len(self.col_positions) - 1: self.being_drawn_item = True self.being_drawn_item = self.add_selection(rowsel, colsel, set_as_current=True, run_binding_func=False) sel_event = self.get_select_event(being_drawn_item=self.being_drawn_item) if self.ctrl_selection_binding_func: self.ctrl_selection_binding_func(sel_event) self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True, redraw_table=True) self.PAR.emit_event("<>", data=sel_event) elif not self.ctrl_select_enabled: self.b1_press(event) def ctrl_shift_b1_press(self, event=None): self.mouseclick_outside_editor_or_dropdown_all_canvases() self.focus_set() if self.ctrl_select_enabled and self.drag_selection_enabled and self.not_currently_resizing(): self.b1_pressed_loc = None rowsel = int(self.identify_row(y=event.y)) colsel = int(self.identify_col(x=event.x)) if rowsel < len(self.row_positions) - 1 and colsel < len(self.col_positions) - 1: if self.selected and self.selected.type_ == "cells": self.being_drawn_item = self.recreate_selection_box( *self.get_shift_select_box(self.selected.row, rowsel, self.selected.column, colsel), fill_iid=self.selected.fill_iid, ) else: self.being_drawn_item = self.add_selection( rowsel, colsel, set_as_current=True, run_binding_func=False, ) self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True, redraw_table=True) sel_event = self.get_select_event(being_drawn_item=self.being_drawn_item) if self.shift_selection_binding_func: self.shift_selection_binding_func(sel_event) self.PAR.emit_event("<>", data=sel_event) elif not self.ctrl_select_enabled: self.shift_b1_press(event) def shift_b1_press(self, event=None): self.mouseclick_outside_editor_or_dropdown_all_canvases() self.focus_set() if self.drag_selection_enabled and self.not_currently_resizing(): self.b1_pressed_loc = None rowsel = int(self.identify_row(y=event.y)) colsel = int(self.identify_col(x=event.x)) if rowsel < len(self.row_positions) - 1 and colsel < len(self.col_positions) - 1: if self.selected and self.selected.type_ == "cells": r_to_sel, c_to_sel = self.selected.row, self.selected.column self.deselect("all", redraw=False) self.being_drawn_item = self.create_selection_box( *self.get_shift_select_box(r_to_sel, rowsel, c_to_sel, colsel), ) self.set_currently_selected(r_to_sel, c_to_sel, self.being_drawn_item) else: self.being_drawn_item = self.select_cell( rowsel, colsel, redraw=False, run_binding_func=False, ) self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True, redraw_table=True) sel_event = self.get_select_event(being_drawn_item=self.being_drawn_item) if self.shift_selection_binding_func: self.shift_selection_binding_func(sel_event) self.PAR.emit_event("<>", data=sel_event) def get_shift_select_box(self, min_r: int, rowsel: int, min_c: int, colsel: int): if rowsel >= min_r and colsel >= min_c: return min_r, min_c, rowsel + 1, colsel + 1 elif rowsel >= min_r and min_c >= colsel: return min_r, colsel, rowsel + 1, min_c + 1 elif min_r >= rowsel and colsel >= min_c: return rowsel, min_c, min_r + 1, colsel + 1 elif min_r >= rowsel and min_c >= colsel: return rowsel, colsel, min_r + 1, min_c + 1 def get_b1_motion_box(self, start_row: int, start_col: int, end_row: int, end_col: int): if end_row >= start_row and end_col >= start_col and (end_row - start_row or end_col - start_col): return start_row, start_col, end_row + 1, end_col + 1, "cells" elif end_row >= start_row and end_col < start_col and (end_row - start_row or start_col - end_col): return start_row, end_col, end_row + 1, start_col + 1, "cells" elif end_row < start_row and end_col >= start_col and (start_row - end_row or end_col - start_col): return end_row, start_col, start_row + 1, end_col + 1, "cells" elif end_row < start_row and end_col < start_col and (start_row - end_row or start_col - end_col): return end_row, end_col, start_row + 1, start_col + 1, "cells" else: return start_row, start_col, start_row + 1, start_col + 1, "cells" def b1_motion(self, event: object): if self.drag_selection_enabled and all( v is None for v in ( self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w, ) ): need_redraw = False end_row = self.identify_row(y=event.y) end_col = self.identify_col(x=event.x) if ( end_row < len(self.row_positions) - 1 and end_col < len(self.col_positions) - 1 and self.selected and self.selected.type_ == "cells" ): box = self.get_b1_motion_box( self.selected.row, self.selected.column, end_row, end_col, ) if ( box is not None and self.being_drawn_item is not None and self.coords_and_type(self.being_drawn_item) != box ): if box[2] - box[0] != 1 or box[3] - box[1] != 1: self.being_drawn_item = self.recreate_selection_box(*box[:-1], fill_iid=self.selected.fill_iid) else: self.being_drawn_item = self.select_cell( box[0], box[1], run_binding_func=False, ) need_redraw = True sel_event = self.get_select_event(being_drawn_item=self.being_drawn_item) if self.drag_selection_binding_func: self.drag_selection_binding_func(sel_event) self.PAR.emit_event("<>", data=sel_event) if self.scroll_if_event_offscreen(event): need_redraw = True if need_redraw: self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True, redraw_table=True) try_binding(self.extra_b1_motion_func, event) def ctrl_b1_motion(self, event: object): if self.ctrl_select_enabled and self.drag_selection_enabled and self.not_currently_resizing(): need_redraw = False end_row = self.identify_row(y=event.y) end_col = self.identify_col(x=event.x) if ( end_row < len(self.row_positions) - 1 and end_col < len(self.col_positions) - 1 and self.selected and self.selected.type_ == "cells" ): box = self.get_b1_motion_box( self.selected.row, self.selected.column, end_row, end_col, ) if ( box is not None and self.being_drawn_item is not None and self.coords_and_type(self.being_drawn_item) != box ): if box[2] - box[0] != 1 or box[3] - box[1] != 1: self.being_drawn_item = self.recreate_selection_box(*box[:-1], self.selected.fill_iid) else: self.hide_selection_box(self.selected.fill_iid) self.being_drawn_item = self.add_selection( box[0], box[1], run_binding_func=False, set_as_current=True, ) need_redraw = True sel_event = self.get_select_event(being_drawn_item=self.being_drawn_item) if self.drag_selection_binding_func: self.drag_selection_binding_func(sel_event) self.PAR.emit_event("<>", data=sel_event) if self.scroll_if_event_offscreen(event): need_redraw = True if need_redraw: self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True, redraw_table=True) elif not self.ctrl_select_enabled: self.b1_motion(event) def b1_release(self, event=None): if self.being_drawn_item is not None and (to_sel := self.coords_and_type(self.being_drawn_item)): r_to_sel, c_to_sel = self.selected.row, self.selected.column self.hide_selection_box(self.being_drawn_item) self.set_currently_selected( r_to_sel, c_to_sel, item=self.create_selection_box( *to_sel, state=( "hidden" if (to_sel.upto_r - to_sel.from_r == 1 and to_sel.upto_c - to_sel.from_c == 1) else "normal" ), set_current=False, ), ) sel_event = self.get_select_event(being_drawn_item=self.being_drawn_item) if self.drag_selection_binding_func: self.drag_selection_binding_func(sel_event) self.PAR.emit_event("<>", data=sel_event) else: self.being_drawn_item = None if self.RI.width_resizing_enabled and self.RI.rsz_w is not None and self.RI.currently_resizing_width: self.delete_resize_lines() self.RI.delete_resize_lines() self.RI.currently_resizing_width = False self.RI.set_width(self.new_row_width, set_TL=True) self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) self.b1_pressed_loc = None elif self.CH.height_resizing_enabled and self.CH.rsz_h is not None and self.CH.currently_resizing_height: self.delete_resize_lines() self.CH.delete_resize_lines() self.CH.currently_resizing_height = False self.CH.set_height(self.new_header_height, set_TL=True) self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) self.b1_pressed_loc = None self.RI.rsz_w = None self.CH.rsz_h = None if self.b1_pressed_loc is not None: r = self.identify_row(y=event.y, allow_end=False) c = self.identify_col(x=event.x, allow_end=False) if r is not None and c is not None and (r, c) == self.b1_pressed_loc: datarn = self.datarn(r) datacn = self.datacn(c) if self.get_cell_kwargs(datarn, datacn, key="dropdown") or self.get_cell_kwargs( datarn, datacn, key="checkbox" ): canvasx = self.canvasx(event.x) if ( self.closed_dropdown != self.b1_pressed_loc and self.get_cell_kwargs(datarn, datacn, key="dropdown") and canvasx > self.col_positions[c + 1] - self.table_txt_height - 4 and canvasx < self.col_positions[c + 1] - 1 ) or ( self.get_cell_kwargs(datarn, datacn, key="checkbox") and canvasx < self.col_positions[c] + self.table_txt_height + 4 and self.canvasy(event.y) < self.row_positions[r] + self.table_txt_height + 4 ): self.open_cell(event) else: self.mouseclick_outside_editor_or_dropdown_all_canvases() self.b1_pressed_loc = None self.closed_dropdown = None try_binding(self.extra_b1_release_func, event) def double_b1(self, event=None): self.mouseclick_outside_editor_or_dropdown_all_canvases() self.focus_set() if ( self.identify_col(x=event.x, allow_end=False) is None or self.identify_row(y=event.y, allow_end=False) is None ): self.deselect("all") elif self.single_selection_enabled and self.not_currently_resizing(): r = self.identify_row(y=event.y) c = self.identify_col(x=event.x) if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: self.select_cell(r, c, redraw=True) if self.edit_cell_enabled: self.open_cell(event) elif self.toggle_selection_enabled and self.not_currently_resizing(): r = self.identify_row(y=event.y) c = self.identify_col(x=event.x) if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: self.toggle_select_cell(r, c, redraw=True) if self.edit_cell_enabled: self.open_cell(event) try_binding(self.extra_double_b1_func, event) def identify_row(self, event=None, y=None, allow_end=True): if event is None: y2 = self.canvasy(y) elif y is None: y2 = self.canvasy(event.y) r = bisect_left(self.row_positions, y2) if r != 0: r -= 1 if not allow_end and r >= len(self.row_positions) - 1: return None return r def identify_col(self, event=None, x=None, allow_end=True): if event is None: x2 = self.canvasx(x) elif x is None: x2 = self.canvasx(event.x) c = bisect_left(self.col_positions, x2) if c != 0: c -= 1 if not allow_end and c >= len(self.col_positions) - 1: return None return c def fix_views(self) -> None: xcheck = self.xview() ycheck = self.yview() if xcheck and xcheck[0] <= 0: self.xview("moveto", 0) if self.show_header: self.CH.xview("moveto", 0) elif len(xcheck) > 1 and xcheck[1] >= 1: self.xview("moveto", 1) if self.show_header: self.CH.xview("moveto", 1) if ycheck and ycheck[0] <= 0: self.yview("moveto", 0) if self.show_index: self.RI.yview("moveto", 0) elif len(ycheck) > 1 and ycheck[1] >= 1: self.yview("moveto", 1) if self.show_index: self.RI.yview("moveto", 1) def scroll_if_event_offscreen(self, event: object) -> bool: need_redraw = False if self.data: xcheck = self.xview() ycheck = self.yview() if len(xcheck) > 1 and xcheck[0] > 0 and event.x < 0: try: self.xview_scroll(-1, "units") self.CH.xview_scroll(-1, "units") except Exception: pass need_redraw = True if len(ycheck) > 1 and ycheck[0] > 0 and event.y < 0: try: self.yview_scroll(-1, "units") self.RI.yview_scroll(-1, "units") except Exception: pass need_redraw = True if len(xcheck) > 1 and xcheck[1] < 1 and event.x > self.winfo_width(): try: self.xview_scroll(1, "units") self.CH.xview_scroll(1, "units") except Exception: pass need_redraw = True if len(ycheck) > 1 and ycheck[1] < 1 and event.y > self.winfo_height(): try: self.yview_scroll(1, "units") self.RI.yview_scroll(1, "units") except Exception: pass need_redraw = True if need_redraw: self.fix_views() self.x_move_synced_scrolls("moveto", self.xview()[0]) self.y_move_synced_scrolls("moveto", self.yview()[0]) return need_redraw def x_move_synced_scrolls(self, *args, redraw: bool = True, use_scrollbar: bool = False): for widget in self.synced_scrolls: # try: if hasattr(widget, "MT"): if use_scrollbar: widget.MT._xscrollbar(*args, move_synced=False) else: widget.MT.set_xviews(*args, move_synced=False, redraw=redraw) else: widget.xview(*args) # except Exception: # continue def y_move_synced_scrolls(self, *args, redraw: bool = True, use_scrollbar: bool = False): for widget in self.synced_scrolls: # try: if hasattr(widget, "MT"): if use_scrollbar: widget.MT._yscrollbar(*args, move_synced=False) else: widget.MT.set_yviews(*args, move_synced=False, redraw=redraw) else: widget.yview(*args) # except Exception: # continue def _xscrollbar(self, *args, move_synced: bool = True): self.xview(*args) if self.show_header: self.CH.xview(*args) self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False) if move_synced: self.x_move_synced_scrolls(*args, use_scrollbar=True) def _yscrollbar(self, *args, move_synced: bool = True): self.yview(*args) if self.show_index: self.RI.yview(*args) self.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=True) if move_synced: self.y_move_synced_scrolls(*args, use_scrollbar=True) def set_xviews(self, *args, move_synced: bool = True, redraw: bool = True) -> None: self.main_table_redraw_grid_and_text(setting_views=True) self.update_idletasks() self.xview(*args) if self.show_header: self.CH.update_idletasks() self.CH.xview(*args) self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False) if move_synced: self.x_move_synced_scrolls(*args) self.fix_views() def set_yviews(self, *args, move_synced: bool = True, redraw: bool = True) -> None: self.main_table_redraw_grid_and_text(setting_views=True) self.update_idletasks() self.yview(*args) if self.show_index: self.RI.update_idletasks() self.RI.yview(*args) self.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=True) if move_synced: self.y_move_synced_scrolls(*args) self.fix_views() def set_view(self, x_args: list[str, float], y_args: list[str, float]) -> None: self.set_xviews(*x_args) self.set_yviews(*y_args) def mousewheel(self, event: object) -> None: if event.delta < 0 or event.num == 5: self.yview_scroll(1, "units") self.RI.yview_scroll(1, "units") self.y_move_synced_scrolls("moveto", self.yview()[0]) elif event.delta >= 0 or event.num == 4: if self.canvasy(0) <= 0: return self.yview_scroll(-1, "units") self.RI.yview_scroll(-1, "units") self.y_move_synced_scrolls("moveto", self.yview()[0]) self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) def shift_mousewheel(self, event: object) -> None: if event.delta < 0 or event.num == 5: self.xview_scroll(1, "units") self.CH.xview_scroll(1, "units") self.x_move_synced_scrolls("moveto", self.xview()[0]) elif event.delta >= 0 or event.num == 4: if self.canvasx(0) <= 0: return self.xview_scroll(-1, "units") self.CH.xview_scroll(-1, "units") self.x_move_synced_scrolls("moveto", self.xview()[0]) self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) def ctrl_mousewheel(self, event): if event.delta < 0 or event.num == 5: self.zoom_out() elif event.delta >= 0 or event.num == 4: self.zoom_in() def zoom_in(self, event=None): self.zoom_font( (self.PAR.ops.table_font[0], self.PAR.ops.table_font[1] + 1, self.PAR.ops.table_font[2]), (self.PAR.ops.index_font[0], self.PAR.ops.index_font[1] + 1, self.PAR.ops.index_font[2]), (self.PAR.ops.header_font[0], self.PAR.ops.header_font[1] + 1, self.PAR.ops.header_font[2]), "in", ) def zoom_out(self, event=None): if self.PAR.ops.table_font[1] < 2 or self.PAR.ops.index_font[1] < 2 or self.PAR.ops.header_font[1] < 2: return self.zoom_font( (self.PAR.ops.table_font[0], self.PAR.ops.table_font[1] - 1, self.PAR.ops.table_font[2]), (self.PAR.ops.index_font[0], self.PAR.ops.index_font[1] - 1, self.PAR.ops.index_font[2]), (self.PAR.ops.header_font[0], self.PAR.ops.header_font[1] - 1, self.PAR.ops.header_font[2]), "out", ) def zoom_font(self, table_font: tuple, index_font: tuple, header_font: tuple, zoom: Literal["in", "out"]) -> None: self.saved_column_widths = {} self.saved_row_heights = {} # should record position prior to change and then see after change y = self.canvasy(0) x = self.canvasx(0) r = self.identify_row(y=0) c = self.identify_col(x=0) try: r_pc = (y - self.row_positions[r]) / (self.row_positions[r + 1] - self.row_positions[r]) except Exception: r_pc = 0.0 try: c_pc = (x - self.col_positions[c]) / (self.col_positions[c + 1] - self.col_positions[c]) except Exception: c_pc = 0.0 old_min_row_height = int(self.min_row_height) old_default_row_height = int(self.get_default_row_height()) self.set_table_font( table_font, reset_row_positions=False, ) self.set_index_font(index_font) self.set_header_font(header_font) if self.PAR.ops.set_cell_sizes_on_zoom: self.set_all_cell_sizes_to_text() self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) elif not self.PAR.ops.set_cell_sizes_on_zoom: default_row_height = self.get_default_row_height() self.row_positions = list( accumulate( chain( [0], ( ( self.min_row_height if h == old_min_row_height else ( default_row_height if h == old_default_row_height else self.min_row_height if h < self.min_row_height else h ) ) for h in self.gen_row_heights() ), ) ) ) self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) self.recreate_all_selection_boxes() self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) self.refresh_open_window_positions(zoom=zoom) self.RI.refresh_open_window_positions(zoom=zoom) self.CH.refresh_open_window_positions(zoom=zoom) self.see( r, c, check_cell_visibility=False, redraw=True, r_pc=r_pc, c_pc=c_pc, ) def get_txt_w(self, txt, font=None): self.txt_measure_canvas.itemconfig( self.txt_measure_canvas_text, text=txt, font=self.PAR.ops.table_font if font is None else font, ) b = self.txt_measure_canvas.bbox(self.txt_measure_canvas_text) return b[2] - b[0] def get_txt_h(self, txt, font=None): self.txt_measure_canvas.itemconfig( self.txt_measure_canvas_text, text=txt, font=self.PAR.ops.table_font if font is None else font, ) b = self.txt_measure_canvas.bbox(self.txt_measure_canvas_text) return b[3] - b[1] def get_txt_dimensions(self, txt, font=None): self.txt_measure_canvas.itemconfig( self.txt_measure_canvas_text, text=txt, font=self.PAR.ops.table_font if font is None else font, ) b = self.txt_measure_canvas.bbox(self.txt_measure_canvas_text) return b[2] - b[0], b[3] - b[1] def get_lines_cell_height(self, n, font=None): return ( self.get_txt_h( txt="\n".join("|" for _ in range(n)) if n > 1 else "|", font=self.PAR.ops.table_font if font is None else font, ) + 5 ) def set_min_column_width(self): self.min_column_width = 1 if self.min_column_width > self.max_column_width: self.max_column_width = self.min_column_width + 20 if ( isinstance(self.PAR.ops.auto_resize_columns, (int, float)) and self.PAR.ops.auto_resize_columns < self.min_column_width ): self.PAR.ops.auto_resize_columns = self.min_column_width def get_default_row_height(self) -> int: if isinstance(self.PAR.ops.default_row_height, str): if int(self.PAR.ops.default_row_height) == 1: return self.min_row_height else: return self.min_row_height + self.get_lines_cell_height(int(self.PAR.ops.default_row_height) - 1) return self.PAR.ops.default_row_height def get_default_header_height(self) -> int: if isinstance(self.PAR.ops.default_header_height, str): if int(self.PAR.ops.default_header_height) == 1: return self.min_header_height else: return self.min_header_height + self.get_lines_cell_height( int(self.PAR.ops.default_header_height) - 1, font=self.PAR.ops.header_font ) return self.PAR.ops.default_header_height def set_table_font(self, newfont: tuple | None = None, reset_row_positions: bool = False) -> tuple[str, int, str]: if newfont: if not isinstance(newfont, tuple): raise ValueError("Argument must be tuple e.g. " "('Carlito', 12, 'normal')") if len(newfont) != 3: raise ValueError("Argument must be three-tuple") if not isinstance(newfont[0], str) or not isinstance(newfont[1], int) or not isinstance(newfont[2], str): raise ValueError( "Argument must be font, size and 'normal', 'bold' or" "'italic' e.g. ('Carlito',12,'normal')" ) self.PAR.ops.table_font = FontTuple(*newfont) self.set_table_font_help() if reset_row_positions: if isinstance(reset_row_positions, bool): self.reset_row_positions() else: self.set_row_positions(itr=reset_row_positions) self.recreate_all_selection_boxes() return self.PAR.ops.table_font def set_table_font_help(self): self.table_txt_width, self.table_txt_height = self.get_txt_dimensions("|", self.PAR.ops.table_font) self.table_half_txt_height = ceil(self.table_txt_height / 2) if not self.table_half_txt_height % 2: self.table_first_ln_ins = self.table_half_txt_height + 2 else: self.table_first_ln_ins = self.table_half_txt_height + 3 self.min_row_height = int(self.table_first_ln_ins * 2.22) self.table_xtra_lines_increment = int(self.table_txt_height) if self.min_row_height < 12: self.min_row_height = 12 self.set_min_column_width() def set_header_font(self, newfont: tuple | None = None) -> tuple[str, int, str]: if newfont: if not isinstance(newfont, tuple): raise ValueError("Argument must be tuple e.g. ('Carlito', 12, 'normal')") if len(newfont) != 3: raise ValueError("Argument must be three-tuple") if not isinstance(newfont[0], str) or not isinstance(newfont[1], int) or not isinstance(newfont[2], str): raise ValueError( "Argument must be font, size and 'normal', 'bold' or" "'italic' e.g. ('Carlito',12,'normal')" ) self.PAR.ops.header_font = FontTuple(*newfont) self.set_header_font_help() self.recreate_all_selection_boxes() return self.PAR.ops.header_font def set_header_font_help(self): self.header_txt_width, self.header_txt_height = self.get_txt_dimensions("|", self.PAR.ops.header_font) self.header_half_txt_height = ceil(self.header_txt_height / 2) if not self.header_half_txt_height % 2: self.header_first_ln_ins = self.header_half_txt_height + 2 else: self.header_first_ln_ins = self.header_half_txt_height + 3 self.header_xtra_lines_increment = self.header_txt_height self.min_header_height = int(self.header_first_ln_ins * 2.22) if ( isinstance(self.PAR.ops.default_header_height, int) and self.PAR.ops.default_header_height < self.min_header_height ): self.PAR.ops.default_header_height = int(self.min_header_height) self.set_min_column_width() self.CH.set_height(self.get_default_header_height(), set_TL=True) def set_index_font(self, newfont: tuple | None = None) -> tuple[str, int, str]: if newfont: if not isinstance(newfont, tuple): raise ValueError("Argument must be tuple e.g. ('Carlito', 12, 'normal')") if len(newfont) != 3: raise ValueError("Argument must be three-tuple") if not isinstance(newfont[0], str) or not isinstance(newfont[1], int) or not isinstance(newfont[2], str): raise ValueError( "Argument must be font, size and 'normal', 'bold' or" "'italic' e.g. ('Carlito',12,'normal')" ) self.PAR.ops.index_font = FontTuple(*newfont) self.set_index_font_help() return self.PAR.ops.index_font def set_index_font_help(self): self.index_txt_width, self.index_txt_height = self.get_txt_dimensions("|", self.PAR.ops.index_font) self.index_half_txt_height = ceil(self.index_txt_height / 2) if not self.index_half_txt_height % 2: self.index_first_ln_ins = self.index_half_txt_height + 2 else: self.index_first_ln_ins = self.index_half_txt_height + 3 self.index_xtra_lines_increment = self.index_txt_height self.min_index_width = 5 def purge_undo_and_redo_stack(self): self.undo_stack = deque(maxlen=self.PAR.ops.max_undos) self.redo_stack = deque(maxlen=self.PAR.ops.max_undos) def purge_redo_stack(self): self.redo_stack = deque(maxlen=self.PAR.ops.max_undos) def data_reference( self, newdataref: list | tuple | None = None, reset_col_positions: bool = True, reset_row_positions: bool = True, redraw: bool = False, return_id: bool = True, keep_formatting: bool = True, ) -> object: if isinstance(newdataref, (list, tuple)): self.hide_dropdown_editor_all_canvases() self.data = newdataref if keep_formatting: self.reapply_formatting() else: self.delete_all_formatting(clear_values=False) self.purge_undo_and_redo_stack() if reset_col_positions: self.reset_col_positions() if reset_row_positions: self.reset_row_positions() if redraw: self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) if return_id: return id(self.data) else: return self.data def get_cell_dimensions(self, datarn, datacn): txt = self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True) if txt: self.txt_measure_canvas.itemconfig(self.txt_measure_canvas_text, text=txt, font=self.PAR.ops.table_font) b = self.txt_measure_canvas.bbox(self.txt_measure_canvas_text) w = b[2] - b[0] + 7 h = b[3] - b[1] + 5 else: w = self.min_column_width h = self.min_row_height if self.get_cell_kwargs(datarn, datacn, key="dropdown") or self.get_cell_kwargs(datarn, datacn, key="checkbox"): return w + self.table_txt_height, h return w, h def set_cell_size_to_text(self, r, c, only_if_too_small=False, redraw: bool = True, run_binding=False): min_column_width = int(self.min_column_width) min_rh = int(self.min_row_height) w = min_column_width h = min_rh datacn = self.datacn(c) datarn = self.datarn(r) tw, h = self.get_cell_dimensions(datarn, datacn) if tw > w: w = tw if h < min_rh: h = int(min_rh) elif h > self.max_row_height: h = int(self.max_row_height) if w < min_column_width: w = int(min_column_width) elif w > self.max_column_width: w = int(self.max_column_width) cell_needs_resize_w = False cell_needs_resize_h = False if only_if_too_small: if w > self.col_positions[c + 1] - self.col_positions[c]: cell_needs_resize_w = True if h > self.row_positions[r + 1] - self.row_positions[r]: cell_needs_resize_h = True else: if w != self.col_positions[c + 1] - self.col_positions[c]: cell_needs_resize_w = True if h != self.row_positions[r + 1] - self.row_positions[r]: cell_needs_resize_h = True if cell_needs_resize_w: old_width = self.col_positions[c + 1] - self.col_positions[c] new_col_pos = self.col_positions[c] + w increment = new_col_pos - self.col_positions[c + 1] self.col_positions[c + 2 :] = [ e + increment for e in islice(self.col_positions, c + 2, len(self.col_positions)) ] self.col_positions[c + 1] = new_col_pos new_width = self.col_positions[c + 1] - self.col_positions[c] if run_binding and self.CH.column_width_resize_func and old_width != new_width: self.CH.column_width_resize_func( event_dict( name="resize", sheet=self.PAR.name, resized_columns={c: {"old_size": old_width, "new_size": new_width}}, ) ) if cell_needs_resize_h: old_height = self.row_positions[r + 1] - self.row_positions[r] new_row_pos = self.row_positions[r] + h increment = new_row_pos - self.row_positions[r + 1] self.row_positions[r + 2 :] = [ e + increment for e in islice(self.row_positions, r + 2, len(self.row_positions)) ] self.row_positions[r + 1] = new_row_pos new_height = self.row_positions[r + 1] - self.row_positions[r] if run_binding and self.RI.row_height_resize_func and old_height != new_height: self.RI.row_height_resize_func( event_dict( name="resize", sheet=self.PAR.name, resized_rows={r: {"old_size": old_height, "new_size": new_height}}, ) ) if cell_needs_resize_w or cell_needs_resize_h: self.recreate_all_selection_boxes() if redraw: self.refresh() return True else: return False def set_all_cell_sizes_to_text( self, width: int | None = None, slim: bool = False, ) -> tuple[list[float], list[float]]: min_column_width = int(self.min_column_width) min_rh = int(self.min_row_height) h = min_rh rhs = defaultdict(lambda: int(min_rh)) cws = [] qconf = self.txt_measure_canvas.itemconfig qbbox = self.txt_measure_canvas.bbox qtxtm = self.txt_measure_canvas_text qtxth = self.table_txt_height qfont = self.PAR.ops.table_font numrows = self.total_data_rows() numcols = self.total_data_cols() if self.all_columns_displayed: itercols = range(numcols) else: itercols = self.displayed_columns if self.all_rows_displayed: iterrows = range(numrows) else: iterrows = self.displayed_rows if is_iterable(self._row_index): for datarn in iterrows: w_, h = self.RI.get_cell_dimensions(datarn) if h < min_rh: h = int(min_rh) elif h > self.max_row_height: h = int(self.max_row_height) if h > rhs[datarn]: rhs[datarn] = h added_w_space = 1 if slim else 7 for datacn in itercols: w = min_column_width if width is None else width if (hw := self.CH.get_cell_dimensions(datacn)[0]) > w: w = hw else: w = min_column_width for datarn in iterrows: if txt := self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True): qconf(qtxtm, text=txt, font=qfont) b = qbbox(qtxtm) tw = b[2] - b[0] + added_w_space h = b[3] - b[1] + 5 else: tw = min_column_width h = min_rh if self.get_cell_kwargs( datarn, datacn, key="dropdown", ) or self.get_cell_kwargs( datarn, datacn, key="checkbox", ): tw += qtxth if tw > w: w = tw if h < min_rh: h = int(min_rh) elif h > self.max_row_height: h = int(self.max_row_height) if h > rhs[datarn]: rhs[datarn] = h if w < min_column_width: w = int(min_column_width) elif w > self.max_column_width: w = int(self.max_column_width) cws.append(w) self.set_row_positions(itr=rhs.values()) self.set_col_positions(itr=cws) self.recreate_all_selection_boxes() return self.row_positions, self.col_positions def set_col_positions(self, itr: Iterator[float]) -> None: self.col_positions = list(accumulate(chain([0], itr))) def reset_col_positions(self, ncols: int | None = None): colpos = self.PAR.ops.default_column_width if isinstance(ncols, int): self.set_col_positions(itr=repeat(colpos, ncols)) elif self.all_columns_displayed: self.set_col_positions(itr=repeat(colpos, self.total_data_cols())) else: self.set_col_positions(itr=repeat(colpos, len(self.displayed_columns))) def set_row_positions(self, itr: Iterator[float]) -> None: self.row_positions = list(accumulate(chain([0], itr))) def reset_row_positions(self, nrows: int | None = None): rowpos = self.get_default_row_height() if isinstance(nrows, int): self.set_row_positions(itr=repeat(rowpos, nrows)) elif self.all_rows_displayed: self.set_row_positions(itr=repeat(rowpos, self.total_data_rows())) else: self.set_row_positions(itr=repeat(rowpos, len(self.displayed_rows))) def del_col_position(self, idx: int, deselect_all: bool = False): if deselect_all: self.deselect("all", redraw=False) if idx == "end" or len(self.col_positions) <= idx + 1: del self.col_positions[-1] else: w = self.col_positions[idx + 1] - self.col_positions[idx] idx += 1 del self.col_positions[idx] self.col_positions[idx:] = [e - w for e in islice(self.col_positions, idx, len(self.col_positions))] def del_row_position(self, idx: int, deselect_all: bool = False): if deselect_all: self.deselect("all", redraw=False) if idx == "end" or len(self.row_positions) <= idx + 1: del self.row_positions[-1] else: w = self.row_positions[idx + 1] - self.row_positions[idx] idx += 1 del self.row_positions[idx] self.row_positions[idx:] = [e - w for e in islice(self.row_positions, idx, len(self.row_positions))] def del_col_positions(self, idxs: Iterator[int] | None = None): if idxs is None: del self.col_positions[-1] else: if not isinstance(idxs, set): idxs = set(idxs) self.set_col_positions(itr=(w for i, w in enumerate(self.gen_column_widths()) if i not in idxs)) def del_row_positions(self, idxs: Iterator[int] | None = None): if idxs is None: del self.row_positions[-1] else: if not isinstance(idxs, set): idxs = set(idxs) self.set_row_positions(itr=(h for i, h in enumerate(self.gen_row_heights()) if i not in idxs)) def get_column_widths(self) -> list[int]: return diff_list(self.col_positions) def get_row_heights(self) -> list[int]: return diff_list(self.row_positions) def gen_column_widths(self) -> Generator[int]: return diff_gen(self.col_positions) def gen_row_heights(self) -> Generator[int]: return diff_gen(self.row_positions) def insert_col_position( self, idx: Literal["end"] | int = "end", width: int | None = None, deselect_all: bool = False, ) -> None: if deselect_all: self.deselect("all", redraw=False) if width is None: w = self.PAR.ops.default_column_width else: w = width if idx == "end" or len(self.col_positions) == idx + 1: self.col_positions.append(self.col_positions[-1] + w) else: idx += 1 self.col_positions.insert(idx, self.col_positions[idx - 1] + w) idx += 1 self.col_positions[idx:] = [e + w for e in islice(self.col_positions, idx, len(self.col_positions))] def insert_row_position( self, idx: Literal["end"] | int = "end", height: int | None = None, deselect_all: bool = False, ) -> None: if deselect_all: self.deselect("all", redraw=False) if height is None: h = self.get_default_row_height() else: h = height if idx == "end" or len(self.row_positions) == idx + 1: self.row_positions.append(self.row_positions[-1] + h) else: idx += 1 self.row_positions.insert(idx, self.row_positions[idx - 1] + h) idx += 1 self.row_positions[idx:] = [e + h for e in islice(self.row_positions, idx, len(self.row_positions))] def insert_col_positions( self, idx: Literal["end"] | int = "end", widths: Sequence[float] | int | None = None, deselect_all: bool = False, ) -> None: if deselect_all: self.deselect("all", redraw=False) if widths is None: w = [self.PAR.ops.default_column_width] elif isinstance(widths, int): w = list(repeat(self.PAR.ops.default_column_width, widths)) else: w = widths if idx == "end" or len(self.col_positions) == idx + 1: if len(w) > 1: self.col_positions += list(accumulate(chain([self.col_positions[-1] + w[0]], islice(w, 1, None)))) else: self.col_positions.append(self.col_positions[-1] + w[0]) else: if len(w) > 1: idx += 1 self.col_positions[idx:idx] = list( accumulate(chain([self.col_positions[idx - 1] + w[0]], islice(w, 1, None))) ) idx += len(w) sumw = sum(w) self.col_positions[idx:] = [e + sumw for e in islice(self.col_positions, idx, len(self.col_positions))] else: w = w[0] idx += 1 self.col_positions.insert(idx, self.col_positions[idx - 1] + w) idx += 1 self.col_positions[idx:] = [e + w for e in islice(self.col_positions, idx, len(self.col_positions))] def insert_row_positions( self, idx: Literal["end"] | int = "end", heights: Sequence[float] | int | None = None, deselect_all: bool = False, ) -> None: if deselect_all: self.deselect("all", redraw=False) default_row_height = self.get_default_row_height() if heights is None: h = [default_row_height] elif isinstance(heights, int): h = list(repeat(default_row_height, heights)) else: h = heights if idx == "end" or len(self.row_positions) == idx + 1: if len(h) > 1: self.row_positions += list(accumulate(chain([self.row_positions[-1] + h[0]], islice(h, 1, None)))) else: self.row_positions.append(self.row_positions[-1] + h[0]) else: if len(h) > 1: idx += 1 self.row_positions[idx:idx] = list( accumulate(chain([self.row_positions[idx - 1] + h[0]], islice(h, 1, None))) ) idx += len(h) sumh = sum(h) self.row_positions[idx:] = [e + sumh for e in islice(self.row_positions, idx, len(self.row_positions))] else: h = h[0] idx += 1 self.row_positions.insert(idx, self.row_positions[idx - 1] + h) idx += 1 self.row_positions[idx:] = [e + h for e in islice(self.row_positions, idx, len(self.row_positions))] def named_span_coords(self, name: str | dict) -> tuple[int, int, int | None, int | None]: dct = self.named_spans[name] if isinstance(name, str) else name return ( dct["from_r"], dct["from_c"], dct["upto_r"], dct["upto_c"], ) def adjust_options_post_add_columns( self, cols: list | tuple, create_ops: bool = True, ) -> None: self.tagged_cells = { tags: {(r, c if not (num := bisect_right(cols, c)) else c + num) for (r, c) in tagged} for tags, tagged in self.tagged_cells.items() } self.cell_options = { (r, c if not (num := bisect_right(cols, c)) else c + num): v for (r, c), v in self.cell_options.items() } self.progress_bars = { (r, c if not (num := bisect_right(cols, c)) else c + num): v for (r, c), v in self.progress_bars.items() } self.tagged_columns = { tags: {c if not (num := bisect_right(cols, c)) else c + num for c in tagged} for tags, tagged in self.tagged_columns.items() } self.col_options = { c if not (num := bisect_right(cols, c)) else c + num: v for c, v in self.col_options.items() } self.CH.cell_options = { c if not (num := bisect_right(cols, c)) else c + num: v for c, v in self.CH.cell_options.items() } # if there are named spans where columns were added # add options to gap which was created by adding columns totalrows = None new_ops = self.PAR.create_options_from_span qkspan = self.span() for span in self.named_spans.values(): if isinstance(span["from_c"], int): for datacn in cols: if span["from_c"] > datacn: span["from_c"] += 1 if isinstance(span["upto_c"], int): span["upto_c"] += 1 elif span["from_c"] <= datacn and ( (isinstance(span["upto_c"], int) and span["upto_c"] > datacn) or span["upto_c"] is None ): if isinstance(span["upto_c"], int): span["upto_c"] += 1 # if to_add then it's an undo/redo and don't # need to create fresh options if create_ops: # if rows are none it's a column options span if span["from_r"] is None: new_ops( mod_span( qkspan, span, from_c=datacn, upto_c=datacn + 1, ) ) # cells else: if totalrows is None: totalrows = self.total_data_rows() rng_upto_r = totalrows if span["upto_r"] is None else span["upto_r"] for rn in range(span["from_r"], rng_upto_r): new_ops( mod_span( qkspan, span, from_r=rn, from_c=datacn, upto_r=rn + 1, upto_c=datacn + 1, ) ) def adjust_options_post_add_rows( self, rows: list | tuple, create_ops: bool = True, ) -> None: self.tagged_cells = { tags: {(r if not (num := bisect_right(rows, r)) else r + num, c) for (r, c) in tagged} for tags, tagged in self.tagged_cells.items() } self.cell_options = { (r if not (num := bisect_right(rows, r)) else r + num, c): v for (r, c), v in self.cell_options.items() } self.progress_bars = { (r if not (num := bisect_right(rows, r)) else r + num, c): v for (r, c), v in self.progress_bars.items() } self.tagged_rows = { tags: {r if not (num := bisect_right(rows, r)) else r + num for r in tagged} for tags, tagged in self.tagged_rows.items() } self.row_options = { r if not (num := bisect_right(rows, r)) else r + num: v for r, v in self.row_options.items() } self.RI.cell_options = { r if not (num := bisect_right(rows, r)) else r + num: v for r, v in self.RI.cell_options.items() } self.RI.tree_rns = { v: r if not (num := bisect_right(rows, r)) else r + num for v, r in self.RI.tree_rns.items() } # if there are named spans where rows were added # add options to gap which was created by adding rows totalcols = None new_ops = self.PAR.create_options_from_span qkspan = self.span() for span in self.named_spans.values(): if isinstance(span["from_r"], int): for datarn in rows: if span["from_r"] > datarn: span["from_r"] += 1 if isinstance(span["upto_r"], int): span["upto_r"] += 1 elif span["from_r"] <= datarn and ( (isinstance(span["upto_r"], int) and span["upto_r"] > datarn) or span["upto_r"] is None ): if isinstance(span["upto_r"], int): span["upto_r"] += 1 # if to_add then it's an undo/redo and don't # need to create fresh options if create_ops: # if cols are none it's a row options span if span["from_c"] is None: new_ops( mod_span( qkspan, span, from_r=datarn, upto_r=datarn + 1, ) ) # cells else: if totalcols is None: totalcols = self.total_data_cols() rng_upto_c = totalcols if span["upto_c"] is None else span["upto_c"] for cn in range(span["from_c"], rng_upto_c): new_ops( mod_span( qkspan, span, from_r=datarn, from_c=cn, upto_r=datarn + 1, upto_c=cn + 1, ) ) def adjust_options_post_delete_columns( self, to_del: None | set = None, to_bis: None | list = None, named_spans: None | set = None, ) -> list[int]: if to_del is None: to_del = set() if not to_bis: to_bis = sorted(to_del) self.tagged_cells = { tags: { ( r, c if not (num := bisect_left(to_bis, c)) else c - num, ) for (r, c) in tagged if c not in to_del } for tags, tagged in self.tagged_cells.items() } self.cell_options = { ( r, c if not (num := bisect_left(to_bis, c)) else c - num, ): v for (r, c), v in self.cell_options.items() if c not in to_del } self.progress_bars = { ( r, c if not (num := bisect_left(to_bis, c)) else c - num, ): v for (r, c), v in self.progress_bars.items() if c not in to_del } self.tagged_columns = { tags: {c if not (num := bisect_left(to_bis, c)) else c - num for c in tagged if c not in to_del} for tags, tagged in self.tagged_columns.items() } self.col_options = { c if not (num := bisect_left(to_bis, c)) else c - num: v for c, v in self.col_options.items() if c not in to_del } self.CH.cell_options = { c if not (num := bisect_left(to_bis, c)) else c - num: v for c, v in self.CH.cell_options.items() if c not in to_del } self.del_columns_from_named_spans( to_del=to_del, to_bis=to_bis, named_spans=named_spans, ) return to_bis def del_columns_from_named_spans( self, to_del: set, to_bis: list, named_spans: None | set = None, ) -> None: if named_spans is None: named_spans = self.get_spans_to_del_from_cols(cols=to_del) for name in named_spans: del self.named_spans[name] for span in self.named_spans.values(): if isinstance(span["from_c"], int): for c in to_bis: if span["from_c"] > c: span["from_c"] -= 1 if isinstance(span["upto_c"], int) and span["upto_c"] > c: span["upto_c"] -= 1 def get_spans_to_del_from_cols( self, cols: set, ) -> set: total = self.total_data_cols() return { nm for nm, sp in self.named_spans.items() if isinstance(sp["from_c"], int) and all(c in cols for c in range(sp["from_c"], total if sp["upto_c"] is None else sp["upto_c"])) } def adjust_options_post_delete_rows( self, to_del: None | set = None, to_bis: None | list = None, named_spans: None | set = None, ) -> list[int]: if to_del is None: to_del = set() if not to_bis: to_bis = sorted(to_del) self.tagged_cells = { tags: { ( r if not (num := bisect_left(to_bis, r)) else r - num, c, ) for (r, c) in tagged if r not in to_del } for tags, tagged in self.tagged_cells.items() } self.cell_options = { ( r if not (num := bisect_left(to_bis, r)) else r - num, c, ): v for (r, c), v in self.cell_options.items() if r not in to_del } self.progress_bars = { ( r if not (num := bisect_left(to_bis, r)) else r - num, c, ): v for (r, c), v in self.progress_bars.items() if r not in to_del } self.tagged_rows = { tags: {r if not (num := bisect_left(to_bis, r)) else r - num for r in tagged if r not in to_del} for tags, tagged in self.tagged_rows.items() } self.row_options = { r if not (num := bisect_left(to_bis, r)) else r - num: v for r, v in self.row_options.items() if r not in to_del } self.RI.cell_options = { r if not (num := bisect_left(to_bis, r)) else r - num: v for r, v in self.RI.cell_options.items() if r not in to_del } self.RI.tree_rns = { v: r if not (num := bisect_left(to_bis, r)) else r - num for v, r in self.RI.tree_rns.items() if r not in to_del } self.del_rows_from_named_spans( to_del=to_del, to_bis=to_bis, named_spans=named_spans, ) return to_bis def del_rows_from_named_spans( self, to_del: set, to_bis: list, named_spans: None | set = None, ) -> None: if named_spans is None: named_spans = self.get_spans_to_del_from_rows(rows=to_del) for name in named_spans: del self.named_spans[name] for span in self.named_spans.values(): if isinstance(span["from_r"], int): for r in to_bis: if span["from_r"] > r: span["from_r"] -= 1 if isinstance(span["upto_r"], int) and span["upto_r"] > r: span["upto_r"] -= 1 def get_spans_to_del_from_rows( self, rows: set, ) -> set: total = self.total_data_rows() return { nm for nm, sp in self.named_spans.items() if isinstance(sp["from_r"], int) and all(r in rows for r in range(sp["from_r"], total if sp["upto_r"] is None else sp["upto_r"])) } def add_columns( self, columns: dict, header: dict, column_widths: dict, event_data: dict, displayed_columns: None | list[int] = None, create_ops: bool = True, create_selections: bool = True, add_row_positions: bool = True, push_ops: bool = True, mod_event_boxes: bool = True, ) -> EventDataDict: self.saved_column_widths = {} saved_displayed_columns = list(self.displayed_columns) if isinstance(displayed_columns, list): self.displayed_columns = displayed_columns elif not self.all_columns_displayed: # push displayed indexes by one for every inserted column self.displayed_columns.sort() # highest index is first in columns up_to = len(self.displayed_columns) for cn in columns: self.displayed_columns.insert((last_ins := bisect_left(self.displayed_columns, cn)), cn) self.displayed_columns[last_ins + 1 : up_to] = [ i + 1 for i in islice(self.displayed_columns, last_ins + 1, up_to) ] up_to = last_ins cws = self.get_column_widths() if column_widths and next(reversed(column_widths)) > len(cws): for i in reversed(range(len(cws), len(cws) + next(reversed(column_widths)) - len(cws))): column_widths[i] = self.PAR.ops.default_column_width self.set_col_positions( itr=insert_items( cws, column_widths, ) ) # rn needed for indexing but cn insert maxrn = 0 for cn, rowdict in reversed(columns.items()): for rn, v in rowdict.items(): if rn < len(self.data) and cn > len(self.data[rn]): self.fix_row_len(rn, cn - 1) elif rn >= len(self.data): self.fix_data_len(rn, cn - 1) if rn > maxrn: maxrn = rn self.data[rn].insert(cn, v) # if not hiding rows then we can extend row positions if necessary if add_row_positions and self.all_rows_displayed and maxrn >= len(self.row_positions) - 1: default_height = self.get_default_row_height() event_data["added"]["rows"] = { "table": {}, "index": {}, "row_heights": {rn: default_height for rn in range(len(self.row_positions) - 1, maxrn + 1)}, "displayed_rows": self.displayed_rows, } self.set_row_positions( itr=chain( self.gen_row_heights(), repeat(default_height, maxrn + 1 - (len(self.row_positions) - 1)), ) ) if isinstance(self._headers, list) and header: self._headers = insert_items(self._headers, header, self.CH.fix_header) if push_ops: self.adjust_options_post_add_columns( cols=tuple(reversed(columns)), create_ops=create_ops, ) if create_selections: self.deselect("all", redraw=False) for boxst, boxend in consecutive_ranges(tuple(reversed(column_widths))): self.create_selection_box( 0, boxst, len(self.row_positions) - 1, boxend, "columns", run_binding=True, ) if mod_event_boxes: event_data["selection_boxes"] = self.get_boxes() event_data["selected"] = self.selected event_data["added"]["columns"] = { "table": columns, "header": header, "column_widths": column_widths, "displayed_columns": saved_displayed_columns, } return event_data def rc_add_columns(self, event: object = None): rowlen = self.equalize_data_row_lengths() selcols = sorted(self.get_selected_cols()) if ( selcols and isinstance(self.CH.popup_menu_loc, int) and selcols[0] <= self.CH.popup_menu_loc and selcols[-1] >= self.CH.popup_menu_loc ): selcols = get_seq_without_gaps_at_index(selcols, self.CH.popup_menu_loc) numcols = len(selcols) displayed_ins_col = selcols[0] if event == "left" else selcols[-1] + 1 if self.all_columns_displayed: data_ins_col = int(displayed_ins_col) else: if displayed_ins_col == len(self.col_positions) - 1: data_ins_col = rowlen else: if len(self.displayed_columns) > displayed_ins_col: data_ins_col = int(self.displayed_columns[displayed_ins_col]) else: data_ins_col = int(self.displayed_columns[-1]) else: numcols = 1 displayed_ins_col = len(self.col_positions) - 1 data_ins_col = int(displayed_ins_col) if ( isinstance(self.PAR.ops.paste_insert_column_limit, int) and self.PAR.ops.paste_insert_column_limit < displayed_ins_col + numcols ): numcols = self.PAR.ops.paste_insert_column_limit - len(self.col_positions) - 1 if numcols < 1: return event_data = event_dict( name="add_columns", sheet=self.PAR.name, widget=self, boxes=self.get_boxes(), selected=self.selected, ) if not try_binding(self.extra_begin_insert_cols_rc_func, event_data, "begin_add_columns"): return event_data = self.add_columns( *self.get_args_for_add_columns(data_ins_col, displayed_ins_col, numcols), event_data=event_data, ) if self.undo_enabled: self.undo_stack.append(pickled_event_dict(event_data)) self.refresh() try_binding(self.extra_end_insert_cols_rc_func, event_data, "end_add_columns") self.sheet_modified(event_data) def add_rows( self, rows: dict, index: dict, row_heights: dict, event_data: dict, displayed_rows: None | list[int] = None, create_ops: bool = True, create_selections: bool = True, add_col_positions: bool = True, push_ops: bool = True, mod_event_boxes: bool = True, ) -> EventDataDict: self.saved_row_heights = {} saved_displayed_rows = list(self.displayed_rows) if isinstance(displayed_rows, list): self.displayed_rows = displayed_rows elif not self.all_rows_displayed: # push displayed indexes by one for every inserted row self.displayed_rows.sort() # highest index is first in rows up_to = len(self.displayed_rows) for rn in rows: self.displayed_rows.insert((last_ins := bisect_left(self.displayed_rows, rn)), rn) self.displayed_rows[last_ins + 1 : up_to] = [ i + 1 for i in islice(self.displayed_rows, last_ins + 1, up_to) ] up_to = last_ins rhs = self.get_row_heights() if row_heights and next(reversed(row_heights)) > len(rhs): default_row_height = self.get_default_row_height() for i in reversed(range(len(rhs), len(rhs) + next(reversed(row_heights)) - len(rhs))): row_heights[i] = default_row_height self.set_row_positions( itr=insert_items( rhs, row_heights, ) ) maxcn = 0 # rn needed for insert but cn indexing for rn, row in reversed(rows.items()): cn = len(row) - 1 if rn > len(self.data): self.fix_data_len(rn - 1, cn) self.data.insert(rn, row) if cn > maxcn: maxcn = cn if isinstance(self._row_index, list) and index: self._row_index = insert_items(self._row_index, index, self.RI.fix_index) # if not hiding columns then we can extend col positions if necessary if add_col_positions and self.all_columns_displayed and maxcn >= len(self.col_positions) - 1: default_width = self.PAR.ops.default_column_width event_data["added"]["columns"] = { "table": {}, "header": {}, "column_widths": {cn: default_width for cn in range(len(self.col_positions) - 1, maxcn + 1)}, "displayed_columns": self.displayed_columns, } self.set_col_positions( itr=chain( self.gen_column_widths(), repeat(default_width, maxcn + 1 - (len(self.col_positions) - 1)), ) ) if push_ops: self.adjust_options_post_add_rows( rows=tuple(reversed(rows)), create_ops=create_ops, ) if create_selections: self.deselect("all", redraw=False) for boxst, boxend in consecutive_ranges(tuple(reversed(row_heights))): self.create_selection_box( boxst, 0, boxend, len(self.col_positions) - 1, "rows", run_binding=True, ) if mod_event_boxes: event_data["selection_boxes"] = self.get_boxes() event_data["selected"] = self.selected event_data["added"]["rows"] = { "table": rows, "index": index, "row_heights": row_heights, "displayed_rows": saved_displayed_rows, } return event_data def rc_add_rows(self, event: object = None): total_data_rows = self.total_data_rows() selrows = sorted(self.get_selected_rows()) if ( selrows and isinstance(self.RI.popup_menu_loc, int) and selrows[0] <= self.RI.popup_menu_loc and selrows[-1] >= self.RI.popup_menu_loc ): selrows = get_seq_without_gaps_at_index(selrows, self.RI.popup_menu_loc) numrows = len(selrows) displayed_ins_row = selrows[0] if event == "above" else selrows[-1] + 1 if self.all_rows_displayed: data_ins_row = int(displayed_ins_row) else: if displayed_ins_row == len(self.row_positions) - 1: data_ins_row = total_data_rows else: if len(self.displayed_rows) > displayed_ins_row: data_ins_row = int(self.displayed_rows[displayed_ins_row]) else: data_ins_row = int(self.displayed_rows[-1]) else: numrows = 1 displayed_ins_row = len(self.row_positions) - 1 data_ins_row = int(displayed_ins_row) if ( isinstance(self.PAR.ops.paste_insert_row_limit, int) and self.PAR.ops.paste_insert_row_limit < displayed_ins_row + numrows ): numrows = self.PAR.ops.paste_insert_row_limit - len(self.row_positions) - 1 if numrows < 1: return event_data = event_dict( name="add_rows", sheet=self.PAR.name, widget=self, boxes=self.get_boxes(), selected=self.selected, ) if not try_binding(self.extra_begin_insert_rows_rc_func, event_data, "begin_add_rows"): return event_data = self.add_rows( *self.get_args_for_add_rows(data_ins_row, displayed_ins_row, numrows), event_data=event_data, ) if self.undo_enabled: self.undo_stack.append(pickled_event_dict(event_data)) self.refresh() try_binding(self.extra_end_insert_rows_rc_func, event_data, "end_add_rows") self.sheet_modified(event_data) def get_args_for_add_columns( self, data_ins_col: int, displayed_ins_col: int, numcols: int, columns: list | None = None, widths: list | None = None, headers: bool = False, ) -> tuple[dict, dict, dict]: header_data = {} if isinstance(self._headers, list): if headers and columns: header_data = { datacn: column[0] for datacn, column in zip(reversed(range(data_ins_col, data_ins_col + numcols)), reversed(columns)) } else: header_data = { datacn: self.CH.get_value_for_empty_cell(datacn, c_ops=False) for datacn in reversed(range(data_ins_col, data_ins_col + numcols)) } if columns is None: rowrange = len(self.data) if self.data else 1 columns = { datacn: { datarn: self.get_value_for_empty_cell(datarn, datacn, c_ops=False) for datarn in range(rowrange) } for datacn in reversed(range(data_ins_col, data_ins_col + numcols)) } else: if headers: start = 1 else: start = 0 columns = { datacn: {datarn: v for datarn, v in enumerate(islice(column, start, None))} for datacn, column in zip(reversed(range(data_ins_col, data_ins_col + numcols)), reversed(columns)) } if widths is None: widths = { c: self.PAR.ops.default_column_width for c in reversed(range(displayed_ins_col, displayed_ins_col + numcols)) } else: widths = { c: width for c, width in zip(reversed(range(displayed_ins_col, displayed_ins_col + numcols)), reversed(widths)) } return columns, header_data, widths def get_args_for_add_rows( self, data_ins_row: int, displayed_ins_row: int, numrows: int, rows: list | None = None, heights: list | None = None, row_index: bool = False, total_data_cols=None, ) -> tuple[dict, dict, dict]: index_data = {} if isinstance(self._row_index, list): if row_index and rows: index_data = { datarn: v[0] for datarn, v in zip(reversed(range(data_ins_row, data_ins_row + numrows)), reversed(rows)) } else: index_data = { datarn: self.RI.get_value_for_empty_cell(datarn, r_ops=False) for datarn in reversed(range(data_ins_row, data_ins_row + numrows)) } if rows is None: if total_data_cols is None: total_data_cols = self.total_data_cols() colrange = total_data_cols if total_data_cols else 1 rows = { datarn: [self.get_value_for_empty_cell(datarn, c, c_ops=False) for c in range(colrange)] for datarn in reversed(range(data_ins_row, data_ins_row + numrows)) } else: if row_index: start = 1 else: start = 0 rows = { datarn: v[start:] if start and v else v for datarn, v in zip(reversed(range(data_ins_row, data_ins_row + numrows)), reversed(rows)) } if heights is None: default_row_height = self.get_default_row_height() heights = {r: default_row_height for r in reversed(range(displayed_ins_row, displayed_ins_row + numrows))} else: heights = { r: height for r, height in zip(reversed(range(displayed_ins_row, displayed_ins_row + numrows)), reversed(heights)) } return rows, index_data, heights def pickle_options(self) -> bytes: return pickle_obj( { "cell_options": self.cell_options, "column_options": self.col_options, "row_options": self.row_options, "CH_cell_options": self.CH.cell_options, "RI_cell_options": self.RI.cell_options, "tagged_cells": self.tagged_cells, "tagged_rows": self.tagged_rows, "tagged_columns": self.tagged_columns, } ) def delete_columns_data(self, cols: list, event_data: dict) -> EventDataDict: self.mouseclick_outside_editor_or_dropdown_all_canvases() event_data["deleted"]["displayed_columns"] = ( list(self.displayed_columns) if not isinstance(self.displayed_columns, int) else int(self.displayed_columns) ) event_data["options"] = self.pickle_options() event_data["named_spans"] = {k: span.pickle_self() for k, span in self.named_spans.items()} for datacn in reversed(cols): for rn in range(len(self.data)): if datacn not in event_data["deleted"]["columns"]: event_data["deleted"]["columns"][datacn] = {} try: event_data["deleted"]["columns"][datacn][rn] = self.data[rn].pop(datacn) except Exception: continue try: event_data["deleted"]["header"][datacn] = self._headers.pop(datacn) except Exception: continue cols_set = set(cols) self.adjust_options_post_delete_columns( to_del=cols_set, to_bis=cols, named_spans=self.get_spans_to_del_from_cols(cols=cols_set), ) if not self.all_columns_displayed: self.displayed_columns = [ c if not (num := bisect_left(cols, c)) else c - num for c in filterfalse(cols_set.__contains__, self.displayed_columns) ] return event_data def delete_columns_displayed(self, cols: list, event_data: dict) -> EventDataDict: self.saved_column_widths = {} cols_set = set(cols) for c in reversed(cols): event_data["deleted"]["column_widths"][c] = self.col_positions[c + 1] - self.col_positions[c] self.set_col_positions(itr=(width for c, width in enumerate(self.gen_column_widths()) if c not in cols_set)) return event_data def rc_delete_columns(self, event: object = None): selected = sorted(self.get_selected_cols()) if not self.selected: return event_data = event_dict( name="delete_columns", sheet=self.PAR.name, widget=self, boxes=self.get_boxes(), selected=self.selected, ) if not try_binding(self.extra_begin_del_cols_rc_func, event_data, "begin_delete_columns"): return event_data = self.delete_columns_displayed(selected, event_data) data_cols = selected if self.all_columns_displayed else [self.displayed_columns[c] for c in selected] event_data = self.delete_columns_data(data_cols, event_data) if self.undo_enabled: self.undo_stack.append(pickled_event_dict(event_data)) self.deselect("all") try_binding(self.extra_end_del_cols_rc_func, event_data, "end_delete_columns") self.sheet_modified(event_data) def delete_rows_data(self, rows: list, event_data: dict) -> EventDataDict: self.mouseclick_outside_editor_or_dropdown_all_canvases() event_data["deleted"]["displayed_rows"] = ( list(self.displayed_rows) if not isinstance(self.displayed_rows, int) else int(self.displayed_rows) ) event_data["options"] = self.pickle_options() event_data["named_spans"] = {k: span.pickle_self() for k, span in self.named_spans.items()} for datarn in reversed(rows): event_data["deleted"]["rows"][datarn] = self.data.pop(datarn) try: event_data["deleted"]["index"][datarn] = self._row_index.pop(datarn) except Exception: continue rows_set = set(rows) self.adjust_options_post_delete_rows( to_del=rows_set, to_bis=rows, named_spans=self.get_spans_to_del_from_rows(rows=rows_set), ) if not self.all_rows_displayed: self.displayed_rows = [ r if not (num := bisect_left(rows, r)) else r - num for r in filterfalse(rows_set.__contains__, self.displayed_rows) ] return event_data def delete_rows_displayed(self, rows: list, event_data: dict) -> EventDataDict: self.saved_row_heights = {} rows_set = set(rows) for r in reversed(rows): event_data["deleted"]["row_heights"][r] = self.row_positions[r + 1] - self.row_positions[r] self.set_row_positions(itr=(height for r, height in enumerate(self.gen_row_heights()) if r not in rows_set)) return event_data def rc_delete_rows(self, event: object = None): selected = sorted(self.get_selected_rows()) if not self.selected: return event_data = event_dict( name="delete_rows", sheet=self.PAR.name, widget=self, boxes=self.get_boxes(), selected=self.selected, ) if not try_binding(self.extra_begin_del_rows_rc_func, event_data, "begin_delete_rows"): return event_data = self.delete_rows_displayed(selected, event_data) data_rows = selected if self.all_rows_displayed else [self.displayed_rows[r] for r in selected] event_data = self.delete_rows_data(data_rows, event_data) if self.undo_enabled: self.undo_stack.append(pickled_event_dict(event_data)) self.deselect("all") try_binding(self.extra_end_del_rows_rc_func, event_data, "end_delete_rows") self.sheet_modified(event_data) def move_row_position(self, idx1: int, idx2: int): if not len(self.row_positions) <= 2: if idx1 < idx2: height = self.row_positions[idx1 + 1] - self.row_positions[idx1] self.row_positions.insert(idx2 + 1, self.row_positions.pop(idx1 + 1)) for i in range(idx1 + 1, idx2 + 1): self.row_positions[i] -= height self.row_positions[idx2 + 1] = self.row_positions[idx2] + height else: height = self.row_positions[idx1 + 1] - self.row_positions[idx1] self.row_positions.insert(idx2 + 1, self.row_positions.pop(idx1 + 1)) for i in range(idx2 + 2, idx1 + 2): self.row_positions[i] += height self.row_positions[idx2 + 1] = self.row_positions[idx2] + height def move_col_position(self, idx1: int, idx2: int): if not len(self.col_positions) <= 2: if idx1 < idx2: width = self.col_positions[idx1 + 1] - self.col_positions[idx1] self.col_positions.insert(idx2 + 1, self.col_positions.pop(idx1 + 1)) for i in range(idx1 + 1, idx2 + 1): self.col_positions[i] -= width self.col_positions[idx2 + 1] = self.col_positions[idx2] + width else: width = self.col_positions[idx1 + 1] - self.col_positions[idx1] self.col_positions.insert(idx2 + 1, self.col_positions.pop(idx1 + 1)) for i in range(idx2 + 2, idx1 + 2): self.col_positions[i] += width self.col_positions[idx2 + 1] = self.col_positions[idx2] + width def display_rows( self, rows: int | Iterator | None = None, all_rows_displayed: bool | None = None, reset_row_positions: bool = True, deselect_all: bool = True, ) -> list[int] | None: if rows is None and all_rows_displayed is None: return list(range(self.total_data_rows())) if self.all_rows_displayed else self.displayed_rows if rows is not None and rows != self.displayed_rows: self.purge_undo_and_redo_stack() self.displayed_rows = sorted(rows) # setting all_rows_displayed if all_rows_displayed is not None: # setting it to True and it's currently False if all_rows_displayed and not self.all_rows_displayed: self.purge_undo_and_redo_stack() self.all_rows_displayed = True # setting it to False and it's currently True elif not all_rows_displayed and self.all_rows_displayed: # if rows is None then displayed_rows needs to be reset if rows is None: self.displayed_rows = list(range(self.total_data_rows())) self.all_rows_displayed = False if reset_row_positions: self.reset_row_positions() if deselect_all: self.deselect("all", redraw=False) def display_columns( self, columns: int | Iterator | None = None, all_columns_displayed: bool | None = None, reset_col_positions: bool = True, deselect_all: bool = True, ) -> list[int] | None: if columns is None and all_columns_displayed is None: return list(range(self.total_data_cols())) if self.all_columns_displayed else self.displayed_columns if columns is not None and columns != self.displayed_columns: self.purge_undo_and_redo_stack() self.displayed_columns = sorted(columns) # setting all_columns_displayed if all_columns_displayed is not None: # setting it to True and it's currently False if all_columns_displayed and not self.all_columns_displayed: self.purge_undo_and_redo_stack() self.all_columns_displayed = True # setting it to False and it's currently True elif not all_columns_displayed and self.all_columns_displayed: # if columns is None then displayed_columns needs to be reset if columns is None: self.displayed_columns = list(range(self.total_data_cols())) self.all_columns_displayed = False if reset_col_positions: self.reset_col_positions() if deselect_all: self.deselect("all", redraw=False) def headers( self, newheaders: object = None, index: int | None = None, reset_col_positions: bool = False, show_headers_if_not_sheet: bool = True, redraw: bool = False, ) -> object: if newheaders is not None: if isinstance(newheaders, (list, tuple)): self._headers = list(newheaders) if isinstance(newheaders, tuple) else newheaders elif isinstance(newheaders, int): self._headers = int(newheaders) elif isinstance(self._headers, list) and isinstance(index, int): if index >= len(self._headers): self.CH.fix_header(index) self._headers[index] = f"{newheaders}" elif not isinstance(newheaders, (list, tuple, int)) and index is None: try: self._headers = list(newheaders) except Exception: raise ValueError( """ New header must be iterable or int \ (use int to use a row as the header """ ) if reset_col_positions: self.reset_col_positions() elif ( show_headers_if_not_sheet and isinstance(self._headers, list) and (self.col_positions == [0] or not self.col_positions) ): colpos = int(self.PAR.ops.default_column_width) if self.all_columns_displayed: self.set_col_positions(itr=repeat(colpos, len(self._headers))) else: self.set_col_positions(itr=repeat(colpos, len(self.displayed_columns))) if redraw: self.refresh() else: if not isinstance(self._headers, int) and index is not None and isinstance(index, int): return self._headers[index] else: return self._headers def row_index( self, newindex: object = None, index: int | None = None, reset_row_positions: bool = False, show_index_if_not_sheet: bool = True, redraw: bool = False, ) -> object: if newindex is not None: if not self._row_index and not isinstance(self._row_index, int): self.RI.set_width(self.PAR.ops.default_row_index_width, set_TL=True) if isinstance(newindex, (list, tuple)): self._row_index = list(newindex) if isinstance(newindex, tuple) else newindex elif isinstance(newindex, int): self._row_index = int(newindex) elif isinstance(index, int): if index >= len(self._row_index): self.RI.fix_index(index) self._row_index[index] = f"{newindex}" elif not isinstance(newindex, (list, tuple, int)) and index is None: try: self._row_index = list(newindex) except Exception: raise ValueError( """ New index must be iterable or int \ (use int to use a column as the index """ ) if reset_row_positions: self.reset_row_positions() elif ( show_index_if_not_sheet and isinstance(self._row_index, list) and (self.row_positions == [0] or not self.row_positions) ): rowpos = self.get_default_row_height() if self.all_rows_displayed: self.set_row_positions(itr=repeat(rowpos, len(self._row_index))) else: self.set_row_positions(itr=repeat(rowpos, len(self.displayed_rows))) if redraw: self.refresh() else: if not isinstance(self._row_index, int) and index is not None and isinstance(index, int): return self._row_index[index] else: return self._row_index def total_data_cols(self, include_header: bool = True) -> int: h_total = len(self._headers) if include_header and isinstance(self._headers, (list, tuple)) else 0 # map() for some reason is 15% faster than max(key=len) using python 3.11 windows 11 d_total = max(map(len, self.data), default=0) return max(h_total, d_total) def total_data_rows(self, include_index: bool = True) -> int: i_total = len(self._row_index) if include_index and isinstance(self._row_index, (list, tuple)) else 0 d_total = len(self.data) return max(i_total, d_total) def data_dimensions(self, total_rows: int | None = None, total_columns: int | None = None): if total_rows is None and total_columns is None: return self.total_data_rows(), self.total_data_cols() if total_rows is not None: if len(self.data) < total_rows: ncols = self.total_data_cols() if total_columns is None else total_columns self.data.extend(self.get_empty_row_seq(r, ncols) for r in range(total_rows - len(self.data))) else: self.data[total_rows:] = [] if total_columns is not None: for rn, r in enumerate(self.data): if (lnr := len(r)) > total_columns: r = r[:total_columns] elif lnr < total_columns: r += self.get_empty_row_seq(rn, end=total_columns, start=lnr) def equalize_data_row_lengths( self, include_header: bool = True, total_data_cols: int | None = None, at_least_cols: int | None = None, ) -> int: if not isinstance(total_data_cols, int): total_data_cols = self.total_data_cols(include_header=include_header) if isinstance(at_least_cols, int) and at_least_cols > total_data_cols: total_data_cols = at_least_cols total_data_cols = max(total_data_cols, len(self.col_positions) - 1) if not isinstance(self._headers, int) and include_header and total_data_cols > len(self._headers): self.CH.fix_header(total_data_cols - 1) for rn, r in enumerate(self.data): if total_data_cols > (lnr := len(r)): r += self.get_empty_row_seq(rn, end=total_data_cols, start=lnr) return total_data_cols def get_canvas_visible_area(self) -> tuple[float, float, float, float]: return ( self.canvasx(0), self.canvasy(0), self.canvasx(self.winfo_width()), self.canvasy(self.winfo_height()), ) @property def visible_text_rows(self) -> tuple[int, int]: start = bisect_left(self.row_positions, self.canvasy(0)) end = bisect_right(self.row_positions, self.canvasy(self.winfo_height())) start = start - 1 if start else start end = end - 1 if end == len(self.row_positions) else end return start, end @property def visible_text_columns(self) -> tuple[int, int]: start = bisect_left(self.col_positions, self.canvasx(0)) end = bisect_right(self.col_positions, self.canvasx(self.winfo_width())) start = start - 1 if start else start end = end - 1 if end == len(self.col_positions) else end return start, end def redraw_highlight_get_text_fg( self, r: int, c: int, fc: int | float, fr: int | float, sc: int | float, sr: int | float, c_2_: tuple[int, int, int], c_3_: tuple[int, int, int], c_4_: tuple[int, int, int], selections: dict, datarn: int, datacn: int, can_width: int | None, ) -> str: redrawn = False if (datarn, datacn) in self.progress_bars: kwargs = self.progress_bars[(datarn, datacn)] else: kwargs = self.get_cell_kwargs(datarn, datacn, key="highlight") if kwargs: if kwargs[0] is not None: c_1 = kwargs[0] if kwargs[0].startswith("#") else color_map[kwargs[0]] if "cells" in selections and (r, c) in selections["cells"]: tf = ( self.PAR.ops.table_selected_cells_fg if kwargs[1] is None or self.PAR.ops.display_selected_fg_over_highlights else kwargs[1] ) if kwargs[0] is not None: fill = ( f"#{int((int(c_1[1:3], 16) + c_2_[0]) / 2):02X}" + f"{int((int(c_1[3:5], 16) + c_2_[1]) / 2):02X}" + f"{int((int(c_1[5:], 16) + c_2_[2]) / 2):02X}" ) elif "rows" in selections and r in selections["rows"]: tf = ( self.PAR.ops.table_selected_rows_fg if kwargs[1] is None or self.PAR.ops.display_selected_fg_over_highlights else kwargs[1] ) if kwargs[0] is not None: fill = ( f"#{int((int(c_1[1:3], 16) + c_4_[0]) / 2):02X}" + f"{int((int(c_1[3:5], 16) + c_4_[1]) / 2):02X}" + f"{int((int(c_1[5:], 16) + c_4_[2]) / 2):02X}" ) elif "columns" in selections and c in selections["columns"]: tf = ( self.PAR.ops.table_selected_columns_fg if kwargs[1] is None or self.PAR.ops.display_selected_fg_over_highlights else kwargs[1] ) if kwargs[0] is not None: fill = ( f"#{int((int(c_1[1:3], 16) + c_3_[0]) / 2):02X}" + f"{int((int(c_1[3:5], 16) + c_3_[1]) / 2):02X}" + f"{int((int(c_1[5:], 16) + c_3_[2]) / 2):02X}" ) else: tf = self.PAR.ops.table_fg if kwargs[1] is None else kwargs[1] if kwargs[0] is not None: fill = kwargs[0] if kwargs[0] is not None: highlight_fn = partial( self.redraw_highlight, x1=fc + 1, y1=fr + 1, x2=sc, y2=sr, fill=fill, outline=( self.PAR.ops.table_fg if self.get_cell_kwargs(datarn, datacn, key="dropdown") and self.PAR.ops.show_dropdown_borders else "" ), tag="hi", ) if isinstance(kwargs, ProgressBar): if kwargs.del_when_done and kwargs.percent >= 100: del self.progress_bars[(datarn, datacn)] else: redrawn = highlight_fn( can_width=None, pc=kwargs.percent, ) else: redrawn = highlight_fn( can_width=can_width if (len(kwargs) > 2 and kwargs[2]) else None, pc=None, ) elif not kwargs: if "cells" in selections and (r, c) in selections["cells"]: tf = self.PAR.ops.table_selected_cells_fg elif "rows" in selections and r in selections["rows"]: tf = self.PAR.ops.table_selected_rows_fg elif "columns" in selections and c in selections["columns"]: tf = self.PAR.ops.table_selected_columns_fg else: tf = self.PAR.ops.table_fg return tf, redrawn def redraw_highlight(self, x1, y1, x2, y2, fill, outline, tag, can_width=None, pc=None): if not is_type_int(pc) or pc >= 100: coords = ( x1 - 1 if outline else x1, y1 - 1 if outline else y1, x2 if can_width is None else x2 + can_width, y2, ) elif pc <= 0: coords = (x1, y1, x1, y2) else: coords = (x1, y1, (x2 - x1) * (pc / 100), y2) if self.hidd_high: iid, showing = self.hidd_high.popitem() self.coords(iid, coords) if showing: self.itemconfig(iid, fill=fill, outline=outline) else: self.itemconfig(iid, fill=fill, outline=outline, tag=tag, state="normal") else: iid = self.create_rectangle(coords, fill=fill, outline=outline, tag=tag) self.disp_high[iid] = True return True def redraw_gridline( self, points, fill, width, tag, ): if self.hidd_grid: iid, sh = self.hidd_grid.popitem() self.coords(iid, points) if sh: self.itemconfig( iid, fill=fill, width=width, capstyle=tk.BUTT, joinstyle=tk.ROUND, ) else: self.itemconfig( iid, fill=fill, width=width, capstyle=tk.BUTT, joinstyle=tk.ROUND, state="normal", ) else: iid = self.create_line( points, fill=fill, width=width, capstyle=tk.BUTT, joinstyle=tk.ROUND, tag=tag, ) self.disp_grid[iid] = True return iid def redraw_dropdown( self, x1, y1, x2, y2, fill, outline, tag, draw_outline=True, draw_arrow=True, open_=False, ): if draw_outline and self.PAR.ops.show_dropdown_borders: self.redraw_highlight(x1 + 1, y1 + 1, x2, y2, fill="", outline=self.PAR.ops.table_fg, tag=tag) if draw_arrow: mod = (self.table_txt_height - 1) if self.table_txt_height % 2 else self.table_txt_height small_mod = int(mod / 5) mid_y = floor(self.min_row_height / 2) if open_: # up arrow points = ( x2 - 4 - small_mod - small_mod - small_mod - small_mod, y1 + mid_y + small_mod, x2 - 4 - small_mod - small_mod, y1 + mid_y - small_mod, x2 - 4, y1 + mid_y + small_mod, ) else: # down arrow points = ( x2 - 4 - small_mod - small_mod - small_mod - small_mod, y1 + mid_y - small_mod, x2 - 4 - small_mod - small_mod, y1 + mid_y + small_mod, x2 - 4, y1 + mid_y - small_mod, ) if self.hidd_dropdown: t, sh = self.hidd_dropdown.popitem() self.coords(t, points) if sh: self.itemconfig(t, fill=fill) else: self.itemconfig(t, fill=fill, tag=tag, state="normal") self.lift(t) else: t = self.create_line( points, fill=fill, tag=tag, width=2, capstyle=tk.ROUND, joinstyle=tk.BEVEL, ) self.disp_dropdown[t] = True def redraw_checkbox( self, x1: int | float, y1: int | float, x2: int | float, y2: int | float, fill: str, outline: str, tag: str | tuple, draw_check: bool = False, ) -> None: points = rounded_box_coords(x1, y1, x2, y2) if self.hidd_checkbox: t, sh = self.hidd_checkbox.popitem() self.coords(t, points) if sh: self.itemconfig(t, fill=outline, outline=fill) else: self.itemconfig(t, fill=outline, outline=fill, tag=tag, state="normal") self.lift(t) else: t = self.create_polygon(points, fill=outline, outline=fill, tag=tag, smooth=True) self.disp_checkbox[t] = True if draw_check: x1 = x1 + 4 y1 = y1 + 4 x2 = x2 - 3 y2 = y2 - 3 points = rounded_box_coords(x1, y1, x2, y2, radius=4) if self.hidd_checkbox: t, sh = self.hidd_checkbox.popitem() self.coords(t, points) if sh: self.itemconfig(t, fill=fill, outline=outline) else: self.itemconfig(t, fill=fill, outline=outline, tag=tag, state="normal") self.lift(t) else: t = self.create_polygon(points, fill=fill, outline=outline, tag=tag, smooth=True) self.disp_checkbox[t] = True def main_table_redraw_grid_and_text( self, redraw_header: bool = False, redraw_row_index: bool = False, redraw_table: bool = True, setting_views: bool = False, ) -> bool: try: can_width = self.winfo_width() can_height = self.winfo_height() except Exception: return False row_pos_exists = self.row_positions != [0] and self.row_positions col_pos_exists = self.col_positions != [0] and self.col_positions resized_cols = False resized_rows = False if self.PAR.ops.auto_resize_columns and self.allow_auto_resize_columns and col_pos_exists: max_w = can_width - self.PAR.ops.empty_horizontal if self.PAR.ops.auto_resize_columns < self.min_column_width: min_column_width = self.column_width else: min_column_width = self.PAR.ops.auto_resize_columns if (len(self.col_positions) - 1) * min_column_width < max_w: resized_cols = True change = int((max_w - self.col_positions[-1]) / (len(self.col_positions) - 1)) widths = [ int(b - a) + change - 1 for a, b in zip(self.col_positions, islice(self.col_positions, 1, len(self.col_positions))) ] diffs = {} for i, w in enumerate(widths): if w < min_column_width: diffs[i] = min_column_width - w widths[i] = min_column_width if diffs and len(diffs) < len(widths): change = sum(diffs.values()) / (len(widths) - len(diffs)) for i, w in enumerate(widths): if i not in diffs: widths[i] -= change self.col_positions = list(accumulate(chain([0], widths))) if self.PAR.ops.auto_resize_rows and self.allow_auto_resize_rows and row_pos_exists: max_h = can_height - self.PAR.ops.empty_vertical if self.PAR.ops.auto_resize_rows < self.min_row_height: min_row_height = self.min_row_height else: min_row_height = self.PAR.ops.auto_resize_rows if (len(self.row_positions) - 1) * min_row_height < max_h: resized_rows = True change = int((max_h - self.row_positions[-1]) / (len(self.row_positions) - 1)) heights = [ int(b - a) + change - 1 for a, b in zip(self.row_positions, islice(self.row_positions, 1, len(self.row_positions))) ] diffs = {} for i, h in enumerate(heights): if h < min_row_height: diffs[i] = min_row_height - h heights[i] = min_row_height if diffs and len(diffs) < len(heights): change = sum(diffs.values()) / (len(heights) - len(diffs)) for i, h in enumerate(heights): if i not in diffs: heights[i] -= change self.row_positions = list(accumulate(chain([0], heights))) if ( self.PAR.ops.auto_resize_row_index is not True and can_width >= self.col_positions[-1] + self.PAR.ops.empty_horizontal and self.PAR.xscroll_showing ): self.PAR.xscroll.grid_forget() self.PAR.xscroll_showing = False elif ( can_width < self.col_positions[-1] + self.PAR.ops.empty_horizontal and not self.PAR.xscroll_showing and not self.PAR.xscroll_disabled and can_height > 40 ): self.PAR.xscroll.grid(row=2, column=0, columnspan=2, sticky="nswe") self.PAR.xscroll_showing = True if can_height >= self.row_positions[-1] + self.PAR.ops.empty_vertical and self.PAR.yscroll_showing: self.PAR.yscroll.grid_forget() self.PAR.yscroll_showing = False elif ( can_height < self.row_positions[-1] + self.PAR.ops.empty_vertical and not self.PAR.yscroll_showing and not self.PAR.yscroll_disabled and can_width > 40 ): self.PAR.yscroll.grid(row=0, column=2, rowspan=3, sticky="nswe") self.PAR.yscroll_showing = True last_col_line_pos = self.col_positions[-1] + 1 last_row_line_pos = self.row_positions[-1] + 1 scrollregion = ( 0, 0, last_col_line_pos + self.PAR.ops.empty_horizontal + 2, last_row_line_pos + self.PAR.ops.empty_vertical + 2, ) if setting_views or scrollregion != self.scrollregion: self.configure(scrollregion=scrollregion) self.scrollregion = scrollregion self.CH.configure_scrollregion(last_col_line_pos) self.RI.configure_scrollregion(last_row_line_pos) if setting_views: return False scrollpos_top = self.canvasy(0) scrollpos_bot = self.canvasy(can_height) scrollpos_left = self.canvasx(0) scrollpos_right = self.canvasx(can_width) grid_start_row = bisect_left(self.row_positions, scrollpos_top) grid_end_row = bisect_right(self.row_positions, scrollpos_bot) grid_start_col = bisect_left(self.col_positions, scrollpos_left) grid_end_col = bisect_right(self.col_positions, scrollpos_right) text_start_row = grid_start_row - 1 if grid_start_row else grid_start_row text_end_row = grid_end_row - 1 if grid_end_row == len(self.row_positions) else grid_end_row text_start_col = grid_start_col - 1 if grid_start_col else grid_start_col text_end_col = grid_end_col - 1 if grid_end_col == len(self.col_positions) else grid_end_col changed_w = False if self.PAR.ops.auto_resize_row_index and redraw_row_index and self.show_index: changed_w = self.RI.auto_set_index_width( end_row=grid_end_row, only_rows=map(self.datarn, range(text_start_row, text_end_row)), ) if resized_cols or resized_rows or changed_w: self.recreate_all_selection_boxes() if changed_w: self.update_idletasks() self.RI.update_idletasks() self.CH.update_idletasks() self.TL.update_idletasks() return False self.hidd_text.update(self.disp_text) self.disp_text = {} self.hidd_high.update(self.disp_high) self.disp_high = {} self.hidd_grid.update(self.disp_grid) self.disp_grid = {} self.hidd_dropdown.update(self.disp_dropdown) self.disp_dropdown = {} self.hidd_checkbox.update(self.disp_checkbox) self.disp_checkbox = {} if last_col_line_pos > scrollpos_right: x_stop = scrollpos_right else: x_stop = last_col_line_pos if last_row_line_pos > scrollpos_bot: y_stop = scrollpos_bot else: y_stop = last_row_line_pos if redraw_table and self.PAR.ops.show_horizontal_grid and row_pos_exists: if self.PAR.ops.horizontal_grid_to_end_of_window: x_grid_stop = scrollpos_right + can_width else: if last_col_line_pos > scrollpos_right: x_grid_stop = x_stop + 1 else: x_grid_stop = x_stop - 1 points = list( chain.from_iterable( [ ( self.canvasx(0) - 1, self.row_positions[r], x_grid_stop, self.row_positions[r], self.canvasx(0) - 1, self.row_positions[r], self.canvasx(0) - 1, self.row_positions[r + 1] if len(self.row_positions) - 1 > r else self.row_positions[r], ) for r in range(grid_start_row, grid_end_row) ] ) ) if points: self.redraw_gridline( points=points, fill=self.PAR.ops.table_grid_fg, width=1, tag="g", ) if redraw_table and self.PAR.ops.show_vertical_grid and col_pos_exists: if self.PAR.ops.vertical_grid_to_end_of_window: y_grid_stop = scrollpos_bot + can_height else: if last_row_line_pos > scrollpos_bot: y_grid_stop = y_stop + 1 else: y_grid_stop = y_stop - 1 points = list( chain.from_iterable( [ ( self.col_positions[c], scrollpos_top - 1, self.col_positions[c], y_grid_stop, self.col_positions[c], scrollpos_top - 1, self.col_positions[c + 1] if len(self.col_positions) - 1 > c else self.col_positions[c], scrollpos_top - 1, ) for c in range(grid_start_col, grid_end_col) ] ) ) if points: self.redraw_gridline( points=points, fill=self.PAR.ops.table_grid_fg, width=1, tag="g", ) if redraw_table: selections = self.get_redraw_selections(text_start_row, grid_end_row, text_start_col, grid_end_col) c_2 = ( self.PAR.ops.table_selected_cells_bg if self.PAR.ops.table_selected_cells_bg.startswith("#") else color_map[self.PAR.ops.table_selected_cells_bg] ) c_2_ = (int(c_2[1:3], 16), int(c_2[3:5], 16), int(c_2[5:], 16)) c_3 = ( self.PAR.ops.table_selected_columns_bg if self.PAR.ops.table_selected_columns_bg.startswith("#") else color_map[self.PAR.ops.table_selected_columns_bg] ) c_3_ = (int(c_3[1:3], 16), int(c_3[3:5], 16), int(c_3[5:], 16)) c_4 = ( self.PAR.ops.table_selected_rows_bg if self.PAR.ops.table_selected_rows_bg.startswith("#") else color_map[self.PAR.ops.table_selected_rows_bg] ) c_4_ = (int(c_4[1:3], 16), int(c_4[3:5], 16), int(c_4[5:], 16)) rows_ = tuple(range(text_start_row, text_end_row)) font = self.PAR.ops.table_font dd_coords = self.dropdown.get_coords() for c in range(text_start_col, text_end_col): for r in rows_: rtopgridln = self.row_positions[r] rbotgridln = self.row_positions[r + 1] if rbotgridln - rtopgridln < self.table_txt_height: continue cleftgridln = self.col_positions[c] crightgridln = self.col_positions[c + 1] datarn = self.datarn(r) datacn = self.datacn(c) fill, dd_drawn = self.redraw_highlight_get_text_fg( r, c, cleftgridln, rtopgridln, crightgridln, rbotgridln, c_2_, c_3_, c_4_, selections, datarn, datacn, can_width, ) align = self.get_cell_kwargs(datarn, datacn, key="align") if align: align = align else: align = self.align kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown") if align == "w": draw_x = cleftgridln + 3 if kwargs: mw = crightgridln - cleftgridln - self.table_txt_height - 2 self.redraw_dropdown( cleftgridln, rtopgridln, crightgridln, self.row_positions[r + 1], fill=fill, outline=fill, tag=f"dd_{r}_{c}", draw_outline=not dd_drawn, draw_arrow=mw >= 5, open_=dd_coords == (r, c), ) else: mw = crightgridln - cleftgridln - 1 elif align == "e": if kwargs: mw = crightgridln - cleftgridln - self.table_txt_height - 2 draw_x = crightgridln - 5 - self.table_txt_height self.redraw_dropdown( cleftgridln, rtopgridln, crightgridln, self.row_positions[r + 1], fill=fill, outline=fill, tag=f"dd_{r}_{c}", draw_outline=not dd_drawn, draw_arrow=mw >= 5, open_=dd_coords == (r, c), ) else: mw = crightgridln - cleftgridln - 1 draw_x = crightgridln - 3 elif align == "center": if kwargs: mw = crightgridln - cleftgridln - self.table_txt_height - 2 draw_x = cleftgridln + ceil((crightgridln - cleftgridln - self.table_txt_height) / 2) self.redraw_dropdown( cleftgridln, rtopgridln, crightgridln, self.row_positions[r + 1], fill=fill, outline=fill, tag=f"dd_{r}_{c}", draw_outline=not dd_drawn, draw_arrow=mw >= 5, open_=dd_coords == (r, c), ) else: mw = crightgridln - cleftgridln - 1 draw_x = cleftgridln + floor((crightgridln - cleftgridln) / 2) if not kwargs: kwargs = self.get_cell_kwargs(datarn, datacn, key="checkbox") if kwargs and mw > self.table_txt_height + 1: box_w = self.table_txt_height + 1 if align == "w": draw_x += box_w + 3 elif align == "center": draw_x += ceil(box_w / 2) + 1 mw -= box_w + 3 try: draw_check = self.data[datarn][datacn] except Exception: draw_check = False self.redraw_checkbox( cleftgridln + 2, rtopgridln + 2, cleftgridln + self.table_txt_height + 3, rtopgridln + self.table_txt_height + 3, fill=fill if kwargs["state"] == "normal" else self.PAR.ops.table_grid_fg, outline="", tag="cb", draw_check=draw_check, ) lns = self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True).split("\n") if ( lns != [""] and mw > self.table_txt_width and not ( (align == "w" and draw_x > scrollpos_right) or (align == "e" and cleftgridln + 5 > scrollpos_right) or (align == "center" and cleftgridln + 5 > scrollpos_right) ) ): draw_y = rtopgridln + self.table_first_ln_ins start_ln = int((scrollpos_top - rtopgridln) / self.table_xtra_lines_increment) if start_ln < 0: start_ln = 0 draw_y += start_ln * self.table_xtra_lines_increment if draw_y + self.table_half_txt_height - 1 <= rbotgridln and len(lns) > start_ln: for txt in islice(lns, start_ln, None): if self.hidd_text: iid, showing = self.hidd_text.popitem() self.coords(iid, draw_x, draw_y) if showing: self.itemconfig( iid, text=txt, fill=fill, font=font, anchor=align, ) else: self.itemconfig( iid, text=txt, fill=fill, font=font, anchor=align, state="normal", ) self.tag_raise(iid) else: iid = self.create_text( draw_x, draw_y, text=txt, fill=fill, font=font, anchor=align, tag="t", ) self.disp_text[iid] = True wd = self.bbox(iid) wd = wd[2] - wd[0] if wd > mw: if align == "w": txt = txt[: int(len(txt) * (mw / wd))] self.itemconfig(iid, text=txt) wd = self.bbox(iid) while wd[2] - wd[0] > mw: txt = txt[:-1] self.itemconfig(iid, text=txt) wd = self.bbox(iid) elif align == "e": txt = txt[len(txt) - int(len(txt) * (mw / wd)) :] self.itemconfig(iid, text=txt) wd = self.bbox(iid) while wd[2] - wd[0] > mw: txt = txt[1:] self.itemconfig(iid, text=txt) wd = self.bbox(iid) elif align == "center": self.c_align_cyc = cycle(self.centre_alignment_text_mod_indexes) tmod = ceil((len(txt) - int(len(txt) * (mw / wd))) / 2) txt = txt[tmod - 1 : -tmod] self.itemconfig(iid, text=txt) wd = self.bbox(iid) while wd[2] - wd[0] > mw: txt = txt[next(self.c_align_cyc)] self.itemconfig(iid, text=txt) wd = self.bbox(iid) self.coords(iid, draw_x, draw_y) draw_y += self.table_xtra_lines_increment if draw_y + self.table_half_txt_height - 1 > rbotgridln: break if redraw_table: for dct in (self.hidd_text, self.hidd_high, self.hidd_grid, self.hidd_dropdown, self.hidd_checkbox): for iid, showing in dct.items(): if showing: self.itemconfig(iid, state="hidden") dct[iid] = False if self.PAR.ops.show_selected_cells_border: for iid, box in self.selection_boxes.items(): if box.bd_iid: self.tag_raise(box.bd_iid) if self.selected: self.tag_raise(self.selected.iid) if redraw_header and self.show_header: self.CH.redraw_grid_and_text( last_col_line_pos=last_col_line_pos, scrollpos_left=scrollpos_left, x_stop=x_stop, grid_start_col=grid_start_col, grid_end_col=grid_end_col, text_start_col=text_start_col, text_end_col=text_end_col, scrollpos_right=scrollpos_right, col_pos_exists=col_pos_exists, ) if redraw_row_index and self.show_index: self.RI.redraw_grid_and_text( last_row_line_pos=last_row_line_pos, scrollpos_top=scrollpos_top, y_stop=y_stop, grid_start_row=grid_start_row, grid_end_row=grid_end_row, text_start_row=text_start_row, text_end_row=text_end_row, scrollpos_bot=scrollpos_bot, row_pos_exists=row_pos_exists, ) event_data = {"sheetname": "", "header": redraw_header, "row_index": redraw_row_index, "table": redraw_table} self.PAR.emit_event("<>", data=event_data) return True def get_selection_items( self, cells: bool = True, rows: bool = True, columns: bool = True, reverse: bool = False, ) -> Generator[int]: """ Most recent selection box should be last """ itr = reversed(self.selection_boxes.items()) if reverse else self.selection_boxes.items() return tuple( (iid, box) for iid, box in itr if cells and box.type_ == "cells" or rows and box.type_ == "rows" or columns and box.type_ == "columns" ) def get_boxes(self) -> dict[Box_nt, Literal["cells", "rows", "columns"]]: return {box.coords: box.type_ for box in self.selection_boxes.values()} def reselect_from_get_boxes( self, boxes: dict, selected: tuple = tuple(), ) -> None: for (r1, c1, r2, c2), v in boxes.items(): if r2 < len(self.row_positions) and c2 < len(self.col_positions): self.create_selection_box(r1, c1, r2, c2, v, run_binding=True) if selected: self.set_currently_selected(selected.row, selected.column, box=selected.box) def set_currently_selected( self, r: int | None = None, c: int | None = None, item: int | None = None, box: tuple[int, int, int, int] | None = None, ) -> None: if isinstance(item, int) and item in self.selection_boxes: selection_box = self.selection_boxes[item] r1, c1, r2, c2 = selection_box.coords if r is None: r = r1 if c is None: c = c1 if r1 <= r and c1 <= c and r2 >= r and c2 >= c: self.create_currently_selected_box( r, c, selection_box.type_, selection_box.fill_iid, ) return # currently selected is pointed at any selection box with "box" coordinates if isinstance(box, tuple): if r is None: r = box[0] if c is None: c = box[1] for item, selection_box in self.get_selection_items(reverse=True): r1, c1, r2, c2 = selection_box.coords if box == (r1, c1, r2, c2) and r1 <= r and c1 <= c and r2 >= r and c2 >= c: self.create_currently_selected_box( r, c, selection_box.type_, selection_box.fill_iid, ) return # currently selected is just pointed at a coordinate # find the top most box there, requires r and c if r is not None and c is not None: for item, selection_box in self.get_selection_items(reverse=True): r1, c1, r2, c2 = selection_box.coords if r1 <= r and c1 <= c and r2 >= r and c2 >= c: self.create_currently_selected_box( r, c, selection_box.type_, selection_box.fill_iid, ) return # wasn't provided an item and couldn't find a box at coords so select cell if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: self.select_cell(r, c, redraw=True) def set_current_to_last(self) -> None: if self.selection_boxes: box = next(iter(reversed(self.selection_boxes.values()))) r1, c1, r2, c2 = box.coords if r2 - r1 == 1 and c2 - c1 == 1: self.itemconfig(box.fill_iid, state="hidden") self.set_currently_selected(item=box.fill_iid) def coords_and_type(self, item: int) -> tuple: if item in self.selection_boxes: return Box_t(*(self.selection_boxes[item].coords + (self.selection_boxes[item].type_,))) return tuple() def get_selected_box_bg_fg(self, type_: str) -> tuple: if type_ == "cells": return self.PAR.ops.table_selected_cells_bg, self.PAR.ops.table_selected_box_cells_fg elif type_ == "rows": return self.PAR.ops.table_selected_rows_bg, self.PAR.ops.table_selected_box_rows_fg elif type_ == "columns": return self.PAR.ops.table_selected_columns_bg, self.PAR.ops.table_selected_box_columns_fg def create_currently_selected_box( self, r: int, c: int, type_: Literal["cells", "rows", "columns"], fill_iid: int, lower_selection_boxes: bool = True, ) -> int: fill, outline = self.get_selected_box_bg_fg(type_=type_) x1 = self.col_positions[c] + 1 y1 = self.row_positions[r] + 1 x2 = self.col_positions[c + 1] if index_exists(self.col_positions, c + 1) else self.col_positions[c] + 1 y2 = self.row_positions[r + 1] if index_exists(self.row_positions, r + 1) else self.row_positions[r] + 1 self.hide_selected() if self.PAR.ops.show_selected_cells_border: fill = "" else: fill = outline outline = "" iid = self.display_box( x1, y1, x2, y2, fill=fill, outline=outline, state="normal", tags="selected", width=2, ) self.selected = Selected( row=r, column=c, type_=type_, box=self.selection_boxes[fill_iid].coords, iid=iid, fill_iid=fill_iid, ) if lower_selection_boxes: self.lower_selection_boxes() return iid def display_box( self, x1: int, y1: int, x2: int, y2: int, fill: str, outline: str, state: str, tags: str | tuple[str], width: int, iid: None | int = None, ) -> int: if not self.PAR.ops.rounded_boxes or not x2 - x1 or not y2 - y1: radius = 0 else: radius = 5 coords = rounded_box_coords( x1, y1, x2, y2, radius=radius, ) if isinstance(iid, int): self.itemconfig(iid, fill=fill, outline=outline, state=state, tags=tags, width=width) self.coords(iid, coords) else: if self.hidd_boxes: iid = self.hidd_boxes.pop() self.itemconfig(iid, fill=fill, outline=outline, state=state, tags=tags, width=width) self.coords(iid, coords) else: iid = self.create_polygon( coords, fill=fill, outline=outline, state=state, tags=tags, width=width, smooth=True, ) self.disp_boxes.add(iid) return iid def hide_box(self, item: int | None) -> None: if isinstance(item, int): self.disp_boxes.discard(item) self.hidd_boxes.add(item) self.itemconfig(item, state="hidden") def hide_selection_box(self, item: int | None) -> bool: if item is None or item is True: return box = self.selection_boxes.pop(item) self.hide_box(box.fill_iid) self.hide_box(box.bd_iid) self.RI.hide_box(box.index) self.CH.hide_box(box.header) if self.selected.fill_iid == item: self.hide_selected() self.set_current_to_last() if item == self.being_drawn_item: self.being_drawn_item = None elif item == self.RI.being_drawn_item: self.RI.being_drawn_item = None elif item == self.CH.being_drawn_item: self.CH.being_drawn_item = None return True def hide_selected(self) -> None: if self.selected: self.hide_box(self.selected.iid) self.selected = tuple() def create_selection_box( self, r1: int, c1: int, r2: int, c2: int, type_: str = "cells", state: str = "normal", set_current: bool | tuple[int, int] = True, run_binding: bool = False, ext: bool = False, ) -> int: if self.col_positions == [0]: c1 = 0 c2 = 0 if self.row_positions == [0]: r1 = 0 r2 = 0 if type_ == "cells": mt_bg = self.PAR.ops.table_selected_cells_bg mt_border_col = self.PAR.ops.table_selected_cells_border_fg elif type_ == "rows": mt_bg = self.PAR.ops.table_selected_rows_bg mt_border_col = self.PAR.ops.table_selected_rows_border_fg elif type_ == "columns": mt_bg = self.PAR.ops.table_selected_columns_bg mt_border_col = self.PAR.ops.table_selected_columns_border_fg if self.selection_boxes: self.itemconfig(next(reversed(self.selection_boxes)), state="normal") x1, y1, x2, y2 = self.box_coords_x_canvas_coords(r1, c1, r2, c2, type_) fill_iid = self.display_box( x1, y1, x2, y2, fill=mt_bg, outline="", state=state if self.PAR.ops.show_selected_cells_border else "normal", tags=type_, width=1, ) index_iid = self.RI.display_box( 0, y1, self.RI.current_width - 1, y2, fill=self.PAR.ops.index_selected_rows_bg if type_ == "rows" else self.PAR.ops.index_selected_cells_bg, outline="", state="normal", tags="cells" if type_ == "columns" else type_, ) header_iid = self.CH.display_box( x1, 0, self.col_positions[c2], self.CH.current_height - 1, fill=( self.PAR.ops.header_selected_columns_bg if type_ == "columns" else self.PAR.ops.header_selected_cells_bg ), outline="", state="normal", tags="cells" if type_ == "rows" else type_, ) bd_iid = None if self.PAR.ops.show_selected_cells_border and ( ext or (self.being_drawn_item is None and self.RI.being_drawn_item is None and self.CH.being_drawn_item is None) or self.selection_boxes ): bd_iid = self.display_box( x1, y1, x2, y2, fill="", outline=mt_border_col, state="normal", tags=f"{type_}bd", width=1, ) self.tag_raise(bd_iid) self.selection_boxes[fill_iid] = SelectionBox( fill_iid=fill_iid, bd_iid=bd_iid, index=index_iid, header=header_iid, coords=Box_nt(r1, c1, r2, c2), type_=type_, ) if set_current: if set_current is True: curr_r = r1 curr_c = c1 elif isinstance(set_current, tuple): curr_r = set_current[0] curr_c = set_current[1] self.create_currently_selected_box(curr_r, curr_c, type_, fill_iid, lower_selection_boxes=False) self.lower_selection_boxes() if run_binding: self.run_selection_binding(type_) return fill_iid def lower_selection_boxes(self) -> None: if self.selected: if not self.PAR.ops.show_selected_cells_border: self.tag_lower(self.selected.iid) self.tag_lower("rows") self.tag_lower("columns") self.tag_lower("cells") self.RI.tag_lower("rows") self.RI.tag_lower("cells") self.CH.tag_lower("columns") self.CH.tag_lower("cells") if self.PAR.ops.show_selected_cells_border: self.tag_raise(self.selected.iid) def box_coords_x_canvas_coords( self, r1: int, c1: int, r2: int, c2: int, type_: Literal["cells", "rows", "columns"], ) -> tuple[float, float, float, float]: x1 = self.col_positions[c1] y1 = self.row_positions[r1] y2 = self.row_positions[r2] if type_ == "rows" and self.PAR.ops.selected_rows_to_end_of_window: x2 = self.canvasx(self.winfo_width()) else: x2 = self.col_positions[c2] return x1, y1, x2, y2 def recreate_selection_box( self, r1: int, c1: int, r2: int, c2: int, fill_iid: int, state: str = "", run_binding: bool = False, ) -> int: type_ = self.selection_boxes[fill_iid].type_ self.selection_boxes[fill_iid].coords = Box_nt(r1, c1, r2, c2) if type_ == "cells": mt_bg = self.PAR.ops.table_selected_cells_bg mt_border_col = self.PAR.ops.table_selected_cells_border_fg elif type_ == "rows": mt_bg = self.PAR.ops.table_selected_rows_bg mt_border_col = self.PAR.ops.table_selected_rows_border_fg elif type_ == "columns": mt_bg = self.PAR.ops.table_selected_columns_bg mt_border_col = self.PAR.ops.table_selected_columns_border_fg if not state: if r2 - r1 > 1 or c2 - c1 > 1: state = "normal" elif next(reversed(self.selection_boxes)) == fill_iid: state = "hidden" else: state = "normal" if self.selected.fill_iid == fill_iid: self.selected = self.selected._replace(box=Box_nt(r1, c1, r2, c2)) x1, y1, x2, y2 = self.box_coords_x_canvas_coords(r1, c1, r2, c2, type_) self.display_box(x1, y1, x2, y2, fill=mt_bg, outline="", state=state, tags=type_, width=1, iid=fill_iid) self.RI.display_box( 0, y1, self.RI.current_width - 1, y2, fill=self.PAR.ops.index_selected_rows_bg if type_ == "rows" else self.PAR.ops.index_selected_cells_bg, outline="", state="normal", tags="cells" if type_ == "columns" else type_, iid=self.selection_boxes[fill_iid].index, ) self.CH.display_box( x1, 0, self.col_positions[c2], self.CH.current_height - 1, fill=( self.PAR.ops.header_selected_columns_bg if type_ == "columns" else self.PAR.ops.header_selected_cells_bg ), outline="", state="normal", tags="cells" if type_ == "rows" else type_, iid=self.selection_boxes[fill_iid].header, ) if bd_iid := self.selection_boxes[fill_iid].bd_iid: if self.PAR.ops.show_selected_cells_border: self.display_box( x1, y1, x2, y2, fill="", outline=mt_border_col, state="normal", tags=f"{type_}bd", width=1, iid=bd_iid, ) self.tag_raise(bd_iid) else: self.hide_box(bd_iid) if run_binding: self.run_selection_binding(type_) return fill_iid def run_selection_binding(self, type_: str) -> None: if type_ == "cells": sel_event = self.get_select_event(being_drawn_item=self.being_drawn_item) if self.selection_binding_func: self.selection_binding_func(sel_event) elif type_ == "rows": sel_event = self.get_select_event(being_drawn_item=self.RI.being_drawn_item) if self.RI.selection_binding_func: self.RI.selection_binding_func(sel_event) elif type_ == "columns": sel_event = self.get_select_event(being_drawn_item=self.CH.being_drawn_item) if self.CH.selection_binding_func: self.CH.selection_binding_func(sel_event) self.PAR.emit_event("<>", data=sel_event) def recreate_all_selection_boxes(self) -> None: if not self.selected: return for item, box in self.get_selection_items(): r1, c1, r2, c2 = box.coords if r1 >= len(self.row_positions) - 1: if len(self.row_positions) > 1: r1 = len(self.row_positions) - 2 else: r1 = len(self.row_positions) - 1 if c1 >= len(self.col_positions) - 1: if len(self.col_positions) > 1: c1 = len(self.col_positions) - 2 else: c1 = len(self.col_positions) - 1 if r2 > len(self.row_positions) - 1: r2 = len(self.row_positions) - 1 if c2 > len(self.col_positions) - 1: c2 = len(self.col_positions) - 1 self.recreate_selection_box(r1, c1, r2, c2, item) if self.selected: r = self.selected.row c = self.selected.column if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: self.set_currently_selected(r, c, item=self.selected.fill_iid) else: box = self.selection_boxes[self.selected.fill_iid] self.set_currently_selected(box.coords.from_r, box.coords.from_c, item=box.fill_iid) def get_redraw_selections(self, startr: int, endr: int, startc: int, endc: int) -> dict: d = defaultdict(set) for item, box in self.get_selection_items(): r1, c1, r2, c2 = box.coords if box.type_ == "cells": for r in range(startr, endr): for c in range(startc, endc): if r1 <= r and c1 <= c and r2 > r and c2 > c: d["cells"].add((r, c)) elif box.type_ == "rows": for r in range(startr, endr): if r1 <= r and r2 > r: d["rows"].add(r) elif box.type_ == "columns": for c in range(startc, endc): if c1 <= c and c2 > c: d["columns"].add(c) return d def get_selected_min_max(self) -> tuple[int, int, int, int] | tuple[None, None, None, None]: min_x = float("inf") min_y = float("inf") max_x = 0 max_y = 0 for item, box in self.get_selection_items(): r1, c1, r2, c2 = box.coords if r1 < min_y: min_y = r1 if c1 < min_x: min_x = c1 if r2 > max_y: max_y = r2 if c2 > max_x: max_x = c2 if min_x != float("inf") and min_y != float("inf") and max_x > 0 and max_y > 0: return min_y, min_x, max_y, max_x return None, None, None, None def get_selected_rows( self, get_cells: bool = False, get_cells_as_rows: bool = False, ) -> set[int] | set[tuple[int, int]]: if get_cells: s = { (r, c) for item, box in self.get_selection_items(cells=False, columns=False) for r in range(box.coords.from_r, box.coords.upto_r) for c in range(0, len(self.col_positions) - 1) } if get_cells_as_rows: return s | self.get_selected_cells() else: s = { r for item, box in self.get_selection_items(cells=False, columns=False) for r in range(box.coords.from_r, box.coords.upto_r) } if get_cells_as_rows: return s | set(tup[0] for tup in self.get_selected_cells()) return s def get_selected_cols( self, get_cells: bool = False, get_cells_as_cols: bool = False, ) -> set[int] | set[tuple[int, int]]: if get_cells: s = { (r, c) for item, box in self.get_selection_items(cells=False, rows=False) for r in range(0, len(self.row_positions) - 1) for c in range(box.coords.from_c, box.coords.upto_c) } if get_cells_as_cols: return s | self.get_selected_cells() else: s = { c for item, box in self.get_selection_items(cells=False, rows=False) for c in range(box.coords.from_c, box.coords.upto_c) } if get_cells_as_cols: return s | set(tup[1] for tup in self.get_selected_cells()) return s def get_selected_cells( self, get_rows: bool = False, get_cols: bool = False, ) -> set[tuple[int, int]]: return { (r, c) for item, box in self.get_selection_items(rows=get_rows, columns=get_cols) for r in range(box.coords.from_r, box.coords.upto_r) for c in range(box.coords.from_c, box.coords.upto_c) } def gen_selected_cells( self, get_rows: bool = False, get_cols: bool = False, ) -> Generator[tuple[int, int]]: yield from ( (r, c) for item, box in self.get_selection_items(rows=get_rows, columns=get_cols) for r in range(box.coords.from_r, box.coords.upto_r) for c in range(box.coords.from_c, box.coords.upto_c) ) def get_all_selection_boxes(self) -> tuple[tuple[int, int, int, int]]: return tuple(box.coords for item, box in self.get_selection_items()) def get_all_selection_boxes_with_types(self) -> list[tuple[tuple[int, int, int, int], str]]: return [Box_st(box.coords, box.type_) for item, box in self.get_selection_items()] def all_selected(self) -> bool: return any( not r1 and not c1 and r2 == len(self.row_positions) - 1 and c2 == len(self.col_positions) - 1 for r1, c1, r2, c2 in self.get_all_selection_boxes() ) def cell_selected( self, r: int, c: int, inc_cols: bool = False, inc_rows: bool = False, ) -> bool: return ( isinstance(r, int) and isinstance(c, int) and any( box.coords.from_r <= r and box.coords.upto_r > r and box.coords.from_c <= c and box.coords.upto_c > c for item, box in self.get_selection_items( rows=inc_rows, columns=inc_cols, ) ) ) def col_selected(self, c: int, cells: bool = False) -> bool: return isinstance(c, int) and any( box.coords.from_c <= c and box.coords.upto_c > c for item, box in self.get_selection_items( cells=cells, rows=False, ) ) def row_selected(self, r: int, cells: bool = False) -> bool: return isinstance(r, int) and any( box.coords.from_r <= r and box.coords.upto_r > r for item, box in self.get_selection_items( cells=cells, columns=False, ) ) def anything_selected( self, exclude_columns: bool = False, exclude_rows: bool = False, exclude_cells: bool = False, ) -> list[int]: return [ item for item, box in self.get_selection_items( columns=not exclude_columns, rows=not exclude_rows, cells=not exclude_cells, ) ] def open_cell( self, event: object = None, ignore_existing_editor: bool = False, ) -> None: if not self.anything_selected() or (not ignore_existing_editor and self.text_editor.open): return if not self.selected: return r, c = self.selected.row, self.selected.column datacn = self.datacn(c) datarn = self.datarn(r) if self.get_cell_kwargs(datarn, datacn, key="readonly"): return elif self.get_cell_kwargs(datarn, datacn, key="dropdown") or self.get_cell_kwargs( datarn, datacn, key="checkbox" ): if self.event_opens_dropdown_or_checkbox(event): if self.get_cell_kwargs(datarn, datacn, key="dropdown"): self.open_dropdown_window(r, c, event=event) elif self.get_cell_kwargs(datarn, datacn, key="checkbox"): self.click_checkbox(r=r, c=c, datarn=datarn, datacn=datacn) else: self.open_text_editor(event=event, r=r, c=c, dropdown=False) def event_opens_dropdown_or_checkbox(self, event=None) -> bool: if event is None: return False elif event == "rc": return True elif ( (hasattr(event, "keysym") and event.keysym == "Return") or (hasattr(event, "keysym") and event.keysym == "F2") or ( event is not None and hasattr(event, "keycode") and event.keycode == "??" and hasattr(event, "num") and event.num == 1 ) # mouseclick or (hasattr(event, "keysym") and event.keysym == "BackSpace") ): return True else: return False # displayed indexes def get_cell_align(self, r: int, c: int) -> str: datarn = self.datarn(r) datacn = self.datacn(c) cell_alignment = self.get_cell_kwargs(datarn, datacn, key="align") if cell_alignment: return cell_alignment return self.align # displayed indexes def open_text_editor( self, event: object = None, r: int = 0, c: int = 0, text: str | None = None, state: str = "normal", dropdown: bool = False, ) -> bool: text = None extra_func_key = "??" if event is None or self.event_opens_dropdown_or_checkbox(event): if event is not None: if hasattr(event, "keysym") and event.keysym == "Return": extra_func_key = "Return" elif hasattr(event, "keysym") and event.keysym == "F2": extra_func_key = "F2" if event is not None and (hasattr(event, "keysym") and event.keysym == "BackSpace"): extra_func_key = "BackSpace" text = "" else: text = f"{self.get_cell_data(self.datarn(r), self.datacn(c), none_to_empty_str = True)}" elif event is not None and ( (hasattr(event, "char") and event.char.isalpha()) or (hasattr(event, "char") and event.char.isdigit()) or (hasattr(event, "char") and event.char in symbols_set) ): extra_func_key = event.char text = event.char else: return False if self.extra_begin_edit_cell_func: try: text = self.extra_begin_edit_cell_func( event_dict( name="begin_edit_table", sheet=self.PAR.name, key=extra_func_key, value=text, loc=Loc(r, c), row=r, column=c, boxes=self.get_boxes(), selected=self.selected, ) ) except Exception: return False if text is None: return False else: text = text if isinstance(text, str) else f"{text}" text = "" if text is None else text if self.PAR.ops.cell_auto_resize_enabled: self.set_cell_size_to_text(r, c, only_if_too_small=True, redraw=True, run_binding=True) if self.text_editor.open and (r, c) == self.text_editor.coords: self.text_editor.window.set_text(self.text_editor.get() + "" if not isinstance(text, str) else text) return self.hide_text_editor() if not self.see(r=r, c=c, check_cell_visibility=True): self.refresh() x = self.col_positions[c] y = self.row_positions[r] w = self.col_positions[c + 1] - x + 1 h = self.row_positions[r + 1] - y + 1 if text is None: text = f"{self.get_cell_data(self.datarn(r), self.datacn(c), none_to_empty_str = True)}" bg, fg = self.PAR.ops.table_bg, self.PAR.ops.table_fg kwargs = { "menu_kwargs": DotDict( { "font": self.PAR.ops.table_font, "foreground": self.PAR.ops.popup_menu_fg, "background": self.PAR.ops.popup_menu_bg, "activebackground": self.PAR.ops.popup_menu_highlight_bg, "activeforeground": self.PAR.ops.popup_menu_highlight_fg, } ), "sheet_ops": self.PAR.ops, "border_color": self.PAR.ops.table_selected_box_cells_fg, "text": text, "state": state, "width": w, "height": h, "show_border": True, "bg": bg, "fg": fg, "align": self.get_cell_align(r, c), "r": r, "c": c, } if not self.text_editor.window: self.text_editor.window = TextEditor(self, newline_binding=self.text_editor_newline_binding) self.text_editor.canvas_id = self.create_window((x, y), window=self.text_editor.window, anchor="nw") self.text_editor.window.reset(**kwargs) if not self.text_editor.open: self.itemconfig(self.text_editor.canvas_id, state="normal") self.text_editor.open = True self.coords(self.text_editor.canvas_id, x, y) for b in text_editor_newline_bindings: self.text_editor.tktext.bind(b, self.text_editor_newline_binding) for b in text_editor_close_bindings: self.text_editor.tktext.bind(b, self.close_text_editor) if not dropdown: self.text_editor.tktext.focus_set() self.text_editor.window.scroll_to_bottom() self.text_editor.tktext.bind("", self.close_text_editor) for key, func in self.text_editor_user_bound_keys.items(): self.text_editor.tktext.bind(key, func) return True # displayed indexes def text_editor_newline_binding( self, event: object = None, check_lines: bool = True, ) -> None: r, c = self.text_editor.coords curr_height = self.text_editor.window.winfo_height() if curr_height < self.min_row_height: return if ( not check_lines or self.get_lines_cell_height( self.text_editor.window.get_num_lines() + 1, font=self.text_editor.tktext.cget("font"), ) > curr_height ): new_height = curr_height + self.table_xtra_lines_increment space_bot = self.get_space_bot(r) if new_height > space_bot: new_height = space_bot if new_height != curr_height: self.text_editor.window.config(height=new_height) if self.dropdown.open and self.dropdown.get_coords() == (r, c): text_editor_h = self.text_editor.window.winfo_height() win_h, anchor = self.get_dropdown_height_anchor(r, c, text_editor_h) if anchor == "nw": self.coords( self.dropdown.canvas_id, self.col_positions[c], self.row_positions[r] + text_editor_h - 1, ) self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) elif anchor == "sw": self.coords( self.dropdown.canvas_id, self.col_positions[c], self.row_positions[r], ) self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) def refresh_open_window_positions(self, zoom: Literal["in", "out"]): if self.text_editor.open: r, c = self.text_editor.coords self.text_editor.window.config(height=self.row_positions[r + 1] - self.row_positions[r]) self.text_editor.tktext.config(font=self.PAR.ops.table_font) self.coords( self.text_editor.canvas_id, self.col_positions[c], self.row_positions[r], ) if self.dropdown.open: if zoom == "in": self.dropdown.window.zoom_in() elif zoom == "out": self.dropdown.window.zoom_out() r, c = self.dropdown.get_coords() if self.text_editor.open: text_editor_h = self.text_editor.window.winfo_height() win_h, anchor = self.get_dropdown_height_anchor(r, c, text_editor_h) else: text_editor_h = self.row_positions[r + 1] - self.row_positions[r] + 1 anchor = self.itemcget(self.dropdown.canvas_id, "anchor") # win_h = 0 self.dropdown.window.config(width=self.col_positions[c + 1] - self.col_positions[c] + 1) if anchor == "nw": self.coords( self.dropdown.canvas_id, self.col_positions[c], self.row_positions[r] + text_editor_h - 1, ) # self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) elif anchor == "sw": self.coords( self.dropdown.canvas_id, self.col_positions[c], self.row_positions[r], ) # self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) def hide_text_editor(self) -> None: if self.text_editor.open: for binding in text_editor_to_unbind: self.text_editor.tktext.unbind(binding) self.itemconfig(self.text_editor.canvas_id, state="hidden") self.text_editor.open = False def close_text_editor(self, event: tk.Event) -> Literal["break"] | None: # checking if text editor should be closed or not # errors if __tk_filedialog is open try: focused = self.focus_get() except Exception: focused = None try: if focused == self.text_editor.tktext.rc_popup_menu: return "break" except Exception: pass if focused is None: return "break" if event.keysym == "Escape": self.hide_text_editor_and_dropdown() self.focus_set() return # setting cell data with text editor value text_editor_value = self.text_editor.get() r, c = self.text_editor.coords datarn, datacn = self.datarn(r), self.datacn(c) event_data = event_dict( name="end_edit_table", sheet=self.PAR.name, widget=self, cells_table={(datarn, datacn): text_editor_value}, key=event.keysym, value=text_editor_value, loc=Loc(r, c), row=r, column=c, boxes=self.get_boxes(), selected=self.selected, ) edited = False set_data = partial( self.set_cell_data_undo, r=r, c=c, datarn=datarn, datacn=datacn, redraw=False, check_input_valid=False, ) if self.edit_validation_func: text_editor_value = self.edit_validation_func(event_data) if text_editor_value is not None and self.input_valid_for_cell(datarn, datacn, text_editor_value): edited = set_data(value=text_editor_value) elif self.input_valid_for_cell(datarn, datacn, text_editor_value): edited = set_data(value=text_editor_value) if edited: try_binding(self.extra_end_edit_cell_func, event_data) if ( r is not None and c is not None and self.selected and r == self.selected.row and c == self.selected.column and (self.single_selection_enabled or self.toggle_selection_enabled) and (edited or self.cell_equal_to(datarn, datacn, text_editor_value)) ): r1, c1, r2, c2 = self.selection_boxes[self.selected.fill_iid].coords numcols = c2 - c1 numrows = r2 - r1 if numcols == 1 and numrows == 1: if event.keysym == "Return": self.select_cell(r + 1 if r < len(self.row_positions) - 2 else r, c) self.see( r + 1 if r < len(self.row_positions) - 2 else r, c, keep_xscroll=True, bottom_right_corner=True, check_cell_visibility=True, ) elif event.keysym == "Tab": self.select_cell(r, c + 1 if c < len(self.col_positions) - 2 else c) self.see( r, c + 1 if c < len(self.col_positions) - 2 else c, keep_xscroll=True, bottom_right_corner=True, check_cell_visibility=True, ) else: moved = False new_r = r new_c = c if event.keysym == "Return": if r + 1 == r2: new_r = r1 elif numrows > 1: new_r = r + 1 moved = True if not moved: if c + 1 == c2: new_c = c1 elif numcols > 1: new_c = c + 1 elif event.keysym == "Tab": if c + 1 == c2: new_c = c1 elif numcols > 1: new_c = c + 1 moved = True if not moved: if r + 1 == r2: new_r = r1 elif numrows > 1: new_r = r + 1 self.set_currently_selected(new_r, new_c, item=self.selected.fill_iid) self.see( new_r, new_c, keep_xscroll=False, bottom_right_corner=True, check_cell_visibility=True, ) self.recreate_all_selection_boxes() self.hide_text_editor_and_dropdown() if event.keysym != "FocusOut": self.focus_set() return "break" def tab_key(self, event: object = None) -> str: if not self.selected: return r, c = self.selected.row, self.selected.column r1, c1, r2, c2 = self.selection_boxes[self.selected.fill_iid].coords numcols = c2 - c1 numrows = r2 - r1 if numcols == 1 and numrows == 1: new_r = r new_c = c + 1 if c < len(self.col_positions) - 2 else c self.select_cell(new_r, new_c) else: moved = False new_r = r new_c = c if c + 1 == c2: new_c = c1 elif numcols > 1: new_c = c + 1 moved = True if not moved: if r + 1 == r2: new_r = r1 elif numrows > 1: new_r = r + 1 self.set_currently_selected(new_r, new_c, item=self.selected.fill_iid) self.see( new_r, new_c, keep_xscroll=False, bottom_right_corner=True, check_cell_visibility=True, ) return "break" def get_space_bot(self, r: int, text_editor_h: int | None = None) -> int: if len(self.row_positions) <= 1: if text_editor_h is None: win_h = int(self.winfo_height()) sheet_h = int(1 + self.PAR.ops.empty_vertical) else: win_h = int(self.winfo_height() - text_editor_h) sheet_h = int(1 + self.PAR.ops.empty_vertical - text_editor_h) else: if text_editor_h is None: win_h = int(self.canvasy(0) + self.winfo_height() - self.row_positions[r + 1]) sheet_h = int(self.row_positions[-1] + 1 + self.PAR.ops.empty_vertical - self.row_positions[r + 1]) else: win_h = int(self.canvasy(0) + self.winfo_height() - (self.row_positions[r] + text_editor_h)) sheet_h = int( self.row_positions[-1] + 1 + self.PAR.ops.empty_vertical - (self.row_positions[r] + text_editor_h) ) if win_h > 0: win_h -= 1 if sheet_h > 0: sheet_h -= 1 return win_h if win_h >= sheet_h else sheet_h def get_dropdown_height_anchor(self, r: int, c: int, text_editor_h: int | None = None) -> tuple: win_h = 5 datarn, datacn = self.datarn(r), self.datacn(c) for i, v in enumerate(self.get_cell_kwargs(datarn, datacn, key="dropdown")["values"]): v_numlines = len(v.split("\n") if isinstance(v, str) else f"{v}".split("\n")) if v_numlines > 1: win_h += self.table_first_ln_ins + (v_numlines * self.table_xtra_lines_increment) + 5 # end of cell else: win_h += self.min_row_height if i == 5: break if win_h > 500: win_h = 500 space_bot = self.get_space_bot(r, text_editor_h) space_top = int(self.row_positions[r]) anchor = "nw" win_h2 = int(win_h) if win_h > space_bot: if space_bot >= space_top: anchor = "nw" win_h = space_bot - 1 elif space_top > space_bot: anchor = "sw" win_h = space_top - 1 if win_h < self.table_txt_height + 5: win_h = self.table_txt_height + 5 elif win_h > win_h2: win_h = win_h2 return win_h, anchor def dropdown_text_editor_modified( self, dd_window: object, event: dict, modified_func: Callable | None, ) -> None: if modified_func: modified_func(event) dd_window.search_and_see(event) # c is displayed col def open_dropdown_window( self, r: int, c: int, event: object = None, ) -> None: self.hide_text_editor() datarn = self.datarn(r) datacn = self.datacn(c) kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown") if kwargs["state"] == "normal": if not self.open_text_editor(event=event, r=r, c=c, dropdown=True): return win_h, anchor = self.get_dropdown_height_anchor(r, c) win_w = self.col_positions[c + 1] - self.col_positions[c] + 1 if anchor == "nw": if kwargs["state"] == "normal": self.text_editor.window.update_idletasks() ypos = self.row_positions[r] + self.text_editor.window.winfo_height() - 1 else: ypos = self.row_positions[r + 1] else: ypos = self.row_positions[r] reset_kwargs = { "r": r, "c": c, "width": win_w, "height": win_h, "font": self.PAR.ops.table_font, "ops": self.PAR.ops, "outline_color": self.get_selected_box_bg_fg(type_="cells")[1], "align": self.get_cell_align(r, c), "values": kwargs["values"], } if self.dropdown.window: self.dropdown.window.reset(**reset_kwargs) self.coords(self.dropdown.canvas_id, self.col_positions[c], ypos) self.itemconfig(self.dropdown.canvas_id, state="normal", anchor=anchor) self.dropdown.window.tkraise() else: self.dropdown.window = self.PAR.dropdown_class( self.winfo_toplevel(), **reset_kwargs, close_dropdown_window=self.close_dropdown_window, search_function=kwargs["search_function"], arrowkey_RIGHT=self.arrowkey_RIGHT, arrowkey_LEFT=self.arrowkey_LEFT, ) self.dropdown.canvas_id = self.create_window( (self.col_positions[c], ypos), window=self.dropdown.window, anchor=anchor, ) if kwargs["state"] == "normal": self.text_editor.tktext.bind( "<>", lambda _: self.dropdown.window.search_and_see( event_dict( name="table_dropdown_modified", sheet=self.PAR.name, value=self.text_editor.get(), loc=Loc(r, c), row=r, column=c, boxes=self.get_boxes(), selected=self.selected, ) ), ) if kwargs["modified_function"] is not None: self.dropdown.window.modified_function = kwargs["modified_function"] self.update_idletasks() try: self.after(1, lambda: self.text_editor.tktext.focus()) self.after(2, self.text_editor.window.scroll_to_bottom()) except Exception: return redraw = False else: self.update_idletasks() self.dropdown.window.bind("", lambda _: self.close_dropdown_window(r, c)) self.dropdown.window.bind("", self.close_dropdown_window) self.dropdown.window.focus_set() redraw = True self.dropdown.open = True if redraw: self.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=False) # displayed indexes, not data def close_dropdown_window( self, r: int | None = None, c: int | None = None, selection: object = None, redraw: bool = True, ) -> None: if r is not None and c is not None and selection is not None: datacn = self.datacn(c) datarn = self.datarn(r) kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown") pre_edit_value = self.get_cell_data(datarn, datacn) event_data = event_dict( name="end_edit_table", sheet=self.PAR.name, widget=self, cells_table={(datarn, datacn): pre_edit_value}, key="??", value=selection, loc=Loc(r, c), row=r, column=c, boxes=self.get_boxes(), selected=self.selected, ) if kwargs["select_function"] is not None: kwargs["select_function"](event_data) if self.edit_validation_func: selection, edited = self.edit_validation_func(event_data), False if selection is not None: edited = self.set_cell_data_undo( r, c, datarn=datarn, datacn=datacn, value=selection, redraw=not redraw, ) else: edited = self.set_cell_data_undo( r, c, datarn=datarn, datacn=datacn, value=selection, redraw=not redraw, ) if edited: try_binding(self.extra_end_edit_cell_func, event_data) self.recreate_all_selection_boxes() self.focus_set() self.hide_text_editor_and_dropdown(redraw=redraw) def hide_text_editor_and_dropdown(self, redraw: bool = True) -> None: self.hide_text_editor() self.hide_dropdown_window() if redraw: self.refresh() def mouseclick_outside_editor_or_dropdown(self) -> tuple[int, int] | None: closed_dd_coords = self.dropdown.get_coords() if self.text_editor.open: self.close_text_editor(new_tk_event("ButtonPress-1")) self.hide_dropdown_window() self.focus_set() return closed_dd_coords def mouseclick_outside_editor_or_dropdown_all_canvases(self): self.CH.mouseclick_outside_editor_or_dropdown() self.RI.mouseclick_outside_editor_or_dropdown() return self.mouseclick_outside_editor_or_dropdown() def hide_dropdown_editor_all_canvases(self): self.hide_text_editor_and_dropdown(redraw=False) self.RI.hide_text_editor_and_dropdown(redraw=False) self.CH.hide_text_editor_and_dropdown(redraw=False) def hide_dropdown_window(self) -> None: if self.dropdown.open: self.dropdown.window.unbind("") self.itemconfig(self.dropdown.canvas_id, state="hidden") self.dropdown.open = False def click_checkbox( self, r: int, c: int, datarn: int | None = None, datacn: int | None = None, undo: bool = True, redraw: bool = True, ) -> None: if datarn is None: datarn = self.datarn(r) if datacn is None: datacn = self.datacn(c) kwargs = self.get_cell_kwargs(datarn, datacn, key="checkbox") if kwargs["state"] == "normal": pre_edit_value = self.get_cell_data(datarn, datacn) value = not self.data[datarn][datacn] if isinstance(self.data[datarn][datacn], bool) else False self.set_cell_data_undo( r, c, value=value, undo=undo, cell_resize=False, check_input_valid=False, ) event_data = event_dict( name="end_edit_table", sheet=self.PAR.name, widget=self, cells_table={(datarn, datacn): pre_edit_value}, key="??", value=value, loc=Loc(r, c), row=r, column=c, boxes=self.get_boxes(), selected=self.selected, ) if kwargs["check_function"] is not None: kwargs["check_function"](event_data) try_binding(self.extra_end_edit_cell_func, event_data) if redraw: self.refresh() # internal event use def set_cell_data_undo( self, r: int = 0, c: int = 0, datarn: int | None = None, datacn: int | None = None, value: str | None = None, undo: bool = True, cell_resize: bool = True, redraw: bool = True, check_input_valid: bool = True, ) -> bool: if value is None: value = "" if datacn is None: datacn = self.datacn(c) if datarn is None: datarn = self.datarn(r) event_data = event_dict( name="edit_table", sheet=self.PAR.name, widget=self, cells_table={(datarn, datacn): self.get_cell_data(datarn, datacn)}, boxes=self.get_boxes(), selected=self.selected, ) if not check_input_valid or self.input_valid_for_cell(datarn, datacn, value): if self.undo_enabled and undo: self.undo_stack.append(pickled_event_dict(event_data)) self.set_cell_data(datarn, datacn, value) if cell_resize and self.PAR.ops.cell_auto_resize_enabled: self.set_cell_size_to_text(r, c, only_if_too_small=True, redraw=redraw, run_binding=True) self.sheet_modified(event_data) return True return False def set_cell_data( self, datarn: int, datacn: int, value: object, kwargs: dict = {}, expand_sheet: bool = True, ) -> None: if expand_sheet: if datarn >= len(self.data): self.fix_data_len(datarn, datacn) elif datacn >= len(self.data[datarn]): self.fix_row_len(datarn, datacn) if expand_sheet or (len(self.data) > datarn and len(self.data[datarn]) > datacn): if ( datarn, datacn, ) in self.cell_options and "checkbox" in self.cell_options[(datarn, datacn)]: self.data[datarn][datacn] = try_to_bool(value) else: if not kwargs: kwargs = self.get_cell_kwargs(datarn, datacn, key="format") if kwargs: if kwargs["formatter"] is None: self.data[datarn][datacn] = format_data(value=value, **kwargs) else: self.data[datarn][datacn] = kwargs["formatter"](value, **kwargs) else: self.data[datarn][datacn] = value def get_value_for_empty_cell(self, datarn: int, datacn: int, r_ops: bool = True, c_ops: bool = True) -> object: if self.get_cell_kwargs( datarn, datacn, key="checkbox", cell=r_ops and c_ops, row=r_ops, column=c_ops, ): return False kwargs = self.get_cell_kwargs( datarn, datacn, key="dropdown", cell=r_ops and c_ops, row=r_ops, column=c_ops, ) if kwargs and kwargs["validate_input"] and kwargs["values"]: return kwargs["values"][0] return "" def get_empty_row_seq( self, datarn: int, end: int, start: int = 0, r_ops: bool = True, c_ops: bool = True, ) -> list[object]: return [self.get_value_for_empty_cell(datarn, datacn, r_ops=r_ops, c_ops=c_ops) for datacn in range(start, end)] def fix_row_len(self, datarn: int, datacn: int) -> None: self.data[datarn].extend(self.get_empty_row_seq(datarn, end=datacn + 1, start=len(self.data[datarn]))) def fix_row_values(self, datarn: int, start: int | None = None, end: int | None = None): if datarn < len(self.data): for datacn, v in enumerate(islice(self.data[datarn], start, end)): if not self.input_valid_for_cell(datarn, datacn, v): self.data[datarn][datacn] = self.get_value_for_empty_cell(datarn, datacn) def fix_data_len(self, datarn: int, datacn: int | None = None) -> int: ncols = self.total_data_cols() if datacn is None else datacn + 1 self.data.extend(self.get_empty_row_seq(rn, end=ncols, start=0) for rn in range(len(self.data), datarn + 1)) return len(self.data) def reapply_formatting(self) -> None: for c in gen_formatted(self.col_options): for r in range(len(self.data)): if not ( (r, c) in self.cell_options and "format" in self.cell_options[(r, c)] or r in self.row_options and "format" in self.row_options[r] ): self.set_cell_data(r, c, value=self.data[r][c]) for r in gen_formatted(self.row_options): for c in range(len(self.data[r])): if not ((r, c) in self.cell_options and "format" in self.cell_options[(r, c)]): self.set_cell_data(r, c, value=self.data[r][c]) for r, c in gen_formatted(self.cell_options): if len(self.data) > r and len(self.data[r]) > c: self.set_cell_data(r, c, value=self.data[r][c]) def delete_all_formatting(self, clear_values: bool = False) -> None: self.delete_cell_format("all", clear_values=clear_values) self.delete_row_format("all", clear_values=clear_values) self.delete_column_format("all", clear_values=clear_values) def delete_cell_format( self, datarn: Literal["all"] | int = "all", datacn: int = 0, clear_values: bool = False, ) -> None: if isinstance(datarn, str) and datarn.lower() == "all": itr = gen_formatted(self.cell_options) else: itr = ((datarn, datacn),) get_val = self.get_value_for_empty_cell for key in itr: try: del self.cell_options[key]["format"] except Exception: continue if clear_values: self.set_cell_data(*key, get_val(*key), expand_sheet=False) def delete_row_format(self, datarn: Literal["all"] | int = "all", clear_values: bool = False) -> None: if isinstance(datarn, str) and datarn.lower() == "all": itr = gen_formatted(self.row_options) else: itr = (datarn,) get_val = self.get_value_for_empty_cell for datarn in itr: try: del self.row_options[datarn]["format"] except Exception: continue if clear_values: for datacn in range(len(self.data[datarn])): self.set_cell_data(datarn, datacn, get_val(datarn, datacn), expand_sheet=False) def delete_column_format(self, datacn: Literal["all"] | int = "all", clear_values: bool = False) -> None: if isinstance(datacn, str) and datacn.lower() == "all": itr = gen_formatted(self.col_options) else: itr = (datacn,) get_val = self.get_value_for_empty_cell for datacn in itr: try: del self.col_options[datacn]["format"] except Exception: continue if clear_values: for datarn in range(len(self.data)): self.set_cell_data(datarn, datacn, get_val(datarn, datacn), expand_sheet=False) # deals with possibility of formatter class being in self.data cell # if cell is formatted - possibly returns invalid_value kwarg if # cell value is not in datatypes kwarg # if get displayed is true then Nones are replaced by "" def get_valid_cell_data_as_str(self, datarn: int, datacn: int, get_displayed: bool = False, **kwargs) -> str: if get_displayed: kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown") if kwargs: if kwargs["text"] is not None: return f"{kwargs['text']}" else: kwargs = self.get_cell_kwargs(datarn, datacn, key="checkbox") if kwargs: return f"{kwargs['text']}" value = self.data[datarn][datacn] if len(self.data) > datarn and len(self.data[datarn]) > datacn else "" kwargs = self.get_cell_kwargs(datarn, datacn, key="format") if kwargs: if kwargs["formatter"] is None: if get_displayed: return data_to_str(value, **kwargs) else: return f"{get_data_with_valid_check(value, **kwargs)}" else: if get_displayed: # assumed given formatter class has __str__() return f"{value}" else: # assumed given formatter class has get_data_with_valid_check() return f"{value.get_data_with_valid_check()}" return "" if value is None else f"{value}" def get_cell_data( self, datarn: int, datacn: int, get_displayed: bool = False, none_to_empty_str: bool = False, fmt_kw: dict | None = None, **kwargs, ) -> object: if get_displayed: return self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True) value = self.data[datarn][datacn] if len(self.data) > datarn and len(self.data[datarn]) > datacn else "" kwargs = self.get_cell_kwargs(datarn, datacn, key="format") if kwargs and kwargs["formatter"] is not None: value = value.value # assumed given formatter class has value attribute if isinstance(fmt_kw, dict): value = format_data(value=value, **fmt_kw) return "" if (value is None and none_to_empty_str) else value def input_valid_for_cell( self, datarn: int, datacn: int, value: object, check_readonly: bool = True, ignore_empty: bool = False, ) -> bool: if check_readonly and self.get_cell_kwargs(datarn, datacn, key="readonly"): return False if self.get_cell_kwargs(datarn, datacn, key="format"): return True if self.cell_equal_to(datarn, datacn, value, ignore_empty=ignore_empty): return False kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown") if kwargs and kwargs["validate_input"] and value not in kwargs["values"]: return False if self.get_cell_kwargs(datarn, datacn, key="checkbox"): return is_bool_like(value) return True def cell_equal_to(self, datarn: int, datacn: int, value: object, ignore_empty: bool = False, **kwargs) -> bool: v = self.get_cell_data(datarn, datacn) kwargs = self.get_cell_kwargs(datarn, datacn, key="format") if kwargs and kwargs["formatter"] is None: if ignore_empty: if not (x := format_data(value=value, **kwargs)) and not v: return False return v == x return v == format_data(value=value, **kwargs) # assumed if there is a formatter class in cell then it has a # __eq__() function anyway # else if there is not a formatter class in cell and cell is not formatted # then compare value as is if ignore_empty and not v and not value: return False return v == value def get_cell_clipboard(self, datarn: int, datacn: int) -> str | int | float | bool: value = self.data[datarn][datacn] if len(self.data) > datarn and len(self.data[datarn]) > datacn else "" kwargs = self.get_cell_kwargs(datarn, datacn, key="format") if kwargs: if kwargs["formatter"] is None: return get_clipboard_data(value, **kwargs) else: # assumed given formatter class has get_clipboard_data() function # and it returns one of above type hints return value.get_clipboard_data() return f"{value}" def get_cell_kwargs( self, datarn: int, datacn: int, key: Hashable = "format", cell: bool = True, row: bool = True, column: bool = True, ) -> dict: if cell and (datarn, datacn) in self.cell_options and key in self.cell_options[(datarn, datacn)]: return self.cell_options[(datarn, datacn)][key] if row and datarn in self.row_options and key in self.row_options[datarn]: return self.row_options[datarn][key] if column and datacn in self.col_options and key in self.col_options[datacn]: return self.col_options[datacn][key] return {} def datacn(self, c: int) -> int: return c if self.all_columns_displayed else self.displayed_columns[c] def datarn(self, r: int) -> int: return r if self.all_rows_displayed else self.displayed_rows[r] def dispcn(self, datacn: int) -> int: return datacn if self.all_columns_displayed else b_index(self.displayed_columns, datacn) def try_dispcn(self, datacn: int) -> int | None: try: return self.dispcn(datacn) except Exception: return None def disprn(self, datarn: int) -> int: return datarn if self.all_rows_displayed else b_index(self.displayed_rows, datarn) def try_disprn(self, datarn: int) -> int | None: try: return self.disprn(datarn) except Exception: return None tksheet-7.2.12/tksheet/other_classes.py000066400000000000000000000353671463432073300201540ustar00rootroot00000000000000from __future__ import annotations import pickle from collections import namedtuple from collections.abc import Callable, Generator, Hashable, Iterator from functools import partial from typing import Literal pickle_obj = partial(pickle.dumps, protocol=pickle.HIGHEST_PROTOCOL) FontTuple = namedtuple("FontTuple", "family size style") Box_nt = namedtuple( "Box_nt", "from_r from_c upto_r upto_c", ) Box_t = namedtuple( "Box_t", "from_r from_c upto_r upto_c type_", ) Box_st = namedtuple("Box_st", "coords type_") Loc = namedtuple("Loc", "row column") Highlight = namedtuple( "Highlight", ( "bg", "fg", "end", # only used for row options highlights ), defaults=( None, None, False, ), ) DrawnItem = namedtuple("DrawnItem", "iid showing") TextCfg = namedtuple("TextCfg", "txt tf font align") DraggedRowColumn = namedtuple("DraggedRowColumn", "dragged to_move") def num2alpha(n: int) -> str | None: try: s = "" n += 1 while n > 0: n, r = divmod(n - 1, 26) s = chr(65 + r) + s return s except Exception: return None class SpanRange: def __init__(self, from_: int, upto_: int) -> None: __slots__ = ("from_", "upto_") # noqa: F841 self.from_ = from_ self.upto_ = upto_ def __iter__(self) -> Iterator: return iter(range(self.from_, self.upto_)) def __reversed__(self) -> Iterator: return reversed(range(self.from_, self.upto_)) def __contains__(self, n: int) -> bool: if n >= self.from_ and n < self.upto_: return True return False def __eq__(self, v: SpanRange) -> bool: return self.from_ == v.from_ and self.upto_ == v.upto_ def __ne__(self, v: SpanRange) -> bool: return self.from_ != v.from_ or self.upto_ != v.upto_ def __len__(self) -> int: return self.upto_ - self.from_ class DotDict(dict): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) # Recursively turn nested dicts into DotDicts for key, value in self.items(): if type(value) is dict: # noqa: E721 self[key] = DotDict(value) def __getstate__(self) -> DotDict: return self def __setstate__(self, state: DotDict) -> None: self.update(state) def __setitem__(self, key: Hashable, item: object) -> None: if type(item) is dict: # noqa: E721 super().__setitem__(key, DotDict(item)) else: super().__setitem__(key, item) __setattr__ = __setitem__ __getattr__ = dict.__getitem__ __delattr__ = dict.__delitem__ class EventDataDict(DotDict): """ A subclass of DotDict with no changes For better clarity in type hinting """ class Span(dict): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) # Recursively turn nested dicts into DotDicts for key, item in self.items(): if key == "data" or key == "value": self["widget"].set_data(self, data=item) elif type(item) is dict: # noqa: E721 self[key] = DotDict(item) def __getstate__(self) -> Span: return self def __setstate__(self, state: Span) -> None: self.update(state) def __getitem__(self, key: Hashable) -> object: if key == "data" or key == "value": return self["widget"].get_data(self) else: return super().__getitem__(key) def __setitem__(self, key: Hashable, item: object) -> None: if key == "data" or key == "value": self["widget"].set_data(self, data=item) elif key == "bg": self["widget"].highlight(self, bg=item) elif key == "fg": self["widget"].highlight(self, fg=item) elif key == "align": self["widget"].align(self, align=item) elif type(item) is dict: # noqa: E721 super().__setitem__(key, DotDict(item)) else: super().__setitem__(key, item) def format( self, formatter_options: dict = {}, formatter_class: object = None, redraw: bool = True, **kwargs, ) -> Span: return self["widget"].format( self, formatter_options={"formatter": formatter_class, **formatter_options, **kwargs}, formatter_class=formatter_class, redraw=redraw, **kwargs, ) def del_format(self) -> Span: return self["widget"].del_format(self) def highlight( self, bg: bool | None | str = False, fg: bool | None | str = False, end: bool | None = None, overwrite: bool = False, redraw: bool = True, ) -> Span: return self["widget"].highlight( self, bg=bg, fg=fg, end=end, overwrite=overwrite, redraw=redraw, ) def dehighlight(self, redraw: bool = True) -> Span: return self["widget"].dehighlight(self, redraw=redraw) del_highlight = dehighlight def readonly(self, readonly: bool = True) -> Span: return self["widget"].readonly(self, readonly=readonly) def dropdown( self, values: list = [], edit_data: bool = True, set_values: dict[tuple[int, int], object] = {}, set_value: object = None, state: str = "normal", redraw: bool = True, selection_function: Callable | None = None, modified_function: Callable | None = None, search_function: Callable | None = None, validate_input: bool = True, text: None | str = None, ) -> Span: return self["widget"].dropdown( self, values=values, edit_data=edit_data, set_values=set_values, set_value=set_value, state=state, redraw=redraw, selection_function=selection_function, modified_function=modified_function, search_function=search_function, validate_input=validate_input, text=text, ) def del_dropdown(self) -> Span: return self["widget"].del_dropdown(self) def checkbox( self, edit_data: bool = True, checked: bool | None = None, state: str = "normal", redraw: bool = True, check_function: Callable | None = None, text: str = "", ) -> Span: return self["widget"].checkbox( self, edit_data=edit_data, checked=checked, state=state, redraw=redraw, check_function=check_function, text=text, ) def del_checkbox(self) -> Span: return self["widget"].del_checkbox(self) def align(self, align: str | None, redraw: bool = True) -> Span: return self["widget"].align(self, align=align, redraw=redraw) def del_align(self, redraw: bool = True) -> Span: return self["widget"].del_align(self, redraw=redraw) def clear(self, undo: bool | None = None, redraw: bool = True) -> Span: if undo is not None: self["widget"].clear(self, undo=undo, redraw=redraw) else: self["widget"].clear(self, redraw=redraw) return self def tag(self, *tags) -> Span: self["widget"].tag(self, tags=tags) return self def untag(self) -> Span: if self.kind == "cell": for r in self.rows: for c in self.columns: self["widget"].untag(cell=(r, c)) elif self.kind == "row": self["widget"].untag(rows=self.rows) elif self.kind == "column": self["widget"].untag(columns=self.columns) return self def options( self, type_: str | None = None, name: str | None = None, table: bool | None = None, index: bool | None = None, header: bool | None = None, tdisp: bool | None = None, idisp: bool | None = None, hdisp: bool | None = None, transposed: bool | None = None, ndim: int | None = None, convert: Callable | None = None, undo: bool | None = None, emit_event: bool | None = None, widget: object = None, expand: str | None = None, formatter_options: dict | None = None, **kwargs, ) -> Span: if isinstance(expand, str) and expand.lower() in ("down", "right", "both", "table"): self.expand(expand) if isinstance(convert, Callable): self["convert"] = convert if isinstance(type_, str): self["type_"] = type_.lower() if isinstance(name, str): if isinstance(name, str) and not name: name = f"{num2alpha(self['widget'].named_span_id)}" self["widget"].named_span_id += 1 self["name"] = name if isinstance(table, bool): self["table"] = table if isinstance(index, bool): self["index"] = index if isinstance(header, bool): self["header"] = header if isinstance(transposed, bool): self["transposed"] = transposed if isinstance(tdisp, bool): self["tdisp"] = tdisp if isinstance(idisp, bool): self["idisp"] = idisp if isinstance(hdisp, bool): self["hdisp"] = hdisp if isinstance(undo, bool): self["undo"] = undo if isinstance(emit_event, bool): self["emit_event"] = emit_event if isinstance(ndim, int) and ndim in (0, 1, 2): self["ndim"] = ndim if isinstance(formatter_options, dict): self["type_"] = "format" self["kwargs"] = {"formatter": None, **formatter_options} elif kwargs: self["kwargs"] = kwargs if widget is not None: self["widget"] = widget return self def transpose(self) -> Span: self["transposed"] = not self["transposed"] return self def expand(self, direction: Literal["both", "table", "down", "right"] = "both") -> Span: if direction == "both" or direction == "table": self["upto_r"], self["upto_c"] = None, None elif direction == "down": self["upto_r"] = None elif direction == "right": self["upto_c"] = None else: raise ValueError(f"Expand argument must be either 'both', 'table', 'down' or 'right'. Not {direction}") return self @property def kind(self) -> str: if self["from_r"] is None: return "column" if self["from_c"] is None: return "row" return "cell" @property def rows(self) -> Generator[int]: rng_from_r = 0 if self["from_r"] is None else self["from_r"] if self["upto_r"] is None: rng_upto_r = self["widget"].total_rows() else: rng_upto_r = self["upto_r"] return SpanRange(rng_from_r, rng_upto_r) @property def columns(self) -> Generator[int]: rng_from_c = 0 if self["from_c"] is None else self["from_c"] if self["upto_c"] is None: rng_upto_c = self["widget"].total_columns() else: rng_upto_c = self["upto_c"] return SpanRange(rng_from_c, rng_upto_c) def pickle_self(self) -> bytes: x = self["widget"] self["widget"] = None p = pickle_obj(self) self["widget"] = x return p __setattr__ = __setitem__ __getattr__ = __getitem__ __delattr__ = dict.__delitem__ class GeneratedMouseEvent: def __init__(self): self.keycode = "??" self.num = 1 class Node: __slots__ = ("text", "iid", "parent", "children") def __init__( self, text: str, iid: str, parent: Node | Literal[""] | None = None, ) -> None: self.text = text self.iid = iid self.parent = parent self.children = [] def __str__(self) -> str: return self.text class DropdownStorage: __slots__ = ("canvas_id", "window", "open") def __init__(self) -> None: self.canvas_id = None self.window = None self.open = False def get_coords(self) -> int | tuple[int, int] | None: """ Returns None if not open or window is None """ if self.open and self.window is not None: return self.window.get_coords() return None class TextEditorStorage: __slots__ = ("canvas_id", "window", "open") def __init__(self) -> None: self.canvas_id = None self.window = None self.open = False def focus(self) -> None: if self.window: self.window.tktext.focus_set() def get(self) -> str: if self.window: return self.window.get() return "" @property def tktext(self) -> object: if self.window: return self.window.tktext return self.window @property def coords(self) -> tuple[int, int]: return self.window.r, self.window.c @property def row(self) -> int: return self.window.r @property def column(self) -> int: return self.window.c class SelectionBox: __slots__ = ("fill_iid", "bd_iid", "index", "header", "coords", "type_") def __init__( self, fill_iid: int | None = None, bd_iid: int | None = None, index: int | None = None, header: int | None = None, coords: tuple[int, int, int, int] = None, type_: Literal["cells", "rows", "columns"] = "cells", ) -> None: self.fill_iid = fill_iid self.bd_iid = bd_iid self.index = index self.header = header self.coords = coords self.type_ = type_ Selected = namedtuple( "Selected", ( "row", "column", "type_", "box", "iid", "fill_iid", ), defaults=( None, None, None, None, None, None, ), ) class ProgressBar: __slots__ = ("bg", "fg", "name", "percent", "del_when_done") def __init__(self, bg: str, fg: str, name: Hashable, percent: int, del_when_done: bool) -> None: self.bg = bg self.fg = fg self.name = name self.percent = percent self.del_when_done = del_when_done def __len__(self): return 2 def __getitem__(self, key: Hashable) -> object: if key == 0: return self.bg elif key == 1: return self.fg elif key == 2: return self.name elif key == 3: return self.percent elif key == 4: return self.del_when_done else: return self.__getattribute__(key) tksheet-7.2.12/tksheet/row_index.py000066400000000000000000003200361463432073300173020ustar00rootroot00000000000000from __future__ import annotations import tkinter as tk from collections import defaultdict from collections.abc import ( Callable, Generator, Hashable, Iterator, Sequence, ) from functools import ( partial, ) from itertools import ( chain, cycle, islice, repeat, ) from math import ( ceil, floor, ) from operator import ( itemgetter, ) from typing import Literal from .colors import ( color_map, ) from .formatters import ( is_bool_like, try_to_bool, ) from .functions import ( consecutive_chunks, event_dict, get_n2a, is_contiguous, new_tk_event, num2alpha, pickled_event_dict, rounded_box_coords, try_binding, ) from .other_classes import ( DotDict, DraggedRowColumn, DropdownStorage, Node, TextEditorStorage, ) from .text_editor import ( TextEditor, ) from .vars import ( rc_binding, symbols_set, text_editor_close_bindings, text_editor_newline_bindings, text_editor_to_unbind, ) class RowIndex(tk.Canvas): def __init__(self, *args, **kwargs): tk.Canvas.__init__( self, kwargs["parent"], background=kwargs["parent"].ops.index_bg, highlightthickness=0, ) self.PAR = kwargs["parent"] self.MT = None # is set from within MainTable() __init__ self.CH = None # is set from within MainTable() __init__ self.TL = None # is set from within TopLeftRectangle() __init__ self.popup_menu_loc = None self.extra_begin_edit_cell_func = None self.extra_end_edit_cell_func = None self.b1_pressed_loc = None self.closed_dropdown = None self.centre_alignment_text_mod_indexes = (slice(1, None), slice(None, -1)) self.c_align_cyc = cycle(self.centre_alignment_text_mod_indexes) self.being_drawn_item = None self.extra_motion_func = None self.extra_b1_press_func = None self.extra_b1_motion_func = None self.extra_b1_release_func = None self.extra_rc_func = None self.selection_binding_func = None self.shift_selection_binding_func = None self.ctrl_selection_binding_func = None self.drag_selection_binding_func = None self.ri_extra_begin_drag_drop_func = None self.ri_extra_end_drag_drop_func = None self.extra_double_b1_func = None self.row_height_resize_func = None self.new_row_width = 0 self.cell_options = {} self.drag_and_drop_enabled = False self.dragged_row = None self.width_resizing_enabled = False self.height_resizing_enabled = False self.double_click_resizing_enabled = False self.row_selection_enabled = False self.rc_insert_row_enabled = False self.rc_delete_row_enabled = False self.edit_cell_enabled = False self.visible_row_dividers = {} self.row_width_resize_bbox = tuple() self.rsz_w = None self.rsz_h = None self.currently_resizing_width = False self.currently_resizing_height = False self.ri_rc_popup_menu = None self.dropdown = DropdownStorage() self.text_editor = TextEditorStorage() self.disp_text = {} self.disp_high = {} self.disp_grid = {} self.disp_fill_sels = {} self.disp_bord_sels = {} self.disp_resize_lines = {} self.disp_dropdown = {} self.disp_checkbox = {} self.disp_tree_arrow = {} self.disp_boxes = set() self.hidd_text = {} self.hidd_high = {} self.hidd_grid = {} self.hidd_fill_sels = {} self.hidd_bord_sels = {} self.hidd_resize_lines = {} self.hidd_dropdown = {} self.hidd_checkbox = {} self.hidd_tree_arrow = {} self.hidd_boxes = set() self.align = kwargs["row_index_align"] self.default_index = kwargs["default_row_index"].lower() self.tree_reset() self.basic_bindings() def event_generate(self, *args, **kwargs) -> None: for arg in args: if self.MT and arg in self.MT.event_linker: self.MT.event_linker[arg]() else: super().event_generate(*args, **kwargs) def basic_bindings(self, enable: bool = True) -> None: if enable: self.bind("", self.mouse_motion) self.bind("", self.b1_press) self.bind("", self.b1_motion) self.bind("", self.b1_release) self.bind("", self.double_b1) self.bind(rc_binding, self.rc) else: self.unbind("") self.unbind("") self.unbind("") self.unbind("") self.unbind("") self.unbind(rc_binding) def tree_reset(self) -> None: # treeview mode self.tree = {} self.tree_open_ids = set() self.tree_rns = {} if self.MT: self.MT.displayed_rows = [] self.MT._row_index = [] self.MT.data = [] self.MT.row_positions = [0] self.MT.saved_row_heights = {} def set_width(self, new_width: int, set_TL: bool = False, recreate_selection_boxes: bool = True) -> None: self.current_width = new_width try: self.config(width=new_width) except Exception: return if set_TL: self.TL.set_dimensions(new_w=new_width, recreate_selection_boxes=recreate_selection_boxes) def rc(self, event: object) -> None: self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) self.focus_set() popup_menu = None if self.MT.identify_row(y=event.y, allow_end=False) is None: self.MT.deselect("all") r = len(self.MT.col_positions) - 1 if self.MT.rc_popup_menus_enabled: popup_menu = self.MT.empty_rc_popup_menu elif self.row_selection_enabled and not self.currently_resizing_width and not self.currently_resizing_height: r = self.MT.identify_row(y=event.y) if r < len(self.MT.row_positions) - 1: if self.MT.row_selected(r): if self.MT.rc_popup_menus_enabled: popup_menu = self.ri_rc_popup_menu else: if self.MT.single_selection_enabled and self.MT.rc_select_enabled: self.select_row(r, redraw=True) elif self.MT.toggle_selection_enabled and self.MT.rc_select_enabled: self.toggle_select_row(r, redraw=True) if self.MT.rc_popup_menus_enabled: popup_menu = self.ri_rc_popup_menu try_binding(self.extra_rc_func, event) if popup_menu is not None: self.popup_menu_loc = r popup_menu.tk_popup(event.x_root, event.y_root) def ctrl_b1_press(self, event: object) -> None: self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) if ( (self.drag_and_drop_enabled or self.row_selection_enabled) and self.MT.ctrl_select_enabled and self.rsz_h is None and self.rsz_w is None ): r = self.MT.identify_row(y=event.y) if r < len(self.MT.row_positions) - 1: r_selected = self.MT.row_selected(r) if not r_selected and self.row_selection_enabled: self.being_drawn_item = True self.being_drawn_item = self.add_selection(r, set_as_current=True, run_binding_func=False) self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) sel_event = self.MT.get_select_event(being_drawn_item=self.being_drawn_item) try_binding(self.ctrl_selection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) elif r_selected: self.MT.deselect(r=r) elif not self.MT.ctrl_select_enabled: self.b1_press(event) def ctrl_shift_b1_press(self, event: object) -> None: self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) y = event.y r = self.MT.identify_row(y=y) if ( (self.drag_and_drop_enabled or self.row_selection_enabled) and self.MT.ctrl_select_enabled and self.rsz_h is None and self.rsz_w is None ): if r < len(self.MT.row_positions) - 1: r_selected = self.MT.row_selected(r) if not r_selected and self.row_selection_enabled: if self.MT.selected and self.MT.selected.type_ == "rows": self.being_drawn_item = self.MT.recreate_selection_box( *self.get_shift_select_box(r, self.MT.selected.row), fill_iid=self.MT.selected.fill_iid, ) else: self.being_drawn_item = self.add_selection( r, run_binding_func=False, set_as_current=True, ) self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) sel_event = self.MT.get_select_event(being_drawn_item=self.being_drawn_item) try_binding(self.ctrl_selection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) elif r_selected: self.dragged_row = DraggedRowColumn( dragged=r, to_move=sorted(self.MT.get_selected_rows()), ) elif not self.MT.ctrl_select_enabled: self.shift_b1_press(event) def shift_b1_press(self, event: object) -> None: self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) y = event.y r = self.MT.identify_row(y=y) if (self.drag_and_drop_enabled or self.row_selection_enabled) and self.rsz_h is None and self.rsz_w is None: if r < len(self.MT.row_positions) - 1: r_selected = self.MT.row_selected(r) if not r_selected and self.row_selection_enabled: if self.MT.selected and self.MT.selected.type_ == "rows": r_to_sel, c_to_sel = self.MT.selected.row, self.MT.selected.column self.MT.deselect("all", redraw=False) self.being_drawn_item = self.MT.create_selection_box( *self.get_shift_select_box(r, r_to_sel), "rows" ) self.MT.set_currently_selected(r_to_sel, c_to_sel, self.being_drawn_item) else: self.being_drawn_item = self.select_row(r, run_binding_func=False) self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) sel_event = self.MT.get_select_event(being_drawn_item=self.being_drawn_item) try_binding(self.shift_selection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) elif r_selected: self.dragged_row = DraggedRowColumn( dragged=r, to_move=sorted(self.MT.get_selected_rows()), ) def get_shift_select_box(self, r: int, min_r: int) -> tuple[int, int, int, int, str]: if r > min_r: return min_r, 0, r + 1, len(self.MT.col_positions) - 1 elif r < min_r: return r, 0, min_r + 1, len(self.MT.col_positions) - 1 def create_resize_line( self, x1: int, y1: int, x2: int, y2: int, width: int, fill: str, tag: str | tuple[str], ) -> None: if self.hidd_resize_lines: t, sh = self.hidd_resize_lines.popitem() self.coords(t, x1, y1, x2, y2) if sh: self.itemconfig(t, width=width, fill=fill, tag=tag) else: self.itemconfig(t, width=width, fill=fill, tag=tag, state="normal") self.lift(t) else: t = self.create_line(x1, y1, x2, y2, width=width, fill=fill, tag=tag) self.disp_resize_lines[t] = True def delete_resize_lines(self) -> None: self.hidd_resize_lines.update(self.disp_resize_lines) self.disp_resize_lines = {} for t, sh in self.hidd_resize_lines.items(): if sh: self.itemconfig(t, tags=("",), state="hidden") self.hidd_resize_lines[t] = False def check_mouse_position_height_resizers(self, x: int, y: int) -> int | None: for r, (x1, y1, x2, y2) in self.visible_row_dividers.items(): if x >= x1 and y >= y1 and x <= x2 and y <= y2: return r def mouse_motion(self, event: object) -> None: if not self.currently_resizing_height and not self.currently_resizing_width: x = self.canvasx(event.x) y = self.canvasy(event.y) mouse_over_resize = False mouse_over_selected = False if self.height_resizing_enabled and not mouse_over_resize: r = self.check_mouse_position_height_resizers(x, y) if r is not None: self.rsz_h, mouse_over_resize = r, True if self.MT.current_cursor != "sb_v_double_arrow": self.config(cursor="sb_v_double_arrow") self.MT.current_cursor = "sb_v_double_arrow" else: self.rsz_h = None if ( self.width_resizing_enabled and not mouse_over_resize and self.PAR.ops.auto_resize_row_index is not True and not ( self.PAR.ops.auto_resize_row_index == "empty" and not isinstance(self.MT._row_index, int) and not self.MT._row_index ) ): try: x1, y1, x2, y2 = ( self.row_width_resize_bbox[0], self.row_width_resize_bbox[1], self.row_width_resize_bbox[2], self.row_width_resize_bbox[3], ) if x >= x1 and y >= y1 and x <= x2 and y <= y2: self.rsz_w, mouse_over_resize = True, True if self.MT.current_cursor != "sb_h_double_arrow": self.config(cursor="sb_h_double_arrow") self.MT.current_cursor = "sb_h_double_arrow" else: self.rsz_w = None except Exception: self.rsz_w = None if not mouse_over_resize: if self.MT.row_selected(self.MT.identify_row(event, allow_end=False)): mouse_over_selected = True if self.MT.current_cursor != "hand2": self.config(cursor="hand2") self.MT.current_cursor = "hand2" if not mouse_over_resize and not mouse_over_selected: self.MT.reset_mouse_motion_creations() try_binding(self.extra_motion_func, event) def double_b1(self, event: object): self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) self.focus_set() if ( self.double_click_resizing_enabled and self.height_resizing_enabled and self.rsz_h is not None and not self.currently_resizing_height ): row = self.rsz_h - 1 old_height = self.MT.row_positions[self.rsz_h] - self.MT.row_positions[self.rsz_h - 1] new_height = self.set_row_height(row) self.MT.allow_auto_resize_rows = False self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) if self.row_height_resize_func is not None and old_height != new_height: self.row_height_resize_func( event_dict( name="resize", sheet=self.PAR.name, resized_rows={row: {"old_size": old_height, "new_size": new_height}}, ) ) elif self.width_resizing_enabled and self.rsz_h is None and self.rsz_w is True: self.set_width_of_index_to_text() elif (self.row_selection_enabled or self.PAR.ops.treeview) and self.rsz_h is None and self.rsz_w is None: r = self.MT.identify_row(y=event.y) if r < len(self.MT.row_positions) - 1: iid = self.event_over_tree_arrow(r, self.canvasy(event.y), event.x) if self.row_selection_enabled: if self.MT.single_selection_enabled: self.select_row(r, redraw=iid is None) elif self.MT.toggle_selection_enabled: self.toggle_select_row(r, redraw=iid is None) datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] if ( self.get_cell_kwargs(datarn, key="dropdown") or self.get_cell_kwargs(datarn, key="checkbox") or self.edit_cell_enabled ): self.open_cell(event) elif iid is not None: self.PAR.item(iid, open_=iid not in self.tree_open_ids) self.rsz_h = None self.mouse_motion(event) try_binding(self.extra_double_b1_func, event) def b1_press(self, event: object): self.MT.unbind("") self.focus_set() self.closed_dropdown = self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) x = self.canvasx(event.x) y = self.canvasy(event.y) r = self.MT.identify_row(y=event.y) self.b1_pressed_loc = r if self.check_mouse_position_height_resizers(x, y) is None: self.rsz_h = None if ( not x >= self.row_width_resize_bbox[0] and y >= self.row_width_resize_bbox[1] and x <= self.row_width_resize_bbox[2] and y <= self.row_width_resize_bbox[3] ): self.rsz_w = None if self.height_resizing_enabled and self.rsz_h is not None: self.currently_resizing_height = True y = self.MT.row_positions[self.rsz_h] line2y = self.MT.row_positions[self.rsz_h - 1] x1, y1, x2, y2 = self.MT.get_canvas_visible_area() self.create_resize_line( 0, y, self.current_width, y, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rhl", ) self.MT.create_resize_line(x1, y, x2, y, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rhl") self.create_resize_line( 0, line2y, self.current_width, line2y, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rhl2", ) self.MT.create_resize_line(x1, line2y, x2, line2y, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rhl2") elif self.width_resizing_enabled and self.rsz_h is None and self.rsz_w is True: self.currently_resizing_width = True x1, y1, x2, y2 = self.MT.get_canvas_visible_area() x = int(event.x) if x < self.MT.min_column_width: x = int(self.MT.min_column_width) self.new_row_width = x elif self.MT.identify_row(y=event.y, allow_end=False) is None: self.MT.deselect("all") elif self.row_selection_enabled and self.rsz_h is None and self.rsz_w is None: r = self.MT.identify_row(y=event.y) if r < len(self.MT.row_positions) - 1: datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] if ( self.MT.row_selected(r) and not self.event_over_dropdown(r, datarn, event, y) and not self.event_over_checkbox(r, datarn, event, y) ): self.dragged_row = DraggedRowColumn( dragged=r, to_move=sorted(self.MT.get_selected_rows()), ) else: if self.MT.single_selection_enabled: self.being_drawn_item = True self.being_drawn_item = self.select_row(r, redraw=True) elif self.MT.toggle_selection_enabled: self.toggle_select_row(r, redraw=True) try_binding(self.extra_b1_press_func, event) def b1_motion(self, event: object): x1, y1, x2, y2 = self.MT.get_canvas_visible_area() if self.height_resizing_enabled and self.rsz_h is not None and self.currently_resizing_height: y = self.canvasy(event.y) size = y - self.MT.row_positions[self.rsz_h - 1] if size >= self.MT.min_row_height and size < self.MT.max_row_height: self.hide_resize_and_ctrl_lines(ctrl_lines=False) line2y = self.MT.row_positions[self.rsz_h - 1] self.create_resize_line( 0, y, self.current_width, y, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rhl", ) self.MT.create_resize_line(x1, y, x2, y, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rhl") self.create_resize_line( 0, line2y, self.current_width, line2y, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rhl2", ) self.MT.create_resize_line( x1, line2y, x2, line2y, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rhl2", ) self.drag_height_resize() elif self.width_resizing_enabled and self.rsz_w is not None and self.currently_resizing_width: evx = event.x if evx > self.current_width: x = self.MT.canvasx(evx - self.current_width) if evx > self.MT.max_index_width: evx = int(self.MT.max_index_width) x = self.MT.canvasx(evx - self.current_width) self.new_row_width = evx else: x = evx if x < self.MT.min_column_width: x = int(self.MT.min_column_width) self.new_row_width = x self.drag_width_resize() if ( self.drag_and_drop_enabled and self.row_selection_enabled and self.MT.anything_selected(exclude_cells=True, exclude_columns=True) and self.rsz_h is None and self.rsz_w is None and self.dragged_row is not None ): y = self.canvasy(event.y) if y > 0: self.show_drag_and_drop_indicators( self.drag_and_drop_motion(event), x1, x2, self.dragged_row.to_move, ) elif ( self.MT.drag_selection_enabled and self.row_selection_enabled and self.rsz_h is None and self.rsz_w is None ): need_redraw = False end_row = self.MT.identify_row(y=event.y) if end_row < len(self.MT.row_positions) - 1 and self.MT.selected: if self.MT.selected.type_ == "rows": box = self.get_b1_motion_box(self.MT.selected.row, end_row) if ( box is not None and self.being_drawn_item is not None and self.MT.coords_and_type(self.being_drawn_item) != box ): if box[2] - box[0] != 1: self.being_drawn_item = self.MT.recreate_selection_box( *box[:-1], fill_iid=self.MT.selected.fill_iid, ) else: self.being_drawn_item = self.select_row(self.MT.selected.row, run_binding_func=False) need_redraw = True sel_event = self.MT.get_select_event(being_drawn_item=self.being_drawn_item) try_binding(self.drag_selection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) if self.scroll_if_event_offscreen(event): need_redraw = True if need_redraw: self.MT.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=True) try_binding(self.extra_b1_motion_func, event) def get_b1_motion_box(self, start_row: int, end_row: int) -> tuple[int, int, int, int, Literal["rows"]]: if end_row >= start_row: return start_row, 0, end_row + 1, len(self.MT.col_positions) - 1, "rows" elif end_row < start_row: return end_row, 0, start_row + 1, len(self.MT.col_positions) - 1, "rows" def ctrl_b1_motion(self, event: object) -> None: x1, y1, x2, y2 = self.MT.get_canvas_visible_area() if ( self.drag_and_drop_enabled and self.row_selection_enabled and self.rsz_h is None and self.rsz_w is None and self.dragged_row is not None and self.MT.anything_selected(exclude_cells=True, exclude_columns=True) ): y = self.canvasy(event.y) if y > 0: self.show_drag_and_drop_indicators( self.drag_and_drop_motion(event), x1, x2, self.dragged_row.to_move, ) elif ( self.MT.ctrl_select_enabled and self.row_selection_enabled and self.MT.drag_selection_enabled and self.rsz_h is None and self.rsz_w is None ): need_redraw = False end_row = self.MT.identify_row(y=event.y) if end_row < len(self.MT.row_positions) - 1 and self.MT.selected: if self.MT.selected.type_ == "rows": box = self.get_b1_motion_box(self.MT.selected.row, end_row) if ( box is not None and self.being_drawn_item is not None and self.MT.coords_and_type(self.being_drawn_item) != box ): if box[2] - box[0] != 1: self.being_drawn_item = self.MT.recreate_selection_box( *box[:-1], self.MT.selected.fill_iid, ) else: self.MT.hide_selection_box(self.MT.selected.fill_iid) self.being_drawn_item = self.add_selection(box[0], run_binding_func=False) need_redraw = True sel_event = self.MT.get_select_event(being_drawn_item=self.being_drawn_item) try_binding(self.drag_selection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) if self.scroll_if_event_offscreen(event): need_redraw = True if need_redraw: self.MT.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=True) elif not self.MT.ctrl_select_enabled: self.b1_motion(event) def drag_and_drop_motion(self, event: object) -> float: y = event.y hend = self.winfo_height() ycheck = self.yview() if y >= hend - 0 and len(ycheck) > 1 and ycheck[1] < 1: if y >= hend + 15: self.MT.yview_scroll(2, "units") self.yview_scroll(2, "units") else: self.MT.yview_scroll(1, "units") self.yview_scroll(1, "units") self.fix_yview() self.MT.y_move_synced_scrolls("moveto", self.MT.yview()[0]) self.MT.main_table_redraw_grid_and_text(redraw_row_index=True) elif y <= 0 and len(ycheck) > 1 and ycheck[0] > 0: if y >= -15: self.MT.yview_scroll(-1, "units") self.yview_scroll(-1, "units") else: self.MT.yview_scroll(-2, "units") self.yview_scroll(-2, "units") self.fix_yview() self.MT.y_move_synced_scrolls("moveto", self.MT.yview()[0]) self.MT.main_table_redraw_grid_and_text(redraw_row_index=True) row = self.MT.identify_row(y=y) if row == len(self.MT.row_positions) - 1: row -= 1 if row >= self.dragged_row.to_move[0] and row <= self.dragged_row.to_move[-1]: if is_contiguous(self.dragged_row.to_move): return self.MT.row_positions[self.dragged_row.to_move[0]] return self.MT.row_positions[row] elif row > self.dragged_row.to_move[-1]: return self.MT.row_positions[row + 1] return self.MT.row_positions[row] def show_drag_and_drop_indicators( self, ypos: float, x1: float, x2: float, rows: Sequence[int], ) -> None: self.hide_resize_and_ctrl_lines() self.create_resize_line( 0, ypos, self.current_width, ypos, width=3, fill=self.PAR.ops.drag_and_drop_bg, tag="move_rows", ) self.MT.create_resize_line(x1, ypos, x2, ypos, width=3, fill=self.PAR.ops.drag_and_drop_bg, tag="move_rows") for chunk in consecutive_chunks(rows): self.MT.show_ctrl_outline( start_cell=(0, chunk[0]), end_cell=(len(self.MT.col_positions) - 1, chunk[-1] + 1), dash=(), outline=self.PAR.ops.drag_and_drop_bg, delete_on_timer=False, ) def hide_resize_and_ctrl_lines(self, ctrl_lines: bool = True) -> None: self.delete_resize_lines() self.MT.delete_resize_lines() if ctrl_lines: self.MT.delete_ctrl_outlines() def scroll_if_event_offscreen(self, event: object) -> bool: ycheck = self.yview() need_redraw = False if event.y > self.winfo_height() and len(ycheck) > 1 and ycheck[1] < 1: try: self.MT.yview_scroll(1, "units") self.yview_scroll(1, "units") except Exception: pass self.fix_yview() self.MT.y_move_synced_scrolls("moveto", self.MT.yview()[0]) need_redraw = True elif event.y < 0 and self.canvasy(self.winfo_height()) > 0 and ycheck and ycheck[0] > 0: try: self.yview_scroll(-1, "units") self.MT.yview_scroll(-1, "units") except Exception: pass self.fix_yview() self.MT.y_move_synced_scrolls("moveto", self.MT.yview()[0]) need_redraw = True return need_redraw def fix_yview(self) -> None: ycheck = self.yview() if ycheck and ycheck[0] < 0: self.MT.set_yviews("moveto", 0) if len(ycheck) > 1 and ycheck[1] > 1: self.MT.set_yviews("moveto", 1) def event_over_dropdown(self, r: int, datarn: int, event: object, canvasy: float) -> bool: return ( canvasy < self.MT.row_positions[r] + self.MT.index_txt_height and self.get_cell_kwargs(datarn, key="dropdown") and event.x > self.current_width - self.MT.index_txt_height - 4 ) def event_over_checkbox(self, r: int, datarn: int, event: object, canvasy: float) -> bool: return ( canvasy < self.MT.row_positions[r] + self.MT.index_txt_height and self.get_cell_kwargs(datarn, key="checkbox") and event.x < self.MT.index_txt_height + 4 ) def drag_width_resize(self) -> None: self.set_width(self.new_row_width, set_TL=True) self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) def drag_height_resize(self) -> None: new_row_pos = int(self.coords("rhl")[1]) old_height = self.MT.row_positions[self.rsz_h] - self.MT.row_positions[self.rsz_h - 1] size = new_row_pos - self.MT.row_positions[self.rsz_h - 1] if size < self.MT.min_row_height: new_row_pos = ceil(self.MT.row_positions[self.rsz_h - 1] + self.MT.min_row_height) elif size > self.MT.max_row_height: new_row_pos = floor(self.MT.row_positions[self.rsz_h - 1] + self.MT.max_row_height) increment = new_row_pos - self.MT.row_positions[self.rsz_h] self.MT.row_positions[self.rsz_h + 1 :] = [ e + increment for e in islice(self.MT.row_positions, self.rsz_h + 1, None) ] self.MT.row_positions[self.rsz_h] = new_row_pos new_height = self.MT.row_positions[self.rsz_h] - self.MT.row_positions[self.rsz_h - 1] self.MT.allow_auto_resize_rows = False self.MT.recreate_all_selection_boxes() self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) if self.row_height_resize_func is not None and old_height != new_height: self.row_height_resize_func( event_dict( name="resize", sheet=self.PAR.name, resized_rows={self.rsz_h - 1: {"old_size": old_height, "new_size": new_height}}, ) ) def b1_release(self, event: object) -> None: if self.being_drawn_item is not None and (to_sel := self.MT.coords_and_type(self.being_drawn_item)): r_to_sel, c_to_sel = self.MT.selected.row, self.MT.selected.column self.MT.hide_selection_box(self.being_drawn_item) self.MT.set_currently_selected( r_to_sel, c_to_sel, item=self.MT.create_selection_box(*to_sel, set_current=False), ) sel_event = self.MT.get_select_event(being_drawn_item=self.being_drawn_item) try_binding(self.drag_selection_binding_func, sel_event) self.PAR.emit_event("<>", data=sel_event) else: self.being_drawn_item = None self.MT.bind("", self.MT.mousewheel) if self.height_resizing_enabled and self.rsz_h is not None and self.currently_resizing_height: self.drag_height_resize() self.currently_resizing_height = False self.hide_resize_and_ctrl_lines(ctrl_lines=False) elif self.width_resizing_enabled and self.rsz_w is not None and self.currently_resizing_width: self.currently_resizing_width = False self.drag_width_resize() if ( self.drag_and_drop_enabled and self.MT.anything_selected(exclude_cells=True, exclude_columns=True) and self.row_selection_enabled and self.rsz_h is None and self.rsz_w is None and self.dragged_row is not None and self.find_withtag("move_rows") ): self.hide_resize_and_ctrl_lines() r = self.MT.identify_row(y=event.y) totalrows = len(self.dragged_row.to_move) if ( r is not None and totalrows != len(self.MT.row_positions) - 1 and not ( r >= self.dragged_row.to_move[0] and r <= self.dragged_row.to_move[-1] and is_contiguous(self.dragged_row.to_move) ) ): if r > self.dragged_row.to_move[-1]: r += 1 if r > len(self.MT.row_positions) - 1: r = len(self.MT.row_positions) - 1 event_data = event_dict( name="move_rows", sheet=self.PAR.name, widget=self, boxes=self.MT.get_boxes(), selected=self.MT.selected, value=r, ) if try_binding(self.ri_extra_begin_drag_drop_func, event_data, "begin_move_rows"): data_new_idxs, disp_new_idxs, event_data = self.MT.move_rows_adjust_options_dict( *self.MT.get_args_for_move_rows( move_to=r, to_move=self.dragged_row.to_move, ), move_data=self.PAR.ops.row_drag_and_drop_perform, move_heights=self.PAR.ops.row_drag_and_drop_perform, event_data=event_data, ) event_data["moved"]["rows"] = { "data": data_new_idxs, "displayed": disp_new_idxs, } if self.MT.undo_enabled: self.MT.undo_stack.append(pickled_event_dict(event_data)) self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) try_binding(self.ri_extra_end_drag_drop_func, event_data, "end_move_rows") self.MT.sheet_modified(event_data) elif self.b1_pressed_loc is not None and self.rsz_w is None and self.rsz_h is None: r = self.MT.identify_row(y=event.y) if ( r is not None and r < len(self.MT.row_positions) - 1 and r == self.b1_pressed_loc and self.b1_pressed_loc != self.closed_dropdown ): datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] canvasy = self.canvasy(event.y) if self.event_over_dropdown(r, datarn, event, canvasy) or self.event_over_checkbox( r, datarn, event, canvasy ): self.open_cell(event) elif (iid := self.event_over_tree_arrow(r, canvasy, event.x)) is not None: if self.MT.selection_boxes: self.select_row(r, ext=True, redraw=False) self.PAR.item(iid, open_=iid not in self.tree_open_ids) else: self.mouseclick_outside_editor_or_dropdown_all_canvases(inside=True) self.b1_pressed_loc = None self.closed_dropdown = None self.dragged_row = None self.currently_resizing_width = False self.currently_resizing_height = False self.rsz_w = None self.rsz_h = None self.mouse_motion(event) try_binding(self.extra_b1_release_func, event) def event_over_tree_arrow( self, r: int, canvasy: float, eventx: int, ) -> bool: if self.PAR.ops.treeview and ( canvasy < self.MT.row_positions[r] + self.MT.index_txt_height + 3 and isinstance(self.MT._row_index, list) and (datarn := self.MT.datarn(r)) < len(self.MT._row_index) and eventx < self.get_treeview_indent((iid := self.MT._row_index[datarn].iid)) + self.MT.index_txt_height + 1 ): return iid return None def toggle_select_row( self, row: int, add_selection: bool = True, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ext: bool = False, ) -> int | None: if add_selection: if self.MT.row_selected(row): fill_iid = self.MT.deselect(r=row, redraw=redraw) else: fill_iid = self.add_selection( r=row, redraw=redraw, run_binding_func=run_binding_func, set_as_current=set_as_current, ext=ext, ) else: if self.MT.row_selected(row): fill_iid = self.MT.deselect(r=row, redraw=redraw) else: fill_iid = self.select_row(row, redraw=redraw, ext=ext) return fill_iid def select_row( self, r: int, redraw: bool = False, run_binding_func: bool = True, ext: bool = False, ) -> int: boxes_to_hide = tuple(self.MT.selection_boxes) fill_iid = self.MT.create_selection_box(r, 0, r + 1, len(self.MT.col_positions) - 1, "rows", ext=ext) for iid in boxes_to_hide: self.MT.hide_selection_box(iid) if redraw: self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) if run_binding_func: self.MT.run_selection_binding("rows") return fill_iid def add_selection( self, r: int, redraw: bool = False, run_binding_func: bool = True, set_as_current: bool = True, ext: bool = False, ) -> int: box = (r, 0, r + 1, len(self.MT.col_positions) - 1, "rows") fill_iid = self.MT.create_selection_box(*box, set_current=set_as_current, ext=ext) if redraw: self.MT.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=True) if run_binding_func: self.MT.run_selection_binding("rows") return fill_iid def display_box( self, x1: int, y1: int, x2: int, y2: int, fill: str, outline: str, state: str, tags: str | tuple[str], iid: None | int = None, ) -> int: coords = rounded_box_coords( x1, y1, x2, y2, radius=5 if self.PAR.ops.rounded_boxes else 0, ) if isinstance(iid, int): self.coords(iid, coords) self.itemconfig(iid, fill=fill, outline=outline, state=state, tags=tags) else: if self.hidd_boxes: iid = self.hidd_boxes.pop() self.coords(iid, coords) self.itemconfig(iid, fill=fill, outline=outline, state=state, tags=tags) else: iid = self.create_polygon( coords, fill=fill, outline=outline, state=state, tags=tags, smooth=True, ) self.disp_boxes.add(iid) return iid def hide_box(self, item: int | None) -> None: if isinstance(item, int): self.disp_boxes.discard(item) self.hidd_boxes.add(item) self.itemconfig(item, state="hidden") def get_cell_dimensions(self, datarn: int) -> tuple[int, int]: txt = self.get_valid_cell_data_as_str(datarn, fix=False) if txt: self.MT.txt_measure_canvas.itemconfig( self.MT.txt_measure_canvas_text, text=txt, font=self.PAR.ops.index_font ) b = self.MT.txt_measure_canvas.bbox(self.MT.txt_measure_canvas_text) w = b[2] - b[0] + 7 h = b[3] - b[1] + 5 else: w = self.PAR.ops.default_row_index_width h = self.MT.min_row_height if self.get_cell_kwargs(datarn, key="dropdown") or self.get_cell_kwargs(datarn, key="checkbox"): w += self.MT.index_txt_height + 2 if self.PAR.ops.treeview: if datarn in self.cell_options and "align" in self.cell_options[datarn]: align = self.cell_options[datarn]["align"] else: align = self.align if align == "w": w += self.MT.index_txt_height w += self.get_treeview_indent(self.MT._row_index[datarn].iid) + 10 return w, h def get_row_text_height( self, row: int, visible_only: bool = False, only_if_too_small: bool = False, ) -> int: h = self.MT.min_row_height datarn = row if self.MT.all_rows_displayed else self.MT.displayed_rows[row] # index _w, ih = self.get_cell_dimensions(datarn) # table if self.MT.data: if self.MT.all_columns_displayed: if visible_only: iterable = range(*self.MT.visible_text_columns) else: if not self.MT.data or datarn >= len(self.MT.data): iterable = range(0, 0) else: iterable = range(0, len(self.MT.data[datarn])) else: if visible_only: start_col, end_col = self.MT.visible_text_columns else: start_col, end_col = 0, len(self.MT.displayed_columns) iterable = self.MT.displayed_columns[start_col:end_col] for datacn in iterable: if (txt := self.MT.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True)) and ( th := self.MT.get_txt_h(txt) + 5 ) > h: h = th if ih > h: h = ih if only_if_too_small and h < self.MT.row_positions[row + 1] - self.MT.row_positions[row]: return self.MT.row_positions[row + 1] - self.MT.row_positions[row] if h < self.MT.min_row_height: h = int(self.MT.min_row_height) elif h > self.MT.max_row_height: h = int(self.MT.max_row_height) return h def set_row_height( self, row: int, height: None | int = None, only_if_too_small: bool = False, visible_only: bool = False, recreate: bool = True, ) -> int: if height is None: height = self.get_row_text_height(row=row, visible_only=visible_only) if height < self.MT.min_row_height: height = int(self.MT.min_row_height) elif height > self.MT.max_row_height: height = int(self.MT.max_row_height) if only_if_too_small and height <= self.MT.row_positions[row + 1] - self.MT.row_positions[row]: return self.MT.row_positions[row + 1] - self.MT.row_positions[row] new_row_pos = self.MT.row_positions[row] + height increment = new_row_pos - self.MT.row_positions[row + 1] self.MT.row_positions[row + 2 :] = [ e + increment for e in islice(self.MT.row_positions, row + 2, len(self.MT.row_positions)) ] self.MT.row_positions[row + 1] = new_row_pos if recreate: self.MT.recreate_all_selection_boxes() return height def get_index_text_width( self, only_rows: Iterator[int] | None = None, ) -> int: self.fix_index() w = self.PAR.ops.default_row_index_width if (not self.MT._row_index and isinstance(self.MT._row_index, list)) or ( isinstance(self.MT._row_index, int) and self.MT._row_index >= len(self.MT.data) ): return w if only_rows: iterable = only_rows elif self.MT.all_rows_displayed: if isinstance(self.MT._row_index, list): iterable = range(len(self.MT._row_index)) else: iterable = range(len(self.MT.data)) else: iterable = self.MT.displayed_rows if (new_w := max(map(itemgetter(0), map(self.get_cell_dimensions, iterable)), default=w)) > w: w = new_w if w > self.MT.max_index_width: w = int(self.MT.max_index_width) return w def set_width_of_index_to_text( self, text: None | str = None, only_rows: list = [], ) -> int: self.fix_index() w = self.PAR.ops.default_row_index_width if (text is None and isinstance(self.MT._row_index, list) and not self.MT._row_index) or ( isinstance(self.MT._row_index, int) and self.MT._row_index >= len(self.MT.data) ): return w if text is not None and text: self.MT.txt_measure_canvas.itemconfig(self.MT.txt_measure_canvas_text, text=text) b = self.MT.txt_measure_canvas.bbox(self.MT.txt_measure_canvas_text) if (tw := b[2] - b[0] + 10) > w: w = tw elif text is None: w = self.get_index_text_width(only_rows=only_rows) if w > self.MT.max_index_width: w = int(self.MT.max_index_width) self.set_width(w, set_TL=True) self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) return w def set_height_of_all_rows( self, height: int | None = None, only_if_too_small: bool = False, recreate: bool = True, ) -> None: if height is None: if self.MT.all_columns_displayed: iterable = range(self.MT.total_data_rows()) else: iterable = range(len(self.MT.displayed_rows)) self.MT.set_row_positions( itr=(self.get_row_text_height(rn, only_if_too_small=only_if_too_small) for rn in iterable) ) elif height is not None: if self.MT.all_rows_displayed: self.MT.set_row_positions(itr=repeat(height, len(self.MT.data))) else: self.MT.set_row_positions(itr=repeat(height, len(self.MT.displayed_rows))) if recreate: self.MT.recreate_all_selection_boxes() def auto_set_index_width(self, end_row: int, only_rows: list) -> bool: if not isinstance(self.MT._row_index, int) and not self.MT._row_index: if self.default_index == "letters": new_w = self.MT.get_txt_w(f"{num2alpha(end_row)}") + 20 elif self.default_index == "numbers": new_w = self.MT.get_txt_w(f"{end_row}") + 20 elif self.default_index == "both": new_w = self.MT.get_txt_w(f"{end_row + 1} {num2alpha(end_row)}") + 20 elif self.PAR.ops.auto_resize_row_index is True: new_w = self.get_index_text_width(only_rows=only_rows) else: new_w = None if new_w is not None and (sheet_w_x := floor(self.PAR.winfo_width() * 0.7)) < new_w: new_w = sheet_w_x if new_w and (self.current_width - new_w > 20 or new_w - self.current_width > 3): self.set_width(new_w, set_TL=True, recreate_selection_boxes=False) return True return False def redraw_highlight_get_text_fg( self, fr: float, sr: float, r: int, c_2: str, c_3: str, selections: dict, datarn: int, ) -> tuple[str, str, bool]: redrawn = False kwargs = self.get_cell_kwargs(datarn, key="highlight") if kwargs: if kwargs[0] is not None: c_1 = kwargs[0] if kwargs[0].startswith("#") else color_map[kwargs[0]] if "rows" in selections and r in selections["rows"]: txtfg = ( self.PAR.ops.index_selected_rows_fg if kwargs[1] is None or self.PAR.ops.display_selected_fg_over_highlights else kwargs[1] ) if kwargs[0] is not None: fill = ( f"#{int((int(c_1[1:3], 16) + int(c_3[1:3], 16)) / 2):02X}" + f"{int((int(c_1[3:5], 16) + int(c_3[3:5], 16)) / 2):02X}" + f"{int((int(c_1[5:], 16) + int(c_3[5:], 16)) / 2):02X}" ) elif "cells" in selections and r in selections["cells"]: txtfg = ( self.PAR.ops.index_selected_cells_fg if kwargs[1] is None or self.PAR.ops.display_selected_fg_over_highlights else kwargs[1] ) if kwargs[0] is not None: fill = ( f"#{int((int(c_1[1:3], 16) + int(c_2[1:3], 16)) / 2):02X}" + f"{int((int(c_1[3:5], 16) + int(c_2[3:5], 16)) / 2):02X}" + f"{int((int(c_1[5:], 16) + int(c_2[5:], 16)) / 2):02X}" ) else: txtfg = self.PAR.ops.index_fg if kwargs[1] is None else kwargs[1] if kwargs[0] is not None: fill = kwargs[0] if kwargs[0] is not None: redrawn = self.redraw_highlight( 0, fr + 1, self.current_width - 1, sr, fill=fill, outline=( self.PAR.ops.index_fg if self.get_cell_kwargs(datarn, key="dropdown") and self.PAR.ops.show_dropdown_borders else "" ), tag="s", ) tree_arrow_fg = txtfg elif not kwargs: if "rows" in selections and r in selections["rows"]: txtfg = self.PAR.ops.index_selected_rows_fg tree_arrow_fg = self.PAR.ops.selected_rows_tree_arrow_fg elif "cells" in selections and r in selections["cells"]: txtfg = self.PAR.ops.index_selected_cells_fg tree_arrow_fg = self.PAR.ops.selected_cells_tree_arrow_fg else: txtfg = self.PAR.ops.index_fg tree_arrow_fg = self.PAR.ops.tree_arrow_fg return txtfg, tree_arrow_fg, redrawn def redraw_highlight( self, x1: float, y1: float, x2: float, y2: float, fill: str, outline: str, tag: str | tuple[str], ) -> bool: coords = (x1, y1, x2, y2) if self.hidd_high: iid, showing = self.hidd_high.popitem() self.coords(iid, coords) if showing: self.itemconfig(iid, fill=fill, outline=outline) else: self.itemconfig(iid, fill=fill, outline=outline, tag=tag, state="normal") else: iid = self.create_rectangle(coords, fill=fill, outline=outline, tag=tag) self.disp_high[iid] = True return True def redraw_gridline( self, points: tuple[float], fill: str, width: int, tag: str | tuple[str], ) -> None: if self.hidd_grid: t, sh = self.hidd_grid.popitem() self.coords(t, points) if sh: self.itemconfig(t, fill=fill, width=width, tag=tag) else: self.itemconfig(t, fill=fill, width=width, tag=tag, state="normal") self.disp_grid[t] = True else: self.disp_grid[self.create_line(points, fill=fill, width=width, tag=tag)] = True def redraw_tree_arrow( self, x1: float, y1: float, r: int, fill: str, tag: str | tuple[str], indent: float, open_: bool = False, ) -> None: mod = (self.MT.index_txt_height - 1) if self.MT.index_txt_height % 2 else self.MT.index_txt_height small_mod = int(mod / 5) mid_y = floor(self.MT.min_row_height / 2) # up arrow if open_: points = ( # the left hand downward point x1 + 5 + indent, y1 + mid_y + small_mod, # the middle upward point x1 + 5 + indent + small_mod + small_mod, y1 + mid_y - small_mod, # the right hand downward point x1 + 5 + indent + small_mod + small_mod + small_mod + small_mod, y1 + mid_y + small_mod, ) # right pointing arrow else: points = ( # the upper point x1 + 5 + indent + small_mod + small_mod, y1 + mid_y - small_mod - small_mod, # the middle point x1 + 5 + indent + small_mod + small_mod + small_mod + small_mod, y1 + mid_y, # the bottom point x1 + 5 + indent + small_mod + small_mod, y1 + mid_y + small_mod + small_mod, ) if self.hidd_tree_arrow: t, sh = self.hidd_tree_arrow.popitem() self.coords(t, points) if sh: self.itemconfig(t, fill=fill) else: self.itemconfig(t, fill=fill, tag=tag, state="normal") self.lift(t) else: t = self.create_line( points, fill=fill, tag=tag, width=2, capstyle=tk.ROUND, joinstyle=tk.BEVEL, ) self.disp_tree_arrow[t] = True def redraw_dropdown( self, x1: float, y1: float, x2: float, y2: float, fill: str, outline: str, tag: str | tuple[str], draw_outline: bool = True, draw_arrow: bool = True, open_: bool = False, ) -> None: if draw_outline and self.PAR.ops.show_dropdown_borders: self.redraw_highlight(x1 + 1, y1 + 1, x2, y2, fill="", outline=self.PAR.ops.index_fg, tag=tag) if draw_arrow: mod = (self.MT.index_txt_height - 1) if self.MT.index_txt_height % 2 else self.MT.index_txt_height small_mod = int(mod / 5) mid_y = floor(self.MT.min_row_height / 2) if open_: # up arrow points = ( x2 - 4 - small_mod - small_mod - small_mod - small_mod, y1 + mid_y + small_mod, x2 - 4 - small_mod - small_mod, y1 + mid_y - small_mod, x2 - 4, y1 + mid_y + small_mod, ) else: # down arrow points = ( x2 - 4 - small_mod - small_mod - small_mod - small_mod, y1 + mid_y - small_mod, x2 - 4 - small_mod - small_mod, y1 + mid_y + small_mod, x2 - 4, y1 + mid_y - small_mod, ) if self.hidd_dropdown: t, sh = self.hidd_dropdown.popitem() self.coords(t, points) if sh: self.itemconfig(t, fill=fill) else: self.itemconfig(t, fill=fill, tag=tag, state="normal") self.lift(t) else: t = self.create_line( points, fill=fill, tag=tag, width=2, capstyle=tk.ROUND, joinstyle=tk.BEVEL, ) self.disp_dropdown[t] = True def redraw_checkbox( self, x1: float, y1: float, x2: float, y2: float, fill: str, outline: str, tag: str | tuple[str], draw_check: bool = False, ) -> None: points = rounded_box_coords(x1, y1, x2, y2) if self.hidd_checkbox: t, sh = self.hidd_checkbox.popitem() self.coords(t, points) if sh: self.itemconfig(t, fill=outline, outline=fill) else: self.itemconfig(t, fill=outline, outline=fill, tag=tag, state="normal") self.lift(t) else: t = self.create_polygon(points, fill=outline, outline=fill, tag=tag, smooth=True) self.disp_checkbox[t] = True if draw_check: # draw filled box x1 = x1 + 4 y1 = y1 + 4 x2 = x2 - 3 y2 = y2 - 3 points = rounded_box_coords(x1, y1, x2, y2, radius=4) if self.hidd_checkbox: t, sh = self.hidd_checkbox.popitem() self.coords(t, points) if sh: self.itemconfig(t, fill=fill, outline=outline) else: self.itemconfig(t, fill=fill, outline=outline, tag=tag, state="normal") self.lift(t) else: t = self.create_polygon(points, fill=fill, outline=outline, tag=tag, smooth=True) self.disp_checkbox[t] = True def configure_scrollregion(self, last_row_line_pos: float) -> None: self.configure( scrollregion=( 0, 0, self.current_width, last_row_line_pos + self.PAR.ops.empty_vertical + 2, ) ) def redraw_grid_and_text( self, last_row_line_pos: float, scrollpos_top: int, y_stop: int, grid_start_row: int, grid_end_row: int, text_start_row: int, text_end_row: int, scrollpos_bot: int, row_pos_exists: bool, ) -> None: try: self.configure_scrollregion(last_row_line_pos=last_row_line_pos) except Exception: return self.hidd_text.update(self.disp_text) self.disp_text = {} self.hidd_high.update(self.disp_high) self.disp_high = {} self.hidd_grid.update(self.disp_grid) self.disp_grid = {} self.hidd_dropdown.update(self.disp_dropdown) self.disp_dropdown = {} self.hidd_checkbox.update(self.disp_checkbox) self.disp_checkbox = {} self.hidd_tree_arrow.update(self.disp_tree_arrow) self.disp_tree_arrow = {} self.visible_row_dividers = {} draw_y = self.MT.row_positions[grid_start_row] xend = self.current_width - 6 self.row_width_resize_bbox = ( self.current_width - 2, scrollpos_top, self.current_width, scrollpos_bot, ) if (self.PAR.ops.show_horizontal_grid or self.height_resizing_enabled) and row_pos_exists: points = [ self.current_width - 1, y_stop - 1, self.current_width - 1, scrollpos_top - 1, -1, scrollpos_top - 1, ] for r in range(grid_start_row, grid_end_row): draw_y = self.MT.row_positions[r] if r and self.height_resizing_enabled: self.visible_row_dividers[r] = (1, draw_y - 2, xend, draw_y + 2) points.extend( ( -1, draw_y, self.current_width, draw_y, -1, draw_y, -1, self.MT.row_positions[r + 1] if len(self.MT.row_positions) - 1 > r else draw_y, ) ) self.redraw_gridline(points=points, fill=self.PAR.ops.index_grid_fg, width=1, tag="h") c_2 = ( self.PAR.ops.index_selected_cells_bg if self.PAR.ops.index_selected_cells_bg.startswith("#") else color_map[self.PAR.ops.index_selected_cells_bg] ) c_3 = ( self.PAR.ops.index_selected_rows_bg if self.PAR.ops.index_selected_rows_bg.startswith("#") else color_map[self.PAR.ops.index_selected_rows_bg] ) font = self.PAR.ops.index_font selections = self.get_redraw_selections(text_start_row, grid_end_row) dd_coords = self.dropdown.get_coords() treeview = self.PAR.ops.treeview for r in range(text_start_row, text_end_row): rtopgridln = self.MT.row_positions[r] rbotgridln = self.MT.row_positions[r + 1] if rbotgridln - rtopgridln < self.MT.index_txt_height: continue datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] fill, tree_arrow_fg, dd_drawn = self.redraw_highlight_get_text_fg( rtopgridln, rbotgridln, r, c_2, c_3, selections, datarn, ) if datarn in self.cell_options and "align" in self.cell_options[datarn]: align = self.cell_options[datarn]["align"] else: align = self.align dropdown_kwargs = self.get_cell_kwargs(datarn, key="dropdown") if align == "w": draw_x = 3 if dropdown_kwargs: mw = self.current_width - self.MT.index_txt_height - 2 self.redraw_dropdown( 0, rtopgridln, self.current_width - 1, rbotgridln - 1, fill=fill, outline=fill, tag="dd", draw_outline=not dd_drawn, draw_arrow=mw >= 5, open_=dd_coords == r, ) else: mw = self.current_width - 2 elif align == "e": if dropdown_kwargs: mw = self.current_width - self.MT.index_txt_height - 2 draw_x = self.current_width - 5 - self.MT.index_txt_height self.redraw_dropdown( 0, rtopgridln, self.current_width - 1, rbotgridln - 1, fill=fill, outline=fill, tag="dd", draw_outline=not dd_drawn, draw_arrow=mw >= 5, open_=dd_coords == r, ) else: mw = self.current_width - 2 draw_x = self.current_width - 3 elif align == "center": if dropdown_kwargs: mw = self.current_width - self.MT.index_txt_height - 2 draw_x = ceil((self.current_width - self.MT.index_txt_height) / 2) self.redraw_dropdown( 0, rtopgridln, self.current_width - 1, rbotgridln - 1, fill=fill, outline=fill, tag="dd", draw_outline=not dd_drawn, draw_arrow=mw >= 5, open_=dd_coords == r, ) else: mw = self.current_width - 1 draw_x = floor(self.current_width / 2) checkbox_kwargs = self.get_cell_kwargs(datarn, key="checkbox") if checkbox_kwargs and not dropdown_kwargs and mw > self.MT.index_txt_height + 1: box_w = self.MT.index_txt_height + 1 if align == "w": draw_x += box_w + 3 mw -= box_w + 3 elif align == "center": draw_x += ceil(box_w / 2) + 1 mw -= box_w + 2 else: mw -= box_w + 1 try: draw_check = ( self.MT._row_index[datarn] if isinstance(self.MT._row_index, (list, tuple)) else self.MT.data[datarn][self.MT._row_index] ) except Exception: draw_check = False self.redraw_checkbox( 2, rtopgridln + 2, self.MT.index_txt_height + 3, rtopgridln + self.MT.index_txt_height + 3, fill=fill if checkbox_kwargs["state"] == "normal" else self.PAR.ops.index_grid_fg, outline="", tag="cb", draw_check=draw_check, ) if treeview and isinstance(self.MT._row_index, list) and len(self.MT._row_index) > datarn: iid = self.MT._row_index[datarn].iid mw -= self.MT.index_txt_height if align == "w": draw_x += self.MT.index_txt_height + 3 indent = self.get_treeview_indent(iid) draw_x += indent + 5 if self.tree[iid].children: self.redraw_tree_arrow( 2, rtopgridln, r=r, fill=tree_arrow_fg, tag="ta", indent=indent, open_=self.MT._row_index[datarn].iid in self.tree_open_ids, ) lns = self.get_valid_cell_data_as_str(datarn, fix=False) if not lns: continue draw_y = rtopgridln + self.MT.index_first_ln_ins if mw > 5: draw_y = rtopgridln + self.MT.index_first_ln_ins start_ln = int((scrollpos_top - rtopgridln) / self.MT.index_xtra_lines_increment) if start_ln < 0: start_ln = 0 draw_y += start_ln * self.MT.index_xtra_lines_increment lns = lns.split("\n") if draw_y + self.MT.index_half_txt_height - 1 <= rbotgridln and len(lns) > start_ln: for txt in islice(lns, start_ln, None): if self.hidd_text: iid, showing = self.hidd_text.popitem() self.coords(iid, draw_x, draw_y) if showing: self.itemconfig( iid, text=txt, fill=fill, font=font, anchor=align, ) else: self.itemconfig( iid, text=txt, fill=fill, font=font, anchor=align, state="normal", ) self.tag_raise(iid) else: iid = self.create_text( draw_x, draw_y, text=txt, fill=fill, font=font, anchor=align, tag="t", ) self.disp_text[iid] = True wd = self.bbox(iid) wd = wd[2] - wd[0] if wd > mw: if align == "w" and dropdown_kwargs: txt = txt[: int(len(txt) * (mw / wd))] self.itemconfig(iid, text=txt) wd = self.bbox(iid) while wd[2] - wd[0] > mw: txt = txt[:-1] self.itemconfig(iid, text=txt) wd = self.bbox(iid) elif align == "e" and (dropdown_kwargs or checkbox_kwargs): txt = txt[len(txt) - int(len(txt) * (mw / wd)) :] self.itemconfig(iid, text=txt) wd = self.bbox(iid) while wd[2] - wd[0] > mw: txt = txt[1:] self.itemconfig(iid, text=txt) wd = self.bbox(iid) elif align == "center" and (dropdown_kwargs or checkbox_kwargs): tmod = ceil((len(txt) - int(len(txt) * (mw / wd))) / 2) txt = txt[tmod - 1 : -tmod] self.itemconfig(iid, text=txt) wd = self.bbox(iid) self.c_align_cyc = cycle(self.centre_alignment_text_mod_indexes) while wd[2] - wd[0] > mw: txt = txt[next(self.c_align_cyc)] self.itemconfig(iid, text=txt) wd = self.bbox(iid) self.coords(iid, draw_x, draw_y) draw_y += self.MT.index_xtra_lines_increment if draw_y + self.MT.index_half_txt_height - 1 > rbotgridln: break for dct in ( self.hidd_text, self.hidd_high, self.hidd_grid, self.hidd_dropdown, self.hidd_checkbox, self.hidd_tree_arrow, ): for iid, showing in dct.items(): if showing: self.itemconfig(iid, state="hidden") dct[iid] = False return True def get_redraw_selections(self, startr: int, endr: int) -> dict[str, set[int]]: d = defaultdict(set) for item, box in self.MT.get_selection_items(): r1, c1, r2, c2 = box.coords for r in range(startr, endr): if r1 <= r and r2 > r: d[box.type_ if box.type_ != "columns" else "cells"].add(r) return d def open_cell(self, event: object = None, ignore_existing_editor: bool = False) -> None: if not self.MT.anything_selected() or (not ignore_existing_editor and self.text_editor.open): return if not self.MT.selected: return r = self.MT.selected.row datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] if self.get_cell_kwargs(datarn, key="readonly"): return elif self.get_cell_kwargs(datarn, key="dropdown") or self.get_cell_kwargs(datarn, key="checkbox"): if self.MT.event_opens_dropdown_or_checkbox(event): if self.get_cell_kwargs(datarn, key="dropdown"): self.open_dropdown_window(r, event=event) elif self.get_cell_kwargs(datarn, key="checkbox"): self.click_checkbox(r, datarn) elif self.edit_cell_enabled: self.open_text_editor(event=event, r=r, dropdown=False) # displayed indexes def get_cell_align(self, r: int) -> str: datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] if datarn in self.cell_options and "align" in self.cell_options[datarn]: align = self.cell_options[datarn]["align"] else: align = self.align return align # r is displayed row def open_text_editor( self, event: object = None, r: int = 0, text: None | str = None, state: str = "normal", dropdown: bool = False, ) -> bool: text = None extra_func_key = "??" if event is None or self.MT.event_opens_dropdown_or_checkbox(event): if event is not None: if hasattr(event, "keysym") and event.keysym == "Return": extra_func_key = "Return" elif hasattr(event, "keysym") and event.keysym == "F2": extra_func_key = "F2" text = self.get_cell_data(self.MT.datarn(r), none_to_empty_str=True, redirect_int=True) elif event is not None and ( (hasattr(event, "keysym") and event.keysym == "BackSpace") or event.keycode in (8, 855638143) ): extra_func_key = "BackSpace" text = "" elif event is not None and ( (hasattr(event, "char") and event.char.isalpha()) or (hasattr(event, "char") and event.char.isdigit()) or (hasattr(event, "char") and event.char in symbols_set) ): extra_func_key = event.char text = event.char else: return False if self.extra_begin_edit_cell_func: try: text = self.extra_begin_edit_cell_func( event_dict( name="begin_edit_index", sheet=self.PAR.name, key=extra_func_key, value=text, loc=r, row=r, boxes=self.MT.get_boxes(), selected=self.MT.selected, ) ) except Exception: return False if text is None: return False else: text = text if isinstance(text, str) else f"{text}" text = "" if text is None else text if self.PAR.ops.cell_auto_resize_enabled: self.set_row_height_run_binding(r) if self.text_editor.open and r == self.text_editor.row: self.text_editor.set_text(self.text_editor.get() + "" if not isinstance(text, str) else text) return self.hide_text_editor() if not self.MT.see(r=r, c=0, keep_yscroll=True, check_cell_visibility=True): self.MT.refresh() x = 0 y = self.MT.row_positions[r] w = self.current_width + 1 h = self.MT.row_positions[r + 1] - y + 1 if text is None: text = self.get_cell_data(self.MT.datarn(r), none_to_empty_str=True, redirect_int=True) bg, fg = self.PAR.ops.index_bg, self.PAR.ops.index_fg kwargs = { "menu_kwargs": DotDict( { "font": self.PAR.ops.table_font, "foreground": self.PAR.ops.popup_menu_fg, "background": self.PAR.ops.popup_menu_bg, "activebackground": self.PAR.ops.popup_menu_highlight_bg, "activeforeground": self.PAR.ops.popup_menu_highlight_fg, } ), "sheet_ops": self.PAR.ops, "border_color": self.PAR.ops.index_selected_rows_bg, "text": text, "state": state, "width": w, "height": h, "show_border": True, "bg": bg, "fg": fg, "align": self.get_cell_align(r), "r": r, } if not self.text_editor.window: self.text_editor.window = TextEditor(self, newline_binding=self.text_editor_newline_binding) self.text_editor.canvas_id = self.create_window((x, y), window=self.text_editor.window, anchor="nw") self.text_editor.window.reset(**kwargs) if not self.text_editor.open: self.itemconfig(self.text_editor.canvas_id, state="normal") self.text_editor.open = True self.coords(self.text_editor.canvas_id, x, y) for b in text_editor_newline_bindings: self.text_editor.tktext.bind(b, self.text_editor_newline_binding) for b in text_editor_close_bindings: self.text_editor.tktext.bind(b, self.close_text_editor) if not dropdown: self.text_editor.tktext.focus_set() self.text_editor.window.scroll_to_bottom() self.text_editor.tktext.bind("", self.close_text_editor) for key, func in self.MT.text_editor_user_bound_keys.items(): self.text_editor.tktext.bind(key, func) return True def text_editor_newline_binding(self, event: object = None, check_lines: bool = True) -> None: if not self.height_resizing_enabled: return curr_height = self.text_editor.window.winfo_height() if curr_height < self.MT.min_row_height: return if ( not check_lines or self.MT.get_lines_cell_height( self.text_editor.window.get_num_lines() + 1, font=self.text_editor.tktext.cget("font"), ) > curr_height ): r = self.text_editor.row new_height = curr_height + self.MT.index_xtra_lines_increment space_bot = self.MT.get_space_bot(r) if new_height > space_bot: new_height = space_bot if new_height != curr_height: self.set_row_height(r, new_height) self.MT.refresh() self.text_editor.window.config(height=new_height) self.coords(self.text_editor.canvas_id, 0, self.MT.row_positions[r] + 1) if self.dropdown.open and self.dropdown.get_coords() == r: text_editor_h = self.text_editor.window.winfo_height() win_h, anchor = self.get_dropdown_height_anchor(r, text_editor_h) if anchor == "nw": self.coords( self.dropdown.canvas_id, 0, self.MT.row_positions[r] + text_editor_h - 1, ) self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) elif anchor == "sw": self.coords( self.dropdown.canvas_id, 0, self.MT.row_positions[r], ) self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) def refresh_open_window_positions(self, zoom: Literal["in", "out"]) -> None: if self.text_editor.open: r = self.text_editor.row self.text_editor.window.config(height=self.MT.row_positions[r + 1] - self.MT.row_positions[r]) self.text_editor.tktext.config(font=self.PAR.ops.index_font) self.coords( self.text_editor.canvas_id, 0, self.MT.row_positions[r], ) if self.dropdown.open: if zoom == "in": self.dropdown.window.zoom_in() elif zoom == "out": self.dropdown.window.zoom_out() r = self.dropdown.get_coords() if self.text_editor.open: text_editor_h = self.text_editor.window.winfo_height() win_h, anchor = self.get_dropdown_height_anchor(r, text_editor_h) else: text_editor_h = self.MT.row_positions[r + 1] - self.MT.row_positions[r] + 1 anchor = self.itemcget(self.dropdown.canvas_id, "anchor") # win_h = 0 self.dropdown.window.config(width=self.current_width + 1) if anchor == "nw": self.coords( self.dropdown.canvas_id, 0, self.MT.row_positions[r] + text_editor_h - 1, ) # self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) elif anchor == "sw": self.coords( self.dropdown.canvas_id, 0, self.MT.row_positions[r], ) # self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) def hide_text_editor(self) -> None: if self.text_editor.open: for binding in text_editor_to_unbind: self.text_editor.tktext.unbind(binding) self.itemconfig(self.text_editor.canvas_id, state="hidden") self.text_editor.open = False # r is displayed row def close_text_editor(self, event: tk.Event) -> Literal["break"] | None: # checking if text editor should be closed or not # errors if __tk_filedialog is open try: focused = self.focus_get() except Exception: focused = None try: if focused == self.text_editor.tktext.rc_popup_menu: return "break" except Exception: pass if focused is None: return "break" if event.keysym == "Escape": self.hide_text_editor_and_dropdown() self.focus_set() return text_editor_value = self.text_editor.get() r = self.text_editor.row datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] event_data = event_dict( name="end_edit_index", sheet=self.PAR.name, widget=self, cells_index={datarn: self.get_cell_data(datarn)}, key=event.keysym, value=text_editor_value, loc=r, row=r, boxes=self.MT.get_boxes(), selected=self.MT.selected, ) edited = False set_data = partial( self.set_cell_data_undo, r=r, datarn=datarn, check_input_valid=False, ) if self.MT.edit_validation_func: text_editor_value = self.MT.edit_validation_func(event_data) if text_editor_value is not None and self.input_valid_for_cell(datarn, text_editor_value): edited = set_data(value=text_editor_value) elif self.input_valid_for_cell(datarn, text_editor_value): edited = set_data(value=text_editor_value) if edited: try_binding(self.extra_end_edit_cell_func, event_data) self.MT.recreate_all_selection_boxes() self.hide_text_editor_and_dropdown() if event.keysym != "FocusOut": self.focus_set() return "break" def get_dropdown_height_anchor(self, r: int, text_editor_h: None | int = None) -> tuple[int, str]: win_h = 5 datarn = self.MT.datarn(r) for i, v in enumerate(self.get_cell_kwargs(datarn, key="dropdown")["values"]): v_numlines = len(v.split("\n") if isinstance(v, str) else f"{v}".split("\n")) if v_numlines > 1: win_h += ( self.MT.index_first_ln_ins + (v_numlines * self.MT.index_xtra_lines_increment) + 5 ) # end of cell else: win_h += self.MT.min_row_height if i == 5: break if win_h > 500: win_h = 500 space_bot = self.MT.get_space_bot(r, text_editor_h) space_top = int(self.MT.row_positions[r]) anchor = "nw" win_h2 = int(win_h) if win_h > space_bot: if space_bot >= space_top: anchor = "nw" win_h = space_bot - 1 elif space_top > space_bot: anchor = "sw" win_h = space_top - 1 if win_h < self.MT.index_txt_height + 5: win_h = self.MT.index_txt_height + 5 elif win_h > win_h2: win_h = win_h2 return win_h, anchor def dropdown_text_editor_modified( self, dd_window: object, event: dict, modified_func: Callable | None, ) -> None: if modified_func: modified_func(event) dd_window.search_and_see(event) # r is displayed row def open_dropdown_window(self, r: int, event: object = None) -> None: self.hide_text_editor() kwargs = self.get_cell_kwargs(self.MT.datarn(r), key="dropdown") if kwargs["state"] == "normal": if not self.open_text_editor(event=event, r=r, dropdown=True): return win_h, anchor = self.get_dropdown_height_anchor(r) win_w = self.current_width + 1 if anchor == "nw": if kwargs["state"] == "normal": self.text_editor.window.update_idletasks() ypos = self.MT.row_positions[r] + self.text_editor.window.winfo_height() - 1 else: ypos = self.MT.row_positions[r + 1] else: ypos = self.MT.row_positions[r] reset_kwargs = { "r": r, "c": 0, "width": win_w, "height": win_h, "font": self.PAR.ops.index_font, "ops": self.PAR.ops, "outline_color": self.PAR.ops.index_selected_rows_bg, "align": self.get_cell_align(r), "values": kwargs["values"], } if self.dropdown.window: self.dropdown.window.reset(**reset_kwargs) self.itemconfig(self.dropdown.canvas_id, state="normal", anchor=anchor) self.coords(self.dropdown.canvas_id, 0, ypos) self.dropdown.window.tkraise() else: self.dropdown.window = self.PAR.dropdown_class( self.winfo_toplevel(), **reset_kwargs, single_index="r", close_dropdown_window=self.close_dropdown_window, search_function=kwargs["search_function"], arrowkey_RIGHT=self.MT.arrowkey_RIGHT, arrowkey_LEFT=self.MT.arrowkey_LEFT, ) self.dropdown.canvas_id = self.create_window( (0, ypos), window=self.dropdown.window, anchor=anchor, ) if kwargs["state"] == "normal": self.text_editor.tktext.bind( "<>", lambda _x: self.dropdown_text_editor_modified( self.dropdown.window, event_dict( name="index_dropdown_modified", sheet=self.PAR.name, value=self.text_editor.get(), loc=r, row=r, boxes=self.MT.get_boxes(), selected=self.MT.selected, ), kwargs["modified_function"], ), ) self.update_idletasks() try: self.after(1, lambda: self.text_editor.tktext.focus()) self.after(2, self.text_editor.window.scroll_to_bottom()) except Exception: return redraw = False else: self.update_idletasks() self.dropdown.window.bind("", lambda _x: self.close_dropdown_window(r)) self.dropdown.window.bind("", self.close_dropdown_window) self.dropdown.window.focus_set() redraw = True self.dropdown.open = True if redraw: self.MT.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=True, redraw_table=False) # r is displayed row def close_dropdown_window( self, r: int | None = None, selection: object = None, redraw: bool = True, ) -> None: if r is not None and selection is not None: datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] kwargs = self.get_cell_kwargs(datarn, key="dropdown") pre_edit_value = self.get_cell_data(datarn) edited = False event_data = event_dict( name="end_edit_index", sheet=self.PAR.name, widget=self, cells_header={datarn: pre_edit_value}, key="??", value=selection, loc=r, row=r, boxes=self.MT.get_boxes(), selected=self.MT.selected, ) if kwargs["select_function"] is not None: kwargs["select_function"](event_data) if self.MT.edit_validation_func: selection = self.MT.edit_validation_func(event_data) if selection is not None: edited = self.set_cell_data_undo(r, datarn=datarn, value=selection, redraw=not redraw) else: edited = self.set_cell_data_undo(r, datarn=datarn, value=selection, redraw=not redraw) if edited: try_binding(self.extra_end_edit_cell_func, event_data) self.MT.recreate_all_selection_boxes() self.focus_set() self.hide_text_editor_and_dropdown(redraw=redraw) def hide_text_editor_and_dropdown(self, redraw: bool = True) -> None: self.hide_text_editor() self.hide_dropdown_window() if redraw: self.MT.refresh() def mouseclick_outside_editor_or_dropdown(self, inside: bool = False) -> int | None: closed_dd_coords = self.dropdown.get_coords() if self.text_editor.open: self.close_text_editor(new_tk_event("ButtonPress-1")) if closed_dd_coords is not None: self.hide_dropdown_window() if inside: self.MT.main_table_redraw_grid_and_text( redraw_header=False, redraw_row_index=True, redraw_table=False, ) return closed_dd_coords def mouseclick_outside_editor_or_dropdown_all_canvases(self, inside: bool = False) -> int | None: self.CH.mouseclick_outside_editor_or_dropdown() self.MT.mouseclick_outside_editor_or_dropdown() return self.mouseclick_outside_editor_or_dropdown(inside) def hide_dropdown_window(self) -> None: if self.dropdown.open: self.dropdown.window.unbind("") self.itemconfig(self.dropdown.canvas_id, state="hidden") self.dropdown.open = False # internal event use def set_cell_data_undo( self, r: int = 0, datarn: None | int = None, value: str = "", cell_resize: bool = True, undo: bool = True, redraw: bool = True, check_input_valid: bool = True, ) -> bool: if datarn is None: datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] event_data = event_dict( name="edit_index", sheet=self.PAR.name, widget=self, cells_index={datarn: self.get_cell_data(datarn)}, boxes=self.MT.get_boxes(), selected=self.MT.selected, ) edited = False if isinstance(self.MT._row_index, int): dispcn = self.MT.try_dispcn(self.MT._row_index) edited = self.MT.set_cell_data_undo( r=r, c=dispcn if isinstance(dispcn, int) else 0, datarn=datarn, datacn=self.MT._row_index, value=value, undo=True, cell_resize=isinstance(dispcn, int), ) else: self.fix_index(datarn) if not check_input_valid or self.input_valid_for_cell(datarn, value): if self.MT.undo_enabled and undo: self.MT.undo_stack.append(pickled_event_dict(event_data)) self.set_cell_data(datarn=datarn, value=value) edited = True if edited and cell_resize and self.PAR.ops.cell_auto_resize_enabled: self.set_row_height_run_binding(r, only_if_too_small=False) if redraw: self.MT.refresh() if edited: self.MT.sheet_modified(event_data) return edited def set_cell_data(self, datarn: int | None = None, value: object = "") -> None: if isinstance(self.MT._row_index, int): self.MT.set_cell_data(datarn=datarn, datacn=self.MT._row_index, value=value) else: self.fix_index(datarn) if self.get_cell_kwargs(datarn, key="checkbox"): self.MT._row_index[datarn] = try_to_bool(value) else: self.MT._row_index[datarn] = value def input_valid_for_cell(self, datarn: int, value: object, check_readonly: bool = True) -> bool: if check_readonly and self.get_cell_kwargs(datarn, key="readonly"): return False if self.get_cell_kwargs(datarn, key="checkbox"): return is_bool_like(value) if self.cell_equal_to(datarn, value): return False kwargs = self.get_cell_kwargs(datarn, key="dropdown") if kwargs and kwargs["validate_input"] and value not in kwargs["values"]: return False return True def cell_equal_to(self, datarn: int, value: object) -> bool: self.fix_index(datarn) if isinstance(self.MT._row_index, list): return self.MT._row_index[datarn] == value elif isinstance(self.MT._row_index, int): return self.MT.cell_equal_to(datarn, self.MT._row_index, value) def get_cell_data( self, datarn: int, get_displayed: bool = False, none_to_empty_str: bool = False, redirect_int: bool = False, ) -> object: if get_displayed: return self.get_valid_cell_data_as_str(datarn, fix=False) if redirect_int and isinstance(self.MT._row_index, int): # internal use return self.MT.get_cell_data(datarn, self.MT._row_index, none_to_empty_str=True) if ( isinstance(self.MT._row_index, int) or not self.MT._row_index or datarn >= len(self.MT._row_index) or (self.MT._row_index[datarn] is None and none_to_empty_str) ): return "" return self.MT._row_index[datarn] def get_valid_cell_data_as_str(self, datarn: int, fix: bool = True) -> str: kwargs = self.get_cell_kwargs(datarn, key="dropdown") if kwargs: if kwargs["text"] is not None: return f"{kwargs['text']}" else: kwargs = self.get_cell_kwargs(datarn, key="checkbox") if kwargs: return f"{kwargs['text']}" if isinstance(self.MT._row_index, int): return self.MT.get_valid_cell_data_as_str(datarn, self.MT._row_index, get_displayed=True) if fix: self.fix_index(datarn) try: value = "" if self.MT._row_index[datarn] is None else f"{self.MT._row_index[datarn]}" except Exception: value = "" if not value and self.PAR.ops.show_default_index_for_empty: value = get_n2a(datarn, self.default_index) return value def get_value_for_empty_cell(self, datarn: int, r_ops: bool = True) -> object: if self.get_cell_kwargs(datarn, key="checkbox", cell=r_ops): return False kwargs = self.get_cell_kwargs(datarn, key="dropdown", cell=r_ops) if kwargs and kwargs["validate_input"] and kwargs["values"]: return kwargs["values"][0] return "" def get_empty_index_seq(self, end: int, start: int = 0, r_ops: bool = True) -> list[object]: return [self.get_value_for_empty_cell(datarn, r_ops=r_ops) for datarn in range(start, end)] def fix_index(self, datarn: int | None = None) -> None: if isinstance(self.MT._row_index, int): return if isinstance(self.MT._row_index, float): self.MT._row_index = int(self.MT._row_index) return if not isinstance(self.MT._row_index, list): try: self.MT._row_index = list(self.MT._row_index) except Exception: self.MT._row_index = [] if isinstance(datarn, int) and datarn >= len(self.MT._row_index): self.MT._row_index.extend(self.get_empty_index_seq(end=datarn + 1, start=len(self.MT._row_index))) def set_row_height_run_binding(self, r: int, only_if_too_small: bool = True) -> None: old_height = self.MT.row_positions[r + 1] - self.MT.row_positions[r] new_height = self.set_row_height(r, only_if_too_small=only_if_too_small) if self.row_height_resize_func is not None and old_height != new_height: self.row_height_resize_func( event_dict( name="resize", sheet=self.PAR.name, resized_rows={r: {"old_size": old_height, "new_size": new_height}}, ) ) # internal event use def click_checkbox(self, r: int, datarn: int | None = None, undo: bool = True, redraw: bool = True) -> None: if datarn is None: datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] kwargs = self.get_cell_kwargs(datarn, key="checkbox") if kwargs["state"] == "normal": pre_edit_value = self.get_cell_data(datarn) if isinstance(self.MT._row_index, list): value = not self.MT._row_index[datarn] if isinstance(self.MT._row_index[datarn], bool) else False elif isinstance(self.MT._row_index, int): value = ( not self.MT.data[datarn][self.MT._row_index] if isinstance(self.MT.data[datarn][self.MT._row_index], bool) else False ) else: value = False self.set_cell_data_undo(r, datarn=datarn, value=value, cell_resize=False) event_data = event_dict( name="end_edit_index", sheet=self.PAR.name, widget=self, cells_index={datarn: pre_edit_value}, key="??", value=value, loc=r, row=r, boxes=self.MT.get_boxes(), selected=self.MT.selected, ) if kwargs["check_function"] is not None: kwargs["check_function"](event_data) try_binding(self.extra_end_edit_cell_func, event_data) if redraw: self.MT.refresh() def get_cell_kwargs(self, datarn: int, key: Hashable = "dropdown", cell: bool = True) -> dict: if cell and datarn in self.cell_options and key in self.cell_options[datarn]: return self.cell_options[datarn][key] return {} # Treeview Mode def get_node_level(self, node: Node, level: int = 0) -> Generator[int]: yield level if node.parent: yield from self.get_node_level(node.parent, level + 1) def ancestors_all_open(self, iid: str, stop_at: str | Node = "") -> bool: if stop_at: stop_at = stop_at.iid for iid in self.get_iid_ancestors(iid): if iid == stop_at: return True if iid not in self.tree_open_ids: return False return True return all(map(self.tree_open_ids.__contains__, self.get_iid_ancestors(iid))) def get_iid_ancestors(self, iid: str) -> Generator[str]: if self.tree[iid].parent: yield self.tree[iid].parent.iid yield from self.get_iid_ancestors(self.tree[iid].parent.iid) def get_iid_descendants(self, iid: str, check_open: bool = False) -> Generator[str]: for cnode in self.tree[iid].children: yield cnode.iid if (check_open and cnode.children and cnode.iid in self.tree_open_ids) or ( not check_open and cnode.children ): yield from self.get_iid_descendants(cnode.iid, check_open) def items_parent(self, iid: str) -> str: if self.tree[iid].parent: return self.tree[iid].parent.iid return "" def gen_top_nodes(self) -> Generator[Node]: yield from (node for node in self.MT._row_index if node.parent == "") def get_treeview_indent(self, iid: str) -> int: if isinstance(self.PAR.ops.treeview_indent, str): indent = self.MT.index_txt_width * int(self.PAR.ops.treeview_indent) else: indent = self.PAR.ops.treeview_indent return indent * max(self.get_node_level(self.tree[iid])) def remove_node_from_parents_children(self, node: Node) -> None: if node.parent: node.parent.children.remove(node) if not node.parent.children: self.tree_open_ids.discard(node.parent) def build_pid_causes_recursive_loop(self, iid: str, pid: str) -> bool: return any( i == pid for i in chain( self.get_iid_descendants(iid), islice(self.get_iid_ancestors(iid), 1, None), ) ) def move_pid_causes_recursive_loop(self, to_move_iid: str, move_to_parent: str) -> bool: # if the parent the item is being moved under is one of the item's descendants # then it is a recursive loop return any(move_to_parent == diid for diid in self.get_iid_descendants(to_move_iid)) tksheet-7.2.12/tksheet/sheet.py000066400000000000000000010133171463432073300164160ustar00rootroot00000000000000from __future__ import annotations import tkinter as tk from bisect import bisect_left from collections import defaultdict, deque from collections.abc import ( Callable, Generator, Hashable, Iterator, Sequence, ) from itertools import ( accumulate, chain, filterfalse, islice, product, repeat, ) from timeit import default_timer from tkinter import ttk from typing import Literal from .column_headers import ColumnHeaders from .functions import ( add_highlight, add_to_options, consecutive_ranges, convert_align, data_to_displayed_idxs, del_from_options, del_named_span_options, del_named_span_options_nested, dropdown_search_function, event_dict, fix_format_kwargs, force_bool, get_checkbox_dict, get_checkbox_kwargs, get_dropdown_dict, get_dropdown_kwargs, idx_param_to_int, is_iterable, key_to_span, new_tk_event, num2alpha, pickled_event_dict, pop_positions, set_align, set_readonly, span_froms, span_ranges, tksheet_type_error, unpack, ) from .main_table import MainTable from .other_classes import ( DotDict, EventDataDict, FontTuple, GeneratedMouseEvent, Node, ProgressBar, Selected, SelectionBox, Span, ) from .row_index import RowIndex from .sheet_options import ( new_sheet_options, ) from .themes import ( theme_black, theme_dark, theme_dark_blue, theme_dark_green, theme_light_blue, theme_light_green, ) from .top_left_rectangle import TopLeftRectangle from .types import ( CreateSpanTypes, ) from .vars import ( USER_OS, backwards_compatibility_keys, emitted_events, named_span_types, rc_binding, scrollbar_options_keys, ) class Sheet(tk.Frame): def __init__( self, parent: tk.Misc, name: str = "!sheet", show_table: bool = True, show_top_left: bool = True, show_row_index: bool = True, show_header: bool = True, show_x_scrollbar: bool = True, show_y_scrollbar: bool = True, width: int | None = None, height: int | None = None, headers: None | list[object] = None, header: None | list[object] = None, row_index: None | list[object] = None, index: None | list[object] = None, default_header: Literal["letters", "numbers", "both"] = "letters", default_row_index: Literal["letters", "numbers", "both"] = "numbers", data_reference: None | Sequence[Sequence[object]] = None, data: None | Sequence[Sequence[object]] = None, # either (start row, end row, "rows"), (start column, end column, "rows") or # (cells start row, cells start column, cells end row, cells end column, "cells") # noqa: E501 startup_select: tuple[int, int, str] | tuple[int, int, int, int, str] = None, startup_focus: bool = True, total_columns: int | None = None, total_rows: int | None = None, default_column_width: int = 120, default_header_height: str | int = "1", default_row_index_width: int = 70, default_row_height: str | int = "1", max_column_width: Literal["inf"] | float = "inf", max_row_height: Literal["inf"] | float = "inf", max_header_height: Literal["inf"] | float = "inf", max_index_width: Literal["inf"] | float = "inf", after_redraw_time_ms: int = 20, set_all_heights_and_widths: bool = False, zoom: int = 100, align: str = "w", header_align: str = "center", row_index_align: str | None = None, index_align: str = "center", displayed_columns: list[int] = [], all_columns_displayed: bool = True, displayed_rows: list[int] = [], all_rows_displayed: bool = True, to_clipboard_delimiter: str = "\t", to_clipboard_quotechar: str = '"', to_clipboard_lineterminator: str = "\n", from_clipboard_delimiters: list[str] | str = ["\t"], show_default_header_for_empty: bool = True, show_default_index_for_empty: bool = True, page_up_down_select_row: bool = True, paste_can_expand_x: bool = False, paste_can_expand_y: bool = False, paste_insert_column_limit: int | None = None, paste_insert_row_limit: int | None = None, show_dropdown_borders: bool = False, arrow_key_down_right_scroll_page: bool = False, cell_auto_resize_enabled: bool = True, auto_resize_row_index: bool | Literal["empty"] = "empty", auto_resize_columns: int | None = None, auto_resize_rows: int | None = None, set_cell_sizes_on_zoom: bool = False, font: tuple[str, int, str] = FontTuple( "Calibri", 13 if USER_OS == "darwin" else 11, "normal", ), header_font: tuple[str, int, str] = FontTuple( "Calibri", 13 if USER_OS == "darwin" else 11, "normal", ), index_font: tuple[str, int, str] = FontTuple( "Calibri", 13 if USER_OS == "darwin" else 11, "normal", ), # currently has no effect popup_menu_font: tuple[str, int, str] = FontTuple( "Calibri", 13 if USER_OS == "darwin" else 11, "normal", ), max_undos: int = 30, column_drag_and_drop_perform: bool = True, row_drag_and_drop_perform: bool = True, empty_horizontal: int = 50, empty_vertical: int = 50, selected_rows_to_end_of_window: bool = False, horizontal_grid_to_end_of_window: bool = False, vertical_grid_to_end_of_window: bool = False, show_vertical_grid: bool = True, show_horizontal_grid: bool = True, display_selected_fg_over_highlights: bool = False, show_selected_cells_border: bool = True, treeview: bool = False, treeview_indent: str | int = "6", rounded_boxes: bool = True, alternate_color: str = "", # colors outline_thickness: int = 0, outline_color: str = theme_light_blue["outline_color"], theme: str = "light blue", frame_bg: str = theme_light_blue["table_bg"], popup_menu_fg: str = theme_light_blue["popup_menu_fg"], popup_menu_bg: str = theme_light_blue["popup_menu_bg"], popup_menu_highlight_bg: str = theme_light_blue["popup_menu_highlight_bg"], popup_menu_highlight_fg: str = theme_light_blue["popup_menu_highlight_fg"], table_grid_fg: str = theme_light_blue["table_grid_fg"], table_bg: str = theme_light_blue["table_bg"], table_fg: str = theme_light_blue["table_fg"], table_selected_box_cells_fg: str = theme_light_blue["table_selected_box_cells_fg"], table_selected_box_rows_fg: str = theme_light_blue["table_selected_box_rows_fg"], table_selected_box_columns_fg: str = theme_light_blue["table_selected_box_columns_fg"], table_selected_cells_border_fg: str = theme_light_blue["table_selected_cells_border_fg"], table_selected_cells_bg: str = theme_light_blue["table_selected_cells_bg"], table_selected_cells_fg: str = theme_light_blue["table_selected_cells_fg"], table_selected_rows_border_fg: str = theme_light_blue["table_selected_rows_border_fg"], table_selected_rows_bg: str = theme_light_blue["table_selected_rows_bg"], table_selected_rows_fg: str = theme_light_blue["table_selected_rows_fg"], table_selected_columns_border_fg: str = theme_light_blue["table_selected_columns_border_fg"], table_selected_columns_bg: str = theme_light_blue["table_selected_columns_bg"], table_selected_columns_fg: str = theme_light_blue["table_selected_columns_fg"], resizing_line_fg: str = theme_light_blue["resizing_line_fg"], drag_and_drop_bg: str = theme_light_blue["drag_and_drop_bg"], index_bg: str = theme_light_blue["index_bg"], index_border_fg: str = theme_light_blue["index_border_fg"], index_grid_fg: str = theme_light_blue["index_grid_fg"], index_fg: str = theme_light_blue["index_fg"], index_selected_cells_bg: str = theme_light_blue["index_selected_cells_bg"], index_selected_cells_fg: str = theme_light_blue["index_selected_cells_fg"], index_selected_rows_bg: str = theme_light_blue["index_selected_rows_bg"], index_selected_rows_fg: str = theme_light_blue["index_selected_rows_fg"], index_hidden_rows_expander_bg: str = theme_light_blue["index_hidden_rows_expander_bg"], header_bg: str = theme_light_blue["header_bg"], header_border_fg: str = theme_light_blue["header_border_fg"], header_grid_fg: str = theme_light_blue["header_grid_fg"], header_fg: str = theme_light_blue["header_fg"], header_selected_cells_bg: str = theme_light_blue["header_selected_cells_bg"], header_selected_cells_fg: str = theme_light_blue["header_selected_cells_fg"], header_selected_columns_bg: str = theme_light_blue["header_selected_columns_bg"], header_selected_columns_fg: str = theme_light_blue["header_selected_columns_fg"], header_hidden_columns_expander_bg: str = theme_light_blue["header_hidden_columns_expander_bg"], top_left_bg: str = theme_light_blue["top_left_bg"], top_left_fg: str = theme_light_blue["top_left_fg"], top_left_fg_highlight: str = theme_light_blue["top_left_fg_highlight"], vertical_scroll_background: str = theme_light_blue["vertical_scroll_background"], horizontal_scroll_background: str = theme_light_blue["horizontal_scroll_background"], vertical_scroll_troughcolor: str = theme_light_blue["vertical_scroll_troughcolor"], horizontal_scroll_troughcolor: str = theme_light_blue["horizontal_scroll_troughcolor"], vertical_scroll_lightcolor: str = theme_light_blue["vertical_scroll_lightcolor"], horizontal_scroll_lightcolor: str = theme_light_blue["horizontal_scroll_lightcolor"], vertical_scroll_darkcolor: str = theme_light_blue["vertical_scroll_darkcolor"], horizontal_scroll_darkcolor: str = theme_light_blue["horizontal_scroll_darkcolor"], vertical_scroll_relief: str = theme_light_blue["vertical_scroll_relief"], horizontal_scroll_relief: str = theme_light_blue["horizontal_scroll_relief"], vertical_scroll_troughrelief: str = theme_light_blue["vertical_scroll_troughrelief"], horizontal_scroll_troughrelief: str = theme_light_blue["horizontal_scroll_troughrelief"], vertical_scroll_bordercolor: str = theme_light_blue["vertical_scroll_bordercolor"], horizontal_scroll_bordercolor: str = theme_light_blue["horizontal_scroll_bordercolor"], vertical_scroll_borderwidth: int = 1, horizontal_scroll_borderwidth: int = 1, vertical_scroll_gripcount: int = 0, horizontal_scroll_gripcount: int = 0, vertical_scroll_active_bg: str = theme_light_blue["vertical_scroll_active_bg"], horizontal_scroll_active_bg: str = theme_light_blue["horizontal_scroll_active_bg"], vertical_scroll_not_active_bg: str = theme_light_blue["vertical_scroll_not_active_bg"], horizontal_scroll_not_active_bg: str = theme_light_blue["horizontal_scroll_not_active_bg"], vertical_scroll_pressed_bg: str = theme_light_blue["vertical_scroll_pressed_bg"], horizontal_scroll_pressed_bg: str = theme_light_blue["horizontal_scroll_pressed_bg"], vertical_scroll_active_fg: str = theme_light_blue["vertical_scroll_active_fg"], horizontal_scroll_active_fg: str = theme_light_blue["horizontal_scroll_active_fg"], vertical_scroll_not_active_fg: str = theme_light_blue["vertical_scroll_not_active_fg"], horizontal_scroll_not_active_fg: str = theme_light_blue["horizontal_scroll_not_active_fg"], vertical_scroll_pressed_fg: str = theme_light_blue["vertical_scroll_pressed_fg"], horizontal_scroll_pressed_fg: str = theme_light_blue["horizontal_scroll_pressed_fg"], scrollbar_theme_inheritance: str = "default", scrollbar_show_arrows: bool = True, # changing the arrowsize (width) of the scrollbars # is not working with 'default' theme # use 'clam' theme instead if you want to change the width vertical_scroll_arrowsize: str | int = "", horizontal_scroll_arrowsize: str | int = "", # backwards compatibility column_width: int | None = None, header_height: str | int | None = None, row_height: str | int | None = None, row_index_width: int | None = None, expand_sheet_if_paste_too_big: bool | None = None, ) -> None: tk.Frame.__init__( self, parent, background=frame_bg, highlightthickness=outline_thickness, highlightbackground=outline_color, highlightcolor=outline_color, ) self.ops = new_sheet_options() if column_width is not None: default_column_width = column_width if header_height is not None: default_header_height = header_height if row_height is not None: default_row_height = row_height if row_index_width is not None: default_row_index_width = row_index_width if expand_sheet_if_paste_too_big is not None: paste_can_expand_x = expand_sheet_if_paste_too_big paste_can_expand_y = expand_sheet_if_paste_too_big if treeview: index_align = "w" auto_resize_row_index = True for k, v in locals().items(): if (xk := backwards_compatibility_keys.get(k, k)) in self.ops and v != self.ops[xk]: self.ops[xk] = v self.ops.from_clipboard_delimiters = ( from_clipboard_delimiters if isinstance(from_clipboard_delimiters, str) else "".join(from_clipboard_delimiters) ) self.PAR = parent self.name = name self.last_event_data = EventDataDict() self.bound_events = DotDict({k: [] for k in emitted_events}) self.dropdown_class = Dropdown self.after_redraw_id = None self.after_redraw_time_ms = after_redraw_time_ms self.named_span_id = 0 if width is not None or height is not None: self.grid_propagate(0) if width is not None: self.config(width=width) if height is not None: self.config(height=height) if width is not None and height is None: self.config(height=300) if height is not None and width is None: self.config(width=350) self.grid_columnconfigure(1, weight=1) self.grid_rowconfigure(1, weight=1) self.RI = RowIndex( parent=self, row_index_align=( convert_align(row_index_align) if row_index_align is not None else convert_align(index_align) ), default_row_index=default_row_index, ) self.CH = ColumnHeaders( parent=self, default_header=default_header, header_align=convert_align(header_align), ) self.MT = MainTable( parent=self, max_column_width=max_column_width, max_header_height=max_header_height, max_row_height=max_row_height, max_index_width=max_index_width, show_index=show_row_index, show_header=show_header, column_headers_canvas=self.CH, row_index_canvas=self.RI, headers=headers, header=header, data_reference=data if data_reference is None else data_reference, total_cols=total_columns, total_rows=total_rows, row_index=row_index, index=index, zoom=zoom, align=convert_align(align), displayed_columns=displayed_columns, all_columns_displayed=all_columns_displayed, displayed_rows=displayed_rows, all_rows_displayed=all_rows_displayed, ) self.TL = TopLeftRectangle( parent=self, main_canvas=self.MT, row_index_canvas=self.RI, header_canvas=self.CH, ) self.unique_id = f"{default_timer()}{self.winfo_id()}".replace(".", "") style = ttk.Style() for orientation in ("Vertical", "Horizontal"): style.element_create( f"Sheet{self.unique_id}.{orientation}.TScrollbar.trough", "from", scrollbar_theme_inheritance, ) style.element_create( f"Sheet{self.unique_id}.{orientation}.TScrollbar.thumb", "from", scrollbar_theme_inheritance, ) style.element_create( f"Sheet{self.unique_id}.{orientation}.TScrollbar.grip", "from", scrollbar_theme_inheritance, ) if not scrollbar_show_arrows: style.layout( f"Sheet{self.unique_id}.{orientation}.TScrollbar", [ ( f"Sheet{self.unique_id}.{orientation}.TScrollbar.trough", { "children": [ ( f"Sheet{self.unique_id}.{orientation}.TScrollbar.thumb", { "unit": "1", "children": [ ( f"Sheet{self.unique_id}.{orientation}.TScrollbar.grip", {"sticky": ""}, ) ], "sticky": "nswe", }, ) ], "sticky": "ns" if orientation == "Vertical" else "ew", }, ) ], ) self.set_scrollbar_options() self.yscroll = ttk.Scrollbar( self, command=self.MT._yscrollbar, orient="vertical", style=f"Sheet{self.unique_id}.Vertical.TScrollbar", ) self.xscroll = ttk.Scrollbar( self, command=self.MT._xscrollbar, orient="horizontal", style=f"Sheet{self.unique_id}.Horizontal.TScrollbar", ) if show_top_left: self.TL.grid(row=0, column=0) if show_table: self.MT.grid(row=1, column=1, sticky="nswe") self.MT["xscrollcommand"] = self.xscroll.set self.MT["yscrollcommand"] = self.yscroll.set if show_row_index: self.RI.grid(row=1, column=0, sticky="nswe") self.RI["yscrollcommand"] = self.yscroll.set if show_header: self.CH.grid(row=0, column=1, sticky="nswe") self.CH["xscrollcommand"] = self.xscroll.set if show_x_scrollbar: self.xscroll.grid(row=2, column=0, columnspan=2, sticky="nswe") self.xscroll_showing = True self.xscroll_disabled = False else: self.xscroll_showing = False self.xscroll_disabled = True if show_y_scrollbar: self.yscroll.grid(row=0, column=2, rowspan=3, sticky="nswe") self.yscroll_showing = True self.yscroll_disabled = False else: self.yscroll_showing = False self.yscroll_disabled = True self.update_idletasks() self.MT.update_idletasks() self.RI.update_idletasks() self.CH.update_idletasks() if theme != "light blue": self.change_theme(theme) for k, v in locals().items(): if k in theme_light_blue and v != theme_light_blue[k]: self.set_options(**{k: v}) if set_all_heights_and_widths: self.set_all_cell_sizes_to_text() if startup_select is not None: try: if startup_select[-1] == "cells": self.MT.create_selection_box(*startup_select) self.see(startup_select[0], startup_select[1]) elif startup_select[-1] == "rows": self.MT.create_selection_box( startup_select[0], 0, startup_select[1], len(self.MT.col_positions) - 1, "rows", ) self.see(startup_select[0], 0) elif startup_select[-1] in ("cols", "columns"): self.MT.create_selection_box( 0, startup_select[0], len(self.MT.row_positions) - 1, startup_select[1], "columns", ) self.see(0, startup_select[0]) except Exception: pass self.refresh() if startup_focus: self.MT.focus_set() # Sheet Colors def change_theme(self, theme: str = "light blue", redraw: bool = True) -> Sheet: if theme.lower() in ("light blue", "light_blue"): self.set_options(**theme_light_blue, redraw=False) self.config(bg=theme_light_blue["table_bg"]) elif theme.lower() == "dark": self.set_options(**theme_dark, redraw=False) self.config(bg=theme_dark["table_bg"]) elif theme.lower() in ("light green", "light_green"): self.set_options(**theme_light_green, redraw=False) self.config(bg=theme_light_green["table_bg"]) elif theme.lower() in ("dark blue", "dark_blue"): self.set_options(**theme_dark_blue, redraw=False) self.config(bg=theme_dark_blue["table_bg"]) elif theme.lower() in ("dark green", "dark_green"): self.set_options(**theme_dark_green, redraw=False) self.config(bg=theme_dark_green["table_bg"]) elif theme.lower() == "black": self.set_options(**theme_black, redraw=False) self.config(bg=theme_black["table_bg"]) self.MT.recreate_all_selection_boxes() self.set_refresh_timer(redraw) self.TL.redraw() return self # Header and Index def set_header_data( self, value: object, c: int | None | Iterator[int] = None, redraw: bool = True, ) -> Sheet: if c is None: if not isinstance(value, int) and not is_iterable(value): raise ValueError(f"Argument 'value' must be non-string iterable or int, not {type(value)} type.") if not isinstance(value, list) and not isinstance(value, int): value = list(value) self.MT._headers = value elif isinstance(c, int): self.CH.set_cell_data(c, value) elif is_iterable(value) and is_iterable(c): for c_, v in zip(c, value): self.CH.set_cell_data(c_, v) return self.set_refresh_timer(redraw) def headers( self, newheaders: object = None, index: None | int = None, reset_col_positions: bool = False, show_headers_if_not_sheet: bool = True, redraw: bool = True, ) -> object: self.set_refresh_timer(redraw) return self.MT.headers( newheaders, index, reset_col_positions=reset_col_positions, show_headers_if_not_sheet=show_headers_if_not_sheet, redraw=False, ) def set_index_data( self, value: object, r: int | None | Iterator[int] = None, redraw: bool = True, ) -> Sheet: if r is None: if not isinstance(value, int) and not is_iterable(value): raise ValueError(f"Argument 'value' must be non-string iterable or int, not {type(value)} type.") if not isinstance(value, list) and not isinstance(value, int): value = list(value) self.MT._row_index = value elif isinstance(r, int): self.RI.set_cell_data(r, value) elif is_iterable(value) and is_iterable(r): for r_, v in zip(r, value): self.RI.set_cell_data(r_, v) return self.set_refresh_timer(redraw) def row_index( self, newindex: object = None, index: None | int = None, reset_row_positions: bool = False, show_index_if_not_sheet: bool = True, redraw: bool = True, ) -> object: self.set_refresh_timer(redraw) return self.MT.row_index( newindex, index, reset_row_positions=reset_row_positions, show_index_if_not_sheet=show_index_if_not_sheet, redraw=False, ) # Bindings and Functionality def enable_bindings(self, *bindings: str) -> Sheet: self.MT.enable_bindings(bindings) return self def disable_bindings(self, *bindings: str) -> Sheet: self.MT.disable_bindings(bindings) return self def extra_bindings( self, bindings: str | list | tuple | None = None, func: Callable | None = None, ) -> Sheet: # bindings is None, unbind all if bindings is None: bindings = "all" # bindings is str, func arg is None or Callable if isinstance(bindings, str): iterable = [(bindings, func)] # bindings is list or tuple of strings, func arg is None or Callable elif is_iterable(bindings) and isinstance(bindings[0], str): iterable = [(b, func) for b in bindings] # bindings is a list or tuple of two tuples or lists # in this case the func arg is ignored # e.g. [(binding, function), (binding, function), ...] elif is_iterable(bindings): iterable = bindings for b, f in iterable: b = b.lower() if f is not None and b in emitted_events: self.bind(b, f) if b in ( "all", "bind_all", "unbind_all", ): self.MT.extra_begin_ctrl_c_func = f self.MT.extra_begin_ctrl_x_func = f self.MT.extra_begin_ctrl_v_func = f self.MT.extra_begin_ctrl_z_func = f self.MT.extra_begin_delete_key_func = f self.RI.ri_extra_begin_drag_drop_func = f self.CH.ch_extra_begin_drag_drop_func = f self.MT.extra_begin_del_rows_rc_func = f self.MT.extra_begin_del_cols_rc_func = f self.MT.extra_begin_insert_cols_rc_func = f self.MT.extra_begin_insert_rows_rc_func = f self.MT.extra_begin_edit_cell_func = f self.CH.extra_begin_edit_cell_func = f self.RI.extra_begin_edit_cell_func = f self.CH.column_width_resize_func = f self.RI.row_height_resize_func = f if b in ( "all", "bind_all", "unbind_all", "all_select_events", "select", "selectevents", "select_events", ): self.MT.selection_binding_func = f self.MT.select_all_binding_func = f self.RI.selection_binding_func = f self.CH.selection_binding_func = f self.MT.drag_selection_binding_func = f self.RI.drag_selection_binding_func = f self.CH.drag_selection_binding_func = f self.MT.shift_selection_binding_func = f self.RI.shift_selection_binding_func = f self.CH.shift_selection_binding_func = f self.MT.ctrl_selection_binding_func = f self.RI.ctrl_selection_binding_func = f self.CH.ctrl_selection_binding_func = f self.MT.deselection_binding_func = f if b in ( "all", "bind_all", "unbind_all", "all_modified_events", "sheetmodified", "sheet_modified" "modified_events", "modified", ): self.MT.extra_end_ctrl_c_func = f self.MT.extra_end_ctrl_x_func = f self.MT.extra_end_ctrl_v_func = f self.MT.extra_end_ctrl_z_func = f self.MT.extra_end_delete_key_func = f self.RI.ri_extra_end_drag_drop_func = f self.CH.ch_extra_end_drag_drop_func = f self.MT.extra_end_del_rows_rc_func = f self.MT.extra_end_del_cols_rc_func = f self.MT.extra_end_insert_cols_rc_func = f self.MT.extra_end_insert_rows_rc_func = f self.MT.extra_end_edit_cell_func = f self.CH.extra_end_edit_cell_func = f self.RI.extra_end_edit_cell_func = f if b in ( "begin_copy", "begin_ctrl_c", ): self.MT.extra_begin_ctrl_c_func = f if b in ( "ctrl_c", "end_copy", "end_ctrl_c", "copy", ): self.MT.extra_end_ctrl_c_func = f if b in ( "begin_cut", "begin_ctrl_x", ): self.MT.extra_begin_ctrl_x_func = f if b in ( "ctrl_x", "end_cut", "end_ctrl_x", "cut", ): self.MT.extra_end_ctrl_x_func = f if b in ( "begin_paste", "begin_ctrl_v", ): self.MT.extra_begin_ctrl_v_func = f if b in ( "ctrl_v", "end_paste", "end_ctrl_v", "paste", ): self.MT.extra_end_ctrl_v_func = f if b in ( "begin_undo", "begin_ctrl_z", ): self.MT.extra_begin_ctrl_z_func = f if b in ( "ctrl_z", "end_undo", "end_ctrl_z", "undo", ): self.MT.extra_end_ctrl_z_func = f if b in ( "begin_delete_key", "begin_delete", ): self.MT.extra_begin_delete_key_func = f if b in ( "delete_key", "end_delete", "end_delete_key", "delete", ): self.MT.extra_end_delete_key_func = f if b in ( "begin_edit_cell", "begin_edit_table", ): self.MT.extra_begin_edit_cell_func = f if b in ( "end_edit_cell", "edit_cell", "edit_table", ): self.MT.extra_end_edit_cell_func = f if b == "begin_edit_header": self.CH.extra_begin_edit_cell_func = f if b in ( "end_edit_header", "edit_header", ): self.CH.extra_end_edit_cell_func = f if b == "begin_edit_index": self.RI.extra_begin_edit_cell_func = f if b in ( "end_edit_index", "edit_index", ): self.RI.extra_end_edit_cell_func = f if b in ( "begin_row_index_drag_drop", "begin_move_rows", ): self.RI.ri_extra_begin_drag_drop_func = f if b in ( "row_index_drag_drop", "move_rows", "end_move_rows", "end_row_index_drag_drop", ): self.RI.ri_extra_end_drag_drop_func = f if b in ( "begin_column_header_drag_drop", "begin_move_columns", ): self.CH.ch_extra_begin_drag_drop_func = f if b in ( "column_header_drag_drop", "move_columns", "end_move_columns", "end_column_header_drag_drop", ): self.CH.ch_extra_end_drag_drop_func = f if b in ( "begin_rc_delete_row", "begin_delete_rows", ): self.MT.extra_begin_del_rows_rc_func = f if b in ( "rc_delete_row", "end_rc_delete_row", "end_delete_rows", "delete_rows", ): self.MT.extra_end_del_rows_rc_func = f if b in ( "begin_rc_delete_column", "begin_delete_columns", ): self.MT.extra_begin_del_cols_rc_func = f if b in ( "rc_delete_column", "end_rc_delete_column", "end_delete_columns", "delete_columns", ): self.MT.extra_end_del_cols_rc_func = f if b in ( "begin_rc_insert_column", "begin_insert_column", "begin_insert_columns", "begin_add_column", "begin_rc_add_column", "begin_add_columns", ): self.MT.extra_begin_insert_cols_rc_func = f if b in ( "rc_insert_column", "end_rc_insert_column", "end_insert_column", "end_insert_columns", "rc_add_column", "end_rc_add_column", "end_add_column", "end_add_columns", ): self.MT.extra_end_insert_cols_rc_func = f if b in ( "begin_rc_insert_row", "begin_insert_row", "begin_insert_rows", "begin_rc_add_row", "begin_add_row", "begin_add_rows", ): self.MT.extra_begin_insert_rows_rc_func = f if b in ( "rc_insert_row", "end_rc_insert_row", "end_insert_row", "end_insert_rows", "rc_add_row", "end_rc_add_row", "end_add_row", "end_add_rows", ): self.MT.extra_end_insert_rows_rc_func = f if b == "column_width_resize": self.CH.column_width_resize_func = f if b == "row_height_resize": self.RI.row_height_resize_func = f if b == "cell_select": self.MT.selection_binding_func = f if b in ( "select_all", "ctrl_a", ): self.MT.select_all_binding_func = f if b == "row_select": self.RI.selection_binding_func = f if b in ( "col_select", "column_select", ): self.CH.selection_binding_func = f if b == "drag_select_cells": self.MT.drag_selection_binding_func = f if b == "drag_select_rows": self.RI.drag_selection_binding_func = f if b == "drag_select_columns": self.CH.drag_selection_binding_func = f if b == "shift_cell_select": self.MT.shift_selection_binding_func = f if b == "shift_row_select": self.RI.shift_selection_binding_func = f if b == "shift_column_select": self.CH.shift_selection_binding_func = f if b == "ctrl_cell_select": self.MT.ctrl_selection_binding_func = f if b == "ctrl_row_select": self.RI.ctrl_selection_binding_func = f if b == "ctrl_column_select": self.CH.ctrl_selection_binding_func = f if b == "deselect": self.MT.deselection_binding_func = f return self def bind( self, binding: str, func: Callable, add: str | None = None, ) -> Sheet: if binding == "": self.MT.extra_b1_press_func = func self.CH.extra_b1_press_func = func self.RI.extra_b1_press_func = func self.TL.extra_b1_press_func = func elif binding == "": self.MT.extra_b1_motion_func = func self.CH.extra_b1_motion_func = func self.RI.extra_b1_motion_func = func self.TL.extra_b1_motion_func = func elif binding == "": self.MT.extra_b1_release_func = func self.CH.extra_b1_release_func = func self.RI.extra_b1_release_func = func self.TL.extra_b1_release_func = func elif binding == "": self.MT.extra_double_b1_func = func self.CH.extra_double_b1_func = func self.RI.extra_double_b1_func = func self.TL.extra_double_b1_func = func elif binding == "": self.MT.extra_motion_func = func self.CH.extra_motion_func = func self.RI.extra_motion_func = func self.TL.extra_motion_func = func elif binding == rc_binding: self.MT.extra_rc_func = func self.CH.extra_rc_func = func self.RI.extra_rc_func = func self.TL.extra_rc_func = func elif binding in emitted_events: if add: self.bound_events[binding].append(func) else: self.bound_events[binding] = [func] else: self.MT.bind(binding, func, add=add) self.CH.bind(binding, func, add=add) self.RI.bind(binding, func, add=add) self.TL.bind(binding, func, add=add) return self def unbind(self, binding: str) -> Sheet: if binding in emitted_events: self.bound_events[binding] = [] elif binding == "": self.MT.extra_b1_press_func = None self.CH.extra_b1_press_func = None self.RI.extra_b1_press_func = None self.TL.extra_b1_press_func = None elif binding == "": self.MT.extra_b1_motion_func = None self.CH.extra_b1_motion_func = None self.RI.extra_b1_motion_func = None self.TL.extra_b1_motion_func = None elif binding == "": self.MT.extra_b1_release_func = None self.CH.extra_b1_release_func = None self.RI.extra_b1_release_func = None self.TL.extra_b1_release_func = None elif binding == "": self.MT.extra_double_b1_func = None self.CH.extra_double_b1_func = None self.RI.extra_double_b1_func = None self.TL.extra_double_b1_func = None elif binding == "": self.MT.extra_motion_func = None self.CH.extra_motion_func = None self.RI.extra_motion_func = None self.TL.extra_motion_func = None elif binding == rc_binding: self.MT.extra_rc_func = None self.CH.extra_rc_func = None self.RI.extra_rc_func = None self.TL.extra_rc_func = None else: self.MT.unbind(binding) self.CH.unbind(binding) self.RI.unbind(binding) self.TL.unbind(binding) return self def edit_validation(self, func: Callable | None = None) -> Sheet: self.MT.edit_validation_func = func return self def popup_menu_add_command( self, label: str, func: Callable, table_menu: bool = True, index_menu: bool = True, header_menu: bool = True, empty_space_menu: bool = True, ) -> Sheet: if label not in self.MT.extra_table_rc_menu_funcs and table_menu: self.MT.extra_table_rc_menu_funcs[label] = func if label not in self.MT.extra_index_rc_menu_funcs and index_menu: self.MT.extra_index_rc_menu_funcs[label] = func if label not in self.MT.extra_header_rc_menu_funcs and header_menu: self.MT.extra_header_rc_menu_funcs[label] = func if label not in self.MT.extra_empty_space_rc_menu_funcs and empty_space_menu: self.MT.extra_empty_space_rc_menu_funcs[label] = func self.MT.create_rc_menus() return self def popup_menu_del_command(self, label: str | None = None) -> Sheet: if label is None: self.MT.extra_table_rc_menu_funcs = {} self.MT.extra_index_rc_menu_funcs = {} self.MT.extra_header_rc_menu_funcs = {} self.MT.extra_empty_space_rc_menu_funcs = {} else: if label in self.MT.extra_table_rc_menu_funcs: del self.MT.extra_table_rc_menu_funcs[label] if label in self.MT.extra_index_rc_menu_funcs: del self.MT.extra_index_rc_menu_funcs[label] if label in self.MT.extra_header_rc_menu_funcs: del self.MT.extra_header_rc_menu_funcs[label] if label in self.MT.extra_empty_space_rc_menu_funcs: del self.MT.extra_empty_space_rc_menu_funcs[label] self.MT.create_rc_menus() return self def basic_bindings(self, enable: bool = False) -> Sheet: for canvas in (self.MT, self.CH, self.RI, self.TL): canvas.basic_bindings(enable) return self def cut(self, event: object = None, validation: bool = True) -> None | EventDataDict: return self.MT.ctrl_x(event, validation) def copy(self, event: object = None) -> None | EventDataDict: return self.MT.ctrl_c(event) def paste(self, event: object = None, validation: bool = True) -> None | EventDataDict: return self.MT.ctrl_v(event, validation) def delete(self, event: object = None, validation: bool = True) -> None | EventDataDict: return self.MT.delete_key(event, validation) def undo(self, event: object = None) -> None | EventDataDict: return self.MT.undo(event) def redo(self, event: object = None) -> None | EventDataDict: return self.MT.redo(event) def has_focus( self, ) -> bool: """ Check if any Sheet widgets have focus Includes child widgets such as scroll bars Returns bool """ try: widget = self.focus_get() return widget == self or any(widget == c for c in self.children.values()) except Exception: return False def focus_set( self, canvas: Literal[ "table", "header", "row_index", "index", "topleft", "top_left", ] = "table", ) -> Sheet: if canvas == "table": self.MT.focus_set() elif canvas == "header": self.CH.focus_set() elif canvas in ("row_index", "index"): self.RI.focus_set() elif canvas in ("topleft", "top_left"): self.TL.focus_set() return self def zoom_in(self) -> Sheet: self.MT.zoom_in() return self def zoom_out(self) -> Sheet: self.MT.zoom_out() return self @property def event(self) -> EventDataDict: return self.last_event_data # Span objects def span( self, *key: CreateSpanTypes, type_: str = "", name: str = "", table: bool = True, index: bool = False, header: bool = False, tdisp: bool = False, idisp: bool = True, hdisp: bool = True, transposed: bool = False, ndim: int = 0, convert: object = None, undo: bool = False, emit_event: bool = False, widget: object = None, expand: None | str = None, formatter_options: dict | None = None, **kwargs, ) -> Span: """ Create a span / get an existing span by name Returns the created span """ if name and name in self.MT.named_spans: return self.MT.named_spans[name] elif not name: name = num2alpha(self.named_span_id) self.named_span_id += 1 while name in self.MT.named_spans: name = num2alpha(self.named_span_id) self.named_span_id += 1 span = self.span_from_key(*key) span.name = name if expand is not None: span.expand(expand) if isinstance(formatter_options, dict): span.type_ = "format" span.kwargs = {"formatter": None, **formatter_options} else: span.type_ = type_.lower() span.kwargs = kwargs span.table = table span.header = header span.index = index span.tdisp = tdisp span.idisp = idisp span.hdisp = hdisp span.transposed = transposed span.ndim = ndim span.convert = convert span.undo = undo span.emit_event = emit_event span.widget = self if widget is None else widget return span # Named Spans def named_span( self, span: Span, ) -> Span: if span.name in self.MT.named_spans: raise ValueError(f"Span '{span.name}' already exists.") if not span.name: raise ValueError("Span must have a name.") if span.type_ not in named_span_types: raise ValueError(f"Span 'type_' must be one of the following: {', '.join(named_span_types)}.") self.MT.named_spans[span.name] = span self.create_options_from_span(span) return span def create_options_from_span(self, span: Span) -> Sheet: getattr(self, span.type_)(span, **span.kwargs) return self def del_named_span(self, name: str) -> Sheet: if name not in self.MT.named_spans: raise ValueError(f"Span '{name}' does not exist.") span = self.MT.named_spans[name] type_ = span.type_ from_r, from_c, upto_r, upto_c = self.MT.named_span_coords(name) totalrows = self.MT.get_max_row_idx() + 1 totalcols = self.MT.get_max_column_idx() + 1 rng_from_r = 0 if from_r is None else from_r rng_from_c = 0 if from_c is None else from_c rng_upto_r = totalrows if upto_r is None else upto_r rng_upto_c = totalcols if upto_c is None else upto_c if span["header"]: del_named_span_options( self.CH.cell_options, range(rng_from_c, rng_upto_c), type_, ) if span["index"]: del_named_span_options( self.RI.cell_options, range(rng_from_r, rng_upto_r), type_, ) # col options if from_r is None: del_named_span_options( self.MT.col_options, range(rng_from_c, rng_upto_c), type_, ) # row options elif from_c is None: del_named_span_options( self.MT.row_options, range(rng_from_r, rng_upto_r), type_, ) # cell options elif isinstance(from_r, int) and isinstance(from_c, int): del_named_span_options_nested( self.MT.cell_options, range(rng_from_r, rng_upto_r), range(rng_from_c, rng_upto_c), type_, ) del self.MT.named_spans[name] return self def set_named_spans(self, named_spans: None | dict = None) -> Sheet: if named_spans is None: for name in self.MT.named_spans: self.del_named_span(name) named_spans = {} self.MT.named_spans = named_spans return self def get_named_span(self, name: str) -> dict: return self.MT.named_spans[name] def get_named_spans(self) -> dict: return self.MT.named_spans # Getting Sheet Data def __getitem__( self, *key: CreateSpanTypes, ) -> Span: return self.span_from_key(*key) def span_from_key( self, *key: CreateSpanTypes, ) -> None | Span: if not key: key = (None, None, None, None) span = key_to_span(key if len(key) != 1 else key[0], self.MT.named_spans, self) if isinstance(span, str): raise ValueError(span) return span def ranges_from_span(self, span: Span) -> tuple[Generator, Generator]: return span_ranges( span, totalrows=self.MT.total_data_rows, totalcols=self.MT.total_data_cols, ) def get_data( self, *key: CreateSpanTypes, ) -> object: """ e.g. retrieves entire table as pandas dataframe sheet["A1"].expand().options(pandas.DataFrame).data must deal with - format - transpose - ndim - index - header - tdisp - idisp - hdisp - convert format - if span.type_ == "format" and span.kwargs - does not format the sheets internal data, only data being returned - formats the data before retrieval with specified formatter function e.g. - format=int_formatter() -> returns kwargs, span["kwargs"] now has formatter kwargs - uses format_data(value=cell, **span["kwargs"]) on every table cell returned tranpose - make sublists become columns rather than rows - no effect on single cell ndim - defaults to None - when None it will follow rules: - if single cell in a list of lists return single cell - if single sublist in a list of lists then return single list - when ndim == 1 force single list - when ndim == 2 force list of lists table - gets table data, if false table data will not be included index - gets index data, is at the beginning of each row normally becomes its own column if transposed header - get header data, is its own row normally goes at the top of each column if transposed tdisp - if True gets displayed sheet values instead of actual data idisp - if True gets displayed index values instead of actual data hdisp - if True gets displayed header values instead of actual data convert - instead of returning data normally it returns convert(data) - sends the data to an optional convert function """ span = self.span_from_key(*key) rows, cols = self.ranges_from_span(span) tdisp, idisp, hdisp = span.tdisp, span.idisp, span.hdisp table, index, header = span.table, span.index, span.header fmt_kw = span.kwargs if span.type_ == "format" and span.kwargs else None quick_tdata, quick_idata, quick_hdata = self.MT.get_cell_data, self.RI.get_cell_data, self.CH.get_cell_data res = [] if span.transposed: if index: if index and header: if table: res.append([""] + [quick_idata(r, get_displayed=idisp) for r in rows]) else: res.append([quick_idata(r, get_displayed=idisp) for r in rows]) else: res.append([quick_idata(r, get_displayed=idisp) for r in rows]) if header: if table: res.extend( [quick_hdata(c, get_displayed=hdisp)] + [quick_tdata(r, c, get_displayed=tdisp, fmt_kw=fmt_kw) for r in rows] for c in cols ) else: res.extend([quick_hdata(c, get_displayed=hdisp)] for c in cols) elif table: res.extend([quick_tdata(r, c, get_displayed=tdisp, fmt_kw=fmt_kw) for r in rows] for c in cols) elif not span.transposed: if header: if header and index: if table: res.append([""] + [quick_hdata(c, get_displayed=hdisp) for c in cols]) else: res.append([quick_hdata(c, get_displayed=hdisp) for c in cols]) else: res.append([quick_hdata(c, get_displayed=hdisp) for c in cols]) if index: if table: res.extend( [quick_idata(r, get_displayed=idisp)] + [quick_tdata(r, c, get_displayed=tdisp, fmt_kw=fmt_kw) for c in cols] for r in rows ) else: res.extend([quick_idata(r, get_displayed=idisp)] for r in rows) elif table: res.extend([quick_tdata(r, c, get_displayed=tdisp, fmt_kw=fmt_kw) for c in cols] for r in rows) if not span.ndim: # it's a cell if len(res) == 1 and len(res[0]) == 1: res = res[0][0] # it's a single list elif len(res) == 1: res = res[0] # retrieving a list of index cells or elif (index and not span.transposed and not table and not header) or ( # it's a column that's spread across sublists table and res and not span.transposed and len(res[0]) == 1 and len(res[-1]) == 1 ): res = list(chain.from_iterable(res)) elif span.ndim == 1: # flatten sublists if len(res) == 1 and len(res[0]) == 1: res = res[0] else: res = list(chain.from_iterable(res)) # if span.ndim == 2 res keeps its current # dimensions as a list of lists if span.convert is not None: return span.convert(res) return res def get_total_rows(self, include_index: bool = False) -> int: return self.MT.total_data_rows(include_index=include_index) def get_total_columns(self, include_header: bool = False) -> int: return self.MT.total_data_cols(include_header=include_header) def get_value_for_empty_cell( self, r: int, c: int, r_ops: bool = True, c_ops: bool = True, ) -> object: return self.MT.get_value_for_empty_cell(r, c, r_ops, c_ops) @property def data(self): return self.MT.data def __iter__(self) -> Iterator[list[object] | tuple[object]]: return self.MT.data.__iter__() def __reversed__(self) -> Iterator[list[object] | tuple[object]]: return reversed(self.MT.data) def __contains__(self, key: object) -> bool: if isinstance(key, (list, tuple)): return key in self.MT.data return any(key in row for row in self.MT.data) # Setting Sheet Data def reset( self, table: bool = True, header: bool = True, index: bool = True, row_heights: bool = True, column_widths: bool = True, cell_options: bool = True, tags: bool = True, undo_stack: bool = True, selections: bool = True, sheet_options: bool = False, displayed_rows: bool = True, displayed_columns: bool = True, tree: bool = True, redraw: bool = True, ) -> Sheet: if table: self.MT.hide_text_editor_and_dropdown(redraw=False) self.MT.data = [] if header: self.CH.hide_text_editor_and_dropdown(redraw=False) self.MT._headers = [] if index: self.RI.hide_text_editor_and_dropdown(redraw=False) self.MT._row_index = [] if displayed_columns: self.MT.displayed_columns = [] self.MT.all_columns_displayed = True if displayed_rows: self.MT.displayed_rows = [] self.MT.all_rows_displayed = True if row_heights: self.MT.saved_row_heights = {} self.MT.set_row_positions([]) if column_widths: self.MT.saved_column_widths = {} self.MT.set_col_positions([]) if cell_options: self.reset_all_options() if tags: self.MT.reset_tags() if undo_stack: self.reset_undos() if selections: self.MT.deselect(redraw=False) if sheet_options: self.ops = new_sheet_options() self.change_theme(redraw=False) if tree: self.RI.tree_reset() if tree or row_heights or column_widths: self.MT.hide_dropdown_editor_all_canvases() return self.set_refresh_timer(redraw) def set_sheet_data( self, data: list | tuple | None = None, reset_col_positions: bool = True, reset_row_positions: bool = True, redraw: bool = True, verify: bool = False, reset_highlights: bool = False, keep_formatting: bool = True, delete_options: bool = False, ) -> object: if data is None: data = [] if verify and (not isinstance(data, list) or not all(isinstance(row, list) for row in data)): raise ValueError("Data argument must be a list of lists, sublists being rows") if delete_options: self.reset_all_options() elif reset_highlights: self.dehighlight_all() return self.MT.data_reference( data, reset_col_positions, reset_row_positions, redraw, return_id=False, keep_formatting=keep_formatting, ) @data.setter def data(self, value: list[list[object]]) -> None: self.data_reference(value) def new_tksheet_event(self) -> EventDataDict: return event_dict( name="", sheet=self.name, widget=self, selected=self.MT.selected, ) def set_data( self, *key: CreateSpanTypes, data: object = None, undo: bool | None = None, emit_event: bool | None = None, redraw: bool = True, ) -> EventDataDict: """ e.g. df = pandas.DataFrame([[1, 2, 3], [4, 5, 6]]) sheet["A1"] = df.values.tolist() just uses the spans from_r, from_c values 'data' parameter could be: - list of lists - list of values - not a list or tuple - can have all three of the below set at the same time or just one or two: - if span.table then sets table cell - if span.index then sets index cell - if span.header than sets header cell expands sheet if required but can only undo any added displayed row/column positions, not expanded MT.data list format - if span.type_ == "format" and span.kwargs - formats the data before setting with specified formatter function e.g. - format=int_formatter() -> returns kwargs, span["kwargs"] now has formatter kwargs - uses format_data(value=cell, **span["kwargs"]) on every set cell transposed - switches range orientation - a single list will go to row without transposed - multi lists will go to rows without transposed - with transposed a single list will go to column - with transposed multi lists will go to columns header - assumes there's a header, sets header cell data as well index - assumes there's an index, sets index cell data as well undo - if True adds to undo stack and if undo is enabled for end user they can undo/redo the change in the table comments below - - t stands for table - i stands for index - h stands for header """ span = self.span_from_key(*key) if data is None: data = [] startr, startc = span_froms(span) table, index, header = span.table, span.index, span.header fmt_kw = span.kwargs if span.type_ == "format" and span.kwargs else None transposed = span.transposed maxr, maxc = startr, startc event_data = event_dict( name="edit_table", sheet=self.name, widget=self, selected=self.MT.selected, ) set_t = self.event_data_set_table_cell set_i, set_h = self.event_data_set_index_cell, self.event_data_set_header_cell istart = 1 if index else 0 hstart = 1 if header else 0 # data is list if isinstance(data, (list, tuple)): if not data: return # data is a list of lists if isinstance(data[0], (list, tuple)): if transposed: if table: """ - sublists are columns 1 2 3 A [[-, i, i], B [h, t, t], C [h, t, t],] """ if index: for r, v in enumerate(islice(data[0], hstart, None)): maxr = r event_data = set_i(r, v, event_data) if header: for c, sl in enumerate(islice(data, istart, None), start=startc): maxc = c for v in islice(sl, 0, 1): event_data = set_h(c, v, event_data) for c, sl in enumerate(islice(data, istart, None), start=startc): maxc = c for r, v in enumerate(islice(sl, hstart, None), start=startr): event_data = set_t(r, c, v, event_data, fmt_kw) maxr = r elif not table: if index and header: """ - first sublist is index, rest are headers [['1', '2', '3'], ['A'], ['B'], ['C']] """ for r, v in enumerate(data[0], start=startr): maxr = r event_data = set_i(r, v, event_data) for c, sl in enumerate(islice(data, 1, None), start=startc): maxc = c if sl: event_data = set_h(c, sl[0], event_data) elif index: """ ['1', '2', '3'] """ for r, v in enumerate(data, start=startr): maxr = r event_data = set_i(r, v, event_data) elif header: """ [['A'], ['B'], ['C']] """ for c, sl in enumerate(data, start=startc): maxc = c if sl: event_data = set_h(c, sl[0], event_data) elif not transposed: if table: """ - sublists are rows A B C 1 [[-, h, h], 2 [i, t, t], 3 [i, t, t],] """ if index: for r, sl in enumerate(islice(data, hstart, None), start=startr): maxr = r for v in islice(sl, 0, 1): event_data = set_i(r, v, event_data) if header: for c, v in enumerate(islice(data[0], istart, None)): maxc = c event_data = set_h(c, v, event_data) for r, sl in enumerate(islice(data, hstart, None), start=startr): maxr = r for c, v in enumerate(islice(sl, istart, None), start=startc): maxc = c event_data = set_t(r, c, v, event_data, fmt_kw) elif not table: if index and header: """ - first sublist is headers, rest are index values [['A', 'B', 'C'], ['1'], ['2'], ['3']] """ for c, v in enumerate(data[0], start=startc): maxc = c event_data = set_h(c, v, event_data) for r, sl in enumerate(islice(data, 1, None), start=startr): maxr = r if sl: event_data = set_i(r, sl[0], event_data) elif index: """ [['1'], ['2'], ['3']] """ for r, sl in enumerate(data, start=startr): maxr = r if sl: event_data = set_i(r, sl[0], event_data) elif header: """ ['A', 'B', 'C'] """ for c, v in enumerate(data, start=startc): maxc = c event_data = set_h(c, v, event_data) # data is list of values else: # setting a list of index values, ignore transposed if index and not table: """ 1 2 3 [i, i, i] """ for r, v in enumerate(data, start=startr): maxr = r event_data = set_i(r, v, event_data) # setting a list of header values, ignore transposed elif header and not table: """ A B C [h, h, h] """ for c, v in enumerate(data, start=startc): maxc = c event_data = set_h(c, v, event_data) # includes table values, transposed taken into account elif table: if transposed: """ - single list is column, span.index ignored 1 2 3 A [h, t, t] """ if header: event_data = set_h(startc, data[0], event_data) for r, v in enumerate(islice(data, hstart, None), start=startr): maxr = r event_data = set_t(r, startc, v, event_data, fmt_kw) elif not transposed: """ - single list is row, span.header ignored A B C 1 [i, t, t] """ if index: event_data = set_i(startr, data[0], event_data) for c, v in enumerate(islice(data, istart, None), start=startc): maxc = c event_data = set_t(startr, c, v, event_data, fmt_kw) # data is a value else: """ A 1 t/i/h """ if table: event_data = set_t(startr, startc, data, event_data, fmt_kw) if index: event_data = set_i(startr, data, event_data) if header: event_data = set_h(startc, data, event_data) # add row/column lines (positions) if required if self.MT.all_columns_displayed and maxc >= (ncols := len(self.MT.col_positions) - 1): event_data = self.MT.add_columns( *self.MT.get_args_for_add_columns( data_ins_col=len(self.MT.col_positions) - 1, displayed_ins_col=len(self.MT.col_positions) - 1, numcols=maxc + 1 - ncols, columns=[], widths=None, headers=False, ), event_data=event_data, create_selections=False, ) if self.MT.all_rows_displayed and maxr >= (nrows := len(self.MT.row_positions) - 1): event_data = self.MT.add_rows( *self.MT.get_args_for_add_rows( data_ins_row=len(self.MT.row_positions) - 1, displayed_ins_row=len(self.MT.row_positions) - 1, numrows=maxr + 1 - nrows, rows=[], heights=None, row_index=False, ), event_data=event_data, create_selections=False, ) if ( event_data["cells"]["table"] or event_data["cells"]["index"] or event_data["cells"]["header"] or event_data["added"]["columns"] or event_data["added"]["rows"] ): if undo is True or (undo is None and span.undo): self.MT.undo_stack.append(pickled_event_dict(event_data)) if emit_event is True or (emit_event is None and span.emit_event): self.emit_event("<>", event_data) self.set_refresh_timer(redraw) return event_data def clear( self, *key: CreateSpanTypes, undo: bool | None = None, emit_event: bool | None = None, redraw: bool = True, ) -> EventDataDict: span = self.span_from_key(*key) rows, cols = self.ranges_from_span(span) clear_t = self.event_data_set_table_cell clear_i = self.event_data_set_index_cell clear_h = self.event_data_set_header_cell quick_tval = self.MT.get_value_for_empty_cell quick_ival = self.RI.get_value_for_empty_cell quick_hval = self.CH.get_value_for_empty_cell table, index, header = span.table, span.index, span.header event_data = event_dict( name="edit_table", sheet=self.name, widget=self, selected=self.MT.selected, ) if index: for r in rows: event_data = clear_i(r, quick_ival(r), event_data) if header: for c in cols: event_data = clear_h(c, quick_hval(c), event_data) if table: for r in rows: for c in cols: event_data = clear_t(r, c, quick_tval(r, c), event_data) if event_data["cells"]["table"] or event_data["cells"]["header"] or event_data["cells"]["index"]: if undo is True or (undo is None and span.undo): self.MT.undo_stack.append(pickled_event_dict(event_data)) if emit_event is True or (emit_event is None and span.emit_event): self.emit_event("<>", event_data) self.set_refresh_timer(redraw) return event_data def event_data_set_table_cell( self, datarn: int, datacn: int, value: object, event_data: dict, fmt_kw: dict | None = None, check_readonly: bool = False, ) -> EventDataDict: if fmt_kw is not None or self.MT.input_valid_for_cell(datarn, datacn, value, check_readonly=check_readonly): event_data["cells"]["table"][(datarn, datacn)] = self.MT.get_cell_data(datarn, datacn) self.MT.set_cell_data(datarn, datacn, value, kwargs=fmt_kw) return event_data def event_data_set_index_cell( self, datarn: int, value: object, event_data: dict, check_readonly: bool = False, ) -> EventDataDict: if self.RI.input_valid_for_cell(datarn, value, check_readonly=check_readonly): event_data["cells"]["index"][datarn] = self.RI.get_cell_data(datarn) self.RI.set_cell_data(datarn, value) return event_data def event_data_set_header_cell( self, datacn: int, value: object, event_data: dict, check_readonly: bool = False, ) -> EventDataDict: if self.CH.input_valid_for_cell(datacn, value, check_readonly=check_readonly): event_data["cells"]["header"][datacn] = self.CH.get_cell_data(datacn) self.CH.set_cell_data(datacn, value) return event_data def insert_row( self, row: list[object] | tuple[object] | None = None, idx: str | int | None = None, height: int | None = None, row_index: bool = False, fill: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> EventDataDict: return self.insert_rows( rows=1 if row is None else [row] if isinstance(row, (list, tuple)) else row, idx=idx, heights=[height] if isinstance(height, int) else height, row_index=row_index, fill=fill, undo=undo, emit_event=emit_event, redraw=redraw, ) def insert_column( self, column: list[object] | tuple[object] | None = None, idx: str | int | None = None, width: int | None = None, header: bool = False, fill: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> EventDataDict: return self.insert_columns( columns=1 if column is None else [column] if isinstance(column, (list, tuple)) else column, idx=idx, widths=[width] if isinstance(width, int) else width, headers=header, fill=fill, undo=undo, emit_event=emit_event, redraw=redraw, ) def insert_rows( self, rows: list[tuple[object] | list[object]] | tuple[tuple[object] | list[object]] | int = 1, idx: str | int | None = None, heights: list[int] | tuple[int] | None = None, row_index: bool = False, fill: bool = True, undo: bool = False, emit_event: bool = False, create_selections: bool = True, add_column_widths: bool = True, push_ops: bool = True, redraw: bool = True, ) -> EventDataDict: total_cols = None if (idx := idx_param_to_int(idx)) is None: idx = len(self.MT.data) if isinstance(rows, int): if rows < 1: raise ValueError(f"rows arg must be greater than 0, not {rows}") total_cols = self.MT.total_data_cols() if row_index: data = [ [self.RI.get_value_for_empty_cell(idx + i, r_ops=False)] + self.MT.get_empty_row_seq( idx + i, total_cols, r_ops=False, c_ops=False, ) for i in range(rows) ] else: data = [ self.MT.get_empty_row_seq( idx + i, total_cols, r_ops=False, c_ops=False, ) for i in range(rows) ] else: data = rows if not isinstance(rows, int) and fill: total_cols = self.MT.total_data_cols() if total_cols is None else total_cols len_check = (total_cols + 1) if row_index else total_cols for rn, r in enumerate(data): if len_check > (lnr := len(r)): r += self.MT.get_empty_row_seq( rn, end=total_cols, start=(lnr - 1) if row_index else lnr, r_ops=False, c_ops=False, ) numrows = len(data) if self.MT.all_rows_displayed: displayed_ins_idx = idx else: displayed_ins_idx = bisect_left(self.MT.displayed_rows, idx) event_data = self.MT.add_rows( *self.MT.get_args_for_add_rows( data_ins_row=idx, displayed_ins_row=displayed_ins_idx, numrows=numrows, rows=data, heights=heights, row_index=row_index, ), add_col_positions=add_column_widths, event_data=event_dict( name="add_rows", sheet=self.name, boxes=self.MT.get_boxes(), selected=self.MT.selected, ), create_selections=create_selections, push_ops=push_ops, ) if undo: self.MT.undo_stack.append(pickled_event_dict(event_data)) if emit_event: self.emit_event("<>", event_data) self.set_refresh_timer(redraw) return event_data def insert_columns( self, columns: list[tuple[object] | list[object]] | tuple[tuple[object] | list[object]] | int = 1, idx: str | int | None = None, widths: list[int] | tuple[int] | None = None, headers: bool = False, fill: bool = True, undo: bool = False, emit_event: bool = False, create_selections: bool = True, add_row_heights: bool = True, push_ops: bool = True, redraw: bool = True, ) -> EventDataDict: old_total = self.MT.equalize_data_row_lengths() total_rows = self.MT.total_data_rows() if (idx := idx_param_to_int(idx)) is None: idx = old_total if isinstance(columns, int): if columns < 1: raise ValueError(f"columns arg must be greater than 0, not {columns}") if headers: data = [ [self.CH.get_value_for_empty_cell(datacn, c_ops=False)] + [ self.MT.get_value_for_empty_cell( datarn, datacn, r_ops=False, c_ops=False, ) for datarn in range(total_rows) ] for datacn in range(idx, idx + columns) ] else: data = [ [ self.MT.get_value_for_empty_cell( datarn, datacn, r_ops=False, c_ops=False, ) for datarn in range(total_rows) ] for datacn in range(idx, idx + columns) ] numcols = columns else: data = columns numcols = len(columns) if fill: len_check = (total_rows + 1) if headers else total_rows for i, column in enumerate(data): if (col_len := len(column)) < len_check: column += [ self.MT.get_value_for_empty_cell( r, idx + i, r_ops=False, c_ops=False, ) for r in range((col_len - 1) if headers else col_len, total_rows) ] if self.MT.all_columns_displayed: displayed_ins_idx = idx elif not self.MT.all_columns_displayed: displayed_ins_idx = bisect_left(self.MT.displayed_columns, idx) event_data = self.MT.add_columns( *self.MT.get_args_for_add_columns( data_ins_col=idx, displayed_ins_col=displayed_ins_idx, numcols=numcols, columns=data, widths=widths, headers=headers, ), add_row_positions=add_row_heights, event_data=event_dict( name="add_columns", sheet=self.name, boxes=self.MT.get_boxes(), selected=self.MT.selected, ), create_selections=create_selections, push_ops=push_ops, ) if undo: self.MT.undo_stack.append(pickled_event_dict(event_data)) if emit_event: self.emit_event("<>", event_data) self.set_refresh_timer(redraw) return event_data def del_row( self, idx: int = 0, data_indexes: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> EventDataDict: return self.del_rows( rows=idx, data_indexes=data_indexes, undo=undo, emit_event=emit_event, redraw=redraw, ) delete_row = del_row def del_column( self, idx: int = 0, data_indexes: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> EventDataDict: return self.del_columns( columns=idx, data_indexes=data_indexes, undo=undo, emit_event=emit_event, redraw=redraw, ) delete_column = del_column def del_rows( self, rows: int | Iterator[int], data_indexes: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> EventDataDict: self.MT.deselect("all", redraw=False) rows = [rows] if isinstance(rows, int) else sorted(rows) event_data = event_dict( name="delete_rows", sheet=self.name, widget=self, boxes=self.MT.get_boxes(), selected=self.MT.selected, ) if not data_indexes: event_data = self.MT.delete_rows_displayed(rows, event_data) event_data = self.MT.delete_rows_data( rows if self.MT.all_rows_displayed else [self.MT.displayed_rows[r] for r in rows], event_data, ) else: if self.MT.all_rows_displayed: rows = rows else: rows = data_to_displayed_idxs(rows, self.MT.displayed_rows) event_data = self.MT.delete_rows_data(rows, event_data) event_data = self.MT.delete_rows_displayed( rows, event_data, ) if undo: self.MT.undo_stack.append(pickled_event_dict(event_data)) if emit_event: self.emit_event("<>", event_data) self.set_refresh_timer(redraw) return event_data delete_rows = del_rows def del_columns( self, columns: int | Iterator[int], data_indexes: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> EventDataDict: self.MT.deselect("all", redraw=False) columns = [columns] if isinstance(columns, int) else sorted(columns) event_data = event_dict( name="delete_columns", sheet=self.name, widget=self, boxes=self.MT.get_boxes(), selected=self.MT.selected, ) if not data_indexes: event_data = self.MT.delete_columns_displayed(columns, event_data) event_data = self.MT.delete_columns_data( columns if self.MT.all_columns_displayed else [self.MT.displayed_columns[c] for c in columns], event_data, ) else: if self.MT.all_columns_displayed: columns = columns else: columns = data_to_displayed_idxs(columns, self.MT.displayed_columns) event_data = self.MT.delete_columns_data(columns, event_data) event_data = self.MT.delete_columns_displayed( columns, event_data, ) if undo: self.MT.undo_stack.append(pickled_event_dict(event_data)) if emit_event: self.emit_event("<>", event_data) self.set_refresh_timer(redraw) return event_data delete_columns = del_columns def sheet_data_dimensions( self, total_rows: int | None = None, total_columns: int | None = None, ) -> Sheet: self.MT.data_dimensions(total_rows, total_columns) return self def set_sheet_data_and_display_dimensions( self, total_rows: int | None = None, total_columns: int | None = None, ) -> Sheet: self.sheet_display_dimensions(total_rows=total_rows, total_columns=total_columns) self.MT.data_dimensions(total_rows=total_rows, total_columns=total_columns) return self def total_rows( self, number: int | None = None, mod_positions: bool = True, mod_data: bool = True, ) -> int | Sheet: total_rows = self.MT.total_data_rows() if number is None: return total_rows if not isinstance(number, int) or number < 0: raise ValueError("number argument must be integer and > 0") if number > total_rows and mod_positions: self.MT.insert_row_positions(heights=number - total_rows) elif number < total_rows: if not self.MT.all_rows_displayed: self.MT.display_rows(enable=False, reset_row_positions=False, deselect_all=True) self.MT.row_positions[number + 1 :] = [] if mod_data: self.MT.data_dimensions(total_rows=number) return self def total_columns( self, number: int | None = None, mod_positions: bool = True, mod_data: bool = True, ) -> int | Sheet: total_cols = self.MT.total_data_cols() if number is None: return total_cols if not isinstance(number, int) or number < 0: raise ValueError("number argument must be integer and > 0") if number > total_cols and mod_positions: self.MT.insert_col_positions(widths=number - total_cols) elif number < total_cols: if not self.MT.all_columns_displayed: self.MT.display_columns(enable=False, reset_col_positions=False, deselect_all=True) self.MT.col_positions[number + 1 :] = [] if mod_data: self.MT.data_dimensions(total_columns=number) return self def move_row(self, row: int, moveto: int) -> tuple[dict, dict, dict]: return self.move_rows(moveto, row) def move_column(self, column: int, moveto: int) -> tuple[dict, dict, dict]: return self.move_columns(moveto, column) def move_rows( self, move_to: int | None = None, to_move: list[int] | None = None, move_data: bool = True, data_indexes: bool = False, create_selections: bool = True, undo: bool = False, emit_event: bool = False, move_heights: bool = True, redraw: bool = True, ) -> tuple[dict, dict, dict]: data_idxs, disp_idxs, event_data = self.MT.move_rows_adjust_options_dict( *self.MT.get_args_for_move_rows( move_to=move_to, to_move=to_move, data_indexes=data_indexes, ), move_data=move_data, move_heights=move_heights, create_selections=create_selections, data_indexes=data_indexes, ) if undo: self.MT.undo_stack.append(pickled_event_dict(event_data)) if emit_event: self.emit_event("<>", event_data) self.set_refresh_timer(redraw) return data_idxs, disp_idxs, event_data def move_columns( self, move_to: int | None = None, to_move: list[int] | None = None, move_data: bool = True, data_indexes: bool = False, create_selections: bool = True, undo: bool = False, emit_event: bool = False, move_widths: bool = True, redraw: bool = True, ) -> tuple[dict, dict, dict]: data_idxs, disp_idxs, event_data = self.MT.move_columns_adjust_options_dict( *self.MT.get_args_for_move_columns( move_to=move_to, to_move=to_move, data_indexes=data_indexes, ), move_data=move_data, move_widths=move_widths, create_selections=create_selections, data_indexes=data_indexes, ) if undo: self.MT.undo_stack.append(pickled_event_dict(event_data)) if emit_event: self.emit_event("<>", event_data) self.set_refresh_timer(redraw) return data_idxs, disp_idxs, event_data def mapping_move_columns( self, data_new_idxs: dict[int, int], disp_new_idxs: None | dict[int, int] = None, move_data: bool = True, data_indexes: bool = False, create_selections: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> tuple[dict[int, int], dict[int, int], EventDataDict]: data_idxs, disp_idxs, event_data = self.MT.move_columns_adjust_options_dict( data_new_idxs=data_new_idxs, data_old_idxs=dict(zip(data_new_idxs.values(), data_new_idxs)), totalcols=None, disp_new_idxs=disp_new_idxs, move_data=move_data, create_selections=create_selections, data_indexes=data_indexes, ) if undo: self.MT.undo_stack.append(pickled_event_dict(event_data)) if emit_event: self.emit_event("<>", event_data) self.set_refresh_timer(redraw) return data_idxs, disp_idxs, event_data def mapping_move_rows( self, data_new_idxs: dict[int, int], disp_new_idxs: None | dict[int, int] = None, move_data: bool = True, data_indexes: bool = False, create_selections: bool = True, undo: bool = False, emit_event: bool = False, redraw: bool = True, ) -> tuple[dict[int, int], dict[int, int], EventDataDict]: data_idxs, disp_idxs, event_data = self.MT.move_rows_adjust_options_dict( data_new_idxs=data_new_idxs, data_old_idxs=dict(zip(data_new_idxs.values(), data_new_idxs)), totalrows=None, disp_new_idxs=disp_new_idxs, move_data=move_data, create_selections=create_selections, data_indexes=data_indexes, ) if undo: self.MT.undo_stack.append(pickled_event_dict(event_data)) if emit_event: self.emit_event("<>", event_data) self.set_refresh_timer(redraw) return data_idxs, disp_idxs, event_data def equalize_data_row_lengths( self, include_header: bool = True, ) -> int: return self.MT.equalize_data_row_lengths( include_header=include_header, ) def full_move_rows_idxs(self, data_idxs: dict[int, int], max_idx: int | None = None) -> dict[int, int]: """ Converts the dict provided by moving rows event data Under the keys ['moved']['rows']['data'] Into a dict of {old index: new index} for every row Includes row numbers in cell options, spans, etc. """ return self.MT.get_full_new_idxs( self.MT.get_max_row_idx() if max_idx is None else max_idx, data_idxs, ) def full_move_columns_idxs(self, data_idxs: dict[int, int], max_idx: int | None = None) -> dict[int, int]: """ Converts the dict provided by moving columns event data Under the keys ['moved']['columns']['data'] Into a dict of {old index: new index} for every column Includes column numbers in cell options, spans, etc. """ return self.MT.get_full_new_idxs( self.MT.get_max_column_idx() if max_idx is None else max_idx, data_idxs, ) # Highlighting Cells def highlight( self, *key: CreateSpanTypes, bg: bool | None | str = False, fg: bool | None | str = False, end: bool | None = None, overwrite: bool = False, redraw: bool = True, ) -> Span: span = self.span_from_key(*key) if bg is False and fg is False and end is None: return span rows, cols = self.ranges_from_span(span) table, index, header = span.table, span.index, span.header if span.kind == "cell": if header: for c in cols: add_highlight(self.CH.cell_options, c, bg, fg, end, overwrite) for r in rows: if index: add_highlight(self.RI.cell_options, r, bg, fg, end, overwrite) if table: for c in cols: add_highlight(self.MT.cell_options, (r, c), bg, fg, end, overwrite) elif span.kind == "row": for r in rows: if index: add_highlight(self.RI.cell_options, r, bg, fg, end, overwrite) if table: add_highlight(self.MT.row_options, r, bg, fg, end, overwrite) elif span.kind == "column": for c in cols: if header: add_highlight(self.CH.cell_options, c, bg, fg, end, overwrite) if table: add_highlight(self.MT.col_options, c, bg, fg, end, overwrite) self.set_refresh_timer(redraw) return span def dehighlight( self, *key: CreateSpanTypes, redraw: bool = True, ) -> Span: span = self.span_from_key(*key) self.del_options_using_span(span, "highlight") self.set_refresh_timer(redraw) return span def dehighlight_all( self, cells: bool = True, rows: bool = True, columns: bool = True, header: bool = True, index: bool = True, redraw: bool = True, ) -> Sheet: if cells: del_from_options(self.MT.cell_options, "highlight") if rows: del_from_options(self.MT.row_options, "highlight") if columns: del_from_options(self.MT.col_options, "highlight") if index: del_from_options(self.RI.cell_options, "highlight") if header: del_from_options(self.CH.cell_options, "highlight") return self.set_refresh_timer(redraw) # Dropdown Boxes def dropdown( self, *key: CreateSpanTypes, values: list = [], edit_data: bool = True, set_values: dict[tuple[int, int], object] = {}, set_value: object = None, state: str = "normal", redraw: bool = True, selection_function: Callable | None = None, modified_function: Callable | None = None, search_function: Callable | None = None, validate_input: bool = True, text: None | str = None, ) -> Span: if not search_function: search_function = dropdown_search_function v = set_value if set_value is not None else values[0] if values else "" kwargs = { "values": values, "state": state, "selection_function": selection_function, "modified_function": modified_function, "search_function": search_function, "validate_input": validate_input, "text": text, } d = get_dropdown_dict(**kwargs) span = self.span_from_key(*key) rows, cols = self.ranges_from_span(span) table, index, header = span.table, span.index, span.header set_tdata = self.MT.set_cell_data set_idata = self.RI.set_cell_data set_hdata = self.CH.set_cell_data if index: for r in rows: self.del_index_cell_options_dropdown_and_checkbox(r) add_to_options(self.RI.cell_options, r, "dropdown", d) if edit_data: set_idata(r, value=set_values[r] if r in set_values else v) if header: for c in cols: self.del_header_cell_options_dropdown_and_checkbox(c) add_to_options(self.CH.cell_options, c, "dropdown", d) if edit_data: set_hdata(c, value=set_values[c] if c in set_values else v) if table: if span.kind == "cell": for r in rows: for c in cols: self.del_cell_options_dropdown_and_checkbox(r, c) add_to_options(self.MT.cell_options, (r, c), "dropdown", d) if edit_data: set_tdata(r, c, value=set_values[(r, c)] if (r, c) in set_values else v) elif span.kind == "row": for r in rows: self.del_row_options_dropdown_and_checkbox(r) add_to_options(self.MT.row_options, r, "dropdown", d) if edit_data: for c in cols: set_tdata(r, c, value=set_values[(r, c)] if (r, c) in set_values else v) elif span.kind == "column": for c in cols: self.del_column_options_dropdown_and_checkbox(c) add_to_options(self.MT.col_options, c, "dropdown", d) if edit_data: for r in rows: set_tdata(r, c, value=set_values[(r, c)] if (r, c) in set_values else v) self.set_refresh_timer(redraw) return span def del_dropdown( self, *key: CreateSpanTypes, redraw: bool = True, ) -> Span: span = self.span_from_key(*key) if span.table: self.MT.hide_dropdown_window() if span.index: self.RI.hide_dropdown_window() if span.header: self.CH.hide_dropdown_window() self.del_options_using_span(span, "dropdown") self.set_refresh_timer(redraw) return span def open_dropdown(self, r: int, c: int) -> Sheet: self.MT.open_dropdown_window(r, c) return self def close_dropdown(self, r: int | None = None, c: int | None = None) -> Sheet: self.MT.close_dropdown_window(r, c) return self def open_header_dropdown(self, c: int) -> Sheet: self.CH.open_dropdown_window(c) return self def close_header_dropdown(self, c: int | None = None) -> Sheet: self.CH.close_dropdown_window(c) return self def open_index_dropdown(self, r: int) -> Sheet: self.RI.open_dropdown_window(r) return self def close_index_dropdown(self, r: int | None = None) -> Sheet: self.RI.close_dropdown_window(r) return self # Check Boxes def checkbox( self, *key: CreateSpanTypes, edit_data: bool = True, checked: bool | None = None, state: str = "normal", redraw: bool = True, check_function: Callable | None = None, text: str = "", ) -> Span: kwargs = { "state": state, "check_function": check_function, "text": text, } d = get_checkbox_dict(**kwargs) span = self.span_from_key(*key) rows, cols = self.ranges_from_span(span) table, index, header = span.table, span.index, span.header set_tdata = self.MT.set_cell_data set_idata = self.RI.set_cell_data set_hdata = self.CH.set_cell_data if index: for r in rows: self.del_index_cell_options_dropdown_and_checkbox(r) add_to_options(self.RI.cell_options, r, "checkbox", d) if edit_data: set_idata(r, checked if isinstance(checked, bool) else force_bool(self.get_index_data(r))) if header: for c in cols: self.del_header_cell_options_dropdown_and_checkbox(c) add_to_options(self.CH.cell_options, c, "checkbox", d) if edit_data: set_hdata(c, checked if isinstance(checked, bool) else force_bool(self.get_header_data(c))) if table: if span.kind == "cell": for r in rows: for c in cols: self.MT.delete_cell_format(r, c, clear_values=False) self.del_cell_options_dropdown_and_checkbox(r, c) add_to_options(self.MT.cell_options, (r, c), "checkbox", d) if edit_data: set_tdata( r, c, checked if isinstance(checked, bool) else force_bool(self.get_cell_data(r, c)) ) elif span.kind == "row": for r in rows: self.MT.delete_row_format(r, clear_values=False) self.del_row_options_dropdown_and_checkbox(r) add_to_options(self.MT.row_options, r, "checkbox", d) if edit_data: for c in cols: set_tdata( r, c, checked if isinstance(checked, bool) else force_bool(self.get_cell_data(r, c)) ) elif span.kind == "column": for c in cols: self.MT.delete_column_format(c, clear_values=False) self.del_column_options_dropdown_and_checkbox(c) add_to_options(self.MT.col_options, c, "checkbox", d) if edit_data: for r in rows: set_tdata( r, c, checked if isinstance(checked, bool) else force_bool(self.get_cell_data(r, c)) ) self.set_refresh_timer(redraw) return span def del_checkbox( self, *key: CreateSpanTypes, redraw: bool = True, ) -> Span: span = self.span_from_key(*key) self.del_options_using_span(span, "checkbox") self.set_refresh_timer(redraw) return span def click_checkbox( self, *key: CreateSpanTypes, checked: bool | None = None, redraw: bool = True, ) -> Span: span = self.span_from_key(*key) rows, cols = self.ranges_from_span(span) table, index, header = span.table, span.index, span.header set_tdata = self.MT.set_cell_data set_idata = self.RI.set_cell_data set_hdata = self.CH.set_cell_data get_tdata = self.MT.get_cell_data get_idata = self.RI.get_cell_data get_hdata = self.CH.get_cell_data get_tkw = self.MT.get_cell_kwargs get_ikw = self.RI.get_cell_kwargs get_hkw = self.CH.get_cell_kwargs if span.kind == "cell": if header: for c in cols: if get_hkw(c, key="checkbox"): set_hdata(c, not get_hdata(c) if checked is None else bool(checked)) for r in rows: if index and get_ikw(r, key="checkbox"): set_idata(r, not get_idata(r) if checked is None else bool(checked)) if table: for c in cols: if get_tkw(r, c, key="checkbox"): set_tdata(r, c, not get_tdata(r, c) if checked is None else bool(checked)) elif span.kind == "row": totalcols = range(self.MT.total_data_cols()) for r in rows: if index and get_ikw(r, key="checkbox"): set_idata(r, not get_idata(r) if checked is None else bool(checked)) if table: for c in totalcols: if get_tkw(r, c, key="checkbox"): set_tdata(r, c, not get_tdata(r, c) if checked is None else bool(checked)) elif span.kind == "column": totalrows = range(self.MT.total_data_rows()) for c in cols: if header and get_hkw(c, key="checkbox"): set_hdata(c, not get_hdata(c) if checked is None else bool(checked)) if table: for r in totalrows: if get_tkw(r, c, key="checkbox"): set_tdata(r, c, not get_tdata(r, c) if checked is None else bool(checked)) self.set_refresh_timer(redraw) return span # Data Formatting def format( self, *key: CreateSpanTypes, formatter_options: dict = {}, formatter_class: object = None, redraw: bool = True, **kwargs, ) -> Span: span = self.span_from_key(*key) rows, cols = self.ranges_from_span(span) kwargs = fix_format_kwargs({"formatter": formatter_class, **formatter_options, **kwargs}) if span.kind == "cell" and span.table: for r in rows: for c in cols: self.del_cell_options_checkbox(r, c) add_to_options(self.MT.cell_options, (r, c), "format", kwargs) self.MT.set_cell_data( r, c, value=kwargs["value"] if "value" in kwargs else self.MT.get_cell_data(r, c), kwargs=kwargs, ) elif span.kind == "row": for r in rows: self.del_row_options_checkbox(r) kwargs = fix_format_kwargs(kwargs) add_to_options(self.MT.row_options, r, "format", kwargs) for c in cols: self.MT.set_cell_data( r, c, value=kwargs["value"] if "value" in kwargs else self.MT.get_cell_data(r, c), kwargs=kwargs, ) elif span.kind == "column": for c in cols: self.del_column_options_checkbox(c) kwargs = fix_format_kwargs(kwargs) add_to_options(self.MT.col_options, c, "format", kwargs) for r in rows: self.MT.set_cell_data( r, c, value=kwargs["value"] if "value" in kwargs else self.MT.get_cell_data(r, c), kwargs=kwargs, ) self.set_refresh_timer(redraw) return span def del_format( self, *key: CreateSpanTypes, clear_values: bool = False, redraw: bool = True, ) -> Span: span = self.span_from_key(*key) rows, cols = self.ranges_from_span(span) if span.table: if span.kind == "cell": for r in rows: for c in cols: self.MT.delete_cell_format(r, c, clear_values=clear_values) elif span.kind == "row": for r in rows: self.MT.delete_row_format(r, clear_values=clear_values) elif span.kind == "column": for c in cols: self.MT.delete_column_format(c, clear_values=clear_values) self.set_refresh_timer(redraw) return span def reapply_formatting(self) -> Sheet: self.MT.reapply_formatting() return self def del_all_formatting(self, clear_values: bool = False) -> Sheet: self.MT.delete_all_formatting(clear_values=clear_values) return self delete_all_formatting = del_all_formatting def formatted(self, r: int, c: int) -> dict: return self.MT.get_cell_kwargs(r, c, key="format") # Readonly Cells def readonly( self, *key: CreateSpanTypes, readonly: bool = True, ) -> Span: span = self.span_from_key(*key) rows, cols = self.ranges_from_span(span) table, index, header = span.table, span.index, span.header if span.kind == "cell": if header: for c in cols: set_readonly(self.CH.cell_options, c, readonly) for r in rows: if index: set_readonly(self.RI.cell_options, r, readonly) if table: for c in cols: set_readonly(self.MT.cell_options, (r, c), readonly) elif span.kind == "row": for r in rows: set_readonly(self.MT.row_options, r, readonly) elif span.kind == "column": for c in cols: set_readonly(self.MT.col_options, c, readonly) return span # Text Font and Alignment def font( self, newfont: tuple[str, int, str] | None = None, reset_row_positions: bool = True, ) -> tuple[str, int, str]: return self.MT.set_table_font(newfont, reset_row_positions=reset_row_positions) def header_font(self, newfont: tuple[str, int, str] | None = None) -> tuple[str, int, str]: return self.MT.set_header_font(newfont) def table_align( self, align: str = None, redraw: bool = True, ) -> str | Sheet: if align is None: return self.MT.align elif convert_align(align): self.MT.align = convert_align(align) else: raise ValueError("Align must be one of the following values: c, center, w, west, e, east") return self.set_refresh_timer(redraw) def header_align( self, align: str = None, redraw: bool = True, ) -> str | Sheet: if align is None: return self.CH.align elif convert_align(align): self.CH.align = convert_align(align) else: raise ValueError("Align must be one of the following values: c, center, w, west, e, east") return self.set_refresh_timer(redraw) def row_index_align( self, align: str = None, redraw: bool = True, ) -> str | Sheet: if align is None: return self.RI.align elif convert_align(align): self.RI.align = convert_align(align) else: raise ValueError("Align must be one of the following values: c, center, w, west, e, east") return self.set_refresh_timer(redraw) index_align = row_index_align def align( self, *key: CreateSpanTypes, align: str | None = None, redraw: bool = True, ) -> Span: span = self.span_from_key(*key) rows, cols = self.ranges_from_span(span) table, index, header = span.table, span.index, span.header align = convert_align(align) if span.kind == "cell": if header: for c in cols: set_align(self.CH.cell_options, c, align) for r in rows: if index: set_align(self.RI.cell_options, r, align) if table: for c in cols: set_align(self.MT.cell_options, (r, c), align) elif span.kind == "row": for r in rows: if index: set_align(self.RI.cell_options, r, align) if table: set_align(self.MT.row_options, r, align) elif span.kind == "column": for c in cols: if header: set_align(self.CH.cell_options, c, align) if table: set_align(self.MT.col_options, c, align) self.set_refresh_timer(redraw) return span def del_align( self, *key: CreateSpanTypes, redraw: bool = True, ) -> Span: span = self.span_from_key(*key) self.del_options_using_span(span, "align") self.set_refresh_timer(redraw) return span # Getting Selected Cells def get_currently_selected(self) -> tuple[()] | Selected: # if self.MT.selected: # return self.MT.selected._replace(type_=self.MT.selected.type_[:-1]) return self.MT.selected @property def selected(self) -> tuple[()] | Selected: return self.MT.selected @selected.setter def selected(self, selected: tuple[()] | Selected) -> Sheet: if selected: self.MT.set_currently_selected( r=selected[0], c=selected[1], item=selected[4], box=selected[3], ) else: self.MT.deselect(redraw=False) return self.set_refresh_timer() def get_selected_rows( self, get_cells: bool = False, get_cells_as_rows: bool = False, return_tuple: bool = False, ) -> tuple[int] | tuple[tuple[int, int]] | set[int] | set[tuple[int, int]]: if return_tuple: return tuple(self.MT.get_selected_rows(get_cells=get_cells, get_cells_as_rows=get_cells_as_rows)) return self.MT.get_selected_rows(get_cells=get_cells, get_cells_as_rows=get_cells_as_rows) def get_selected_columns( self, get_cells: bool = False, get_cells_as_columns: bool = False, return_tuple: bool = False, ) -> tuple[int] | tuple[tuple[int, int]] | set[int] | set[tuple[int, int]]: if return_tuple: return tuple(self.MT.get_selected_cols(get_cells=get_cells, get_cells_as_cols=get_cells_as_columns)) return self.MT.get_selected_cols(get_cells=get_cells, get_cells_as_cols=get_cells_as_columns) def get_selected_cells( self, get_rows: bool = False, get_columns: bool = False, sort_by_row: bool = False, sort_by_column: bool = False, ) -> list[tuple[int, int]] | set[tuple[int, int]]: if sort_by_row and sort_by_column: sels = sorted( self.MT.get_selected_cells(get_rows=get_rows, get_cols=get_columns), key=lambda t: t[1], ) return sorted(sels, key=lambda t: t[0]) elif sort_by_row: return sorted( self.MT.get_selected_cells(get_rows=get_rows, get_cols=get_columns), key=lambda t: t[0], ) elif sort_by_column: return sorted( self.MT.get_selected_cells(get_rows=get_rows, get_cols=get_columns), key=lambda t: t[1], ) return self.MT.get_selected_cells(get_rows=get_rows, get_cols=get_columns) def gen_selected_cells( self, get_rows: bool = False, get_columns: bool = False, ) -> Generator[tuple[int, int]]: yield from self.MT.gen_selected_cells(get_rows=get_rows, get_cols=get_columns) def get_all_selection_boxes(self) -> tuple[tuple[int, int, int, int]]: return self.MT.get_all_selection_boxes() def get_all_selection_boxes_with_types(self) -> list[tuple[tuple[int, int, int, int], str]]: return self.MT.get_all_selection_boxes_with_types() @property def boxes(self) -> list[tuple[tuple[int, int, int, int], str]]: return self.MT.get_all_selection_boxes_with_types() @boxes.setter def boxes(self, boxes: Sequence[tuple[tuple[int, int, int, int], str]]) -> Sheet: self.MT.deselect(redraw=False) self.MT.reselect_from_get_boxes( boxes={box[0] if isinstance(box[0], tuple) else tuple(box[0]): box[1] for box in boxes} ) return self.set_refresh_timer() @property def canvas_boxes(self) -> dict[int, SelectionBox]: return self.MT.selection_boxes def cell_selected( self, r: int, c: int, rows: bool = False, columns: bool = False, ) -> bool: return self.MT.cell_selected(r, c, inc_cols=columns, inc_rows=rows) def row_selected(self, r: int, cells: bool = False) -> bool: return self.MT.row_selected(r, cells=cells) def column_selected(self, c: int, cells: bool = False) -> bool: return self.MT.col_selected(c, cells=cells) def anything_selected( self, exclude_columns: bool = False, exclude_rows: bool = False, exclude_cells: bool = False, ) -> bool: if self.MT.anything_selected( exclude_columns=exclude_columns, exclude_rows=exclude_rows, exclude_cells=exclude_cells, ): return True return False def all_selected(self) -> bool: return self.MT.all_selected() def get_ctrl_x_c_boxes( self, nrows: bool = True, ) -> tuple[dict[tuple[int, int, int, int], str], int] | dict[tuple[int, int, int, int], str]: if nrows: return self.MT.get_ctrl_x_c_boxes() return self.MT.get_ctrl_x_c_boxes()[0] @property def ctrl_boxes(self) -> dict[tuple[int, int, int, int], str]: return self.MT.get_ctrl_x_c_boxes()[0] def get_selected_min_max( self, ) -> tuple[int, int, int, int] | tuple[None, None, None, None]: """ Returns (min_y, min_x, max_y, max_x) of all selection boxes """ return self.MT.get_selected_min_max() # Modifying Selected Cells def set_currently_selected(self, row: int | None = None, column: int | None = None, **kwargs) -> Sheet: self.MT.set_currently_selected( r=row, c=column, **kwargs, ) return self def select_row(self, row: int, redraw: bool = True, run_binding_func: bool = True) -> Sheet: self.RI.select_row( row if isinstance(row, int) else int(row), redraw=False, run_binding_func=run_binding_func, ext=True, ) return self.set_refresh_timer(redraw) def select_column(self, column: int, redraw: bool = True, run_binding_func: bool = True) -> Sheet: self.CH.select_col( column if isinstance(column, int) else int(column), redraw=False, run_binding_func=run_binding_func, ext=True, ) return self.set_refresh_timer(redraw) def select_cell(self, row: int, column: int, redraw: bool = True, run_binding_func: bool = True) -> Sheet: self.MT.select_cell( row if isinstance(row, int) else int(row), column if isinstance(column, int) else int(column), redraw=False, run_binding_func=run_binding_func, ext=True, ) return self.set_refresh_timer(redraw) def select_all(self, redraw: bool = True, run_binding_func: bool = True) -> Sheet: self.MT.select_all(redraw=False, run_binding_func=run_binding_func) return self.set_refresh_timer(redraw) def add_cell_selection( self, row: int, column: int, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ) -> Sheet: self.MT.add_selection( r=row, c=column, redraw=False, run_binding_func=run_binding_func, set_as_current=set_as_current, ext=True, ) return self.set_refresh_timer(redraw) def add_row_selection( self, row: int, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ) -> Sheet: self.RI.add_selection( r=row, redraw=False, run_binding_func=run_binding_func, set_as_current=set_as_current, ext=True, ) return self.set_refresh_timer(redraw) def add_column_selection( self, column: int, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ) -> Sheet: self.CH.add_selection( c=column, redraw=False, run_binding_func=run_binding_func, set_as_current=set_as_current, ext=True, ) return self.set_refresh_timer(redraw) def toggle_select_cell( self, row: int, column: int, add_selection: bool = True, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ) -> Sheet: self.MT.toggle_select_cell( row=row, column=column, add_selection=add_selection, redraw=False, run_binding_func=run_binding_func, set_as_current=set_as_current, ext=True, ) return self.set_refresh_timer(redraw) def toggle_select_row( self, row: int, add_selection: bool = True, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ) -> Sheet: self.RI.toggle_select_row( row=row, add_selection=add_selection, redraw=False, run_binding_func=run_binding_func, set_as_current=set_as_current, ext=True, ) return self.set_refresh_timer(redraw) def toggle_select_column( self, column: int, add_selection: bool = True, redraw: bool = True, run_binding_func: bool = True, set_as_current: bool = True, ) -> Sheet: self.CH.toggle_select_col( column=column, add_selection=add_selection, redraw=False, run_binding_func=run_binding_func, set_as_current=set_as_current, ext=True, ) return self.set_refresh_timer(redraw) def create_selection_box( self, r1: int, c1: int, r2: int, c2: int, type_: Literal["cells", "rows", "columns", "cols"] = "cells", ) -> int: return self.MT.create_selection_box( r1=r1, c1=c1, r2=r2, c2=c2, type_="columns" if type_ == "cols" else type_, ext=True, ) def recreate_all_selection_boxes(self) -> Sheet: self.MT.recreate_all_selection_boxes() return self def deselect( self, row: int | None | Literal["all"] = None, column: int | None = None, cell: tuple[int, int] | None = None, redraw: bool = True, ) -> Sheet: self.MT.deselect(r=row, c=column, cell=cell, redraw=False) return self.set_refresh_timer(redraw) def deselect_any( self, rows: Iterator[int] | int | None, columns: Iterator[int] | int | None, redraw: bool = True, ) -> Sheet: self.MT.deselect_any(rows=rows, columns=columns, redraw=False) return self.set_refresh_timer(redraw) # Row Heights and Column Widths def default_column_width(self, width: int | None = None) -> int: if isinstance(width, int): self.ops.default_column_width = width return self.ops.default_column_width def default_row_height(self, height: int | str | None = None) -> int: if isinstance(height, (int, str)): self.ops.default_row_height = height return self.ops.default_row_height def default_header_height(self, height: int | str | None = None) -> int: if isinstance(height, (int, str)): self.ops.default_header_height = height return self.ops.default_header_height def set_cell_size_to_text( self, row: int, column: int, only_set_if_too_small: bool = False, redraw: bool = True, ) -> Sheet: self.MT.set_cell_size_to_text(r=row, c=column, only_if_too_small=only_set_if_too_small) return self.set_refresh_timer(redraw) def set_all_cell_sizes_to_text( self, redraw: bool = True, width: int | None = None, slim: bool = False, ) -> tuple[list[float], list[float]]: self.MT.set_all_cell_sizes_to_text(width=width, slim=slim) self.set_refresh_timer(redraw) return self.MT.row_positions, self.MT.col_positions def set_all_column_widths( self, width: int | None = None, only_set_if_too_small: bool = False, redraw: bool = True, recreate_selection_boxes: bool = True, ) -> Sheet: self.CH.set_width_of_all_cols( width=width, only_if_too_small=only_set_if_too_small, recreate=recreate_selection_boxes, ) return self.set_refresh_timer(redraw) def set_all_row_heights( self, height: int | None = None, only_set_if_too_small: bool = False, redraw: bool = True, recreate_selection_boxes: bool = True, ) -> Sheet: self.RI.set_height_of_all_rows( height=height, only_if_too_small=only_set_if_too_small, recreate=recreate_selection_boxes, ) return self.set_refresh_timer(redraw) def column_width( self, column: int | Literal["all", "displayed"] | None = None, width: int | Literal["default", "text"] | None = None, only_set_if_too_small: bool = False, redraw: bool = True, ) -> Sheet | int: if column == "all" and width == "default": self.MT.reset_col_positions() elif column == "displayed" and width == "text": for c in range(*self.MT.visible_text_columns): self.CH.set_col_width(c) elif width == "text" and isinstance(column, int): self.CH.set_col_width(col=column, width=None, only_if_too_small=only_set_if_too_small) elif isinstance(width, int) and isinstance(column, int): self.CH.set_col_width(col=column, width=width, only_if_too_small=only_set_if_too_small) elif isinstance(column, int): return int(self.MT.col_positions[column + 1] - self.MT.col_positions[column]) return self.set_refresh_timer(redraw) def row_height( self, row: int | Literal["all", "displayed"] | None = None, height: int | Literal["default", "text"] | None = None, only_set_if_too_small: bool = False, redraw: bool = True, ) -> Sheet | int: if row == "all" and height == "default": self.MT.reset_row_positions() elif row == "displayed" and height == "text": for r in range(*self.MT.visible_text_rows): self.RI.set_row_height(r) elif height == "text" and isinstance(row, int): self.RI.set_row_height(row=row, height=None, only_if_too_small=only_set_if_too_small) elif isinstance(height, int) and isinstance(row, int): self.RI.set_row_height(row=row, height=height, only_if_too_small=only_set_if_too_small) elif isinstance(row, int): return int(self.MT.row_positions[row + 1] - self.MT.row_positions[row]) return self.set_refresh_timer(redraw) def get_column_widths(self, canvas_positions: bool = False) -> list[float]: if canvas_positions: return self.MT.col_positions return self.MT.get_column_widths() def get_row_heights(self, canvas_positions: bool = False) -> list[float]: if canvas_positions: return self.MT.row_positions return self.MT.get_row_heights() def get_row_text_height( self, row: int, visible_only: bool = False, only_if_too_small: bool = False, ) -> int: return self.RI.get_row_text_height( row=row, visible_only=visible_only, only_if_too_small=only_if_too_small, ) def get_column_text_width( self, column: int, visible_only: bool = False, only_if_too_small: bool = False, ) -> int: return self.CH.get_col_text_width( col=column, visible_only=visible_only, only_if_too_small=only_if_too_small, ) def set_column_widths( self, column_widths: Iterator[float] | None = None, canvas_positions: bool = False, reset: bool = False, ) -> Sheet: if reset or column_widths is None: self.MT.reset_col_positions() elif is_iterable(column_widths): if canvas_positions and isinstance(column_widths, list): self.MT.col_positions = column_widths else: self.MT.col_positions = list(accumulate(chain([0], column_widths))) return self def set_row_heights( self, row_heights: Iterator[float] | None = None, canvas_positions: bool = False, reset: bool = False, ) -> Sheet: if reset or row_heights is None: self.MT.reset_row_positions() elif is_iterable(row_heights): if canvas_positions and isinstance(row_heights, list): self.MT.row_positions = row_heights else: self.MT.row_positions = list(accumulate(chain([0], row_heights))) return self def set_width_of_index_to_text(self, text: None | str = None, *args, **kwargs) -> Sheet: self.RI.set_width_of_index_to_text(text=text) return self def set_index_width(self, pixels: int, redraw: bool = True) -> Sheet: if self.ops.auto_resize_row_index: self.ops.auto_resize_row_index = False self.RI.set_width(pixels, set_TL=True) return self.set_refresh_timer(redraw) def set_height_of_header_to_text(self, text: None | str = None) -> Sheet: self.CH.set_height_of_header_to_text(text=text) return self def set_header_height_pixels(self, pixels: int, redraw: bool = True) -> Sheet: self.CH.set_height(pixels, set_TL=True) return self.set_refresh_timer(redraw) def set_header_height_lines(self, nlines: int, redraw: bool = True) -> Sheet: self.CH.set_height( self.MT.get_lines_cell_height( nlines, font=self.ops.header_font, ), set_TL=True, ) return self.set_refresh_timer(redraw) def del_row_position(self, idx: int, deselect_all: bool = False) -> Sheet: self.MT.del_row_position(idx=idx, deselect_all=deselect_all) return self delete_row_position = del_row_position def del_row_positions(self, idxs: Iterator[int] | None = None) -> Sheet: self.MT.del_row_positions(idxs=idxs) self.set_refresh_timer() return self def del_column_position(self, idx: int, deselect_all: bool = False) -> Sheet: self.MT.del_col_position(idx, deselect_all=deselect_all) return self delete_column_position = del_column_position def del_column_positions(self, idxs: Iterator[int] | None = None) -> Sheet: self.MT.del_col_positions(idxs=idxs) self.set_refresh_timer() return self def insert_column_position( self, idx: Literal["end"] | int = "end", width: int | None = None, deselect_all: bool = False, redraw: bool = False, ) -> Sheet: self.MT.insert_col_position(idx=idx, width=width, deselect_all=deselect_all) return self.set_refresh_timer(redraw) def insert_row_position( self, idx: Literal["end"] | int = "end", height: int | None = None, deselect_all: bool = False, redraw: bool = False, ) -> Sheet: self.MT.insert_row_position(idx=idx, height=height, deselect_all=deselect_all) return self.set_refresh_timer(redraw) def insert_column_positions( self, idx: Literal["end"] | int = "end", widths: Sequence[float] | int | None = None, deselect_all: bool = False, redraw: bool = False, ) -> Sheet: self.MT.insert_col_positions(idx=idx, widths=widths, deselect_all=deselect_all) return self.set_refresh_timer(redraw) def insert_row_positions( self, idx: Literal["end"] | int = "end", heights: Sequence[float] | int | None = None, deselect_all: bool = False, redraw: bool = False, ) -> Sheet: self.MT.insert_row_positions(idx=idx, heights=heights, deselect_all=deselect_all) return self.set_refresh_timer(redraw) def sheet_display_dimensions( self, total_rows: int | None = None, total_columns: int | None = None, ) -> tuple[int, int] | Sheet: if total_rows is None and total_columns is None: return len(self.MT.row_positions) - 1, len(self.MT.col_positions) - 1 if isinstance(total_rows, int): height = self.MT.get_default_row_height() self.MT.row_positions = list(accumulate(chain([0], repeat(height, total_rows)))) if isinstance(total_columns, int): width = self.ops.default_column_width self.MT.col_positions = list(accumulate(chain([0], repeat(width, total_columns)))) return self def move_row_position(self, row: int, moveto: int) -> Sheet: self.MT.move_row_position(row, moveto) return self def move_column_position(self, column: int, moveto: int) -> Sheet: self.MT.move_col_position(column, moveto) return self def get_example_canvas_column_widths(self, total_cols: int | None = None) -> list[float]: colpos = int(self.ops.default_column_width) if isinstance(total_cols, int): return list(accumulate(chain([0], repeat(colpos, total_cols)))) return list(accumulate(chain([0], repeat(colpos, len(self.MT.col_positions) - 1)))) def get_example_canvas_row_heights(self, total_rows: int | None = None) -> list[float]: rowpos = self.MT.get_default_row_height() if isinstance(total_rows, int): return list(accumulate(chain([0], repeat(rowpos, total_rows)))) return list(accumulate(chain([0], repeat(rowpos, len(self.MT.row_positions) - 1)))) def verify_row_heights(self, row_heights: list[float], canvas_positions: bool = False) -> bool: if not isinstance(row_heights, list): return False if canvas_positions: if row_heights[0] != 0: return False return not any( x - z < self.MT.min_row_height or not isinstance(x, int) or isinstance(x, bool) for z, x in zip(row_heights, islice(row_heights, 1, None)) ) return not any(z < self.MT.min_row_height or not isinstance(z, int) or isinstance(z, bool) for z in row_heights) def verify_column_widths(self, column_widths: list[float], canvas_positions: bool = False) -> bool: if not isinstance(column_widths, list): return False if canvas_positions: if column_widths[0] != 0: return False return not any( x - z < self.MT.min_column_width or not isinstance(x, int) or isinstance(x, bool) for z, x in zip(column_widths, islice(column_widths, 1, None)) ) return not any( z < self.MT.min_column_width or not isinstance(z, int) or isinstance(z, bool) for z in column_widths ) def valid_row_height(self, height: int) -> int: if height < self.MT.min_row_height: return self.MT.min_row_height elif height > self.MT.max_row_height: return self.MT.max_row_height return height def valid_column_width(self, width: int) -> int: if width < self.MT.min_column_width: return self.MT.min_column_width elif width > self.MT.max_column_width: return self.MT.max_column_width return width @property def visible_rows(self) -> tuple[int, int]: """ returns: tuple[visible start row int, visible end row int] """ return self.MT.visible_text_rows @property def visible_columns(self) -> tuple[int, int]: """ returns: tuple[visible start column int, visible end column int] """ return self.MT.visible_text_columns # Identifying Bound Event Mouse Position def identify_region(self, event: object) -> Literal["table", "index", "header", "top left"]: if event.widget == self.MT: return "table" elif event.widget == self.RI: return "index" elif event.widget == self.CH: return "header" elif event.widget == self.TL: return "top left" def identify_row( self, event: object, exclude_index: bool = False, allow_end: bool = True, ) -> int | None: ev_w = event.widget if ev_w == self.MT: return self.MT.identify_row(y=event.y, allow_end=allow_end) elif ev_w == self.RI: if exclude_index: return None else: return self.MT.identify_row(y=event.y, allow_end=allow_end) elif ev_w == self.CH or ev_w == self.TL: return None def identify_column( self, event: object, exclude_header: bool = False, allow_end: bool = True, ) -> int | None: ev_w = event.widget if ev_w == self.MT: return self.MT.identify_col(x=event.x, allow_end=allow_end) elif ev_w == self.RI or ev_w == self.TL: return None elif ev_w == self.CH: if exclude_header: return None else: return self.MT.identify_col(x=event.x, allow_end=allow_end) # Scroll Positions and Cell Visibility def sync_scroll(self, widget: object) -> Sheet: if widget is self: return self self.MT.synced_scrolls.add(widget) if isinstance(widget, Sheet): widget.MT.synced_scrolls.add(self) return self def unsync_scroll(self, widget: object = None) -> Sheet: if widget is None: for widget in self.MT.synced_scrolls: if isinstance(widget, Sheet): widget.MT.synced_scrolls.discard(self) self.MT.synced_scrolls = set() else: if isinstance(widget, Sheet) and self in widget.MT.synced_scrolls: widget.MT.synced_scrolls.discard(self) self.MT.synced_scrolls.discard(widget) return self def see( self, row: int = 0, column: int = 0, keep_yscroll: bool = False, keep_xscroll: bool = False, bottom_right_corner: bool = False, check_cell_visibility: bool = True, redraw: bool = True, ) -> Sheet: self.MT.see( row, column, keep_yscroll, keep_xscroll, bottom_right_corner, check_cell_visibility=check_cell_visibility, redraw=False, ) return self.set_refresh_timer(redraw) def cell_visible(self, r: int, c: int) -> bool: return self.MT.cell_visible(r, c) def cell_completely_visible(self, r: int, c: int, seperate_axes: bool = False) -> bool: return self.MT.cell_completely_visible(r, c, seperate_axes) def set_xview(self, position: None | float = None, option: str = "moveto") -> Sheet | tuple[float, float]: if position is not None: self.MT.set_xviews(option, position) return self return self.MT.xview() xview = set_xview xview_moveto = set_xview def set_yview(self, position: None | float = None, option: str = "moveto") -> Sheet | tuple[float, float]: if position is not None: self.MT.set_yviews(option, position) return self return self.MT.yview() yview = set_yview yview_moveto = set_yview def set_view(self, x_args: list[str, float], y_args: list[str, float]) -> Sheet: self.MT.set_view(x_args, y_args) return self def get_xview(self) -> tuple[float, float]: return self.MT.xview() def get_yview(self) -> tuple[float, float]: return self.MT.yview() # Hiding Columns def displayed_column_to_data(self, c: int) -> int: return c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] data_c = displayed_column_to_data dcol = displayed_column_to_data def display_columns( self, columns: None | Literal["all"] | Iterator[int] = None, all_columns_displayed: None | bool = None, reset_col_positions: bool = True, redraw: bool = False, deselect_all: bool = True, **kwargs, ) -> list[int] | None: if "all_displayed" in kwargs: all_columns_displayed = kwargs["all_displayed"] res = self.MT.display_columns( columns=None if isinstance(columns, str) and columns.lower() == "all" else columns, all_columns_displayed=( True if isinstance(columns, str) and columns.lower() == "all" else all_columns_displayed ), reset_col_positions=reset_col_positions, deselect_all=deselect_all, ) if "refresh" in kwargs: redraw = kwargs["refresh"] self.set_refresh_timer(redraw) return res def hide_columns( self, columns: int | set[int] | Iterator[int], redraw: bool = True, deselect_all: bool = True, data_indexes: bool = False, ) -> Sheet: if isinstance(columns, int): columns = {columns} elif not isinstance(columns, set): columns = set(columns) if not columns: return if self.MT.all_columns_displayed: self.MT.displayed_columns = list(filterfalse(columns.__contains__, range(self.MT.total_data_cols()))) to_pop = {c: c for c in columns} else: to_pop = {} new_disp = [] if data_indexes: for i, c in enumerate(self.MT.displayed_columns): if c not in columns: new_disp.append(c) else: to_pop[i] = c else: for i, c in enumerate(self.MT.displayed_columns): if i not in columns: new_disp.append(c) else: to_pop[i] = c self.MT.displayed_columns = new_disp self.MT.all_columns_displayed = False self.MT.set_col_positions( pop_positions( itr=self.MT.gen_column_widths, to_pop=to_pop, save_to=self.MT.saved_column_widths, ), ) if deselect_all: self.MT.deselect(redraw=False) return self.set_refresh_timer(redraw) def show_columns( self, columns: int | Iterator[int], redraw: bool = True, deselect_all: bool = True, ) -> Sheet: """ 'columns' argument must be data indexes Function will return without action if Sheet.all_columns """ if self.MT.all_columns_displayed: return if isinstance(columns, int): columns = [columns] cws = self.MT.get_column_widths() for column in columns: idx = bisect_left(self.MT.displayed_columns, column) if len(self.MT.displayed_columns) == idx or self.MT.displayed_columns[idx] != column: self.MT.displayed_columns.insert(idx, column) cws.insert(idx, self.MT.saved_column_widths.pop(column, self.ops.default_column_width)) self.MT.set_col_positions(cws) if deselect_all: self.MT.deselect(redraw=False) return self.set_refresh_timer(redraw) def all_columns_displayed(self, a: bool | None = None) -> bool: v = bool(self.MT.all_columns_displayed) if isinstance(a, bool): self.MT.all_columns_displayed = a return v @property def all_columns(self) -> bool: return self.MT.all_columns_displayed @all_columns.setter def all_columns(self, a: bool) -> Sheet: self.MT.display_columns( columns=None, all_columns_displayed=a, ) return self @property def displayed_columns(self) -> list[int]: return self.MT.displayed_columns @displayed_columns.setter def displayed_columns(self, columns: list[int]) -> Sheet: self.display_columns(columns=columns, reset_col_positions=True, redraw=False) return self.set_refresh_timer() # Hiding Rows def displayed_row_to_data(self, r: int) -> int: return r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] data_r = displayed_row_to_data drow = displayed_row_to_data def display_rows( self, rows: None | Literal["all"] | Iterator[int] = None, all_rows_displayed: None | bool = None, reset_row_positions: bool = True, redraw: bool = False, deselect_all: bool = True, **kwargs, ) -> list[int] | None: if "all_displayed" in kwargs: all_rows_displayed = kwargs["all_displayed"] res = self.MT.display_rows( rows=None if isinstance(rows, str) and rows.lower() == "all" else rows, all_rows_displayed=True if isinstance(rows, str) and rows.lower() == "all" else all_rows_displayed, reset_row_positions=reset_row_positions, deselect_all=deselect_all, ) if "refresh" in kwargs: redraw = kwargs["refresh"] self.set_refresh_timer(redraw) return res def hide_rows( self, rows: int | set[int] | Iterator[int], redraw: bool = True, deselect_all: bool = True, data_indexes: bool = False, row_heights: bool = True, ) -> Sheet: if isinstance(rows, int): rows = {rows} elif not isinstance(rows, set): rows = set(rows) if not rows: return if self.MT.all_rows_displayed: self.MT.displayed_rows = list(filterfalse(rows.__contains__, range(self.MT.total_data_rows()))) to_pop = {r: r for r in rows} else: to_pop = {} new_disp = [] if data_indexes: for i, r in enumerate(self.MT.displayed_rows): if r not in rows: new_disp.append(r) else: to_pop[i] = r else: for i, r in enumerate(self.MT.displayed_rows): if i not in rows: new_disp.append(r) else: to_pop[i] = r self.MT.displayed_rows = new_disp self.MT.all_rows_displayed = False if row_heights: self.MT.set_row_positions( pop_positions( itr=self.MT.gen_row_heights, to_pop=to_pop, save_to=self.MT.saved_row_heights, ), ) if deselect_all: self.MT.deselect(redraw=False) return self.set_refresh_timer(redraw) def show_rows( self, rows: int | Iterator[int], redraw: bool = True, deselect_all: bool = True, ) -> Sheet: """ 'rows' argument must be data indexes Function will return without action if Sheet.all_rows """ if self.MT.all_rows_displayed: return if isinstance(rows, int): rows = [rows] rhs = self.MT.get_row_heights() for row in rows: idx = bisect_left(self.MT.displayed_rows, row) if len(self.MT.displayed_rows) == idx or self.MT.displayed_rows[idx] != row: self.MT.displayed_rows.insert(idx, row) rhs.insert(idx, self.MT.saved_row_heights.pop(row, self.MT.get_default_row_height())) self.MT.set_row_positions(rhs) if deselect_all: self.MT.deselect(redraw=False) return self.set_refresh_timer(redraw) def all_rows_displayed(self, a: bool | None = None) -> bool: v = bool(self.MT.all_rows_displayed) if isinstance(a, bool): self.MT.all_rows_displayed = a return v @property def all_rows(self) -> bool: return self.MT.all_rows_displayed @all_rows.setter def all_rows(self, a: bool) -> Sheet: self.MT.display_rows( rows=None, all_rows_displayed=a, ) return self @property def displayed_rows(self) -> list[int]: return self.MT.displayed_rows @displayed_rows.setter def displayed_rows(self, rows: list[int]) -> Sheet: self.display_rows(rows=rows, reset_row_positions=True, redraw=False) return self.set_refresh_timer() # Hiding Sheet Elements def hide( self, canvas: Literal[ "all", "row_index", "header", "top_left", "x_scrollbar", "y_scrollbar", ] = "all", ) -> Sheet: if canvas.lower() == "all": self.TL.grid_forget() self.RI.grid_forget() self.RI["yscrollcommand"] = 0 self.MT.show_index = False self.CH.grid_forget() self.CH["xscrollcommand"] = 0 self.MT.show_header = False self.MT.grid_forget() self.yscroll.grid_forget() self.xscroll.grid_forget() self.xscroll_showing = False self.yscroll_showing = False self.xscroll_disabled = True self.yscroll_disabled = True elif canvas.lower() == "row_index": self.RI.grid_forget() self.RI["yscrollcommand"] = 0 self.MT.show_index = False elif canvas.lower() == "header": self.CH.grid_forget() self.CH["xscrollcommand"] = 0 self.MT.show_header = False elif canvas.lower() == "top_left": self.TL.grid_forget() elif canvas.lower() == "x_scrollbar": self.xscroll.grid_forget() self.xscroll_showing = False self.xscroll_disabled = True elif canvas.lower() == "y_scrollbar": self.yscroll.grid_forget() self.yscroll_showing = False self.yscroll_disabled = True return self def show( self, canvas: Literal[ "all", "row_index", "header", "top_left", "x_scrollbar", "y_scrollbar", ] = "all", ) -> Sheet: if canvas == "all": self.hide() self.TL.grid(row=0, column=0) self.RI.grid(row=1, column=0, sticky="nswe") self.CH.grid(row=0, column=1, sticky="nswe") self.MT.grid(row=1, column=1, sticky="nswe") self.yscroll.grid(row=0, column=2, rowspan=3, sticky="nswe") self.xscroll.grid(row=2, column=0, columnspan=2, sticky="nswe") self.MT["xscrollcommand"] = self.xscroll.set self.CH["xscrollcommand"] = self.xscroll.set self.MT["yscrollcommand"] = self.yscroll.set self.RI["yscrollcommand"] = self.yscroll.set self.xscroll_showing = True self.yscroll_showing = True self.xscroll_disabled = False self.yscroll_disabled = False elif canvas == "row_index": self.RI.grid(row=1, column=0, sticky="nswe") self.MT["yscrollcommand"] = self.yscroll.set self.RI["yscrollcommand"] = self.yscroll.set self.MT.show_index = True elif canvas == "header": self.CH.grid(row=0, column=1, sticky="nswe") self.MT["xscrollcommand"] = self.xscroll.set self.CH["xscrollcommand"] = self.xscroll.set self.MT.show_header = True elif canvas == "top_left": self.TL.grid(row=0, column=0) elif canvas == "x_scrollbar": self.xscroll.grid(row=2, column=0, columnspan=2, sticky="nswe") self.xscroll_showing = True self.xscroll_disabled = False elif canvas == "y_scrollbar": self.yscroll.grid(row=0, column=2, rowspan=3, sticky="nswe") self.yscroll_showing = True self.yscroll_disabled = False self.MT.update_idletasks() return self # Sheet Height and Width def height_and_width( self, height: int | None = None, width: int | None = None, ) -> Sheet: if width is not None or height is not None: self.grid_propagate(0) elif width is None and height is None: self.grid_propagate(1) if width is not None: self.config(width=width) if height is not None: self.config(height=height) return self def get_frame_y(self, y: int) -> int: return y + self.CH.current_height def get_frame_x(self, x: int) -> int: return x + self.RI.current_width # Cell Text Editor # works on currently selected box def open_cell(self, ignore_existing_editor: bool = True) -> Sheet: self.MT.open_cell(event=GeneratedMouseEvent(), ignore_existing_editor=ignore_existing_editor) return self def open_header_cell(self, ignore_existing_editor: bool = True) -> Sheet: self.CH.open_cell(event=GeneratedMouseEvent(), ignore_existing_editor=ignore_existing_editor) return self def open_index_cell(self, ignore_existing_editor: bool = True) -> Sheet: self.RI.open_cell(event=GeneratedMouseEvent(), ignore_existing_editor=ignore_existing_editor) return self def set_text_editor_value( self, text: str = "", ) -> Sheet: if self.MT.text_editor.open: self.MT.text_editor.window.set_text(text) return self def set_index_text_editor_value( self, text: str = "", ) -> Sheet: if self.RI.text_editor.open: self.RI.text_editor.window.set_text(text) return self def set_header_text_editor_value( self, text: str = "", ) -> Sheet: if self.CH.text_editor.open: self.CH.text_editor.window.set_text(text) return self def destroy_text_editor(self, event: object = None) -> Sheet: self.MT.hide_text_editor(reason=event) self.RI.hide_text_editor(reason=event) self.CH.hide_text_editor(reason=event) return self def get_text_editor_widget(self, event: object = None) -> tk.Text | None: try: return self.MT.text_editor.tktext except Exception: return None def bind_key_text_editor(self, key: str, function: Callable) -> Sheet: self.MT.text_editor_user_bound_keys[key] = function return self def unbind_key_text_editor(self, key: str) -> Sheet: if key == "all": for key in self.MT.text_editor_user_bound_keys: try: self.MT.text_editor.tktext.unbind(key) except Exception: pass self.MT.text_editor_user_bound_keys = {} else: if key in self.MT.text_editor_user_bound_keys: del self.MT.text_editor_user_bound_keys[key] try: self.MT.text_editor.tktext.unbind(key) except Exception: pass return self def get_text_editor_value(self) -> str | None: return self.MT.text_editor.get() def close_text_editor(self, set_data: bool = True) -> Sheet: event = new_tk_event("ButtonPress-1" if set_data else "Escape") if self.MT.text_editor.open: self.MT.close_text_editor(event=event) if self.RI.text_editor.open: self.RI.close_text_editor(event=event) if self.CH.text_editor.open: self.CH.close_text_editor(event=event) return self # Sheet Options and Other Functions def set_options(self, redraw: bool = True, **kwargs) -> Sheet: for k, v in kwargs.items(): if k in self.ops and v != self.ops[k]: if k.endswith("bindings"): self.MT._disable_binding(k.split("_")[0]) self.ops[k] = v if k.endswith("bindings"): self.MT._enable_binding(k.split("_")[0]) if "from_clipboard_delimiters" in kwargs: self.ops.from_clipboard_delimiters = ( self.ops.from_clipboard_delimiters if isinstance(self.ops.from_clipboard_delimiters, str) else "".join(self.ops.from_clipboard_delimiters) ) if "default_row_height" in kwargs: self.default_row_height(kwargs["default_row_height"]) if "default_header" in kwargs: self.CH.default_header = kwargs["default_header"].lower() if "default_row_index" in kwargs: self.RI.default_index = kwargs["default_row_index"].lower() if "max_column_width" in kwargs: self.MT.max_column_width = float(kwargs["max_column_width"]) if "max_row_height" in kwargs: self.MT.max_row_height = float(kwargs["max_row_height"]) if "max_header_height" in kwargs: self.MT.max_header_height = float(kwargs["max_header_height"]) if "max_index_width" in kwargs: self.MT.max_index_width = float(kwargs["max_index_width"]) if "expand_sheet_if_paste_too_big" in kwargs: self.ops.paste_can_expand_x = kwargs["expand_sheet_if_paste_too_big"] self.ops.paste_can_expand_y = kwargs["expand_sheet_if_paste_too_big"] if "font" in kwargs: self.MT.set_table_font(kwargs["font"]) elif "table_font" in kwargs: self.MT.set_table_font(kwargs["table_font"]) if "header_font" in kwargs: self.MT.set_header_font(kwargs["header_font"]) if "index_font" in kwargs: self.MT.set_index_font(kwargs["index_font"]) if "theme" in kwargs: self.change_theme(kwargs["theme"]) if "header_bg" in kwargs: self.CH.config(background=kwargs["header_bg"]) if "index_bg" in kwargs: self.RI.config(background=kwargs["index_bg"]) if "top_left_bg" in kwargs: self.TL.config(background=kwargs["top_left_bg"]) self.TL.itemconfig(self.TL.select_all_box, fill=kwargs["top_left_bg"]) if "top_left_fg" in kwargs: self.TL.itemconfig("rw", fill=kwargs["top_left_fg"]) self.TL.itemconfig("rh", fill=kwargs["top_left_fg"]) if "frame_bg" in kwargs: self.config(background=kwargs["frame_bg"]) if "table_bg" in kwargs: self.MT.config(background=kwargs["table_bg"]) if "outline_thickness" in kwargs: self.config(highlightthickness=kwargs["outline_thickness"]) if "outline_color" in kwargs: self.config( highlightbackground=kwargs["outline_color"], highlightcolor=kwargs["outline_color"], ) if any(k in kwargs for k in scrollbar_options_keys): self.set_scrollbar_options() self.MT.create_rc_menus() if "treeview" in kwargs: self.set_options(auto_resize_row_index=True, redraw=False) self.index_align("w", redraw=False) return self.set_refresh_timer(redraw) def set_scrollbar_options(self) -> Sheet: style = ttk.Style() for orientation in ("vertical", "horizontal"): style.configure( f"Sheet{self.unique_id}.{orientation.capitalize()}.TScrollbar", background=self.ops[f"{orientation}_scroll_background"], troughcolor=self.ops[f"{orientation}_scroll_troughcolor"], lightcolor=self.ops[f"{orientation}_scroll_lightcolor"], darkcolor=self.ops[f"{orientation}_scroll_darkcolor"], relief=self.ops[f"{orientation}_scroll_relief"], troughrelief=self.ops[f"{orientation}_scroll_troughrelief"], bordercolor=self.ops[f"{orientation}_scroll_bordercolor"], borderwidth=self.ops[f"{orientation}_scroll_borderwidth"], gripcount=self.ops[f"{orientation}_scroll_gripcount"], arrowsize=self.ops[f"{orientation}_scroll_arrowsize"], ) style.map( f"Sheet{self.unique_id}.{orientation.capitalize()}.TScrollbar", foreground=[ ("!active", self.ops[f"{orientation}_scroll_not_active_fg"]), ("pressed", self.ops[f"{orientation}_scroll_pressed_fg"]), ("active", self.ops[f"{orientation}_scroll_active_fg"]), ], background=[ ("!active", self.ops[f"{orientation}_scroll_not_active_bg"]), ("pressed", self.ops[f"{orientation}_scroll_pressed_bg"]), ("active", self.ops[f"{orientation}_scroll_active_bg"]), ], ) return self def event_widget_is_sheet( self, event: object, table: bool = True, index: bool = True, header: bool = True, top_left: bool = True, ) -> bool: return ( table and event.widget == self.MT or index and event.widget == self.RI or header and event.widget == self.CH or top_left and event.widget == self.TL ) def get_cell_options( self, key: None | str = None, canvas: Literal["table", "row_index", "header"] = "table", ) -> dict: if canvas == "table": target = self.MT.cell_options elif canvas == "row_index": target = self.RI.cell_options elif canvas == "header": target = self.CH.cell_options if key is None: return target return {k: v[key] for k, v in target.items() if key in v} def get_row_options(self, key: None | str = None) -> dict: if key is None: return self.MT.row_options return {k: v[key] for k, v in self.MT.row_options.items() if key in v} def get_column_options(self, key: None | str = None) -> dict: if key is None: return self.MT.col_options return {k: v[key] for k, v in self.MT.col_options.items() if key in v} def get_index_options(self, key: None | str = None) -> dict: if key is None: return self.RI.cell_options return {k: v[key] for k, v in self.RI.cell_options.items() if key in v} def get_header_options(self, key: None | str = None) -> dict: if key is None: return self.CH.cell_options return {k: v[key] for k, v in self.CH.cell_options.items() if key in v} def del_out_of_bounds_options(self) -> Sheet: maxc = self.total_columns() maxr = self.total_rows() for name in tuple(self.MT.named_spans): span = self.MT.named_spans[name] if ( (isinstance(span.upto_r, int) and span.upto_r > maxr) or (isinstance(span.upto_c, int) and span.upto_c > maxc) or span.from_r >= maxr or span.from_c >= maxc ): self.del_named_span(name) self.MT.cell_options = {k: v for k, v in self.MT.cell_options.items() if k[0] < maxr and k[1] < maxc} self.RI.cell_options = {k: v for k, v in self.RI.cell_options.items() if k < maxr} self.CH.cell_options = {k: v for k, v in self.CH.cell_options.items() if k < maxc} self.MT.col_options = {k: v for k, v in self.MT.col_options.items() if k < maxc} self.MT.row_options = {k: v for k, v in self.MT.row_options.items() if k < maxr} return self delete_out_of_bounds_options = del_out_of_bounds_options def reset_all_options(self) -> Sheet: self.MT.named_spans = {} self.MT.cell_options = {} self.RI.cell_options = {} self.CH.cell_options = {} self.MT.col_options = {} self.MT.row_options = {} return self def show_ctrl_outline( self, canvas: Literal["table"] = "table", start_cell: tuple[int, int] = (0, 0), end_cell: tuple[int, int] = (1, 1), ) -> Sheet: self.MT.show_ctrl_outline(canvas=canvas, start_cell=start_cell, end_cell=end_cell) return self def reset_undos(self) -> Sheet: self.MT.purge_undo_and_redo_stack() return self def get_undo_stack(self) -> deque: return self.MT.undo_stack def get_redo_stack(self) -> deque: return self.MT.redo_stack def set_undo_stack(self, stack: deque) -> Sheet: self.MT.undo_stack = stack return self def set_redo_stack(self, stack: deque) -> Sheet: self.MT.redo_stack = stack return self def redraw(self, redraw_header: bool = True, redraw_row_index: bool = True) -> Sheet: self.MT.main_table_redraw_grid_and_text(redraw_header=redraw_header, redraw_row_index=redraw_row_index) return self refresh = redraw # Progress Bars def create_progress_bar( self, row: int, column: int, bg: str, fg: str, name: Hashable, percent: int = 0, del_when_done: bool = False, ) -> Sheet: self.MT.progress_bars[(row, column)] = ProgressBar( bg=bg, fg=fg, name=name, percent=percent, del_when_done=del_when_done, ) return self.set_refresh_timer() def progress_bar( self, name: Hashable | None = None, cell: tuple[int, int] | None = None, percent: int | None = None, bg: str | None = None, fg: str | None = None, ) -> Sheet: if name is not None: bars = (bar for bar in self.MT.progress_bars.values() if bar.name == name) elif cell is not None: bars = (self.MT.progress_bars[cell],) for bar in bars: if isinstance(percent, int): bar.percent = percent if isinstance(bg, str): bar.bg = bg if isinstance(fg, str): bar.fg = fg return self.set_refresh_timer() def del_progress_bar( self, name: Hashable | None = None, cell: tuple[int, int] | None = None, ) -> Sheet: if name is not None: for cell in tuple(cell for cell, bar in self.MT.progress_bars.items() if bar.name == name): del self.MT.progress_bars[cell] elif cell is not None: del self.MT.progress_bars[cell] return self.set_refresh_timer() # Tags def tag( self, *key: CreateSpanTypes, tags: Iterator[str] | str = "", ) -> Sheet: span = self.span_from_key(*key) rows, cols = self.ranges_from_span(span) if isinstance(tags, str): tags = (tags,) if span.kind == "cell": for tag in tags: if tag not in self.MT.tagged_cells: self.MT.tagged_cells[tag] = set() for r in rows: for c in cols: self.MT.tagged_cells[tag].add((r, c)) elif span.kind == "row": for tag in tags: if tag not in self.MT.tagged_rows: self.MT.tagged_rows[tag] = set() self.MT.tagged_rows[tag].update(set(rows)) elif span.kind == "column": for tag in tags: if tag not in self.MT.tagged_columns: self.MT.tagged_columns[tag] = set() self.MT.tagged_columns[tag].update(set(cols)) return self def tag_cell( self, cell: tuple[int, int], *tags, ) -> Sheet: if ( not isinstance(cell, tuple) or not len(cell) == 2 or not isinstance(cell[0], int) or not isinstance(cell[1], int) ): raise ValueError("'cell' argument must be tuple[int, int].") for tag in unpack(tags): if tag not in self.MT.tagged_cells: self.MT.tagged_cells[tag] = set() self.MT.tagged_cells[tag].add(cell) return self def tag_rows( self, rows: int | Iterator[int], *tags, ) -> Sheet: if isinstance(rows, int): rows = [rows] for tag in unpack(tags): if tag not in self.MT.tagged_rows: self.MT.tagged_rows[tag] = set() self.MT.tagged_rows[tag].update(rows) return self def tag_columns( self, columns: int | Iterator[int], *tags, ) -> Sheet: if isinstance(columns, int): columns = [columns] for tag in unpack(tags): if tag not in self.MT.tagged_columns: self.MT.tagged_columns[tag] = set() self.MT.tagged_columns[tag].update(columns) return self def untag( self, cell: tuple[int, int] | None = None, rows: int | Iterator[int] | None = None, columns: int | Iterator[int] | None = None, ) -> Sheet: if isinstance(cell, tuple): for tagged in self.MT.tagged_cells.values(): tagged.discard(cell) if isinstance(rows, int): rows = (rows,) if is_iterable(rows): for tagged in self.MT.tagged_rows.values(): for row in rows: tagged.discard(row) if isinstance(columns, int): columns = (columns,) if is_iterable(columns): for tagged in self.MT.tagged_columns.values(): for column in columns: tagged.discard(column) return self def tag_del( self, *tags, cells: bool = True, rows: bool = True, columns: bool = True, ) -> Sheet: for tag in unpack(tags): if cells and tag in self.MT.tagged_cells: del self.MT.tagged_cells[tag] if rows and tag in self.MT.tagged_rows: del self.MT.tagged_rows[tag] if columns and tag in self.MT.tagged_columns: del self.MT.tagged_columns[tag] return self def tag_has( self, *tags, ) -> DotDict: res = DotDict( cells=set(), rows=set(), columns=set(), ) for tag in unpack(tags): res.cells.update(self.MT.tagged_cells[tag] if tag in self.MT.tagged_cells else set()) res.rows.update(self.MT.tagged_rows[tag] if tag in self.MT.tagged_rows else set()) res.columns.update(self.MT.tagged_columns[tag] if tag in self.MT.tagged_columns else set()) return res # Treeview Mode def tree_build( self, data: list[list[object]], iid_column: int, parent_column: int, text_column: None | int = None, push_ops: bool = False, row_heights: Sequence[int] | None | False = None, open_ids: Iterator[str] | None = None, safety: bool = True, ncols: int | None = None, ) -> Sheet: self.reset(cell_options=False, column_widths=False, header=False, redraw=False) if text_column is None: text_column = iid_column tally_of_ids = defaultdict(lambda: -1) if not isinstance(ncols, int): ncols = max(map(len, data), default=0) for rn, row in enumerate(data): if safety and ncols > (lnr := len(row)): row += self.MT.get_empty_row_seq(rn, end=ncols, start=lnr) iid, pid = row[iid_column].lower(), row[parent_column].lower() if safety: if not iid: continue tally_of_ids[iid] += 1 if tally_of_ids[iid] > 0: x = 1 while iid in tally_of_ids: new = f"{row[iid_column]}_DUPLICATED_{x}" iid = new.lower() x += 1 tally_of_ids[iid] += 1 row[iid_column] = new if iid in self.RI.tree: self.RI.tree[iid].text = row[text_column] else: self.RI.tree[iid] = Node(row[text_column], iid, "") if safety and (iid == pid or self.RI.build_pid_causes_recursive_loop(iid, pid)): row[parent_column] = "" pid = "" if pid: if pid not in self.RI.tree: self.RI.tree[pid] = Node(row[text_column], pid) self.RI.tree[iid].parent = self.RI.tree[pid] self.RI.tree[pid].children.append(self.RI.tree[iid]) else: self.RI.tree[iid].parent = "" self.RI.tree_rns[iid] = rn for n in self.RI.tree.values(): if n.parent is None: n.parent = "" newrow = self.MT.get_empty_row_seq(len(data), ncols) newrow[iid_column] = n.iid self.RI.tree_rns[n.iid] = len(data) data.append(newrow) self.insert_rows( rows=[[self.RI.tree[iid]] + data[self.RI.tree_rns[iid]] for iid in self.get_children()], idx=0, heights={} if row_heights is False else row_heights, row_index=True, create_selections=False, fill=False, push_ops=push_ops, redraw=False, ) self.MT.all_rows_displayed = False self.MT.displayed_rows = list(range(len(self.MT._row_index))) self.RI.tree_rns = {n.iid: i for i, n in enumerate(self.MT._row_index)} if open_ids: self.tree_set_open(open_ids=open_ids) else: self.hide_rows( {self.RI.tree_rns[iid] for iid in self.get_children() if self.RI.tree[iid].parent}, deselect_all=False, data_indexes=True, row_heights=False if row_heights is False else True, ) return self def tree_reset(self) -> Sheet: self.deselect() self.RI.tree_reset() return self def tree_get_open(self) -> set[str]: """ Returns the set[str] of iids that are open in the treeview """ return self.RI.tree_open_ids def tree_set_open(self, open_ids: Iterator[str]) -> Sheet: """ Accepts set[str] of iids that are open in the treeview Closes everything else """ self.hide_rows( rows={rn for rn in self.MT.displayed_rows if self.MT._row_index[rn].parent}, redraw=False, deselect_all=False, data_indexes=True, ) open_ids = set(filter(self.exists, map(str.lower, open_ids))) self.RI.tree_open_ids = set() if open_ids: self.show_rows( rows=self._tree_open(open_ids), redraw=False, deselect_all=False, ) return self.set_refresh_timer() def _tree_open(self, items: set[str]) -> list[int]: """ Only meant for internal use """ to_open = [] disp_set = set(self.MT.displayed_rows) tree = self.RI.tree rns = self.RI.tree_rns open_ids = self.RI.tree_open_ids descendants = self.RI.get_iid_descendants for item in filter(items.__contains__, self.get_children()): if tree[item].children: open_ids.add(item) if rns[item] in disp_set: to_disp = [rns[did] for did in descendants(item, check_open=True)] for i in to_disp: disp_set.add(i) to_open.extend(to_disp) return to_open def tree_open(self, *items, redraw: bool = True) -> Sheet: """ If used without args all items are opened """ if items := set(unpack(items)): to_open = self._tree_open(items) else: to_open = self._tree_open(set(self.get_children())) return self.show_rows( rows=to_open, redraw=redraw, deselect_all=False, ) def _tree_close(self, items: Iterator[str]) -> list[int]: """ Only meant for internal use """ to_close = set() disp_set = set(self.MT.displayed_rows) quick_rns = self.RI.tree_rns quick_open_ids = self.RI.tree_open_ids for item in items: if self.RI.tree[item].children: quick_open_ids.discard(item) if quick_rns[item] in disp_set: for did in self.RI.get_iid_descendants(item, check_open=True): to_close.add(quick_rns[did]) return to_close def tree_close(self, *items, redraw: bool = True) -> Sheet: """ If used without args all items are closed """ if items: to_close = self._tree_close(unpack(items)) else: to_close = self._tree_close(self.get_children()) return self.hide_rows( rows=to_close, redraw=redraw, deselect_all=False, data_indexes=True, ) def insert( self, parent: str = "", index: None | int | Literal["end"] = None, iid: None | str = None, text: None | str = None, values: None | list = None, create_selections: bool = False, ) -> str: if iid is None: i = 0 while (iid := f"{i}") in self.RI.tree: i += 1 iid, pid = iid.lower(), parent.lower() if not iid: raise ValueError("iid cannot be empty string.") if iid in self.RI.tree: raise ValueError(f"iid '{iid}' already exists.") if iid == pid: raise ValueError(f"iid '{iid}' cannot be equal to parent '{pid}'.") if pid and pid not in self.RI.tree: raise ValueError(f"parent '{parent}' does not exist.") if text is None: text = iid parent_node = self.RI.tree[pid] if parent else "" self.RI.tree[iid] = Node(text, iid, parent_node) if parent_node: if isinstance(index, int): idx = self.RI.tree_rns[pid] + index + 1 for count, cid in enumerate(self.get_children(pid)): if count >= index: break idx += sum(1 for _ in self.RI.get_iid_descendants(cid)) self.RI.tree[pid].children.insert(index, self.RI.tree[iid]) else: idx = self.RI.tree_rns[pid] + sum(1 for _ in self.RI.get_iid_descendants(pid)) + 1 self.RI.tree[pid].children.append(self.RI.tree[iid]) else: if isinstance(index, int): idx = index if index: for count, cid in enumerate(self.get_children("")): if count >= index: break idx += sum(1 for _ in self.RI.get_iid_descendants(cid)) + 1 else: idx = len(self.MT._row_index) if values is None: values = [] self.insert_rows( idx=idx, rows=[[self.RI.tree[iid]] + values], row_index=True, create_selections=create_selections, fill=False, ) self.RI.tree_rns[iid] = idx if pid and (pid not in self.RI.tree_open_ids or not self.item_displayed(pid)): self.hide_rows(idx, deselect_all=False, data_indexes=True) return iid def item( self, item: str, iid: str | None = None, text: str | None = None, values: list | None = None, open_: bool | None = None, redraw: bool = True, ) -> DotDict | Sheet: """ Modify options for item If no options are set then returns DotDict of options for item Else returns Sheet """ if not (item := item.lower()) or item not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") if isinstance(iid, str): if iid in self.RI.tree: raise ValueError(f"Cannot rename '{iid}', it already exists.") iid = iid.lower() self.RI.tree[item].iid = iid self.RI.tree[iid] = self.RI.tree.pop(item) self.RI.tree_rns[iid] = self.RI.tree_rns.pop(item) if iid in self.RI.tree_open_ids: self.RI.tree_open_ids[iid] = self.RI.tree_open_ids.pop(item) if isinstance(text, str): self.RI.tree[item].text = text if isinstance(values, list): self.set_data(self.RI.tree_rns[item], data=values) if isinstance(open_, bool): if self.RI.tree[item].children: if open_: self.RI.tree_open_ids.add(item) if self.item_displayed(item): self.show_rows( rows=map(self.RI.tree_rns.__getitem__, self.RI.get_iid_descendants(item, check_open=True)), redraw=False, deselect_all=False, ) else: self.RI.tree_open_ids.discard(item) if self.item_displayed(item): self.hide_rows( rows=set( map(self.RI.tree_rns.__getitem__, self.RI.get_iid_descendants(item, check_open=True)) ), redraw=False, deselect_all=False, data_indexes=True, ) else: self.RI.tree_open_ids.discard(item) get = not (isinstance(iid, str) or isinstance(text, str) or isinstance(values, list) or isinstance(open_, bool)) self.set_refresh_timer(redraw=not get and redraw) if get: return DotDict( text=self.RI.tree[item].text, values=self[self.RI.tree_rns[item]].options(ndim=1).data, open_=item in self.RI.tree_open_ids, ) return self def itemrow(self, item: str) -> int: try: return self.RI.tree_rns[item.lower()] except Exception: raise ValueError(f"item '{item.lower()}' does not exist.") def rowitem(self, row: int, data_index: bool = False) -> str | None: try: if data_index: return self.MT._row_index[row].iid return self.MT._row_index[self.MT.displayed_rows[row]].iid except Exception: return None def get_children(self, item: None | str = None) -> Generator[str]: if item is None: for n in self.RI.tree.values(): if not n.parent: yield n.iid for iid in self.RI.get_iid_descendants(n.iid): yield iid elif item == "": yield from (n.iid for n in self.RI.tree.values() if not n.parent) else: yield from (n.iid for n in self.RI.tree[item].children) def del_items(self, *items) -> Sheet: """ Also deletes all descendants of items """ rows_to_del = [] iids_to_del = [] for item in unpack(items): item = item.lower() if item not in self.RI.tree: continue rows_to_del.append(self.RI.tree_rns[item]) iids_to_del.append(item) for did in self.RI.get_iid_descendants(item): rows_to_del.append(self.RI.tree_rns[did]) iids_to_del.append(did) for item in unpack(items): self.RI.remove_node_from_parents_children(self.RI.tree[item]) self.del_rows(rows_to_del) for iid in iids_to_del: self.RI.tree_open_ids.discard(iid) if self.RI.tree[iid].parent and len(self.RI.tree[iid].parent.children) == 1: self.RI.tree_open_ids.discard(self.RI.tree[iid].parent.iid) del self.RI.tree[iid] return self.set_refresh_timer() def set_children(self, parent: str, *newchildren) -> Sheet: """ Moves everything in '*newchildren' under 'parent' """ for iid in unpack(newchildren): self.move(iid, parent) return self def top_index_row(self, index: int) -> int: try: return next(self.RI.tree_rns[n.iid] for i, n in enumerate(self.RI.gen_top_nodes()) if i == index) except Exception: return None def move(self, item: str, parent: str, index: int | None = None) -> Sheet: """ Moves item to be under parent as child at index 'parent' can be an empty str which will put the item at top level Performance is not great """ if (item := item.lower()) and item not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") if (parent := parent.lower()) and parent not in self.RI.tree: raise ValueError(f"Parent '{parent}' does not exist.") mapping = {} to_show = [] item_node = self.RI.tree[item] item_r = self.RI.tree_rns[item] if parent: if self.RI.move_pid_causes_recursive_loop(item, parent): raise ValueError(f"iid '{item}' causes a recursive loop with parent '{parent}'.") parent_node = self.RI.tree[parent] if parent_node.children: if index is None or index >= len(parent_node.children): index = len(parent_node.children) new_r = self.RI.tree_rns[parent] + sum(1 for _ in self.RI.get_iid_descendants(parent)) # new parent has children # index is on end # item row is less than move to row if item_r < new_r: r_ctr = new_r - sum(1 for _ in self.RI.get_iid_descendants(item)) # new parent has children # index is on end # item row is greater than move to row else: r_ctr = new_r + 1 else: new_r = self.RI.tree_rns[parent_node.children[index].iid] # new parent has children # index is not end # item row is less than move to row if item_r < new_r: if self.RI.items_parent(item) == parent: r_ctr = ( new_r + sum(1 for _ in self.RI.get_iid_descendants(parent_node.children[index].iid)) - sum(1 for _ in self.RI.get_iid_descendants(item)) ) else: r_ctr = new_r - sum(1 for _ in self.RI.get_iid_descendants(item)) - 1 # new parent has children # index is not end # item row is greater than move to row else: r_ctr = new_r else: index = 0 new_r = self.RI.tree_rns[parent_node.iid] # new parent doesn't have children # index always start # item row is less than move to row if item_r < new_r: r_ctr = new_r - sum(1 for _ in self.RI.get_iid_descendants(item)) # new parent doesn't have children # index always start # item row is greater than move to row else: r_ctr = new_r + 1 mapping[item_r] = r_ctr if parent in self.RI.tree_open_ids and self.item_displayed(parent): to_show.append(r_ctr) r_ctr += 1 for did in self.RI.get_iid_descendants(item): mapping[self.RI.tree_rns[did]] = r_ctr if to_show and self.RI.ancestors_all_open(did, item_node.parent): to_show.append(r_ctr) r_ctr += 1 if parent == self.RI.items_parent(item): pop_index = parent_node.children.index(item_node) parent_node.children.insert(index, parent_node.children.pop(pop_index)) else: self.RI.remove_node_from_parents_children(item_node) item_node.parent = parent_node parent_node.children.insert(index, item_node) else: if index is None: new_r = self.top_index_row((sum(1 for _ in self.RI.gen_top_nodes()) - 1)) else: if (new_r := self.top_index_row(index)) is None: new_r = self.top_index_row((sum(1 for _ in self.RI.gen_top_nodes()) - 1)) if item_r < new_r: r_ctr = ( new_r + sum(1 for _ in self.RI.get_iid_descendants(self.rowitem(new_r, data_index=True))) - sum(1 for _ in self.RI.get_iid_descendants(item)) ) else: r_ctr = new_r mapping[item_r] = r_ctr to_show.append(r_ctr) r_ctr += 1 for did in self.RI.get_iid_descendants(item): mapping[self.RI.tree_rns[did]] = r_ctr if to_show and self.RI.ancestors_all_open(did, item_node.parent): to_show.append(r_ctr) r_ctr += 1 self.RI.remove_node_from_parents_children(item_node) self.RI.tree[item].parent = "" self.mapping_move_rows( data_new_idxs=mapping, data_indexes=True, create_selections=False, redraw=False, ) if parent and (parent not in self.RI.tree_open_ids or not self.item_displayed(parent)): self.hide_rows(set(mapping.values()), data_indexes=True) self.show_rows(to_show) return self.set_refresh_timer() reattach = move def exists(self, item: str) -> bool: return item.lower() in self.RI.tree def parent(self, item: str) -> str: if (item := item.lower()) not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") return self.RI.tree[item].parent.iid if self.RI.tree[item].parent else self.RI.tree[item].parent def index(self, item: str) -> int: if (item := item.lower()) not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") if not self.RI.tree[item].parent: find_node = self.RI.tree[item] return next(index for index, node in enumerate(self.RI.gen_top_nodes()) if node == find_node) return self.RI.tree[item].parent.children.index(self.RI.tree[item]) def item_displayed(self, item: str) -> bool: """ Check if an item (row) is currently displayed on the sheet - Does not check if the item is visible to the user """ if (item := item.lower()) not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") return self.RI.tree_rns[item] in self.MT.displayed_rows def display_item(self, item: str, redraw=False) -> Sheet: """ Ensure that item is displayed in the tree - Opens all of an item's ancestors - Unlike the ttk treeview 'see' function this function does **NOT** scroll to the item """ if (item := item.lower()) not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") if self.RI.tree[item].parent: self.show_rows( rows=self._tree_open(list(self.RI.get_iid_ancestors(item))), redraw=False, deselect_all=False, ) return self.set_refresh_timer(redraw) def scroll_to_item(self, item: str, redraw=False) -> Sheet: """ Scrolls to an item and ensures that it is displayed """ if (item := item.lower()) not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") self.display_item(item, redraw=False) self.see( row=bisect_left(self.MT.displayed_rows, self.RI.tree_rns[item]), keep_xscroll=True, redraw=False, ) return self.set_refresh_timer(redraw) def selection(self, cells: bool = False) -> list[str]: """ Get currently selected item ids """ return [ self.MT._row_index[self.MT.displayed_rows[rn]].iid for rn in self.get_selected_rows(get_cells_as_rows=cells) ] def selection_set(self, *items, redraw: bool = True) -> Sheet: if any(item.lower() in self.RI.tree for item in unpack(items)): boxes_to_hide = tuple(self.MT.selection_boxes) self.selection_add(*items, redraw=False) for iid in boxes_to_hide: self.MT.hide_selection_box(iid) return self.set_refresh_timer(redraw) def selection_add(self, *items, redraw: bool = True) -> Sheet: to_open = [] quick_displayed_check = set(self.MT.displayed_rows) for item in unpack(items): if self.RI.tree_rns[(item := item.lower())] not in quick_displayed_check and self.RI.tree[item].parent: to_open.extend(list(self.RI.get_iid_ancestors(item))) if to_open: self.show_rows( rows=self._tree_open(to_open), redraw=False, deselect_all=False, ) for startr, endr in consecutive_ranges( sorted( bisect_left( self.MT.displayed_rows, self.RI.tree_rns[item.lower()], ) for item in unpack(items) ) ): self.MT.create_selection_box( startr, 0, endr, len(self.MT.col_positions) - 1, "rows", set_current=True, ext=True, ) self.MT.run_selection_binding("rows") return self.set_refresh_timer(redraw) def selection_remove(self, *items, redraw: bool = True) -> Sheet: for item in unpack(items): if (item := item.lower()) not in self.RI.tree: continue try: self.deselect(bisect_left(self.MT.displayed_rows, self.RI.tree_rns[item]), redraw=False) except Exception: continue return self.set_refresh_timer(redraw) def selection_toggle(self, *items, redraw: bool = True) -> Sheet: selected = set(self.MT._row_index[self.displayed_row_to_data(rn)].iid for rn in self.get_selected_rows()) add = [] remove = [] for item in unpack(items): if (item := item.lower()) in self.RI.tree: if item in selected: remove.append(item) else: add.append(item) self.selection_remove(*remove, redraw=False) self.selection_add(*add, redraw=False) return self.set_refresh_timer(redraw) # Functions not in docs def event_generate(self, *args, **kwargs) -> None: self.MT.event_generate(*args, **kwargs) def emit_event( self, event: str, data: EventDataDict | None = None, ) -> None: if data is None: data = EventDataDict() data["sheetname"] = self.name self.last_event_data = data for func in self.bound_events[event]: func(data) def set_refresh_timer(self, redraw: bool = True) -> Sheet: if redraw and self.after_redraw_id is None: self.after_redraw_id = self.after(self.after_redraw_time_ms, self.after_redraw) return self def after_redraw(self) -> None: self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) self.after_redraw_id = None def del_options_using_span( self, span: Span, key: str, redraw: bool = True, ) -> Span: rows, cols = self.ranges_from_span(span) table, index, header = span.table, span.index, span.header # index header if header and span.kind in ("cell", "column"): self.CH.hide_dropdown_window() del_from_options(self.CH.cell_options, key, cols) if index and span.kind in ("cell", "row"): self.RI.hide_dropdown_window() del_from_options(self.RI.cell_options, key, rows) # table if table and span.kind == "cell": del_from_options(self.MT.cell_options, key, product(rows, cols)) elif table and span.kind == "row": del_from_options(self.MT.row_options, key, rows) elif table and span.kind == "column": del_from_options(self.MT.col_options, key, cols) self.set_refresh_timer(redraw) return span # ########## TABLE ########## def del_cell_options_dropdown(self, datarn: int, datacn: int) -> None: self.MT.hide_dropdown_window() if (datarn, datacn) in self.MT.cell_options and "dropdown" in self.MT.cell_options[(datarn, datacn)]: del self.MT.cell_options[(datarn, datacn)]["dropdown"] def del_cell_options_checkbox(self, datarn: int, datacn: int) -> None: if (datarn, datacn) in self.MT.cell_options and "checkbox" in self.MT.cell_options[(datarn, datacn)]: del self.MT.cell_options[(datarn, datacn)]["checkbox"] def del_cell_options_dropdown_and_checkbox(self, datarn: int, datacn: int) -> None: self.del_cell_options_dropdown(datarn, datacn) self.del_cell_options_checkbox(datarn, datacn) def del_row_options_dropdown(self, datarn: int) -> None: self.MT.hide_dropdown_window() if datarn in self.MT.row_options and "dropdown" in self.MT.row_options[datarn]: del self.MT.row_options[datarn]["dropdown"] def del_row_options_checkbox(self, datarn: int) -> None: if datarn in self.MT.row_options and "checkbox" in self.MT.row_options[datarn]: del self.MT.row_options[datarn]["checkbox"] def del_row_options_dropdown_and_checkbox(self, datarn: int) -> None: self.del_row_options_dropdown(datarn) self.del_row_options_checkbox(datarn) def del_column_options_dropdown(self, datacn: int) -> None: self.MT.hide_dropdown_window() if datacn in self.MT.col_options and "dropdown" in self.MT.col_options[datacn]: del self.MT.col_options[datacn]["dropdown"] def del_column_options_checkbox(self, datacn: int) -> None: if datacn in self.MT.col_options and "checkbox" in self.MT.col_options[datacn]: del self.MT.col_options[datacn]["checkbox"] def del_column_options_dropdown_and_checkbox(self, datacn: int) -> None: self.del_column_options_dropdown(datacn) self.del_column_options_checkbox(datacn) # ########## INDEX ########## def del_index_cell_options_dropdown(self, datarn: int) -> None: self.RI.hide_dropdown_window() if datarn in self.RI.cell_options and "dropdown" in self.RI.cell_options[datarn]: del self.RI.cell_options[datarn]["dropdown"] def del_index_cell_options_checkbox(self, datarn: int) -> None: if datarn in self.RI.cell_options and "checkbox" in self.RI.cell_options[datarn]: del self.RI.cell_options[datarn]["checkbox"] def del_index_cell_options_dropdown_and_checkbox(self, datarn: int) -> None: self.del_index_cell_options_dropdown(datarn) self.del_index_cell_options_checkbox(datarn) # ########## HEADER ########## def del_header_cell_options_dropdown(self, datacn: int) -> None: self.CH.hide_dropdown_window() if datacn in self.CH.cell_options and "dropdown" in self.CH.cell_options[datacn]: del self.CH.cell_options[datacn]["dropdown"] def del_header_cell_options_checkbox(self, datacn: int) -> None: if datacn in self.CH.cell_options and "checkbox" in self.CH.cell_options[datacn]: del self.CH.cell_options[datacn]["checkbox"] def del_header_cell_options_dropdown_and_checkbox(self, datacn: int) -> None: self.del_header_cell_options_dropdown(datacn) self.del_header_cell_options_checkbox(datacn) # ########## OLD FUNCTIONS ########## def get_cell_data(self, r: int, c: int, get_displayed: bool = False) -> object: return self.MT.get_cell_data(r, c, get_displayed) def get_row_data( self, r: int, get_displayed: bool = False, get_index: bool = False, get_index_displayed: bool = True, only_columns: int | Iterator[int] | None = None, ) -> list[object]: if only_columns is not None: if isinstance(only_columns, int): only_columns = (only_columns,) elif not is_iterable(only_columns): raise ValueError(tksheet_type_error("only_columns", ["int", "iterable", "None"], only_columns)) if r >= self.MT.total_data_rows(): raise IndexError(f"Row #{r} is out of range.") if r >= len(self.MT.data): total_data_cols = self.MT.total_data_cols() self.MT.fix_data_len(r, total_data_cols - 1) iterable = only_columns if only_columns is not None else range(len(self.MT.data[r])) if get_index: return [self.get_index_data(r, get_displayed=get_index_displayed)] + [ self.MT.get_cell_data(r, c, get_displayed=get_displayed) for c in iterable ] else: return [self.MT.get_cell_data(r, c, get_displayed=get_displayed) for c in iterable] def get_column_data( self, c: int, get_displayed: bool = False, get_header: bool = False, get_header_displayed: bool = True, only_rows: int | Iterator[int] | None = None, ) -> list[object]: if only_rows is not None: if isinstance(only_rows, int): only_rows = (only_rows,) elif not is_iterable(only_rows): raise ValueError(tksheet_type_error("only_rows", ["int", "iterable", "None"], only_rows)) iterable = only_rows if only_rows is not None else range(len(self.MT.data)) return ([self.get_header_data(c, get_displayed=get_header_displayed)] if get_header else []) + [ self.MT.get_cell_data(r, c, get_displayed=get_displayed) for r in iterable ] def get_sheet_data( self, get_displayed: bool = False, get_header: bool = False, get_index: bool = False, get_header_displayed: bool = True, get_index_displayed: bool = True, only_rows: Iterator[int] | int | None = None, only_columns: Iterator[int] | int | None = None, ) -> list[object]: if only_rows is not None: if isinstance(only_rows, int): only_rows = (only_rows,) elif not is_iterable(only_rows): raise ValueError(tksheet_type_error("only_rows", ["int", "iterable", "None"], only_rows)) if only_columns is not None: if isinstance(only_columns, int): only_columns = (only_columns,) elif not is_iterable(only_columns): raise ValueError(tksheet_type_error("only_columns", ["int", "iterable", "None"], only_columns)) if get_header: maxlen = len(self.MT._headers) if isinstance(self.MT._headers, (list, tuple)) else 0 data = [] for rn in only_rows if only_rows is not None else range(len(self.MT.data)): r = self.get_row_data(rn, get_displayed=get_displayed, only_columns=only_columns) if len(r) > maxlen: maxlen = len(r) if get_index: data.append([self.get_index_data(rn, get_displayed=get_index_displayed)] + r) else: data.append(r) iterable = only_columns if only_columns is not None else range(maxlen) if get_index: return [[""] + [self.get_header_data(cn, get_displayed=get_header_displayed) for cn in iterable]] + data else: return [[self.get_header_data(cn, get_displayed=get_header_displayed) for cn in iterable]] + data elif not get_header: iterable = only_rows if only_rows is not None else range(len(self.MT.data)) return [ self.get_row_data( rn, get_displayed=get_displayed, get_index=get_index, get_index_displayed=get_index_displayed, only_columns=only_columns, ) for rn in iterable ] def yield_sheet_rows( self, get_displayed: bool = False, get_header: bool = False, get_index: bool = False, get_index_displayed: bool = True, get_header_displayed: bool = True, only_rows: int | Iterator[int] | None = None, only_columns: int | Iterator[int] | None = None, ) -> Iterator[list[object]]: if only_rows is not None: if isinstance(only_rows, int): only_rows = (only_rows,) elif not is_iterable(only_rows): raise ValueError(tksheet_type_error("only_rows", ["int", "iterable", "None"], only_rows)) if only_columns is not None: if isinstance(only_columns, int): only_columns = (only_columns,) elif not is_iterable(only_columns): raise ValueError(tksheet_type_error("only_columns", ["int", "iterable", "None"], only_columns)) if get_header: iterable = only_columns if only_columns is not None else range(self.MT.total_data_cols()) yield ([""] if get_index else []) + [ self.get_header_data(c, get_displayed=get_header_displayed) for c in iterable ] iterable = only_rows if only_rows is not None else range(len(self.MT.data)) yield from ( self.get_row_data( r, get_displayed=get_displayed, get_index=get_index, get_index_displayed=get_index_displayed, only_columns=only_columns, ) for r in iterable ) def get_header_data(self, c: int, get_displayed: bool = False): return self.CH.get_cell_data(datacn=c, get_displayed=get_displayed) def get_index_data(self, r: int, get_displayed: bool = False): return self.RI.get_cell_data(datarn=r, get_displayed=get_displayed) def data_reference( self, newdataref=None, reset_col_positions: bool = True, reset_row_positions: bool = True, redraw: bool = True, ) -> object: self.set_refresh_timer(redraw) return self.MT.data_reference(newdataref, reset_col_positions, reset_row_positions) def set_cell_data( self, r: int, c: int, value: object = "", redraw: bool = True, keep_formatting: bool = True, ) -> Sheet: if not keep_formatting: self.MT.delete_cell_format(r, c, clear_values=False) self.MT.set_cell_data(r, c, value) return self.set_refresh_timer(redraw) def set_row_data( self, r: int, values=tuple(), add_columns: bool = True, redraw: bool = True, keep_formatting: bool = True, ) -> Sheet: if r >= len(self.MT.data): raise Exception("Row number is out of range") if not keep_formatting: self.MT.delete_row_format(r, clear_values=False) maxidx = len(self.MT.data[r]) - 1 if not values: self.MT.data[r] = self.MT.get_empty_row_seq(r, len(self.MT.data[r])) else: if add_columns: for c, v in enumerate(values): if c > maxidx: self.MT.data[r].append(v) if self.MT.all_columns_displayed: self.MT.insert_col_position("end") else: self.set_cell_data(r=r, c=c, value=v, redraw=False, keep_formatting=keep_formatting) else: for c, v in enumerate(values): if c > maxidx: self.MT.data[r].append(v) else: self.set_cell_data(r=r, c=c, value=v, redraw=False, keep_formatting=keep_formatting) return self.set_refresh_timer(redraw) def set_column_data( self, c: int, values=tuple(), add_rows: bool = True, redraw: bool = True, keep_formatting: bool = True, ) -> Sheet: if not keep_formatting: self.MT.delete_column_format(c, clear_values=False) if add_rows: maxidx = len(self.MT.data) - 1 total_cols = None height = self.MT.get_default_row_height() for rn, v in enumerate(values): if rn > maxidx: if total_cols is None: total_cols = self.MT.total_data_cols() self.MT.fix_data_len(rn, total_cols - 1) if self.MT.all_rows_displayed: self.MT.insert_row_position("end", height=height) maxidx += 1 if c >= len(self.MT.data[rn]): self.MT.fix_row_len(rn, c) self.set_cell_data(r=rn, c=c, value=v, redraw=False, keep_formatting=keep_formatting) else: for rn, v in enumerate(values): if c >= len(self.MT.data[rn]): self.MT.fix_row_len(rn, c) self.set_cell_data(r=rn, c=c, value=v, redraw=False, keep_formatting=keep_formatting) return self.set_refresh_timer(redraw) def readonly_rows( self, rows: list | int = [], readonly: bool = True, redraw: bool = False, ) -> Sheet: if isinstance(rows, int): rows = [rows] else: rows = rows if not readonly: for r in rows: if r in self.MT.row_options and "readonly" in self.MT.row_options[r]: del self.MT.row_options[r]["readonly"] else: for r in rows: if r not in self.MT.row_options: self.MT.row_options[r] = {} self.MT.row_options[r]["readonly"] = True return self.set_refresh_timer(redraw) def readonly_columns( self, columns: list | int = [], readonly: bool = True, redraw: bool = False, ) -> Sheet: if isinstance(columns, int): columns = [columns] else: columns = columns if not readonly: for c in columns: if c in self.MT.col_options and "readonly" in self.MT.col_options[c]: del self.MT.col_options[c]["readonly"] else: for c in columns: if c not in self.MT.col_options: self.MT.col_options[c] = {} self.MT.col_options[c]["readonly"] = True return self.set_refresh_timer(redraw) def readonly_cells( self, row: int = 0, column: int = 0, cells: list = [], readonly: bool = True, redraw: bool = False, ) -> Sheet: if not readonly: if cells: for r, c in cells: if (r, c) in self.MT.cell_options and "readonly" in self.MT.cell_options[(r, c)]: del self.MT.cell_options[(r, c)]["readonly"] else: if ( row, column, ) in self.MT.cell_options and "readonly" in self.MT.cell_options[(row, column)]: del self.MT.cell_options[(row, column)]["readonly"] else: if cells: for r, c in cells: if (r, c) not in self.MT.cell_options: self.MT.cell_options[(r, c)] = {} self.MT.cell_options[(r, c)]["readonly"] = True else: if (row, column) not in self.MT.cell_options: self.MT.cell_options[(row, column)] = {} self.MT.cell_options[(row, column)]["readonly"] = True return self.set_refresh_timer(redraw) def readonly_header( self, columns: list = [], readonly: bool = True, redraw: bool = False, ) -> Sheet: self.CH.readonly_header(columns=columns, readonly=readonly) return self.set_refresh_timer(redraw) def readonly_index( self, rows: list = [], readonly: bool = True, redraw: bool = False, ) -> Sheet: self.RI.readonly_index(rows=rows, readonly=readonly) return self.set_refresh_timer(redraw) def dehighlight_rows( self, rows: list[int] | Literal["all"] = [], redraw: bool = True, ) -> Sheet: if isinstance(rows, int): rows = [rows] if not rows or rows == "all": for r in self.MT.row_options: if "highlight" in self.MT.row_options[r]: del self.MT.row_options[r]["highlight"] for r in self.RI.cell_options: if "highlight" in self.RI.cell_options[r]: del self.RI.cell_options[r]["highlight"] else: for r in rows: try: del self.MT.row_options[r]["highlight"] except Exception: pass try: del self.RI.cell_options[r]["highlight"] except Exception: pass return self.set_refresh_timer(redraw) def dehighlight_columns( self, columns: list[int] | Literal["all"] = [], redraw: bool = True, ) -> Sheet: if isinstance(columns, int): columns = [columns] if not columns or columns == "all": for c in self.MT.col_options: if "highlight" in self.MT.col_options[c]: del self.MT.col_options[c]["highlight"] for c in self.CH.cell_options: if "highlight" in self.CH.cell_options[c]: del self.CH.cell_options[c]["highlight"] else: for c in columns: try: del self.MT.col_options[c]["highlight"] except Exception: pass try: del self.CH.cell_options[c]["highlight"] except Exception: pass return self.set_refresh_timer(redraw) def highlight_rows( self, rows: Iterator[int] | int, bg: None | str = None, fg: None | str = None, highlight_index: bool = True, redraw: bool = True, end_of_screen: bool = False, overwrite: bool = True, ) -> Sheet: if bg is None and fg is None: return for r in (rows,) if isinstance(rows, int) else rows: add_highlight(self.MT.row_options, r, bg, fg, end_of_screen, overwrite) if highlight_index: self.highlight_cells(cells=rows, canvas="index", bg=bg, fg=fg, redraw=False) return self.set_refresh_timer(redraw) def highlight_columns( self, columns: Iterator[int] | int, bg: bool | None | str = False, fg: bool | None | str = False, highlight_header: bool = True, redraw: bool = True, overwrite: bool = True, ) -> Sheet: if bg is False and fg is False: return for c in (columns,) if isinstance(columns, int) else columns: add_highlight(self.MT.col_options, c, bg, fg, None, overwrite) if highlight_header: self.highlight_cells(cells=columns, canvas="header", bg=bg, fg=fg, redraw=False) return self.set_refresh_timer(redraw) def highlight_cells( self, row: int | Literal["all"] = 0, column: int | Literal["all"] = 0, cells: list[tuple[int, int]] = [], canvas: Literal["table", "index", "header"] = "table", bg: bool | None | str = False, fg: bool | None | str = False, redraw: bool = True, overwrite: bool = True, ) -> Sheet: if bg is False and fg is False: return if canvas == "table": if cells: for r_, c_ in cells: add_highlight(self.MT.cell_options, (r_, c_), bg, fg, None, overwrite) else: if ( isinstance(row, str) and row.lower() == "all" and isinstance(column, str) and column.lower() == "all" ): totalrows, totalcols = self.MT.total_data_rows(), self.MT.total_data_cols() for r_ in range(totalrows): for c_ in range(totalcols): add_highlight(self.MT.cell_options, (r_, c_), bg, fg, None, overwrite) elif isinstance(row, str) and row.lower() == "all" and isinstance(column, int): for r_ in range(self.MT.total_data_rows()): add_highlight(self.MT.cell_options, (r_, column), bg, fg, None, overwrite) elif isinstance(column, str) and column.lower() == "all" and isinstance(row, int): for c_ in range(self.MT.total_data_cols()): add_highlight(self.MT.cell_options, (row, c_), bg, fg, None, overwrite) elif isinstance(row, int) and isinstance(column, int): add_highlight(self.MT.cell_options, (row, column), bg, fg, None, overwrite) elif canvas in ("row_index", "index"): if isinstance(cells, int): add_highlight(self.RI.cell_options, cells, bg, fg, None, overwrite) elif cells: for r_ in cells: add_highlight(self.RI.cell_options, r_, bg, fg, None, overwrite) else: add_highlight(self.RI.cell_options, row, bg, fg, None, overwrite) elif canvas == "header": if isinstance(cells, int): add_highlight(self.CH.cell_options, cells, bg, fg, None, overwrite) elif cells: for c_ in cells: add_highlight(self.CH.cell_options, c_, bg, fg, None, overwrite) else: add_highlight(self.CH.cell_options, column, bg, fg, None, overwrite) return self.set_refresh_timer(redraw) def dehighlight_cells( self, row: int | Literal["all"] = 0, column: int = 0, cells: list[tuple[int, int]] = [], canvas: Literal["table", "row_index", "header"] = "table", all_: bool = False, redraw: bool = True, ) -> Sheet: if row == "all" and canvas == "table": for k, v in self.MT.cell_options.items(): if "highlight" in v: del self.MT.cell_options[k]["highlight"] elif row == "all" and canvas == "row_index": for k, v in self.RI.cell_options.items(): if "highlight" in v: del self.RI.cell_options[k]["highlight"] elif row == "all" and canvas == "header": for k, v in self.CH.cell_options.items(): if "highlight" in v: del self.CH.cell_options[k]["highlight"] if canvas == "table": if cells and not all_: for t in cells: try: del self.MT.cell_options[t]["highlight"] except Exception: continue elif not all_: if ( row, column, ) in self.MT.cell_options and "highlight" in self.MT.cell_options[(row, column)]: del self.MT.cell_options[(row, column)]["highlight"] elif all_: for k in self.MT.cell_options: if "highlight" in self.MT.cell_options[k]: del self.MT.cell_options[k]["highlight"] elif canvas == "row_index": if cells and not all_: for r in cells: try: del self.RI.cell_options[r]["highlight"] except Exception: continue elif not all_: if row in self.RI.cell_options and "highlight" in self.RI.cell_options[row]: del self.RI.cell_options[row]["highlight"] elif all_: for r in self.RI.cell_options: if "highlight" in self.RI.cell_options[r]: del self.RI.cell_options[r]["highlight"] elif canvas == "header": if cells and not all_: for c in cells: try: del self.CH.cell_options[c]["highlight"] except Exception: continue elif not all_: if column in self.CH.cell_options and "highlight" in self.CH.cell_options[column]: del self.CH.cell_options[column]["highlight"] elif all_: for c in self.CH.cell_options: if "highlight" in self.CH.cell_options[c]: del self.CH.cell_options[c]["highlight"] return self.set_refresh_timer(redraw) def get_highlighted_cells(self, canvas: str = "table") -> dict | None: if canvas == "table": return {k: v["highlight"] for k, v in self.MT.cell_options.items() if "highlight" in v} elif canvas == "row_index": return {k: v["highlight"] for k, v in self.RI.cell_options.items() if "highlight" in v} elif canvas == "header": return {k: v["highlight"] for k, v in self.CH.cell_options.items() if "highlight" in v} def align_cells( self, row: int = 0, column: int = 0, cells: list = [], align: str | None = "global", redraw: bool = True, ) -> Sheet: if isinstance(cells, dict): for k, v in cells.items(): set_align(self.MT.cell_options, k, convert_align(v)) elif cells: align = convert_align(align) for k in cells: set_align(self.MT.cell_options, k, align) else: set_align(self.MT.cell_options, (row, column), convert_align(align)) return self.set_refresh_timer(redraw) def align_rows( self, rows: list | dict | int = [], align: str | None = "global", align_index: bool = False, redraw: bool = True, ) -> Sheet: if isinstance(rows, dict): for k, v in rows.items(): align = convert_align(v) set_align(self.MT.row_options, k, align) if align_index: set_align(self.RI.cell_options, k, align) elif is_iterable(rows): align = convert_align(align) for k in rows: set_align(self.MT.row_options, k, align) if align_index: set_align(self.RI.cell_options, k, align) elif isinstance(rows, int): set_align(self.MT.row_options, rows, convert_align(align)) if align_index: set_align(self.RI.cell_options, rows, align) return self.set_refresh_timer(redraw) def align_columns( self, columns: list | dict | int = [], align: str | None = "global", align_header: bool = False, redraw: bool = True, ) -> Sheet: if isinstance(columns, dict): for k, v in columns.items(): align = convert_align(v) set_align(self.MT.col_options, k, align) if align_header: set_align(self.CH.cell_options, k, align) elif is_iterable(columns): align = convert_align(align) for k in columns: set_align(self.MT.col_options, k, align) if align_header: set_align(self.CH.cell_options, k, align) elif isinstance(columns, int): set_align(self.MT.col_options, columns, convert_align(align)) if align_header: set_align(self.CH.cell_options, columns, align) return self.set_refresh_timer(redraw) def align_header( self, columns: list | dict | int = [], align: str | None = "global", redraw: bool = True, ) -> Sheet: if isinstance(columns, dict): for k, v in columns.items(): set_align(self.CH.cell_options, k, convert_align(v)) elif is_iterable(columns): align = convert_align(align) for k in columns: set_align(self.CH.cell_options, k, align) elif isinstance(columns, int): set_align(self.CH.cell_options, columns, convert_align(align)) return self.set_refresh_timer(redraw) def align_index( self, rows: list | dict | int = [], align: str | None = "global", redraw: bool = True, ) -> Sheet: if isinstance(rows, dict): for k, v in rows.items(): set_align(self.RI.cell_options, k, convert_align(v)) elif is_iterable(rows): align = convert_align(align) for k in rows: set_align(self.RI.cell_options, k, align) elif isinstance(rows, int): set_align(self.RI.cell_options, rows, convert_align(align)) return self.set_refresh_timer(redraw) def get_cell_alignments(self) -> dict: return {(r, c): v["align"] for (r, c), v in self.MT.cell_options.items() if "align" in v} def get_column_alignments(self) -> dict: return {c: v["align"] for c, v in self.MT.col_options.items() if "align" in v} def get_row_alignments(self) -> dict: return {r: v["align"] for r, v in self.MT.row_options.items() if "align" in v} def create_checkbox( self, r: int | Literal["all"] = 0, c: int | Literal["all"] = 0, *args, **kwargs, ) -> None: kwargs = get_checkbox_kwargs(*args, **kwargs) v = kwargs["checked"] d = get_checkbox_dict(**kwargs) if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): for r_ in range(self.MT.total_data_rows()): self._create_checkbox(r_, c, v, d) elif isinstance(c, str) and c.lower() == "all" and isinstance(r, int): for c_ in range(self.MT.total_data_cols()): self._create_checkbox(r, c_, v, d) elif isinstance(r, str) and r.lower() == "all" and isinstance(c, str) and c.lower() == "all": totalcols = self.MT.total_data_cols() for r_ in range(self.MT.total_data_rows()): for c_ in range(totalcols): self._create_checkbox(r_, c_, v, d) elif isinstance(r, int) and isinstance(c, int): self._create_checkbox(r, c, v, d) self.set_refresh_timer(kwargs["redraw"]) def _create_checkbox(self, r: int, c: int, v: bool, d: dict) -> None: self.MT.delete_cell_format(r, c, clear_values=False) self.del_cell_options_dropdown_and_checkbox(r, c) add_to_options(self.MT.cell_options, (r, c), "checkbox", d) self.MT.set_cell_data(r, c, v) def checkbox_cell( self, r: int | Literal["all"] = 0, c: int | Literal["all"] = 0, *args, **kwargs, ) -> None: self.create_checkbox(r=r, c=c, **get_checkbox_kwargs(*args, **kwargs)) def checkbox_row(self, r: Iterator[int] | int | Literal["all"] = 0, *args, **kwargs) -> None: kwargs = get_checkbox_kwargs(*args, **kwargs) d = get_checkbox_dict(**kwargs) if isinstance(r, str) and r.lower() == "all": for r_ in range(self.MT.total_data_rows()): self._checkbox_row(r_, kwargs["checked"], d) elif isinstance(r, int): self._checkbox_row(r, kwargs["checked"], d) elif is_iterable(r): for r_ in r: self._checkbox_row(r_, kwargs["checked"], d) self.set_refresh_timer(kwargs["redraw"]) def _checkbox_row(self, r: int, v: bool, d: dict) -> None: self.MT.delete_row_format(r, clear_values=False) self.del_row_options_dropdown_and_checkbox(r) add_to_options(self.MT.row_options, r, "checkbox", d) for c in range(self.MT.total_data_cols()): self.MT.set_cell_data(r, c, v) def checkbox_column( self, c: Iterator[int] | int | Literal["all"] = 0, *args, **kwargs, ) -> None: kwargs = get_checkbox_kwargs(*args, **kwargs) d = get_checkbox_dict(**kwargs) if isinstance(c, str) and c.lower() == "all": for c in range(self.MT.total_data_cols()): self._checkbox_column(c, kwargs["checked"], d) elif isinstance(c, int): self._checkbox_column(c, kwargs["checked"], d) elif is_iterable(c): for c_ in c: self._checkbox_column(c_, kwargs["checked"], d) self.set_refresh_timer(kwargs["redraw"]) def _checkbox_column(self, c: int, v: bool, d: dict) -> None: self.MT.delete_column_format(c, clear_values=False) self.del_column_options_dropdown_and_checkbox(c) add_to_options(self.MT.col_options, c, "checkbox", d) for r in range(self.MT.total_data_rows()): self.MT.set_cell_data(r, c, v) def create_header_checkbox(self, c: Iterator[int] | int | Literal["all"] = 0, *args, **kwargs) -> None: kwargs = get_checkbox_kwargs(*args, **kwargs) d = get_checkbox_dict(**kwargs) if isinstance(c, str) and c.lower() == "all": for c_ in range(self.MT.total_data_cols()): self._create_header_checkbox(c_, kwargs["checked"], d) elif isinstance(c, int): self._create_header_checkbox(c, kwargs["checked"], d) elif is_iterable(c): for c_ in c: self._create_header_checkbox(c_, kwargs["checked"], d) self.set_refresh_timer(kwargs["redraw"]) def _create_header_checkbox(self, c: int, v: bool, d: dict) -> None: self.del_header_cell_options_dropdown_and_checkbox(c) add_to_options(self.CH.cell_options, c, "checkbox", d) self.CH.set_cell_data(c, v) def create_index_checkbox(self, r: Iterator[int] | int | Literal["all"] = 0, *args, **kwargs) -> None: kwargs = get_checkbox_kwargs(*args, **kwargs) d = get_checkbox_dict(**kwargs) if isinstance(r, str) and r.lower() == "all": for r_ in range(self.MT.total_data_rows()): self._create_index_checkbox(r_, kwargs["checked"], d) elif isinstance(r, int): self._create_index_checkbox(r, kwargs["checked"], d) elif is_iterable(r): for r_ in r: self._create_index_checkbox(r_, kwargs["checked"], d) self.set_refresh_timer(kwargs["redraw"]) def _create_index_checkbox(self, r: int, v: bool, d: dict) -> None: self.del_index_cell_options_dropdown_and_checkbox(r) add_to_options(self.RI.cell_options, r, "checkbox", d) self.RI.set_cell_data(r, v) def delete_checkbox( self, r: int | Literal["all"] = 0, c: int | Literal["all"] = 0, ) -> None: if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): for r_, c_ in self.MT.cell_options: if "checkbox" in self.MT.cell_options[(r_, c)]: self.del_cell_options_checkbox(r_, c) elif isinstance(c, str) and c.lower() == "all" and isinstance(r, int): for r_, c_ in self.MT.cell_options: if "checkbox" in self.MT.cell_options[(r, c_)]: self.del_cell_options_checkbox(r, c_) elif isinstance(r, str) and r.lower() == "all" and isinstance(c, str) and c.lower() == "all": for r_, c_ in self.MT.cell_options: if "checkbox" in self.MT.cell_options[(r_, c_)]: self.del_cell_options_checkbox(r_, c_) elif isinstance(r, int) and isinstance(c, int): self.del_cell_options_checkbox(r, c) def delete_cell_checkbox( self, r: int | Literal["all"] = 0, c: int | Literal["all"] = 0, ) -> None: self.delete_checkbox(r, c) def delete_row_checkbox(self, r: Iterator[int] | int | Literal["all"] = 0) -> None: if isinstance(r, str) and r.lower() == "all": for r_ in self.MT.row_options: self.del_table_row_options_checkbox(r_) elif isinstance(r, int): self.del_table_row_options_checkbox(r) elif is_iterable(r): for r_ in r: self.del_table_row_options_checkbox(r_) def delete_column_checkbox(self, c: Iterator[int] | int | Literal["all"] = 0) -> None: if isinstance(c, str) and c.lower() == "all": for c_ in self.MT.col_options: self.del_table_column_options_checkbox(c_) elif isinstance(c, int): self.del_table_column_options_checkbox(c) elif is_iterable(c): for c_ in c: self.del_table_column_options_checkbox(c_) def delete_header_checkbox(self, c: Iterator[int] | int | Literal["all"] = 0) -> None: if isinstance(c, str) and c.lower() == "all": for c_ in self.CH.cell_options: if "checkbox" in self.CH.cell_options[c_]: self.del_header_cell_options_checkbox(c_) if isinstance(c, int): self.del_header_cell_options_checkbox(c) elif is_iterable(c): for c_ in c: self.del_header_cell_options_checkbox(c_) def delete_index_checkbox(self, r: Iterator[int] | int | Literal["all"] = 0) -> None: if isinstance(r, str) and r.lower() == "all": for r_ in self.RI.cell_options: if "checkbox" in self.RI.cell_options[r_]: self.del_index_cell_options_checkbox(r_) if isinstance(r, int): self.del_index_cell_options_checkbox(r) elif is_iterable(r): for r_ in r: self.del_index_cell_options_checkbox(r_) def click_header_checkbox(self, c: int, checked: bool | None = None) -> Sheet: kwargs = self.CH.get_cell_kwargs(c, key="checkbox") if kwargs: if not isinstance(self.MT._headers[c], bool): if checked is None: self.MT._headers[c] = False else: self.MT._headers[c] = bool(checked) else: self.MT._headers[c] = not self.MT._headers[c] return self def click_index_checkbox(self, r: int, checked: bool | None = None) -> Sheet: kwargs = self.RI.get_cell_kwargs(r, key="checkbox") if kwargs: if not isinstance(self.MT._row_index[r], bool): if checked is None: self.MT._row_index[r] = False else: self.MT._row_index[r] = bool(checked) else: self.MT._row_index[r] = not self.MT._row_index[r] return self def get_checkboxes(self) -> dict: return { **{k: v["checkbox"] for k, v in self.MT.cell_options.items() if "checkbox" in v}, **{k: v["checkbox"] for k, v in self.MT.row_options.items() if "checkbox" in v}, **{k: v["checkbox"] for k, v in self.MT.col_options.items() if "checkbox" in v}, } def get_header_checkboxes(self) -> dict: return {k: v["checkbox"] for k, v in self.CH.cell_options.items() if "checkbox" in v} def get_index_checkboxes(self) -> dict: return {k: v["checkbox"] for k, v in self.RI.cell_options.items() if "checkbox" in v} def create_dropdown( self, r: int | Literal["all"] = 0, c: int | Literal["all"] = 0, *args, **kwargs, ) -> Sheet: kwargs = get_dropdown_kwargs(*args, **kwargs) d = get_dropdown_dict(**kwargs) v = kwargs["set_value"] if kwargs["set_value"] is not None else kwargs["values"][0] if kwargs["values"] else "" if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): for r_ in range(self.MT.total_data_rows()): self._create_dropdown(r_, c, v, d) elif isinstance(c, str) and c.lower() == "all" and isinstance(r, int): for c_ in range(self.MT.total_data_cols()): self._create_dropdown(r, c_, v, d) elif isinstance(r, str) and r.lower() == "all" and isinstance(c, str) and c.lower() == "all": totalcols = self.MT.total_data_cols() for r_ in range(self.MT.total_data_rows()): for c_ in range(totalcols): self._create_dropdown(r_, c_, v, d) elif isinstance(r, int) and isinstance(c, int): self._create_dropdown(r, c, v, d) return self.set_refresh_timer(kwargs["redraw"]) def _create_dropdown(self, r: int, c: int, v: object, d: dict) -> None: self.del_cell_options_dropdown_and_checkbox(r, c) add_to_options(self.MT.cell_options, (r, c), "dropdown", d) self.MT.set_cell_data(r, c, v) def dropdown_cell( self, r: int | Literal["all"] = 0, c: int | Literal["all"] = 0, *args, **kwargs, ) -> Sheet: return self.create_dropdown(r=r, c=c, **get_dropdown_kwargs(*args, **kwargs)) def dropdown_row( self, r: Iterator[int] | int | Literal["all"] = 0, *args, **kwargs, ) -> Sheet: kwargs = get_dropdown_kwargs(*args, **kwargs) d = get_dropdown_dict(**kwargs) v = kwargs["set_value"] if kwargs["set_value"] is not None else kwargs["values"][0] if kwargs["values"] else "" if isinstance(r, str) and r.lower() == "all": for r_ in range(self.MT.total_data_rows()): self._dropdown_row(r_, v, d) elif isinstance(r, int): self._dropdown_row(r, v, d) elif is_iterable(r): for r_ in r: self._dropdown_row(r_, v, d) return self.set_refresh_timer(kwargs["redraw"]) def _dropdown_row(self, r: int, v: object, d: dict) -> None: self.del_row_options_dropdown_and_checkbox(r) add_to_options(self.MT.row_options, r, "dropdown", d) for c in range(self.MT.total_data_cols()): self.MT.set_cell_data(r, c, v) def dropdown_column( self, c: Iterator[int] | int | Literal["all"] = 0, *args, **kwargs, ) -> Sheet: kwargs = get_dropdown_kwargs(*args, **kwargs) d = get_dropdown_dict(**kwargs) v = kwargs["set_value"] if kwargs["set_value"] is not None else kwargs["values"][0] if kwargs["values"] else "" if isinstance(c, str) and c.lower() == "all": for c_ in range(self.MT.total_data_cols()): self._dropdown_column(c_, v, d) elif isinstance(c, int): self._dropdown_column(c, v, d) elif is_iterable(c): for c_ in c: self._dropdown_column(c_, v, d) return self.set_refresh_timer(kwargs["redraw"]) def _dropdown_column(self, c: int, v: object, d: dict) -> None: self.del_column_options_dropdown_and_checkbox(c) add_to_options(self.MT.col_options, c, "dropdown", d) for r in range(self.MT.total_data_rows()): self.MT.set_cell_data(r, c, v) def create_header_dropdown( self, c: Iterator[int] | int | Literal["all"] = 0, *args, **kwargs, ) -> Sheet: kwargs = get_dropdown_kwargs(*args, **kwargs) d = get_dropdown_dict(**kwargs) v = kwargs["set_value"] if kwargs["set_value"] is not None else kwargs["values"][0] if kwargs["values"] else "" if isinstance(c, str) and c.lower() == "all": for c_ in range(self.MT.total_data_cols()): self._create_header_dropdown(c_, v, d) elif isinstance(c, int): self._create_header_dropdown(c, v, d) elif is_iterable(c): for c_ in c: self._create_header_dropdown(c_, v, d) return self.set_refresh_timer(kwargs["redraw"]) def _create_header_dropdown(self, c: int, v: object, d: dict) -> None: self.del_header_cell_options_dropdown_and_checkbox(c) add_to_options(self.CH.cell_options, c, "dropdown", d) self.CH.set_cell_data(c, v) def create_index_dropdown( self, r: Iterator[int] | int | Literal["all"] = 0, *args, **kwargs, ) -> Sheet: kwargs = get_dropdown_kwargs(*args, **kwargs) d = get_dropdown_dict(**kwargs) v = kwargs["set_value"] if kwargs["set_value"] is not None else kwargs["values"][0] if kwargs["values"] else "" if isinstance(r, str) and r.lower() == "all": for r_ in range(self.MT.total_data_rows()): self._create_index_dropdown(r_, v, d) elif isinstance(r, int): self._create_index_dropdown(r, v, d) elif is_iterable(r): for r_ in r: self._create_index_dropdown(r_, v, d) return self.set_refresh_timer(kwargs["redraw"]) def _create_index_dropdown(self, r: int, v: object, d: dict) -> None: self.del_index_cell_options_dropdown_and_checkbox(r) add_to_options(self.RI.cell_options, r, "dropdown", d) self.RI.set_cell_data(r, v) def delete_dropdown( self, r: int | Literal["all"] = 0, c: int | Literal["all"] = 0, ) -> None: if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): for r_, c_ in self.MT.cell_options: if "dropdown" in self.MT.cell_options[(r_, c)]: self.del_cell_options_dropdown(r_, c) elif isinstance(c, str) and c.lower() == "all" and isinstance(r, int): for r_, c_ in self.MT.cell_options: if "dropdown" in self.MT.cell_options[(r, c_)]: self.del_cell_options_dropdown(r, c_) elif isinstance(r, str) and r.lower() == "all" and isinstance(c, str) and c.lower() == "all": for r_, c_ in self.MT.cell_options: if "dropdown" in self.MT.cell_options[(r_, c_)]: self.del_cell_options_dropdown(r_, c_) elif isinstance(r, int) and isinstance(c, int): self.del_cell_options_dropdown(r, c) def delete_cell_dropdown( self, r: int | Literal["all"] = 0, c: int | Literal["all"] = 0, ) -> None: self.delete_dropdown(r=r, c=c) def delete_row_dropdown( self, r: Iterator[int] | int | Literal["all"] = "all", ) -> None: if isinstance(r, str) and r.lower() == "all": for r_ in self.MT.row_options: if "dropdown" in self.MT.row_options[r_]: self.del_table_row_options_dropdown(r_) elif isinstance(r, int): self.del_table_row_options_dropdown(r) elif is_iterable(r): for r_ in r: self.del_table_row_options_dropdown(r_) def delete_column_dropdown( self, c: Iterator[int] | int | Literal["all"] = "all", ) -> None: if isinstance(c, str) and c.lower() == "all": for c_ in self.MT.col_options: if "dropdown" in self.MT.col_options[c_]: self.del_column_options_dropdown(datacn=c_) elif isinstance(c, int): self.del_column_options_dropdown(datacn=c) elif is_iterable(c): for c_ in c: self.del_column_options_dropdown(datacn=c_) def delete_header_dropdown(self, c: Iterator[int] | int | Literal["all"]) -> None: if isinstance(c, str) and c.lower() == "all": for c_ in self.CH.cell_options: if "dropdown" in self.CH.cell_options[c_]: self.del_header_cell_options_dropdown(c_) elif isinstance(c, int): self.del_header_cell_options_dropdown(c) elif is_iterable(c): for c_ in c: self.del_header_cell_options_dropdown(c_) def delete_index_dropdown(self, r: Iterator[int] | int | Literal["all"]) -> None: if isinstance(r, str) and r.lower() == "all": for r_ in self.RI.cell_options: if "dropdown" in self.RI.cell_options[r_]: self.del_index_cell_options_dropdown(r_) elif isinstance(r, int): self.del_index_cell_options_dropdown(r) elif is_iterable(r): for r_ in r: self.del_index_cell_options_dropdown(r_) def get_dropdowns(self) -> dict: d = { **{k: v["dropdown"] for k, v in self.MT.cell_options.items() if "dropdown" in v}, **{k: v["dropdown"] for k, v in self.MT.row_options.items() if "dropdown" in v}, **{k: v["dropdown"] for k, v in self.MT.col_options.items() if "dropdown" in v}, } if "dropdown" in self.MT.options: return {**d, "dropdown": self.MT.options["dropdown"]} return d def get_header_dropdowns(self) -> dict: d = {k: v["dropdown"] for k, v in self.CH.cell_options.items() if "dropdown" in v} if "dropdown" in self.CH.options: return {**d, "dropdown": self.CH.options["dropdown"]} return d def get_index_dropdowns(self) -> dict: d = {k: v["dropdown"] for k, v in self.RI.cell_options.items() if "dropdown" in v} if "dropdown" in self.RI.options: return {**d, "dropdown": self.RI.options["dropdown"]} return d def set_dropdown_values( self, r: int = 0, c: int = 0, set_existing_dropdown: bool = False, values: list[object] = [], set_value: object = None, ) -> Sheet: if set_existing_dropdown: if self.MT.dropdown.open: r_, c_ = self.MT.dropdown.get_coords() else: raise Exception("No dropdown box is currently open") else: r_ = r c_ = c kwargs = self.MT.get_cell_kwargs(r, c, key="dropdown") kwargs["values"] = values if self.MT.dropdown.open: self.MT.dropdown.window.values(values) if set_value is not None: self.set_cell_data(r_, c_, set_value) if self.MT.dropdown.open: self.MT.text_editor.window.set_text(set_value) return self def set_header_dropdown_values( self, c: int = 0, set_existing_dropdown: bool = False, values: list[object] = [], set_value: object = None, ) -> Sheet: if set_existing_dropdown: if self.CH.dropdown.open: c_ = self.CH.dropdown.get_coords() else: raise Exception("No dropdown box is currently open") else: c_ = c kwargs = self.CH.get_cell_kwargs(c_, key="dropdown") if kwargs: kwargs["values"] = values if self.CH.dropdown.open: self.CH.dropdown.window.values(values) if set_value is not None: self.MT.headers(newheaders=set_value, index=c_) return self def set_index_dropdown_values( self, r: int = 0, set_existing_dropdown: bool = False, values: list[object] = [], set_value: object = None, ) -> Sheet: if set_existing_dropdown: if self.RI.current_dropdown_window is not None: r_ = self.RI.current_dropdown_window.r else: raise Exception("No dropdown box is currently open") else: r_ = r kwargs = self.RI.get_cell_kwargs(r_, key="dropdown") if kwargs: kwargs["values"] = values if self.RI.dropdown.open: self.RI.dropdown.window.values(values) if set_value is not None: self.MT.row_index(newindex=set_value, index=r_) # here return self def get_dropdown_values(self, r: int = 0, c: int = 0) -> None | list: kwargs = self.MT.get_cell_kwargs(r, c, key="dropdown") if kwargs: return kwargs["values"] def get_header_dropdown_values(self, c: int = 0) -> None | list: kwargs = self.CH.get_cell_kwargs(c, key="dropdown") if kwargs: return kwargs["values"] def get_index_dropdown_values(self, r: int = 0) -> None | list: kwargs = self.RI.get_cell_kwargs(r, key="dropdown") if kwargs: return kwargs["values"] def dropdown_functions( self, r: int, c: int, selection_function: Literal[""] | Callable | None = "", modified_function: Literal[""] | Callable | None = "", ) -> None | dict: kwargs = self.MT.get_cell_kwargs(r, c, key="dropdown") if kwargs: if selection_function != "": kwargs["select_function"] = selection_function if modified_function != "": kwargs["modified_function"] = modified_function return kwargs def header_dropdown_functions( self, c: int, selection_function: Literal[""] | Callable | None = "", modified_function: Literal[""] | Callable | None = "", ) -> None | dict: kwargs = self.CH.get_cell_kwargs(c, key="dropdown") if selection_function != "": kwargs["selection_function"] = selection_function if modified_function != "": kwargs["modified_function"] = modified_function return kwargs def index_dropdown_functions( self, r: int, selection_function: Literal[""] | Callable | None = "", modified_function: Literal[""] | Callable | None = "", ) -> None | dict: kwargs = self.RI.get_cell_kwargs(r, key="dropdown") if selection_function != "": kwargs["select_function"] = selection_function if modified_function != "": kwargs["modified_function"] = modified_function return kwargs def get_dropdown_value(self, r: int = 0, c: int = 0) -> object: if self.MT.get_cell_kwargs(r, c, key="dropdown"): return self.get_cell_data(r, c) def get_header_dropdown_value(self, c: int = 0) -> object: if self.CH.get_cell_kwargs(c, key="dropdown"): return self.MT._headers[c] def get_index_dropdown_value(self, r: int = 0) -> object: if self.RI.get_cell_kwargs(r, key="dropdown"): return self.MT._row_index[r] def delete_all_formatting(self, clear_values: bool = False) -> None: self.MT.delete_all_formatting(clear_values=clear_values) def format_cell( self, r: int | Literal["all"], c: int | Literal["all"], formatter_options: dict = {}, formatter_class: object = None, redraw: bool = True, **kwargs, ) -> Sheet: kwargs = fix_format_kwargs({"formatter": formatter_class, **formatter_options, **kwargs}) if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): for r_ in range(self.MT.total_data_rows()): self._format_cell(r_, c, kwargs) elif isinstance(c, str) and c.lower() == "all" and isinstance(r, int): for c_ in range(self.MT.total_data_cols()): self._format_cell(r, c_, kwargs) elif isinstance(r, str) and r.lower() == "all" and isinstance(c, str) and c.lower() == "all": for r_ in range(self.MT.total_data_rows()): for c_ in range(self.MT.total_data_cols()): self._format_cell(r_, c_, kwargs) else: self._format_cell(r, c, kwargs) return self.set_refresh_timer(redraw) def _format_cell(self, r: int, c: int, d: dict) -> None: v = d["value"] if "value" in d else self.MT.get_cell_data(r, c) self.del_cell_options_checkbox(r, c) add_to_options(self.MT.cell_options, (r, c), "format", d) self.MT.set_cell_data(r, c, value=v, kwargs=d) def delete_cell_format( self, r: Literal["all"] | int = "all", c: Literal["all"] | int = "all", clear_values: bool = False, ) -> Sheet: if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): for r_, c_ in self.MT.cell_options: if "format" in self.MT.cell_options[(r_, c)]: self.MT.delete_cell_format(r_, c, clear_values=clear_values) elif isinstance(c, str) and c.lower() == "all" and isinstance(r, int): for r_, c_ in self.MT.cell_options: if "format" in self.MT.cell_options[(r, c_)]: self.MT.delete_cell_format(r, c_, clear_values=clear_values) elif isinstance(r, str) and r.lower() == "all" and isinstance(c, str) and c.lower() == "all": for r_, c_ in self.MT.cell_options: if "format" in self.MT.cell_options[(r_, c_)]: self.MT.delete_cell_format(r_, c_, clear_values=clear_values) else: self.MT.delete_cell_format(r, c, clear_values=clear_values) return self def format_row( self, r: Iterator[int] | int | Literal["all"], formatter_options: dict = {}, formatter_class: object = None, redraw: bool = True, **kwargs, ) -> Sheet: kwargs = fix_format_kwargs({"formatter": formatter_class, **formatter_options, **kwargs}) if isinstance(r, str) and r.lower() == "all": for r_ in range(len(self.MT.data)): self._format_row(r_, kwargs) elif is_iterable(r): for r_ in r: self._format_row(r_, kwargs) else: self._format_row(r, kwargs) return self.set_refresh_timer(redraw) def _format_row(self, r: int, d: dict) -> None: self.del_row_options_checkbox(r) add_to_options(self.MT.row_options, r, "format", d) for c in range(self.MT.total_data_cols()): self.MT.set_cell_data( r, c, value=d["value"] if "value" in d else self.MT.get_cell_data(r, c), kwargs=d, ) def delete_row_format( self, r: Iterator[int] | int | Literal["all"] = "all", clear_values: bool = False, ) -> Sheet: if is_iterable(r): for r_ in r: self.MT.delete_row_format(r_, clear_values=clear_values) else: self.MT.delete_row_format(r, clear_values=clear_values) return self def format_column( self, c: Iterator[int] | int | Literal["all"], formatter_options: dict = {}, formatter_class: object = None, redraw: bool = True, **kwargs, ) -> Sheet: kwargs = fix_format_kwargs({"formatter": formatter_class, **formatter_options, **kwargs}) if isinstance(c, str) and c.lower() == "all": for c_ in range(self.MT.total_data_cols()): self._format_column(c_, kwargs) elif is_iterable(c): for c_ in c: self._format_column(c_, kwargs) else: self._format_column(c, kwargs) return self.set_refresh_timer(redraw) def _format_column(self, c: int, d: dict) -> None: self.del_column_options_checkbox(c) add_to_options(self.MT.col_options, c, "format", d) for r in range(self.MT.total_data_rows()): self.MT.set_cell_data( r, c, value=d["value"] if "value" in d else self.MT.get_cell_data(r, c), kwargs=d, ) def delete_column_format( self, c: Iterator[int] | int | Literal["all"] = "all", clear_values: bool = False, ) -> Sheet: if is_iterable(c): for c_ in c: self.MT.delete_column_format(c_, clear_values=clear_values) else: self.MT.delete_column_format(c, clear_values=clear_values) return self class Dropdown(Sheet): def __init__( self, parent: tk.Misc, r: int, c: int, ops: dict, outline_color: str, width: int | None = None, height: int | None = None, font: None | tuple[str, int, str] = None, outline_thickness: int = 2, values: list[object] = [], close_dropdown_window: Callable | None = None, search_function: Callable = dropdown_search_function, arrowkey_RIGHT: Callable | None = None, arrowkey_LEFT: Callable | None = None, align: str = "w", # False for using r, c # "r" for r # "c" for c single_index: Literal["r", "c"] | bool = False, ) -> None: Sheet.__init__( self, parent=parent, name="!SheetDropdown", outline_thickness=outline_thickness, show_horizontal_grid=True, show_vertical_grid=False, show_header=False, show_row_index=False, show_top_left=False, empty_horizontal=0, empty_vertical=0, selected_rows_to_end_of_window=True, horizontal_grid_to_end_of_window=True, set_cell_sizes_on_zoom=True, show_selected_cells_border=False, scrollbar_show_arrows=False, ) self.parent = parent self.close_dropdown_window = close_dropdown_window self.search_function = search_function self.arrowkey_RIGHT = arrowkey_RIGHT self.arrowkey_LEFT = arrowkey_LEFT self.single_index = single_index self.bind("", self.mouse_motion) self.bind("", self.b1) self.bind("", self.arrowkey_UP) self.bind("", self.arrowkey_RIGHT) self.bind("", self.arrowkey_RIGHT) self.bind("", self.arrowkey_DOWN) self.bind("", self.arrowkey_LEFT) self.bind("", self.arrowkey_UP) self.bind("", self.arrowkey_DOWN) self.bind("", self.b1) self.reset(r, c, width, height, font, ops, outline_color, align, values) def reset( self, r: int, c: int, width: int, height: int, font: tuple[str, int, str], ops: DotDict, outline_color: str, align: str, values: list[object] | None = None, ) -> None: self.deselect(redraw=False) self.r = r self.c = c self.row = -1 self.height_and_width(height=height, width=width) self.table_align(align) self.set_options( outline_color=outline_color, table_grid_fg=ops.popup_menu_fg, table_selected_cells_border_fg=ops.popup_menu_fg, table_selected_cells_bg=ops.popup_menu_highlight_bg, table_selected_rows_border_fg=ops.popup_menu_fg, table_selected_rows_bg=ops.popup_menu_highlight_bg, table_selected_rows_fg=ops.popup_menu_highlight_fg, table_selected_box_cells_fg=ops.popup_menu_highlight_bg, table_selected_box_rows_fg=ops.popup_menu_highlight_bg, font=font, table_fg=ops.popup_menu_fg, table_bg=ops.popup_menu_bg, **{k: ops[k] for k in scrollbar_options_keys}, ) self.values(values, width=width - self.yscroll.winfo_width() - 4) def arrowkey_UP(self, event: object = None) -> None: if self.row > 0: self.row -= 1 else: self.row = 0 self.see(self.row, 0, redraw=False) self.select_row(self.row) def arrowkey_DOWN(self, event: object = None) -> None: if len(self.MT.data) - 1 > self.row: self.row += 1 self.see(self.row, 0, redraw=False) self.select_row(self.row) def search_and_see(self, event: object = None) -> None: if self.search_function is not None: rn = self.search_function(search_for=rf"{event['value']}".lower(), data=self.MT.data) if rn is not None: self.row = rn self.see(self.row, 0, redraw=False) self.select_row(self.row) def mouse_motion(self, event: object) -> None: row = self.identify_row(event, exclude_index=True, allow_end=False) if row is not None and row != self.row: self.row = row self.select_row(self.row) def _reselect(self) -> None: rows = self.get_selected_rows() if rows: self.select_row(next(iter(rows))) def b1(self, event: object = None) -> None: if event is None: row = None elif event.keysym == "Return": row = self.get_selected_rows() if not row: row = None else: row = next(iter(row)) else: row = self.identify_row(event, exclude_index=True, allow_end=False) if self.single_index: if row is None: self.close_dropdown_window(self.r if self.single_index == "r" else self.c) else: self.close_dropdown_window( self.r if self.single_index == "r" else self.c, self.get_cell_data(row, 0), ) else: if row is None: self.close_dropdown_window(self.r, self.c) else: self.close_dropdown_window(self.r, self.c, self.get_cell_data(row, 0)) def get_coords(self) -> int | tuple[int, int]: if self.single_index == "r": return self.r elif self.single_index == "c": return self.c return self.r, self.c def values( self, values: list = [], redraw: bool = True, width: int | None = None, ) -> None: self.set_sheet_data( [[v] for v in values], reset_col_positions=False, reset_row_positions=False, redraw=False, verify=False, ) self.set_all_cell_sizes_to_text(redraw=redraw, width=width, slim=True) tksheet-7.2.12/tksheet/sheet_options.py000066400000000000000000000273611463432073300201740ustar00rootroot00000000000000from __future__ import annotations from .other_classes import ( DotDict, FontTuple, ) from .themes import ( theme_light_blue, ) from .vars import ( USER_OS, ctrl_key, ) def new_sheet_options() -> DotDict: return DotDict( { "popup_menu_font": FontTuple( "Calibri", 13 if USER_OS == "darwin" else 11, "normal", ), "table_font": FontTuple( "Calibri", 13 if USER_OS == "darwin" else 11, "normal", ), "header_font": FontTuple( "Calibri", 13 if USER_OS == "darwin" else 11, "normal", ), "index_font": FontTuple( "Calibri", 13 if USER_OS == "darwin" else 11, "normal", ), "edit_header_label": "Edit header", "edit_header_accelerator": "", "edit_index_label": "Edit index", "edit_index_accelerator": "", "edit_cell_label": "Edit cell", "edit_cell_accelerator": "", "cut_label": "Cut", "cut_accelerator": "Ctrl+X", "cut_contents_label": "Cut contents", "cut_contents_accelerator": "Ctrl+X", "copy_label": "Copy", "copy_accelerator": "Ctrl+C", "copy_contents_label": "Copy contents", "copy_contents_accelerator": "Ctrl+C", "paste_label": "Paste", "paste_accelerator": "Ctrl+V", "delete_label": "Delete", "delete_accelerator": "Del", "clear_contents_label": "Clear contents", "clear_contents_accelerator": "Del", "delete_columns_label": "Delete columns", "delete_columns_accelerator": "", "insert_columns_left_label": "Insert columns left", "insert_columns_left_accelerator": "", "insert_column_label": "Insert column", "insert_column_accelerator": "", "insert_columns_right_label": "Insert columns right", "insert_columns_right_accelerator": "", "delete_rows_label": "Delete rows", "delete_rows_accelerator": "", "insert_rows_above_label": "Insert rows above", "insert_rows_above_accelerator": "", "insert_rows_below_label": "Insert rows below", "insert_rows_below_accelerator": "", "insert_row_label": "Insert row", "insert_row_accelerator": "", "select_all_label": "Select all", "select_all_accelerator": "Ctrl+A", "undo_label": "Undo", "undo_accelerator": "Ctrl+Z", "copy_bindings": [ f"<{ctrl_key}-c>", f"<{ctrl_key}-C>", ], "cut_bindings": [ f"<{ctrl_key}-x>", f"<{ctrl_key}-X>", ], "paste_bindings": [ f"<{ctrl_key}-v>", f"<{ctrl_key}-V>", ], "undo_bindings": [ f"<{ctrl_key}-z>", f"<{ctrl_key}-Z>", ], "redo_bindings": [ f"<{ctrl_key}-Shift-z>", f"<{ctrl_key}-Shift-Z>", ], "delete_bindings": [ "", "", ], "select_all_bindings": [ f"<{ctrl_key}-a>", f"<{ctrl_key}-A>", ], "tab_bindings": [ "", ], "up_bindings": [ "", ], "right_bindings": [ "", ], "down_bindings": [ "", ], "left_bindings": [ "", ], "prior_bindings": [ "", ], "next_bindings": [ "", ], "popup_menu_fg": theme_light_blue["popup_menu_fg"], "popup_menu_bg": theme_light_blue["popup_menu_bg"], "popup_menu_highlight_bg": theme_light_blue["popup_menu_highlight_bg"], "popup_menu_highlight_fg": theme_light_blue["popup_menu_highlight_fg"], "index_hidden_rows_expander_bg": theme_light_blue["index_hidden_rows_expander_bg"], "header_hidden_columns_expander_bg": theme_light_blue["header_hidden_columns_expander_bg"], "header_bg": theme_light_blue["header_bg"], "header_border_fg": theme_light_blue["header_border_fg"], "header_grid_fg": theme_light_blue["header_grid_fg"], "header_fg": theme_light_blue["header_fg"], "header_selected_cells_bg": theme_light_blue["header_selected_cells_bg"], "header_selected_cells_fg": theme_light_blue["header_selected_cells_fg"], "index_bg": theme_light_blue["index_bg"], "index_border_fg": theme_light_blue["index_border_fg"], "index_grid_fg": theme_light_blue["index_grid_fg"], "index_fg": theme_light_blue["index_fg"], "index_selected_cells_bg": theme_light_blue["index_selected_cells_bg"], "index_selected_cells_fg": theme_light_blue["index_selected_cells_fg"], "top_left_bg": theme_light_blue["top_left_bg"], "top_left_fg": theme_light_blue["top_left_fg"], "top_left_fg_highlight": theme_light_blue["top_left_fg_highlight"], "table_bg": theme_light_blue["table_bg"], "table_grid_fg": theme_light_blue["table_grid_fg"], "table_fg": theme_light_blue["table_fg"], "tree_arrow_fg": theme_light_blue["tree_arrow_fg"], "selected_cells_tree_arrow_fg": theme_light_blue["selected_cells_tree_arrow_fg"], "selected_rows_tree_arrow_fg": theme_light_blue["selected_rows_tree_arrow_fg"], "table_selected_box_cells_fg": theme_light_blue["table_selected_box_cells_fg"], "table_selected_box_rows_fg": theme_light_blue["table_selected_box_rows_fg"], "table_selected_box_columns_fg": theme_light_blue["table_selected_box_columns_fg"], "table_selected_cells_border_fg": theme_light_blue["table_selected_cells_border_fg"], "table_selected_cells_bg": theme_light_blue["table_selected_cells_bg"], "table_selected_cells_fg": theme_light_blue["table_selected_cells_fg"], "resizing_line_fg": theme_light_blue["resizing_line_fg"], "drag_and_drop_bg": theme_light_blue["drag_and_drop_bg"], "outline_color": theme_light_blue["outline_color"], "header_selected_columns_bg": theme_light_blue["header_selected_columns_bg"], "header_selected_columns_fg": theme_light_blue["header_selected_columns_fg"], "index_selected_rows_bg": theme_light_blue["index_selected_rows_bg"], "index_selected_rows_fg": theme_light_blue["index_selected_rows_fg"], "table_selected_rows_border_fg": theme_light_blue["table_selected_rows_border_fg"], "table_selected_rows_bg": theme_light_blue["table_selected_rows_bg"], "table_selected_rows_fg": theme_light_blue["table_selected_rows_fg"], "table_selected_columns_border_fg": theme_light_blue["table_selected_columns_border_fg"], "table_selected_columns_bg": theme_light_blue["table_selected_columns_bg"], "table_selected_columns_fg": theme_light_blue["table_selected_columns_fg"], "vertical_scroll_background": theme_light_blue["vertical_scroll_background"], "horizontal_scroll_background": theme_light_blue["horizontal_scroll_background"], "vertical_scroll_troughcolor": theme_light_blue["vertical_scroll_troughcolor"], "horizontal_scroll_troughcolor": theme_light_blue["horizontal_scroll_troughcolor"], "vertical_scroll_lightcolor": theme_light_blue["vertical_scroll_lightcolor"], "horizontal_scroll_lightcolor": theme_light_blue["horizontal_scroll_lightcolor"], "vertical_scroll_darkcolor": theme_light_blue["vertical_scroll_darkcolor"], "horizontal_scroll_darkcolor": theme_light_blue["horizontal_scroll_darkcolor"], "vertical_scroll_relief": theme_light_blue["vertical_scroll_relief"], "horizontal_scroll_relief": theme_light_blue["horizontal_scroll_relief"], "vertical_scroll_troughrelief": theme_light_blue["vertical_scroll_troughrelief"], "horizontal_scroll_troughrelief": theme_light_blue["horizontal_scroll_troughrelief"], "vertical_scroll_bordercolor": theme_light_blue["vertical_scroll_bordercolor"], "horizontal_scroll_bordercolor": theme_light_blue["horizontal_scroll_bordercolor"], "vertical_scroll_active_bg": theme_light_blue["vertical_scroll_active_bg"], "horizontal_scroll_active_bg": theme_light_blue["horizontal_scroll_active_bg"], "vertical_scroll_not_active_bg": theme_light_blue["vertical_scroll_not_active_bg"], "horizontal_scroll_not_active_bg": theme_light_blue["horizontal_scroll_not_active_bg"], "vertical_scroll_pressed_bg": theme_light_blue["vertical_scroll_pressed_bg"], "horizontal_scroll_pressed_bg": theme_light_blue["horizontal_scroll_pressed_bg"], "vertical_scroll_active_fg": theme_light_blue["vertical_scroll_active_fg"], "horizontal_scroll_active_fg": theme_light_blue["horizontal_scroll_active_fg"], "vertical_scroll_not_active_fg": theme_light_blue["vertical_scroll_not_active_fg"], "horizontal_scroll_not_active_fg": theme_light_blue["horizontal_scroll_not_active_fg"], "vertical_scroll_pressed_fg": theme_light_blue["vertical_scroll_pressed_fg"], "horizontal_scroll_pressed_fg": theme_light_blue["horizontal_scroll_pressed_fg"], "vertical_scroll_borderwidth": 1, "horizontal_scroll_borderwidth": 1, "vertical_scroll_gripcount": 0, "horizontal_scroll_gripcount": 0, "vertical_scroll_arrowsize": "", "horizontal_scroll_arrowsize": "", "set_cell_sizes_on_zoom": False, "auto_resize_columns": None, "auto_resize_rows": None, "to_clipboard_delimiter": "\t", "to_clipboard_quotechar": '"', "to_clipboard_lineterminator": "\n", "from_clipboard_delimiters": ["\t"], "show_dropdown_borders": False, "show_default_header_for_empty": True, "show_default_index_for_empty": True, "default_header_height": "1", "default_row_height": "1", "default_column_width": 120, "default_row_index_width": 70, "page_up_down_select_row": True, "paste_can_expand_x": False, "paste_can_expand_y": False, "paste_insert_column_limit": None, "paste_insert_row_limit": None, "arrow_key_down_right_scroll_page": False, "cell_auto_resize_enabled": True, "auto_resize_row_index": True, "max_undos": 30, "column_drag_and_drop_perform": True, "row_drag_and_drop_perform": True, "empty_horizontal": 50, "empty_vertical": 50, "selected_rows_to_end_of_window": False, "horizontal_grid_to_end_of_window": False, "vertical_grid_to_end_of_window": False, "show_vertical_grid": True, "show_horizontal_grid": True, "display_selected_fg_over_highlights": False, "show_selected_cells_border": True, "treeview": False, "treeview_indent": "6", "rounded_boxes": True, "alternate_color": "", } ) tksheet-7.2.12/tksheet/text_editor.py000066400000000000000000000142661463432073300176430ustar00rootroot00000000000000from __future__ import annotations import tkinter as tk from collections.abc import Callable from typing import Literal from .functions import ( convert_align, ) from .other_classes import ( DotDict, ) from .vars import ( ctrl_key, rc_binding, ) class TextEditorTkText(tk.Text): def __init__( self, parent: tk.Misc, newline_binding: None | Callable = None, ) -> None: tk.Text.__init__( self, parent, spacing1=0, spacing2=1, spacing3=0, bd=0, highlightthickness=0, undo=True, maxundo=30, ) self.parent = parent self.newline_bindng = newline_binding self.rc_popup_menu = tk.Menu(self, tearoff=0) self.bind("<1>", lambda event: self.focus_set()) self.bind(rc_binding, self.rc) self.bind(f"<{ctrl_key}-a>", self.select_all) self.bind(f"<{ctrl_key}-A>", self.select_all) self._orig = self._w + "_orig" self.tk.call("rename", self._w, self._orig) self.tk.createcommand(self._w, self._proxy) def reset( self, menu_kwargs: dict, sheet_ops: dict, align: str, font: tuple, bg: str, fg: str, state: str, text: str = "", ) -> None: self.config( font=font, background=bg, foreground=fg, insertbackground=fg, state=state, ) self.align = align self.rc_popup_menu.delete(0, None) self.rc_popup_menu.add_command( label=sheet_ops.select_all_label, accelerator=sheet_ops.select_all_accelerator, command=self.select_all, **menu_kwargs, ) self.rc_popup_menu.add_command( label=sheet_ops.cut_label, accelerator=sheet_ops.cut_accelerator, command=self.cut, **menu_kwargs, ) self.rc_popup_menu.add_command( label=sheet_ops.copy_label, accelerator=sheet_ops.copy_accelerator, command=self.copy, **menu_kwargs, ) self.rc_popup_menu.add_command( label=sheet_ops.paste_label, accelerator=sheet_ops.paste_accelerator, command=self.paste, **menu_kwargs, ) self.rc_popup_menu.add_command( label=sheet_ops.undo_label, accelerator=sheet_ops.undo_accelerator, command=self.undo, **menu_kwargs, ) align = convert_align(align) if align == "w": self.align = "left" elif align == "e": self.align = "right" self.delete(1.0, "end") self.insert(1.0, text) self.yview_moveto(1) self.tag_configure("align", justify=self.align) self.tag_add("align", 1.0, "end") def _proxy(self, command: object, *args) -> object: try: result = self.tk.call((self._orig, command) + args) except Exception: return if command in ( "insert", "delete", "replace", ): self.tag_add("align", 1.0, "end") self.event_generate("<>") if args and len(args) > 1 and args[1] != "\n" and args != ("1.0", "end"): if self.yview() != (0.0, 1.0) and self.newline_bindng is not None: self.newline_bindng(check_lines=False) return result def rc(self, event: object) -> None: self.focus_set() self.rc_popup_menu.tk_popup(event.x_root, event.y_root) def select_all(self, event: object = None) -> Literal["break"]: self.tag_add(tk.SEL, "1.0", tk.END) self.mark_set(tk.INSERT, tk.END) # self.see(tk.INSERT) return "break" def cut(self, event: object = None) -> Literal["break"]: self.event_generate(f"<{ctrl_key}-x>") return "break" def copy(self, event: object = None) -> Literal["break"]: self.event_generate(f"<{ctrl_key}-c>") return "break" def paste(self, event: object = None) -> Literal["break"]: self.event_generate(f"<{ctrl_key}-v>") return "break" def undo(self, event: object = None) -> Literal["break"]: self.event_generate(f"<{ctrl_key}-z>") return "break" class TextEditor(tk.Frame): def __init__( self, parent: tk.Misc, newline_binding: None | Callable = None, ) -> None: tk.Frame.__init__( self, parent, width=0, height=0, bd=0, ) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.grid_propagate(False) self.parent = parent self.r = 0 self.c = 0 self.tktext = TextEditorTkText(self, newline_binding=newline_binding) self.tktext.grid(row=0, column=0, sticky="nswe") def get(self) -> str: return self.tktext.get("1.0", "end-1c") def get_num_lines(self) -> int: return int(self.tktext.index("end-1c").split(".")[0]) def set_text(self, text: str = "") -> None: self.tktext.delete(1.0, "end") self.tktext.insert(1.0, text) def scroll_to_bottom(self) -> None: self.tktext.yview_moveto(1) def reset( self, width: int, height: int, border_color: str, show_border: bool, menu_kwargs: DotDict, sheet_ops: DotDict, bg: str, fg: str, align: str, state: str, r: int = 0, c: int = 0, text: str = "", ) -> None: self.r = r self.c = c self.tktext.reset( menu_kwargs=menu_kwargs, sheet_ops=sheet_ops, align=align, font=menu_kwargs.font, bg=bg, fg=fg, state=state, text=text, ) self.config( width=width, height=height, background=bg, highlightbackground=border_color, highlightcolor=border_color, highlightthickness=2 if show_border else 0, ) tksheet-7.2.12/tksheet/themes.py000066400000000000000000000334641463432073300165770ustar00rootroot00000000000000from __future__ import annotations from .other_classes import DotDict theme_light_blue: dict[str, str] = DotDict({ "popup_menu_fg": "#000000", "popup_menu_bg": "#FFFFFF", "popup_menu_highlight_bg": "#DCDEE0", "popup_menu_highlight_fg": "#000000", "index_hidden_rows_expander_bg": "#747775", "header_hidden_columns_expander_bg": "#747775", "header_bg": "#FFFFFF", "header_border_fg": "#C4C7C5", "header_grid_fg": "#C4C7C5", "header_fg": "#444746", "header_selected_cells_bg": "#D3E3FD", "header_selected_cells_fg": "black", "index_bg": "#FFFFFF", "index_border_fg": "#C4C7C5", "index_grid_fg": "#C4C7C5", "index_fg": "black", "index_selected_cells_bg": "#D3E3FD", "index_selected_cells_fg": "black", "top_left_bg": "#F9FBFD", "top_left_fg": "#C7C7C7", "top_left_fg_highlight": "#747775", "table_bg": "#FFFFFF", "table_grid_fg": "#E1E1E1", "table_fg": "black", "table_selected_box_cells_fg": "#0B57D0", "table_selected_box_rows_fg": "#0B57D0", "table_selected_box_columns_fg": "#0B57D0", "table_selected_cells_border_fg": "#0B57D0", "table_selected_cells_bg": "#E6EFFD", "table_selected_cells_fg": "black", "resizing_line_fg": "black", "drag_and_drop_bg": "#0B57D0", "outline_color": "gray2", "header_selected_columns_bg": "#0B57D0", "header_selected_columns_fg": "#FFFFFF", "index_selected_rows_bg": "#0B57D0", "index_selected_rows_fg": "#FFFFFF", "table_selected_rows_border_fg": "#0B57D0", "table_selected_rows_bg": "#E6EFFD", "table_selected_rows_fg": "black", "table_selected_columns_border_fg": "#0B57D0", "table_selected_columns_bg": "#E6EFFD", "table_selected_columns_fg": "black", "tree_arrow_fg": "black", "selected_cells_tree_arrow_fg": "black", "selected_rows_tree_arrow_fg": "#FFFFFF", "vertical_scroll_background": "#FFFFFF", "horizontal_scroll_background": "#FFFFFF", "vertical_scroll_troughcolor": "#f9fbfd", "horizontal_scroll_troughcolor": "#f9fbfd", "vertical_scroll_lightcolor": "#FFFFFF", "horizontal_scroll_lightcolor": "#FFFFFF", "vertical_scroll_darkcolor": "gray50", "horizontal_scroll_darkcolor": "gray50", "vertical_scroll_relief": "flat", "horizontal_scroll_relief": "flat", "vertical_scroll_troughrelief": "flat", "horizontal_scroll_troughrelief": "flat", "vertical_scroll_bordercolor": "#f9fbfd", "horizontal_scroll_bordercolor": "#f9fbfd", "vertical_scroll_active_bg": "#bdc1c6", "horizontal_scroll_active_bg": "#bdc1c6", "vertical_scroll_not_active_bg": "#DADCE0", "horizontal_scroll_not_active_bg": "#DADCE0", "vertical_scroll_pressed_bg": "#bdc1c6", "horizontal_scroll_pressed_bg": "#bdc1c6", "vertical_scroll_active_fg": "#bdc1c6", "horizontal_scroll_active_fg": "#bdc1c6", "vertical_scroll_not_active_fg": "#DADCE0", "horizontal_scroll_not_active_fg": "#DADCE0", "vertical_scroll_pressed_fg": "#bdc1c6", "horizontal_scroll_pressed_fg": "#bdc1c6", }) theme_light_green: dict[str, str] = DotDict({ "popup_menu_fg": "#000000", "popup_menu_bg": "#FFFFFF", "popup_menu_highlight_bg": "#DCDEE0", "popup_menu_highlight_fg": "#000000", "index_hidden_rows_expander_bg": "gray30", "header_hidden_columns_expander_bg": "gray30", "header_bg": "#F5F5F5", "header_border_fg": "#ababab", "header_grid_fg": "#ababab", "header_fg": "black", "header_selected_cells_bg": "#CAEAD8", "header_selected_cells_fg": "#217346", "index_bg": "#F5F5F5", "index_border_fg": "#ababab", "index_grid_fg": "#ababab", "index_fg": "black", "index_selected_cells_bg": "#CAEAD8", "index_selected_cells_fg": "#107C41", "top_left_bg": "#F5F5F5", "top_left_fg": "#b7b7b7", "top_left_fg_highlight": "#5f6368", "table_bg": "#FFFFFF", "table_grid_fg": "#bfbfbf", "table_fg": "black", "table_selected_box_cells_fg": "#107C41", "table_selected_box_rows_fg": "#107C41", "table_selected_box_columns_fg": "#107C41", "table_selected_cells_border_fg": "#107C41", "table_selected_cells_bg": "#E3E3E3", "table_selected_cells_fg": "black", "resizing_line_fg": "black", "drag_and_drop_bg": "#107C41", "outline_color": "gray2", "header_selected_columns_bg": "#107C41", "header_selected_columns_fg": "#FFFFFF", "index_selected_rows_bg": "#107C41", "index_selected_rows_fg": "#FFFFFF", "table_selected_rows_border_fg": "#107C41", "table_selected_rows_bg": "#E3E3E3", "table_selected_rows_fg": "black", "table_selected_columns_border_fg": "#107C41", "table_selected_columns_bg": "#E3E3E3", "table_selected_columns_fg": "black", "tree_arrow_fg": "black", "selected_cells_tree_arrow_fg": "#107C41", "selected_rows_tree_arrow_fg": "#FFFFFF", "vertical_scroll_background": "#FFFFFF", "horizontal_scroll_background": "#FFFFFF", "vertical_scroll_troughcolor": "#f1f1f1", "horizontal_scroll_troughcolor": "#f1f1f1", "vertical_scroll_lightcolor": "#FFFFFF", "horizontal_scroll_lightcolor": "#FFFFFF", "vertical_scroll_darkcolor": "gray50", "horizontal_scroll_darkcolor": "gray50", "vertical_scroll_relief": "flat", "horizontal_scroll_relief": "flat", "vertical_scroll_troughrelief": "flat", "horizontal_scroll_troughrelief": "flat", "vertical_scroll_bordercolor": "#f1f1f1", "horizontal_scroll_bordercolor": "#f1f1f1", "vertical_scroll_active_bg": "#707070", "horizontal_scroll_active_bg": "#707070", "vertical_scroll_not_active_bg": "#c1c1c1", "horizontal_scroll_not_active_bg": "#c1c1c1", "vertical_scroll_pressed_bg": "#707070", "horizontal_scroll_pressed_bg": "#707070", "vertical_scroll_active_fg": "#707070", "horizontal_scroll_active_fg": "#707070", "vertical_scroll_not_active_fg": "#c1c1c1", "horizontal_scroll_not_active_fg": "#c1c1c1", "vertical_scroll_pressed_fg": "#707070", "horizontal_scroll_pressed_fg": "#707070", }) theme_dark: dict[str, str] = DotDict({ "popup_menu_fg": "white", "popup_menu_bg": "gray15", "popup_menu_highlight_bg": "gray40", "popup_menu_highlight_fg": "white", "index_hidden_rows_expander_bg": "gray30", "header_hidden_columns_expander_bg": "gray30", "header_bg": "#141414", "header_border_fg": "#505054", "header_grid_fg": "#8C8C8C", "header_fg": "gray70", "header_selected_cells_bg": "#4b4b4b", "header_selected_cells_fg": "#6aa2fc", "index_bg": "#141414", "index_border_fg": "#505054", "index_grid_fg": "#8C8C8C", "index_fg": "gray70", "index_selected_cells_bg": "#4b4b4b", "index_selected_cells_fg": "#6aa2fc", "top_left_bg": "#28282a", "top_left_fg": "#505054", "top_left_fg_highlight": "white", "table_bg": "#000000", "table_grid_fg": "#595959", "table_fg": "#E3E3E3", "table_selected_box_cells_fg": "#6aa2fc", "table_selected_box_rows_fg": "#6aa2fc", "table_selected_box_columns_fg": "#6aa2fc", "table_selected_cells_border_fg": "#6aa2fc", "table_selected_cells_bg": "#404040", "table_selected_cells_fg": "#F7F7F7", "resizing_line_fg": "white", "drag_and_drop_bg": "#ecf0f2", "outline_color": "gray95", "header_selected_columns_bg": "#4489F7", "header_selected_columns_fg": "white", "index_selected_rows_bg": "#4489F7", "index_selected_rows_fg": "white", "table_selected_rows_border_fg": "#4489F7", "table_selected_rows_bg": "#404040", "table_selected_rows_fg": "#F7F7F7", "table_selected_columns_border_fg": "#4489F7", "table_selected_columns_bg": "#404040", "table_selected_columns_fg": "#F7F7F7", "tree_arrow_fg": "#8C8C8C", "selected_cells_tree_arrow_fg": "#6aa2fc", "selected_rows_tree_arrow_fg": "white", "vertical_scroll_background": "#3b3b3d", "horizontal_scroll_background": "#3b3b3d", "vertical_scroll_troughcolor": "#000000", "horizontal_scroll_troughcolor": "#000000", "vertical_scroll_lightcolor": "gray50", "horizontal_scroll_lightcolor": "gray50", "vertical_scroll_darkcolor": "gray20", "horizontal_scroll_darkcolor": "gray20", "vertical_scroll_relief": "flat", "horizontal_scroll_relief": "flat", "vertical_scroll_troughrelief": "flat", "horizontal_scroll_troughrelief": "flat", "vertical_scroll_bordercolor": "#000000", "horizontal_scroll_bordercolor": "#000000", "vertical_scroll_active_bg": "#a0a0a0", "horizontal_scroll_active_bg": "#a0a0a0", "vertical_scroll_not_active_bg": "#3b3b3d", "horizontal_scroll_not_active_bg": "#3b3b3d", "vertical_scroll_pressed_bg": "#a0a0a0", "horizontal_scroll_pressed_bg": "#a0a0a0", "vertical_scroll_active_fg": "#a0a0a0", "horizontal_scroll_active_fg": "#a0a0a0", "vertical_scroll_not_active_fg": "#3b3b3d", "horizontal_scroll_not_active_fg": "#3b3b3d", "vertical_scroll_pressed_fg": "#a0a0a0", "horizontal_scroll_pressed_fg": "#a0a0a0", }) theme_black: dict[str, str] = DotDict({ "popup_menu_fg": "white", "popup_menu_bg": "gray15", "popup_menu_highlight_bg": "gray40", "popup_menu_highlight_fg": "white", "index_hidden_rows_expander_bg": "gray30", "header_hidden_columns_expander_bg": "gray30", "header_bg": "#000000", "header_border_fg": "#505054", "header_grid_fg": "#8C8C8C", "header_fg": "#FBB86C", "header_selected_cells_bg": "#4b4b4b", "header_selected_cells_fg": "#FBB86C", "index_bg": "#000000", "index_border_fg": "#505054", "index_grid_fg": "#8C8C8C", "index_fg": "#FBB86C", "index_selected_cells_bg": "#4b4b4b", "index_selected_cells_fg": "#FBB86C", "top_left_bg": "#141416", "top_left_fg": "#505054", "top_left_fg_highlight": "#FBB86C", "table_bg": "#000000", "table_grid_fg": "#595959", "table_fg": "#E3E3E3", "table_selected_box_cells_fg": "#FBB86C", "table_selected_box_rows_fg": "#FBB86C", "table_selected_box_columns_fg": "#FBB86C", "table_selected_cells_border_fg": "#FBB86C", "table_selected_cells_bg": "#404040", "table_selected_cells_fg": "#F7F7F7", "resizing_line_fg": "white", "drag_and_drop_bg": "#ecf0f2", "outline_color": "gray95", "header_selected_columns_bg": "#FBB86C", "header_selected_columns_fg": "#000000", "index_selected_rows_bg": "#FBB86C", "index_selected_rows_fg": "#000000", "table_selected_rows_border_fg": "#FBB86C", "table_selected_rows_bg": "#404040", "table_selected_rows_fg": "#F7F7F7", "table_selected_columns_border_fg": "#FBB86C", "table_selected_columns_bg": "#404040", "table_selected_columns_fg": "#F7F7F7", "tree_arrow_fg": "#8C8C8C", "selected_cells_tree_arrow_fg": "#FBB86C", "selected_rows_tree_arrow_fg": "#000000", "vertical_scroll_background": "#3b3a39", "horizontal_scroll_background": "#3b3a39", "vertical_scroll_troughcolor": "#000000", "horizontal_scroll_troughcolor": "#000000", "vertical_scroll_lightcolor": "gray50", "horizontal_scroll_lightcolor": "gray50", "vertical_scroll_darkcolor": "gray20", "horizontal_scroll_darkcolor": "gray20", "vertical_scroll_relief": "flat", "horizontal_scroll_relief": "flat", "vertical_scroll_troughrelief": "flat", "horizontal_scroll_troughrelief": "flat", "vertical_scroll_bordercolor": "#000000", "horizontal_scroll_bordercolor": "#000000", "vertical_scroll_active_bg": "#a0a0a0", "horizontal_scroll_active_bg": "#a0a0a0", "vertical_scroll_not_active_bg": "#3b3a39", "horizontal_scroll_not_active_bg": "#3b3a39", "vertical_scroll_pressed_bg": "#a0a0a0", "horizontal_scroll_pressed_bg": "#a0a0a0", "vertical_scroll_active_fg": "#a0a0a0", "horizontal_scroll_active_fg": "#a0a0a0", "vertical_scroll_not_active_fg": "#3b3a39", "horizontal_scroll_not_active_fg": "#3b3a39", "vertical_scroll_pressed_fg": "#a0a0a0", "horizontal_scroll_pressed_fg": "#a0a0a0", }) theme_dark_blue: dict[str, str] = DotDict(theme_dark.copy()) theme_dark_blue["top_left_fg_highlight"] = "#94EBEB" theme_dark_blue["table_selected_box_cells_fg"] = "#94EBEB" theme_dark_blue["table_selected_box_rows_fg"] = "#94EBEB" theme_dark_blue["table_selected_box_columns_fg"] = "#94EBEB" theme_dark_blue["table_selected_cells_border_fg"] = "#94EBEB" theme_dark_blue["table_selected_rows_border_fg"] = "#94EBEB" theme_dark_blue["table_selected_columns_border_fg"] = "#94EBEB" theme_dark_blue["header_fg"] = "#94EBEB" theme_dark_blue["header_selected_cells_fg"] = "#94EBEB" theme_dark_blue["header_selected_columns_fg"] = "#000000" theme_dark_blue["header_selected_columns_bg"] = "#94EBEB" theme_dark_blue["index_fg"] = "#94EBEB" theme_dark_blue["index_selected_rows_bg"] = "#94EBEB" theme_dark_blue["index_selected_rows_fg"] = "#000000" theme_dark_blue["index_selected_cells_fg"] = "#94EBEB" theme_dark_blue["selected_rows_tree_arrow_fg"] = "#000000" theme_dark_blue["selected_cells_tree_arrow_fg"] = "#94EBEB" theme_dark_green: dict[str, str] = DotDict(theme_dark.copy()) theme_dark_green["header_fg"] = "#66FFBF" theme_dark_green["header_selected_cells_fg"] = "#66FFBF" theme_dark_green["index_fg"] = "#66FFBF" theme_dark_green["index_selected_cells_fg"] = "#66FFBF" theme_dark_green["top_left_fg_highlight"] = "#66FFBF" theme_dark_green["table_selected_box_cells_fg"] = "#66FFBF" theme_dark_green["table_selected_box_rows_fg"] = "#66FFBF" theme_dark_green["table_selected_box_columns_fg"] = "#66FFBF" theme_dark_green["table_selected_cells_border_fg"] = "#66FFBF" theme_dark_green["header_selected_columns_bg"] = "#66FFBF" theme_dark_green["header_selected_columns_fg"] = "#000000" theme_dark_green["index_selected_rows_bg"] = "#66FFBF" theme_dark_green["index_selected_rows_fg"] = "#000000" theme_dark_green["table_selected_rows_border_fg"] = "#66FFBF" theme_dark_green["table_selected_columns_border_fg"] = "#66FFBF" theme_dark_green["selected_rows_tree_arrow_fg"] = "#000000" theme_dark_green["selected_cells_tree_arrow_fg"] = "#000000" tksheet-7.2.12/tksheet/top_left_rectangle.py000066400000000000000000000200051463432073300211350ustar00rootroot00000000000000from __future__ import annotations import tkinter as tk from .vars import rc_binding class TopLeftRectangle(tk.Canvas): def __init__(self, *args, **kwargs) -> None: tk.Canvas.__init__( self, kwargs["parent"], background=kwargs["parent"].ops.top_left_bg, highlightthickness=0, ) self.PAR = kwargs["parent"] self.MT = kwargs["main_canvas"] self.RI = kwargs["row_index_canvas"] self.CH = kwargs["header_canvas"] try: self.config( width=self.RI.current_width, height=self.CH.current_height, ) except Exception: return self.extra_motion_func = None self.extra_b1_press_func = None self.extra_b1_motion_func = None self.extra_b1_release_func = None self.extra_double_b1_func = None self.extra_rc_func = None self.MT.TL = self self.RI.TL = self self.CH.TL = self w = self.RI.current_width h = self.CH.current_height self.rw_box = self.create_rectangle( 0, h - 5, w, h, fill=self.PAR.ops.top_left_fg, outline="", tag="rw", state="normal" if self.RI.width_resizing_enabled else "hidden", ) self.rh_box = self.create_rectangle( w - 5, 0, w, h, fill=self.PAR.ops.top_left_fg, outline="", tag="rh", state="normal" if self.CH.height_resizing_enabled else "hidden", ) self.select_all_box = self.create_rectangle( 0, 0, w - 5, h - 5, fill=self.PAR.ops.top_left_bg, outline="", tag="sa", state="normal" if self.MT.select_all_enabled else "hidden", ) self.select_all_tri = self.create_polygon( w - 7, h - 7 - 10, w - 7, h - 7, w - 7 - 10, h - 7, fill=self.PAR.ops.top_left_fg, outline="", tag="sa", state="normal" if self.MT.select_all_enabled else "hidden", ) self.tag_bind("rw", "", self.rw_enter) self.tag_bind("rh", "", self.rh_enter) self.tag_bind("sa", "", self.sa_enter) self.tag_bind("rw", "", self.rw_leave) self.tag_bind("rh", "", self.rh_leave) self.tag_bind("sa", "", self.sa_leave) self.bind("", self.mouse_motion) self.bind("", self.b1_press) self.bind("", self.b1_motion) self.bind("", self.b1_release) self.bind("", self.double_b1) self.bind(rc_binding, self.rc) def redraw(self) -> None: self.itemconfig("rw", fill=self.PAR.ops.top_left_fg) self.itemconfig("rh", fill=self.PAR.ops.top_left_fg) self.itemconfig( self.select_all_tri, fill=self.PAR.ops.top_left_fg, ) def rw_state(self, state: str = "normal") -> None: self.itemconfig("rw", state=state) def rh_state(self, state: str = "normal") -> None: self.itemconfig("rh", state=state) def sa_state(self, state: str = "normal") -> None: self.itemconfig("sa", state=state) def rw_enter(self, event: object = None) -> None: if self.RI.width_resizing_enabled: self.itemconfig( "rw", fill=self.PAR.ops.top_left_fg_highlight, ) def sa_enter(self, event: object = None) -> None: if self.MT.select_all_enabled: self.itemconfig( self.select_all_tri, fill=self.PAR.ops.top_left_fg_highlight, ) def rh_enter(self, event: object = None) -> None: if self.CH.height_resizing_enabled: self.itemconfig( "rh", fill=self.PAR.ops.top_left_fg_highlight, ) def rw_leave(self, event: object = None) -> None: self.itemconfig("rw", fill=self.PAR.ops.top_left_fg) def rh_leave(self, event: object = None) -> None: self.itemconfig("rh", fill=self.PAR.ops.top_left_fg) def sa_leave(self, event: object = None) -> None: self.itemconfig( self.select_all_tri, fill=self.PAR.ops.top_left_fg, ) def basic_bindings(self, enable: bool = True) -> None: if enable: self.bind("", self.mouse_motion) self.bind("", self.b1_press) self.bind("", self.b1_motion) self.bind("", self.b1_release) self.bind("", self.double_b1) self.bind(rc_binding, self.rc) else: self.unbind("") self.unbind("") self.unbind("") self.unbind("") self.unbind("") self.unbind(rc_binding) def set_dimensions( self, new_w: None | int = None, new_h: None | int = None, recreate_selection_boxes: bool = True, ) -> None: try: if isinstance(new_h, int): h = new_h self.config(height=h) else: h = self.CH.current_height if isinstance(new_w, int): w = new_w self.config(width=w) else: w = self.RI.current_width except Exception: return self.coords(self.rw_box, 0, h - 5, w, h) self.coords(self.rh_box, w - 5, 0, w, h) self.coords( self.select_all_tri, w - 7, h - 7 - 10, w - 7, h - 7, w - 7 - 10, h - 7, ) self.coords(self.select_all_box, 0, 0, w - 5, h - 5) if recreate_selection_boxes: self.MT.recreate_all_selection_boxes() def mouse_motion(self, event: object = None) -> None: self.MT.reset_mouse_motion_creations() if self.extra_motion_func is not None: self.extra_motion_func(event) def b1_press(self, event: object = None) -> None: self.focus_set() rect = self.find_overlapping(event.x, event.y, event.x, event.y) if not rect or rect[0] in ( self.select_all_box, self.select_all_tri, ): if self.MT.select_all_enabled: self.MT.deselect("all") self.MT.select_all() else: self.MT.deselect("all") elif rect[0] == self.rw_box: if self.RI.width_resizing_enabled: self.RI.set_width( self.PAR.ops.default_row_index_width, set_TL=True, ) elif rect[0] == self.rh_box: if self.CH.height_resizing_enabled: self.CH.set_height( self.MT.get_default_header_height(), set_TL=True, ) self.MT.main_table_redraw_grid_and_text( redraw_header=True, redraw_row_index=True, ) if self.extra_b1_press_func is not None: self.extra_b1_press_func(event) def b1_motion(self, event: object = None) -> None: self.focus_set() if self.extra_b1_motion_func is not None: self.extra_b1_motion_func(event) def b1_release(self, event: object = None) -> None: self.focus_set() if self.extra_b1_release_func is not None: self.extra_b1_release_func(event) def double_b1(self, event: object = None) -> None: self.focus_set() if self.extra_double_b1_func is not None: self.extra_double_b1_func(event) def rc(self, event: object = None) -> None: self.focus_set() if self.extra_rc_func is not None: self.extra_rc_func(event) tksheet-7.2.12/tksheet/types.py000066400000000000000000000005031463432073300164420ustar00rootroot00000000000000from typing import Tuple, Union from .other_classes import ( Span, ) CreateSpanTypes = Union[ Tuple[()], None, str, int, slice, Tuple[int, None, int, None], Tuple[Tuple[int, None, int, None], Tuple[int, None, int, None]], Tuple[int, None, int, None, int, None, int, None], Span, ] tksheet-7.2.12/tksheet/vars.py000066400000000000000000000063651463432073300162650ustar00rootroot00000000000000from __future__ import annotations from platform import system as get_os USER_OS: str = f"{get_os()}".lower() ctrl_key: str = "Command" if USER_OS == "darwin" else "Control" rc_binding: str = "<2>" if USER_OS == "darwin" else "<3>" symbols_set: set[str] = set("""!#$%&'()*+,-./:;"@[]^_`{|}~>?= \\""") nonelike: set[object] = {None, "none", ""} truthy: set[object] = {True, "true", "t", "yes", "y", "on", "1", 1, 1.0} falsy: set[object] = {False, "false", "f", "no", "n", "off", "0", 0, 0.0} val_modifying_options: set[str] = {"checkbox", "format", "dropdown"} named_span_types: set[str] = { "format", "highlight", "dropdown", "checkbox", "readonly", "align", } emitted_events: set[str] = { "<>", "<>", "<>", "<>", "<>", "<>", "<>", "<>", "<>", "<>", } backwards_compatibility_keys: dict[str, str] = { "font": "table_font", } text_editor_newline_bindings: set[str] = { "", "", "", } if USER_OS == "darwin": text_editor_newline_bindings.add("") text_editor_close_bindings: dict[str, str] = { "": "Tab", "": "Return", "": "Return", "": "Escape", } text_editor_to_unbind: set[str] = text_editor_newline_bindings | set(text_editor_close_bindings) | {""} scrollbar_options_keys: set[str] = { "vertical_scroll_background", "horizontal_scroll_background", "vertical_scroll_troughcolor", "horizontal_scroll_troughcolor", "vertical_scroll_lightcolor", "horizontal_scroll_lightcolor", "vertical_scroll_darkcolor", "horizontal_scroll_darkcolor", "vertical_scroll_relief", "horizontal_scroll_relief", "vertical_scroll_troughrelief", "horizontal_scroll_troughrelief", "vertical_scroll_bordercolor", "horizontal_scroll_bordercolor", "vertical_scroll_borderwidth", "horizontal_scroll_borderwidth", "vertical_scroll_gripcount", "horizontal_scroll_gripcount", "vertical_scroll_arrowsize", "horizontal_scroll_arrowsize", "vertical_scroll_active_bg", "horizontal_scroll_active_bg", "vertical_scroll_not_active_bg", "horizontal_scroll_not_active_bg", "vertical_scroll_pressed_bg", "horizontal_scroll_pressed_bg", "vertical_scroll_active_fg", "horizontal_scroll_active_fg", "vertical_scroll_not_active_fg", "horizontal_scroll_not_active_fg", "vertical_scroll_pressed_fg", "horizontal_scroll_pressed_fg", } bind_add_columns: set[str] = { "all", "rc_insert_column", "insert_column", "add_column", "insert_columns", "add_columns", "insert column", "add column", } bind_del_columns: set[str] = { "all", "rc_delete_column", "delete_column", "del_column", "delete_columns", "del_columns", "delete columns", "del columns", } bind_add_rows: set[str] = { "all", "rc_insert_row", "insert_row", "add_row", "insert_rows", "add_rows", "insert row", "add row", } bind_del_rows: set[str] = { "all", "rc_delete_row", "delete_row", "del_row", "delete_rows", "del_rows", "delete rows", "del rows", }