CTkScrollableDropdownPP 2.3.1__tar.gz → 2.4__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.
@@ -46,6 +46,7 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
46
46
  self.animating = False
47
47
  self.multiple = multiple
48
48
  self.selected_values = []
49
+ self.is_built = False
49
50
 
50
51
  if groups is not None:
51
52
  for g in groups:
@@ -106,55 +107,27 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
106
107
  if not scrollbar:
107
108
  self.scroll_button_color = self.fg_color
108
109
  self.scroll_hover_color = self.fg_color
109
- if self.pagination:
110
- self.search_var = customtkinter.StringVar()
111
- self.search_var.trace_add('write', lambda *args: self.live_update(self.search_var.get()))
112
- self.search_entry = customtkinter.CTkEntry(self, textvariable=self.search_var)
113
- self.search_entry.pack(fill="x", pady=(0, 5))
114
- if self.groups:
115
- self.group_frame = customtkinter.CTkFrame(self, fg_color=self.fg_color, bg_color=self.transparent_color)
116
- self.group_frame.pack(fill="x", padx=5, pady=(0, 5))
117
- self.group_buttons = []
118
- for idx, name in enumerate(self.group_names):
119
- btn = customtkinter.CTkButton(
120
- self.group_frame,
121
- text=name,
122
- font=self.font,
123
- height=button_height,
124
- fg_color=self.button_color,
125
- text_color=self.text_color,
126
- hover_color=self.hover_color,
127
- command=lambda i=idx: self.switch_group(i),
128
- )
129
- self.group_buttons.append(btn)
130
- self.group_frame.bind("<Configure>", self._on_group_frame_configure, add="+")
131
- self.group_button_colors = [btn.cget("fg_color") for btn in self.group_buttons]
132
- self.frame = customtkinter.CTkScrollableFrame(
133
- self,
134
- bg_color=self.transparent_color,
135
- fg_color=self.fg_color,
136
- scrollbar_button_hover_color=self.scroll_hover_color,
137
- corner_radius=self.corner,
138
- border_width=frame_border_width,
139
- scrollbar_button_color=self.scroll_button_color,
140
- border_color=self.frame_border_color
141
- )
142
- self.frame._scrollbar.grid_configure(padx=3)
143
- self.frame.pack(expand=True, fill="both", pady=(3, 0))
144
- if self.pagination:
145
- self.button_container = customtkinter.CTkFrame(self.frame, fg_color=self.fg_color)
146
- self.button_container.pack(expand=True, fill="both")
147
-
148
- self.pagination_frame = customtkinter.CTkFrame(self, fg_color=self.fg_color)
149
- self.pagination_frame.pack(fill="x", pady=(3, 0))
150
- else:
151
- self.button_container = self.frame
152
- self.dummy_entry = customtkinter.CTkEntry(self.frame, fg_color="transparent", border_width=0, height=1, width=1)
153
- self.is_height = bool(height)
154
- if height is None:
155
- self.height_new = attach.winfo_toplevel().winfo_height()
110
+
111
+ self.resizable(width=False, height=False)
112
+ self.transient(self.master)
113
+ if double_click or isinstance(self.attach, customtkinter.CTkEntry) or isinstance(self.attach, customtkinter.CTkComboBox):
114
+ self.attach.bind('<Double-Button-1>', lambda e: self._iconify(), add="+")
156
115
  else:
157
- self.height_new = height
116
+ self.attach.bind('<Button-1>', lambda e: self._iconify(), add="+")
117
+ if isinstance(self.attach, customtkinter.CTkComboBox):
118
+ self.attach._canvas.tag_bind("right_parts", "<Button-1>", lambda e: self._iconify())
119
+ self.attach._canvas.tag_bind("dropdown_arrow", "<Button-1>", lambda e: self._iconify())
120
+ if self.command is None:
121
+ self.command = self.attach.set
122
+ if isinstance(self.attach, customtkinter.CTkOptionMenu):
123
+ self.attach._canvas.bind("<Button-1>", lambda e: self._iconify())
124
+ self.attach._text_label.bind("<Button-1>", lambda e: self._iconify())
125
+ if self.command is None:
126
+ self.command = self.attach.set
127
+ self.attach.bind("<Destroy>", lambda _: self._withdraw(), add="+")
128
+ self.update_idletasks()
129
+ self.x = x
130
+ self.y = y
158
131
  self.width = width
159
132
  self.command = command
160
133
  self.fade = False
@@ -173,33 +146,94 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
173
146
  if image_values and len(image_values) == len(values):
174
147
  for val, img in zip(values, image_values):
175
148
  self.value_to_image[val] = img
176
- self.resizable(width=False, height=False)
177
- self.transient(self.master)
178
- if double_click or isinstance(self.attach, customtkinter.CTkEntry) or isinstance(self.attach, customtkinter.CTkComboBox):
179
- self.attach.bind('<Double-Button-1>', lambda e: self._iconify(), add="+")
149
+
150
+ self.is_height = bool(height)
151
+ if height is None:
152
+ self.height_new = 10
180
153
  else:
181
- self.attach.bind('<Button-1>', lambda e: self._iconify(), add="+")
182
- if isinstance(self.attach, customtkinter.CTkComboBox):
183
- self.attach._canvas.tag_bind("right_parts", "<Button-1>", lambda e: self._iconify())
184
- self.attach._canvas.tag_bind("dropdown_arrow", "<Button-1>", lambda e: self._iconify())
185
- if self.command is None:
186
- self.command = self.attach.set
187
- if isinstance(self.attach, customtkinter.CTkOptionMenu):
188
- self.attach._canvas.bind("<Button-1>", lambda e: self._iconify())
189
- self.attach._text_label.bind("<Button-1>", lambda e: self._iconify())
190
- if self.command is None:
191
- self.command = self.attach.set
192
- self.attach.bind("<Destroy>", lambda _: self._destroy(), add="+")
193
- self.update_idletasks()
194
- self.x = x
195
- self.y = y
154
+ self.height_new = height
155
+
196
156
  if self.autocomplete:
197
157
  self.bind_autocomplete()
158
+
159
+ self._build_ui()
160
+
198
161
  self.withdraw()
199
162
  self.attributes("-alpha", 0)
200
163
  if self.groups:
201
164
  self.switch_group(0)
202
165
  self._init_buttons()
166
+ self.is_built = True
167
+
168
+ def _build_ui(self):
169
+ if self.is_built:
170
+ return
171
+
172
+ if self.pagination:
173
+ if not hasattr(self, 'search_var'):
174
+ self.search_var = customtkinter.StringVar()
175
+ self.search_var.trace_add('write', lambda *args: self.live_update(self.search_var.get()))
176
+ if not hasattr(self, 'search_entry'):
177
+ self.search_entry = customtkinter.CTkEntry(self, textvariable=self.search_var)
178
+ self.search_entry.pack(fill="x", pady=(0, 5))
179
+
180
+ if self.groups:
181
+ if not hasattr(self, 'group_frame'):
182
+ self.group_frame = customtkinter.CTkFrame(self, fg_color=self.fg_color, bg_color=self.transparent_color)
183
+ self.group_frame.pack(fill="x", padx=5, pady=(0, 5))
184
+ if not hasattr(self, 'group_buttons'):
185
+ self.group_buttons = []
186
+ for idx, name in enumerate(self.group_names):
187
+ btn = customtkinter.CTkButton(
188
+ self.group_frame,
189
+ text=name,
190
+ font=self.font,
191
+ height=self.button_height,
192
+ fg_color=self.button_color,
193
+ text_color=self.text_color,
194
+ hover_color=self.hover_color,
195
+ command=lambda i=idx: self.switch_group(i),
196
+ )
197
+ self.group_buttons.append(btn)
198
+ self.group_frame.bind("<Configure>", self._on_group_frame_configure, add="+")
199
+ self.group_button_colors = [btn.cget("fg_color") for btn in self.group_buttons]
200
+
201
+ if self.pagination:
202
+ if not hasattr(self, 'pagination_frame'):
203
+ self.pagination_frame = customtkinter.CTkFrame(self, fg_color=self.fg_color)
204
+ self.pagination_frame.pack(fill="x", pady=(3, 0), side="bottom")
205
+ if not hasattr(self, 'scroll_frame'):
206
+ self.scroll_frame = customtkinter.CTkScrollableFrame(
207
+ self,
208
+ bg_color=self.transparent_color,
209
+ fg_color=self.fg_color,
210
+ scrollbar_button_hover_color=self.scroll_hover_color,
211
+ corner_radius=self.corner,
212
+ border_width=0,
213
+ scrollbar_button_color=self.scroll_button_color,
214
+ border_color=self.frame_border_color
215
+ )
216
+ self.scroll_frame._scrollbar.grid_configure(padx=3)
217
+ self.scroll_frame.pack(expand=True, fill="both", pady=(3, 0), side="top")
218
+ if not hasattr(self, 'button_container'):
219
+ self.button_container = customtkinter.CTkFrame(self.scroll_frame, fg_color=self.fg_color)
220
+ self.button_container.pack(expand=True, fill="both")
221
+ else:
222
+ if not hasattr(self, 'scroll_frame'):
223
+ self.scroll_frame = customtkinter.CTkScrollableFrame(
224
+ self,
225
+ bg_color=self.transparent_color,
226
+ fg_color=self.fg_color,
227
+ scrollbar_button_hover_color=self.scroll_hover_color,
228
+ corner_radius=self.corner,
229
+ border_width=0,
230
+ scrollbar_button_color=self.scroll_button_color,
231
+ border_color=self.frame_border_color
232
+ )
233
+ self.scroll_frame._scrollbar.grid_configure(padx=3)
234
+ self.scroll_frame.pack(expand=True, fill="both", pady=(3, 0))
235
+ if not hasattr(self, 'button_container'):
236
+ self.button_container = self.scroll_frame
203
237
 
204
238
  def _on_group_frame_configure(self, event):
205
239
  if not self.group_buttons:
@@ -247,6 +281,9 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
247
281
  btn.configure(fg_color=self.group_button_colors[i])
248
282
  self.filtered_values = None
249
283
  self.current_page = 0
284
+ if hasattr(self, "search_var") and self.search_var.get().strip() != "":
285
+ string = self.search_var.get().lower()
286
+ self.filtered_values = [val for val in self.values if string in val.lower()]
250
287
  self._init_buttons()
251
288
 
252
289
  def _update_button_appearance(self, btn, value):
@@ -287,9 +324,6 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
287
324
  self.widgets[i][1] = False
288
325
  i += 1
289
326
 
290
- def _destroy(self):
291
- self.after(500, self.destroy_popup)
292
-
293
327
  def _withdraw(self):
294
328
  if self.animating:
295
329
  return
@@ -320,89 +354,102 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
320
354
 
321
355
  def _init_buttons(self):
322
356
  if self.pagination:
323
- values_to_show = self.values[self.current_page * self.items_per_page:
324
- (self.current_page + 1) * self.items_per_page]
325
- self._update_pagination_buttons()
357
+ self.scroll_frame.update_idletasks()
358
+ if self.filtered_values is not None:
359
+ values_to_show = self.filtered_values[self.current_page * self.items_per_page:
360
+ (self.current_page + 1) * self.items_per_page]
361
+ else:
362
+ values_to_show = self.values[self.current_page * self.items_per_page:
363
+ (self.current_page + 1) * self.items_per_page]
364
+ self._update_pagination_buttons(filtered=bool(self.filtered_values))
365
+ self.pagination_frame.update_idletasks()
326
366
  else:
327
367
  values_to_show = self.values
328
368
  self.update_buttons(values_to_show)
329
- self.frame._parent_canvas.yview_moveto(0)
369
+ self.scroll_frame._parent_canvas.yview_moveto(0)
330
370
 
331
371
  def _update_pagination_buttons(self, filtered=False):
332
372
  if not self.pagination:
333
373
  return
334
- for child in self.pagination_frame.winfo_children():
335
- child.destroy()
336
374
  values_list = self.filtered_values if (filtered and self.filtered_values is not None) else self.values
337
375
  total_pages = (len(values_list) + self.items_per_page - 1) // self.items_per_page
338
376
  button_width = 30
339
377
  button_padding = 4
340
- available_width = self.pagination_frame.winfo_width() or self.width_new or self.attach.winfo_width() or 200
378
+ available_width = self.width_new or self.attach.winfo_width() or 200
341
379
  max_buttons_per_row = max(1, available_width // (button_width + button_padding))
342
380
  total_rows = (total_pages + max_buttons_per_row - 1) // max_buttons_per_row
343
- for row in range(total_rows):
344
- for col in range(max_buttons_per_row):
345
- page_index = row * max_buttons_per_row + col
346
- if page_index >= total_pages:
347
- break
348
- state = "disabled" if page_index == self.current_page else "normal"
349
- btn = customtkinter.CTkButton(
350
- self.pagination_frame,
351
- text=str(page_index + 1),
352
- width=button_width,
353
- height=30,
354
- fg_color=self.button_color,
355
- text_color=self.text_color,
356
- command=lambda p=page_index: self._change_page(p),
357
- state=state
358
- )
381
+ needed_buttons = total_pages
382
+ existing_buttons = list(self.pagination_frame.winfo_children())
383
+ for i, btn in enumerate(existing_buttons):
384
+ if i < needed_buttons:
385
+ row = i // max_buttons_per_row
386
+ col = i % max_buttons_per_row
387
+ state = "disabled" if i == self.current_page else "normal"
388
+ btn.configure(text=str(i + 1), state=state, command=lambda p=i: self._change_page(p))
359
389
  btn.grid(row=row, column=col, padx=2, pady=2, sticky="ew")
390
+ else:
391
+ btn.grid_forget()
392
+ for i in range(len(existing_buttons), needed_buttons):
393
+ row = i // max_buttons_per_row
394
+ col = i % max_buttons_per_row
395
+ state = "disabled" if i == self.current_page else "normal"
396
+ btn = customtkinter.CTkButton(
397
+ self.pagination_frame,
398
+ text=str(i + 1),
399
+ width=button_width,
400
+ height=30,
401
+ fg_color=self.button_color,
402
+ text_color=self.text_color,
403
+ command=lambda p=i: self._change_page(p),
404
+ state=state
405
+ )
406
+ btn.grid(row=row, column=col, padx=2, pady=2, sticky="ew")
360
407
  for col in range(max_buttons_per_row):
361
408
  self.pagination_frame.grid_columnconfigure(col, weight=1)
362
409
 
363
410
  def _change_page(self, page_index):
364
411
  self.current_page = page_index
365
- if self.filtered_values is None:
366
- values_to_show = self.values[self.current_page * self.items_per_page:
367
- (self.current_page + 1) * self.items_per_page]
368
- self.update_buttons(values_to_show)
369
- self._update_pagination_buttons(filtered=bool(self.filtered_values))
370
- self.frame.update_idletasks()
371
- self.frame._parent_canvas.yview_moveto(0)
372
-
373
- def destroy_popup(self):
374
- self.destroy()
375
- self.disable = True
412
+ values_list = self.filtered_values if self.filtered_values is not None else self.values
413
+ values_to_show = values_list[self.current_page * self.items_per_page:
414
+ (self.current_page + 1) * self.items_per_page]
415
+ self.update_buttons(values_to_show)
416
+ self.scroll_frame.update_idletasks()
417
+ self._update_pagination_buttons(filtered=bool(self.filtered_values))
418
+ self.scroll_frame._parent_canvas.yview_moveto(0)
376
419
 
377
420
  def place_dropdown(self):
378
421
  if not self.winfo_exists():
379
422
  return
380
- current_width = self.winfo_width()
381
- current_height = self.winfo_height()
382
- current_x_pos = self.winfo_x()
383
- current_y_pos = self.winfo_y()
384
423
  self.x_pos = self.attach.winfo_rootx() if self.x is None else self.x + self.attach.winfo_rootx()
385
424
  self.width_new = self.attach.winfo_width() if self.width is None else self.width
425
+
386
426
  if not self.is_height:
387
- self.height_new = self.attach.winfo_toplevel().winfo_height()
427
+ needed_height = 10
428
+ if hasattr(self, 'search_entry') and self.search_entry.winfo_exists():
429
+ needed_height += self.search_entry.winfo_reqheight() + 5
430
+ if self.groups and hasattr(self, 'group_frame') and self.group_frame.winfo_exists():
431
+ needed_height += self.group_frame.winfo_reqheight() + 5
432
+ if self.pagination and hasattr(self, 'pagination_frame') and self.pagination_frame.winfo_exists():
433
+ needed_height += self.pagination_frame.winfo_reqheight() + 5
434
+ needed_height += min(len(self.values) if self.filtered_values is None else len(self.filtered_values), self.items_per_page) * (self.button_height + 4) + 10
435
+ screen_height = self.winfo_screenheight()
436
+ available = screen_height - self.attach.winfo_rooty() - self.attach.winfo_height() - 20
437
+ available_above = self.attach.winfo_rooty() - 20
438
+ max_available = max(available, available_above)
439
+ self.height_new = min(needed_height, max_available, 500)
440
+ else:
441
+ self.height_new = self.height_new
442
+
388
443
  screen_height = self.winfo_screenheight()
389
444
  dropdown_bottom = self.attach.winfo_rooty() + self.height_new
390
445
  if dropdown_bottom > screen_height:
391
446
  self.y_pos = max(0, self.attach.winfo_rooty() - self.height_new)
392
447
  else:
393
448
  self.y_pos = self.attach.winfo_rooty() + self.attach.winfo_height()
394
- if (current_width != self.width_new
395
- or current_height != self.height_new
396
- or current_x_pos != self.x_pos
397
- or current_y_pos != self.y_pos):
398
- self.geometry(f"{self.width_new}x{self.height_new}+{self.x_pos}+{self.y_pos}")
399
- if self.fade_enabled:
400
- self.attributes('-alpha', 0)
401
- else:
402
- self.attributes('-alpha', self.alpha)
403
- self.update_idletasks()
404
- if self.pagination:
405
- self._update_pagination_buttons(filtered=bool(self.filtered_values))
449
+ self.geometry(f"{self.width_new}x{self.height_new}+{self.x_pos}+{self.y_pos}")
450
+ self.update_idletasks()
451
+ if self.pagination:
452
+ self._update_pagination_buttons(filtered=bool(self.filtered_values))
406
453
 
407
454
  def _iconify(self):
408
455
  if self.animating:
@@ -458,7 +505,7 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
458
505
  def live_update(self, string=None):
459
506
  if self.disable or self.fade or self.animating:
460
507
  return
461
- self.frame._parent_canvas.yview_moveto(0)
508
+ self.scroll_frame._parent_canvas.yview_moveto(0)
462
509
  if string and string.strip() != "":
463
510
  string = string.lower()
464
511
  filtered = [val for val in self.values if string in val.lower()]
@@ -474,15 +521,13 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
474
521
  self.update_buttons(values_to_show)
475
522
  if self.pagination:
476
523
  self.pagination_frame.pack(fill="x", pady=(3, 0))
477
- self.place_dropdown()
478
524
  else:
479
525
  if self.pagination:
480
526
  self.pagination_frame.pack(fill="x", pady=(3, 0))
481
527
  self.filtered_values = None
482
528
  self.current_page = 0
483
529
  self._init_buttons()
484
- self.place_dropdown()
485
- self.appear = False
530
+ self.appear = False
486
531
 
487
532
  def insert(self, value, **kwargs):
488
533
  index = len(self.values)
@@ -542,11 +587,14 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
542
587
  if "width" in kwargs:
543
588
  self.width = kwargs.pop("width")
544
589
  if "fg_color" in kwargs:
545
- self.frame.configure(fg_color=kwargs.pop("fg_color"))
590
+ self.scroll_frame.configure(fg_color=kwargs.pop("fg_color"))
546
591
  if "values" in kwargs:
547
592
  self.all_values = kwargs.pop("values")
548
593
  self.values = self.all_values.copy()
549
- self.image_values = None
594
+ self.value_to_image = {}
595
+ if self.image_values and len(self.image_values) == len(self.all_values):
596
+ for val, img in zip(self.all_values, self.image_values):
597
+ self.value_to_image[val] = img
550
598
  self.current_page = 0
551
599
  self._init_buttons()
552
600
  if hasattr(self, "search_var"):
@@ -693,4 +741,4 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
693
741
  except Exception:
694
742
  pass
695
743
  self.after(interval, lambda: step_fn(frame + 1, new_value))
696
- step_fn(0, current_alpha)
744
+ step_fn(0, current_alpha)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CTkScrollableDropdownPP
3
- Version: 2.3.1
3
+ Version: 2.4
4
4
  Summary: Enhanced CTkScrollableDropdown with pagination, search and groups support
5
5
  Author: PLauncher-Team, Akash Bora
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CTkScrollableDropdownPP
3
- Version: 2.3.1
3
+ Version: 2.4
4
4
  Summary: Enhanced CTkScrollableDropdown with pagination, search and groups support
5
5
  Author: PLauncher-Team, Akash Bora
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "CTkScrollableDropdownPP"
7
- version = "2.3.1"
7
+ version = "2.4"
8
8
  description = "Enhanced CTkScrollableDropdown with pagination, search and groups support"
9
9
  authors = [
10
10
  {name = "PLauncher-Team"},