From 053b0c22c567d29a51286f5a87b44c1af4860434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A9sir=C3=A9=20Werner=20Menrath?= Date: Fri, 8 Aug 2025 00:55:32 +0200 Subject: [PATCH] commit 67 --- __pycache__/cfd_animated_icon.cpython-312.pyc | Bin 0 -> 5951 bytes __pycache__/cfd_ui_setup.cpython-312.pyc | Bin 35399 -> 36089 bytes .../custom_file_dialog.cpython-312.pyc | Bin 96236 -> 100044 bytes cfd_animated_icon.py | 82 ++++++ cfd_ui_setup.py | 12 +- custom_file_dialog.py | 239 +++++++----------- mainwindow.py | 2 +- 7 files changed, 190 insertions(+), 145 deletions(-) create mode 100644 __pycache__/cfd_animated_icon.cpython-312.pyc create mode 100644 cfd_animated_icon.py diff --git a/__pycache__/cfd_animated_icon.cpython-312.pyc b/__pycache__/cfd_animated_icon.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..918253c16559665355553ddfc61e4429a1e695c1 GIT binary patch literal 5951 zcmcIoU2GFq7M>Z;*c1Q6NlHR~Tu4Ha#*jh-qT8P>l%GNntEBvFfwW@Qc*gbwV~0Bv z@*`X71JXLHwuA?kpxsqXwNiu9)uvCS(n?!tU#R-f3gOzpJ1`{PU766tfww$ZVaIFIMZHL1ujSvtNg8k6l6DYB%3^;<_LNgZgJYFixvTb+GEztKJd^257K zQLHV9Er!@K`&@qelGsyj_vZI5iO+p22BtlR*gV^`DE8;OZj1eMJwS@0|5ELA?QHMD z`CRRi_;RtarO?=GH1^(U+??az^A?+03r(AirppM z(EGJcAh7YiV8Iu8NVYN|6w1K%2lE=L}aI-f9yNgX!UY02{g1;c0Mqa9nuM6-8#x_QJxO zfx8mTZ7G?X0`!tyQxNNL^ri8s*VfR+Q*B$HP8 x#eJPSVd{>lB9$O4si z>m{@uDPe0_Y6{}iwxwh(=)-%fqLkej`1KV`Xd-%#G_39~-!&FSyEOo|q_T_#stGtE z(=DgR&Loxi4qE=Tdl`3i8Br|krJ+I*jjV$X(ujoOI}Z?s-MlY=IGuHRr6`x-n1-QL53jRQ&0J97o zYF4S{_i%b84{+odX5JoyzJ$J#W%bfM4f1savwVb577X*<)8SBi&7rxUwF(L2jkZ6-rK#5drR{Zf_aYZ z|KIH<&qM1+c9ZFPVf#c_Alsm6x*%*U$9JhIovepGVBvjXx?_>3o+c&>zqrYs(O^fd zE8e<9o{1~EG&+{lXii}z2~#~)0-%cAR#2sV1q5QCe%&nl=lvfJTpP$AFLduTx_90Q zy=XMQXw?5I=l<3ooZ+tqF9-9iQP-J|&h7vE(2b#+dyKwaM$fJ3FIeHnqCA;2%d9qv%26&~!zTNwOIZ=mxip&PMh)MC}9jTt>7G3;2ovDWd6CCUkfWu&zox3Dc7v)01jS zA$`zV{v!@XU;%B2;AFrdVK3akN>YIf^EK>4=%cSP1kv5}Mu|exR1&3Eu*vCXP1l~I z`{-R9uC*{ou-;y{i5E<668;nY@a#9mi5DbT2EIT|mTD9IrM^-PhR^^e8=U}`QsnIef{l){_D~Vm)l$P*IgQ#9-7^o=kj~! zxP`9IwtTwfi|EpkV}&Cpj3Xx&rQa=%q;o@e{i7>$gL@s_hJQnDU?x%Q=>E9%dh1-| z`i6ydpS6A3cJutF8)p)^0Y~#dKCvL)?7h|X^_DNUES`=msb>pn+ECMr;}iE8W*;}e z<%Ygzn7|?Km)81-YQ102)OI}vH#>hmZens*QEXg1Y@-RARnjEaFwvQIp`6*6TTT@MjqTSQ3$woD@8;*pc8hIUSh-p)vO*{Kv z-WiLLz1W45hF#ng!Kx<^4W-$z7R>8d=#>paDbu| z8ARzJByS?YFF)icl4D4YBl#_ow~(AbauUfYAR4A<4(QOQg*>g$J=MJy^58e4(kGh( zs3y1?dY%?+3jUDc56vFUH{_4bH7uO^B63UqTK!U8Jo(O7=|z9&uK)B(oB?$C88~Ar zJ(-_aXukQBUj~PHYS`FZnP0g4*u-FWe6}xDsZmSA>1|sVx;b zr@*H+uOyvI%VSBb`z7i8SR`q2=;tTW1N%vOk#r%Umls|H^vi=8us?Z4m1I(qKz%!~ z=%7a|&!xWQP`9l-rj{IzAzE}}paJb}?R0=e&ES@s)d a-8YQydw!5*$JyD24_mIaJYr~rD)=wJQ`;Z_ literal 0 HcmV?d00001 diff --git a/__pycache__/cfd_ui_setup.cpython-312.pyc b/__pycache__/cfd_ui_setup.cpython-312.pyc index f6e430ad147c5f906fe03789dcbe176af4c0bc2d..efbbbdebbf95b942690581cc694881ce13341093 100644 GIT binary patch delta 5375 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 delta 4930 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

53Hhb3Z0KP8m>hO_bY;>Q@uWMD}|T_9Sw% zMfoI(yyr>$2>duv>4*@ykKkqaGO@Q%Tr*lW*6LWTsS@`E!u}|}%GHwcJZRaQ2S4pC z5HA3H1N9lZ)u|UQxY)Zmy#>3K56uQcvaqkfnEd6yAC2YbOwJCA{rx4*4!b=-22MR` zNKo8f`z{JRV}K8L=fT%QWyyaH^_h&()gr+?Fxn5FKb#Mv!>*Aa!@sPcGw3`yA5UbDOuGv(0W~ zi%w8)c5Ec-(+g-HmlSzW;&Bu~XCSL;_8vdFkIqj~s5#LjSYYWA3Occ2>b0W>TZ6N1 z{Pv)K66q7)Bb1+@X5|RNsNt{fTb}7S&~Z>blKkw^r`fi0lx^`kc@TfOM9@tjKAfB) zO6=wwQST7EOF%2-5dxZ;hX_cK{*o>eMG%9_kIk=JWLa`G!aSC3?AjVO*HWZ&(l}OE z4&q&mnzS@^r;Ifu=i_kC{<0qNZYDm`2vs-D@r7+ER>_How49T>;lDvyfbfZlF?QaN qe%_FA-jI9Ikbluob=8{73O=n_$5{Rq1L3FJjqMgzbk#s8R`Wl9n$yq# diff --git a/__pycache__/custom_file_dialog.cpython-312.pyc b/__pycache__/custom_file_dialog.cpython-312.pyc index fe6cbd2d7946e59f9f8a78c40231c51aa6c6a442..32a58bc33edc36608e5eefc097b62b1b398252dc 100644 GIT binary patch delta 15050 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 diff --git a/cfd_animated_icon.py b/cfd_animated_icon.py new file mode 100644 index 0000000..6619bb8 --- /dev/null +++ b/cfd_animated_icon.py @@ -0,0 +1,82 @@ +import tkinter as tk +import math + +class AnimatedSearchIcon(tk.Canvas): + def __init__(self, parent, bg_color, style="single", *args, **kwargs): + kwargs.setdefault('width', 22) + kwargs.setdefault('height', 22) + super().__init__(parent, *args, **kwargs) + self.configure(bg=bg_color, highlightthickness=0) + self.width = self.winfo_reqwidth() + self.height = self.winfo_reqheight() + + self.angle1 = 0 + self.angle2 = 0 + self.color_angle = 0 + self.base_color = (81, 149, 255) # #5195ff + self.is_animating = False + self.style = style + + self.draw_initial_state() + + def start_animation(self): + if self.is_animating: + return + self.is_animating = True + self.update_animation() + + def stop_animation(self): + self.is_animating = False + self.draw_initial_state() + + def update_animation(self): + if not self.is_animating: + return + + if self.style == "single": + self.angle1 = (self.angle1 - 6) % 360 + elif self.style == "double": + self.angle1 = (self.angle1 - 6) % 360 + self.angle2 = (self.angle2 + 6) % 360 + + self.color_angle = (self.color_angle + 0.15) % (2 * math.pi) + self.draw_animated_arc() + self.after(25, self.update_animation) + + def get_pulsating_color(self): + factor = 0.5 * (1 + math.sin(self.color_angle)) + r = int(self.base_color[0] + (255 - self.base_color[0]) * factor * 0.6) + g = int(self.base_color[1] + (255 - self.base_color[1]) * factor * 0.6) + b = int(self.base_color[2] + (255 - self.base_color[2]) * factor * 0.6) + return f"#{r:02x}{g:02x}{b:02x}" + + def draw_animated_arc(self): + self.delete("all") + color = self.get_pulsating_color() + if self.style == "single": + x0, y0 = 3, 3 + x1, y1 = self.width - 3, self.height - 3 + self.create_arc(x0, y0, x1, y1, start=self.angle1, extent=300, style=tk.ARC, width=4, outline=color) + elif self.style == "double": + # Outer arc + x0_outer, y0_outer = 3, 3 + x1_outer, y1_outer = self.width - 3, self.height - 3 + self.create_arc(x0_outer, y0_outer, x1_outer, y1_outer, start=self.angle1, extent=150, style=tk.ARC, width=2, outline=color) + # Inner arc + x0_inner, y0_inner = 7, 7 + x1_inner, y1_inner = self.width - 7, self.height - 7 + self.create_arc(x0_inner, y0_inner, x1_inner, y1_inner, start=self.angle2, extent=150, style=tk.ARC, width=2, outline=color) + + def draw_initial_state(self): + self.delete("all") + if self.style == "single": + x0, y0 = 3, 3 + x1, y1 = self.width - 3, self.height - 3 + self.create_oval(x0, y0, x1, y1, outline="#5195ff", width=4, fill=self.cget("bg")) + elif self.style == "double": + x0_outer, y0_outer = 3, 3 + x1_outer, y1_outer = self.width - 3, self.height - 3 + self.create_oval(x0_outer, y0_outer, x1_outer, y1_outer, outline="#5195ff", width=2, fill=self.cget("bg")) + x0_inner, y0_inner = 7, 7 + x1_inner, y1_inner = self.width - 7, self.height - 7 + self.create_oval(x0_inner, y0_inner, x1_inner, y1_inner, outline="#5195ff", width=2, fill=self.cget("bg")) diff --git a/cfd_ui_setup.py b/cfd_ui_setup.py index 36181a1..c3e5d54 100644 --- a/cfd_ui_setup.py +++ b/cfd_ui_setup.py @@ -3,6 +3,7 @@ import shutil import tkinter as tk from tkinter import ttk from shared_libs.common_tools import Tooltip +from cfd_animated_icon import AnimatedSearchIcon def get_xdg_user_dir(dir_key, fallback_name): @@ -357,6 +358,15 @@ class WidgetManager: self.settings_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon('settings-2_small'), command=self.dialog.open_settings_dialog, style="Bottom.TButton.Borderless.Round") + self.search_animation = AnimatedSearchIcon(self.status_container, + bg_color=self.style_manager.bottom_color, + style="double", + width=23, height=23) + self.search_animation.grid(row=0, column=0, sticky='w', padx=(0, 5), pady=(4,0)) + self.search_animation.bind("", lambda e: self.dialog.activate_search()) + self.search_status_label.grid(row=0, column=1, sticky="w") + + button_box_pos = self.settings.get("button_box_pos", "left") if self.dialog.dialog_mode == "save": @@ -417,7 +427,7 @@ class WidgetManager: self.center_container.grid_columnconfigure(1, weight=1) self.filter_combobox.grid(in_=self.center_container, row=1, column=1, sticky="e", pady=(5, 0)) - self.search_status_label.grid(row=0, column=0, sticky="w", pady=(5, 0), padx=(5, 0)) + #self.search_status_label.grid(row=0, column=0, sticky="w", pady=(5, 0), padx=(5, 0)) diff --git a/custom_file_dialog.py b/custom_file_dialog.py index 4a7f410..126f25c 100644 --- a/custom_file_dialog.py +++ b/custom_file_dialog.py @@ -5,6 +5,7 @@ from tkinter import ttk from datetime import datetime import subprocess import json +import threading from shared_libs.message import MessageDialog from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTools from cfd_app_config import AppConfig, CfdConfigManager @@ -201,6 +202,8 @@ class CustomFileDialog(tk.Toplevel): self.item_path_map = {} self.responsive_buttons_hidden = None # State for responsive buttons self.search_job = None + self.search_thread = None + self.search_process = None self.icon_manager = IconManager() self.style_manager = StyleManager(self) @@ -235,34 +238,112 @@ class CustomFileDialog(tk.Toplevel): + def activate_search(self, event=None): + """Activates the search entry or cancels an ongoing search.""" + if self.widget_manager.search_animation.is_animating: + # If animating, it means a search is active, so cancel it + if self.search_thread and self.search_thread.is_alive(): + if self.search_process: + try: + os.killpg(os.getpgid(self.search_process.pid), 9) # Send SIGKILL to process group + except (ProcessLookupError, AttributeError): + pass # Process might have already finished or not started + self.widget_manager.search_animation.stop_animation() + self.widget_manager.search_status_label.config(text="Suche abgebrochen.") + self.hide_search_bar() # Reset UI after cancellation + else: + # If not animating, activate search entry + self.widget_manager.filename_entry.focus_set() + self.search_mode = True # Ensure search mode is active + self.widget_manager.filename_entry.bind("", self.execute_search) + self.widget_manager.filename_entry.bind("", self.hide_search_bar) + def show_search_bar(self, event=None): - # Ignore key presses if they are coming from an entry widget or have no character if isinstance(event.widget, (ttk.Entry, tk.Entry)) or not event.char.strip(): return - self.search_mode = True self.widget_manager.filename_entry.focus_set() - # Clear the field before inserting the new character to start a fresh search self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, event.char) self.widget_manager.filename_entry.bind("", self.execute_search) self.widget_manager.filename_entry.bind("", self.hide_search_bar) + # Removed: self.widget_manager.search_animation.start_animation() def hide_search_bar(self, event=None): self.search_mode = False self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.search_status_label.config(text="") - # Unbind search-specific events to restore normal behavior self.widget_manager.filename_entry.unbind("") self.widget_manager.filename_entry.unbind("") - # Re-bind the default save action for the save dialog if self.dialog_mode == "save": self.widget_manager.filename_entry.bind("", lambda e: self.on_save()) self.populate_files() + self.widget_manager.search_animation.stop_animation() - def toggle_search_mode(self, event=None): - # This method might not be needed anymore if search is always active in the entry - pass + def execute_search(self, event=None): + if self.search_thread and self.search_thread.is_alive(): + return + search_term = self.widget_manager.filename_entry.get().strip() + if not search_term: + self.hide_search_bar() + return + self.widget_manager.search_status_label.config(text=f"Suche nach '{search_term}'...") + self.widget_manager.search_animation.start_animation() + self.update_idletasks() + self.search_thread = threading.Thread(target=self._perform_search_in_thread, args=(search_term,)) + self.search_thread.start() + + def _perform_search_in_thread(self, search_term): + self.search_results.clear() + search_dirs = [self.current_dir] + home_dir = os.path.expanduser("~") + if os.path.abspath(self.current_dir) == os.path.abspath(home_dir): + xdg_dirs = [get_xdg_user_dir(d, f) for d, f in [("XDG_DOWNLOAD_DIR", "Downloads"), ("XDG_DOCUMENTS_DIR", "Documents"), ("XDG_PICTURES_DIR", "Pictures"), ("XDG_MUSIC_DIR", "Music"), ("XDG_VIDEO_DIR", "Videos")]] + search_dirs.extend([d for d in xdg_dirs if os.path.exists(d) and os.path.abspath(d) != home_dir and d not in search_dirs]) + + try: + all_files = [] + for search_dir in search_dirs: + if not (self.search_thread and self.search_thread.is_alive()): break + if not os.path.exists(search_dir): continue + original_cwd = os.getcwd() + try: + os.chdir(search_dir) + cmd = ['find', '-L', '.', '-iname', f'*{search_term}*'] + if not self.settings.get("recursive_search", True): + cmd.insert(3, '-maxdepth') + cmd.insert(4, '1') + self.search_process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, preexec_fn=os.setsid) + stdout, _ = self.search_process.communicate() + if self.search_process.returncode == 0: + all_files.extend([os.path.join(search_dir, f[2:]) for f in stdout.strip().split('\n') if f and f.startswith('./') and os.path.exists(os.path.join(search_dir, f[2:]))]) + finally: + os.chdir(original_cwd) + + if not (self.search_thread and self.search_thread.is_alive()): raise subprocess.SubprocessError("Search cancelled by user") + + seen = set() + unique_files = [x for x in all_files if not (x in seen or seen.add(x))] + search_hidden = self.settings.get("search_hidden_files", False) + self.search_results = [p for p in unique_files if (search_hidden or not any(part.startswith('.') for part in p.split(os.sep))) and (self._matches_filetype(os.path.basename(p)) or os.path.isdir(p))] + + def update_ui(): + if self.search_results: + self.show_search_results_treeview() + folder_count = sum(1 for p in self.search_results if os.path.isdir(p)) + file_count = len(self.search_results) - folder_count + self.widget_manager.search_status_label.config(text=f"{folder_count} Ordner und {file_count} Dateien gefunden.") + else: + self.widget_manager.search_status_label.config(text=f"Keine Ergebnisse für '{search_term}'.") + self.after(0, update_ui) + except Exception as e: + if isinstance(e, subprocess.SubprocessError): + self.after(0, lambda: self.widget_manager.search_status_label.config(text="Suche abgebrochen.")) + else: + self.after(0, lambda: MessageDialog(message_type="error", text=f"Fehler bei der Suche: {e}", title="Suchfehler", master=self).show()) + finally: + self.after(0, self.widget_manager.search_animation.stop_animation) + self.search_process = None def handle_path_entry_return(self, event): """Handles the Enter key in the path entry to navigate. @@ -313,7 +394,7 @@ class CustomFileDialog(tk.Toplevel): # If search was active, reset it to avoid inconsistent state if self.search_mode: - self.toggle_search_mode() # This will correctly reset the UI + self.hide_search_bar() # This will correctly reset the UI self.navigate_to(self.current_dir) @@ -507,134 +588,6 @@ class CustomFileDialog(tk.Toplevel): - def execute_search(self, event=None): - search_term = self.widget_manager.filename_entry.get().strip() - - if not search_term: - self.hide_search_bar() - return - - self.widget_manager.search_status_label.config(text=f"Suche nach '{search_term}'...") - self.update_idletasks() - - # Clear previous search results - self.search_results.clear() - - # Determine search directories - search_dirs = [self.current_dir] - - # If searching from home directory, also include XDG directories - home_dir = os.path.expanduser("~") - if os.path.abspath(self.current_dir) == os.path.abspath(home_dir): - xdg_dirs = [ - get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads"), - get_xdg_user_dir("XDG_DOCUMENTS_DIR", "Documents"), - get_xdg_user_dir("XDG_PICTURES_DIR", "Pictures"), - get_xdg_user_dir("XDG_MUSIC_DIR", "Music"), - get_xdg_user_dir("XDG_VIDEO_DIR", "Videos") - ] - # Add XDG directories that exist and are not already in home - for xdg_dir in xdg_dirs: - if (os.path.exists(xdg_dir) and - os.path.abspath(xdg_dir) != os.path.abspath(home_dir) and - xdg_dir not in search_dirs): - search_dirs.append(xdg_dir) - - try: - all_files = [] - - # Search in each directory - for search_dir in search_dirs: - if not os.path.exists(search_dir): - continue - - # Change to directory and use relative paths to avoid path issues - original_cwd = os.getcwd() - try: - os.chdir(search_dir) - - # Build find command based on recursive setting (use . for current directory) - if self.settings.get("recursive_search", True): - # Find both files and directories, following symlinks - find_cmd = ['find', '-L', '.', '-iname', f'*{search_term}*'] - else: - # Find both files and directories, but only in the current level, following symlinks - find_cmd = ['find', '-L', '.', '-maxdepth', '1', - '-iname', f'*{search_term}*'] - - result = subprocess.run( - find_cmd, capture_output=True, text=True, timeout=30) - - if result.returncode == 0: - files = result.stdout.strip().split('\n') - # Convert relative paths back to absolute paths - directory_files = [] - for f in files: - if f and f.startswith('./'): - abs_path = os.path.join( - search_dir, f[2:]) # Remove './' prefix - # Check if the path exists, as it might be a broken symlink or deleted - if os.path.exists(abs_path): - directory_files.append(abs_path) - all_files.extend(directory_files) - - finally: - os.chdir(original_cwd) - - # Remove duplicates while preserving order - seen = set() - unique_files = [] - for file_path in all_files: - if file_path not in seen: - seen.add(file_path) - unique_files.append(file_path) - - # Filter based on currently selected filter pattern and hidden file setting - self.search_results = [] - search_hidden = self.settings.get("search_hidden_files", False) - - for file_path in unique_files: - # Check if path contains a hidden component (e.g., /.config/ or /some/path/to/.hidden_file) - if not search_hidden: - if any(part.startswith('.') for part in file_path.split(os.sep)): - continue # Skip hidden files/files in hidden directories - - # Check if the path exists (it might have been deleted during the search) - if os.path.exists(file_path): - filename = os.path.basename(file_path) - if self._matches_filetype(filename) or os.path.isdir(file_path): - self.search_results.append(file_path) - - # Show search results in TreeView - if self.search_results: - self.show_search_results_treeview() - folder_count = sum(1 for p in self.search_results if os.path.isdir(p)) - file_count = len(self.search_results) - folder_count - self.widget_manager.search_status_label.config(text=f"{folder_count} Ordner und {file_count} Dateien gefunden.") - else: - self.widget_manager.search_status_label.config(text=f"Keine Ergebnisse für '{search_term}'.") - MessageDialog( - message_type="info", - text=f"Keine Dateien mit '{search_term}' gefunden.", - title="Suche", - master=self - ).show() - - except subprocess.TimeoutExpired: - MessageDialog( - message_type="error", - text="Suche dauert zu lange und wurde abgebrochen.", - title="Suche", - master=self - ).show() - except Exception as e: - MessageDialog( - message_type="error", - text=f"Fehler bei der Suche: {e}", - title="Suchfehler", - master=self - ).show() - def show_search_results_treeview(self): """Show search results in TreeView format""" # Clear current file list and replace with search results @@ -969,13 +922,13 @@ class CustomFileDialog(tk.Toplevel): Tooltip(item_frame, name) for widget in [item_frame, icon_label, name_label]: - widget.bind("", lambda e, + widget.bind("", lambda e, p=path: self.on_item_double_click(p)) - widget.bind("", lambda e, p=path, + widget.bind("", lambda e, p=path, f=item_frame: self.on_item_select(p, f)) - widget.bind("", lambda e, + widget.bind("", lambda e, p=path: self._show_context_menu(e, p)) - widget.bind("", lambda e, p=path, + widget.bind("", lambda e, p=path, f=item_frame: self.on_rename_request(e, p, f)) if name == item_to_select: @@ -1387,7 +1340,7 @@ class CustomFileDialog(tk.Toplevel): filename = os.path.basename(file_path) if self.search_mode: - self.toggle_search_mode() + self.hide_search_bar() self.navigate_to(directory) self.after(100, lambda: self._select_file_in_view(filename)) @@ -1578,4 +1531,4 @@ class CustomFileDialog(tk.Toplevel): except Exception as e: print(f"Error getting mounted devices: {e}") - return devices + return devices \ No newline at end of file diff --git a/mainwindow.py b/mainwindow.py index c7d103a..3368df5 100755 --- a/mainwindow.py +++ b/mainwindow.py @@ -55,7 +55,7 @@ if __name__ == "__main__": style = ttk.Style(root) root.tk.call('source', f"{theme_path}/water.tcl") try: - root.tk.call('set_theme', 'dark') + root.tk.call('set_theme', 'light') except tk.TclError: pass root.mainloop()