Files
shared_libs/menu_bar.py

246 lines
9.8 KiB
Python

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()