treeviewex 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
treeviewex/__init__.py
ADDED
treeviewex/treeviewex.py
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
# python3
|
|
2
|
+
"""Treeview extension."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
from tkinter import HORIZONTAL, VERTICAL, Entry, Event, Frame
|
|
8
|
+
from tkinter.ttk import Combobox, Scrollbar, Treeview
|
|
9
|
+
from typing import Callable, Union
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CellType(Enum):
|
|
13
|
+
"""Enum defining cell types."""
|
|
14
|
+
|
|
15
|
+
ENTRY = auto()
|
|
16
|
+
READONLY = auto()
|
|
17
|
+
COMBOBOX = auto()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["CellType", "TreeviewEx"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _colid2colindex(column_id: str) -> int:
|
|
24
|
+
"""
|
|
25
|
+
Convert a column ID to a column index.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
column_id : str
|
|
30
|
+
Column ID.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
int
|
|
35
|
+
Column index.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
return int(column_id[1:]) - 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TreeviewEx(Treeview): # pylint: disable=too-many-ancestors
|
|
42
|
+
"""Extended Treeview widget."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, master=None, **kwargs):
|
|
45
|
+
"""
|
|
46
|
+
Initialize the widget.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
master : widget, optional
|
|
51
|
+
Parent widget. The default is None.
|
|
52
|
+
**kwargs : dict
|
|
53
|
+
Additional options passed to tkinter.ttk.Treeview.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
None.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
# Initialization
|
|
61
|
+
self.readonly_rows = set() # Keep read-only row IDs
|
|
62
|
+
self.readonly_columns = set() # Keep read-only column IDs
|
|
63
|
+
self.readonly_cells = set() # Keep read-only cells as (row, col)
|
|
64
|
+
self.combobox_rows = set() # Keep row IDs that use a combobox
|
|
65
|
+
self.combobox_columns = set() # Keep column IDs that use a combobox
|
|
66
|
+
self.combobox_cells = set() # Keep combobox cells as (row, col)
|
|
67
|
+
self.combobox_row_values = {} # Map row IDs to combobox value lists
|
|
68
|
+
self.combobox_column_values = {} # Map columns to combobox value lists
|
|
69
|
+
self.combobox_cell_values = {} # Map cells to combobox value lists
|
|
70
|
+
|
|
71
|
+
# Other initialization
|
|
72
|
+
self.frame = Frame(master=master)
|
|
73
|
+
super().__init__(self.frame, **kwargs)
|
|
74
|
+
|
|
75
|
+
# Create the Entry widget as a member
|
|
76
|
+
self.entry = Entry(self)
|
|
77
|
+
self.entry.bind("<Return>", self._on_return)
|
|
78
|
+
self.entry.bind("<FocusOut>", self._on_focus_out)
|
|
79
|
+
self.entry.bind("<Escape>", self._on_escape)
|
|
80
|
+
|
|
81
|
+
# Create the Combobox widget as a member
|
|
82
|
+
self.combobox = Combobox(self)
|
|
83
|
+
self.combobox.bind("<Return>", self._on_return)
|
|
84
|
+
self.combobox.bind("<Escape>", self._on_escape)
|
|
85
|
+
self.combobox.bind("<<ComboboxSelected>>", self._on_combobox_selected)
|
|
86
|
+
|
|
87
|
+
# Create a vertical scrollbar and connect it
|
|
88
|
+
self.scrollbar_y = Scrollbar(
|
|
89
|
+
self.frame, orient=VERTICAL, command=self._on_scroll_y
|
|
90
|
+
)
|
|
91
|
+
self.configure(yscrollcommand=self.scrollbar_y.set)
|
|
92
|
+
|
|
93
|
+
# Create a horizontal scrollbar and connect it
|
|
94
|
+
self.scrollbar_x = Scrollbar(
|
|
95
|
+
self.frame, orient=HORIZONTAL, command=self._on_scroll_x
|
|
96
|
+
)
|
|
97
|
+
self.configure(xscrollcommand=self.scrollbar_x.set)
|
|
98
|
+
|
|
99
|
+
super().grid(row=0, column=0, sticky="nsew")
|
|
100
|
+
self.scrollbar_y.grid(row=0, column=1, sticky="ns")
|
|
101
|
+
self.scrollbar_x.grid(row=1, column=0, sticky="ew")
|
|
102
|
+
|
|
103
|
+
# Set frame row/column weights to adjust the layout
|
|
104
|
+
self.frame.grid_rowconfigure(0, weight=1)
|
|
105
|
+
self.frame.grid_columnconfigure(0, weight=1)
|
|
106
|
+
|
|
107
|
+
# Bind additional behavior for the <Double-1> event
|
|
108
|
+
self._additional_bind_double_click()
|
|
109
|
+
|
|
110
|
+
# Bind the mouse wheel event
|
|
111
|
+
self.bind("<MouseWheel>", self._on_mouse_wheel)
|
|
112
|
+
|
|
113
|
+
# Variables to keep editing state
|
|
114
|
+
self._editing_cell = None
|
|
115
|
+
self._editing_combobox_values = None # Values for active combobox edit
|
|
116
|
+
|
|
117
|
+
def _on_scroll_y(self, *args):
|
|
118
|
+
"""
|
|
119
|
+
Handle vertical scroll events.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
*args : tuple
|
|
124
|
+
Scrollbar callback arguments.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
None.
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
if self._editing_cell:
|
|
132
|
+
self.cancel_edit()
|
|
133
|
+
self.yview(*args)
|
|
134
|
+
|
|
135
|
+
def _on_scroll_x(self, *args):
|
|
136
|
+
"""
|
|
137
|
+
Handle horizontal scroll events.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
*args : tuple
|
|
142
|
+
Scrollbar callback arguments.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
None.
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
if self._editing_cell:
|
|
150
|
+
self.cancel_edit()
|
|
151
|
+
self.xview(*args)
|
|
152
|
+
|
|
153
|
+
def _on_mouse_wheel(self, event):
|
|
154
|
+
"""
|
|
155
|
+
Handle mouse wheel events.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
event : Event
|
|
160
|
+
Mouse wheel event.
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
None.
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
if self._editing_cell:
|
|
168
|
+
self.cancel_edit()
|
|
169
|
+
|
|
170
|
+
# Run vertical scrolling
|
|
171
|
+
self.yview_scroll(-1 * (event.delta // 120), "units")
|
|
172
|
+
|
|
173
|
+
def _additional_bind_double_click(self):
|
|
174
|
+
"""
|
|
175
|
+
Add a double-click handler.
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
None.
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
# Keep the existing <Double-1> binding and add this handler
|
|
183
|
+
super().bind("<Double-1>", self._combined_handler, add="+")
|
|
184
|
+
|
|
185
|
+
def _combined_handler(self, event: Event):
|
|
186
|
+
"""
|
|
187
|
+
Handle double-click events.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
event : Event
|
|
192
|
+
Event object.
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
None.
|
|
197
|
+
|
|
198
|
+
"""
|
|
199
|
+
self.on_double_click(event) # Additional behavior
|
|
200
|
+
|
|
201
|
+
def bind(
|
|
202
|
+
self,
|
|
203
|
+
sequence: str | None = None,
|
|
204
|
+
func: Callable | None = None,
|
|
205
|
+
add: bool | None = None,
|
|
206
|
+
) -> str:
|
|
207
|
+
"""
|
|
208
|
+
Override bind.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
sequence : str, optional
|
|
213
|
+
Same as the sequence argument of Treeview.bind().
|
|
214
|
+
func : Callable, optional
|
|
215
|
+
Same as the func argument of Treeview.bind().
|
|
216
|
+
add : bool, optional
|
|
217
|
+
Same as the add argument of Treeview.bind().
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
str
|
|
222
|
+
Same return value as Treeview.bind().
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
if sequence == "<Double-1>":
|
|
226
|
+
|
|
227
|
+
def combined_handler(event):
|
|
228
|
+
self.on_double_click(event)
|
|
229
|
+
if func:
|
|
230
|
+
func(event)
|
|
231
|
+
|
|
232
|
+
return super().bind(sequence, combined_handler, add=add)
|
|
233
|
+
else:
|
|
234
|
+
return super().bind(sequence, func, add=add)
|
|
235
|
+
|
|
236
|
+
def pack(self, **kwargs):
|
|
237
|
+
"""
|
|
238
|
+
Override pack.
|
|
239
|
+
|
|
240
|
+
Parameters
|
|
241
|
+
----------
|
|
242
|
+
**kwargs : dict
|
|
243
|
+
Keyword arguments for pack.
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
None.
|
|
248
|
+
|
|
249
|
+
"""
|
|
250
|
+
self.frame.pack(**kwargs)
|
|
251
|
+
|
|
252
|
+
def grid(self, **kwargs):
|
|
253
|
+
"""
|
|
254
|
+
Override grid.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
**kwargs : dict
|
|
259
|
+
Keyword arguments for grid.
|
|
260
|
+
|
|
261
|
+
Returns
|
|
262
|
+
-------
|
|
263
|
+
None.
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
self.frame.grid(**kwargs)
|
|
267
|
+
|
|
268
|
+
def column(self, column: str, option=None, **kw):
|
|
269
|
+
"""
|
|
270
|
+
Override column.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
column : str
|
|
275
|
+
Column ID.
|
|
276
|
+
option : str, optional
|
|
277
|
+
Column option. The default is None.
|
|
278
|
+
**kw : dict
|
|
279
|
+
Additional keyword arguments.
|
|
280
|
+
|
|
281
|
+
Returns
|
|
282
|
+
-------
|
|
283
|
+
Any
|
|
284
|
+
Return value from Treeview.column().
|
|
285
|
+
|
|
286
|
+
"""
|
|
287
|
+
if option is None and "stretch" not in kw:
|
|
288
|
+
kw["stretch"] = False
|
|
289
|
+
return super().column(column, option, **kw)
|
|
290
|
+
|
|
291
|
+
def get_clicked_cell_id_pair(self, event: Event) -> tuple:
|
|
292
|
+
"""
|
|
293
|
+
Get the cell IDs at the clicked position.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
event : Event
|
|
298
|
+
Click event.
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
-------
|
|
302
|
+
tuple
|
|
303
|
+
Pair of (row ID, column ID).
|
|
304
|
+
|
|
305
|
+
"""
|
|
306
|
+
cell_id_pair = ("", "")
|
|
307
|
+
region = self.identify_region(event.x, event.y)
|
|
308
|
+
if region != "cell":
|
|
309
|
+
return ("", "")
|
|
310
|
+
cell_id_pair = (
|
|
311
|
+
self.identify_row(event.y),
|
|
312
|
+
self.identify_column(event.x),
|
|
313
|
+
)
|
|
314
|
+
return cell_id_pair
|
|
315
|
+
|
|
316
|
+
def on_double_click(self, event: Event) -> None:
|
|
317
|
+
"""
|
|
318
|
+
Handle double-click action.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
event : Event
|
|
323
|
+
Event object.
|
|
324
|
+
|
|
325
|
+
Returns
|
|
326
|
+
-------
|
|
327
|
+
None.
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
cell_id_pair = self.get_clicked_cell_id_pair(event)
|
|
331
|
+
if cell_id_pair != ("", ""):
|
|
332
|
+
self.start_edit(cell_id_pair)
|
|
333
|
+
|
|
334
|
+
def get_cell_value(self, cell_id_pair: tuple) -> str:
|
|
335
|
+
"""
|
|
336
|
+
Get a cell value.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
cell_id_pair : tuple
|
|
341
|
+
Pair of (row ID, column ID).
|
|
342
|
+
|
|
343
|
+
Returns
|
|
344
|
+
-------
|
|
345
|
+
str
|
|
346
|
+
Cell value.
|
|
347
|
+
|
|
348
|
+
"""
|
|
349
|
+
row_id, column_id = cell_id_pair
|
|
350
|
+
return self.item(row_id, "values")[_colid2colindex(column_id)]
|
|
351
|
+
|
|
352
|
+
def start_edit(self, cell_id_pair: tuple) -> None:
|
|
353
|
+
"""Start editing a cell."""
|
|
354
|
+
if not self.is_valid_cell(cell_id_pair):
|
|
355
|
+
raise ValueError(f"Invalid cell specified: {cell_id_pair}")
|
|
356
|
+
|
|
357
|
+
row_id, column_id = cell_id_pair
|
|
358
|
+
|
|
359
|
+
cell_type = self._get_cell_type(cell_id_pair)
|
|
360
|
+
|
|
361
|
+
# Skip editing when the cell is read-only
|
|
362
|
+
if cell_type == CellType.READONLY:
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# Continue with edit processing
|
|
366
|
+
self._editing_cell = cell_id_pair
|
|
367
|
+
cell_value = self.get_cell_value(cell_id_pair)
|
|
368
|
+
|
|
369
|
+
# Get the cell position and size
|
|
370
|
+
bbox = self.bbox(row_id, column_id)
|
|
371
|
+
if not bbox:
|
|
372
|
+
raise ValueError(
|
|
373
|
+
f"Cannot determine the position of the cell: {cell_id_pair}"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
x, y, width, height = bbox
|
|
377
|
+
|
|
378
|
+
# For combobox cells
|
|
379
|
+
if cell_type == CellType.COMBOBOX:
|
|
380
|
+
# Keep the current value list
|
|
381
|
+
if cell_id_pair in self.combobox_cell_values:
|
|
382
|
+
self._editing_combobox_values = self.combobox_cell_values[
|
|
383
|
+
cell_id_pair
|
|
384
|
+
]
|
|
385
|
+
elif row_id in self.combobox_row_values:
|
|
386
|
+
self._editing_combobox_values = self.combobox_row_values[row_id]
|
|
387
|
+
elif column_id in self.combobox_column_values:
|
|
388
|
+
self._editing_combobox_values = self.combobox_column_values[
|
|
389
|
+
column_id
|
|
390
|
+
]
|
|
391
|
+
else:
|
|
392
|
+
self._editing_combobox_values = []
|
|
393
|
+
|
|
394
|
+
# Configure the Combobox widget
|
|
395
|
+
self.combobox.delete(0, "end")
|
|
396
|
+
self.combobox.insert(0, cell_value)
|
|
397
|
+
self.combobox["values"] = self._editing_combobox_values
|
|
398
|
+
|
|
399
|
+
self.combobox.place(x=x, y=y, width=width, height=height)
|
|
400
|
+
self.combobox.focus_set()
|
|
401
|
+
elif cell_type == CellType.ENTRY:
|
|
402
|
+
# Configure the Entry widget
|
|
403
|
+
self.entry.delete(0, "end")
|
|
404
|
+
self.entry.insert(0, cell_value)
|
|
405
|
+
self.entry.place(x=x, y=y, width=width, height=height)
|
|
406
|
+
self.entry.focus_set()
|
|
407
|
+
|
|
408
|
+
def is_valid_cell(self, cell_id_pair: tuple) -> bool:
|
|
409
|
+
"""
|
|
410
|
+
Check whether a cell exists.
|
|
411
|
+
|
|
412
|
+
Parameters
|
|
413
|
+
----------
|
|
414
|
+
cell_id_pair : tuple
|
|
415
|
+
Pair of (row ID, column ID).
|
|
416
|
+
|
|
417
|
+
Returns
|
|
418
|
+
-------
|
|
419
|
+
bool
|
|
420
|
+
True if the cell is valid, otherwise False.
|
|
421
|
+
|
|
422
|
+
"""
|
|
423
|
+
row_id, column_id = cell_id_pair
|
|
424
|
+
try:
|
|
425
|
+
col_index = _colid2colindex(column_id) # Convert column ID to index
|
|
426
|
+
except (ValueError, IndexError): # pragma: no cover
|
|
427
|
+
return False # pragma: no cover
|
|
428
|
+
|
|
429
|
+
if row_id not in self.get_children() or col_index >= len(
|
|
430
|
+
self["columns"]
|
|
431
|
+
):
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
return True
|
|
435
|
+
|
|
436
|
+
def _on_return(self, event): # pylint: disable=unused-argument
|
|
437
|
+
"""Handle the <Return> event."""
|
|
438
|
+
if self._editing_cell:
|
|
439
|
+
widget = event.widget
|
|
440
|
+
self.update_cell(self._editing_cell, widget)
|
|
441
|
+
|
|
442
|
+
def _on_focus_out(self, event): # pylint: disable=unused-argument
|
|
443
|
+
"""Handle the <FocusOut> event."""
|
|
444
|
+
if self._editing_cell:
|
|
445
|
+
widget = event.widget
|
|
446
|
+
self.update_cell(self._editing_cell, widget)
|
|
447
|
+
|
|
448
|
+
def _on_escape(self, event): # pylint: disable=unused-argument
|
|
449
|
+
"""Handle the <Escape> event."""
|
|
450
|
+
self.cancel_edit()
|
|
451
|
+
|
|
452
|
+
def _on_combobox_selected(self, event): # pylint: disable=unused-argument
|
|
453
|
+
"""Handle combobox selection events."""
|
|
454
|
+
if self._editing_cell:
|
|
455
|
+
widget = event.widget
|
|
456
|
+
self.update_cell(self._editing_cell, widget)
|
|
457
|
+
|
|
458
|
+
def _get_cell_type(self, cell_id_pair: tuple) -> CellType:
|
|
459
|
+
"""
|
|
460
|
+
Determine a cell type.
|
|
461
|
+
|
|
462
|
+
Parameters
|
|
463
|
+
----------
|
|
464
|
+
cell_id_pair : tuple
|
|
465
|
+
Pair of (row ID, column ID).
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
CellType
|
|
470
|
+
CellType.READONLY, CellType.COMBOBOX, or CellType.ENTRY.
|
|
471
|
+
|
|
472
|
+
"""
|
|
473
|
+
row_id, column_id = cell_id_pair
|
|
474
|
+
|
|
475
|
+
# Check read-only settings
|
|
476
|
+
if (
|
|
477
|
+
row_id in self.readonly_rows
|
|
478
|
+
or column_id in self.readonly_columns
|
|
479
|
+
or cell_id_pair in self.readonly_cells
|
|
480
|
+
):
|
|
481
|
+
return CellType.READONLY
|
|
482
|
+
|
|
483
|
+
# Check combobox settings
|
|
484
|
+
if (
|
|
485
|
+
row_id in self.combobox_rows
|
|
486
|
+
or column_id in self.combobox_columns
|
|
487
|
+
or cell_id_pair in self.combobox_cells
|
|
488
|
+
):
|
|
489
|
+
return CellType.COMBOBOX
|
|
490
|
+
|
|
491
|
+
return CellType.ENTRY
|
|
492
|
+
|
|
493
|
+
def update_cell(
|
|
494
|
+
self, cell_id_pair: tuple, widget: Union[Entry, Combobox]
|
|
495
|
+
) -> None:
|
|
496
|
+
"""Update a cell value."""
|
|
497
|
+
if not self.is_valid_cell(cell_id_pair):
|
|
498
|
+
raise ValueError(f"Invalid cell specified: {cell_id_pair}")
|
|
499
|
+
|
|
500
|
+
cell_type = self._get_cell_type(cell_id_pair)
|
|
501
|
+
|
|
502
|
+
# Do not update when the cell is read-only
|
|
503
|
+
if cell_type == CellType.READONLY:
|
|
504
|
+
self.cancel_edit()
|
|
505
|
+
return
|
|
506
|
+
|
|
507
|
+
# Update value for ENTRY or COMBOBOX cells
|
|
508
|
+
if cell_type == CellType.ENTRY or cell_type == CellType.COMBOBOX:
|
|
509
|
+
# Get the new value
|
|
510
|
+
new_value = widget.get()
|
|
511
|
+
# Update only when the value changed
|
|
512
|
+
if new_value != self.get_cell_value(cell_id_pair):
|
|
513
|
+
values = list(self.item(cell_id_pair[0], "values"))
|
|
514
|
+
col_index = _colid2colindex(cell_id_pair[1])
|
|
515
|
+
values[col_index] = new_value
|
|
516
|
+
self.item(cell_id_pair[0], values=values)
|
|
517
|
+
|
|
518
|
+
self.cancel_edit()
|
|
519
|
+
|
|
520
|
+
def cancel_edit(self):
|
|
521
|
+
"""
|
|
522
|
+
Cancel editing.
|
|
523
|
+
|
|
524
|
+
Returns
|
|
525
|
+
-------
|
|
526
|
+
None.
|
|
527
|
+
|
|
528
|
+
"""
|
|
529
|
+
self.entry.place_forget() # Hide Entry
|
|
530
|
+
self.combobox.place_forget() # Hide Combobox
|
|
531
|
+
self._editing_cell = None
|
|
532
|
+
self._editing_combobox_values = None
|
|
533
|
+
|
|
534
|
+
def set_readonly_row(self, row_id: str, readonly: bool = True) -> None:
|
|
535
|
+
"""Set a row as read-only."""
|
|
536
|
+
if readonly:
|
|
537
|
+
self.readonly_rows.add(row_id)
|
|
538
|
+
else:
|
|
539
|
+
self.readonly_rows.discard(row_id)
|
|
540
|
+
|
|
541
|
+
def set_readonly_column(
|
|
542
|
+
self, column_id: str, readonly: bool = True
|
|
543
|
+
) -> None:
|
|
544
|
+
"""Set a column as read-only."""
|
|
545
|
+
if readonly:
|
|
546
|
+
self.readonly_columns.add(column_id)
|
|
547
|
+
else:
|
|
548
|
+
self.readonly_columns.discard(column_id)
|
|
549
|
+
|
|
550
|
+
def set_readonly_cell(
|
|
551
|
+
self, cell_id_pair: tuple, readonly: bool = True
|
|
552
|
+
) -> None:
|
|
553
|
+
"""Set a cell as read-only."""
|
|
554
|
+
if readonly:
|
|
555
|
+
self.readonly_cells.add(cell_id_pair)
|
|
556
|
+
else:
|
|
557
|
+
self.readonly_cells.discard(cell_id_pair)
|
|
558
|
+
|
|
559
|
+
def set_combobox_row(
|
|
560
|
+
self, row_id: str, values: list | None = None, is_combobox: bool = True
|
|
561
|
+
) -> None:
|
|
562
|
+
"""Set a row to use a combobox."""
|
|
563
|
+
if is_combobox:
|
|
564
|
+
self.combobox_rows.add(row_id)
|
|
565
|
+
if values is not None:
|
|
566
|
+
self.combobox_row_values[row_id] = values
|
|
567
|
+
else:
|
|
568
|
+
self.combobox_rows.discard(row_id)
|
|
569
|
+
self.combobox_row_values.pop(row_id, None)
|
|
570
|
+
|
|
571
|
+
def set_combobox_column(
|
|
572
|
+
self,
|
|
573
|
+
column_id: str,
|
|
574
|
+
values: list | None = None,
|
|
575
|
+
is_combobox: bool = True,
|
|
576
|
+
) -> None:
|
|
577
|
+
"""Set a column to use a combobox."""
|
|
578
|
+
if is_combobox:
|
|
579
|
+
self.combobox_columns.add(column_id)
|
|
580
|
+
if values is not None:
|
|
581
|
+
self.combobox_column_values[column_id] = values
|
|
582
|
+
else:
|
|
583
|
+
self.combobox_columns.discard(column_id)
|
|
584
|
+
self.combobox_column_values.pop(column_id, None)
|
|
585
|
+
|
|
586
|
+
def set_combobox_cell(
|
|
587
|
+
self,
|
|
588
|
+
cell_id_pair: tuple,
|
|
589
|
+
values: list | None = None,
|
|
590
|
+
is_combobox: bool = True,
|
|
591
|
+
) -> None:
|
|
592
|
+
"""Set a cell to use a combobox."""
|
|
593
|
+
if is_combobox:
|
|
594
|
+
self.combobox_cells.add(cell_id_pair)
|
|
595
|
+
if values is not None:
|
|
596
|
+
self.combobox_cell_values[cell_id_pair] = values
|
|
597
|
+
else:
|
|
598
|
+
self.combobox_cells.discard(cell_id_pair)
|
|
599
|
+
self.combobox_cell_values.pop(cell_id_pair, None)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: treeviewex
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A TreeView extension for Tkinter
|
|
5
|
+
Project-URL: Homepage, https://github.com/fangface-hub/treeviewex
|
|
6
|
+
Project-URL: Repository, https://github.com/fangface-hub/treeviewex
|
|
7
|
+
Project-URL: Issues, https://github.com/fangface-hub/treeviewex/issues
|
|
8
|
+
Author: fangface
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: tkinter,treeview,ui
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# TreeviewEx
|
|
16
|
+
|
|
17
|
+
[日本語](README_ja.md)
|
|
18
|
+
|
|
19
|
+
Treeview extension for Tkinter.
|
|
20
|
+
|
|
21
|
+
This package adds the following features to the standard tkinter Treeview:
|
|
22
|
+
|
|
23
|
+
- Vertical and horizontal scroll bars
|
|
24
|
+
- Cell editing
|
|
25
|
+
- Read-only settings for rows, columns, and cells
|
|
26
|
+
|
|
27
|
+
All other behavior is the same as the standard tkinter Treeview.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Testing
|
|
32
|
+
|
|
33
|
+
1. Install dependencies
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv sync --group dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
2. Run the tests
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv run pytest -q
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
3. Show coverage report
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv run coverage run -m pytest -q
|
|
49
|
+
uv run coverage report -m
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
4. Generate an HTML report (optional)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv run coverage html
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Build
|
|
61
|
+
|
|
62
|
+
1. Install dependencies
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv sync --group dev
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
2. Build the package
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv build
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
3. Check the generated wheel in the dist folder
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
1. Install the package
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uv pip install path/to/TreeviewEx.whl
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
2. Run the sample
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv run python sample/sample.py
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from treeviewex import TreeviewEx
|
|
96
|
+
from tkinter import Tk
|
|
97
|
+
|
|
98
|
+
root = Tk()
|
|
99
|
+
root.title("TreeviewEx Example")
|
|
100
|
+
|
|
101
|
+
treeview_ex = TreeviewEx(root)
|
|
102
|
+
treeview_ex.grid(row=0, column=0, sticky="nsew")
|
|
103
|
+
|
|
104
|
+
columns = ("col1", "col2", "col3", "col4")
|
|
105
|
+
treeview_ex["columns"] = columns
|
|
106
|
+
treeview_ex.heading("#0", text="", anchor="w")
|
|
107
|
+
treeview_ex.column("#0", width=0, stretch=False)
|
|
108
|
+
for col in columns:
|
|
109
|
+
treeview_ex.heading(col, text=f"{col.capitalize()}")
|
|
110
|
+
treeview_ex.column(col, width=100)
|
|
111
|
+
|
|
112
|
+
for i in range(100):
|
|
113
|
+
treeview_ex.insert(
|
|
114
|
+
"",
|
|
115
|
+
"end",
|
|
116
|
+
text="",
|
|
117
|
+
values=(f"Value {i}A", f"Value {i}B", f"Value {i}C", f"Value {i}D"),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
root.grid_rowconfigure(0, weight=1)
|
|
121
|
+
root.grid_columnconfigure(0, weight=1)
|
|
122
|
+
|
|
123
|
+
root.mainloop()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Public interfaces
|
|
129
|
+
|
|
130
|
+
### set_readonly_row(row_id: str, readonly: bool = True) -> None
|
|
131
|
+
|
|
132
|
+
Set the specified row to readonly.
|
|
133
|
+
|
|
134
|
+
### set_readonly_column(column_id: str, readonly: bool = True) -> None
|
|
135
|
+
|
|
136
|
+
Set the specified column to readonly.
|
|
137
|
+
|
|
138
|
+
### set_readonly_cell(cell_id_pair: tuple, readonly: bool = True) -> None
|
|
139
|
+
|
|
140
|
+
Set the specified cell to readonly.
|
|
141
|
+
|
|
142
|
+
### set_combobox_row(row_id: str, values: list = None, is_combobox: bool = True) -> None
|
|
143
|
+
|
|
144
|
+
Set the specified row to be editable with a Combobox.
|
|
145
|
+
|
|
146
|
+
### set_combobox_column(column_id: str, values: list = None, is_combobox: bool = True) -> None
|
|
147
|
+
|
|
148
|
+
Set the specified column to be editable with a Combobox.
|
|
149
|
+
|
|
150
|
+
### set_combobox_cell(cell_id_pair: tuple, values: list = None, is_combobox: bool = True) -> None
|
|
151
|
+
|
|
152
|
+
Set the specified cell to be editable with a Combobox.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
treeviewex/__init__.py,sha256=nBsuFVnD6AqRyBqmp-d8a6MG-lVR7IZLEGOOetCgXfg,86
|
|
2
|
+
treeviewex/treeviewex.py,sha256=XrYvhH9eNACXt8JAyykC8nT9EnQCOaH9PsuuuvmNg-s,16838
|
|
3
|
+
treeviewex-1.0.0.dist-info/METADATA,sha256=wf1_xn9tYHI8piyJ1l9YeP6mcEb-5ZA4YEMIANyUQbc,3089
|
|
4
|
+
treeviewex-1.0.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
5
|
+
treeviewex-1.0.0.dist-info/licenses/LICENSE,sha256=CIAHTomy63TLfdjZOibgqUQd6-IEmm7raWIxEqizNo8,1065
|
|
6
|
+
treeviewex-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 fangface
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|