Reduced redundancy, logviewer fulll removed , add log_window and menu_bar
This commit is contained in:
21
Changelog
21
Changelog
@@ -2,9 +2,26 @@ Changelog for shared_libs
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
### Added
|
||||||
09.07.2025
|
09.07.2025
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
440
animated_icon.py
440
animated_icon.py
@@ -16,13 +16,16 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
PIL_AVAILABLE = False
|
PIL_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
def _hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
|
def _hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
|
||||||
"""Converts a hex color string to an RGB tuple."""
|
"""Converts a hex color string to an RGB tuple."""
|
||||||
hex_color = hex_color.lstrip('#')
|
hex_color = hex_color.lstrip('#')
|
||||||
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||||
|
|
||||||
|
|
||||||
class AnimatedIcon(tk.Canvas):
|
class AnimatedIcon(tk.Canvas):
|
||||||
"""A custom Tkinter Canvas widget for displaying animations."""
|
"""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:
|
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.
|
Initializes the AnimatedIcon widget.
|
||||||
@@ -59,7 +62,8 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
self.highlight_color_rgb = _hex_to_rgb(self.highlight_color)
|
self.highlight_color_rgb = _hex_to_rgb(self.highlight_color)
|
||||||
|
|
||||||
if self.use_pillow:
|
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.draw = ImageDraw.Draw(self.image)
|
||||||
self.photo_image = None
|
self.photo_image = None
|
||||||
|
|
||||||
@@ -88,9 +92,12 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
"""Draws the pulse animation using canvas methods."""
|
"""Draws the pulse animation using canvas methods."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
alpha = (sin(self.angle * 5) + 1) / 2 # Faster pulse
|
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])
|
r = int(
|
||||||
g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||||
b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
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}"
|
pulse_color = f"#{r:02x}{g:02x}{b:02x}"
|
||||||
|
|
||||||
if self.animation_type == "line":
|
if self.animation_type == "line":
|
||||||
@@ -100,19 +107,25 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
start_y = center_y + sin(angle) * (self.height * 0.2)
|
start_y = center_y + sin(angle) * (self.height * 0.2)
|
||||||
end_x = center_x + cos(angle) * (self.width * 0.4)
|
end_x = center_x + cos(angle) * (self.width * 0.4)
|
||||||
end_y = center_y + sin(angle) * (self.height * 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":
|
elif self.animation_type == "double_arc":
|
||||||
radius = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.create_arc(bbox, start=0, extent=359.9, style=tk.ARC, outline=pulse_color, width=2)
|
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":
|
elif self.animation_type == "counter_arc":
|
||||||
radius_outer = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.create_arc(bbox_outer, start=0, extent=359.9, style=tk.ARC, outline=pulse_color, width=2)
|
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
|
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,
|
||||||
self.create_arc(bbox_inner, start=0, extent=359.9, style=tk.ARC, outline=self.color, width=2)
|
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:
|
def _draw_canvas_line(self) -> None:
|
||||||
"""Draws the line animation using canvas methods."""
|
"""Draws the line animation using canvas methods."""
|
||||||
@@ -125,55 +138,71 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
end_y = center_y + sin(angle) * (self.height * 0.4)
|
end_y = center_y + sin(angle) * (self.height * 0.4)
|
||||||
alpha = (cos(self.angle * 2 + i * (pi / 4)) + 1) / 2
|
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])
|
r = int(
|
||||||
g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||||
b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
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}"
|
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:
|
def _draw_canvas_double_arc(self) -> None:
|
||||||
"""Draws the double arc animation using canvas methods."""
|
"""Draws the double arc animation using canvas methods."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
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
|
start_angle1 = -self.angle * 180 / pi
|
||||||
extent1 = 120 + 60 * sin(-self.angle)
|
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
|
start_angle2 = (-self.angle + pi) * 180 / pi
|
||||||
extent2 = 120 + 60 * sin(-self.angle + pi / 2)
|
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:
|
def _draw_canvas_counter_arc(self) -> None:
|
||||||
"""Draws the counter arc animation using canvas methods."""
|
"""Draws the counter arc animation using canvas methods."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
|
|
||||||
radius_outer = min(center_x, center_y) * 0.8
|
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
|
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
|
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
|
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:
|
def _draw_canvas_blink(self) -> None:
|
||||||
"""Draws the blink animation using canvas methods."""
|
"""Draws the blink animation using canvas methods."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
radius = min(center_x, center_y) * 0.8
|
||||||
alpha = (sin(self.angle * 2) + 1) / 2 # Slower blinking speed
|
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])
|
r = int(
|
||||||
g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||||
b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
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}"
|
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:
|
def _draw_pillow_frame(self) -> None:
|
||||||
"""Draws a frame using Pillow for anti-aliased graphics."""
|
"""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:
|
if self.pulse_animation:
|
||||||
self._draw_pillow_pulse()
|
self._draw_pillow_pulse()
|
||||||
elif self.animation_type == "line":
|
elif self.animation_type == "line":
|
||||||
@@ -185,7 +214,8 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
elif self.animation_type == "blink":
|
elif self.animation_type == "blink":
|
||||||
self._draw_pillow_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.photo_image = ImageTk.PhotoImage(resized_image)
|
||||||
self.delete("all")
|
self.delete("all")
|
||||||
self.create_image(0, 0, anchor="nw", image=self.photo_image)
|
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."""
|
"""Draws the pulse animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
alpha = (sin(self.angle * 5) + 1) / 2 # Faster pulse
|
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])
|
r = int(
|
||||||
g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||||
b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
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)
|
pulse_color = (r, g, b)
|
||||||
|
|
||||||
if self.animation_type == "line":
|
if self.animation_type == "line":
|
||||||
@@ -206,18 +239,24 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
start_y = center_y + sin(angle) * (self.height * 0.8)
|
start_y = center_y + sin(angle) * (self.height * 0.8)
|
||||||
end_x = center_x + cos(angle) * (self.width * 1.6)
|
end_x = center_x + cos(angle) * (self.width * 1.6)
|
||||||
end_y = center_y + sin(angle) * (self.height * 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":
|
elif self.animation_type == "double_arc":
|
||||||
radius = min(center_x, center_y) * 0.8
|
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)
|
self.draw.arc(bbox, start=0, end=360, fill=pulse_color, width=5)
|
||||||
elif self.animation_type == "counter_arc":
|
elif self.animation_type == "counter_arc":
|
||||||
radius_outer = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.draw.arc(bbox_outer, start=0, end=360, fill=pulse_color, width=7)
|
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
|
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,
|
||||||
self.draw.arc(bbox_inner, start=0, end=360, fill=self.color_rgb, width=7)
|
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:
|
def _draw_pillow_line(self) -> None:
|
||||||
"""Draws the line animation using Pillow."""
|
"""Draws the line animation using Pillow."""
|
||||||
@@ -230,51 +269,66 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
end_y = center_y + sin(angle) * (self.height * 1.6)
|
end_y = center_y + sin(angle) * (self.height * 1.6)
|
||||||
alpha = (cos(self.angle * 2.5 + i * (pi / 6)) + 1) / 2
|
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])
|
r = int(
|
||||||
g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||||
b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
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)
|
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:
|
def _draw_pillow_double_arc(self) -> None:
|
||||||
"""Draws the double arc animation using Pillow."""
|
"""Draws the double arc animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
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
|
start_angle1 = self.angle * 180 / pi
|
||||||
extent1 = 120 + 60 * sin(self.angle)
|
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
|
start_angle2 = (self.angle + pi) * 180 / pi
|
||||||
extent2 = 120 + 60 * sin(self.angle + pi / 2)
|
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:
|
def _draw_pillow_counter_arc(self) -> None:
|
||||||
"""Draws the counter arc animation using Pillow."""
|
"""Draws the counter arc animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
|
|
||||||
radius_outer = min(center_x, center_y) * 0.8
|
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
|
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
|
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
|
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:
|
def _draw_pillow_blink(self) -> None:
|
||||||
"""Draws the blink animation using Pillow."""
|
"""Draws the blink animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
radius = min(center_x, center_y) * 0.8
|
||||||
alpha = (sin(self.angle * 2) + 1) / 2 # Slower blinking speed
|
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])
|
r = int(
|
||||||
g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||||
b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
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)
|
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:
|
def _draw_stopped_frame(self) -> None:
|
||||||
"""Draws the icon in its stopped (static) state."""
|
"""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)
|
start_y = center_y + sin(angle) * (self.height * 0.2)
|
||||||
end_x = center_x + cos(angle) * (self.width * 0.4)
|
end_x = center_x + cos(angle) * (self.width * 0.4)
|
||||||
end_y = center_y + sin(angle) * (self.height * 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:
|
def _draw_canvas_double_arc_stopped(self) -> None:
|
||||||
"""Draws the stopped state for the double arc animation."""
|
"""Draws the stopped state for the double arc animation."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.create_arc(bbox, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=2)
|
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:
|
def _draw_canvas_counter_arc_stopped(self) -> None:
|
||||||
"""Draws the stopped state for the counter arc animation."""
|
"""Draws the stopped state for the counter arc animation."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
radius_outer = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.create_arc(bbox_outer, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=2)
|
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
|
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,
|
||||||
self.create_arc(bbox_inner, start=0, extent=359.9, style=tk.ARC, outline=self.color, width=2)
|
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:
|
def _draw_canvas_blink_stopped(self) -> None:
|
||||||
"""Draws the stopped state for the blink animation."""
|
"""Draws the stopped state for the blink animation."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
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:
|
def _draw_pillow_stopped_frame(self) -> None:
|
||||||
"""Draws the stopped state using Pillow."""
|
"""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":
|
if self.animation_type == "line":
|
||||||
self._draw_pillow_line_stopped()
|
self._draw_pillow_line_stopped()
|
||||||
elif self.animation_type == "double_arc":
|
elif self.animation_type == "double_arc":
|
||||||
@@ -341,7 +404,8 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
elif self.animation_type == "blink":
|
elif self.animation_type == "blink":
|
||||||
self._draw_pillow_blink_stopped()
|
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.photo_image = ImageTk.PhotoImage(resized_image)
|
||||||
self.create_image(0, 0, anchor="nw", image=self.photo_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)
|
start_y = center_y + sin(angle) * (self.height * 0.8)
|
||||||
end_x = center_x + cos(angle) * (self.width * 1.6)
|
end_x = center_x + cos(angle) * (self.width * 1.6)
|
||||||
end_y = center_y + sin(angle) * (self.height * 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:
|
def _draw_pillow_double_arc_stopped(self) -> None:
|
||||||
"""Draws the stopped state for the double arc animation using Pillow."""
|
"""Draws the stopped state for the double arc animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.draw.arc(bbox, start=0, end=360, fill=self.highlight_color_rgb, width=5)
|
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:
|
def _draw_pillow_counter_arc_stopped(self) -> None:
|
||||||
"""Draws the stopped state for the counter arc animation using Pillow."""
|
"""Draws the stopped state for the counter arc animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
radius_outer = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.draw.arc(bbox_outer, start=0, end=360, fill=self.highlight_color_rgb, width=7)
|
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
|
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,
|
||||||
self.draw.arc(bbox_inner, start=0, end=360, fill=self.color_rgb, width=7)
|
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:
|
def _draw_pillow_blink_stopped(self) -> None:
|
||||||
"""Draws the stopped state for the blink animation using Pillow."""
|
"""Draws the stopped state for the blink animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
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:
|
def _animate(self) -> None:
|
||||||
"""The main animation loop."""
|
"""The main animation loop."""
|
||||||
if self.running:
|
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
|
self.angle += 0.1
|
||||||
if self.angle > 2 * pi:
|
if self.angle > 2 * pi:
|
||||||
self.angle -= 2 * pi
|
self.angle -= 2 * pi
|
||||||
@@ -417,9 +498,6 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
if not self.running:
|
if not self.running:
|
||||||
self._draw_stopped_frame()
|
self._draw_stopped_frame()
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
from math import sin, cos, pi
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageDraw, ImageTk
|
from PIL import Image, ImageDraw, ImageTk
|
||||||
@@ -427,13 +505,16 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
PIL_AVAILABLE = False
|
PIL_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
def _hex_to_rgb(hex_color):
|
def _hex_to_rgb(hex_color):
|
||||||
"""Converts a hex color string to an RGB tuple."""
|
"""Converts a hex color string to an RGB tuple."""
|
||||||
hex_color = hex_color.lstrip('#')
|
hex_color = hex_color.lstrip('#')
|
||||||
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||||
|
|
||||||
|
|
||||||
class AnimatedIcon(tk.Canvas):
|
class AnimatedIcon(tk.Canvas):
|
||||||
"""A custom Tkinter Canvas widget for displaying animations."""
|
"""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):
|
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.
|
Initializes the AnimatedIcon widget.
|
||||||
@@ -470,7 +551,8 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
self.highlight_color_rgb = _hex_to_rgb(self.highlight_color)
|
self.highlight_color_rgb = _hex_to_rgb(self.highlight_color)
|
||||||
|
|
||||||
if self.use_pillow:
|
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.draw = ImageDraw.Draw(self.image)
|
||||||
self.photo_image = None
|
self.photo_image = None
|
||||||
|
|
||||||
@@ -499,9 +581,12 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
"""Draws the pulse animation using canvas methods."""
|
"""Draws the pulse animation using canvas methods."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
alpha = (sin(self.angle * 5) + 1) / 2 # Faster pulse
|
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])
|
r = int(
|
||||||
g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||||
b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
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}"
|
pulse_color = f"#{r:02x}{g:02x}{b:02x}"
|
||||||
|
|
||||||
if self.animation_type == "line":
|
if self.animation_type == "line":
|
||||||
@@ -511,19 +596,25 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
start_y = center_y + sin(angle) * (self.height * 0.2)
|
start_y = center_y + sin(angle) * (self.height * 0.2)
|
||||||
end_x = center_x + cos(angle) * (self.width * 0.4)
|
end_x = center_x + cos(angle) * (self.width * 0.4)
|
||||||
end_y = center_y + sin(angle) * (self.height * 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":
|
elif self.animation_type == "double_arc":
|
||||||
radius = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.create_arc(bbox, start=0, extent=359.9, style=tk.ARC, outline=pulse_color, width=2)
|
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":
|
elif self.animation_type == "counter_arc":
|
||||||
radius_outer = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.create_arc(bbox_outer, start=0, extent=359.9, style=tk.ARC, outline=pulse_color, width=2)
|
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
|
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,
|
||||||
self.create_arc(bbox_inner, start=0, extent=359.9, style=tk.ARC, outline=self.color, width=2)
|
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):
|
def _draw_canvas_line(self):
|
||||||
"""Draws the line animation using canvas methods."""
|
"""Draws the line animation using canvas methods."""
|
||||||
@@ -536,55 +627,71 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
end_y = center_y + sin(angle) * (self.height * 0.4)
|
end_y = center_y + sin(angle) * (self.height * 0.4)
|
||||||
alpha = (cos(self.angle * 2 + i * (pi / 4)) + 1) / 2
|
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])
|
r = int(
|
||||||
g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||||
b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
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}"
|
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):
|
def _draw_canvas_double_arc(self):
|
||||||
"""Draws the double arc animation using canvas methods."""
|
"""Draws the double arc animation using canvas methods."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
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
|
start_angle1 = -self.angle * 180 / pi
|
||||||
extent1 = 120 + 60 * sin(-self.angle)
|
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
|
start_angle2 = (-self.angle + pi) * 180 / pi
|
||||||
extent2 = 120 + 60 * sin(-self.angle + pi / 2)
|
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):
|
def _draw_canvas_counter_arc(self):
|
||||||
"""Draws the counter arc animation using canvas methods."""
|
"""Draws the counter arc animation using canvas methods."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
|
|
||||||
radius_outer = min(center_x, center_y) * 0.8
|
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
|
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
|
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
|
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):
|
def _draw_canvas_blink(self):
|
||||||
"""Draws the blink animation using canvas methods."""
|
"""Draws the blink animation using canvas methods."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
radius = min(center_x, center_y) * 0.8
|
||||||
alpha = (sin(self.angle * 2) + 1) / 2 # Slower blinking speed
|
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])
|
r = int(
|
||||||
g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||||
b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
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}"
|
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):
|
def _draw_pillow_frame(self):
|
||||||
"""Draws a frame using Pillow for anti-aliased graphics."""
|
"""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:
|
if self.pulse_animation:
|
||||||
self._draw_pillow_pulse()
|
self._draw_pillow_pulse()
|
||||||
elif self.animation_type == "line":
|
elif self.animation_type == "line":
|
||||||
@@ -596,7 +703,8 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
elif self.animation_type == "blink":
|
elif self.animation_type == "blink":
|
||||||
self._draw_pillow_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.photo_image = ImageTk.PhotoImage(resized_image)
|
||||||
self.delete("all")
|
self.delete("all")
|
||||||
self.create_image(0, 0, anchor="nw", image=self.photo_image)
|
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."""
|
"""Draws the pulse animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
alpha = (sin(self.angle * 5) + 1) / 2 # Faster pulse
|
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])
|
r = int(
|
||||||
g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||||
b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
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)
|
pulse_color = (r, g, b)
|
||||||
|
|
||||||
if self.animation_type == "line":
|
if self.animation_type == "line":
|
||||||
@@ -617,18 +728,24 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
start_y = center_y + sin(angle) * (self.height * 0.8)
|
start_y = center_y + sin(angle) * (self.height * 0.8)
|
||||||
end_x = center_x + cos(angle) * (self.width * 1.6)
|
end_x = center_x + cos(angle) * (self.width * 1.6)
|
||||||
end_y = center_y + sin(angle) * (self.height * 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":
|
elif self.animation_type == "double_arc":
|
||||||
radius = min(center_x, center_y) * 0.8
|
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)
|
self.draw.arc(bbox, start=0, end=360, fill=pulse_color, width=5)
|
||||||
elif self.animation_type == "counter_arc":
|
elif self.animation_type == "counter_arc":
|
||||||
radius_outer = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.draw.arc(bbox_outer, start=0, end=360, fill=pulse_color, width=7)
|
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
|
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,
|
||||||
self.draw.arc(bbox_inner, start=0, end=360, fill=self.color_rgb, width=7)
|
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):
|
def _draw_pillow_line(self):
|
||||||
"""Draws the line animation using Pillow."""
|
"""Draws the line animation using Pillow."""
|
||||||
@@ -641,51 +758,66 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
end_y = center_y + sin(angle) * (self.height * 1.6)
|
end_y = center_y + sin(angle) * (self.height * 1.6)
|
||||||
alpha = (cos(self.angle * 2.5 + i * (pi / 6)) + 1) / 2
|
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])
|
r = int(
|
||||||
g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||||
b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
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)
|
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):
|
def _draw_pillow_double_arc(self):
|
||||||
"""Draws the double arc animation using Pillow."""
|
"""Draws the double arc animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
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
|
start_angle1 = self.angle * 180 / pi
|
||||||
extent1 = 120 + 60 * sin(self.angle)
|
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
|
start_angle2 = (self.angle + pi) * 180 / pi
|
||||||
extent2 = 120 + 60 * sin(self.angle + pi / 2)
|
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):
|
def _draw_pillow_counter_arc(self):
|
||||||
"""Draws the counter arc animation using Pillow."""
|
"""Draws the counter arc animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
|
|
||||||
radius_outer = min(center_x, center_y) * 0.8
|
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
|
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
|
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
|
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):
|
def _draw_pillow_blink(self):
|
||||||
"""Draws the blink animation using Pillow."""
|
"""Draws the blink animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
radius = min(center_x, center_y) * 0.8
|
||||||
alpha = (sin(self.angle * 2) + 1) / 2 # Slower blinking speed
|
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])
|
r = int(
|
||||||
g = int(alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||||
b = int(alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
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)
|
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):
|
def _draw_stopped_frame(self):
|
||||||
"""Draws the icon in its stopped (static) state."""
|
"""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)
|
start_y = center_y + sin(angle) * (self.height * 0.2)
|
||||||
end_x = center_x + cos(angle) * (self.width * 0.4)
|
end_x = center_x + cos(angle) * (self.width * 0.4)
|
||||||
end_y = center_y + sin(angle) * (self.height * 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):
|
def _draw_canvas_double_arc_stopped(self):
|
||||||
"""Draws the stopped state for the double arc animation."""
|
"""Draws the stopped state for the double arc animation."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.create_arc(bbox, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=2)
|
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):
|
def _draw_canvas_counter_arc_stopped(self):
|
||||||
"""Draws the stopped state for the counter arc animation."""
|
"""Draws the stopped state for the counter arc animation."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
radius_outer = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.create_arc(bbox_outer, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=2)
|
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
|
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,
|
||||||
self.create_arc(bbox_inner, start=0, extent=359.9, style=tk.ARC, outline=self.color, width=2)
|
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):
|
def _draw_canvas_blink_stopped(self):
|
||||||
"""Draws the stopped state for the blink animation."""
|
"""Draws the stopped state for the blink animation."""
|
||||||
center_x, center_y = self.width / 2, self.height / 2
|
center_x, center_y = self.width / 2, self.height / 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
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):
|
def _draw_pillow_stopped_frame(self):
|
||||||
"""Draws the stopped state using Pillow."""
|
"""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":
|
if self.animation_type == "line":
|
||||||
self._draw_pillow_line_stopped()
|
self._draw_pillow_line_stopped()
|
||||||
elif self.animation_type == "double_arc":
|
elif self.animation_type == "double_arc":
|
||||||
@@ -752,7 +893,8 @@ class AnimatedIcon(tk.Canvas):
|
|||||||
elif self.animation_type == "blink":
|
elif self.animation_type == "blink":
|
||||||
self._draw_pillow_blink_stopped()
|
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.photo_image = ImageTk.PhotoImage(resized_image)
|
||||||
self.create_image(0, 0, anchor="nw", image=self.photo_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)
|
start_y = center_y + sin(angle) * (self.height * 0.8)
|
||||||
end_x = center_x + cos(angle) * (self.width * 1.6)
|
end_x = center_x + cos(angle) * (self.width * 1.6)
|
||||||
end_y = center_y + sin(angle) * (self.height * 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):
|
def _draw_pillow_double_arc_stopped(self):
|
||||||
"""Draws the stopped state for the double arc animation using Pillow."""
|
"""Draws the stopped state for the double arc animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.draw.arc(bbox, start=0, end=360, fill=self.highlight_color_rgb, width=5)
|
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):
|
def _draw_pillow_counter_arc_stopped(self):
|
||||||
"""Draws the stopped state for the counter arc animation using Pillow."""
|
"""Draws the stopped state for the counter arc animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
radius_outer = min(center_x, center_y) * 0.8
|
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,
|
||||||
self.draw.arc(bbox_outer, start=0, end=360, fill=self.highlight_color_rgb, width=7)
|
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
|
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,
|
||||||
self.draw.arc(bbox_inner, start=0, end=360, fill=self.color_rgb, width=7)
|
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):
|
def _draw_pillow_blink_stopped(self):
|
||||||
"""Draws the stopped state for the blink animation using Pillow."""
|
"""Draws the stopped state for the blink animation using Pillow."""
|
||||||
center_x, center_y = self.width * 2, self.height * 2
|
center_x, center_y = self.width * 2, self.height * 2
|
||||||
radius = min(center_x, center_y) * 0.8
|
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):
|
def _animate(self):
|
||||||
"""The main animation loop."""
|
"""The main animation loop."""
|
||||||
|
|||||||
513
common_tools.py
513
common_tools.py
@@ -1,16 +1,21 @@
|
|||||||
""" Classes Method and Functions for lx Apps """
|
" Classes Method and Functions for lx Apps "
|
||||||
|
|
||||||
import logging
|
|
||||||
import signal
|
import signal
|
||||||
import base64
|
import base64
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from .logger import app_logger
|
||||||
from subprocess import CompletedProcess, run
|
from subprocess import CompletedProcess, run
|
||||||
|
import gettext
|
||||||
|
import locale
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import shutil
|
import shutil
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
import os
|
||||||
from typing import Optional, Dict, Any, NoReturn
|
from typing import Optional, Dict, Any, NoReturn
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tkinter import Toplevel
|
|
||||||
|
|
||||||
|
|
||||||
class CryptoUtil:
|
class CryptoUtil:
|
||||||
@@ -34,14 +39,14 @@ class CryptoUtil:
|
|||||||
|
|
||||||
# Output from Openssl Error
|
# Output from Openssl Error
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
logging.error(process.stderr, exc_info=True)
|
app_logger.log(process.stderr)
|
||||||
|
|
||||||
if process.returncode == 0:
|
if process.returncode == 0:
|
||||||
logging.info("Files successfully decrypted...", exc_info=True)
|
app_logger.log("Files successfully decrypted...")
|
||||||
else:
|
else:
|
||||||
|
|
||||||
logging.error(
|
app_logger.log(
|
||||||
f"Error process decrypt: Code {process.returncode}", exc_info=True
|
f"Error process decrypt: Code {process.returncode}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -58,13 +63,13 @@ class CryptoUtil:
|
|||||||
|
|
||||||
# Output from Openssl Error
|
# Output from Openssl Error
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
logging.error(process.stderr, exc_info=True)
|
app_logger.log(process.stderr)
|
||||||
|
|
||||||
if process.returncode == 0:
|
if process.returncode == 0:
|
||||||
logging.info("Files successfully encrypted...", exc_info=True)
|
app_logger.log("Files successfully encrypted...")
|
||||||
else:
|
else:
|
||||||
logging.error(
|
app_logger.log(
|
||||||
f"Error process encrypt: Code {process.returncode}", exc_info=True
|
f"Error process encrypt: Code {process.returncode}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -83,9 +88,8 @@ class CryptoUtil:
|
|||||||
return True
|
return True
|
||||||
elif "False" in process.stdout:
|
elif "False" in process.stdout:
|
||||||
return False
|
return False
|
||||||
logging.error(
|
app_logger.log(
|
||||||
f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}",
|
f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}"
|
||||||
exc_info=True,
|
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -110,7 +114,7 @@ class CryptoUtil:
|
|||||||
if len(decoded) != 32: # 32 bytes = 256 bits
|
if len(decoded) != 32: # 32 bytes = 256 bits
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
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 False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -273,17 +277,16 @@ class LxTools:
|
|||||||
# End program for certain signals, report to others only reception
|
# End program for certain signals, report to others only reception
|
||||||
if signum in (signal.SIGINT, signal.SIGTERM):
|
if signum in (signal.SIGINT, signal.SIGTERM):
|
||||||
exit_code: int = 1
|
exit_code: int = 1
|
||||||
logging.error(
|
app_logger.log(
|
||||||
f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}.",
|
f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}."
|
||||||
exc_info=True,
|
|
||||||
)
|
)
|
||||||
LxTools.clean_files(file_path, file)
|
LxTools.clean_files(file_path, file)
|
||||||
logging.info("Breakdown by user...")
|
app_logger.log("Breakdown by user...")
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
else:
|
else:
|
||||||
logging.info(f"Signal {signum} received and ignored.")
|
app_logger.log(f"Signal {signum} received and ignored.")
|
||||||
LxTools.clean_files(file_path, file)
|
LxTools.clean_files(file_path, file)
|
||||||
logging.error("Process unexpectedly ended...")
|
app_logger.log("Process unexpectedly ended...")
|
||||||
|
|
||||||
# Register signal handlers for various signals
|
# Register signal handlers for various signals
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
@@ -324,14 +327,14 @@ class ConfigManager:
|
|||||||
"""Load the config file and return the config as dict"""
|
"""Load the config file and return the config as dict"""
|
||||||
if not cls._config:
|
if not cls._config:
|
||||||
try:
|
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 = {
|
cls._config = {
|
||||||
"updates": lines[1].strip(),
|
"updates": lines[1].strip(),
|
||||||
"theme": lines[3].strip(),
|
"theme": lines[3].strip(),
|
||||||
"tooltips": lines[5].strip()
|
"tooltips": lines[5].strip()
|
||||||
== "True", # is converted here to boolean!!!
|
== "True", # is converted here to boolean!!!
|
||||||
"autostart": lines[7].strip() if len(lines) > 7 else "off",
|
"autostart": lines[7].strip() if len(lines) > 7 else "off",
|
||||||
"logfile": lines[9].strip(),
|
|
||||||
}
|
}
|
||||||
except (IndexError, FileNotFoundError):
|
except (IndexError, FileNotFoundError):
|
||||||
# DeDefault values in case of error
|
# DeDefault values in case of error
|
||||||
@@ -340,7 +343,6 @@ class ConfigManager:
|
|||||||
"theme": "light",
|
"theme": "light",
|
||||||
"tooltips": "True", # Default Value as string!
|
"tooltips": "True", # Default Value as string!
|
||||||
"autostart": "off",
|
"autostart": "off",
|
||||||
"logfile": LOG_FILE_PATH,
|
|
||||||
}
|
}
|
||||||
return cls._config
|
return cls._config
|
||||||
|
|
||||||
@@ -357,8 +359,6 @@ class ConfigManager:
|
|||||||
f"{str(cls._config['tooltips'])}\n",
|
f"{str(cls._config['tooltips'])}\n",
|
||||||
"# Autostart\n",
|
"# Autostart\n",
|
||||||
f"{cls._config['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")
|
Path(cls._config_file).write_text("".join(lines), encoding="utf-8")
|
||||||
|
|
||||||
@@ -396,110 +396,435 @@ class ThemeManager:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def change_theme(root, theme_in_use, theme_name=None):
|
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)
|
root.tk.call("set_theme", theme_in_use)
|
||||||
if theme_in_use == theme_name:
|
if theme_in_use == theme_name:
|
||||||
ConfigManager.set("theme", theme_in_use)
|
ConfigManager.set("theme", theme_in_use)
|
||||||
|
|
||||||
|
|
||||||
class Tooltip:
|
class Tooltip:
|
||||||
"""Class for Tooltip
|
"""
|
||||||
from common_tools.py import Tooltip
|
A flexible tooltip class for Tkinter widgets that supports dynamic activation/deactivation.
|
||||||
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)
|
|
||||||
|
|
||||||
info: label and button are parent widgets.
|
This class provides customizable tooltips that appear when the mouse hovers over a widget.
|
||||||
NOTE: When using with state_var, pass the tk.BooleanVar object directly,
|
It can be used for simple, always-active tooltips or for tooltips whose visibility is
|
||||||
NOT its value. For example: use state_var=my_bool_var, NOT state_var=my_bool_var.get()
|
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__(
|
def __init__(self, widget, text, wraplength=250, state_var=None):
|
||||||
self,
|
self.widget = widget
|
||||||
widget: Any,
|
self.text = text
|
||||||
text: str,
|
self.wraplength = wraplength
|
||||||
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
|
|
||||||
self.state_var = state_var
|
self.state_var = state_var
|
||||||
self.x_offset = x_offset
|
self.tooltip_window = None
|
||||||
self.y_offset = y_offset
|
self.id = None
|
||||||
|
|
||||||
# Initial binding based on the current state
|
|
||||||
self.update_bindings()
|
self.update_bindings()
|
||||||
|
if self.state_var:
|
||||||
# Add trace to the state_var if provided
|
|
||||||
if self.state_var is not None:
|
|
||||||
self.state_var.trace_add("write", self.update_bindings)
|
self.state_var.trace_add("write", self.update_bindings)
|
||||||
|
|
||||||
def update_bindings(self, *args) -> None:
|
def update_bindings(self, *args):
|
||||||
"""Updates the bindings based on the current state"""
|
"""
|
||||||
# Remove existing bindings first
|
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("<Enter>")
|
||||||
self.widget.unbind("<Leave>")
|
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():
|
if self.state_var is None or self.state_var.get():
|
||||||
self.widget.bind("<Enter>", self.show_tooltip)
|
self.widget.bind("<Enter>", self.enter)
|
||||||
self.widget.bind("<Leave>", self.hide_tooltip)
|
self.widget.bind("<Leave>", self.leave)
|
||||||
|
self.widget.bind("<ButtonPress>", self.leave)
|
||||||
|
|
||||||
def show_tooltip(self, event: Optional[Any] = None) -> None:
|
def enter(self, event=None):
|
||||||
"""Shows the tooltip"""
|
"""
|
||||||
|
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:
|
if self.tooltip_window or not self.text:
|
||||||
return
|
return
|
||||||
|
x, y, _, _ = self.widget.bbox("insert")
|
||||||
x: int
|
x += self.widget.winfo_rootx() + 25
|
||||||
y: int
|
y += self.widget.winfo_rooty() + 20
|
||||||
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
|
|
||||||
|
|
||||||
self.tooltip_window = tw = tk.Toplevel(self.widget)
|
self.tooltip_window = tw = tk.Toplevel(self.widget)
|
||||||
tw.wm_overrideredirect(True)
|
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(
|
def hide_tooltip(self):
|
||||||
tw,
|
"""
|
||||||
text=self.text,
|
Hides and destroys the tooltip window if it is currently visible.
|
||||||
background="lightgreen",
|
"""
|
||||||
foreground="black",
|
tw = self.tooltip_window
|
||||||
relief="solid",
|
self.tooltip_window = None
|
||||||
borderwidth=1,
|
if tw:
|
||||||
padx=5,
|
tw.destroy()
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class LogConfig:
|
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
|
@staticmethod
|
||||||
def logger(file_path) -> None:
|
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(
|
file_handler = logging.FileHandler(
|
||||||
filename=f"{file_path}",
|
filename=f"{file_path}",
|
||||||
mode="a",
|
mode="a",
|
||||||
encoding="utf-8",
|
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.setFormatter(formatter)
|
||||||
file_handler.setLevel(logging.DEBUG)
|
file_handler.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(logging.DEBUG) # Set the root logger level
|
||||||
logger.addHandler(file_handler)
|
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()
|
||||||
|
|||||||
1
custom_file_dialog/__init__.py
Normal file
1
custom_file_dialog/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .custom_file_dialog import CustomFileDialog
|
||||||
@@ -3,47 +3,20 @@
|
|||||||
"""App configuration for Custom File Dialog"""
|
"""App configuration for Custom File Dialog"""
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
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 typing import Dict, Any, Optional, Type
|
||||||
from shared_libs.common_tools import Translate
|
from shared_libs.common_tools import Translate
|
||||||
|
|
||||||
|
|
||||||
class AppConfig:
|
# here is initializing the class for translation strings
|
||||||
"""
|
_ = Translate.setup_translations("custom_file_dialog")
|
||||||
Holds static configuration values for the application.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
SCRIPT_DIR (str): The absolute path to the directory where the script is running.
|
class CfdConfigManager:
|
||||||
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
|
Manages CFD-specific settings using a JSON file for flexibility.
|
||||||
SCRIPT_DIR: str = os.path.dirname(os.path.abspath(__file__))
|
"""
|
||||||
MAX_ITEMS_TO_DISPLAY: int = 1000
|
|
||||||
|
MAX_ITEMS_TO_DISPLAY = 1000
|
||||||
|
|
||||||
# Base paths
|
# Base paths
|
||||||
BASE_DIR: Path = Path.home()
|
BASE_DIR: Path = Path.home()
|
||||||
@@ -58,17 +31,8 @@ class AppConfig:
|
|||||||
"resizable_window": (True, True),
|
"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: 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] = {
|
_default_settings: Dict[str, Any] = {
|
||||||
"search_icon_pos": "left", # 'left' or 'right'
|
"search_icon_pos": "left", # 'left' or 'right'
|
||||||
"button_box_pos": "left", # 'left' or 'right'
|
"button_box_pos": "left", # 'left' or 'right'
|
||||||
@@ -272,233 +236,3 @@ class LocaleStrings:
|
|||||||
"videos": _("Videos"),
|
"videos": _("Videos"),
|
||||||
"computer": _("Computer"),
|
"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"),
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,8 @@ except ImportError:
|
|||||||
SEND2TRASH_AVAILABLE = False
|
SEND2TRASH_AVAILABLE = False
|
||||||
|
|
||||||
from shared_libs.message import MessageDialog
|
from shared_libs.message import MessageDialog
|
||||||
from cfd_app_config import LocaleStrings, _
|
from .cfd_app_config import LocaleStrings
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_file_dialog import CustomFileDialog
|
from custom_file_dialog import CustomFileDialog
|
||||||
@@ -163,10 +164,10 @@ class FileOperationsManager:
|
|||||||
self.dialog.context_menu.destroy()
|
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,
|
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"],
|
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(
|
self.dialog.context_menu.add_command(
|
||||||
label=LocaleStrings.UI["copy_path_to_clipboard"], command=lambda: self._copy_to_clipboard(item_path))
|
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.search_manager.hide_search_bar()
|
||||||
|
|
||||||
self.dialog.navigation_manager.navigate_to(directory)
|
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:
|
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
|
return
|
||||||
try:
|
try:
|
||||||
os.rename(item_path, new_path)
|
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:
|
except Exception as e:
|
||||||
self.dialog.widget_manager.search_status_label.config(
|
self.dialog.widget_manager.search_status_label.config(
|
||||||
text=f"{LocaleStrings.FILE['error_renaming']}: {e}")
|
text=f"{LocaleStrings.FILE['error_renaming']}: {e}")
|
||||||
self.dialog.view_manager.populate_files()
|
self.dialog.view_manager.populate_files()
|
||||||
else:
|
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:
|
def cancel_rename(event: tk.Event) -> None:
|
||||||
self.dialog.view_manager.populate_files()
|
self.dialog.view_manager.populate_files()
|
||||||
@@ -317,17 +321,20 @@ class FileOperationsManager:
|
|||||||
if os.path.exists(new_path):
|
if os.path.exists(new_path):
|
||||||
self.dialog.widget_manager.search_status_label.config(
|
self.dialog.widget_manager.search_status_label.config(
|
||||||
text=f"'{new_name}' {LocaleStrings.FILE['folder_exists_error']}")
|
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:
|
else:
|
||||||
try:
|
try:
|
||||||
os.rename(old_path, new_path)
|
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:
|
except Exception as e:
|
||||||
self.dialog.widget_manager.search_status_label.config(
|
self.dialog.widget_manager.search_status_label.config(
|
||||||
text=f"{LocaleStrings.FILE['error_renaming']}: {e}")
|
text=f"{LocaleStrings.FILE['error_renaming']}: {e}")
|
||||||
self.dialog.view_manager.populate_files()
|
self.dialog.view_manager.populate_files()
|
||||||
else:
|
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()
|
entry.destroy()
|
||||||
|
|
||||||
def cancel_rename(event: tk.Event) -> None:
|
def cancel_rename(event: tk.Event) -> None:
|
||||||
@@ -336,4 +343,3 @@ class FileOperationsManager:
|
|||||||
entry.bind("<Return>", finish_rename)
|
entry.bind("<Return>", finish_rename)
|
||||||
entry.bind("<FocusOut>", finish_rename)
|
entry.bind("<FocusOut>", finish_rename)
|
||||||
entry.bind("<Escape>", cancel_rename)
|
entry.bind("<Escape>", cancel_rename)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from cfd_app_config import LocaleStrings, _
|
from .cfd_app_config import LocaleStrings
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_file_dialog import CustomFileDialog
|
from custom_file_dialog import CustomFileDialog
|
||||||
@@ -76,12 +76,14 @@ class NavigationManager:
|
|||||||
|
|
||||||
self.dialog.widget_manager.search_animation.stop()
|
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.update_nav_buttons()
|
||||||
self.dialog.update_status_bar()
|
self.dialog.update_status_bar()
|
||||||
self.dialog.update_action_buttons_state()
|
self.dialog.update_action_buttons_state()
|
||||||
except Exception as e:
|
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:
|
def go_back(self) -> None:
|
||||||
"""Navigates to the previous directory in the history."""
|
"""Navigates to the previous directory in the history."""
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from tkinter import ttk
|
|||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from shared_libs.message import MessageDialog
|
from shared_libs.message import MessageDialog
|
||||||
from cfd_ui_setup import get_xdg_user_dir
|
from .cfd_ui_setup import get_xdg_user_dir
|
||||||
from cfd_app_config import LocaleStrings, _
|
from .cfd_app_config import LocaleStrings
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_file_dialog import CustomFileDialog
|
from custom_file_dialog import CustomFileDialog
|
||||||
@@ -41,7 +41,8 @@ class SearchManager:
|
|||||||
if self.dialog.search_thread and self.dialog.search_thread.is_alive():
|
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_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:
|
else:
|
||||||
self.execute_search()
|
self.execute_search()
|
||||||
|
|
||||||
@@ -83,10 +84,12 @@ class SearchManager:
|
|||||||
if not search_term:
|
if not search_term:
|
||||||
self.hide_search_bar()
|
self.hide_search_bar()
|
||||||
return
|
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.widget_manager.search_animation.start(pulse=False)
|
||||||
self.dialog.update_idletasks()
|
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()
|
self.dialog.search_thread.start()
|
||||||
|
|
||||||
def _perform_search_in_thread(self, search_term: str) -> None:
|
def _perform_search_in_thread(self, search_term: str) -> None:
|
||||||
@@ -104,13 +107,16 @@ class SearchManager:
|
|||||||
search_dirs = [self.dialog.current_dir]
|
search_dirs = [self.dialog.current_dir]
|
||||||
home_dir = os.path.expanduser("~")
|
home_dir = os.path.expanduser("~")
|
||||||
if os.path.abspath(self.dialog.current_dir) == os.path.abspath(home_dir):
|
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")]]
|
xdg_dirs = [get_xdg_user_dir(d, f) for d, f in [("XDG_DOWNLOAD_DIR", "Downloads"), ("XDG_DOCUMENTS_DIR", "Documents"), (
|
||||||
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_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:
|
try:
|
||||||
all_files = []
|
all_files = []
|
||||||
is_recursive = self.dialog.settings.get("recursive_search", True)
|
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()
|
search_term_lower = search_term.lower()
|
||||||
|
|
||||||
for search_dir in search_dirs:
|
for search_dir in search_dirs:
|
||||||
@@ -125,10 +131,12 @@ class SearchManager:
|
|||||||
if is_recursive:
|
if is_recursive:
|
||||||
for root, dirs, files in os.walk(search_dir, followlinks=follow_links):
|
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()):
|
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:
|
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('.')]
|
files = [f for f in files if not f.startswith('.')]
|
||||||
|
|
||||||
for name in files:
|
for name in files:
|
||||||
@@ -140,7 +148,8 @@ class SearchManager:
|
|||||||
else:
|
else:
|
||||||
for name in os.listdir(search_dir):
|
for name in os.listdir(search_dir):
|
||||||
if not (self.dialog.search_thread and self.dialog.search_thread.is_alive()):
|
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('.'):
|
if not search_hidden and name.startswith('.'):
|
||||||
continue
|
continue
|
||||||
@@ -158,15 +167,18 @@ class SearchManager:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not (self.dialog.search_thread and self.dialog.search_thread.is_alive()):
|
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()
|
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:
|
def update_ui() -> None:
|
||||||
if self.dialog.search_results:
|
if self.dialog.search_results:
|
||||||
self.show_search_results_treeview()
|
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
|
file_count = len(self.dialog.search_results) - folder_count
|
||||||
self.dialog.widget_manager.search_status_label.config(
|
self.dialog.widget_manager.search_status_label.config(
|
||||||
text=f"{folder_count} {LocaleStrings.UI['folders_and']} {file_count} {LocaleStrings.UI['files_found']}")
|
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:
|
except (Exception, InterruptedError) as e:
|
||||||
if isinstance(e, (InterruptedError, subprocess.SubprocessError)):
|
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:
|
else:
|
||||||
self.dialog.after(0, lambda: MessageDialog(
|
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())
|
message_type="error", text=f"{LocaleStrings.UI['error_during_search']}: {e}", title=LocaleStrings.UI["search_error"], master=self.dialog).show())
|
||||||
finally:
|
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
|
self.dialog.search_process = None
|
||||||
|
|
||||||
def show_search_results_treeview(self) -> None:
|
def show_search_results_treeview(self) -> None:
|
||||||
@@ -199,13 +213,17 @@ class SearchManager:
|
|||||||
search_tree = ttk.Treeview(
|
search_tree = ttk.Treeview(
|
||||||
tree_frame, columns=columns, show="tree headings")
|
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.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.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.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)
|
search_tree.column("modified", anchor="w", width=160, stretch=False)
|
||||||
|
|
||||||
v_scrollbar = ttk.Scrollbar(
|
v_scrollbar = ttk.Scrollbar(
|
||||||
@@ -268,7 +286,8 @@ class SearchManager:
|
|||||||
directory = item['values'][0]
|
directory = item['values'][0]
|
||||||
|
|
||||||
self.hide_search_bar()
|
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)
|
search_tree.bind("<Double-1>", on_search_double_click)
|
||||||
|
|
||||||
@@ -304,4 +323,5 @@ class SearchManager:
|
|||||||
self.hide_search_bar()
|
self.hide_search_bar()
|
||||||
self.dialog.navigation_manager.navigate_to(directory)
|
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))
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import tkinter as tk
|
|||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from cfd_app_config import CfdConfigManager, LocaleStrings, _
|
from .cfd_app_config import CfdConfigManager, LocaleStrings
|
||||||
from animated_icon import PIL_AVAILABLE
|
from shared_libs.animated_icon import PIL_AVAILABLE
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_file_dialog import CustomFileDialog
|
from custom_file_dialog import CustomFileDialog
|
||||||
@@ -120,7 +120,7 @@ class SettingsDialog(tk.Toplevel):
|
|||||||
main_frame, text=LocaleStrings.SET["animation_settings"], padding=10)
|
main_frame, text=LocaleStrings.SET["animation_settings"], padding=10)
|
||||||
pillow_frame.pack(fill="x", pady=5)
|
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']})",
|
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")
|
self.use_pillow_animation_checkbutton.pack(anchor="w")
|
||||||
if not PIL_AVAILABLE:
|
if not PIL_AVAILABLE:
|
||||||
self.use_pillow_animation_checkbutton.config(state=tk.DISABLED)
|
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)
|
main_frame, text=LocaleStrings.SET["animation_type"], padding=10)
|
||||||
anim_type_frame.pack(fill="x", pady=5)
|
anim_type_frame.pack(fill="x", pady=5)
|
||||||
ttk.Radiobutton(anim_type_frame, text=LocaleStrings.SET["counter_arc"], variable=self.animation_type,
|
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,
|
ttk.Radiobutton(anim_type_frame, text=LocaleStrings.SET["double_arc"], variable=self.animation_type,
|
||||||
value="double_arc").pack(side="left", padx=5)
|
value="double_arc").pack(side="left", padx=5)
|
||||||
ttk.Radiobutton(anim_type_frame, text=LocaleStrings.SET["line"], variable=self.animation_type,
|
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.recursive_search.set(defaults["recursive_search"])
|
||||||
self.use_trash.set(defaults["use_trash"])
|
self.use_trash.set(defaults["use_trash"])
|
||||||
self.confirm_delete.set(defaults["confirm_delete"])
|
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"))
|
self.animation_type.set(defaults.get("animation_type", "counter_arc"))
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ if TYPE_CHECKING:
|
|||||||
from custom_file_dialog import CustomFileDialog
|
from custom_file_dialog import CustomFileDialog
|
||||||
|
|
||||||
from shared_libs.common_tools import Tooltip
|
from shared_libs.common_tools import Tooltip
|
||||||
from animated_icon import AnimatedIcon
|
from shared_libs.animated_icon import AnimatedIcon
|
||||||
from cfd_app_config import LocaleStrings, _
|
from .cfd_app_config import LocaleStrings
|
||||||
|
|
||||||
|
|
||||||
def get_xdg_user_dir(dir_key: str, fallback_name: str) -> str:
|
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:
|
class StyleManager:
|
||||||
"""Manages the visual styling of the application using ttk styles."""
|
"""Manages the visual styling of the application using ttk styles."""
|
||||||
|
|
||||||
def __init__(self, dialog: 'CustomFileDialog') -> None:
|
def __init__(self, dialog: 'CustomFileDialog') -> None:
|
||||||
"""
|
"""
|
||||||
Initializes the StyleManager.
|
Initializes the StyleManager.
|
||||||
@@ -140,6 +141,7 @@ class StyleManager:
|
|||||||
|
|
||||||
class WidgetManager:
|
class WidgetManager:
|
||||||
"""Manages the creation, layout, and management of all widgets in the dialog."""
|
"""Manages the creation, layout, and management of all widgets in the dialog."""
|
||||||
|
|
||||||
def __init__(self, dialog: 'CustomFileDialog', settings: Dict[str, Any]) -> None:
|
def __init__(self, dialog: 'CustomFileDialog', settings: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes the WidgetManager.
|
Initializes the WidgetManager.
|
||||||
@@ -155,7 +157,8 @@ class WidgetManager:
|
|||||||
|
|
||||||
def _setup_top_bar(self, parent_frame: ttk.Frame) -> None:
|
def _setup_top_bar(self, parent_frame: ttk.Frame) -> None:
|
||||||
"""Sets up the top bar with navigation and control buttons."""
|
"""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(row=0, column=0, columnspan=2, sticky="ew")
|
||||||
top_bar.grid_columnconfigure(1, weight=1)
|
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 = ttk.Frame(top_bar, style='Accent.TFrame')
|
||||||
path_search_container.grid(row=0, column=1, sticky="ew")
|
path_search_container.grid(row=0, column=1, sticky="ew")
|
||||||
self.path_entry = ttk.Entry(path_search_container)
|
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")
|
search_icon_pos = self.settings.get("search_icon_pos", "left")
|
||||||
if search_icon_pos == 'left':
|
if search_icon_pos == 'left':
|
||||||
path_search_container.grid_columnconfigure(1, weight=1)
|
path_search_container.grid_columnconfigure(1, weight=1)
|
||||||
self.path_entry.grid(row=0, column=1, sticky="ew")
|
self.path_entry.grid(row=0, column=1, sticky="ew")
|
||||||
else: # right
|
else: # right
|
||||||
path_search_container.grid_columnconfigure(0, weight=1)
|
path_search_container.grid_columnconfigure(0, weight=1)
|
||||||
self.path_entry.grid(row=0, column=0, sticky="ew")
|
self.path_entry.grid(row=0, column=0, sticky="ew")
|
||||||
|
|
||||||
# Right controls
|
# Right controls
|
||||||
right_controls_container = ttk.Frame(top_bar, style='Accent.TFrame')
|
right_controls_container = ttk.Frame(top_bar, style='Accent.TFrame')
|
||||||
right_controls_container.grid(row=0, column=2, sticky="e")
|
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.responsive_buttons_container.pack(side="left")
|
||||||
|
|
||||||
self.new_folder_button = ttk.Button(self.responsive_buttons_container, image=self.dialog.icon_manager.get_icon(
|
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_folder_button.config(state=tk.DISABLED)
|
||||||
self.new_file_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.view_switch.pack(side="left")
|
||||||
self.icon_view_button = ttk.Button(self.view_switch, image=self.dialog.icon_manager.get_icon(
|
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")
|
'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(
|
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")
|
'hide'), command=self.dialog.view_manager.toggle_hidden_files, style="Header.TButton.Borderless.Round")
|
||||||
self.hidden_files_button.pack(side="left", padx=10)
|
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="...",
|
self.more_button = ttk.Button(right_controls_container, text="...",
|
||||||
command=self.dialog.show_more_menu, style="Header.TButton.Borderless.Round", width=3)
|
command=self.dialog.show_more_menu, style="Header.TButton.Borderless.Round", width=3)
|
||||||
|
|
||||||
def _setup_sidebar(self, parent_paned_window: ttk.PanedWindow) -> None:
|
def _setup_sidebar(self, parent_paned_window: ttk.PanedWindow) -> None:
|
||||||
"""Sets up the sidebar with bookmarks and devices."""
|
"""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.grid_propagate(False)
|
||||||
sidebar_frame.bind("<Configure>", self.dialog.on_sidebar_resize)
|
sidebar_frame.bind("<Configure>", self.dialog.on_sidebar_resize)
|
||||||
parent_paned_window.add(sidebar_frame, weight=0)
|
parent_paned_window.add(sidebar_frame, weight=0)
|
||||||
@@ -249,25 +257,34 @@ class WidgetManager:
|
|||||||
self._setup_sidebar_bookmarks(sidebar_frame)
|
self._setup_sidebar_bookmarks(sidebar_frame)
|
||||||
|
|
||||||
separator_color = "#a9a9a9" if self.style_manager.is_dark else "#7c7c7c"
|
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)
|
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)
|
self._setup_sidebar_storage(sidebar_frame)
|
||||||
|
|
||||||
def _setup_sidebar_bookmarks(self, sidebar_frame: ttk.Frame) -> None:
|
def _setup_sidebar_bookmarks(self, sidebar_frame: ttk.Frame) -> None:
|
||||||
"""Sets up the bookmark buttons in the sidebar."""
|
"""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_frame.grid(row=0, column=0, sticky="nsew")
|
||||||
sidebar_buttons_config = [
|
sidebar_buttons_config = [
|
||||||
{'name': LocaleStrings.NAV["computer"], 'icon': self.dialog.icon_manager.get_icon('computer_small'), 'path': '/'},
|
{'name': LocaleStrings.NAV["computer"], 'icon': self.dialog.icon_manager.get_icon(
|
||||||
{'name': LocaleStrings.NAV["downloads"], 'icon': self.dialog.icon_manager.get_icon('downloads_small'), 'path': get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads")},
|
'computer_small'), 'path': '/'},
|
||||||
{'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["downloads"], 'icon': self.dialog.icon_manager.get_icon(
|
||||||
{'name': LocaleStrings.NAV["pictures"], 'icon': self.dialog.icon_manager.get_icon('pictures_small'), 'path': get_xdg_user_dir("XDG_PICTURES_DIR", "Pictures")},
|
'downloads_small'), 'path': get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads")},
|
||||||
{'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["documents"], 'icon': self.dialog.icon_manager.get_icon(
|
||||||
{'name': LocaleStrings.NAV["videos"], 'icon': self.dialog.icon_manager.get_icon('video_small'), 'path': get_xdg_user_dir("XDG_VIDEO_DIR", "Videos")},
|
'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 = []
|
self.sidebar_buttons = []
|
||||||
for config in sidebar_buttons_config:
|
for config in sidebar_buttons_config:
|
||||||
@@ -278,7 +295,8 @@ class WidgetManager:
|
|||||||
|
|
||||||
def _setup_sidebar_devices(self, sidebar_frame: ttk.Frame) -> None:
|
def _setup_sidebar_devices(self, sidebar_frame: ttk.Frame) -> None:
|
||||||
"""Sets up the mounted devices section in the sidebar."""
|
"""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(row=2, column=0, sticky="nsew", padx=10)
|
||||||
mounted_devices_frame.grid_columnconfigure(0, weight=1)
|
mounted_devices_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
@@ -369,70 +387,93 @@ class WidgetManager:
|
|||||||
"""Sets up the storage indicator in the sidebar."""
|
"""Sets up the storage indicator in the sidebar."""
|
||||||
storage_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame")
|
storage_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame")
|
||||||
storage_frame.grid(row=5, column=0, sticky="sew", padx=10, pady=10)
|
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_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)
|
self.storage_bar.pack(fill="x", pady=(2, 5), padx=15)
|
||||||
|
|
||||||
def _setup_bottom_bar(self) -> None:
|
def _setup_bottom_bar(self) -> None:
|
||||||
"""Sets up the bottom bar including filename entry, action buttons, and status info."""
|
"""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 = ttk.Frame(
|
||||||
self.action_status_frame.grid(row=1, column=0, sticky="ew", pady=(5, 5), padx=10)
|
self.content_frame, style="AccentBottom.TFrame")
|
||||||
self.status_container = ttk.Frame(self.action_status_frame, style="AccentBottom.TFrame")
|
self.action_status_frame.grid(
|
||||||
self.left_container = ttk.Frame(self.action_status_frame, style="AccentBottom.TFrame")
|
row=1, column=0, sticky="ew", pady=(5, 5), padx=10)
|
||||||
self.center_container = ttk.Frame(self.action_status_frame, style="AccentBottom.TFrame")
|
self.status_container = ttk.Frame(
|
||||||
self.right_container = ttk.Frame(self.action_status_frame, style="AccentBottom.TFrame")
|
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_columnconfigure(1, weight=1)
|
||||||
self.action_status_frame.grid_rowconfigure(0, 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.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.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_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 = tk.Frame(
|
||||||
self.separator.grid(row=1, column=0, columnspan=3, sticky="ew", pady=(4, 0))
|
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')
|
self.status_container.grid(row=2, column=0, columnspan=3, sticky='ew')
|
||||||
|
|
||||||
# --- Define Widgets ---
|
# --- 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.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'),
|
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")
|
command=self.dialog.open_settings_dialog, style="Bottom.TButton.Borderless.Round")
|
||||||
|
|
||||||
self.search_animation = AnimatedIcon(self.status_container,
|
self.search_animation = AnimatedIcon(self.status_container,
|
||||||
width=23, height=23, bg=self.style_manager.bottom_color,
|
width=23, height=23, bg=self.style_manager.bottom_color,
|
||||||
animation_type=self.settings.get('animation_type', 'counter_arc'))
|
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.grid(
|
||||||
self.search_animation.bind("<Button-1>", lambda e: self.dialog.search_manager.activate_search())
|
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")
|
self.search_status_label.grid(row=0, column=1, sticky="w")
|
||||||
|
|
||||||
|
|
||||||
button_box_pos = self.settings.get("button_box_pos", "left")
|
button_box_pos = self.settings.get("button_box_pos", "left")
|
||||||
|
|
||||||
if self.dialog.dialog_mode == "save":
|
if self.dialog.dialog_mode == "save":
|
||||||
self.trash_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon('trash_small2'),
|
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")
|
command=self.dialog.file_op_manager.delete_selected_item, style="Bottom.TButton.Borderless.Round")
|
||||||
Tooltip(self.trash_button, LocaleStrings.UI["delete_move"])
|
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.save_button = ttk.Button(
|
||||||
self.cancel_button = ttk.Button(self.action_status_frame, text=LocaleStrings.SET["cancel_button"], command=self.dialog.on_cancel)
|
self.action_status_frame, text=LocaleStrings.SET["save_button"], command=self.dialog.on_save)
|
||||||
self.filter_combobox = ttk.Combobox(self.center_container, values=[ft[0] for ft in self.dialog.filetypes], state="readonly")
|
self.cancel_button = ttk.Button(
|
||||||
self.filter_combobox.bind("<<ComboboxSelected>>", self.dialog.view_manager.on_filter_change)
|
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.filter_combobox.set(self.dialog.filetypes[0][0])
|
||||||
|
|
||||||
self.center_container.grid_rowconfigure(0, weight=1)
|
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)
|
self._layout_bottom_buttons(button_box_pos)
|
||||||
else: # Open mode
|
else: # Open mode
|
||||||
self.open_button = ttk.Button(self.action_status_frame, text=LocaleStrings.CFD["open"], command=self.dialog.on_open)
|
self.open_button = ttk.Button(
|
||||||
self.cancel_button = ttk.Button(self.action_status_frame, text=LocaleStrings.CFD["cancel"], command=self.dialog.on_cancel)
|
self.action_status_frame, text=LocaleStrings.CFD["open"], command=self.dialog.on_open)
|
||||||
self.filter_combobox = ttk.Combobox(self.center_container, values=[ft[0] for ft in self.dialog.filetypes], state="readonly")
|
self.cancel_button = ttk.Button(
|
||||||
self.filter_combobox.bind("<<ComboboxSelected>>", self.dialog.view_manager.on_filter_change)
|
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.filter_combobox.set(self.dialog.filetypes[0][0])
|
||||||
|
|
||||||
self.center_container.grid_rowconfigure(0, weight=1)
|
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)
|
self._layout_bottom_buttons(button_box_pos)
|
||||||
|
|
||||||
@@ -453,22 +494,27 @@ class WidgetManager:
|
|||||||
|
|
||||||
# Place settings and trash buttons
|
# Place settings and trash buttons
|
||||||
if button_box_pos == 'left':
|
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":
|
if self.dialog.dialog_mode == "save":
|
||||||
self.trash_button.grid(in_=other_container, row=1, column=0, sticky="se", padx=(5, 0))
|
self.trash_button.grid(
|
||||||
else: # right
|
in_=other_container, row=1, column=0, sticky="se", padx=(5, 0))
|
||||||
self.settings_button.grid(in_=action_container, row=0, column=1, sticky="ne", 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":
|
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)
|
# Layout for the center container (filename, filter, status)
|
||||||
if button_box_pos == 'left':
|
if button_box_pos == 'left':
|
||||||
self.center_container.grid_columnconfigure(0, weight=1)
|
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))
|
self.filter_combobox.grid(
|
||||||
else: # right
|
in_=self.center_container, row=1, column=0, sticky="w", pady=(5, 0))
|
||||||
|
else: # right
|
||||||
self.center_container.grid_columnconfigure(1, weight=1)
|
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:
|
def setup_widgets(self) -> None:
|
||||||
"""Creates and arranges all widgets in the main dialog window."""
|
"""Creates and arranges all widgets in the main dialog window."""
|
||||||
@@ -492,15 +538,16 @@ class WidgetManager:
|
|||||||
|
|
||||||
self._setup_sidebar(paned_window)
|
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)
|
paned_window.add(self.content_frame, weight=1)
|
||||||
self.content_frame.grid_rowconfigure(0, weight=1)
|
self.content_frame.grid_rowconfigure(0, weight=1)
|
||||||
self.content_frame.grid_columnconfigure(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.file_list_frame.grid(row=0, column=0, sticky="nsew")
|
||||||
self.dialog.bind("<Configure>", self.dialog.on_window_resize)
|
self.dialog.bind("<Configure>", self.dialog.on_window_resize)
|
||||||
|
|
||||||
# --- Bottom Bar ---
|
# --- Bottom Bar ---
|
||||||
self._setup_bottom_bar()
|
self._setup_bottom_bar()
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ if TYPE_CHECKING:
|
|||||||
from custom_file_dialog import CustomFileDialog
|
from custom_file_dialog import CustomFileDialog
|
||||||
|
|
||||||
from shared_libs.common_tools import Tooltip
|
from shared_libs.common_tools import Tooltip
|
||||||
from cfd_app_config import AppConfig, LocaleStrings, _
|
from .cfd_app_config import CfdConfigManager, LocaleStrings
|
||||||
|
|
||||||
|
|
||||||
class ViewManager:
|
class ViewManager:
|
||||||
"""Manages the display of files and folders in list and icon views."""
|
"""Manages the display of files and folders in list and icon views."""
|
||||||
|
|
||||||
def __init__(self, dialog: 'CustomFileDialog'):
|
def __init__(self, dialog: 'CustomFileDialog'):
|
||||||
"""
|
"""
|
||||||
Initializes the ViewManager.
|
Initializes the ViewManager.
|
||||||
@@ -41,7 +43,8 @@ class ViewManager:
|
|||||||
for widget in self.dialog.widget_manager.file_list_frame.winfo_children():
|
for widget in self.dialog.widget_manager.file_list_frame.winfo_children():
|
||||||
widget.destroy()
|
widget.destroy()
|
||||||
self.dialog.widget_manager.path_entry.delete(0, tk.END)
|
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.selected_file = None
|
||||||
self.dialog.update_status_bar()
|
self.dialog.update_status_bar()
|
||||||
if self.dialog.view_mode.get() == "list":
|
if self.dialog.view_mode.get() == "list":
|
||||||
@@ -60,9 +63,9 @@ class ViewManager:
|
|||||||
items = os.listdir(self.dialog.current_dir)
|
items = os.listdir(self.dialog.current_dir)
|
||||||
num_items = len(items)
|
num_items = len(items)
|
||||||
warning_message = None
|
warning_message = None
|
||||||
if num_items > AppConfig.MAX_ITEMS_TO_DISPLAY:
|
if num_items > CfdConfigManager.MAX_ITEMS_TO_DISPLAY:
|
||||||
warning_message = f"{LocaleStrings.CFD['showing']} {AppConfig.MAX_ITEMS_TO_DISPLAY} {LocaleStrings.CFD['of']} {num_items} {LocaleStrings.CFD['entries']}."
|
warning_message = f"{LocaleStrings.CFD['showing']} {CfdConfigManager.MAX_ITEMS_TO_DISPLAY} {LocaleStrings.CFD['of']} {num_items} {LocaleStrings.CFD['entries']}."
|
||||||
items = items[:AppConfig.MAX_ITEMS_TO_DISPLAY]
|
items = items[:CfdConfigManager.MAX_ITEMS_TO_DISPLAY]
|
||||||
dirs = sorted([d for d in items if os.path.isdir(
|
dirs = sorted([d for d in items if os.path.isdir(
|
||||||
os.path.join(self.dialog.current_dir, d))], key=str.lower)
|
os.path.join(self.dialog.current_dir, d))], key=str.lower)
|
||||||
files = sorted([f for f in items if not os.path.isdir(
|
files = sorted([f for f in items if not os.path.isdir(
|
||||||
@@ -138,7 +141,8 @@ class ViewManager:
|
|||||||
item_frame = event.widget
|
item_frame = event.widget
|
||||||
while not hasattr(item_frame, 'item_path'):
|
while not hasattr(item_frame, 'item_path'):
|
||||||
item_frame = item_frame.master
|
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:
|
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.currently_loaded_count = 0
|
||||||
|
|
||||||
self.dialog.icon_canvas = tk.Canvas(self.dialog.widget_manager.file_list_frame,
|
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(
|
v_scrollbar = ttk.Scrollbar(
|
||||||
self.dialog.widget_manager.file_list_frame, orient="vertical", command=self.dialog.icon_canvas.yview)
|
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.pack(side="left", fill="both", expand=True)
|
||||||
self.dialog.icon_canvas.focus_set()
|
self.dialog.icon_canvas.focus_set()
|
||||||
v_scrollbar.pack(side="right", fill="y")
|
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(
|
self.dialog.icon_canvas.create_window(
|
||||||
(0, 0), window=container_frame, anchor="nw")
|
(0, 0), window=container_frame, anchor="nw")
|
||||||
container_frame.bind("<Configure>", lambda e: self.dialog.icon_canvas.configure(
|
container_frame.bind("<Configure>", lambda e: self.dialog.icon_canvas.configure(
|
||||||
@@ -173,7 +178,8 @@ class ViewManager:
|
|||||||
delta = -1 * int(event.delta / 120)
|
delta = -1 * int(event.delta / 120)
|
||||||
self.dialog.icon_canvas.yview_scroll(delta, "units")
|
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:
|
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]:
|
for widget in [self.dialog.icon_canvas, container_frame]:
|
||||||
widget.bind("<MouseWheel>", _on_mouse_wheel)
|
widget.bind("<MouseWheel>", _on_mouse_wheel)
|
||||||
@@ -320,21 +326,26 @@ class ViewManager:
|
|||||||
self.dialog.tree = ttk.Treeview(
|
self.dialog.tree = ttk.Treeview(
|
||||||
tree_frame, columns=columns, show="tree headings")
|
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.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.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.column("type", anchor="w", width=120, stretch=False)
|
||||||
self.dialog.tree.heading("modified", text=LocaleStrings.VIEW["date_modified"], anchor="w")
|
self.dialog.tree.heading(
|
||||||
self.dialog.tree.column("modified", anchor="w", width=160, stretch=False)
|
"modified", text=LocaleStrings.VIEW["date_modified"], anchor="w")
|
||||||
|
self.dialog.tree.column("modified", anchor="w",
|
||||||
|
width=160, stretch=False)
|
||||||
|
|
||||||
v_scrollbar = ttk.Scrollbar(
|
v_scrollbar = ttk.Scrollbar(
|
||||||
tree_frame, orient="vertical", command=self.dialog.tree.yview)
|
tree_frame, orient="vertical", command=self.dialog.tree.yview)
|
||||||
h_scrollbar = ttk.Scrollbar(
|
h_scrollbar = ttk.Scrollbar(
|
||||||
tree_frame, orient="horizontal", command=self.dialog.tree.xview)
|
tree_frame, orient="horizontal", command=self.dialog.tree.xview)
|
||||||
self.dialog.tree.configure(yscrollcommand=v_scrollbar.set,
|
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.grid(row=0, column=0, sticky='nsew')
|
||||||
self.dialog.tree.focus_set()
|
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("<Double-1>", self.on_list_double_click)
|
||||||
self.dialog.tree.bind("<<TreeviewSelect>>", self.on_list_select)
|
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)
|
self.dialog.tree.bind("<ButtonRelease-3>", self.on_list_context_menu)
|
||||||
|
|
||||||
if warning:
|
if warning:
|
||||||
@@ -359,7 +371,8 @@ class ViewManager:
|
|||||||
return
|
return
|
||||||
|
|
||||||
while self.dialog.currently_loaded_count < len(self.dialog.all_items):
|
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:
|
if item_found:
|
||||||
break
|
break
|
||||||
if not (item_to_rename or item_to_select):
|
if not (item_to_rename or item_to_select):
|
||||||
@@ -444,7 +457,8 @@ class ViewManager:
|
|||||||
self.dialog.search_manager.show_search_ready()
|
self.dialog.search_manager.show_search_ready()
|
||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
|
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:
|
def on_list_select(self, event: tk.Event) -> None:
|
||||||
"""Handles the selection of an item in the list view."""
|
"""Handles the selection of an item in the list view."""
|
||||||
@@ -533,11 +547,13 @@ class ViewManager:
|
|||||||
|
|
||||||
def scroll_to_widget() -> None:
|
def scroll_to_widget() -> None:
|
||||||
self.dialog.update_idletasks()
|
self.dialog.update_idletasks()
|
||||||
if not widget.winfo_exists(): return
|
if not widget.winfo_exists():
|
||||||
|
return
|
||||||
y = widget.winfo_y()
|
y = widget.winfo_y()
|
||||||
canvas_height = self.dialog.icon_canvas.winfo_height()
|
canvas_height = self.dialog.icon_canvas.winfo_height()
|
||||||
scroll_region = self.dialog.icon_canvas.bbox("all")
|
scroll_region = self.dialog.icon_canvas.bbox("all")
|
||||||
if not scroll_region: return
|
if not scroll_region:
|
||||||
|
return
|
||||||
|
|
||||||
scroll_height = scroll_region[3]
|
scroll_height = scroll_region[3]
|
||||||
if scroll_height > canvas_height:
|
if scroll_height > canvas_height:
|
||||||
@@ -574,7 +590,8 @@ class ViewManager:
|
|||||||
|
|
||||||
def toggle_hidden_files(self) -> None:
|
def toggle_hidden_files(self) -> None:
|
||||||
"""Toggles the visibility of hidden files and refreshes the view."""
|
"""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():
|
if self.dialog.show_hidden_files.get():
|
||||||
self.dialog.widget_manager.hidden_files_button.config(
|
self.dialog.widget_manager.hidden_files_button.config(
|
||||||
image=self.dialog.icon_manager.get_icon('unhide'))
|
image=self.dialog.icon_manager.get_icon('unhide'))
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
|
||||||
from datetime import datetime
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
import threading
|
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):
|
class CustomFileDialog(tk.Toplevel):
|
||||||
"""
|
"""
|
||||||
A custom file dialog window that provides functionalities for file selection,
|
A custom file dialog window that provides functionalities for file selection,
|
||||||
directory navigation, search, and file operations.
|
directory navigation, search, and file operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent: tk.Widget, initial_dir: Optional[str] = None,
|
def __init__(self, parent: tk.Widget, initial_dir: Optional[str] = None,
|
||||||
filetypes: Optional[List[Tuple[str, str]]] = None,
|
filetypes: Optional[List[Tuple[str, str]]] = None,
|
||||||
dialog_mode: str = "open", title: str = LocaleStrings.CFD["title"]):
|
dialog_mode: str = "open", title: str = LocaleStrings.CFD["title"]):
|
||||||
@@ -61,7 +59,8 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
self.selected_file: Optional[str] = None
|
self.selected_file: Optional[str] = None
|
||||||
self.current_dir: str = os.path.abspath(
|
self.current_dir: str = os.path.abspath(
|
||||||
initial_dir) if initial_dir else os.path.expanduser("~")
|
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.current_filter_pattern: str = self.filetypes[0][1]
|
||||||
self.history: List[str] = []
|
self.history: List[str] = []
|
||||||
self.history_pos: int = -1
|
self.history_pos: int = -1
|
||||||
@@ -83,14 +82,16 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
self.icon_manager: IconManager = IconManager()
|
self.icon_manager: IconManager = IconManager()
|
||||||
self.style_manager: StyleManager = StyleManager(self)
|
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.search_manager: SearchManager = SearchManager(self)
|
||||||
self.navigation_manager: NavigationManager = NavigationManager(self)
|
self.navigation_manager: NavigationManager = NavigationManager(self)
|
||||||
self.view_manager: ViewManager = ViewManager(self)
|
self.view_manager: ViewManager = ViewManager(self)
|
||||||
|
|
||||||
self.widget_manager: WidgetManager = WidgetManager(self, self.settings)
|
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()
|
self.update_animation_settings()
|
||||||
|
|
||||||
@@ -155,7 +156,8 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
self.navigation_manager = NavigationManager(self)
|
self.navigation_manager = NavigationManager(self)
|
||||||
self.view_manager = ViewManager(self)
|
self.view_manager = ViewManager(self)
|
||||||
self.widget_manager = WidgetManager(self, self.settings)
|
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.view_manager._update_view_mode_buttons()
|
||||||
|
|
||||||
self.responsive_buttons_hidden = None
|
self.responsive_buttons_hidden = None
|
||||||
@@ -192,10 +194,14 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
highlight_color="#5195ff",
|
highlight_color="#5195ff",
|
||||||
bg=self.style_manager.bottom_color
|
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.grid(
|
||||||
self.widget_manager.search_animation.bind("<Button-1>", lambda e: self.search_manager.activate_search())
|
row=0, column=0, sticky='w', padx=(0, 5), pady=(4, 0))
|
||||||
self.widget_manager.search_animation.bind("<Enter>", self._show_tooltip)
|
self.widget_manager.search_animation.bind(
|
||||||
self.widget_manager.search_animation.bind("<Leave>", self._hide_tooltip)
|
"<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:
|
if is_running:
|
||||||
self.widget_manager.search_animation.start()
|
self.widget_manager.search_animation.start()
|
||||||
@@ -295,7 +301,8 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
image=self.icon_manager.get_icon('list_view'), compound='left')
|
image=self.icon_manager.get_icon('list_view'), compound='left')
|
||||||
more_menu.add_separator()
|
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(
|
hidden_files_icon = self.icon_manager.get_icon(
|
||||||
'unhide') if self.show_hidden_files.get() else self.icon_manager.get_icon('hide')
|
'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,
|
more_menu.add_command(label=hidden_files_label, command=self.view_manager.toggle_hidden_files,
|
||||||
@@ -574,7 +581,8 @@ class CustomFileDialog(tk.Toplevel):
|
|||||||
self.tooltip_window = tk.Toplevel(self)
|
self.tooltip_window = tk.Toplevel(self)
|
||||||
self.tooltip_window.wm_overrideredirect(True)
|
self.tooltip_window.wm_overrideredirect(True)
|
||||||
self.tooltip_window.wm_geometry(f"+{x}+{y}")
|
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()
|
label.pack()
|
||||||
|
|
||||||
def _hide_tooltip(self, event: tk.Event) -> None:
|
def _hide_tooltip(self, event: tk.Event) -> None:
|
||||||
|
|||||||
@@ -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.
@@ -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
95
log_window.py
Normal 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
33
logger.py
Normal 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()
|
||||||
@@ -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"),
|
|
||||||
}
|
|
||||||
526
logviewer.py
526
logviewer.py
@@ -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
245
menu_bar.py
Normal 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()
|
||||||
98
message.py
98
message.py
@@ -1,12 +1,12 @@
|
|||||||
import os
|
import os
|
||||||
from typing import List, Optional, Dict
|
from typing import List, Optional
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from manager import LxTools
|
from manager import LxTools
|
||||||
except (ModuleNotFoundError, NameError):
|
except (ModuleNotFoundError, NameError):
|
||||||
from shared_libs.common_tools import LxTools
|
from shared_libs.common_tools import LxTools, IconManager
|
||||||
|
|
||||||
|
|
||||||
class MessageDialog:
|
class MessageDialog:
|
||||||
@@ -41,7 +41,6 @@ class MessageDialog:
|
|||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
_get_title(): Returns the default window title based on message type.
|
_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.
|
_on_button_click(button_text): Sets result and closes the dialog.
|
||||||
show(): Displays the dialog and waits for user response.
|
show(): Displays the dialog and waits for user response.
|
||||||
|
|
||||||
@@ -102,8 +101,6 @@ class MessageDialog:
|
|||||||
- Font and wraplength parameters enable text styling
|
- Font and wraplength parameters enable text styling
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_ICON_PATH = "/usr/share/icons/lx-icons"
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
message_type: str = "info",
|
message_type: str = "info",
|
||||||
@@ -122,7 +119,6 @@ class MessageDialog:
|
|||||||
self.master = master
|
self.master = master
|
||||||
self.result: bool = False # Default is False
|
self.result: bool = False # Default is False
|
||||||
|
|
||||||
self.icon_path = self._get_icon_path()
|
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.title = title
|
self.title = title
|
||||||
# Window creation
|
# Window creation
|
||||||
@@ -132,11 +128,35 @@ class MessageDialog:
|
|||||||
ttk.Style().configure("TButton")
|
ttk.Style().configure("TButton")
|
||||||
self.buttons_widgets = []
|
self.buttons_widgets = []
|
||||||
self.current_button_index = 0
|
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
|
# Window title and icon
|
||||||
self.window.title(self._get_title() if not self.title else self.title)
|
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
|
# Layout
|
||||||
frame = ttk.Frame(self.window)
|
frame = ttk.Frame(self.window)
|
||||||
@@ -148,7 +168,7 @@ class MessageDialog:
|
|||||||
frame.grid_columnconfigure(1, weight=3)
|
frame.grid_columnconfigure(1, weight=3)
|
||||||
|
|
||||||
# Icon and Text
|
# 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
|
pady_value = 5 if self.icon is not None else 15
|
||||||
icon_label.grid(
|
icon_label.grid(
|
||||||
row=0, column=0, padx=(20, 10), pady=(pady_value, 15), sticky="nsew"
|
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),
|
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
|
padx_value = 50 if self.icon is not None and len(
|
||||||
btn.pack(side="left" if i == 0 else "right", padx=padx_value, pady=5)
|
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
|
btn.focus_set() if i == 0 else None # Set focus on first button
|
||||||
self.buttons_widgets.append(btn)
|
self.buttons_widgets.append(btn)
|
||||||
|
|
||||||
@@ -219,60 +241,6 @@ class MessageDialog:
|
|||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
}[self.message_type]
|
}[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):
|
def _navigate_left(self):
|
||||||
if not self.buttons_widgets:
|
if not self.buttons_widgets:
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user