add type-hints on cfd_file_operations, cfd_app_config, animated_icon
This commit is contained in:
412
animated_icon.py
412
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:
|
||||
|
@@ -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
|
||||
|
@@ -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("<Return>", finish_rename)
|
||||
entry.bind("<FocusOut>", finish_rename)
|
||||
entry.bind("<Escape>", 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("<Return>", finish_rename)
|
||||
|
Reference in New Issue
Block a user