Kompletter Rewrite
This commit is contained in:
396
gui/main_window.py
Normal file
396
gui/main_window.py
Normal 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()
|
||||
Reference in New Issue
Block a user