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.
Files changed (101) hide show
  1. scustomtkinter/__init__.py +81 -0
  2. scustomtkinter/assets/images/error.png +0 -0
  3. scustomtkinter/assets/images/info.png +0 -0
  4. scustomtkinter/assets/images/warning.png +0 -0
  5. scustomtkinter/assets/sCTkThemes.json +1963 -0
  6. scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
  7. scustomtkinter/sctk_button_primary.py +366 -0
  8. scustomtkinter/sctk_button_secondary.py +354 -0
  9. scustomtkinter/sctk_button_tertiary.py +375 -0
  10. scustomtkinter/sctk_checkbox.py +270 -0
  11. scustomtkinter/sctk_combobox.py +155 -0
  12. scustomtkinter/sctk_core.py +15 -0
  13. scustomtkinter/sctk_dial.py +1472 -0
  14. scustomtkinter/sctk_dialog.py +701 -0
  15. scustomtkinter/sctk_dialog_toplevel.py +268 -0
  16. scustomtkinter/sctk_entry_primary.py +400 -0
  17. scustomtkinter/sctk_entry_secondary.py +342 -0
  18. scustomtkinter/sctk_file_explorer.py +879 -0
  19. scustomtkinter/sctk_frame.py +210 -0
  20. scustomtkinter/sctk_frame_labeled_primary.py +667 -0
  21. scustomtkinter/sctk_frame_labeled_secondary.py +598 -0
  22. scustomtkinter/sctk_frequency_display.py +636 -0
  23. scustomtkinter/sctk_label_primary.py +283 -0
  24. scustomtkinter/sctk_label_secondary.py +275 -0
  25. scustomtkinter/sctk_label_tertiary.py +278 -0
  26. scustomtkinter/sctk_messagebox.py +275 -0
  27. scustomtkinter/sctk_notebook.py +1103 -0
  28. scustomtkinter/sctk_optionmenu_border_mixin.py +203 -0
  29. scustomtkinter/sctk_optionmenu_primary.py +378 -0
  30. scustomtkinter/sctk_optionmenu_secondary.py +346 -0
  31. scustomtkinter/sctk_path_chooser.py +516 -0
  32. scustomtkinter/sctk_progress_bar.py +274 -0
  33. scustomtkinter/sctk_radiobutton.py +360 -0
  34. scustomtkinter/sctk_scroll_mixin.py +546 -0
  35. scustomtkinter/sctk_scrollable_frame.py +795 -0
  36. scustomtkinter/sctk_scrollbar.py +313 -0
  37. scustomtkinter/sctk_segmentedbutton.py +424 -0
  38. scustomtkinter/sctk_selector.py +581 -0
  39. scustomtkinter/sctk_separator.py +308 -0
  40. scustomtkinter/sctk_slider.py +264 -0
  41. scustomtkinter/sctk_smeter.py +405 -0
  42. scustomtkinter/sctk_smeter_bar.py +557 -0
  43. scustomtkinter/sctk_spinbox.py +664 -0
  44. scustomtkinter/sctk_switch.py +463 -0
  45. scustomtkinter/sctk_tableview.py +869 -0
  46. scustomtkinter/sctk_tabview.py +409 -0
  47. scustomtkinter/sctk_textbox_primary.py +291 -0
  48. scustomtkinter/sctk_textbox_secondary.py +225 -0
  49. scustomtkinter/sctk_toplevel.py +153 -0
  50. scustomtkinter/themeable_widget.py +938 -0
  51. scustomtkinter-0.2.1.dist-info/METADATA +336 -0
  52. scustomtkinter-0.2.1.dist-info/RECORD +101 -0
  53. scustomtkinter-0.2.1.dist-info/WHEEL +5 -0
  54. scustomtkinter-0.2.1.dist-info/entry_points.txt +2 -0
  55. scustomtkinter-0.2.1.dist-info/licenses/LICENSE +21 -0
  56. scustomtkinter-0.2.1.dist-info/licenses/scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
  57. scustomtkinter-0.2.1.dist-info/top_level.txt +2 -0
  58. scustomtkinter_pygubu/__init__.py +1 -0
  59. scustomtkinter_pygubu/designer/__init__.py +0 -0
  60. scustomtkinter_pygubu/designer/plugin.py +1854 -0
  61. scustomtkinter_pygubu/designer/properties.py +247 -0
  62. scustomtkinter_pygubu/sCTkButtonPrimarybo.py +46 -0
  63. scustomtkinter_pygubu/sCTkButtonSecondarybo.py +45 -0
  64. scustomtkinter_pygubu/sCTkButtonTertiarybo.py +44 -0
  65. scustomtkinter_pygubu/sCTkCheckBoxbo.py +45 -0
  66. scustomtkinter_pygubu/sCTkComboBoxbo.py +45 -0
  67. scustomtkinter_pygubu/sCTkCorebo.py +120 -0
  68. scustomtkinter_pygubu/sCTkDialbo.py +371 -0
  69. scustomtkinter_pygubu/sCTkDialogbo.py +397 -0
  70. scustomtkinter_pygubu/sCTkEntryPrimarybo.py +100 -0
  71. scustomtkinter_pygubu/sCTkEntrySecondarybo.py +98 -0
  72. scustomtkinter_pygubu/sCTkFileExplorerbo.py +189 -0
  73. scustomtkinter_pygubu/sCTkFrameLabeledPrimarybo.py +69 -0
  74. scustomtkinter_pygubu/sCTkFrameLabeledSecondarybo.py +71 -0
  75. scustomtkinter_pygubu/sCTkFramebo.py +46 -0
  76. scustomtkinter_pygubu/sCTkFrequencyDisplaybo.py +175 -0
  77. scustomtkinter_pygubu/sCTkLabelPrimarybo.py +45 -0
  78. scustomtkinter_pygubu/sCTkLabelSecondarybo.py +44 -0
  79. scustomtkinter_pygubu/sCTkLabelTertiarybo.py +45 -0
  80. scustomtkinter_pygubu/sCTkNotebookbo.py +392 -0
  81. scustomtkinter_pygubu/sCTkOptionMenuPrimarybo.py +45 -0
  82. scustomtkinter_pygubu/sCTkOptionMenuSecondarybo.py +39 -0
  83. scustomtkinter_pygubu/sCTkPathChooserbo.py +204 -0
  84. scustomtkinter_pygubu/sCTkProgressBarbo.py +45 -0
  85. scustomtkinter_pygubu/sCTkRadioButtonbo.py +45 -0
  86. scustomtkinter_pygubu/sCTkSMeterBarbo.py +153 -0
  87. scustomtkinter_pygubu/sCTkSMeterbo.py +101 -0
  88. scustomtkinter_pygubu/sCTkScrollableFramebo.py +66 -0
  89. scustomtkinter_pygubu/sCTkScrollbarbo.py +48 -0
  90. scustomtkinter_pygubu/sCTkSegmentedButtonbo.py +47 -0
  91. scustomtkinter_pygubu/sCTkSelectorbo.py +167 -0
  92. scustomtkinter_pygubu/sCTkSeparatorbo.py +141 -0
  93. scustomtkinter_pygubu/sCTkSliderbo.py +45 -0
  94. scustomtkinter_pygubu/sCTkSpinboxbo.py +200 -0
  95. scustomtkinter_pygubu/sCTkSwitchbo.py +45 -0
  96. scustomtkinter_pygubu/sCTkTableviewbo.py +188 -0
  97. scustomtkinter_pygubu/sCTkTabviewbo.py +226 -0
  98. scustomtkinter_pygubu/sCTkTextboxPrimarybo.py +45 -0
  99. scustomtkinter_pygubu/sCTkTextboxSecondarybo.py +45 -0
  100. scustomtkinter_pygubu/sCTkToplevelbo.py +55 -0
  101. 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])