CTkScrollableDropdownPP 2.0.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.
- ctkscrollabledropdownpp-2.0.0/CTkScrollableDropdownPP/__init__.py +12 -0
- ctkscrollabledropdownpp-2.0.0/CTkScrollableDropdownPP/ctk_scrollable_dropdown.py +556 -0
- ctkscrollabledropdownpp-2.0.0/CTkScrollableDropdownPP.egg-info/PKG-INFO +20 -0
- ctkscrollabledropdownpp-2.0.0/CTkScrollableDropdownPP.egg-info/SOURCES.txt +10 -0
- ctkscrollabledropdownpp-2.0.0/CTkScrollableDropdownPP.egg-info/dependency_links.txt +1 -0
- ctkscrollabledropdownpp-2.0.0/CTkScrollableDropdownPP.egg-info/requires.txt +1 -0
- ctkscrollabledropdownpp-2.0.0/CTkScrollableDropdownPP.egg-info/top_level.txt +1 -0
- ctkscrollabledropdownpp-2.0.0/LICENSE +21 -0
- ctkscrollabledropdownpp-2.0.0/PKG-INFO +20 -0
- ctkscrollabledropdownpp-2.0.0/README.md +2 -0
- ctkscrollabledropdownpp-2.0.0/pyproject.toml +35 -0
- ctkscrollabledropdownpp-2.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CustomTkinter Scrollable Dropdown Menu
|
|
3
|
+
Original Author: Akash Bora (https://github.com/Akascape)
|
|
4
|
+
Modified by: PLauncher-Team (https://github.com/PLauncher-Team) with pagination, search, and grouping features.
|
|
5
|
+
|
|
6
|
+
License: MIT
|
|
7
|
+
Homepage: https://github.com/ваш-репозиторий/CTkScrollableDropdownPP
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = '2.0'
|
|
11
|
+
|
|
12
|
+
from .ctk_scrollable_dropdown import CTkScrollableDropdown
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import customtkinter
|
|
2
|
+
import sys
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
class CTkScrollableDropdown(customtkinter.CTkToplevel):
|
|
6
|
+
|
|
7
|
+
def __init__(self, attach, x=None, y=None, button_color=None, height: int = 55, width: int = None,
|
|
8
|
+
fg_color=None, button_height: int = 20, justify="center", scrollbar_button_color=None,
|
|
9
|
+
scrollbar=True, scrollbar_button_hover_color=None, frame_border_width=0, values=[],
|
|
10
|
+
command=None, image_values=[], alpha: float = 0.95, frame_corner_radius=20, double_click=False,
|
|
11
|
+
resize=False, frame_border_color=None, text_color=None, autocomplete=False,
|
|
12
|
+
hover_color=None, pagination: bool = True, items_per_page: int = 50,
|
|
13
|
+
groups=None, **button_kwargs):
|
|
14
|
+
super().__init__(master=attach.winfo_toplevel(), takefocus=1)
|
|
15
|
+
|
|
16
|
+
self.group_patterns = None
|
|
17
|
+
self.grouped_values = None
|
|
18
|
+
self.y_pos = None
|
|
19
|
+
self.width_new = None
|
|
20
|
+
self.x_pos = None
|
|
21
|
+
self.focus()
|
|
22
|
+
self.lift()
|
|
23
|
+
self.alpha = alpha
|
|
24
|
+
self.attach = attach
|
|
25
|
+
self.corner = frame_corner_radius
|
|
26
|
+
self.padding = 0
|
|
27
|
+
self.focus_something = False
|
|
28
|
+
self.disable = False
|
|
29
|
+
self.update()
|
|
30
|
+
self.old_kwargs = None
|
|
31
|
+
self.widgets = {}
|
|
32
|
+
self.all_values = values.copy()
|
|
33
|
+
self.values = values.copy()
|
|
34
|
+
self.hide_flag = True
|
|
35
|
+
self.pagination = pagination
|
|
36
|
+
self.items_per_page = items_per_page
|
|
37
|
+
self.current_page = 0
|
|
38
|
+
self.filtered_values = None
|
|
39
|
+
self.current_group = 0
|
|
40
|
+
self.groups = []
|
|
41
|
+
if groups is not None:
|
|
42
|
+
for g in groups:
|
|
43
|
+
if isinstance(g, (list, tuple)) and len(g) >= 2:
|
|
44
|
+
self.groups.append({"name": g[0], "pattern": g[1]})
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError(f"groups must be a list of [name, pattern], got {g!r}")
|
|
47
|
+
|
|
48
|
+
self.group_names = [g["name"] for g in self.groups]
|
|
49
|
+
self.group_patterns = []
|
|
50
|
+
included_values = set()
|
|
51
|
+
|
|
52
|
+
for g in self.groups:
|
|
53
|
+
pattern = g["pattern"]
|
|
54
|
+
if pattern == "__OTHERS__":
|
|
55
|
+
self.group_patterns.append("__OTHERS__")
|
|
56
|
+
else:
|
|
57
|
+
compiled = re.compile(pattern)
|
|
58
|
+
self.group_patterns.append(compiled)
|
|
59
|
+
matched = [v for v in self.all_values if compiled.search(v)]
|
|
60
|
+
included_values.update(matched)
|
|
61
|
+
|
|
62
|
+
self.grouped_values = {}
|
|
63
|
+
for i, pat in enumerate(self.group_patterns):
|
|
64
|
+
if pat == "__OTHERS__":
|
|
65
|
+
self.grouped_values[i] = [v for v in self.all_values if v not in included_values]
|
|
66
|
+
else:
|
|
67
|
+
self.grouped_values[i] = [v for v in self.all_values if pat.search(v)]
|
|
68
|
+
|
|
69
|
+
if sys.platform.startswith("win"):
|
|
70
|
+
self.after(100, lambda: self.overrideredirect(True))
|
|
71
|
+
self.transparent_color = self._apply_appearance_mode(self._fg_color) if hasattr(self, '_fg_color') else "#FFFFFF"
|
|
72
|
+
self.attributes("-transparentcolor", self.transparent_color)
|
|
73
|
+
elif sys.platform.startswith("darwin"):
|
|
74
|
+
self.overrideredirect(True)
|
|
75
|
+
self.transparent_color = 'systemTransparent'
|
|
76
|
+
self.attributes("-transparent", True)
|
|
77
|
+
self.focus_something = True
|
|
78
|
+
else:
|
|
79
|
+
self.overrideredirect(True)
|
|
80
|
+
self.transparent_color = '#000001'
|
|
81
|
+
self.corner = 0
|
|
82
|
+
self.padding = 18
|
|
83
|
+
self.withdraw()
|
|
84
|
+
self.hide_flag = True
|
|
85
|
+
self.attach.bind('<Configure>', lambda e: self._withdraw() if not self.disable else None, add="+")
|
|
86
|
+
self.attach.winfo_toplevel().bind('<Configure>', lambda e: self._withdraw() if not self.disable else None, add="+")
|
|
87
|
+
self.attach.winfo_toplevel().bind("<ButtonPress>", lambda e: self._withdraw() if not self.disable else None, add="+")
|
|
88
|
+
self.bind("<Escape>", lambda e: self._withdraw() if not self.disable else None, add="+")
|
|
89
|
+
self.attributes('-alpha', 0)
|
|
90
|
+
self.fg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if fg_color is None else fg_color
|
|
91
|
+
self.scroll_button_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_color"] if scrollbar_button_color is None else scrollbar_button_color
|
|
92
|
+
self.scroll_hover_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if scrollbar_button_hover_color is None else scrollbar_button_hover_color
|
|
93
|
+
self.frame_border_color = customtkinter.ThemeManager.theme["CTkFrame"]["border_color"] if frame_border_color is None else frame_border_color
|
|
94
|
+
self.button_color = customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"] if button_color is None else button_color
|
|
95
|
+
self.text_color = customtkinter.ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else text_color
|
|
96
|
+
self.hover_color = customtkinter.ThemeManager.theme["CTkButton"]["hover_color"] if hover_color is None else hover_color
|
|
97
|
+
if not scrollbar:
|
|
98
|
+
self.scroll_button_color = self.fg_color
|
|
99
|
+
self.scroll_hover_color = self.fg_color
|
|
100
|
+
if self.pagination:
|
|
101
|
+
self.search_var = customtkinter.StringVar()
|
|
102
|
+
self.search_var.trace_add('write', lambda *args: self.live_update(self.search_var.get()))
|
|
103
|
+
self.search_entry = customtkinter.CTkEntry(self, textvariable=self.search_var)
|
|
104
|
+
self.search_entry.pack(fill="x")
|
|
105
|
+
if self.groups:
|
|
106
|
+
self.group_frame = customtkinter.CTkFrame(self, fg_color=self.fg_color)
|
|
107
|
+
self.group_frame.pack(fill="x", padx=5)
|
|
108
|
+
self.group_buttons = []
|
|
109
|
+
for idx, name in enumerate(self.group_names):
|
|
110
|
+
btn = customtkinter.CTkButton(
|
|
111
|
+
self.group_frame,
|
|
112
|
+
text=name,
|
|
113
|
+
height=button_height,
|
|
114
|
+
fg_color=self.button_color,
|
|
115
|
+
text_color=self.text_color,
|
|
116
|
+
hover_color=self.hover_color,
|
|
117
|
+
command=lambda i=idx: self.switch_group(i),
|
|
118
|
+
)
|
|
119
|
+
self.group_buttons.append(btn)
|
|
120
|
+
self.group_frame.bind("<Configure>", self._on_group_frame_configure, add="+")
|
|
121
|
+
self.group_button_colors = [btn.cget("fg_color") for btn in self.group_buttons]
|
|
122
|
+
self.frame = customtkinter.CTkScrollableFrame(
|
|
123
|
+
self,
|
|
124
|
+
bg_color=self.transparent_color,
|
|
125
|
+
fg_color=self.fg_color,
|
|
126
|
+
scrollbar_button_hover_color=self.scroll_hover_color,
|
|
127
|
+
corner_radius=self.corner,
|
|
128
|
+
border_width=frame_border_width,
|
|
129
|
+
scrollbar_button_color=self.scroll_button_color,
|
|
130
|
+
border_color=self.frame_border_color
|
|
131
|
+
)
|
|
132
|
+
self.frame._scrollbar.grid_configure(padx=3)
|
|
133
|
+
self.frame.pack(expand=True, fill="both")
|
|
134
|
+
if self.pagination:
|
|
135
|
+
self.button_container = customtkinter.CTkFrame(self.frame, fg_color=self.fg_color)
|
|
136
|
+
self.button_container.pack(expand=True, fill="both")
|
|
137
|
+
self.pagination_frame = customtkinter.CTkFrame(self.frame, fg_color=self.fg_color)
|
|
138
|
+
self.pagination_frame.pack(fill="x", side="bottom")
|
|
139
|
+
else:
|
|
140
|
+
self.button_container = self.frame
|
|
141
|
+
self.dummy_entry = customtkinter.CTkEntry(self.frame, fg_color="transparent", border_width=0, height=1, width=1)
|
|
142
|
+
self.height = height
|
|
143
|
+
self.height_new = height
|
|
144
|
+
self.width = width
|
|
145
|
+
self.command = command
|
|
146
|
+
self.fade = False
|
|
147
|
+
self.resize = resize
|
|
148
|
+
self.autocomplete = autocomplete
|
|
149
|
+
self.var_update = customtkinter.StringVar()
|
|
150
|
+
self.appear = True
|
|
151
|
+
if justify.lower() == "left":
|
|
152
|
+
self.justify = "w"
|
|
153
|
+
elif justify.lower() == "right":
|
|
154
|
+
self.justify = "e"
|
|
155
|
+
else:
|
|
156
|
+
self.justify = "c"
|
|
157
|
+
self.button_height = button_height
|
|
158
|
+
self.image_values = None if len(image_values) != len(self.values) else image_values
|
|
159
|
+
self.resizable(width=False, height=False)
|
|
160
|
+
self.transient(self.master)
|
|
161
|
+
if double_click or isinstance(self.attach, customtkinter.CTkEntry) or isinstance(self.attach, customtkinter.CTkComboBox):
|
|
162
|
+
self.attach.bind('<Double-Button-1>', lambda e: self._iconify(), add="+")
|
|
163
|
+
else:
|
|
164
|
+
self.attach.bind('<Button-1>', lambda e: self._iconify(), add="+")
|
|
165
|
+
if isinstance(self.attach, customtkinter.CTkComboBox):
|
|
166
|
+
self.attach._canvas.tag_bind("right_parts", "<Button-1>", lambda e: self._iconify())
|
|
167
|
+
self.attach._canvas.tag_bind("dropdown_arrow", "<Button-1>", lambda e: self._iconify())
|
|
168
|
+
if self.command is None:
|
|
169
|
+
self.command = self.attach.set
|
|
170
|
+
if isinstance(self.attach, customtkinter.CTkOptionMenu):
|
|
171
|
+
self.attach._canvas.bind("<Button-1>", lambda e: self._iconify())
|
|
172
|
+
self.attach._text_label.bind("<Button-1>", lambda e: self._iconify())
|
|
173
|
+
if self.command is None:
|
|
174
|
+
self.command = self.attach.set
|
|
175
|
+
self.attach.bind("<Destroy>", lambda _: self._destroy(), add="+")
|
|
176
|
+
self.update_idletasks()
|
|
177
|
+
self.x = x
|
|
178
|
+
self.y = y
|
|
179
|
+
if self.autocomplete:
|
|
180
|
+
self.bind_autocomplete()
|
|
181
|
+
self.withdraw()
|
|
182
|
+
self.attributes("-alpha", self.alpha)
|
|
183
|
+
if self.groups:
|
|
184
|
+
self.switch_group(0)
|
|
185
|
+
self._init_buttons()
|
|
186
|
+
|
|
187
|
+
def _on_group_frame_configure(self, event):
|
|
188
|
+
min_btn_width = 100
|
|
189
|
+
cols = max(1, event.width // min_btn_width)
|
|
190
|
+
|
|
191
|
+
for child in self.group_frame.grid_slaves():
|
|
192
|
+
child.grid_forget()
|
|
193
|
+
|
|
194
|
+
for idx, btn in enumerate(self.group_buttons):
|
|
195
|
+
row = idx // cols
|
|
196
|
+
col = idx % cols
|
|
197
|
+
btn.grid(row=row, column=col, sticky="ew", padx=2, pady=2)
|
|
198
|
+
|
|
199
|
+
for c in range(cols):
|
|
200
|
+
self.group_frame.grid_columnconfigure(c, weight=1)
|
|
201
|
+
|
|
202
|
+
def switch_group(self, idx):
|
|
203
|
+
if idx == self.current_group:
|
|
204
|
+
self.values = self.all_values.copy()
|
|
205
|
+
self.current_group = None
|
|
206
|
+
else:
|
|
207
|
+
self.current_group = idx
|
|
208
|
+
self.values = self.grouped_values[idx].copy()
|
|
209
|
+
for i, btn in enumerate(self.group_buttons):
|
|
210
|
+
if i == self.current_group:
|
|
211
|
+
btn.configure(fg_color=self.hover_color)
|
|
212
|
+
else:
|
|
213
|
+
btn.configure(fg_color=self.group_button_colors[i])
|
|
214
|
+
self.filtered_values = None
|
|
215
|
+
self.current_page = 0
|
|
216
|
+
self._init_buttons()
|
|
217
|
+
|
|
218
|
+
def update_buttons(self, values_list):
|
|
219
|
+
for i, value in enumerate(values_list):
|
|
220
|
+
if i in self.widgets:
|
|
221
|
+
btn, _ = self.widgets[i]
|
|
222
|
+
btn.configure(text=value, command=lambda v=value: self._attach_key_press(v))
|
|
223
|
+
btn.pack(fill="x", pady=2, padx=(self.padding, 0))
|
|
224
|
+
self.widgets[i][1] = True
|
|
225
|
+
else:
|
|
226
|
+
btn = customtkinter.CTkButton(
|
|
227
|
+
self.button_container,
|
|
228
|
+
text=value,
|
|
229
|
+
font=("Segoe UI", 12),
|
|
230
|
+
command=lambda v=value: self._attach_key_press(v),
|
|
231
|
+
height=self.button_height,
|
|
232
|
+
fg_color=self.button_color,
|
|
233
|
+
text_color=self.text_color,
|
|
234
|
+
anchor=self.justify,
|
|
235
|
+
hover_color=self.hover_color
|
|
236
|
+
)
|
|
237
|
+
btn.pack(fill="x", pady=2, padx=(self.padding, 0))
|
|
238
|
+
self.widgets[i] = [btn, True]
|
|
239
|
+
i = len(values_list)
|
|
240
|
+
while i in self.widgets:
|
|
241
|
+
btn, visible = self.widgets[i]
|
|
242
|
+
if visible:
|
|
243
|
+
btn.pack_forget()
|
|
244
|
+
self.widgets[i][1] = False
|
|
245
|
+
i += 1
|
|
246
|
+
|
|
247
|
+
def _destroy(self):
|
|
248
|
+
self.after(500, self.destroy_popup)
|
|
249
|
+
|
|
250
|
+
def _withdraw(self):
|
|
251
|
+
if not self.winfo_exists():
|
|
252
|
+
return
|
|
253
|
+
if self.winfo_viewable() and self.hide_flag:
|
|
254
|
+
self.withdraw()
|
|
255
|
+
self.event_generate("<<Closed>>")
|
|
256
|
+
self.hide_flag = True
|
|
257
|
+
|
|
258
|
+
def _update(self, a, b, c):
|
|
259
|
+
self.live_update(self.attach._entry.get())
|
|
260
|
+
|
|
261
|
+
def bind_autocomplete(self):
|
|
262
|
+
def appear(x):
|
|
263
|
+
self.appear = True
|
|
264
|
+
if isinstance(self.attach, customtkinter.CTkComboBox):
|
|
265
|
+
self.attach._entry.configure(textvariable=self.var_update)
|
|
266
|
+
self.attach._entry.bind("<Key>", appear)
|
|
267
|
+
if self.values:
|
|
268
|
+
self.attach.set(self.values[0])
|
|
269
|
+
self.var_update.trace_add('write', self._update)
|
|
270
|
+
if isinstance(self.attach, customtkinter.CTkEntry):
|
|
271
|
+
self.attach.configure(textvariable=self.var_update)
|
|
272
|
+
self.attach.bind("<Key>", appear)
|
|
273
|
+
self.var_update.trace_add('write', self._update)
|
|
274
|
+
|
|
275
|
+
def _init_buttons(self):
|
|
276
|
+
if self.pagination:
|
|
277
|
+
values_to_show = self.values[self.current_page * self.items_per_page:
|
|
278
|
+
(self.current_page + 1) * self.items_per_page]
|
|
279
|
+
self._update_pagination_buttons()
|
|
280
|
+
else:
|
|
281
|
+
values_to_show = self.values
|
|
282
|
+
self.update_buttons(values_to_show)
|
|
283
|
+
self.frame._parent_canvas.yview_moveto(0)
|
|
284
|
+
|
|
285
|
+
def _update_pagination_buttons(self, filtered=False):
|
|
286
|
+
if not self.pagination:
|
|
287
|
+
return
|
|
288
|
+
for child in self.pagination_frame.winfo_children():
|
|
289
|
+
child.destroy()
|
|
290
|
+
values_list = self.filtered_values if (filtered and self.filtered_values is not None) else self.values
|
|
291
|
+
total_pages = (len(values_list) + self.items_per_page - 1) // self.items_per_page
|
|
292
|
+
button_width = 30
|
|
293
|
+
button_padding = 4
|
|
294
|
+
available_width = self.pagination_frame.winfo_width() if self.pagination_frame.winfo_ismapped() else self.width_new
|
|
295
|
+
if available_width < 1:
|
|
296
|
+
available_width = self.attach.winfo_width() if self.width is None else self.width
|
|
297
|
+
max_buttons_per_row = max(1, available_width // (button_width + button_padding))
|
|
298
|
+
total_rows = (total_pages + max_buttons_per_row - 1) // max_buttons_per_row
|
|
299
|
+
for row in range(total_rows):
|
|
300
|
+
for col in range(max_buttons_per_row):
|
|
301
|
+
page_index = row * max_buttons_per_row + col
|
|
302
|
+
if page_index >= total_pages:
|
|
303
|
+
break
|
|
304
|
+
state = "disabled" if page_index == self.current_page else "normal"
|
|
305
|
+
btn = customtkinter.CTkButton(
|
|
306
|
+
self.pagination_frame,
|
|
307
|
+
text=str(page_index + 1),
|
|
308
|
+
width=button_width,
|
|
309
|
+
height=30,
|
|
310
|
+
fg_color=self.button_color,
|
|
311
|
+
text_color=self.text_color,
|
|
312
|
+
command=lambda p=page_index: self._change_page(p),
|
|
313
|
+
state=state
|
|
314
|
+
)
|
|
315
|
+
btn.grid(row=row, column=col, padx=2, pady=2, sticky="ew")
|
|
316
|
+
for col in range(max_buttons_per_row):
|
|
317
|
+
self.pagination_frame.grid_columnconfigure(col, weight=1)
|
|
318
|
+
|
|
319
|
+
def _change_page(self, page_index):
|
|
320
|
+
self.current_page = page_index
|
|
321
|
+
if self.filtered_values is not None:
|
|
322
|
+
values_to_show = self.filtered_values[self.current_page * self.items_per_page:
|
|
323
|
+
(self.current_page + 1) * self.items_per_page]
|
|
324
|
+
else:
|
|
325
|
+
values_to_show = self.values[self.current_page * self.items_per_page:
|
|
326
|
+
(self.current_page + 1) * self.items_per_page]
|
|
327
|
+
self.update_buttons(values_to_show)
|
|
328
|
+
self._update_pagination_buttons(filtered=bool(self.filtered_values))
|
|
329
|
+
self.frame.update_idletasks()
|
|
330
|
+
self.frame._parent_canvas.yview_moveto(len(values_to_show) / self.items_per_page)
|
|
331
|
+
|
|
332
|
+
def destroy_popup(self):
|
|
333
|
+
self.destroy()
|
|
334
|
+
self.disable = True
|
|
335
|
+
|
|
336
|
+
def place_dropdown(self):
|
|
337
|
+
if not self.winfo_exists():
|
|
338
|
+
return
|
|
339
|
+
current_width = self.winfo_width()
|
|
340
|
+
current_height = self.winfo_height()
|
|
341
|
+
current_x_pos = self.winfo_x()
|
|
342
|
+
current_y_pos = self.winfo_y()
|
|
343
|
+
self.x_pos = self.attach.winfo_rootx() if self.x is None else self.x + self.attach.winfo_rootx()
|
|
344
|
+
self.width_new = self.attach.winfo_width() if self.width is None else self.width
|
|
345
|
+
if self.resize:
|
|
346
|
+
displayed_items = len(self.filtered_values) if self.filtered_values is not None else len(self.values)
|
|
347
|
+
if self.pagination:
|
|
348
|
+
displayed_items = min(displayed_items, self.items_per_page)
|
|
349
|
+
base_height = 50 if self.pagination else 35
|
|
350
|
+
items_height = displayed_items * self.button_height
|
|
351
|
+
self.height_new = items_height + base_height
|
|
352
|
+
screen_height = self.winfo_screenheight()
|
|
353
|
+
dropdown_bottom = self.attach.winfo_rooty() + self.height_new
|
|
354
|
+
if dropdown_bottom > screen_height:
|
|
355
|
+
self.y_pos = max(0, self.attach.winfo_rooty() - self.height_new)
|
|
356
|
+
else:
|
|
357
|
+
self.y_pos = self.attach.winfo_rooty() + self.attach.winfo_height()
|
|
358
|
+
if (current_width != self.width_new
|
|
359
|
+
or current_height != self.height_new
|
|
360
|
+
or current_x_pos != self.x_pos
|
|
361
|
+
or current_y_pos != self.y_pos):
|
|
362
|
+
self.geometry(f"{self.width_new}x{self.height_new}+{self.x_pos}+{self.y_pos}")
|
|
363
|
+
self.attributes('-alpha', self.alpha)
|
|
364
|
+
self.update_idletasks()
|
|
365
|
+
if self.pagination:
|
|
366
|
+
self._update_pagination_buttons(filtered=bool(self.filtered_values))
|
|
367
|
+
|
|
368
|
+
def _iconify(self):
|
|
369
|
+
if self.attach.cget("state") == "disabled":
|
|
370
|
+
return
|
|
371
|
+
if self.disable:
|
|
372
|
+
return
|
|
373
|
+
if self.winfo_ismapped():
|
|
374
|
+
self.hide_flag = False
|
|
375
|
+
if self.hide_flag and self.all_values:
|
|
376
|
+
self.event_generate("<<Opened>>")
|
|
377
|
+
self.focus()
|
|
378
|
+
self.hide_flag = False
|
|
379
|
+
self.place_dropdown()
|
|
380
|
+
self._deiconify()
|
|
381
|
+
else:
|
|
382
|
+
self.withdraw()
|
|
383
|
+
self.hide_flag = True
|
|
384
|
+
|
|
385
|
+
def _attach_key_press(self, k):
|
|
386
|
+
if hasattr(self, "search_var"):
|
|
387
|
+
self.search_var.set("")
|
|
388
|
+
self.event_generate("<<Selected>>")
|
|
389
|
+
self.fade = True
|
|
390
|
+
if self.command:
|
|
391
|
+
self.command(k)
|
|
392
|
+
if hasattr(self, "search_var") and self.search_var.get().strip() != "":
|
|
393
|
+
self.filtered_values = None
|
|
394
|
+
self.current_page = 0
|
|
395
|
+
self._init_buttons()
|
|
396
|
+
if self.pagination:
|
|
397
|
+
self.pagination_frame.pack(fill="x", side="bottom")
|
|
398
|
+
self.fade = False
|
|
399
|
+
self.withdraw()
|
|
400
|
+
self.hide_flag = True
|
|
401
|
+
|
|
402
|
+
def live_update(self, string=None):
|
|
403
|
+
if self.disable or self.fade:
|
|
404
|
+
return
|
|
405
|
+
self.frame._parent_canvas.yview_moveto(0)
|
|
406
|
+
if string and string.strip() != "":
|
|
407
|
+
string = string.lower()
|
|
408
|
+
filtered = [val for val in self.values if string in val.lower()]
|
|
409
|
+
self.filtered_values = filtered
|
|
410
|
+
if self.pagination:
|
|
411
|
+
total_pages = (len(filtered) + self.items_per_page - 1) // self.items_per_page
|
|
412
|
+
if self.current_page >= total_pages:
|
|
413
|
+
self.current_page = 0
|
|
414
|
+
self._update_pagination_buttons(filtered=True)
|
|
415
|
+
values_to_show = filtered[self.current_page * self.items_per_page:(self.current_page + 1) * self.items_per_page]
|
|
416
|
+
else:
|
|
417
|
+
values_to_show = filtered
|
|
418
|
+
self.update_buttons(values_to_show)
|
|
419
|
+
if self.pagination:
|
|
420
|
+
self.pagination_frame.pack(fill="x", side="bottom")
|
|
421
|
+
self.place_dropdown()
|
|
422
|
+
else:
|
|
423
|
+
if self.pagination:
|
|
424
|
+
self.pagination_frame.pack(fill="x", side="bottom")
|
|
425
|
+
self.filtered_values = None
|
|
426
|
+
self.current_page = 0
|
|
427
|
+
self._init_buttons()
|
|
428
|
+
self.place_dropdown()
|
|
429
|
+
self.appear = False
|
|
430
|
+
|
|
431
|
+
def insert(self, value, **kwargs):
|
|
432
|
+
index = len(self.values)
|
|
433
|
+
if index in self.widgets:
|
|
434
|
+
btn, _ = self.widgets[index]
|
|
435
|
+
btn.configure(text=value, command=lambda v=value: self._attach_key_press(v))
|
|
436
|
+
btn.pack(fill="x", pady=2, padx=(self.padding, 0))
|
|
437
|
+
self.widgets[index][1] = True
|
|
438
|
+
else:
|
|
439
|
+
btn = customtkinter.CTkButton(self.button_container,
|
|
440
|
+
text=value,
|
|
441
|
+
height=self.button_height,
|
|
442
|
+
fg_color=self.button_color,
|
|
443
|
+
text_color=self.text_color,
|
|
444
|
+
hover_color=self.hover_color,
|
|
445
|
+
anchor=self.justify,
|
|
446
|
+
command=lambda v=value: self._attach_key_press(v),
|
|
447
|
+
width=0
|
|
448
|
+
**kwargs)
|
|
449
|
+
btn.pack(fill="x", pady=2, padx=(self.padding, 0))
|
|
450
|
+
self.widgets[index] = [btn, True]
|
|
451
|
+
self.values.append(value)
|
|
452
|
+
self.all_values.append(value)
|
|
453
|
+
if self.groups:
|
|
454
|
+
self.grouped_values[self.current_group].append(value)
|
|
455
|
+
if self.pagination:
|
|
456
|
+
self._update_pagination_buttons()
|
|
457
|
+
|
|
458
|
+
def _deiconify(self):
|
|
459
|
+
if self.all_values:
|
|
460
|
+
self.deiconify()
|
|
461
|
+
|
|
462
|
+
def popup(self, x=None, y=None):
|
|
463
|
+
self.x = x
|
|
464
|
+
self.y = y
|
|
465
|
+
self.hide_flag = True
|
|
466
|
+
self._iconify()
|
|
467
|
+
|
|
468
|
+
def hide(self):
|
|
469
|
+
self._withdraw()
|
|
470
|
+
|
|
471
|
+
def configure(self, **kwargs):
|
|
472
|
+
if self.old_kwargs == kwargs:
|
|
473
|
+
return
|
|
474
|
+
if "height" in kwargs:
|
|
475
|
+
self.height = kwargs.pop("height")
|
|
476
|
+
self.height_new = self.height
|
|
477
|
+
if "alpha" in kwargs:
|
|
478
|
+
self.alpha = kwargs.pop("alpha")
|
|
479
|
+
if "width" in kwargs:
|
|
480
|
+
self.width = kwargs.pop("width")
|
|
481
|
+
if "fg_color" in kwargs:
|
|
482
|
+
self.frame.configure(fg_color=kwargs.pop("fg_color"))
|
|
483
|
+
if "values" in kwargs:
|
|
484
|
+
self.all_values = kwargs.pop("values")
|
|
485
|
+
self.values = self.all_values.copy()
|
|
486
|
+
self.image_values = None
|
|
487
|
+
self.current_page = 0
|
|
488
|
+
self._init_buttons()
|
|
489
|
+
if hasattr(self, "search_var"):
|
|
490
|
+
self.search_var.set("")
|
|
491
|
+
if getattr(self, "groups", None):
|
|
492
|
+
self.group_patterns = []
|
|
493
|
+
included_values = set()
|
|
494
|
+
for g in self.groups:
|
|
495
|
+
pattern = g["pattern"]
|
|
496
|
+
if pattern == "__OTHERS__":
|
|
497
|
+
self.group_patterns.append("__OTHERS__")
|
|
498
|
+
else:
|
|
499
|
+
compiled = re.compile(pattern)
|
|
500
|
+
self.group_patterns.append(compiled)
|
|
501
|
+
matched = [v for v in self.all_values if compiled.search(v)]
|
|
502
|
+
included_values.update(matched)
|
|
503
|
+
self.grouped_values = {}
|
|
504
|
+
for i, pat in enumerate(self.group_patterns):
|
|
505
|
+
if pat == "__OTHERS__":
|
|
506
|
+
self.grouped_values[i] = [v for v in self.all_values if v not in included_values]
|
|
507
|
+
else:
|
|
508
|
+
self.grouped_values[i] = [v for v in self.all_values if pat.search(v)]
|
|
509
|
+
self.switch_group(self.current_group)
|
|
510
|
+
if "groups" in kwargs:
|
|
511
|
+
raw_groups = kwargs.pop("groups")
|
|
512
|
+
self.groups = []
|
|
513
|
+
for g in raw_groups:
|
|
514
|
+
if isinstance(g, (list, tuple)) and len(g) >= 2:
|
|
515
|
+
self.groups.append({"name": g[0], "pattern": g[1]})
|
|
516
|
+
else:
|
|
517
|
+
raise ValueError(f"groups must be list of [name, pattern], got {g!r}")
|
|
518
|
+
|
|
519
|
+
self.group_names = [g["name"] for g in self.groups]
|
|
520
|
+
self.group_patterns = []
|
|
521
|
+
included_values = set()
|
|
522
|
+
|
|
523
|
+
for g in self.groups:
|
|
524
|
+
pattern = g["pattern"]
|
|
525
|
+
if pattern == "__OTHERS__":
|
|
526
|
+
self.group_patterns.append("__OTHERS__")
|
|
527
|
+
else:
|
|
528
|
+
compiled = re.compile(pattern)
|
|
529
|
+
self.group_patterns.append(compiled)
|
|
530
|
+
matched = [v for v in self.all_values if compiled.search(v)]
|
|
531
|
+
included_values.update(matched)
|
|
532
|
+
|
|
533
|
+
self.grouped_values = {}
|
|
534
|
+
for i, pat in enumerate(self.group_patterns):
|
|
535
|
+
if pat == "__OTHERS__":
|
|
536
|
+
self.grouped_values[i] = [v for v in self.all_values if v not in included_values]
|
|
537
|
+
else:
|
|
538
|
+
self.grouped_values[i] = [v for v in self.all_values if pat.search(v)]
|
|
539
|
+
|
|
540
|
+
self.current_group = 0
|
|
541
|
+
self.switch_group(self.current_group)
|
|
542
|
+
if "image_values" in kwargs:
|
|
543
|
+
self.image_values = kwargs.pop("image_values")
|
|
544
|
+
self.image_values = None if len(self.image_values) != len(self.values) else self.image_values
|
|
545
|
+
if self.image_values is not None:
|
|
546
|
+
for i in range(len(self.values)):
|
|
547
|
+
self.widgets[i][0].configure(image=self.image_values[i])
|
|
548
|
+
if "button_color" in kwargs:
|
|
549
|
+
bc = kwargs.pop("button_color")
|
|
550
|
+
for key in self.widgets:
|
|
551
|
+
self.widgets[key][0].configure(fg_color=bc)
|
|
552
|
+
if self.old_kwargs != kwargs:
|
|
553
|
+
for key in self.widgets:
|
|
554
|
+
self.widgets[key][0].configure(**kwargs)
|
|
555
|
+
self.old_kwargs = kwargs
|
|
556
|
+
self.master.update()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: CTkScrollableDropdownPP
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Enhanced CTkScrollableDropdown with pagination, search and groups support
|
|
5
|
+
Author: PLauncher-Team, Akash Bora
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PLauncher-Team/CTkScrollableDropdownPP
|
|
8
|
+
Project-URL: Original, https://github.com/Akascape/CTkScrollableDropdown
|
|
9
|
+
Keywords: customtkinter,dropdown,pagination,search,groups
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: customtkinter>=5.2.0
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# CTkScrollableDropdownPP
|
|
20
|
+
Enhanced CTkScrollableDropdown - A modern, feature-packed upgrade of the original dropdown widget for CustomTkinter with pagination, search, and grouping support.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
CTkScrollableDropdownPP/__init__.py
|
|
5
|
+
CTkScrollableDropdownPP/ctk_scrollable_dropdown.py
|
|
6
|
+
CTkScrollableDropdownPP.egg-info/PKG-INFO
|
|
7
|
+
CTkScrollableDropdownPP.egg-info/SOURCES.txt
|
|
8
|
+
CTkScrollableDropdownPP.egg-info/dependency_links.txt
|
|
9
|
+
CTkScrollableDropdownPP.egg-info/requires.txt
|
|
10
|
+
CTkScrollableDropdownPP.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
customtkinter>=5.2.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
CTkScrollableDropdownPP
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 PLauncher Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: CTkScrollableDropdownPP
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Enhanced CTkScrollableDropdown with pagination, search and groups support
|
|
5
|
+
Author: PLauncher-Team, Akash Bora
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PLauncher-Team/CTkScrollableDropdownPP
|
|
8
|
+
Project-URL: Original, https://github.com/Akascape/CTkScrollableDropdown
|
|
9
|
+
Keywords: customtkinter,dropdown,pagination,search,groups
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: customtkinter>=5.2.0
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# CTkScrollableDropdownPP
|
|
20
|
+
Enhanced CTkScrollableDropdown - A modern, feature-packed upgrade of the original dropdown widget for CustomTkinter with pagination, search, and grouping support.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "CTkScrollableDropdownPP"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Enhanced CTkScrollableDropdown with pagination, search and groups support"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "PLauncher-Team"},
|
|
11
|
+
{name = "Akash Bora"}
|
|
12
|
+
]
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
license = {text = "MIT"}
|
|
15
|
+
requires-python = ">=3.8"
|
|
16
|
+
classifiers = [
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
keywords = ["customtkinter", "dropdown", "pagination", "search", "groups"]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"customtkinter>=5.2.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/PLauncher-Team/CTkScrollableDropdownPP"
|
|
28
|
+
Original = "https://github.com/Akascape/CTkScrollableDropdown"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
packages = ["CTkScrollableDropdownPP"]
|
|
32
|
+
include-package-data = true
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.package-data]
|
|
35
|
+
"CTkScrollableDropdownPP" = ["*.py", "*.md", "LICENSE"]
|