CTkDataTable 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/ctk_data_table.py +258 -55
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/examples/basic_table.py +2 -1
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/table_column.py +1 -1
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/table_renderer.py +74 -53
- ctkdatatable-0.2.0/CTkDataTable.egg-info/PKG-INFO +384 -0
- ctkdatatable-0.2.0/PKG-INFO +384 -0
- ctkdatatable-0.2.0/README.md +352 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/pyproject.toml +5 -5
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/tests/test_ctk_data_table_gui.py +67 -11
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/tests/test_table_renderer.py +3 -2
- ctkdatatable-0.1.0/CTkDataTable.egg-info/PKG-INFO +0 -681
- ctkdatatable-0.1.0/PKG-INFO +0 -681
- ctkdatatable-0.1.0/README.md +0 -649
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/__init__.py +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/_utils.py +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/examples/__init__.py +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/py.typed +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/table_events.py +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/table_model.py +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/table_style.py +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable.egg-info/SOURCES.txt +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable.egg-info/dependency_links.txt +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable.egg-info/requires.txt +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable.egg-info/top_level.txt +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/LICENSE +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/setup.cfg +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/tests/test_table_column.py +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/tests/test_table_model.py +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/tests/test_table_style.py +0 -0
- {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/tests/test_utils.py +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"""A virtualized Canvas-rendered data table for CustomTkinter."""
|
|
2
|
+
# By Harry Gomm
|
|
3
|
+
# And OpenAI GPT 5.5
|
|
2
4
|
|
|
3
5
|
from __future__ import annotations
|
|
4
6
|
|
|
@@ -36,6 +38,7 @@ SummaryDefinition = str | Callable[[list[RowData]], Any]
|
|
|
36
38
|
AsyncFetchCallback = Callable[[], Iterable[Any]]
|
|
37
39
|
AsyncSuccessCallback = Callable[[list[dict[str, Any]]], None]
|
|
38
40
|
AsyncErrorCallback = Callable[[BaseException], None]
|
|
41
|
+
ColumnWidthMode = Literal["fixed", "fill"]
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
class CTkDataTable(ctk.CTkFrame):
|
|
@@ -56,6 +59,7 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
56
59
|
font: Any | None = None,
|
|
57
60
|
header_font: Any | None = None,
|
|
58
61
|
horizontal_scroll: bool = False,
|
|
62
|
+
column_width_mode: ColumnWidthMode = "fixed",
|
|
59
63
|
multi_select: bool = False,
|
|
60
64
|
searchable: bool = False,
|
|
61
65
|
search_delay_ms: int = 0,
|
|
@@ -116,19 +120,23 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
116
120
|
raise ValueError("search_delay_ms must not be negative.")
|
|
117
121
|
if min_column_width < 24:
|
|
118
122
|
raise ValueError("min_column_width must be at least 24 pixels.")
|
|
123
|
+
if column_width_mode not in {"fixed", "fill"}:
|
|
124
|
+
raise ValueError("column_width_mode must be 'fixed' or 'fill'.")
|
|
119
125
|
if not enable_style_hooks and (row_style is not None or cell_style is not None):
|
|
120
126
|
raise ValueError("Set enable_style_hooks=True before passing row_style or cell_style callbacks.")
|
|
121
127
|
|
|
122
128
|
self._columns = normalize_columns(columns)
|
|
123
129
|
self._model = TableModel(self._columns)
|
|
124
130
|
self._visible_columns_cache: list[TableColumn] = []
|
|
131
|
+
self._layout_columns_cache: list[TableColumn] = []
|
|
125
132
|
self._column_edges_cache: tuple[float, ...] = ()
|
|
126
133
|
self._column_edge_columns_cache: tuple[TableColumn, ...] = ()
|
|
127
|
-
self._total_table_width_cache = 0
|
|
134
|
+
self._total_table_width_cache = 0.0
|
|
128
135
|
self._row_height = row_height
|
|
129
136
|
self._header_height = header_height
|
|
130
137
|
self._footer_height = footer_height
|
|
131
138
|
self._horizontal_scroll_enabled = horizontal_scroll
|
|
139
|
+
self._column_width_mode = column_width_mode
|
|
132
140
|
self._multi_select = multi_select
|
|
133
141
|
self._searchable = searchable
|
|
134
142
|
self._search_delay_ms = search_delay_ms
|
|
@@ -176,13 +184,16 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
176
184
|
self._y_offset = 0.0
|
|
177
185
|
self._x_offset = 0.0
|
|
178
186
|
|
|
179
|
-
self.
|
|
187
|
+
self._font_source = font if font is not None else self._make_font(size=13)
|
|
180
188
|
if header_font is not None:
|
|
181
|
-
self.
|
|
189
|
+
self._header_font_source = header_font
|
|
182
190
|
elif font is not None:
|
|
183
|
-
self.
|
|
191
|
+
self._header_font_source = font
|
|
184
192
|
else:
|
|
185
|
-
self.
|
|
193
|
+
self._header_font_source = self._make_font(size=13, weight="bold")
|
|
194
|
+
self._font = self._font_source
|
|
195
|
+
self._header_font = self._header_font_source
|
|
196
|
+
self._font_callback_sources: list[Any] = []
|
|
186
197
|
self._refresh_column_cache()
|
|
187
198
|
|
|
188
199
|
self.grid_columnconfigure(0, weight=1)
|
|
@@ -200,7 +211,14 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
200
211
|
else:
|
|
201
212
|
self.grid_rowconfigure(0, weight=1)
|
|
202
213
|
|
|
203
|
-
self._table_canvas = tk.Canvas(
|
|
214
|
+
self._table_canvas = tk.Canvas(
|
|
215
|
+
self,
|
|
216
|
+
bd=0,
|
|
217
|
+
highlightthickness=0,
|
|
218
|
+
takefocus=True,
|
|
219
|
+
width=self._scale_dimension(self._desired_width),
|
|
220
|
+
height=self._scale_dimension(self._desired_height),
|
|
221
|
+
)
|
|
204
222
|
self._table_canvas.grid(row=canvas_row, column=0, sticky="nsew")
|
|
205
223
|
|
|
206
224
|
self._vertical_scrollbar = ctk.CTkScrollbar(
|
|
@@ -219,6 +237,8 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
219
237
|
)
|
|
220
238
|
self._horizontal_scrollbar.grid(row=canvas_row + 1, column=0, sticky="ew", padx=2, pady=(0, 2))
|
|
221
239
|
|
|
240
|
+
self._refresh_scaled_fonts()
|
|
241
|
+
self._register_font_callbacks()
|
|
222
242
|
self._renderer = TableRenderer(self._table_canvas, self._font, self._header_font, self._resolve_color)
|
|
223
243
|
self._apply_renderer_style()
|
|
224
244
|
self._apply_layout_insets()
|
|
@@ -264,9 +284,27 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
264
284
|
|
|
265
285
|
return self._model.require_column(column_key).width
|
|
266
286
|
|
|
287
|
+
def set_column_width_mode(self, mode: ColumnWidthMode) -> None:
|
|
288
|
+
"""Set how visible columns are laid out across the table width."""
|
|
289
|
+
|
|
290
|
+
if mode not in {"fixed", "fill"}:
|
|
291
|
+
raise ValueError("column_width_mode must be 'fixed' or 'fill'.")
|
|
292
|
+
if mode == self._column_width_mode:
|
|
293
|
+
return
|
|
294
|
+
self._column_width_mode = mode
|
|
295
|
+
self._refresh_column_cache()
|
|
296
|
+
self._clamp_offsets()
|
|
297
|
+
self._redraw(full=True)
|
|
298
|
+
|
|
299
|
+
def get_column_width_mode(self) -> ColumnWidthMode:
|
|
300
|
+
"""Return the current column width layout mode."""
|
|
301
|
+
|
|
302
|
+
return self._column_width_mode
|
|
303
|
+
|
|
267
304
|
def refresh(self) -> None:
|
|
268
305
|
"""Refresh the current rendered view without changing data or selection."""
|
|
269
306
|
|
|
307
|
+
self._refresh_column_cache()
|
|
270
308
|
self._clamp_offsets()
|
|
271
309
|
self._redraw(full=True)
|
|
272
310
|
|
|
@@ -526,9 +564,90 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
526
564
|
font.configure(size=size, weight=weight)
|
|
527
565
|
return font
|
|
528
566
|
|
|
567
|
+
def _register_font_callbacks(self) -> None:
|
|
568
|
+
for font in (self._font_source, self._header_font_source):
|
|
569
|
+
already_registered = any(font is registered for registered in self._font_callback_sources)
|
|
570
|
+
if isinstance(font, ctk.CTkFont) and not already_registered:
|
|
571
|
+
font.add_size_configure_callback(self._handle_font_configure)
|
|
572
|
+
self._font_callback_sources.append(font)
|
|
573
|
+
|
|
574
|
+
def _remove_font_callbacks(self) -> None:
|
|
575
|
+
for font in list(self._font_callback_sources):
|
|
576
|
+
with suppress(ValueError):
|
|
577
|
+
font.remove_size_configure_callback(self._handle_font_configure)
|
|
578
|
+
self._font_callback_sources.clear()
|
|
579
|
+
|
|
580
|
+
def _handle_font_configure(self) -> None:
|
|
581
|
+
self._refresh_scaled_fonts()
|
|
582
|
+
if hasattr(self, "_renderer"):
|
|
583
|
+
self._renderer.configure_fonts(self._font, self._header_font)
|
|
584
|
+
self._redraw(full=True)
|
|
585
|
+
|
|
586
|
+
def _refresh_scaled_fonts(self) -> None:
|
|
587
|
+
if not hasattr(self, "_table_canvas"):
|
|
588
|
+
return
|
|
589
|
+
self._font = self._scaled_font(self._font_source)
|
|
590
|
+
self._header_font = self._scaled_font(self._header_font_source)
|
|
591
|
+
if hasattr(self, "_renderer"):
|
|
592
|
+
self._renderer.configure_fonts(self._font, self._header_font)
|
|
593
|
+
|
|
594
|
+
def _scaled_font(self, font: Any) -> Any:
|
|
595
|
+
if isinstance(font, (ctk.CTkFont, tuple)):
|
|
596
|
+
try:
|
|
597
|
+
return tkfont.Font(root=self._table_canvas, font=self._apply_font_scaling(font))
|
|
598
|
+
except Exception:
|
|
599
|
+
return font
|
|
600
|
+
if isinstance(font, tkfont.Font):
|
|
601
|
+
try:
|
|
602
|
+
options = font.actual()
|
|
603
|
+
size = int(font.cget("size"))
|
|
604
|
+
scaled_size = max(1, round(abs(size) * self._widget_scale()))
|
|
605
|
+
options["size"] = -scaled_size if size < 0 else scaled_size
|
|
606
|
+
return tkfont.Font(root=self._table_canvas, **options)
|
|
607
|
+
except Exception:
|
|
608
|
+
return font
|
|
609
|
+
return font
|
|
610
|
+
|
|
611
|
+
def _widget_scale(self) -> float:
|
|
612
|
+
try:
|
|
613
|
+
return max(0.01, float(self._get_widget_scaling()))
|
|
614
|
+
except Exception:
|
|
615
|
+
return 1.0
|
|
616
|
+
|
|
617
|
+
def _scale_dimension(self, value: float) -> float:
|
|
618
|
+
try:
|
|
619
|
+
return float(self._apply_widget_scaling(value))
|
|
620
|
+
except Exception:
|
|
621
|
+
return float(value)
|
|
622
|
+
|
|
623
|
+
def _unscale_dimension(self, value: float) -> float:
|
|
624
|
+
try:
|
|
625
|
+
return float(self._reverse_widget_scaling(value))
|
|
626
|
+
except Exception:
|
|
627
|
+
return float(value)
|
|
628
|
+
|
|
629
|
+
def _scaled_row_height(self) -> float:
|
|
630
|
+
return self._scale_dimension(self._row_height)
|
|
631
|
+
|
|
632
|
+
def _scaled_header_height(self) -> float:
|
|
633
|
+
return self._scale_dimension(self._header_height)
|
|
634
|
+
|
|
635
|
+
def _scaled_footer_height(self) -> float:
|
|
636
|
+
return self._scale_dimension(self._footer_height)
|
|
637
|
+
|
|
638
|
+
def _scaled_resize_hit_width(self) -> float:
|
|
639
|
+
return max(self._scale_dimension(self._resize_hit_width), 1.0)
|
|
640
|
+
|
|
641
|
+
def _scaled_table_corner_radius(self) -> float:
|
|
642
|
+
return self._scale_dimension(self._table_corner_radius())
|
|
643
|
+
|
|
644
|
+
def _scaled_table_border_width(self) -> float:
|
|
645
|
+
return self._scale_dimension(self._table_border_width())
|
|
646
|
+
|
|
529
647
|
def _handle_configure(self, event: tk.Event[Any]) -> None:
|
|
530
648
|
self._canvas_width = max(1, int(event.width))
|
|
531
649
|
self._canvas_height = max(1, int(event.height))
|
|
650
|
+
self._refresh_column_cache()
|
|
532
651
|
self._clamp_offsets()
|
|
533
652
|
self._redraw(full=True)
|
|
534
653
|
|
|
@@ -540,7 +659,8 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
540
659
|
elif args[0] == "scroll" and len(args) >= 3:
|
|
541
660
|
amount = int(args[1])
|
|
542
661
|
unit = args[2]
|
|
543
|
-
|
|
662
|
+
row_height = self._scaled_row_height()
|
|
663
|
+
step = row_height if unit == "units" else max(row_height, self._body_height())
|
|
544
664
|
self._set_y_offset(self._y_offset + amount * step)
|
|
545
665
|
|
|
546
666
|
def _handle_horizontal_scrollbar(self, *args: str) -> None:
|
|
@@ -551,7 +671,7 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
551
671
|
elif args[0] == "scroll" and len(args) >= 3:
|
|
552
672
|
amount = int(args[1])
|
|
553
673
|
unit = args[2]
|
|
554
|
-
step = 40 if unit == "units" else max(80, self._canvas_width)
|
|
674
|
+
step = self._scale_dimension(40) if unit == "units" else max(self._scale_dimension(80), self._canvas_width)
|
|
555
675
|
self._set_x_offset(self._x_offset + amount * step)
|
|
556
676
|
|
|
557
677
|
def _handle_mousewheel(self, event: tk.Event[Any]) -> str:
|
|
@@ -566,17 +686,18 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
566
686
|
units = -1 if delta > 0 else 1
|
|
567
687
|
|
|
568
688
|
if self._horizontal_scroll_enabled and getattr(event, "state", 0) & self._SHIFT_MASK:
|
|
569
|
-
self._set_x_offset(self._x_offset + units * 40)
|
|
689
|
+
self._set_x_offset(self._x_offset + units * self._scale_dimension(40))
|
|
570
690
|
else:
|
|
571
|
-
self._set_y_offset(self._y_offset + units * self.
|
|
691
|
+
self._set_y_offset(self._y_offset + units * self._scaled_row_height())
|
|
572
692
|
return "break"
|
|
573
693
|
|
|
574
694
|
def _handle_click(self, event: tk.Event[Any]) -> str | None:
|
|
575
695
|
self._table_canvas.focus_set()
|
|
576
|
-
if event.y < self.
|
|
696
|
+
if event.y < self._scaled_header_height():
|
|
577
697
|
resize_column = self._resize_column_from_x(event.x)
|
|
578
698
|
if resize_column is not None:
|
|
579
|
-
|
|
699
|
+
start_width = self._model.require_column(resize_column.key).width
|
|
700
|
+
self._resize_state = (resize_column.key, float(event.x), start_width)
|
|
580
701
|
return "break"
|
|
581
702
|
column = self._column_from_x(event.x)
|
|
582
703
|
if column is not None and column.sortable:
|
|
@@ -639,7 +760,8 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
639
760
|
if self._resize_state is None:
|
|
640
761
|
return None
|
|
641
762
|
column_key, start_x, start_width = self._resize_state
|
|
642
|
-
|
|
763
|
+
delta = self._unscale_dimension(float(event.x) - start_x)
|
|
764
|
+
width = max(self._min_column_width, round(start_width + delta))
|
|
643
765
|
self._set_column_width(column_key, width)
|
|
644
766
|
return "break"
|
|
645
767
|
|
|
@@ -676,7 +798,7 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
676
798
|
return lambda: self._invoke_context_action(row_event, action_key)
|
|
677
799
|
|
|
678
800
|
def _handle_double_click(self, event: tk.Event[Any]) -> str | None:
|
|
679
|
-
if event.y < self.
|
|
801
|
+
if event.y < self._scaled_header_height() or self._loading:
|
|
680
802
|
return "break"
|
|
681
803
|
action_region = self._hit_action(event.x, event.y)
|
|
682
804
|
if action_region is not None and action_region.kind == "checkbox":
|
|
@@ -700,7 +822,7 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
700
822
|
keysym = getattr(event, "keysym", "")
|
|
701
823
|
focused_view_index = self._model.focused_view_index()
|
|
702
824
|
|
|
703
|
-
page_size = max(1, self._body_height() // self.
|
|
825
|
+
page_size = max(1, int(self._body_height() // self._scaled_row_height()))
|
|
704
826
|
last_index = len(self._model.view_indices) - 1
|
|
705
827
|
target_index: int | None = None
|
|
706
828
|
|
|
@@ -781,7 +903,7 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
781
903
|
def _handle_motion(self, event: tk.Event[Any]) -> None:
|
|
782
904
|
if self._resize_state is not None:
|
|
783
905
|
return
|
|
784
|
-
if event.y < self.
|
|
906
|
+
if event.y < self._scaled_header_height():
|
|
785
907
|
self._update_header_cursor(self._resize_column_from_x(event.x))
|
|
786
908
|
else:
|
|
787
909
|
action_region = None if self._loading else self._hit_action(event.x, event.y)
|
|
@@ -841,7 +963,7 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
841
963
|
for index in {edge_index - 1, edge_index}:
|
|
842
964
|
if (
|
|
843
965
|
0 <= index < len(self._column_edges_cache)
|
|
844
|
-
and abs(content_x - self._column_edges_cache[index]) <= self.
|
|
966
|
+
and abs(content_x - self._column_edges_cache[index]) <= self._scaled_resize_hit_width()
|
|
845
967
|
):
|
|
846
968
|
return self._column_edge_columns_cache[index]
|
|
847
969
|
return None
|
|
@@ -1086,7 +1208,7 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1086
1208
|
self._renderer.draw_surface(
|
|
1087
1209
|
canvas_width=self._canvas_width,
|
|
1088
1210
|
canvas_height=self._canvas_height,
|
|
1089
|
-
radius=self.
|
|
1211
|
+
radius=self._scaled_table_corner_radius(),
|
|
1090
1212
|
colors=colors,
|
|
1091
1213
|
)
|
|
1092
1214
|
|
|
@@ -1094,8 +1216,8 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1094
1216
|
self._renderer.draw_chrome(
|
|
1095
1217
|
canvas_width=self._canvas_width,
|
|
1096
1218
|
canvas_height=self._canvas_height,
|
|
1097
|
-
radius=self.
|
|
1098
|
-
border_width=self.
|
|
1219
|
+
radius=self._scaled_table_corner_radius(),
|
|
1220
|
+
border_width=self._scaled_table_border_width(),
|
|
1099
1221
|
bottom_cap_height=self._bottom_cap_height(),
|
|
1100
1222
|
colors=colors,
|
|
1101
1223
|
)
|
|
@@ -1107,8 +1229,8 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1107
1229
|
self._visible_columns(),
|
|
1108
1230
|
x_offset=self._x_offset,
|
|
1109
1231
|
canvas_width=self._canvas_width,
|
|
1110
|
-
header_height=self.
|
|
1111
|
-
radius=self.
|
|
1232
|
+
header_height=self._scaled_header_height(),
|
|
1233
|
+
radius=self._scaled_table_corner_radius(),
|
|
1112
1234
|
sort_key=sort_key,
|
|
1113
1235
|
sort_ascending=sort_ascending,
|
|
1114
1236
|
filtered_column_keys=set(self._model.column_filters),
|
|
@@ -1125,8 +1247,8 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1125
1247
|
x_offset=self._x_offset,
|
|
1126
1248
|
canvas_width=self._canvas_width,
|
|
1127
1249
|
footer_top=self._footer_top(),
|
|
1128
|
-
footer_height=self.
|
|
1129
|
-
radius=self.
|
|
1250
|
+
footer_height=self._scaled_footer_height(),
|
|
1251
|
+
radius=self._scaled_table_corner_radius(),
|
|
1130
1252
|
colors=colors,
|
|
1131
1253
|
)
|
|
1132
1254
|
self._table_canvas.tag_raise("footer")
|
|
@@ -1174,7 +1296,7 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1174
1296
|
row,
|
|
1175
1297
|
self._visible_columns(),
|
|
1176
1298
|
y=self._row_y(row_index),
|
|
1177
|
-
row_height=self.
|
|
1299
|
+
row_height=self._scaled_row_height(),
|
|
1178
1300
|
x_offset=self._x_offset,
|
|
1179
1301
|
canvas_width=self._canvas_width,
|
|
1180
1302
|
selected=source_index in self._model.selected_source_indices,
|
|
@@ -1197,7 +1319,7 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1197
1319
|
):
|
|
1198
1320
|
message = "No matching records"
|
|
1199
1321
|
|
|
1200
|
-
body_top = self.
|
|
1322
|
+
body_top = self._scaled_header_height()
|
|
1201
1323
|
body_height = self._body_height()
|
|
1202
1324
|
self._table_canvas.create_rectangle(
|
|
1203
1325
|
0,
|
|
@@ -1210,12 +1332,14 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1210
1332
|
)
|
|
1211
1333
|
center_y = body_top + body_height / 2
|
|
1212
1334
|
if self._loading:
|
|
1213
|
-
indicator_width = min(160, max(80, self._canvas_width * 0.3))
|
|
1335
|
+
indicator_width = min(self._scale_dimension(160), max(self._scale_dimension(80), self._canvas_width * 0.3))
|
|
1336
|
+
indicator_top = center_y + self._scale_dimension(20)
|
|
1337
|
+
indicator_height = self._scale_dimension(4)
|
|
1214
1338
|
self._table_canvas.create_rectangle(
|
|
1215
1339
|
(self._canvas_width - indicator_width) / 2,
|
|
1216
|
-
|
|
1340
|
+
indicator_top,
|
|
1217
1341
|
(self._canvas_width + indicator_width) / 2,
|
|
1218
|
-
|
|
1342
|
+
indicator_top + indicator_height,
|
|
1219
1343
|
fill=colors["loading_indicator"],
|
|
1220
1344
|
outline="",
|
|
1221
1345
|
tags=("state", "loading_indicator"),
|
|
@@ -1268,16 +1392,18 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1268
1392
|
body_height = self._body_height()
|
|
1269
1393
|
if body_height <= 0 or not self._model.view_indices:
|
|
1270
1394
|
return range(0)
|
|
1271
|
-
|
|
1272
|
-
|
|
1395
|
+
row_height = self._scaled_row_height()
|
|
1396
|
+
start = max(0, int(self._y_offset // row_height))
|
|
1397
|
+
end = min(len(self._model.view_indices), int((self._y_offset + body_height) // row_height))
|
|
1273
1398
|
return range(start, end)
|
|
1274
1399
|
|
|
1275
1400
|
def _row_y(self, row_index: int) -> float:
|
|
1276
|
-
return self.
|
|
1401
|
+
return self._scaled_header_height() + row_index * self._scaled_row_height() - self._y_offset
|
|
1277
1402
|
|
|
1278
1403
|
def _scroll_view_index_into_view(self, view_index: int) -> None:
|
|
1279
|
-
|
|
1280
|
-
|
|
1404
|
+
row_height = self._scaled_row_height()
|
|
1405
|
+
row_top = view_index * row_height
|
|
1406
|
+
row_bottom = row_top + row_height
|
|
1281
1407
|
viewport_top = self._y_offset
|
|
1282
1408
|
viewport_bottom = self._y_offset + self._body_height()
|
|
1283
1409
|
|
|
@@ -1287,10 +1413,12 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1287
1413
|
self._set_y_offset(float(row_bottom - self._body_height()))
|
|
1288
1414
|
|
|
1289
1415
|
def _row_index_from_y(self, y: float) -> int | None:
|
|
1290
|
-
|
|
1416
|
+
header_height = self._scaled_header_height()
|
|
1417
|
+
row_height = self._scaled_row_height()
|
|
1418
|
+
if y < header_height or y > self._footer_top():
|
|
1291
1419
|
return None
|
|
1292
|
-
row_index = int((y -
|
|
1293
|
-
row_bottom = (row_index + 1) *
|
|
1420
|
+
row_index = int((y - header_height + self._y_offset) // row_height)
|
|
1421
|
+
row_bottom = (row_index + 1) * row_height
|
|
1294
1422
|
if row_bottom > self._y_offset + self._body_height():
|
|
1295
1423
|
return None
|
|
1296
1424
|
if 0 <= row_index < len(self._model.view_indices):
|
|
@@ -1313,19 +1441,58 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1313
1441
|
return None
|
|
1314
1442
|
|
|
1315
1443
|
def _visible_columns(self) -> list[TableColumn]:
|
|
1316
|
-
return self.
|
|
1444
|
+
return self._layout_columns_cache
|
|
1317
1445
|
|
|
1318
1446
|
def _refresh_column_cache(self) -> None:
|
|
1319
1447
|
visible_columns = [column for column in self._columns if column.visible]
|
|
1448
|
+
layout_widths = self._layout_column_widths(visible_columns)
|
|
1449
|
+
layout_columns = [
|
|
1450
|
+
replace(column, width=max(0, int(round(width))))
|
|
1451
|
+
for column, width in zip(visible_columns, layout_widths, strict=True)
|
|
1452
|
+
]
|
|
1320
1453
|
edges: list[float] = []
|
|
1321
1454
|
cursor = 0.0
|
|
1322
|
-
for
|
|
1323
|
-
cursor +=
|
|
1455
|
+
for width in layout_widths:
|
|
1456
|
+
cursor += width
|
|
1324
1457
|
edges.append(cursor)
|
|
1325
1458
|
self._visible_columns_cache = visible_columns
|
|
1459
|
+
self._layout_columns_cache = layout_columns
|
|
1326
1460
|
self._column_edges_cache = tuple(edges)
|
|
1327
1461
|
self._column_edge_columns_cache = tuple(visible_columns)
|
|
1328
|
-
self._total_table_width_cache =
|
|
1462
|
+
self._total_table_width_cache = cursor
|
|
1463
|
+
|
|
1464
|
+
def _layout_column_widths(self, visible_columns: Sequence[TableColumn]) -> list[float]:
|
|
1465
|
+
preferred_widths = [self._scale_dimension(column.width) for column in visible_columns]
|
|
1466
|
+
if not preferred_widths:
|
|
1467
|
+
return []
|
|
1468
|
+
if self._column_width_mode == "fixed" or self._canvas_width <= 1:
|
|
1469
|
+
return preferred_widths
|
|
1470
|
+
|
|
1471
|
+
viewport_width = float(self._canvas_width)
|
|
1472
|
+
preferred_total = sum(preferred_widths)
|
|
1473
|
+
if preferred_total <= 0:
|
|
1474
|
+
return [viewport_width / len(preferred_widths) for _column in preferred_widths]
|
|
1475
|
+
|
|
1476
|
+
if preferred_total < viewport_width:
|
|
1477
|
+
extra = viewport_width - preferred_total
|
|
1478
|
+
return [width + extra * (width / preferred_total) for width in preferred_widths]
|
|
1479
|
+
|
|
1480
|
+
min_width = self._scale_dimension(self._min_column_width)
|
|
1481
|
+
minimum_widths = [min(width, min_width) for width in preferred_widths]
|
|
1482
|
+
minimum_total = sum(minimum_widths)
|
|
1483
|
+
if minimum_total >= viewport_width:
|
|
1484
|
+
return minimum_widths
|
|
1485
|
+
|
|
1486
|
+
shrink = preferred_total - viewport_width
|
|
1487
|
+
capacities = [preferred - minimum for preferred, minimum in zip(preferred_widths, minimum_widths, strict=True)]
|
|
1488
|
+
total_capacity = sum(capacities)
|
|
1489
|
+
if total_capacity <= 0:
|
|
1490
|
+
return preferred_widths
|
|
1491
|
+
|
|
1492
|
+
return [
|
|
1493
|
+
preferred - shrink * (capacity / total_capacity)
|
|
1494
|
+
for preferred, capacity in zip(preferred_widths, capacities, strict=True)
|
|
1495
|
+
]
|
|
1329
1496
|
|
|
1330
1497
|
def _set_y_offset(self, value: float) -> None:
|
|
1331
1498
|
old_offset = self._y_offset
|
|
@@ -1351,7 +1518,7 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1351
1518
|
0,
|
|
1352
1519
|
0,
|
|
1353
1520
|
max(self._canvas_width, self._total_table_width()),
|
|
1354
|
-
self.
|
|
1521
|
+
self._scaled_header_height() + self._total_body_height() + self._active_footer_height(),
|
|
1355
1522
|
)
|
|
1356
1523
|
)
|
|
1357
1524
|
|
|
@@ -1375,18 +1542,21 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1375
1542
|
self._horizontal_scrollbar.set(first, last)
|
|
1376
1543
|
|
|
1377
1544
|
def _body_height(self) -> int:
|
|
1378
|
-
return max(0, self._footer_top() - self.
|
|
1545
|
+
return max(0, int(self._footer_top() - self._scaled_header_height()))
|
|
1379
1546
|
|
|
1380
|
-
def _active_footer_height(self) ->
|
|
1381
|
-
return self.
|
|
1547
|
+
def _active_footer_height(self) -> float:
|
|
1548
|
+
return self._scaled_footer_height() if self._footer_enabled else 0.0
|
|
1382
1549
|
|
|
1383
|
-
def _footer_top(self) ->
|
|
1384
|
-
return max(
|
|
1550
|
+
def _footer_top(self) -> float:
|
|
1551
|
+
return max(
|
|
1552
|
+
self._scaled_header_height(),
|
|
1553
|
+
self._canvas_height - self._active_footer_height() - self._bottom_cap_height(),
|
|
1554
|
+
)
|
|
1385
1555
|
|
|
1386
|
-
def _total_body_height(self) ->
|
|
1387
|
-
return len(self._model.view_indices) * self.
|
|
1556
|
+
def _total_body_height(self) -> float:
|
|
1557
|
+
return len(self._model.view_indices) * self._scaled_row_height()
|
|
1388
1558
|
|
|
1389
|
-
def _total_table_width(self) ->
|
|
1559
|
+
def _total_table_width(self) -> float:
|
|
1390
1560
|
return self._total_table_width_cache
|
|
1391
1561
|
|
|
1392
1562
|
def _max_y_offset(self) -> float:
|
|
@@ -1409,9 +1579,15 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1409
1579
|
return
|
|
1410
1580
|
scrollbar_gap = self._scrollbar_gap()
|
|
1411
1581
|
self._table_canvas.grid_configure(padx=0, pady=0)
|
|
1412
|
-
self._vertical_scrollbar.grid_configure(
|
|
1582
|
+
self._vertical_scrollbar.grid_configure(
|
|
1583
|
+
padx=(self._scale_dimension(scrollbar_gap), 0),
|
|
1584
|
+
pady=self._scale_dimension(4),
|
|
1585
|
+
)
|
|
1413
1586
|
if self._horizontal_scrollbar is not None:
|
|
1414
|
-
self._horizontal_scrollbar.grid_configure(
|
|
1587
|
+
self._horizontal_scrollbar.grid_configure(
|
|
1588
|
+
padx=(0, self._scale_dimension(scrollbar_gap)),
|
|
1589
|
+
pady=(self._scale_dimension(scrollbar_gap), 0),
|
|
1590
|
+
)
|
|
1415
1591
|
|
|
1416
1592
|
def _scrollbar_gap(self) -> int:
|
|
1417
1593
|
radius = self._table_corner_radius()
|
|
@@ -1419,13 +1595,13 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1419
1595
|
return 0
|
|
1420
1596
|
return max(4, min(8, math.ceil(radius / 2)))
|
|
1421
1597
|
|
|
1422
|
-
def _bottom_cap_height(self) ->
|
|
1598
|
+
def _bottom_cap_height(self) -> float:
|
|
1423
1599
|
if self._footer_enabled:
|
|
1424
1600
|
return 0
|
|
1425
|
-
radius = self.
|
|
1601
|
+
radius = self._scaled_table_corner_radius()
|
|
1426
1602
|
if radius <= 0:
|
|
1427
1603
|
return 0
|
|
1428
|
-
return max(2, min(6, math.ceil(radius / 3)))
|
|
1604
|
+
return max(self._scale_dimension(2), min(self._scale_dimension(6), math.ceil(radius / 3)))
|
|
1429
1605
|
|
|
1430
1606
|
def _apply_frame_style(self) -> None:
|
|
1431
1607
|
self.configure(corner_radius=0, border_width=0, fg_color="transparent")
|
|
@@ -1442,6 +1618,7 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1442
1618
|
progress_radius=self._style_number("progress_radius"),
|
|
1443
1619
|
pill_radius=self._style_number("pill_radius"),
|
|
1444
1620
|
action_radius=self._style_number("action_radius"),
|
|
1621
|
+
scale=self._widget_scale(),
|
|
1445
1622
|
)
|
|
1446
1623
|
|
|
1447
1624
|
def _style_number(self, key: str) -> float | None:
|
|
@@ -1649,6 +1826,28 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1649
1826
|
except tk.TclError:
|
|
1650
1827
|
return (127, 127, 127)
|
|
1651
1828
|
|
|
1829
|
+
def _set_scaling(self, *args: Any, **kwargs: Any) -> None:
|
|
1830
|
+
old_scale = self._widget_scale()
|
|
1831
|
+
logical_y_offset = self._y_offset / old_scale if hasattr(self, "_y_offset") else 0.0
|
|
1832
|
+
logical_x_offset = self._x_offset / old_scale if hasattr(self, "_x_offset") else 0.0
|
|
1833
|
+
|
|
1834
|
+
super()._set_scaling(*args, **kwargs)
|
|
1835
|
+
|
|
1836
|
+
if not hasattr(self, "_table_canvas"):
|
|
1837
|
+
return
|
|
1838
|
+
self._table_canvas.configure(
|
|
1839
|
+
width=self._scale_dimension(self._desired_width),
|
|
1840
|
+
height=self._scale_dimension(self._desired_height),
|
|
1841
|
+
)
|
|
1842
|
+
self._refresh_scaled_fonts()
|
|
1843
|
+
self._apply_renderer_style()
|
|
1844
|
+
self._apply_layout_insets()
|
|
1845
|
+
self._refresh_column_cache()
|
|
1846
|
+
self._y_offset = logical_y_offset * self._widget_scale()
|
|
1847
|
+
self._x_offset = logical_x_offset * self._widget_scale()
|
|
1848
|
+
self._clamp_offsets()
|
|
1849
|
+
self._redraw(full=True)
|
|
1850
|
+
|
|
1652
1851
|
def _set_appearance_mode(self, mode_string: str) -> None:
|
|
1653
1852
|
try:
|
|
1654
1853
|
super()._set_appearance_mode(mode_string)
|
|
@@ -1657,3 +1856,7 @@ class CTkDataTable(ctk.CTkFrame):
|
|
|
1657
1856
|
self._theme_colors_cache = None
|
|
1658
1857
|
if hasattr(self, "_table_canvas"):
|
|
1659
1858
|
self._redraw(full=True)
|
|
1859
|
+
|
|
1860
|
+
def destroy(self) -> None:
|
|
1861
|
+
self._remove_font_callbacks()
|
|
1862
|
+
super().destroy()
|