Kompletter Rewrite
This commit is contained in:
0
gui/__init__.py
Normal file
0
gui/__init__.py
Normal file
351
gui/dialogs.py
Normal file
351
gui/dialogs.py
Normal file
@@ -0,0 +1,351 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, simpledialog
|
||||
import os
|
||||
import webbrowser
|
||||
from typing import Callable, Optional, Dict, Any, List
|
||||
from models.program import Program
|
||||
from config import Config
|
||||
from utils.icon_utils import IconManager
|
||||
from gui.modern_widgets import ModernStyle, ModernCard, ModernButton
|
||||
|
||||
|
||||
class BaseDialog:
|
||||
def __init__(self, parent: tk.Widget, title: str, width: int = 500, height: int = 400):
|
||||
self.parent = parent
|
||||
self.result = None
|
||||
self.modern_style = ModernStyle()
|
||||
self.icon_manager = IconManager()
|
||||
theme = Config.get_theme()
|
||||
|
||||
self.window = tk.Toplevel(parent)
|
||||
self.window.title(title)
|
||||
self.window.geometry(f"{width}x{height}")
|
||||
self.window.minsize(width, height) # Mindestgröße setzen
|
||||
self.window.resizable(True, True) # Größe änderbar machen
|
||||
self.window.grab_set()
|
||||
self.window.configure(bg=theme["bg_primary"])
|
||||
|
||||
# Set icon
|
||||
try:
|
||||
self.window.iconbitmap(self.icon_manager.resource_path(Config.ICON_FILE))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Configure modern styling
|
||||
style = ttk.Style()
|
||||
self.modern_style.configure_styles(style)
|
||||
|
||||
self._center_window()
|
||||
self._create_widgets()
|
||||
|
||||
def _center_window(self):
|
||||
self.window.update_idletasks()
|
||||
width = self.window.winfo_width()
|
||||
height = self.window.winfo_height()
|
||||
x = (self.window.winfo_screenwidth() // 2) - (width // 2)
|
||||
y = (self.window.winfo_screenheight() // 2) - (height // 2)
|
||||
self.window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
def _create_widgets(self):
|
||||
pass
|
||||
|
||||
def show(self):
|
||||
self.window.wait_window()
|
||||
return self.result
|
||||
|
||||
|
||||
class ProgramDialog(BaseDialog):
|
||||
def __init__(self, parent: tk.Widget, title: str, categories: List[str],
|
||||
program: Optional[Program] = None):
|
||||
self.categories = categories
|
||||
self.program = program
|
||||
self.is_edit = program is not None
|
||||
super().__init__(parent, title, 450, 280)
|
||||
|
||||
def _create_widgets(self):
|
||||
# Simple container
|
||||
container = ttk.Frame(self.window, padding="20")
|
||||
container.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Form fields using grid
|
||||
self._create_form_fields(container)
|
||||
|
||||
# Buttons
|
||||
self._create_buttons(container)
|
||||
|
||||
def _create_form_fields(self, parent):
|
||||
# Name field
|
||||
ttk.Label(parent, text="Programmname:").grid(row=0, column=0, sticky="w", pady=5)
|
||||
self.name_entry = ttk.Entry(parent, width=30)
|
||||
self.name_entry.grid(row=0, column=1, columnspan=2, sticky="we", pady=5)
|
||||
|
||||
# Path field
|
||||
ttk.Label(parent, text="Programmpfad:").grid(row=1, column=0, sticky="w", pady=5)
|
||||
self.path_entry = ttk.Entry(parent, width=30)
|
||||
self.path_entry.grid(row=1, column=1, sticky="we", pady=5)
|
||||
|
||||
browse_button = ttk.Button(parent, text="Durchsuchen", command=self._browse_file)
|
||||
browse_button.grid(row=1, column=2, padx=5, pady=5)
|
||||
|
||||
# Category field
|
||||
ttk.Label(parent, text="Kategorie:").grid(row=2, column=0, sticky="w", pady=5)
|
||||
self.category_combobox = ttk.Combobox(parent, width=28, values=sorted(self.categories))
|
||||
self.category_combobox.grid(row=2, column=1, columnspan=2, sticky="we", pady=5)
|
||||
|
||||
# Admin rights checkbox
|
||||
self.admin_var = tk.BooleanVar(value=False)
|
||||
admin_check = ttk.Checkbutton(parent, text="Benötigt Administratorrechte",
|
||||
variable=self.admin_var)
|
||||
admin_check.grid(row=3, column=0, columnspan=3, sticky="w", pady=5)
|
||||
|
||||
# Fill fields if editing
|
||||
if self.program:
|
||||
self.name_entry.insert(0, self.program.name)
|
||||
self.path_entry.insert(0, self.program.path)
|
||||
if self.program.category:
|
||||
self.category_combobox.set(self.program.category)
|
||||
elif self.categories:
|
||||
self.category_combobox.current(0)
|
||||
self.admin_var.set(self.program.requires_admin)
|
||||
elif self.categories:
|
||||
self.category_combobox.current(0)
|
||||
|
||||
self.name_entry.focus_set()
|
||||
|
||||
# Make column 1 expandable
|
||||
parent.columnconfigure(1, weight=1)
|
||||
|
||||
def _create_buttons(self, parent):
|
||||
# Button frame
|
||||
button_frame = ttk.Frame(parent)
|
||||
button_frame.grid(row=4, column=0, columnspan=3, pady=15)
|
||||
|
||||
ttk.Button(button_frame, text="Speichern", command=self._save).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="Abbrechen", command=self.window.destroy).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
def _browse_file(self):
|
||||
if not self.is_edit:
|
||||
self.path_entry.delete(0, tk.END)
|
||||
filename = filedialog.askopenfilename(
|
||||
filetypes=[
|
||||
("Ausführbare Dateien", "*.exe"),
|
||||
("Batch-Dateien", "*.bat;*.cmd"),
|
||||
("Alle ausführbaren Dateien", "*.exe;*.bat;*.cmd"),
|
||||
("Alle Dateien", "*.*")
|
||||
]
|
||||
)
|
||||
if filename:
|
||||
if not self.is_edit:
|
||||
self.path_entry.insert(0, filename)
|
||||
else:
|
||||
self.path_entry.delete(0, tk.END)
|
||||
self.path_entry.insert(0, filename)
|
||||
|
||||
def _save(self):
|
||||
name = self.name_entry.get().strip()
|
||||
path = self.path_entry.get().strip()
|
||||
category = self.category_combobox.get().strip()
|
||||
requires_admin = self.admin_var.get()
|
||||
|
||||
if not name or not path:
|
||||
messagebox.showwarning("Warnung", "Bitte füllen Sie alle Felder aus.")
|
||||
return
|
||||
|
||||
if not os.path.exists(path):
|
||||
messagebox.showwarning("Warnung", "Der angegebene Pfad existiert nicht.")
|
||||
return
|
||||
|
||||
self.result = Program(
|
||||
name=name,
|
||||
path=path,
|
||||
category=category if category else None,
|
||||
requires_admin=requires_admin
|
||||
)
|
||||
self.window.destroy()
|
||||
|
||||
|
||||
class OptionsDialog(BaseDialog):
|
||||
def __init__(self, parent: tk.Widget, current_options: Dict[str, Any]):
|
||||
self.current_options = current_options.copy()
|
||||
super().__init__(parent, "Programmoptionen", 400, 320)
|
||||
|
||||
def _create_widgets(self):
|
||||
# Simple container with normal padding
|
||||
container = ttk.Frame(self.window, padding="20")
|
||||
container.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Title
|
||||
title_label = ttk.Label(container, text="Programmoptionen",
|
||||
font=("Segoe UI", 12, "bold"))
|
||||
title_label.grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 15))
|
||||
|
||||
# Sort option
|
||||
ttk.Label(container, text="Standardsortierung:").grid(row=1, column=0, sticky="w", pady=5)
|
||||
self.sort_var = tk.StringVar(value=self._get_sort_option_text())
|
||||
sort_combobox = ttk.Combobox(container, textvariable=self.sort_var,
|
||||
values=Config.SORT_OPTIONS,
|
||||
width=20)
|
||||
sort_combobox.grid(row=1, column=1, sticky="we", pady=5)
|
||||
sort_combobox.configure(state="readonly")
|
||||
|
||||
# Remember category option
|
||||
self.remember_category_var = tk.BooleanVar(
|
||||
value=self.current_options.get("remember_last_category", False)
|
||||
)
|
||||
remember_category_check = ttk.Checkbutton(
|
||||
container,
|
||||
text="Letzte ausgewählte Kategorie merken",
|
||||
variable=self.remember_category_var
|
||||
)
|
||||
remember_category_check.grid(row=2, column=0, columnspan=2, sticky="w", pady=10)
|
||||
|
||||
# Separator
|
||||
separator = ttk.Separator(container, orient="horizontal")
|
||||
separator.grid(row=3, column=0, columnspan=2, sticky="ew", pady=10)
|
||||
|
||||
# Info section
|
||||
info_frame = ttk.Frame(container)
|
||||
info_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=5)
|
||||
|
||||
ttk.Label(info_frame, text=f"Version: {Config.APP_VERSION}",
|
||||
font=("Segoe UI", 9)).pack(anchor="w")
|
||||
ttk.Label(info_frame, text=f"{Config.APP_AUTHOR}",
|
||||
font=("Segoe UI", 9)).pack(anchor="w")
|
||||
ttk.Label(info_frame, text=f"{Config.APP_DESCRIPTION}",
|
||||
font=("Segoe UI", 9)).pack(anchor="w")
|
||||
|
||||
# Link
|
||||
link_label = ttk.Label(info_frame, text="Quellcode auf git.ponywave.de",
|
||||
font=("Segoe UI", 9, "underline"),
|
||||
foreground="blue", cursor="hand2")
|
||||
link_label.pack(anchor="w", pady=(5, 0))
|
||||
link_label.bind("<Button-1>", lambda e: self._open_url(Config.SOURCE_URL))
|
||||
|
||||
# Buttons
|
||||
button_frame = ttk.Frame(container)
|
||||
button_frame.grid(row=5, column=0, columnspan=2, pady=20)
|
||||
|
||||
ttk.Button(button_frame, text="Speichern", command=self._save).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="Abbrechen", command=self.window.destroy).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Make column 1 expandable
|
||||
container.columnconfigure(1, weight=1)
|
||||
|
||||
|
||||
def _get_sort_option_text(self) -> str:
|
||||
column = self.current_options.get("default_sort_column")
|
||||
reverse = self.current_options.get("default_sort_reverse", False)
|
||||
|
||||
if column == "name":
|
||||
return "Programm (Z-A)" if reverse else "Programm (A-Z)"
|
||||
elif column == "category":
|
||||
return "Kategorie (Z-A)" if reverse else "Kategorie (A-Z)"
|
||||
else:
|
||||
return "Keine"
|
||||
|
||||
def _save(self):
|
||||
sort_option = self.sort_var.get()
|
||||
remember_category = self.remember_category_var.get()
|
||||
|
||||
# Translate sort options
|
||||
if sort_option == "Programm (A-Z)":
|
||||
sort_column, sort_reverse = "name", False
|
||||
elif sort_option == "Programm (Z-A)":
|
||||
sort_column, sort_reverse = "name", True
|
||||
elif sort_option == "Kategorie (A-Z)":
|
||||
sort_column, sort_reverse = "category", False
|
||||
elif sort_option == "Kategorie (Z-A)":
|
||||
sort_column, sort_reverse = "category", True
|
||||
else: # "Keine"
|
||||
sort_column, sort_reverse = None, False
|
||||
|
||||
self.result = {
|
||||
"default_sort_column": sort_column,
|
||||
"default_sort_reverse": sort_reverse,
|
||||
"remember_last_category": remember_category
|
||||
}
|
||||
self.window.destroy()
|
||||
|
||||
def _open_url(self, url: str):
|
||||
try:
|
||||
webbrowser.open(url)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Fehler", f"Fehler beim Öffnen der URL: {str(e)}")
|
||||
|
||||
|
||||
class CategoryDialog(BaseDialog):
|
||||
def __init__(self, parent: tk.Widget, dialog_type: str = "add", categories: List[str] = None):
|
||||
self.dialog_type = dialog_type
|
||||
self.categories = categories or []
|
||||
|
||||
if dialog_type == "add":
|
||||
title = "Neue Kategorie"
|
||||
width, height = 400, 150
|
||||
else: # delete
|
||||
title = "Kategorie löschen"
|
||||
width, height = 450, 200
|
||||
|
||||
super().__init__(parent, title, width, height)
|
||||
|
||||
def _create_widgets(self):
|
||||
container = ttk.Frame(self.window, padding="20")
|
||||
container.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
if self.dialog_type == "add":
|
||||
# Label
|
||||
ttk.Label(container, text="Geben Sie den Namen der neuen Kategorie ein:").pack(pady=(0, 10))
|
||||
|
||||
# Entry
|
||||
self.name_entry = ttk.Entry(container, width=30)
|
||||
self.name_entry.pack(pady=(0, 15))
|
||||
self.name_entry.focus_set()
|
||||
|
||||
# Bind Enter key
|
||||
self.name_entry.bind("<Return>", lambda e: self._ok())
|
||||
else: # delete
|
||||
# Label
|
||||
label_text = f"Verfügbare Kategorien: {', '.join(sorted(self.categories))}\n\nWelche Kategorie soll gelöscht werden?"
|
||||
ttk.Label(container, text=label_text, wraplength=400).pack(pady=(0, 10))
|
||||
|
||||
# Combobox for category selection
|
||||
self.category_combobox = ttk.Combobox(container, values=sorted(self.categories), width=30)
|
||||
self.category_combobox.pack(pady=(0, 15))
|
||||
self.category_combobox.focus_set()
|
||||
if self.categories:
|
||||
self.category_combobox.current(0)
|
||||
|
||||
# Buttons
|
||||
button_frame = ttk.Frame(container)
|
||||
button_frame.pack()
|
||||
|
||||
ttk.Button(button_frame, text="OK", command=self._ok).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="Abbrechen", command=self.window.destroy).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
def _ok(self):
|
||||
if self.dialog_type == "add":
|
||||
self.result = self.name_entry.get().strip()
|
||||
else: # delete
|
||||
self.result = self.category_combobox.get().strip()
|
||||
self.window.destroy()
|
||||
|
||||
@staticmethod
|
||||
def add_category(parent: tk.Widget) -> Optional[str]:
|
||||
dialog = CategoryDialog(parent, "add")
|
||||
return dialog.show()
|
||||
|
||||
@staticmethod
|
||||
def delete_category(parent: tk.Widget, categories: List[str]) -> Optional[str]:
|
||||
dialog = CategoryDialog(parent, "delete", categories)
|
||||
return dialog.show()
|
||||
|
||||
@staticmethod
|
||||
def confirm_remove_category(parent: tk.Widget, category: str) -> bool:
|
||||
return messagebox.askyesno("Bestätigung",
|
||||
Config.MESSAGES["confirm_remove_category"].format(category),
|
||||
parent=parent)
|
||||
|
||||
@staticmethod
|
||||
def confirm_remove_program(parent: tk.Widget, program_name: str) -> bool:
|
||||
return messagebox.askyesno("Bestätigung",
|
||||
f"Möchten Sie '{program_name}' wirklich entfernen?",
|
||||
parent=parent)
|
||||
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()
|
||||
288
gui/modern_widgets.py
Normal file
288
gui/modern_widgets.py
Normal file
@@ -0,0 +1,288 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import Optional, Callable, Any
|
||||
from config import Config
|
||||
|
||||
|
||||
class ModernStyle:
|
||||
"""Manages modern styling for tkinter widgets"""
|
||||
|
||||
def __init__(self):
|
||||
self.theme = Config.get_theme()
|
||||
self.fonts = Config.FONTS
|
||||
self.spacing = Config.SPACING
|
||||
|
||||
def configure_styles(self, style: ttk.Style):
|
||||
"""Configure modern ttk styles"""
|
||||
theme = self.theme
|
||||
|
||||
# Configure main frame
|
||||
style.configure("Modern.TFrame",
|
||||
background=theme["bg_primary"],
|
||||
relief="flat")
|
||||
|
||||
# Configure card frames
|
||||
style.configure("Card.TFrame",
|
||||
background=theme["bg_secondary"],
|
||||
relief="flat",
|
||||
borderwidth=1)
|
||||
|
||||
# Configure modern buttons
|
||||
style.configure("Modern.TButton",
|
||||
background=theme["accent"],
|
||||
foreground=theme["text_primary"] if "dark" in Config.CURRENT_THEME else "white",
|
||||
font=self.fonts["button"],
|
||||
borderwidth=0,
|
||||
focuscolor="none",
|
||||
relief="flat",
|
||||
padding=(12, 8))
|
||||
|
||||
style.map("Modern.TButton",
|
||||
background=[("active", theme["accent_hover"]),
|
||||
("pressed", theme["accent"])],
|
||||
foreground=[("active", "white"),
|
||||
("pressed", "white")])
|
||||
|
||||
# Configure success buttons
|
||||
style.configure("Success.TButton",
|
||||
background=theme["success"],
|
||||
foreground="white",
|
||||
font=self.fonts["button"],
|
||||
borderwidth=0,
|
||||
focuscolor="none",
|
||||
relief="flat",
|
||||
padding=(12, 8))
|
||||
|
||||
style.map("Success.TButton",
|
||||
background=[("active", theme["success_hover"]),
|
||||
("pressed", theme["success"])],
|
||||
foreground=[("active", "white"),
|
||||
("pressed", "white")])
|
||||
|
||||
# Configure secondary buttons
|
||||
style.configure("Secondary.TButton",
|
||||
background=theme["bg_tertiary"],
|
||||
foreground=theme["text_primary"],
|
||||
font=self.fonts["body"],
|
||||
borderwidth=1,
|
||||
focuscolor="none",
|
||||
relief="flat",
|
||||
padding=(8, 6))
|
||||
|
||||
style.map("Secondary.TButton",
|
||||
background=[("active", theme["border"]),
|
||||
("pressed", theme["bg_tertiary"])],
|
||||
relief=[("pressed", "flat")])
|
||||
|
||||
# Configure icon buttons
|
||||
style.configure("Icon.TButton",
|
||||
background=theme["bg_tertiary"],
|
||||
foreground=theme["text_primary"],
|
||||
font=self.fonts["subheading"],
|
||||
borderwidth=1,
|
||||
focuscolor="none",
|
||||
relief="flat",
|
||||
padding=(8, 8))
|
||||
|
||||
style.map("Icon.TButton",
|
||||
background=[("active", theme["border"]),
|
||||
("pressed", theme["bg_tertiary"])],
|
||||
relief=[("pressed", "flat")])
|
||||
|
||||
# Configure labels
|
||||
style.configure("Heading.TLabel",
|
||||
background=theme["bg_primary"],
|
||||
foreground=theme["text_primary"],
|
||||
font=self.fonts["heading"])
|
||||
|
||||
style.configure("Subheading.TLabel",
|
||||
background=theme["bg_primary"],
|
||||
foreground=theme["text_secondary"],
|
||||
font=self.fonts["subheading"])
|
||||
|
||||
style.configure("Body.TLabel",
|
||||
background=theme["bg_primary"],
|
||||
foreground=theme["text_secondary"],
|
||||
font=self.fonts["body"])
|
||||
|
||||
style.configure("Caption.TLabel",
|
||||
background=theme["bg_primary"],
|
||||
foreground=theme["text_muted"],
|
||||
font=self.fonts["caption"])
|
||||
|
||||
# Configure modern treeview
|
||||
style.configure("Modern.Treeview",
|
||||
background=theme["bg_secondary"],
|
||||
foreground=theme["text_primary"],
|
||||
fieldbackground=theme["bg_secondary"],
|
||||
borderwidth=0,
|
||||
relief="flat",
|
||||
font=self.fonts["body"])
|
||||
|
||||
style.configure("Modern.Treeview.Heading",
|
||||
background=theme["bg_tertiary"],
|
||||
foreground=theme["text_primary"],
|
||||
font=self.fonts["subheading"],
|
||||
relief="flat",
|
||||
borderwidth=0)
|
||||
|
||||
style.map("Modern.Treeview",
|
||||
background=[("selected", theme["selection"])],
|
||||
foreground=[("selected", "white")])
|
||||
|
||||
style.map("Modern.Treeview.Heading",
|
||||
background=[("active", theme["border"])])
|
||||
|
||||
# Configure modern listbox (via tk styling)
|
||||
# Note: Listbox doesn't support ttk styling, will be handled in widget creation
|
||||
|
||||
# Configure modern entry
|
||||
style.configure("Modern.TEntry",
|
||||
fieldbackground=theme["bg_secondary"],
|
||||
foreground=theme["text_primary"],
|
||||
borderwidth=1,
|
||||
relief="flat",
|
||||
insertcolor=theme["text_primary"],
|
||||
font=self.fonts["body"])
|
||||
|
||||
style.map("Modern.TEntry",
|
||||
focuscolor=[("focus", theme["accent"])],
|
||||
bordercolor=[("focus", theme["accent"])])
|
||||
|
||||
# Configure modern combobox
|
||||
style.configure("Modern.TCombobox",
|
||||
fieldbackground=theme["bg_secondary"],
|
||||
foreground=theme["text_primary"],
|
||||
background=theme["bg_secondary"],
|
||||
borderwidth=1,
|
||||
relief="flat",
|
||||
font=self.fonts["body"])
|
||||
|
||||
# Configure modern checkbutton
|
||||
style.configure("Modern.TCheckbutton",
|
||||
background=theme["bg_secondary"],
|
||||
foreground=theme["text_primary"],
|
||||
font=self.fonts["body"],
|
||||
focuscolor="none")
|
||||
|
||||
style.map("Modern.TCheckbutton",
|
||||
background=[("active", theme["bg_secondary"]),
|
||||
("pressed", theme["bg_secondary"])])
|
||||
|
||||
|
||||
class ModernCard(ttk.Frame):
|
||||
"""A modern card-like container with shadow effect simulation"""
|
||||
|
||||
def __init__(self, parent, padding=None, **kwargs):
|
||||
super().__init__(parent, style="Card.TFrame",
|
||||
padding=padding or Config.CARD_PADDING, **kwargs)
|
||||
|
||||
|
||||
class ModernButton(ttk.Button):
|
||||
"""Enhanced button with modern styling and hover effects"""
|
||||
|
||||
def __init__(self, parent, text="", style_type="primary", icon=None,
|
||||
command=None, **kwargs):
|
||||
|
||||
# Determine style based on type
|
||||
if style_type == "primary":
|
||||
style = "Modern.TButton"
|
||||
elif style_type == "success":
|
||||
style = "Success.TButton"
|
||||
elif style_type == "secondary":
|
||||
style = "Secondary.TButton"
|
||||
else:
|
||||
style = "Modern.TButton"
|
||||
|
||||
super().__init__(parent, text=text, style=style, command=command, **kwargs)
|
||||
|
||||
# Add hover effects
|
||||
self.bind("<Enter>", self._on_enter)
|
||||
self.bind("<Leave>", self._on_leave)
|
||||
|
||||
def _on_enter(self, event):
|
||||
"""Handle mouse enter"""
|
||||
self.configure(cursor="hand2")
|
||||
|
||||
def _on_leave(self, event):
|
||||
"""Handle mouse leave"""
|
||||
self.configure(cursor="")
|
||||
|
||||
|
||||
class ModernListbox(tk.Listbox):
|
||||
"""Modern styled listbox with custom colors"""
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
theme = Config.get_theme()
|
||||
|
||||
# Configure modern listbox appearance
|
||||
defaults = {
|
||||
"background": theme["bg_secondary"],
|
||||
"foreground": theme["text_primary"],
|
||||
"selectbackground": theme["selection"],
|
||||
"selectforeground": "white",
|
||||
"activestyle": "none",
|
||||
"relief": "flat",
|
||||
"borderwidth": 0,
|
||||
"highlightthickness": 1,
|
||||
"highlightcolor": theme["accent"],
|
||||
"highlightbackground": theme["border"],
|
||||
"font": Config.FONTS["body"]
|
||||
}
|
||||
|
||||
# Merge with user provided kwargs
|
||||
defaults.update(kwargs)
|
||||
|
||||
super().__init__(parent, **defaults)
|
||||
|
||||
|
||||
class ModernTreeview(ttk.Treeview):
|
||||
"""Enhanced treeview with modern styling"""
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(parent, style="Modern.Treeview", **kwargs)
|
||||
|
||||
# Configure modern row height
|
||||
style = ttk.Style()
|
||||
style.configure("Modern.Treeview", rowheight=32)
|
||||
|
||||
|
||||
class ModernScrollbar(ttk.Scrollbar):
|
||||
"""Modern styled scrollbar"""
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
# Configure modern scrollbar
|
||||
style = ttk.Style()
|
||||
theme = Config.get_theme()
|
||||
|
||||
style.configure("Modern.Vertical.TScrollbar",
|
||||
background=theme["bg_tertiary"],
|
||||
troughcolor=theme["bg_primary"],
|
||||
borderwidth=0,
|
||||
arrowcolor=theme["text_muted"],
|
||||
darkcolor=theme["bg_tertiary"],
|
||||
lightcolor=theme["bg_tertiary"])
|
||||
|
||||
|
||||
class IconButton(ttk.Button):
|
||||
"""Button designed specifically for icons"""
|
||||
|
||||
def __init__(self, parent, icon_text="", command=None, **kwargs):
|
||||
super().__init__(parent, text=icon_text, style="Icon.TButton",
|
||||
command=command, **kwargs)
|
||||
|
||||
self.configure(width=3)
|
||||
|
||||
# Add hover effects
|
||||
self.bind("<Enter>", self._on_enter)
|
||||
self.bind("<Leave>", self._on_leave)
|
||||
|
||||
def _on_enter(self, event):
|
||||
"""Handle mouse enter"""
|
||||
self.configure(cursor="hand2")
|
||||
|
||||
def _on_leave(self, event):
|
||||
"""Handle mouse leave"""
|
||||
self.configure(cursor="")
|
||||
Reference in New Issue
Block a user