pointcab_renamer/release/PointCab_Renamer_v4.2.1/pointcab_renamer.py

2059 lines
88 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PointCab Project Renamer v4.2
A GUI tool for renaming scans in PointCab projects with full scan names.
Now with Batch Renamer and Project Merger support!
LSDX-Struktur:
- <LSDX version="4.0">
- <Project>
- <Elements>
- <Element type="registration" uuid="{...}" parents=""> # Wurzelelement
- <Element type="cluster" uuid="{...}" parents="{registration_uuid}" name="ClusterName">
- <Element type="scan" uuid="{...}" parents="{cluster_uuid}" name="1">
- <References>
- <FilePath type="lsd">1.lsd</FilePath>
- <FilePath type="preview">Previews/1.png</FilePath>
Author: DeepAgent
Date: 2026-01-14
"""
import os
import re
import shutil
import logging
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from datetime import datetime
import xml.etree.ElementTree as ET
from pathlib import Path
import uuid
import copy
class PointCabRenamer:
"""Main application class for PointCab project renaming (Single Project Mode)."""
def __init__(self, root, return_callback=None):
self.root = root
self.return_callback = return_callback
self.root.title("PointCab Projekt Umbenenner - Einzelprojekt")
self.root.geometry("850x750")
self.root.minsize(700, 600)
# Variables
self.root_dir = tk.StringVar()
self.pointcloud_dir = None
self.lsdx_file = None
self.preview_dir = None
self.changes = []
self.logger = None
self.cleanup_strings = []
# Load cleanup configuration
self.load_cleanup_config()
self.setup_ui()
def load_cleanup_config(self):
"""Load cleanup strings from cluster_cleanup.txt."""
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cluster_cleanup.txt")
self.cleanup_strings = []
if os.path.exists(config_path):
try:
# Use utf-8-sig to handle BOM from Windows editors
with open(config_path, 'r', encoding='utf-8-sig') as f:
for line in f:
# Strip whitespace including BOM characters
line = line.strip()
# Skip empty lines and comments
if not line:
continue
if line.startswith('#'):
continue
# Remove any remaining BOM or special chars
line = line.lstrip('\ufeff')
if line:
self.cleanup_strings.append(line)
print(f"Cleanup-Konfiguration geladen: {len(self.cleanup_strings)} Einträge")
except Exception as e:
print(f"Fehler beim Laden der Cleanup-Konfiguration: {e}")
def clean_cluster_name(self, name):
"""Remove cleanup strings from the cluster name."""
cleaned_name = name
removed_strings = []
for cleanup_str in self.cleanup_strings:
if cleanup_str in cleaned_name:
removed_strings.append(cleanup_str)
cleaned_name = cleaned_name.replace(cleanup_str, '')
return cleaned_name, removed_strings
def setup_ui(self):
"""Setup the user interface."""
# Main frame with padding
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# Back button if we have a return callback
if self.return_callback:
back_frame = ttk.Frame(main_frame)
back_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Button(back_frame, text="← Zurück zum Hauptmenü", command=self.go_back).pack(side=tk.LEFT)
# Directory selection frame
dir_frame = ttk.LabelFrame(main_frame, text="Projektverzeichnis", padding="5")
dir_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Entry(dir_frame, textvariable=self.root_dir, width=60).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
ttk.Button(dir_frame, text="Durchsuchen...", command=self.select_directory).pack(side=tk.LEFT)
ttk.Button(dir_frame, text="Analysieren", command=self.analyze_project).pack(side=tk.LEFT, padx=(5, 0))
# Info frame
self.info_frame = ttk.LabelFrame(main_frame, text="Projektinformationen", padding="5")
self.info_frame.pack(fill=tk.X, pady=(0, 10))
self.info_label = ttk.Label(self.info_frame, text="Bitte wählen Sie ein Projektverzeichnis aus.", wraplength=800)
self.info_label.pack(fill=tk.X)
# Cleanup config info frame
cleanup_frame = ttk.LabelFrame(main_frame, text="Clustername-Bereinigung", padding="5")
cleanup_frame.pack(fill=tk.X, pady=(0, 10))
cleanup_info = f"Strings die aus dem Clusternamen entfernt werden: {', '.join(self.cleanup_strings) if self.cleanup_strings else '(keine)'}\n"
cleanup_info += f"📁 Konfigurationsdatei: cluster_cleanup.txt (im Programmverzeichnis)"
self.cleanup_label = ttk.Label(cleanup_frame, text=cleanup_info, wraplength=800, foreground="gray")
self.cleanup_label.pack(fill=tk.X)
ttk.Button(cleanup_frame, text="Konfiguration neu laden", command=self.reload_cleanup_config).pack(side=tk.LEFT, pady=(5, 0))
# Changes preview frame
preview_frame = ttk.LabelFrame(main_frame, text="Vorschau der Änderungen", padding="5")
preview_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
self.preview_text = scrolledtext.ScrolledText(preview_frame, wrap=tk.WORD, height=20, font=('Consolas', 9))
self.preview_text.pack(fill=tk.BOTH, expand=True)
self.preview_text.config(state=tk.DISABLED)
# Status frame
status_frame = ttk.Frame(main_frame)
status_frame.pack(fill=tk.X, pady=(0, 10))
self.status_label = ttk.Label(status_frame, text="Bereit", foreground="gray")
self.status_label.pack(side=tk.LEFT)
self.progress = ttk.Progressbar(status_frame, mode='indeterminate', length=200)
self.progress.pack(side=tk.RIGHT)
# Buttons frame
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X)
self.execute_btn = ttk.Button(button_frame, text="Änderungen ausführen", command=self.execute_changes, state=tk.DISABLED)
self.execute_btn.pack(side=tk.RIGHT)
ttk.Button(button_frame, text="Beenden", command=self.root.quit).pack(side=tk.LEFT)
def go_back(self):
"""Return to main menu."""
if self.return_callback:
self.return_callback()
def reload_cleanup_config(self):
"""Reload cleanup configuration and update display."""
self.load_cleanup_config()
cleanup_info = f"Strings die aus dem Clusternamen entfernt werden: {', '.join(self.cleanup_strings) if self.cleanup_strings else '(keine)'}\n"
cleanup_info += f"📁 Konfigurationsdatei: cluster_cleanup.txt (im Programmverzeichnis)"
self.cleanup_label.config(text=cleanup_info)
messagebox.showinfo("Info", f"Konfiguration neu geladen.\n{len(self.cleanup_strings)} Cleanup-Strings gefunden.")
def select_directory(self):
"""Open directory selection dialog."""
directory = filedialog.askdirectory(title="Stammverzeichnis auswählen")
if directory:
self.root_dir.set(directory)
self.analyze_project()
def setup_logging(self, log_dir):
"""Setup logging to file."""
log_file = os.path.join(log_dir, "renamer.log")
# Create logger
self.logger = logging.getLogger('PointCabRenamer')
self.logger.setLevel(logging.INFO)
# Clear existing handlers
self.logger.handlers.clear()
# File handler
fh = logging.FileHandler(log_file, encoding='utf-8')
fh.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
self.logger.addHandler(fh)
return log_file
def find_pointcloud_folder(self, root_dir):
"""Find the PointCloud folder in the root directory."""
root_name = os.path.basename(root_dir)
expected_name = f"{root_name}_PointCloud"
# Check for expected folder name
expected_path = os.path.join(root_dir, expected_name)
if os.path.isdir(expected_path):
return expected_path
# Search for any folder ending with _PointCloud
for item in os.listdir(root_dir):
item_path = os.path.join(root_dir, item)
if os.path.isdir(item_path) and item.endswith("_PointCloud"):
return item_path
return None
def find_lsdx_file(self, pointcloud_dir):
"""Find the LSDX file in the PointCloud directory."""
for item in os.listdir(pointcloud_dir):
if item.endswith(".lsdx"):
return os.path.join(pointcloud_dir, item)
return None
def get_file_mapping(self, file_list, extension, scan_prefix):
"""Create mapping for files with full scan names."""
# Extract numbers from filenames
files_with_nums = []
for f in file_list:
match = re.match(r'^(\d+)\.' + extension + '$', f)
if match:
num = int(match.group(1))
files_with_nums.append((num, f))
if not files_with_nums:
return {}, 2
files_with_nums.sort(key=lambda x: x[0])
max_num = max(num for num, _ in files_with_nums)
# Determine padding width
if max_num >= 100:
width = 3
else:
width = 2
# Create mapping with full scan name
mapping = {}
for num, old_name in files_with_nums:
new_num_str = str(num).zfill(width)
new_name = f"{scan_prefix}_{new_num_str}.{extension}"
if old_name != new_name:
mapping[old_name] = new_name
return mapping, width
def get_scan_name_mappings(self, lsdx_file, width, scan_prefix):
"""Get scan name mappings from LSDX file."""
mappings = []
try:
tree = ET.parse(lsdx_file)
root = tree.getroot()
# Find all scan elements with name attribute
for element in root.findall(".//Element[@type='scan']"):
old_name = element.get('name')
if old_name:
# Try to extract number from name
match = re.match(r'^(\d+)$', old_name)
if match:
num = int(match.group(1))
new_num_str = str(num).zfill(width)
new_name = f"{scan_prefix}_{new_num_str}"
if old_name != new_name:
mappings.append((old_name, new_name))
except Exception as e:
print(f"Fehler beim Lesen der Scan-Namen: {e}")
return mappings
def analyze_project(self):
"""Analyze the selected project directory."""
root_dir = self.root_dir.get()
if not root_dir or not os.path.isdir(root_dir):
messagebox.showerror("Fehler", "Bitte wählen Sie ein gültiges Verzeichnis aus.")
return
self.update_status("Analysiere Projekt...")
self.progress.start()
self.changes = []
try:
# Setup logging
log_file = self.setup_logging(root_dir)
self.logger.info(f"Analyse gestartet für: {root_dir}")
# Find PointCloud folder
self.pointcloud_dir = self.find_pointcloud_folder(root_dir)
if not self.pointcloud_dir:
messagebox.showerror("Fehler", "PointCloud-Ordner nicht gefunden!")
self.progress.stop()
self.update_status("Fehler: PointCloud-Ordner nicht gefunden")
return
# Find LSDX file
self.lsdx_file = self.find_lsdx_file(self.pointcloud_dir)
if not self.lsdx_file:
messagebox.showerror("Fehler", "LSDX-Datei nicht gefunden!")
self.progress.stop()
self.update_status("Fehler: LSDX-Datei nicht gefunden")
return
# Find Previews folder
self.preview_dir = os.path.join(self.pointcloud_dir, "Previews")
if not os.path.isdir(self.preview_dir):
self.preview_dir = None
# Get root name and clean cluster name
root_name = os.path.basename(root_dir)
cluster_name, removed_strings = self.clean_cluster_name(root_name)
scan_prefix = root_name
# Update info
info_text = f"Stammverzeichnis: {root_name}\n"
info_text += f"PointCloud-Ordner: {os.path.basename(self.pointcloud_dir)}\n"
info_text += f"LSDX-Datei: {os.path.basename(self.lsdx_file)}\n"
info_text += f"Preview-Ordner: {'Gefunden' if self.preview_dir else 'Nicht gefunden'}\n"
info_text += f"Clustername: {root_name}{cluster_name}"
if removed_strings:
info_text += f" (entfernt: {', '.join(removed_strings)})"
info_text += f"\nScan-Namen-Präfix: {scan_prefix} (unverändert)"
self.info_label.config(text=info_text)
# Collect LSD files
lsd_files = [f for f in os.listdir(self.pointcloud_dir) if f.endswith('.lsd')]
# Get mapping for LSD files
lsd_mapping, lsd_width = self.get_file_mapping(lsd_files, 'lsd', scan_prefix)
# Collect PNG files if preview folder exists
png_mapping = {}
png_width = lsd_width
if self.preview_dir:
png_files = [f for f in os.listdir(self.preview_dir) if f.endswith('.png')]
png_mapping, png_width = self.get_file_mapping(png_files, 'png', scan_prefix)
# Get scan name mappings
scan_name_mappings = self.get_scan_name_mappings(self.lsdx_file, lsd_width, scan_prefix)
# Build changes list
preview_text = "=== GEPLANTE ÄNDERUNGEN ===\n\n"
# LSDX Backup
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_name = f"{os.path.basename(self.lsdx_file)}.backup_{timestamp}"
preview_text += f"📁 BACKUP der LSDX-Datei:\n"
preview_text += f"{backup_name}\n\n"
self.changes.append(('backup', self.lsdx_file, os.path.join(self.pointcloud_dir, backup_name)))
# LSD file renames
if lsd_mapping:
preview_text += f"📄 LSD-Dateien umbenennen ({len(lsd_mapping)} Dateien):\n"
for old, new in sorted(lsd_mapping.items(), key=lambda x: x[0]):
preview_text += f" {old}{new}\n"
self.changes.append(('rename_lsd',
os.path.join(self.pointcloud_dir, old),
os.path.join(self.pointcloud_dir, new)))
preview_text += "\n"
else:
preview_text += "📄 LSD-Dateien: Keine Umbenennung erforderlich\n\n"
# PNG file renames
if self.preview_dir and png_mapping:
preview_text += f"🖼️ PNG-Dateien umbenennen ({len(png_mapping)} Dateien):\n"
for old, new in sorted(png_mapping.items(), key=lambda x: x[0]):
preview_text += f" {old}{new}\n"
self.changes.append(('rename_png',
os.path.join(self.preview_dir, old),
os.path.join(self.preview_dir, new)))
preview_text += "\n"
elif self.preview_dir:
preview_text += "🖼️ PNG-Dateien: Keine Umbenennung erforderlich\n\n"
else:
preview_text += "🖼️ PNG-Dateien: Preview-Ordner nicht vorhanden\n\n"
# Scan name changes
if scan_name_mappings:
preview_text += f"🏷️ Scan-Namen in LSDX aktualisieren ({len(scan_name_mappings)} Scans):\n"
for old_name, new_name in scan_name_mappings[:10]:
preview_text += f" name=\"{old_name}\" → name=\"{new_name}\"\n"
if len(scan_name_mappings) > 10:
preview_text += f" ... und {len(scan_name_mappings) - 10} weitere\n"
preview_text += "\n"
else:
preview_text += "🏷️ Scan-Namen: Keine Änderungen erforderlich\n\n"
# LSDX updates
preview_text += "📝 LSDX-Datei aktualisieren:\n"
preview_text += f" - Clustername: '{root_name}''{cluster_name}'"
if removed_strings:
preview_text += f" (entfernt: {', '.join(removed_strings)})"
preview_text += "\n"
preview_text += f" - Dateinamen in FilePath-Elementen mit vollständigem Scan-Namen\n"
preview_text += f" - Beispiel: 1.lsd → {scan_prefix}_01.lsd\n"
preview_text += f" - Scan-Namen mit Präfix '{scan_prefix}_' versehen\n"
self.changes.append(('update_lsdx', self.lsdx_file, {
'cluster_name': cluster_name,
'scan_prefix': scan_prefix,
'original_name': root_name,
'removed_strings': removed_strings,
'lsd_width': lsd_width,
'png_width': png_width if self.preview_dir else lsd_width
}))
preview_text += f"\n=== ZUSAMMENFASSUNG ===\n"
preview_text += f"Gesamtänderungen: {len(self.changes)}\n"
if removed_strings:
preview_text += f"Bereinigte Strings: {', '.join(removed_strings)}\n"
preview_text += f"Log-Datei: {log_file}\n"
# Update preview
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete(1.0, tk.END)
self.preview_text.insert(tk.END, preview_text)
self.preview_text.config(state=tk.DISABLED)
# Enable execute button if there are changes
if self.changes:
self.execute_btn.config(state=tk.NORMAL)
self.logger.info(f"Analyse abgeschlossen: {len(self.changes)} Änderungen geplant")
self.logger.info(f"Clustername: {root_name}{cluster_name}")
self.logger.info(f"Scan-Namen-Präfix: {scan_prefix}")
if removed_strings:
self.logger.info(f"Entfernte Strings: {', '.join(removed_strings)}")
self.update_status(f"Analyse abgeschlossen: {len(self.changes)} Änderungen geplant")
except Exception as e:
messagebox.showerror("Fehler", f"Fehler bei der Analyse:\n{str(e)}")
if self.logger:
self.logger.error(f"Analysefehler: {str(e)}")
self.update_status("Fehler bei der Analyse")
finally:
self.progress.stop()
def execute_changes(self):
"""Execute all planned changes."""
if not self.changes:
messagebox.showinfo("Info", "Keine Änderungen zum Ausführen.")
return
# Confirmation dialog
result = messagebox.askyesno(
"Bestätigung",
f"Möchten Sie wirklich {len(self.changes)} Änderungen ausführen?\n\n"
"Ein Backup der LSDX-Datei wird automatisch erstellt."
)
if not result:
return
self.update_status("Führe Änderungen aus...")
self.progress.start()
self.execute_btn.config(state=tk.DISABLED)
try:
self.logger.info("Starte Ausführung der Änderungen")
# First pass: backup and collect rename operations
renames_lsd = []
renames_png = []
lsdx_update = None
for change in self.changes:
if change[0] == 'backup':
# Create backup
_, src, dst = change
shutil.copy2(src, dst)
self.logger.info(f"Backup erstellt: {dst}")
elif change[0] == 'rename_lsd':
renames_lsd.append((change[1], change[2]))
elif change[0] == 'rename_png':
renames_png.append((change[1], change[2]))
elif change[0] == 'update_lsdx':
lsdx_update = change
# Execute renames with temporary names to avoid conflicts
def safe_rename(rename_list, file_type):
"""Rename files using temporary names to avoid conflicts."""
temp_suffix = f"_temp_{datetime.now().strftime('%Y%m%d%H%M%S')}"
# First: rename to temporary names
temp_mappings = []
for src, dst in rename_list:
if os.path.exists(src):
temp_name = src + temp_suffix
os.rename(src, temp_name)
temp_mappings.append((temp_name, dst))
self.logger.info(f"{file_type}: {os.path.basename(src)} → temp")
# Second: rename from temporary to final names
for temp_name, dst in temp_mappings:
os.rename(temp_name, dst)
self.logger.info(f"{file_type}: temp → {os.path.basename(dst)}")
safe_rename(renames_lsd, "LSD")
safe_rename(renames_png, "PNG")
# Update LSDX file
if lsdx_update:
self.update_lsdx_file(lsdx_update[1], lsdx_update[2])
self.logger.info("Alle Änderungen erfolgreich ausgeführt")
self.update_status("Alle Änderungen erfolgreich ausgeführt!")
messagebox.showinfo("Erfolg", "Alle Änderungen wurden erfolgreich ausgeführt!")
# Clear changes
self.changes = []
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete(1.0, tk.END)
self.preview_text.insert(tk.END, "Alle Änderungen wurden erfolgreich ausgeführt.\n\nSie können nun ein neues Projekt auswählen.")
self.preview_text.config(state=tk.DISABLED)
except Exception as e:
messagebox.showerror("Fehler", f"Fehler bei der Ausführung:\n{str(e)}")
self.logger.error(f"Ausführungsfehler: {str(e)}")
self.update_status("Fehler bei der Ausführung")
self.execute_btn.config(state=tk.NORMAL)
finally:
self.progress.stop()
def update_lsdx_file(self, lsdx_path, params):
"""Update the LSDX file with new filenames, cluster name, and scan names."""
cluster_name = params['cluster_name']
scan_prefix = params.get('scan_prefix', params.get('original_name', cluster_name))
original_name = params.get('original_name', cluster_name)
removed_strings = params.get('removed_strings', [])
lsd_width = params['lsd_width']
png_width = params['png_width']
# Parse XML
tree = ET.parse(lsdx_path)
root = tree.getroot()
# Update cluster name
for element in root.findall(".//Element[@type='cluster']"):
old_cluster = element.get('name')
element.set('name', cluster_name)
self.logger.info(f"Clustername: '{old_cluster}''{cluster_name}'")
if removed_strings:
self.logger.info(f" Entfernte Strings: {', '.join(removed_strings)}")
# Update scan names with prefix
for element in root.findall(".//Element[@type='scan']"):
old_name = element.get('name')
if old_name:
match = re.match(r'^(\d+)$', old_name)
if match:
num = int(match.group(1))
new_num_str = str(num).zfill(lsd_width)
new_name = f"{scan_prefix}_{new_num_str}"
element.set('name', new_name)
self.logger.info(f"Scan-Name: '{old_name}''{new_name}'")
# Update FilePath elements
for filepath in root.findall(".//FilePath"):
file_type = filepath.get('type')
text = filepath.text
if file_type == 'lsd' and text:
match = re.match(r'^(\d+)\.lsd$', text)
if match:
num = int(match.group(1))
new_num_str = str(num).zfill(lsd_width)
new_name = f"{scan_prefix}_{new_num_str}.lsd"
filepath.text = new_name
self.logger.info(f"LSDX FilePath: {text}{new_name}")
elif file_type == 'preview' and text:
match = re.match(r'^Previews/(\d+)\.png$', text)
if match:
num = int(match.group(1))
new_num_str = str(num).zfill(png_width)
new_name = f"Previews/{scan_prefix}_{new_num_str}.png"
filepath.text = new_name
self.logger.info(f"LSDX FilePath: {text}{new_name}")
# Write back
tree.write(lsdx_path, encoding='utf-8', xml_declaration=True)
# Add DOCTYPE back
with open(lsdx_path, 'r', encoding='utf-8') as f:
content = f.read()
if '<!DOCTYPE LSDX>' not in content:
content = content.replace(
"<?xml version='1.0' encoding='utf-8'?>",
"<?xml version='1.0' encoding='utf-8'?>\n<!DOCTYPE LSDX>"
)
with open(lsdx_path, 'w', encoding='utf-8') as f:
f.write(content)
self.logger.info("LSDX-Datei erfolgreich aktualisiert")
def update_status(self, message):
"""Update the status label."""
self.status_label.config(text=message)
self.root.update_idletasks()
class BatchRenamer:
"""Batch Renamer for processing multiple PointCab projects."""
def __init__(self, root, return_callback=None):
self.root = root
self.return_callback = return_callback
self.root.title("PointCab Batch Renamer")
self.root.geometry("900x700")
self.root.minsize(800, 600)
# Variables
self.main_dir = tk.StringVar()
self.projects = []
self.cleanup_strings = []
self.batch_logger = None
self.processing = False
# Load cleanup configuration
self.load_cleanup_config()
self.setup_ui()
def load_cleanup_config(self):
"""Load cleanup strings from cluster_cleanup.txt."""
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cluster_cleanup.txt")
self.cleanup_strings = []
if os.path.exists(config_path):
try:
# Use utf-8-sig to handle BOM from Windows editors
with open(config_path, 'r', encoding='utf-8-sig') as f:
for line in f:
# Strip whitespace including BOM characters
line = line.strip()
# Skip empty lines and comments
if not line:
continue
if line.startswith('#'):
continue
# Remove any remaining BOM or special chars
line = line.lstrip('\ufeff')
if line:
self.cleanup_strings.append(line)
print(f"Cleanup-Konfiguration geladen: {len(self.cleanup_strings)} Einträge")
except Exception as e:
print(f"Fehler beim Laden der Cleanup-Konfiguration: {e}")
def clean_cluster_name(self, name):
"""Remove cleanup strings from the cluster name."""
cleaned_name = name
removed_strings = []
for cleanup_str in self.cleanup_strings:
if cleanup_str in cleaned_name:
removed_strings.append(cleanup_str)
cleaned_name = cleaned_name.replace(cleanup_str, '')
return cleaned_name, removed_strings
def setup_ui(self):
"""Setup the batch renamer UI."""
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# Back button
if self.return_callback:
back_frame = ttk.Frame(main_frame)
back_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Button(back_frame, text="← Zurück zum Hauptmenü", command=self.go_back).pack(side=tk.LEFT)
# Directory selection
dir_frame = ttk.LabelFrame(main_frame, text="Hauptverzeichnis auswählen", padding="5")
dir_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Entry(dir_frame, textvariable=self.main_dir, width=60).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
ttk.Button(dir_frame, text="Durchsuchen...", command=self.select_directory).pack(side=tk.LEFT)
ttk.Button(dir_frame, text="Projekte suchen", command=self.scan_for_projects).pack(side=tk.LEFT, padx=(5, 0))
# Projects list
projects_frame = ttk.LabelFrame(main_frame, text="Gefundene PointCab-Projekte", padding="5")
projects_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# Treeview for projects
columns = ('status', 'project', 'cluster', 'scans')
self.projects_tree = ttk.Treeview(projects_frame, columns=columns, show='headings', height=10)
self.projects_tree.heading('status', text='Status')
self.projects_tree.heading('project', text='Projekt')
self.projects_tree.heading('cluster', text='Clustername')
self.projects_tree.heading('scans', text='Scans')
self.projects_tree.column('status', width=100)
self.projects_tree.column('project', width=250)
self.projects_tree.column('cluster', width=150)
self.projects_tree.column('scans', width=80)
scrollbar = ttk.Scrollbar(projects_frame, orient=tk.VERTICAL, command=self.projects_tree.yview)
self.projects_tree.configure(yscrollcommand=scrollbar.set)
self.projects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Info label
self.info_label = ttk.Label(main_frame, text="Wählen Sie ein Hauptverzeichnis und klicken Sie auf 'Projekte suchen'.")
self.info_label.pack(fill=tk.X, pady=(0, 10))
# Progress section
progress_frame = ttk.LabelFrame(main_frame, text="Fortschritt", padding="5")
progress_frame.pack(fill=tk.X, pady=(0, 10))
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100)
self.progress_bar.pack(fill=tk.X, pady=(0, 5))
self.progress_label = ttk.Label(progress_frame, text="")
self.progress_label.pack(fill=tk.X)
# Log area
log_frame = ttk.LabelFrame(main_frame, text="Verarbeitungslog", padding="5")
log_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=10, font=('Consolas', 9))
self.log_text.pack(fill=tk.BOTH, expand=True)
self.log_text.config(state=tk.DISABLED)
# Buttons
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X)
self.process_btn = ttk.Button(button_frame, text="Alle verarbeiten", command=self.process_all, state=tk.DISABLED)
self.process_btn.pack(side=tk.RIGHT)
ttk.Button(button_frame, text="Beenden", command=self.root.quit).pack(side=tk.LEFT)
def go_back(self):
"""Return to main menu."""
if self.processing:
messagebox.showwarning("Warnung", "Bitte warten Sie, bis die Verarbeitung abgeschlossen ist.")
return
if self.return_callback:
self.return_callback()
def select_directory(self):
"""Open directory selection dialog."""
directory = filedialog.askdirectory(title="Hauptverzeichnis auswählen")
if directory:
self.main_dir.set(directory)
def scan_for_projects(self):
"""Scan the main directory for PointCab projects."""
main_dir = self.main_dir.get()
if not main_dir or not os.path.isdir(main_dir):
messagebox.showerror("Fehler", "Bitte wählen Sie ein gültiges Hauptverzeichnis aus.")
return
self.projects = []
self.projects_tree.delete(*self.projects_tree.get_children())
# Scan for subdirectories with _PointCloud folder
for item in os.listdir(main_dir):
item_path = os.path.join(main_dir, item)
if os.path.isdir(item_path):
pointcloud_folder = self.find_pointcloud_folder(item_path)
if pointcloud_folder:
lsdx_file = self.find_lsdx_file(pointcloud_folder)
if lsdx_file:
scan_count = self.count_lsd_files(pointcloud_folder)
cluster_name, _ = self.clean_cluster_name(item)
self.projects.append({
'path': item_path,
'name': item,
'pointcloud': pointcloud_folder,
'lsdx': lsdx_file,
'cluster_name': cluster_name,
'scan_count': scan_count,
'status': 'Ausstehend'
})
self.projects_tree.insert('', tk.END, values=(
'Ausstehend', item, cluster_name, scan_count
))
if self.projects:
self.info_label.config(text=f"{len(self.projects)} PointCab-Projekte gefunden.")
self.process_btn.config(state=tk.NORMAL)
else:
self.info_label.config(text="Keine PointCab-Projekte gefunden.")
self.process_btn.config(state=tk.DISABLED)
def find_pointcloud_folder(self, root_dir):
"""Find the PointCloud folder in the root directory."""
root_name = os.path.basename(root_dir)
expected_name = f"{root_name}_PointCloud"
expected_path = os.path.join(root_dir, expected_name)
if os.path.isdir(expected_path):
return expected_path
for item in os.listdir(root_dir):
item_path = os.path.join(root_dir, item)
if os.path.isdir(item_path) and item.endswith("_PointCloud"):
return item_path
return None
def find_lsdx_file(self, pointcloud_dir):
"""Find the LSDX file in the PointCloud directory."""
for item in os.listdir(pointcloud_dir):
if item.endswith(".lsdx"):
return os.path.join(pointcloud_dir, item)
return None
def count_lsd_files(self, pointcloud_dir):
"""Count LSD files in directory."""
return len([f for f in os.listdir(pointcloud_dir) if f.endswith('.lsd')])
def setup_batch_logging(self, main_dir):
"""Setup batch summary logging."""
log_file = os.path.join(main_dir, "batch_summary.log")
self.batch_logger = logging.getLogger('BatchRenamer')
self.batch_logger.setLevel(logging.INFO)
self.batch_logger.handlers.clear()
fh = logging.FileHandler(log_file, encoding='utf-8')
fh.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
self.batch_logger.addHandler(fh)
return log_file
def log_message(self, message, level='info'):
"""Add message to log display and batch log."""
self.log_text.config(state=tk.NORMAL)
timestamp = datetime.now().strftime("%H:%M:%S")
self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
if self.batch_logger:
if level == 'error':
self.batch_logger.error(message)
else:
self.batch_logger.info(message)
self.root.update_idletasks()
def update_project_status(self, index, status):
"""Update project status in treeview."""
item_id = self.projects_tree.get_children()[index]
values = list(self.projects_tree.item(item_id)['values'])
values[0] = status
self.projects_tree.item(item_id, values=values)
self.root.update_idletasks()
def process_all(self):
"""Process all found projects."""
if not self.projects:
messagebox.showinfo("Info", "Keine Projekte zum Verarbeiten.")
return
result = messagebox.askyesno(
"Bestätigung",
f"Möchten Sie wirklich {len(self.projects)} Projekte verarbeiten?\n\n"
"Ein Backup der LSDX-Dateien wird automatisch erstellt."
)
if not result:
return
self.processing = True
self.process_btn.config(state=tk.DISABLED)
# Clear log
self.log_text.config(state=tk.NORMAL)
self.log_text.delete(1.0, tk.END)
self.log_text.config(state=tk.DISABLED)
# Setup batch logging
batch_log = self.setup_batch_logging(self.main_dir.get())
self.log_message(f"=== Batch-Verarbeitung gestartet ===")
self.log_message(f"Hauptverzeichnis: {self.main_dir.get()}")
self.log_message(f"Anzahl Projekte: {len(self.projects)}")
self.log_message(f"Batch-Log: {batch_log}")
self.log_message("")
success_count = 0
error_count = 0
for i, project in enumerate(self.projects):
self.progress_var.set((i / len(self.projects)) * 100)
self.progress_label.config(text=f"Verarbeite {i+1}/{len(self.projects)}: {project['name']}")
self.update_project_status(i, 'Verarbeite...')
self.log_message(f"--- Projekt: {project['name']} ---")
try:
self.process_single_project(project)
self.update_project_status(i, '✓ Erfolgreich')
self.log_message(f"{project['name']} erfolgreich verarbeitet")
success_count += 1
except Exception as e:
self.update_project_status(i, '✗ Fehler')
self.log_message(f"✗ Fehler bei {project['name']}: {str(e)}", 'error')
error_count += 1
self.log_message("")
self.progress_var.set(100)
self.progress_label.config(text="Fertig")
# Summary
self.log_message("=== ZUSAMMENFASSUNG ===")
self.log_message(f"Gesamt: {len(self.projects)}")
self.log_message(f"Erfolgreich: {success_count}")
self.log_message(f"Fehlgeschlagen: {error_count}")
self.processing = False
messagebox.showinfo(
"Batch-Verarbeitung abgeschlossen",
f"Verarbeitung abgeschlossen!\n\n"
f"Erfolgreich: {success_count}\n"
f"Fehlgeschlagen: {error_count}\n\n"
f"Details siehe: {batch_log}"
)
def process_single_project(self, project):
"""Process a single project without GUI interaction."""
root_dir = project['path']
pointcloud_dir = project['pointcloud']
lsdx_file = project['lsdx']
# Setup project-specific logging
project_logger = logging.getLogger(f'Project_{project["name"]}')
project_logger.setLevel(logging.INFO)
project_logger.handlers.clear()
log_file = os.path.join(root_dir, "renamer.log")
fh = logging.FileHandler(log_file, encoding='utf-8')
fh.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
project_logger.addHandler(fh)
project_logger.info(f"Batch-Verarbeitung gestartet für: {root_dir}")
# Get names
root_name = os.path.basename(root_dir)
cluster_name, removed_strings = self.clean_cluster_name(root_name)
scan_prefix = root_name
project_logger.info(f"Clustername: {root_name}{cluster_name}")
project_logger.info(f"Scan-Namen-Präfix: {scan_prefix}")
if removed_strings:
project_logger.info(f"Entfernte Strings: {', '.join(removed_strings)}")
# Find preview dir
preview_dir = os.path.join(pointcloud_dir, "Previews")
if not os.path.isdir(preview_dir):
preview_dir = None
# Get LSD files and mapping
lsd_files = [f for f in os.listdir(pointcloud_dir) if f.endswith('.lsd')]
lsd_mapping, lsd_width = self.get_file_mapping(lsd_files, 'lsd', scan_prefix)
# Get PNG files and mapping
png_mapping = {}
png_width = lsd_width
if preview_dir:
png_files = [f for f in os.listdir(preview_dir) if f.endswith('.png')]
png_mapping, png_width = self.get_file_mapping(png_files, 'png', scan_prefix)
# Create backup
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_name = f"{os.path.basename(lsdx_file)}.backup_{timestamp}"
backup_path = os.path.join(pointcloud_dir, backup_name)
shutil.copy2(lsdx_file, backup_path)
project_logger.info(f"Backup erstellt: {backup_name}")
# Rename LSD files
if lsd_mapping:
self.safe_rename(pointcloud_dir, lsd_mapping, project_logger, "LSD")
# Rename PNG files
if preview_dir and png_mapping:
self.safe_rename(preview_dir, png_mapping, project_logger, "PNG")
# Update LSDX file
self.update_lsdx_file(lsdx_file, {
'cluster_name': cluster_name,
'scan_prefix': scan_prefix,
'original_name': root_name,
'removed_strings': removed_strings,
'lsd_width': lsd_width,
'png_width': png_width
}, project_logger)
project_logger.info("Verarbeitung abgeschlossen")
def get_file_mapping(self, file_list, extension, scan_prefix):
"""Create mapping for files with full scan names."""
files_with_nums = []
for f in file_list:
match = re.match(r'^(\d+)\.' + extension + '$', f)
if match:
num = int(match.group(1))
files_with_nums.append((num, f))
if not files_with_nums:
return {}, 2
files_with_nums.sort(key=lambda x: x[0])
max_num = max(num for num, _ in files_with_nums)
width = 3 if max_num >= 100 else 2
mapping = {}
for num, old_name in files_with_nums:
new_num_str = str(num).zfill(width)
new_name = f"{scan_prefix}_{new_num_str}.{extension}"
if old_name != new_name:
mapping[old_name] = new_name
return mapping, width
def safe_rename(self, directory, mapping, logger, file_type):
"""Rename files using temporary names to avoid conflicts."""
temp_suffix = f"_temp_{datetime.now().strftime('%Y%m%d%H%M%S')}"
temp_mappings = []
for old_name, new_name in mapping.items():
src = os.path.join(directory, old_name)
if os.path.exists(src):
temp_name = src + temp_suffix
os.rename(src, temp_name)
temp_mappings.append((temp_name, os.path.join(directory, new_name)))
logger.info(f"{file_type}: {old_name} → temp")
for temp_name, dst in temp_mappings:
os.rename(temp_name, dst)
logger.info(f"{file_type}: temp → {os.path.basename(dst)}")
def update_lsdx_file(self, lsdx_path, params, logger):
"""Update the LSDX file with new filenames, cluster name, and scan names."""
cluster_name = params['cluster_name']
scan_prefix = params.get('scan_prefix', params.get('original_name', cluster_name))
removed_strings = params.get('removed_strings', [])
lsd_width = params['lsd_width']
png_width = params['png_width']
tree = ET.parse(lsdx_path)
root = tree.getroot()
# Update cluster name
for element in root.findall(".//Element[@type='cluster']"):
old_cluster = element.get('name')
element.set('name', cluster_name)
logger.info(f"Clustername: '{old_cluster}''{cluster_name}'")
if removed_strings:
logger.info(f" Entfernte Strings: {', '.join(removed_strings)}")
# Update scan names with prefix
for element in root.findall(".//Element[@type='scan']"):
old_name = element.get('name')
if old_name:
match = re.match(r'^(\d+)$', old_name)
if match:
num = int(match.group(1))
new_num_str = str(num).zfill(lsd_width)
new_name = f"{scan_prefix}_{new_num_str}"
element.set('name', new_name)
logger.info(f"Scan-Name: '{old_name}''{new_name}'")
# Update FilePath elements
for filepath in root.findall(".//FilePath"):
file_type = filepath.get('type')
text = filepath.text
if file_type == 'lsd' and text:
match = re.match(r'^(\d+)\.lsd$', text)
if match:
num = int(match.group(1))
new_num_str = str(num).zfill(lsd_width)
new_name = f"{scan_prefix}_{new_num_str}.lsd"
filepath.text = new_name
logger.info(f"LSDX FilePath: {text}{new_name}")
elif file_type == 'preview' and text:
match = re.match(r'^Previews/(\d+)\.png$', text)
if match:
num = int(match.group(1))
new_num_str = str(num).zfill(png_width)
new_name = f"Previews/{scan_prefix}_{new_num_str}.png"
filepath.text = new_name
logger.info(f"LSDX FilePath: {text}{new_name}")
tree.write(lsdx_path, encoding='utf-8', xml_declaration=True)
# Add DOCTYPE back
with open(lsdx_path, 'r', encoding='utf-8') as f:
content = f.read()
if '<!DOCTYPE LSDX>' not in content:
content = content.replace(
"<?xml version='1.0' encoding='utf-8'?>",
"<?xml version='1.0' encoding='utf-8'?>\n<!DOCTYPE LSDX>"
)
with open(lsdx_path, 'w', encoding='utf-8') as f:
f.write(content)
logger.info("LSDX-Datei erfolgreich aktualisiert")
class ProjectMerger:
"""Project Merger for combining multiple PointCab projects into one."""
def __init__(self, root, return_callback=None):
self.root = root
self.return_callback = return_callback
self.root.title("PointCab Projektmerger")
self.root.geometry("1000x800")
self.root.minsize(900, 700)
# Variables
self.target_dir = tk.StringVar()
self.source_dir = tk.StringVar()
self.merge_mode = tk.StringVar(value="single")
self.target_project = None
self.source_projects = []
self.merge_logger = None
self.processing = False
self.setup_ui()
def setup_ui(self):
"""Setup the project merger UI."""
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# Back button
if self.return_callback:
back_frame = ttk.Frame(main_frame)
back_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Button(back_frame, text="← Zurück zum Hauptmenü", command=self.go_back).pack(side=tk.LEFT)
# Target project selection
target_frame = ttk.LabelFrame(main_frame, text="🎯 Stammprojekt (Ziel)", padding="5")
target_frame.pack(fill=tk.X, pady=(0, 10))
target_entry_frame = ttk.Frame(target_frame)
target_entry_frame.pack(fill=tk.X)
ttk.Entry(target_entry_frame, textvariable=self.target_dir, width=60).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
ttk.Button(target_entry_frame, text="Durchsuchen...", command=self.select_target).pack(side=tk.LEFT)
ttk.Button(target_entry_frame, text="Analysieren", command=self.analyze_target).pack(side=tk.LEFT, padx=(5, 0))
self.target_info = ttk.Label(target_frame, text="Bitte Stammprojekt auswählen", foreground="gray")
self.target_info.pack(fill=tk.X, pady=(5, 0))
# Mode selection
mode_frame = ttk.LabelFrame(main_frame, text="Merge-Modus", padding="5")
mode_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Radiobutton(mode_frame, text="📄 Einzelprojekt hinzufügen", variable=self.merge_mode,
value="single", command=self.on_mode_change).pack(side=tk.LEFT, padx=(0, 20))
ttk.Radiobutton(mode_frame, text="📂 Batch-Merge (mehrere Projekte)", variable=self.merge_mode,
value="batch", command=self.on_mode_change).pack(side=tk.LEFT)
# Source project selection
source_frame = ttk.LabelFrame(main_frame, text="📁 Quellprojekt(e)", padding="5")
source_frame.pack(fill=tk.X, pady=(0, 10))
source_entry_frame = ttk.Frame(source_frame)
source_entry_frame.pack(fill=tk.X)
ttk.Entry(source_entry_frame, textvariable=self.source_dir, width=60).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
ttk.Button(source_entry_frame, text="Durchsuchen...", command=self.select_source).pack(side=tk.LEFT)
ttk.Button(source_entry_frame, text="Projekte suchen", command=self.scan_source).pack(side=tk.LEFT, padx=(5, 0))
self.source_mode_label = ttk.Label(source_frame, text="Wählen Sie ein Quellprojekt aus", foreground="gray")
self.source_mode_label.pack(fill=tk.X, pady=(5, 0))
# Source projects list
projects_frame = ttk.LabelFrame(main_frame, text="Zu mergende Projekte", padding="5")
projects_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
columns = ('status', 'project', 'scans', 'lsd_files', 'png_files')
self.projects_tree = ttk.Treeview(projects_frame, columns=columns, show='headings', height=8)
self.projects_tree.heading('status', text='Status')
self.projects_tree.heading('project', text='Projekt')
self.projects_tree.heading('scans', text='Scans')
self.projects_tree.heading('lsd_files', text='LSD-Dateien')
self.projects_tree.heading('png_files', text='PNG-Dateien')
self.projects_tree.column('status', width=100)
self.projects_tree.column('project', width=250)
self.projects_tree.column('scans', width=80)
self.projects_tree.column('lsd_files', width=100)
self.projects_tree.column('png_files', width=100)
scrollbar = ttk.Scrollbar(projects_frame, orient=tk.VERTICAL, command=self.projects_tree.yview)
self.projects_tree.configure(yscrollcommand=scrollbar.set)
self.projects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Preview frame
preview_frame = ttk.LabelFrame(main_frame, text="Vorschau der Merge-Operationen", padding="5")
preview_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
self.preview_text = scrolledtext.ScrolledText(preview_frame, wrap=tk.WORD, height=10, font=('Consolas', 9))
self.preview_text.pack(fill=tk.BOTH, expand=True)
self.preview_text.config(state=tk.DISABLED)
# Progress section
progress_frame = ttk.LabelFrame(main_frame, text="Fortschritt", padding="5")
progress_frame.pack(fill=tk.X, pady=(0, 10))
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100)
self.progress_bar.pack(fill=tk.X, pady=(0, 5))
self.progress_label = ttk.Label(progress_frame, text="")
self.progress_label.pack(fill=tk.X)
# Buttons
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X)
self.preview_btn = ttk.Button(button_frame, text="Vorschau", command=self.show_preview, state=tk.DISABLED)
self.preview_btn.pack(side=tk.RIGHT, padx=(5, 0))
self.merge_btn = ttk.Button(button_frame, text="🔀 Merge durchführen", command=self.execute_merge, state=tk.DISABLED)
self.merge_btn.pack(side=tk.RIGHT)
ttk.Button(button_frame, text="Beenden", command=self.root.quit).pack(side=tk.LEFT)
def go_back(self):
"""Return to main menu."""
if self.processing:
messagebox.showwarning("Warnung", "Bitte warten Sie, bis der Merge abgeschlossen ist.")
return
if self.return_callback:
self.return_callback()
def on_mode_change(self):
"""Handle mode change."""
if self.merge_mode.get() == "single":
self.source_mode_label.config(text="Wählen Sie ein einzelnes Quellprojekt aus")
else:
self.source_mode_label.config(text="Wählen Sie ein Hauptverzeichnis mit mehreren Quellprojekten aus")
self.source_projects = []
self.projects_tree.delete(*self.projects_tree.get_children())
self.update_buttons()
def select_target(self):
"""Select target project directory."""
directory = filedialog.askdirectory(title="Stammprojekt auswählen")
if directory:
self.target_dir.set(directory)
self.analyze_target()
def select_source(self):
"""Select source project/directory."""
directory = filedialog.askdirectory(title="Quellprojekt(e) auswählen")
if directory:
self.source_dir.set(directory)
def find_pointcloud_folder(self, root_dir):
"""Find the PointCloud folder in the root directory."""
root_name = os.path.basename(root_dir)
expected_name = f"{root_name}_PointCloud"
expected_path = os.path.join(root_dir, expected_name)
if os.path.isdir(expected_path):
return expected_path
for item in os.listdir(root_dir):
item_path = os.path.join(root_dir, item)
if os.path.isdir(item_path) and item.endswith("_PointCloud"):
return item_path
return None
def find_lsdx_file(self, pointcloud_dir):
"""Find the LSDX file in the PointCloud directory."""
for item in os.listdir(pointcloud_dir):
if item.endswith(".lsdx"):
return os.path.join(pointcloud_dir, item)
return None
def count_files(self, directory, extension):
"""Count files with given extension."""
if not directory or not os.path.isdir(directory):
return 0
return len([f for f in os.listdir(directory) if f.endswith(extension)])
def get_files(self, directory, extension):
"""Get list of files with given extension."""
if not directory or not os.path.isdir(directory):
return []
return [f for f in os.listdir(directory) if f.endswith(extension)]
def analyze_target(self):
"""Analyze the target project."""
target_dir = self.target_dir.get()
if not target_dir or not os.path.isdir(target_dir):
messagebox.showerror("Fehler", "Bitte wählen Sie ein gültiges Stammprojekt aus.")
return
pointcloud_dir = self.find_pointcloud_folder(target_dir)
if not pointcloud_dir:
messagebox.showerror("Fehler", "Kein PointCloud-Ordner im Stammprojekt gefunden!")
self.target_project = None
self.update_buttons()
return
lsdx_file = self.find_lsdx_file(pointcloud_dir)
if not lsdx_file:
messagebox.showerror("Fehler", "Keine LSDX-Datei im Stammprojekt gefunden!")
self.target_project = None
self.update_buttons()
return
preview_dir = os.path.join(pointcloud_dir, "Previews")
if not os.path.isdir(preview_dir):
preview_dir = None
lsd_count = self.count_files(pointcloud_dir, '.lsd')
png_count = self.count_files(preview_dir, '.png') if preview_dir else 0
# Count scans in LSDX
scan_count = 0
try:
tree = ET.parse(lsdx_file)
root = tree.getroot()
scan_count = len(root.findall(".//Element[@type='scan']"))
except:
pass
self.target_project = {
'path': target_dir,
'name': os.path.basename(target_dir),
'pointcloud': pointcloud_dir,
'preview': preview_dir,
'lsdx': lsdx_file,
'scan_count': scan_count,
'lsd_count': lsd_count,
'png_count': png_count
}
info = f"{self.target_project['name']} | Scans: {scan_count} | LSD: {lsd_count} | PNG: {png_count}"
self.target_info.config(text=info, foreground="green")
self.update_buttons()
def scan_source(self):
"""Scan source directory for projects."""
source_dir = self.source_dir.get()
if not source_dir or not os.path.isdir(source_dir):
messagebox.showerror("Fehler", "Bitte wählen Sie ein gültiges Quellverzeichnis aus.")
return
self.source_projects = []
self.projects_tree.delete(*self.projects_tree.get_children())
if self.merge_mode.get() == "single":
# Single project mode
project = self.analyze_source_project(source_dir)
if project:
self.source_projects.append(project)
self.projects_tree.insert('', tk.END, values=(
'Bereit', project['name'], project['scan_count'],
project['lsd_count'], project['png_count']
))
else:
messagebox.showerror("Fehler", "Kein gültiges PointCab-Projekt gefunden!")
else:
# Batch mode - scan subdirectories
for item in sorted(os.listdir(source_dir)):
item_path = os.path.join(source_dir, item)
if os.path.isdir(item_path):
# Skip if this is the target project
if self.target_project and os.path.samefile(item_path, self.target_project['path']):
continue
project = self.analyze_source_project(item_path)
if project:
self.source_projects.append(project)
self.projects_tree.insert('', tk.END, values=(
'Bereit', project['name'], project['scan_count'],
project['lsd_count'], project['png_count']
))
if self.source_projects:
self.source_mode_label.config(text=f"{len(self.source_projects)} Quellprojekt(e) gefunden", foreground="green")
else:
self.source_mode_label.config(text="Keine gültigen Quellprojekte gefunden", foreground="red")
self.update_buttons()
def analyze_source_project(self, project_dir):
"""Analyze a source project and return its info."""
pointcloud_dir = self.find_pointcloud_folder(project_dir)
if not pointcloud_dir:
return None
lsdx_file = self.find_lsdx_file(pointcloud_dir)
if not lsdx_file:
return None
preview_dir = os.path.join(pointcloud_dir, "Previews")
if not os.path.isdir(preview_dir):
preview_dir = None
lsd_count = self.count_files(pointcloud_dir, '.lsd')
png_count = self.count_files(preview_dir, '.png') if preview_dir else 0
# Count scans in LSDX
scan_count = 0
try:
tree = ET.parse(lsdx_file)
root = tree.getroot()
scan_count = len(root.findall(".//Element[@type='scan']"))
except:
pass
return {
'path': project_dir,
'name': os.path.basename(project_dir),
'pointcloud': pointcloud_dir,
'preview': preview_dir,
'lsdx': lsdx_file,
'scan_count': scan_count,
'lsd_count': lsd_count,
'png_count': png_count,
'lsd_files': self.get_files(pointcloud_dir, '.lsd'),
'png_files': self.get_files(preview_dir, '.png') if preview_dir else []
}
def update_buttons(self):
"""Update button states based on current selection."""
if self.target_project and self.source_projects:
self.preview_btn.config(state=tk.NORMAL)
self.merge_btn.config(state=tk.NORMAL)
else:
self.preview_btn.config(state=tk.DISABLED)
self.merge_btn.config(state=tk.DISABLED)
def get_conflict_resolved_name(self, filename, existing_files, suffix_counter):
"""Get a conflict-resolved filename."""
base, ext = os.path.splitext(filename)
new_name = filename
while new_name in existing_files:
suffix_counter[0] += 1
new_name = f"{base}_merged_{suffix_counter[0]}{ext}"
return new_name
def show_preview(self):
"""Show preview of merge operations."""
if not self.target_project or not self.source_projects:
return
preview_text = "=== MERGE-VORSCHAU ===\n\n"
preview_text += f"🎯 STAMMPROJEKT: {self.target_project['name']}\n"
preview_text += f" Aktuelle Scans: {self.target_project['scan_count']}\n"
preview_text += f" LSD-Dateien: {self.target_project['lsd_count']}\n"
preview_text += f" PNG-Dateien: {self.target_project['png_count']}\n\n"
# Get existing files in target
existing_lsd = set(self.get_files(self.target_project['pointcloud'], '.lsd'))
existing_png = set(self.get_files(self.target_project['preview'], '.png')) if self.target_project['preview'] else set()
total_new_scans = 0
total_new_lsd = 0
total_new_png = 0
for project in self.source_projects:
preview_text += f"📁 QUELLPROJEKT: {project['name']}\n"
preview_text += f" Scans: {project['scan_count']}\n"
# Check for LSD conflicts
lsd_conflicts = 0
for lsd in project.get('lsd_files', []):
if lsd in existing_lsd:
lsd_conflicts += 1
preview_text += f" LSD-Dateien: {project['lsd_count']}"
if lsd_conflicts > 0:
preview_text += f" ({lsd_conflicts} Namenskonflikte → werden umbenannt)"
preview_text += "\n"
# Check for PNG conflicts
png_conflicts = 0
for png in project.get('png_files', []):
if png in existing_png:
png_conflicts += 1
preview_text += f" PNG-Dateien: {project['png_count']}"
if png_conflicts > 0:
preview_text += f" ({png_conflicts} Namenskonflikte → werden umbenannt)"
preview_text += "\n\n"
total_new_scans += project['scan_count']
total_new_lsd += project['lsd_count']
total_new_png += project['png_count']
# Add to existing for next iteration
for lsd in project.get('lsd_files', []):
existing_lsd.add(lsd)
for png in project.get('png_files', []):
existing_png.add(png)
preview_text += "=== ZUSAMMENFASSUNG ===\n"
preview_text += f"Neue Scans: {total_new_scans}\n"
preview_text += f"Neue LSD-Dateien: {total_new_lsd}\n"
preview_text += f"Neue PNG-Dateien: {total_new_png}\n"
preview_text += f"\nNach Merge:\n"
preview_text += f" Scans gesamt: {self.target_project['scan_count'] + total_new_scans}\n"
preview_text += f" LSD-Dateien: {self.target_project['lsd_count'] + total_new_lsd}\n"
preview_text += f" PNG-Dateien: {self.target_project['png_count'] + total_new_png}\n"
preview_text += "\n⚠️ HINWEIS:\n"
preview_text += " - Ein Backup der Stammprojekt-LSDX wird erstellt\n"
preview_text += " - Bei Namenskonflikten werden Dateien mit Suffix '_merged_N' umbenannt\n"
preview_text += " - Alle Referenzen in der LSDX werden entsprechend aktualisiert\n"
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete(1.0, tk.END)
self.preview_text.insert(tk.END, preview_text)
self.preview_text.config(state=tk.DISABLED)
def setup_merge_logging(self, target_dir):
"""Setup merge logging."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_file = os.path.join(target_dir, f"merge_{timestamp}.log")
self.merge_logger = logging.getLogger(f'ProjectMerger_{timestamp}')
self.merge_logger.setLevel(logging.INFO)
self.merge_logger.handlers.clear()
fh = logging.FileHandler(log_file, encoding='utf-8')
fh.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
self.merge_logger.addHandler(fh)
return log_file
def log_message(self, message, level='info'):
"""Log message."""
if self.merge_logger:
if level == 'error':
self.merge_logger.error(message)
else:
self.merge_logger.info(message)
def update_project_status(self, index, status):
"""Update project status in treeview."""
if index < len(self.projects_tree.get_children()):
item_id = self.projects_tree.get_children()[index]
values = list(self.projects_tree.item(item_id)['values'])
values[0] = status
self.projects_tree.item(item_id, values=values)
self.root.update_idletasks()
def execute_merge(self):
"""Execute the merge operation."""
if not self.target_project or not self.source_projects:
return
result = messagebox.askyesno(
"Bestätigung",
f"Möchten Sie wirklich {len(self.source_projects)} Projekt(e) in das Stammprojekt mergen?\n\n"
"Ein Backup der LSDX-Datei wird automatisch erstellt."
)
if not result:
return
self.processing = True
self.merge_btn.config(state=tk.DISABLED)
self.preview_btn.config(state=tk.DISABLED)
# Setup logging
log_file = self.setup_merge_logging(self.target_project['path'])
self.log_message("=== MERGE GESTARTET ===")
self.log_message(f"Stammprojekt: {self.target_project['name']}")
self.log_message(f"Quellprojekte: {len(self.source_projects)}")
self.log_message(f"Log-Datei: {log_file}")
# Update preview with log
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete(1.0, tk.END)
self.preview_text.insert(tk.END, f"=== MERGE LÄUFT ===\n\nLog: {log_file}\n\n")
try:
# Create backup of target LSDX
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = f"{self.target_project['lsdx']}.backup_{timestamp}"
shutil.copy2(self.target_project['lsdx'], backup_path)
self.log_message(f"Backup erstellt: {backup_path}")
self.preview_text.insert(tk.END, f"✓ Backup erstellt\n")
# Parse target LSDX
target_tree = ET.parse(self.target_project['lsdx'])
target_root = target_tree.getroot()
target_elements = target_root.find('Elements')
# Get existing files
existing_lsd = set(self.get_files(self.target_project['pointcloud'], '.lsd'))
existing_png = set(self.get_files(self.target_project['preview'], '.png')) if self.target_project['preview'] else set()
# Track UUID mappings for parent references
suffix_counter = [0]
success_count = 0
error_count = 0
for i, project in enumerate(self.source_projects):
self.progress_var.set((i / len(self.source_projects)) * 100)
self.progress_label.config(text=f"Merging {i+1}/{len(self.source_projects)}: {project['name']}")
self.update_project_status(i, 'Verarbeite...')
self.root.update_idletasks()
try:
self.merge_single_project(
project,
target_elements,
existing_lsd,
existing_png,
suffix_counter
)
self.update_project_status(i, '✓ Erfolgreich')
self.preview_text.insert(tk.END, f"{project['name']} gemerged\n")
success_count += 1
except Exception as e:
self.update_project_status(i, '✗ Fehler')
self.preview_text.insert(tk.END, f"✗ Fehler bei {project['name']}: {e}\n")
self.log_message(f"Fehler bei {project['name']}: {e}", 'error')
error_count += 1
self.preview_text.see(tk.END)
self.root.update_idletasks()
# Zähle finale Elemente vor dem Speichern
final_clusters = len(target_elements.findall("Element[@type='cluster']"))
final_scans = len(target_elements.findall("Element[@type='scan']"))
self.log_message(f"")
self.log_message(f"{'='*60}")
self.log_message(f"FINALE LSDX STATISTIK")
self.log_message(f"{'='*60}")
self.log_message(f" Cluster gesamt: {final_clusters}")
self.log_message(f" Scans gesamt: {final_scans}")
# Save merged LSDX
target_tree.write(self.target_project['lsdx'], encoding='utf-8', xml_declaration=True)
# Add DOCTYPE back
with open(self.target_project['lsdx'], 'r', encoding='utf-8') as f:
content = f.read()
if '<!DOCTYPE LSDX>' not in content:
content = content.replace(
"<?xml version='1.0' encoding='utf-8'?>",
"<?xml version='1.0' encoding='utf-8'?>\n<!DOCTYPE LSDX>"
)
with open(self.target_project['lsdx'], 'w', encoding='utf-8') as f:
f.write(content)
self.log_message("LSDX-Datei gespeichert")
self.log_message(f"Pfad: {self.target_project['lsdx']}")
self.progress_var.set(100)
self.progress_label.config(text="Fertig")
# Summary
self.log_message("=== MERGE ABGESCHLOSSEN ===")
self.log_message(f"Erfolgreich: {success_count}")
self.log_message(f"Fehlgeschlagen: {error_count}")
self.preview_text.insert(tk.END, f"\n=== ZUSAMMENFASSUNG ===\n")
self.preview_text.insert(tk.END, f"Erfolgreich: {success_count}\n")
self.preview_text.insert(tk.END, f"Fehlgeschlagen: {error_count}\n")
self.preview_text.insert(tk.END, f"Log: {log_file}\n")
self.preview_text.config(state=tk.DISABLED)
messagebox.showinfo(
"Merge abgeschlossen",
f"Merge abgeschlossen!\n\n"
f"Erfolgreich: {success_count}\n"
f"Fehlgeschlagen: {error_count}\n\n"
f"Details siehe: {log_file}"
)
except Exception as e:
self.log_message(f"Kritischer Fehler: {e}", 'error')
messagebox.showerror("Fehler", f"Fehler beim Merge:\n{e}")
finally:
self.processing = False
self.merge_btn.config(state=tk.NORMAL)
self.preview_btn.config(state=tk.NORMAL)
def merge_single_project(self, source_project, target_elements, existing_lsd, existing_png, suffix_counter):
"""
Merge a single source project into target.
LSDX Struktur:
- Registration (Wurzel, parents="")
- Cluster (parents=registration_uuid, name="ClusterName")
- Scan (parents=cluster_uuid, name="1", enthält FilePath-Referenzen)
Merge-Logik:
1. Finde Target-Registration UUID
2. Für jeden Cluster im Quellprojekt:
- Prüfe ob Cluster mit gleichem Namen im Ziel existiert
- Wenn ja: Verwende existierende Cluster-UUID für Scans
- Wenn nein: Füge neuen Cluster hinzu mit neuer UUID
3. Für jeden Scan: Füge hinzu mit korrekter Cluster-UUID als Parent
"""
self.log_message(f"")
self.log_message(f"{'='*60}")
self.log_message(f"MERGE START: {source_project['name']}")
self.log_message(f"{'='*60}")
# Parse source LSDX
source_tree = ET.parse(source_project['lsdx'])
source_root = source_tree.getroot()
source_elements = source_root.find('Elements')
if source_elements is None:
self.log_message("FEHLER: Keine Elements in Quell-LSDX gefunden!")
raise ValueError("Keine Elements in Quell-LSDX")
# === SCHRITT 1: Finde Target-Registration UUID ===
target_registration_uuid = None
for elem in target_elements.findall("Element[@type='registration']"):
target_registration_uuid = elem.get('uuid')
self.log_message(f"[TARGET] Registration UUID: {target_registration_uuid}")
break
if not target_registration_uuid:
self.log_message("FEHLER: Keine Registration im Ziel gefunden!")
raise ValueError("Keine Registration im Ziel-LSDX")
# === SCHRITT 2: Sammle existierende Cluster im Target ===
target_clusters = {} # name -> uuid
for elem in target_elements.findall("Element[@type='cluster']"):
cluster_name = elem.get('name', '')
cluster_uuid = elem.get('uuid')
target_clusters[cluster_name] = cluster_uuid
self.log_message(f"[TARGET] Existierender Cluster: '{cluster_name}' UUID={cluster_uuid}")
self.log_message(f"[TARGET] Cluster gesamt: {len(target_clusters)}")
# === SCHRITT 3: Analysiere Source-Struktur ===
source_registration_uuid = None
source_clusters = {} # source_uuid -> {name, scans: []}
source_scans = []
for elem in source_elements.findall("Element"):
elem_type = elem.get('type')
elem_uuid = elem.get('uuid')
elem_name = elem.get('name', '')
elem_parents = elem.get('parents', '')
if elem_type == 'registration':
source_registration_uuid = elem_uuid
self.log_message(f"[SOURCE] Registration UUID: {elem_uuid}")
elif elem_type == 'cluster':
source_clusters[elem_uuid] = {'name': elem_name, 'parent': elem_parents, 'scans': []}
self.log_message(f"[SOURCE] Cluster: '{elem_name}' UUID={elem_uuid} parent={elem_parents}")
elif elem_type == 'scan':
source_scans.append({'elem': elem, 'uuid': elem_uuid, 'name': elem_name, 'parent': elem_parents})
self.log_message(f"[SOURCE] Scan: '{elem_name}' UUID={elem_uuid} parent={elem_parents}")
self.log_message(f"[SOURCE] Cluster: {len(source_clusters)}, Scans: {len(source_scans)}")
# === SCHRITT 4: Ordne Scans den Clustern zu ===
for scan in source_scans:
scan_parent = scan['parent']
if scan_parent in source_clusters:
source_clusters[scan_parent]['scans'].append(scan)
else:
self.log_message(f"WARNUNG: Scan '{scan['name']}' hat unbekannten Parent: {scan_parent}")
# === SCHRITT 5: Dateien kopieren ===
lsd_renames = {} # old_name -> new_name
png_renames = {} # old_name -> new_name
self.log_message(f"")
self.log_message(f"--- Kopiere LSD-Dateien ---")
for lsd_file in source_project.get('lsd_files', []):
src_path = os.path.join(source_project['pointcloud'], lsd_file)
new_name = self.get_conflict_resolved_name(lsd_file, existing_lsd, suffix_counter)
dst_path = os.path.join(self.target_project['pointcloud'], new_name)
if os.path.exists(src_path):
shutil.copy2(src_path, dst_path)
existing_lsd.add(new_name)
if lsd_file != new_name:
lsd_renames[lsd_file] = new_name
self.log_message(f" LSD kopiert (umbenannt): {lsd_file}{new_name}")
else:
self.log_message(f" LSD kopiert: {lsd_file}")
self.log_message(f"--- Kopiere PNG-Dateien ---")
if source_project['preview'] and self.target_project['preview']:
if not os.path.exists(self.target_project['preview']):
os.makedirs(self.target_project['preview'])
for png_file in source_project.get('png_files', []):
src_path = os.path.join(source_project['preview'], png_file)
new_name = self.get_conflict_resolved_name(png_file, existing_png, suffix_counter)
dst_path = os.path.join(self.target_project['preview'], new_name)
if os.path.exists(src_path):
shutil.copy2(src_path, dst_path)
existing_png.add(new_name)
if png_file != new_name:
png_renames[png_file] = new_name
self.log_message(f" PNG kopiert (umbenannt): {png_file}{new_name}")
else:
self.log_message(f" PNG kopiert: {png_file}")
# === SCHRITT 6: Cluster und Scans hinzufügen ===
self.log_message(f"")
self.log_message(f"--- Füge Cluster und Scans hinzu ---")
cluster_uuid_mapping = {} # source_cluster_uuid -> target_cluster_uuid
clusters_added = 0
clusters_reused = 0
scans_added = 0
for source_cluster_uuid, cluster_info in source_clusters.items():
cluster_name = cluster_info['name']
# Prüfe ob Cluster mit gleichem Namen im Ziel existiert
if cluster_name in target_clusters:
# Cluster existiert bereits - verwende existierende UUID
target_cluster_uuid = target_clusters[cluster_name]
cluster_uuid_mapping[source_cluster_uuid] = target_cluster_uuid
self.log_message(f" CLUSTER EXISTIERT: '{cluster_name}' → verwende UUID {target_cluster_uuid}")
clusters_reused += 1
else:
# Neuen Cluster hinzufügen
new_cluster_uuid = "{" + str(uuid.uuid4()) + "}"
cluster_uuid_mapping[source_cluster_uuid] = new_cluster_uuid
# Finde das Cluster-Element im Source
for elem in source_elements.findall("Element[@type='cluster']"):
if elem.get('uuid') == source_cluster_uuid:
new_cluster = copy.deepcopy(elem)
new_cluster.set('uuid', new_cluster_uuid)
new_cluster.set('parents', target_registration_uuid) # Parent = Target-Registration
target_elements.append(new_cluster)
# Merke für zukünftige Projekte
target_clusters[cluster_name] = new_cluster_uuid
self.log_message(f" CLUSTER HINZUGEFÜGT: '{cluster_name}' UUID={new_cluster_uuid} parent={target_registration_uuid}")
clusters_added += 1
break
# Füge Scans dieses Clusters hinzu
target_cluster_uuid = cluster_uuid_mapping[source_cluster_uuid]
for scan_info in cluster_info['scans']:
scan_elem = scan_info['elem']
new_scan_uuid = "{" + str(uuid.uuid4()) + "}"
new_scan = copy.deepcopy(scan_elem)
new_scan.set('uuid', new_scan_uuid)
new_scan.set('parents', target_cluster_uuid) # Parent = Cluster-UUID
# Update FilePath references
for filepath in new_scan.findall(".//FilePath"):
file_type = filepath.get('type')
text = filepath.text or ''
if file_type == 'lsd' and text:
if text in lsd_renames:
old_text = text
filepath.text = lsd_renames[text]
self.log_message(f" FilePath lsd: {old_text}{filepath.text}")
elif file_type == 'preview' and text:
if text.startswith("Previews/"):
png_name = text[9:]
if png_name in png_renames:
old_text = text
filepath.text = f"Previews/{png_renames[png_name]}"
self.log_message(f" FilePath preview: {old_text}{filepath.text}")
target_elements.append(new_scan)
self.log_message(f" SCAN HINZUGEFÜGT: '{scan_info['name']}' UUID={new_scan_uuid} parent={target_cluster_uuid}")
scans_added += 1
# === ZUSAMMENFASSUNG ===
self.log_message(f"")
self.log_message(f"--- MERGE ZUSAMMENFASSUNG ---")
self.log_message(f" Cluster hinzugefügt: {clusters_added}")
self.log_message(f" Cluster wiederverwendet: {clusters_reused}")
self.log_message(f" Scans hinzugefügt: {scans_added}")
self.log_message(f" LSD-Dateien kopiert: {len(source_project.get('lsd_files', []))}")
self.log_message(f" PNG-Dateien kopiert: {len(source_project.get('png_files', []))}")
self.log_message(f"{'='*60}")
class MainMenu:
"""Main menu for selecting between Single Project, Batch mode, and Project Merger."""
def __init__(self, root):
self.root = root
self.root.title("PointCab Projekt Umbenenner v4.1")
self.root.geometry("500x450")
self.root.resizable(False, False)
self.current_frame = None
self.setup_menu()
def setup_menu(self):
"""Setup the main menu."""
self.clear_window()
self.root.geometry("500x450")
self.root.resizable(False, False)
main_frame = ttk.Frame(self.root, padding="30")
main_frame.pack(fill=tk.BOTH, expand=True)
# Title
title_label = ttk.Label(main_frame, text="PointCab Projekt Umbenenner", font=('Helvetica', 18, 'bold'))
title_label.pack(pady=(0, 5))
version_label = ttk.Label(main_frame, text="Version 4.1", font=('Helvetica', 10), foreground='gray')
version_label.pack(pady=(0, 30))
# Description
desc_label = ttk.Label(
main_frame,
text="Wählen Sie einen Modus:",
font=('Helvetica', 11)
)
desc_label.pack(pady=(0, 20))
# Buttons frame
buttons_frame = ttk.Frame(main_frame)
buttons_frame.pack(fill=tk.X, pady=10)
# Style for big buttons
style = ttk.Style()
style.configure('Big.TButton', font=('Helvetica', 12), padding=15)
# Single project button
single_btn = ttk.Button(
buttons_frame,
text="📁 Einzelprojekt bearbeiten",
style='Big.TButton',
command=self.open_single_project
)
single_btn.pack(fill=tk.X, pady=5)
single_desc = ttk.Label(
buttons_frame,
text="Ein einzelnes PointCab-Projekt auswählen und verarbeiten",
foreground='gray'
)
single_desc.pack(pady=(0, 15))
# Batch button
batch_btn = ttk.Button(
buttons_frame,
text="📂 Batch Renamer",
style='Big.TButton',
command=self.open_batch_renamer
)
batch_btn.pack(fill=tk.X, pady=5)
batch_desc = ttk.Label(
buttons_frame,
text="Mehrere Projekte in einem Hauptverzeichnis automatisch verarbeiten",
foreground='gray'
)
batch_desc.pack(pady=(0, 15))
# Merger button
merger_btn = ttk.Button(
buttons_frame,
text="🔀 Projektmerger",
style='Big.TButton',
command=self.open_project_merger
)
merger_btn.pack(fill=tk.X, pady=5)
merger_desc = ttk.Label(
buttons_frame,
text="Mehrere Projekte in ein Stammprojekt zusammenführen",
foreground='gray'
)
merger_desc.pack(pady=(0, 15))
# Exit button
ttk.Button(main_frame, text="Beenden", command=self.root.quit).pack(pady=(20, 0))
def clear_window(self):
"""Clear all widgets from window."""
for widget in self.root.winfo_children():
widget.destroy()
def open_single_project(self):
"""Open single project mode."""
self.clear_window()
self.root.geometry("850x750")
self.root.resizable(True, True)
PointCabRenamer(self.root, return_callback=self.setup_menu)
def open_batch_renamer(self):
"""Open batch renamer mode."""
self.clear_window()
self.root.geometry("900x700")
self.root.resizable(True, True)
BatchRenamer(self.root, return_callback=self.setup_menu)
def open_project_merger(self):
"""Open project merger mode."""
self.clear_window()
self.root.geometry("1000x800")
self.root.resizable(True, True)
ProjectMerger(self.root, return_callback=self.setup_menu)
def main():
"""Main entry point."""
root = tk.Tk()
app = MainMenu(root)
root.mainloop()
if __name__ == "__main__":
main()