scustomtkinter 0.2.1__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.
- scustomtkinter/__init__.py +81 -0
- scustomtkinter/assets/images/error.png +0 -0
- scustomtkinter/assets/images/info.png +0 -0
- scustomtkinter/assets/images/warning.png +0 -0
- scustomtkinter/assets/sCTkThemes.json +1963 -0
- scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
- scustomtkinter/sctk_button_primary.py +366 -0
- scustomtkinter/sctk_button_secondary.py +354 -0
- scustomtkinter/sctk_button_tertiary.py +375 -0
- scustomtkinter/sctk_checkbox.py +270 -0
- scustomtkinter/sctk_combobox.py +155 -0
- scustomtkinter/sctk_core.py +15 -0
- scustomtkinter/sctk_dial.py +1472 -0
- scustomtkinter/sctk_dialog.py +701 -0
- scustomtkinter/sctk_dialog_toplevel.py +268 -0
- scustomtkinter/sctk_entry_primary.py +400 -0
- scustomtkinter/sctk_entry_secondary.py +342 -0
- scustomtkinter/sctk_file_explorer.py +879 -0
- scustomtkinter/sctk_frame.py +210 -0
- scustomtkinter/sctk_frame_labeled_primary.py +667 -0
- scustomtkinter/sctk_frame_labeled_secondary.py +598 -0
- scustomtkinter/sctk_frequency_display.py +636 -0
- scustomtkinter/sctk_label_primary.py +283 -0
- scustomtkinter/sctk_label_secondary.py +275 -0
- scustomtkinter/sctk_label_tertiary.py +278 -0
- scustomtkinter/sctk_messagebox.py +275 -0
- scustomtkinter/sctk_notebook.py +1103 -0
- scustomtkinter/sctk_optionmenu_border_mixin.py +203 -0
- scustomtkinter/sctk_optionmenu_primary.py +378 -0
- scustomtkinter/sctk_optionmenu_secondary.py +346 -0
- scustomtkinter/sctk_path_chooser.py +516 -0
- scustomtkinter/sctk_progress_bar.py +274 -0
- scustomtkinter/sctk_radiobutton.py +360 -0
- scustomtkinter/sctk_scroll_mixin.py +546 -0
- scustomtkinter/sctk_scrollable_frame.py +795 -0
- scustomtkinter/sctk_scrollbar.py +313 -0
- scustomtkinter/sctk_segmentedbutton.py +424 -0
- scustomtkinter/sctk_selector.py +581 -0
- scustomtkinter/sctk_separator.py +308 -0
- scustomtkinter/sctk_slider.py +264 -0
- scustomtkinter/sctk_smeter.py +405 -0
- scustomtkinter/sctk_smeter_bar.py +557 -0
- scustomtkinter/sctk_spinbox.py +664 -0
- scustomtkinter/sctk_switch.py +463 -0
- scustomtkinter/sctk_tableview.py +869 -0
- scustomtkinter/sctk_tabview.py +409 -0
- scustomtkinter/sctk_textbox_primary.py +291 -0
- scustomtkinter/sctk_textbox_secondary.py +225 -0
- scustomtkinter/sctk_toplevel.py +153 -0
- scustomtkinter/themeable_widget.py +938 -0
- scustomtkinter-0.2.1.dist-info/METADATA +336 -0
- scustomtkinter-0.2.1.dist-info/RECORD +101 -0
- scustomtkinter-0.2.1.dist-info/WHEEL +5 -0
- scustomtkinter-0.2.1.dist-info/entry_points.txt +2 -0
- scustomtkinter-0.2.1.dist-info/licenses/LICENSE +21 -0
- scustomtkinter-0.2.1.dist-info/licenses/scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
- scustomtkinter-0.2.1.dist-info/top_level.txt +2 -0
- scustomtkinter_pygubu/__init__.py +1 -0
- scustomtkinter_pygubu/designer/__init__.py +0 -0
- scustomtkinter_pygubu/designer/plugin.py +1854 -0
- scustomtkinter_pygubu/designer/properties.py +247 -0
- scustomtkinter_pygubu/sCTkButtonPrimarybo.py +46 -0
- scustomtkinter_pygubu/sCTkButtonSecondarybo.py +45 -0
- scustomtkinter_pygubu/sCTkButtonTertiarybo.py +44 -0
- scustomtkinter_pygubu/sCTkCheckBoxbo.py +45 -0
- scustomtkinter_pygubu/sCTkComboBoxbo.py +45 -0
- scustomtkinter_pygubu/sCTkCorebo.py +120 -0
- scustomtkinter_pygubu/sCTkDialbo.py +371 -0
- scustomtkinter_pygubu/sCTkDialogbo.py +397 -0
- scustomtkinter_pygubu/sCTkEntryPrimarybo.py +100 -0
- scustomtkinter_pygubu/sCTkEntrySecondarybo.py +98 -0
- scustomtkinter_pygubu/sCTkFileExplorerbo.py +189 -0
- scustomtkinter_pygubu/sCTkFrameLabeledPrimarybo.py +69 -0
- scustomtkinter_pygubu/sCTkFrameLabeledSecondarybo.py +71 -0
- scustomtkinter_pygubu/sCTkFramebo.py +46 -0
- scustomtkinter_pygubu/sCTkFrequencyDisplaybo.py +175 -0
- scustomtkinter_pygubu/sCTkLabelPrimarybo.py +45 -0
- scustomtkinter_pygubu/sCTkLabelSecondarybo.py +44 -0
- scustomtkinter_pygubu/sCTkLabelTertiarybo.py +45 -0
- scustomtkinter_pygubu/sCTkNotebookbo.py +392 -0
- scustomtkinter_pygubu/sCTkOptionMenuPrimarybo.py +45 -0
- scustomtkinter_pygubu/sCTkOptionMenuSecondarybo.py +39 -0
- scustomtkinter_pygubu/sCTkPathChooserbo.py +204 -0
- scustomtkinter_pygubu/sCTkProgressBarbo.py +45 -0
- scustomtkinter_pygubu/sCTkRadioButtonbo.py +45 -0
- scustomtkinter_pygubu/sCTkSMeterBarbo.py +153 -0
- scustomtkinter_pygubu/sCTkSMeterbo.py +101 -0
- scustomtkinter_pygubu/sCTkScrollableFramebo.py +66 -0
- scustomtkinter_pygubu/sCTkScrollbarbo.py +48 -0
- scustomtkinter_pygubu/sCTkSegmentedButtonbo.py +47 -0
- scustomtkinter_pygubu/sCTkSelectorbo.py +167 -0
- scustomtkinter_pygubu/sCTkSeparatorbo.py +141 -0
- scustomtkinter_pygubu/sCTkSliderbo.py +45 -0
- scustomtkinter_pygubu/sCTkSpinboxbo.py +200 -0
- scustomtkinter_pygubu/sCTkSwitchbo.py +45 -0
- scustomtkinter_pygubu/sCTkTableviewbo.py +188 -0
- scustomtkinter_pygubu/sCTkTabviewbo.py +226 -0
- scustomtkinter_pygubu/sCTkTextboxPrimarybo.py +45 -0
- scustomtkinter_pygubu/sCTkTextboxSecondarybo.py +45 -0
- scustomtkinter_pygubu/sCTkToplevelbo.py +55 -0
- scustomtkinter_pygubu/sCTkWidgetSetForPygubuDesigner.py +141 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkOptionMenuBorderMixin
|
|
4
|
+
|
|
5
|
+
Gives a CTkOptionMenu subclass a real border.
|
|
6
|
+
|
|
7
|
+
WHY THIS EXISTS. Native ctk.CTkOptionMenu has no border option -- its
|
|
8
|
+
constructor accepts corner_radius and fg_color but neither border_width nor
|
|
9
|
+
border_color. That single gap is what forced sCTkOptionMenuSecondary to be a
|
|
10
|
+
COMPOSITE: a CTkFrame wrapping a plain option menu, because the frame could
|
|
11
|
+
draw the outline the option menu could not.
|
|
12
|
+
|
|
13
|
+
The composite cost more than it looked. Secondary's values, command and
|
|
14
|
+
variable live on the inner menu, so cget("values") on the widget itself
|
|
15
|
+
returned None while Primary returned a list -- two widgets meant to be
|
|
16
|
+
interchangeable, disagreeing. Every option CustomTkinter adds to CTkOptionMenu
|
|
17
|
+
has to be forwarded by hand, forever. And the widget's SHAPE was dictated by
|
|
18
|
+
one theme's colour choices: Secondary needs a border only because its fill is
|
|
19
|
+
nearly the same as the background it sits on. Swap the two themes and the
|
|
20
|
+
structure is wrong.
|
|
21
|
+
|
|
22
|
+
CustomTkinter can already draw this border. CTkOptionMenu._draw() calls
|
|
23
|
+
|
|
24
|
+
self._draw_engine.draw_rounded_rect_with_border_vertical_split(
|
|
25
|
+
width, height, corner_radius, 0, left_section_width)
|
|
26
|
+
|
|
27
|
+
with a hardcoded 0 where the border width belongs. The draw engine supports it
|
|
28
|
+
and tags the result 'border_parts_left' and 'border_parts_right'; the widget
|
|
29
|
+
simply never asks. This mixin asks.
|
|
30
|
+
|
|
31
|
+
UPSTREAM DEPENDENCY. This overrides a private method and calls a private draw
|
|
32
|
+
engine, which the rest of this library avoids -- sCTkTabview explicitly
|
|
33
|
+
declined to write CTkTabview's private spacing attributes for exactly this
|
|
34
|
+
reason. It is justified here because the alternative is two widgets with
|
|
35
|
+
different shapes and a forwarding list that grows forever. But it is a real
|
|
36
|
+
dependency on CustomTkinter internals and could break on an upstream release.
|
|
37
|
+
If it does, the symptom is a missing border, not a crash: _draw() falls back to
|
|
38
|
+
whatever the native widget drew.
|
|
39
|
+
"""
|
|
40
|
+
from typing import Any, Optional, Tuple, Union
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class sCTkOptionMenuBorderMixin:
|
|
44
|
+
"""
|
|
45
|
+
Adds `border_width` and `border_color` to a CTkOptionMenu subclass.
|
|
46
|
+
|
|
47
|
+
Use it before the native class in the base list, so this class's _draw()
|
|
48
|
+
runs first and can call super() to let the native one draw underneath:
|
|
49
|
+
|
|
50
|
+
class sCTkOptionMenuPrimary(sCTkOptionMenuBorderMixin,
|
|
51
|
+
ctk.CTkOptionMenu, ThemeableWidget):
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def _init_border(self, border_width=0, border_color=None):
|
|
55
|
+
"""
|
|
56
|
+
Records the border settings. Call from __init__ AFTER the native
|
|
57
|
+
constructor has run, since the first redraw needs the canvas.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
border_width: Thickness in pixels. 0 draws nothing, which is the
|
|
61
|
+
native widget's own appearance.
|
|
62
|
+
border_color: A colour, or a (light, dark) pair. None with a
|
|
63
|
+
non-zero width draws nothing, since there would be no colour
|
|
64
|
+
to draw in.
|
|
65
|
+
"""
|
|
66
|
+
self._border_width = float(border_width or 0)
|
|
67
|
+
self._border_color = border_color
|
|
68
|
+
self._redraw_border()
|
|
69
|
+
|
|
70
|
+
def _draw(self, no_color_updates=False):
|
|
71
|
+
"""
|
|
72
|
+
Draws the widget, with a border when one is set.
|
|
73
|
+
|
|
74
|
+
REPLACES ctk.CTkOptionMenu._draw() rather than extending it. That is a
|
|
75
|
+
deliberate fork of a private method, and the reason is in the draw
|
|
76
|
+
engine: draw_rounded_rect_with_border_vertical_split() only insets the
|
|
77
|
+
inner fill when border_width > 0 --
|
|
78
|
+
|
|
79
|
+
if corner_radius >= border_width:
|
|
80
|
+
inner_corner_radius = corner_radius - border_width
|
|
81
|
+
|
|
82
|
+
-- and CTkOptionMenu calls it with a hardcoded 0. The fill therefore
|
|
83
|
+
covers the full area, and a border drawn afterwards is painted over no
|
|
84
|
+
matter how it is stacked or raised. Calling the engine a second time
|
|
85
|
+
does not help either: the inner parts from the first call are still
|
|
86
|
+
there at full size.
|
|
87
|
+
|
|
88
|
+
One call at the real border width draws both parts correctly.
|
|
89
|
+
|
|
90
|
+
With no border set this defers to the native method, so the widget
|
|
91
|
+
behaves exactly as CustomTkinter's when the feature is unused.
|
|
92
|
+
|
|
93
|
+
UPSTREAM DEPENDENCY. This reproduces the body of a private method. If
|
|
94
|
+
CustomTkinter changes CTkOptionMenu._draw(), we do not inherit the
|
|
95
|
+
change. Contained to this one method, and the failure mode is visual.
|
|
96
|
+
"""
|
|
97
|
+
if not getattr(self, "_border_width", 0) or self._border_color is None:
|
|
98
|
+
super()._draw(no_color_updates)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
left_section_width = self._current_width - self._current_height
|
|
102
|
+
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border_vertical_split(
|
|
103
|
+
self._apply_widget_scaling(self._current_width),
|
|
104
|
+
self._apply_widget_scaling(self._current_height),
|
|
105
|
+
self._apply_widget_scaling(self._corner_radius),
|
|
106
|
+
self._apply_widget_scaling(self._border_width),
|
|
107
|
+
self._apply_widget_scaling(left_section_width),
|
|
108
|
+
)
|
|
109
|
+
requires_recoloring_2 = self._draw_engine.draw_dropdown_arrow(
|
|
110
|
+
self._apply_widget_scaling(self._current_width - (self._current_height / 2)),
|
|
111
|
+
self._apply_widget_scaling(self._current_height / 2),
|
|
112
|
+
self._apply_widget_scaling(self._current_height / 3),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if no_color_updates is False or requires_recoloring or requires_recoloring_2:
|
|
116
|
+
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
|
117
|
+
self._canvas.itemconfig(
|
|
118
|
+
"inner_parts_left",
|
|
119
|
+
outline=self._apply_appearance_mode(self._fg_color),
|
|
120
|
+
fill=self._apply_appearance_mode(self._fg_color))
|
|
121
|
+
self._canvas.itemconfig(
|
|
122
|
+
"inner_parts_right",
|
|
123
|
+
outline=self._apply_appearance_mode(self._button_color),
|
|
124
|
+
fill=self._apply_appearance_mode(self._button_color))
|
|
125
|
+
|
|
126
|
+
# The part the native method has no equivalent for. Both tags,
|
|
127
|
+
# because the shape is split into body and arrow button -- missing
|
|
128
|
+
# either leaves half the outline in the wrong colour.
|
|
129
|
+
border_colour = self._apply_appearance_mode(self._border_color)
|
|
130
|
+
for tag in ("border_parts_left", "border_parts_right"):
|
|
131
|
+
self._canvas.itemconfig(tag, outline=border_colour,
|
|
132
|
+
fill=border_colour)
|
|
133
|
+
|
|
134
|
+
self._text_label.configure(
|
|
135
|
+
fg=self._apply_appearance_mode(self._text_color))
|
|
136
|
+
if self._state == "disabled":
|
|
137
|
+
self._text_label.configure(
|
|
138
|
+
fg=self._apply_appearance_mode(self._text_color_disabled))
|
|
139
|
+
self._canvas.itemconfig(
|
|
140
|
+
"dropdown_arrow",
|
|
141
|
+
fill=self._apply_appearance_mode(self._text_color_disabled))
|
|
142
|
+
else:
|
|
143
|
+
self._canvas.itemconfig(
|
|
144
|
+
"dropdown_arrow",
|
|
145
|
+
fill=self._apply_appearance_mode(self._text_color))
|
|
146
|
+
|
|
147
|
+
def _redraw_border(self):
|
|
148
|
+
"""
|
|
149
|
+
Triggers a redraw after a border property changes.
|
|
150
|
+
|
|
151
|
+
The drawing itself is in _draw() -- the border cannot be added
|
|
152
|
+
separately, because the inner fill has to be inset by the border width
|
|
153
|
+
in the same engine call.
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
self._draw()
|
|
157
|
+
except Exception:
|
|
158
|
+
# An upstream change to the engine or to the attributes read above
|
|
159
|
+
# lands here. Losing the border is a visual regression; raising
|
|
160
|
+
# would take the whole widget down.
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
# Property access
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
def _configure_border(self, kwargs):
|
|
167
|
+
"""
|
|
168
|
+
Consumes border_width and border_color from a configure() call.
|
|
169
|
+
|
|
170
|
+
Call from configure() BEFORE forwarding to the native widget, which
|
|
171
|
+
would reject both as unknown options.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
kwargs: The keyword dict. Consumed keys are removed in place.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
True if either was present, so the caller knows to redraw.
|
|
178
|
+
"""
|
|
179
|
+
touched = False
|
|
180
|
+
if "border_width" in kwargs:
|
|
181
|
+
self._border_width = float(kwargs.pop("border_width") or 0)
|
|
182
|
+
touched = True
|
|
183
|
+
if "border_color" in kwargs:
|
|
184
|
+
self._border_color = kwargs.pop("border_color")
|
|
185
|
+
touched = True
|
|
186
|
+
if touched:
|
|
187
|
+
self._redraw_border()
|
|
188
|
+
return touched
|
|
189
|
+
|
|
190
|
+
def _cget_border(self, key):
|
|
191
|
+
"""
|
|
192
|
+
Returns a border property, or the sentinel below if `key` is neither.
|
|
193
|
+
|
|
194
|
+
Written this way rather than returning None because None is a valid
|
|
195
|
+
value for border_color.
|
|
196
|
+
"""
|
|
197
|
+
if key == "border_width":
|
|
198
|
+
return getattr(self, "_border_width", 0)
|
|
199
|
+
if key == "border_color":
|
|
200
|
+
return getattr(self, "_border_color", None)
|
|
201
|
+
return self._NOT_A_BORDER_PROPERTY
|
|
202
|
+
|
|
203
|
+
_NOT_A_BORDER_PROPERTY = object()
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkOptionMenuPrimary
|
|
4
|
+
|
|
5
|
+
A theme-compliant dropdown option-selection menu widget (see also
|
|
6
|
+
sCTkOptionMenuSecondary, a composite bordered variant). Inherits directly from
|
|
7
|
+
ctk.CTkOptionMenu so CustomTkinter handles native rendering and dropdown
|
|
8
|
+
behavior; this class layers automatic light/dark theme resolution and a
|
|
9
|
+
distinct enabled/disabled visual state on top.
|
|
10
|
+
|
|
11
|
+
Base class order matters here. sCTkOptionMenuBorderMixin comes FIRST so its
|
|
12
|
+
_draw() runs before the native one and can add a border on top; native
|
|
13
|
+
ctk.CTkOptionMenu comes next, so every other `super()` call in this file's own
|
|
14
|
+
methods resolves to ctk.CTkOptionMenu -- and, beneath it,
|
|
15
|
+
tkinter.Misc -- never to ThemeableWidget. ThemeableWidget's own
|
|
16
|
+
configure()/cget()/_set_appearance_mode() overrides have been removed entirely
|
|
17
|
+
for this reason (see themeable_widget.py's docstring); this widget owns all of
|
|
18
|
+
its own runtime color-swapping logic.
|
|
19
|
+
|
|
20
|
+
Disabling uses CTk's native state="disabled", consistent with every widget in
|
|
21
|
+
this library confirmed to correctly block interaction this way (the button
|
|
22
|
+
family, sCTkSegmentedButton) -- not independently re-tested for this specific
|
|
23
|
+
widget, but there's no equivalent here of the manual-unbind approach that was
|
|
24
|
+
found broken on the buttons.
|
|
25
|
+
"""
|
|
26
|
+
from typing import Any, Callable, Optional
|
|
27
|
+
import customtkinter as ctk
|
|
28
|
+
from .themeable_widget import ThemeableWidget
|
|
29
|
+
from .sctk_optionmenu_border_mixin import sCTkOptionMenuBorderMixin
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class sCTkOptionMenuPrimary(sCTkOptionMenuBorderMixin, ctk.CTkOptionMenu,
|
|
33
|
+
ThemeableWidget):
|
|
34
|
+
"""Themeable dropdown option-selection menu.
|
|
35
|
+
|
|
36
|
+
Adds to native ctk.CTkOptionMenu:
|
|
37
|
+
- Automatic light/dark theme resolution from sCTkThemes.json (via
|
|
38
|
+
ThemeableWidget.__init__ -- see that class's docstring for what it does,
|
|
39
|
+
and just as importantly, what it no longer does).
|
|
40
|
+
- A distinct enabled/disabled visual state.
|
|
41
|
+
- Pygubu Designer property introspection for `state`, `fg_color`,
|
|
42
|
+
`button_color`, `button_hover_color`, and `text_color` via a
|
|
43
|
+
single-argument configure() call.
|
|
44
|
+
- update_list(), a convenience method for replacing the dropdown's
|
|
45
|
+
options and resetting the visible selection in one call.
|
|
46
|
+
- border_width/border_color, via sCTkOptionMenuBorderMixin. Native
|
|
47
|
+
CTkOptionMenu has no border option at all -- see that module for why
|
|
48
|
+
this matters and what it depends on.
|
|
49
|
+
|
|
50
|
+
Colors are passed through to configure() as raw (light, dark) tuples rather
|
|
51
|
+
than pre-resolved to a single value, so CustomTkinter's own appearance-mode
|
|
52
|
+
tracking repaints them automatically on a light/dark switch -- the same
|
|
53
|
+
approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
|
|
54
|
+
family. Not separately re-confirmed for this specific widget.
|
|
55
|
+
|
|
56
|
+
The theme block's disabled_map only covers fg_color, button_color, and
|
|
57
|
+
text_color -- it has no entries for button_hover_color, dropdown_fg_color,
|
|
58
|
+
or dropdown_text_color. This is consistent with every other themed widget
|
|
59
|
+
in this library: once natively disabled, hover and dropdown-open
|
|
60
|
+
interactions can't fire in the first place, so there's nothing for those
|
|
61
|
+
disabled-state colors to ever visibly apply to.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, master: Optional[Any] = None, **kw: Any) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Args:
|
|
67
|
+
master: Parent container.
|
|
68
|
+
**kw: `values` (list[str]), `command` (callable), and `variable`
|
|
69
|
+
(tkinter.StringVar) are pulled out explicitly below. Everything
|
|
70
|
+
else is either a native CTkOptionMenu argument or a theme-key
|
|
71
|
+
override (see the "sCTkOptionMenuPrimary" block in
|
|
72
|
+
sCTkThemes.json).
|
|
73
|
+
"""
|
|
74
|
+
# 1. Capture widget-specific attributes early, before the mixin's own
|
|
75
|
+
# configuration pass, so they don't get merged into final_kw and
|
|
76
|
+
# treated as theme-overridable properties.
|
|
77
|
+
values = kw.pop("values", None)
|
|
78
|
+
command = kw.pop("command", None)
|
|
79
|
+
variable = kw.pop("variable", None)
|
|
80
|
+
|
|
81
|
+
# 2. Fire our shared theme logic. This resolves final_kw
|
|
82
|
+
# (construction-time properties) and the disabled color map. See
|
|
83
|
+
# ThemeableWidget.__init__ for what actually happens here.
|
|
84
|
+
ThemeableWidget.__init__(self, kw)
|
|
85
|
+
|
|
86
|
+
# 3. Deep-copy the resolved map onto this instance, so later changes
|
|
87
|
+
# here never leak back into the shared theme registry.
|
|
88
|
+
self._local_defaults = dict(self.final_kw)
|
|
89
|
+
self._custom_disabled_map = dict(self._widget_disabled_map)
|
|
90
|
+
|
|
91
|
+
# 4. Initialize CustomTkinter natively.
|
|
92
|
+
#
|
|
93
|
+
# border_width and border_color are REMOVED first. They are this
|
|
94
|
+
# library's own additions -- native CTkOptionMenu has no border option
|
|
95
|
+
# and rejects any keyword it does not recognise, so leaving them in
|
|
96
|
+
# final_kw would raise at construction. _local_defaults above already
|
|
97
|
+
# holds copies for _init_border() to read.
|
|
98
|
+
native_kw = {k: v for k, v in self.final_kw.items()
|
|
99
|
+
if k not in ("border_width", "border_color")}
|
|
100
|
+
super().__init__(master, **native_kw)
|
|
101
|
+
|
|
102
|
+
# 5. Apply the extracted values/command/variable now that the native
|
|
103
|
+
# widget exists.
|
|
104
|
+
if values is not None:
|
|
105
|
+
super().configure(values=values)
|
|
106
|
+
if command is not None:
|
|
107
|
+
super().configure(command=command)
|
|
108
|
+
if variable is not None:
|
|
109
|
+
super().configure(variable=variable)
|
|
110
|
+
|
|
111
|
+
# Border, after the native widget exists: the first redraw needs a
|
|
112
|
+
# canvas. Absent from the theme block means no border, which is the
|
|
113
|
+
# native widget's own appearance.
|
|
114
|
+
self._init_border(
|
|
115
|
+
border_width=self._local_defaults.get("border_width", 0),
|
|
116
|
+
border_color=self._local_defaults.get("border_color"),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
self._custom_current_state = "normal"
|
|
120
|
+
self._update_current_visual_state()
|
|
121
|
+
|
|
122
|
+
# 6. Register lifecycle handshake hook, notifying Pygubu-style consumers
|
|
123
|
+
# that construction is complete.
|
|
124
|
+
self._finalize_themeable_lifecycle()
|
|
125
|
+
|
|
126
|
+
def configure(self, *args: Any, **kwargs: Any) -> Any:
|
|
127
|
+
"""
|
|
128
|
+
Standard widget configuration, with Pygubu/positional-argument handling.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
*args: At most one positional argument is meaningful:
|
|
132
|
+
- a dict: merged into kwargs and processed normally below.
|
|
133
|
+
- the literal string "state": returns a Tkinter-style
|
|
134
|
+
(name, name, name, default, current) tuple.
|
|
135
|
+
- one of "fg_color"/"button_color"/"button_hover_color"/
|
|
136
|
+
"text_color": returns the same style of tuple, with
|
|
137
|
+
`current` reflecting the disabled or normal value as
|
|
138
|
+
appropriate. Note the returned value is str(value), where
|
|
139
|
+
value may itself be a (light, dark) tuple rather than a
|
|
140
|
+
single resolved color -- a known limitation shared with the
|
|
141
|
+
wider Pygubu-query investigation set aside elsewhere in this
|
|
142
|
+
project, not fixed here.
|
|
143
|
+
- anything else: forwarded directly to the native widget's
|
|
144
|
+
configure(), which does not support single-argument property
|
|
145
|
+
queries for arbitrary properties (same limitation).
|
|
146
|
+
**kwargs: Standard CTkOptionMenu configuration options, plus:
|
|
147
|
+
`values`/`command`/`variable` are routed through
|
|
148
|
+
super().configure() individually; `state=...` routes through
|
|
149
|
+
self.state() rather than being forwarded as-is.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The query tuple described above for the single-argument case, or
|
|
153
|
+
whatever super().configure() returns for the keyword-argument case
|
|
154
|
+
(typically None).
|
|
155
|
+
"""
|
|
156
|
+
# args is always a tuple -- args[0] is the actual value passed, whether
|
|
157
|
+
# that's a string or a dict. An earlier version of this method set
|
|
158
|
+
# `pname = args` directly, with no unwrapping attempt at all, so the
|
|
159
|
+
# query branches below never matched anything and the fallback
|
|
160
|
+
# forwarded the wrapped tuple itself to super().configure() -- not a
|
|
161
|
+
# valid call shape for the native widget. Don't reintroduce that.
|
|
162
|
+
if len(args) == 1:
|
|
163
|
+
if isinstance(args[0], dict):
|
|
164
|
+
kwargs = {**args[0], **kwargs}
|
|
165
|
+
else:
|
|
166
|
+
pname = args[0]
|
|
167
|
+
if pname == "state":
|
|
168
|
+
return ("state", "state", "state", "normal", str(self.state()))
|
|
169
|
+
|
|
170
|
+
border_value = self._cget_border(pname)
|
|
171
|
+
if border_value is not self._NOT_A_BORDER_PROPERTY:
|
|
172
|
+
return (pname, pname, pname,
|
|
173
|
+
self._query_value(self._theme_default(pname)),
|
|
174
|
+
self._query_value(border_value))
|
|
175
|
+
|
|
176
|
+
if pname in ["fg_color", "button_color", "button_hover_color", "text_color"]:
|
|
177
|
+
current_state = str(self.state()).lower()
|
|
178
|
+
val = self._custom_disabled_map.get(pname) if current_state == "disabled" else self._local_defaults.get(pname)
|
|
179
|
+
return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
|
|
180
|
+
|
|
181
|
+
return self._configure_query(pname)
|
|
182
|
+
|
|
183
|
+
# Consumed before the native call below, which rejects both as
|
|
184
|
+
# unknown options.
|
|
185
|
+
self._configure_border(kwargs)
|
|
186
|
+
|
|
187
|
+
if "values" in kwargs:
|
|
188
|
+
super().configure(values=kwargs.pop("values"))
|
|
189
|
+
if "command" in kwargs:
|
|
190
|
+
super().configure(command=kwargs.pop("command"))
|
|
191
|
+
if "variable" in kwargs:
|
|
192
|
+
super().configure(variable=kwargs.pop("variable"))
|
|
193
|
+
|
|
194
|
+
# Runtime overrides have to reach the map a repaint reads, or the
|
|
195
|
+
# repaint puts the theme value straight back -- see
|
|
196
|
+
# ThemeableWidget._record_theme_overrides().
|
|
197
|
+
self._record_theme_overrides(kwargs)
|
|
198
|
+
|
|
199
|
+
if "state" in kwargs:
|
|
200
|
+
target_state = str(kwargs.pop("state")).lower()
|
|
201
|
+
self.state(target_state)
|
|
202
|
+
|
|
203
|
+
self._drop_cleared(kwargs)
|
|
204
|
+
|
|
205
|
+
if kwargs:
|
|
206
|
+
result = super().configure(**kwargs)
|
|
207
|
+
else:
|
|
208
|
+
result = None
|
|
209
|
+
self._redraw_border() # last, after any native redraw
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
# Tkinter/CTk convention binds .config to .configure as a SEPARATE class
|
|
213
|
+
# attribute -- it does not automatically track whichever configure() a
|
|
214
|
+
# subclass defines. Without this line, calling .config(...) on an instance
|
|
215
|
+
# would silently skip this entire override and land on the native widget's
|
|
216
|
+
# configure() directly, bypassing theming and state handling entirely.
|
|
217
|
+
config = configure
|
|
218
|
+
|
|
219
|
+
def cget(self, attribute_name: str) -> Any:
|
|
220
|
+
"""
|
|
221
|
+
Standard cget(), extended to this widget's own border properties.
|
|
222
|
+
|
|
223
|
+
FIX: border_width and border_color are not native CTkOptionMenu
|
|
224
|
+
options, so without this they reached CTkBaseClass.cget() and raised
|
|
225
|
+
|
|
226
|
+
ValueError: 'border_width' is not a supported argument.
|
|
227
|
+
|
|
228
|
+
configure(pname) had a border branch already; the ordinary cget() path
|
|
229
|
+
did not, so reading a border value in application code failed while
|
|
230
|
+
querying it through the Designer worked.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
attribute_name: The property to read.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
The property's value.
|
|
237
|
+
"""
|
|
238
|
+
border_value = self._cget_border(attribute_name)
|
|
239
|
+
if border_value is not self._NOT_A_BORDER_PROPERTY:
|
|
240
|
+
return border_value
|
|
241
|
+
return super().cget(attribute_name)
|
|
242
|
+
|
|
243
|
+
def _set_appearance_mode(self, mode_string: str) -> None:
|
|
244
|
+
"""
|
|
245
|
+
Forwards CustomTkinter's internal light/dark mode change notification to
|
|
246
|
+
the native widget.
|
|
247
|
+
|
|
248
|
+
No longer manually re-triggers _update_current_visual_state(). That
|
|
249
|
+
method now passes raw (light, dark) tuples straight through to
|
|
250
|
+
configure() instead of pre-resolving to a single color, so CTk's own
|
|
251
|
+
appearance-mode tracking should repaint correctly on its own -- the
|
|
252
|
+
same approach validated on sCTkComboBox, sCTkSegmentedButton, and the
|
|
253
|
+
button family.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
mode_string: The new appearance mode ("Light" or "Dark"), as passed
|
|
257
|
+
by CustomTkinter's internal appearance-mode change machinery.
|
|
258
|
+
"""
|
|
259
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
260
|
+
try:
|
|
261
|
+
super()._set_appearance_mode(mode_string)
|
|
262
|
+
except Exception:
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
def get_state(self) -> str:
|
|
266
|
+
"""Equivalent to calling state() with no argument."""
|
|
267
|
+
return self.state()
|
|
268
|
+
|
|
269
|
+
def state(self, mode: Optional[str] = None) -> str:
|
|
270
|
+
"""
|
|
271
|
+
Gets or sets the widget's enabled/disabled visual state.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
mode: If None, returns the current state without changing anything.
|
|
275
|
+
Otherwise, only the literal string "disabled" (case-insensitive)
|
|
276
|
+
is treated as disabled; anything in ("normal", "enabled",
|
|
277
|
+
"active") is treated as enabled. Any other value matches
|
|
278
|
+
neither branch, though _update_current_visual_state() still
|
|
279
|
+
runs (harmlessly re-applying the current state's colors).
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
The resulting state ("normal" or "disabled", lowercase).
|
|
283
|
+
"""
|
|
284
|
+
if mode is None:
|
|
285
|
+
return str(getattr(self, "_custom_current_state", "normal")).lower()
|
|
286
|
+
|
|
287
|
+
mode = mode.lower()
|
|
288
|
+
if mode in ("normal", "enabled", "active"):
|
|
289
|
+
self._custom_current_state = "normal"
|
|
290
|
+
elif mode == "disabled":
|
|
291
|
+
self._custom_current_state = "disabled"
|
|
292
|
+
|
|
293
|
+
self._update_current_visual_state()
|
|
294
|
+
return self._custom_current_state
|
|
295
|
+
|
|
296
|
+
def _update_current_visual_state(self) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Recomputes and applies this widget's colors from the theme file, based
|
|
299
|
+
on the current state, then sets the native interactive lock.
|
|
300
|
+
|
|
301
|
+
Called after construction and on every state() change.
|
|
302
|
+
|
|
303
|
+
Passes raw (light, dark) tuples straight through to configure() instead
|
|
304
|
+
of resolving to a single color first, so CTk's native tracking can
|
|
305
|
+
handle appearance-mode repaints without help from _set_appearance_mode.
|
|
306
|
+
Every value here traces back to sCTkThemes.json; there are no
|
|
307
|
+
hardcoded colors in this method.
|
|
308
|
+
|
|
309
|
+
font, dropdown_fg_color, and dropdown_text_color are included in the
|
|
310
|
+
property list below for both states, but disabled_map doesn't define
|
|
311
|
+
any of them -- they simply keep whatever value was last set (from
|
|
312
|
+
construction, in the normal case) when the widget is disabled. See
|
|
313
|
+
this class's docstring for why that's not a problem in practice.
|
|
314
|
+
"""
|
|
315
|
+
is_disabled = getattr(self, "_custom_current_state", "normal") == "disabled"
|
|
316
|
+
target_map = self._custom_disabled_map if is_disabled else self._local_defaults
|
|
317
|
+
|
|
318
|
+
config_payload = {}
|
|
319
|
+
for key in ("fg_color", "button_color", "button_hover_color", "dropdown_fg_color", "dropdown_text_color", "font"):
|
|
320
|
+
val = target_map.get(key)
|
|
321
|
+
if val is not None:
|
|
322
|
+
config_payload[key] = val
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# text_color is handled separately, because this widget sets CTk's own
|
|
326
|
+
# state="disabled" -- and the native draw code then uses its
|
|
327
|
+
# text_color_disabled option, overriding whatever text_color was set
|
|
328
|
+
# to. Swapping text_color out of disabled_map was therefore discarded
|
|
329
|
+
# at draw time and the theme key had no visible effect.
|
|
330
|
+
#
|
|
331
|
+
# Both options are set, so CTk chooses between them as designed.
|
|
332
|
+
_normal_text = self._local_defaults.get("text_color")
|
|
333
|
+
if _normal_text is not None:
|
|
334
|
+
config_payload["text_color"] = _normal_text
|
|
335
|
+
_disabled_text = self._custom_disabled_map.get("text_color")
|
|
336
|
+
if _disabled_text is not None:
|
|
337
|
+
config_payload["text_color_disabled"] = _disabled_text
|
|
338
|
+
|
|
339
|
+
if config_payload:
|
|
340
|
+
super().configure(**config_payload)
|
|
341
|
+
|
|
342
|
+
# The border is not a native option, so it cannot ride along in the
|
|
343
|
+
# payload above. Its disabled colour comes from disabled_map like any
|
|
344
|
+
# other, falling back to the normal one when unspecified.
|
|
345
|
+
border_colour = target_map.get("border_color")
|
|
346
|
+
if border_colour is None:
|
|
347
|
+
border_colour = self._local_defaults.get("border_color")
|
|
348
|
+
if border_colour is not None:
|
|
349
|
+
self._border_color = border_colour
|
|
350
|
+
self._redraw_border()
|
|
351
|
+
|
|
352
|
+
if is_disabled:
|
|
353
|
+
super().configure(state="disabled")
|
|
354
|
+
else:
|
|
355
|
+
super().configure(state="normal")
|
|
356
|
+
|
|
357
|
+
def update_list(self, new_values: list, default_index: int = 0) -> None:
|
|
358
|
+
"""
|
|
359
|
+
Replaces the dropdown's options and resets the visible selection.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
new_values: The new list of options. If empty, the widget is set
|
|
363
|
+
to a single blank option and the display is cleared.
|
|
364
|
+
default_index: Which option to select after updating. If out of
|
|
365
|
+
range for new_values, falls back to index 0 rather than
|
|
366
|
+
raising.
|
|
367
|
+
"""
|
|
368
|
+
if not new_values:
|
|
369
|
+
self.configure(values=[""])
|
|
370
|
+
self.set("")
|
|
371
|
+
return
|
|
372
|
+
|
|
373
|
+
self.configure(values=new_values)
|
|
374
|
+
|
|
375
|
+
if default_index < len(new_values):
|
|
376
|
+
self.set(new_values[default_index])
|
|
377
|
+
else:
|
|
378
|
+
self.set(new_values[0])
|