commit d7f49197c0cafaa5779d84a2c29c8c9eb8dc06d2 Author: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Thu Feb 12 09:51:22 2026 +0100 Migrate to GitLab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0763afe --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +excel_filter/build/ +excel_filter/dist/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3e99ede --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "." + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6cf755 --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +# Excel Filter Tool + +A Python application for filtering Excel files based on regex patterns with both CLI and GUI interfaces. + +## Overview + +The Excel Filter Tool allows you to: +- Analyze Excel files without opening them +- Filter rows based on configurable regex patterns +- Create new Excel files with filtered data +- Use a graphical interface or command line +- Configure settings via JSON files + +## Quick Start + +### Windows Users (Recommended) + +**Pre-built installer available!** + +Download and run `ExcelFilterSetup.exe` from the `dist/` directory for automatic installation including Python and all dependencies. + +**OR** use the provided batch files: +- `launch_gui_tkinter.bat` - Launches the Tkinter GUI (recommended) +- `launch_gui.bat` - Launches the PySimpleGUI interface +- `launch_cli.bat` - Launches the command line version + +### macOS/Linux Users + +```bash +cd excel_filter +pip install -r requirements.txt +python -m src.excel_filter.gui +``` + +For CLI usage: +```bash +python -m src.excel_filter.main --input input.xlsx --output output.xlsx --pattern "error|warning" +``` + +### Command Line Usage + +```bash +python -m src.excel_filter.main --input input.xlsx --output output.xlsx --pattern "error|warning" +``` + +### Graphical Interface + +```bash +python -m src.excel_filter.gui +``` + +## Usage Guide + +### Basic Filtering + +```bash +python -m src.excel_filter.main --input data.xlsx --output filtered.xlsx --pattern "error|warning|critical" +``` + +### Filter Specific Columns + +```bash +python -m src.excel_filter.main --input data.xlsx --output filtered.xlsx --pattern "active" --columns "Status Message" +``` + +### Using Configuration Files + +Create a `config.json` file: + +```json +{ + "input_file": "input.xlsx", + "output_file": "output_filtered.xlsx", + "pattern": "error|warning|critical", + "sheet_name": "Sheet1", + "columns": ["Status", "Message", "Description"] +} +``` + +Then run: + +```bash +python -m src.excel_filter.main --config config.json +``` + +## Regex Patterns + +The tool uses Python's `re` module. Examples: + +- `error|warning` - Match "error" or "warning" +- `^A.*` - Lines starting with "A" +- `\d{3}` - Three-digit numbers +- `[A-Z].*` - Lines starting with capital letter + +## Packaging & Distribution + +### Build Executable + +```bash +pip install pyinstaller pandas openpyxl customtkinter +python build_executable.py +``` + +### Create Installer + +```bash +double-click build_installer.bat +``` + +This creates `ExcelFilterSetup.exe` that: +- Installs the application as a native Windows executable +- Automatically installs Python and dependencies if needed +- Creates desktop and Start menu shortcuts + +## Technical Details + +``` + +### Key Components + +- **ExcelFilter Class**: Core filtering functionality +- **CLI Interface**: Command line processing +- **GUI Interface**: Graphical interface +- **Configuration Management**: JSON-based settings + +### Dependencies + +- `openpyxl` - Excel file operations +- `pandas` - Data manipulation +- `customtkinter` - GUI interface + + +## Examples + +### Filter Log Files + +```bash +python -m src.excel_filter.main --input logs.xlsx --output errors.xlsx --pattern "error|warning" +``` + +### Filter Customer Data + +```bash +python -m src.excel_filter.main --input customers.xlsx --output active_customers.xlsx --pattern "active" --columns Status +``` + +### Filter Numeric Data + +```bash +python -m src.excel_filter.main --input sales.xlsx --output high_sales.xlsx --pattern "1000.*" --columns Amount +``` \ No newline at end of file diff --git a/excel_filter/.env b/excel_filter/.env new file mode 100644 index 0000000..56a282d --- /dev/null +++ b/excel_filter/.env @@ -0,0 +1 @@ +PYTHONPATH=src diff --git a/excel_filter/.vscode/settings.json b/excel_filter/.vscode/settings.json new file mode 100644 index 0000000..7657d55 --- /dev/null +++ b/excel_filter/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "python.testing.pytestArgs": [ + "--rootdir=.", + "tests" + ], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.envFile": "${workspaceFolder}/.env", + "python.defaultInterpreterPath": "python" +} diff --git a/excel_filter/ExcelFilter.spec b/excel_filter/ExcelFilter.spec new file mode 100644 index 0000000..e8078b5 --- /dev/null +++ b/excel_filter/ExcelFilter.spec @@ -0,0 +1,39 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['gui_new.py'], + pathex=[], + binaries=[], + datas=[('app_icon.ico', '.'), ('check_python.py', '.'), ('check_environment.bat', '.'), ('LICENSE.txt', '.'), ('locales', 'locales')], + hiddenimports=['openpyxl', 'pandas.io.excel._openpyxl', 'pandas.io.excel._base', 'pandas.io.common', 'tkinter', 'tkinter.filedialog', 'tkinter.messagebox', 'tkinter.ttk'], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='ExcelFilter', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['app_icon.ico'], +) diff --git a/excel_filter/LICENSE.txt b/excel_filter/LICENSE.txt new file mode 100644 index 0000000..a79127a --- /dev/null +++ b/excel_filter/LICENSE.txt @@ -0,0 +1,52 @@ +Excel Filter Tool - License Agreement +====================================== + +Allgemeine Bestimmungen (die niemand liest) +Herzlich willkommen zu "Excel Tool" – dem Tool, das Ihre Tabellenkalkulationen revolutionieren wird (oder auch nicht). Durch die Installation dieses Tools erklären Sie sich damit einverstanden, dass wir Ihre Seele nicht stehlen, Ihre Katze nicht entführen und Ihre Kaffeetasse nicht heimlich umstellen. Sollten Sie diese Vereinbarung nicht akzeptieren, installieren Sie das Tool trotzdem, denn wir wissen beide: Sie klicken eh einfach auf "Weiter". + +Nutzungsrechte (oder: Was Sie tun dürfen, ohne dass wir Sie verklagen) + +Sie dürfen "Excel Tool" nutzen, um Zahlen zu filtern, zu löschen oder einfach nur anzustarren. +Sie dürfen das Tool auf maximal 37 Geräten installieren – oder auf 42, wenn Sie an einem Dienstag geboren sind. +Sie dürfen das Tool nicht nutzen, um: + +Die Weltherrschaft zu erlangen. +Ihren Chef davon zu überzeugen, dass Sie eigentlich mehr Gehalt verdienen. +Toast zu rösten. Dafür gibt es Toaster. + +Haftungsausschluss (weil Anwälte das mögen) +Wir haften nicht für: + +Plötzliche Lust auf Käse, die während der Nutzung auftritt. +Die Erkenntnis, dass Ihr Leben vielleicht doch nicht in Excel-Tabellen organisiert werden sollte. +Falls Ihr Computer nach der Installation anfängt, in Reimen zu sprechen (siehe Abschnitt 5). + + +Datenschutz (oder: Was wir mit Ihren Daten machen – Spoiler: Nichts!) +Wir sammeln keine Daten. Außer vielleicht Ihre Lieblingsfarbe. + + +Das obligatorische Gedicht +"Excel, Excel, wunderbar, +voll mit Zahlen, wunderbar. +Doch wenn die Formel flucht, +ist’s mit der Freude flugs verflucht." + + +Gewährleistung (oder: Warum wir nichts versprechen) +"Excel Tool" funktioniert garantiert – außer bei Vollmond, wenn Merkur rückläufig ist oder Ihr WLAN mal wieder spinnt. In diesen Fällen empfehlen wir: Einfach neu starten. Oder beten. + + +Kündigung (weil jeder Vertrag eine braucht) +Diese Lizenz endet automatisch, wenn: + + +Sie das Tool löschen (oder vergessen, wo Sie es gespeichert haben). +Die Menschheit von KI überrannt wird (dann haben wir andere Sorgen). +Sie beschließen, dass Tabellen doch überbewertet sind. + +Sonstiges (der Teil, den wirklich niemand liest) + +Sollten Sie diese Lizenz ausdrucken und als Tapete verwenden, schicken Sie uns bitte ein Foto. +Dieser Vertrag ist in allen Universen gültig – außer in dem, in dem Hunde die Herrschaft übernommen haben. +Durch Weiterklicken bestätigen Sie, dass Sie diesen Text gelesen haben (wir wissen, dass Sie das nicht getan haben). \ No newline at end of file diff --git a/excel_filter/__init__.py b/excel_filter/__init__.py new file mode 100644 index 0000000..a60141c --- /dev/null +++ b/excel_filter/__init__.py @@ -0,0 +1 @@ +"" diff --git a/excel_filter/app_icon.ico b/excel_filter/app_icon.ico new file mode 100644 index 0000000..15dfc41 Binary files /dev/null and b/excel_filter/app_icon.ico differ diff --git a/excel_filter/build_executable.py b/excel_filter/build_executable.py new file mode 100644 index 0000000..1c27bd3 --- /dev/null +++ b/excel_filter/build_executable.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +""" +Script to build the Excel Filter application into a standalone executable using PyInstaller. +""" + +import os +import subprocess +import sys + +# Define the main script to be converted +MAIN_SCRIPT = "gui_new.py" + +# Define the output directory for the executable +OUTPUT_DIR = "dist" + +# Define the name of the executable +EXECUTABLE_NAME = "ExcelFilter" + +# Define additional files to include (e.g., icons, scripts, etc.) +# Note: config.json and presets.json are no longer included as they are now stored +# in the user's data directory (AppData on Windows) to avoid permission issues +ADDITIONAL_FILES = [ + ("app_icon.ico", "."), + ("check_python.py", "."), + ("check_environment.bat", "."), + ("LICENSE.txt", "."), + ("locales", "locales"), +] + +# Define hidden imports (if any) +HIDDEN_IMPORTS = [ + "openpyxl", + "pandas.io.excel._openpyxl", + "pandas.io.excel._base", + "pandas.io.common", + "tkinter", + "tkinter.filedialog", + "tkinter.messagebox", + "tkinter.ttk" +] + +# Define excluded modules (if any) +EXCLUDED_MODULES = [] + +# Define the PyInstaller command +pyinstaller_command = [ + "pyinstaller", + "--onefile", # Create a single executable file + "--windowed", # Prevent console from showing (for GUI applications) + f"--name={EXECUTABLE_NAME}", # Name of the executable + f"--distpath={OUTPUT_DIR}", # Output directory + "--icon=app_icon.ico", # Set the icon for the executable + MAIN_SCRIPT, # Main script to convert +] + +# Add the additional files using the --add-data option +for src, dest in ADDITIONAL_FILES: + pyinstaller_command.append(f"--add-data={src};{dest}") + +# Add hidden imports +for hidden_import in HIDDEN_IMPORTS: + pyinstaller_command.append(f"--hidden-import={hidden_import}") + +# Filter out empty strings from the command +pyinstaller_command = [cmd for cmd in pyinstaller_command if cmd] + +# Run the PyInstaller command +print("Building the executable...") +print("Command:", " ".join(pyinstaller_command)) + +try: + # Use python -m PyInstaller to ensure PyInstaller is found + pyinstaller_full_command = [sys.executable, "-m", "PyInstaller"] + pyinstaller_command[1:] + print("Full command:", " ".join(pyinstaller_full_command)) + subprocess.run(pyinstaller_full_command, check=True) + print("Executable built successfully!") +except subprocess.CalledProcessError as e: + print(f"Error building the executable: {e}") + sys.exit(1) +except Exception as e: + print(f"An unexpected error occurred: {e}") + sys.exit(1) + +# Clean up the build directory (optional) +def cleanup_build_directory(): + """Attempt to clean up the build directory with multiple strategies""" + if os.path.exists("build"): + import shutil + import time + import stat + + # Try multiple cleanup strategies + for attempt in range(3): + try: + # First, try to make files writable + for root, dirs, files in os.walk("build"): + for d in dirs: + try: + os.chmod(os.path.join(root, d), stat.S_IWRITE) + except: + pass + for f in files: + try: + os.chmod(os.path.join(root, f), stat.S_IWRITE) + except: + pass + + # Now try to remove the directory + shutil.rmtree("build") + print("Build directory cleaned up.") + return True + except Exception as e: + if attempt < 2: # Don't wait on last attempt + time.sleep(1) # Wait a bit and try again + else: + print(f"Warning: Could not clean up build directory: {e}") + print("You can manually delete the 'build' directory if needed.") + return False + return True + +# Attempt cleanup +cleanup_build_directory() + +print("Done!") diff --git a/excel_filter/build_installer.bat b/excel_filter/build_installer.bat new file mode 100644 index 0000000..7b1f31f --- /dev/null +++ b/excel_filter/build_installer.bat @@ -0,0 +1,111 @@ +@echo off +setlocal enabledelayedexpansion + +:: Check if Python is installed +python --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo Python is not installed. Please install Python first. + pause + exit /b 1 +) + +:: Install PyInstaller if not already installed +python -m pip show pyinstaller >nul 2>&1 +if %errorlevel% neq 0 ( + echo Installing PyInstaller... + python -m pip install pyinstaller + if %errorlevel% neq 0 ( + echo Failed to install PyInstaller. + pause + exit /b 1 + ) +) + +:: Build the executable +python build_executable.py +if %errorlevel% neq 0 ( + echo Failed to build the executable. + pause + exit /b 1 +) + +:: Find Inno Setup installation path +set ISCC_PATH= + +:: Check if ISCC_PATH is already set in environment variables +if defined ISCC_PATH ( + if exist "%ISCC_PATH%" ( + goto :found_iscc + ) +) + +:: Check common installation paths +set "common_paths=C:\Program Files (x86)\Inno Setup 6 +C:\Program Files\Inno Setup 6 +C:\Program Files (x86)\Inno Setup 5 +C:\Program Files\Inno Setup 5 +C:\Inno Setup 6 +C:\Inno Setup 5" + +for %%p in (%common_paths%) do ( + if exist "%%p\ISCC.exe" ( + set "ISCC_PATH=%%p\ISCC.exe" + goto :found_iscc + ) +) + +:: Check for any Inno Setup folder in Program Files directories +for /d %%d in ("C:\Program Files (x86)\Inno Setup *") do ( + if exist "%%d\ISCC.exe" ( + set "ISCC_PATH=%%d\ISCC.exe" + goto :found_iscc + ) +) + +for /d %%d in ("C:\Program Files\Inno Setup *") do ( + if exist "%%d\ISCC.exe" ( + set "ISCC_PATH=%%d\ISCC.exe" + goto :found_iscc + ) +) + +:: Try to find ISCC.exe in system PATH +where ISCC.exe >nul 2>&1 +if %errorlevel% equ 0 ( + set "ISCC_PATH=ISCC.exe" + goto :found_iscc +) + +:found_iscc + +:: Debug: Show what path was found +if defined ISCC_PATH ( + echo Debug: Found ISCC.exe at "%ISCC_PATH%" +) + +if "%ISCC_PATH%"=="" ( + echo Inno Setup is not installed. Please install Inno Setup first. + echo. + echo The script looked for ISCC.exe in: + echo - Environment variables + echo - Common installation paths + echo - Program Files directories + echo - System PATH + echo. + echo If Inno Setup is installed in a custom location, please: + echo 1. Set the ISCC_PATH environment variable to point to ISCC.exe + echo 2. Or add the Inno Setup directory to your system PATH + pause + exit /b 1 +) + +:: Create the installer +"%ISCC_PATH%" create_installer.iss +if %errorlevel% neq 0 ( + echo Failed to create the installer. + pause + exit /b 1 +) + +echo Installer created successfully! +pause diff --git a/excel_filter/check_dependencies.bat b/excel_filter/check_dependencies.bat new file mode 100644 index 0000000..6d68182 --- /dev/null +++ b/excel_filter/check_dependencies.bat @@ -0,0 +1,61 @@ +@echo off +:: Excel Filter Tool - Abhaengigkeitspruefung +:: Diese Datei ueberprueft und installiert Abhaengigkeiten nur einmal + +:: Wechsel in das Projektverzeichnis +cd /d "%~dp0" + +:: Marker-Datei fuer erfolgreiche Installation +set MARKER_FILE=dependencies_installed.marker + +:: Ueberpruefen, ob die Marker-Datei existiert +if exist "%MARKER_FILE%" ( + :: Marker-Datei existiert - Abhaengigkeiten sollten installiert sein + exit /b 0 +) + +:: Ueberpruefen, ob Python installiert ist +where python >nul 2>nul +if not "%ERRORLEVEL%"=="0" ( + echo Fehler: Python ist nicht installiert oder nicht im PATH. + echo Bitte installieren Sie Python 3.7 oder hoeher. + exit /b 1 +) + +:: Versuchen, die Hauptmodule zu importieren +python -c "import gui_new; import main" >nul 2>nul +if "%ERRORLEVEL%"=="0" ( + :: Module koennen importiert werden - Abhaengigkeiten sind installiert + :: Marker-Datei erstellen + echo. > "%MARKER_FILE%" + exit /b 0 +) + +:: Abhaengigkeiten installieren +echo Installiere Abhaengigkeiten... +echo Dies geschieht nur einmal beim ersten Start. +echo. +pip install -r requirements.txt + +if not "%ERRORLEVEL%"=="0" ( + echo. + echo Fehler: Konnte Abhaengigkeiten nicht installieren. + echo. + echo Moegliche Loesungen: + echo 1. Versuchen Sie, pip zu aktualisieren: + echo pip install --upgrade pip + echo 2. Installieren Sie die Abhaengigkeiten manuell: + echo pip install openpyxl pandas PySimpleGUI python-docx + echo 3. Verwenden Sie eine virtuelle Umgebung: + echo python -m venv venv + echo venv\Scripts\activate + echo pip install -r requirements.txt + echo. + pause + exit /b 1 +) + +:: Marker-Datei erstellen, um zukuenftige Installationen zu vermeiden +echo. > "%MARKER_FILE%" +echo Abhaengigkeiten erfolgreich installiert. +exit /b 0 \ No newline at end of file diff --git a/excel_filter/check_dependencies_simple.bat b/excel_filter/check_dependencies_simple.bat new file mode 100644 index 0000000..5b36d94 --- /dev/null +++ b/excel_filter/check_dependencies_simple.bat @@ -0,0 +1,54 @@ +@echo off +:: Einfache Abhaengigkeitspruefung +:: Diese Version vermeidet komplexe if-Bedingungen + +:: Wechsel in das Projektverzeichnis +cd /d "%~dp0" + +:: Marker-Datei fuer erfolgreiche Installation +set MARKER_FILE=dependencies_installed.marker + +:: Ueberpruefen, ob die Marker-Datei existiert +if exist "%MARKER_FILE%" goto dependencies_ok + +:: Ueberpruefen, ob Python installiert ist +where python >nul 2>nul +if errorlevel 1 ( + echo Fehler: Python ist nicht installiert oder nicht im PATH. + echo Bitte installieren Sie Python 3.7 oder hoeher. + exit /b 1 +) + +:: Versuchen, die Hauptmodule zu importieren +python -c "import gui_new; import main" >nul 2>nul +if errorlevel 1 goto install_dependencies + +:: Marker-Datei erstellen +echo. > "%MARKER_FILE%" +goto dependencies_ok + +:install_dependencies +echo Installiere Abhaengigkeiten... +echo Dies geschieht nur einmal beim ersten Start. +echo. +pip install -r requirements.txt + +if errorlevel 1 ( + echo. + echo Fehler: Konnte Abhaengigkeiten nicht installieren. + echo. + echo Moegliche Loesungen: + echo 1. pip install --upgrade pip + echo 2. pip install openpyxl pandas PySimpleGUI python-docx + echo 3. Verwenden Sie eine virtuelle Umgebung + echo. + pause + exit /b 1 +) + +:: Marker-Datei erstellen +echo. > "%MARKER_FILE%" +echo Abhaengigkeiten erfolgreich installiert. + +:dependencies_ok +exit /b 0 \ No newline at end of file diff --git a/excel_filter/check_environment.bat b/excel_filter/check_environment.bat new file mode 100644 index 0000000..f42b9c3 --- /dev/null +++ b/excel_filter/check_environment.bat @@ -0,0 +1,111 @@ +@echo off +setlocal enabledelayedexpansion + +:: Excel Filter Tool - Environment Check +:: This script checks if Python is available and provides appropriate guidance + +echo Excel Filter Tool - Environment Check +call :log_message "Checking system environment..." + +:: Check if Python is available +call :find_python +if "!PYTHON_FOUND!" == "1" ( + call :log_message "Python found: !PYTHON_PATH!" + call :run_python_check +) else ( + call :log_message "Python not found on this system" + call :show_python_instructions +) + +endlocal +goto :eof + +:find_python +set PYTHON_FOUND=0 +set PYTHON_PATH= + +:: Method 1: Check if python is in PATH +call :log_message "Checking PATH for Python..." +where python >nul 2>&1 +if !errorlevel! equ 0 ( + set PYTHON_FOUND=1 + set PYTHON_PATH=python + goto :eof +) + +:: Method 2: Check common Python installation locations +call :log_message "Checking common Python installation locations..." +for %%P in ( + "%ProgramFiles%\Python*\python.exe", + "%ProgramFiles(x86)%\Python*\python.exe", + "%LocalAppData%\Programs\Python*\python.exe", + "%UserProfile%\AppData\Local\Microsoft\WindowsApps\python.exe", + "%UserProfile%\AppData\Local\Programs\Python*\python.exe" +) do ( + if exist %%P ( + set PYTHON_FOUND=1 + set PYTHON_PATH=%%P + call :log_message "Found Python at: %%P" + goto :eof + ) +) + +goto :eof + +:run_python_check +call :log_message "Running Python environment check..." + +:: Run the Python check script +"!PYTHON_PATH!" "%~dp0check_python.py" +if !errorlevel! equ 0 ( + call :log_message "Python environment check completed successfully" +) else ( + call :log_message "Python environment check failed" + call :show_python_instructions +) + +goto :eof + +:show_python_instructions +call :log_message "========================================" +call :log_message "Python is required to run Excel Filter Tool" +call :log_message "========================================" +call :log_message "" +call :log_message "Please install Python 3.9 or later:" +call :log_message "1. Download Python from: https://www.python.org/downloads/" +call :log_message "2. Run the installer" +call :log_message "3. Make sure to check 'Add Python to PATH' during installation" +call :log_message "4. Restart your computer after installation" +call :log_message "5. Run Excel Filter Tool again" +call :log_message "" +call :log_message "For technical users:" +call :log_message "- Recommended version: Python 3.9+" +call :log_message "- Architecture: 64-bit (recommended)" +call :log_message "- Required packages: pandas, openpyxl, customtkinter" +call :log_message "" + +:: Create a help file with instructions +set HELP_FILE=%TEMP%\ExcelFilter_Python_Help.txt +echo Excel Filter Tool - Python Installation Instructions > "!HELP_FILE!" +echo. >> "!HELP_FILE!" +echo Python is required to run the Excel Filter Tool. >> "!HELP_FILE!" +echo. >> "!HELP_FILE!" +echo Installation Steps: >> "!HELP_FILE!" +echo 1. Download Python from: https://www.python.org/downloads/ >> "!HELP_FILE!" +echo 2. Run the Python installer >> "!HELP_FILE!" +echo 3. Check "Add Python to PATH" during installation >> "!HELP_FILE!" +echo 4. Complete the installation >> "!HELP_FILE!" +echo 5. Restart your computer >> "!HELP_FILE!" +echo 6. Run Excel Filter Tool again >> "!HELP_FILE!" +echo. >> "!HELP_FILE!" +echo Note: Excel Filter Tool requires Python 3.9 or later. >> "!HELP_FILE!" + +:: Show the help file to the user +notepad "!HELP_FILE!" + +goto :eof + +:log_message +:: Helper function to log messages with timestamp +echo [%TIME%] %* +goto :eof \ No newline at end of file diff --git a/excel_filter/check_python.py b/excel_filter/check_python.py new file mode 100644 index 0000000..f2ee461 --- /dev/null +++ b/excel_filter/check_python.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +""" +Script to check if Python is installed and verify dependencies. +This script assumes Python is already available and focuses on dependency checking. +""" + +import os +import subprocess +import sys +import pkg_resources + +def check_python_version(): + """Check Python version and ensure it meets minimum requirements.""" + try: + result = subprocess.run([sys.executable, "--version"], capture_output=True, text=True, check=True) + version_str = result.stdout.strip().replace("Python", "").strip() + + # Parse version + major, minor, _ = map(int, version_str.split('.')) + + if major < 3 or (major == 3 and minor < 9): + print(f"[ERROR] Python {version_str} is too old. Excel Filter Tool requires Python 3.9 or later.") + return False + else: + print(f"[SUCCESS] Python {version_str} is compatible.") + return True + + except Exception as e: + print(f"[ERROR] checking Python version: {e}") + return False + +def check_dependencies(): + """Check if all required Python packages are installed.""" + required_packages = { + 'pandas': 'Data analysis library', + 'openpyxl': 'Excel file handling', + 'customtkinter': 'Modern UI components', + } + + missing_packages = [] + + print("Checking required Python packages...") + + for package, description in required_packages.items(): + try: + pkg_resources.get_distribution(package) + print(f"[SUCCESS] {package} ({description}) - installed") + except pkg_resources.DistributionNotFound: + print(f"[ERROR] {package} ({description}) - NOT installed") + missing_packages.append(package) + + return missing_packages + +def install_missing_dependencies(missing_packages): + """Install missing Python packages.""" + if not missing_packages: + return True + + print(f"\n🔧 Installing {len(missing_packages)} missing package(s)...") + + try: + for package in missing_packages: + print(f"Installing {package}...") + subprocess.run([sys.executable, "-m", "pip", "install", package], check=True) + print(f"[SUCCESS] {package} installed successfully") + + return True + except subprocess.CalledProcessError as e: + print(f"[ERROR] Failed to install packages: {e}") + return False + except Exception as e: + print(f"[ERROR] Unexpected error during installation: {e}") + return False + +def check_tkinter(): + """Check if Tkinter is available (it's part of standard library but sometimes missing).""" + try: + import tkinter + from tkinter import ttk + print("[SUCCESS] Tkinter (GUI library) - available") + return True + except ImportError: + print("[ERROR] Tkinter (GUI library) - NOT available") + print(" Note: Tkinter is required for the graphical interface") + return False + except Exception as e: + print(f"[ERROR] Error checking Tkinter: {e}") + return False + +def main(): + """Main function to check Python environment.""" + print("Excel Filter Tool - Python Environment Check") + print("=" * 50) + + # Check Python version + python_ok = check_python_version() + if not python_ok: + print("\nPlease install Python 3.9 or later from:") + print(" https://www.python.org/downloads/") + print(" Make sure to check 'Add Python to PATH' during installation") + return False + + # Check Tkinter + tkinter_ok = check_tkinter() + + # Check other dependencies + missing_packages = check_dependencies() + + # Install missing dependencies if any + if missing_packages: + install_success = install_missing_dependencies(missing_packages) + if not install_success: + print(f"\n[WARNING] Some dependencies could not be installed automatically") + print(" You may need to install them manually using:") + print(" pip install " + " ".join(missing_packages)) + return False + + # Final summary + print("\n" + "=" * 50) + if python_ok and tkinter_ok and not missing_packages: + print("[SUCCESS] All checks passed! Excel Filter Tool is ready to use.") + return True + else: + print("[WARNING] Some issues were found. Please review the messages above.") + return False + +if __name__ == "__main__": + success = main() + print(f"\nEnvironment check completed with status: {'SUCCESS' if success else 'ISSUES_FOUND'}") + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/excel_filter/create_icon.py b/excel_filter/create_icon.py new file mode 100644 index 0000000..4d418da --- /dev/null +++ b/excel_filter/create_icon.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +import os +from PIL import Image, ImageDraw + +# Create a simple Windows 11 style icon +def create_icon(): + # Create a 256x256 image with transparent background + icon_size = 256 + icon = Image.new('RGBA', (icon_size, icon_size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(icon) + + # Draw Windows 11 style icon (blue square with white filter symbol) + # Background square + draw.rounded_rectangle([(50, 50), (206, 206)], radius=20, fill='#0078d4') + + # White filter symbol + draw.rounded_rectangle([(80, 80), (226, 120)], radius=5, fill='white') + draw.rounded_rectangle([(80, 130), (226, 170)], radius=5, fill='white') + draw.rounded_rectangle([(80, 180), (226, 220)], radius=5, fill='white') + + # Save as ICO + icon.save('app_icon.ico', format='ICO', sizes=[(256, 256), (128, 128), (64, 64), (32, 32), (16, 16)]) + print("Icon created successfully!") + +if __name__ == "__main__": + create_icon() \ No newline at end of file diff --git a/excel_filter/create_installer.iss b/excel_filter/create_installer.iss new file mode 100644 index 0000000..f5714be --- /dev/null +++ b/excel_filter/create_installer.iss @@ -0,0 +1,55 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "Excel Filter" +#define MyAppVersion "1.1" +#define MyAppPublisher "Jonas Gaudian" +#define MyAppURL "https://www.gaudian.eu/" +#define MyAppExeName "ExcelFilter.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. +; Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{BAF687B9-1B85-4DA4-810A-ABC6172B3765} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +DefaultGroupName={#MyAppName} +AllowNoIcons=yes +LicenseFile=LICENSE.txt +OutputDir=. +OutputBaseFilename=ExcelFilterSetup +SetupIconFile=app_icon.ico +Compression=lzma +SolidCompression=yes +WizardStyle=modern + +[Languages] +Name: "german"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "presets.json"; DestDir: "{app}"; Flags: ignoreversion +Source: "app_icon.ico"; DestDir: "{app}"; Flags: ignoreversion +Source: "check_python.py"; DestDir: "{app}"; Flags: ignoreversion +Source: "check_environment.bat"; DestDir: "{app}"; Flags: ignoreversion +Source: "locales\*"; DestDir: "{app}\locales"; Flags: ignoreversion recursesubdirs createallsubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" +Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\check_environment.bat"; Description: "Checking system environment"; Flags: runhidden nowait postinstall skipifsilent +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent diff --git a/excel_filter/dependencies_installed.marker b/excel_filter/dependencies_installed.marker new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/excel_filter/dependencies_installed.marker @@ -0,0 +1 @@ + diff --git a/excel_filter/filter.py b/excel_filter/filter.py new file mode 100644 index 0000000..dd31f30 --- /dev/null +++ b/excel_filter/filter.py @@ -0,0 +1,504 @@ +""" +Excel Filter Module +Main functionality for filtering Excel files based on regex patterns +""" + +import re +import pandas as pd +from typing import List, Dict, Any, Optional +import logging +import time +import os +import json +from pathlib import Path + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Simple translation system for backend messages +class BackendTranslations: + """ + Simple translation system for backend modules + """ + + def __init__(self, language="de"): + self.current_language = language + self.translations = {} + + # Load translations from JSON files + self.load_translations() + + def load_translations(self): + """ + Load translation files from the locales directory + """ + # Get the directory where this script is located + script_dir = Path(__file__).parent + locales_dir = script_dir / "locales" + + # Load the language file + lang_file = locales_dir / f"{self.current_language}.json" + if lang_file.exists(): + try: + with open(lang_file, 'r', encoding='utf-8') as f: + self.translations = json.load(f) + except Exception as e: + print(f"Error loading {lang_file}: {e}") + self.translations = {} + else: + self.translations = {} + + def get(self, key, default=None): + """ + Get a translation for a key + """ + return self.translations.get(key, default if default is not None else key) + + def __getitem__(self, key): + """ + Get a translation using dictionary-style access + """ + return self.get(key) + +# Global backend translations instance +_backend_translations = BackendTranslations() + +def get_backend_translation(key, **kwargs): + """ + Get a backend translation and format it with provided arguments + """ + message = _backend_translations.get(key, key) + if kwargs: + try: + message = message.format(**kwargs) + except (KeyError, ValueError): + pass # Keep original message if formatting fails + return message + + +class ExcelFilter: + """ + Class for filtering Excel files based on regex patterns and numeric filters + """ + + def __init__(self, input_file: str, output_file: str, pattern: str = None, + sheet_name: str = None, columns: List[str] = None, + numeric_filter: Dict[str, Any] = None, language: str = "de"): + """ + Initializes the ExcelFilter + + Args: + input_file: Path to the input file + output_file: Path to the output file + pattern: Regex pattern for filtering (optional) + sheet_name: Name of the worksheet (optional) + columns: List of column names to search (optional) + numeric_filter: Dictionary with numeric filter settings (optional) + Format: {'column': str or None, 'operator': str, 'value': float} + If 'column' is None, the filter applies to all columns + """ + self.input_file = input_file + self.output_file = output_file + self.pattern = pattern + self.sheet_name = sheet_name + self.columns = columns + self.numeric_filter = numeric_filter + + # Statistics collection + self.stats = { + 'start_time': None, + 'end_time': None, + 'input_file_size': 0, + 'output_file_size': 0, + 'input_rows': 0, + 'input_columns': 0, + 'output_rows': 0, + 'output_columns': 0, + 'memory_usage_mb': 0, + 'filters_applied': [], + 'processing_time_seconds': 0, + 'compression_ratio': 0.0, + 'rows_filtered': 0, + 'rows_removed': 0 + } + + # Log initialization with all parameters + logger.info(f"ExcelFilter initialized: input_file='{input_file}', output_file='{output_file}', " + f"pattern='{pattern}', sheet_name='{sheet_name}', columns={columns}, " + f"numeric_filter={numeric_filter}") + + def read_excel(self) -> pd.DataFrame: + """ + Reads the Excel file and returns a DataFrame + """ + try: + # Get input file size + if os.path.exists(self.input_file): + self.stats['input_file_size'] = os.path.getsize(self.input_file) + + if self.sheet_name: + df = pd.read_excel(self.input_file, sheet_name=self.sheet_name) + else: + df = pd.read_excel(self.input_file) + + # Collect input statistics + self.stats['input_rows'] = len(df) + self.stats['input_columns'] = len(df.columns) + self.stats['memory_usage_mb'] = df.memory_usage(deep=True).sum() / (1024 * 1024) + + logger.info(get_backend_translation("input_file_loaded", rows=len(df), columns=len(df.columns))) + logger.info(get_backend_translation("file_size_info", size=self.stats['input_file_size'] / (1024*1024))) + logger.info(get_backend_translation("memory_usage_info", size=self.stats['memory_usage_mb'])) + + return df + except FileNotFoundError: + logger.error(get_backend_translation("file_not_found_error", input_file=self.input_file)) + raise FileNotFoundError(f"The file {self.input_file} was not found") + except Exception as e: + logger.error(get_backend_translation("error_reading_excel_file", error=str(e))) + raise Exception(f"Error reading the Excel file: {e}") + + def filter_dataframe(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Filters the DataFrame based on regex patterns and/or numeric filters + """ + try: + filtered_df = df + applied_filters = [] + + # Apply regex filtering if pattern is provided + if self.pattern and self.pattern.strip(): + filtered_df = self._apply_regex_filter(filtered_df) + applied_filters.append("Regex") + + # Apply numeric filtering if enabled + if self.numeric_filter: + filtered_df = self._apply_numeric_filter(filtered_df) + applied_filters.append("Numeric") + + # Update statistics + self.stats['filters_applied'] = applied_filters + self.stats['rows_filtered'] = len(filtered_df) + self.stats['rows_removed'] = len(df) - len(filtered_df) + + if not applied_filters: + logger.warning(get_backend_translation("no_filter_criteria_specified")) + logger.info(get_backend_translation("no_filters_applied_rows_remain", rows=len(df))) + return df + + # Calculate filtering efficiency + retention_rate = (len(filtered_df) / len(df)) * 100 if len(df) > 0 else 0 + removal_rate = (self.stats['rows_removed'] / len(df)) * 100 if len(df) > 0 else 0 + + logger.info(get_backend_translation("filters_applied_list", filters=', '.join(applied_filters))) + logger.info(get_backend_translation("filter_results_summary", retained=len(filtered_df), removed=self.stats['rows_removed'])) + logger.info(get_backend_translation("retention_removal_rates", retention=retention_rate, removal=removal_rate)) + + return filtered_df + + except Exception as e: + logger.error(f"Error filtering: {e}") + raise Exception(f"Error filtering: {e}") + + def _apply_regex_filter(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Applies regex filtering to the DataFrame + """ + try: + # Compile the regex pattern + # Intelligent pattern recognition: + # - If the pattern contains spaces, search as exact phrase + # - If the pattern seems to be a complete word, use word boundaries + # - Otherwise allow substring matching + + if ' ' in self.pattern: + # Exact phrase with word boundaries + regex_pattern = rf"\b{re.escape(self.pattern)}\b" + elif len(self.pattern) <= 4: + # Short patterns (4 or fewer characters) - allow substring matching + regex_pattern = self.pattern + elif len(self.pattern) > 2 and self.pattern.isalpha(): + # Probably a complete word + regex_pattern = rf"\b{re.escape(self.pattern)}\b" + else: + # Substring matching for other cases + regex_pattern = self.pattern + + regex = re.compile(regex_pattern, re.IGNORECASE) + logger.info(get_backend_translation("regex_pattern_compiled", original=self.pattern, compiled=regex_pattern)) + + # Determine the columns to search + if self.columns: + columns_to_search = self.columns + logger.info(get_backend_translation("regex_filter_searching_columns", columns=columns_to_search)) + else: + columns_to_search = df.columns + logger.info(get_backend_translation("regex_filter_searching_all_columns", columns=list(columns_to_search))) + + # Filter function with detailed logging + def regex_filter_row(row): + row_matches = False + for col in columns_to_search: + if col in row and pd.notna(row[col]): + cell_value = str(row[col]) + if regex.search(cell_value): + logger.debug(get_backend_translation("regex_match_found", row=row.name, column=col, value=cell_value)) + row_matches = True + break + + return row_matches + + # Apply filter + filtered_df = df[df.apply(regex_filter_row, axis=1)] + logger.info(get_backend_translation("regex_filter_results", rows=len(filtered_df))) + + return filtered_df + + except re.error as e: + logger.error(get_backend_translation("invalid_regex_pattern", error=str(e))) + raise Exception(f"Invalid regex pattern: {e}") + + def _apply_numeric_filter(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Applies numeric filtering to the DataFrame + """ + column = self.numeric_filter['column'] + operator = self.numeric_filter['operator'] + value = self.numeric_filter['value'] + + logger.info(get_backend_translation("numeric_filter_applied", column=column, operator=operator, value=value)) + + if column is None: + # Apply filter across all columns - a row matches if ANY column meets the criteria + return self._apply_numeric_filter_all_columns(df, operator, value) + else: + # Apply filter to specific column + return self._apply_numeric_filter_single_column(df, column, operator, value) + + def _apply_numeric_filter_single_column(self, df: pd.DataFrame, column: str, + operator: str, value: float) -> pd.DataFrame: + """ + Apply numeric filter to a single column + """ + # Check if the column exists + if column not in df.columns: + logger.error(get_backend_translation("column_does_not_exist", column=column)) + raise ValueError(f"Column '{column}' does not exist in the DataFrame") + + # Convert the column to numeric values (ignore errors for non-numeric values) + numeric_series = pd.to_numeric(df[column], errors='coerce') + + # Apply the comparison operator + if operator == '>': + mask = numeric_series > value + elif operator == '<': + mask = numeric_series < value + elif operator == '>=': + mask = numeric_series >= value + elif operator == '<=': + mask = numeric_series <= value + elif operator == '=': + mask = numeric_series == value + else: + logger.error(get_backend_translation("unknown_operator", operator=operator)) + raise ValueError(f"Unknown operator: {operator}") + + # Apply filter + filtered_df = df[mask] + logger.info(get_backend_translation("numeric_filter_single_column_results", matches=mask.sum(), total=len(df), column=column, operator=operator, value=value)) + + # Log some examples of the filtered values + if len(filtered_df) > 0: + sample_values = filtered_df[column].head(3).tolist() + logger.debug(get_backend_translation("sample_filtered_values", values=sample_values)) + + return filtered_df + + def _apply_numeric_filter_all_columns(self, df: pd.DataFrame, operator: str, value: float) -> pd.DataFrame: + """ + Apply numeric filter across all columns - a row matches if ANY column meets the criteria + """ + logger.info(get_backend_translation("numeric_filter_all_columns", operator=operator, value=value)) + + # Create a mask that will be True for rows where ANY column meets the criteria + combined_mask = pd.Series([False] * len(df), index=df.index) + + # Check each column + for col in df.columns: + # Convert the column to numeric values + numeric_series = pd.to_numeric(df[col], errors='coerce') + + # Apply the comparison operator + if operator == '>': + col_mask = numeric_series > value + elif operator == '<': + col_mask = numeric_series < value + elif operator == '>=': + col_mask = numeric_series >= value + elif operator == '<=': + col_mask = numeric_series <= value + elif operator == '=': + col_mask = numeric_series == value + else: + logger.error(get_backend_translation("unknown_operator", operator=operator)) + raise ValueError(f"Unknown operator: {operator}") + + # Combine with OR logic (any column matching makes the row match) + combined_mask = combined_mask | col_mask + + # Log matches for this column + matches = col_mask.sum() + if matches > 0: + logger.debug(get_backend_translation("column_matches_found", column=col, matches=matches)) + + # Apply filter + filtered_df = df[combined_mask] + logger.info(get_backend_translation("numeric_filter_all_columns_results", matches=combined_mask.sum(), total=len(df), operator=operator, value=value)) + + return filtered_df + + def write_excel(self, df: pd.DataFrame): + """ + Writes the filtered DataFrame to a new Excel file + """ + try: + # If specific columns were selected, only write those + if self.columns: + # Only keep the selected columns (if they exist in the DataFrame) + columns_to_keep = [col for col in self.columns if col in df.columns] + df_filtered = df[columns_to_keep] + logger.info(get_backend_translation("writing_selected_columns", columns=columns_to_keep)) + else: + # Write all columns + df_filtered = df + logger.info(get_backend_translation("writing_all_columns", columns=list(df.columns))) + + # Collect output statistics + self.stats['output_rows'] = len(df_filtered) + self.stats['output_columns'] = len(df_filtered.columns) + + df_filtered.to_excel(self.output_file, index=False) + + # Get output file size and calculate compression ratio + if os.path.exists(self.output_file): + self.stats['output_file_size'] = os.path.getsize(self.output_file) + if self.stats['input_file_size'] > 0: + self.stats['compression_ratio'] = self.stats['output_file_size'] / self.stats['input_file_size'] + + logger.info(get_backend_translation("output_file_written", file=self.output_file)) + logger.info(get_backend_translation("output_dimensions", rows=self.stats['output_rows'], columns=self.stats['output_columns'])) + logger.info(get_backend_translation("output_file_size", size=self.stats['output_file_size'] / (1024*1024))) + + if self.stats['input_file_size'] > 0: + compression_pct = (self.stats['compression_ratio'] - 1) * 100 + if compression_pct > 0: + logger.info(get_backend_translation("compression_larger", percent=compression_pct)) + else: + logger.info(get_backend_translation("compression_smaller", percent=compression_pct)) + + except PermissionError: + logger.error(get_backend_translation("no_write_permission", file=self.output_file)) + raise PermissionError(f"No write permission for the file {self.output_file}") + except Exception as e: + logger.error(get_backend_translation("error_writing_excel_file", error=str(e))) + raise Exception(f"Error writing the Excel file: {e}") + + def process(self): + """ + Main method for processing the Excel file + """ + # Start timing + self.stats['start_time'] = time.time() + + try: + logger.info(get_backend_translation("starting_excel_filter_processing")) + df = self.read_excel() + filtered_df = self.filter_dataframe(df) + self.write_excel(filtered_df) + + # End timing and calculate final statistics + self.stats['end_time'] = time.time() + self.stats['processing_time_seconds'] = self.stats['end_time'] - self.stats['start_time'] + + self._log_final_statistics() + + logger.info(get_backend_translation("excel_filter_processing_completed")) + return True, None + + except FileNotFoundError as e: + error_msg = get_backend_translation("error_file_not_found", error=str(e)) + logger.error(error_msg) + return False, error_msg + except PermissionError as e: + error_msg = get_backend_translation("error_permission", error=str(e)) + logger.error(error_msg) + return False, error_msg + except pd.errors.EmptyDataError as e: + error_msg = get_backend_translation("error_empty_excel", error=str(e)) + logger.error(error_msg) + return False, error_msg + except pd.errors.ParserError as e: + error_msg = get_backend_translation("error_parser", error=str(e)) + logger.error(error_msg) + return False, error_msg + except re.error as e: + error_msg = get_backend_translation("error_invalid_regex", error=str(e)) + logger.error(error_msg) + return False, error_msg + except ValueError as e: + error_msg = get_backend_translation("error_invalid_input", error=str(e)) + logger.error(error_msg) + return False, error_msg + except Exception as e: + error_msg = get_backend_translation("error_unexpected", type=type(e).__name__, error=str(e)) + logger.error(error_msg) + return False, error_msg + + def get_statistics(self) -> Dict[str, Any]: + """ + Returns the collected statistics + + Returns: + Dictionary with all collected statistics + """ + return self.stats.copy() + + def _log_final_statistics(self): + """ + Logs the final comprehensive statistics of the processing + """ + logger.info(get_backend_translation("processing_statistics")) + logger.info(get_backend_translation("processing_time", time=self.stats['processing_time_seconds'])) + + # File statistics + logger.info(get_backend_translation("file_statistics")) + logger.info(get_backend_translation("input_file_size", size=self.stats['input_file_size'] / (1024*1024))) + logger.info(get_backend_translation("output_file_size", size=self.stats['output_file_size'] / (1024*1024))) + if self.stats['compression_ratio'] > 0: + compression_pct = (self.stats['compression_ratio'] - 1) * 100 + logger.info(get_backend_translation("compression_rate", rate=compression_pct)) + + # Data dimensions + logger.info(get_backend_translation("data_dimensions")) + logger.info(get_backend_translation("input_dimensions", rows=self.stats['input_rows'], columns=self.stats['input_columns'])) + logger.info(get_backend_translation("output_dimensions", rows=self.stats['output_rows'], columns=self.stats['output_columns'])) + + # Filtering results + if self.stats['filters_applied']: + logger.info(get_backend_translation("filter_results")) + logger.info(get_backend_translation("applied_filters", filters=', '.join(self.stats['filters_applied']))) + if self.stats['input_rows'] > 0: + retention_rate = (self.stats['rows_filtered'] / self.stats['input_rows']) * 100 + removal_rate = (self.stats['rows_removed'] / self.stats['input_rows']) * 100 + logger.info(get_backend_translation("rows_retained", rows=self.stats['rows_filtered'], rate=retention_rate)) + logger.info(get_backend_translation("rows_removed", rows=self.stats['rows_removed'], rate=removal_rate)) + + # Performance metrics + logger.info(get_backend_translation("performance_metrics")) + logger.info(get_backend_translation("memory_usage", size=self.stats['memory_usage_mb'])) + if self.stats['processing_time_seconds'] > 0 and self.stats['input_rows'] > 0: + rows_per_second = self.stats['input_rows'] / self.stats['processing_time_seconds'] + logger.info(get_backend_translation("processing_speed", speed=rows_per_second)) + + logger.info(get_backend_translation("end_statistics")) diff --git a/excel_filter/gui_components/config_tab.py b/excel_filter/gui_components/config_tab.py new file mode 100644 index 0000000..62c2450 --- /dev/null +++ b/excel_filter/gui_components/config_tab.py @@ -0,0 +1,1317 @@ +""" +Configuration tab component for the Excel Filter GUI +Matches the original layout with two-column design and configuration at top +""" + +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +import tkinter.font as tkfont +import subprocess +import sys +import os + +# Update the import to use absolute path +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from translations import Translations + + +class ConfigTab: + """ + Configuration tab component that matches the original UI layout + """ + + def __init__(self, parent_frame, win11_colors, pattern_presets, pattern_descriptions, translations=None, scale_factor=1.0): + """ + Initialize the configuration tab + + Args: + parent_frame: Parent frame to attach to + win11_colors: Windows 11 color palette dictionary + pattern_presets: Dictionary of pattern presets + pattern_descriptions: Dictionary of pattern descriptions + translations: Translations object for internationalization + scale_factor: DPI scaling factor for high-resolution displays + """ + self.frame = ttk.Frame(parent_frame) + self.win11_colors = win11_colors + # Store references to the main GUI's preset dictionaries + self.pattern_presets = pattern_presets + self.pattern_descriptions = pattern_descriptions + self.translations = translations or Translations() + self.scale_factor = scale_factor + + # Variables for input fields + self.input_file_var = tk.StringVar() + self.output_file_var = tk.StringVar() + self.pattern_var = tk.StringVar(value="error|warning|critical") + self.sheet_var = tk.StringVar(value="Sheet1") + self.columns_var = tk.StringVar() + self.status_var = tk.StringVar(value=self.translations["status_ready"]) + self.regex_column_var = tk.StringVar(value=self.translations.get("all_columns", "Alle Spalten")) + self.pattern_description_var = tk.StringVar() + self.regex_enabled_var = tk.BooleanVar(value=True) # Regex mode enabled by default + self.column_selection_enabled = tk.BooleanVar(value=False) # Column selection disabled by default (all columns selected) + self.columns_vars = {} # Dictionary to track selected columns + + # UI helpers + self._tooltip_after_id = None + self._tooltip_text_var = tk.StringVar(value="") + + # NEW: Numeric filtering variables + self.numeric_filter_enabled = tk.BooleanVar(value=False) + self.numeric_column_var = tk.StringVar(value="") + self.numeric_operator_var = tk.StringVar(value=">") + self.numeric_value_var = tk.StringVar(value="") + self.numeric_filter_description_var = tk.StringVar(value="") + + # Reference to execution tab for logging (will be set later) + self.execution_tab = None + + # Callback for column selection changes + self.on_columns_changed = None + + self.create_widgets() + + def create_widgets(self): + """ + Create all widgets for the configuration tab - matches original layout + """ + # ===== UI metrics (Windows-like spacing & typography) ===== + # Keep sizes DPI-aware but consistent. + pad_outer = int(12 * self.scale_factor) + pad_section_y = int(10 * self.scale_factor) + pad_cell_x = int(8 * self.scale_factor) + pad_cell_y = int(6 * self.scale_factor) + # Readability: enforce minimum size 12 for all visible text + min_font_size = 12 + + base_family = 'Segoe UI' if 'Segoe UI' in tkfont.families() else 'Helvetica' + font_base = (base_family, max(int(10 * self.scale_factor), min_font_size)) + font_label = (base_family, max(int(10 * self.scale_factor), min_font_size)) + font_section = (base_family, max(int(10 * self.scale_factor), min_font_size), 'bold') + font_hint = (base_family, max(int(9 * self.scale_factor), min_font_size), 'italic') + font_info = (base_family, max(int(11 * self.scale_factor), min_font_size), 'bold') + + def _grid(widget, row, column, **kw): + """Small helper to ensure consistent grid padding.""" + if 'padx' not in kw: + kw['padx'] = pad_cell_x + if 'pady' not in kw: + kw['pady'] = pad_cell_y + widget.grid(row=row, column=column, **kw) + + # ===== Styles ===== + style = ttk.Style() + + # Base widget fonts (keeps controls uniform) + try: + style.configure('TLabel', font=font_label) + style.configure('TEntry', font=font_base) + style.configure('TCheckbutton', font=font_label) + style.configure('TButton', font=font_base) + style.configure('TCombobox', font=font_base) + except Exception: + pass + + # Ensure combobox dropdown list uses readable font + try: + self.frame.option_add('*TCombobox*Listbox.font', font_base) + self.frame.option_add('*ComboboxPopdownFrame*Listbox.font', font_base) + self.frame.option_add('*Menu.font', font_base) + except Exception: + pass + + # Main container with two-column layout + config_main_frame = ttk.Frame(self.frame, padding=str(pad_outer)) + config_main_frame.pack(fill=tk.BOTH, expand=True, padx=pad_outer, pady=pad_outer) + + # Left frame for input fields + left_frame = ttk.Frame(config_main_frame) + left_frame.grid(row=0, column=0, padx=(0, pad_outer), pady=0, sticky=tk.NSEW) + + # Right frame for sheet and columns selection + right_frame = ttk.Frame(config_main_frame) + right_frame.grid(row=0, column=1, padx=(pad_outer, 0), pady=0, sticky=tk.NSEW) + + # No vertical separator here: spacing between the columns is enough and + # looks more native on Windows. + + # Column configuration + config_main_frame.columnconfigure(0, weight=1) + config_main_frame.columnconfigure(1, weight=1) + config_main_frame.rowconfigure(0, weight=1) + + # Column configuration + right_frame.columnconfigure(0, weight=1) + right_frame.columnconfigure(1, weight=1) + right_frame.rowconfigure(6, weight=1) + + # Left frame grid configuration + left_frame.columnconfigure(0, weight=0) + left_frame.columnconfigure(1, weight=1) + left_frame.columnconfigure(2, weight=0) + + # ===== LEFT FRAME CONTENT ===== + + # Configuration section at the top (moved from bottom) + config_section = ttk.LabelFrame(left_frame, text=self.translations["config_section"], padding=str(int(10 * self.scale_factor))) + _grid(config_section, row=0, column=0, columnspan=3, sticky=tk.W+tk.E, pady=(0, pad_section_y)) + + # Add info tooltip for config section + # Header line with info + config_title_frame = ttk.Frame(config_section) + config_title_frame.pack(fill=tk.X, pady=(0, pad_cell_y)) + + header_label = ttk.Label(config_title_frame, text=self.translations["config_section"], font=font_section) + header_label.pack(side=tk.LEFT) + + config_info = ttk.Label(config_title_frame, text="ⓘ", foreground=self.win11_colors.get('primary', 'blue'), font=font_info) + config_info.pack(side=tk.LEFT, padx=(6, 0)) + self._bind_tooltip(config_info, "Du kannst Konfigurationen speichern und laden, statt alles jedes Mal neu einzugeben.") + + # Config buttons frame + config_buttons_frame = ttk.Frame(config_section) + config_buttons_frame.pack(fill=tk.X, pady=(0, pad_cell_y)) + + load_button = ttk.Button(config_buttons_frame, text=self.translations["load_button"], command=self.load_config) + load_button.pack(side=tk.LEFT, padx=(0, int(5 * self.scale_factor))) + + save_button = ttk.Button(config_buttons_frame, text=self.translations["save_button"], command=self.save_config) + save_button.pack(side=tk.LEFT) + + # Status text inside config section + config_status_label = ttk.Label(config_section, textvariable=self.status_var, foreground=self.win11_colors.get('primary', 'blue')) + config_status_label.pack(anchor=tk.W) + + # Input file + input_label = ttk.Label(left_frame, text=self.translations["input_file"]) + _grid(input_label, row=1, column=0, sticky=tk.W) + + input_entry = ttk.Entry(left_frame, textvariable=self.input_file_var) + _grid(input_entry, row=1, column=1, sticky=tk.W+tk.E) + + input_button = ttk.Button(left_frame, text=self.translations["browse_button"]) + _grid(input_button, row=1, column=2, sticky=tk.E) + # Command will be set in the main GUI after initialization + + # Output file + output_label = ttk.Label(left_frame, text=self.translations["output_file"]) + _grid(output_label, row=2, column=0, sticky=tk.W) + + output_entry = ttk.Entry(left_frame, textvariable=self.output_file_var) + _grid(output_entry, row=2, column=1, sticky=tk.W+tk.E) + + output_button = ttk.Button(left_frame, text=self.translations["browse_button"]) + _grid(output_button, row=2, column=2, sticky=tk.E) + # Command will be set in the main GUI after initialization + + # Sheet selection - MOVED HERE from right column to group with file inputs + sheet_label = ttk.Label(left_frame, text=self.translations.get("sheet", "Arbeitsblatt"), font=font_label) + _grid(sheet_label, row=3, column=0, sticky=tk.W) + + self.sheet_combobox = ttk.Combobox(left_frame, textvariable=self.sheet_var, width=35) + _grid(self.sheet_combobox, row=3, column=1, columnspan=2, sticky=tk.W+tk.E) + self.sheet_combobox.bind("<>", self.on_sheet_selected) + + # No regex checkbox - NEW FEATURE (moved to top for better visibility) + no_regex_frame = ttk.Frame(left_frame) + _grid(no_regex_frame, row=4, column=0, columnspan=3, sticky=tk.W) + + regex_checkbox = ttk.Checkbutton( + no_regex_frame, + text="Regex-Filter aktivieren", + variable=self.regex_enabled_var, + command=self.toggle_regex_mode + ) + regex_checkbox.pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + + # Info tooltip for no regex mode + no_regex_info = ttk.Label(no_regex_frame, text="ⓘ", foreground=self.win11_colors.get('primary', 'blue'), font=font_info) + no_regex_info.pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + self._bind_tooltip(no_regex_info, "Wenn aktiviert, werden Regex-Muster für die Filterung verwendet") + + # Regex Options Box - GROUPED AND DISABLEABLE + self.regex_options_frame = ttk.LabelFrame( + left_frame, + text=" Regex-Optionen ", + padding="10", + style='Regex.TLabelframe' + ) + _grid(self.regex_options_frame, row=5, column=0, columnspan=3, sticky=tk.W+tk.E, pady=(0, pad_section_y)) + + self.regex_options_frame.columnconfigure(0, weight=0) + self.regex_options_frame.columnconfigure(1, weight=1) + + # Regex pattern (inside the box) + pattern_label = ttk.Label(self.regex_options_frame, text="Regex-Muster:") + pattern_label.grid(row=0, column=0, sticky=tk.W, pady=5) + + pattern_entry = ttk.Entry(self.regex_options_frame, textvariable=self.pattern_var) + pattern_entry.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W+tk.E) + + # Pattern presets (inside the box) + pattern_preset_label = ttk.Label(self.regex_options_frame, text="Voreingestellte Muster:") + pattern_preset_label.grid(row=1, column=0, sticky=tk.W, pady=5) + + self.pattern_preset_combobox = ttk.Combobox( + self.regex_options_frame, + values=list(self.pattern_presets.keys()), + state="readonly", + width=35 + ) + self.pattern_preset_combobox.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W+tk.E) + self.pattern_preset_combobox.bind("<>", self.on_pattern_preset_selected) + + # Pattern description (inside the box) + pattern_description_label = ttk.Label( + self.regex_options_frame, + textvariable=self.pattern_description_var, + foreground="gray" + ) + pattern_description_label.grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=5) + + # Regex column selection (inside the box) + regex_column_label = ttk.Label(self.regex_options_frame, text="Regex auf Spalte anwählen:") + regex_column_label.grid(row=3, column=0, sticky=tk.W, pady=5) + + self.regex_column_combobox = ttk.Combobox( + self.regex_options_frame, + textvariable=self.regex_column_var, + state="readonly", + width=35 + ) + self.regex_column_combobox.grid(row=3, column=1, padx=5, pady=5, sticky=tk.W+tk.E) + + # Presets manager button at the end of regex options + presets_button_frame = ttk.Frame(self.regex_options_frame) + presets_button_frame.grid(row=4, column=0, columnspan=2, pady=(10, 0), sticky=tk.W+tk.E) + + presets_button = ttk.Button(presets_button_frame, text="Muster verwalten…", command=self.manage_presets, style='Outlined.TButton') + presets_button.pack(side=tk.RIGHT) + + # Style the presets button with outline + style.configure('Outlined.TButton', + font=font_base, + borderwidth=2, + relief='solid', + bordercolor=self.win11_colors['border']) + style.map('Outlined.TButton', + background=[('active', self.win11_colors['hover'])], + bordercolor=[('active', self.win11_colors['primary'])]) + + # Style the regex options box with Windows 11 styling + style.configure('Regex.TLabelframe', + background=self.win11_colors['surface'], + borderwidth=1, + relief='groove', + bordercolor=self.win11_colors['border']) + style.configure('Regex.TLabelframe.Label', + foreground=self.win11_colors['primary'], + background=self.win11_colors['surface'], + font=font_section) + + # ===== RIGHT FRAME CONTENT ===== + + # Numeric filter enable checkbox - OUTSIDE the box (above the blue text) + numeric_enable_frame = ttk.Frame(right_frame) + _grid(numeric_enable_frame, row=0, column=0, columnspan=2, sticky=tk.W, pady=(0, pad_cell_y)) + + numeric_checkbox = ttk.Checkbutton( + numeric_enable_frame, + text="Numerische Filter aktivieren", + variable=self.numeric_filter_enabled, + command=self.toggle_numeric_mode + ) + numeric_checkbox.pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + + # NEW: Numeric Filter Options Box - AT TOP OF SECOND COLUMN (below checkbox) + self.numeric_options_frame = ttk.LabelFrame( + right_frame, + text="Numerische Filter", + padding="10", + style='Numeric.TLabelframe' + ) + self.numeric_options_frame.grid(row=1, column=0, columnspan=2, pady=(0, 10), sticky=tk.W+tk.E) + + self.numeric_options_frame.columnconfigure(0, weight=0) + self.numeric_options_frame.columnconfigure(1, weight=1) + + # Numeric column selection (inside the box) + numeric_column_label = ttk.Label(self.numeric_options_frame, text="Spalte:") + numeric_column_label.grid(row=1, column=0, sticky=tk.W, pady=5) + + self.numeric_column_combobox = ttk.Combobox( + self.numeric_options_frame, + textvariable=self.numeric_column_var, + state="readonly", + width=28 + ) + self.numeric_column_combobox.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W+tk.E) + + # Numeric operator selection + numeric_operator_label = ttk.Label(self.numeric_options_frame, text="Vergleich:") + numeric_operator_label.grid(row=2, column=0, sticky=tk.W, pady=5) + + numeric_operator_combobox = ttk.Combobox( + self.numeric_options_frame, + textvariable=self.numeric_operator_var, + values=["> (größer als)", "< (kleiner als)", ">= (größer/gleich)", "<= (kleiner/gleich)", "= (gleich)"], + state="readonly", + width=28 + ) + numeric_operator_combobox.grid(row=2, column=1, padx=5, pady=5, sticky=tk.W+tk.E) + + # Numeric value input + numeric_value_label = ttk.Label(self.numeric_options_frame, text="Wert:") + numeric_value_label.grid(row=3, column=0, sticky=tk.W, pady=5) + + numeric_value_entry = ttk.Entry( + self.numeric_options_frame, + textvariable=self.numeric_value_var, + width=28 + ) + numeric_value_entry.grid(row=3, column=1, padx=5, pady=5, sticky=tk.W+tk.E) + + # Numeric filter description + numeric_description_label = ttk.Label( + self.numeric_options_frame, + textvariable=self.numeric_filter_description_var, + foreground=self.win11_colors.get('primary', 'blue'), + font=font_hint + ) + numeric_description_label.grid(row=4, column=0, columnspan=2, sticky=tk.W, pady=5) + + # Bind numeric value changes to update description + self.numeric_value_var.trace_add("write", lambda *args: self.update_numeric_description()) + self.numeric_operator_var.trace_add("write", lambda *args: self.update_numeric_description()) + self.numeric_column_var.trace_add("write", lambda *args: self.update_numeric_description()) + + # Style the numeric options box + style.configure('Numeric.TLabelframe', + background=self.win11_colors['surface'], + borderwidth=1, + relief='groove', + bordercolor=self.win11_colors['border']) + style.configure('Numeric.TLabelframe.Label', + foreground=self.win11_colors['primary'], + background=self.win11_colors['surface'], + font=font_section) + + # Style the columns selection box + style.configure('Columns.TLabelframe', + background=self.win11_colors['surface'], + borderwidth=1, + relief='groove', + bordercolor=self.win11_colors['border']) + style.configure('Columns.TLabelframe.Label', + foreground=self.win11_colors['primary'], + background=self.win11_colors['surface'], + font=font_section) + + # Initially disable numeric options + self.toggle_numeric_mode() + + # Regex builder info + regex_builder_info = ttk.Label( + left_frame, + text="Eigene Muster zur Filterung können um Regex-Builder erstellt werden.", + foreground=self.win11_colors['primary'], + font=font_hint + ) + _grid(regex_builder_info, row=6, column=0, columnspan=3, sticky=tk.W, pady=(pad_section_y, 0)) + + # Column selection enable checkbox - OUTSIDE the box (above the blue text) + column_enable_frame = ttk.Frame(right_frame) + _grid(column_enable_frame, row=2, column=0, columnspan=2, sticky=tk.W, pady=(0, pad_cell_y)) + + column_checkbox = ttk.Checkbutton( + column_enable_frame, + text="Spaltenauswahl aktivieren", + variable=self.column_selection_enabled, + command=self.toggle_column_selection_mode + ) + column_checkbox.pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + + # Columns selection frame with consistent styling + self.columns_frame = ttk.LabelFrame(right_frame, text="Spaltenauswahl", padding="8", style='Columns.TLabelframe') + self.columns_frame.grid(row=3, column=0, columnspan=2, pady=(0, pad_section_y), sticky=tk.W+tk.E) + + + # Column selection buttons + select_button_frame = ttk.Frame(self.columns_frame) + select_button_frame.grid(row=1, column=0, columnspan=2, sticky=tk.W+tk.E, pady=(0, 5)) + + select_all_button = ttk.Button( + select_button_frame, + text="Alle auswählen", + command=self.select_all_columns + ) + select_all_button.pack(side=tk.LEFT, padx=(0, 6)) + + deselect_all_button = ttk.Button( + select_button_frame, + text="Alle abwählen", + command=self.deselect_all_columns + ) + deselect_all_button.pack(side=tk.LEFT) + + # Columns container + self.columns_container = ttk.Frame(self.columns_frame) + self.columns_container.grid(row=2, column=0, columnspan=2, sticky=tk.W+tk.E) + self.columns_container.columnconfigure(0, weight=1) + self.columns_container.columnconfigure(1, weight=1) + self.columns_container.columnconfigure(2, weight=1) + + # Initially disable column selection + self.toggle_column_selection_mode() + + # Process button + button_frame = ttk.Frame(right_frame) + button_frame.grid(row=4, column=0, columnspan=2, pady=(pad_section_y, 0), sticky=tk.EW) + + process_button = ttk.Button( + button_frame, + text="Weiter zur Ausführung", + command=self.navigate_to_execution_tab + ) + process_button.pack(side=tk.RIGHT, ipadx=18, ipady=10) + + # Style the process button + process_button.configure(style='Accent.TButton') + style.configure('Accent.TButton', + font=(base_family, max(int(10 * self.scale_factor), min_font_size), 'bold'), + padding=int(10 * self.scale_factor), + borderwidth=1) + style.map('Accent.TButton', + foreground=[('active', 'white'), ('disabled', 'gray')], + background=[('active', self.win11_colors['primary']), ('disabled', self.win11_colors['background'])], + lightcolor=[('active', self.win11_colors['primary_light'])], + darkcolor=[('active', self.win11_colors['primary_dark'])]) + + # Variable tracing for validation + self.input_file_var.trace_add("write", lambda *args: self.validate_process_button()) + self.output_file_var.trace_add("write", lambda *args: self.validate_process_button()) + self.pattern_var.trace_add("write", lambda *args: self.validate_process_button()) + self.regex_enabled_var.trace_add("write", lambda *args: self.validate_process_button()) + + # Initial validation + self.validate_process_button() + + def browse_input_file(self): + """Browse for input file""" + file_path = filedialog.askopenfilename( + title="Eingabedatei auswählen", + filetypes=[("Excel Dateien", "*.xlsx"), ("Alle Dateien", "*.*")] + ) + if file_path: + self.input_file_var.set(file_path) + self.log_message(f"Eingabedatei ausgewählt: {file_path}") + # Update sheet and column selection when input file is chosen + self.update_sheet_selection() + self.update_columns_selection() + + def browse_output_file(self): + """Browse for output file""" + file_path = filedialog.asksaveasfilename( + title="Ausgabedatei speichern", + defaultextension=".xlsx", + filetypes=[("Excel Dateien", "*.xlsx"), ("Alle Dateien", "*.*")] + ) + if file_path: + self.output_file_var.set(file_path) + self.log_message(f"Ausgabedatei ausgewählt: {file_path}") + + def browse_config_file(self): + """Browse for config file location""" + file_path = filedialog.asksaveasfilename( + title="Konfigurationsdatei auswählen", + defaultextension=".json", + filetypes=[("JSON Dateien", "*.json"), ("Alle Dateien", "*.*")] + ) + if file_path: + self.config_file_var.set(file_path) + self.log_message(f"Konfigurationsdatei ausgewählt: {file_path}") + + def on_pattern_preset_selected(self, event): + """Handle pattern preset selection""" + selected_pattern = event.widget.get() + if selected_pattern in self.pattern_presets: + self.pattern_var.set(self.pattern_presets[selected_pattern]) + self.pattern_description_var.set(self.pattern_descriptions.get(selected_pattern, "")) + + def toggle_regex_mode(self): + """Toggle between regex enabled/disabled mode""" + if self.regex_enabled_var.get(): + # Regex mode enabled - enable regex options box + self.log_message("Modus: Regex-Filterung aktiviert") + # Set a default pattern if empty + if not self.pattern_var.get(): + self.pattern_var.set("error|warning|critical") + + # Enable all widgets in the regex options frame + for child in self.regex_options_frame.winfo_children(): + if isinstance(child, (ttk.Entry, ttk.Combobox)): + child.config(state=tk.NORMAL) + elif isinstance(child, ttk.Label): + child.config(foreground="black") + + # Restore the frame style + style = ttk.Style() + style.configure('Regex.TLabelframe', background=self.win11_colors['surface']) + + else: + # Regex mode disabled - disable regex options box + self.log_message("Modus: Nur Spalten filtern (kein Regex)") + self.pattern_var.set("") # Clear pattern when regex is disabled + + # Disable all widgets in the regex options frame + for child in self.regex_options_frame.winfo_children(): + if isinstance(child, (ttk.Entry, ttk.Combobox)): + child.config(state=tk.DISABLED) + elif isinstance(child, ttk.Label): + child.config(foreground="gray") + + # Change the frame style to show it's disabled + style = ttk.Style() + style.configure('Regex.TLabelframe', background=self.win11_colors['background']) + + def _bind_tooltip(self, widget, text): + """Bind Windows-like tooltip behavior to a widget.""" + widget.bind("", lambda e, t=text: self.show_tooltip(t)) + widget.bind("", lambda e: self.hide_tooltip()) + + def show_tooltip(self, text): + """Show tooltip text.""" + # Delay slightly to avoid flicker when moving the mouse quickly. + self._tooltip_text_var.set(text) + if self._tooltip_after_id is not None: + try: + self.frame.after_cancel(self._tooltip_after_id) + except Exception: + pass + self._tooltip_after_id = None + + self._tooltip_after_id = self.frame.after(350, self._show_tooltip_now) + + def _show_tooltip_now(self): + text = self._tooltip_text_var.get() + if not text: + return + + # Create tooltip if it doesn't exist + if not hasattr(self, 'tooltip_window'): + self.tooltip_window = tk.Toplevel(self.frame) + self.tooltip_window.overrideredirect(True) + self.tooltip_window.attributes('-topmost', True) + + tooltip_frame = ttk.Frame(self.tooltip_window, padding=6) + tooltip_frame.pack(fill=tk.BOTH, expand=True) + + tooltip_label = ttk.Label( + tooltip_frame, + textvariable=self._tooltip_text_var, + background="#FFFFE1", # classic tooltip yellow + relief="solid", + borderwidth=1 + ) + tooltip_label.pack() + + # Position tooltip near mouse + try: + x = self.frame.winfo_pointerx() + 12 + y = self.frame.winfo_pointery() + 16 + self.tooltip_window.geometry(f"+{x}+{y}") + self.tooltip_window.deiconify() + except Exception: + pass + + def hide_tooltip(self): + """Hide tooltip""" + if self._tooltip_after_id is not None: + try: + self.frame.after_cancel(self._tooltip_after_id) + except Exception: + pass + self._tooltip_after_id = None + + if hasattr(self, 'tooltip_window'): + try: + self.tooltip_window.withdraw() + except Exception: + self.tooltip_window.destroy() + del self.tooltip_window + + def on_sheet_selected(self, event): + """Handle sheet selection""" + selected_sheet = event.widget.get() + self.sheet_var.set(selected_sheet) + self.update_columns_selection() + + def update_sheet_selection(self): + """Update sheet selection from the Excel file""" + input_file = self.input_file_var.get() + if not input_file: + messagebox.showerror("Fehler", "Bitte geben Sie eine Eingabedatei an") + return + + try: + # Import pandas for Excel file reading + import pandas as pd + + # Read Excel file to get sheet names + xls = pd.ExcelFile(input_file) + sheets = xls.sheet_names + + # Update sheet combobox + self.sheet_combobox['values'] = sheets + if sheets: + self.sheet_combobox.current(0) + self.sheet_var.set(sheets[0]) + + self.log_message(f"Arbeitsblattauswahl aktualisiert: {sheets}") + + except Exception as e: + self.log_error(f"Fehler beim Aktualisieren der Arbeitsblattauswahl: {e}") + messagebox.showerror("Fehler", f"Fehler beim Aktualisieren der Arbeitsblattauswahl: {e}") + + def update_columns_selection(self, selected_columns=None): + """ + Update columns selection from the Excel file + + Args: + selected_columns: List of column names that should be initially selected + """ + input_file = self.input_file_var.get() + if not input_file: + messagebox.showerror("Fehler", "Bitte geben Sie eine Eingabedatei an") + return + + try: + # Import pandas for Excel file reading + import pandas as pd + + # Get current sheet + sheet_name = self.sheet_var.get() + + # Read Excel file to get columns + if sheet_name: + df = pd.read_excel(input_file, sheet_name=sheet_name) + else: + df = pd.read_excel(input_file) + + columns = df.columns.tolist() + + # Handle columns without names + for i, column in enumerate(columns): + if pd.isna(column) or column == "": + # Find first non-empty value in the column + for value in df.iloc[:, i]: + if pd.notna(value) and value != "": + columns[i] = str(value)[:10] # Limit to 10 characters + break + else: + columns[i] = f"Spalte_{i+1}" # If all values are empty + + # Clear existing columns + for widget in self.columns_container.winfo_children(): + widget.destroy() + + # Update regex column combobox + self.regex_column_combobox['values'] = ["Alle Spalten"] + columns + self.regex_column_combobox.current(0) + + # Create new column checkboxes + self.columns_vars = {} + for i, column in enumerate(columns): + var = tk.IntVar() + # If selected_columns is provided and column is in it, select the checkbox + if selected_columns and column in selected_columns: + var.set(1) + self.columns_vars[column] = var + # Add command to trigger callback when checkbox state changes + checkbox = ttk.Checkbutton( + self.columns_container, + text=column, + variable=var, + command=self._column_selection_changed + ) + checkbox.grid(row=i // 3, column=i % 3, sticky=tk.W, padx=5, pady=2) + + self.log_message(f"Spaltenauswahl aktualisiert: {columns}") + + # Notify listeners that columns have changed + if self.on_columns_changed: + self.on_columns_changed() + + except Exception as e: + self.log_error(f"Fehler beim Aktualisieren der Spaltenauswahl: {e}") + messagebox.showerror("Fehler", f"Fehler beim Aktualisieren der Spaltenauswahl: {e}") + + def select_all_columns(self): + """Select all columns""" + if self.columns_vars: + for var in self.columns_vars.values(): + var.set(1) + self.log_message("Alle Spalten ausgewählt") + if self.on_columns_changed: + self.on_columns_changed() + else: + self.log_message("Keine Spalten verfügbar zum Auswählen") + + def deselect_all_columns(self): + """Deselect all columns""" + if self.columns_vars: + for var in self.columns_vars.values(): + var.set(0) + self.log_message("Alle Spalten abgewählt") + if self.on_columns_changed: + self.on_columns_changed() + else: + self.log_message("Keine Spalten verfügbar zum Abwählen") + + def toggle_column_selection_mode(self): + """Toggle between column selection enabled/disabled""" + if self.column_selection_enabled.get(): + # Column selection enabled - enable column selection UI + self.log_message("Spaltenauswahl aktiviert") + + # Enable all widgets in the columns frame + for child in self.columns_frame.winfo_children(): + if isinstance(child, (ttk.Button, ttk.Checkbutton)): + child.config(state=tk.NORMAL) + elif isinstance(child, ttk.Label): + child.config(foreground="black") + + # Restore the frame style + style = ttk.Style() + style.configure('Columns.TLabelframe', background=self.win11_colors['surface']) + + else: + # Column selection disabled - select all columns automatically + self.log_message("Spaltenauswahl deaktiviert - alle Spalten werden ausgewählt") + + # Select all columns + if self.columns_vars: + for var in self.columns_vars.values(): + var.set(1) + + # Disable all widgets in the columns frame (except we want to show them as selected) + for child in self.columns_frame.winfo_children(): + if isinstance(child, ttk.Button): + child.config(state=tk.DISABLED) + elif isinstance(child, ttk.Checkbutton): + child.config(state=tk.DISABLED) + elif isinstance(child, ttk.Label): + child.config(foreground="gray") + + # Change the frame style to show it's disabled + style = ttk.Style() + style.configure('Columns.TLabelframe', background=self.win11_colors['background']) + + def get_selected_columns(self): + """ + Gibt die ausgewählten Spalten zurück + Wenn Spaltenauswahl deaktiviert ist, werden alle Spalten zurückgegeben + """ + # If column selection is disabled, return all columns + if not self.column_selection_enabled.get(): + return list(self.columns_vars.keys()) if self.columns_vars else None + + # Otherwise, return only selected columns + selected_columns = [] + for column, var in self.columns_vars.items(): + if var.get(): + selected_columns.append(column) + return selected_columns if selected_columns else None + + def validate_process_button(self): + """Validate process button state""" + # Validation logic + has_input = bool(self.input_file_var.get()) + has_output = bool(self.output_file_var.get()) + + # In regex mode, pattern is required; in non-regex mode, pattern is not required + regex_enabled = self.regex_enabled_var.get() + has_pattern = bool(self.pattern_var.get()) if regex_enabled else True + + # Enable button if: + # - Input and output files are specified + # - Either: regex mode with pattern OR no regex mode + should_enable = has_input and has_output and has_pattern + + # Get the process button (it's in the right frame) + for child in self.frame.winfo_children(): + if isinstance(child, ttk.Frame): + for subchild in child.winfo_children(): + if isinstance(subchild, ttk.Frame) and subchild.grid_info()["column"] == 1: + for button in subchild.winfo_children(): + if isinstance(button, ttk.Frame): + for btn in button.winfo_children(): + if isinstance(btn, ttk.Button) and "VERARBEITEN" in btn.cget("text"): + if should_enable: + btn.config(state=tk.NORMAL) + else: + btn.config(state=tk.DISABLED) + + def process_file(self): + """ + Verarbeitet die Excel-Datei + """ + input_file = self.input_file_var.get() + output_file = self.output_file_var.get() + pattern = self.pattern_var.get() + sheet_name = self.sheet_var.get() + regex_column = self.regex_column_var.get() + + # Validierung + if not input_file or not output_file: + self.status_var.set('Bitte Eingabe- und Ausgabedatei angeben') + self.log_message('Fehler: Eingabe- und/oder Ausgabedatei fehlt') + self.log_error('Fehler: Eingabe- und/oder Ausgabedatei fehlt') + messagebox.showerror("Fehler", "Bitte Eingabe- und Ausgabedatei angeben") + return + + # Check if regex is enabled or if pattern is provided + regex_enabled = self.regex_enabled_var.get() and pattern.strip() + + if not regex_enabled: + self.log_message('Kein Regex-Muster verwendet - es werden nur die ausgewählten Spalten kopiert') + + # Warn if no columns are selected and no regex is provided + columns = self.get_selected_columns() + if not regex_enabled and not columns: + self.status_var.set('Warnung: Kein Regex-Muster und keine Spalten ausgewählt') + self.log_message('Warnung: Kein Regex-Muster und keine Spalten ausgewählt - alle Daten werden kopiert') + if not messagebox.askyesno("Warnung", "Kein Regex-Muster und keine Spalten ausgewählt. Möchten Sie alle Daten kopieren?"): + return + + columns = self.get_selected_columns() + + try: + self.status_var.set('Verarbeitung läuft...') + self.log_message('Verarbeitung gestartet...') + + # Import ExcelFilter using relative import (same as main GUI) + from ..filter import ExcelFilter + import pandas as pd + + # Handle the case where regex is not enabled + if not regex_enabled: + # Just copy the selected columns (or all columns if none selected) + try: + # Read the Excel file + if sheet_name: + df = pd.read_excel(input_file, sheet_name=sheet_name) + else: + df = pd.read_excel(input_file) + + # Select columns if specified + if columns: + # Only keep the selected columns + available_columns = [col for col in columns if col in df.columns] + df = df[available_columns] + self.log_message(f'Kopiere ausgewählte Spalten: {available_columns}') + else: + # Keep all columns + self.log_message('Kopiere alle Spalten') + + # Write to output file + df.to_excel(output_file, index=False) + self.log_message(f'Erfolgreich geschrieben: {output_file}') + + # Ask if the output file should be opened + if messagebox.askyesno("Erfolg", "Möchten Sie die Ausgabedatei öffnen?"): + self.open_file(output_file) + + self.status_var.set('Verarbeitung erfolgreich!') + self.log_message('Verarbeitung erfolgreich abgeschlossen') + return + + except Exception as e: + self.status_var.set(f'Fehler: {e}') + self.log_message(f'Fehler: {e}') + self.log_error(f'Fehler: {e}') + messagebox.showerror("Fehler", f'Fehler: {e}') + return + + # Create ExcelFilter instance for regex filtering + if regex_column == "Alle Spalten": + excel_filter = ExcelFilter( + input_file=input_file, + output_file=output_file, + pattern=pattern, + sheet_name=sheet_name if sheet_name else None, + columns=columns if columns else None + ) + else: + excel_filter = ExcelFilter( + input_file=input_file, + output_file=output_file, + pattern=pattern, + sheet_name=sheet_name if sheet_name else None, + columns=[regex_column] if regex_column else None + ) + + # Process the file with regex filtering + success = excel_filter.process() + + if success: + self.status_var.set('Verarbeitung erfolgreich!') + self.log_message('Verarbeitung erfolgreich abgeschlossen') + messagebox.showinfo("Erfolg", "Verarbeitung erfolgreich abgeschlossen!") + + # Ask if the output file should be opened + if messagebox.askyesno("Erfolg", "Möchten Sie die Ausgabedatei öffnen?"): + self.open_file(output_file) + else: + self.status_var.set('Verarbeitung fehlgeschlagen') + self.log_message('Verarbeitung fehlgeschlagen') + self.log_error('Verarbeitung fehlgeschlagen') + messagebox.showerror("Fehler", "Verarbeitung fehlgeschlagen") + + except Exception as e: + self.status_var.set(f'Fehler: {e}') + self.log_message(f'Fehler: {e}') + self.log_error(f'Fehler: {e}') + messagebox.showerror("Fehler", f'Fehler: {e}') + + def open_file(self, file_path): + """ + Öffnet eine Datei mit dem Standardprogramm + """ + try: + if sys.platform == 'win32': + os.startfile(file_path) + elif sys.platform == 'darwin': + subprocess.run(['open', file_path]) + else: + subprocess.run(['xdg-open', file_path]) + except Exception as e: + self.log_message(f'Konnte Datei nicht öffnen: {e}') + messagebox.showerror("Fehler", f'Konnte Datei nicht öffnen: {e}') + + def save_config(self): + """Save current configuration - prompts user for save location""" + try: + import json + import os + + config = { + 'input_file': self.input_file_var.get(), + 'output_file': self.output_file_var.get(), + 'pattern': self.pattern_var.get(), + 'sheet_name': self.sheet_var.get(), + 'columns': [col for col, var in self.columns_vars.items() if var.get()] + } + + # Prompt user to choose save location + config_file = filedialog.asksaveasfilename( + title="Konfiguration speichern", + defaultextension=".json", + filetypes=[("JSON Dateien", "*.json"), ("Alle Dateien", "*.*")], + initialfile="config.json" + ) + + if not config_file: # User cancelled + return + + with open(config_file, 'w') as f: + json.dump(config, f, indent=2) + + self.status_var.set('Konfiguration gespeichert') + self.log_message(f'Konfiguration gespeichert: {config_file}') + + except Exception as e: + self.status_var.set(f'Fehler beim Speichern: {e}') + self.log_message(f'Fehler beim Speichern: {e}') + self.log_error(f'Fehler beim Speichern: {e}') + + def load_config(self): + """Load configuration - prompts user for file to load""" + try: + import json + import os + + # Prompt user to choose file to load + config_file = filedialog.askopenfilename( + title="Konfiguration laden", + filetypes=[("JSON Dateien", "*.json"), ("Alle Dateien", "*.*")], + initialfile="config.json" + ) + + if not config_file: # User cancelled + return + + if os.path.exists(config_file): + with open(config_file, 'r') as f: + config = json.load(f) + + self.input_file_var.set(config.get('input_file', '')) + self.output_file_var.set(config.get('output_file', '')) + self.pattern_var.set(config.get('pattern', 'error|warning|critical')) + self.sheet_var.set(config.get('sheet_name', 'Sheet1')) + + self.status_var.set('Konfiguration geladen') + self.log_message(f'Konfiguration geladen: {config_file}') + + # Update sheet and column selection if input file is set + if config.get('input_file', ''): + self.update_sheet_selection() + # Pass saved columns to update_columns_selection to restore selections + saved_columns = config.get('columns', []) + self.update_columns_selection(selected_columns=saved_columns) + else: + self.status_var.set('Konfigurationsdatei nicht gefunden') + self.log_message(f'Konfigurationsdatei nicht gefunden: {config_file}') + + except Exception as e: + self.status_var.set(f'Fehler beim Laden: {e}') + self.log_message(f'Fehler beim Laden: {e}') + self.log_error(f'Fehler beim Laden: {e}') + + def log_message(self, message): + """Log a message to the log text widget""" + if self.execution_tab and hasattr(self.execution_tab, 'log_message'): + # Redirect to execution tab + self.execution_tab.log_message(message) + else: + # Fallback to status bar if execution tab not available + self.status_var.set(message) + + def log_error(self, message): + """Log an error message to the log text widget""" + if self.execution_tab and hasattr(self.execution_tab, 'log_message'): + # Redirect to execution tab with error formatting + self.execution_tab.log_message(f"FEHLER: {message}") + else: + # Fallback to status bar if execution tab not available + self.status_var.set(f"FEHLER: {message}") + + def set_execution_tab(self, execution_tab): + """ + Set the reference to the execution tab for logging + + Args: + execution_tab: Reference to the execution tab component + """ + self.execution_tab = execution_tab + + def set_main_gui(self, main_gui): + """ + Set the reference to the main GUI for accessing management functions + + Args: + main_gui: Reference to the main GUI instance + """ + self.main_gui = main_gui + + def set_on_columns_changed(self, callback): + """ + Set a callback to be called when column selection changes + + Args: + callback: Function to call when columns change + """ + self.on_columns_changed = callback + + def _column_selection_changed(self): + """ + Handle column selection changes and notify listeners + """ + if self.on_columns_changed: + self.on_columns_changed() + # Also update execution tab directly if available + if hasattr(self, 'execution_tab') and self.execution_tab: + self.execution_tab.update_command_display() + + def navigate_to_execution_tab(self): + """ + Navigate to the execution tab + """ + if hasattr(self, 'main_gui') and self.main_gui and hasattr(self.main_gui, 'main_window'): + # Find the notebook and select the execution tab + notebook = self.main_gui.main_window.notebook + if notebook: + # Find the execution tab index + tabs = notebook.tabs() + execution_tab_name = self.translations["tab_execution"] if self.translations else "Ausführung" + for i, tab_id in enumerate(tabs): + tab_text = notebook.tab(i, 'text') + if tab_text == execution_tab_name: + notebook.select(i) + break + + def toggle_numeric_mode(self): + """Toggle between numeric filter enabled/disabled""" + if self.numeric_filter_enabled.get(): + # Enable numeric filter options + self.log_message("Numerische Filter aktiviert") + + # Enable all widgets in the numeric options frame + for child in self.numeric_options_frame.winfo_children(): + if isinstance(child, (ttk.Entry, ttk.Combobox)): + child.config(state=tk.NORMAL) + elif isinstance(child, ttk.Label): + child.config(foreground="black") + + # Restore the frame style + style = ttk.Style() + style.configure('Numeric.TLabelframe', background=self.win11_colors['surface']) + + # Update the column combobox with available columns + self.update_numeric_columns() + + else: + # Disable numeric filter options + self.log_message("Numerische Filter deaktiviert") + + # Disable all widgets in the numeric options frame + for child in self.numeric_options_frame.winfo_children(): + if isinstance(child, (ttk.Entry, ttk.Combobox)): + child.config(state=tk.DISABLED) + elif isinstance(child, ttk.Label): + child.config(foreground="gray") + + # Change the frame style to show it's disabled + style = ttk.Style() + style.configure('Numeric.TLabelframe', background=self.win11_colors['background']) + + # Clear numeric filter values + self.numeric_column_var.set("") + self.numeric_value_var.set("") + self.numeric_filter_description_var.set("") + + def update_numeric_columns(self): + """Update the numeric column combobox with available columns""" + input_file = self.input_file_var.get() + if not input_file: + return + + try: + import pandas as pd + + # Get current sheet + sheet_name = self.sheet_var.get() + + # Read Excel file to get columns + if sheet_name: + df = pd.read_excel(input_file, sheet_name=sheet_name) + else: + df = pd.read_excel(input_file) + + columns = df.columns.tolist() + + # Handle columns without names + for i, column in enumerate(columns): + if pd.isna(column) or column == "": + # Find first non-empty value in the column + for value in df.iloc[:, i]: + if pd.notna(value) and value != "": + columns[i] = str(value)[:10] # Limit to 10 characters + break + else: + columns[i] = f"Spalte_{i+1}" # If all values are empty + + # Update numeric column combobox with "Alle Spalten" option + self.numeric_column_combobox['values'] = ["Alle Spalten"] + columns + self.numeric_column_combobox.current(0) # Default to "Alle Spalten" + self.numeric_column_var.set("Alle Spalten") + + except Exception as e: + self.log_error(f"Fehler beim Aktualisieren der numerischen Spalten: {e}") + + def update_numeric_description(self): + """Update the numeric filter description""" + if not self.numeric_filter_enabled.get(): + self.numeric_filter_description_var.set("") + return + + column = self.numeric_column_var.get() + operator = self.numeric_operator_var.get() + value = self.numeric_value_var.get() + + if not column or not operator or not value: + self.numeric_filter_description_var.set("Konfiguration unvollständig") + return + + # Extract operator symbol + operator_map = { + "> (größer als)": ">", + "< (kleiner als)": "<", + ">= (größer/gleich)": ">=", + "<= (kleiner/gleich)": "<=", + "= (gleich)": "=" + } + + operator_symbol = operator_map.get(operator, operator) + + try: + # Validate numeric value + float(value) + description = f"Filter: {column} {operator_symbol} {value}" + self.numeric_filter_description_var.set(description) + except ValueError: + self.numeric_filter_description_var.set("❌ Ungültiger numerischer Wert") + + def get_numeric_filter_settings(self): + """ + Get the current numeric filter settings + + Returns: + dict: Numeric filter configuration or None if disabled + """ + if not self.numeric_filter_enabled.get(): + return None + + column = self.numeric_column_var.get() + operator = self.numeric_operator_var.get() + value = self.numeric_value_var.get() + + if not column or not operator or not value: + return None + + # Convert display operator to symbol + operator_map = { + "> (größer als)": ">", + "< (kleiner als)": "<", + ">= (größer/gleich)": ">=", + "<= (kleiner/gleich)": "<=", + "= (gleich)": "=" + } + + operator_symbol = operator_map.get(operator, operator) + + try: + # Validate and convert value + numeric_value = float(value) + return { + 'column': column, + 'operator': operator_symbol, + 'value': numeric_value + } + except ValueError: + return None + + def manage_presets(self): + """ + Open presets management dialog + """ + # Get main GUI reference to access manage_presets method + if hasattr(self, 'main_gui') and self.main_gui and hasattr(self.main_gui, 'manage_presets'): + self.main_gui.manage_presets() + + def refresh_preset_combobox(self): + """ + Refresh the preset combobox with current preset values + """ + if hasattr(self, 'pattern_preset_combobox'): + self.pattern_preset_combobox['values'] = list(self.pattern_presets.keys()) + + def get_frame(self): + """ + Get the frame for this tab + + Returns: + The frame containing all widgets + """ + return self.frame diff --git a/excel_filter/gui_components/execution_tab.py b/excel_filter/gui_components/execution_tab.py new file mode 100644 index 0000000..43f7eb3 --- /dev/null +++ b/excel_filter/gui_components/execution_tab.py @@ -0,0 +1,403 @@ +""" +Execution tab component for the Excel Filter GUI +""" + +import tkinter as tk +from tkinter import ttk +import tkinter.font as tkfont + + +class ExecutionTab: + """ + Execution tab component that shows the command and logs + """ + + def __init__(self, parent_frame, win11_colors, scale_factor=1.0, translations=None): + """ + Initialize the execution tab + + Args: + parent_frame: Parent frame to attach to + win11_colors: Windows 11 color palette dictionary + scale_factor: DPI scaling factor for high-resolution displays + translations: Translations object for internationalization + """ + self.frame = ttk.Frame(parent_frame) + self.win11_colors = win11_colors + self.scale_factor = scale_factor + self.translations = translations + + # Variables + self.command_var = tk.StringVar(value=self.translations.get("ready_to_execute", "Bereit zur Ausführung...")) + self.status_var = tk.StringVar(value="🔄 " + (self.translations.get("status_ready", "Bereit"))) + self.progress_var = tk.DoubleVar(value=0.0) + + # Execution state + self.is_executing = False + + # References to other components (will be set later) + self.config_tab = None + self.main_gui = None + + self.create_widgets() + + def create_widgets(self): + """ + Create all widgets for the execution tab with modern design + """ + # Scale factors for DPI-aware sizing - ensure minimum text size of 12 + scaled_padding = int(8 * self.scale_factor) + scaled_small_padding = int(5 * self.scale_factor) + scaled_large_padding = int(15 * self.scale_factor) + scaled_font_size = max(int(10 * self.scale_factor), 12) + scaled_bold_font_size = max(int(12 * self.scale_factor), 12) + scaled_title_font_size = max(int(14 * self.scale_factor), 12) + + # Main container with minimal padding + main_frame = ttk.Frame(self.frame) + main_frame.pack(fill=tk.BOTH, expand=True, padx=scaled_small_padding, pady=scaled_small_padding) + + # Status indicator at the top + status_frame = ttk.Frame(main_frame) + status_frame.pack(fill=tk.X, pady=(0, scaled_padding)) + + status_icon = ttk.Label(status_frame, textvariable=self.status_var, + font=('Segoe UI', scaled_bold_font_size, 'bold') if 'Segoe UI' in tkfont.families() else ('Helvetica', scaled_bold_font_size, 'bold')) + status_icon.pack(side=tk.LEFT) + + # Progress bar (initially hidden) + self.progress_bar = ttk.Progressbar(status_frame, variable=self.progress_var, maximum=100, + mode='determinate', length=int(300 * self.scale_factor)) + self.progress_bar.pack(side=tk.RIGHT, padx=(scaled_padding, 0)) + self.progress_bar.pack_forget() # Hide initially + + # Command display with smart line breaking + command_frame = ttk.LabelFrame(main_frame, text="🎯 " + (self.translations["command_to_execute"] if self.translations else "Auszuführender Befehl"), + padding=str(scaled_padding)) + command_frame.pack(fill=tk.X, pady=(0, scaled_padding)) + + # Create a text widget for better command display with line breaks + command_text_frame = ttk.Frame(command_frame) + command_text_frame.pack(fill=tk.X) + + self.command_text = tk.Text(command_text_frame, height=6, state=tk.DISABLED, wrap=tk.WORD, + bg=self.win11_colors.get('surface', '#ffffff'), + font=('Courier', scaled_font_size), + borderwidth=1, relief="solid", + padx=scaled_padding, pady=scaled_padding, + selectbackground=self.win11_colors.get('primary', '#0078d4')) + self.command_text.pack(fill=tk.X) + + # Execution controls frame + controls_frame = ttk.Frame(main_frame) + controls_frame.pack(fill=tk.X, pady=(0, scaled_large_padding)) + + # Execute button with modern styling + self.execute_button = ttk.Button( + controls_frame, + text="🚀 " + (self.translations["execute_button"] if self.translations else "AUSFÜHREN"), + command=self.execute_command, + state=tk.NORMAL + ) + self.execute_button.pack(side=tk.TOP, pady=scaled_padding, ipadx=int(40 * self.scale_factor), ipady=int(10 * self.scale_factor)) + + # Style the execute button + style = ttk.Style() + style.configure('Execute.TButton', + font=('Segoe UI', scaled_bold_font_size, 'bold') if 'Segoe UI' in tkfont.families() else ('Helvetica', scaled_bold_font_size, 'bold'), + padding=int(20 * self.scale_factor), borderwidth=0, + background=self.win11_colors['primary'], + foreground='white') + style.map('Execute.TButton', + foreground=[('active', 'white'), ('pressed', 'white'), ('disabled', '#cccccc')], + background=[('active', self.win11_colors.get('primary_dark', '#005a9e')), + ('pressed', self.win11_colors.get('primary_dark', '#005a9e')), + ('disabled', '#f0f0f0')]) + + self.execute_button.configure(style='Execute.TButton') + + # Log display with modern design + log_frame = ttk.LabelFrame(main_frame, text="📋 " + (self.translations["activity_log"] if self.translations else "Aktivitätsprotokoll"), + padding=str(scaled_padding)) + log_frame.pack(fill=tk.BOTH, expand=True) + + # Control buttons for log + log_controls = ttk.Frame(log_frame) + log_controls.pack(fill=tk.X, pady=(0, scaled_padding)) + + ttk.Button(log_controls, text="🧹 " + (self.translations["clear_log"] if self.translations else "Löschen"), command=self.clear_log).pack(side=tk.LEFT, padx=(0, scaled_small_padding)) + ttk.Button(log_controls, text="💾 " + (self.translations["save_log"] if self.translations else "Speichern"), command=self.save_log).pack(side=tk.LEFT) + + # Log text with enhanced styling + log_container = ttk.Frame(log_frame) + log_container.pack(fill=tk.BOTH, expand=True) + + # Scrollbar + log_scrollbar = ttk.Scrollbar(log_container) + log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Log text widget + self.log_text = tk.Text(log_container, height=int(12 * self.scale_factor), state=tk.DISABLED, + wrap=tk.WORD, bg=self.win11_colors.get('surface', '#ffffff'), + font=('Segoe UI', scaled_font_size) if 'Segoe UI' in tkfont.families() else ('Helvetica', scaled_font_size), + borderwidth=1, relief="solid", + padx=scaled_padding, pady=scaled_padding, + selectbackground=self.win11_colors.get('primary', '#0078d4'), + selectforeground='white', + yscrollcommand=log_scrollbar.set) + + self.log_text.pack(fill=tk.BOTH, expand=True) + log_scrollbar.config(command=self.log_text.yview) + + # Initial log message + self.log_message(self.translations["ready_for_execution"] if self.translations else "Excel Filter bereit zur Ausführung") + self.log_message(""+(self.translations["configure_and_execute"] if self.translations else "Konfigurieren Sie die Einstellungen und klicken Sie auf 'AUSFÜHREN'")) + + def execute_command(self): + """Execute the command""" + # This will be connected to the main GUI's process_file method + if hasattr(self, 'main_gui') and self.main_gui: + self.main_gui.process_file() + else: + self.log_message(self.translations["error_main_gui_not_connected"] if self.translations else "Fehler: Haupt-GUI nicht verbunden") + + def update_command_display(self): + """ + Update the command display based on current configuration with smart line breaking + """ + if not self.config_tab: + self._set_command_text(self.translations["ready_to_execute"] if self.translations else "Bereit zur Ausführung...") + return + + try: + input_file = self.config_tab.input_file_var.get() + output_file = self.config_tab.output_file_var.get() + pattern = self.config_tab.pattern_var.get() + sheet_name = self.config_tab.sheet_var.get() + + # Build formatted command display + lines = [] + + # Input file + if input_file: + lines.append(f"{self.translations['input_file_label'] if self.translations else 'Eingabedatei:'} {input_file}") + else: + lines.append(f"{self.translations['input_file_label'] if self.translations else 'Eingabedatei:'} {self.translations['not_selected'] if self.translations else '(nicht ausgewählt)'}") + + # Output file + if output_file: + lines.append(f"{self.translations['output_file_label'] if self.translations else 'Ausgabedatei:'} {output_file}") + else: + lines.append(f"{self.translations['output_file_label'] if self.translations else 'Ausgabedatei:'} {self.translations['not_selected'] if self.translations else '(nicht ausgewählt)'}") + + # Pattern + if pattern: + # Break long patterns into multiple lines if needed + if len(pattern) > 50: + lines.append(f"{self.translations['search_pattern_label'] if self.translations else 'Suchmuster:'} {pattern[:47]}...") + else: + lines.append(f"{self.translations['search_pattern_label'] if self.translations else 'Suchmuster:'} {pattern}") + else: + lines.append(f"{self.translations['search_pattern_label'] if self.translations else 'Suchmuster:'} {self.translations['not_specified'] if self.translations else '(nicht angegeben)'}") + + # Sheet name + if sheet_name: + lines.append(f"{self.translations['worksheet_label'] if self.translations else 'Arbeitsblatt:'} {sheet_name}") + + # Selected columns + if hasattr(self.config_tab, 'get_selected_columns'): + selected_columns = self.config_tab.get_selected_columns() + if selected_columns: + # Format columns nicely + if len(selected_columns) <= 3: + columns_text = ", ".join(selected_columns) + else: + columns_text = f"{', '.join(selected_columns[:3])} {self.translations['more_columns'].format(count=len(selected_columns) - 3) if self.translations else f'(+{len(selected_columns) - 3} weitere)'}" + lines.append(f"{self.translations['columns_label'] if self.translations else 'Spalten:'} {columns_text}") + + # Numeric filter settings + if hasattr(self.config_tab, 'get_numeric_filter_settings'): + numeric_settings = self.config_tab.get_numeric_filter_settings() + if numeric_settings: + if self.translations: + lines.append(f"🔢 {self.translations['numeric_filter_label'].format(column=numeric_settings['column'], operator=numeric_settings['operator'], value=numeric_settings['value'])}") + else: + lines.append(f"🔢 Numerischer Filter: {numeric_settings['column']} {numeric_settings['operator']} {numeric_settings['value']}") + + # Set the formatted command text + self._set_command_text("\n".join(lines)) + + except Exception as e: + error_text = self.translations["error_updating_command_display"].format(error=str(e)) if self.translations else f"Fehler beim Aktualisieren der Befehlsanzeige: {e}" + self._set_command_text(error_text) + self.log_message(error_text) + + def _set_command_text(self, text): + """ + Set the command text in the text widget + + Args: + text: Text to display + """ + self.command_text.config(state=tk.NORMAL) + self.command_text.delete(1.0, tk.END) + self.command_text.insert(1.0, text) + self.command_text.config(state=tk.DISABLED) + + def log_message(self, message): + """ + Log a message to the log text widget + + Args: + message: Message to log + """ + self.log_text.config(state=tk.NORMAL) + self.log_text.insert(tk.END, message + "\n") + self.log_text.see(tk.END) + self.log_text.config(state=tk.DISABLED) + + def set_config_tab(self, config_tab): + """ + Set the reference to the config tab + + Args: + config_tab: Reference to the config tab component + """ + self.config_tab = config_tab + + # Set up variable tracing to update command display when config changes + if hasattr(config_tab, 'input_file_var'): + config_tab.input_file_var.trace_add("write", lambda *args: self.update_command_display()) + if hasattr(config_tab, 'output_file_var'): + config_tab.output_file_var.trace_add("write", lambda *args: self.update_command_display()) + if hasattr(config_tab, 'pattern_var'): + config_tab.pattern_var.trace_add("write", lambda *args: self.update_command_display()) + if hasattr(config_tab, 'sheet_var'): + config_tab.sheet_var.trace_add("write", lambda *args: self.update_command_display()) + if hasattr(config_tab, 'no_regex_var'): + config_tab.no_regex_var.trace_add("write", lambda *args: self.update_command_display()) + if hasattr(config_tab, 'numeric_filter_enabled'): + config_tab.numeric_filter_enabled.trace_add("write", lambda *args: self.update_command_display()) + if hasattr(config_tab, 'numeric_column_var'): + config_tab.numeric_column_var.trace_add("write", lambda *args: self.update_command_display()) + if hasattr(config_tab, 'numeric_operator_var'): + config_tab.numeric_operator_var.trace_add("write", lambda *args: self.update_command_display()) + if hasattr(config_tab, 'numeric_value_var'): + config_tab.numeric_value_var.trace_add("write", lambda *args: self.update_command_display()) + if hasattr(config_tab, 'sheet_var'): + config_tab.sheet_var.trace_add("write", lambda *args: self.update_command_display()) + + # Set up callback for column changes + if hasattr(config_tab, 'set_on_columns_changed'): + config_tab.set_on_columns_changed(lambda: self.update_command_display()) + + # Initial update + self.update_command_display() + + def set_main_gui(self, main_gui): + """ + Set the reference to the main GUI + + Args: + main_gui: Reference to the main GUI instance + """ + self.main_gui = main_gui + + def clear_log(self): + """ + Clear the log text + """ + self.log_text.config(state=tk.NORMAL) + self.log_text.delete(1.0, tk.END) + self.log_text.config(state=tk.DISABLED) + self.log_message(self.translations["log_cleared"] if self.translations else "Protokoll gelöscht") + self.log_message(self.translations["ready_for_execution"] if self.translations else "Excel Filter bereit zur Ausführung") + + def save_log(self): + """ + Save the log content to a file + """ + try: + from tkinter import filedialog + import datetime + + # Generate default filename with timestamp + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + default_filename = f"excel_filter_log_{timestamp}.txt" + + # Ask user for save location + file_path = filedialog.asksaveasfilename( + defaultextension=".txt", + filetypes=[("Textdateien", "*.txt"), ("Alle Dateien", "*.*")], + initialfile=default_filename, + title="Protokoll speichern" + ) + + if file_path: + # Get log content + log_content = self.log_text.get(1.0, tk.END).strip() + + # Add header with timestamp + header = f"Excel Filter Protokoll - {datetime.datetime.now().strftime('%d.%m.%Y %H:%M:%S')}\n" + header += "=" * 50 + "\n\n" + + # Write to file + with open(file_path, 'w', encoding='utf-8') as f: + f.write(header + log_content) + + self.log_message(self.translations["log_saved"].format(file=file_path) if self.translations else f"💾 Protokoll gespeichert: {file_path}") + + except Exception as e: + self.log_message(f"❌ Fehler beim Speichern des Protokolls: {e}") + + def start_execution(self): + """ + Start execution - show progress bar and update status + """ + self.is_executing = True + self.status_var.set("⚡ " + (self.translations["execution_running"] if self.translations else "Läuft...")) + self.progress_var.set(0.0) + self.progress_bar.pack(side=tk.RIGHT, padx=(int(8 * self.scale_factor), 0)) + self.execute_button.config(state=tk.DISABLED, text="⏳ " + (self.translations["waiting"] if self.translations else "WARTEN...")) + self.log_message(self.translations["execution_started"] if self.translations else "▶️ Ausführung gestartet") + + def update_progress(self, value, message=None): + """ + Update progress bar and optionally log a message + + Args: + value: Progress value (0-100) + message: Optional message to log + """ + self.progress_var.set(value) + if message: + self.log_message(message) + + def finish_execution(self, success=True): + """ + Finish execution - hide progress bar and update status + """ + self.is_executing = False + self.progress_var.set(100.0) + + if success: + self.status_var.set("✅ Fertig") + self.log_message(self.translations["execution_completed"] if self.translations else "✅ Ausführung erfolgreich abgeschlossen") + else: + self.status_var.set("❌ Fehler") + self.log_message(self.translations["execution_failed"] if self.translations else "❌ Ausführung mit Fehlern beendet") + + # Hide progress bar after a short delay + self.frame.after(2000, lambda: self.progress_bar.pack_forget()) + + # Re-enable execute button + self.execute_button.config(state=tk.NORMAL, text="🚀 " + (self.translations["execute_button"] if self.translations else "AUSFÜHREN")) + + def get_frame(self): + """ + Get the frame for this tab + + Returns: + The frame containing all widgets + """ + return self.frame diff --git a/excel_filter/gui_components/help_tab.py b/excel_filter/gui_components/help_tab.py new file mode 100644 index 0000000..ee36e7f --- /dev/null +++ b/excel_filter/gui_components/help_tab.py @@ -0,0 +1,178 @@ +""" +Help tab component for the Excel Filter GUI +""" + +import tkinter as tk +from tkinter import ttk +import tkinter.font as tkfont + + +class HelpTab: + """ + Help tab component that shows documentation and usage information + """ + + def __init__(self, parent_frame, win11_colors, scale_factor=1.0, translations=None, switch_language_callback=None): + """ + Initialize the help tab + + Args: + parent_frame: Parent frame to attach to + win11_colors: Windows 11 color palette dictionary + scale_factor: DPI scaling factor for high-resolution displays + translations: Translations object for internationalization + switch_language_callback: Callback function to switch language + """ + self.frame = ttk.Frame(parent_frame) + self.win11_colors = win11_colors + self.scale_factor = scale_factor + self.translations = translations + self.switch_language_callback = switch_language_callback + + self.create_widgets() + + def create_widgets(self): + """ + Create all widgets for the help tab + """ + # Scale factors for DPI-aware sizing - ensure minimum text size of 12 + scaled_padding = int(10 * self.scale_factor) + scaled_text_padding = int(12 * self.scale_factor) + scaled_pack_padding = int(2 * self.scale_factor) + scaled_font_size = max(int(10 * self.scale_factor), 12) + + # Main container + help_main_frame = ttk.Frame(self.frame) + help_main_frame.pack(fill=tk.BOTH, expand=True, padx=scaled_padding, pady=scaled_padding) + + # Language selector at the top of help tab + if self.translations: + language_frame = ttk.Frame(help_main_frame) + language_frame.pack(fill=tk.X, pady=(0, scaled_padding)) + + # Language selector label + language_label = ttk.Label(language_frame, text=self.translations["language"]) + language_label.pack(side=tk.LEFT, padx=(0, int(5 * self.scale_factor))) + + # Frame for flag buttons + flags_frame = ttk.Frame(language_frame) + flags_frame.pack(side=tk.LEFT, padx=5) + + # German flag button + german_button = ttk.Button( + flags_frame, + text="🇩🇪 German", + command=lambda: self.switch_language('de'), + style='TButton' + ) + german_button.pack(side=tk.LEFT, padx=(0, 5)) + + # English flag button + english_button = ttk.Button( + flags_frame, + text="🇺🇸 English", + command=lambda: self.switch_language('en'), + style='TButton' + ) + english_button.pack(side=tk.LEFT) + + # Highlight current language + if self.translations.current_language == 'de': + german_button.config(style='Primary.TButton') + else: + english_button.config(style='Primary.TButton') + + # Scrollbar for help content + help_scrollbar = ttk.Scrollbar(help_main_frame) + help_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Help text with scrollbar - Windows 11 Fluent Design + help_text_widget = tk.Text(help_main_frame, wrap=tk.WORD, + bg=self.win11_colors['surface'], + fg=self.win11_colors['text'], + font=('Segoe UI', scaled_font_size) if 'Segoe UI' in tkfont.families() else ('Helvetica', scaled_font_size), + borderwidth=1, relief="solid", padx=scaled_text_padding, pady=scaled_text_padding, + selectbackground=self.win11_colors['primary'], + selectforeground='white', + yscrollcommand=help_scrollbar.set) + help_text_widget.pack(fill=tk.BOTH, expand=True, padx=scaled_pack_padding, pady=scaled_pack_padding) + help_scrollbar.config(command=help_text_widget.yview) + + # Configure scrollbar with Windows 11 styling + style = ttk.Style() + style.configure('TScrollbar', + background=self.win11_colors['border'], + troughcolor=self.win11_colors['surface'], + bordercolor=self.win11_colors['border'], + arrowcolor=self.win11_colors['text']) + + # Help content from translations + help_content = self.translations.get("help_content", "Help content not available") + + help_text_widget.insert(tk.END, help_content) + help_text_widget.config(state=tk.DISABLED) + + def switch_language(self, new_language): + """ + Switch the application language and update help content + + Args: + new_language: New language code ('en' or 'de') + """ + if self.switch_language_callback: + self.switch_language_callback(new_language) + + # Update help content after language change + self.update_help_content() + + # Update button styles to reflect current language + self.update_button_styles() + + def update_help_content(self): + """ + Update the help content text when language changes + """ + # Find the help text widget and update its content + for child in self.frame.winfo_children(): + if isinstance(child, ttk.Frame): + for subchild in child.winfo_children(): + if isinstance(subchild, tk.Text): + # Clear existing content and insert new translated content + subchild.config(state=tk.NORMAL) + subchild.delete(1.0, tk.END) + subchild.insert(tk.END, self.translations.get("help_content", "Help content not available")) + subchild.config(state=tk.DISABLED) + break + + def update_button_styles(self): + """ + Update the language button styles to reflect the current language + """ + # Find the language buttons and update their styles + for child in self.frame.winfo_children(): + if isinstance(child, ttk.Frame): + for subchild in child.winfo_children(): + if isinstance(subchild, ttk.Frame): + for widget in subchild.winfo_children(): + if isinstance(widget, ttk.Frame): + for button in widget.winfo_children(): + if isinstance(button, ttk.Button): + if "German" in button.cget("text"): + if self.translations.current_language == 'de': + button.config(style='Primary.TButton') + else: + button.config(style='TButton') + elif "English" in button.cget("text"): + if self.translations.current_language == 'en': + button.config(style='Primary.TButton') + else: + button.config(style='TButton') + + def get_frame(self): + """ + Get the frame for this tab + + Returns: + The frame containing all widgets + """ + return self.frame diff --git a/excel_filter/gui_components/main_window.py b/excel_filter/gui_components/main_window.py new file mode 100644 index 0000000..cfc2a7a --- /dev/null +++ b/excel_filter/gui_components/main_window.py @@ -0,0 +1,365 @@ +""" +Main window component for the Excel Filter GUI +""" + +import tkinter as tk +from tkinter import ttk +import tkinter.font as tkfont + + +class MainWindow: + """ + Main window component that handles the overall GUI structure + """ + + def __init__(self, root): + """ + Initialize the main window + + Args: + root: Tkinter root window + """ + self.root = root + self.root.title("Excel Filter Tool") + self.root.geometry("1200x700") + self.root.resizable(True, True) + + # Windows 11 specific: Enable modern window styling and DPI awareness first + self.scale_factor = 1.0 + try: + import ctypes + import sys + if sys.platform == 'win32': + try: + # Set DPI awareness for sharp rendering on high-DPI displays + try: + ctypes.windll.shcore.SetProcessDpiAwareness(2) # PROCESS_PER_MONITOR_DPI_AWARE + except: + try: + ctypes.windll.shcore.SetProcessDpiAwareness(1) # PROCESS_SYSTEM_DPI_AWARE + except: + ctypes.windll.user32.SetProcessDPIAware() + + # Set Tkinter scaling to match system DPI for proper element sizes + try: + hdc = ctypes.windll.user32.GetDC(0) + dpi_x = ctypes.windll.gdi32.GetDeviceCaps(hdc, 88) # LOGPIXELSX + ctypes.windll.user32.ReleaseDC(0, hdc) + self.scale_factor = dpi_x / 96.0 # 96 DPI = 100% scaling + self.root.tk.call('tk', 'scaling', self.scale_factor) + except: + pass # If scaling setup fails, continue without it + + # Enable dark mode for title bar + ctypes.windll.dwmapi.DwmSetWindowAttribute( + self.root.winfo_id(), + 20, + ctypes.c_int(2), + ctypes.sizeof(ctypes.c_int) + ) + except: + pass # If DPI awareness or DwmSetWindowAttribute fails, continue without it + except: + pass # If ctypes import fails, continue without it + + # Windows 11 Design + self.root.configure(bg='#f0f0f0') + + # Font configuration - ensure minimum size of 12 + try: + base_font_size = max(int(10 * self.scale_factor), 12) + self.default_font = ('Segoe UI', base_font_size) if 'Segoe UI' in tkfont.families() else ('Helvetica', base_font_size) + except: + self.default_font = ('Helvetica', 12) + + # Windows 11 Color palette + self.win11_primary = '#0078d4' # Windows 11 Blue + self.win11_light = '#f0f0f0' # Light gray + self.win11_dark = '#e0e0e0' # Dark gray + self.win11_accent = '#005a9e' # Accent color + + # Create main frame with minimal padding - tabs right at the top + self.main_frame = ttk.Frame(self.root) + self.main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) + + # Create notebook for tabs with Windows 11 styling - right at the top + self.notebook = ttk.Notebook(self.main_frame) + self.notebook.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) + + # Configure notebook padding and spacing for Windows 11 look + style = ttk.Style() + style.layout('TNotebook.Tab', [ + ('Notebook.tab', { + 'sticky': 'nswe', + 'children': [ + ('Notebook.padding', { + 'side': 'top', + 'children': [ + ('Notebook.label', {'side': 'top', 'sticky': ''}) + ] + }) + ] + }) + ]) + + # Set window icon and other Windows 11 properties + try: + self.root.iconbitmap('app_icon.ico') # Windows 11 style icon + except: + pass # Icon not found, continue without it + + # Windows 11 window properties + self.root.attributes('-alpha', 1.0) # Ensure full opacity + + # Add Windows 11 style window controls (minimize, maximize, close) + # This is handled automatically by Windows for Tkinter windows + + # Set window minimum size for better usability (larger minimum to accommodate the bigger interface) + min_width = int(1200 * self.scale_factor) + min_height = int(700 * self.scale_factor) + self.root.minsize(min_width, min_height) + + self.root.update() + + # Configure styles + self.configure_styles() + + def configure_styles(self): + """ + Configure Tkinter styles for Windows 11 Fluent Design look + """ + style = ttk.Style() + + # Use clam theme for better appearance + style.theme_use('clam') + + # Scale factors for DPI-aware sizing - ensure minimum size of 12 + font_size_normal = max(int(10 * self.scale_factor), 12) + font_size_bold = max(int(10 * self.scale_factor), 12) + font_size_heading = max(int(12 * self.scale_factor), 12) + tab_padding = [int(16 * self.scale_factor), int(8 * self.scale_factor)] + button_padding = (int(12 * self.scale_factor), int(6 * self.scale_factor)) + primary_button_padding = (int(16 * self.scale_factor), int(8 * self.scale_factor)) + entry_padding = int(10 * self.scale_factor) + combobox_padding = int(8 * self.scale_factor) + notebook_padding = [int(10 * self.scale_factor), int(5 * self.scale_factor)] + + # Windows 11 Fluent Design color palette + win11_primary = '#0078d4' # Windows 11 Blue + win11_primary_dark = '#005a9e' # Darker blue for hover effects + win11_primary_light = '#cce4f7' # Light blue for active states + win11_background = '#f0f0f0' # Light gray background + win11_surface = '#ffffff' # White surface + win11_text = '#212121' # Dark text + win11_text_secondary = '#616161' # Secondary text + win11_border = '#e0e0e0' # Border color + win11_hover = '#e8f0fe' # Hover effect + win11_pressed = '#d0e0f5' # Pressed effect + + # Tab styling - Windows 11 Fluent Design style + style.configure('TNotebook', background=win11_background, borderwidth=1, relief='solid') + style.configure('TNotebook.Tab', + font=('Segoe UI', font_size_normal, 'normal') if 'Segoe UI' in tkfont.families() else ('Helvetica', font_size_normal, 'normal'), + padding=tab_padding, + background=win11_background, + foreground=win11_text, + borderwidth=0, + relief='flat') + style.map('TNotebook.Tab', + background=[("selected", win11_surface), ("active", win11_hover), ("!selected", win11_background)], + foreground=[("selected", win11_primary), ("!selected", win11_text_secondary)], + relief=[("selected", 'flat'), ("active", 'flat'), ("!selected", 'flat')]) + + # Configure notebook padding and spacing + style.configure('TNotebook', tabposition='n', padding=notebook_padding) + + # Button styling - Windows 11 Fluent Design + style.configure('TButton', + font=('Segoe UI', font_size_normal, 'normal') if 'Segoe UI' in tkfont.families() else ('Helvetica', font_size_normal, 'normal'), + padding=button_padding, + borderwidth=1, + focuscolor=win11_primary, + background=win11_surface, + foreground=win11_text, + bordercolor=win11_border, + relief='flat') + style.map('TButton', + foreground=[('active', win11_text), ('disabled', win11_text_secondary)], + background=[('active', win11_hover), ('pressed', win11_pressed), ('disabled', win11_background)], + bordercolor=[('active', win11_primary), ('pressed', win11_primary_dark)], + lightcolor=[('active', win11_hover)], + darkcolor=[('active', win11_pressed)]) + + # Primary button style (for important actions) + style.configure('Primary.TButton', + font=('Segoe UI', font_size_bold, 'bold') if 'Segoe UI' in tkfont.families() else ('Helvetica', font_size_bold, 'bold'), + padding=primary_button_padding, + background=win11_primary, + foreground=win11_surface, + borderwidth=0, + focuscolor=win11_primary_dark) + style.map('Primary.TButton', + background=[('active', win11_primary_dark), ('pressed', win11_primary_dark), ('disabled', win11_background)], + foreground=[('active', win11_surface), ('pressed', win11_surface), ('disabled', win11_text_secondary)]) + + # Entry field styling - Windows 11 style + style.configure('TEntry', + font=('Segoe UI', font_size_normal, 'normal') if 'Segoe UI' in tkfont.families() else ('Helvetica', font_size_normal, 'normal'), + padding=entry_padding, + fieldbackground=win11_surface, + borderwidth=1, + relief='solid', + bordercolor=win11_border, + foreground=win11_text) + style.map('TEntry', + fieldbackground=[('focus', win11_surface), ('readonly', win11_background)], + bordercolor=[('focus', win11_primary), ('readonly', win11_border)]) + + # Label styling - Windows 11 style + style.configure('TLabel', + font=('Segoe UI', font_size_normal, 'normal') if 'Segoe UI' in tkfont.families() else ('Helvetica', font_size_normal, 'normal'), + background=win11_background, + foreground=win11_text) + + # Heading label style + style.configure('Heading.TLabel', + font=('Segoe UI', font_size_heading, 'bold') if 'Segoe UI' in tkfont.families() else ('Helvetica', font_size_heading, 'bold'), + background=win11_background, + foreground=win11_primary) + + # Frame styling + style.configure('TFrame', background=win11_background) + style.configure('TLabelframe', + font=('Segoe UI', font_size_bold, 'bold') if 'Segoe UI' in tkfont.families() else ('Helvetica', font_size_bold, 'bold'), + background=win11_background, + borderwidth=1, + relief='groove', + bordercolor=win11_border) + style.configure('TLabelframe.Label', + font=('Segoe UI', font_size_bold, 'bold') if 'Segoe UI' in tkfont.families() else ('Helvetica', font_size_bold, 'bold'), + foreground=win11_primary, + background=win11_background) + + # Checkbutton styling + style.configure('TCheckbutton', + font=('Segoe UI', font_size_normal, 'normal') if 'Segoe UI' in tkfont.families() else ('Helvetica', font_size_normal, 'normal'), + background=win11_background, + foreground=win11_text) + style.map('TCheckbutton', + background=[('active', win11_hover), ('selected', win11_background)], + foreground=[('active', win11_text), ('selected', win11_text)]) + + # Combobox styling + style.configure('TCombobox', + font=('Segoe UI', font_size_normal, 'normal') if 'Segoe UI' in tkfont.families() else ('Helvetica', font_size_normal, 'normal'), + padding=combobox_padding, + fieldbackground=win11_surface, + borderwidth=1, + relief='solid', + bordercolor=win11_border, + foreground=win11_text) + style.map('TCombobox', + fieldbackground=[('focus', win11_surface), ('readonly', win11_background)], + bordercolor=[('focus', win11_primary), ('readonly', win11_border)]) + + # Scrollbar styling + style.configure('TScrollbar', + background=win11_background, + troughcolor=win11_background, + bordercolor=win11_border, + arrowcolor=win11_text) + style.map('TScrollbar', + background=[('active', win11_border), ('disabled', win11_background)], + troughcolor=[('active', win11_hover), ('disabled', win11_background)]) + + # Treeview styling + style.configure('Treeview', + font=('Segoe UI', font_size_normal, 'normal') if 'Segoe UI' in tkfont.families() else ('Helvetica', font_size_normal, 'normal'), + background=win11_surface, + foreground=win11_text, + fieldbackground=win11_surface, + bordercolor=win11_border) + style.map('Treeview', + background=[('selected', win11_primary_light)], + foreground=[('selected', win11_text)]) + + # Progressbar styling + style.configure('TProgressbar', + background=win11_primary, + troughcolor=win11_background, + bordercolor=win11_border) + + def add_tab(self, tab_frame, text): + """ + Add a tab to the notebook with Windows 11 styling + + Args: + tab_frame: Frame to add as tab + text: Tab title + + Returns: + The added tab frame + """ + # Add the tab with Windows 11 styling + self.notebook.add(tab_frame, text=text) + + # Apply Windows 11 specific styling to the tab + style = ttk.Style() + + # Get the tab index + tab_index = len(self.notebook.tabs()) - 1 + tab_id = self.notebook.tabs()[tab_index] + + # Configure the tab to have Windows 11 appearance with scaled padding + scaled_tab_padding = [int(16 * self.scale_factor), int(8 * self.scale_factor)] + self.notebook.tab(tab_id, padding=scaled_tab_padding) + + return tab_frame + + def enhance_tab_appearance(self): + """ + Enhance tab appearance to look more like Windows 11 + This should be called after all tabs are added + """ + style = ttk.Style() + + # Windows 11 specific: Make tabs look more modern + style.configure('TNotebook.Tab', + borderwidth=0, + focuscolor='#0078d4', + lightcolor='#f0f0f0', + darkcolor='#f0f0f0') + + # Add a subtle border to the notebook itself + try: + self.notebook.configure(borderwidth=1, relief='solid') + except: + pass # Some Tkinter versions don't support borderwidth for notebooks + + # Configure the notebook background to match Windows 11 + style.configure('TNotebook', + background='#f0f0f0', + borderwidth=1, + relief='solid', + bordercolor='#e0e0e0') + + # Windows 11 specific: Add hover and selection effects + style.map('TNotebook.Tab', + background=[('selected', '#ffffff'), + ('active', '#e8f0fe'), + ('!selected', '#f0f0f0')], + foreground=[('selected', '#0078d4'), + ('!selected', '#616161')]) + + # Windows 11 specific: Make selected tab more prominent with scaled values + scaled_tab_padding = [int(16 * self.scale_factor), int(8 * self.scale_factor)] + scaled_font_size = int(10 * self.scale_factor) + style.configure('TNotebook.Tab', + padding=scaled_tab_padding, + font=('Segoe UI', scaled_font_size, 'normal') if 'Segoe UI' in tkfont.families() else ('Helvetica', scaled_font_size, 'normal')) + + def run(self): + """ + Run the main application loop + """ + self.root.mainloop() diff --git a/excel_filter/gui_components/regex_builder_tab.py b/excel_filter/gui_components/regex_builder_tab.py new file mode 100644 index 0000000..607a94e --- /dev/null +++ b/excel_filter/gui_components/regex_builder_tab.py @@ -0,0 +1,1070 @@ +""" +Regex Builder tab component for the Excel Filter GUI +""" + +import tkinter as tk +from tkinter import ttk, messagebox +import tkinter.font as tkfont +import re +import os +import sys + +# Update the import to use absolute path +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from translations import Translations + + +class RegexBuilderTab: + + def __init__(self, parent_frame, win11_colors, scale_factor=1.0, main_gui=None): + """ + Initialize the enhanced regex builder tab + + Args: + parent_frame: Parent frame to attach to + win11_colors: Windows 11 color palette dictionary + scale_factor: DPI scaling factor for high-resolution displays + main_gui: Reference to main GUI for pattern application + """ + self.frame = ttk.Frame(parent_frame) + self.win11_colors = win11_colors + self.scale_factor = scale_factor + self.main_gui = main_gui + self.translations = getattr(main_gui, 'translations', Translations()) if main_gui else Translations() + + # Scale factors for DPI-aware sizing - ensure minimum text size of 12 + self.scaled_padding = int(10 * self.scale_factor) + self.scaled_title_font = max(int(14 * self.scale_factor), 12) + self.scaled_normal_font = max(int(10 * self.scale_factor), 12) + self.scaled_bold_font = max(int(11 * self.scale_factor), 12) + + # Variables for pattern building and testing + self.regex_pattern_var = tk.StringVar() + self.test_text_var = tk.StringVar(value=self.translations.get("default_test_text", "Beispieltext mit error und warning 123 Zahlen und E-Mail: test@example.com")) + self.test_result_var = tk.StringVar(value=self.translations.get("default_result", "Hier erscheint das Testergebnis...")) + self.test_matches_var = tk.StringVar(value=self.translations.get("no_matches", "Keine Treffer")) + self.pattern_description_var = tk.StringVar(value=self.translations.get("auto_desc", "Beschreibung wird automatisch generiert...")) + + # New variables for intuitive builder + self.current_char_type = tk.StringVar(value="") + self.current_quantity_type = tk.StringVar(value="") + self.custom_quantity_var = tk.StringVar(value="1") + self.min_quantity_var = tk.StringVar(value="1") + self.max_quantity_var = tk.StringVar(value="5") + self.custom_text_var = tk.StringVar(value="") + self.current_preview_var = tk.StringVar(value=self.translations.get("select_char_first", "Wählen Sie zuerst einen Zeichentyp aus...")) + self.builder_description_var = tk.StringVar(value="") + + # Undo/Redo functionality + self.pattern_history = [] + self.history_index = -1 + + # Tooltips storage + self.tooltips = {} + + self.create_widgets() + + def create_widgets(self): + """ + Create all widgets for the regex builder tab + """ + # Scale factors for DPI-aware sizing + scaled_padding = int(10 * self.scale_factor) + scaled_title_font = int(14 * self.scale_factor) + scaled_normal_font = int(10 * self.scale_factor) + scaled_bold_font = int(11 * self.scale_factor) + + # Main container - minimal padding for maximum space + regex_main_frame = ttk.Frame(self.frame) + regex_main_frame.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) + + + # Main notebook for different views + self.notebook = ttk.Notebook(regex_main_frame) + self.notebook.pack(fill=tk.BOTH, expand=True) + + # Tab 1: Builder + builder_frame = ttk.Frame(self.notebook) + self.notebook.add(builder_frame, text=self.translations.get("tab_builder", "🧱 Baustein-Builder")) + + # Tab 2: Tester + self.tester_frame = ttk.Frame(self.notebook) + self.notebook.add(self.tester_frame, text=self.translations.get("tab_tester", "🧪 Tester")) + + # Tab 3: Examples + examples_frame = ttk.Frame(self.notebook) + self.notebook.add(examples_frame, text=self.translations.get("tab_examples", "📚 Beispiele")) + + # Builder tab content + self.create_builder_tab(builder_frame) + + # Tester tab content + self.create_tester_tab(self.tester_frame) + + # Examples tab content + self.create_examples_tab(examples_frame) + + def create_builder_tab(self, builder_frame): + """ + Create the intuitive step-by-step regex builder interface + """ + builder_main = ttk.Frame(builder_frame) + builder_main.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) + + # No title or intro text - maximum space utilization + + # Step indicator (compact version) + steps_frame = ttk.Frame(builder_main) + steps_frame.pack(fill=tk.X, pady=(0, int(5 * self.scale_factor))) + + # Three-column layout for the steps + steps_container = ttk.Frame(builder_main) + steps_container.pack(fill=tk.BOTH, expand=True, pady=(0, int(10 * self.scale_factor))) + + # STEP 1: Character Type Selection (Left Column) + step1_frame = ttk.LabelFrame(steps_container, text=self.translations.get("step_1", "📝 Schritt 1: Zeichen suchen"), + padding=str(int(6 * self.scale_factor))) + step1_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, int(3 * self.scale_factor))) + + char_types = [ + (self.translations.get("btn_letters", "🔤 Buchstaben"), "letters", "[a-zA-Z]", self.translations.get("desc_letters", "a-z, A-Z")), + (self.translations.get("btn_digits", "🔢 Zahlen"), "digits", "\\d", self.translations.get("desc_digits", "0-9")), + (self.translations.get("btn_alphanum", "🔤🔢 Alphanum."), "alphanum", "[a-zA-Z0-9]", self.translations.get("desc_alphanum", "Buchst.+Zahlen")), + (self.translations.get("btn_any", "❓ Beliebig"), "any", ".", self.translations.get("desc_any", "Ein Zeichen")), + (self.translations.get("btn_custom", "📝 Eigener Text"), "custom", "", self.translations.get("desc_custom", "Ihr Text")), + (self.translations.get("btn_special", "⚙️ Spezial"), "special", "", self.translations.get("desc_special", "Satzzeichen")) + ] + + self.char_buttons = [] + for name, value, pattern, desc in char_types: + # Button frame + btn_frame = ttk.Frame(step1_frame) + btn_frame.pack(fill=tk.X, pady=int(1 * self.scale_factor)) + + btn = ttk.Button(btn_frame, text=name, + command=lambda v=value, p=pattern: self.select_char_type(v, p)) + btn.pack(fill=tk.X, padx=int(1 * self.scale_factor), pady=(0, int(1 * self.scale_factor))) + self.char_buttons.append(btn) + + # Description below button + desc_label = ttk.Label(btn_frame, text=desc, + font=('Helvetica', max(self.scaled_normal_font - 2, 12)), + foreground="gray") + desc_label.pack(anchor=tk.W, padx=int(3 * self.scale_factor)) + + # Custom text input (shown when "Eigener Text" is selected) + self.custom_text_frame = ttk.Frame(step1_frame) + ttk.Label(self.custom_text_frame, text="Text:", font=('Helvetica', max(int((self.scaled_normal_font - 1) * self.scale_factor), 12))).pack(side=tk.LEFT, padx=int(2 * self.scale_factor)) + self.custom_entry = ttk.Entry(self.custom_text_frame, textvariable=self.custom_text_var, width=15, + font=('Segoe UI', max(int(self.scaled_normal_font * self.scale_factor), 12)) if 'Segoe UI' in tkfont.families() else ('Helvetica', max(int(self.scaled_normal_font * self.scale_factor), 12))) + self.custom_entry.pack(side=tk.LEFT, padx=int(2 * self.scale_factor), fill=tk.X, expand=True) + # Initially hidden + self.custom_text_frame.pack_forget() + + # STEP 2: Quantity Selection (Middle Column) + step2_frame = ttk.LabelFrame(steps_container, text=self.translations.get("step_2", "🔢 Schritt 2: Wie oft?"), + padding=str(int(6 * self.scale_factor))) + step2_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, int(3 * self.scale_factor))) + + quantity_types = [ + (self.translations.get("btn_once", "🎯 1-mal"), "once", "", self.translations.get("desc_once", "Genau einmal")), + (self.translations.get("btn_one_plus", "➕ 1+ mal"), "one_or_more", "+", self.translations.get("desc_one_plus", "Mindestens 1-mal")), + (self.translations.get("btn_zero_one", "❓ 0-1 mal"), "zero_or_one", "?", self.translations.get("desc_zero_one", "Optional")), + (self.translations.get("btn_zero_plus", "♾️ 0+ mal"), "zero_or_more", "*", self.translations.get("desc_zero_plus", "Beliebig oft")), + (self.translations.get("btn_n_times", "📏 N-mal"), "exactly_n", "{n}", self.translations.get("desc_n_times", "Genau N-mal")), + (self.translations.get("btn_m_n_times", "📊 M-N mal"), "between_m_n", "{m,n}", self.translations.get("desc_m_n_times", "M bis N-mal")) + ] + + self.quantity_buttons = [] + for name, value, pattern, desc in quantity_types: + # Button frame + btn_frame = ttk.Frame(step2_frame) + btn_frame.pack(fill=tk.X, pady=int(1 * self.scale_factor)) + + btn = ttk.Button(btn_frame, text=name, + command=lambda v=value, p=pattern: self.select_quantity_type(v, p)) + btn.pack(fill=tk.X, padx=int(1 * self.scale_factor), pady=(0, int(1 * self.scale_factor))) + self.quantity_buttons.append(btn) + + # Description below button + desc_label = ttk.Label(btn_frame, text=desc, + font=('Helvetica', max(self.scaled_normal_font - 2, 12)), + foreground="gray") + desc_label.pack(anchor=tk.W, padx=int(3 * self.scale_factor)) + + # Custom quantity inputs + self.exactly_frame = ttk.Frame(step2_frame) + ttk.Label(self.exactly_frame, text="N:", font=('Helvetica', max(int((self.scaled_normal_font - 1) * self.scale_factor), 12))).pack(side=tk.LEFT, padx=int(2 * self.scale_factor)) + ttk.Spinbox(self.exactly_frame, from_=1, to=99, textvariable=self.custom_quantity_var, width=3, + font=('Segoe UI', max(int(self.scaled_normal_font * self.scale_factor), 12)) if 'Segoe UI' in tkfont.families() else ('Helvetica', max(int(self.scaled_normal_font * self.scale_factor), 12))).pack(side=tk.LEFT) + self.exactly_frame.pack_forget() + + self.between_frame = ttk.Frame(step2_frame) + ttk.Label(self.between_frame, text="M:", font=('Helvetica', max(int((self.scaled_normal_font - 1) * self.scale_factor), 12))).pack(side=tk.LEFT, padx=int(2 * self.scale_factor)) + ttk.Spinbox(self.between_frame, from_=0, to=99, textvariable=self.min_quantity_var, width=3, + font=('Segoe UI', max(int(self.scaled_normal_font * self.scale_factor), 12)) if 'Segoe UI' in tkfont.families() else ('Helvetica', max(int(self.scaled_normal_font * self.scale_factor), 12))).pack(side=tk.LEFT) + ttk.Label(self.between_frame, text="N:", font=('Helvetica', max(int((self.scaled_normal_font - 1) * self.scale_factor), 12))).pack(side=tk.LEFT, padx=int(2 * self.scale_factor)) + ttk.Spinbox(self.between_frame, from_=1, to=99, textvariable=self.max_quantity_var, width=3, + font=('Segoe UI', max(int(self.scaled_normal_font * self.scale_factor), 12)) if 'Segoe UI' in tkfont.families() else ('Helvetica', max(int(self.scaled_normal_font * self.scale_factor), 12))).pack(side=tk.LEFT) + self.between_frame.pack_forget() + + # STEP 3: Modifiers and Anchors (Right Column) + step3_frame = ttk.LabelFrame(steps_container, text=self.translations.get("step_3", "⚙️ Schritt 3: Optionen"), + padding=str(int(6 * self.scale_factor))) + step3_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # Compact button layout with descriptions + btn_frame = ttk.Frame(step3_frame) + btn_frame.pack(fill=tk.BOTH, expand=True) + + # Row 1: Anchors with descriptions + anchor_row = ttk.Frame(btn_frame) + anchor_row.pack(fill=tk.X, pady=int(1 * self.scale_factor)) + + # ^ Anfang + anchor_start_frame = ttk.Frame(anchor_row) + anchor_start_frame.pack(side=tk.LEFT, padx=int(1 * self.scale_factor), expand=True, fill=tk.X) + ttk.Button(anchor_start_frame, text=self.translations.get("btn_start", "^ Anfang"), command=lambda: self.add_modifier("^")).pack(fill=tk.X) + ttk.Label(anchor_start_frame, text=self.translations.get("desc_start", "Zeilenanfang"), font=('Helvetica', max(self.scaled_normal_font - 2, 12)), foreground="gray").pack(anchor=tk.W) + + # $ Ende + anchor_end_frame = ttk.Frame(anchor_row) + anchor_end_frame.pack(side=tk.LEFT, padx=int(1 * self.scale_factor), expand=True, fill=tk.X) + ttk.Button(anchor_end_frame, text=self.translations.get("btn_end", "$ Ende"), command=lambda: self.add_modifier("$")).pack(fill=tk.X) + ttk.Label(anchor_end_frame, text=self.translations.get("desc_end", "Zeilenende"), font=('Helvetica', max(self.scaled_normal_font - 2, 12)), foreground="gray").pack(anchor=tk.W) + + # \b Wort + word_boundary_frame = ttk.Frame(anchor_row) + word_boundary_frame.pack(side=tk.LEFT, padx=int(1 * self.scale_factor), expand=True, fill=tk.X) + ttk.Button(word_boundary_frame, text=self.translations.get("btn_word", "\\b Wort"), command=lambda: self.add_modifier("\\b")).pack(fill=tk.X) + ttk.Label(word_boundary_frame, text=self.translations.get("desc_word", "Wortgrenze"), font=('Helvetica', max(self.scaled_normal_font - 2, 12)), foreground="gray").pack(anchor=tk.W) + + # Row 2: Groups with descriptions + group_row = ttk.Frame(btn_frame) + group_row.pack(fill=tk.X, pady=int(1 * self.scale_factor)) + + # ( ) Gruppe + group_frame = ttk.Frame(group_row) + group_frame.pack(side=tk.LEFT, padx=int(1 * self.scale_factor), expand=True, fill=tk.X) + ttk.Button(group_frame, text=self.translations.get("btn_group", "( ) Gruppe"), command=lambda: self.add_modifier("()")).pack(fill=tk.X) + ttk.Label(group_frame, text=self.translations.get("desc_group", "Gruppierung"), font=('Helvetica', max(self.scaled_normal_font - 2, 12)), foreground="gray").pack(anchor=tk.W) + + # | ODER + or_frame = ttk.Frame(group_row) + or_frame.pack(side=tk.LEFT, padx=int(1 * self.scale_factor), expand=True, fill=tk.X) + ttk.Button(or_frame, text=self.translations.get("btn_or", "| ODER"), command=lambda: self.add_modifier("|")).pack(fill=tk.X) + ttk.Label(or_frame, text=self.translations.get("desc_or", "Alternative"), font=('Helvetica', max(self.scaled_normal_font - 2, 12)), foreground="gray").pack(anchor=tk.W) + + # (?: ) Alt. + non_capture_frame = ttk.Frame(group_row) + non_capture_frame.pack(side=tk.LEFT, padx=int(1 * self.scale_factor), expand=True, fill=tk.X) + ttk.Button(non_capture_frame, text=self.translations.get("btn_alt", "(?: ) Alt."), command=lambda: self.add_modifier("(?:)")).pack(fill=tk.X) + ttk.Label(non_capture_frame, text=self.translations.get("desc_alt", "Gruppe o. Speicher"), font=('Helvetica', max(self.scaled_normal_font - 2, 12)), foreground="gray").pack(anchor=tk.W) + + # Combined compact preview and pattern section + combined_frame = ttk.Frame(builder_main) + combined_frame.pack(fill=tk.BOTH, expand=True, pady=(0, int(5 * self.scale_factor))) + + # Left side: Live Preview (ultra-compact) + preview_frame = ttk.LabelFrame(combined_frame, text=self.translations.get("preview", "👀 Vorschau"), + padding=str(int(4 * self.scale_factor))) + preview_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, int(3 * self.scale_factor))) + + ttk.Label(preview_frame, text=self.translations.get("selection", "Auswahl:"), font=('Helvetica', max(self.scaled_normal_font, 12))).pack(anchor=tk.W) + preview_display = ttk.Label(preview_frame, textvariable=self.current_preview_var, + font=('Courier', max(self.scaled_bold_font - 1, 12)), + background="#f0f0f0", relief="sunken", padding=int(2 * self.scale_factor)) + preview_display.pack(fill=tk.X, pady=int(2 * self.scale_factor)) + + ttk.Label(preview_frame, textvariable=self.builder_description_var, + wraplength=int(300 * self.scale_factor), justify=tk.LEFT, + foreground="blue", font=('Helvetica', max(self.scaled_normal_font - 1, 12))).pack(fill=tk.X) + + # Action buttons (side by side) + button_frame = ttk.Frame(preview_frame) + button_frame.pack(fill=tk.X, pady=int(2 * self.scale_factor)) + + ttk.Button(button_frame, text=self.translations.get("btn_add", "➕ Hinzufügen"), + command=self.add_to_pattern).pack(side=tk.LEFT, padx=(0, int(2 * self.scale_factor)), expand=True, fill=tk.X) + ttk.Button(button_frame, text=self.translations.get("btn_reset", "🔄 Zurücksetzen"), + command=self.reset_builder_selection).pack(side=tk.LEFT, expand=True, fill=tk.X) + + # Right side: Final regex pattern (ultra-compact) + pattern_frame = ttk.LabelFrame(combined_frame, text=self.translations.get("regex_result", "🎯 Regex-Muster"), + padding=str(int(4 * self.scale_factor))) + pattern_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + self.regex_entry = ttk.Entry(pattern_frame, textvariable=self.regex_pattern_var, + font=('Courier', max(self.scaled_bold_font - 1, 12))) + self.regex_entry.pack(fill=tk.X, pady=int(2 * self.scale_factor)) + + ttk.Label(pattern_frame, textvariable=self.pattern_description_var, + wraplength=int(300 * self.scale_factor), justify=tk.LEFT, + foreground="gray", font=('Helvetica', max(self.scaled_normal_font - 1, 12))).pack(fill=tk.X) + + # Bottom action buttons (shared) + final_action_frame = ttk.Frame(builder_main) + final_action_frame.pack(fill=tk.X, pady=int(5 * self.scale_factor)) + + ttk.Button(final_action_frame, text=self.translations.get("btn_manage", "📝 Muster verwalten"), command=self.manage_presets).pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + ttk.Button(final_action_frame, text=self.translations.get("btn_save_preset", "💾 Als Muster speichern"), command=self.save_as_preset).pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + ttk.Button(final_action_frame, text=self.translations.get("btn_copy", "📋 Kopieren"), command=self.copy_regex_to_clipboard).pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + ttk.Button(final_action_frame, text=self.translations.get("btn_reset_pattern", "🔄 Muster zurücksetzen"), command=self.reset_pattern).pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + ttk.Button(final_action_frame, text=self.translations.get("btn_undo", "↶ Rückgängig"), command=self.undo_pattern).pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + ttk.Button(final_action_frame, text=self.translations.get("btn_redo", "↷ Wiederholen"), command=self.redo_pattern).pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + ttk.Button(final_action_frame, text=self.translations.get("btn_apply", "💾 In Hauptfenster übernehmen"), command=self.apply_regex_to_main).pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + ttk.Button(final_action_frame, text=self.translations.get("btn_test", "🧪 Testen"), command=lambda: self.notebook.select(self.tester_frame)).pack(side=tk.RIGHT, padx=int(5 * self.scale_factor)) + + # Bind pattern changes to description update + self.regex_pattern_var.trace_add("write", lambda *args: self.update_pattern_description()) + + # Bind custom text changes to preview update + self.custom_text_var.trace_add("write", lambda *args: self.update_preview()) + self.custom_quantity_var.trace_add("write", lambda *args: self.update_preview()) + self.min_quantity_var.trace_add("write", lambda *args: self.update_preview()) + self.max_quantity_var.trace_add("write", lambda *args: self.update_preview()) + + # Initialize the builder + self.reset_builder_selection() + + def create_tester_tab(self, tester_frame): + + tester_main = ttk.Frame(tester_frame) + tester_main.pack(fill=tk.BOTH, expand=True, padx=self.scaled_padding, pady=self.scaled_padding) + + # Test-Text Eingabe + test_label = ttk.Label(tester_main, text=self.translations.get("test_input", "Testtext eingeben:"), + font=('Helvetica', max(self.scaled_bold_font, 12), 'bold')) + test_label.pack(anchor=tk.W, pady=(0, int(5 * self.scale_factor))) + + test_frame = ttk.LabelFrame(tester_main, text=self.translations.get("test_text_frame", "📝 Testtext"), padding=str(int(5 * self.scale_factor))) + test_frame.pack(fill=tk.X, pady=(0, int(10 * self.scale_factor))) + + test_entry = ttk.Entry(test_frame, textvariable=self.test_text_var, + font=('Segoe UI', max(self.scaled_normal_font, 12)) if 'Segoe UI' in tkfont.families() else ('Helvetica', max(self.scaled_normal_font, 12))) + test_entry.pack(fill=tk.X, pady=int(5 * self.scale_factor)) + + # Regex-Muster Eingabe + regex_label = ttk.Label(tester_main, text=self.translations.get("regex_pattern", "Regex-Muster:"), + font=('Segoe UI', max(self.scaled_bold_font, 12), 'bold') if 'Segoe UI' in tkfont.families() else ('Helvetica', max(self.scaled_bold_font, 12), 'bold')) + regex_label.pack(anchor=tk.W, pady=(int(10 * self.scale_factor), int(5 * self.scale_factor))) + + regex_frame = ttk.LabelFrame(tester_main, text=self.translations.get("test_pattern_frame", "🔍 Regex-Muster"), padding=str(int(5 * self.scale_factor))) + regex_frame.pack(fill=tk.X, pady=(0, int(10 * self.scale_factor))) + + regex_test_entry = ttk.Entry(regex_frame, textvariable=self.regex_pattern_var, + font=('Courier', max(self.scaled_bold_font, 12)) if 'Courier' in tkfont.families() else ('Helvetica', max(self.scaled_bold_font, 12))) + regex_test_entry.pack(fill=tk.X, pady=int(5 * self.scale_factor)) + + # Test-Buttons + test_button_frame = ttk.Frame(tester_main) + test_button_frame.pack(fill=tk.X, pady=(int(10 * self.scale_factor), 0)) + + ttk.Button(test_button_frame, text=self.translations.get("btn_test", "🔍 Testen"), command=self.test_regex_pattern).pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + ttk.Button(test_button_frame, text=self.translations.get("btn_load_example", "📋 Beispiel laden"), command=self.load_test_example).pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + ttk.Button(test_button_frame, text=self.translations.get("btn_reset", "🔄 Zurücksetzen"), command=self.reset_test_fields).pack(side=tk.LEFT, padx=int(5 * self.scale_factor)) + + # Testergebnis + result_label = ttk.Label(tester_main, text=self.translations.get("result_label", "Ergebnis:"), + font=('Helvetica', max(self.scaled_bold_font, 12), 'bold')) + result_label.pack(anchor=tk.W, pady=(int(15 * self.scale_factor), int(5 * self.scale_factor))) + + result_display = ttk.Label(tester_main, textvariable=self.test_result_var, + wraplength=int(800 * self.scale_factor), justify=tk.LEFT, + relief="sunken", padding=int(5 * self.scale_factor)) + result_display.pack(fill=tk.X, pady=(0, int(10 * self.scale_factor))) + + # Treffer-Anzeige + matches_label = ttk.Label(tester_main, text=self.translations.get("matches_label", "Gefundene Treffer:"), + font=('Helvetica', max(self.scaled_bold_font, 12), 'bold')) + matches_label.pack(anchor=tk.W, pady=(int(10 * self.scale_factor), int(5 * self.scale_factor))) + + matches_display = ttk.Label(tester_main, textvariable=self.test_matches_var, + wraplength=int(800 * self.scale_factor), justify=tk.LEFT, + foreground="green", relief="sunken", padding=int(5 * self.scale_factor)) + matches_display.pack(fill=tk.X, pady=(0, int(10 * self.scale_factor))) + + def create_examples_tab(self, examples_frame): + + examples_main = ttk.Frame(examples_frame) + examples_main.pack(fill=tk.BOTH, expand=True, padx=self.scaled_padding, pady=self.scaled_padding) + + # Beispiele-Kategorien + examples_label = ttk.Label(examples_main, text=self.translations.get("common_examples", "📚 Häufig verwendete Regex-Beispiele"), + font=('Helvetica', max(int(12 * self.scale_factor), 12), 'bold')) + examples_label.pack(anchor=tk.W, pady=(0, int(10 * self.scale_factor))) + + # Scrollbar für Beispiele + examples_scrollbar = ttk.Scrollbar(examples_main) + examples_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + examples_text = tk.Text(examples_main, wrap=tk.WORD, bg="white", + font=('Segoe UI', max(self.scaled_normal_font, 12)) if 'Segoe UI' in tkfont.families() else ('Helvetica', max(self.scaled_normal_font, 12)), + borderwidth=1, relief="solid", padx=int(10 * self.scale_factor), pady=int(10 * self.scale_factor), + selectbackground=self.win11_colors.get('primary', '#0078d4'), + selectforeground='white', + yscrollcommand=examples_scrollbar.set) + examples_text.pack(fill=tk.BOTH, expand=True, padx=int(2 * self.scale_factor), pady=int(2 * self.scale_factor)) + examples_scrollbar.config(command=examples_text.yview) + + # Beispiele einfügen - Keeping this untranslated for now as it's complex content + examples_content = """ + HÄUFIGE REGEX-BEISPIELE + ========================== + + 1. E-MAIL-ADRESSEN + ------------------------------- + Muster: [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,} + Beispiele: + • user@example.com ✅ + • test.email+tag@domain.co.uk ✅ + • invalid-email@ ❌ (fehlende Domain) + • user@.com ❌ (ungültige Domain) + + 2. DEUTSCHE TELEFONNUMMERN + -------------------------- + Muster: \\+49\\s?[0-9\\s]{8,14}|[0-9\\s]{8,14} + Beispiele: + • +49 123 456789 ✅ (internationale Schreibweise) + • 0123 456789 ✅ (deutsche Schreibweise) + • 0151 23456789 ✅ (Handynummer) + • 123 ❌ (zu kurz) + + 3. DATUMSFORMATE (ISO 8601) + --------------------------- + YYYY-MM-DD: \\b\\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\\d|3[01])\\b + Beispiele: + • 2024-12-31 ✅ + • 2024-02-29 ✅ (Schaltjahr) + • 2024-13-01 ❌ (ungültiger Monat) + • 2024-02-32 ❌ (ungültiger Tag) + + Deutsche Schreibweise DD.MM.YYYY: \\b(?:0[1-9]|[12]\\d|3[01])\\.(?:0[1-9]|1[0-2])\\.\\d{4}\\b + • 31.12.2024 ✅ + • 01.01.2024 ✅ + • 32.12.2024 ❌ (ungültiger Tag) + + 4. ZEITFORMATE (24h) + ------------------- + HH:MM:SS: \\b(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d\\b + Beispiele: + • 14:30:45 ✅ + • 09:15:30 ✅ + • 25:00:00 ❌ (ungültige Stunde) + + HH:MM: \\b(?:[01]\\d|2[0-3]):[0-5]\\d\\b + • 14:30 ✅ + • 09:15 ✅ + • 25:00 ❌ (ungültige Stunde) + + 5. PREISE UND BETRÄGE + ---------------------- + €-Format (deutsch): \\b\\d{1,3}(?:\\.\\d{3})*,\\d{2}\\s?€\\b + Beispiele: + • 1.234,56 € ✅ + • 123,45 € ✅ + • 1.234.567,89 € ✅ + • 123,456 € ❌ (zu viele Dezimalstellen) + + $-Format (englisch): \\b\\$\\d{1,3}(?:,\\d{3})*(?:\\.\\d{2})?\\b + • $1,234.56 ✅ + • $123.45 ✅ + • $1,234,567.89 ✅ + + 6. SEMANTISCHE VERSIONIERUNG + ---------------------------- + Standard: \\b\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.-]+)?(?:\\+[a-zA-Z0-9.-]+)?\\b + Beispiele: + • 1.2.3 ✅ + • 2.0.0-alpha.1 ✅ + • 1.0.0+build.123 ✅ + • 1.2 ❌ (fehlende Patch-Version) + + 7. IP-ADRESSEN (IPv4) + ------------------- + Streng: \\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b + Beispiele: + • 192.168.1.1 ✅ + • 10.0.0.1 ✅ + • 172.16.0.1 ✅ + • 999.999.999.999 ❌ (Werte zu hoch) + + Einfach (für meisten Fälle): \\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b + • 192.168.1.1 ✅ + • 999.999.999.999 ⚠️ (technisch ungültig, aber matched) + + 8. URLs (HTTP/HTTPS) + ------------------ + Vollständig: \\bhttps?://(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?::\\d{1,5})?(?:/[^\\s]*)?\\b + Beispiele: + • https://www.example.com ✅ + • http://sub.domain.co.uk:8080/path?query=value ✅ + • ftp://example.com ❌ (nur HTTP/HTTPS) + + Einfach: \\bhttps?://[^\\s/$.?#][^\\s]*\\b + • https://example.com ✅ + • https://sub.example.com/path ✅ + + 9. HASHTAGS + ----------- + Twitter: \\B#\\w+[a-zA-Z0-9_]*\\b + Beispiele: + • #Regex ✅ + • #Python3 ✅ + • #Windkraft ✅ + • #123 ❌ (muss mit Buchstabe beginnen) + • #hashtag-with-dash ✅ + + 10. SOZIALE MEDIEN HANDLES + ------------------------- + Twitter: \\B@\\w{1,15}\\b + Beispiele: + • @username ✅ + • @user_name ✅ + • @user-name ❌ (Bindestriche nicht erlaubt) + • @verylongusername ❌ (zu lang) + + Instagram: \\B@\\w{1,30}\\b (ähnlich wie Twitter) + + REGEX-TIPPS + ============================== + + 1. Verwende Anker für Präzision + - ❌ \\d{4} (findet auch 12345) + - ✅ \\b\\d{4}\\b (nur 4-stellige Zahlen als ganzes Wort) + + 2. Verwende Negative Lookbehinds für Ausschlüsse + - ❌ password.*admin (findet auch "password123admin") + - ✅ password(?!.*admin) (schließt "admin" aus) + + 3. Verwende Named Groups für bessere Lesbarkeit + - ❌ (\\d{4})-(\\d{2})-(\\d{2}) + - ✅ (?P\\d{4})-(?P\\d{2})-(?P\\d{2}) + + 4. Setze Quantoren sparsam ein + - ❌ .* (zu gierig, kann Performance-Probleme verursachen) + - ✅ [^\\n]* (spezifischer und effizienter) + + 5. Validierung vs. Extraktion + - Für Validierung: Verwende ^ und $ für vollständige Übereinstimmung + - Für Extraktion: Verwende \\b für Wortgrenzen + + 6. Beachte Zeichenkodierung + - Verwende \\w mit Vorsicht (bedeutet [a-zA-Z0-9_] in ASCII) + - Für Unicode: Verwende \\p{L} (falls unterstützt) oder [\\wäöüÄÖÜß] + + 7. Performance-Optimierung + - Vermeide verschachtelte Quantoren wie (a+)+ + - Verwende nicht-capturing Groups (?:) wenn möglich + - Teste mit repräsentativen Datenmengen + """ + + examples_text.insert(tk.END, examples_content) + examples_text.config(state=tk.DISABLED) + + def add_regex_component(self, component): + """ + Add a regex component to the current pattern with undo support + + Args: + component: Regex component to add + """ + current_pattern = self.regex_pattern_var.get() + self.save_to_history(current_pattern) + self.regex_pattern_var.set(current_pattern + component) + + def apply_regex_to_main(self): + """Apply the regex pattern to the main configuration""" + pattern = self.regex_pattern_var.get() + if pattern: + # Check if apply_to_main callback is set (from main GUI) + if hasattr(self, 'apply_to_main') and self.apply_to_main: + # Use the callback provided by main GUI + self.apply_to_main() + messagebox.showinfo(self.translations.get("success", "Erfolg"), self.translations.get("msg_pattern_applied", "Regex-Muster wurde in das Hauptfenster übernommen!")) + elif self.main_gui: + # Fallback: Apply to main GUI pattern field directly + self.main_gui.pattern_var.set(pattern) + messagebox.showinfo(self.translations.get("success", "Erfolg"), self.translations.get("msg_pattern_applied", "Regex-Muster wurde in das Hauptfenster übernommen!")) + else: + messagebox.showwarning(self.translations.get("error", "Fehler"), self.translations.get("msg_main_not_avail", "Hauptfenster-Referenz nicht verfügbar. Kopieren Sie das Muster manuell.")) + else: + messagebox.showwarning(self.translations.get("error", "Fehler"), self.translations.get("msg_no_pattern_apply", "Kein Regex-Muster zum Übernehmen verfügbar")) + + def copy_regex_to_clipboard(self): + """Copy the current regex pattern to clipboard""" + pattern = self.regex_pattern_var.get() + if pattern: + self.frame.clipboard_clear() + self.frame.clipboard_append(pattern) + self.frame.update() + messagebox.showinfo(self.translations.get("success", "Erfolg"), self.translations.get("msg_copied", "Regex-Muster wurde in die Zwischenablage kopiert!")) + else: + messagebox.showwarning(self.translations.get("error", "Fehler"), self.translations.get("msg_no_pattern_copy", "Kein Regex-Muster zum Kopieren verfügbar")) + + def reset_pattern(self): + """Reset the regex pattern and clear results""" + self.save_to_history(self.regex_pattern_var.get()) + self.regex_pattern_var.set("") + self.test_result_var.set(self.translations.get("default_result", "Hier erscheint das Testergebnis...")) + self.test_matches_var.set(self.translations.get("no_matches", "Keine Treffer")) + self.pattern_description_var.set(self.translations.get("auto_desc", "Beschreibung wird automatisch generiert...")) + + def undo_pattern(self): + """Undo the last pattern change""" + if self.history_index > 0: + self.history_index -= 1 + self.regex_pattern_var.set(self.pattern_history[self.history_index]) + elif self.history_index == 0: + self.regex_pattern_var.set("") + else: + messagebox.showinfo(self.translations.get("success", "Info"), self.translations.get("msg_no_undo", "Keine weiteren Rückgängig-Aktionen verfügbar")) + + def redo_pattern(self): + """Redo the last undone pattern change""" + if self.history_index < len(self.pattern_history) - 1: + self.history_index += 1 + self.regex_pattern_var.set(self.pattern_history[self.history_index]) + else: + messagebox.showinfo(self.translations.get("success", "Info"), self.translations.get("msg_no_redo", "Keine Wiederholen-Aktionen verfügbar")) + + def save_to_history(self, pattern): + """Save current pattern to history for undo/redo""" + # Remove any history after current index + self.pattern_history = self.pattern_history[:self.history_index + 1] + # Add new pattern + self.pattern_history.append(pattern) + self.history_index = len(self.pattern_history) - 1 + # Limit history to 50 entries + if len(self.pattern_history) > 50: + self.pattern_history.pop(0) + self.history_index -= 1 + + def test_regex_pattern(self): + """Test the current regex pattern with enhanced feedback""" + pattern = self.regex_pattern_var.get() + test_text = self.test_text_var.get() + + if not pattern: + self.test_result_var.set(self.translations.get("msg_enter_pattern", "❌ Bitte geben Sie ein Regex-Muster ein")) + self.test_matches_var.set(self.translations.get("no_matches", "Keine Treffer")) + return + + if not test_text: + self.test_result_var.set(self.translations.get("msg_enter_text", "❌ Bitte geben Sie einen Testtext ein")) + self.test_matches_var.set(self.translations.get("no_matches", "Keine Treffer")) + return + + try: + # Compile regex for better error handling + regex = re.compile(pattern) + matches = regex.findall(test_text) + + if matches: + unique_matches = list(set(matches)) # Remove duplicates for display + msg = self.translations.get("msg_pattern_found", "✅ Muster gefunden! {} Treffer (davon {} einzigartig)").format(len(matches), len(unique_matches)) + self.test_result_var.set(msg) + match_display = ", ".join([f"'{match}'" for match in unique_matches[:10]]) # Limit display + if len(unique_matches) > 10: + match_display += f" ... {self.translations.get('more_columns', '(+{} weitere)').format(count=len(unique_matches) - 10) if hasattr(self.translations, 'get') else f'(+{len(unique_matches) - 10} weitere)'}" + self.test_matches_var.set(match_display) + else: + self.test_result_var.set(self.translations.get("msg_no_matches", "❌ Keine Treffer gefunden")) + self.test_matches_var.set(self.translations.get("no_matches", "Keine Treffer")) + + except re.error as e: + self.test_result_var.set(self.translations.get("msg_invalid_regex", "❌ Ungültiges Regex-Muster: {}").format(e)) + self.test_matches_var.set(self.translations.get("no_matches", "Keine Treffer")) + + def load_test_example(self): + example = "Fehler in Zeile 42: error_code=123, warning_level=high, status=critical. E-Mail: user@example.com, Telefon: +49 123 456789, Datum: 2024-12-31, IP: 192.168.1.1" + self.test_text_var.set(example) + self.test_result_var.set(self.translations.get("msg_example_loaded", "Beispiel geladen - klicken Sie auf 'Testen'")) + self.test_matches_var.set(self.translations.get("no_matches", "Keine Treffer")) + + def reset_test_fields(self): + """Reset all test fields""" + self.test_text_var.set(self.translations.get("default_test_text", "Beispieltext mit error und warning 123 Zahlen und E-Mail: test@example.com")) + self.regex_pattern_var.set("") + self.test_result_var.set(self.translations.get("default_result", "Hier erscheint das Testergebnis...")) + self.test_matches_var.set(self.translations.get("no_matches", "Keine Treffer")) + + + + + + def update_pattern_description(self): + """Update the pattern description based on current regex""" + pattern = self.regex_pattern_var.get() + if not pattern: + self.pattern_description_var.set(self.translations.get("auto_desc", "Beschreibung wird automatisch generiert...")) + return + + try: + # Simple pattern analysis + descriptions = [] + + if '^' in pattern and '$' in pattern: + descriptions.append("Ganze Zeile muss passen") + elif '^' in pattern: + descriptions.append("Beginnt am Zeilenanfang") + elif '$' in pattern: + descriptions.append("Endet am Zeilenende") + + if '\\b' in pattern: + descriptions.append("Verwendet Wortgrenzen") + + if '|' in pattern: + descriptions.append("Enthält Alternativen (ODER)") + + if '\\d' in pattern: + descriptions.append("Sucht nach Zahlen") + + if '[a-zA-Z]' in pattern or '[A-Za-z]' in pattern: + descriptions.append("Sucht nach Buchstaben") + + if '@' in pattern: + descriptions.append("Könnte E-Mail-Adressen finden") + + if not descriptions: + descriptions.append("Benutzerdefiniertes Muster") + + self.pattern_description_var.set(" • ".join(descriptions)) + + except: + self.pattern_description_var.set("Muster-Analyse nicht möglich") + + def select_char_type(self, char_type, base_pattern): + """ + Handle character type selection and show/hide relevant inputs + + Args: + char_type: Type of character selected + base_pattern: Base regex pattern for this character type + """ + self.current_char_type.set(char_type) + + # Hide custom text frame by default + self.custom_text_frame.pack_forget() + + # Show custom text input for custom type + if char_type == "custom": + self.custom_text_frame.pack(fill=tk.X, pady=int(5 * self.scale_factor)) + base_pattern = "" # Will be filled by user input + + # Show special characters selection + elif char_type == "special": + # For now, just show common special chars + base_pattern = "[\\s\\.,\\-\\_]" + + # Update preview + self.update_preview() + + def select_quantity_type(self, quantity_type, pattern_modifier): + """ + Handle quantity type selection and show/hide custom inputs + + Args: + quantity_type: Type of quantity selected + pattern_modifier: Regex modifier for quantity + """ + self.current_quantity_type.set(quantity_type) + + # Hide custom input frames + self.exactly_frame.pack_forget() + self.between_frame.pack_forget() + + # Show relevant custom inputs + if quantity_type == "exactly_n": + self.exactly_frame.pack(fill=tk.X, pady=int(5 * self.scale_factor)) + elif quantity_type == "between_m_n": + self.between_frame.pack(fill=tk.X, pady=int(5 * self.scale_factor)) + + # Update preview + self.update_preview() + + def add_modifier(self, modifier): + """ + Add a modifier (anchor, group, etc.) to the current selection + + Args: + modifier: Modifier to add + """ + current_preview = self.current_preview_var.get() + if current_preview == self.translations.get("select_char_first", "Wählen Sie zuerst einen Zeichentyp aus..."): + # If no selection yet, just set the modifier + self.current_preview_var.set(modifier) + else: + # Add modifier to existing selection + if modifier in ["^", "$"]: + # Anchors go at start/end + if modifier == "^": + self.current_preview_var.set(modifier + current_preview) + else: + self.current_preview_var.set(current_preview + modifier) + elif modifier == "\\b": + # Word boundaries wrap the content + self.current_preview_var.set(modifier + current_preview + modifier) + else: + # Other modifiers (groups, etc.) + if modifier == "()": + self.current_preview_var.set("(" + current_preview + ")") + elif modifier == "|": + self.current_preview_var.set(current_preview + "|") + elif modifier == "(?:)": + self.current_preview_var.set("(?:)" + current_preview) + + self.update_preview_description() + + def update_preview(self): + """ + Update the live preview based on current selections + """ + char_type = self.current_char_type.get() + quantity_type = self.current_quantity_type.get() + + if not char_type: + self.current_preview_var.set(self.translations.get("select_char_first", "Wählen Sie zuerst einen Zeichentyp aus...")) + self.builder_description_var.set("") + return + + # Get base pattern + base_pattern = "" + if char_type == "letters": + base_pattern = "[a-zA-Z]" + elif char_type == "digits": + base_pattern = "\\d" + elif char_type == "alphanum": + base_pattern = "[a-zA-Z0-9]" + elif char_type == "any": + base_pattern = "." + elif char_type == "custom": + custom_text = self.custom_text_var.get() + if custom_text: + # Escape special regex characters + base_pattern = re.escape(custom_text) + else: + base_pattern = "[Ihr-Text]" + elif char_type == "special": + base_pattern = "[\\s\\.,\\-\\_]" + + # Apply quantity + if quantity_type: + if quantity_type == "once": + # No modifier needed + pass + elif quantity_type == "one_or_more": + base_pattern += "+" + elif quantity_type == "zero_or_one": + base_pattern += "?" + elif quantity_type == "zero_or_more": + base_pattern += "*" + elif quantity_type == "exactly_n": + n = self.custom_quantity_var.get() + if n.isdigit() and int(n) > 0: + base_pattern += f"{{{n}}}" + else: + base_pattern += "{N}" + elif quantity_type == "between_m_n": + min_val = self.min_quantity_var.get() + max_val = self.max_quantity_var.get() + if min_val.isdigit() and max_val.isdigit(): + min_int = int(min_val) + max_int = int(max_val) + if min_int <= max_int: + base_pattern += f"{{{min_int},{max_int}}}" + else: + base_pattern += "{Min-Max}" + else: + base_pattern += "{Min-Max}" + + self.current_preview_var.set(base_pattern) + self.update_preview_description() + + def update_preview_description(self): + """ + Update the description of the current preview + """ + char_type = self.current_char_type.get() + quantity_type = self.current_quantity_type.get() + preview = self.current_preview_var.get() + + if not char_type or preview == self.translations.get("select_char_first", "Wählen Sie zuerst einen Zeichentyp aus..."): + self.builder_description_var.set("") + return + + descriptions = [] + + # Character type description + if char_type == "letters": + descriptions.append(self.translations.get("builder_desc_letters", "Buchstaben (a-z, A-Z)")) + elif char_type == "digits": + descriptions.append(self.translations.get("builder_desc_digits", "Zahlen (0-9)")) + elif char_type == "alphanum": + descriptions.append(self.translations.get("builder_desc_alphanum", "Buchstaben und Zahlen")) + elif char_type == "any": + descriptions.append(self.translations.get("builder_desc_any", "Ein beliebiges Zeichen")) + elif char_type == "custom": + custom_text = self.custom_text_var.get() + if custom_text: + descriptions.append(self.translations.get("builder_desc_custom", "Der Text: '{}'").format(custom_text)) + else: + descriptions.append(self.translations.get("builder_desc_custom_generic", "Ihr eigener Suchtext")) + elif char_type == "special": + descriptions.append(self.translations.get("builder_desc_special", "Sonderzeichen (Leerzeichen, Punkt, Komma, etc.)")) + + # Quantity description + if quantity_type: + if quantity_type == "once": + descriptions.append(self.translations.get("builder_desc_once", "erscheint genau einmal")) + elif quantity_type == "one_or_more": + descriptions.append(self.translations.get("builder_desc_one_plus", "erscheint ein- oder mehrmals")) + elif quantity_type == "zero_or_one": + descriptions.append(self.translations.get("builder_desc_zero_one", "ist optional (0- oder 1-mal)")) + elif quantity_type == "zero_or_more": + descriptions.append(self.translations.get("builder_desc_zero_plus", "erscheint beliebig oft (0-mal oder mehr)")) + elif quantity_type == "exactly_n": + n = self.custom_quantity_var.get() + if n.isdigit() and int(n) > 0: + descriptions.append(self.translations.get("builder_desc_n_times", "erscheint genau {}-mal").format(n)) + else: + descriptions.append("erscheint genau N-mal") + elif quantity_type == "between_m_n": + min_val = self.min_quantity_var.get() + max_val = self.max_quantity_var.get() + if min_val.isdigit() and max_val.isdigit(): + descriptions.append(self.translations.get("builder_desc_m_n_times", "erscheint {} bis {}-mal").format(min_val, max_val)) + else: + descriptions.append("erscheint zwischen Min- und Max-mal") + + # Check for anchors and modifiers + if preview.startswith("^"): + descriptions.append(self.translations.get("builder_desc_start", "am Zeilenanfang")) + if preview.endswith("$"): + descriptions.append(self.translations.get("builder_desc_end", "am Zeilenende")) + if preview.startswith("\\b") and preview.endswith("\\b"): + descriptions.append(self.translations.get("builder_desc_word", "als ganzes Wort")) + if "|" in preview: + descriptions.append(self.translations.get("builder_desc_or", "mit Alternativen")) + + self.builder_description_var.set(" • ".join(descriptions)) + + def add_to_pattern(self): + """ + Add the current preview to the main regex pattern + """ + preview = self.current_preview_var.get() + if preview and preview != self.translations.get("select_char_first", "Wählen Sie zuerst einen Zeichentyp aus..."): + current_pattern = self.regex_pattern_var.get() + self.save_to_history(current_pattern) + new_pattern = current_pattern + preview + self.regex_pattern_var.set(new_pattern) + + # Reset builder selection for next component + self.reset_builder_selection() + else: + messagebox.showwarning(self.translations.get("error", "Hinweis"), "Bitte wählen Sie zuerst einen Zeichentyp und optional eine Anzahl aus.") + + def reset_builder_selection(self): + """ + Reset the current builder selection + """ + self.current_char_type.set("") + self.current_quantity_type.set("") + self.custom_text_var.set("") + self.current_preview_var.set(self.translations.get("select_char_first", "Wählen Sie zuerst einen Zeichentyp aus...")) + self.builder_description_var.set("") + + # Hide custom input frames + self.custom_text_frame.pack_forget() + self.exactly_frame.pack_forget() + self.between_frame.pack_forget() + + def manage_presets(self): + """ + Open presets management dialog + """ + # Get main GUI reference to access manage_presets method + if hasattr(self, 'main_gui') and self.main_gui and hasattr(self.main_gui, 'manage_presets'): + self.main_gui.manage_presets() + + def save_as_preset(self): + """ + Save the current regex pattern as a new preset + """ + pattern = self.regex_pattern_var.get().strip() + if not pattern: + messagebox.showwarning(self.translations.get("error", "Warnung"), self.translations.get("msg_no_pattern_copy", "Kein Regex-Muster zum Speichern verfügbar")) + return + + # Ask for preset name + name_dialog = tk.Toplevel(self.frame) + name_dialog.title(self.translations.get("save_pattern", "Muster speichern")) + name_dialog.geometry("400x200") + name_dialog.resizable(False, False) + name_dialog.transient(self.frame) + name_dialog.grab_set() + + # Center the dialog + name_dialog.update_idletasks() + width = name_dialog.winfo_width() + height = name_dialog.winfo_height() + x = (self.frame.winfo_toplevel().winfo_width() // 2) - (width // 2) + self.frame.winfo_toplevel().winfo_x() + y = (self.frame.winfo_toplevel().winfo_height() // 2) - (height // 2) + self.frame.winfo_toplevel().winfo_y() + name_dialog.geometry(f'{width}x{height}+{x}+{y}') + + # Content + content_frame = ttk.Frame(name_dialog, padding="15") + content_frame.pack(fill=tk.BOTH, expand=True) + + ttk.Label(content_frame, text=self.translations.get("name_for_pattern", "Name für das neue Muster:")).pack(anchor=tk.W, pady=(0, 5)) + + name_var = tk.StringVar() + name_entry = ttk.Entry(content_frame, textvariable=name_var, width=40, font=('Segoe UI', 12)) + name_entry.pack(fill=tk.X, pady=(0, 10)) + name_entry.focus() + + # Buttons + button_frame = ttk.Frame(content_frame) + button_frame.pack(fill=tk.X, pady=(10, 0)) + + def on_save(): + name = name_var.get().strip() + if not name: + messagebox.showerror(self.translations.get("error", "Fehler"), self.translations.get("enter_name", "Bitte geben Sie einen Namen ein")) + return + + # Check if name already exists + if hasattr(self, 'main_gui') and self.main_gui and name in self.main_gui.pattern_presets: + if not messagebox.askyesno(self.translations.get("save_button", "Überschreiben"), self.translations.get("overwrite_confirm", "Ein Muster mit dem Namen '{}' existiert bereits. Möchten Sie es überschreiben?").format(name)): + return + + # Save the preset + if hasattr(self, 'main_gui') and self.main_gui: + self.main_gui.pattern_presets[name] = pattern + self.main_gui.pattern_descriptions[name] = f"Muster aus Regex-Builder: {pattern[:50]}{'...' if len(pattern) > 50 else ''}" + self.main_gui.save_presets() + + messagebox.showinfo(self.translations.get("success", "Erfolg"), self.translations.get("msg_save_success", "Muster '{}' wurde gespeichert").format(name)) + name_dialog.destroy() + else: + messagebox.showerror(self.translations.get("error", "Fehler"), self.translations.get("error_main_gui_not_connected", "Haupt-GUI nicht verfügbar")) + + def on_cancel(): + name_dialog.destroy() + + ttk.Button(button_frame, text=self.translations.get("save_button", "💾 Speichern"), command=on_save).pack(side=tk.RIGHT, padx=(5, 0)) + ttk.Button(button_frame, text="❌ Abbrechen", command=on_cancel).pack(side=tk.RIGHT) + + # Bind Enter key to save + name_entry.bind('', lambda e: on_save()) + name_entry.bind('', lambda e: on_cancel()) + + def get_frame(self): + """ + Get the frame for this tab + + Returns: + The frame containing all widgets + """ + return self.frame diff --git a/excel_filter/gui_new.py b/excel_filter/gui_new.py new file mode 100644 index 0000000..2ce58bc --- /dev/null +++ b/excel_filter/gui_new.py @@ -0,0 +1,1032 @@ +""" +Refactored GUI for the Excel Filter Tool +Main GUI class that orchestrates all components +""" + +import tkinter as tk +from tkinter import filedialog, messagebox, ttk +import os +import json +import subprocess +import sys +import pandas as pd +import platform + +# Update the imports to use absolute paths +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from filter import ExcelFilter +from gui_components.main_window import MainWindow +from gui_components.config_tab import ConfigTab +from gui_components.execution_tab import ExecutionTab +from gui_components.regex_builder_tab import RegexBuilderTab +from gui_components.help_tab import HelpTab +from utils.file_utils import browse_file, browse_save_file, load_config, save_config +from utils.logging_utils import log_message, log_error +from translations import Translations + + +class ExcelFilterGUI: + """ + Main GUI class for the Excel Filter Tool + """ + + def __init__(self, root): + """ + Initialize the GUI + """ + self.root = root + + # Initialize translations + self.translations = Translations() + + # Get user data directory for config files + self.user_data_dir = self.get_user_data_directory() + + # Configuration + self.config_file = os.path.join(self.user_data_dir, "config.json") + + # Load presets from file + self.presets_file = os.path.join(self.user_data_dir, "presets.json") + self.pattern_presets, self.pattern_descriptions = self.load_presets() + + # Windows 11 Fluent Design color palette + self.win11_colors = { + 'primary': '#0078d4', # Windows 11 Blue + 'primary_dark': '#005a9e', # Darker blue for hover effects + 'primary_light': '#cce4f7', # Light blue for active states + 'background': '#f0f0f0', # Light gray background + 'surface': '#ffffff', # White surface + 'text': '#212121', # Dark text + 'text_secondary': '#616161', # Secondary text + 'border': '#e0e0e0', # Border color + 'hover': '#e8f0fe', # Hover effect + 'pressed': '#d0e0f5', # Pressed effect + 'success': '#107c10', # Success color + 'warning': '#c55c00', # Warning color + 'error': '#c42b1c' # Error color + } + + # Create main window with Windows 11 styling (now larger) + self.main_window = MainWindow(root) + + # Apply Windows 11 window properties (size is now handled by MainWindow) + root.title(self.translations["app_title"]) + root.configure(bg=self.win11_colors['background']) + + # Set window icon if available + try: + root.iconbitmap('app_icon.ico') + except: + pass + + # Create tabs + self.create_tabs() + + # Load configuration + self.load_config() + + def get_user_data_directory(self): + """ + Get the user data directory for storing config files. + On Windows: %APPDATA%\\ExcelFilter + On Linux/Mac: ~/.config/ExcelFilter or ~/Library/Application Support/ExcelFilter + """ + app_name = "ExcelFilter" + + if platform.system() == "Windows": + # Use %APPDATA%\ExcelFilter + base_dir = os.path.expandvars("%APPDATA%") + data_dir = os.path.join(base_dir, app_name) + elif platform.system() == "Darwin": # macOS + # Use ~/Library/Application Support/ExcelFilter + home = os.path.expanduser("~") + data_dir = os.path.join(home, "Library", "Application Support", app_name) + else: # Linux and others + # Use ~/.config/ExcelFilter + home = os.path.expanduser("~") + data_dir = os.path.join(home, ".config", app_name) + + # Create the directory if it doesn't exist + try: + os.makedirs(data_dir, exist_ok=True) + except Exception as e: + print(f"Warning: Could not create user data directory {data_dir}: {e}") + # Fallback to current directory if we can't create the user directory + data_dir = os.getcwd() + + return data_dir + + def connect_browse_buttons(self, config_tab): + """ + Connect the browse buttons to their respective methods + This ensures the buttons work even if created before methods are assigned + """ + # Find and connect the browse buttons + for child in config_tab.frame.winfo_children(): + if isinstance(child, ttk.Frame): + for subchild in child.winfo_children(): + if isinstance(subchild, ttk.Frame): + for widget in subchild.winfo_children(): + if isinstance(widget, ttk.Button) and "Durchsuchen" in widget.cget("text"): + # Determine which button it is based on grid position + try: + grid_info = widget.grid_info() + if grid_info["row"] == 1: # Input file row + widget.config(command=self.browse_input_file) + elif grid_info["row"] == 2: # Output file row + widget.config(command=self.browse_output_file) + except Exception as e: + print(f"Error connecting browse button: {e}") + + def on_sheet_selected(self, event): + """ + Wird aufgerufen, wenn ein Arbeitsblatt ausgewählt wird + """ + selected_sheet = event.widget.get() + self.config_tab.sheet_var.set(selected_sheet) + self.update_columns_selection() + + def update_sheet_selection(self): + """ + Aktualisiert die Arbeitsblattauswahl + """ + input_file = self.config_tab.input_file_var.get() + if not input_file: + messagebox.showerror("Fehler", "Bitte geben Sie eine Eingabedatei an") + return + try: + # Arbeitsblätter aus der Excel-Datei lesen + xls = pd.ExcelFile(input_file) + sheets = xls.sheet_names + # Arbeitsblattauswahl aktualisieren + self.config_tab.sheet_combobox['values'] = sheets + if sheets: + self.config_tab.sheet_combobox.current(0) + self.config_tab.sheet_var.set(sheets[0]) + self.config_tab.log_message(f"Arbeitsblattauswahl aktualisiert: {sheets}") + except Exception as e: + self.config_tab.log_error(f"Fehler beim Aktualisieren der Arbeitsblattauswahl: {e}") + messagebox.showerror("Fehler", f"Fehler beim Aktualisieren der Arbeitsblattauswahl: {e}") + + def update_columns_selection(self, selected_columns=None): + """ + Aktualisiert die Spaltenauswahl + + Args: + selected_columns: List of column names that should be initially selected + """ + input_file = self.config_tab.input_file_var.get() + if not input_file: + messagebox.showerror("Fehler", "Bitte geben Sie eine Eingabedatei an") + return + try: + # Spalten aus der Excel-Datei lesen + sheet_name = self.config_tab.sheet_var.get() + if sheet_name: + df = pd.read_excel(input_file, sheet_name=sheet_name) + else: + df = pd.read_excel(input_file) + columns = df.columns.tolist() + # Spalten ohne Namen behandeln + for i, column in enumerate(columns): + if pd.isna(column) or column == "": + # Ersten nicht leeren Wert in der Spalte finden + for value in df.iloc[:, i]: + if pd.notna(value) and value != "": + columns[i] = str(value)[:10] # Auf 10 Zeichen begrenzen + break + else: + columns[i] = f"Spalte_{i+1}" # Falls alle Werte leer sind + # Spaltenauswahl-Container leeren + for widget in self.config_tab.columns_container.winfo_children(): + widget.destroy() + # Regex-Spaltenauswahl aktualisieren + self.config_tab.regex_column_combobox['values'] = ["Alle Spalten"] + columns + self.config_tab.regex_column_combobox.current(0) + # Spaltenauswahl erstellen + self.config_tab.columns_vars = {} + for i, column in enumerate(columns): + var = tk.IntVar() + # If selected_columns is provided and column is in it, select the checkbox + if selected_columns and column in selected_columns: + var.set(1) + self.config_tab.columns_vars[column] = var + checkbox = ttk.Checkbutton(self.config_tab.columns_container, text=column, variable=var) + checkbox.grid(row=i // 3, column=i % 3, sticky=tk.W, padx=5, pady=2) + checkbox.configure(style='TCheckbutton') # Ensure proper styling + + # Ensure container is properly configured and visible + self.config_tab.columns_container.update_idletasks() + + # Debug: Check if checkboxes were actually created + checkboxes = self.config_tab.columns_container.winfo_children() + print(f"DEBUG: Created {len(checkboxes)} checkboxes for columns: {columns}") + + self.config_tab.log_message(f"Spaltenauswahl aktualisiert: {columns}") + except Exception as e: + self.config_tab.log_error(f"Fehler beim Aktualisieren der Spaltenauswahl: {e}") + messagebox.showerror("Fehler", f"Fehler beim Aktualisieren der Spaltenauswahl: {e}") + print(f"DEBUG: Error in update_columns_selection: {e}") + + def select_all_columns(self): + """ + Wählt alle Spalten aus + """ + for column, var in self.config_tab.columns_vars.items(): + var.set(1) + self.config_tab.log_message("Alle Spalten ausgewählt") + + def deselect_all_columns(self): + """ + Wählt alle Spalten ab + """ + for column, var in self.config_tab.columns_vars.items(): + var.set(0) + self.config_tab.log_message("Alle Spalten abgewählt") + + def create_tabs(self): + """ + Create all tabs for the application + """ + # Get scale factor from main window for DPI-aware scaling + scale_factor = self.main_window.scale_factor + + # Configuration tab + config_tab = ConfigTab( + self.main_window.notebook, + self.win11_colors, + self.pattern_presets, + self.pattern_descriptions, + self.translations, + scale_factor + ) + + # Connect config tab methods + config_tab.browse_input_file = self.browse_input_file + config_tab.browse_output_file = self.browse_output_file + config_tab.process_file = self.process_file + config_tab.save_config = self.save_config + config_tab.load_config = self.load_config + config_tab.on_sheet_selected = self.on_sheet_selected + config_tab.update_columns_selection = self.update_columns_selection + config_tab.select_all_columns = self.select_all_columns + config_tab.deselect_all_columns = self.deselect_all_columns + # Note: get_selected_columns is handled by config_tab's own method + + # Connect the browse buttons directly - this ensures they work + self.connect_browse_buttons(config_tab) + + self.config_tab = config_tab + self.main_window.add_tab(config_tab.get_frame(), "Konfiguration") + + # Execution tab + execution_tab = ExecutionTab(self.main_window.notebook, self.win11_colors, scale_factor, self.translations) + execution_tab.execute_command = self.process_file + self.execution_tab = execution_tab + self.main_window.add_tab(execution_tab.get_frame(), self.translations["tab_execution"] if self.translations else "Ausführung") + + # Connect config tab to execution tab and main window for logging and navigation + self.config_tab.set_execution_tab(self.execution_tab) + self.config_tab.set_main_gui(self) # Pass the main GUI instance instead of main_window + self.config_tab.set_on_columns_changed(lambda: self.execution_tab.update_command_display()) + + # Connect execution tab to config tab for command display updates + self.execution_tab.set_config_tab(self.config_tab) + self.execution_tab.set_main_gui(self) + + # Regex Builder tab + regex_builder_tab = RegexBuilderTab(self.main_window.notebook, self.win11_colors, scale_factor, main_gui=self) + regex_builder_tab.apply_to_main = lambda: self.config_tab.pattern_var.set(regex_builder_tab.regex_pattern_var.get()) + self.regex_builder_tab = regex_builder_tab + self.main_window.add_tab(regex_builder_tab.get_frame(), "Regex-Builder") + + # Help tab + help_tab = HelpTab(self.main_window.notebook, self.win11_colors, scale_factor, self.translations, self.switch_language) + self.main_window.add_tab(help_tab.get_frame(), "Hilfe") + + # Enhance tab appearance to look more like Windows 11 + self.main_window.enhance_tab_appearance() + + def browse_input_file(self): + """ + Öffnet einen Dateidialog zum Auswählen der Eingabedatei + """ + file_path = browse_file( + title="Eingabedatei auswählen", + filetypes=[("Excel Dateien", "*.xlsx;*.xls"), ("Alle Dateien", "*.*")] + ) + + if file_path: + self.config_tab.input_file_var.set(file_path) + self.config_tab.log_message(f"Eingabedatei ausgewählt: {file_path}") + self.update_sheet_selection() + self.update_columns_selection() + + # Auto-fill output file path + if not self.config_tab.output_file_var.get(): + base_name = os.path.splitext(os.path.basename(file_path))[0] + output_path = os.path.join(os.path.dirname(file_path), f"{base_name}_filtered.xlsx") + self.config_tab.output_file_var.set(output_path) + + def browse_output_file(self): + """ + Browse for output file + """ + file_path = browse_save_file( + title="Ausgabedatei speichern", + filetypes=[("Excel Dateien", "*.xlsx"), ("Alle Dateien", "*.*")], + defaultextension=".xlsx" + ) + + if file_path: + self.config_tab.output_file_var.set(file_path) + + def load_config(self): + """ + Load configuration from file + """ + try: + config = load_config(self.config_file) + + if config: + self.config_tab.input_file_var.set(config.get("input_file", "")) + self.config_tab.output_file_var.set(config.get("output_file", "")) + self.config_tab.pattern_var.set(config.get("pattern", "error|warning|critical")) + self.config_tab.sheet_var.set(config.get("sheet_name", "Sheet1")) + self.config_tab.columns_var.set(config.get("columns", "")) + + self.config_tab.status_var.set("Konfiguration geladen") + messagebox.showinfo("Erfolg", "Konfiguration erfolgreich geladen") + + # Auto-update sheets and columns if input file is set + input_file = config.get("input_file", "") + if input_file and os.path.exists(input_file): + self.update_sheet_selection() + self.update_columns_selection() + else: + self.config_tab.status_var.set("Keine Konfiguration gefunden") + + except Exception as e: + self.config_tab.status_var.set(f"Fehler beim Laden: {e}") + messagebox.showerror("Fehler", f"Fehler beim Laden der Konfiguration: {e}") + + def save_config(self): + """ + Save current configuration to file + """ + try: + config = { + "input_file": self.config_tab.input_file_var.get(), + "output_file": self.config_tab.output_file_var.get(), + "pattern": self.config_tab.pattern_var.get(), + "sheet_name": self.config_tab.sheet_var.get(), + "columns": self.config_tab.columns_var.get() + } + + save_config(self.config_file, config) + self.config_tab.status_var.set("Konfiguration gespeichert") + messagebox.showinfo("Erfolg", "Konfiguration erfolgreich gespeichert") + + except Exception as e: + self.config_tab.status_var.set(f"Fehler beim Speichern: {e}") + messagebox.showerror("Fehler", f"Fehler beim Speichern der Konfiguration: {e}") + + def process_file(self): + """ + Process the Excel file with current settings and display detailed professional statistics + """ + input_file = self.config_tab.input_file_var.get() + output_file = self.config_tab.output_file_var.get() + pattern = self.config_tab.pattern_var.get() + sheet_name = self.config_tab.sheet_var.get() + columns = self.config_tab.columns_var.get() + + # Validate inputs + if not input_file: + self.config_tab.status_var.set("Fehler: Keine Eingabedatei ausgewählt") + messagebox.showerror("Fehler", "Bitte wählen Sie eine Eingabedatei aus") + return + + if not output_file: + self.config_tab.status_var.set("Fehler: Keine Ausgabedatei ausgewählt") + messagebox.showerror("Fehler", "Bitte wählen Sie eine Ausgabedatei aus") + return + + # Check if pattern is required (required when regex is enabled) + regex_enabled = self.config_tab.regex_enabled_var.get() + if not pattern and regex_enabled: + self.config_tab.status_var.set("Fehler: Kein Filtermuster angegeben") + messagebox.showerror("Fehler", "Bitte geben Sie ein Filtermuster ein") + return + + # Parse columns using the config tab's method + if hasattr(self.config_tab, 'get_selected_columns'): + column_list = self.config_tab.get_selected_columns() + else: + column_list = None + + if not column_list: # If no columns selected, use all + column_list = None + + try: + # Start execution with progress indication + self.execution_tab.start_execution() + self.config_tab.status_var.set("🔬 Erweiterte Excel-Analyse läuft...") + + # Professional progress messages + self.execution_tab.update_progress(5, "🔬 Initialisiere erweiterte Analyse-Engine...") + self.root.update() + + self.execution_tab.update_progress(15, "📊 Lade und analysiere Excel-Dateistruktur...") + self.root.update() + + # Get numeric filter settings from config tab + numeric_filter = self.config_tab.get_numeric_filter_settings() + + # Create ExcelFilter with advanced statistics collection + excel_filter = ExcelFilter( + input_file=input_file, + output_file=output_file, + pattern=pattern, + sheet_name=sheet_name if sheet_name else None, + columns=column_list if column_list else None, + numeric_filter=numeric_filter, + language=self.translations.current_language + ) + + self.execution_tab.update_progress(25, "🔍 Führe intelligente Mustererkennung durch...") + self.root.update() + + self.execution_tab.update_progress(40, "⚡ Wende fortschrittliche Filteralgorithmen an...") + self.root.update() + + self.execution_tab.update_progress(60, "📈 Berechne Performance-Metriken und Statistiken...") + self.root.update() + + self.execution_tab.update_progress(80, "💾 Optimiere und komprimiere Ausgabedatei...") + self.root.update() + + # Execute the filtering process + success, error_msg = excel_filter.process() + + self.execution_tab.update_progress(95, "📊 Erstelle detaillierte Verarbeitungsberichte...") + + if success: + self.execution_tab.update_progress(100, "🎉 Excel-Filter-Analyse erfolgreich abgeschlossen!") + + stats = excel_filter.get_statistics() + + # Log detailed statistics directly to the execution tab log + self.execution_tab.log_message("=" * 60) + self.execution_tab.log_message("EXCEL-FILTER-ANALYSE ERFOLGREICH ABGESCHLOSSEN!") + self.execution_tab.log_message("=" * 60) + + # File statistics + self.execution_tab.log_message("DATEI-STATISTIKEN:") + self.execution_tab.log_message(f" Eingabedatei: {stats['input_file_size'] / (1024*1024):.2f} MB") + self.execution_tab.log_message(f" Ausgabedatei: {stats['output_file_size'] / (1024*1024):.2f} MB") + if stats['compression_ratio'] > 0: + compression_pct = (stats['compression_ratio'] - 1) * 100 + self.execution_tab.log_message(f" Kompressionsrate: {compression_pct:+.1f}%") + + # Data processing statistics + self.execution_tab.log_message("\nDATENVERARBEITUNG:") + self.execution_tab.log_message(f" Eingabe: {stats['input_rows']:,} Zeilen × {stats['input_columns']} Spalten") + self.execution_tab.log_message(f" Ausgabe: {stats['output_rows']:,} Zeilen × {stats['output_columns']} Spalten") + if stats['input_rows'] > 0: + reduction_pct = (stats['rows_removed'] / stats['input_rows']) * 100 + self.execution_tab.log_message(f" Zeilen reduziert: {stats['rows_removed']:,} ({reduction_pct:.1f}%)") + + # Applied filters + if stats['filters_applied']: + self.execution_tab.log_message(f"\n🎯 ANGEWENDETE FILTER: {', '.join(stats['filters_applied'])}") + if stats['input_rows'] > 0: + retention_rate = (stats['rows_filtered'] / stats['input_rows']) * 100 + self.execution_tab.log_message(f" Filter-Effizienz: {retention_rate:.1f}% Zeilen behalten") + + # Performance metrics + self.execution_tab.log_message("\n⚡ PERFORMANCE-METRIKEN:") + self.execution_tab.log_message(f" Verarbeitungszeit: {stats['processing_time_seconds']:.2f} Sekunden") + self.execution_tab.log_message(f" Speicherverbrauch: {stats['memory_usage_mb']:.2f} MB") + if stats['processing_time_seconds'] > 0 and stats['input_rows'] > 0: + rows_per_second = stats['input_rows'] / stats['processing_time_seconds'] + self.execution_tab.log_message(f" Geschwindigkeit: {rows_per_second:.0f} Zeilen/Sekunde") + + self.execution_tab.log_message("\n💾 Ausgabedatei wird automatisch geöffnet...") + self.execution_tab.log_message("=" * 60) + + self.config_tab.status_var.set("✅ Erweiterte Analyse erfolgreich abgeschlossen") + + # Open the output file + self.open_file(output_file) + self.execution_tab.finish_execution(success=True) + else: + # Enhanced error handling with detailed error messages + error_title = "Fehler bei der Excel-Filter-Analyse" + detailed_error = error_msg if error_msg else "Unbekannter Fehler" + + self.execution_tab.update_progress(100, "Analyse fehlgeschlagen") + self.config_tab.status_var.set("Erweiterte Analyse fehlgeschlagen") + + # Log detailed error information + self.execution_tab.log_message("=" * 60) + self.execution_tab.log_message("EXCEL-FILTER-ANALYSE FEHLGESCHLAGEN!") + self.execution_tab.log_message("=" * 60) + self.execution_tab.log_message(f"FEHLERDETAILS:") + self.execution_tab.log_message(f" Fehler: {detailed_error}") + self.execution_tab.log_message("=" * 60) + + messagebox.showerror(error_title, f"Fehler bei der Excel-Filter-Analyse:\n\n{detailed_error}") + self.execution_tab.finish_execution(success=False) + + except Exception as e: + # Enhanced exception handling with more details + error_details = f"{type(e).__name__}: {e}" + self.execution_tab.update_progress(100, f"Kritischer Fehler in Analyse-Engine") + self.config_tab.status_var.set(f"Analyse-Engine Fehler") + + # Log detailed exception information + self.execution_tab.log_message("=" * 60) + self.execution_tab.log_message("KRITISCHER FEHLER IN DER ANALYSE-ENGINE!") + self.execution_tab.log_message("=" * 60) + self.execution_tab.log_message(f"FEHLERDETAILS:") + self.execution_tab.log_message(f" Fehler: {error_details}") + self.execution_tab.log_message("=" * 60) + + messagebox.showerror("Kritischer Fehler", f"Fehler in der Analyse-Engine:\n\n{error_details}") + self.execution_tab.finish_execution(success=False) + + def open_file(self, file_path): + """ + Open a file with the default application + """ + try: + if sys.platform == "win32": + os.startfile(file_path) + elif sys.platform == "darwin": + subprocess.run(["open", file_path]) + else: + subprocess.run(["xdg-open", file_path]) + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Öffnen der Datei: {e}") + + + + def switch_language(self, new_language): + """ + Switch the application language + + Args: + new_language: New language code ('en' or 'de') + """ + try: + # Update translations + self.translations.set_language(new_language) + + # Update window title + self.root.title(self.translations["app_title"]) + + # Update pattern presets and descriptions + self.pattern_presets = { + self.translations["errors_and_warnings"]: r"error|warning|critical", + self.translations["errors_only"]: r"error", + self.translations["warnings_only"]: r"warning", + self.translations["critical_errors"]: r"critical", + self.translations["numbers_100_199"]: r"1\d{2}", + self.translations["email_addresses"]: r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", + self.translations["phone_numbers"]: r"\+?[0-9\s-]{10,}", + self.translations["date_yyyy_mm_dd"]: r"\d{4}-\d{2}-\d{2}" + } + + self.pattern_descriptions = { + self.translations["errors_and_warnings"]: self.translations["desc_errors_and_warnings"], + self.translations["errors_only"]: self.translations["desc_errors_only"], + self.translations["warnings_only"]: self.translations["desc_warnings_only"], + self.translations["critical_errors"]: self.translations["desc_critical_errors"], + self.translations["numbers_100_199"]: self.translations["desc_numbers_100_199"], + self.translations["email_addresses"]: self.translations["desc_email_addresses"], + self.translations["phone_numbers"]: self.translations["desc_phone_numbers"], + self.translations["date_yyyy_mm_dd"]: self.translations["desc_date_yyyy_mm_dd"] + } + + # Update tab names + tab_names = [ + self.translations["tab_config"], + self.translations["tab_execution"], + self.translations["tab_regex_builder"], + self.translations["tab_help"] + ] + + for i, tab_name in enumerate(tab_names): + try: + self.main_window.notebook.tab(i, text=tab_name) + except: + pass # Tab might not exist yet + + + + # Show success message + messagebox.showinfo( + self.translations["success"], + f"{self.translations['language']} {self.translations['english'] if new_language == 'en' else self.translations['german']}" + ) + + except Exception as e: + messagebox.showerror( + self.translations["error"], + f"{self.translations['error']}: {e}" + ) + + def load_presets(self): + """ + Load pattern presets from file + """ + try: + if os.path.exists(self.presets_file): + with open(self.presets_file, 'r', encoding='utf-8') as f: + presets_data = json.load(f) + presets = presets_data.get('presets', {}) + descriptions = presets_data.get('descriptions', {}) + else: + # Default presets if file doesn't exist + presets = { + self.translations["errors_and_warnings"]: r"error|warning|critical", + self.translations["errors_only"]: r"error", + self.translations["warnings_only"]: r"warning", + self.translations["critical_errors"]: r"critical", + self.translations["numbers_100_199"]: r"1\d{2}", + self.translations["email_addresses"]: r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", + self.translations["phone_numbers"]: r"\+?[0-9\s-]{10,}", + self.translations["date_yyyy_mm_dd"]: r"\d{4}-\d{2}-\d{2}" + } + descriptions = { + self.translations["errors_and_warnings"]: self.translations["desc_errors_and_warnings"], + self.translations["errors_only"]: self.translations["desc_errors_only"], + self.translations["warnings_only"]: self.translations["desc_warnings_only"], + self.translations["critical_errors"]: self.translations["desc_critical_errors"], + self.translations["numbers_100_199"]: self.translations["desc_numbers_100_199"], + self.translations["email_addresses"]: self.translations["desc_email_addresses"], + self.translations["phone_numbers"]: self.translations["desc_phone_numbers"], + self.translations["date_yyyy_mm_dd"]: self.translations["desc_date_yyyy_mm_dd"] + } + # Save default presets + self.save_presets(presets, descriptions) + return presets, descriptions + except Exception as e: + print(f"Error loading presets: {e}") + # Return empty presets on error + return {}, {} + + def save_presets(self, presets=None, descriptions=None): + """ + Save pattern presets to file + """ + try: + if presets is None: + presets = self.pattern_presets + if descriptions is None: + descriptions = self.pattern_descriptions + + presets_data = { + 'presets': presets, + 'descriptions': descriptions + } + + with open(self.presets_file, 'w', encoding='utf-8') as f: + json.dump(presets_data, f, indent=2, ensure_ascii=False) + except Exception as e: + print(f"Error saving presets: {e}") + + def manage_presets(self): + """ + Open presets management dialog + """ + PresetsManagerDialog(self.root, self) + + def run(self): + """ + Run the main application loop + """ + self.main_window.run() + + +class PresetsManagerDialog: + """ + Dialog for managing pattern presets + """ + + def __init__(self, parent, main_gui): + """ + Initialize the presets manager dialog + + Args: + parent: Parent window + main_gui: Main GUI instance + """ + self.parent = parent + self.main_gui = main_gui + self.dialog = tk.Toplevel(parent) + self.dialog.title("Voreingestellte Muster verwalten") + self.dialog.geometry("600x500") + self.dialog.resizable(True, True) + self.dialog.transient(parent) + self.dialog.grab_set() + + # Get scaling factor from main GUI if available + self.scale_factor = getattr(main_gui, 'main_window', None) + if self.scale_factor and hasattr(self.scale_factor, 'scale_factor'): + self.scale_factor = self.scale_factor.scale_factor + else: + self.scale_factor = 1.0 + + # Variables + self.selected_preset_var = tk.StringVar() + self.name_var = tk.StringVar() + self.pattern_var = tk.StringVar() + self.description_var = tk.StringVar() + + self.create_widgets() + self.load_presets_list() + self.center_dialog() + + def create_widgets(self): + """ + Create dialog widgets + """ + # Main frame + main_frame = ttk.Frame(self.dialog, padding="10") + main_frame.pack(fill=tk.BOTH, expand=True) + + # Title + title_label = ttk.Label(main_frame, text="📝 Voreingestellte Regex-Muster verwalten", + font=('Helvetica', 12, 'bold')) + title_label.pack(pady=(0, 10)) + + # Content frame + content_frame = ttk.Frame(main_frame) + content_frame.pack(fill=tk.BOTH, expand=True) + + # Left side - Presets list + left_frame = ttk.LabelFrame(content_frame, text="Vorhandene Muster", padding="5") + left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5)) + + # Presets listbox with scrollbar + listbox_frame = ttk.Frame(left_frame) + listbox_frame.pack(fill=tk.BOTH, expand=True, pady=5) + + scrollbar = ttk.Scrollbar(listbox_frame) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + self.presets_listbox = tk.Listbox( + listbox_frame, + selectmode=tk.SINGLE, + yscrollcommand=scrollbar.set, + font=('Segoe UI', int(10 * self.scale_factor)) + ) + self.presets_listbox.pack(fill=tk.BOTH, expand=True) + self.presets_listbox.bind('<>', self.on_preset_selected) + + scrollbar.config(command=self.presets_listbox.yview) + + # Buttons for presets list + buttons_frame = ttk.Frame(left_frame) + buttons_frame.pack(fill=tk.X, pady=(5, 0)) + + ttk.Button(buttons_frame, text="➕ Neu", command=self.add_preset).pack(side=tk.LEFT, padx=(0, 5)) + ttk.Button(buttons_frame, text="🗑️ Löschen", command=self.delete_preset).pack(side=tk.LEFT) + + # Right side - Preset editor + right_frame = ttk.LabelFrame(content_frame, text="Muster bearbeiten", padding="5") + right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0)) + + # Name field + name_frame = ttk.Frame(right_frame) + name_frame.pack(fill=tk.X, pady=2) + ttk.Label(name_frame, text="Name:").pack(side=tk.LEFT) + name_entry = ttk.Entry(name_frame, textvariable=self.name_var, font=('Segoe UI', 12)) + name_entry.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=(5, 0)) + + # Pattern field + pattern_frame = ttk.Frame(right_frame) + pattern_frame.pack(fill=tk.X, pady=2) + ttk.Label(pattern_frame, text="Regex-Muster:").pack(side=tk.LEFT, anchor=tk.N) + pattern_entry = ttk.Entry(pattern_frame, textvariable=self.pattern_var, font=('Segoe UI', 12)) + pattern_entry.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=(5, 0)) + + # Description field + desc_frame = ttk.Frame(right_frame) + desc_frame.pack(fill=tk.X, pady=2) + ttk.Label(desc_frame, text="Beschreibung:").pack(side=tk.LEFT, anchor=tk.N) + desc_entry = tk.Text(desc_frame, height=3, wrap=tk.WORD, font=('Segoe UI', 12)) + desc_entry.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=(5, 0)) + # Connect description text widget to variable + desc_entry.bind('', lambda e: self.description_var.set(desc_entry.get(1.0, tk.END).strip())) + + # Action buttons + action_frame = ttk.Frame(right_frame) + action_frame.pack(fill=tk.X, pady=(10, 0)) + + ttk.Button(action_frame, text="💾 Speichern", command=self.save_preset).pack(side=tk.LEFT, padx=(0, 5)) + ttk.Button(action_frame, text="🔄 Zurücksetzen", command=self.reset_form).pack(side=tk.LEFT) + + # Dialog buttons + dialog_buttons = ttk.Frame(main_frame) + dialog_buttons.pack(fill=tk.X, pady=(10, 0)) + + ttk.Button(dialog_buttons, text="✅ OK", command=self.on_ok).pack(side=tk.RIGHT, padx=(5, 0)) + ttk.Button(dialog_buttons, text="❌ Abbrechen", command=self.on_cancel).pack(side=tk.RIGHT) + + def load_presets_list(self): + """ + Load presets into the listbox + """ + self.presets_listbox.delete(0, tk.END) + for name in self.main_gui.pattern_presets.keys(): + self.presets_listbox.insert(tk.END, name) + + def on_preset_selected(self, event): + """ + Handle preset selection from listbox + """ + selection = self.presets_listbox.curselection() + if selection: + index = selection[0] + preset_name = self.presets_listbox.get(index) + + self.selected_preset_var.set(preset_name) + self.name_var.set(preset_name) + self.pattern_var.set(self.main_gui.pattern_presets.get(preset_name, "")) + self.description_var.set(self.main_gui.pattern_descriptions.get(preset_name, "")) + + # Update description text widget + for child in self.dialog.winfo_children(): + if isinstance(child, ttk.Frame): + for subchild in child.winfo_children(): + if isinstance(subchild, ttk.Frame): + for widget in subchild.winfo_children(): + if isinstance(widget, tk.Text): + widget.delete(1.0, tk.END) + widget.insert(1.0, self.description_var.get()) + + def add_preset(self): + """ + Add a new preset + """ + self.selected_preset_var.set("") + self.name_var.set("") + self.pattern_var.set("") + self.description_var.set("") + + # Clear description text widget + for child in self.dialog.winfo_children(): + if isinstance(child, ttk.Frame): + for subchild in child.winfo_children(): + if isinstance(subchild, ttk.Frame): + for widget in subchild.winfo_children(): + if isinstance(widget, tk.Text): + widget.delete(1.0, tk.END) + + def delete_preset(self): + """ + Delete the selected preset + """ + selection = self.presets_listbox.curselection() + if not selection: + messagebox.showwarning("Warnung", "Bitte wählen Sie zuerst ein Muster aus") + return + + preset_name = self.presets_listbox.get(selection[0]) + + if messagebox.askyesno("Löschen bestätigen", f"Möchten Sie das Muster '{preset_name}' wirklich löschen?"): + # Remove from dictionaries + if preset_name in self.main_gui.pattern_presets: + del self.main_gui.pattern_presets[preset_name] + if preset_name in self.main_gui.pattern_descriptions: + del self.main_gui.pattern_descriptions[preset_name] + + # Save changes + self.main_gui.save_presets() + + # Reload list + self.load_presets_list() + + # Clear form + self.reset_form() + + messagebox.showinfo("Erfolg", f"Muster '{preset_name}' wurde gelöscht") + + def save_preset(self): + """ + Save the current preset + """ + name = self.name_var.get().strip() + pattern = self.pattern_var.get().strip() + description = self.description_var.get().strip() + + if not name: + messagebox.showerror("Fehler", "Bitte geben Sie einen Namen ein") + return + + if not pattern: + messagebox.showerror("Fehler", "Bitte geben Sie ein Regex-Muster ein") + return + + # Check if name already exists (when not editing existing) + selected_preset = self.selected_preset_var.get() + if selected_preset and selected_preset != name: + # Renaming - remove old name + if selected_preset in self.main_gui.pattern_presets: + del self.main_gui.pattern_presets[selected_preset] + if selected_preset in self.main_gui.pattern_descriptions: + del self.main_gui.pattern_descriptions[selected_preset] + elif not selected_preset and name in self.main_gui.pattern_presets: + if not messagebox.askyesno("Überschreiben", f"Ein Muster mit dem Namen '{name}' existiert bereits. Möchten Sie es überschreiben?"): + return + + # Save preset + self.main_gui.pattern_presets[name] = pattern + self.main_gui.pattern_descriptions[name] = description + + # Save to file + self.main_gui.save_presets() + + # Reload list + self.load_presets_list() + + # Select the saved preset + if name in self.main_gui.pattern_presets: + for i in range(self.presets_listbox.size()): + if self.presets_listbox.get(i) == name: + self.presets_listbox.selection_clear(0, tk.END) + self.presets_listbox.selection_set(i) + break + + self.selected_preset_var.set(name) + messagebox.showinfo("Erfolg", f"Muster '{name}' wurde gespeichert") + + def reset_form(self): + """ + Reset the form to empty state + """ + self.selected_preset_var.set("") + self.name_var.set("") + self.pattern_var.set("") + self.description_var.set("") + + # Clear description text widget + for child in self.dialog.winfo_children(): + if isinstance(child, ttk.Frame): + for subchild in child.winfo_children(): + if isinstance(subchild, ttk.Frame): + for widget in subchild.winfo_children(): + if isinstance(widget, tk.Text): + widget.delete(1.0, tk.END) + + def on_ok(self): + """ + Handle OK button - save current changes and close + """ + # Save any pending changes + if self.name_var.get().strip(): + self.save_preset() + + # Update the config tab with new presets + if hasattr(self.main_gui, 'config_tab'): + self.main_gui.config_tab.pattern_presets = self.main_gui.pattern_presets + self.main_gui.config_tab.pattern_descriptions = self.main_gui.pattern_descriptions + # Refresh the combobox values using the dedicated method + if hasattr(self.main_gui.config_tab, 'refresh_preset_combobox'): + self.main_gui.config_tab.refresh_preset_combobox() + + self.dialog.destroy() + + def on_cancel(self): + """ + Handle Cancel button + """ + self.dialog.destroy() + + def center_dialog(self): + """ + Center the dialog on the parent window + """ + self.dialog.update_idletasks() + width = self.dialog.winfo_width() + height = self.dialog.winfo_height() + x = (self.parent.winfo_width() // 2) - (width // 2) + self.parent.winfo_x() + y = (self.parent.winfo_height() // 2) - (height // 2) + self.parent.winfo_y() + self.dialog.geometry(f'{width}x{height}+{x}+{y}') + + +def main(): + """ + Main function to launch the GUI + """ + root = tk.Tk() + app = ExcelFilterGUI(root) + app.run() + + +if __name__ == "__main__": + main() diff --git a/excel_filter/launch_gui.bat b/excel_filter/launch_gui.bat new file mode 100644 index 0000000..0251124 --- /dev/null +++ b/excel_filter/launch_gui.bat @@ -0,0 +1,22 @@ +@echo off +:: Excel Filter Tool - GUI Launcher +cd /d "%~dp0" + +call check_dependencies_simple.bat +if errorlevel 1 ( + echo Error checking dependencies + pause + exit /b 1 +) + +echo Start Excel Filter Tool GUI... +python gui_new.py + +if errorlevel 1 ( + echo Error when starting the GUI + pause + exit /b 1 +) + +echo GUI was closed +pause diff --git a/excel_filter/locales/de.json b/excel_filter/locales/de.json new file mode 100644 index 0000000..efbf2c7 --- /dev/null +++ b/excel_filter/locales/de.json @@ -0,0 +1,173 @@ +{ + "app_title": "Excel Filter Tool", + "tab_config": "Konfiguration", + "tab_execution": "Ausführung", + "tab_regex_builder": "Regex-Builder", + "tab_help": "Hilfe", + "config_section": "Konfiguration", + "load_button": "Laden", + "save_button": "Speichern", + "input_file": "Eingabedatei:", + "output_file": "Ausgabedatei:", + "browse_button": "Durchsuchen...", + "worksheet": "Arbeitsblatt:", + "process_button": "🚀 VERARBEITEN", + "status_ready": "Bereit", + "success": "Erfolg", + "error": "Fehler", + "language": "Sprache:", + "english": "Englisch", + "german": "Deutsch", + "help_content": "\nExcel Filter Tool\n\nFUNKTION\n--------------------------\nDas Excel Filter Tool ist ein Werkzeug zur Datenanalyse und -filterung von Excel-Dateien.\nEs ermöglicht das automatische Extrahieren, Filtern und Transformieren von Daten basierend auf einstellbaren Suchkriterien.\n\nHauptfunktionen:\n• Intelligente Textsuche anhand von Regex-Mustern\n• Numerische Filterung (größer/kleiner als, zwischen Werten)\n• Spaltenbasierte Filterung\n\nARBEITSABLAUF\n----------------------------------------\nDer Arbeitsablauf ist in vier Hauptphasen unterteilt, die jeweils eigene Tabs haben:\n\n1. KONFIGURATION\n- Wähle die Eingabe-Excel-Datei aus\n- Bestimme die Ausgabedatei\n- Wähle das zu filternde Arbeitsblatt\n- Konfiguriere die Filterkriterien (Regex und/oder numerisch)\n- Wählen die Spalten aus, welche übernommen werden sollen\n- Speicher/Lade die Konfigurationen für wiederkehrende Aufgaben\n\n2. MUSTER-ERSTELLUNG (Regex-Builder-Tab)\n- Nutze den visuellen Regex-Builder für zur einfachen Mustererstellung\n- Wähle aus vorgefertigten Bausteinen (Text, Zahlen, Spezialzeichen)\n- Definiere Mengen (einmal, mehrmals, optional)\n- Füge optionale Anker und Gruppen hinzu\n- Teste die Muster mit Beispieltexten\n- Verwalte gespeicherte Muster\n\n3. AUSFÜHRUNG\n- Überprüfe den auszuführenden Befehl vor der Verarbeitung\n- Starten die Analyse\n- Verfolgen den Fortschritt in Echtzeit\n- Automatische Öffnung der Ergebnisdatei\n\nERWEITERTE KONFIGURATIONSOPTIONEN\n----------------------------------\n\nRegex-Filterung (Standardmodus)\n- Suche nach Textmustern mit voller Regex-Unterstützung\n- Unterstützung für komplexe Suchmuster\n- Wortgrenzen, Groß-/Kleinschreibung, Spezialzeichen\n\nNumerische Filterung\n- Filtern nach Zahlenwerten (größer/kleiner als)\n- Bereichsfilterung (zwischen Werten)\n- Spaltenübergreifende numerische Suche\n\nSpaltenbasierte Filterung\n- Auswahl spezifischer zu durchsuchender Spalten\n- Automatische Spaltenerkennung aus Excel-Dateien\n- Individuelle Spaltenauswahl für zielgerichtete Suche\n\nKONFIGURATIONSMANAGEMENT\n--------------------------\n• Speichern/Laden von Konfigurationen für wiederkehrende Aufgaben\n• Persönliche Musterbibliothek mit benutzerdefinierten Regex-Mustern\n• Automatische Speicherung von zuletzt verwendeten Dateipfaden\n• Wiederherstellung vorheriger Arbeitssitzungen\n", + + "file_not_found_error": "Fehler: Die Datei {input_file} wurde nicht gefunden", + "error_reading_excel_file": "Fehler beim Lesen der Excel-Datei: {error}", + "no_filter_criteria_specified": "Keine Filterkriterien angegeben - alle Zeilen werden beibehalten", + "no_filters_applied_rows_remain": "Keine Filter angewendet: {rows} Zeilen bleiben", + "filters_applied_list": "Filter angewendet: {filters}", + "filter_results_summary": "Filterergebnisse: {retained:,} Zeilen beibehalten, {removed:,} Zeilen entfernt", + "retention_removal_rates": "Beibehaltungsrate: {retention:.1f}%, Entfernungsrate: {removal:.1f}%", + "regex_pattern_compiled": "Regex-Muster: '{original}' -> Kompiliert als: '{compiled}'", + "regex_filter_searching_columns": "Regex-Filter: Suche in bestimmten Spalten: {columns}", + "regex_filter_searching_all_columns": "Regex-Filter: Suche in allen Spalten: {columns}", + "regex_match_found": "Zeile {row}: Regex-Übereinstimmung in Spalte '{column}' mit Wert '{value}'", + "regex_filter_results": "Regex-Filter: {rows} Zeilen gefunden", + "invalid_regex_pattern": "Ungültiges Regex-Muster: {error}", + "numeric_filter_applied": "Numerischer Filter: {column} {operator} {value}", + "column_does_not_exist": "Spalte '{column}' existiert nicht im DataFrame", + "unknown_operator": "Unbekannter Operator: {operator}", + "numeric_filter_single_column_results": "Numerischer Filter: {matches} von {total} Zeilen erfüllen {column} {operator} {value}", + "sample_filtered_values": "Beispielwerte: {values}", + "numeric_filter_all_columns": "Numerischer Filter auf alle Spalten: {operator} {value}", + "column_matches_found": "Spalte '{column}': {matches} Übereinstimmungen", + "numeric_filter_all_columns_results": "Numerischer Filter (alle Spalten): {matches} von {total} Zeilen erfüllen {operator} {value}", + "writing_selected_columns": "Nur ausgewählte Spalten werden geschrieben: {columns}", + "writing_all_columns": "Alle Spalten werden geschrieben: {columns}", + "output_file_written": "Ausgabedatei geschrieben: {file}", + "output_dimensions": "Ausgabedimensionen: {rows:,} Zeilen × {columns} Spalten", + "output_file_size": "Ausgabedateigröße: {size:.2f} MB", + "compression_larger": "Kompression: +{percent:.1f}% (größer als Original)", + "compression_smaller": "Kompression: {percent:.1f}% (kleiner als Original)", + "no_write_permission": "Fehler: Keine Schreibberechtigung für die Datei {file}", + "error_writing_excel_file": "Fehler beim Schreiben der Excel-Datei: {error}", + "starting_excel_filter_processing": "Excel-Filter-Verarbeitung wird gestartet...", + "excel_filter_processing_completed": "Excel-Filter-Verarbeitung erfolgreich abgeschlossen!", + "processing_statistics": "=== VERARBEITUNGSSTATISTIKEN ===", + "processing_time": "Verarbeitungszeit: {time:.2f} Sekunden", + "file_statistics": "Dateistatistiken:", + "input_file_size": " Eingabedatei: {size:.2f} MB", + "output_file_size": " Ausgabedatei: {size:.2f} MB", + "compression_rate": " Kompressionsrate: {rate:+.1f}%", + "data_dimensions": "Datendimensionen:", + "input_dimensions": " Eingabe: {rows:,} Zeilen × {columns} Spalten", + "output_dimensions": " Ausgabe: {rows:,} Zeilen × {columns} Spalten", + "filter_results": "Filterergebnisse:", + "applied_filters": " Angewendete Filter: {filters}", + "rows_retained": " Zeilen beibehalten: {rows:,} ({rate:.1f}%)", + "rows_removed": " Zeilen entfernt: {rows:,} ({rate:.1f}%)", + "performance_metrics": "Leistungsmetriken:", + "memory_usage": " Speicherverbrauch: {size:.2f} MB", + "processing_speed": " Verarbeitungsgeschwindigkeit: {speed:.0f} Zeilen/Sekunde", + "end_statistics": "=== ENDE STATISTIKEN ===", + + "error_file_not_found": "Datei nicht gefunden: {error}", + "error_permission": "Berechtigungsfehler: {error}", + "error_empty_excel": "Leere Excel-Datei oder ungültiges Format: {error}", + "error_parser": "Excel-Datei kann nicht geparst werden: {error}", + "error_invalid_regex": "Ungültiges Regex-Muster: {error}", + "error_invalid_input": "Ungültige Eingabe oder Konfiguration: {error}", + "error_unexpected": "Unerwarteter Fehler: {type}: {error}", + + "input_file_loaded": "Eingabedatei geladen: {rows} Zeilen × {columns} Spalten", + "file_size_info": "Dateigröße: {size:.2f} MB", + "memory_usage_info": "Speicherverbrauch: {size:.2f} MB", + + "ready_to_execute": "Bereit zur Ausführung...", + "status_ready": "Bereit", + "command_to_execute": "Auszuführender Befehl", + "execute_button": "AUSFÜHREN", + "activity_log": "Aktivitätsprotokoll", + "log_cleared": "Protokoll gelöscht", + "log_saved": "Protokoll gespeichert: {file}", + "error_saving_log": "Fehler beim Speichern des Protokolls: {error}", + "execution_started": "Ausführung gestartet", + "execution_running": "Läuft...", + "waiting": "WARTEN...", + "ready_for_execution": "Excel Filter bereit zur Ausführung", + "configure_and_execute": "Konfigurieren Sie die Einstellungen und klicken Sie auf 'AUSFÜHREN'", + "error_main_gui_not_connected": "Fehler: Haupt-GUI nicht verbunden", + "input_file_label": "Eingabedatei:", + "output_file_label": "Ausgabedatei:", + "search_pattern_label": "Suchmuster:", + "worksheet_label": "Arbeitsblatt:", + "columns_label": "Spalten:", + "not_selected": "(nicht ausgewählt)", + "not_specified": "(nicht angegeben)", + "more_columns": "(+{count} weitere)", + "numeric_filter_label": "Numerischer Filter: {column} {operator} {value}", + "error_updating_command_display": "Fehler beim Aktualisieren der Befehlsanzeige: {error}", + "clear_log": "Protokoll löschen", + "save_log": "Protokoll speichern", + "save_log_title": "Protokoll speichern", + "log_header": "Excel Filter Protokoll - {timestamp}\n{'=' * 50}\n\n", + "execution_completed": "Ausführung erfolgreich abgeschlossen", + "execution_failed": "Ausführung mit Fehlern beendet", + "execution_finished": "Ausführung beendet", + + "input_file_selected": "Eingabedatei ausgewählt: {file}", + "output_file_selected": "Ausgabedatei ausgewählt: {file}", + "sheet_selection_updated": "Arbeitsblattauswahl aktualisiert: {sheets}", + "column_selection_updated": "Spaltenauswahl aktualisiert: {columns}", + "all_columns_selected": "Alle Spalten ausgewählt", + "all_columns_deselected": "Alle Spalten abgewählt", + "config_loaded_success": "Konfiguration erfolgreich geladen", + "config_saved_success": "Konfiguration erfolgreich gespeichert", + "error_loading_config": "Fehler beim Laden der Konfiguration: {error}", + "error_saving_config": "Fehler beim Speichern der Konfiguration: {error}", + "no_config_found": "Keine Konfiguration gefunden", + "select_input_file_title": "Eingabedatei auswählen", + "save_output_file_title": "Ausgabedatei speichern", + "excel_files_filter": "Excel-Dateien", + "all_files_filter": "Alle Dateien", + "input_file_required_error": "Bitte wählen Sie eine Eingabedatei aus", + "output_file_required_error": "Bitte wählen Sie eine Ausgabedatei aus", + "pattern_required_error": "Bitte geben Sie ein Filtermuster ein", + "no_regex_mode_message": "Kein Regex-Muster verwendet - es werden nur die ausgewählten Spalten kopiert", + "warning_no_pattern_no_columns": "Warnung: Kein Regex-Muster und keine Spalten ausgewählt", + "copy_all_data_question": "Kein Regex-Muster und keine Spalten ausgewählt. Möchten Sie alle Daten kopieren?", + "initializing_analysis_engine": "Erweiterte Analyse-Engine wird initialisiert...", + "loading_excel_structure": "Excel-Dateistruktur wird geladen und analysiert...", + "performing_pattern_recognition": "Intelligente Mustererkennung wird durchgeführt...", + "applying_advanced_filters": "Erweiterte Filteralgorithmen werden angewendet...", + "calculating_performance_metrics": "Leistungsmetriken und Statistiken werden berechnet...", + "optimizing_output_file": "Ausgabedatei wird optimiert und komprimiert...", + "creating_detailed_reports": "Detaillierte Verarbeitungsberichte werden erstellt...", + "analysis_completed_successfully": "Excel-Filter-Analyse erfolgreich abgeschlossen!", + "analysis_failed": "Analyse fehlgeschlagen", + "advanced_analysis_running": "Erweiterte Excel-Analyse läuft...", + "advanced_analysis_completed": "Erweiterte Analyse erfolgreich abgeschlossen", + "advanced_analysis_failed": "Erweiterte Analyse fehlgeschlagen", + "analysis_engine_error": "Analyse-Engine-Fehler", + "critical_error_analysis_engine": "Kritischer Fehler in der Analyse-Engine", + "file_statistics_header": "DATEI-STATISTIKEN:", + "data_processing_header": "DATENVERARBEITUNG:", + "applied_filters_header": "ANGEWENDETE FILTER:", + "performance_metrics_header": "LEISTUNGSMETRIKEN:", + "auto_opening_output_file": "Ausgabedatei wird automatisch geöffnet...", + "opening_file_error": "Fehler beim Öffnen der Datei: {error}", + "file_saved_at": "Datei gespeichert unter: {path}", + "file_size_mb": "{size:.2f} MB", + "compression_rate_pct": "{rate:+.1f}%", + "rows_reduced": "{count:,} Zeilen reduziert ({rate:.1f}%)", + "filter_efficiency": "Filter-Effizienz: {rate:.1f}% Zeilen beibehalten", + "processing_time_sec": "{time:.2f} Sekunden", + "memory_usage_mb": "{size:.2f} MB", + "processing_speed_rows_per_sec": "{speed:.0f} Zeilen/Sekunde", + "file_size_mb_template": "{size:.2f} MB", + "compression_rate_template": "{rate:+.1f}%", + "rows_count_template": "{count:,} Zeilen", + "percentage_template": "{rate:.1f}%", + "time_seconds_template": "{time:.2f} Sekunden", + "speed_template": "{speed:.0f} Zeilen/Sekunde", + "memory_mb_template": "{size:.2f} MB" +} diff --git a/excel_filter/locales/en.json b/excel_filter/locales/en.json new file mode 100644 index 0000000..cab943a --- /dev/null +++ b/excel_filter/locales/en.json @@ -0,0 +1,173 @@ +{ + "app_title": "Excel Filter Tool", + "tab_config": "Configuration", + "tab_execution": "Execution", + "tab_regex_builder": "Regex Builder", + "tab_help": "Help", + "config_section": "Configuration", + "load_button": "Load", + "save_button": "Save", + "input_file": "Input file:", + "output_file": "Output file:", + "browse_button": "Browse...", + "worksheet": "Worksheet:", + "process_button": "🚀 PROCESS", + "status_ready": "Ready", + "success": "Success", + "error": "Error", + "language": "Language:", + "english": "English", + "german": "German", + "help_content": "\nExcel Filter Tool\n\nFUNCTION\n--------------------------\nThe Excel Filter Tool is a data analysis and filtering tool for Excel files.\nIt enables automatic extraction, filtering, and transformation of data based on configurable search criteria.\n\nMain features:\n• Intelligent text search using regex patterns\n• Numeric filtering (greater/less than, between values)\n• Column-based filtering\n\nWORKFLOW\n----------------------------------------\nThe workflow is divided into four main phases, each with its own tabs:\n\n1. CONFIGURATION\n- Select the input Excel file\n- Specify the output file\n- Select the worksheet to filter\n- Configure filter criteria (regex and/or numeric)\n- Select columns to be included\n- Save/load configurations for recurring tasks\n\n2. PATTERN CREATION (Regex-Builder Tab)\n- Use the visual regex builder for easy pattern creation\n- Select from predefined components (text, numbers, special characters)\n- Define quantities (once, multiple times, optional)\n- Add optional anchors and groups\n- Test patterns with sample texts\n- Manage stored patterns\n\n3. EXECUTION\n- Review the command to execute before processing\n- Start the analysis\n- Track progress in real time\n- Automatic opening of the result file\n\nADVANCED CONFIGURATION OPTIONS\n----------------------------------\n\nRegex Filtering (Standard Mode)\n- Search for text patterns with full regex support\n- Support for complex search patterns\n- Word boundaries, case sensitivity, special characters\n\nNumeric Filtering\n- Filter by numeric values (greater/less than)\n- Range filtering (between values)\n- Cross-column numeric search\n\nColumn-Based Filtering\n- Selection of specific columns to search\n- Automatic column detection from Excel files\n- Individual column selection for targeted search\n\nCONFIGURATION MANAGEMENT\n--------------------------\n• Save/load configurations for recurring tasks\n• Personal pattern library with custom regex patterns\n• Automatic saving of recently used file paths\n• Restoration of previous work sessions\n", + + "file_not_found_error": "Error: The file {input_file} was not found", + "error_reading_excel_file": "Error reading the Excel file: {error}", + "no_filter_criteria_specified": "No filter criteria specified - all rows will be retained", + "no_filters_applied_rows_remain": "No filters applied: {rows} rows remain", + "filters_applied_list": "Filters applied: {filters}", + "filter_results_summary": "Filter results: {retained:,} rows retained, {removed:,} rows removed", + "retention_removal_rates": "Retention Rate: {retention:.1f}%, Removal Rate: {removal:.1f}%", + "regex_pattern_compiled": "Regex pattern: '{original}' -> Compiled as: '{compiled}'", + "regex_filter_searching_columns": "Regex filter: Searching specific columns: {columns}", + "regex_filter_searching_all_columns": "Regex filter: Searching all columns: {columns}", + "regex_match_found": "Row {row}: Regex match in column '{column}' with value '{value}'", + "regex_filter_results": "Regex filter: {rows} rows found", + "invalid_regex_pattern": "Invalid regex pattern: {error}", + "numeric_filter_applied": "Numeric filter: {column} {operator} {value}", + "column_does_not_exist": "Column '{column}' does not exist in the DataFrame", + "unknown_operator": "Unknown operator: {operator}", + "numeric_filter_single_column_results": "Numeric filter: {matches} of {total} rows meet {column} {operator} {value}", + "sample_filtered_values": "Sample values: {values}", + "numeric_filter_all_columns": "Numeric filter on all columns: {operator} {value}", + "column_matches_found": "Column '{column}': {matches} matches", + "numeric_filter_all_columns_results": "Numeric filter (all columns): {matches} of {total} rows meet {operator} {value}", + "writing_selected_columns": "Writing only selected columns: {columns}", + "writing_all_columns": "Writing all columns: {columns}", + "output_file_written": "Output file written: {file}", + "output_dimensions": "Output dimensions: {rows:,} rows × {columns} columns", + "output_file_size": "Output file size: {size:.2f} MB", + "compression_larger": "Compression: +{percent:.1f}% (larger than original)", + "compression_smaller": "Compression: {percent:.1f}% (smaller than original)", + "no_write_permission": "Error: No write permission for the file {file}", + "error_writing_excel_file": "Error writing the Excel file: {error}", + "starting_excel_filter_processing": "Starting Excel filter processing...", + "excel_filter_processing_completed": "Excel filter processing completed successfully!", + "processing_statistics": "=== PROCESSING STATISTICS ===", + "processing_time": "Processing time: {time:.2f} seconds", + "file_statistics": "File statistics:", + "input_file_size": " Input file: {size:.2f} MB", + "output_file_size": " Output file: {size:.2f} MB", + "compression_rate": " Compression rate: {rate:+.1f}%", + "data_dimensions": "Data dimensions:", + "input_dimensions": " Input: {rows:,} rows × {columns} columns", + "output_dimensions": " Output: {rows:,} rows × {columns} columns", + "filter_results": "Filter results:", + "applied_filters": " Applied filters: {filters}", + "rows_retained": " Rows retained: {rows:,} ({rate:.1f}%)", + "rows_removed": " Rows removed: {rows:,} ({rate:.1f}%)", + "performance_metrics": "Performance metrics:", + "memory_usage": " Memory usage: {size:.2f} MB", + "processing_speed": " Processing speed: {speed:.0f} rows/second", + "end_statistics": "=== END STATISTICS ===", + + "error_file_not_found": "File not found: {error}", + "error_permission": "Permission error: {error}", + "error_empty_excel": "Empty Excel file or invalid format: {error}", + "error_parser": "Excel file cannot be parsed: {error}", + "error_invalid_regex": "Invalid regex pattern: {error}", + "error_invalid_input": "Invalid input or configuration: {error}", + "error_unexpected": "Unexpected error: {type}: {error}", + + "input_file_loaded": "Input file loaded: {rows} rows × {columns} columns", + "file_size_info": "File size: {size:.2f} MB", + "memory_usage_info": "Memory usage: {size:.2f} MB", + + "ready_to_execute": "Ready to execute...", + "status_ready": "Ready", + "command_to_execute": "Command to Execute", + "execute_button": "EXECUTE", + "activity_log": "Activity Log", + "log_cleared": "Log cleared", + "log_saved": "Log saved: {file}", + "error_saving_log": "Error saving log: {error}", + "execution_started": "Execution started", + "execution_running": "Running...", + "waiting": "WAITING...", + "ready_for_execution": "Excel Filter ready for execution", + "configure_and_execute": "Configure settings and click 'EXECUTE'", + "error_main_gui_not_connected": "Error: Main GUI not connected", + "input_file_label": "Input file:", + "output_file_label": "Output file:", + "search_pattern_label": "Search pattern:", + "worksheet_label": "Worksheet:", + "columns_label": "Columns:", + "not_selected": "(not selected)", + "not_specified": "(not specified)", + "more_columns": "(+{count} more)", + "numeric_filter_label": "Numeric Filter: {column} {operator} {value}", + "error_updating_command_display": "Error updating command display: {error}", + "clear_log": "Clear Log", + "save_log": "Save Log", + "save_log_title": "Save Log", + "log_header": "Excel Filter Log - {timestamp}\n{'=' * 50}\n\n", + "execution_completed": "Execution completed successfully", + "execution_failed": "Execution completed with errors", + "execution_finished": "Execution finished", + + "input_file_selected": "Input file selected: {file}", + "output_file_selected": "Output file selected: {file}", + "sheet_selection_updated": "Sheet selection updated: {sheets}", + "column_selection_updated": "Column selection updated: {columns}", + "all_columns_selected": "All columns selected", + "all_columns_deselected": "All columns deselected", + "config_loaded_success": "Configuration successfully loaded", + "config_saved_success": "Configuration successfully saved", + "error_loading_config": "Error loading configuration: {error}", + "error_saving_config": "Error saving configuration: {error}", + "no_config_found": "No configuration found", + "select_input_file_title": "Select input file", + "save_output_file_title": "Save output file", + "excel_files_filter": "Excel Files", + "all_files_filter": "All Files", + "input_file_required_error": "Please select an input file", + "output_file_required_error": "Please select an output file", + "pattern_required_error": "Please enter a filter pattern", + "no_regex_mode_message": "No regex pattern used - only selected columns will be copied", + "warning_no_pattern_no_columns": "Warning: No regex pattern and no columns selected", + "copy_all_data_question": "No regex pattern and no columns selected. Copy all data?", + "initializing_analysis_engine": "Initializing advanced analysis engine...", + "loading_excel_structure": "Loading and analyzing Excel file structure...", + "performing_pattern_recognition": "Performing intelligent pattern recognition...", + "applying_advanced_filters": "Applying advanced filter algorithms...", + "calculating_performance_metrics": "Calculating performance metrics and statistics...", + "optimizing_output_file": "Optimizing and compressing output file...", + "creating_detailed_reports": "Creating detailed processing reports...", + "analysis_completed_successfully": "Excel-filter analysis completed successfully!", + "analysis_failed": "Analysis failed", + "advanced_analysis_running": "Advanced Excel analysis running...", + "advanced_analysis_completed": "Advanced analysis successfully completed", + "advanced_analysis_failed": "Advanced analysis failed", + "analysis_engine_error": "Analysis engine error", + "critical_error_analysis_engine": "Critical error in analysis engine", + "file_statistics_header": "FILE STATISTICS:", + "data_processing_header": "DATA PROCESSING:", + "applied_filters_header": "APPLIED FILTERS:", + "performance_metrics_header": "PERFORMANCE METRICS:", + "auto_opening_output_file": "Output file will be automatically opened...", + "opening_file_error": "Error opening file: {error}", + "file_saved_at": "File saved at: {path}", + "file_size_mb": "{size:.2f} MB", + "compression_rate_pct": "{rate:+.1f}%", + "rows_reduced": "{count:,} rows reduced ({rate:.1f}%)", + "filter_efficiency": "Filter efficiency: {rate:.1f}% rows retained", + "processing_time_sec": "{time:.2f} seconds", + "memory_usage_mb": "{size:.2f} MB", + "processing_speed_rows_per_sec": "{speed:.0f} rows/second", + "file_size_mb_template": "{size:.2f} MB", + "compression_rate_template": "{rate:+.1f}%", + "rows_count_template": "{count:,} rows", + "percentage_template": "{rate:.1f}%", + "time_seconds_template": "{time:.2f} seconds", + "speed_template": "{speed:.0f} rows/second", + "memory_mb_template": "{size:.2f} MB" +} diff --git a/excel_filter/main.py b/excel_filter/main.py new file mode 100644 index 0000000..52cde40 --- /dev/null +++ b/excel_filter/main.py @@ -0,0 +1,75 @@ +""" +Main module for command line functionality +""" + +import argparse +import json +import logging +from filter import ExcelFilter + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def load_config(config_file: str) -> dict: + """ + Loads a configuration file + """ + try: + with open(config_file, 'r') as f: + config = json.load(f) + logger.info(f"Configuration loaded: {config_file}") + return config + except Exception as e: + logger.error(f"Error loading configuration: {e}") + raise + + +def main(): + """ + Main function for command line application + """ + parser = argparse.ArgumentParser(description='Excel Filter Tool') + parser.add_argument('--input', required=True, help='Input file (Excel)') + parser.add_argument('--output', required=True, help='Output file (Excel)') + parser.add_argument('--pattern', help='Regex pattern for filtering') + parser.add_argument('--config', help='Configuration file (JSON)') + parser.add_argument('--sheet', help='Name of the worksheet') + parser.add_argument('--columns', nargs='+', help='Columns to search') + + args = parser.parse_args() + + # Load configuration or use command line arguments + if args.config: + config = load_config(args.config) + pattern = config.get('pattern', args.pattern) + sheet_name = config.get('sheet_name', args.sheet) + columns = config.get('columns', args.columns) + else: + pattern = args.pattern + sheet_name = args.sheet + columns = args.columns + + if not pattern: + logger.error("No regex pattern specified") + return + + # Create ExcelFilter instance and execute + excel_filter = ExcelFilter( + input_file=args.input, + output_file=args.output, + pattern=pattern, + sheet_name=sheet_name, + columns=columns + ) + + success = excel_filter.process() + if success: + logger.info("Processing completed successfully") + else: + logger.error("Processing failed") + + +if __name__ == '__main__': + main() diff --git a/excel_filter/presets.json b/excel_filter/presets.json new file mode 100644 index 0000000..fad1660 --- /dev/null +++ b/excel_filter/presets.json @@ -0,0 +1,20 @@ +{ + "presets": { + "Fehler und Warnungen": "error|warning|critical", + "Nur Fehler": "error", + "Nur Warnungen": "warning", + "Zahlen 100-199": "1\\d{2}", + "E-Mail-Adressen": "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", + "Telefonnummern": "\\+?[0-9\\s-]{10,}", + "Datum (YYYY-MM-DD)": "\\d{4}-\\d{2}-\\d{2}" + }, + "descriptions": { + "Fehler und Warnungen": "Zeigt entweder \"error\", \"warning\" oder \"critical\"", + "Nur Fehler": "Findet Zeilen mit 'error'", + "Nur Warnungen": "Findet Zeilen mit 'warning'", + "Zahlen 100-199": "Findet Zahlen zwischen 100 und 199", + "E-Mail-Adressen": "Findet E-Mail-Adressen", + "Telefonnummern": "Findet Telefonnummern", + "Datum (YYYY-MM-DD)": "Findet Daten im Format YYYY-MM-DD" + } +} \ No newline at end of file diff --git a/excel_filter/pytest.ini b/excel_filter/pytest.ini new file mode 100644 index 0000000..2c9bb22 --- /dev/null +++ b/excel_filter/pytest.ini @@ -0,0 +1,13 @@ +[tool:pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + -v + --tb=short + --strict-markers +markers = + slow: marks tests as slow (deselect with '-m "not slow"') + integration: marks tests as integration tests + unit: marks tests as unit tests diff --git a/excel_filter/requirements.txt b/excel_filter/requirements.txt new file mode 100644 index 0000000..c5bf5cb --- /dev/null +++ b/excel_filter/requirements.txt @@ -0,0 +1,5 @@ +openpyxl>=3.1.2 +pandas>=2.0.3 +python-docx>=1.1.0 +pytest>=8.0.0 +psutil>=5.8.0 diff --git a/excel_filter/run_pyinstaller.bat b/excel_filter/run_pyinstaller.bat new file mode 100644 index 0000000..2fc4a44 --- /dev/null +++ b/excel_filter/run_pyinstaller.bat @@ -0,0 +1,10 @@ +@echo off +setlocal enabledelayedexpansion + +cd /d "%~dp0" + +echo Building the executable... +python -m PyInstaller --onefile --windowed --name=ExcelFilter --distpath=dist --icon=app_icon.ico gui_new.py --add-data=config.json;. --add-data=presets.json;. --add-data=app_icon.ico;. --add-data=locales;locales + +echo Executable built successfully! +pause diff --git a/excel_filter/run_pyinstaller_simple.bat b/excel_filter/run_pyinstaller_simple.bat new file mode 100644 index 0000000..d847468 --- /dev/null +++ b/excel_filter/run_pyinstaller_simple.bat @@ -0,0 +1,10 @@ +@echo off +setlocal enabledelayedexpansion + +cd /d "%~dp0" + +echo Building the executable... +python -m PyInstaller --windowed --name=ExcelFilter --distpath=dist --icon=app_icon.ico gui_new.py + +echo Executable built successfully! +pause \ No newline at end of file diff --git a/excel_filter/test_iscc.bat b/excel_filter/test_iscc.bat new file mode 100644 index 0000000..e4d7556 --- /dev/null +++ b/excel_filter/test_iscc.bat @@ -0,0 +1,9 @@ +off +echo +This +is +a +mock +ISCC.exe +exit +0 diff --git a/excel_filter/tests/__init__.py b/excel_filter/tests/__init__.py new file mode 100644 index 0000000..d4839a6 --- /dev/null +++ b/excel_filter/tests/__init__.py @@ -0,0 +1 @@ +# Tests package diff --git a/excel_filter/tests/debug_import.py b/excel_filter/tests/debug_import.py new file mode 100644 index 0000000..0c7ef8b --- /dev/null +++ b/excel_filter/tests/debug_import.py @@ -0,0 +1,26 @@ +import sys +import os + +# Add the src directory to the path +sys.path.append(os.path.join(os.path.dirname(__file__), 'excel_filter', 'src')) + +print("Python path:", sys.path) +print("Current directory:", os.getcwd()) + +try: + from excel_filter.gui_components.config_tab import ConfigTab + print("SUCCESS: ConfigTab imported") + + # Try to instantiate + import tkinter as tk + root = tk.Tk() + + config_tab = ConfigTab(root, {}, {}, {}) + print("SUCCESS: ConfigTab instantiated") + + root.destroy() + +except Exception as e: + print(f"ERROR: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/excel_filter/tests/generate_test_data.py b/excel_filter/tests/generate_test_data.py new file mode 100644 index 0000000..a80a768 --- /dev/null +++ b/excel_filter/tests/generate_test_data.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +""" +Skript zum Generieren von Testdaten für die Excel-Filter-Tests +""" + +import pandas as pd + +# Testdaten erstellen +test_data = { + 'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank', 'Grace', 'Hank'], + 'Status': ['Active', 'Inactive', 'Active', 'Inactive', 'Active', 'Inactive', 'Active', 'Inactive'], + 'Message': [ + 'Hello World', + 'Error occurred', + 'Warning: Low disk space', + 'Critical failure', + 'System running normally', + 'Error: Database connection failed', + 'Warning: High CPU usage', + 'Info: System update available' + ], + 'Value': [100, 200, 150, 50, 300, 120, 180, 90], + 'Description': [ + 'Normal operation', + 'Error in module X', + 'Disk space warning', + 'Critical system error', + 'All systems operational', + 'Database error', + 'CPU warning', + 'Update information' + ] +} + +# DataFrame erstellen +df = pd.DataFrame(test_data) + +# In Excel-Datei speichern +df.to_excel('tests/test_data.xlsx', index=False) + +print("Testdaten erfolgreich generiert!") +print(df) diff --git a/excel_filter/tests/simple_test.py b/excel_filter/tests/simple_test.py new file mode 100644 index 0000000..3a8e20d --- /dev/null +++ b/excel_filter/tests/simple_test.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +""" +Simple test to verify the config_tab.py fixes +""" + +import sys +sys.path.append('src') + +from excel_filter.gui_components.config_tab import ConfigTab + +def test_methods(): + """Test that the methods are properly implemented""" + print("Testing ConfigTab methods...") + + # Test that the class can be instantiated + try: + # Create a minimal mock frame + import tkinter as tk + root = tk.Tk() + + win11_colors = {'primary': '#0078d4', 'light': '#f0f0f0', 'dark': '#e0e0e0', 'accent': '#005a9e'} + pattern_presets = {"test": "test"} + pattern_descriptions = {"test": "test"} + + config_tab = ConfigTab(root, win11_colors, pattern_presets, pattern_descriptions) + + # Test that methods exist and are callable + assert hasattr(config_tab, 'select_all_columns'), "select_all_columns method missing" + assert hasattr(config_tab, 'deselect_all_columns'), "deselect_all_columns method missing" + assert hasattr(config_tab, 'process_file'), "process_file method missing" + assert hasattr(config_tab, 'save_config'), "save_config method missing" + assert hasattr(config_tab, 'load_config'), "load_config method missing" + assert hasattr(config_tab, 'browse_input_file'), "browse_input_file method missing" + assert hasattr(config_tab, 'browse_output_file'), "browse_output_file method missing" + assert hasattr(config_tab, 'update_columns_selection'), "update_columns_selection method missing" + + # Test that methods are callable + config_tab.select_all_columns() + config_tab.deselect_all_columns() + config_tab.process_file() + config_tab.save_config() + config_tab.load_config() + config_tab.browse_input_file() + config_tab.browse_output_file() + config_tab.update_columns_selection() + + print("[SUCCESS] All methods are properly implemented and callable!") + + # Test with some data + config_tab.input_file_var.set("test_input.xlsx") + config_tab.output_file_var.set("test_output.xlsx") + config_tab.pattern_var.set("error|warning") + + # This should work now (previously would have done nothing) + config_tab.process_file() + + print("[SUCCESS] Process file method works with valid data!") + + root.destroy() + return True + + except Exception as e: + print(f"[ERROR] Error: {e}") + return False + +if __name__ == "__main__": + success = test_methods() + if success: + print("\n[INFO] All tests passed! The config_tab.py fixes are working correctly.") + else: + print("\n[ERROR] Some tests failed. Please check the implementation.") diff --git a/excel_filter/tests/test_batch.bat b/excel_filter/tests/test_batch.bat new file mode 100644 index 0000000..4c3c2e0 --- /dev/null +++ b/excel_filter/tests/test_batch.bat @@ -0,0 +1,29 @@ +@echo off +:: Test Batchdatei zur Fehlerdiagnose + +echo Test 1: Einfache if-Bedingung +if "%ERRORLEVEL%"=="0" ( + echo Test 1 erfolgreich +) else ( + echo Test 1 fehlgeschlagen +) + +echo. +echo Test 2: if not-Bedingung +set TESTVAR=0 +if not "%TESTVAR%"=="0" ( + echo Test 2 erfolgreich +) else ( + echo Test 2 fehlgeschlagen +) + +echo. +echo Test 3: if exist-Bedingung +if exist "test_batch.bat" ( + echo Test 3 erfolgreich - Datei existiert +) else ( + echo Test 3 fehlgeschlagen - Datei existiert nicht +) + +echo. +pause \ No newline at end of file diff --git a/excel_filter/tests/test_column_update.py b/excel_filter/tests/test_column_update.py new file mode 100644 index 0000000..878127b --- /dev/null +++ b/excel_filter/tests/test_column_update.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +""" +Test script to verify column selection updates +""" + +import sys +import os +sys.path.append('src') + +from excel_filter.gui_new import ExcelFilterGUI +import tkinter as tk + +def test_column_update(): + """Test that column selection updates are properly reflected in command display""" + + # Create main window + root = tk.Tk() + root.withdraw() # Hide the main window for testing + + try: + # Create the GUI + app = ExcelFilterGUI(root) + + print("Testing column selection updates...") + + # Set up test data + app.config_tab.input_file_var.set("test_input.xlsx") + app.config_tab.output_file_var.set("test_output.xlsx") + app.config_tab.pattern_var.set("test_pattern") + app.config_tab.sheet_var.set("Sheet1") + + # Mock some column variables + app.config_tab.columns_vars = { + 'Column1': tk.IntVar(value=1), + 'Column2': tk.IntVar(value=0), + 'Column3': tk.IntVar(value=1) + } + + # Set up the connections (this should have been done in create_tabs) + app.config_tab.set_execution_tab(app.execution_tab) + app.config_tab.set_on_columns_changed(lambda: app.execution_tab.update_command_display()) + + # Add tracing for individual column variables + for var in app.config_tab.columns_vars.values(): + var.trace_add("write", lambda *args: app.execution_tab.update_command_display()) + + print("Initial command display:") + initial_command = app.execution_tab.command_var.get() + print(f"Command: {initial_command}") + + # Change a column selection + print("\nChanging Column2 selection...") + app.config_tab.columns_vars['Column2'].set(1) + root.update() # Process the trace updates + + updated_command = app.execution_tab.command_var.get() + print(f"Updated command: {updated_command}") + + # Check if columns appear in command + columns_in_command = "Spalten:" in updated_command + print(f"Columns appear in command: {columns_in_command}") + + if columns_in_command: + print("[SUCCESS] Column selection update works!") + return True + else: + print("[ERROR] Column selection update failed!") + return False + + except Exception as e: + print(f"Test failed with error: {e}") + import traceback + traceback.print_exc() + return False + + finally: + root.destroy() + +if __name__ == "__main__": + success = test_column_update() + sys.exit(0 if success else 1) diff --git a/excel_filter/tests/test_complete_implementation.py b/excel_filter/tests/test_complete_implementation.py new file mode 100644 index 0000000..4021574 --- /dev/null +++ b/excel_filter/tests/test_complete_implementation.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +""" +Complete test to verify all the implemented functionality in config_tab.py +""" + +import sys +sys.path.append('excel_filter/src') + +from excel_filter.gui_components.config_tab import ConfigTab + +def test_complete_implementation(): + """Test all the implemented methods""" + print("Testing complete implementation of ConfigTab...") + + try: + # Create a minimal mock frame + import tkinter as tk + root = tk.Tk() + + win11_colors = {'primary': '#0078d4', 'light': '#f0f0f0', 'dark': '#e0e0e0', 'accent': '#005a9e'} + pattern_presets = {"test": "test"} + pattern_descriptions = {"test": "test"} + + config_tab = ConfigTab(root, win11_colors, pattern_presets, pattern_descriptions) + + print("[SUCCESS] ConfigTab instantiated successfully") + + # Test file browsing methods + print("Testing browse methods...") + # These will open dialogs, but we can at least verify they don't crash + try: + config_tab.browse_input_file() + print("[SUCCESS] browse_input_file() works") + except Exception as e: + print(f"[ERROR] browse_input_file() failed: {e}") + + try: + config_tab.browse_output_file() + print("[SUCCESS] browse_output_file() works") + except Exception as e: + print(f"[ERROR] browse_output_file() failed: {e}") + + # Test column selection methods + print("Testing column selection methods...") + config_tab.select_all_columns() + print("[SUCCESS] select_all_columns() works") + + config_tab.deselect_all_columns() + print("[SUCCESS] deselect_all_columns() works") + + # Test config methods + print("Testing config methods...") + config_tab.save_config() + print("[SUCCESS] save_config() works") + + config_tab.load_config() + print("[SUCCESS] load_config() works") + + # Test process file with valid data + print("Testing process_file with valid data...") + config_tab.input_file_var.set("test_input.xlsx") + config_tab.output_file_var.set("test_output.xlsx") + config_tab.pattern_var.set("error|warning") + config_tab.process_file() + print("[SUCCESS] process_file() works with valid data") + + # Test update methods (these might fail without actual files, but shouldn't crash) + print("Testing update methods...") + try: + config_tab.update_sheet_selection() + print("[SUCCESS] update_sheet_selection() works") + except Exception as e: + print(f"[WARNING] update_sheet_selection() failed (expected without file): {e}") + + try: + config_tab.update_columns_selection() + print("[SUCCESS] update_columns_selection() works") + except Exception as e: + print(f"[WARNING] update_columns_selection() failed (expected without file): {e}") + + print("\n[INFO] All methods are properly implemented and functional!") + + root.destroy() + return True + + except Exception as e: + print(f"[ERROR]: {e}") + import traceback + traceback.print_exc() + return False + +if __name__ == "__main__": + success = test_complete_implementation() + if success: + print("\n[INFO] All tests passed! The config_tab.py is fully functional.") + else: + print("\n[ERROR] Some tests failed. Please check the implementation.") diff --git a/excel_filter/tests/test_filter.py b/excel_filter/tests/test_filter.py new file mode 100644 index 0000000..191217e --- /dev/null +++ b/excel_filter/tests/test_filter.py @@ -0,0 +1,159 @@ +""" +Test module for ExcelFilter +""" + +import os +import sys +import pandas as pd + +# Add src directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from excel_filter.filter import ExcelFilter + + +class TestExcelFilter: + """ + Test class for ExcelFilter + """ + + def setup_method(self): + """ + Setup for the tests + """ + import tempfile + import os + + # Create temporary files + self.temp_dir = tempfile.mkdtemp() + self.test_input = os.path.join(self.temp_dir, "test_data.xlsx") + self.test_output = os.path.join(self.temp_dir, "test_output.xlsx") + + # Create test data + test_data = { + 'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'], + 'Status': ['Active', 'Inactive', 'Active', 'Inactive', 'Active'], + 'Message': ['Hello World', 'Error occurred', 'Warning: Low disk space', 'Critical failure', 'System running normally'], + 'Value': [100, 200, 150, 50, 300] + } + + df = pd.DataFrame(test_data) + df.to_excel(self.test_input, index=False) + + def teardown_method(self): + """ + Cleanup after the tests + """ + import shutil + if os.path.exists(self.test_output): + os.remove(self.test_output) + if os.path.exists(self.test_input): + os.remove(self.test_input) + if hasattr(self, 'temp_dir') and os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + + def test_filter_error_pattern(self): + """ + Test filtering with an error pattern + """ + pattern = "error|warning|critical" + excel_filter = ExcelFilter( + input_file=self.test_input, + output_file=self.test_output, + pattern=pattern + ) + + success = excel_filter.process() + assert success + + # Verify output file + result_df = pd.read_excel(self.test_output) + assert len(result_df) == 3 # Bob, Charlie, David + + def test_filter_specific_column(self): + """ + Test filtering in a specific column + """ + pattern = "active" + excel_filter = ExcelFilter( + input_file=self.test_input, + output_file=self.test_output, + pattern=pattern, + columns=['Status'] + ) + + success = excel_filter.process() + assert success + + # Verify output file + result_df = pd.read_excel(self.test_output) + assert len(result_df) == 3 # Alice, Charlie, Eve + + def test_filter_value_pattern(self): + """ + Test filtering with a numeric pattern + """ + pattern = "1\d{2}" # Numbers between 100-199 + excel_filter = ExcelFilter( + input_file=self.test_input, + output_file=self.test_output, + pattern=pattern, + columns=['Value'] + ) + + success = excel_filter.process() + assert success + + # Verify output file + result_df = pd.read_excel(self.test_output) + assert len(result_df) == 2 # Alice (100), Charlie (150) + + def test_filter_no_matches(self): + """ + Test filtering with a pattern that finds no matches + """ + pattern = "nonexistent" + excel_filter = ExcelFilter( + input_file=self.test_input, + output_file=self.test_output, + pattern=pattern + ) + + success = excel_filter.process() + assert success + + # Verify output file + result_df = pd.read_excel(self.test_output) + assert len(result_df) == 0 # No matches + + def test_filter_invalid_file(self): + """ + Test filtering with an invalid file + """ + pattern = "error" + excel_filter = ExcelFilter( + input_file="nonexistent.xlsx", + output_file=self.test_output, + pattern=pattern + ) + + success = excel_filter.process() + assert not success + + def test_filter_empty_pattern(self): + """ + Test filtering with an empty pattern + """ + pattern = "" + excel_filter = ExcelFilter( + input_file=self.test_input, + output_file=self.test_output, + pattern=pattern + ) + + success = excel_filter.process() + assert success + + # Verify output file + result_df = pd.read_excel(self.test_output) + assert len(result_df) == len(pd.read_excel(self.test_input)) # All rows diff --git a/excel_filter/tests/test_final_implementation.py b/excel_filter/tests/test_final_implementation.py new file mode 100644 index 0000000..6f73a95 --- /dev/null +++ b/excel_filter/tests/test_final_implementation.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +Final test to verify the complete implementation including ExcelFilter integration +""" + +import sys +sys.path.append('excel_filter/src') + +def test_final_implementation(): + """Test the complete implementation""" + print("Testing final implementation...") + + try: + # Test import + from excel_filter.gui_components.config_tab import ConfigTab + print("[SUCCESS] ConfigTab imported successfully") + + # Test that all required methods exist + required_methods = [ + 'browse_input_file', 'browse_output_file', + 'select_all_columns', 'deselect_all_columns', + 'get_selected_columns', 'update_columns_selection', + 'update_sheet_selection', 'save_config', 'load_config', + 'process_file', 'open_file' + ] + + print("Checking required methods:") + for method in required_methods: + if hasattr(ConfigTab, method): + print(f"[SUCCESS] {method}() - EXISTS") + else: + print(f"[ERROR] {method}() - MISSING") + return False + + # Test that ExcelFilter can be imported (this tests the integration) + try: + from excel_filter.filter import ExcelFilter + print("[SUCCESS] ExcelFilter integration available") + except ImportError as e: + print(f"[WARNING] ExcelFilter import warning: {e}") + print(" (This is expected if running in isolation)") + + # Test instantiation + import tkinter as tk + root = tk.Tk() + + win11_colors = {'primary': '#0078d4', 'light': '#f0f0f0', 'dark': '#e0e0e0', 'accent': '#005a9e'} + pattern_presets = {"test": "test"} + pattern_descriptions = {"test": "test"} + + config_tab = ConfigTab(root, win11_colors, pattern_presets, pattern_descriptions) + print("[SUCCESS] ConfigTab instantiated successfully") + + # Test method functionality + print("Testing method functionality:") + + # Test column selection + config_tab.select_all_columns() + print(" [SUCCESS] select_all_columns() works") + + config_tab.deselect_all_columns() + print(" [SUCCESS] deselect_all_columns() works") + + # Test get_selected_columns + selected = config_tab.get_selected_columns() + print(f" [SUCCESS] get_selected_columns() returns: {selected}") + + # Test config methods + config_tab.save_config() + print(" [SUCCESS] save_config() works") + + config_tab.load_config() + print(" [SUCCESS] load_config() works") + + # Test process_file with mock data + config_tab.input_file_var.set("test_input.xlsx") + config_tab.output_file_var.set("test_output.xlsx") + config_tab.pattern_var.set("error|warning") + + # This should now work with proper validation + try: + config_tab.process_file() + print(" [SUCCESS] process_file() works (will fail without real files, but validates correctly)") + except Exception as e: + print(f" [WARNING] process_file() failed as expected: {e}") + + root.destroy() + + print("\n[INFO] All tests passed! The config_tab.py is now fully implemented with ExcelFilter integration.") + return True + + except Exception as e: + print(f"[ERROR]: {e}") + import traceback + traceback.print_exc() + return False + +if __name__ == "__main__": + success = test_final_implementation() + if success: + print("\n[INFO] Complete implementation verified successfully!") + else: + print("\n[ERROR] Implementation verification failed.") diff --git a/excel_filter/tests/test_gui.py b/excel_filter/tests/test_gui.py new file mode 100644 index 0000000..5d482b3 --- /dev/null +++ b/excel_filter/tests/test_gui.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'src')) + +import tkinter as tk +from excel_filter.gui_new import ExcelFilterGUI + +def test_gui_initialization(): + """Test GUI initialization without running the main loop""" + print("Testing GUI initialization...") + + # Create a test window + root = tk.Tk() + root.title("GUI Test") + root.geometry("800x600") + + try: + # Initialize the GUI + app = ExcelFilterGUI(root) + print("ExcelFilterGUI initialized successfully") + + # Test that main components exist + assert hasattr(app, 'root'), "Root window not set" + assert hasattr(app, 'main_window'), "Main window not set" + assert hasattr(app, 'config_tab'), "Config tab not set" + assert hasattr(app, 'execution_tab'), "Execution tab not set" + assert hasattr(app, 'regex_builder_tab'), "Regex builder tab not set" + + print("All main GUI components initialized successfully") + print("GUI initialization test passed") + + except Exception as e: + print(f"GUI initialization failed: {e}") + raise + finally: + # Clean up: destroy the root window without running mainloop + root.destroy() + +if __name__ == "__main__": + test_gui_initialization() diff --git a/excel_filter/tests/test_gui_new_comprehensive.py b/excel_filter/tests/test_gui_new_comprehensive.py new file mode 100644 index 0000000..95e3a92 --- /dev/null +++ b/excel_filter/tests/test_gui_new_comprehensive.py @@ -0,0 +1,727 @@ +#!/usr/bin/env python3 + + +import sys +import os +import tempfile +import json +import pytest +from unittest.mock import Mock, patch, MagicMock +from pathlib import Path + +# Add the parent directory to the path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +import tkinter as tk +from gui_new import ExcelFilterGUI + + +@pytest.fixture +def mock_root(): + """Create a mock tkinter root window""" + root = Mock(spec=tk.Tk) + root.title = Mock() + root.configure = Mock() + root.iconbitmap = Mock() + root.destroy = Mock() + root.update = Mock() + return root + + +@pytest.fixture +def temp_files(): + """Create temporary files for testing""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create a temporary config file + config_file = os.path.join(temp_dir, 'config.json') + with open(config_file, 'w') as f: + json.dump({ + 'input_file': 'test_input.xlsx', + 'output_file': 'test_output.xlsx', + 'pattern': 'error|warning', + 'sheet_name': 'Sheet1', + 'columns': ['Column1', 'Column2'] + }, f) + + # Create a temporary presets file + presets_file = os.path.join(temp_dir, 'presets.json') + with open(presets_file, 'w') as f: + json.dump({ + 'presets': {'test_pattern': 'test.*'}, + 'descriptions': {'test_pattern': 'Test pattern'} + }, f) + + yield temp_dir, config_file, presets_file + + +class TestExcelFilterGUI: + + def test_update_columns_selection_signature(self): + """Test that update_columns_selection method accepts selected_columns parameter""" + # Test the method signature directly without full GUI initialization + import inspect + method = getattr(ExcelFilterGUI, 'update_columns_selection') + sig = inspect.signature(method) + + # Check if 'selected_columns' parameter exists + assert 'selected_columns' in sig.parameters + param = sig.parameters['selected_columns'] + assert param.default is None + + def test_color_palette(self): + """Test that Windows 11 color palette is properly defined""" + # Test colors without full initialization + expected_colors = ['primary', 'primary_dark', 'primary_light', 'background', + 'surface', 'text', 'text_secondary', 'border', 'hover', 'pressed'] + + # Check that all expected colors are defined + for color in expected_colors: + assert hasattr(ExcelFilterGUI, 'win11_colors') or color in ExcelFilterGUI().win11_colors + + def test_file_paths(self): + """Test that file paths are correctly set""" + gui = ExcelFilterGUI.__new__(ExcelFilterGUI) # Create instance without __init__ + gui.__init__ = Mock() # Mock __init__ to avoid full initialization + + # Manually set attributes for testing + gui.config_file = "config.json" + gui.presets_file = "presets.json" + + assert gui.config_file == "config.json" + assert gui.presets_file == "presets.json" + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + def test_create_tabs(self, mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root): + """Test tab creation""" + # Setup mocks similar to initialization test + mock_main_window_instance = Mock() + mock_main_window_instance.notebook = Mock() + mock_main_window_instance.scale_factor = 1.0 + mock_main_window_instance.language_frame = Mock() + mock_main_window_instance.enhance_tab_appearance = Mock() + mock_main_window.return_value = mock_main_window_instance + + mock_config_tab_instance = Mock() + mock_config_tab_instance.get_frame.return_value = Mock() + mock_config_tab_instance.set_execution_tab = Mock() + mock_config_tab_instance.set_main_gui = Mock() + mock_config_tab_instance.set_on_columns_changed = Mock() + mock_config_tab.return_value = mock_config_tab_instance + + mock_execution_tab_instance = Mock() + mock_execution_tab_instance.get_frame.return_value = Mock() + mock_execution_tab_instance.set_config_tab = Mock() + mock_execution_tab_instance.set_main_gui = Mock() + mock_execution_tab_instance.update_command_display = Mock() + mock_execution_tab.return_value = mock_execution_tab_instance + + mock_regex_tab_instance = Mock() + mock_regex_tab_instance.get_frame.return_value = Mock() + mock_regex_tab.return_value = mock_regex_tab_instance + + mock_help_tab_instance = Mock() + mock_help_tab_instance.get_frame.return_value = Mock() + mock_help_tab.return_value = mock_help_tab_instance + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + gui = ExcelFilterGUI(mock_root) + + # Verify tabs were added to notebook + assert mock_main_window_instance.notebook.add.call_count == 4 + + # Verify config tab connections + assert mock_config_tab_instance.browse_input_file is not None + assert mock_config_tab_instance.browse_output_file is not None + assert mock_config_tab_instance.update_columns_selection is not None + assert mock_config_tab_instance.select_all_columns is not None + assert mock_config_tab_instance.deselect_all_columns is not None + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + def test_update_columns_selection_no_file(self, mock_help_tab, mock_regex_tab, + mock_execution_tab, mock_config_tab, + mock_main_window, mock_root): + """Test update_columns_selection with no input file""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + with patch('tkinter.messagebox.showerror') as mock_error: + gui = ExcelFilterGUI(mock_root) + + # Set empty input file + gui.config_tab.input_file_var.get.return_value = "" + + # Call update_columns_selection + gui.update_columns_selection() + + # Verify error message was shown + mock_error.assert_called_once() + args = mock_error.call_args[0] + assert "Bitte geben Sie eine Eingabedatei an" in args[1] + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + @patch('pandas.read_excel') + def test_update_columns_selection_with_file(self, mock_read_excel, mock_help_tab, + mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root): + """Test update_columns_selection with valid input file""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + # Mock pandas DataFrame + mock_df = Mock() + mock_df.columns.tolist.return_value = ['Column1', 'Column2', 'Column3'] + mock_read_excel.return_value = mock_df + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + with patch('tkinter.ttk.Checkbutton') as mock_checkbutton: + gui = ExcelFilterGUI(mock_root) + + # Set valid input file + gui.config_tab.input_file_var.get.return_value = "test.xlsx" + gui.config_tab.sheet_var.get.return_value = "Sheet1" + + # Mock container operations + mock_container = Mock() + gui.config_tab.columns_container = mock_container + mock_container.winfo_children.return_value = [] + mock_container.grid = Mock() + mock_container.update_idletasks = Mock() + + # Call update_columns_selection + gui.update_columns_selection() + + # Verify pandas was called + mock_read_excel.assert_called_once_with("test.xlsx", sheet_name="Sheet1") + + # Verify checkboxes were created (3 columns) + assert mock_checkbutton.call_count == 3 + + # Verify regex column combobox was updated + gui.config_tab.regex_column_combobox.__setitem__.assert_called_with( + 'values', ['Alle Spalten', 'Column1', 'Column2', 'Column3'] + ) + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + def test_update_columns_selection_with_selected_columns(self, mock_help_tab, mock_regex_tab, + mock_execution_tab, mock_config_tab, + mock_main_window, mock_root): + """Test update_columns_selection with pre-selected columns""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + with patch('pandas.read_excel') as mock_read_excel: + mock_df = Mock() + mock_df.columns.tolist.return_value = ['Column1', 'Column2', 'Column3'] + mock_read_excel.return_value = mock_df + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + with patch('tkinter.ttk.Checkbutton') as mock_checkbutton: + gui = ExcelFilterGUI(mock_root) + + # Set valid input file + gui.config_tab.input_file_var.get.return_value = "test.xlsx" + gui.config_tab.sheet_var.get.return_value = "Sheet1" + + # Mock container + mock_container = Mock() + gui.config_tab.columns_container = mock_container + mock_container.winfo_children.return_value = [] + mock_container.grid = Mock() + mock_container.update_idletasks = Mock() + + # Create mock variables for columns + mock_vars = {} + for i, col in enumerate(['Column1', 'Column2', 'Column3']): + mock_var = Mock() + mock_vars[col] = mock_var + gui.config_tab.columns_vars = mock_vars + + # Call with selected columns + selected_columns = ['Column1', 'Column3'] + gui.update_columns_selection(selected_columns=selected_columns) + + # Verify that selected columns were set + mock_vars['Column1'].set.assert_called_with(1) + mock_vars['Column3'].set.assert_called_with(1) + # Column2 should not be set (not in selected_columns) + mock_vars['Column2'].set.assert_not_called() + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + def test_select_all_columns(self, mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root): + """Test select_all_columns method""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + gui = ExcelFilterGUI(mock_root) + + # Setup mock column variables + mock_var1 = Mock() + mock_var2 = Mock() + gui.config_tab.columns_vars = { + 'Column1': mock_var1, + 'Column2': mock_var2 + } + + # Call select_all_columns + gui.select_all_columns() + + # Verify all variables were set to 1 + mock_var1.set.assert_called_with(1) + mock_var2.set.assert_called_with(1) + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + def test_deselect_all_columns(self, mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root): + """Test deselect_all_columns method""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + gui = ExcelFilterGUI(mock_root) + + # Setup mock column variables + mock_var1 = Mock() + mock_var2 = Mock() + gui.config_tab.columns_vars = { + 'Column1': mock_var1, + 'Column2': mock_var2 + } + + # Call deselect_all_columns + gui.deselect_all_columns() + + # Verify all variables were set to 0 + mock_var1.set.assert_called_with(0) + mock_var2.set.assert_called_with(0) + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + @patch('gui_new.browse_file') + def test_browse_input_file(self, mock_browse_file, mock_help_tab, mock_regex_tab, + mock_execution_tab, mock_config_tab, mock_main_window, mock_root): + """Test browse_input_file method""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + mock_browse_file.return_value = "/path/to/test.xlsx" + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + gui = ExcelFilterGUI(mock_root) + + # Mock the methods that will be called + gui.update_sheet_selection = Mock() + gui.update_columns_selection = Mock() + + # Call browse_input_file + gui.browse_input_file() + + # Verify file was set + gui.config_tab.input_file_var.set.assert_called_with("/path/to/test.xlsx") + + # Verify update methods were called + gui.update_sheet_selection.assert_called_once() + gui.update_columns_selection.assert_called_once() + + # Verify output file was auto-filled + expected_output = "/path/to/test_filtered.xlsx" + gui.config_tab.output_file_var.set.assert_called_with(expected_output) + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + @patch('gui_new.browse_save_file') + def test_browse_output_file(self, mock_browse_save_file, mock_help_tab, mock_regex_tab, + mock_execution_tab, mock_config_tab, mock_main_window, mock_root): + """Test browse_output_file method""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + mock_browse_save_file.return_value = "/path/to/output.xlsx" + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + gui = ExcelFilterGUI(mock_root) + + # Call browse_output_file + gui.browse_output_file() + + # Verify file was set + gui.config_tab.output_file_var.set.assert_called_with("/path/to/output.xlsx") + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + @patch('gui_new.load_config') + def test_load_config(self, mock_load_config, mock_help_tab, mock_regex_tab, + mock_execution_tab, mock_config_tab, mock_main_window, mock_root): + """Test load_config method""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + mock_load_config.return_value = { + 'input_file': 'test.xlsx', + 'output_file': 'output.xlsx', + 'pattern': 'error.*', + 'sheet_name': 'Sheet1', + 'columns': ['Col1', 'Col2'] + } + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + with patch('tkinter.messagebox.showinfo') as mock_info: + with patch('os.path.exists', return_value=True): + gui = ExcelFilterGUI(mock_root) + + # Mock update methods + gui.update_sheet_selection = Mock() + gui.update_columns_selection = Mock() + + # Call load_config + gui.load_config() + + # Verify config values were set + gui.config_tab.input_file_var.set.assert_called_with('test.xlsx') + gui.config_tab.output_file_var.set.assert_called_with('output.xlsx') + gui.config_tab.pattern_var.set.assert_called_with('error.*') + gui.config_tab.sheet_var.set.assert_called_with('Sheet1') + + # Verify update methods were called + gui.update_sheet_selection.assert_called_once() + gui.update_columns_selection.assert_called_once_with(selected_columns=['Col1', 'Col2']) + + # Verify success message + mock_info.assert_called_once() + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + @patch('gui_new.save_config') + def test_save_config(self, mock_save_config, mock_help_tab, mock_regex_tab, + mock_execution_tab, mock_config_tab, mock_main_window, mock_root): + """Test save_config method""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + with patch('tkinter.messagebox.showinfo') as mock_info: + gui = ExcelFilterGUI(mock_root) + + # Setup mock return values + gui.config_tab.input_file_var.get.return_value = 'input.xlsx' + gui.config_tab.output_file_var.get.return_value = 'output.xlsx' + gui.config_tab.pattern_var.get.return_value = 'pattern.*' + gui.config_tab.sheet_var.get.return_value = 'Sheet1' + gui.config_tab.columns_var.get.return_value = 'columns' + + # Call save_config + gui.save_config() + + # Verify save_config was called with correct data + expected_config = { + 'input_file': 'input.xlsx', + 'output_file': 'output.xlsx', + 'pattern': 'pattern.*', + 'sheet_name': 'Sheet1', + 'columns': 'columns' + } + mock_save_config.assert_called_once_with("config.json", expected_config) + + # Verify success message + mock_info.assert_called_once() + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + def test_load_presets(self, mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root): + """Test load_presets method""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Default Pattern') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + with patch('os.path.exists', return_value=True): + with patch('gui_new.save_presets') as mock_save_presets: + gui = ExcelFilterGUI(mock_root) + + # Call load_presets + presets, descriptions = gui.load_presets() + + # Verify presets were loaded + assert isinstance(presets, dict) + assert isinstance(descriptions, dict) + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + def test_save_presets(self, mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root): + """Test save_presets method""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + with patch('gui_new.open', Mock()) as mock_open: + with patch('json.dump') as mock_dump: + gui = ExcelFilterGUI(mock_root) + + # Set up test presets + gui.pattern_presets = {'test': 'pattern'} + gui.pattern_descriptions = {'test': 'description'} + + # Call save_presets + gui.save_presets() + + # Verify json.dump was called + mock_dump.assert_called_once() + args = mock_dump.call_args[0] + assert args[0]['presets'] == {'test': 'pattern'} + assert args[0]['descriptions'] == {'test': 'description'} + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + def test_open_file_windows(self, mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root): + """Test open_file method on Windows""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + with patch('sys.platform', 'win32'): + with patch('os.startfile') as mock_startfile: + gui = ExcelFilterGUI(mock_root) + + # Call open_file + gui.open_file('test.xlsx') + + # Verify os.startfile was called + mock_startfile.assert_called_once_with('test.xlsx') + + @patch('gui_new.MainWindow') + @patch('gui_new.ConfigTab') + @patch('gui_new.ExecutionTab') + @patch('gui_new.RegexBuilderTab') + @patch('gui_new.HelpTab') + def test_open_file_unix(self, mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root): + """Test open_file method on Unix-like systems""" + # Setup basic mocks + self._setup_basic_mocks(mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root) + + with patch('gui_new.Translations') as mock_translations: + mock_translations_instance = Mock() + mock_translations_instance.__getitem__ = Mock(return_value='Test') + mock_translations_instance.app_title = 'Test App' + mock_translations.return_value = mock_translations_instance + + with patch('builtins.open', Mock()): + with patch('json.load', return_value={'presets': {}, 'descriptions': {}}): + with patch('sys.platform', 'linux'): + with patch('subprocess.run') as mock_run: + gui = ExcelFilterGUI(mock_root) + + # Call open_file + gui.open_file('test.xlsx') + + # Verify subprocess.run was called with xdg-open + mock_run.assert_called_once_with(['xdg-open', 'test.xlsx']) + + def _setup_basic_mocks(self, mock_help_tab, mock_regex_tab, mock_execution_tab, + mock_config_tab, mock_main_window, mock_root): + """Helper method to setup basic mocks for tests""" + # Setup MainWindow mock + mock_main_window_instance = Mock() + mock_main_window_instance.notebook = Mock() + mock_main_window_instance.scale_factor = 1.0 + mock_main_window_instance.language_frame = Mock() + mock_main_window_instance.enhance_tab_appearance = Mock() + mock_main_window.return_value = mock_main_window_instance + + # Setup ConfigTab mock + mock_config_tab_instance = Mock() + mock_config_frame = Mock() + mock_config_frame.winfo_children.return_value = [] # Make it iterable + mock_config_tab_instance.frame = mock_config_frame + mock_config_tab_instance.get_frame.return_value = mock_config_frame + mock_config_tab_instance.set_execution_tab = Mock() + mock_config_tab_instance.set_main_gui = Mock() + mock_config_tab_instance.set_on_columns_changed = Mock() + mock_config_tab_instance.input_file_var = Mock() + mock_config_tab_instance.output_file_var = Mock() + mock_config_tab_instance.pattern_var = Mock() + mock_config_tab_instance.sheet_var = Mock() + mock_config_tab_instance.columns_var = Mock() + mock_config_tab_instance.regex_column_combobox = Mock() + mock_config_tab_instance.columns_container = Mock() + mock_config_tab_instance.columns_vars = {} + mock_config_tab_instance.log_message = Mock() + mock_config_tab_instance.log_error = Mock() + mock_config_tab_instance.status_var = Mock() + mock_config_tab.return_value = mock_config_tab_instance + + # Setup ExecutionTab mock + mock_execution_tab_instance = Mock() + mock_execution_tab_instance.get_frame.return_value = Mock() + mock_execution_tab_instance.set_config_tab = Mock() + mock_execution_tab_instance.set_main_gui = Mock() + mock_execution_tab_instance.update_command_display = Mock() + mock_execution_tab.return_value = mock_execution_tab_instance + + # Setup RegexBuilderTab mock + mock_regex_tab_instance = Mock() + mock_regex_tab_instance.get_frame.return_value = Mock() + mock_regex_tab.return_value = mock_regex_tab_instance + + # Setup HelpTab mock + mock_help_tab_instance = Mock() + mock_help_tab_instance.get_frame.return_value = Mock() + mock_help_tab.return_value = mock_help_tab_instance + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/excel_filter/tests/test_gui_new_simple.py b/excel_filter/tests/test_gui_new_simple.py new file mode 100644 index 0000000..8171cd4 --- /dev/null +++ b/excel_filter/tests/test_gui_new_simple.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +""" +Simple focused tests for gui_new.py - ExcelFilterGUI class +Tests the key functionality that was fixed +""" + +import sys +import os +import pytest +from unittest.mock import Mock, patch + +# Add the parent directory to the path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from gui_new import ExcelFilterGUI + + +class TestExcelFilterGUISimple: + """Simple focused tests for ExcelFilterGUI""" + + def test_update_columns_selection_method_signature(self): + """Test that update_columns_selection method accepts selected_columns parameter""" + import inspect + + # Get the method signature + method = getattr(ExcelFilterGUI, 'update_columns_selection') + sig = inspect.signature(method) + + # Verify 'selected_columns' parameter exists with correct default + assert 'selected_columns' in sig.parameters + param = sig.parameters['selected_columns'] + assert param.default is None + + print("SUCCESS: update_columns_selection method signature is correct") + + def test_class_attributes_exist(self): + """Test that key class attributes can be accessed""" + # Test that we can access class attributes without instantiation + assert hasattr(ExcelFilterGUI, 'update_columns_selection') + assert hasattr(ExcelFilterGUI, 'select_all_columns') + assert hasattr(ExcelFilterGUI, 'deselect_all_columns') + assert hasattr(ExcelFilterGUI, 'load_config') + assert hasattr(ExcelFilterGUI, 'save_config') + + print("SUCCESS: All key methods exist on the class") + + def test_win11_colors_attribute(self): + """Test that win11_colors is properly defined""" + # Create a minimal instance to test colors (avoiding full initialization) + gui = ExcelFilterGUI.__new__(ExcelFilterGUI) + gui.win11_colors = { + 'primary': '#0078d4', + 'primary_dark': '#005a9e', + 'primary_light': '#cce4f7', + 'background': '#f0f0f0', + 'surface': '#ffffff', + 'text': '#212121', + 'text_secondary': '#616161', + 'border': '#e0e0e0', + 'hover': '#e8f0fe', + 'pressed': '#d0e0f5', + 'success': '#107c10', + 'warning': '#c55c00', + 'error': '#c42b1c' + } + + # Check that it's a dictionary with expected keys + colors = gui.win11_colors + assert isinstance(colors, dict) + expected_keys = ['primary', 'background', 'surface', 'text', 'border'] + for key in expected_keys: + assert key in colors + + print("SUCCESS: Windows 11 color palette is properly defined") + + def test_method_can_be_called_with_selected_columns(self): + """Test that the method can be called with selected_columns parameter""" + # Create a minimal mock instance to test method calling + gui = ExcelFilterGUI.__new__(ExcelFilterGUI) + + # Mock the required attributes + gui.config_tab = Mock() + gui.config_tab.input_file_var = Mock() + gui.config_tab.input_file_var.get.return_value = "" # Empty file to avoid full execution + + # This should not raise an exception about unexpected keyword arguments + try: + gui.update_columns_selection(selected_columns=['Col1', 'Col2']) + print("SUCCESS: Method accepts selected_columns parameter without error") + except TypeError as e: + if "unexpected keyword argument" in str(e): + pytest.fail(f"Method still doesn't accept selected_columns: {e}") + else: + # Other TypeErrors are expected (due to mocking), just not the kwarg error + print("SUCCESS: Method accepts selected_columns parameter (other errors expected due to mocking)") + + def test_selected_columns_parameter_restoration_logic(self): + """Test that the selected_columns parameter restoration logic works""" + # Create a minimal mock instance to test the restoration logic + gui = ExcelFilterGUI.__new__(ExcelFilterGUI) + + # Mock the required attributes for the method + gui.config_tab = Mock() + gui.config_tab.input_file_var = Mock() + gui.config_tab.input_file_var.get.return_value = "test.xlsx" + gui.config_tab.sheet_var = Mock() + gui.config_tab.sheet_var.get.return_value = "Sheet1" + gui.config_tab.columns_container = Mock() + gui.config_tab.columns_container.winfo_children.return_value = [] + gui.config_tab.columns_container.grid = Mock() + gui.config_tab.columns_container.update_idletasks = Mock() + gui.config_tab.columns_vars = {} + gui.config_tab.log_message = Mock() + gui.config_tab.log_error = Mock() + + # Mock regex column combobox + gui.config_tab.regex_column_combobox = Mock() + gui.config_tab.regex_column_combobox.__setitem__ = Mock() + + # Mock pandas operations + with patch('pandas.read_excel') as mock_read_excel, \ + patch('tkinter.ttk.Checkbutton') as mock_checkbutton: + + mock_df = Mock() + mock_df.columns.tolist.return_value = ['Column1', 'Column2', 'Column3'] + mock_read_excel.return_value = mock_df + + # Create mock variable for testing selection restoration + mock_var1 = Mock() + mock_var2 = Mock() + mock_var3 = Mock() + + # Simulate the method creating column variables + def side_effect(*args, **kwargs): + column = args[1] if args else kwargs.get('text', '') + if column == 'Column1': + gui.config_tab.columns_vars['Column1'] = mock_var1 + elif column == 'Column2': + gui.config_tab.columns_vars['Column2'] = mock_var2 + elif column == 'Column3': + gui.config_tab.columns_vars['Column3'] = mock_var3 + + mock_checkbutton.side_effect = side_effect + + # Call update_columns_selection with selected_columns + selected_columns = ['Column1', 'Column3'] + gui.update_columns_selection(selected_columns=selected_columns) + + # Verify that selected columns were set to 1 + mock_var1.set.assert_called_with(1) + mock_var3.set.assert_called_with(1) + # Column2 should not be set since it's not in selected_columns + mock_var2.set.assert_not_called() + + print("SUCCESS: selected_columns parameter correctly restores column selections") + + +if __name__ == "__main__": + # Run the tests manually + test_instance = TestExcelFilterGUISimple() + + print("Running simple GUI tests...") + print() + + try: + test_instance.test_update_columns_selection_method_signature() + test_instance.test_class_attributes_exist() + test_instance.test_win11_colors_attribute() + test_instance.test_method_can_be_called_with_selected_columns() + test_instance.test_selected_columns_parameter_restoration_logic() + + print() + print("SUCCESS: All simple tests passed!") + print("The fix for the selected_columns parameter is working correctly.") + + except Exception as e: + print(f"FAIL: Test failed: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/excel_filter/tests/test_large_files.py b/excel_filter/tests/test_large_files.py new file mode 100644 index 0000000..36b7dd0 --- /dev/null +++ b/excel_filter/tests/test_large_files.py @@ -0,0 +1,349 @@ + + +import os +import sys +import pandas as pd +import numpy as np +import tempfile +import time +import gc +import pytest + +# Add src directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from excel_filter.filter import ExcelFilter + + +class TestLargeFileHandling: + """ + Test cases for handling very large Excel files + """ + + def generate_large_test_data(self, num_rows=50000): + """ + Generate a large dataset for testing + """ + np.random.seed(42) # For reproducible results + + # Create diverse data + names = [f"User_{i}" for i in range(num_rows)] + ages = np.random.randint(18, 80, num_rows) + salaries = np.random.uniform(30000, 200000, num_rows).round(2) + departments = np.random.choice(['HR', 'IT', 'Finance', 'Marketing', 'Sales', 'Operations'], num_rows) + statuses = np.random.choice(['Active', 'Inactive', 'Pending', 'Terminated'], num_rows) + cities = np.random.choice(['New York', 'London', 'Tokyo', 'Berlin', 'Paris', 'Sydney', 'Toronto', 'Singapore'], num_rows) + + # Create various message types + messages = [] + for i in range(num_rows): + if i % 100 == 0: + messages.append("ERROR: Critical system failure detected") + elif i % 50 == 0: + messages.append("WARNING: Low disk space on server") + elif i % 25 == 0: + messages.append("INFO: User login successful") + elif i % 10 == 0: + messages.append("DEBUG: Processing completed successfully") + else: + messages.append(f"Normal operation log entry {i}") + + # Create some rows with specific patterns for testing + special_indices = np.random.choice(num_rows, size=int(num_rows * 0.1), replace=False) + for idx in special_indices[:len(special_indices)//3]: + messages[idx] = "CRITICAL: Database connection lost" + for idx in special_indices[len(special_indices)//3:2*len(special_indices)//3]: + messages[idx] = "ALERT: Security breach detected" + for idx in special_indices[2*len(special_indices)//3:]: + messages[idx] = "NOTICE: System maintenance scheduled" + + # Create DataFrame + # For very large datasets, limit the date range to avoid pandas datetime limits + if num_rows > 50000: + # For large datasets, use a repeating date range to avoid datetime overflow + join_dates = [] + base_dates = pd.date_range('2010-01-01', periods=min(50000, num_rows), freq='D') + for i in range(num_rows): + join_dates.append(base_dates[i % len(base_dates)]) + else: + join_dates = pd.date_range('2010-01-01', periods=num_rows, freq='D') + + df = pd.DataFrame({ + 'Name': names, + 'Age': ages, + 'Salary': salaries, + 'Department': departments, + 'Status': statuses, + 'City': cities, + 'Message': messages, + 'Employee_ID': range(100000, 100000 + num_rows), + 'Join_Date': join_dates, + 'Performance_Score': np.random.uniform(1.0, 5.0, num_rows).round(1) + }) + + return df + + def test_large_file_generation_and_basic_filtering(self): + """ + Test basic filtering on a large file (10k rows) + """ + # Generate large test data + df = self.generate_large_test_data(10000) + + with tempfile.TemporaryDirectory() as temp_dir: + input_file = os.path.join(temp_dir, "large_test_data.xlsx") + output_file = os.path.join(temp_dir, "filtered_output.xlsx") + + # Save to Excel + start_time = time.time() + df.to_excel(input_file, index=False) + save_time = time.time() - start_time + print(f"Generated and saved {len(df)} rows in {save_time:.2f} seconds") + + # Test filtering for ERROR messages + pattern = r"ERROR|CRITICAL" + excel_filter = ExcelFilter( + input_file=input_file, + output_file=output_file, + pattern=pattern, + columns=['Message'] + ) + + # Process and measure time + start_time = time.time() + success = excel_filter.process() + process_time = time.time() - start_time + + assert success, "Filtering should succeed" + + # Verify results + result_df = pd.read_excel(output_file) + expected_count = len(df[df['Message'].str.contains(pattern, case=False, na=False)]) + + print(f"Filtered {len(df)} rows to {len(result_df)} rows in {process_time:.2f} seconds") + print(f"Expected {expected_count} matches") + + assert len(result_df) == expected_count, f"Expected {expected_count} rows, got {len(result_df)}" + + # Verify all results contain the pattern + import re + for msg in result_df['Message']: + assert re.search(pattern, str(msg), re.IGNORECASE), f"Message '{msg}' should match pattern '{pattern}'" + + def test_large_file_numeric_filtering(self): + """ + Test numeric filtering on large files + """ + df = self.generate_large_test_data(30000) + + with tempfile.TemporaryDirectory() as temp_dir: + input_file = os.path.join(temp_dir, "large_numeric_test.xlsx") + output_file = os.path.join(temp_dir, "numeric_filtered.xlsx") + + df.to_excel(input_file, index=False) + + # Filter for high salaries (> 150000) + numeric_filter = {'column': 'Salary', 'operator': '>', 'value': 150000} + + excel_filter = ExcelFilter( + input_file=input_file, + output_file=output_file, + numeric_filter=numeric_filter + ) + + success = excel_filter.process() + assert success + + result_df = pd.read_excel(output_file) + expected_count = len(df[df['Salary'] > 150000]) + + assert len(result_df) == expected_count + assert all(result_df['Salary'] > 150000) + + def test_large_file_combined_filtering(self): + """ + Test combined regex and numeric filtering on large files + """ + df = self.generate_large_test_data(40000) + + with tempfile.TemporaryDirectory() as temp_dir: + input_file = os.path.join(temp_dir, "combined_test.xlsx") + output_file = os.path.join(temp_dir, "combined_filtered.xlsx") + + df.to_excel(input_file, index=False) + + # Filter: IT department employees with salary > 100000 + pattern = r"IT" + numeric_filter = {'column': 'Salary', 'operator': '>', 'value': 100000} + + excel_filter = ExcelFilter( + input_file=input_file, + output_file=output_file, + pattern=pattern, + columns=['Department', 'Salary'], + numeric_filter=numeric_filter + ) + + success = excel_filter.process() + assert success + + result_df = pd.read_excel(output_file) + expected_df = df[ + (df['Department'].str.contains(pattern, case=False, na=False)) & + (df['Salary'] > 100000) + ] + + assert len(result_df) == len(expected_df) + + # Verify all results meet both criteria + assert all(result_df['Department'].str.contains(pattern, case=False, na=False)) + assert all(result_df['Salary'] > 100000) + + def test_memory_efficiency_large_file(self): + """ + Test memory efficiency with very large files (100k+ rows) + """ + # Skip this test if we're in a memory-constrained environment + try: + df = self.generate_large_test_data(100000) + except MemoryError: + pytest.skip("Not enough memory for 100k row test") + + with tempfile.TemporaryDirectory() as temp_dir: + input_file = os.path.join(temp_dir, "memory_test.xlsx") + output_file = os.path.join(temp_dir, "memory_filtered.xlsx") + + df.to_excel(input_file, index=False) + + # Simple filter that should match ~10% of rows + pattern = r"ERROR|WARNING|CRITICAL|ALERT" + + excel_filter = ExcelFilter( + input_file=input_file, + output_file=output_file, + pattern=pattern, + columns=['Message'] + ) + + # Monitor memory usage if possible + try: + import psutil + process = psutil.Process(os.getpid()) + initial_memory = process.memory_info().rss / 1024 / 1024 # MB + + success = excel_filter.process() + + final_memory = process.memory_info().rss / 1024 / 1024 # MB + memory_used = final_memory - initial_memory + + assert success + print(f"Memory used: {memory_used:.1f}MB") + + # Should not use excessive memory (arbitrary threshold) + assert memory_used < 2000, f"Used {memory_used:.1f}MB, should be less than 2000MB" + except ImportError: + # psutil not available, just run the filter without memory monitoring + print("psutil not available, skipping memory monitoring") + success = excel_filter.process() + assert success + + result_df = pd.read_excel(output_file) + expected_count = len(df[df['Message'].str.contains(pattern, case=False, na=False)]) + + assert len(result_df) == expected_count + + def test_edge_cases_large_file(self): + """ + Test edge cases with large files + """ + df = self.generate_large_test_data(25000) + + # Add some edge case data + df.loc[0, 'Message'] = "" # Empty string + df.loc[1, 'Message'] = None # NaN value + df.loc[2, 'Salary'] = np.nan # NaN numeric + df.loc[3, 'Message'] = "A" * 10000 # Very long string + + with tempfile.TemporaryDirectory() as temp_dir: + input_file = os.path.join(temp_dir, "edge_case_test.xlsx") + output_file = os.path.join(temp_dir, "edge_case_filtered.xlsx") + + df.to_excel(input_file, index=False) + + # Filter that should handle edge cases gracefully + pattern = r"ERROR" + + excel_filter = ExcelFilter( + input_file=input_file, + output_file=output_file, + pattern=pattern, + columns=['Message'] + ) + + success = excel_filter.process() + assert success + + result_df = pd.read_excel(output_file) + + # Should find ERROR messages but handle edge cases + error_messages = df[df['Message'].str.contains(pattern, case=False, na=False)] + assert len(result_df) == len(error_messages) + + @pytest.mark.slow + def test_very_large_file_stress_test(self): + """ + Stress test with a very large file (marked as slow test) + """ + # This test is marked as slow and may be skipped in regular runs + try: + df = self.generate_large_test_data(200000) # 200k rows + except MemoryError: + pytest.skip("Not enough memory for 200k row stress test") + + with tempfile.TemporaryDirectory() as temp_dir: + input_file = os.path.join(temp_dir, "stress_test.xlsx") + output_file = os.path.join(temp_dir, "stress_filtered.xlsx") + + # Time the save operation + start_time = time.time() + df.to_excel(input_file, index=False) + save_time = time.time() - start_time + + file_size = os.path.getsize(input_file) / 1024 / 1024 # MB + print(f"Created {file_size:.1f}MB test file with {len(df)} rows in {save_time:.2f} seconds") + + # Apply complex filtering + pattern = r"ERROR|WARNING|CRITICAL" + numeric_filter = {'column': 'Age', 'operator': '>', 'value': 50} + + excel_filter = ExcelFilter( + input_file=input_file, + output_file=output_file, + pattern=pattern, + columns=['Message', 'Age'], + numeric_filter=numeric_filter + ) + + start_time = time.time() + success = excel_filter.process() + process_time = time.time() - start_time + + assert success + + result_df = pd.read_excel(output_file) + expected_df = df[ + (df['Message'].str.contains(pattern, case=False, na=False)) & + (df['Age'] > 50) + ] + + print(f"Processed {len(df)} rows in {process_time:.2f} seconds") + print(f"Filtered to {len(result_df)} rows (expected {len(expected_df)})") + + assert len(result_df) == len(expected_df) + + # Verify results are correct + assert all(result_df['Message'].str.contains(pattern, case=False, na=False)) + assert all(result_df['Age'] > 50) + + # Cleanup + gc.collect() diff --git a/excel_filter/tests/test_main.py b/excel_filter/tests/test_main.py new file mode 100644 index 0000000..ee66b0f --- /dev/null +++ b/excel_filter/tests/test_main.py @@ -0,0 +1,104 @@ +""" +Unit tests for main.py +""" + +import unittest +import json +import tempfile +import os +from unittest.mock import patch +from excel_filter.main import load_config, main + + +class TestMain(unittest.TestCase): + """ + Test class for main module functions + """ + + def test_load_config_success(self): + """ + Test successful config loading + """ + config_data = { + "pattern": "test.*", + "sheet_name": "Sheet1", + "columns": ["A", "B"] + } + + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + json.dump(config_data, f) + config_file = f.name + + try: + result = load_config(config_file) + self.assertEqual(result, config_data) + finally: + os.unlink(config_file) + + def test_load_config_file_not_found(self): + """ + Test config loading with non-existent file + """ + with self.assertRaises(Exception): + load_config("nonexistent.json") + + @patch('sys.argv', ['main.py', '--input', 'input.xlsx', '--output', 'output.xlsx', '--pattern', 'test']) + @patch('excel_filter.main.ExcelFilter') + def test_main_with_args(self, mock_filter_class): + """ + Test main function with command line arguments + """ + mock_filter = mock_filter_class.return_value + mock_filter.process.return_value = True + + main() + + mock_filter_class.assert_called_once_with( + input_file='input.xlsx', + output_file='output.xlsx', + pattern='test', + sheet_name=None, + columns=None + ) + mock_filter.process.assert_called_once() + + @patch('sys.argv', ['main.py', '--input', 'input.xlsx', '--output', 'output.xlsx', '--config', 'config.json']) + @patch('excel_filter.main.load_config') + @patch('excel_filter.main.ExcelFilter') + def test_main_with_config(self, mock_filter_class, mock_load_config): + """ + Test main function with config file + """ + mock_load_config.return_value = { + "pattern": "config_pattern", + "sheet_name": "ConfigSheet", + "columns": ["Col1", "Col2"] + } + mock_filter = mock_filter_class.return_value + mock_filter.process.return_value = True + + main() + + mock_load_config.assert_called_once_with('config.json') + mock_filter_class.assert_called_once_with( + input_file='input.xlsx', + output_file='output.xlsx', + pattern='config_pattern', + sheet_name='ConfigSheet', + columns=['Col1', 'Col2'] + ) + mock_filter.process.assert_called_once() + + @patch('sys.argv', ['main.py', '--input', 'input.xlsx', '--output', 'output.xlsx']) + @patch('excel_filter.main.logger') + def test_main_no_pattern(self, mock_logger): + """ + Test main function with no pattern specified + """ + main() + + mock_logger.error.assert_called_once_with("Kein Regex-Muster angegeben") + + +if __name__ == '__main__': + unittest.main() diff --git a/excel_filter/tests/test_numeric_filter.py b/excel_filter/tests/test_numeric_filter.py new file mode 100644 index 0000000..606b403 --- /dev/null +++ b/excel_filter/tests/test_numeric_filter.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python3 + + +import pytest +import pandas as pd +import tempfile +import os +import sys +import unittest +from unittest.mock import Mock, patch + +# Add src directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +# Import the classes to test +from excel_filter.filter import ExcelFilter +from excel_filter.gui_components.config_tab import ConfigTab +from excel_filter.translations import Translations + + +class TestNumericFilter: + """Test cases for numeric filtering functionality""" + + def setup_method(self): + """Set up test fixtures""" + # Create test data + self.test_data = { + 'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'], + 'Age': [25, 30, 35, 40, 45], + 'Salary': [50000.50, 60000.75, 70000.25, 80000.00, 90000.90], + 'Score': [85.5, 92.0, 78.3, 96.7, 88.1], + 'Department': ['HR', 'IT', 'Finance', 'IT', 'HR'] + } + self.df = pd.DataFrame(self.test_data) + + def test_numeric_filter_greater_than(self): + """Test filtering values greater than a threshold""" + numeric_filter = {'column': 'Age', 'operator': '>', 'value': 30} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(self.df) + + expected_ages = [35, 40, 45] # Ages > 30 + assert len(result) == 3 + assert result['Age'].tolist() == expected_ages + + def test_numeric_filter_less_than(self): + """Test filtering values less than a threshold""" + numeric_filter = {'column': 'Salary', 'operator': '<', 'value': 70000.00} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(self.df) + + expected_salaries = [50000.50, 60000.75] # Salaries < 70000 + assert len(result) == 2 + assert result['Salary'].tolist() == expected_salaries + + def test_numeric_filter_greater_equal(self): + """Test filtering values greater than or equal to a threshold""" + numeric_filter = {'column': 'Score', 'operator': '>=', 'value': 88.1} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(self.df) + + expected_scores = [92.0, 96.7, 88.1] # Scores >= 88.1 + assert len(result) == 3 + assert result['Score'].tolist() == expected_scores + + def test_numeric_filter_less_equal(self): + """Test filtering values less than or equal to a threshold""" + numeric_filter = {'column': 'Age', 'operator': '<=', 'value': 30} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(self.df) + + expected_ages = [25, 30] # Ages <= 30 + assert len(result) == 2 + assert result['Age'].tolist() == expected_ages + + def test_numeric_filter_equal(self): + """Test filtering values equal to a specific value""" + numeric_filter = {'column': 'Age', 'operator': '=', 'value': 35} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(self.df) + + assert len(result) == 1 + assert result['Age'].iloc[0] == 35 + assert result['Name'].iloc[0] == 'Charlie' + + def test_numeric_filter_combined_with_regex(self): + """Test combining numeric filter with regex filter""" + numeric_filter = {'column': 'Age', 'operator': '>', 'value': 30} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + pattern=r'HR', # Regex pattern for HR department + numeric_filter=numeric_filter + ) + + result = excel_filter.filter_dataframe(self.df) + + # Should find people in HR department who are older than 30 + # Alice (25, HR) - too young + # Eve (45, HR) - matches both criteria + assert len(result) == 1 + assert result['Name'].iloc[0] == 'Eve' + assert result['Age'].iloc[0] == 45 + assert result['Department'].iloc[0] == 'HR' + + def test_numeric_filter_no_matches(self): + """Test numeric filter that matches no rows""" + numeric_filter = {'column': 'Age', 'operator': '>', 'value': 100} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(self.df) + + assert len(result) == 0 + assert result.shape[0] == 0 + + def test_numeric_filter_invalid_column(self): + """Test numeric filter with non-existent column""" + numeric_filter = {'column': 'NonExistentColumn', 'operator': '>', 'value': 30} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + with pytest.raises(ValueError, match="Spalte 'NonExistentColumn' existiert nicht"): + excel_filter._apply_numeric_filter(self.df) + + def test_numeric_filter_invalid_operator(self): + """Test numeric filter with invalid operator""" + numeric_filter = {'column': 'Age', 'operator': 'invalid', 'value': 30} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + with pytest.raises(ValueError, match="Unbekannter Operator"): + excel_filter._apply_numeric_filter(self.df) + + def test_numeric_filter_non_numeric_values(self): + """Test numeric filter on column with non-numeric values""" + # Add a column with mixed data types + df_mixed = self.df.copy() + df_mixed['Mixed'] = ['text', 25, 30.5, '45', None] + + numeric_filter = {'column': 'Mixed', 'operator': '>', 'value': 25} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(df_mixed) + + # pandas to_numeric with errors='coerce' converts '45' to 45.0 + # So we get both 30.5 and 45.0 as matches for > 25 + assert len(result) == 2 + + # Check that we have the expected numeric values (ignoring non-numeric) + numeric_values = [] + for val in result['Mixed']: + try: + numeric_values.append(float(val)) + except (ValueError, TypeError): + continue # Skip non-numeric values + + assert sorted(numeric_values) == [30.5, 45.0] + + def test_numeric_filter_with_decimals(self): + """Test numeric filter with decimal values""" + numeric_filter = {'column': 'Salary', 'operator': '>', 'value': 65000.50} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(self.df) + + expected_salaries = [70000.25, 80000.00, 90000.90] # Salaries > 65000.50 + assert len(result) == 3 + assert result['Salary'].tolist() == expected_salaries + + def test_numeric_filter_boundary_values(self): + """Test numeric filter with boundary values""" + # Test exactly equal to boundary + numeric_filter = {'column': 'Score', 'operator': '=', 'value': 88.1} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(self.df) + + assert len(result) == 1 + assert result['Score'].iloc[0] == 88.1 + + # Test greater than with boundary + numeric_filter = {'column': 'Score', 'operator': '>', 'value': 88.1} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(self.df) + + expected_scores = [92.0, 96.7] # Scores > 88.1 + assert len(result) == 2 + assert result['Score'].tolist() == expected_scores + + def test_numeric_filter_all_columns(self): + """Test numeric filter applied to all columns""" + # Test filtering across all columns for values > 30 + numeric_filter = {'column': 'Alle Spalten', 'operator': '>', 'value': 30} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(self.df) + + # Should find rows where ANY column has a value > 30 + # All rows have Salary and Score values > 30, so all rows match + assert len(result) == 5 # All rows match + + # Verify all rows are included + names = sorted(result['Name'].tolist()) + assert names == ['Alice', 'Bob', 'Charlie', 'David', 'Eve'] + + def test_numeric_filter_all_columns_less_than(self): + """Test numeric filter applied to all columns with less than""" + # Test filtering across all columns for values < 30 + numeric_filter = {'column': 'Alle Spalten', 'operator': '<', 'value': 30} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(self.df) + + # Should find rows where ANY column has a value < 30 + # Row 0: Age=25 (Age < 30) + # Row 1: Age=30 (Age = 30, not < 30), but Score=92.0? Wait, let me check data + # Actually, looking at the data: Row 0 has Age=25 (< 30), others don't have values < 30 + # Row 4 has Score=88.1 but that's not < 30 + # So only Row 0 should match + assert len(result) == 1 + assert result['Age'].iloc[0] == 25 + assert result['Name'].iloc[0] == 'Alice' + + def test_numeric_filter_all_columns_no_matches(self): + """Test numeric filter on all columns with no matches""" + numeric_filter = {'column': 'Alle Spalten', 'operator': '>', 'value': 100000} + + excel_filter = ExcelFilter( + input_file=None, output_file=None, + numeric_filter=numeric_filter + ) + + result = excel_filter._apply_numeric_filter(self.df) + + assert len(result) == 0 + + +class TestNumericFilterLogic: + """Test cases for numeric filter logic without UI dependencies""" + + def setup_method(self): + """Set up test fixtures""" + self.translations = Translations() + + @pytest.mark.parametrize("display_op,expected_op", [ + ("> (größer als)", ">"), + ("< (kleiner als)", "<"), + (">= (größer/gleich)", ">="), + ("<= (kleiner/gleich)", "<="), + ("= (gleich)", "=") + ]) + def test_numeric_filter_settings_conversion(self, display_op, expected_op): + """Test conversion of UI display values to filter settings""" + # Create operator mapping dict + operator_map = { + "> (größer als)": ">", + "< (kleiner als)": "<", + ">= (größer/gleich)": ">=", + "<= (kleiner/gleich)": "<=", + "= (gleich)": "=" + } + + # Simulate the logic from get_numeric_filter_settings method + enabled = True + column = 'TestColumn' + operator = display_op + value = '123.45' + + # Test the logic + result = ( + None if not enabled + else { + 'column': column, + 'operator': operator_map.get(operator, operator), + 'value': float(value) + } if column and operator and value + else None + ) + + assert result is not None + assert result['operator'] == expected_op + assert result['column'] == 'TestColumn' + assert result['value'] == 123.45 + + +class TestNumericFilterIntegration: + """Integration tests for numeric filtering with full workflow""" + + def setup_method(self): + """Set up test fixtures""" + self.test_data = { + 'Product': ['Widget A', 'Widget B', 'Widget C', 'Widget D', 'Widget E'], + 'Price': [10.99, 25.50, 15.75, 30.00, 8.25], + 'Stock': [100, 250, 75, 300, 50], + 'Rating': [4.2, 4.8, 3.9, 4.9, 3.5], + 'Category': ['Electronics', 'Tools', 'Electronics', 'Tools', 'Accessories'] + } + + def test_full_workflow_regex_and_numeric(self): + """Test complete workflow with both regex and numeric filtering""" + # Create test Excel file + df = pd.DataFrame(self.test_data) + + with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as temp_input: + with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as temp_output: + input_file = temp_input.name + output_file = temp_output.name + + try: + # Save test data to input file + df.to_excel(input_file, index=False) + + # Apply filters: Category contains 'Tool' AND Price > 20 + numeric_filter = {'column': 'Price', 'operator': '>', 'value': 20.0} + + excel_filter = ExcelFilter( + input_file=input_file, + output_file=output_file, + pattern=r'Tool', # Regex for category + numeric_filter=numeric_filter + ) + + success = excel_filter.process() + assert success + + # Load and verify results + result_df = pd.read_excel(output_file) + + # Should find Widget B (Tools, $25.50) and Widget D (Tools, $30.00) + assert len(result_df) == 2 + assert 'Widget B' in result_df['Product'].tolist() + assert 'Widget D' in result_df['Product'].tolist() + + # Verify prices are > 20 + for price in result_df['Price']: + assert price > 20.0 + + finally: + # Clean up + try: + os.unlink(input_file) + os.unlink(output_file) + except: + pass + + def test_numeric_filter_only(self): + """Test numeric filtering without regex""" + df = pd.DataFrame(self.test_data) + + with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as temp_input: + with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as temp_output: + input_file = temp_input.name + output_file = temp_output.name + + try: + # Save test data + df.to_excel(input_file, index=False) + + # Filter: Stock >= 100 + numeric_filter = {'column': 'Stock', 'operator': '>=', 'value': 100} + + excel_filter = ExcelFilter( + input_file=input_file, + output_file=output_file, + numeric_filter=numeric_filter + ) + + success = excel_filter.process() + assert success + + # Verify results + result_df = pd.read_excel(output_file) + + # Should find Widget A (100), Widget B (250), Widget D (300) + assert len(result_df) == 3 + for stock in result_df['Stock']: + assert stock >= 100 + + finally: + try: + os.unlink(input_file) + os.unlink(output_file) + except: + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/excel_filter/tests/test_styling.py b/excel_filter/tests/test_styling.py new file mode 100644 index 0000000..21a6fb5 --- /dev/null +++ b/excel_filter/tests/test_styling.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) + +import tkinter as tk +from tkinter import ttk +from excel_filter.gui_components.main_window import MainWindow + +def test_styling(): + root = tk.Tk() + + # Test the main window creation + try: + main_window = MainWindow(root) + print("MainWindow created successfully") + + # Test color palette + print("Windows 11 color palette loaded") + + # Test styles + style = ttk.Style() + print("Tkinter styles configured") + + # Test window properties + print(f"Window title: {root.title()}") + print(f"Window size: {root.geometry()}") + + print("\nWindows 11 Styling Test Results:") + print("All styling components loaded successfully!") + print("GUI should now look like a Windows 11 application") + + # Close the window without running mainloop + root.destroy() + + except Exception as e: + print(f"Error: {e}") + root.destroy() + +if __name__ == "__main__": + test_styling() \ No newline at end of file diff --git a/excel_filter/tests/test_tabs.py b/excel_filter/tests/test_tabs.py new file mode 100644 index 0000000..288be67 --- /dev/null +++ b/excel_filter/tests/test_tabs.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) + +import tkinter as tk +from tkinter import ttk +from excel_filter.gui_components.main_window import MainWindow + +def test_tab_styling(): + root = tk.Tk() + + try: + main_window = MainWindow(root) + + # Add some test tabs + tab1 = ttk.Frame(main_window.notebook) + tab2 = ttk.Frame(main_window.notebook) + tab3 = ttk.Frame(main_window.notebook) + + main_window.add_tab(tab1, "Configuration") + main_window.add_tab(tab2, "Execution") + main_window.add_tab(tab3, "Help") + + # Enhance tab appearance + main_window.enhance_tab_appearance() + + print("Tab styling test results:") + print("Tabs created successfully") + print("Windows 11 tab styling applied") + print("Tab enhancement completed") + + # Test tab properties + style = ttk.Style() + tab_style = style.configure('TNotebook.Tab') + print(f"Tab font: {tab_style.get('font', 'default')}") + print(f"Tab padding: {tab_style.get('padding', 'default')}") + + # Test notebook properties + notebook_style = style.configure('TNotebook') + print(f"Notebook background: {notebook_style.get('background', 'default')}") + + print("\nWindows 11 Tab Styling:") + print("Selected tabs have white background with blue text") + print("Unselected tabs have light gray background with gray text") + print("Hover effects applied") + print("Proper padding and spacing") + print("Segoe UI font (if available)") + + # Close the window + root.destroy() + + except Exception as e: + print(f"Error: {e}") + root.destroy() + +if __name__ == "__main__": + test_tab_styling() \ No newline at end of file diff --git a/excel_filter/tests/test_utils.py b/excel_filter/tests/test_utils.py new file mode 100644 index 0000000..96f6cee --- /dev/null +++ b/excel_filter/tests/test_utils.py @@ -0,0 +1,157 @@ +""" +Unit tests for utility modules +""" + +import json +import tempfile +import os +import logging +import sys +import pytest +from unittest.mock import patch, MagicMock + +# Add src directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from excel_filter.utils.file_utils import load_config, save_config +from excel_filter.utils.logging_utils import setup_logging, log_message, log_error + + +class TestFileUtils: + """ + Test class for file_utils functions + """ + + def test_load_config_success(self): + """ + Test successful config loading + """ + config_data = { + "pattern": "test.*", + "sheet_name": "Sheet1", + "columns": ["A", "B"] + } + + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + json.dump(config_data, f) + config_file = f.name + + try: + result = load_config(config_file) + assert result == config_data + finally: + os.unlink(config_file) + + def test_load_config_file_not_found(self): + """ + Test config loading with non-existent file returns empty dict + """ + result = load_config("nonexistent.json") + assert result == {} + + def test_load_config_invalid_json(self): + """ + Test config loading with invalid JSON + """ + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + f.write("invalid json") + config_file = f.name + + try: + with pytest.raises(Exception): + load_config(config_file) + finally: + os.unlink(config_file) + + def test_save_config_success(self): + """ + Test successful config saving + """ + config_data = { + "pattern": "test.*", + "sheet_name": "Sheet1" + } + + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + config_file = f.name + + try: + save_config(config_file, config_data) + + # Verify file was saved correctly + with open(config_file, 'r') as f: + saved_data = json.load(f) + assert saved_data == config_data + finally: + os.unlink(config_file) + + def test_save_config_error(self): + """ + Test config saving with error + """ + # Try to save to invalid path + with pytest.raises(Exception): + save_config("/invalid/path/config.json", {"test": "data"}) + + +class TestLoggingUtils: + """ + Test class for logging_utils functions + """ + + def test_setup_logging_without_file(self): + """ + Test setup_logging without log file + """ + # Store original level + original_level = logging.getLogger().getEffectiveLevel() + + setup_logging() + # Verify setup_logging can be called without error + # The exact level may be influenced by pytest or other configurations + logger = logging.getLogger() + assert logger is not None # Just verify logging is accessible + + @patch('logging.FileHandler') + def test_setup_logging_with_file(self, mock_file_handler): + """ + Test setup_logging with log file + """ + setup_logging(log_file="test.log", level=logging.DEBUG) + # Verify FileHandler was created + mock_file_handler.assert_called_once_with("test.log") + + def test_log_message_normal(self): + """ + Test logging normal message + """ + mock_widget = MagicMock() + log_message(mock_widget, "Test message") + + # Verify widget methods were called + mock_widget.config.assert_called() + mock_widget.insert.assert_called() + mock_widget.see.assert_called_with('end') + + def test_log_message_error(self): + """ + Test logging error message + """ + mock_widget = MagicMock() + log_message(mock_widget, "Error message", is_error=True) + + # Verify error formatting + mock_widget.tag_add.assert_called() + mock_widget.tag_config.assert_called_with("error", foreground="red") + + def test_log_error(self): + """ + Test log_error function + """ + mock_widget = MagicMock() + log_error(mock_widget, "Test error") + + # Verify it calls log_message with error flag + mock_widget.config.assert_called() + mock_widget.insert.assert_called() + mock_widget.tag_add.assert_called() diff --git a/excel_filter/translations.py b/excel_filter/translations.py new file mode 100644 index 0000000..5b0d3e0 --- /dev/null +++ b/excel_filter/translations.py @@ -0,0 +1,556 @@ +""" +Translations for the Excel Filter GUI +Supports English and German languages using JSON files for better maintainability +""" + +import json +import os +from pathlib import Path + +class Translations: + """ + Translation manager for the Excel Filter GUI + Loads translations from JSON files for better maintainability and collaboration + """ + + def __init__(self, language="de"): + """ + Initialize translations with default language + + Args: + language: Default language code ('en' or 'de') + """ + self.current_language = language + self.translations = {} + + # Load translations from JSON files + self.load_translations() + + def load_translations(self): + """ + Load translation files from the locales directory + """ + import sys + # Determine the base path for resources + if getattr(sys, 'frozen', False): + # If the application is run as a bundle, the PyInstaller bootloader + # extends the sys module by a flag frozen=True and sets the app + # path into variable _MEIPASS'. + base_path = Path(sys._MEIPASS) + else: + base_path = Path(__file__).parent + + locales_dir = base_path / "locales" + + # Create locales directory if it doesn't exist and we are not frozen + # (cannot create files in _MEIPASS usually, or it's temporary) + if not getattr(sys, 'frozen', False): + locales_dir.mkdir(exist_ok=True) + + # Load each language file + for lang_code in ["en", "de"]: + lang_file = locales_dir / f"{lang_code}.json" + if lang_file.exists(): + try: + with open(lang_file, 'r', encoding='utf-8') as f: + self.translations[lang_code] = json.load(f) + except Exception as e: + print(f"Error loading {lang_file}: {e}") + self.translations[lang_code] = {} + else: + # Create default language file if it doesn't exist + self.create_default_translations(lang_code, lang_file) + + def create_default_translations(self, lang_code, lang_file): + """ + Create default translation files + """ + if lang_code == "en": + translations = self.get_english_translations() + else: + translations = self.get_german_translations() + + self.translations[lang_code] = translations + + # Save to file + try: + with open(lang_file, 'w', encoding='utf-8') as f: + json.dump(translations, f, indent=2, ensure_ascii=False) + except Exception as e: + print(f"Error creating {lang_file}: {e}") + + def get_english_translations(self): + """ + Get default English translations + """ + return { + "app_title": "Excel Filter Tool", + "tab_config": "Configuration", + "tab_execution": "Execution", + "tab_regex_builder": "Regex Builder", + "tab_help": "Help", + "config_section": "Configuration", + "load_button": "Load", + "save_button": "Save", + "input_file": "Input file:", + "output_file": "Output file:", + "browse_button": "Browse...", + "worksheet": "Worksheet:", + "process_button": "🚀 PROCESS", + "status_ready": "Ready", + "success": "Success", + "error": "Error", + "language": "Language:", + "english": "English", + "german": "German", + "help_content": "\nExcel Filter Tool\n\nFUNCTION\n--------------------------\nThe Excel Filter Tool is a data analysis and filtering tool for Excel files.\nIt enables automatic extraction, filtering, and transformation of data based on configurable search criteria.\n\nMain features:\n• Intelligent text search using regex patterns\n• Numeric filtering (greater/less than, between values)\n• Column-based filtering\n\nWORKFLOW\n----------------------------------------\nThe workflow is divided into four main phases, each with its own tabs:\n\n1. CONFIGURATION\n- Select the input Excel file\n- Specify the output file\n- Select the worksheet to filter\n- Configure filter criteria (regex and/or numeric)\n- Select columns to be included\n- Save/load configurations for recurring tasks\n\n2. PATTERN CREATION (Regex-Builder Tab)\n- Use the visual regex builder for easy pattern creation\n- Select from predefined components (text, numbers, special characters)\n- Define quantities (once, multiple times, optional)\n- Add optional anchors and groups\n- Test patterns with sample texts\n- Manage stored patterns\n\n3. EXECUTION\n- Review the command to execute before processing\n- Start the analysis\n- Track progress in real time\n- Automatic opening of the result file\n\nADVANCED CONFIGURATION OPTIONS\n----------------------------------\n\nRegex Filtering (Standard Mode)\n- Search for text patterns with full regex support\n- Support for complex search patterns\n- Word boundaries, case sensitivity, special characters\n\nNumeric Filtering\n- Filter by numeric values (greater/less than)\n- Range filtering (between values)\n- Cross-column numeric search\n\nColumn-Based Filtering\n- Selection of specific columns to search\n- Automatic column detection from Excel files\n- Individual column selection for targeted search\n\nCONFIGURATION MANAGEMENT\n--------------------------\n• Save/load configurations for recurring tasks\n• Personal pattern library with custom regex patterns\n• Automatic saving of recently used file paths\n• Restoration of previous work sessions\n", + + # Config Tab + "tooltip_config_section": "You can save and load configurations instead of entering everything every time.", + "enable_regex": "Enable Regex Filter", + "tooltip_enable_regex": "If enabled, regex patterns are used for filtering", + "regex_options": " Regex Options ", + "regex_pattern": "Regex Pattern:", + "preset_patterns": "Preset Patterns:", + "apply_regex_column": "Apply Regex to Column:", + "all_columns": "All Columns", + "manage_patterns": "Manage Patterns...", + "enable_numeric": "Enable Numeric Filter", + "numeric_filters": "Numeric Filters", + "column": "Column:", + "comparison": "Comparison:", + "value": "Value:", + "regex_builder_info": "Custom patterns for filtering can be created in the Regex Builder.", + "enable_column_selection": "Enable Column Selection", + "column_selection": "Column Selection", + "select_all": "Select All", + "deselect_all": "Deselect All", + "proceed_to_execution": "Proceed to Execution", + "msg_no_regex_copy": "No regex pattern used - only selected columns will be copied", + "msg_warn_no_selection": "Warning: No regex pattern and no columns selected - all data will be copied", + "msg_copy_all": "Copying all columns", + "msg_copy_selected": "Copying selected columns: {}", + "msg_written_success": "Successfully written: {}", + "msg_ask_open": "Do you want to open the output file?", + "msg_process_success": "Processing completed successfully", + "msg_process_failed": "Processing failed", + "numeric_invalid": "❌ Invalid numeric value", + "numeric_incomplete": "Configuration incomplete", + "filter_desc": "Filter: {} {} {}", + + # Execution Tab + "ready_to_execute": "Ready to execute...", + "status_ready": "Ready", + "command_to_execute": "Command to Execute", + "execute_button": "EXECUTE", + "activity_log": "Activity Log", + "clear_log": "Clear", + "save_log": "Save", + "log_cleared": "Log cleared", + "ready_for_execution": "Excel Filter ready for execution", + "configure_and_execute": "Configure settings and click 'EXECUTE'", + "error_main_gui_not_connected": "Error: Main GUI not connected", + "input_file_label": "Input File:", + "output_file_label": "Output File:", + "search_pattern_label": "Search Pattern:", + "worksheet_label": "Worksheet:", + "columns_label": "Columns:", + "numeric_filter_label": "Numeric Filter: {column} {operator} {value}", + "not_selected": "(not selected)", + "not_specified": "(not specified)", + "more_columns": "(+{} more)", + "error_updating_command_display": "Error updating command display: {error}", + "execution_running": "Running...", + "waiting": "WAITING...", + "execution_started": "▶️ Execution started", + "execution_completed": "✅ Execution completed successfully", + "execution_failed": "❌ Execution failed with errors", + "log_saved": "💾 Log saved: {file}", + + # Regex Builder Tab + "tab_builder": "🧱 Block Builder", + "tab_tester": "🧪 Tester", + "tab_examples": "📚 Examples", + "step_1": "📝 Step 1: Search Character", + "step_2": "🔢 Step 2: How often?", + "step_3": "⚙️ Step 3: Options", + "btn_letters": "🔤 Letters", + "desc_letters": "a-z, A-Z", + "btn_digits": "🔢 Digits", + "desc_digits": "0-9", + "btn_alphanum": "🔤🔢 Alphanum.", + "desc_alphanum": "Letters+Digits", + "btn_any": "❓ Any", + "desc_any": "Any Character", + "btn_custom": "📝 Custom Text", + "desc_custom": "Your Text", + "btn_special": "⚙️ Special", + "desc_special": "Punctuation", + "btn_once": "🎯 1-time", + "desc_once": "Exactly once", + "btn_one_plus": "➕ 1+ times", + "desc_one_plus": "At least 1-time", + "btn_zero_one": "❓ 0-1 times", + "desc_zero_one": "Optional", + "btn_zero_plus": "♾️ 0+ times", + "desc_zero_plus": "Any number of times", + "btn_n_times": "📏 N-times", + "desc_n_times": "Exactly N-times", + "btn_m_n_times": "📊 M-N times", + "desc_m_n_times": "M to N-times", + "btn_start": "^ Start", + "desc_start": "Line start", + "btn_end": "$ End", + "desc_end": "Line end", + "btn_word": "\\b Word", + "desc_word": "Word boundary", + "btn_group": "( ) Group", + "desc_group": "Grouping", + "btn_or": "| OR", + "desc_or": "Alternative", + "btn_alt": "(?: ) Alt.", + "desc_alt": "Non-capturing group", + "preview": "👀 Preview", + "selection": "Selection:", + "btn_add": "➕ Add", + "btn_reset": "🔄 Reset", + "regex_result": "🎯 Regex Pattern", + "btn_manage": "📝 Manage Patterns", + "btn_save_preset": "💾 Save as Pattern", + "btn_copy": "📋 Copy", + "btn_reset_pattern": "🔄 Reset Pattern", + "btn_undo": "↶ Undo", + "btn_redo": "↷ Redo", + "btn_apply": "💾 Apply to Main", + "btn_test": "🧪 Test", + "test_input": "Enter Test Text:", + "test_text_frame": "📝 Test Text", + "test_pattern_frame": "🔍 Regex Pattern", + "btn_load_example": "📋 Load Example", + "result_label": "Result:", + "matches_label": "Found Matches:", + "common_examples": "📚 Common Regex Examples", + "msg_pattern_applied": "Regex pattern applied to main window!", + "msg_main_not_avail": "Main window reference not available. Copy pattern manually.", + "msg_no_pattern_apply": "No regex pattern available to apply", + "msg_copied": "Regex pattern copied to clipboard!", + "msg_no_pattern_copy": "No regex pattern available to copy", + "msg_no_undo": "No more undo actions available", + "msg_no_redo": "No more redo actions available", + "msg_enter_pattern": "❌ Please enter a regex pattern", + "msg_enter_text": "❌ Please enter a test text", + "msg_pattern_found": "✅ Pattern found! {} matches ({} unique)", + "msg_no_matches": "❌ No matches found", + "msg_invalid_regex": "❌ Invalid regex pattern: {}", + "msg_example_loaded": "Example loaded - click 'Test'", + "builder_desc_letters": "Letters (a-z, A-Z)", + "builder_desc_digits": "Digits (0-9)", + "builder_desc_alphanum": "Letters and Digits", + "builder_desc_any": "Any character", + "builder_desc_custom": "The text: '{}'", + "builder_desc_custom_generic": "Your custom text", + "builder_desc_special": "Special characters (space, dot, comma, etc.)", + "builder_desc_once": "appears exactly once", + "builder_desc_one_plus": "appears once or more", + "builder_desc_zero_one": "is optional (0 or 1 time)", + "builder_desc_zero_plus": "appears any number of times (0 or more)", + "builder_desc_n_times": "appears exactly {} times", + "builder_desc_m_n_times": "appears {} to {} times", + "builder_desc_start": "at line start", + "builder_desc_end": "at line end", + "builder_desc_word": "as whole word", + "builder_desc_or": "with alternatives", + "default_test_text": "Sample text with error and warning 123 numbers and email: test@example.com", + "default_result": "Test result appears here...", + "no_matches": "No matches", + "auto_desc": "Description automatically generated...", + "select_char_first": "Select a character type first...", + "enter_name": "Enter name", + "save_pattern": "Save Pattern", + "name_for_pattern": "Name for new pattern:", + "msg_save_success": "Pattern '{}' saved successfully", + "overwrite_confirm": "Pattern '{}' already exists. Overwrite?", + + # Additional keys needed for gui_new.py + "errors_and_warnings": "Errors and Warnings", + "errors_only": "Errors only", + "warnings_only": "Warnings only", + "critical_errors": "Critical Errors", + "numbers_100_199": "Numbers 100-199", + "email_addresses": "E-mail addresses", + "phone_numbers": "Phone numbers (DE)", + "date_yyyy_mm_dd": "Date (YYYY-MM-DD)", + + "desc_errors_and_warnings": "Finds 'error', 'warning' or 'critical'", + "desc_errors_only": "Finds 'error'", + "desc_warnings_only": "Finds 'warning'", + "desc_critical_errors": "Finds 'critical'", + "desc_numbers_100_199": "Finds numbers from 100 to 199", + "desc_email_addresses": "Finds standard e-mail addresses", + "desc_phone_numbers": "Finds phone numbers (German format)", + "desc_date_yyyy_mm_dd": "Finds dates in ISO format", + } + + def get_german_translations(self): + """ + Get default German translations + """ + return { + "app_title": "Excel Filter Tool", + "tab_config": "Konfiguration", + "tab_execution": "Ausführung", + "tab_regex_builder": "Regex-Builder", + "tab_help": "Hilfe", + "config_section": "Konfiguration", + "load_button": "Laden", + "save_button": "Speichern", + "input_file": "Eingabedatei:", + "output_file": "Ausgabedatei:", + "browse_button": "Durchsuchen...", + "worksheet": "Arbeitsblatt:", + "process_button": "🚀 VERARBEITEN", + "status_ready": "Bereit", + "success": "Erfolg", + "error": "Fehler", + "language": "Sprache:", + "english": "Englisch", + "german": "Deutsch", + "help_content": "\nExcel Filter Tool\n\nFUNKTION\n--------------------------\nDas Excel Filter Tool ist ein Werkzeug zur Datenanalyse und -filterung von Excel-Dateien.\nEs ermöglicht das automatische Extrahieren, Filtern und Transformieren von Daten basierend auf einstellbaren Suchkriterien.\n\nHauptfunktionen:\n• Intelligente Textsuche anhand von Regex-Mustern\n• Numerische Filterung (größer/kleiner als, zwischen Werten)\n• Spaltenbasierte Filterung\n\nARBEITSABLAUF\n----------------------------------------\nDer Arbeitsablauf ist in vier Hauptphasen unterteilt, die jeweils eigene Tabs haben:\n\n1. KONFIGURATION\n- Wähle die Eingabe-Excel-Datei aus\n- Bestimme die Ausgabedatei\n- Wähle das zu filternde Arbeitsblatt\n- Konfiguriere die Filterkriterien (Regex und/oder numerisch)\n- Wählen die Spalten aus, welche übernommen werden sollen\n- Speicher/Lade die Konfigurationen für wiederkehrende Aufgaben\n\n2. MUSTER-ERSTELLUNG (Regex-Builder-Tab)\n- Nutze den visuellen Regex-Builder für zur einfachen Mustererstellung\n- Wähle aus vorgefertigten Bausteinen (Text, Zahlen, Spezialzeichen)\n- Definiere Mengen (einmal, mehrmals, optional)\n- Füge optionale Anker und Gruppen hinzu\n- Teste die Muster mit Beispieltexten\n- Verwalte gespeicherte Muster\n\n3. AUSFÜHRUNG\n- Überprüfe den auszuführenden Befehl vor der Verarbeitung\n- Starten die Analyse\n- Verfolgen den Fortschritt in Echtzeit\n- Automatische Öffnung der Ergebnisdatei\n\nERWEITERTE KONFIGURATIONSOPTIONEN\n----------------------------------\n\nRegex-Filterung (Standardmodus)\n- Suche nach Textmustern mit voller Regex-Unterstützung\n- Unterstützung für komplexe Suchmuster\n- Wortgrenzen, Groß-/Kleinschreibung, Spezialzeichen\n\nNumerische Filterung\n- Filtern nach Zahlenwerten (größer/kleiner als)\n- Bereichsfilterung (zwischen Werten)\n- Spaltenübergreifende numerische Suche\n\nSpaltenbasierte Filterung\n- Auswahl spezifischer zu durchsuchender Spalten\n- Automatische Spaltenerkennung aus Excel-Dateien\n- Individuelle Spaltenauswahl für zielgerichtete Suche\n\nKONFIGURATIONSMANAGEMENT\n--------------------------\n• Speichern/Laden von Konfigurationen für wiederkehrende Aufgaben\n• Persönliche Musterbibliothek mit benutzerdefinierten Regex-Mustern\n• Automatische Speicherung von zuletzt verwendeten Dateipfaden\n• Wiederherstellung vorheriger Arbeitssitzungen\n", + + # Config Tab + "tooltip_config_section": "Du kannst Konfigurationen speichern und laden, statt alles jedes Mal neu einzugeben.", + "enable_regex": "Regex-Filter aktivieren", + "tooltip_enable_regex": "Wenn aktiviert, werden Regex-Muster für die Filterung verwendet", + "regex_options": " Regex-Optionen ", + "regex_pattern": "Regex-Muster:", + "preset_patterns": "Voreingestellte Muster:", + "apply_regex_column": "Regex auf Spalte anwählen:", + "all_columns": "Alle Spalten", + "manage_patterns": "Muster verwalten…", + "enable_numeric": "Numerische Filter aktivieren", + "numeric_filters": "Numerische Filter", + "column": "Spalte:", + "comparison": "Vergleich:", + "value": "Wert:", + "regex_builder_info": "Eigene Muster zur Filterung können im Regex-Builder erstellt werden.", + "enable_column_selection": "Spaltenauswahl aktivieren", + "column_selection": "Spaltenauswahl", + "select_all": "Alle auswählen", + "deselect_all": "Alle abwählen", + "proceed_to_execution": "Weiter zur Ausführung", + "msg_no_regex_copy": "Kein Regex-Muster verwendet - es werden nur die ausgewählten Spalten kopiert", + "msg_warn_no_selection": "Warnung: Kein Regex-Muster und keine Spalten ausgewählt - alle Daten werden kopiert", + "msg_copy_all": "Kopiere alle Spalten", + "msg_copy_selected": "Kopiere ausgewählte Spalten: {}", + "msg_written_success": "Erfolgreich geschrieben: {}", + "msg_ask_open": "Möchten Sie die Ausgabedatei öffnen?", + "msg_process_success": "Verarbeitung erfolgreich abgeschlossen", + "msg_process_failed": "Verarbeitung fehlgeschlagen", + "numeric_invalid": "❌ Ungültiger numerischer Wert", + "numeric_incomplete": "Konfiguration unvollständig", + "filter_desc": "Filter: {} {} {}", + + # Execution Tab + "ready_to_execute": "Bereit zur Ausführung...", + "status_ready": "Bereit", + "command_to_execute": "Auszuführender Befehl", + "execute_button": "AUSFÜHREN", + "activity_log": "Aktivitätsprotokoll", + "clear_log": "Löschen", + "save_log": "Speichern", + "log_cleared": "Protokoll gelöscht", + "ready_for_execution": "Excel Filter bereit zur Ausführung", + "configure_and_execute": "Konfigurieren Sie die Einstellungen und klicken Sie auf 'AUSFÜHREN'", + "error_main_gui_not_connected": "Fehler: Haupt-GUI nicht verbunden", + "input_file_label": "Eingabedatei:", + "output_file_label": "Ausgabedatei:", + "search_pattern_label": "Suchmuster:", + "worksheet_label": "Arbeitsblatt:", + "columns_label": "Spalten:", + "numeric_filter_label": "Numerischer Filter: {column} {operator} {value}", + "not_selected": "(nicht ausgewählt)", + "not_specified": "(nicht angegeben)", + "more_columns": "(+{} weitere)", + "error_updating_command_display": "Fehler beim Aktualisieren der Befehlsanzeige: {error}", + "execution_running": "Läuft...", + "waiting": "WARTEN...", + "execution_started": "▶️ Ausführung gestartet", + "execution_completed": "✅ Ausführung erfolgreich abgeschlossen", + "execution_failed": "❌ Ausführung mit Fehlern beendet", + "log_saved": "💾 Protokoll gespeichert: {file}", + + # Regex Builder Tab + "tab_builder": "🧱 Baustein-Builder", + "tab_tester": "🧪 Tester", + "tab_examples": "📚 Beispiele", + "step_1": "📝 Schritt 1: Zeichen suchen", + "step_2": "🔢 Schritt 2: Wie oft?", + "step_3": "⚙️ Schritt 3: Optionen", + "btn_letters": "🔤 Buchstaben", + "desc_letters": "a-z, A-Z", + "btn_digits": "🔢 Zahlen", + "desc_digits": "0-9", + "btn_alphanum": "🔤🔢 Alphanum.", + "desc_alphanum": "Buchst.+Zahlen", + "btn_any": "❓ Beliebig", + "desc_any": "Ein Zeichen", + "btn_custom": "📝 Eigener Text", + "desc_custom": "Ihr Text", + "btn_special": "⚙️ Spezial", + "desc_special": "Satzzeichen", + "btn_once": "🎯 1-mal", + "desc_once": "Genau einmal", + "btn_one_plus": "➕ 1+ mal", + "desc_one_plus": "Mindestens 1-mal", + "btn_zero_one": "❓ 0-1 mal", + "desc_zero_one": "Optional", + "btn_zero_plus": "♾️ 0+ mal", + "desc_zero_plus": "Beliebig oft", + "btn_n_times": "📏 N-mal", + "desc_n_times": "Genau N-mal", + "btn_m_n_times": "📊 M-N mal", + "desc_m_n_times": "M bis N-mal", + "btn_start": "^ Anfang", + "desc_start": "Zeilenanfang", + "btn_end": "$ Ende", + "desc_end": "Zeilenende", + "btn_word": "\\b Wort", + "desc_word": "Wortgrenze", + "btn_group": "( ) Gruppe", + "desc_group": "Gruppierung", + "btn_or": "| ODER", + "desc_or": "Alternative", + "btn_alt": "(?: ) Alt.", + "desc_alt": "Gruppe o. Speicher", + "preview": "👀 Vorschau", + "selection": "Auswahl:", + "btn_add": "➕ Hinzufügen", + "btn_reset": "🔄 Zurücksetzen", + "regex_result": "🎯 Regex-Muster", + "btn_manage": "📝 Muster verwalten", + "btn_save_preset": "💾 Als Muster speichern", + "btn_copy": "📋 Kopieren", + "btn_reset_pattern": "🔄 Muster zurücksetzen", + "btn_undo": "↶ Rückgängig", + "btn_redo": "↷ Wiederholen", + "btn_apply": "💾 In Hauptfenster übernehmen", + "btn_test": "🧪 Testen", + "test_input": "Testtext eingeben:", + "test_text_frame": "📝 Testtext", + "test_pattern_frame": "🔍 Regex-Muster", + "btn_load_example": "📋 Beispiel laden", + "result_label": "Ergebnis:", + "matches_label": "Gefundene Treffer:", + "common_examples": "📚 Häufig verwendete Regex-Beispiele", + "msg_pattern_applied": "Regex-Muster wurde in das Hauptfenster übernommen!", + "msg_main_not_avail": "Hauptfenster-Referenz nicht verfügbar. Kopieren Sie das Muster manuell.", + "msg_no_pattern_apply": "Kein Regex-Muster zum Übernehmen verfügbar", + "msg_copied": "Regex-Muster wurde in die Zwischenablage kopiert!", + "msg_no_pattern_copy": "Kein Regex-Muster zum Kopieren verfügbar", + "msg_no_undo": "Keine weiteren Rückgängig-Aktionen verfügbar", + "msg_no_redo": "Keine Wiederholen-Aktionen verfügbar", + "msg_enter_pattern": "❌ Bitte geben Sie ein Regex-Muster ein", + "msg_enter_text": "❌ Bitte geben Sie einen Testtext ein", + "msg_pattern_found": "✅ Muster gefunden! {} Treffer (davon {} einzigartig)", + "msg_no_matches": "❌ Keine Treffer gefunden", + "msg_invalid_regex": "❌ Ungültiges Regex-Muster: {}", + "msg_example_loaded": "Beispiel geladen - klicken Sie auf 'Testen'", + "builder_desc_letters": "Buchstaben (a-z, A-Z)", + "builder_desc_digits": "Zahlen (0-9)", + "builder_desc_alphanum": "Buchstaben und Zahlen", + "builder_desc_any": "Ein beliebiges Zeichen", + "builder_desc_custom": "Der Text: '{}'", + "builder_desc_custom_generic": "Ihr eigener Suchtext", + "builder_desc_special": "Sonderzeichen (Leerzeichen, Punkt, Komma, etc.)", + "builder_desc_once": "erscheint genau einmal", + "builder_desc_one_plus": "erscheint ein- oder mehrmals", + "builder_desc_zero_one": "ist optional (0- oder 1-mal)", + "builder_desc_zero_plus": "erscheint beliebig oft (0-mal oder mehr)", + "builder_desc_n_times": "erscheint genau {}-mal", + "builder_desc_m_n_times": "erscheint {} bis {}-mal", + "builder_desc_start": "am Zeilenanfang", + "builder_desc_end": "am Zeilenende", + "builder_desc_word": "als ganzes Wort", + "builder_desc_or": "mit Alternativen", + "default_test_text": "Beispieltext mit error und warning 123 Zahlen und E-Mail: test@example.com", + "default_result": "Hier erscheint das Testergebnis...", + "no_matches": "Keine Treffer", + "auto_desc": "Beschreibung wird automatisch generiert...", + "select_char_first": "Wählen Sie zuerst einen Zeichentyp aus...", + "enter_name": "Geben Sie einen Namen ein", + "save_pattern": "Muster speichern", + "name_for_pattern": "Name für das neue Muster:", + "msg_save_success": "Muster '{}' wurde gespeichert", + "overwrite_confirm": "Ein Muster mit dem Namen '{}' existiert bereits. Möchten Sie es überschreiben?", + + # Additional keys needed for gui_new.py + "errors_and_warnings": "Fehler und Warnungen", + "errors_only": "Nur Fehler", + "warnings_only": "Nur Warnungen", + "critical_errors": "Kritische Fehler", + "numbers_100_199": "Zahlen 100-199", + "email_addresses": "E-Mail-Adressen", + "phone_numbers": "Telefonnummern (DE)", + "date_yyyy_mm_dd": "Datum (YYYY-MM-DD)", + + "desc_errors_and_warnings": "Findet 'error', 'warning' oder 'critical'", + "desc_errors_only": "Findet nur 'error'", + "desc_warnings_only": "Findet nur 'warning'", + "desc_critical_errors": "Findet nur 'critical'", + "desc_numbers_100_199": "Findet Zahlen von 100 bis 199", + "desc_email_addresses": "Findet übliche E-Mail-Adressen", + "desc_phone_numbers": "Findet Telefonnummern (deutsches Format)", + "desc_date_yyyy_mm_dd": "Findet Datum im ISO-Format", + } + + def set_language(self, language): + """ + Set the current language + + Args: + language: Language code ('en' or 'de') + """ + if language in self.translations: + self.current_language = language + else: + raise ValueError(f"Unsupported language: {language}") + + def get(self, key, default=None): + """ + Get a translation for a key + + Args: + key: Translation key + default: Default value if key not found + + Returns: + Translated string or default value + """ + return self.translations[self.current_language].get(key, default if default is not None else key) + + def __getitem__(self, key): + """ + Get a translation using dictionary-style access + """ + return self.get(key) + + def get_available_languages(self): + """ + Get list of available language codes + + Returns: + List of language codes + """ + return list(self.translations.keys()) + + def get_language_names(self): + """ + Get dictionary mapping language codes to display names + + Returns: + Dictionary with language codes as keys and display names as values + """ + return { + "en": self.get("english"), + "de": self.get("german") + } diff --git a/excel_filter/utils/file_utils.py b/excel_filter/utils/file_utils.py new file mode 100644 index 0000000..8c4f121 --- /dev/null +++ b/excel_filter/utils/file_utils.py @@ -0,0 +1,110 @@ +""" +File utility functions for the Excel Filter Tool +""" + +import tkinter as tk +from tkinter import filedialog +import json +import logging + +# Logging konfigurieren +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def browse_file(initialdir=None, filetypes=None, title="Datei auswählen"): + """ + Opens a file dialog to browse and select a file + + Args: + initialdir: Initial directory to open + filetypes: File types to filter by + title: Dialog title + + Returns: + Selected file path or None if canceled + """ + root = tk.Tk() + root.withdraw() # Hide the main window + + file_path = filedialog.askopenfilename( + initialdir=initialdir, + title=title, + filetypes=filetypes + ) + + root.destroy() + return file_path + + +def browse_save_file(initialdir=None, filetypes=None, title="Datei speichern", defaultextension=".xlsx"): + """ + Opens a file dialog to browse and save a file + + Args: + initialdir: Initial directory to open + filetypes: File types to filter by + title: Dialog title + defaultextension: Default file extension + + Returns: + Selected file path or None if canceled + """ + root = tk.Tk() + root.withdraw() # Hide the main window + + file_path = filedialog.asksaveasfilename( + initialdir=initialdir, + title=title, + filetypes=filetypes, + defaultextension=defaultextension + ) + + root.destroy() + return file_path + + +def load_config(config_file: str) -> dict: + """ + Loads configuration from a JSON file + + Args: + config_file: Path to the configuration file + + Returns: + Dictionary containing the configuration + + Raises: + Exception if the file cannot be loaded + """ + try: + with open(config_file, 'r') as f: + config = json.load(f) + logger.info(f"Configuration loaded: {config_file}") + return config + except FileNotFoundError: + logger.warning(f"Configuration file not found: {config_file}") + return {} + except Exception as e: + logger.error(f"Error loading configuration: {e}") + raise Exception(f"Error loading configuration: {e}") + + +def save_config(config_file: str, config: dict): + """ + Saves configuration to a JSON file + + Args: + config_file: Path to the configuration file + config: Dictionary containing the configuration to save + + Raises: + Exception if the file cannot be saved + """ + try: + with open(config_file, 'w') as f: + json.dump(config, f, indent=4) + logger.info(f"Configuration saved: {config_file}") + except Exception as e: + logger.error(f"Error saving configuration: {e}") + raise Exception(f"Error saving configuration: {e}") \ No newline at end of file diff --git a/excel_filter/utils/logging_utils.py b/excel_filter/utils/logging_utils.py new file mode 100644 index 0000000..367d39e --- /dev/null +++ b/excel_filter/utils/logging_utils.py @@ -0,0 +1,114 @@ +""" +Logging utility functions for the Excel Filter Tool +""" + +import logging +from datetime import datetime +import tkinter as tk + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def setup_logging(log_file=None, level=logging.INFO): + """ + Sets up logging configuration + + Args: + log_file: Optional path to log file + level: Logging level + """ + handlers = [logging.StreamHandler()] + + if log_file: + handlers.append(logging.FileHandler(log_file)) + + logging.basicConfig( + level=level, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=handlers + ) + + +def log_message(text_widget, message, is_error=False): + """ + Logs a message to a Tkinter Text widget + + Args: + text_widget: Tkinter Text widget to log to + message: Message to log + is_error: Whether this is an error message + """ + timestamp = datetime.now().strftime("%H:%M:%S") + log_entry = f"[{timestamp}] {message}\n" + + text_widget.config(state=tk.NORMAL) + text_widget.insert(tk.END, log_entry) + + if is_error: + text_widget.tag_add("error", f"{text_widget.index(tk.END)} - 1 lines", tk.END) + text_widget.tag_config("error", foreground="red") + + text_widget.see(tk.END) + text_widget.config(state=tk.DISABLED) + + # Also log to console + if is_error: + logger.error(message) + else: + logger.info(message) + + +def log_error(text_widget, message): + """ + Logs an error message to a Tkinter Text widget + + Args: + text_widget: Tkinter Text widget to log to + message: Error message to log + """ + log_message(text_widget, f"[ERROR]: {message}", is_error=True) + +def log_detailed_error(text_widget, error_type, error_message, context=None): + """ + Logs a detailed error message with context information + + Args: + text_widget: Tkinter Text widget to log to + error_type: Type of error (e.g., "Datei nicht gefunden", "Regex-Fehler") + error_message: Detailed error message + context: Optional context information + """ + timestamp = datetime.now().strftime("%H:%M:%S") + + text_widget.config(state=tk.NORMAL) + + # Add separator + text_widget.insert(tk.END, "=" * 60 + "\n") + + # Add error header + error_header = f"[{timestamp}] [ERROR] {error_type.upper()}" + text_widget.insert(tk.END, error_header + "\n") + + # Add error message + text_widget.insert(tk.END, f"[ERROR] {error_message}\n") + + # Add context if provided + if context: + text_widget.insert(tk.END, f"Kontext: {context}\n") + + text_widget.insert(tk.END, "=" * 60 + "\n") + + # Apply error styling + start_line = float(text_widget.index(tk.END)) - 5.0 # Approximate start line + text_widget.tag_add("error", f"{start_line} lines", tk.END) + text_widget.tag_config("error", foreground="red") + + text_widget.see(tk.END) + text_widget.config(state=tk.DISABLED) + + # Also log to console + logger.error(f"{error_type}: {error_message}") + if context: + logger.error(f"Context: {context}") \ No newline at end of file diff --git a/presets.json b/presets.json new file mode 100644 index 0000000..52eae02 --- /dev/null +++ b/presets.json @@ -0,0 +1,24 @@ +{ + "presets": { + "Fehler und Warnungen": "error|warning|critical", + "Nur Fehler": "error", + "Nur Warnungen": "warning", + "Kritische Fehler": "critical", + "Zahlen 100-199": "1\\d{2}", + "E-Mail-Adressen": "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", + "Telefonnummern": "\\+?[0-9\\s-]{10,}", + "Datum (YYYY-MM-DD)": "\\d{4}-\\d{2}-\\d{2}", + "Zahlen mit Komma oder Punkt":"\\d?[,|.]\\d?" + }, + "descriptions": { + "Fehler und Warnungen": "Findet Zeilen mit 'error', 'warning' oder 'critical'", + "Nur Fehler": "Findet Zeilen mit 'error'", + "Nur Warnungen": "Findet Zeilen mit 'warning'", + "Kritische Fehler": "Findet Zeilen mit 'critical'", + "Zahlen 100-199": "Findet Zahlen zwischen 100 und 199", + "E-Mail-Adressen": "Findet E-Mail-Adressen", + "Telefonnummern": "Findet Telefonnummern", + "Datum (YYYY-MM-DD)": "Findet Daten im Format YYYY-MM-DD", + "Zahlen mit Komma oder Punkt":"Zahlen die durch einen Punkt oder Komma getrennt sind" + } +} \ No newline at end of file