CTkScrollableDropdownPP 2.3__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,90 +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
- ratio = len(values_to_show) / self.items_per_page
372
- self.frame._parent_canvas.yview_moveto(0 if ratio < 0.5 else ratio)
373
-
374
- def destroy_popup(self):
375
- self.destroy()
376
- 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)
377
419
 
378
420
  def place_dropdown(self):
379
421
  if not self.winfo_exists():
380
422
  return
381
- current_width = self.winfo_width()
382
- current_height = self.winfo_height()
383
- current_x_pos = self.winfo_x()
384
- current_y_pos = self.winfo_y()
385
423
  self.x_pos = self.attach.winfo_rootx() if self.x is None else self.x + self.attach.winfo_rootx()
386
424
  self.width_new = self.attach.winfo_width() if self.width is None else self.width
425
+
387
426
  if not self.is_height:
388
- 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
+
389
443
  screen_height = self.winfo_screenheight()
390
444
  dropdown_bottom = self.attach.winfo_rooty() + self.height_new
391
445
  if dropdown_bottom > screen_height:
392
446
  self.y_pos = max(0, self.attach.winfo_rooty() - self.height_new)
393
447
  else:
394
448
  self.y_pos = self.attach.winfo_rooty() + self.attach.winfo_height()
395
- if (current_width != self.width_new
396
- or current_height != self.height_new
397
- or current_x_pos != self.x_pos
398
- or current_y_pos != self.y_pos):
399
- self.geometry(f"{self.width_new}x{self.height_new}+{self.x_pos}+{self.y_pos}")
400
- if self.fade_enabled:
401
- self.attributes('-alpha', 0)
402
- else:
403
- self.attributes('-alpha', self.alpha)
404
- self.update_idletasks()
405
- if self.pagination:
406
- 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))
407
453
 
408
454
  def _iconify(self):
409
455
  if self.animating:
@@ -459,7 +505,7 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
459
505
  def live_update(self, string=None):
460
506
  if self.disable or self.fade or self.animating:
461
507
  return
462
- self.frame._parent_canvas.yview_moveto(0)
508
+ self.scroll_frame._parent_canvas.yview_moveto(0)
463
509
  if string and string.strip() != "":
464
510
  string = string.lower()
465
511
  filtered = [val for val in self.values if string in val.lower()]
@@ -475,15 +521,13 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
475
521
  self.update_buttons(values_to_show)
476
522
  if self.pagination:
477
523
  self.pagination_frame.pack(fill="x", pady=(3, 0))
478
- self.place_dropdown()
479
524
  else:
480
525
  if self.pagination:
481
526
  self.pagination_frame.pack(fill="x", pady=(3, 0))
482
527
  self.filtered_values = None
483
528
  self.current_page = 0
484
529
  self._init_buttons()
485
- self.place_dropdown()
486
- self.appear = False
530
+ self.appear = False
487
531
 
488
532
  def insert(self, value, **kwargs):
489
533
  index = len(self.values)
@@ -543,11 +587,14 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
543
587
  if "width" in kwargs:
544
588
  self.width = kwargs.pop("width")
545
589
  if "fg_color" in kwargs:
546
- self.frame.configure(fg_color=kwargs.pop("fg_color"))
590
+ self.scroll_frame.configure(fg_color=kwargs.pop("fg_color"))
547
591
  if "values" in kwargs:
548
592
  self.all_values = kwargs.pop("values")
549
593
  self.values = self.all_values.copy()
550
- 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
551
598
  self.current_page = 0
552
599
  self._init_buttons()
553
600
  if hasattr(self, "search_var"):
@@ -694,4 +741,4 @@ class CTkScrollableDropdown(customtkinter.CTkToplevel):
694
741
  except Exception:
695
742
  pass
696
743
  self.after(interval, lambda: step_fn(frame + 1, new_value))
697
- 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
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
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"
7
+ version = "2.4"
8
8
  description = "Enhanced CTkScrollableDropdown with pagination, search and groups support"
9
9
  authors = [
10
10
  {name = "PLauncher-Team"},