diff --git a/CTkScrollableDropdown/__init__.py b/CTkScrollableDropdown/__init__.py new file mode 100644 index 00000000..8ba844b7 --- /dev/null +++ b/CTkScrollableDropdown/__init__.py @@ -0,0 +1,12 @@ +""" +CustomTkinter Scrollable Dropdown Menu +Author: Akash Bora +License: MIT +This is a custom dropdown menu for customtkinter. +Homepage: https://github.com/Akascape/CTkScrollableDropdown +""" + +__version__ = '0.9' + +from .ctk_scrollable_dropdown import CTkScrollableDropdown +from .ctk_scrollable_dropdown_frame import CTkScrollableDropdownFrame diff --git a/CTkScrollableDropdown/ctk_scrollable_dropdown.py b/CTkScrollableDropdown/ctk_scrollable_dropdown.py new file mode 100644 index 00000000..c54b6197 --- /dev/null +++ b/CTkScrollableDropdown/ctk_scrollable_dropdown.py @@ -0,0 +1,321 @@ +''' +Advanced Scrollable Dropdown class for customtkinter widgets +Author: Akash Bora +''' + +import customtkinter +import sys +import time + +class CTkScrollableDropdown(customtkinter.CTkToplevel): + + def __init__(self, attach, x=None, y=None, button_color=None, height: int = 200, width: int = None, + fg_color=None, button_height: int = 20, justify="center", scrollbar_button_color=None, + scrollbar=True, scrollbar_button_hover_color=None, frame_border_width=2, values=[], + command=None, image_values=[], alpha: float = 0.97, frame_corner_radius=20, double_click=False, + resize=True, frame_border_color=None, text_color=None, autocomplete=False, **button_kwargs): + + super().__init__(takefocus=1) + + self.focus() + self.alpha = alpha + self.attach = attach + self.corner = frame_corner_radius + self.padding = 0 + self.focus_something = False + self.disable = True + + if sys.platform.startswith("win"): + self.after(100, lambda: self.overrideredirect(True)) + self.transparent_color = self._apply_appearance_mode(self._fg_color) + self.attributes("-transparentcolor", self.transparent_color) + elif sys.platform.startswith("darwin"): + self.overrideredirect(True) + self.transparent_color = 'systemTransparent' + self.attributes("-transparent", True) + self.focus_something = True + else: + self.overrideredirect(True) + self.transparent_color = '#000001' + self.corner = 0 + self.padding = 18 + self.withdraw() + + self.hide = True + self.attach.bind('', lambda e: self._withdraw() if not self.disable else None, add="+") + self.attach.winfo_toplevel().bind('', lambda e: self._withdraw() if not self.disable else None, add="+") + self.attach.winfo_toplevel().bind("", lambda e: self._withdraw() if not self.disable else None, add="+") + self.attach.winfo_toplevel().bind("", lambda e: self._withdraw() if not self.disable else None, add="+") + self.attach.winfo_toplevel().bind("", lambda e: self._withdraw() if not self.disable else None, add="+") + + self.attributes('-alpha', 0) + self.disable = False + self.fg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if fg_color is None else fg_color + self.scroll_button_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_color"] if scrollbar_button_color is None else scrollbar_button_color + self.scroll_hover_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if scrollbar_button_hover_color is None else scrollbar_button_hover_color + self.frame_border_color = customtkinter.ThemeManager.theme["CTkFrame"]["border_color"] if frame_border_color is None else frame_border_color + self.button_color = customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"] if button_color is None else button_color + self.text_color = customtkinter.ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else text_color + + if scrollbar is False: + self.scroll_button_color = self.fg_color + self.scroll_hover_color = self.fg_color + + self.frame = customtkinter.CTkScrollableFrame(self, bg_color=self.transparent_color, fg_color=self.fg_color, + scrollbar_button_hover_color=self.scroll_hover_color, + corner_radius=self.corner, border_width=frame_border_width, + scrollbar_button_color=self.scroll_button_color, + border_color=self.frame_border_color) + self.frame._scrollbar.grid_configure(padx=3) + self.frame.pack(expand=True, fill="both") + self.dummy_entry = customtkinter.CTkEntry(self.frame, fg_color="transparent", border_width=0, height=1, width=1) + self.no_match = customtkinter.CTkLabel(self.frame, text="No Match") + self.height = height + self.height_new = height + self.width = width + self.command = command + self.fade = False + self.resize = resize + self.autocomplete = autocomplete + self.var_update = customtkinter.StringVar() + self.appear = False + + if justify.lower()=="left": + self.justify = "w" + elif justify.lower()=="right": + self.justify = "e" + else: + self.justify = "c" + + self.button_height = button_height + self.values = values + self.button_num = len(self.values) + self.image_values = None if len(image_values)!=len(self.values) else image_values + + self.resizable(width=False, height=False) + self.transient(self.master) + self._init_buttons(**button_kwargs) + + # Add binding for different ctk widgets + if double_click or self.attach.winfo_name().startswith("!ctkentry") or self.attach.winfo_name().startswith("!ctkcombobox"): + self.attach.bind('', lambda e: self._iconify(), add="+") + else: + self.attach.bind('', lambda e: self._iconify(), add="+") + + if self.attach.winfo_name().startswith("!ctkcombobox"): + self.attach._canvas.tag_bind("right_parts", "", lambda e: self._iconify()) + self.attach._canvas.tag_bind("dropdown_arrow", "", lambda e: self._iconify()) + if self.command is None: + self.command = self.attach.set + + if self.attach.winfo_name().startswith("!ctkoptionmenu"): + self.attach._canvas.bind("", lambda e: self._iconify()) + self.attach._text_label.bind("", lambda e: self._iconify()) + if self.command is None: + self.command = self.attach.set + + self.update_idletasks() + self.x = x + self.y = y + + if self.autocomplete: + self.bind_autocomplete() + + self.deiconify() + self.withdraw() + + self.attributes("-alpha", self.alpha) + + def _withdraw(self): + if self.hide is False: self.withdraw() + self.hide = True + + def _update(self, a, b, c): + self.live_update(self.attach._entry.get()) + + def bind_autocomplete(self, ): + def appear(x): + self.appear = True + + if self.attach.winfo_name().startswith("!ctkcombobox"): + self.attach._entry.configure(textvariable=self.var_update) + self.attach._entry.bind("", appear) + self.attach.set(self.values[0]) + self.var_update.trace_add('write', self._update) + + if self.attach.winfo_name().startswith("!ctkentry"): + self.attach.configure(textvariable=self.var_update) + self.attach.bind("", appear) + self.var_update.trace_add('write', self._update) + + def fade_out(self): + for i in range(100,0,-10): + if not self.winfo_exists(): + break + self.attributes("-alpha", i/100) + self.update() + time.sleep(1/100) + + def fade_in(self): + for i in range(0,100,10): + if not self.winfo_exists(): + break + self.attributes("-alpha", i/100) + self.update() + time.sleep(1/100) + + def _init_buttons(self, **button_kwargs): + self.i = 0 + self.widgets = {} + for row in self.values: + self.widgets[self.i] = customtkinter.CTkButton(self.frame, + text=row, + height=self.button_height, + fg_color=self.button_color, + text_color=self.text_color, + image=self.image_values[i] if self.image_values is not None else None, + anchor=self.justify, + command=lambda k=row: self._attach_key_press(k), **button_kwargs) + self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0)) + self.i+=1 + + self.hide = False + + def destroy_popup(self): + self.destroy() + self.disable = True + + def place_dropdown(self): + self.x_pos = self.attach.winfo_rootx() if self.x is None else self.x + self.attach.winfo_rootx() + self.y_pos = self.attach.winfo_rooty() + self.attach.winfo_reqheight() + 5 if self.y is None else self.y + self.attach.winfo_rooty() + self.width_new = self.attach.winfo_width() if self.width is None else self.width + + if self.resize: + if self.button_num==1: + self.height_new = self.button_height * self.button_num + 45 + else: + self.height_new = self.button_height * self.button_num + 35 + if self.height_new>self.height: + self.height_new = self.height + + self.geometry('{}x{}+{}+{}'.format(self.width_new, self.height_new, + self.x_pos, self.y_pos)) + self.fade_in() + self.attributes('-alpha', self.alpha) + self.attach.focus() + + def _iconify(self): + if self.disable: return + if self.hide: + self._deiconify() + self.focus() + self.hide = False + self.place_dropdown() + if self.focus_something: + self.dummy_entry.pack() + self.dummy_entry.focus_set() + self.after(100, self.dummy_entry.pack_forget) + else: + self.withdraw() + self.hide = True + + def _attach_key_press(self, k): + self.fade = True + if self.command: + self.command(k) + self.fade = False + self.fade_out() + self.withdraw() + self.hide = True + + def live_update(self, string=None): + if not self.appear: return + if self.disable: return + if self.fade: return + if string: + self._deiconify() + i=1 + for key in self.widgets.keys(): + s = self.widgets[key].cget("text") + if not s.startswith(string): + self.widgets[key].pack_forget() + else: + self.widgets[key].pack(fill="x", pady=2, padx=(self.padding, 0)) + i+=1 + + if i==1: + self.no_match.pack(fill="x", pady=2, padx=(self.padding, 0)) + else: + self.no_match.pack_forget() + self.button_num = i + self.place_dropdown() + + else: + self.no_match.pack_forget() + self.button_num = len(self.values) + for key in self.widgets.keys(): + self.widgets[key].destroy() + self._init_buttons() + self.place_dropdown() + + self.frame._parent_canvas.yview_moveto(0.0) + self.appear = False + + def insert(self, value, **kwargs): + self.widgets[self.i] = customtkinter.CTkButton(self.frame, + text=value, + height=self.button_height, + fg_color=self.button_color, + text_color=self.text_color, + anchor=self.justify, + command=lambda k=value: self._attach_key_press(k), **kwargs) + self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0)) + self.i+=1 + self.values.append(value) + + def _deiconify(self): + if len(self.values)>0: + self.deiconify() + + def popup(self, x=None, y=None): + self.x = x + self.y = y + self.hide = True + self._iconify() + + def configure(self, **kwargs): + if "height" in kwargs: + self.height = kwargs.pop("height") + self.height_new = self.height + + if "alpha" in kwargs: + self.alpha = kwargs.pop("alpha") + + if "width" in kwargs: + self.width = kwargs.pop("width") + + if "fg_color" in kwargs: + self.frame.configure(fg_color=kwargs.pop("fg_color")) + + if "values" in kwargs: + self.values = kwargs.pop("values") + self.image_values = None + for key in self.widgets.keys(): + self.widgets[key].destroy() + self._init_buttons() + + if "image_values" in kwargs: + self.image_values = kwargs.pop("image_values") + self.image_values = None if len(self.image_values)!=len(self.values) else self.image_values + if self.image_values is not None: + i=0 + for key in self.widgets.keys(): + self.widgets[key].configure(image=self.image_values[i]) + i+=1 + + if "button_color" in kwargs: + for key in self.widgets.keys(): + self.widgets[key].configure(fg_color=kwargs.pop("button_color")) + + for key in self.widgets.keys(): + self.widgets[key].configure(**kwargs) diff --git a/CTkScrollableDropdown/ctk_scrollable_dropdown_frame.py b/CTkScrollableDropdown/ctk_scrollable_dropdown_frame.py new file mode 100644 index 00000000..1bd93c2f --- /dev/null +++ b/CTkScrollableDropdown/ctk_scrollable_dropdown_frame.py @@ -0,0 +1,278 @@ +''' +Advanced Scrollable Dropdown Frame class for customtkinter widgets +Author: Akash Bora +''' + +import customtkinter +import sys + +class CTkScrollableDropdownFrame(customtkinter.CTkFrame): + + def __init__(self, attach, x=None, y=None, button_color=None, height: int = 200, width: int = None, + fg_color=None, button_height: int = 20, justify="center", scrollbar_button_color=None, + scrollbar=True, scrollbar_button_hover_color=None, frame_border_width=2, values=[], + command=None, image_values=[], double_click=False, frame_corner_radius=True, resize=True, frame_border_color=None, + text_color=None, autocomplete=False, **button_kwargs): + + super().__init__(master=attach.winfo_toplevel(), bg_color=attach.cget("bg_color")) + + self.attach = attach + self.corner = 11 if frame_corner_radius else 0 + self.padding = 0 + self.disable = True + + self.hide = True + self.attach.bind('', lambda e: self._withdraw() if not self.disable else None, add="+") + self.attach.winfo_toplevel().bind("", lambda e: self._withdraw() if not self.disable else None, add="+") + self.attach.winfo_toplevel().bind("", lambda e: self._withdraw() if not self.disable else None, add="+") + self.attach.winfo_toplevel().bind("", lambda e: self._withdraw() if not self.disable else None, add="+") + + self.disable = False + self.fg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if fg_color is None else fg_color + self.scroll_button_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_color"] if scrollbar_button_color is None else scrollbar_button_color + self.scroll_hover_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if scrollbar_button_hover_color is None else scrollbar_button_hover_color + self.frame_border_color = customtkinter.ThemeManager.theme["CTkFrame"]["border_color"] if frame_border_color is None else frame_border_color + self.button_color = customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"] if button_color is None else button_color + self.text_color = customtkinter.ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else text_color + + if scrollbar is False: + self.scroll_button_color = self.fg_color + self.scroll_hover_color = self.fg_color + + self.frame = customtkinter.CTkScrollableFrame(self, fg_color=self.fg_color, bg_color=attach.cget("bg_color"), + scrollbar_button_hover_color=self.scroll_hover_color, + corner_radius=self.corner, border_width=frame_border_width, + scrollbar_button_color=self.scroll_button_color, + border_color=self.frame_border_color) + self.frame._scrollbar.grid_configure(padx=3) + self.frame.pack(expand=True, fill="both") + + if self.corner==0: + self.corner = 21 + + self.dummy_entry = customtkinter.CTkEntry(self.frame, fg_color="transparent", border_width=0, height=1, width=1) + self.no_match = customtkinter.CTkLabel(self.frame, text="No Match") + self.height = height + self.height_new = height + self.width = width + self.command = command + self.fade = False + self.resize = resize + self.autocomplete = autocomplete + self.var_update = customtkinter.StringVar() + self.appear = False + + if justify.lower()=="left": + self.justify = "w" + elif justify.lower()=="right": + self.justify = "e" + else: + self.justify = "c" + + self.button_height = button_height + self.values = values + self.button_num = len(self.values) + self.image_values = None if len(image_values)!=len(self.values) else image_values + + self._init_buttons(**button_kwargs) + + # Add binding for different ctk widgets + if double_click or self.attach.winfo_name().startswith("!ctkentry") or self.attach.winfo_name().startswith("!ctkcombobox"): + self.attach.bind('', lambda e: self._iconify(), add="+") + self.attach._entry.bind('', lambda e: self._withdraw() if not self.disable else None, add="+") + else: + self.attach.bind('', lambda e: self._iconify(), add="+") + + if self.attach.winfo_name().startswith("!ctkcombobox"): + self.attach._canvas.tag_bind("right_parts", "", lambda e: self._iconify()) + self.attach._canvas.tag_bind("dropdown_arrow", "", lambda e: self._iconify()) + + if self.command is None: + self.command = self.attach.set + + if self.attach.winfo_name().startswith("!ctkoptionmenu"): + self.attach._canvas.bind("", lambda e: self._iconify()) + self.attach._text_label.bind("", lambda e: self._iconify()) + if self.command is None: + self.command = self.attach.set + + self.x = x + self.y = y + + if self.autocomplete: + self.bind_autocomplete() + + def _withdraw(self): + if self.hide is False: self.place_forget() + self.hide = True + + def _update(self, a, b, c): + self.live_update(self.attach._entry.get()) + + def bind_autocomplete(self, ): + def appear(x): + self.appear = True + + if self.attach.winfo_name().startswith("!ctkcombobox"): + self.attach._entry.configure(textvariable=self.var_update) + self.attach.set(self.values[0]) + self.attach._entry.bind("", appear) + self.var_update.trace_add('write', self._update) + + if self.attach.winfo_name().startswith("!ctkentry"): + self.attach.configure(textvariable=self.var_update) + self.attach.bind("", appear) + self.var_update.trace_add('write', self._update) + + def _init_buttons(self, **button_kwargs): + self.i = 0 + self.widgets = {} + for row in self.values: + self.widgets[self.i] = customtkinter.CTkButton(self.frame, + text=row, + height=self.button_height, + fg_color=self.button_color, + text_color=self.text_color, + image=self.image_values[i] if self.image_values is not None else None, + anchor=self.justify, + command=lambda k=row: self._attach_key_press(k), **button_kwargs) + self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0)) + self.i+=1 + + self.hide = False + + def destroy_popup(self): + self.destroy() + self.disable = True + + def place_dropdown(self): + self.x_pos = self.attach.winfo_x() if self.x is None else self.x + self.attach.winfo_rootx() + self.y_pos = self.attach.winfo_y() + self.attach.winfo_reqheight() + 5 if self.y is None else self.y + self.attach.winfo_rooty() + self.width_new = self.attach.winfo_width()-45+self.corner if self.width is None else self.width + + if self.resize: + if self.button_num==1: + self.height_new = self.button_height * self.button_num + 45 + else: + self.height_new = self.button_height * self.button_num + 35 + if self.height_new>self.height: + self.height_new = self.height + + self.frame.configure(width=self.width_new, height=self.height_new) + self.place(x=self.x_pos, y=self.y_pos) + + if sys.platform.startswith("darwin"): + self.dummy_entry.pack() + self.after(100, self.dummy_entry.pack_forget()) + + self.lift() + self.attach.focus() + + def _iconify(self): + if self.disable: return + if self.hide: + self.hide = False + self.place_dropdown() + else: + self.place_forget() + self.hide = True + + def _attach_key_press(self, k): + self.fade = True + if self.command: + self.command(k) + self.fade = False + self.place_forget() + self.hide = True + + def live_update(self, string=None): + if not self.appear: return + if self.disable: return + if self.fade: return + if string: + self._deiconify() + i=1 + for key in self.widgets.keys(): + s = self.widgets[key].cget("text") + if not s.startswith(string): + self.widgets[key].pack_forget() + else: + self.widgets[key].pack(fill="x", pady=2, padx=(self.padding, 0)) + i+=1 + + if i==1: + self.no_match.pack(fill="x", pady=2, padx=(self.padding, 0)) + else: + self.no_match.pack_forget() + self.button_num = i + self.place_dropdown() + + else: + self.no_match.pack_forget() + self.button_num = len(self.values) + for key in self.widgets.keys(): + self.widgets[key].destroy() + self._init_buttons() + self.place_dropdown() + + self.frame._parent_canvas.yview_moveto(0.0) + self.appear = False + + def insert(self, value, **kwargs): + self.widgets[self.i] = customtkinter.CTkButton(self.frame, + text=value, + height=self.button_height, + fg_color=self.button_color, + text_color=self.text_color, + anchor=self.justify, + command=lambda k=value: self._attach_key_press(k), **kwargs) + self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0)) + self.i+=1 + self.values.append(value) + + def _deiconify(self): + if len(self.values)>0: + self.pack_forget() + + def popup(self, x=None, y=None): + self.x = x + self.y = y + self.hide = True + self._iconify() + + def configure(self, **kwargs): + if "height" in kwargs: + self.height = kwargs.pop("height") + self.height_new = self.height + + if "alpha" in kwargs: + self.alpha = kwargs.pop("alpha") + + if "width" in kwargs: + self.width = kwargs.pop("width") + + if "fg_color" in kwargs: + self.frame.configure(fg_color=kwargs.pop("fg_color")) + + if "values" in kwargs: + self.values = kwargs.pop("values") + self.image_values = None + for key in self.widgets.keys(): + self.widgets[key].destroy() + self._init_buttons() + + if "image_values" in kwargs: + self.image_values = kwargs.pop("image_values") + self.image_values = None if len(self.image_values)!=len(self.values) else self.image_values + if self.image_values is not None: + i=0 + for key in self.widgets.keys(): + self.widgets[key].configure(image=self.image_values[i]) + i+=1 + + if "button_color" in kwargs: + for key in self.widgets.keys(): + self.widgets[key].configure(fg_color=kwargs.pop("button_color")) + + for key in self.widgets.keys(): + self.widgets[key].configure(**kwargs) diff --git a/window_config.py b/window_config.py index 1e4be837..214152e0 100644 --- a/window_config.py +++ b/window_config.py @@ -12,6 +12,8 @@ from audio_utils import get_input_device_list, get_output_device_list, get_defau from audio_recorder import SelectedMicEnergyRecorder, SelectedSpeakeEnergyRecorder from languages import translation_lang, transcription_lang, selectable_languages +from CTkScrollableDropdown import CTkScrollableDropdown + class ToplevelWindowConfig(CTkToplevel): def __init__(self, parent, *args, **kwargs): @@ -61,6 +63,9 @@ class ToplevelWindowConfig(CTkToplevel): save_json(self.parent.PATH_CONFIG, "UI_SCALING", self.parent.UI_SCALING) def optionmenu_font_family_callback(self, choice): + # set choice font + self.optionmenu_font_family.set(choice) + # tab menu self.tabview_config._segmented_button.configure(font=CTkFont(family=choice)) @@ -75,6 +80,7 @@ class ToplevelWindowConfig(CTkToplevel): self.label_font_family.configure(font=CTkFont(family=choice)) self.optionmenu_font_family.configure(font=CTkFont(family=choice)) self.optionmenu_font_family._dropdown_menu.configure(font=CTkFont(family=choice)) + self.scrollableDropdown_font_family.configure(font=CTkFont(family=choice)) self.label_ui_language.configure(font=CTkFont(family=choice)) self.optionmenu_ui_language.configure(font=CTkFont(family=choice)) self.optionmenu_ui_language._dropdown_menu.configure(font=CTkFont(family=choice)) @@ -520,13 +526,21 @@ class ToplevelWindowConfig(CTkToplevel): font_families = list(tk_font.families()) self.optionmenu_font_family = CTkOptionMenu( self.tabview_config.tab(config_tab_title_ui), - values=font_families, - command=self.optionmenu_font_family_callback, variable=StringVar(value=self.parent.FONT_FAMILY), font=CTkFont(family=self.parent.FONT_FAMILY), ) self.optionmenu_font_family.grid(row=row, column=1, columnspan=1, padx=padx, pady=pady, sticky="nsew") - self.optionmenu_font_family._dropdown_menu.configure(font=CTkFont(family=self.parent.FONT_FAMILY)) + + ## scrollableDropdown font family + self.scrollableDropdown_font_family = CTkScrollableDropdown( + self.optionmenu_font_family, + values=font_families, + justify="left", + button_color="transparent", + command=self.optionmenu_font_family_callback, + font=CTkFont(family=self.parent.FONT_FAMILY), + ) + self.scrollableDropdown_font_family.frame.bind("", lambda e: self.scrollableDropdown_font_family._iconify()) ## optionmenu ui language row += 1