1
0
Files
Programm-Shortcut/gui/main_window.py
2025-09-08 17:19:22 +02:00

396 lines
16 KiB
Python

import tkinter as tk
from tkinter import ttk, messagebox
import subprocess
import sys
import os
import ctypes
from typing import Optional, List
from config import Config
from data.data_manager import DataManager
from utils.icon_utils import IconManager
from gui.dialogs import ProgramDialog, OptionsDialog, CategoryDialog
from gui.modern_widgets import (ModernStyle, ModernCard, ModernButton,
ModernListbox, ModernTreeview, IconButton)
from models.program import Program
class MainWindow:
def __init__(self, root: tk.Tk):
self.root = root
self.data_manager = DataManager()
self.icon_manager = IconManager()
self.sort_column: Optional[str] = None
self.sort_reverse: bool = False
self.modern_style = ModernStyle()
self._setup_window()
self._load_data()
self._create_widgets()
self._populate_program_list()
self._apply_options()
self._center_window()
def _setup_window(self):
theme = Config.get_theme()
self.root.title(Config.APP_NAME)
self.root.geometry(f"{Config.DEFAULT_WINDOW_WIDTH}x{Config.DEFAULT_WINDOW_HEIGHT}")
self.root.minsize(Config.MIN_WINDOW_WIDTH, Config.MIN_WINDOW_HEIGHT)
self.root.configure(bg=theme["bg_primary"])
# Load icon if available
try:
self.root.iconbitmap(self.icon_manager.resource_path(Config.ICON_FILE))
except:
pass
def _load_data(self):
if not self.data_manager.load_data():
messagebox.showwarning("Warnung", Config.MESSAGES["error_loading_data"].format("JSON-Fehler"))
def _create_widgets(self):
self._configure_styles()
self._create_menubar()
self._create_program_list()
def _configure_styles(self):
style = ttk.Style()
self.modern_style.configure_styles(style)
def _create_menubar(self):
# Windows-style menu bar
self.menubar = tk.Menu(self.root)
self.root.config(menu=self.menubar)
# Program menu
program_menu = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Programm", menu=program_menu)
program_menu.add_command(label="Hinzufügen...", command=self._add_program, accelerator="Ctrl+N")
program_menu.add_command(label="Bearbeiten...", command=self._edit_program, accelerator="F2")
program_menu.add_command(label="Entfernen", command=self._remove_program, accelerator="Del")
program_menu.add_separator()
program_menu.add_command(label="Starten", command=self._start_program, accelerator="Enter")
# Category menu
self.category_menu = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Kategorie", menu=self.category_menu)
self._create_category_menu(self.category_menu)
# Tools menu
tools_menu = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Extras", menu=tools_menu)
tools_menu.add_command(label="Optionen...", command=self._show_options)
# Bind keyboard shortcuts
self.root.bind("<Control-n>", lambda e: self._add_program())
self.root.bind("<F2>", lambda e: self._edit_program())
self.root.bind("<Delete>", lambda e: self._remove_program())
self.root.bind("<Return>", lambda e: self._start_program())
def _create_category_menu(self, menu):
# Add categories to menu with radio buttons
self.category_var = tk.StringVar(value="Alle")
menu.add_radiobutton(label="Alle", variable=self.category_var,
value="Alle", command=self._on_category_change)
if self.data_manager.category_manager.categories:
menu.add_separator()
for category in sorted(self.data_manager.category_manager.categories):
menu.add_radiobutton(label=category, variable=self.category_var,
value=category, command=self._on_category_change)
menu.add_separator()
menu.add_command(label="Neue Kategorie...", command=self._add_category)
menu.add_command(label="Kategorie löschen...", command=self._remove_category)
def _on_category_change(self):
self._populate_program_list()
def _update_category_menu(self):
# Clear and recreate category menu
self.category_menu.delete(0, 'end')
self._create_category_menu(self.category_menu)
def _create_program_list(self):
# Simple program list taking full window
list_frame = ttk.Frame(self.root, style="Modern.TFrame")
list_frame.pack(fill=tk.BOTH, expand=True, padx=Config.SPACING["sm"],
pady=Config.SPACING["sm"])
# Scrollbar
scrollbar = ttk.Scrollbar(list_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Program treeview
columns = ("name", "category")
show_option = "tree headings" if self.icon_manager.icon_support else "headings"
self.program_tree = ModernTreeview(list_frame, yscrollcommand=scrollbar.set,
columns=columns, show=show_option)
# Headers without emojis for clean look
self.program_tree.heading("name", text="Programm",
command=lambda: self._sort_treeview("name", False))
self.program_tree.heading("category", text="Kategorie",
command=lambda: self._sort_treeview("category", False))
# Better column sizing
self.program_tree.column("name", width=400, anchor="w", minwidth=200)
self.program_tree.column("category", width=150, anchor="w", minwidth=80)
self.program_tree.column("#0", width=24, stretch=False, anchor="w")
self.program_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.program_tree.yview)
# Bindings
self.program_tree.bind("<Double-1>", lambda event: self._start_program())
self.program_tree.bind("<Return>", lambda event: self._start_program())
def _populate_program_list(self):
# Clear existing items
for item in self.program_tree.get_children():
self.program_tree.delete(item)
# Get selected category from menu
selected_category = self.category_var.get()
# Get programs for category
if selected_category == "Alle":
programs = self.data_manager.programs
else:
programs = self.data_manager.get_programs_by_category(selected_category)
# Sort programs
sorted_programs = self._sort_programs(programs)
# Add programs to tree
for program in sorted_programs:
self._add_program_to_tree(program)
def _sort_programs(self, programs: List[Program]) -> List[Program]:
if self.sort_column == "name":
return sorted(programs, key=lambda x: x.name.lower(), reverse=self.sort_reverse)
elif self.sort_column == "category":
return sorted(programs, key=lambda x: (x.category or "").lower(), reverse=self.sort_reverse)
else:
# Default sort: category then name
return sorted(programs, key=lambda x: ((x.category or "").lower(), x.name.lower()))
def _add_program_to_tree(self, program: Program):
program_name = Config.PROGRAM_NAME_PADDING + program.name
category = program.category or ""
# Get icon if supported
icon = None
if self.icon_manager.icon_support:
icon = self.icon_manager.get_icon(program.path)
# Add to tree
if self.icon_manager.icon_support and icon:
self.program_tree.insert("", tk.END, text="",
values=(program_name, category),
tags=(program.path,), image=icon)
else:
self.program_tree.insert("", tk.END, text="",
values=(program_name, category),
tags=(program.path,))
def _sort_treeview(self, column: str, reverse: bool):
if self.sort_column == column:
reverse = not self.sort_reverse
self.sort_column = column
self.sort_reverse = reverse
self._populate_program_list()
def _get_selected_program(self) -> Optional[Program]:
selected_items = self.program_tree.selection()
if not selected_items:
return None
item_id = selected_items[0]
program_values = self.program_tree.item(item_id, "values")
program_name = program_values[0].strip()
# Find program in data
for program in self.data_manager.programs:
if program.name.strip() == program_name:
return program
return None
def _start_program(self):
program = self._get_selected_program()
if not program:
messagebox.showinfo("Information", Config.MESSAGES["no_program_selected"])
return
try:
# Set working directory to the program's directory
program_dir = os.path.dirname(program.path)
# Check if it's a batch file
is_batch_file = program.path.lower().endswith(('.bat', '.cmd'))
if sys.platform == "win32" and program.requires_admin:
try:
# For admin programs, use ShellExecuteW with working directory
result = ctypes.windll.shell32.ShellExecuteW(
None, "runas", program.path, None, program_dir, 1)
if result <= 32:
# Fallback with working directory
if is_batch_file:
subprocess.Popen(['cmd', '/c', program.path], cwd=program_dir)
else:
subprocess.Popen(program.path, cwd=program_dir)
except Exception:
# Fallback with working directory
if is_batch_file:
subprocess.Popen(['cmd', '/c', program.path], cwd=program_dir)
else:
subprocess.Popen(program.path, cwd=program_dir)
else:
# Normal start with working directory
if is_batch_file:
# Batch files need to be run through cmd
subprocess.Popen(['cmd', '/c', program.path], cwd=program_dir)
else:
# Regular executable
subprocess.Popen(program.path, cwd=program_dir)
except Exception as e:
messagebox.showerror("Fehler",
Config.MESSAGES["error_starting_program"].format(str(e)))
def _add_program(self):
dialog = ProgramDialog(self.root, "Programm hinzufügen",
self.data_manager.category_manager.categories)
program = dialog.show()
if program:
if self.data_manager.add_program(program):
self._update_category_menu()
self._populate_program_list()
def _edit_program(self):
program = self._get_selected_program()
if not program:
messagebox.showinfo("Information",
"Bitte wählen Sie ein Programm zum Bearbeiten aus.")
return
dialog = ProgramDialog(self.root, "Programm bearbeiten",
self.data_manager.category_manager.categories,
program)
updated_program = dialog.show()
if updated_program:
if self.data_manager.update_program(program.name, updated_program):
self._update_category_menu()
self._populate_program_list()
def _remove_program(self):
program = self._get_selected_program()
if not program:
messagebox.showinfo("Information",
"Bitte wählen Sie ein Programm zum Entfernen aus.")
return
if CategoryDialog.confirm_remove_program(self.root, program.name):
if self.data_manager.remove_program(program.name):
self._populate_program_list()
def _add_category(self):
category_name = CategoryDialog.add_category(self.root)
if not category_name:
return
if self.data_manager.category_manager.has_category(category_name):
messagebox.showwarning("Warnung",
Config.MESSAGES["category_exists"].format(category_name))
return
if self.data_manager.add_category(category_name):
self._update_category_menu()
self._populate_program_list()
def _remove_category(self):
# Show list of categories to delete
categories = self.data_manager.category_manager.categories
if not categories:
messagebox.showinfo("Information", "Keine Kategorien zum Löschen vorhanden.")
return
# Use custom dialog for category deletion
category = CategoryDialog.delete_category(self.root, list(categories))
if not category or category not in categories:
return
if CategoryDialog.confirm_remove_category(self.root, category):
if self.data_manager.remove_category(category):
# Reset to "Alle" if deleted category was selected
if self.category_var.get() == category:
self.category_var.set("Alle")
self._update_category_menu()
self._populate_program_list()
def _show_options(self):
dialog = OptionsDialog(self.root, self.data_manager.options)
result = dialog.show()
if result:
self.sort_column = result["default_sort_column"]
self.sort_reverse = result["default_sort_reverse"]
self.data_manager.update_options(**result)
self._apply_options()
self._populate_program_list()
def _apply_options(self):
# Apply sort settings
if self.data_manager.options.get("default_sort_column"):
self.sort_column = self.data_manager.options["default_sort_column"]
self.sort_reverse = self.data_manager.options["default_sort_reverse"]
# Apply category selection
if (self.data_manager.options.get("remember_last_category") and
self.data_manager.options.get("last_category")):
last_category = self.data_manager.options["last_category"]
# Check if category still exists
all_categories = ["Alle"] + self.data_manager.category_manager.categories
if last_category in all_categories:
self.category_var.set(last_category)
self._populate_program_list()
# Apply window size
if (self.data_manager.options.get("window_width") and
self.data_manager.options.get("window_height")):
width = self.data_manager.options["window_width"]
height = self.data_manager.options["window_height"]
self.root.geometry(f"{width}x{height}")
def _center_window(self):
self.root.update_idletasks()
width = self.root.winfo_width()
height = self.root.winfo_height()
x = (self.root.winfo_screenwidth() // 2) - (width // 2)
y = (self.root.winfo_screenheight() // 2) - (height // 2)
self.root.geometry(f"{width}x{height}+{x}+{y}")
def on_closing(self):
# Save current state
self.data_manager.update_options(
window_width=self.root.winfo_width(),
window_height=self.root.winfo_height()
)
# Save last category if option is enabled
if self.data_manager.options.get("remember_last_category"):
self.data_manager.update_options(last_category=self.category_var.get())
self.root.destroy()