diff --git a/animated_icon.py b/animated_icon.py index 8ba795b..1f506ea 100644 --- a/animated_icon.py +++ b/animated_icon.py @@ -7,6 +7,418 @@ drawing and Pillow (PIL) for anti-aliased graphics if available. """ import tkinter as tk from math import sin, cos, pi +from typing import Tuple, Optional + + +try: + from PIL import Image, ImageDraw, ImageTk + PIL_AVAILABLE = True +except ImportError: + PIL_AVAILABLE = False + +def _hex_to_rgb(hex_color: str) -> Tuple[int, int, int]: + """Converts a hex color string to an RGB tuple.""" + hex_color = hex_color.lstrip('#') + return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + +class AnimatedIcon(tk.Canvas): + """A custom Tkinter Canvas widget for displaying animations.""" + def __init__(self, master: tk.Misc, width: int = 20, height: int = 20, animation_type: str = "counter_arc", color: str = "#2a6fde", highlight_color: str = "#5195ff", use_pillow: bool = False, bg: Optional[str] = None) -> None: + """ + Initializes the AnimatedIcon widget. + + Args: + master: The parent widget. + width (int): The width of the icon. + height (int): The height of the icon. + animation_type (str): The type of animation to display. + Options: "counter_arc", "double_arc", "line", "blink". + color (str): The primary color of the icon. + highlight_color (str): The highlight color of the icon. + use_pillow (bool): Whether to use Pillow for drawing if available. + bg (str): The background color of the canvas. + """ + 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.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.pulse_animation = False + + self.color_rgb = _hex_to_rgb(self.color) + self.highlight_color_rgb = _hex_to_rgb(self.highlight_color) + + 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 _draw_frame(self) -> None: + """Draws a single frame of the animation.""" + if self.use_pillow: + self._draw_pillow_frame() + else: + self._draw_canvas_frame() + + def _draw_canvas_frame(self) -> None: + """Draws a frame using native Tkinter canvas methods.""" + self.delete("all") + if self.pulse_animation: + self._draw_canvas_pulse() + elif 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() + elif self.animation_type == "blink": + self._draw_canvas_blink() + + def _draw_canvas_pulse(self) -> None: + """Draws the pulse animation using canvas methods.""" + center_x, center_y = self.width / 2, self.height / 2 + alpha = (sin(self.angle * 5) + 1) / 2 # Faster pulse + 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]) + pulse_color = f"#{r:02x}{g:02x}{b:02x}" + + if self.animation_type == "line": + for i in range(8): + 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) + self.create_line(start_x, start_y, end_x, end_y, fill=pulse_color, width=2) + elif self.animation_type == "double_arc": + radius = min(center_x, center_y) * 0.8 + bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) + self.create_arc(bbox, start=0, extent=359.9, style=tk.ARC, outline=pulse_color, width=2) + elif self.animation_type == "counter_arc": + 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) + self.create_arc(bbox_outer, start=0, extent=359.9, style=tk.ARC, outline=pulse_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) + self.create_arc(bbox_inner, start=0, extent=359.9, style=tk.ARC, outline=self.color, width=2) + + + def _draw_canvas_line(self) -> None: + """Draws the line animation using canvas methods.""" + 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) -> None: + """Draws the double arc animation using canvas methods.""" + 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.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) + + def _draw_canvas_counter_arc(self) -> None: + """Draws the counter arc animation using canvas methods.""" + 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 _draw_canvas_blink(self) -> None: + """Draws the blink animation using canvas methods.""" + center_x, center_y = self.width / 2, self.height / 2 + radius = min(center_x, center_y) * 0.8 + alpha = (sin(self.angle * 2) + 1) / 2 # Slower blinking speed + 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]) + blink_color = f"#{r:02x}{g:02x}{b:02x}" + self.create_arc(center_x - radius, center_y - radius, center_x + radius, center_y + radius, start=0, extent=359.9, style=tk.ARC, outline=blink_color, width=4) + + def _draw_pillow_frame(self) -> None: + """Draws a frame using Pillow for anti-aliased graphics.""" + self.draw.rectangle([0, 0, self.width * 4, self.height * 4], fill=(0, 0, 0, 0)) + if self.pulse_animation: + self._draw_pillow_pulse() + elif 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() + elif self.animation_type == "blink": + self._draw_pillow_blink() + + resized_image = self.image.resize((self.width, self.height), Image.Resampling.LANCZOS) + self.photo_image = ImageTk.PhotoImage(resized_image) + self.delete("all") + self.create_image(0, 0, anchor="nw", image=self.photo_image) + + def _draw_pillow_pulse(self) -> None: + """Draws the pulse animation using Pillow.""" + center_x, center_y = self.width * 2, self.height * 2 + alpha = (sin(self.angle * 5) + 1) / 2 # Faster pulse + 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]) + pulse_color = (r, g, b) + + if self.animation_type == "line": + for i in range(12): + 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) + self.draw.line([(start_x, start_y), (end_x, end_y)], fill=pulse_color, width=6, joint="curve") + elif self.animation_type == "double_arc": + radius = min(center_x, center_y) * 0.8 + bbox = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) + self.draw.arc(bbox, start=0, end=360, fill=pulse_color, width=5) + elif self.animation_type == "counter_arc": + 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) + self.draw.arc(bbox_outer, start=0, end=360, fill=pulse_color, width=7) + 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) + self.draw.arc(bbox_inner, start=0, end=360, fill=self.color_rgb, width=7) + + def _draw_pillow_line(self) -> None: + """Draws the line animation using Pillow.""" + 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) -> None: + """Draws the double arc animation using Pillow.""" + 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) -> None: + """Draws the counter arc animation using Pillow.""" + 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=7) + + 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=7) + + def _draw_pillow_blink(self) -> None: + """Draws the blink animation using Pillow.""" + center_x, center_y = self.width * 2, self.height * 2 + radius = min(center_x, center_y) * 0.8 + alpha = (sin(self.angle * 2) + 1) / 2 # Slower blinking speed + 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]) + blink_color = (r, g, b) + self.draw.arc((center_x - radius, center_y - radius, center_x + radius, center_y + radius), start=0, end=360, fill=blink_color, width=10) + + def _draw_stopped_frame(self) -> None: + """Draws the icon in its stopped (static) state.""" + self.delete("all") + if self.use_pillow: + self._draw_pillow_stopped_frame() + else: + self._draw_canvas_stopped_frame() + + def _draw_canvas_stopped_frame(self) -> None: + """Draws the stopped state using canvas methods.""" + if self.animation_type == "line": + self._draw_canvas_line_stopped() + elif self.animation_type == "double_arc": + self._draw_canvas_double_arc_stopped() + elif self.animation_type == "counter_arc": + self._draw_canvas_counter_arc_stopped() + elif self.animation_type == "blink": + self._draw_canvas_blink_stopped() + + def _draw_canvas_line_stopped(self) -> None: + """Draws the stopped state for the line animation.""" + center_x, center_y = self.width / 2, self.height / 2 + for i in range(8): + 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) + self.create_line(start_x, start_y, end_x, end_y, fill=self.highlight_color, width=2) + + def _draw_canvas_double_arc_stopped(self) -> None: + """Draws the stopped state for the double arc animation.""" + 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) + self.create_arc(bbox, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=2) + + def _draw_canvas_counter_arc_stopped(self) -> None: + """Draws the stopped state for the counter arc animation.""" + 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) + self.create_arc(bbox_outer, start=0, extent=359.9, 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) + self.create_arc(bbox_inner, start=0, extent=359.9, style=tk.ARC, outline=self.color, width=2) + + def _draw_canvas_blink_stopped(self) -> None: + """Draws the stopped state for the blink animation.""" + center_x, center_y = self.width / 2, self.height / 2 + radius = min(center_x, center_y) * 0.8 + self.create_arc(center_x - radius, center_y - radius, center_x + radius, center_y + radius, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=4) + + def _draw_pillow_stopped_frame(self) -> None: + """Draws the stopped state using Pillow.""" + 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_stopped() + elif self.animation_type == "double_arc": + self._draw_pillow_double_arc_stopped() + elif self.animation_type == "counter_arc": + self._draw_pillow_counter_arc_stopped() + elif self.animation_type == "blink": + self._draw_pillow_blink_stopped() + + resized_image = self.image.resize((self.width, self.height), Image.Resampling.LANCZOS) + self.photo_image = ImageTk.PhotoImage(resized_image) + self.create_image(0, 0, anchor="nw", image=self.photo_image) + + def _draw_pillow_line_stopped(self) -> None: + """Draws the stopped state for the line animation using Pillow.""" + center_x, center_y = self.width * 2, self.height * 2 + for i in range(12): + 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) + self.draw.line([(start_x, start_y), (end_x, end_y)], fill=self.highlight_color_rgb, width=6, joint="curve") + + def _draw_pillow_double_arc_stopped(self) -> None: + """Draws the stopped state for the double arc animation using Pillow.""" + 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) + self.draw.arc(bbox, start=0, end=360, fill=self.highlight_color_rgb, width=5) + + def _draw_pillow_counter_arc_stopped(self) -> None: + """Draws the stopped state for the counter arc animation using Pillow.""" + 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) + self.draw.arc(bbox_outer, start=0, end=360, fill=self.highlight_color_rgb, width=7) + 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) + self.draw.arc(bbox_inner, start=0, end=360, fill=self.color_rgb, width=7) + + def _draw_pillow_blink_stopped(self) -> None: + """Draws the stopped state for the blink animation using Pillow.""" + center_x, center_y = self.width * 2, self.height * 2 + radius = min(center_x, center_y) * 0.8 + self.draw.arc((center_x - radius, center_y - radius, center_x + radius, center_y + radius), start=0, end=360, fill=self.highlight_color_rgb, width=10) + + def _animate(self) -> None: + """The main animation loop.""" + 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, pulse: bool = False) -> None: + """ + Starts the animation. + + Args: + pulse (bool): If True, plays a pulsing animation instead of the main one. + """ + if not self.running: + self.pulse_animation = pulse + self.running = True + self._animate() + + def stop(self) -> None: + """Stops the animation and shows the static 'stopped' frame.""" + self.running = False + self.pulse_animation = False + self._draw_stopped_frame() + + def hide(self) -> None: + """Stops the animation and clears the canvas.""" + self.running = False + self.pulse_animation = False + self.delete("all") + + def show_full_circle(self) -> None: + """Shows the static 'stopped' frame without starting the animation.""" + if not self.running: + self._draw_stopped_frame() + +import tkinter as tk +from math import sin, cos, pi try: diff --git a/cfd_app_config.py b/cfd_app_config.py index e5c19d0..0b92638 100755 --- a/cfd_app_config.py +++ b/cfd_app_config.py @@ -20,7 +20,259 @@ class AppConfig: UI_CONFIG (Dict[str, Any]): A dictionary containing UI-related settings. """ # Helper to make icon paths robust, so the script can be run from anywhere - SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) + #!/usr/bin/python3 + +"""App configuration for Custom File Dialog""" +import json +from pathlib import Path +import os +from typing import Dict, Any, Optional, Type +from shared_libs.common_tools import Translate + + +class AppConfig: + """ + Holds static configuration values for the application. + + Attributes: + SCRIPT_DIR (str): The absolute path to the directory where the script is running. + MAX_ITEMS_TO_DISPLAY (int): The maximum number of items to show in the file list to prevent performance issues. + BASE_DIR (Path): The user's home directory. + CONFIG_DIR (Path): The directory for storing configuration files. + UI_CONFIG (Dict[str, Any]): A dictionary containing UI-related settings. + """ + # Helper to make icon paths robust, so the script can be run from anywhere + SCRIPT_DIR: str = os.path.dirname(os.path.abspath(__file__)) + MAX_ITEMS_TO_DISPLAY: int = 1000 + + # Base paths + BASE_DIR: Path = Path.home() + CONFIG_DIR: Path = BASE_DIR / ".config/cfiledialog" + + # UI configuration + UI_CONFIG: Dict[str, Any] = { + "window_size": (1050, 850), + "window_min_size": (650, 550), + "font_family": "Ubuntu", + "font_size": 11, + "resizable_window": (True, True), + } + + +# here is initializing the class for translation strings +_ = Translate.setup_translations("custom_file_dialog") + + +class CfdConfigManager: + """ + Manages CFD-specific settings using a JSON file for flexibility. + """ + _config: Optional[Dict[str, Any]] = None + _config_file: Path = AppConfig.CONFIG_DIR / "cfd_settings.json" + _default_settings: Dict[str, Any] = { + "search_icon_pos": "left", # 'left' or 'right' + "button_box_pos": "left", # 'left' or 'right' + "window_size_preset": "1050x850", # e.g., "1050x850" + "default_view_mode": "icons", # 'icons' or 'list' + "search_hidden_files": False, # True or False + "use_trash": False, # True or False + "confirm_delete": False, # True or False + "recursive_search": True, + "use_pillow_animation": True + } + + @classmethod + def _ensure_config_file(cls: Type['CfdConfigManager']) -> None: + """Ensures the configuration file exists, creating it with default settings if necessary.""" + if not cls._config_file.exists(): + try: + cls._config_file.parent.mkdir(parents=True, exist_ok=True) + with open(cls._config_file, 'w', encoding='utf-8') as f: + json.dump(cls._default_settings, f, indent=4) + except IOError as e: + print(f"Error creating default settings file: {e}") + + @classmethod + def load(cls: Type['CfdConfigManager']) -> Dict[str, Any]: + """Loads settings from the JSON file. If the file doesn't exist or is invalid, it loads default settings.""" + cls._ensure_config_file() + if cls._config is None: + try: + with open(cls._config_file, 'r', encoding='utf-8') as f: + loaded_config = json.load(f) + # Merge with defaults to ensure all keys are present + cls._config = cls._default_settings.copy() + cls._config.update(loaded_config) + except (IOError, json.JSONDecodeError): + cls._config = cls._default_settings.copy() + return cls._config + + @classmethod + def save(cls: Type['CfdConfigManager'], settings: Dict[str, Any]) -> None: + """Saves the given settings dictionary to the JSON file.""" + try: + with open(cls._config_file, 'w', encoding='utf-8') as f: + json.dump(settings, f, indent=4) + cls._config = settings # Update cached config + except IOError as e: + print(f"Error saving settings: {e}") + + +class LocaleStrings: + """ + Contains all translatable strings for the application, organized by module. + + This class centralizes all user-facing strings to make translation and management easier. + The strings are grouped into nested dictionaries corresponding to the part of the application + where they are used (e.g., CFD for the main dialog, VIEW for view-related strings). + """ + # Strings from custom_file_dialog.py + CFD: Dict[str, str] = { + "title": _("Custom File Dialog"), + "select_file": _("Select a file"), + "open": _("Open"), + "cancel": _("Cancel"), + "file_label": _("File:"), + "no_file_selected": _("No file selected"), + "error_title": _("Error"), + "select_file_error": _("Please select a file."), + "all_files": _("All Files"), + "free_space": _("Free Space"), + "entries": _("entries"), + "directory_not_found": _("Directory not found"), + "unknown": _("Unknown"), + "showing": _("Showing"), + "of": _("of"), + "access_denied": _("Access denied."), + "path_not_found": _("Path not found"), + "directory": _("Directory"), + "not_found": _("not found."), + "access_to": _("Access to"), + "denied": _("denied."), + } + + # Strings from cfd_view_manager.py + VIEW: Dict[str, str] = { + "name": _("Name"), + "date_modified": _("Date Modified"), + "type": _("Type"), + "size": _("Size"), + "view_mode": _("View Mode"), + "icon_view": _("Icon View"), + "list_view": _("List View"), + "filename": _("Filename"), + "path": _("Path"), + } + + # Strings from cfd_ui_setup.py + UI: Dict[str, str] = { + "search": _("Search"), + "go": _("Go"), + "up": _("Up"), + "back": _("Back"), + "forward": _("Forward"), + "home": _("Home"), + "new_folder": _("New Folder"), + "delete": _("Delete"), + "settings": _("Settings"), + "show_hidden_files": _("Show Hidden Files"), + "places": _("Places"), + "devices": _("Devices"), + "bookmarks": _("Bookmarks"), + "new_document": _("New Document"), + "hide_hidden_files": _("Hide Hidden Files"), + "start_search": _("Start Search"), + "cancel_search": _("Cancel Search"), + "delete_move": _("Delete/Move selected item"), + "copy_filename_to_clipboard": _("Copy Filename to Clipboard"), + "copy_path_to_clipboard": _("Copy Path to Clipboard"), + "open_file_location": _("Open File Location"), + "searching_for": _("Searching for"), + "search_cancelled_by_user": _("Search cancelled by user"), + "folders_and": _("folders and"), + "files_found": _("files found."), + "no_results_for": _("No results for"), + "error_during_search": _("Error during search"), + "search_error": _("Search Error"), + } + + # Strings from cfd_settings_dialog.py + SET: Dict[str, str] = { + "title": _("Settings"), + "search_icon_pos_label": _("Search Icon Position"), + "left_radio": _("Left"), + "right_radio": _("Right"), + "button_box_pos_label": _("Button Box Position"), + "window_size_label": _("Window Size"), + "default_view_mode_label": _("Default View Mode"), + "icons_radio": _("Icons"), + "list_radio": _("List"), + "search_hidden_check": _("Search hidden files"), + "use_trash_check": _("Use trash for deletion"), + "confirm_delete_check": _("Confirm file deletion"), + "recursive_search_check": _("Recursive search"), + "use_pillow_check": _("Use Pillow animation"), + "save_button": _("Save"), + "cancel_button": _("Cancel"), + "search_settings": _("Search Settings"), + "deletion_settings": _("Deletion Settings"), + "recommended": _("recommended"), + "send2trash_not_found": _("send2trash library not found"), + "animation_settings": _("Animation Settings"), + "pillow": _("Pillow"), + "pillow_not_found": _("Pillow library not found"), + "animation_type": _("Animation Type"), + "counter_arc": _("Counter Arc"), + "double_arc": _("Double Arc"), + "line": _("Line"), + "blink": _("Blink"), + "deletion_options_info": _("Deletion options are only available in save mode"), + "reset_to_default": _("Reset to Default"), + } + + # Strings from cfd_file_operations.py + FILE: Dict[str, str] = { + "new_folder_title": _("New Folder"), + "enter_folder_name_label": _("Enter folder name:"), + "untitled_folder": _("Untitled Folder"), + "error_title": _("Error"), + "folder_exists_error": _("Folder already exists."), + "create_folder_error": _("Could not create folder."), + "confirm_delete_title": _("Confirm Deletion"), + "confirm_delete_file_message": _("Are you sure you want to permanently delete this file?"), + "confirm_delete_files_message": _("Are you sure you want to permanently delete these files?"), + "delete_button": _("Delete"), + "cancel_button": _("Cancel"), + "file_not_found_error": _("File not found."), + "trash_error": _("Could not move file to trash."), + "delete_error": _("Could not delete file."), + "folder": _("Folder"), + "file": _("File"), + "move_to_trash": _("move to trash"), + "delete_permanently": _("delete permanently"), + "are_you_sure": _("Are you sure you want to"), + "was_successfully_removed": _("was successfully removed."), + "error_removing": _("Error removing"), + "new_document_txt": _("New Document.txt"), + "error_creating": _("Error creating"), + "copied_to_clipboard": _("copied to clipboard."), + "error_renaming": _("Error renaming"), + "not_accessible": _("not accessible"), + } + + # Strings from cfd_navigation_manager.py + NAV: Dict[str, str] = { + "home": _("Home"), + "trash": _("Trash"), + "desktop": _("Desktop"), + "documents": _("Documents"), + "downloads": _("Downloads"), + "music": _("Music"), + "pictures": _("Pictures"), + "videos": _("Videos"), + "computer": _("Computer"), + } + MAX_ITEMS_TO_DISPLAY = 1000 # Base paths diff --git a/cfd_file_operations.py b/cfd_file_operations.py index e8fc20b..a45da8c 100644 --- a/cfd_file_operations.py +++ b/cfd_file_operations.py @@ -2,6 +2,7 @@ import os import shutil import tkinter as tk from tkinter import ttk +from typing import Optional, Any, TYPE_CHECKING try: import send2trash @@ -12,11 +13,14 @@ except ImportError: from shared_libs.message import MessageDialog from cfd_app_config import LocaleStrings, _ +if TYPE_CHECKING: + from custom_file_dialog import CustomFileDialog + class FileOperationsManager: """Manages file operations like delete, create, and rename.""" - def __init__(self, dialog): + def __init__(self, dialog: 'CustomFileDialog') -> None: """ Initializes the FileOperationsManager. @@ -25,7 +29,7 @@ class FileOperationsManager: """ self.dialog = dialog - def delete_selected_item(self, event=None): + def delete_selected_item(self, event: Optional[tk.Event] = None) -> None: """ Deletes the selected item or moves it to the trash. @@ -77,15 +81,15 @@ class FileOperationsManager: message_type="error" ).show() - def create_new_folder(self): + def create_new_folder(self) -> None: """Creates a new folder in the current directory.""" self._create_new_item(is_folder=True) - def create_new_file(self): + def create_new_file(self) -> None: """Creates a new empty file in the current directory.""" self._create_new_item(is_folder=False) - def _create_new_item(self, is_folder): + def _create_new_item(self, is_folder: bool) -> None: """ Internal helper to create a new file or folder. @@ -108,7 +112,7 @@ class FileOperationsManager: self.dialog.widget_manager.search_status_label.config( text=f"{LocaleStrings.FILE['error_creating']}: {e}") - def _get_unique_name(self, base_name): + def _get_unique_name(self, base_name: str) -> str: """ Generates a unique name for a file or folder. @@ -129,7 +133,7 @@ class FileOperationsManager: new_name = f"{name} {counter}{ext}" return new_name - def _copy_to_clipboard(self, data): + def _copy_to_clipboard(self, data: str) -> None: """ Copies the given data to the system clipboard. @@ -141,7 +145,7 @@ class FileOperationsManager: self.dialog.widget_manager.search_status_label.config( text=f"'{self.dialog.shorten_text(data, 50)}' {LocaleStrings.FILE['copied_to_clipboard']}") - def _show_context_menu(self, event, item_path): + def _show_context_menu(self, event: tk.Event, item_path: str) -> str: """ Displays a context menu for the selected item. @@ -173,7 +177,7 @@ class FileOperationsManager: self.dialog.context_menu.tk_popup(event.x_root, event.y_root) return "break" - def _open_file_location_from_context(self, file_path): + def _open_file_location_from_context(self, file_path: str) -> None: """ Navigates to the location of the given file path. @@ -192,7 +196,7 @@ class FileOperationsManager: self.dialog.navigation_manager.navigate_to(directory) self.dialog.after(100, lambda: self.dialog.view_manager._select_file_in_view(filename)) - def on_rename_request(self, event, item_path=None, item_frame=None): + def on_rename_request(self, event: tk.Event, item_path: Optional[str] = None, item_frame: Optional[tk.Widget] = None) -> None: """ Handles the initial request to rename an item. @@ -215,7 +219,7 @@ class FileOperationsManager: if item_path and item_frame: self.start_rename(item_frame, item_path) - def start_rename(self, item_widget, item_path): + def start_rename(self, item_widget: Any, item_path: str) -> None: """ Starts the renaming UI for an item. @@ -231,7 +235,7 @@ class FileOperationsManager: else: # list view self._start_rename_list_view(item_widget) # item_widget is item_id - def _start_rename_icon_view(self, item_frame, item_path): + def _start_rename_icon_view(self, item_frame: ttk.Frame, item_path: str) -> None: """ Initiates the in-place rename UI for an item in icon view. @@ -250,7 +254,7 @@ class FileOperationsManager: entry.select_range(0, tk.END) entry.focus_set() - def finish_rename(event): + def finish_rename(event: tk.Event) -> None: new_name = entry.get() new_path = os.path.join(self.dialog.current_dir, new_name) if new_name and new_path != item_path: @@ -270,14 +274,14 @@ class FileOperationsManager: else: self.dialog.view_manager.populate_files(item_to_select=os.path.basename(item_path)) - def cancel_rename(event): + def cancel_rename(event: tk.Event) -> None: self.dialog.view_manager.populate_files() entry.bind("", finish_rename) entry.bind("", finish_rename) entry.bind("", cancel_rename) - def _start_rename_list_view(self, item_id): + def _start_rename_list_view(self, item_id: str) -> None: """ Initiates the in-place rename UI for an item in list view. @@ -304,7 +308,7 @@ class FileOperationsManager: entry.select_range(0, tk.END) entry.focus_set() - def finish_rename(event): + def finish_rename(event: tk.Event) -> None: new_name = entry.get() old_path = os.path.join(self.dialog.current_dir, item_text) new_path = os.path.join(self.dialog.current_dir, new_name) @@ -326,7 +330,7 @@ class FileOperationsManager: self.dialog.view_manager.populate_files(item_to_select=item_text) entry.destroy() - def cancel_rename(event): + def cancel_rename(event: tk.Event) -> None: entry.destroy() entry.bind("", finish_rename)