1
0

Kompletter Rewrite

This commit is contained in:
Akamaru
2025-09-08 17:19:22 +02:00
parent edf0012917
commit 7e377363c6
18 changed files with 1688 additions and 1052 deletions

396
gui/main_window.py Normal file
View File

@@ -0,0 +1,396 @@
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()