pax_global_header 0000666 0000000 0000000 00000000064 14634320733 0014517 g ustar 00root root 0000000 0000000 52 comment=1a6a39c2864d1492e980682362d7e0f93cec22ca
tksheet-7.2.12/ 0000775 0000000 0000000 00000000000 14634320733 0013257 5 ustar 00root root 0000000 0000000 tksheet-7.2.12/.editorconfig 0000664 0000000 0000000 00000000410 14634320733 0015727 0 ustar 00root root 0000000 0000000 # 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 = 4 tksheet-7.2.12/.gitignore 0000664 0000000 0000000 00000006114 14634320733 0015251 0 ustar 00root root 0000000 0000000 # 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.yaml 0000664 0000000 0000000 00000000643 14634320733 0017543 0 ustar 00root root 0000000 0000000 repos:
- 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.txt 0000664 0000000 0000000 00000002072 14634320733 0015103 0 ustar 00root root 0000000 0000000 Copyright (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.md 0000664 0000000 0000000 00000010025 14634320733 0014534 0 ustar 00root root 0000000 0000000
# tksheet - python tkinter table widget
[](https://pypi.python.org/pypi/tksheet/)  [](https://github.com/ragardner/tksheet/blob/master/LICENSE.txt)
[](https://github.com/ragardner/tksheet/releases) [](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**

### **dark theme**

tksheet-7.2.12/docs/ 0000775 0000000 0000000 00000000000 14634320733 0014207 5 ustar 00root root 0000000 0000000 tksheet-7.2.12/docs/CHANGELOG.md 0000664 0000000 0000000 00000236660 14634320733 0016035 0 ustar 00root root 0000000 0000000 ### 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.md 0000664 0000000 0000000 00000727267 14634320733 0016567 0 ustar 00root root 0000000 0000000 # 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.toml 0000664 0000000 0000000 00000002051 14634320733 0016171 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 14634320733 0014726 5 ustar 00root root 0000000 0000000 tksheet-7.2.12/tksheet/__init__.py 0000664 0000000 0000000 00000003771 14634320733 0017047 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000141235 14634320733 0016607 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000303215 14634320733 0020274 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000022755 14634320733 0017501 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000115515 14634320733 0017320 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00001163357 14634320733 0017413 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000035367 14634320733 0020154 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000320036 14634320733 0017302 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00001013317 14634320733 0016416 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000027361 14634320733 0020174 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000014266 14634320733 0017643 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000033464 14634320733 0016577 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000020005 14634320733 0021135 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000000503 14634320733 0016442 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000006365 14634320733 0016265 0 ustar 00root root 0000000 0000000 from __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] = {
"",
"