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

0
gui/__init__.py Normal file
View File

351
gui/dialogs.py Normal file
View 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
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()

288
gui/modern_widgets.py Normal file
View 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="")