diff --git a/.gitignore b/.gitignore index f49cca7..aa90d30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,48 @@ +# User data data.json +program_list.json + +# Archive files Programm-Shortcut.7z + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +.venv/ +ENV/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db +*.tmp +*.log + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg diff --git a/README.md b/README.md index 608b250..706766e 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ -# Programm-Shortcut +# Programm-Shortcut v2.0 -Ein Programm-Launcher für Windows mit Kategorieverwaltung und Programm-Icons. +Ein moderner Programm-Launcher für Windows mit Kategorieverwaltung, Programm-Icons und verbesserter Benutzeroberfläche. ![Screenshot des Programm-Shortcut](screenshot.png) ## Features -- Organisiere deine Programme in benutzerdefinierten Kategorien -- Programme werden mit ihren Original-Icons angezeigt -- Sortiere Programme nach Name oder Kategorie -- Einfaches Hinzufügen, Bearbeiten und Entfernen von Programmen -- Einstellungen werden automatisch gespeichert -- Kompaktes und benutzerfreundliches Interface +- **Organisiere Programme** in benutzerdefinierten Kategorien +- **Original-Icons** werden automatisch aus den Programmen extrahiert +- **Flexible Sortierung** nach Name oder Kategorie +- **Intuitive Bedienung** mit modernem Interface +- **Automatische Speicherung** aller Einstellungen +- **Administratorrechte-Unterstützung** für Programme, die diese benötigen +- **Konsistente Icons** in allen Fenstern und Dialogen ## Installation @@ -19,7 +20,7 @@ Ein Programm-Launcher für Windows mit Kategorieverwaltung und Programm-Icons. 2. Klone das Repository oder lade es herunter 3. Installiere die erforderlichen Abhängigkeiten: -``` +```bash pip install pywin32 Pillow ``` @@ -27,37 +28,103 @@ pip install pywin32 Pillow Starte die Anwendung mit: +```bash +python main.py ``` -python run.py -``` -oder mit der start.bat + +oder verwende die mitgelieferte `start.bat` Datei. ### Programme hinzufügen: -1. Klicke auf "Hinzufügen" -2. Gebe den Programmnamen ein +1. Klicke im Menü auf "Programm" → "Hinzufügen..." oder drücke `Ctrl+N` +2. Gib den Programmnamen ein 3. Wähle den Programmpfad über "Durchsuchen" 4. Wähle eine Kategorie oder erstelle eine neue -5. Klicke auf "Speichern" +5. Aktiviere "Benötigt Administratorrechte" falls erforderlich +6. Klicke auf "Speichern" ### Programme starten: -1. Wähle eine Kategorie aus der linken Seitenleiste +1. Wähle eine Kategorie aus dem Menü "Kategorie" 2. Wähle ein Programm aus der Liste -3. Klicke auf "Starten" +3. Doppelklicke oder drücke `Enter` zum Starten -## Abhängigkeiten +### Tastaturkürzel: -- Python 3.6+ -- tkinter (in der Standardinstallation von Python enthalten) -- pywin32 (für das Extrahieren von Programm-Icons) -- Pillow (für die Bildverarbeitung) +- `Ctrl+N`: Neues Programm hinzufügen +- `F2`: Ausgewähltes Programm bearbeiten +- `Del`: Ausgewähltes Programm entfernen +- `Enter`: Ausgewähltes Programm starten + +## Architektur v2.0 + +Das Programm wurde komplett in eine modulare, wartbare Architektur überarbeitet: + +``` +├── main.py # Haupteinstiegspunkt +├── config.py # Zentrale Konfiguration und Themes +├── models/ # Datenmodelle +│ ├── program.py # Program-Klasse mit Admin-Unterstützung +│ └── category.py # CategoryManager +├── data/ # Datenverarbeitung +│ └── data_manager.py # JSON-Operationen und Migration +├── utils/ # Hilfsfunktionen +│ └── icon_utils.py # Icon-Extraktion und -Verwaltung +└── gui/ # GUI-Komponenten + ├── main_window.py # Hauptfenster mit moderner UI + ├── dialogs.py # Alle Dialog-Komponenten + └── modern_widgets.py # Moderne UI-Komponenten +``` + +## Was ist neu in v2.0 + +### 🎨 Verbessertes Design +- **Moderne Themes**: Drei verschiedene Farbschemas (Modern Dark, Modern Light, Gradient Blue) +- **Konsistente Icons**: Alle Fenster und Dialoge verwenden das Anwendungsicon +- **Verbesserte Typography**: Professionelle Schriftarten und Größen +- **Responsive Layout**: Bessere Anpassung an verschiedene Fenstergrößen + +### 🔧 Technische Verbesserungen +- **Eigene Dialog-Klassen**: Alle System-Dialoge wurden durch eigene Implementierungen ersetzt +- **Bessere Icon-Behandlung**: Optimierte Icon-Extraktion und -Caching +- **Robuste Pfad-Auflösung**: Verbesserte Kompatibilität mit WSL und verschiedenen Umgebungen +- **Admin-Rechte-Unterstützung**: Programme können als Administrator gestartet werden + +### 🏗️ Architektur-Verbesserungen +- **Separation of Concerns**: Klare Trennung von GUI, Daten und Geschäftslogik +- **Modulare Komponenten**: Wiederverwendbare UI-Elemente +- **Erweiterte Konfiguration**: Zentrale Theme- und Einstellungsverwaltung +- **Bessere Testbarkeit**: Kleinere, fokussierte Module + +### Vorteile der neuen Architektur: +- **Wartbarkeit**: Übersichtliche Codestruktur mit klaren Verantwortlichkeiten +- **Erweiterbarkeit**: Einfache Integration neuer Features +- **Stabilität**: Robuste Fehlerbehandlung und Validierung +- **Performance**: Optimierte Icon-Verwaltung und UI-Updates + +## Migration von v1.x + +Die Migration erfolgt automatisch beim ersten Start von v2.0: +- Bestehende `data.json` Dateien werden automatisch geladen +- Alte `program_list.json` Dateien werden migriert +- Alle Benutzereinstellungen bleiben erhalten +- Keine manuelle Konfiguration erforderlich + +## Systemanforderungen + +- **Python**: 3.6 oder höher +- **Betriebssystem**: Windows (Icon-Unterstützung), Linux/WSL (grundlegende Funktionalität) +- **Abhängigkeiten**: + - `tkinter` (standardmäßig in Python enthalten) + - `pywin32` (für Icon-Extraktion unter Windows) + - `Pillow` (für Bildverarbeitung) ## Hinweise -- Icon-Unterstützung funktioniert nur unter Windows -- Ohne die optionalen Abhängigkeiten (pywin32 und Pillow) funktioniert das Programm weiterhin, jedoch ohne Icon-Anzeige +- **Icon-Unterstützung**: Funktioniert vollständig nur unter Windows +- **WSL-Kompatibilität**: Grundlegende Funktionalität verfügbar +- **Fallback-Modus**: Ohne optionale Abhängigkeiten läuft das Programm ohne Icons -## Info +## Entwicklung -- Erstellt mit Hilfe von Claude \ No newline at end of file +Erstellt mit Hilfe von Claude AI und kontinuierlich weiterentwickelt. \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..a510f57 --- /dev/null +++ b/config.py @@ -0,0 +1,165 @@ +from typing import Dict, Any + + +class Config: + # Application settings + APP_NAME = "Programm-Shortcut" + APP_VERSION = "2.0" + APP_AUTHOR = "© 2025 Akamaru" + APP_DESCRIPTION = "Erstellt mit Hilfe von Claude" + SOURCE_URL = "https://git.ponywave.de/Akamaru/Programm-Shortcut" + + # Window settings + DEFAULT_WINDOW_WIDTH = 700 + DEFAULT_WINDOW_HEIGHT = 550 + MIN_WINDOW_WIDTH = 600 + MIN_WINDOW_HEIGHT = 450 + + # GUI settings + TREEVIEW_ROW_HEIGHT = 24 + CATEGORY_LIST_HEIGHT = 15 + CATEGORY_LIST_WIDTH = 15 + PROGRAM_NAME_PADDING = " " # 3 spaces for icon alignment + + # Icon settings + ICON_SIZE = (16, 16) + ICON_FILE = "icon.ico" + + # File settings + DATA_FILE = "data.json" + LEGACY_FILE = "program_list.json" + + + # Modern color themes + THEMES = { + "modern_dark": { + "bg_primary": "#2b2b2b", + "bg_secondary": "#3c3c3c", + "bg_tertiary": "#4a4a4a", + "accent": "#007acc", + "accent_hover": "#1e90ff", + "success": "#28a745", + "success_hover": "#34ce57", + "danger": "#dc3545", + "text_primary": "#ffffff", + "text_secondary": "#cccccc", + "text_muted": "#888888", + "border": "#555555", + "selection": "#007acc", + "selection_hover": "#1e90ff" + }, + "modern_light": { + "bg_primary": "#ffffff", + "bg_secondary": "#f8f9fa", + "bg_tertiary": "#e9ecef", + "accent": "#007acc", + "accent_hover": "#1e90ff", + "success": "#28a745", + "success_hover": "#34ce57", + "danger": "#dc3545", + "text_primary": "#212529", + "text_secondary": "#495057", + "text_muted": "#6c757d", + "border": "#dee2e6", + "selection": "#007acc", + "selection_hover": "#1e90ff" + }, + "gradient_blue": { + "bg_primary": "#f0f2f5", + "bg_secondary": "#ffffff", + "bg_tertiary": "#e3f2fd", + "accent": "#1976d2", + "accent_hover": "#1565c0", + "success": "#4caf50", + "success_hover": "#43a047", + "danger": "#f44336", + "text_primary": "#263238", + "text_secondary": "#546e7a", + "text_muted": "#90a4ae", + "border": "#cfd8dc", + "selection": "#1976d2", + "selection_hover": "#1565c0" + } + } + + # Current theme (can be changed) + CURRENT_THEME = "gradient_blue" + + @classmethod + def get_theme(cls) -> dict: + return cls.THEMES[cls.CURRENT_THEME] + + # Modern typography + FONTS = { + "heading": ("Segoe UI", 18, "bold"), + "subheading": ("Segoe UI", 14, "bold"), + "body": ("Segoe UI", 11), + "body_small": ("Segoe UI", 10), + "caption": ("Segoe UI", 9), + "button": ("Segoe UI", 10, "bold"), + "link": ("Segoe UI", 10, "underline") + } + + # Compact UI Spacing + SPACING = { + "xs": 2, + "sm": 4, + "md": 6, + "lg": 8, + "xl": 12, + "xxl": 16 + } + + # Compact button styles + BUTTON_STYLES = { + "primary": {"style": "primary", "width": 80, "padding": (6, 4)}, + "secondary": {"style": "secondary", "width": 70, "padding": (4, 3)}, + "small": {"style": "small", "width": 24, "padding": (2, 2)}, + "icon": {"style": "icon", "width": 28, "padding": (3, 3)} + } + + # Border radius for modern look + BORDER_RADIUS = 8 + + # Compact card styling + CARD_PADDING = 8 + CARD_SHADOW = {"x": 0, "y": 1, "blur": 3} + + # Sort options + SORT_OPTIONS = [ + "Keine", + "Programm (A-Z)", + "Programm (Z-A)", + "Kategorie (A-Z)", + "Kategorie (Z-A)" + ] + + # Messages + MESSAGES = { + "no_program_selected": "Bitte wählen Sie ein Programm aus.", + "no_category_selected": "Bitte wählen Sie eine Kategorie zum Entfernen aus.", + "cannot_remove_all": "Die Kategorie 'Alle' kann nicht entfernt werden.", + "fill_all_fields": "Bitte füllen Sie alle Felder aus.", + "path_not_exists": "Der angegebene Pfad existiert nicht.", + "category_exists": "Die Kategorie '{}' existiert bereits.", + "program_not_found": "Programm '{}' konnte nicht gefunden werden.", + "error_starting_program": "Fehler beim Starten des Programms: {}", + "error_loading_data": "Fehler beim Laden der Daten: {}\nEine neue Liste wird erstellt.", + "error_saving_data": "Fehler beim Speichern der Daten: {}", + "error_opening_url": "Fehler beim Öffnen der URL: {}", + "icon_libraries_missing": "Warnung: Die Bibliotheken für Programm-Icons fehlen. Installieren Sie diese mit: pip install pywin32 Pillow", + "confirm_remove_category": "Möchten Sie die Kategorie '{}' wirklich entfernen?\n\nAlle Programme in dieser Kategorie werden ohne Kategorie gespeichert.", + "confirm_remove_program": "Möchten Sie '{}' wirklich entfernen?", + "help_text": "Wählen Sie ein Programm zum Starten oder fügen Sie ein neues hinzu" + } + + @classmethod + def get_default_options(cls) -> Dict[str, Any]: + return { + "default_sort_column": None, + "default_sort_reverse": False, + "remember_last_category": False, + "last_category": "Alle", + "window_width": cls.DEFAULT_WINDOW_WIDTH, + "window_height": cls.DEFAULT_WINDOW_HEIGHT + } \ No newline at end of file diff --git a/data/__init__.py b/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data/data_manager.py b/data/data_manager.py new file mode 100644 index 0000000..1ea2624 --- /dev/null +++ b/data/data_manager.py @@ -0,0 +1,153 @@ +import json +import os +from typing import List, Dict, Any +from models.program import Program +from models.category import CategoryManager + + +class DataManager: + def __init__(self, json_file: str = 'data.json'): + self.json_file = json_file + self.category_manager = CategoryManager() + self.programs: List[Program] = [] + self.options: Dict[str, Any] = self._get_default_options() + + def _get_default_options(self) -> Dict[str, Any]: + return { + "default_sort_column": None, + "default_sort_reverse": False, + "remember_last_category": False, + "last_category": "Alle", + "window_width": 700, + "window_height": 550 + } + + def load_data(self) -> bool: + try: + # Handle legacy file migration + if not os.path.exists(self.json_file) and os.path.exists('program_list.json'): + self._migrate_from_legacy_file() + return True + + if not os.path.exists(self.json_file): + self._create_empty_file() + return True + + with open(self.json_file, encoding='utf-8') as f: + data = json.load(f) + + # Handle old format without options + if "options" not in data: + data["options"] = self._get_default_options() + + self._load_from_data(data) + return True + + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Error loading data: {e}") + self._create_empty_file() + return False + + def _migrate_from_legacy_file(self): + with open('program_list.json', encoding='utf-8') as f: + old_data = json.load(f) + + if isinstance(old_data, list): + # Old format: just a list of programs + # Extract categories from programs + categories = [] + for program_data in old_data: + if "Kategorie" in program_data and program_data["Kategorie"] not in categories: + categories.append(program_data["Kategorie"]) + + data = { + "categories": categories, + "programs": old_data, + "options": self._get_default_options() + } + else: + # Newer format but without options + old_data["options"] = self._get_default_options() + data = old_data + + self._load_from_data(data) + self.save_data() + + def _create_empty_file(self): + data = { + "categories": [], + "programs": [], + "options": self._get_default_options() + } + self._load_from_data(data) + self.save_data() + + def _load_from_data(self, data: Dict[str, Any]): + self.category_manager.load_from_data(data.get("categories", [])) + self.programs = [Program.from_dict(p) for p in data.get("programs", [])] + self.options = data.get("options", self._get_default_options()) + + def save_data(self) -> bool: + try: + data = { + "categories": self.category_manager.to_list(), + "programs": [p.to_dict() for p in self.programs], + "options": self.options + } + + with open(self.json_file, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + return True + + except Exception as e: + print(f"Error saving data: {e}") + return False + + def add_program(self, program: Program) -> bool: + if program.category and not self.category_manager.has_category(program.category): + self.category_manager.add_category(program.category) + + self.programs.append(program) + return self.save_data() + + def remove_program(self, program_name: str) -> bool: + for i, program in enumerate(self.programs): + if program.name.strip() == program_name.strip(): + del self.programs[i] + return self.save_data() + return False + + def update_program(self, old_name: str, updated_program: Program) -> bool: + for i, program in enumerate(self.programs): + if program.name.strip() == old_name.strip(): + if updated_program.category and not self.category_manager.has_category(updated_program.category): + self.category_manager.add_category(updated_program.category) + + self.programs[i] = updated_program + return self.save_data() + return False + + def get_programs_by_category(self, category: str = None) -> List[Program]: + if not category or category == "Alle": + return self.programs + return [p for p in self.programs if p.category == category] + + def add_category(self, category: str) -> bool: + if self.category_manager.add_category(category): + return self.save_data() + return False + + def remove_category(self, category: str) -> bool: + if not self.category_manager.remove_category(category): + return False + + # Remove category from all programs + for program in self.programs: + if program.category == category: + program.category = None + + return self.save_data() + + def update_options(self, **kwargs): + self.options.update(kwargs) + return self.save_data() \ No newline at end of file diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/dialogs.py b/gui/dialogs.py new file mode 100644 index 0000000..0982700 --- /dev/null +++ b/gui/dialogs.py @@ -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("", 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("", 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) \ No newline at end of file diff --git a/gui/main_window.py b/gui/main_window.py new file mode 100644 index 0000000..787c127 --- /dev/null +++ b/gui/main_window.py @@ -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("", lambda e: self._add_program()) + self.root.bind("", lambda e: self._edit_program()) + self.root.bind("", lambda e: self._remove_program()) + self.root.bind("", 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("", lambda event: self._start_program()) + self.program_tree.bind("", 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() \ No newline at end of file diff --git a/gui/modern_widgets.py b/gui/modern_widgets.py new file mode 100644 index 0000000..b4c2bf4 --- /dev/null +++ b/gui/modern_widgets.py @@ -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("", self._on_enter) + self.bind("", 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("", self._on_enter) + self.bind("", 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="") \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..a7f78a9 --- /dev/null +++ b/main.py @@ -0,0 +1,32 @@ +import tkinter as tk +import sys +import os + +# Add current directory to Python path for imports +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from gui.main_window import MainWindow +from utils.icon_utils import IconManager +from config import Config + + +def main(): + root = tk.Tk() + + # Set icon for main window + icon_manager = IconManager() + try: + root.iconbitmap(icon_manager.resource_path(Config.ICON_FILE)) + except: + pass + + app = MainWindow(root) + + # Handle window closing + root.protocol("WM_DELETE_WINDOW", app.on_closing) + + root.mainloop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/category.py b/models/category.py new file mode 100644 index 0000000..8af45bf --- /dev/null +++ b/models/category.py @@ -0,0 +1,31 @@ +from typing import List, Set + + +class CategoryManager: + def __init__(self): + self._categories: Set[str] = set() + + @property + def categories(self) -> List[str]: + return sorted(list(self._categories)) + + def add_category(self, category: str) -> bool: + if not category or category in self._categories: + return False + self._categories.add(category) + return True + + def remove_category(self, category: str) -> bool: + if category not in self._categories: + return False + self._categories.remove(category) + return True + + def has_category(self, category: str) -> bool: + return category in self._categories + + def load_from_data(self, categories: List[str]): + self._categories = set(categories) + + def to_list(self) -> List[str]: + return self.categories \ No newline at end of file diff --git a/models/program.py b/models/program.py new file mode 100644 index 0000000..bf5de7f --- /dev/null +++ b/models/program.py @@ -0,0 +1,39 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Program: + name: str + path: str + category: Optional[str] = None + requires_admin: bool = False + + def __post_init__(self): + if not self.name: + raise ValueError("Program name cannot be empty") + if not self.path: + raise ValueError("Program path cannot be empty") + + def to_dict(self) -> dict: + result = { + "Name": self.name, + "Pfad": self.path + } + if self.category: + result["Kategorie"] = self.category + if self.requires_admin: + result["Adminrechte"] = True + return result + + @classmethod + def from_dict(cls, data: dict) -> 'Program': + return cls( + name=data["Name"], + path=data["Pfad"], + category=data.get("Kategorie"), + requires_admin=data.get("Adminrechte", False) + ) + + def __str__(self) -> str: + return f"{self.name} ({self.category or 'Ohne Kategorie'})" \ No newline at end of file diff --git a/run.py b/run.py deleted file mode 100644 index 7248e4f..0000000 --- a/run.py +++ /dev/null @@ -1,1025 +0,0 @@ -import tkinter as tk -from tkinter import ttk, filedialog, messagebox, simpledialog -import json -import subprocess -import os -import sys - -# Versuche die Icon-Bibliotheken zu importieren -try: - import win32ui - import win32gui - import win32con - import win32api - from PIL import Image, ImageTk - ICON_SUPPORT = True -except ImportError: - # Wir können messagebox hier nicht verwenden, da es aus tkinter importiert wird - print("Warnung: Die Bibliotheken für Programm-Icons fehlen. Installieren Sie diese mit: pip install pywin32 Pillow") - ICON_SUPPORT = False - -def resource_path(relative_path): - """Get absolute path to resource, works for dev and for PyInstaller""" - try: - # PyInstaller creates a temp folder and stores path in _MEIPASS - base_path = sys._MEIPASS - except Exception: - base_path = os.path.abspath(".") - return os.path.join(base_path, relative_path) - -def extract_icon_from_exe(exe_path): - """Extrahiert das Icon aus einer EXE-Datei""" - if not ICON_SUPPORT: - return None - - try: - # Icon-Handle erhalten - large, small = win32gui.ExtractIconEx(exe_path, 0) - - # Wir verwenden das größere Icon - if large: - # Wir nehmen das erste Icon - ico_x = win32api.GetSystemMetrics(win32con.SM_CXICON) - ico_y = win32api.GetSystemMetrics(win32con.SM_CYICON) - - # Icon in DC (Device Context) zeichnen - hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0)) - hbmp = win32ui.CreateBitmap() - hbmp.CreateCompatibleBitmap(hdc, ico_x, ico_y) - hdc = hdc.CreateCompatibleDC() - - hdc.SelectObject(hbmp) - hdc.DrawIcon((0, 0), large[0]) - - # Bitmap in ein Python-Image-Objekt umwandeln - bmpinfo = hbmp.GetInfo() - bmpstr = hbmp.GetBitmapBits(True) - img = Image.frombuffer( - 'RGBA', - (bmpinfo['bmWidth'], bmpinfo['bmHeight']), - bmpstr, 'raw', 'BGRA', 0, 1 - ) - - # Aufräumen - win32gui.DestroyIcon(large[0]) - for icon in small: - if icon: - win32gui.DestroyIcon(icon) - - # Bild auf die gewünschte Größe skalieren - img = img.resize((16, 16), Image.LANCZOS) - - # In PhotoImage umwandeln für Tkinter - return ImageTk.PhotoImage(img) - return None - except Exception as e: - print(f"Fehler beim Extrahieren des Icons: {str(e)}") - return None - -class ProgramLauncher: - def __init__(self, root): - self.root = root - self.root.title("Programm-Shortcut") - self.root.geometry("700x550") - self.root.minsize(600, 450) - self.root.configure(bg="#f0f0f0") - - # Lade das Symbol, falls vorhanden - try: - self.root.iconbitmap(resource_path("icon.ico")) - except: - pass - - # Initialisiere Standardwerte - self.programs = [] - self.categories = ["Spiele", "Werkzeuge", "Office", "Multimedia", "Internet"] - self.options = self.get_default_options() - self.sort_column = None - self.sort_reverse = False - - # Icon-Cache initialisieren - self.icon_cache = {} - - # Laden der Daten aus der JSON-Datei - self.json_file = 'data.json' - self.load_data() - - self.create_widgets() - self.populate_program_list() - - # Wende gespeicherte Optionen an - self.apply_options() - - # Zentriere das Fenster - self.center_window() - - 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 load_data(self): - """Lädt Programme und Kategorien aus der JSON-Datei""" - try: - # Prüfen, ob die alte Datei existiert und umbenannt werden muss - if not os.path.exists(self.json_file) and os.path.exists('program_list.json'): - # Wandle altes in neues Format um und speichere es - with open('program_list.json', encoding='utf-8') as f: - old_data = json.load(f) - - # Wenn es sich um das alte Format handelt (nur Liste von Programmen) - if isinstance(old_data, list): - self.data = { - "categories": self.categories, - "programs": old_data, - "options": self.get_default_options() - } - # Kategorien aus Programmen extrahieren und mit Standardkategorien zusammenführen - for program in old_data: - if "Kategorie" in program and program["Kategorie"] not in self.data["categories"]: - self.data["categories"].append(program["Kategorie"]) - else: - # Wenn es bereits das neue Format hat, füge nur Optionen hinzu - old_data["options"] = self.get_default_options() - self.data = old_data - - # Speichere in neuer Datei - with open(self.json_file, 'w', encoding='utf-8') as f: - json.dump(self.data, f, indent=2, ensure_ascii=False) - elif not os.path.exists(self.json_file): - # Erstelle eine neue Datei mit leeren Werten und Standardoptionen - self.data = { - "categories": self.categories, - "programs": [], - "options": self.get_default_options() - } - self.save_data() - else: - # Datei laden - with open(self.json_file, encoding='utf-8') as f: - data = json.load(f) - - # Überprüfe, ob es sich um das alte Format handelt (ohne options) - if "options" not in data: - data["options"] = self.get_default_options() - - # Verwende das neue Format - self.data = data - - # Aktualisiere die Werte für den einfacheren Zugriff - self.programs = self.data["programs"] - self.categories = self.data["categories"] - self.options = self.data.get("options", self.get_default_options()) - - except (FileNotFoundError, json.JSONDecodeError) as e: - messagebox.showwarning("Warnung", f"Fehler beim Laden der Daten: {str(e)}\nEine neue Liste wird erstellt.") - self.data = { - "categories": self.categories, - "programs": [], - "options": self.get_default_options() - } - # Speichere die neue Liste - self.save_data() - - # Stelle sicher, dass die Variablen immer richtig gesetzt sind - self.programs = self.data["programs"] - self.categories = self.data["categories"] - self.options = self.data["options"] - - def get_default_options(self): - """Gibt Standardoptionen zurück""" - return { - "default_sort_column": None, - "default_sort_reverse": False, - "remember_last_category": False, - "last_category": "Alle", - "window_width": 700, - "window_height": 550 - } - - def apply_options(self): - """Wendet gespeicherte Optionen an""" - # Wenn eine Standardsortierung gespeichert ist, anwenden - if self.options.get("default_sort_column"): - self.sort_column = self.options["default_sort_column"] - self.sort_reverse = self.options["default_sort_reverse"] - self.populate_program_list() - - # Wenn die letzte Kategorie gespeichert werden soll, auswählen - if self.options.get("remember_last_category") and self.options.get("last_category"): - # Finde die Kategorie in der Listbox - for i in range(self.category_listbox.size()): - if self.category_listbox.get(i) == self.options["last_category"]: - self.category_listbox.selection_clear(0, tk.END) - self.category_listbox.selection_set(i) - self.on_category_select(None) - break - - # Fenstergröße anwenden, falls gespeichert - if self.options.get("window_width") and self.options.get("window_height"): - self.root.geometry(f"{self.options['window_width']}x{self.options['window_height']}") - - def save_data(self): - """Speichert Programme und Kategorien in der JSON-Datei""" - try: - # Aktuelle Optionen aktualisieren - if hasattr(self, 'sort_column'): - self.options["default_sort_column"] = self.sort_column - self.options["default_sort_reverse"] = self.sort_reverse - - # Fenstergröße speichern - self.options["window_width"] = self.root.winfo_width() - self.options["window_height"] = self.root.winfo_height() - - # Aktuelle Kategorie speichern, wenn Option aktiviert ist - if self.options.get("remember_last_category"): - try: - index = self.category_listbox.curselection()[0] - self.options["last_category"] = self.category_listbox.get(index) - except (IndexError, AttributeError): - pass - - # Stelle sicher, dass die Optionen im Hauptdatenobjekt aktualisiert werden - self.data["options"] = self.options - - # In Datei speichern - with open(self.json_file, 'w', encoding='utf-8') as f: - json.dump(self.data, f, indent=2, ensure_ascii=False) - - # Debug: Dateiinhalt überprüfen - # print(f"Saved options: {self.options}") - except Exception as e: - messagebox.showerror("Fehler", f"Fehler beim Speichern der Daten: {str(e)}") - - def create_widgets(self): - # Hauptframe - main_frame = ttk.Frame(self.root, padding="10") - main_frame.pack(fill=tk.BOTH, expand=True) - - # Stil konfigurieren - style = ttk.Style() - style.configure("TFrame", background="#f0f0f0") - style.configure("TButton", background="#4CAF50", foreground="black", padding=5) - style.configure("TLabel", background="#f0f0f0", foreground="#333333", font=("Segoe UI", 10)) - style.configure("Title.TLabel", background="#f0f0f0", foreground="#333333", font=("Segoe UI", 14, "bold")) - style.configure("Category.TLabel", background="#f0f0f0", foreground="#555555", font=("Segoe UI", 12, "bold")) - - # Titel - title_label = ttk.Label(main_frame, text="Programm-Shortcut", style="Title.TLabel") - title_label.pack(pady=10) - - # Erklärungstext - help_text = ttk.Label(main_frame, text="Wählen Sie ein Programm zum Starten oder fügen Sie ein neues hinzu") - help_text.pack(pady=5) - - # Horizontaler Frame für Kategorieauswahl und Programmliste - h_frame = ttk.Frame(main_frame) - h_frame.pack(fill=tk.BOTH, expand=True, pady=10) - - # Linker Frame für Kategorien (vertikal ausgerichtet) - category_frame = ttk.Frame(h_frame, padding="5") - category_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) - - # Container für Kategorien-Bereich als vertikalen Stack - category_content = ttk.Frame(category_frame) - category_content.pack(fill=tk.BOTH, expand=True) - - # Kategorie-Label - cat_label = ttk.Label(category_content, text="Kategorien", style="Category.TLabel") - cat_label.pack(pady=(0, 5), anchor="w") - - # Container für Listbox und Scrollbar - list_container = ttk.Frame(category_content) - list_container.pack(fill=tk.BOTH, expand=True) - - # Scrollbar für Kategorieliste - cat_scrollbar = ttk.Scrollbar(list_container) - cat_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - # Listbox für Kategorien - self.category_listbox = tk.Listbox(list_container, height=15, width=15, - yscrollcommand=cat_scrollbar.set, - font=("Segoe UI", 10), - selectbackground="#4CAF50", - activestyle="none", - exportselection=0) - self.category_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - cat_scrollbar.config(command=self.category_listbox.yview) - - # Füllen der Kategorie-Listbox - for category in sorted(self.categories): - self.category_listbox.insert(tk.END, category) - - # Standardmäßig "Alle" auswählen - self.category_listbox.insert(0, "Alle") - self.category_listbox.selection_set(0) - self.category_listbox.bind("<>", self.on_category_select) - - # Kategorie-Buttons-Container erstellen (unterhalb der Listbox) - button_container = ttk.Frame(category_content) - button_container.pack(fill=tk.X, pady=10) - - # Container für die Buttons, damit sie zentriert werden können - buttons_center = ttk.Frame(button_container) - buttons_center.pack(anchor=tk.CENTER) - - # Die Buttons nebeneinander anordnen - self.add_cat_button = ttk.Button(buttons_center, text="+", width=3, command=self.add_category) - self.add_cat_button.pack(side=tk.LEFT, padx=5) - - self.remove_cat_button = ttk.Button(buttons_center, text="-", width=3, command=self.remove_category) - self.remove_cat_button.pack(side=tk.LEFT, padx=5) - - # Rechter Frame für Programme - program_frame = ttk.Frame(h_frame) - program_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - - # Frame für Programmliste und Scrollen - list_frame = ttk.Frame(program_frame) - list_frame.pack(fill=tk.BOTH, expand=True) - - # Scrollbar für die Treeview - scrollbar = ttk.Scrollbar(list_frame) - scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - # Anzeige von Icons aktivieren und Platz dafür schaffen - style.configure("Treeview", rowheight=24) # Mehr Platz für Icons - - # Statt ein Zellenpadding zu verwenden, verwenden wir einen zusätzlichen Abstand im Text - style.configure("Treeview.Cell", padding=(0, 0)) - - # Treeview für Programme - # Wenn Icon-Unterstützung aktiviert ist, verwenden wir ein anderes Format - if ICON_SUPPORT: - self.program_tree = ttk.Treeview(list_frame, yscrollcommand=scrollbar.set, - columns=("name", "category"), - show="tree headings") - else: - self.program_tree = ttk.Treeview(list_frame, yscrollcommand=scrollbar.set, - columns=("name", "category"), - show="headings") - - # Setze Spaltenüberschriften mit Sortierfunktion - 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)) - - # Konfiguriere Spaltenbreiten und Ausrichtung - self.program_tree.column("name", width=220, anchor="w", minwidth=150) - self.program_tree.column("category", width=100, anchor="w") - self.program_tree.column("#0", width=30, stretch=False, anchor="w") # Icon-Spalte - - self.program_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - - scrollbar.config(command=self.program_tree.yview) - - # Frame für Buttons - button_frame = ttk.Frame(main_frame) - button_frame.pack(fill=tk.X, pady=10) - - # Buttons - self.start_button = ttk.Button(button_frame, text="Starten", command=self.start_program) - self.start_button.pack(side=tk.LEFT, padx=5) - - self.add_button = ttk.Button(button_frame, text="Hinzufügen", command=self.add_program) - self.add_button.pack(side=tk.LEFT, padx=5) - - self.remove_button = ttk.Button(button_frame, text="Entfernen", command=self.remove_program) - self.remove_button.pack(side=tk.LEFT, padx=5) - - self.edit_button = ttk.Button(button_frame, text="Bearbeiten", command=self.edit_program) - self.edit_button.pack(side=tk.LEFT, padx=5) - - self.options_button = ttk.Button(button_frame, text="Optionen", command=self.show_options_dialog) - self.options_button.pack(side=tk.RIGHT, padx=5) - - # Doppelklick auf einen Eintrag - self.program_tree.bind("", lambda event: self.start_program()) - - # Sortierungszustand - self.sort_column = self.options.get("default_sort_column") - self.sort_reverse = self.options.get("default_sort_reverse", False) - - def show_options_dialog(self): - """Zeigt einen Dialog mit Programmoptionen an""" - options_window = tk.Toplevel(self.root) - options_window.title("Programmoptionen") - options_window.geometry("450x350") - options_window.resizable(False, False) - options_window.grab_set() # Modal machen - - # Zentriere das Dialogfenster - options_window.update_idletasks() - width = options_window.winfo_width() - height = options_window.winfo_height() - x = (options_window.winfo_screenwidth() // 2) - (width // 2) - y = (options_window.winfo_screenheight() // 2) - (height // 2) - options_window.geometry(f"{width}x{height}+{x}+{y}") - - # Frame - frame = ttk.Frame(options_window, padding="20") - frame.pack(fill=tk.BOTH, expand=True) - - # Überschrift - ttk.Label(frame, text="Programmoptionen", font=("Segoe UI", 12, "bold")).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 15)) - - # Option für Standardsortierung - ttk.Label(frame, text="Standardsortierung:").grid(row=1, column=0, sticky="w", pady=5) - sort_var = tk.StringVar(value=self.get_sort_option_text()) - sort_combobox = ttk.Combobox(frame, textvariable=sort_var, values=[ - "Keine", - "Programm (A-Z)", - "Programm (Z-A)", - "Kategorie (A-Z)", - "Kategorie (Z-A)" - ]) - sort_combobox.grid(row=1, column=1, sticky="we", pady=5) - sort_combobox.configure(state="readonly") - - # Option zum Speichern der letzten Kategorie - remember_category_var = tk.BooleanVar(value=self.options.get("remember_last_category", False)) - remember_category_check = ttk.Checkbutton( - frame, - text="Letzte ausgewählte Kategorie merken", - variable=remember_category_var - ) - remember_category_check.grid(row=2, column=0, columnspan=2, sticky="w", pady=5) - - # Trennlinie - separator = ttk.Separator(frame, orient="horizontal") - separator.grid(row=3, column=0, columnspan=2, sticky="ew", pady=10) - - # Info-Bereich - info_frame = ttk.Frame(frame) - info_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=5) - - # Version - version_label = ttk.Label(info_frame, text="Version: 1.1", font=("Segoe UI", 9)) - version_label.pack(anchor="w") - - # Copyright & Autor Info - author_label = ttk.Label(info_frame, text="© 2025 Akamaru", font=("Segoe UI", 9)) - author_label.pack(anchor="w") - - created_label = ttk.Label(info_frame, text="Erstellt mit Hilfe von Claude", font=("Segoe UI", 9)) - created_label.pack(anchor="w") - - # Link zum Quellcode mit Unterstreichung - link_style = ttk.Style() - link_style.configure("Link.TLabel", foreground="blue", font=("Segoe UI", 9, "underline")) - - link_label = ttk.Label(info_frame, text="Quellcode auf git.ponywave.de verfügbar", - style="Link.TLabel", cursor="hand2") - link_label.pack(anchor="w", pady=(5, 0)) - link_label.bind("", lambda e: self.open_url("https://git.ponywave.de/Akamaru/Programm-Shortcut")) - - # Buttons - button_frame = ttk.Frame(frame) - button_frame.grid(row=5, column=0, columnspan=2, pady=20) - - save_button = ttk.Button( - button_frame, - text="Speichern", - command=lambda: self.save_options( - sort_combobox.get(), - remember_category_var.get(), - options_window - ) - ) - save_button.pack(side=tk.LEFT, padx=5) - - cancel_button = ttk.Button(button_frame, text="Abbrechen", command=options_window.destroy) - cancel_button.pack(side=tk.LEFT, padx=5) - - def get_sort_option_text(self): - """Gibt den Text für die aktuelle Sortierungsoption zurück""" - if self.sort_column == "name": - if self.sort_reverse: - return "Programm (Z-A)" - else: - return "Programm (A-Z)" - elif self.sort_column == "category": - if self.sort_reverse: - return "Kategorie (Z-A)" - else: - return "Kategorie (A-Z)" - else: - return "Keine" - - def save_options(self, sort_option, remember_category, window): - """Speichert die Programmoptionen""" - # Sortieroptionen übersetzen - if sort_option == "Programm (A-Z)": - self.options["default_sort_column"] = "name" - self.options["default_sort_reverse"] = False - self.sort_column = "name" - self.sort_reverse = False - elif sort_option == "Programm (Z-A)": - self.options["default_sort_column"] = "name" - self.options["default_sort_reverse"] = True - self.sort_column = "name" - self.sort_reverse = True - elif sort_option == "Kategorie (A-Z)": - self.options["default_sort_column"] = "category" - self.options["default_sort_reverse"] = False - self.sort_column = "category" - self.sort_reverse = False - elif sort_option == "Kategorie (Z-A)": - self.options["default_sort_column"] = "category" - self.options["default_sort_reverse"] = True - self.sort_column = "category" - self.sort_reverse = True - else: # "Keine" - self.options["default_sort_column"] = None - self.options["default_sort_reverse"] = False - self.sort_column = None - self.sort_reverse = False - - # Kategorie-Speicher-Option setzen - self.options["remember_last_category"] = remember_category - - # Aktuelle Kategorie speichern, wenn Option aktiviert ist - if remember_category: - try: - index = self.category_listbox.curselection()[0] - self.options["last_category"] = self.category_listbox.get(index) - except (IndexError, AttributeError): - pass - - # Stelle sicher, dass die Optionen im Hauptdatenobjekt aktualisiert werden - self.data["options"] = self.options - - # Änderungen speichern - self.save_data() - - # Liste neu sortieren - self.populate_program_list() - - # Fenster schließen - window.destroy() - - def open_url(self, url): - """Öffnet eine URL im Standard-Browser""" - try: - import webbrowser - webbrowser.open(url) - except Exception as e: - messagebox.showerror("Fehler", f"Fehler beim Öffnen der URL: {str(e)}") - - def on_category_select(self, event): - # Aktualisiere die Programmliste basierend auf der ausgewählten Kategorie - self.populate_program_list() - - def add_category(self): - """Fügt eine neue Kategorie hinzu""" - # Dialog für neue Kategorie - category_name = simpledialog.askstring("Neue Kategorie", "Geben Sie den Namen der neuen Kategorie ein:") - if not category_name: - return - - # Prüfe, ob die Kategorie bereits existiert - if category_name in self.categories: - messagebox.showwarning("Warnung", f"Die Kategorie '{category_name}' existiert bereits.") - return - - # Füge die Kategorie zur Liste hinzu - self.categories.append(category_name) - - # Aktualisiere die Kategorie-Listbox - self.category_listbox.delete(1, tk.END) # Lösche alle außer "Alle" - for category in sorted(self.categories): - self.category_listbox.insert(tk.END, category) - - # Speichere die aktualisierte Kategorienliste - self.save_data() - - def remove_category(self): - """Entfernt eine Kategorie""" - # Hole die ausgewählte Kategorie - try: - index = self.category_listbox.curselection()[0] - category = self.category_listbox.get(index) - except IndexError: - messagebox.showinfo("Information", "Bitte wählen Sie eine Kategorie zum Entfernen aus.") - return - - # "Alle" kann nicht entfernt werden - if category == "Alle": - messagebox.showinfo("Information", "Die Kategorie 'Alle' kann nicht entfernt werden.") - return - - # Bestätigung einholen - confirm = messagebox.askyesno("Bestätigung", - f"Möchten Sie die Kategorie '{category}' wirklich entfernen?\n\n" - "Alle Programme in dieser Kategorie werden ohne Kategorie gespeichert.") - if not confirm: - return - - # Programme ohne Kategorie speichern - for program in self.programs: - if program.get("Kategorie") == category: - if "Kategorie" in program: - del program["Kategorie"] - - # Entferne die Kategorie aus der Liste - self.categories.remove(category) - - # Aktualisiere die Kategorie-Listbox - self.category_listbox.delete(1, tk.END) # Lösche alle außer "Alle" - for category in sorted(self.categories): - self.category_listbox.insert(tk.END, category) - - # Wähle "Alle" aus - self.category_listbox.selection_set(0) - - # Speichere die Änderungen - self.save_data() - self.populate_program_list() - - def populate_program_list(self): - # Lösche alle bestehenden Einträge - for item in self.program_tree.get_children(): - self.program_tree.delete(item) - - # Hole die ausgewählte Kategorie - try: - index = self.category_listbox.curselection()[0] - selected_category = self.category_listbox.get(index) - except IndexError: - selected_category = "Alle" - - # Sortiere Programme nach Kategorie und Name - def get_category(program): - return program.get("Kategorie", "").lower() if "Kategorie" in program else "" - - # Nach dem aktuellen Sortierzustand sortieren - if self.sort_column == "name": - # Nach Programmname sortieren (Groß-/Kleinschreibung ignorieren) - sorted_programs = sorted(self.programs, key=lambda x: x["Name"].lower(), reverse=self.sort_reverse) - elif self.sort_column == "category": - # Nach Kategorie sortieren (Groß-/Kleinschreibung ignorieren) - sorted_programs = sorted(self.programs, key=lambda x: get_category(x), reverse=self.sort_reverse) - else: - # Standardsortierung: Kategorie dann Name (Groß-/Kleinschreibung ignorieren) - sorted_programs = sorted(self.programs, key=lambda x: (get_category(x), x["Name"].lower())) - - # Füge Programme hinzu - for program in sorted_programs: - category = program.get("Kategorie", "") if "Kategorie" in program else "" - - # Filtere nach Kategorie, wenn nicht "Alle" ausgewählt ist - if selected_category != "Alle" and category != selected_category: - continue - - program_path = program["Pfad"] - program_name = " " + program["Name"] # Fester Abstand mit 3 Leerzeichen - - # Hole das Icon nur, wenn Icon-Unterstützung aktiviert ist - icon = None - if ICON_SUPPORT: - # Hole das Icon, wenn es noch nicht im Cache ist - if program_path in self.icon_cache: - icon = self.icon_cache[program_path] - else: - # Versuche, das Icon zu extrahieren - icon = extract_icon_from_exe(program_path) - # Speichere im Cache für zukünftige Verwendung - self.icon_cache[program_path] = icon - - # Füge Programm hinzu - if ICON_SUPPORT and icon: - # Mit Icon in der #0 Spalte - item_id = self.program_tree.insert("", tk.END, text="", values=(program_name, category), - tags=(program_path,), image=icon) - else: - # Ohne Icon, normaler Eintrag - item_id = self.program_tree.insert("", tk.END, text="", values=(program_name, category), - tags=(program_path,)) - - def start_program(self): - selected_items = self.program_tree.selection() - if not selected_items: - messagebox.showinfo("Information", "Bitte wählen Sie ein Programm aus.") - return - - item_id = selected_items[0] - program_values = self.program_tree.item(item_id, "values") - program_name = program_values[0].strip() # Leerzeichen am Anfang entfernen - program_path = self.program_tree.item(item_id, "tags")[0] - - # Prüfen, ob das Programm Adminrechte benötigt - requires_admin = False - for program in self.programs: - if program["Name"].strip() == program_name.strip(): - requires_admin = program.get("Adminrechte", False) - break - - try: - if sys.platform == "win32" and requires_admin: - try: - # Direkter Aufruf über ShellExecute API mit erhöhten Rechten - import ctypes - # Der Parameter "runas" fordert erhöhte Rechte an - result = ctypes.windll.shell32.ShellExecuteW(None, "runas", program_path, None, None, 1) - # Wenn der Rückgabewert <= 32 ist, gab es einen Fehler - if result <= 32: - # Fallback: Starte normal ohne erhöhte Rechte - subprocess.Popen(program_path) - except Exception: - # Wenn es fehlschlägt, starte normal - subprocess.Popen(program_path) - else: - # Normaler Start ohne Adminrechte - subprocess.Popen(program_path) - except Exception as e: - messagebox.showerror("Fehler", f"Fehler beim Starten des Programms: {str(e)}") - - def add_program(self): - add_window = tk.Toplevel(self.root) - add_window.title("Programm hinzufügen") - add_window.geometry("450x280") # Etwas mehr Platz für die Checkbox - add_window.resizable(False, False) - add_window.grab_set() # Modal machen - - # Zentriere das Dialogfenster - add_window.update_idletasks() - width = add_window.winfo_width() - height = add_window.winfo_height() - x = (add_window.winfo_screenwidth() // 2) - (width // 2) - y = (add_window.winfo_screenheight() // 2) - (height // 2) - add_window.geometry(f"{width}x{height}+{x}+{y}") - - # Frame - frame = ttk.Frame(add_window, padding="20") - frame.pack(fill=tk.BOTH, expand=True) - - # Eingabefelder - ttk.Label(frame, text="Programmname:").grid(row=0, column=0, sticky="w", pady=5) - name_entry = ttk.Entry(frame, width=30) - name_entry.grid(row=0, column=1, columnspan=2, sticky="we", pady=5) - - ttk.Label(frame, text="Programmpfad:").grid(row=1, column=0, sticky="w", pady=5) - path_entry = ttk.Entry(frame, width=30) - path_entry.grid(row=1, column=1, sticky="we", pady=5) - - browse_button = ttk.Button(frame, text="Durchsuchen", - command=lambda: self.browse_file_and_clear(path_entry)) - browse_button.grid(row=1, column=2, padx=5, pady=5) - - ttk.Label(frame, text="Kategorie:").grid(row=2, column=0, sticky="w", pady=5) - - # Combobox für Kategorien - category_combobox = ttk.Combobox(frame, width=28, values=sorted(self.categories)) - category_combobox.grid(row=2, column=1, columnspan=2, sticky="we", pady=5) - if self.categories: - category_combobox.current(0) # Erste Kategorie auswählen, falls vorhanden - - # Checkbox für Adminrechte - admin_var = tk.BooleanVar(value=False) - admin_check = ttk.Checkbutton(frame, text="Benötigt Administratorrechte", variable=admin_var) - admin_check.grid(row=3, column=0, columnspan=3, sticky="w", pady=5) - - # Buttons - button_frame = ttk.Frame(frame) - button_frame.grid(row=4, column=0, columnspan=3, pady=15) - - save_button = ttk.Button(button_frame, text="Speichern", - command=lambda: self.save_new_program( - name_entry.get(), - path_entry.get(), - category_combobox.get(), - admin_var.get(), - add_window)) - save_button.pack(side=tk.LEFT, padx=5) - - cancel_button = ttk.Button(button_frame, text="Abbrechen", command=add_window.destroy) - cancel_button.pack(side=tk.LEFT, padx=5) - - # Focus auf erstes Eingabefeld - name_entry.focus_set() - - def browse_file_and_clear(self, entry_widget): - """Durchsucht nach einer Datei und löscht vorher den Inhalt des Eingabefelds""" - entry_widget.delete(0, tk.END) - filename = filedialog.askopenfilename(filetypes=[("Ausführbare Dateien", "*.exe"), ("Alle Dateien", "*.*")]) - if filename: - entry_widget.insert(0, filename) - - def save_new_program(self, name, path, category, requires_admin, window): - 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 - - # Neues Programm hinzufügen (nur mit Kategorie, wenn eine ausgewählt wurde) - new_program = {"Name": name, "Pfad": path} - if category: - new_program["Kategorie"] = category - - # Füge Kategorie hinzu, falls sie noch nicht existiert - if category not in self.categories: - self.categories.append(category) - - # Aktualisiere die Kategorie-Listbox - self.category_listbox.delete(1, tk.END) # Lösche alle außer "Alle" - for cat in sorted(self.categories): - self.category_listbox.insert(tk.END, cat) - - # Adminrechte-Flag hinzufügen - if requires_admin: - new_program["Adminrechte"] = True - - self.programs.append(new_program) - self.save_data() - self.populate_program_list() - window.destroy() - - def remove_program(self): - selected_items = self.program_tree.selection() - if not selected_items: - messagebox.showinfo("Information", "Bitte wählen Sie ein Programm zum Entfernen aus.") - return - - item_id = selected_items[0] - program_values = self.program_tree.item(item_id, "values") - program_name = program_values[0].strip() # Leerzeichen am Anfang entfernen - - # Bestätigung - confirm = messagebox.askyesno("Bestätigung", f"Möchten Sie '{program_name}' wirklich entfernen?") - if not confirm: - return - - # Finde den Index des zu entfernenden Programms - found = False - for i, program in enumerate(self.programs): - if program["Name"] == program_name: - del self.programs[i] - found = True - break - - # Wenn kein exakter Treffer gefunden wurde, versuche es ohne Leerzeichen - if not found: - for i, program in enumerate(self.programs): - if program["Name"].strip() == program_name: - del self.programs[i] - break - - self.save_data() - self.populate_program_list() - - def edit_program(self): - selected_items = self.program_tree.selection() - if not selected_items: - messagebox.showinfo("Information", "Bitte wählen Sie ein Programm zum Bearbeiten aus.") - return - - item_id = selected_items[0] - program_values = self.program_tree.item(item_id, "values") - program_name = program_values[0].strip() # Leerzeichen am Anfang entfernen - program_category = program_values[1] if len(program_values) > 1 else "" - program_path = self.program_tree.item(item_id, "tags")[0] - - # Finde den Index des zu bearbeitenden Programms - program_index = None - for i, program in enumerate(self.programs): - if program["Name"] == program_name.strip(): # Vergleich ohne führende Leerzeichen - program_index = i - break - - if program_index is None: - # Wenn kein exakter Treffer gefunden wird, versuche ohne Leerzeichen zu vergleichen - for i, program in enumerate(self.programs): - if program["Name"].strip() == program_name.strip(): # Beide Namen ohne Leerzeichen vergleichen - program_index = i - break - - if program_index is None: - messagebox.showinfo("Information", f"Programm '{program_name}' konnte nicht gefunden werden.") - return - - # Bearbeitungsfenster - edit_window = tk.Toplevel(self.root) - edit_window.title("Programm bearbeiten") - edit_window.geometry("450x280") # Etwas mehr Platz für die Checkbox - edit_window.resizable(False, False) - edit_window.grab_set() # Modal machen - - # Zentriere das Dialogfenster - edit_window.update_idletasks() - width = edit_window.winfo_width() - height = edit_window.winfo_height() - x = (edit_window.winfo_screenwidth() // 2) - (width // 2) - y = (edit_window.winfo_screenheight() // 2) - (height // 2) - edit_window.geometry(f"{width}x{height}+{x}+{y}") - - # Frame - frame = ttk.Frame(edit_window, padding="20") - frame.pack(fill=tk.BOTH, expand=True) - - # Eingabefelder - ttk.Label(frame, text="Programmname:").grid(row=0, column=0, sticky="w", pady=5) - name_entry = ttk.Entry(frame, width=30) - name_entry.insert(0, program_name.strip()) # Original-Programmnamen ohne Leerzeichen einfügen - name_entry.grid(row=0, column=1, columnspan=2, sticky="we", pady=5) - - ttk.Label(frame, text="Programmpfad:").grid(row=1, column=0, sticky="w", pady=5) - path_entry = ttk.Entry(frame, width=30) - path_entry.insert(0, program_path) - path_entry.grid(row=1, column=1, sticky="we", pady=5) - - browse_button = ttk.Button(frame, text="Durchsuchen", - command=lambda: self.browse_file(path_entry)) - browse_button.grid(row=1, column=2, padx=5, pady=5) - - ttk.Label(frame, text="Kategorie:").grid(row=2, column=0, sticky="w", pady=5) - - # Combobox für Kategorien - category_combobox = ttk.Combobox(frame, width=28, values=sorted(self.categories)) - category_combobox.grid(row=2, column=1, columnspan=2, sticky="we", pady=5) - if program_category: - category_combobox.set(program_category) - elif self.categories: - category_combobox.current(0) # Erste Kategorie auswählen, falls vorhanden - - # Checkbox für Adminrechte - admin_var = tk.BooleanVar(value=self.programs[program_index].get("Adminrechte", False)) - admin_check = ttk.Checkbutton(frame, text="Benötigt Administratorrechte", variable=admin_var) - admin_check.grid(row=3, column=0, columnspan=3, sticky="w", pady=5) - - # Buttons - button_frame = ttk.Frame(frame) - button_frame.grid(row=4, column=0, columnspan=3, pady=15) - - save_button = ttk.Button(button_frame, text="Speichern", - command=lambda: self.update_program( - program_index, - name_entry.get(), - path_entry.get(), - category_combobox.get(), - admin_var.get(), - edit_window)) - save_button.pack(side=tk.LEFT, padx=5) - - cancel_button = ttk.Button(button_frame, text="Abbrechen", command=edit_window.destroy) - cancel_button.pack(side=tk.LEFT, padx=5) - - def browse_file(self, entry_widget): - filename = filedialog.askopenfilename(filetypes=[("Ausführbare Dateien", "*.exe"), ("Alle Dateien", "*.*")]) - if filename: - entry_widget.delete(0, tk.END) - entry_widget.insert(0, filename) - - def update_program(self, index, name, path, category, requires_admin, window): - 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 - - # Programm aktualisieren (nur mit Kategorie, wenn eine ausgewählt wurde) - updated_program = {"Name": name, "Pfad": path} - if category: - updated_program["Kategorie"] = category - - # Füge Kategorie hinzu, falls sie noch nicht existiert - if category not in self.categories: - self.categories.append(category) - - # Aktualisiere die Kategorie-Listbox - self.category_listbox.delete(1, tk.END) # Lösche alle außer "Alle" - for cat in sorted(self.categories): - self.category_listbox.insert(tk.END, cat) - - # Adminrechte-Flag hinzufügen - if requires_admin: - updated_program["Adminrechte"] = True - - self.programs[index] = updated_program - self.save_data() - self.populate_program_list() - window.destroy() - - def sort_treeview(self, column, reverse): - """Sortiert die Treeview nach der angegebenen Spalte""" - if self.sort_column == column: - # Wenn dieselbe Spalte erneut geklickt wird, umkehren - reverse = not self.sort_reverse - - # Speichern des aktuellen Sortierzustands - self.sort_column = column - self.sort_reverse = reverse - - # Aktualisiere die Programmliste mit dem neuen Sortierzustand - self.populate_program_list() - -if __name__ == "__main__": - root = tk.Tk() - app = ProgramLauncher(root) -root.mainloop() \ No newline at end of file diff --git a/screenshot.png b/screenshot.png index d47f2ea..cbbfc81 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/start.bat b/start.bat index 9daf64a..9c2c326 100644 --- a/start.bat +++ b/start.bat @@ -1,2 +1,2 @@ @echo off -start "" /b pythonw.exe run.py \ No newline at end of file +start "" /b pythonw.exe main.py \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/icon_utils.py b/utils/icon_utils.py new file mode 100644 index 0000000..5ea25b3 --- /dev/null +++ b/utils/icon_utils.py @@ -0,0 +1,94 @@ +import sys +import os +from typing import Optional, Dict, Any + +try: + import win32ui + import win32gui + import win32con + import win32api + from PIL import Image, ImageTk + ICON_SUPPORT = True +except ImportError: + print("Warnung: Die Bibliotheken für Programm-Icons fehlen. Installieren Sie diese mit: pip install pywin32 Pillow") + ICON_SUPPORT = False + + +class IconManager: + def __init__(self): + self.icon_cache: Dict[str, Any] = {} + self.icon_support = ICON_SUPPORT + + def resource_path(self, relative_path: str) -> str: + """Get absolute path to resource, works for dev and for PyInstaller""" + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + return os.path.join(base_path, relative_path) + + def extract_icon_from_exe(self, exe_path: str) -> Optional[Any]: + """Extrahiert das Icon aus einer EXE-Datei""" + if not self.icon_support: + return None + + try: + # Icon-Handle erhalten + large, small = win32gui.ExtractIconEx(exe_path, 0) + + # Wir verwenden das größere Icon + if large: + # Wir nehmen das erste Icon + ico_x = win32api.GetSystemMetrics(win32con.SM_CXICON) + ico_y = win32api.GetSystemMetrics(win32con.SM_CYICON) + + # Icon in DC (Device Context) zeichnen + hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0)) + hbmp = win32ui.CreateBitmap() + hbmp.CreateCompatibleBitmap(hdc, ico_x, ico_y) + hdc = hdc.CreateCompatibleDC() + + hdc.SelectObject(hbmp) + hdc.DrawIcon((0, 0), large[0]) + + # Bitmap in ein Python-Image-Objekt umwandeln + bmpinfo = hbmp.GetInfo() + bmpstr = hbmp.GetBitmapBits(True) + img = Image.frombuffer( + 'RGBA', + (bmpinfo['bmWidth'], bmpinfo['bmHeight']), + bmpstr, 'raw', 'BGRA', 0, 1 + ) + + # Aufräumen + win32gui.DestroyIcon(large[0]) + for icon in small: + if icon: + win32gui.DestroyIcon(icon) + + # Bild auf die gewünschte Größe skalieren + img = img.resize((16, 16), Image.LANCZOS) + + # In PhotoImage umwandeln für Tkinter + return ImageTk.PhotoImage(img) + return None + except Exception as e: + print(f"Fehler beim Extrahieren des Icons: {str(e)}") + return None + + def get_icon(self, program_path: str) -> Optional[Any]: + """Holt ein Icon aus dem Cache oder extrahiert es neu""" + if not self.icon_support: + return None + + if program_path in self.icon_cache: + return self.icon_cache[program_path] + + icon = self.extract_icon_from_exe(program_path) + self.icon_cache[program_path] = icon + return icon + + def clear_cache(self): + """Leert den Icon-Cache""" + self.icon_cache.clear() \ No newline at end of file