|
| 1 | +import tkinter as tk |
| 2 | +from tkinter import ttk |
| 3 | +from utils.constants import FONT_UI_HEADER, FONT_UI_NORMAL, SECONDARY_BG, TERTIARY_BG, TEXT_PRIMARY |
| 4 | + |
| 5 | +class CollapsiblePane(ttk.Frame): |
| 6 | + """A collapsible pane widget that can hide or show its content.""" |
| 7 | + def __init__(self, parent, text="", body_background=SECONDARY_BG): |
| 8 | + super().__init__(parent, style='CardInner.TFrame') |
| 9 | + |
| 10 | + self.columnconfigure(0, weight=1) |
| 11 | + self.body_background = body_background |
| 12 | + |
| 13 | + # Header |
| 14 | + self.header_frame = ttk.Frame(self, style='CardInner.TFrame') |
| 15 | + self.header_frame.grid(row=0, column=0, sticky='ew') |
| 16 | + self.header_frame.columnconfigure(1, weight=1) |
| 17 | + |
| 18 | + self.toggle_button = ttk.Label(self.header_frame, text="▼", font=('Segoe UI', 10), style='TLabel') |
| 19 | + self.toggle_button.grid(row=0, column=0, padx=5, sticky='w') |
| 20 | + |
| 21 | + self.title_label = ttk.Label(self.header_frame, text=text, font=FONT_UI_HEADER, style='Header.TLabel') |
| 22 | + self.title_label.grid(row=0, column=1, sticky='w') |
| 23 | + |
| 24 | + # Body |
| 25 | + self.body = ttk.Frame(self, style='CardInner.TFrame', padding=(15, 10)) |
| 26 | + |
| 27 | + self.toggle_button.bind("<Button-1>", self._toggle) |
| 28 | + self.title_label.bind("<Button-1>", self._toggle) |
| 29 | + self._is_collapsed = False |
| 30 | + self.body.grid(row=1, column=0, sticky='nsew', padx=5, pady=5) # Start expanded |
| 31 | + |
| 32 | + def _toggle(self, event): |
| 33 | + if self._is_collapsed: |
| 34 | + self.body.grid(row=1, column=0, sticky='nsew', padx=5, pady=5) |
| 35 | + self.toggle_button.configure(text="▼") |
| 36 | + else: |
| 37 | + self.body.grid_remove() |
| 38 | + self.toggle_button.configure(text="▶") |
| 39 | + self._is_collapsed = not self._is_collapsed |
| 40 | + |
| 41 | +class ToolTip: |
| 42 | + def __init__(self, widget, text): |
| 43 | + self.widget, self.text, self.tooltip_window = widget, text, None |
| 44 | + self.widget.bind("<Enter>", self.show) |
| 45 | + self.widget.bind("<Leave>", self.hide) |
| 46 | + def show(self, event=None): |
| 47 | + if self.tooltip_window or not self.text: return |
| 48 | + x, y, _, _ = self.widget.bbox("insert") |
| 49 | + x += self.widget.winfo_rootx() + 25 |
| 50 | + y += self.widget.winfo_rooty() + 25 |
| 51 | + self.tooltip_window = tw = tk.Toplevel(self.widget) |
| 52 | + tw.wm_overrideredirect(True) |
| 53 | + tw.wm_geometry(f"+{x}+{y}") |
| 54 | + ttk.Label(tw, text=self.text, background=TERTIARY_BG, foreground=TEXT_PRIMARY, font=FONT_UI_NORMAL, relief='solid', borderwidth=1, padding=5).pack() |
| 55 | + def hide(self, event=None): |
| 56 | + if self.tooltip_window: self.tooltip_window.destroy() |
| 57 | + self.tooltip_window = None |
| 58 | + |
| 59 | +class CustomDropdownMenu(ttk.Frame): |
| 60 | + def __init__(self, parent, textvariable, options, style_prefix, **kwargs): |
| 61 | + super().__init__(parent, **kwargs) |
| 62 | + self.textvariable = textvariable |
| 63 | + self.options = options |
| 64 | + self.style_prefix = style_prefix |
| 65 | + self.is_open = False |
| 66 | + |
| 67 | + self.button = ttk.Button(self, textvariable=self.textvariable, command=self.toggle, style=f"{self.style_prefix}.TButton") |
| 68 | + self.button.pack(fill=tk.BOTH, expand=True) |
| 69 | + |
| 70 | + self.window = tk.Toplevel(self.master) |
| 71 | + self.window.withdraw() |
| 72 | + self.window.wm_overrideredirect(True) |
| 73 | + |
| 74 | + self.frame = ttk.Frame(self.window, style=f"{self.style_prefix}.TFrame") |
| 75 | + self.frame.pack() |
| 76 | + |
| 77 | + self.canvas = tk.Canvas(self.frame, background=SECONDARY_BG, highlightthickness=0) |
| 78 | + self.scrollbar = ttk.Scrollbar(self.frame, orient="vertical", command=self.canvas.yview, style="Vertical.TScrollbar") |
| 79 | + self.scrollable_frame = ttk.Frame(self.canvas, style=f"{self.style_prefix}.TFrame") |
| 80 | + |
| 81 | + self.scrollable_frame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) |
| 82 | + self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") |
| 83 | + self.canvas.configure(yscrollcommand=self.scrollbar.set) |
| 84 | + |
| 85 | + self.canvas.pack(side="left", fill="both", expand=True) |
| 86 | + self.scrollbar.pack(side="right", fill="y") |
| 87 | + |
| 88 | + self.master.bind("<Configure>", self.hide) |
| 89 | + self.button.bind("<Destroy>", self.hide) |
| 90 | + |
| 91 | + def toggle(self, event=None): |
| 92 | + if self.is_open: |
| 93 | + self.hide() |
| 94 | + else: |
| 95 | + self.show() |
| 96 | + |
| 97 | + def show(self): |
| 98 | + self.is_open = True |
| 99 | + self.update_options(self.options) |
| 100 | + x = self.button.winfo_rootx() |
| 101 | + y = self.button.winfo_rooty() + self.button.winfo_height() |
| 102 | + self.window.wm_geometry(f"+{x}+{y}") |
| 103 | + self.window.deiconify() |
| 104 | + self.window.lift() |
| 105 | + self.window.bind("<FocusOut>", self.hide) |
| 106 | + self.window.focus_set() |
| 107 | + |
| 108 | + def hide(self, event=None): |
| 109 | + self.is_open = False |
| 110 | + self.window.withdraw() |
| 111 | + |
| 112 | + def update_options(self, options): |
| 113 | + for widget in self.scrollable_frame.winfo_children(): |
| 114 | + widget.destroy() |
| 115 | + |
| 116 | + self.options = options |
| 117 | + for option in self.options: |
| 118 | + btn = ttk.Button(self.scrollable_frame, text=option, |
| 119 | + command=lambda o=option: self.select(o), |
| 120 | + style=f"{self.style_prefix}.Item.TButton") |
| 121 | + btn.pack(fill=tk.X, expand=True) |
| 122 | + |
| 123 | + def select(self, option): |
| 124 | + self.textvariable.set(option) |
| 125 | + self.hide() |
0 commit comments