diff --git a/__pycache__/cfd_animated_icon.cpython-312.pyc b/__pycache__/cfd_animated_icon.cpython-312.pyc index 918253c..b0a909a 100644 Binary files a/__pycache__/cfd_animated_icon.cpython-312.pyc and b/__pycache__/cfd_animated_icon.cpython-312.pyc differ diff --git a/__pycache__/cfd_app_config.cpython-312.pyc b/__pycache__/cfd_app_config.cpython-312.pyc index f32f12b..b3d0a86 100644 Binary files a/__pycache__/cfd_app_config.cpython-312.pyc and b/__pycache__/cfd_app_config.cpython-312.pyc differ diff --git a/__pycache__/cfd_ui_setup.cpython-312.pyc b/__pycache__/cfd_ui_setup.cpython-312.pyc index efbbbde..ca027bc 100644 Binary files a/__pycache__/cfd_ui_setup.cpython-312.pyc and b/__pycache__/cfd_ui_setup.cpython-312.pyc differ diff --git a/__pycache__/custom_file_dialog.cpython-312.pyc b/__pycache__/custom_file_dialog.cpython-312.pyc index 32a58bc..e8772e7 100644 Binary files a/__pycache__/custom_file_dialog.cpython-312.pyc and b/__pycache__/custom_file_dialog.cpython-312.pyc differ diff --git a/cfd_animated_icon.py b/cfd_animated_icon.py index 6619bb8..fdf81dc 100644 --- a/cfd_animated_icon.py +++ b/cfd_animated_icon.py @@ -1,82 +1,170 @@ import tkinter as tk -import math +from math import sin, cos, pi -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() +try: + from PIL import Image, ImageDraw, ImageTk + PIL_AVAILABLE = True +except ImportError: + PIL_AVAILABLE = False + +def _hex_to_rgb(hex_color): + hex_color = hex_color.lstrip('#') + return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + +class AnimatedIcon(tk.Canvas): + def __init__(self, master, width=20, height=20, animation_type="counter_arc", color="#2a6fde", highlight_color="#5195ff", use_pillow=False, bg=None): + if bg is None: + try: + bg = master.cget("background") + except tk.TclError: + bg = "#f0f0f0" # Fallback color + super().__init__(master, width=width, height=height, bg=bg, highlightthickness=0) - 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.width = width + self.height = height + self.animation_type = animation_type + self.color = color + self.highlight_color = highlight_color + self.use_pillow = use_pillow and PIL_AVAILABLE + self.running = False + self.angle = 0 - self.draw_initial_state() + self.color_rgb = _hex_to_rgb(self.color) + self.highlight_color_rgb = _hex_to_rgb(self.highlight_color) - def start_animation(self): - if self.is_animating: - return - self.is_animating = True - self.update_animation() + if self.use_pillow: + self.image = Image.new("RGBA", (width * 4, height * 4), (0, 0, 0, 0)) + self.draw = ImageDraw.Draw(self.image) + self.photo_image = None - def stop_animation(self): - self.is_animating = False - self.draw_initial_state() + def _draw_frame(self): + if self.use_pillow: + self._draw_pillow_frame() + else: + self._draw_canvas_frame() - def update_animation(self): - if not self.is_animating: - return + def _draw_canvas_frame(self): + self.delete("all") + if self.animation_type == "line": + self._draw_canvas_line() + elif self.animation_type == "double_arc": + self._draw_canvas_double_arc() + elif self.animation_type == "counter_arc": + self._draw_canvas_counter_arc() + + def _draw_canvas_line(self): + center_x, center_y = self.width / 2, self.height / 2 + for i in range(8): + angle = self.angle + i * (pi / 4) + start_x = center_x + cos(angle) * (self.width * 0.2) + start_y = center_y + sin(angle) * (self.height * 0.2) + end_x = center_x + cos(angle) * (self.width * 0.4) + end_y = center_y + sin(angle) * (self.height * 0.4) + alpha = (cos(self.angle * 2 + i * (pi / 4)) + 1) / 2 + + r = int(alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) + g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) + b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) + color = f"#{r:02x}{g:02x}{b:02x}" + + self.create_line(start_x, start_y, end_x, end_y, fill=color, width=2) + + def _draw_canvas_double_arc(self): + center_x, center_y = self.width / 2, self.height / 2 + radius = min(center_x, center_y) * 0.8 + bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) - 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 + start_angle1 = self.angle * 180 / pi + extent1 = 120 + 60 * sin(self.angle) + self.create_arc(bbox, start=start_angle1, extent=extent1, style=tk.ARC, outline=self.highlight_color, width=2) + + start_angle2 = (self.angle + pi) * 180 / pi + extent2 = 120 + 60 * sin(self.angle + pi / 2) + self.create_arc(bbox, start=start_angle2, extent=extent2, style=tk.ARC, outline=self.color, width=2) - self.color_angle = (self.color_angle + 0.15) % (2 * math.pi) - self.draw_animated_arc() - self.after(25, self.update_animation) + def _draw_canvas_counter_arc(self): + center_x, center_y = self.width / 2, self.height / 2 + + radius_outer = min(center_x, center_y) * 0.8 + bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) + start_angle1 = self.angle * 180 / pi + self.create_arc(bbox_outer, start=start_angle1, extent=150, style=tk.ARC, outline=self.highlight_color, width=2) + + radius_inner = min(center_x, center_y) * 0.6 + bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) + start_angle2 = -self.angle * 180 / pi + 60 + self.create_arc(bbox_inner, start=start_angle2, extent=150, style=tk.ARC, outline=self.color, width=2) - 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_pillow_frame(self): + self.draw.rectangle([0, 0, self.width * 4, self.height * 4], fill=(0, 0, 0, 0)) + if self.animation_type == "line": + self._draw_pillow_line() + elif self.animation_type == "double_arc": + self._draw_pillow_double_arc() + elif self.animation_type == "counter_arc": + self._draw_pillow_counter_arc() - def draw_animated_arc(self): + resized_image = self.image.resize((self.width, self.height), Image.Resampling.LANCZOS) + self.photo_image = ImageTk.PhotoImage(resized_image) 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) + self.create_image(0, 0, anchor="nw", image=self.photo_image) - def draw_initial_state(self): + def _draw_pillow_line(self): + center_x, center_y = self.width * 2, self.height * 2 + for i in range(12): + angle = self.angle + i * (pi / 6) + start_x = center_x + cos(angle) * (self.width * 0.8) + start_y = center_y + sin(angle) * (self.height * 0.8) + end_x = center_x + cos(angle) * (self.width * 1.6) + end_y = center_y + sin(angle) * (self.height * 1.6) + alpha = (cos(self.angle * 2.5 + i * (pi / 6)) + 1) / 2 + + r = int(alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0]) + g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1]) + b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2]) + color = (r, g, b) + + self.draw.line([(start_x, start_y), (end_x, end_y)], fill=color, width=6, joint="curve") + + def _draw_pillow_double_arc(self): + center_x, center_y = self.width * 2, self.height * 2 + radius = min(center_x, center_y) * 0.8 + bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) + + start_angle1 = self.angle * 180 / pi + extent1 = 120 + 60 * sin(self.angle) + self.draw.arc(bbox, start=start_angle1, end=start_angle1 + extent1, fill=self.highlight_color_rgb, width=5) + + start_angle2 = (self.angle + pi) * 180 / pi + extent2 = 120 + 60 * sin(self.angle + pi / 2) + self.draw.arc(bbox, start=start_angle2, end=start_angle2 + extent2, fill=self.color_rgb, width=5) + + def _draw_pillow_counter_arc(self): + center_x, center_y = self.width * 2, self.height * 2 + + radius_outer = min(center_x, center_y) * 0.8 + bbox_outer = (center_x - radius_outer, center_y - radius_outer, center_x + radius_outer, center_y + radius_outer) + start_angle1 = self.angle * 180 / pi + self.draw.arc(bbox_outer, start=start_angle1, end=start_angle1 + 150, fill=self.highlight_color_rgb, width=5) + + radius_inner = min(center_x, center_y) * 0.6 + bbox_inner = (center_x - radius_inner, center_y - radius_inner, center_x + radius_inner, center_y + radius_inner) + start_angle2 = -self.angle * 180 / pi + 60 + self.draw.arc(bbox_inner, start=start_angle2, end=start_angle2 + 150, fill=self.color_rgb, width=5) + + def _animate(self): + if self.running: + self.angle += 0.1 + if self.angle > 2 * pi: + self.angle -= 2 * pi + self._draw_frame() + self.after(30, self._animate) + + def start(self): + if not self.running: + self.running = True + self._animate() + + def stop(self): + self.running = False 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_app_config.py b/cfd_app_config.py index 42a5ec1..3626242 100755 --- a/cfd_app_config.py +++ b/cfd_app_config.py @@ -63,7 +63,8 @@ class CfdConfigManager: "search_hidden_files": False, # True or False "use_trash": False, # True or False "confirm_delete": False, # True or False - "recursive_search": True + "recursive_search": True, + "use_pillow_animation": False } @classmethod diff --git a/cfd_ui_setup.py b/cfd_ui_setup.py index c3e5d54..6e5cd41 100644 --- a/cfd_ui_setup.py +++ b/cfd_ui_setup.py @@ -3,7 +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 +from cfd_animated_icon import AnimatedIcon def get_xdg_user_dir(dir_key, fallback_name): @@ -358,10 +358,9 @@ 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 = AnimatedIcon(self.status_container, + width=23, height=23, bg=self.style_manager.bottom_color, + animation_type=self.settings.get('animation_type', 'double_arc')) 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") diff --git a/custom_file_dialog.py b/custom_file_dialog.py index 126f25c..fbc498a 100644 --- a/custom_file_dialog.py +++ b/custom_file_dialog.py @@ -10,6 +10,7 @@ from shared_libs.message import MessageDialog from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTools from cfd_app_config import AppConfig, CfdConfigManager from cfd_ui_setup import StyleManager, WidgetManager, get_xdg_user_dir +from cfd_animated_icon import AnimatedIcon, PIL_AVAILABLE try: import send2trash @@ -45,20 +46,15 @@ class SettingsDialog(tk.Toplevel): value=self.settings.get("use_trash", False)) self.confirm_delete = tk.BooleanVar( value=self.settings.get("confirm_delete", False)) + self.use_pillow_animation = tk.BooleanVar( + value=self.settings.get("use_pillow_animation", False)) + self.animation_type = tk.StringVar( + value=self.settings.get("animation_type", "double_arc")) # --- UI Elements --- main_frame = ttk.Frame(self, padding=10) main_frame.pack(fill="both", expand=True) - # Search Icon Position - search_frame = ttk.LabelFrame( - main_frame, text="Position der Such-Lupe", padding=10) - search_frame.pack(fill="x", pady=5) - ttk.Radiobutton(search_frame, text="Links", variable=self.search_icon_pos, - value="left").pack(side="left", padx=5) - ttk.Radiobutton(search_frame, text="Rechts", variable=self.search_icon_pos, - value="right").pack(side="left", padx=5) - # Button Box Position button_box_frame = ttk.LabelFrame( main_frame, text="Position der Dialog-Buttons", padding=10) @@ -113,6 +109,29 @@ class SettingsDialog(tk.Toplevel): variable=self.confirm_delete) self.confirm_delete_checkbutton.pack(anchor="w") + # Pillow Animation + pillow_frame = ttk.LabelFrame( + main_frame, text="Animationseinstellungen", padding=10) + pillow_frame.pack(fill="x", pady=5) + self.use_pillow_animation_checkbutton = ttk.Checkbutton(pillow_frame, text="Hochauflösende Animation verwenden (Pillow)", + variable=self.use_pillow_animation) + self.use_pillow_animation_checkbutton.pack(anchor="w") + if not PIL_AVAILABLE: + self.use_pillow_animation_checkbutton.config(state=tk.DISABLED) + ttk.Label(pillow_frame, text="(Pillow-Bibliothek nicht gefunden)", + font=("TkDefaultFont", 9, "italic")).pack(anchor="w", padx=(20, 0)) + + # Animation Type + anim_type_frame = ttk.LabelFrame( + main_frame, text="Animationstyp", padding=10) + anim_type_frame.pack(fill="x", pady=5) + ttk.Radiobutton(anim_type_frame, text="Gegenläufige Bogen", variable=self.animation_type, + value="counter_arc").pack(side="left", padx=5) + ttk.Radiobutton(anim_type_frame, text="Doppelbogen", variable=self.animation_type, + value="double_arc").pack(side="left", padx=5) + ttk.Radiobutton(anim_type_frame, text="Linie", variable=self.animation_type, + value="line").pack(side="left", padx=5) + # Disable deletion options in "open" mode if not self.dialog_mode == "save": self.use_trash_checkbutton.config(state=tk.DISABLED) @@ -134,14 +153,15 @@ class SettingsDialog(tk.Toplevel): def save_settings(self): new_settings = { - "search_icon_pos": self.search_icon_pos.get(), "button_box_pos": self.button_box_pos.get(), "window_size_preset": self.window_size_preset.get(), "default_view_mode": self.default_view_mode.get(), "search_hidden_files": self.search_hidden_files.get(), "recursive_search": self.recursive_search.get(), "use_trash": self.use_trash.get(), - "confirm_delete": self.confirm_delete.get() + "confirm_delete": self.confirm_delete.get(), + "use_pillow_animation": self.use_pillow_animation.get(), + "animation_type": self.animation_type.get() } CfdConfigManager.save(new_settings) self.master.reload_config_and_rebuild_ui() @@ -149,7 +169,6 @@ class SettingsDialog(tk.Toplevel): def reset_to_defaults(self): defaults = CfdConfigManager._default_settings - self.search_icon_pos.set(defaults["search_icon_pos"]) self.button_box_pos.set(defaults["button_box_pos"]) self.window_size_preset.set(defaults["window_size_preset"]) self.default_view_mode.set(defaults["default_view_mode"]) @@ -157,6 +176,8 @@ class SettingsDialog(tk.Toplevel): self.recursive_search.set(defaults["recursive_search"]) self.use_trash.set(defaults["use_trash"]) self.confirm_delete.set(defaults["confirm_delete"]) + self.use_pillow_animation.set(defaults.get("use_pillow_animation", False)) + self.animation_type.set(defaults.get("animation_type", "counter_arc")) class CustomFileDialog(tk.Toplevel): @@ -209,6 +230,8 @@ class CustomFileDialog(tk.Toplevel): self.style_manager = StyleManager(self) self.widget_manager = WidgetManager(self, self.settings) + self.update_animation_settings() # Add this line + self._update_view_mode_buttons() # Defer initial navigation until the window geometry is calculated @@ -240,7 +263,7 @@ 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 self.widget_manager.search_animation.running: # 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: @@ -248,7 +271,7 @@ class CustomFileDialog(tk.Toplevel): 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_animation.stop() self.widget_manager.search_status_label.config(text="Suche abgebrochen.") self.hide_search_bar() # Reset UI after cancellation else: @@ -267,7 +290,7 @@ class CustomFileDialog(tk.Toplevel): 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() + # Removed: self.widget_manager.search_animation.start() def hide_search_bar(self, event=None): self.search_mode = False @@ -278,7 +301,7 @@ class CustomFileDialog(tk.Toplevel): 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() + self.widget_manager.search_animation.stop() def execute_search(self, event=None): if self.search_thread and self.search_thread.is_alive(): @@ -288,7 +311,7 @@ class CustomFileDialog(tk.Toplevel): 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.widget_manager.search_animation.start() self.update_idletasks() self.search_thread = threading.Thread(target=self._perform_search_in_thread, args=(search_term,)) self.search_thread.start() @@ -342,7 +365,7 @@ class CustomFileDialog(tk.Toplevel): 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.after(0, self.widget_manager.search_animation.stop) self.search_process = None def handle_path_entry_return(self, event): @@ -396,11 +419,36 @@ class CustomFileDialog(tk.Toplevel): if self.search_mode: self.hide_search_bar() # This will correctly reset the UI + self.update_animation_settings() # Add this line self.navigate_to(self.current_dir) def open_settings_dialog(self): SettingsDialog(self, dialog_mode=self.dialog_mode) + def update_animation_settings(self): + use_pillow = self.settings.get('use_pillow_animation', False) + anim_type = self.settings.get('animation_type', 'double') + is_running = self.widget_manager.search_animation.running + if is_running: + self.widget_manager.search_animation.stop() + + self.widget_manager.search_animation.destroy() + self.widget_manager.search_animation = AnimatedIcon( + self.widget_manager.status_container, + width=23, + height=23, + use_pillow=use_pillow, + animation_type=anim_type, + color="#2a6fde", + highlight_color="#5195ff", + bg=self.style_manager.bottom_color + ) + self.widget_manager.search_animation.grid(row=0, column=0, sticky='w', padx=(0, 5), pady=(4,0)) + self.widget_manager.search_animation.bind("", lambda e: self.activate_search()) + + if is_running: + self.widget_manager.search_animation.start() + def get_file_icon(self, filename, size='large'): ext = os.path.splitext(filename)[1].lower()