396 lines
16 KiB
Python
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() |