Reduced redundancy, logviewer fulll removed , add log_window and menu_bar

This commit is contained in:
2025-08-12 22:44:49 +02:00
parent 80aebe3bab
commit dc51bf6f2c
32 changed files with 1431 additions and 1617 deletions

View File

@@ -2,9 +2,26 @@ Changelog for shared_libs
## [Unreleased]
- add Info Window for user in delete logfile
bevore delete logfile.
-
### Added
12.08.2025
- New class loggers, animated icon, methods in common tools
improved and added new methods (contexmanager)
- Own FileDialog added(custom, this is exclusively for linux
An alternative to the X11 file dialogue that is otherwise opened
when working with python
- Reduced redundancy, logviewer fulll removed , add log_window and menu_bar
### Added
01.08.2025
- Add Icon Class to Central Image Management
- Tooltip Class replace
### Added
09.07.2025

View File

@@ -1,3 +1,3 @@
#!/usr/bin/python3

View File

@@ -16,13 +16,16 @@ try:
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.
@@ -44,7 +47,7 @@ class AnimatedIcon(tk.Canvas):
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
@@ -59,7 +62,8 @@ class AnimatedIcon(tk.Canvas):
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.image = Image.new(
"RGBA", (width * 4, height * 4), (0, 0, 0, 0))
self.draw = ImageDraw.Draw(self.image)
self.photo_image = None
@@ -88,9 +92,12 @@ class AnimatedIcon(tk.Canvas):
"""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])
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":
@@ -100,19 +107,25 @@ class AnimatedIcon(tk.Canvas):
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)
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)
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)
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)
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."""
@@ -124,56 +137,72 @@ class AnimatedIcon(tk.Canvas):
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])
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)
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)
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)
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.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)
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)
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)
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)
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])
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)
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))
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":
@@ -185,7 +214,8 @@ class AnimatedIcon(tk.Canvas):
elif self.animation_type == "blink":
self._draw_pillow_blink()
resized_image = self.image.resize((self.width, self.height), Image.Resampling.LANCZOS)
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)
@@ -194,9 +224,12 @@ class AnimatedIcon(tk.Canvas):
"""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])
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":
@@ -206,18 +239,24 @@ class AnimatedIcon(tk.Canvas):
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")
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)
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)
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)
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."""
@@ -229,52 +268,67 @@ class AnimatedIcon(tk.Canvas):
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])
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")
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)
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)
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)
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)
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)
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)
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)
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])
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)
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."""
@@ -304,34 +358,43 @@ class AnimatedIcon(tk.Canvas):
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)
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)
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)
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)
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)
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))
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":
@@ -341,7 +404,8 @@ class AnimatedIcon(tk.Canvas):
elif self.animation_type == "blink":
self._draw_pillow_blink_stopped()
resized_image = self.image.resize((self.width, self.height), Image.Resampling.LANCZOS)
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)
@@ -354,34 +418,51 @@ class AnimatedIcon(tk.Canvas):
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")
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)
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)
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)
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)
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:
try:
# Check if a modal dialog has the grab on the entire application
if self.winfo_toplevel().grab_current() is not None:
self.after(100, self._animate) # Check again after a short delay
return
except Exception:
# This can happen if no grab is active. We can safely ignore it.
pass
self.angle += 0.1
if self.angle > 2 * pi:
self.angle -= 2 * pi
@@ -417,9 +498,6 @@ class AnimatedIcon(tk.Canvas):
if not self.running:
self._draw_stopped_frame()
import tkinter as tk
from math import sin, cos, pi
try:
from PIL import Image, ImageDraw, ImageTk
@@ -427,13 +505,16 @@ try:
except ImportError:
PIL_AVAILABLE = False
def _hex_to_rgb(hex_color):
"""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, width=20, height=20, animation_type="counter_arc", color="#2a6fde", highlight_color="#5195ff", use_pillow=False, bg=None):
"""
Initializes the AnimatedIcon widget.
@@ -455,7 +536,7 @@ class AnimatedIcon(tk.Canvas):
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
@@ -470,7 +551,8 @@ class AnimatedIcon(tk.Canvas):
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.image = Image.new(
"RGBA", (width * 4, height * 4), (0, 0, 0, 0))
self.draw = ImageDraw.Draw(self.image)
self.photo_image = None
@@ -499,9 +581,12 @@ class AnimatedIcon(tk.Canvas):
"""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])
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":
@@ -511,19 +596,25 @@ class AnimatedIcon(tk.Canvas):
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)
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)
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)
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)
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):
"""Draws the line animation using canvas methods."""
@@ -535,56 +626,72 @@ class AnimatedIcon(tk.Canvas):
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])
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)
self.create_line(start_x, start_y, end_x,
end_y, fill=color, width=2)
def _draw_canvas_double_arc(self):
"""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)
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)
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.create_arc(bbox, start=start_angle2, extent=extent2,
style=tk.ARC, outline=self.color, width=2)
def _draw_canvas_counter_arc(self):
"""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)
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)
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)
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)
self.create_arc(bbox_inner, start=start_angle2, extent=150,
style=tk.ARC, outline=self.color, width=2)
def _draw_canvas_blink(self):
"""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])
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)
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):
"""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))
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":
@@ -596,7 +703,8 @@ class AnimatedIcon(tk.Canvas):
elif self.animation_type == "blink":
self._draw_pillow_blink()
resized_image = self.image.resize((self.width, self.height), Image.Resampling.LANCZOS)
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)
@@ -605,9 +713,12 @@ class AnimatedIcon(tk.Canvas):
"""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])
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":
@@ -617,18 +728,24 @@ class AnimatedIcon(tk.Canvas):
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")
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)
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)
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)
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):
"""Draws the line animation using Pillow."""
@@ -640,52 +757,67 @@ class AnimatedIcon(tk.Canvas):
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])
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")
self.draw.line([(start_x, start_y), (end_x, end_y)],
fill=color, width=6, joint="curve")
def _draw_pillow_double_arc(self):
"""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)
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)
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)
self.draw.arc(bbox, start=start_angle2, end=start_angle2 +
extent2, fill=self.color_rgb, width=5)
def _draw_pillow_counter_arc(self):
"""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)
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)
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)
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)
self.draw.arc(bbox_inner, start=start_angle2,
end=start_angle2 + 150, fill=self.color_rgb, width=7)
def _draw_pillow_blink(self):
"""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])
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)
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):
"""Draws the icon in its stopped (static) state."""
@@ -715,34 +847,43 @@ class AnimatedIcon(tk.Canvas):
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)
self.create_line(start_x, start_y, end_x, end_y,
fill=self.highlight_color, width=2)
def _draw_canvas_double_arc_stopped(self):
"""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)
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):
"""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)
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)
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):
"""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)
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):
"""Draws the stopped state using Pillow."""
self.draw.rectangle([0, 0, self.width * 4, self.height * 4], fill=(0, 0, 0, 0))
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":
@@ -752,7 +893,8 @@ class AnimatedIcon(tk.Canvas):
elif self.animation_type == "blink":
self._draw_pillow_blink_stopped()
resized_image = self.image.resize((self.width, self.height), Image.Resampling.LANCZOS)
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)
@@ -765,30 +907,38 @@ class AnimatedIcon(tk.Canvas):
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")
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):
"""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)
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):
"""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)
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)
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):
"""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)
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):
"""The main animation loop."""
@@ -826,4 +976,4 @@ class AnimatedIcon(tk.Canvas):
def show_full_circle(self):
"""Shows the static 'stopped' frame without starting the animation."""
if not self.running:
self._draw_stopped_frame()
self._draw_stopped_frame()

View File

@@ -1,16 +1,21 @@
""" Classes Method and Functions for lx Apps """
" Classes Method and Functions for lx Apps "
import logging
import signal
import base64
from contextlib import contextmanager
from .logger import app_logger
from subprocess import CompletedProcess, run
import gettext
import locale
import re
import sys
import shutil
import tkinter as tk
from tkinter import ttk
import os
from typing import Optional, Dict, Any, NoReturn
from pathlib import Path
from tkinter import Toplevel
class CryptoUtil:
@@ -34,14 +39,14 @@ class CryptoUtil:
# Output from Openssl Error
if process.stderr:
logging.error(process.stderr, exc_info=True)
app_logger.log(process.stderr)
if process.returncode == 0:
logging.info("Files successfully decrypted...", exc_info=True)
app_logger.log("Files successfully decrypted...")
else:
logging.error(
f"Error process decrypt: Code {process.returncode}", exc_info=True
app_logger.log(
f"Error process decrypt: Code {process.returncode}"
)
@staticmethod
@@ -58,13 +63,13 @@ class CryptoUtil:
# Output from Openssl Error
if process.stderr:
logging.error(process.stderr, exc_info=True)
app_logger.log(process.stderr)
if process.returncode == 0:
logging.info("Files successfully encrypted...", exc_info=True)
app_logger.log("Files successfully encrypted...")
else:
logging.error(
f"Error process encrypt: Code {process.returncode}", exc_info=True
app_logger.log(
f"Error process encrypt: Code {process.returncode}"
)
@staticmethod
@@ -83,9 +88,8 @@ class CryptoUtil:
return True
elif "False" in process.stdout:
return False
logging.error(
f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}",
exc_info=True,
app_logger.log(
f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}"
)
return False
@@ -110,7 +114,7 @@ class CryptoUtil:
if len(decoded) != 32: # 32 bytes = 256 bits
return False
except Exception as e:
logging.error(f"Error on decode Base64: {e}", exc_info=True)
app_logger.log(f"Error on decode Base64: {e}")
return False
return True
@@ -273,17 +277,16 @@ class LxTools:
# End program for certain signals, report to others only reception
if signum in (signal.SIGINT, signal.SIGTERM):
exit_code: int = 1
logging.error(
f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}.",
exc_info=True,
app_logger.log(
f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}."
)
LxTools.clean_files(file_path, file)
logging.info("Breakdown by user...")
app_logger.log("Breakdown by user...")
sys.exit(exit_code)
else:
logging.info(f"Signal {signum} received and ignored.")
app_logger.log(f"Signal {signum} received and ignored.")
LxTools.clean_files(file_path, file)
logging.error("Process unexpectedly ended...")
app_logger.log("Process unexpectedly ended...")
# Register signal handlers for various signals
signal.signal(signal.SIGINT, signal_handler)
@@ -324,14 +327,14 @@ class ConfigManager:
"""Load the config file and return the config as dict"""
if not cls._config:
try:
lines = Path(cls._config_file).read_text(encoding="utf-8").splitlines()
lines = Path(cls._config_file).read_text(
encoding="utf-8").splitlines()
cls._config = {
"updates": lines[1].strip(),
"theme": lines[3].strip(),
"tooltips": lines[5].strip()
== "True", # is converted here to boolean!!!
"autostart": lines[7].strip() if len(lines) > 7 else "off",
"logfile": lines[9].strip(),
}
except (IndexError, FileNotFoundError):
# DeDefault values in case of error
@@ -340,7 +343,6 @@ class ConfigManager:
"theme": "light",
"tooltips": "True", # Default Value as string!
"autostart": "off",
"logfile": LOG_FILE_PATH,
}
return cls._config
@@ -357,8 +359,6 @@ class ConfigManager:
f"{str(cls._config['tooltips'])}\n",
"# Autostart\n",
f"{cls._config['autostart']}\n",
"# Logfile\n",
f"{cls._config['logfile']}\n",
]
Path(cls._config_file).write_text("".join(lines), encoding="utf-8")
@@ -396,110 +396,435 @@ class ThemeManager:
@staticmethod
def change_theme(root, theme_in_use, theme_name=None):
"""Change application theme centrally"""
"""
Change application theme centrally.
Args:
root: The root Tkinter window.
theme_in_use (str): The name of the theme to apply.
theme_name (Optional[str]): The name of the theme to save in the config.
If None, the theme is not saved.
"""
root.tk.call("set_theme", theme_in_use)
if theme_in_use == theme_name:
ConfigManager.set("theme", theme_in_use)
class Tooltip:
"""Class for Tooltip
from common_tools.py import Tooltip
example: Tooltip(label, "Show tooltip on label")
example: Tooltip(button, "Show tooltip on button")
example: Tooltip(widget, "Text", state_var=tk.BooleanVar())
example: Tooltip(widget, "Text", state_var=tk.BooleanVar(), x_offset=20, y_offset=10)
"""
A flexible tooltip class for Tkinter widgets that supports dynamic activation/deactivation.
info: label and button are parent widgets.
NOTE: When using with state_var, pass the tk.BooleanVar object directly,
NOT its value. For example: use state_var=my_bool_var, NOT state_var=my_bool_var.get()
This class provides customizable tooltips that appear when the mouse hovers over a widget.
It can be used for simple, always-active tooltips or for tooltips whose visibility is
controlled by a `tk.BooleanVar`, allowing for global enable/disable functionality.
Attributes:
widget (tk.Widget): The Tkinter widget to which the tooltip is attached.
text (str): The text to display in the tooltip.
wraplength (int): The maximum line length for the tooltip text before wrapping.
state_var (Optional[tk.BooleanVar]): An optional Tkinter BooleanVar that controls
the visibility of the tooltip. If True, the tooltip
is active; if False, it is inactive. If None, the
tooltip is always active.
tooltip_window (Optional[tk.Toplevel]): The Toplevel window used to display the tooltip.
id (Optional[str]): The ID of the `after` job used to schedule the tooltip display.
Usage Examples:
# 1. Simple Tooltip (always active):
# Tooltip(my_button, "This is a simple tooltip.")
# 2. State-Controlled Tooltip (can be enabled/disabled globally):
# tooltip_state = tk.BooleanVar(value=True)
# Tooltip(my_button, "This tooltip can be turned off!", state_var=tooltip_state)
# # To toggle visibility:
# # tooltip_state.set(False) # Tooltips will hide
# # tooltip_state.set(True) # Tooltips will show again
"""
def __init__(
self,
widget: Any,
text: str,
state_var: Optional[tk.BooleanVar] = None,
x_offset: int = 65,
y_offset: int = 40,
) -> None:
"""Tooltip Class"""
self.widget: Any = widget
self.text: str = text
self.tooltip_window: Optional[Toplevel] = None
def __init__(self, widget, text, wraplength=250, state_var=None):
self.widget = widget
self.text = text
self.wraplength = wraplength
self.state_var = state_var
self.x_offset = x_offset
self.y_offset = y_offset
# Initial binding based on the current state
self.tooltip_window = None
self.id = None
self.update_bindings()
# Add trace to the state_var if provided
if self.state_var is not None:
if self.state_var:
self.state_var.trace_add("write", self.update_bindings)
def update_bindings(self, *args) -> None:
"""Updates the bindings based on the current state"""
# Remove existing bindings first
def update_bindings(self, *args):
"""
Updates the event bindings for the widget based on the current state_var.
If state_var is True or None, the <Enter>, <Leave>, and <ButtonPress> events
are bound to show/hide the tooltip. Otherwise, they are unbound.
"""
self.widget.unbind("<Enter>")
self.widget.unbind("<Leave>")
self.widget.unbind("<ButtonPress>")
# Add new bindings if tooltips are enabled
if self.state_var is None or self.state_var.get():
self.widget.bind("<Enter>", self.show_tooltip)
self.widget.bind("<Leave>", self.hide_tooltip)
self.widget.bind("<Enter>", self.enter)
self.widget.bind("<Leave>", self.leave)
self.widget.bind("<ButtonPress>", self.leave)
def show_tooltip(self, event: Optional[Any] = None) -> None:
"""Shows the tooltip"""
def enter(self, event=None):
"""
Handles the <Enter> event. Schedules the tooltip to be shown after a delay
if tooltips are enabled (via state_var).
"""
try:
# Check if a modal dialog has the grab on the entire application
if self.winfo_toplevel().grab_current() is not None:
return
except Exception:
# This can happen if no grab is active. We can safely ignore it.
pass
if self.state_var is None or self.state_var.get():
self.schedule()
def leave(self, event=None):
"""
Handles the <Leave> event. Unschedules any pending tooltip display
and immediately hides any visible tooltip.
"""
self.unschedule()
self.hide_tooltip()
def schedule(self):
"""
Schedules the `show_tooltip` method to be called after a short delay.
Cancels any previously scheduled calls to prevent flickering.
"""
self.unschedule()
self.id = self.widget.after(250, self.show_tooltip)
def unschedule(self):
"""
Cancels any pending `show_tooltip` calls.
"""
id = self.id
self.id = None
if id:
self.widget.after_cancel(id)
def show_tooltip(self, event=None):
"""
Displays the tooltip window. The tooltip is a Toplevel window containing a ttk.Label.
It is positioned near the widget and styled for readability.
"""
if self.tooltip_window or not self.text:
return
x: int
y: int
cx: int
cy: int
x, y, cx, cy = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + self.x_offset
y += self.widget.winfo_rooty() + self.y_offset
x, y, _, _ = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + 25
y += self.widget.winfo_rooty() + 20
self.tooltip_window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.wm_geometry(f"+{x}+{y}")
tw.wm_geometry(f"+" + str(x) + "+" + str(y))
label = ttk.Label(tw, text=self.text, justify=tk.LEFT, background="#FFFFE0", foreground="black",
relief=tk.SOLID, borderwidth=1, wraplength=self.wraplength, padding=(4, 2, 4, 2))
label.pack(ipadx=1)
label: tk.Label = tk.Label(
tw,
text=self.text,
background="lightgreen",
foreground="black",
relief="solid",
borderwidth=1,
padx=5,
pady=5,
)
label.grid()
self.tooltip_window.after(2200, lambda: tw.destroy())
def hide_tooltip(self, event: Optional[Any] = None) -> None:
"""Hides the tooltip"""
if self.tooltip_window:
self.tooltip_window.destroy()
self.tooltip_window = None
def hide_tooltip(self):
"""
Hides and destroys the tooltip window if it is currently visible.
"""
tw = self.tooltip_window
self.tooltip_window = None
if tw:
tw.destroy()
class LogConfig:
"""
A static class for configuring application-wide logging.
This class provides a convenient way to set up file-based logging for the application.
It ensures that log messages are written to a specified file with a consistent format.
Methods:
logger(file_path: str) -> None:
Configures the root logger to write messages to the specified file.
Usage Example:
# Assuming LOG_FILE_PATH is defined elsewhere (e.g., in a config file)
# LogConfig.logger(LOG_FILE_PATH)
# logging.info("This message will be written to the log file.")
"""
@staticmethod
def logger(file_path) -> None:
"""
Configures the root logger to write messages to the specified file.
Args:
file_path (str): The absolute path to the log file.
"""
file_handler = logging.FileHandler(
filename=f"{file_path}",
mode="a",
encoding="utf-8",
)
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s")
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.DEBUG)
logger = logging.getLogger()
logger.setLevel(logging.DEBUG) # Set the root logger level
logger.addHandler(file_handler)
class IconManager:
"""
A class for central management and loading of application icons.
This class loads Tkinter PhotoImage objects from a specified base path,
organizing them by logical names and providing a convenient way to retrieve them.
It handles potential errors during image loading by creating a blank image placeholder.
Attributes:
base_path (str): The base directory where icon subfolders (e.g., '16', '32', '48', '64') are located.
icons (Dict[str, tk.PhotoImage]): A dictionary storing loaded PhotoImage objects,
keyed by their logical names (e.g., 'computer_small', 'folder_large').
Methods:
get_icon(name: str) -> Optional[tk.PhotoImage]:
Retrieves a loaded icon by its logical name.
Usage Example:
# Initialize the IconManager with the path to your icon directory
# icon_manager = IconManager(base_path="/usr/share/icons/lx-icons/")
# Retrieve an icon
# computer_icon = icon_manager.get_icon("computer_small")
# if computer_icon:
# my_label = tk.Label(root, image=computer_icon)
# my_label.pack()
"""
def __init__(self, base_path='/usr/share/icons/lx-icons/'):
self.base_path = base_path
self.icons = {}
self._define_icon_paths()
self._load_all()
def _define_icon_paths(self):
self.icon_paths = {
# 16x16
'settings_16': '16/settings.png',
# 32x32
'back': '32/arrow-left.png',
'forward': '32/arrow-right.png',
'up': '32/arrow-up.png',
'audio_small': '32/audio.png',
'icon_view': '32/carrel.png',
'computer_small': '32/computer.png',
'device_small': '32/device.png',
'file_small': '32/document.png',
'download_error_small': '32/download_error.png',
'download_small': '32/download.png',
'error_small': '32/error.png',
'python_small': '32/file-python.png',
'documents_small': '32/folder-water-documents.png',
'downloads_small': '32/folder-water-download.png',
'music_small': '32/folder-water-music.png',
'pictures_small': '32/folder-water-pictures.png',
'folder_small': '32/folder-water.png',
'video_small': '32/folder-water-video.png',
'hide': '32/hide.png',
'home': '32/home.png',
'about': '32/about.png',
'info_small': '32/info.png',
'light_small': '32/light.png',
'dark_small': '32/dark.png',
'update_small': '32/update.png',
'no_update_small': '32/no_update.png',
'tooltip_small': '32/tip.png',
'no_tooltip_small': '32/no_tip.png',
'list_view': '32/list.png',
'log_small': '32/log.png',
'log_blue_small': '32/log_blue.png',
'lunix_tools_small': '32/Lunix_Tools.png',
'key_small': '32/lxtools_key.png',
'iso_small': '32/media-optical.png',
'new_document_small': '32/new-document.png',
'new_folder_small': '32/new-folder.png',
'pdf_small': '32/pdf.png',
'picture_small': '32/picture.png',
'question_mark_small': '32/question_mark.png',
'recursive_small': '32/recursive.png',
'search_small': '32/search.png',
'settings_small': '32/settings.png',
'settings-2_small': '32/settings-2.png',
'archive_small': '32/tar.png',
'unhide': '32/unhide.png',
'usb_small': '32/usb.png',
'video_small_file': '32/video.png',
'warning_small': '32/warning.png',
'export_small': '32/wg_export.png',
'import_small': '32/wg_import.png',
'message_small': '32/wg_msg.png',
'trash_small': '32/wg_trash.png',
'trash_small2': '32/trash.png',
'vpn_small': '32/wg_vpn.png',
'vpn_start_small': '32/wg_vpn-start.png',
'vpn_stop_small': '32/wg_vpn-stop.png',
# 48x48
'back_large': '48/arrow-left.png',
'forward_large': '48/arrow-right.png',
'up_large': '48/arrow-up.png',
'icon_view_large': '48/carrel.png',
'computer_large': '48/computer.png',
'device_large': '48/device.png',
'download_error_large': '48/download_error.png',
'download_large': '48/download.png',
'error_large': '48/error.png',
'documents_large': '48/folder-water-documents.png',
'downloads_large': '48/folder-water-download.png',
'music_large': '48/folder-water-music.png',
'pictures_large': '48/folder-water-pictures.png',
'folder_large_48': '48/folder-water.png',
'video_large_folder': '48/folder-water-video.png',
'hide_large': '48/hide.png',
'home_large': '48/home.png',
'info_large': '48/info.png',
'light_large': '48/light.png',
'dark_large': '48/dark.png',
'update_large': '48/update.png',
'no_update_large': '48/no_update.png',
'tooltip_large': '48/tip.png',
'no_tooltip_large': '48/no_tip.png',
'about_large': '48/about.png',
'list_view_large': '48/list.png',
'log_large': '48/log.png',
'log_blue_large': '48/log_blue.png',
'lunix_tools_large': '48/Lunix_Tools.png',
'new_document_large': '48/new-document.png',
'new_folder_large': '48/new-folder.png',
'question_mark_large': '48/question_mark.png',
'search_large_48': '48/search.png',
'settings_large': '48/settings.png',
'unhide_large': '48/unhide.png',
'usb_large': '48/usb.png',
'warning_large_48': '48/warning.png',
'export_large': '48/wg_export.png',
'import_large': '48/wg_import.png',
'message_large': '48/wg_msg.png',
'trash_large': '48/wg_trash.png',
'trash_large2': '48/trash.png',
'vpn_large': '48/wg_vpn.png',
'vpn_start_large': '48/wg_vpn-start.png',
'vpn_stop_large': '48/wg_vpn-stop.png',
# 64x64
'back_extralarge': '64/arrow-left.png',
'forward_extralarge': '64/arrow-right.png',
'up_extralarge': '64/arrow-up.png',
'audio_large': '64/audio.png',
'icon_view_extralarge': '64/carrel.png',
'computer_extralarge': '64/computer.png',
'device_extralarge': '64/device.png',
'file_large': '64/document.png',
'download_error_extralarge': '64/download_error.png',
'download_extralarge': '64/download.png',
'error_extralarge': '64/error.png',
'python_large': '64/file-python.png',
'documents_extralarge': '64/folder-water-documents.png',
'downloads_extralarge': '64/folder-water-download.png',
'music_extralarge': '64/folder-water-music.png',
'pictures_extralarge': '64/folder-water-pictures.png',
'folder_large': '64/folder-water.png',
'video_extralarge_folder': '64/folder-water-video.png',
'hide_extralarge': '64/hide.png',
'home_extralarge': '64/home.png',
'info_extralarge': '64/info.png',
'light_extralarge': '64/light.png',
'dark_extralarge': '64/dark.png',
'update_extralarge': '64/update.png',
'no_update_extralarge': '64/no_update.png',
'tooltip_extralarge': '64/tip.png',
'no_tooltip_extralarge': '64/no_tip.png',
'about_extralarge': '64/about.png',
'list_view_extralarge': '64/list.png',
'log_extralarge': '64/log.png',
'log_blue_extralarge': '64/log_blue.png',
'lunix_tools_extralarge': '64/Lunix_Tools.png',
'iso_large': '64/media-optical.png',
'new_document_extralarge': '64/new-document.png',
'new_folder_extralarge': '64/new-folder.png',
'pdf_large': '64/pdf.png',
'picture_large': '64/picture.png',
'question_mark_extralarge': '64/question_mark.png',
'recursive_large': '64/recursive.png',
'search_large': '64/search.png',
'settings_extralarge': '64/settings.png',
'archive_large': '64/tar.png',
'unhide_extralarge': '64/unhide.png',
'usb_extralarge': '64/usb.png',
'video_large': '64/video.png',
'warning_large': '64/warning.png',
'export_extralarge': '64/wg_export.png',
'import_extralarge': '64/wg_import.png',
'message_extralarge': '64/wg_msg.png',
'trash_extralarge': '64/wg_trash.png',
'trash_extralarge2': '64/trash.png',
'vpn_extralarge': '64/wg_vpn.png',
'vpn_start_extralarge': '64/wg_vpn-start.png',
'vpn_stop_extralarge': '64/wg_vpn-stop.png',
}
def _load_all(self):
for key, rel_path in self.icon_paths.items():
full_path = os.path.join(self.base_path, rel_path)
try:
self.icons[key] = tk.PhotoImage(file=full_path)
except tk.TclError as e:
print(f"Error loading icon '{key}' from '{full_path}': {e}")
size = 32 # Default size
if '16' in rel_path:
size = 16
elif '48' in rel_path:
size = 48
elif '64' in rel_path:
size = 64
self.icons[key] = tk.PhotoImage(width=size, height=size)
def get_icon(self, name):
return self.icons.get(name)
class Translate:
@staticmethod
def setup_translations(app_name: str, locale_dir="/usr/share/locale/") -> gettext.gettext:
"""
Initialize translations and set the translation function
Special method for translating strings in this file
Returns:
The gettext translation function
"""
locale.bindtextdomain(app_name, locale_dir)
gettext.bindtextdomain(app_name, locale_dir)
gettext.textdomain(app_name)
return gettext.gettext
@contextmanager
def message_box_animation(animated_icon):
"""
A context manager to handle starting and stopping an animated icon
around an operation like showing a message box.
Args:
animated_icon: The animated icon object with start() and stop() methods.
"""
if animated_icon:
animated_icon.stop()
try:
yield
finally:
if animated_icon:
animated_icon.start()

View File

@@ -0,0 +1 @@
from .custom_file_dialog import CustomFileDialog

View File

@@ -3,47 +3,20 @@
"""App configuration for Custom File Dialog"""
import json
from pathlib import Path
import os
from typing import Dict, Any
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
#!/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.
# here is initializing the class for translation strings
_ = Translate.setup_translations("custom_file_dialog")
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.
class CfdConfigManager:
"""
# 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
Manages CFD-specific settings using a JSON file for flexibility.
"""
MAX_ITEMS_TO_DISPLAY = 1000
# Base paths
BASE_DIR: Path = Path.home()
@@ -58,17 +31,8 @@ class AppConfig:
"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"
_config_file: Path = CONFIG_DIR / "cfd_settings.json"
_default_settings: Dict[str, Any] = {
"search_icon_pos": "left", # 'left' or 'right'
"button_box_pos": "left", # 'left' or 'right'
@@ -272,233 +236,3 @@ class LocaleStrings:
"videos": _("Videos"),
"computer": _("Computer"),
}
MAX_ITEMS_TO_DISPLAY = 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 = None
_config_file = AppConfig.CONFIG_DIR / "cfd_settings.json"
_default_settings = {
"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):
"""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):
"""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, settings):
"""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 = {
"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 = {
"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 = {
"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 = {
"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 = {
"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 = {
"home": _("Home"),
"trash": _("Trash"),
"desktop": _("Desktop"),
"documents": _("Documents"),
"downloads": _("Downloads"),
"music": _("Music"),
"pictures": _("Pictures"),
"videos": _("Videos"),
"computer": _("Computer"),
}

View File

@@ -11,7 +11,8 @@ except ImportError:
SEND2TRASH_AVAILABLE = False
from shared_libs.message import MessageDialog
from cfd_app_config import LocaleStrings, _
from .cfd_app_config import LocaleStrings
if TYPE_CHECKING:
from custom_file_dialog import CustomFileDialog
@@ -163,10 +164,10 @@ class FileOperationsManager:
self.dialog.context_menu.destroy()
self.dialog.context_menu = tk.Menu(self.dialog, tearoff=0, background=self.dialog.style_manager.header, foreground=self.dialog.style_manager.color_foreground,
activebackground=self.dialog.style_manager.selection_color, activeforeground=self.dialog.style_manager.color_foreground, relief='flat', borderwidth=0)
activebackground=self.dialog.style_manager.selection_color, activeforeground=self.dialog.style_manager.color_foreground, relief='flat', borderwidth=0)
self.dialog.context_menu.add_command(label=LocaleStrings.UI["copy_filename_to_clipboard"],
command=lambda: self._copy_to_clipboard(os.path.basename(item_path)))
command=lambda: self._copy_to_clipboard(os.path.basename(item_path)))
self.dialog.context_menu.add_command(
label=LocaleStrings.UI["copy_path_to_clipboard"], command=lambda: self._copy_to_clipboard(item_path))
@@ -194,7 +195,8 @@ class FileOperationsManager:
self.dialog.search_manager.hide_search_bar()
self.dialog.navigation_manager.navigate_to(directory)
self.dialog.after(100, lambda: self.dialog.view_manager._select_file_in_view(filename))
self.dialog.after(
100, lambda: self.dialog.view_manager._select_file_in_view(filename))
def on_rename_request(self, event: tk.Event, item_path: Optional[str] = None, item_frame: Optional[tk.Widget] = None) -> None:
"""
@@ -266,13 +268,15 @@ class FileOperationsManager:
return
try:
os.rename(item_path, new_path)
self.dialog.view_manager.populate_files(item_to_select=new_name)
self.dialog.view_manager.populate_files(
item_to_select=new_name)
except Exception as e:
self.dialog.widget_manager.search_status_label.config(
text=f"{LocaleStrings.FILE['error_renaming']}: {e}")
self.dialog.view_manager.populate_files()
else:
self.dialog.view_manager.populate_files(item_to_select=os.path.basename(item_path))
self.dialog.view_manager.populate_files(
item_to_select=os.path.basename(item_path))
def cancel_rename(event: tk.Event) -> None:
self.dialog.view_manager.populate_files()
@@ -317,17 +321,20 @@ class FileOperationsManager:
if os.path.exists(new_path):
self.dialog.widget_manager.search_status_label.config(
text=f"'{new_name}' {LocaleStrings.FILE['folder_exists_error']}")
self.dialog.view_manager.populate_files(item_to_select=item_text)
self.dialog.view_manager.populate_files(
item_to_select=item_text)
else:
try:
os.rename(old_path, new_path)
self.dialog.view_manager.populate_files(item_to_select=new_name)
self.dialog.view_manager.populate_files(
item_to_select=new_name)
except Exception as e:
self.dialog.widget_manager.search_status_label.config(
text=f"{LocaleStrings.FILE['error_renaming']}: {e}")
self.dialog.view_manager.populate_files()
else:
self.dialog.view_manager.populate_files(item_to_select=item_text)
self.dialog.view_manager.populate_files(
item_to_select=item_text)
entry.destroy()
def cancel_rename(event: tk.Event) -> None:
@@ -336,4 +343,3 @@ class FileOperationsManager:
entry.bind("<Return>", finish_rename)
entry.bind("<FocusOut>", finish_rename)
entry.bind("<Escape>", cancel_rename)

View File

@@ -2,7 +2,7 @@ import os
import tkinter as tk
from typing import Optional, TYPE_CHECKING
from cfd_app_config import LocaleStrings, _
from .cfd_app_config import LocaleStrings
if TYPE_CHECKING:
from custom_file_dialog import CustomFileDialog
@@ -73,15 +73,17 @@ class NavigationManager:
if not self.dialog.history or self.dialog.history[-1] != self.dialog.current_dir:
self.dialog.history.append(self.dialog.current_dir)
self.dialog.history_pos = len(self.dialog.history) - 1
self.dialog.widget_manager.search_animation.stop()
self.dialog.view_manager.populate_files(item_to_select=file_to_select)
self.dialog.view_manager.populate_files(
item_to_select=file_to_select)
self.update_nav_buttons()
self.dialog.update_status_bar()
self.dialog.update_action_buttons_state()
except Exception as e:
self.dialog.widget_manager.search_status_label.config(text=f"{LocaleStrings.CFD['error_title']}: {e}")
self.dialog.widget_manager.search_status_label.config(
text=f"{LocaleStrings.CFD['error_title']}: {e}")
def go_back(self) -> None:
"""Navigates to the previous directory in the history."""

View File

@@ -7,8 +7,8 @@ from tkinter import ttk
from typing import Optional, TYPE_CHECKING
from shared_libs.message import MessageDialog
from cfd_ui_setup import get_xdg_user_dir
from cfd_app_config import LocaleStrings, _
from .cfd_ui_setup import get_xdg_user_dir
from .cfd_app_config import LocaleStrings
if TYPE_CHECKING:
from custom_file_dialog import CustomFileDialog
@@ -39,9 +39,10 @@ class SearchManager:
"""
if self.dialog.widget_manager.search_animation.running:
if self.dialog.search_thread and self.dialog.search_thread.is_alive():
self.dialog.search_thread.cancelled = True
self.dialog.search_thread.cancelled = True
self.dialog.widget_manager.search_animation.stop()
self.dialog.widget_manager.search_status_label.config(text=LocaleStrings.UI["cancel_search"])
self.dialog.widget_manager.search_status_label.config(
text=LocaleStrings.UI["cancel_search"])
else:
self.execute_search()
@@ -83,10 +84,12 @@ class SearchManager:
if not search_term:
self.hide_search_bar()
return
self.dialog.widget_manager.search_status_label.config(text=f"{LocaleStrings.UI['searching_for']} '{search_term}'...")
self.dialog.widget_manager.search_status_label.config(
text=f"{LocaleStrings.UI['searching_for']} '{search_term}'...")
self.dialog.widget_manager.search_animation.start(pulse=False)
self.dialog.update_idletasks()
self.dialog.search_thread = threading.Thread(target=self._perform_search_in_thread, args=(search_term,))
self.dialog.search_thread = threading.Thread(
target=self._perform_search_in_thread, args=(search_term,))
self.dialog.search_thread.start()
def _perform_search_in_thread(self, search_term: str) -> None:
@@ -104,13 +107,16 @@ class SearchManager:
search_dirs = [self.dialog.current_dir]
home_dir = os.path.expanduser("~")
if os.path.abspath(self.dialog.current_dir) == os.path.abspath(home_dir):
xdg_dirs = [get_xdg_user_dir(d, f) for d, f in [("XDG_DOWNLOAD_DIR", "Downloads"), ("XDG_DOCUMENTS_DIR", "Documents"), ("XDG_PICTURES_DIR", "Pictures"), ("XDG_MUSIC_DIR", "Music"), ("XDG_VIDEO_DIR", "Videos")]]
search_dirs.extend([d for d in xdg_dirs if os.path.exists(d) and os.path.abspath(d) != home_dir and d not in search_dirs])
xdg_dirs = [get_xdg_user_dir(d, f) for d, f in [("XDG_DOWNLOAD_DIR", "Downloads"), ("XDG_DOCUMENTS_DIR", "Documents"), (
"XDG_PICTURES_DIR", "Pictures"), ("XDG_MUSIC_DIR", "Music"), ("XDG_VIDEO_DIR", "Videos")]]
search_dirs.extend([d for d in xdg_dirs if os.path.exists(
d) and os.path.abspath(d) != home_dir and d not in search_dirs])
try:
all_files = []
is_recursive = self.dialog.settings.get("recursive_search", True)
search_hidden = self.dialog.settings.get("search_hidden_files", False)
search_hidden = self.dialog.settings.get(
"search_hidden_files", False)
search_term_lower = search_term.lower()
for search_dir in search_dirs:
@@ -125,10 +131,12 @@ class SearchManager:
if is_recursive:
for root, dirs, files in os.walk(search_dir, followlinks=follow_links):
if not (self.dialog.search_thread and self.dialog.search_thread.is_alive()):
raise InterruptedError(LocaleStrings.UI["search_cancelled_by_user"])
raise InterruptedError(
LocaleStrings.UI["search_cancelled_by_user"])
if not search_hidden:
dirs[:] = [d for d in dirs if not d.startswith('.')]
dirs[:] = [
d for d in dirs if not d.startswith('.')]
files = [f for f in files if not f.startswith('.')]
for name in files:
@@ -140,11 +148,12 @@ class SearchManager:
else:
for name in os.listdir(search_dir):
if not (self.dialog.search_thread and self.dialog.search_thread.is_alive()):
raise InterruptedError(LocaleStrings.UI["search_cancelled_by_user"])
raise InterruptedError(
LocaleStrings.UI["search_cancelled_by_user"])
if not search_hidden and name.startswith('.'):
continue
path = os.path.join(search_dir, name)
is_dir = os.path.isdir(path)
@@ -158,15 +167,18 @@ class SearchManager:
break
if not (self.dialog.search_thread and self.dialog.search_thread.is_alive()):
raise InterruptedError(LocaleStrings.UI["search_cancelled_by_user"])
raise InterruptedError(
LocaleStrings.UI["search_cancelled_by_user"])
seen = set()
self.dialog.search_results = [x for x in all_files if not (x in seen or seen.add(x))]
self.dialog.search_results = [
x for x in all_files if not (x in seen or seen.add(x))]
def update_ui() -> None:
if self.dialog.search_results:
self.show_search_results_treeview()
folder_count = sum(1 for p in self.dialog.search_results if os.path.isdir(p))
folder_count = sum(
1 for p in self.dialog.search_results if os.path.isdir(p))
file_count = len(self.dialog.search_results) - folder_count
self.dialog.widget_manager.search_status_label.config(
text=f"{folder_count} {LocaleStrings.UI['folders_and']} {file_count} {LocaleStrings.UI['files_found']}")
@@ -177,12 +189,14 @@ class SearchManager:
except (Exception, InterruptedError) as e:
if isinstance(e, (InterruptedError, subprocess.SubprocessError)):
self.dialog.after(0, lambda: self.dialog.widget_manager.search_status_label.config(text=LocaleStrings.UI["cancel_search"]))
self.dialog.after(0, lambda: self.dialog.widget_manager.search_status_label.config(
text=LocaleStrings.UI["cancel_search"]))
else:
self.dialog.after(0, lambda: MessageDialog(
message_type="error", text=f"{LocaleStrings.UI['error_during_search']}: {e}", title=LocaleStrings.UI["search_error"], master=self.dialog).show())
finally:
self.dialog.after(0, self.dialog.widget_manager.search_animation.stop)
self.dialog.after(
0, self.dialog.widget_manager.search_animation.stop)
self.dialog.search_process = None
def show_search_results_treeview(self) -> None:
@@ -199,13 +213,17 @@ class SearchManager:
search_tree = ttk.Treeview(
tree_frame, columns=columns, show="tree headings")
search_tree.heading("#0", text=LocaleStrings.VIEW["filename"], anchor="w")
search_tree.heading(
"#0", text=LocaleStrings.VIEW["filename"], anchor="w")
search_tree.column("#0", anchor="w", width=200, stretch=True)
search_tree.heading("path", text=LocaleStrings.VIEW["path"], anchor="w")
search_tree.heading(
"path", text=LocaleStrings.VIEW["path"], anchor="w")
search_tree.column("path", anchor="w", width=300, stretch=True)
search_tree.heading("size", text=LocaleStrings.VIEW["size"], anchor="e")
search_tree.heading(
"size", text=LocaleStrings.VIEW["size"], anchor="e")
search_tree.column("size", anchor="e", width=100, stretch=False)
search_tree.heading("modified", text=LocaleStrings.VIEW["date_modified"], anchor="w")
search_tree.heading(
"modified", text=LocaleStrings.VIEW["date_modified"], anchor="w")
search_tree.column("modified", anchor="w", width=160, stretch=False)
v_scrollbar = ttk.Scrollbar(
@@ -266,9 +284,10 @@ class SearchManager:
item = search_tree.item(selection[0])
filename = item['text'].strip()
directory = item['values'][0]
self.hide_search_bar()
self.dialog.navigation_manager.navigate_to(directory, file_to_select=filename)
self.dialog.navigation_manager.navigate_to(
directory, file_to_select=filename)
search_tree.bind("<Double-1>", on_search_double_click)
@@ -304,4 +323,5 @@ class SearchManager:
self.hide_search_bar()
self.dialog.navigation_manager.navigate_to(directory)
self.dialog.after(100, lambda: self.dialog.view_manager._select_file_in_view(filename))
self.dialog.after(
100, lambda: self.dialog.view_manager._select_file_in_view(filename))

View File

@@ -2,8 +2,8 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from cfd_app_config import CfdConfigManager, LocaleStrings, _
from animated_icon import PIL_AVAILABLE
from .cfd_app_config import CfdConfigManager, LocaleStrings
from shared_libs.animated_icon import PIL_AVAILABLE
if TYPE_CHECKING:
from custom_file_dialog import CustomFileDialog
@@ -120,7 +120,7 @@ class SettingsDialog(tk.Toplevel):
main_frame, text=LocaleStrings.SET["animation_settings"], padding=10)
pillow_frame.pack(fill="x", pady=5)
self.use_pillow_animation_checkbutton = ttk.Checkbutton(pillow_frame, text=f"{LocaleStrings.SET['use_pillow_check']} ({LocaleStrings.SET['pillow']})",
variable=self.use_pillow_animation)
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)
@@ -132,7 +132,7 @@ class SettingsDialog(tk.Toplevel):
main_frame, text=LocaleStrings.SET["animation_type"], padding=10)
anim_type_frame.pack(fill="x", pady=5)
ttk.Radiobutton(anim_type_frame, text=LocaleStrings.SET["counter_arc"], variable=self.animation_type,
value="counter_arc").pack(side="left", padx=5)
value="counter_arc").pack(side="left", padx=5)
ttk.Radiobutton(anim_type_frame, text=LocaleStrings.SET["double_arc"], variable=self.animation_type,
value="double_arc").pack(side="left", padx=5)
ttk.Radiobutton(anim_type_frame, text=LocaleStrings.SET["line"], variable=self.animation_type,
@@ -190,5 +190,6 @@ 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", True) and PIL_AVAILABLE)
self.use_pillow_animation.set(defaults.get(
"use_pillow_animation", True) and PIL_AVAILABLE)
self.animation_type.set(defaults.get("animation_type", "counter_arc"))

View File

@@ -10,8 +10,8 @@ if TYPE_CHECKING:
from custom_file_dialog import CustomFileDialog
from shared_libs.common_tools import Tooltip
from animated_icon import AnimatedIcon
from cfd_app_config import LocaleStrings, _
from shared_libs.animated_icon import AnimatedIcon
from .cfd_app_config import LocaleStrings
def get_xdg_user_dir(dir_key: str, fallback_name: str) -> str:
@@ -50,6 +50,7 @@ def get_xdg_user_dir(dir_key: str, fallback_name: str) -> str:
class StyleManager:
"""Manages the visual styling of the application using ttk styles."""
def __init__(self, dialog: 'CustomFileDialog') -> None:
"""
Initializes the StyleManager.
@@ -140,6 +141,7 @@ class StyleManager:
class WidgetManager:
"""Manages the creation, layout, and management of all widgets in the dialog."""
def __init__(self, dialog: 'CustomFileDialog', settings: Dict[str, Any]) -> None:
"""
Initializes the WidgetManager.
@@ -155,7 +157,8 @@ class WidgetManager:
def _setup_top_bar(self, parent_frame: ttk.Frame) -> None:
"""Sets up the top bar with navigation and control buttons."""
top_bar = ttk.Frame(parent_frame, style='Accent.TFrame', padding=(0, 5, 0, 5))
top_bar = ttk.Frame(
parent_frame, style='Accent.TFrame', padding=(0, 5, 0, 5))
top_bar.grid(row=0, column=0, columnspan=2, sticky="ew")
top_bar.grid_columnconfigure(1, weight=1)
@@ -188,20 +191,22 @@ class WidgetManager:
path_search_container = ttk.Frame(top_bar, style='Accent.TFrame')
path_search_container.grid(row=0, column=1, sticky="ew")
self.path_entry = ttk.Entry(path_search_container)
self.path_entry.bind("<Return>", lambda e: self.dialog.navigation_manager.navigate_to(self.path_entry.get()))
self.path_entry.bind(
"<Return>", lambda e: self.dialog.navigation_manager.navigate_to(self.path_entry.get()))
search_icon_pos = self.settings.get("search_icon_pos", "left")
if search_icon_pos == 'left':
path_search_container.grid_columnconfigure(1, weight=1)
self.path_entry.grid(row=0, column=1, sticky="ew")
else: # right
else: # right
path_search_container.grid_columnconfigure(0, weight=1)
self.path_entry.grid(row=0, column=0, sticky="ew")
# Right controls
right_controls_container = ttk.Frame(top_bar, style='Accent.TFrame')
right_controls_container.grid(row=0, column=2, sticky="e")
self.responsive_buttons_container = ttk.Frame(right_controls_container, style='Accent.TFrame')
self.responsive_buttons_container = ttk.Frame(
right_controls_container, style='Accent.TFrame')
self.responsive_buttons_container.pack(side="left")
self.new_folder_button = ttk.Button(self.responsive_buttons_container, image=self.dialog.icon_manager.get_icon(
@@ -218,7 +223,8 @@ class WidgetManager:
self.new_folder_button.config(state=tk.DISABLED)
self.new_file_button.config(state=tk.DISABLED)
self.view_switch = ttk.Frame(self.responsive_buttons_container, padding=(5, 0), style='Accent.TFrame')
self.view_switch = ttk.Frame(
self.responsive_buttons_container, padding=(5, 0), style='Accent.TFrame')
self.view_switch.pack(side="left")
self.icon_view_button = ttk.Button(self.view_switch, image=self.dialog.icon_manager.get_icon(
'icon_view'), command=self.dialog.view_manager.set_icon_view, style="Header.TButton.Active.Round")
@@ -233,14 +239,16 @@ class WidgetManager:
self.hidden_files_button = ttk.Button(self.responsive_buttons_container, image=self.dialog.icon_manager.get_icon(
'hide'), command=self.dialog.view_manager.toggle_hidden_files, style="Header.TButton.Borderless.Round")
self.hidden_files_button.pack(side="left", padx=10)
Tooltip(self.hidden_files_button, LocaleStrings.UI["show_hidden_files"])
Tooltip(self.hidden_files_button,
LocaleStrings.UI["show_hidden_files"])
self.more_button = ttk.Button(right_controls_container, text="...",
command=self.dialog.show_more_menu, style="Header.TButton.Borderless.Round", width=3)
def _setup_sidebar(self, parent_paned_window: ttk.PanedWindow) -> None:
"""Sets up the sidebar with bookmarks and devices."""
sidebar_frame = ttk.Frame(parent_paned_window, style="Sidebar.TFrame", padding=(0, 0, 0, 0), width=200)
sidebar_frame = ttk.Frame(
parent_paned_window, style="Sidebar.TFrame", padding=(0, 0, 0, 0), width=200)
sidebar_frame.grid_propagate(False)
sidebar_frame.bind("<Configure>", self.dialog.on_sidebar_resize)
parent_paned_window.add(sidebar_frame, weight=0)
@@ -249,25 +257,34 @@ class WidgetManager:
self._setup_sidebar_bookmarks(sidebar_frame)
separator_color = "#a9a9a9" if self.style_manager.is_dark else "#7c7c7c"
tk.Frame(sidebar_frame, height=1, bg=separator_color).grid(row=1, column=0, sticky="ew", padx=20, pady=15)
tk.Frame(sidebar_frame, height=1, bg=separator_color).grid(
row=1, column=0, sticky="ew", padx=20, pady=15)
self._setup_sidebar_devices(sidebar_frame)
tk.Frame(sidebar_frame, height=1, bg=separator_color).grid(row=3, column=0, sticky="ew", padx=20, pady=15)
tk.Frame(sidebar_frame, height=1, bg=separator_color).grid(
row=3, column=0, sticky="ew", padx=20, pady=15)
self._setup_sidebar_storage(sidebar_frame)
def _setup_sidebar_bookmarks(self, sidebar_frame: ttk.Frame) -> None:
"""Sets up the bookmark buttons in the sidebar."""
sidebar_buttons_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame", padding=(0, 15, 0, 0))
sidebar_buttons_frame = ttk.Frame(
sidebar_frame, style="Sidebar.TFrame", padding=(0, 15, 0, 0))
sidebar_buttons_frame.grid(row=0, column=0, sticky="nsew")
sidebar_buttons_config = [
{'name': LocaleStrings.NAV["computer"], 'icon': self.dialog.icon_manager.get_icon('computer_small'), 'path': '/'},
{'name': LocaleStrings.NAV["downloads"], 'icon': self.dialog.icon_manager.get_icon('downloads_small'), 'path': get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads")},
{'name': LocaleStrings.NAV["documents"], 'icon': self.dialog.icon_manager.get_icon('documents_small'), 'path': get_xdg_user_dir("XDG_DOCUMENTS_DIR", "Documents")},
{'name': LocaleStrings.NAV["pictures"], 'icon': self.dialog.icon_manager.get_icon('pictures_small'), 'path': get_xdg_user_dir("XDG_PICTURES_DIR", "Pictures")},
{'name': LocaleStrings.NAV["music"], 'icon': self.dialog.icon_manager.get_icon('music_small'), 'path': get_xdg_user_dir("XDG_MUSIC_DIR", "Music")},
{'name': LocaleStrings.NAV["videos"], 'icon': self.dialog.icon_manager.get_icon('video_small'), 'path': get_xdg_user_dir("XDG_VIDEO_DIR", "Videos")},
{'name': LocaleStrings.NAV["computer"], 'icon': self.dialog.icon_manager.get_icon(
'computer_small'), 'path': '/'},
{'name': LocaleStrings.NAV["downloads"], 'icon': self.dialog.icon_manager.get_icon(
'downloads_small'), 'path': get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads")},
{'name': LocaleStrings.NAV["documents"], 'icon': self.dialog.icon_manager.get_icon(
'documents_small'), 'path': get_xdg_user_dir("XDG_DOCUMENTS_DIR", "Documents")},
{'name': LocaleStrings.NAV["pictures"], 'icon': self.dialog.icon_manager.get_icon(
'pictures_small'), 'path': get_xdg_user_dir("XDG_PICTURES_DIR", "Pictures")},
{'name': LocaleStrings.NAV["music"], 'icon': self.dialog.icon_manager.get_icon(
'music_small'), 'path': get_xdg_user_dir("XDG_MUSIC_DIR", "Music")},
{'name': LocaleStrings.NAV["videos"], 'icon': self.dialog.icon_manager.get_icon(
'video_small'), 'path': get_xdg_user_dir("XDG_VIDEO_DIR", "Videos")},
]
self.sidebar_buttons = []
for config in sidebar_buttons_config:
@@ -278,7 +295,8 @@ class WidgetManager:
def _setup_sidebar_devices(self, sidebar_frame: ttk.Frame) -> None:
"""Sets up the mounted devices section in the sidebar."""
mounted_devices_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame")
mounted_devices_frame = ttk.Frame(
sidebar_frame, style="Sidebar.TFrame")
mounted_devices_frame.grid(row=2, column=0, sticky="nsew", padx=10)
mounted_devices_frame.grid_columnconfigure(0, weight=1)
@@ -369,70 +387,93 @@ class WidgetManager:
"""Sets up the storage indicator in the sidebar."""
storage_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame")
storage_frame.grid(row=5, column=0, sticky="sew", padx=10, pady=10)
self.storage_label = ttk.Label(storage_frame, text=f"{LocaleStrings.CFD["free_space"]}:", background=self.style_manager.freespace_background)
self.storage_label = ttk.Label(
storage_frame, text=f"{LocaleStrings.CFD["free_space"]}:", background=self.style_manager.freespace_background)
self.storage_label.pack(fill="x", padx=10)
self.storage_bar = ttk.Progressbar(storage_frame, orient="horizontal", length=100, mode="determinate")
self.storage_bar = ttk.Progressbar(
storage_frame, orient="horizontal", length=100, mode="determinate")
self.storage_bar.pack(fill="x", pady=(2, 5), padx=15)
def _setup_bottom_bar(self) -> None:
"""Sets up the bottom bar including filename entry, action buttons, and status info."""
self.action_status_frame = ttk.Frame(self.content_frame, style="AccentBottom.TFrame")
self.action_status_frame.grid(row=1, column=0, sticky="ew", pady=(5, 5), padx=10)
self.status_container = ttk.Frame(self.action_status_frame, style="AccentBottom.TFrame")
self.left_container = ttk.Frame(self.action_status_frame, style="AccentBottom.TFrame")
self.center_container = ttk.Frame(self.action_status_frame, style="AccentBottom.TFrame")
self.right_container = ttk.Frame(self.action_status_frame, style="AccentBottom.TFrame")
self.action_status_frame = ttk.Frame(
self.content_frame, style="AccentBottom.TFrame")
self.action_status_frame.grid(
row=1, column=0, sticky="ew", pady=(5, 5), padx=10)
self.status_container = ttk.Frame(
self.action_status_frame, style="AccentBottom.TFrame")
self.left_container = ttk.Frame(
self.action_status_frame, style="AccentBottom.TFrame")
self.center_container = ttk.Frame(
self.action_status_frame, style="AccentBottom.TFrame")
self.right_container = ttk.Frame(
self.action_status_frame, style="AccentBottom.TFrame")
self.action_status_frame.grid_columnconfigure(1, weight=1)
self.action_status_frame.grid_rowconfigure(0, weight=1)
self.left_container.grid(row=0, column=0, sticky='nsw', pady=(5, 0))
self.center_container.grid(row=0, column=1, sticky='nsew', padx=5, pady=(5, 0))
self.center_container.grid(
row=0, column=1, sticky='nsew', padx=5, pady=(5, 0))
self.right_container.grid(row=0, column=2, sticky='nse', pady=(5, 0))
self.separator_color = "#a9a9a9" if self.style_manager.is_dark else "#7c7c7c"
self.separator = tk.Frame(self.action_status_frame, height=1, bg=self.separator_color)
self.separator.grid(row=1, column=0, columnspan=3, sticky="ew", pady=(4, 0))
self.separator = tk.Frame(
self.action_status_frame, height=1, bg=self.separator_color)
self.separator.grid(row=1, column=0, columnspan=3,
sticky="ew", pady=(4, 0))
self.status_container.grid(row=2, column=0, columnspan=3, sticky='ew')
# --- Define Widgets ---
self.search_status_label = ttk.Label(self.status_container, text="", style="AccentBottom.TLabel")
self.search_status_label = ttk.Label(
self.status_container, text="", style="AccentBottom.TLabel")
self.filename_entry = ttk.Entry(self.center_container)
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 = AnimatedIcon(self.status_container,
width=23, height=23, bg=self.style_manager.bottom_color,
animation_type=self.settings.get('animation_type', 'counter_arc'))
self.search_animation.grid(row=0, column=0, sticky='w', padx=(0, 5), pady=(4,0))
self.search_animation.bind("<Button-1>", lambda e: self.dialog.search_manager.activate_search())
width=23, height=23, bg=self.style_manager.bottom_color,
animation_type=self.settings.get('animation_type', 'counter_arc'))
self.search_animation.grid(
row=0, column=0, sticky='w', padx=(0, 5), pady=(4, 0))
self.search_animation.bind(
"<Button-1>", lambda e: self.dialog.search_manager.activate_search())
self.search_status_label.grid(row=0, column=1, sticky="w")
button_box_pos = self.settings.get("button_box_pos", "left")
if self.dialog.dialog_mode == "save":
self.trash_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon('trash_small2'),
command=self.dialog.file_op_manager.delete_selected_item, style="Bottom.TButton.Borderless.Round")
Tooltip(self.trash_button, LocaleStrings.UI["delete_move"])
self.save_button = ttk.Button(self.action_status_frame, text=LocaleStrings.SET["save_button"], command=self.dialog.on_save)
self.cancel_button = ttk.Button(self.action_status_frame, text=LocaleStrings.SET["cancel_button"], command=self.dialog.on_cancel)
self.filter_combobox = ttk.Combobox(self.center_container, values=[ft[0] for ft in self.dialog.filetypes], state="readonly")
self.filter_combobox.bind("<<ComboboxSelected>>", self.dialog.view_manager.on_filter_change)
self.save_button = ttk.Button(
self.action_status_frame, text=LocaleStrings.SET["save_button"], command=self.dialog.on_save)
self.cancel_button = ttk.Button(
self.action_status_frame, text=LocaleStrings.SET["cancel_button"], command=self.dialog.on_cancel)
self.filter_combobox = ttk.Combobox(self.center_container, values=[
ft[0] for ft in self.dialog.filetypes], state="readonly")
self.filter_combobox.bind(
"<<ComboboxSelected>>", self.dialog.view_manager.on_filter_change)
self.filter_combobox.set(self.dialog.filetypes[0][0])
self.center_container.grid_rowconfigure(0, weight=1)
self.filename_entry.grid(row=0, column=0, columnspan=2, sticky="ew")
self.filename_entry.grid(
row=0, column=0, columnspan=2, sticky="ew")
self._layout_bottom_buttons(button_box_pos)
else: # Open mode
self.open_button = ttk.Button(self.action_status_frame, text=LocaleStrings.CFD["open"], command=self.dialog.on_open)
self.cancel_button = ttk.Button(self.action_status_frame, text=LocaleStrings.CFD["cancel"], command=self.dialog.on_cancel)
self.filter_combobox = ttk.Combobox(self.center_container, values=[ft[0] for ft in self.dialog.filetypes], state="readonly")
self.filter_combobox.bind("<<ComboboxSelected>>", self.dialog.view_manager.on_filter_change)
self.open_button = ttk.Button(
self.action_status_frame, text=LocaleStrings.CFD["open"], command=self.dialog.on_open)
self.cancel_button = ttk.Button(
self.action_status_frame, text=LocaleStrings.CFD["cancel"], command=self.dialog.on_cancel)
self.filter_combobox = ttk.Combobox(self.center_container, values=[
ft[0] for ft in self.dialog.filetypes], state="readonly")
self.filter_combobox.bind(
"<<ComboboxSelected>>", self.dialog.view_manager.on_filter_change)
self.filter_combobox.set(self.dialog.filetypes[0][0])
self.center_container.grid_rowconfigure(0, weight=1)
self.filename_entry.grid(row=0, column=0, columnspan=2, sticky="ew")
self.filename_entry.grid(
row=0, column=0, columnspan=2, sticky="ew")
self._layout_bottom_buttons(button_box_pos)
@@ -441,7 +482,7 @@ class WidgetManager:
# Configure container weights
self.left_container.grid_rowconfigure(0, weight=1)
self.right_container.grid_rowconfigure(0, weight=1)
# Determine action button and its container
action_button = self.save_button if self.dialog.dialog_mode == "save" else self.open_button
action_container = self.left_container if button_box_pos == 'left' else self.right_container
@@ -453,22 +494,27 @@ class WidgetManager:
# Place settings and trash buttons
if button_box_pos == 'left':
self.settings_button.grid(in_=other_container, row=0, column=0, sticky="ne")
self.settings_button.grid(
in_=other_container, row=0, column=0, sticky="ne")
if self.dialog.dialog_mode == "save":
self.trash_button.grid(in_=other_container, row=1, column=0, sticky="se", padx=(5, 0))
else: # right
self.settings_button.grid(in_=action_container, row=0, column=1, sticky="ne", padx=(5, 0))
self.trash_button.grid(
in_=other_container, row=1, column=0, sticky="se", padx=(5, 0))
else: # right
self.settings_button.grid(
in_=action_container, row=0, column=1, sticky="ne", padx=(5, 0))
if self.dialog.dialog_mode == "save":
self.trash_button.grid(in_=other_container, row=0, column=0, sticky="sw")
self.trash_button.grid(
in_=other_container, row=0, column=0, sticky="sw")
# Layout for the center container (filename, filter, status)
if button_box_pos == 'left':
self.center_container.grid_columnconfigure(0, weight=1)
self.filter_combobox.grid(in_=self.center_container, row=1, column=0, sticky="w", pady=(5, 0))
else: # right
self.filter_combobox.grid(
in_=self.center_container, row=1, column=0, sticky="w", pady=(5, 0))
else: # right
self.center_container.grid_columnconfigure(1, weight=1)
self.filter_combobox.grid(in_=self.center_container, row=1, column=1, sticky="e", pady=(5, 0))
self.filter_combobox.grid(
in_=self.center_container, row=1, column=1, sticky="e", pady=(5, 0))
def setup_widgets(self) -> None:
"""Creates and arranges all widgets in the main dialog window."""
@@ -492,15 +538,16 @@ class WidgetManager:
self._setup_sidebar(paned_window)
self.content_frame = ttk.Frame(paned_window, padding=(0, 0, 0, 0), style="AccentBottom.TFrame")
self.content_frame = ttk.Frame(paned_window, padding=(
0, 0, 0, 0), style="AccentBottom.TFrame")
paned_window.add(self.content_frame, weight=1)
self.content_frame.grid_rowconfigure(0, weight=1)
self.content_frame.grid_columnconfigure(0, weight=1)
self.file_list_frame = ttk.Frame(self.content_frame, style="Content.TFrame")
self.file_list_frame = ttk.Frame(
self.content_frame, style="Content.TFrame")
self.file_list_frame.grid(row=0, column=0, sticky="nsew")
self.dialog.bind("<Configure>", self.dialog.on_window_resize)
# --- Bottom Bar ---
self._setup_bottom_bar()

View File

@@ -10,10 +10,12 @@ if TYPE_CHECKING:
from custom_file_dialog import CustomFileDialog
from shared_libs.common_tools import Tooltip
from cfd_app_config import AppConfig, LocaleStrings, _
from .cfd_app_config import CfdConfigManager, LocaleStrings
class ViewManager:
"""Manages the display of files and folders in list and icon views."""
def __init__(self, dialog: 'CustomFileDialog'):
"""
Initializes the ViewManager.
@@ -41,7 +43,8 @@ class ViewManager:
for widget in self.dialog.widget_manager.file_list_frame.winfo_children():
widget.destroy()
self.dialog.widget_manager.path_entry.delete(0, tk.END)
self.dialog.widget_manager.path_entry.insert(0, self.dialog.current_dir)
self.dialog.widget_manager.path_entry.insert(
0, self.dialog.current_dir)
self.dialog.selected_file = None
self.dialog.update_status_bar()
if self.dialog.view_mode.get() == "list":
@@ -60,9 +63,9 @@ class ViewManager:
items = os.listdir(self.dialog.current_dir)
num_items = len(items)
warning_message = None
if num_items > AppConfig.MAX_ITEMS_TO_DISPLAY:
warning_message = f"{LocaleStrings.CFD['showing']} {AppConfig.MAX_ITEMS_TO_DISPLAY} {LocaleStrings.CFD['of']} {num_items} {LocaleStrings.CFD['entries']}."
items = items[:AppConfig.MAX_ITEMS_TO_DISPLAY]
if num_items > CfdConfigManager.MAX_ITEMS_TO_DISPLAY:
warning_message = f"{LocaleStrings.CFD['showing']} {CfdConfigManager.MAX_ITEMS_TO_DISPLAY} {LocaleStrings.CFD['of']} {num_items} {LocaleStrings.CFD['entries']}."
items = items[:CfdConfigManager.MAX_ITEMS_TO_DISPLAY]
dirs = sorted([d for d in items if os.path.isdir(
os.path.join(self.dialog.current_dir, d))], key=str.lower)
files = sorted([f for f in items if not os.path.isdir(
@@ -138,7 +141,8 @@ class ViewManager:
item_frame = event.widget
while not hasattr(item_frame, 'item_path'):
item_frame = item_frame.master
self.dialog.file_op_manager.on_rename_request(event, item_path, item_frame)
self.dialog.file_op_manager.on_rename_request(
event, item_path, item_frame)
def populate_icon_view(self, item_to_rename: Optional[str] = None, item_to_select: Optional[str] = None) -> None:
"""
@@ -152,13 +156,14 @@ class ViewManager:
self.dialog.currently_loaded_count = 0
self.dialog.icon_canvas = tk.Canvas(self.dialog.widget_manager.file_list_frame,
highlightthickness=0, bg=self.dialog.style_manager.icon_bg_color)
highlightthickness=0, bg=self.dialog.style_manager.icon_bg_color)
v_scrollbar = ttk.Scrollbar(
self.dialog.widget_manager.file_list_frame, orient="vertical", command=self.dialog.icon_canvas.yview)
self.dialog.icon_canvas.pack(side="left", fill="both", expand=True)
self.dialog.icon_canvas.focus_set()
v_scrollbar.pack(side="right", fill="y")
container_frame = ttk.Frame(self.dialog.icon_canvas, style="Content.TFrame")
container_frame = ttk.Frame(
self.dialog.icon_canvas, style="Content.TFrame")
self.dialog.icon_canvas.create_window(
(0, 0), window=container_frame, anchor="nw")
container_frame.bind("<Configure>", lambda e: self.dialog.icon_canvas.configure(
@@ -173,7 +178,8 @@ class ViewManager:
delta = -1 * int(event.delta / 120)
self.dialog.icon_canvas.yview_scroll(delta, "units")
if self.dialog.currently_loaded_count < len(self.dialog.all_items) and self.dialog.icon_canvas.yview()[1] > 0.9:
self._load_more_items_icon_view(container_frame, _on_mouse_wheel)
self._load_more_items_icon_view(
container_frame, _on_mouse_wheel)
for widget in [self.dialog.icon_canvas, container_frame]:
widget.bind("<MouseWheel>", _on_mouse_wheel)
@@ -190,7 +196,7 @@ class ViewManager:
while self.dialog.currently_loaded_count < len(self.dialog.all_items):
widget_to_focus = self._load_more_items_icon_view(
container_frame, _on_mouse_wheel, item_to_rename, item_to_select)
if widget_to_focus:
break
@@ -274,13 +280,13 @@ class ViewManager:
Tooltip(item_frame, name)
for widget in [item_frame, icon_label, name_label]:
widget.bind("<Double-Button-1>", lambda e,
widget.bind("<Double-Button-1>", lambda e,
p=path: self.on_item_double_click(p))
widget.bind("<Button-1>", lambda e, p=path,
widget.bind("<Button-1>", lambda e, p=path,
f=item_frame: self.on_item_select(p, f))
widget.bind("<ButtonRelease-3>", lambda e,
widget.bind("<ButtonRelease-3>", lambda e,
p=path: self.dialog.file_op_manager._show_context_menu(e, p))
widget.bind("<F2>", lambda e, p=path,
widget.bind("<F2>", lambda e, p=path,
f=item_frame: self.dialog.file_op_manager.on_rename_request(e, p, f))
widget.bind("<MouseWheel>", scroll_handler)
widget.bind("<Button-4>", scroll_handler)
@@ -320,21 +326,26 @@ class ViewManager:
self.dialog.tree = ttk.Treeview(
tree_frame, columns=columns, show="tree headings")
self.dialog.tree.heading("#0", text=LocaleStrings.VIEW["name"], anchor="w")
self.dialog.tree.heading(
"#0", text=LocaleStrings.VIEW["name"], anchor="w")
self.dialog.tree.column("#0", anchor="w", width=250, stretch=True)
self.dialog.tree.heading("size", text=LocaleStrings.VIEW["size"], anchor="e")
self.dialog.tree.heading(
"size", text=LocaleStrings.VIEW["size"], anchor="e")
self.dialog.tree.column("size", anchor="e", width=120, stretch=False)
self.dialog.tree.heading("type", text=LocaleStrings.VIEW["type"], anchor="w")
self.dialog.tree.heading(
"type", text=LocaleStrings.VIEW["type"], anchor="w")
self.dialog.tree.column("type", anchor="w", width=120, stretch=False)
self.dialog.tree.heading("modified", text=LocaleStrings.VIEW["date_modified"], anchor="w")
self.dialog.tree.column("modified", anchor="w", width=160, stretch=False)
self.dialog.tree.heading(
"modified", text=LocaleStrings.VIEW["date_modified"], anchor="w")
self.dialog.tree.column("modified", anchor="w",
width=160, stretch=False)
v_scrollbar = ttk.Scrollbar(
tree_frame, orient="vertical", command=self.dialog.tree.yview)
h_scrollbar = ttk.Scrollbar(
tree_frame, orient="horizontal", command=self.dialog.tree.xview)
self.dialog.tree.configure(yscrollcommand=v_scrollbar.set,
xscrollcommand=h_scrollbar.set)
xscrollcommand=h_scrollbar.set)
self.dialog.tree.grid(row=0, column=0, sticky='nsew')
self.dialog.tree.focus_set()
@@ -349,7 +360,8 @@ class ViewManager:
self.dialog.tree.bind("<Double-1>", self.on_list_double_click)
self.dialog.tree.bind("<<TreeviewSelect>>", self.on_list_select)
self.dialog.tree.bind("<F2>", self.dialog.file_op_manager.on_rename_request)
self.dialog.tree.bind(
"<F2>", self.dialog.file_op_manager.on_rename_request)
self.dialog.tree.bind("<ButtonRelease-3>", self.on_list_context_menu)
if warning:
@@ -359,7 +371,8 @@ class ViewManager:
return
while self.dialog.currently_loaded_count < len(self.dialog.all_items):
item_found = self._load_more_items_list_view(item_to_rename, item_to_select)
item_found = self._load_more_items_list_view(
item_to_rename, item_to_select)
if item_found:
break
if not (item_to_rename or item_to_select):
@@ -444,7 +457,8 @@ class ViewManager:
self.dialog.search_manager.show_search_ready()
if not os.path.isdir(path):
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
self.dialog.widget_manager.filename_entry.insert(0, os.path.basename(path))
self.dialog.widget_manager.filename_entry.insert(
0, os.path.basename(path))
def on_list_select(self, event: tk.Event) -> None:
"""Handles the selection of an item in the list view."""
@@ -523,7 +537,7 @@ class ViewManager:
elif self.dialog.view_mode.get() == "icons":
if not hasattr(self.dialog, 'icon_canvas') or not self.dialog.icon_canvas.winfo_exists():
return
container_frame = self.dialog.icon_canvas.winfo_children()[0]
target_path = os.path.join(self.dialog.current_dir, filename)
@@ -533,12 +547,14 @@ class ViewManager:
def scroll_to_widget() -> None:
self.dialog.update_idletasks()
if not widget.winfo_exists(): return
if not widget.winfo_exists():
return
y = widget.winfo_y()
canvas_height = self.dialog.icon_canvas.winfo_height()
scroll_region = self.dialog.icon_canvas.bbox("all")
if not scroll_region: return
if not scroll_region:
return
scroll_height = scroll_region[3]
if scroll_height > canvas_height:
fraction = y / scroll_height
@@ -574,7 +590,8 @@ class ViewManager:
def toggle_hidden_files(self) -> None:
"""Toggles the visibility of hidden files and refreshes the view."""
self.dialog.show_hidden_files.set(not self.dialog.show_hidden_files.get())
self.dialog.show_hidden_files.set(
not self.dialog.show_hidden_files.get())
if self.dialog.show_hidden_files.get():
self.dialog.widget_manager.hidden_files_button.config(
image=self.dialog.icon_manager.get_icon('unhide'))

View File

@@ -1,29 +1,27 @@
import os
import shutil
import tkinter as tk
from tkinter import ttk
from datetime import datetime
import subprocess
import json
import threading
from typing import Optional, List, Tuple, Any, Dict
from typing import Optional, List, Tuple, Dict
from shared_libs.common_tools import IconManager, Tooltip, LxTools
from .cfd_app_config import CfdConfigManager, LocaleStrings
from .cfd_ui_setup import StyleManager, WidgetManager
from shared_libs.animated_icon import AnimatedIcon
from .cfd_settings_dialog import SettingsDialog
from .cfd_file_operations import FileOperationsManager
from .cfd_search_manager import SearchManager
from .cfd_navigation_manager import NavigationManager
from .cfd_view_manager import ViewManager
from shared_libs.message import MessageDialog
from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTools
from cfd_app_config import AppConfig, CfdConfigManager, LocaleStrings, _
from cfd_ui_setup import StyleManager, WidgetManager, get_xdg_user_dir
from animated_icon import AnimatedIcon, PIL_AVAILABLE
from cfd_settings_dialog import SettingsDialog
from cfd_file_operations import FileOperationsManager
from cfd_search_manager import SearchManager
from cfd_navigation_manager import NavigationManager
from cfd_view_manager import ViewManager
class CustomFileDialog(tk.Toplevel):
"""
A custom file dialog window that provides functionalities for file selection,
directory navigation, search, and file operations.
"""
def __init__(self, parent: tk.Widget, initial_dir: Optional[str] = None,
filetypes: Optional[List[Tuple[str, str]]] = None,
dialog_mode: str = "open", title: str = LocaleStrings.CFD["title"]):
@@ -61,7 +59,8 @@ class CustomFileDialog(tk.Toplevel):
self.selected_file: Optional[str] = None
self.current_dir: str = os.path.abspath(
initial_dir) if initial_dir else os.path.expanduser("~")
self.filetypes: List[Tuple[str, str]] = filetypes if filetypes else [(LocaleStrings.CFD["all_files"], "*.* ")]
self.filetypes: List[Tuple[str, str]] = filetypes if filetypes else [
(LocaleStrings.CFD["all_files"], "*.* ")]
self.current_filter_pattern: str = self.filetypes[0][1]
self.history: List[str] = []
self.history_pos: int = -1
@@ -83,14 +82,16 @@ class CustomFileDialog(tk.Toplevel):
self.icon_manager: IconManager = IconManager()
self.style_manager: StyleManager = StyleManager(self)
self.file_op_manager: FileOperationsManager = FileOperationsManager(self)
self.file_op_manager: FileOperationsManager = FileOperationsManager(
self)
self.search_manager: SearchManager = SearchManager(self)
self.navigation_manager: NavigationManager = NavigationManager(self)
self.view_manager: ViewManager = ViewManager(self)
self.widget_manager: WidgetManager = WidgetManager(self, self.settings)
self.widget_manager.filename_entry.bind("<Return>", self.search_manager.execute_search)
self.widget_manager.filename_entry.bind(
"<Return>", self.search_manager.execute_search)
self.update_animation_settings()
@@ -107,7 +108,7 @@ class CustomFileDialog(tk.Toplevel):
self.widget_manager.path_entry.bind(
"<Return>", self.navigation_manager.handle_path_entry_return)
self.bind("<Key>", self.search_manager.show_search_bar)
if self.dialog_mode == "save":
@@ -155,7 +156,8 @@ class CustomFileDialog(tk.Toplevel):
self.navigation_manager = NavigationManager(self)
self.view_manager = ViewManager(self)
self.widget_manager = WidgetManager(self, self.settings)
self.widget_manager.filename_entry.bind("<Return>", self.search_manager.execute_search)
self.widget_manager.filename_entry.bind(
"<Return>", self.search_manager.execute_search)
self.view_manager._update_view_mode_buttons()
self.responsive_buttons_hidden = None
@@ -163,7 +165,7 @@ class CustomFileDialog(tk.Toplevel):
self._handle_responsive_buttons(self.winfo_width())
self.update_animation_settings()
if self.search_mode:
self.search_manager.show_search_results_treeview()
else:
@@ -180,11 +182,11 @@ class CustomFileDialog(tk.Toplevel):
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,
width=23,
height=23,
use_pillow=use_pillow,
animation_type=anim_type,
@@ -192,10 +194,14 @@ class CustomFileDialog(tk.Toplevel):
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("<Button-1>", lambda e: self.search_manager.activate_search())
self.widget_manager.search_animation.bind("<Enter>", self._show_tooltip)
self.widget_manager.search_animation.bind("<Leave>", self._hide_tooltip)
self.widget_manager.search_animation.grid(
row=0, column=0, sticky='w', padx=(0, 5), pady=(4, 0))
self.widget_manager.search_animation.bind(
"<Button-1>", lambda e: self.search_manager.activate_search())
self.widget_manager.search_animation.bind(
"<Enter>", self._show_tooltip)
self.widget_manager.search_animation.bind(
"<Leave>", self._hide_tooltip)
if is_running:
self.widget_manager.search_animation.start()
@@ -295,7 +301,8 @@ class CustomFileDialog(tk.Toplevel):
image=self.icon_manager.get_icon('list_view'), compound='left')
more_menu.add_separator()
hidden_files_label = LocaleStrings.UI["hide_hidden_files"] if self.show_hidden_files.get() else LocaleStrings.UI["show_hidden_files"]
hidden_files_label = LocaleStrings.UI["hide_hidden_files"] if self.show_hidden_files.get(
) else LocaleStrings.UI["show_hidden_files"]
hidden_files_icon = self.icon_manager.get_icon(
'unhide') if self.show_hidden_files.get() else self.icon_manager.get_icon('hide')
more_menu.add_command(label=hidden_files_label, command=self.view_manager.toggle_hidden_files,
@@ -446,10 +453,10 @@ class CustomFileDialog(tk.Toplevel):
"""
if self.current_filter_pattern == "*.*":
return True
patterns = self.current_filter_pattern.lower().split()
fn_lower = filename.lower()
for p in patterns:
if p.startswith('*.'):
if fn_lower.endswith(p[1:]):
@@ -566,7 +573,7 @@ class CustomFileDialog(tk.Toplevel):
"""
if hasattr(self, 'tooltip_window') and self.tooltip_window.winfo_exists():
return
tooltip_text = LocaleStrings.UI["start_search"] if not self.widget_manager.search_animation.running else LocaleStrings.UI["cancel_search"]
x = self.widget_manager.search_animation.winfo_rootx() + 25
@@ -574,7 +581,8 @@ class CustomFileDialog(tk.Toplevel):
self.tooltip_window = tk.Toplevel(self)
self.tooltip_window.wm_overrideredirect(True)
self.tooltip_window.wm_geometry(f"+{x}+{y}")
label = tk.Label(self.tooltip_window, text=tooltip_text, relief="solid", borderwidth=1)
label = tk.Label(self.tooltip_window, text=tooltip_text,
relief="solid", borderwidth=1)
label.pack()
def _hide_tooltip(self, event: tk.Event) -> None:

View File

@@ -1,22 +0,0 @@
#!/usr/bin/python3
"""Utility functions for setting up the application."""
from logview_app_config import AppConfig
from pathlib import Path
# Logging
LOG_DIR = Path.home() / ".local/share/lxlogs"
Path(LOG_DIR).mkdir(parents=True, exist_ok=True)
LOG_FILE_PATH = LOG_DIR / "logviewer.log"
def prepare_app_environment() -> None:
"""Ensures that all required files and directories exist."""
AppConfig.ensure_directories()
AppConfig.create_default_settings()
AppConfig.ensure_log()
if __name__ == "__main__":
prepare_app_environment()

Binary file not shown.

View File

@@ -1,163 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR polunga40@unity-mail.de, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-29 17:25+0200\n"
"PO-Revision-Date: 2025-06-29 18:00+0200\n"
"Last-Translator: Désiré Werner Menrath <polunga40@unity-mail.de>\n"
"Language-Team: \n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.4.2\n"
#: gitea.py:127
msgid "Download Successful"
msgstr "Herunterladen erfolgreich"
#: gitea.py:128
msgid "Your zip file is in home directory"
msgstr "Ihre ZIP-Datei befindet sich im Home-Verzeichnis"
#: gitea.py:129
msgid "Download error"
msgstr "Fehler beim Herunterladen"
#: gitea.py:130
msgid "Download failed! Please try again"
msgstr "Herunterladen fehlgeschlagen! Bitte versuchen Sie es erneut."
#: gitea.py:131
msgid "Download failed! No internet connection!"
msgstr "Herunterladen fehlgeschlagen! Keine Internetverbindung!"
#: logviewer.py:102
msgid "Load Log"
msgstr "Logdatei laden"
#: logviewer.py:107
msgid "Options"
msgstr "Optionen"
#: logviewer.py:116
msgid "Disable Updates"
msgstr "Updates deaktivieren"
#: logviewer.py:149
msgid "About"
msgstr "Über"
#: logviewer.py:184
msgid "Update search off"
msgstr "Suche nach Updates ausgeschaltet"
#: logviewer.py:185
msgid "Updates you have disabled"
msgstr "Sie haben Updates deaktiviert"
#: logviewer.py:192
msgid "No Server Connection!"
msgstr "Keine Verbindung zum Server!"
#: logviewer.py:197
msgid "Could not connect to update server"
msgstr "Verbindung zum Update-Server nicht möglich"
#: logviewer.py:202
msgid "No Updates"
msgstr "Keine Updates verfügbar"
#: logviewer.py:203
msgid "Congratulations! Wire-Py is up to date"
msgstr "Glückwunsch! Wire-Py ist aktuell."
#: logviewer.py:223
msgid "Click to install new version"
msgstr "Klicken Sie, um die neue Version zu installieren"
#: logviewer.py:232
msgid ""
"Logviewer a simple Gui for View Logfiles.\n"
"\n"
"Logviewer is open source software written in Python.\n"
"\n"
"Email: polunga40@unity-mail.de also likes for donation.\n"
"\n"
"Use without warranty!\n"
msgstr ""
"Logviewer eine einfache GUI zur Anzeige von Protokolldateien.\n"
"\n"
"Logviewer ist Open-Source-Software, geschrieben in Python.\n"
"\n"
"E-Mail: polunga40@unity-mail.de (Spenden sind willkommen).\n"
"\n"
"Verwendung ohne Gewähr!\n"
#: logviewer.py:288
msgid "Disable Tooltips"
msgstr "Tooltips deaktivieren"
#: logviewer.py:291
msgid "Enable Tooltips"
msgstr "Tooltips aktivieren"
#: logviewer.py:319
msgid "Dark"
msgstr "Dunkel"
#: logviewer.py:321
msgid "Light"
msgstr "Hell"
#: logviewer.py:362
msgid "Copy"
msgstr "Kopieren"
#: logviewer.py:363
msgid "Paste"
msgstr "Einfügen"
#: logviewer.py:367
msgid "Search"
msgstr "Suchen"
#: logviewer.py:371
msgid "Delete_Log"
msgstr "Logdatei löschen"
#: logviewer.py:456
msgid "A mistake occurred: {str(e)}"
msgstr "Ein Fehler ist aufgetreten: {str(e)}"
#: logviewer.py:457
msgid ""
"A mistake occurred:\n"
"{str(e)}\n"
msgstr ""
"Ein Fehler ist aufgetreten:\n"
"{str(e)}\n"
#: logviewer.py:474
#, python-brace-format
msgid "A mistake occurred: {e}"
msgstr "Ein Fehler ist aufgetreten: {e}"
#: logviewer.py:475
#, python-brace-format
msgid ""
"A mistake occurred:\n"
"{e}\n"
msgstr ""
"Ein Fehler ist aufgetreten:\n"
"{e}\n"
#: logview_app_config.py:146
msgid "Click for Settings"
msgstr "Klick für Einstellungen"

95
log_window.py Normal file
View File

@@ -0,0 +1,95 @@
import tkinter as tk
from tkinter import ttk
from datetime import datetime
import typing
if typing.TYPE_CHECKING:
from tkinter import Event
class LogWindow(ttk.Frame):
"""A Tkinter frame that provides a scrollable text widget for logging messages."""
def __init__(self, parent: tk.Misc, copy: str = "Copy") -> None:
"""
Initializes the LogWindow widget.
Args:
parent: The parent widget.
copy: The text label for the 'Copy' context menu item.
"""
super().__init__(parent)
log_container = tk.Frame(self)
# Let the main app control padding via the grid options
log_container.pack(fill="both", expand=True, padx=0, pady=0)
self.log_text: tk.Text = tk.Text(
log_container,
wrap=tk.WORD,
font=("Consolas", 9),
bg="#1e1e1e",
fg="#d4d4d4",
insertbackground="white",
selectbackground="#264f78",
height=10 # Give it a default height
)
log_scrollbar = ttk.Scrollbar(
log_container, orient="vertical", command=self.log_text.yview
)
self.log_text.configure(yscrollcommand=log_scrollbar.set)
self.log_text.pack(side="left", fill="both", expand=True)
log_scrollbar.pack(side="right", fill="y")
self.context_menu: tk.Menu = tk.Menu(self, tearoff=0)
self.context_menu.add_command(label=copy, command=self.copy_text)
self.log_text.bind("<Button-3>", self.show_context_menu)
def log_message(self, message: str) -> None:
"""
Adds a timestamped message to the log view.
Args:
message: The message string to add.
"""
timestamp = datetime.now().strftime("%H:%M:%S")
log_entry = f"[{timestamp}] {message}\n"
self.log_text.insert(tk.END, log_entry)
self.log_text.see(tk.END)
self.log_text.update()
def show_text_menu(self, event: 'Event') -> None:
"""
Displays the context menu at the event's coordinates.
(Note: This seems to be a remnant, show_context_menu is used).
Args:
event: The tkinter event that triggered the menu.
"""
try:
self.context_menu.tk_popup(event.x_root, event.y_root)
finally:
self.context_menu.grab_release()
def copy_text(self) -> None:
"""Copies the currently selected text in the log widget to the clipboard."""
try:
selected_text = self.log_text.selection_get()
self.clipboard_clear()
self.clipboard_append(selected_text)
except tk.TclError:
# No Text selected
pass
def show_context_menu(self, event: 'Event') -> None:
"""
Shows the right-click context menu.
Args:
event: The tkinter Button-3 event.
"""
try:
self.context_menu.tk_popup(event.x_root, event.y_root)
finally:
self.context_menu.grab_release()

33
logger.py Normal file
View File

@@ -0,0 +1,33 @@
import typing
from typing import Callable
class Logger:
"""A simple logger class that can be initialized with a logging function."""
def __init__(self) -> None:
"""Initializes the Logger, defaulting to the print function."""
self._log_func: Callable[[str], None] = print
def init_logger(self, log_func: Callable[[str], None]) -> None:
"""
Initializes the logger with a specific logging function.
Args:
log_func: The function to use for logging. It should accept a single
string argument.
"""
self._log_func = log_func
def log(self, message: str) -> None:
"""
Logs a message using the configured logging function.
Args:
message: The message to log.
"""
self._log_func(message)
# Global instance
app_logger: Logger = Logger()

View File

@@ -1,144 +0,0 @@
"""Configuration for the LogViewer application."""
import gettext
import locale
from pathlib import Path
from typing import Dict, Any
class AppConfig:
"""Central configuration and system setup manager for the LogViewer application.
This class serves as a singleton-like container for all global configuration data,
including paths, UI settings, localization, versioning, and system-specific resources.
It ensures that required directories, files, and services are created and configured
before the application starts. Additionally, it provides tools for managing translations,
default settings, and autostart functionality to maintain a consistent user experience.
Key Responsibilities:
- Centralizes all configuration values (paths, UI preferences, localization).
- Ensures required directories and files exist on startup.
- Handles translation setup via `gettext` for multilingual support.
- Manages default settings file generation.
- Configures autostart services using systemd for user-specific launch behavior.
This class is used globally across the application to access configuration data
consistently and perform system-level setup tasks.
"""
# Logging
LOG_DIR = Path.home() / ".local/share/lxlogs"
Path(LOG_DIR).mkdir(parents=True, exist_ok=True)
LOG_FILE_PATH = LOG_DIR / "logviewer.log"
# Localization
APP_NAME: str = "logviewer"
LOCALE_DIR: Path = Path("/usr/share/locale/")
# Base paths
BASE_DIR: Path = Path.home()
CONFIG_DIR: Path = BASE_DIR / ".config/logviewer"
# Configuration files
SETTINGS_FILE: Path = CONFIG_DIR / "settings"
DEFAULT_SETTINGS: Dict[str, str] = {
"# Configuration": "on",
"# Theme": "light",
"# Tooltips": True,
"# Autostart": "off",
"# Logfile": LOG_FILE_PATH,
}
# Updates
# 1 = 1. Year, 09 = Month of the Year, 2924 = Day and Year of the Year
VERSION: str = "v. 1.07.0925"
UPDATE_URL: str = "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases"
DOWNLOAD_URL: str = "https://git.ilunix.de/punix/shared_libs/archive"
# UI configuration
UI_CONFIG: Dict[str, Any] = {
"window_title2": "LogViewer",
"window_size": (590, 460),
"font_family": "Ubuntu",
"font_size": 11,
"resizable_window": (True, True),
}
# Images and icons paths
IMAGE_PATHS: Dict[str, Path] = {
"icon_log": "/usr/share/icons/lx-icons/48/log.png",
}
# System-dependent paths
SYSTEM_PATHS: Dict[str, Path] = {
"tcl_path": "/usr/share/TK-Themes",
}
@staticmethod
def setup_translations() -> gettext.gettext:
"""
Initialize translations and set the translation function
Special method for translating strings in this file
Returns:
The gettext translation function
"""
locale.bindtextdomain(AppConfig.APP_NAME, AppConfig.LOCALE_DIR)
gettext.bindtextdomain(AppConfig.APP_NAME, AppConfig.LOCALE_DIR)
gettext.textdomain(AppConfig.APP_NAME)
return gettext.gettext
@classmethod
def create_default_settings(cls) -> None:
"""Creates default settings if they don't exist"""
if not cls.SETTINGS_FILE.exists():
content = "\n".join(
f"[{k.upper()}]\n{v}" for k, v in cls.DEFAULT_SETTINGS.items()
)
cls.SETTINGS_FILE.write_text(content)
@classmethod
def ensure_directories(cls) -> None:
"""Ensures that all required directories exist"""
if not cls.CONFIG_DIR.exists():
cls.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
@classmethod
def ensure_log(cls) -> None:
"""Ensures that the log file exists"""
if not cls.LOG_FILE_PATH.exists():
cls.LOG_FILE_PATH.touch()
# here is initializing the class for translation strings
_ = AppConfig.setup_translations()
class Msg:
"""
A utility class that provides centralized access to translated message strings.
This class contains a dictionary of message strings used throughout the Wire-Py application.
All strings are prepared for translation using gettext. The short key names make the code
more concise while maintaining readability.
Attributes:
STR (dict): A dictionary mapping short keys to translated message strings.
Keys are abbreviated for brevity but remain descriptive.
Usage:
Import this class and access messages using the dictionary:
`Msg.STR["sel_tl"]` returns the translated "Select tunnel" message.
Note:
Ensure that gettext translation is properly initialized before
accessing these strings to ensure correct localization.
"""
STR: Dict[str, str] = {
# Strings for messages
}
TTIP: Dict[str, str] = {
# Strings for Tooltips
"settings": _("Click for Settings"),
}

View File

@@ -1,526 +0,0 @@
#!/usr/bin/python3
import argparse
import logging
import tkinter as tk
from tkinter import TclError, filedialog, ttk
from pathlib import Path
import os
import webbrowser
import subprocess
from functools import partial
from shared_libs.gitea import GiteaUpdate
from shared_libs.message import MessageDialog
from shared_libs.common_tools import (
LogConfig,
ConfigManager,
ThemeManager,
LxTools,
Tooltip,
)
import sys
from file_and_dir_ensure import prepare_app_environment
class LogViewer(tk.Tk):
def __init__(self, modul_name):
super().__init__()
self.my_tool_tip = None
self.modul_name = modul_name # Save the module name
# from here the calls must be made with the module name
_ = modul_name.AppConfig.setup_translations()
self.x_width = modul_name.AppConfig.UI_CONFIG["window_size"][0]
self.y_height = modul_name.AppConfig.UI_CONFIG["window_size"][1]
# Set the window size
self.geometry(f"{self.x_width}x{self.y_height}")
self.minsize(
modul_name.AppConfig.UI_CONFIG["window_size"][0],
modul_name.AppConfig.UI_CONFIG["window_size"][1],
)
self.title(modul_name.AppConfig.UI_CONFIG["window_title2"])
self.tk.call(
"source", f"{modul_name.AppConfig.SYSTEM_PATHS['tcl_path']}/water.tcl"
)
ConfigManager.init(modul_name.AppConfig.SETTINGS_FILE)
theme = ConfigManager.get("theme")
ThemeManager.change_theme(self, theme)
LxTools.center_window_cross_platform(self, self.x_width, self.y_height)
self.createWidgets(modul_name, _)
self.load_file(_, modul_name=modul_name)
self.log_icon = tk.PhotoImage(file="/usr/share/icons/lx-icons/48/log.png")
self.update_icon = tk.PhotoImage(
file="/usr/share/icons/lx-icons/16/settings.png"
)
self.iconphoto(True, self.log_icon)
self.grid_rowconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
# StringVar-Variables initialization
self.tooltip_state = tk.BooleanVar()
# Get value from configuration
state = ConfigManager.get("tooltips")
# NOTE: ConfigManager.get("tooltips") can return either a boolean value or a string,
# depending on whether the value was loaded from the file (bool) or the default value is used (string).
# The expression 'lines[5].strip() == "True"' in ConfigManager.load() converts the string to a boolean.
# Convert to boolean and set
if isinstance(state, bool):
# If it's already a boolean, use directly
self.tooltip_state.set(state)
else:
# If it's a string or something else
self.tooltip_state.set(str(state) == "True")
self.tooltip_label = (
tk.StringVar()
) # StringVar-Variable for tooltip label for view Disabled/Enabled
self.tooltip_update_label(modul_name, _)
self.update_label = tk.StringVar() # StringVar-Variable for update label
self.update_tooltip = (
tk.StringVar()
) # StringVar-Variable for update tooltip please not remove!
self.update_foreground = tk.StringVar(value="red")
# Frame for Menu
self.menu_frame = ttk.Frame(self)
self.menu_frame.configure(relief="flat")
if "'logview_app_config'" in f"{modul_name}".split():
self.menu_frame.grid(column=0, row=0, columnspan=4, sticky=tk.NSEW)
# App Menu
self.version_lb = ttk.Label(self.menu_frame, text=modul_name.AppConfig.VERSION)
self.version_lb.config(font=("Ubuntu", 11), foreground="#00c4ff")
self.version_lb.grid(column=0, row=0, rowspan=4, padx=10, pady=10)
Tooltip(
self.version_lb,
f"Version: {modul_name.AppConfig.VERSION[2:]}",
self.tooltip_state,
)
self.load_button = ttk.Button(
self.menu_frame,
text=_("Load Log"),
style="Toolbutton",
command=lambda: self.directory_load(modul_name, _),
)
self.load_button.grid(column=1, row=0)
self.options_btn = ttk.Menubutton(self.menu_frame, text=_("Options"))
self.options_btn.grid(column=2, row=0)
Tooltip(self.options_btn, modul_name.Msg.TTIP["settings"], self.tooltip_state)
self.set_update = tk.IntVar()
self.settings = tk.Menu(self, relief="flat")
self.options_btn.configure(menu=self.settings, style="Toolbutton")
self.settings.add_checkbutton(
label=_("Disable Updates"),
command=lambda: self.update_setting(self.set_update.get(), modul_name, _),
variable=self.set_update,
)
self.updates_lb = ttk.Label(self.menu_frame, textvariable=self.update_label)
self.updates_lb.grid(column=5, row=0, padx=10)
self.updates_lb.grid_remove()
self.update_label.trace_add("write", self.update_label_display)
self.update_foreground.trace_add("write", self.update_label_display)
res = GiteaUpdate.api_down(
modul_name.AppConfig.UPDATE_URL,
modul_name.AppConfig.VERSION,
ConfigManager.get("updates"),
)
self.update_ui_for_update(res, modul_name, _)
# Tooltip Menu
self.settings.add_command(
label=self.tooltip_label.get(),
command=lambda: self.tooltips_toggle(modul_name, _),
)
# Label show dark or light
self.theme_label = tk.StringVar()
self.update_theme_label(modul_name, _)
self.settings.add_command(
label=self.theme_label.get(),
command=lambda: self.on_theme_toggle(modul_name, _),
)
# About BTN Menu / Label
self.about_btn = ttk.Button(
self.menu_frame,
text=_("About"),
style="Toolbutton",
command=lambda: self.about(modul_name, _),
)
self.about_btn.grid(column=3, row=0)
self.readme = tk.Menu(self)
# self.grid_rowconfigure(0, weight=)
self.grid_rowconfigure(1, weight=25)
self.grid_columnconfigure(0, weight=1)
# Method that is called when the variable changes
def update_label_display(self, *args):
# Set the foreground color
self.updates_lb.configure(foreground=self.update_foreground.get())
# Show or hide the label based on whether it contains text
if self.update_label.get():
# Make sure the label is in the correct position every time it's shown
self.updates_lb.grid(column=5, row=0, padx=10)
else:
self.updates_lb.grid_remove()
def updater(self):
"""Start the lxtools_installer"""
tmp_dir = Path("/tmp/lxtools")
Path.mkdir(tmp_dir, exist_ok=True)
os.chdir(tmp_dir)
result = subprocess.run(["/usr/local/bin/lxtools_installer"], check=False)
if result.returncode != 0:
MessageDialog("error", result.stderr)
# Update the labels based on the result
def update_ui_for_update(self, res, modul_name, _):
"""Update UI elements based on an update check result"""
# First, remove the update button if it exists to avoid conflicts
if hasattr(self, "update_btn"):
self.update_btn.grid_forget()
delattr(self, "update_btn")
if res == "False":
self.set_update.set(value=1)
self.update_label.set(_("Update search off"))
self.update_tooltip.set(_("Updates you have disabled"))
# Clear the foreground color as requested
self.update_foreground.set("")
# Set the tooltip for the label
Tooltip(self.updates_lb, self.update_tooltip.get(), self.tooltip_state)
elif res == "No Internet Connection!":
self.update_label.set(_("No Server Connection!"))
self.update_foreground.set("red")
# Set the tooltip for "No Server Connection"
Tooltip(
self.updates_lb,
_("Could not connect to update server"),
self.tooltip_state,
)
elif res == "No Updates":
self.update_label.set(_("No Updates"))
self.update_tooltip.set(_("Congratulations! Wire-Py is up to date"))
self.update_foreground.set("")
# Set the tooltip for the label
Tooltip(self.updates_lb, self.update_tooltip.get(), self.tooltip_state)
else:
self.set_update.set(value=0)
# Clear the label text since we'll show the button instead
self.update_label.set("")
# Create the update button
self.update_btn = ttk.Button(
self.menu_frame,
image=self.update_icon,
style="Toolbutton",
command=self.updater,
)
self.update_btn.grid(column=5, row=0, padx=0)
Tooltip(
self.update_btn, _("Click to install new version"), self.tooltip_state
)
@staticmethod
def about(modul_name, _) -> None:
"""
a tk.Toplevel window
"""
msg_t = _(
"Logviewer a simple Gui for View Logfiles.\n\n"
"Logviewer is open source software written in Python.\n\n"
"Email: polunga40@unity-mail.de also likes for donation.\n\n"
"Use without warranty!\n"
)
MessageDialog(
"info",
text=msg_t,
buttons=["OK", "Go to Logviewer"],
commands=[
None, # Default on "OK"
partial(webbrowser.open, "https://git.ilunix.de/punix/shared_libs"),
],
icon=modul_name.AppConfig.IMAGE_PATHS["icon_log"],
title="Logviewer",
)
def update_setting(self, update_res, modul_name, _) -> None:
"""write off or on in file
Args:
update_res (int): argument that is passed contains 0 or 1
"""
if update_res == 1:
# Disable updates
ConfigManager.set("updates", "off")
# When updates are disabled, we know the result should be "False"
self.update_ui_for_update("False", modul_name, _)
else:
# Enable updates
ConfigManager.set("updates", "on")
# When enabling updates, we need to actually check for updates
try:
# Force a fresh check by passing "on" as the update setting
res = GiteaUpdate.api_down(
modul_name.AppConfig.UPDATE_URL, modul_name.AppConfig.VERSION, "on"
)
# Make sure the UI is updated regardless of the previous state
if hasattr(self, "update_btn"):
self.update_btn.grid_forget()
if hasattr(self, "updates_lb"):
self.updates_lb.grid_forget()
# Now update the UI with the fresh result
self.update_ui_for_update(res, modul_name, _)
except Exception as e:
logging.error(f"Error checking for updates: {e}")
# Fallback to a default message if there's an error
self.update_ui_for_update("No Internet Connection!", modul_name, _)
def tooltip_update_label(self, modul_name, _) -> None:
"""Updates the tooltip menu label based on the current tooltip status"""
# Set the menu text based on the current status
if self.tooltip_state.get():
# If tooltips are enabled, the menu option should be to disable them
self.tooltip_label.set(_("Disable Tooltips"))
else:
# If tooltips are disabled, the menu option should be to enable them
self.tooltip_label.set(_("Enable Tooltips"))
def tooltips_toggle(self, modul_name, _):
"""
Toggles the visibility of tooltips (on/off) and updates
the corresponding menu label. Inverts the current tooltip state
(`self.tooltip_state`), saves the new value in the configuration,
and applies the change immediately. Updates the menu entry's label to
reflect the new tooltip status (e.g., "Tooltips: On" or "Tooltips: Off").
"""
# Toggle the boolean state
new_bool_state = not self.tooltip_state.get()
# Save the converted value in the configuration
ConfigManager.set("tooltips", str(new_bool_state))
# Update the tooltip_state variable for immediate effect
self.tooltip_state.set(new_bool_state)
# Update the menu label
self.tooltip_update_label(modul_name, _)
# Update the menu entry - find the correct index
# This assumes it's the third item (index 2) in your menu
self.settings.entryconfigure(1, label=self.tooltip_label.get())
def update_theme_label(self, modul_name, _) -> None:
"""Update the theme label based on the current theme"""
current_theme = ConfigManager.get("theme")
if current_theme == "light":
self.theme_label.set(_("Dark"))
else:
self.theme_label.set(_("Light"))
def on_theme_toggle(self, modul_name, _) -> None:
"""Toggle between light and dark theme"""
current_theme = ConfigManager.get("theme")
new_theme = "dark" if current_theme == "light" else "light"
ThemeManager.change_theme(self, new_theme, new_theme)
self.update_theme_label(modul_name, _) # Update the theme label
# Update Menulfield
self.settings.entryconfigure(2, label=self.theme_label.get())
def createWidgets(self, modul_name, _):
text_frame = ttk.Frame(self)
text_frame.grid(row=1, column=0, padx=5, pady=5, sticky=tk.NSEW)
text_frame.rowconfigure(0, weight=3)
text_frame.columnconfigure(0, weight=1)
next_frame = ttk.Frame(self)
next_frame.grid(row=2, column=0, sticky=tk.NSEW)
next_frame.rowconfigure(2, weight=1)
next_frame.columnconfigure(1, weight=1)
# Create a Text widget for displaying the log file
self.text_area = tk.Text(
text_frame, wrap=tk.WORD, padx=5, pady=5, relief="flat"
)
self.text_area.grid(row=0, column=0, sticky=tk.NSEW)
self.text_area.tag_configure(
"found-tag", foreground="yellow", background="green"
)
# Create a vertical scrollbar for the Text widget
v_scrollbar = ttk.Scrollbar(
text_frame, orient="vertical", command=self.text_area.yview
)
v_scrollbar.grid(row=0, column=1, sticky=tk.NS)
self.text_area.configure(yscrollcommand=v_scrollbar.set)
self._entry = ttk.Entry(next_frame)
self._entry.bind("<Return>", lambda e: self._onFind())
self._entry.grid(row=0, column=1, padx=5, sticky=tk.EW)
# Add a context menu to the Text widget
self.context_menu = tk.Menu(self, tearoff=0)
self.context_menu.add_command(label=_("Copy"), command=self.copy_text)
self.context_menu.add_command(label=_("Paste"), command=self.paste_into_entry)
self.text_area.bind("<Button-3>", self.show_context_menu)
self._entry.bind("<Button-3>", self.show_context_menu)
search_button = ttk.Button(next_frame, text=_("Search"), command=self._onFind)
search_button.grid(row=0, column=0, padx=5, pady=5, sticky=tk.EW)
delete_button = ttk.Button(
next_frame, text=_("Delete_Log"), command=self.delete_file
)
delete_button.grid(row=0, column=2, padx=5, pady=5, sticky=tk.EW)
def show_text_menu(self, event):
try:
self.configure.tk_popup(event.x_root, event.y_root)
finally:
self.context_menu.grab_release()
def copy_text(self):
try:
selected_text = self.text_area.selection_get()
self.clipboard_clear()
self.clipboard_append(selected_text)
except tk.TclError:
# No Text selected
pass
def show_context_menu(self, event):
try:
self.context_menu.tk_popup(event.x_root, event.y_root)
finally:
self.context_menu.grab_release()
def paste_into_entry(self):
try:
text = self.clipboard_get()
self._entry.delete(0, tk.END)
self._entry.insert(tk.END, text)
except tk.TclError:
# No Text on Clipboard
pass
def _onFind(self):
searchText = self._entry.get()
if len(searchText) == 0:
return
# Set the search start position to the last found position (initial value: "1.0")
start_pos = self.last_search_pos if hasattr(self, "last_search_pos") else "1.0"
var = tk.IntVar()
foundIndex = self.text_area.search(
searchText,
start_pos,
stopindex=tk.END,
nocase=tk.YES,
count=var,
regexp=tk.YES,
)
if not foundIndex:
# No further entry found, reset to the beginning
self.last_search_pos = "1.0"
return
count = var.get()
lastIndex = self.text_area.index(f"{foundIndex} + {count}c")
# Remove and reapply highlighting
self.text_area.tag_remove("found-tag", "1.0", tk.END)
self.text_area.tag_add("found-tag", foundIndex, lastIndex)
# Update the start position for the next search
self.last_search_pos = lastIndex
self.text_area.see(foundIndex)
def delete_file(self, modul_name):
Path.unlink(modul_name.AppConfig.LOG_FILE_PATH)
modul_name.AppConfig.ensure_log()
def load_file(self, _, modul_name):
try:
if not modul_name.AppConfig.LOG_FILE_PATH:
return
with open(
modul_name.AppConfig.LOG_FILE_PATH, "r", encoding="utf-8"
) as file:
self.text_area.delete(1.0, tk.END)
self.text_area.insert(tk.END, file.read())
except Exception as e:
logging.error(_(f"A mistake occurred: {str(e)}"))
MessageDialog("error", _(f"A mistake occurred:\n{str(e)}\n"))
def directory_load(self, modul_name, _):
filepath = filedialog.askopenfilename(
initialdir=f"{Path.home() / '.local/share/lxlogs/'}",
title="Select a Logfile File",
filetypes=[("Logfiles", "*.log")],
)
try:
with open(filepath, "r", encoding="utf-8") as file:
self.text_area.delete(1.0, tk.END)
self.text_area.insert(tk.END, file.read())
except (IsADirectoryError, TypeError, FileNotFoundError):
print("File load: abort by user...")
except Exception as e:
logging.error(_(f"A mistake occurred: {e}"))
MessageDialog("error", _(f"A mistake occurred:\n{e}\n"))
def main():
# Create an ArgumentParser object
parser = argparse.ArgumentParser(
description="LogViewer with optional module loading."
)
parser.add_argument(
"--modul",
type=str,
default="logview_app_config",
help="Give the name of the module to load.",
)
args = parser.parse_args()
import importlib
try:
modul = importlib.import_module(args.modul)
except ModuleNotFoundError:
print(f"Modul '{args.modul}' not found")
print("For help use logviewer -h")
sys.exit(1)
except Exception as e:
print(f"Error load Modul: {str(e)}")
sys.exit(1)
prepare_app_environment()
app = LogViewer(modul)
LogConfig.logger(ConfigManager.get("logfile"))
"""
the hidden files are hidden in Filedialog
"""
try:
app.tk.call("tk_getOpenFile", "-foobarbaz")
except TclError:
pass
app.tk.call("set", "::tk::dialog::file::showHiddenBtn", "1")
app.tk.call("set", "::tk::dialog::file::showHiddenVar", "0")
app.mainloop()
if __name__ == "__main__":
main()

245
menu_bar.py Normal file
View File

@@ -0,0 +1,245 @@
import os
import subprocess
import threading
import webbrowser
from functools import partial
from pathlib import Path
from tkinter import ttk
import typing
if typing.TYPE_CHECKING:
from tkinter import BooleanVar
from typing import Any, Callable
from .logger import app_logger
from .common_tools import ConfigManager, Tooltip, message_box_animation
from .gitea import GiteaUpdate
from .animated_icon import AnimatedIcon, PIL_AVAILABLE
from .message import MessageDialog
class MenuBar(ttk.Frame):
"""A reusable menu bar widget for tkinter applications."""
def __init__(
self,
container: ttk.Frame,
image_manager: 'Any', # Should have a .get_icon(str) -> PhotoImage method
tooltip_state: 'BooleanVar',
on_theme_toggle: 'Callable[[], None]',
toggle_log_window: 'Callable[[], None]',
app_config: 'Any', # Should have .UPDATE_URL and .VERSION attributes
msg_config: 'Any', # Should have .STR and .TTIP dictionaries
about_icon_path: str,
about_url: str,
**kwargs: 'Any',
) -> None:
"""
Initializes the MenuBar.
Args:
container: The parent widget.
image_manager: An object with a `get_icon` method to retrieve icons.
tooltip_state: A tkinter BooleanVar to control tooltip visibility.
on_theme_toggle: Callback function to toggle the application's theme.
toggle_log_window: Callback function to show/hide the log window.
app_config: Project-specific config. Must have UPDATE_URL and VERSION.
msg_config: Project-specific messages. Must have STR and TTIP dicts.
about_icon_path: Filesystem path to the icon for the 'About' dialog.
about_url: URL for the project's repository or website.
**kwargs: Additional keyword arguments for the ttk.Frame.
"""
super().__init__(container, **kwargs)
self.image_manager = image_manager
self.tooltip_state = tooltip_state
self.on_theme_toggle_callback = on_theme_toggle
self.app_config = app_config
self.msg_config = msg_config
self.about_icon_path = about_icon_path
self.about_url = about_url
# --- Horizontal button frame for settings ---
actions_frame = ttk.Frame(self)
actions_frame.grid(column=0, row=0, padx=(5, 10), sticky="w")
# --- Theme Button ---
self.theme_btn = ttk.Button(
actions_frame, command=self.theme_toggle, style="TButton.Borderless.Round"
)
self.theme_btn.grid(column=0, row=0, padx=(0, 2))
self.update_theme_icon()
Tooltip(self.theme_btn, "Thema wechseln (Hell/Dunkel)", state_var=self.tooltip_state)
# --- Tooltip Button ---
self.tooltip_btn = ttk.Button(
actions_frame, command=self.tooltips_toggle, style="TButton.Borderless.Round"
)
self.tooltip_btn.grid(column=1, row=0, padx=(0, 2))
self.update_tooltip_icon()
Tooltip(self.tooltip_btn, "Tooltips an/aus", state_var=self.tooltip_state)
# --- Update Button ---
self.update_btn = ttk.Button(
actions_frame,
command=self.toggle_update_setting,
style="TButton.Borderless.Round",
)
self.update_btn.grid(column=2, row=0)
self.update_update_icon()
Tooltip(self.update_btn, "Updates an/aus", state_var=self.tooltip_state)
# --- Animated Icon for Updates ---
self.animated_icon_frame = ttk.Frame(actions_frame)
self.animated_icon_frame.grid(column=3, row=0, padx=(5, 0))
current_theme = ConfigManager.get("theme")
bg_color = "#ffffff" if current_theme == "light" else "#333333"
self.animated_icon = AnimatedIcon(
self.animated_icon_frame,
animation_type="blink",
use_pillow=PIL_AVAILABLE,
bg=bg_color,
)
self.animated_icon.pack()
self.animated_icon_frame.bind("<Button-1>", lambda e: self.updater())
self.animated_icon.bind("<Button-1>", lambda e: self.updater())
self.animated_icon_frame.grid_remove() # Initially hidden
# Add a spacer column with weight to push subsequent buttons to the right
self.columnconfigure(1, weight=1)
# --- Log Button ---
self.log_btn = ttk.Button(
self,
image=self.image_manager.get_icon("log_blue_small"),
style="TButton.Borderless.Round",
command=toggle_log_window,
)
self.log_btn.grid(column=2, row=0, sticky="e")
Tooltip(self.log_btn, "Show Log", state_var=self.tooltip_state)
# --- About Button ---
self.about_btn = ttk.Button(
self,
image=self.image_manager.get_icon("about"),
style="TButton.Borderless.Round",
command=self.about,
)
self.about_btn.grid(column=3, row=0)
Tooltip(self.about_btn, self.msg_config.STR["about"], state_var=self.tooltip_state)
# --- Start background update check ---
self.update_thread = threading.Thread(target=self.check_for_updates, daemon=True)
self.update_thread.start()
def update_theme_icon(self) -> None:
"""Sets the theme button icon based on the current theme."""
current_theme = ConfigManager.get("theme")
icon_name = "dark_small" if current_theme == "light" else "light_small"
self.theme_btn.configure(image=self.image_manager.get_icon(icon_name))
def update_tooltip_icon(self) -> None:
"""Sets the tooltip button icon based on the tooltip state."""
icon_name = "tooltip_small" if self.tooltip_state.get() else "no_tooltip_small"
self.tooltip_btn.configure(image=self.image_manager.get_icon(icon_name))
def update_update_icon(self) -> None:
"""Sets the update button icon based on the update setting."""
updates_on = ConfigManager.get("updates") == "on"
icon_name = "update_small" if updates_on else "no_update_small"
self.update_btn.configure(image=self.image_manager.get_icon(icon_name))
def theme_toggle(self) -> None:
"""Invokes the theme toggle callback."""
self.on_theme_toggle_callback()
def update_theme(self) -> None:
"""Updates theme-dependent widgets, like icon backgrounds."""
self.update_theme_icon()
current_theme = ConfigManager.get("theme")
bg_color = "#ffffff" if current_theme == "light" else "#333333"
self.animated_icon.configure(bg=bg_color)
def tooltips_toggle(self) -> None:
"""Toggles the tooltip state and updates the icon."""
new_bool_state = not self.tooltip_state.get()
ConfigManager.set("tooltips", str(new_bool_state))
self.tooltip_state.set(new_bool_state)
self.update_tooltip_icon()
def toggle_update_setting(self) -> None:
"""Toggles the automatic update setting and re-checks for updates."""
updates_on = ConfigManager.get("updates") == "on"
ConfigManager.set("updates", "off" if updates_on else "on")
self.update_update_icon()
# After changing the setting, re-run the check to update status
threading.Thread(target=self.check_for_updates, daemon=True).start()
def check_for_updates(self) -> None:
"""Checks for updates via the Gitea API in a background thread."""
try:
res = GiteaUpdate.api_down(
self.app_config.UPDATE_URL,
self.app_config.VERSION,
ConfigManager.get("updates"),
)
self.after(0, self.update_ui_for_update, res)
except Exception as e:
app_logger.log(f"Error during update check: {e}")
self.after(0, self.update_ui_for_update, "No Internet Connection!")
def update_ui_for_update(self, res: str) -> None:
"""
Updates the UI based on the result of the update check.
Args:
res: The result string from the update check.
"""
self.animated_icon_frame.grid_remove()
self.animated_icon.hide()
tooltip_msg = ""
if res == "False":
tooltip_msg = self.msg_config.TTIP["updates_disabled"]
elif res == "No Internet Connection!":
tooltip_msg = self.msg_config.TTIP["no_server_conn_tt"]
elif res == "No Updates":
tooltip_msg = self.msg_config.TTIP["up_to_date"]
self.animated_icon_frame.grid()
self.animated_icon.stop()
else:
tooltip_msg = self.msg_config.TTIP["install_new_version"]
self.animated_icon_frame.grid()
self.animated_icon.start()
Tooltip(self.update_btn, tooltip_msg, state_var=self.tooltip_state)
Tooltip(self.animated_icon_frame, tooltip_msg, state_var=self.tooltip_state)
def updater(self) -> None:
"""Runs the external installer script for updating the application."""
tmp_dir = Path("/tmp/lxtools")
Path.mkdir(tmp_dir, exist_ok=True)
os.chdir(tmp_dir)
with message_box_animation(self.animated_icon):
result = subprocess.run(
["/usr/local/bin/lxtools_installer"], check=False, capture_output=True, text=True
)
if result.returncode != 0:
MessageDialog("error", result.stderr or result.stdout).show()
def about(self) -> None:
"""Displays the application's About dialog."""
with message_box_animation(self.animated_icon):
MessageDialog(
"info",
self.msg_config.STR["about_msg"],
buttons=["OK", self.msg_config.STR["goto_git"]],
title=self.msg_config.STR["info"],
commands=[
None,
partial(webbrowser.open, self.about_url),
],
icon=self.about_icon_path,
wraplength=420,
).show()

View File

@@ -1,12 +1,12 @@
import os
from typing import List, Optional, Dict
from typing import List, Optional
import tkinter as tk
from tkinter import ttk
try:
from manager import LxTools
except (ModuleNotFoundError, NameError):
from shared_libs.common_tools import LxTools
from shared_libs.common_tools import LxTools, IconManager
class MessageDialog:
@@ -41,7 +41,6 @@ class MessageDialog:
Methods:
_get_title(): Returns the default window title based on message type.
_load_icons(): Loads icons from system paths or fallback locations.
_on_button_click(button_text): Sets result and closes the dialog.
show(): Displays the dialog and waits for user response.
@@ -102,8 +101,6 @@ class MessageDialog:
- Font and wraplength parameters enable text styling
"""
DEFAULT_ICON_PATH = "/usr/share/icons/lx-icons"
def __init__(
self,
message_type: str = "info",
@@ -122,7 +119,6 @@ class MessageDialog:
self.master = master
self.result: bool = False # Default is False
self.icon_path = self._get_icon_path()
self.icon = icon
self.title = title
# Window creation
@@ -132,11 +128,35 @@ class MessageDialog:
ttk.Style().configure("TButton")
self.buttons_widgets = []
self.current_button_index = 0
self._load_icons()
# Load icons using IconManager
icon_manager = IconManager()
self.icons = {
"error": icon_manager.get_icon("error_extralarge"),
"info": icon_manager.get_icon("info_extralarge"),
"warning": icon_manager.get_icon("warning_large"),
"ask": icon_manager.get_icon("question_mark_extralarge"),
}
# Handle custom icon override
if self.icon:
if isinstance(self.icon, str) and os.path.exists(self.icon):
# If it's a path, load it
try:
self.icons[self.message_type] = tk.PhotoImage(
file=self.icon)
except tk.TclError as e:
print(
f"Error loading custom icon from path '{self.icon}': {e}")
elif isinstance(self.icon, tk.PhotoImage):
# If it's already a PhotoImage, use it directly
self.icons[self.message_type] = self.icon
# Window title and icon
self.window.title(self._get_title() if not self.title else self.title)
self.window.iconphoto(False, self.icons[self.message_type])
window_icon = self.icons.get(self.message_type)
if window_icon:
self.window.iconphoto(False, window_icon)
# Layout
frame = ttk.Frame(self.window)
@@ -148,7 +168,7 @@ class MessageDialog:
frame.grid_columnconfigure(1, weight=3)
# Icon and Text
icon_label = ttk.Label(frame, image=self.icons[self.message_type])
icon_label = ttk.Label(frame, image=self.icons.get(self.message_type))
pady_value = 5 if self.icon is not None else 15
icon_label.grid(
row=0, column=0, padx=(20, 10), pady=(pady_value, 15), sticky="nsew"
@@ -190,8 +210,10 @@ class MessageDialog:
command=lambda t=btn_text: self._on_button_click(t),
)
padx_value = 50 if self.icon is not None and len(buttons) == 2 else 10
btn.pack(side="left" if i == 0 else "right", padx=padx_value, pady=5)
padx_value = 50 if self.icon is not None and len(
buttons) == 2 else 10
btn.pack(side="left" if i == 0 else "right",
padx=padx_value, pady=5)
btn.focus_set() if i == 0 else None # Set focus on first button
self.buttons_widgets.append(btn)
@@ -219,60 +241,6 @@ class MessageDialog:
"warning": "Warning",
}[self.message_type]
def _load_icons(self):
# Try to load the icon from the provided path
self.icons = {}
icon_paths: Dict[str, str] = {
"error": os.path.join(self.icon_path, "64/error.png"),
"info": os.path.join(self.icon_path, "64/info.png"),
"warning": os.path.join(self.icon_path, "64/warning.png"),
"ask": os.path.join(self.icon_path, "64/question_mark.png"),
}
fallback_paths: Dict[str, str] = {
"error": "./lx-icons/64/error.png",
"info": "./lx-icons/64/info.png",
"warning": "./lx-icons/64/warning.png",
"ask": "./lx-icons/64/question_mark.png",
}
for key in icon_paths:
try:
# Check if an individual icon is provided
if (
self.message_type == key
and self.icon is not None
and os.path.exists(self.icon)
):
try:
self.icons[key] = tk.PhotoImage(file=self.icon)
except Exception as e:
print(
f"Erro on loading individual icon '{key}': {e}\n",
"Try to use the default icon",
)
else:
# Check for standard path
if os.path.exists(icon_paths[key]):
self.icons[key] = tk.PhotoImage(file=icon_paths[key])
else:
if os.path.exists(fallback_paths[key]):
self.icons[key] = tk.PhotoImage(file=fallback_paths[key])
except Exception as e:
print(f"Error on load Icon '{[key]}': {e}")
self.icons[key] = tk.PhotoImage()
print(f"⚠️ No Icon found for '{key}'. Use standard Tkinter icon.")
return self.icons
def _get_icon_path(self) -> str:
"""Get the path to the default icon."""
if os.path.exists(self.DEFAULT_ICON_PATH):
return self.DEFAULT_ICON_PATH
else:
# Fallback to the directory of the script
return os.path.dirname(os.path.abspath(__file__))
def _navigate_left(self):
if not self.buttons_widgets:
return