Skip to content

Commit 426a396

Browse files
lukischclaude
andcommitted
fix: Bugsweep Lauf #8 — 10 Bugs behoben (QThread-Leaks, OSError, SQLite, DB-Spalte)
Bug #8-1: DuplicateFinderDialog fehlte closeEvent → QThread-Destroy-Race Bug #8-2: ExplorerProApp.closeEvent() stoppte index_worker nicht Bug #8-3: FileIndex._init_database() SQLite-Connection-Leak (kein finally) Bug #8-4: AdvancedSearchDialog fehlte closeEvent fuer search_worker Bug #8-5: AdvancedSearchDialog._on_results_ready() las 'name' statt 'filename' Bug #8-6: AppsPanel._save_apps() kein try/except OSError Bug #8-7: PromptsPanel._save_prompts() kein try/except OSError Bug #8-8: SearchPanel._on_results_ready() las 'name' statt 'filename' Bug #8-9: ExplorerProApp.closeEvent() stoppte sidebar.search_panel.search_worker nicht Bug #8-10: SyncPanel._save_config() kein try/except OSError 6 neue Regressionstests; 95/95 Tests gruen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1e1f817 commit 426a396

19 files changed

Lines changed: 586 additions & 107 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/).
3737
- Datei-/Ordner-Öffnen zeigt bei fehlender Systemzuordnung eine UI-Warnung statt still zu scheitern.
3838
- macOS hing beim Datei-/Ordner-Öffnen nicht mehr fälschlich am Linux-Handler `xdg-open`.
3939
- Mojibake in README- und Workflow-Texten bereinigt.
40+
- Die kompakte Haupt-Toolbar exponiert Navigation, Pfadfeld, Suche und Ansichtsmenü jetzt mit klaren Accessible Names, Descriptions und Tooltips statt nur über Pfeilsymbole und Placeholder.
4041

4142
## [1.0.0] - 2026-03-05
4243

src/app.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,24 @@ def closeEvent(self, event):
222222
"""Einstellungen beim Schließen speichern"""
223223
# Privacy Monitor stoppen
224224
self.privacy_monitor.stop()
225-
225+
226+
# IndexWorker stoppen (override run(), kein exec() → cancel() statt quit())
227+
if hasattr(self, "index_worker") and self.index_worker and self.index_worker.isRunning():
228+
self.index_worker.cancel()
229+
self.index_worker.wait(3000)
230+
231+
# SearchPanel-Worker stoppen (Child-Widget bekommt kein closeEvent → explizit stoppen)
232+
if hasattr(self, "sidebar") and self.sidebar:
233+
sp = self.sidebar.search_panel
234+
if sp.search_worker and sp.search_worker.isRunning():
235+
sp.search_worker.cancel()
236+
sp.search_worker.wait(3000)
237+
226238
# Einstellungen speichern
227239
settings = QSettings()
228240
settings.setValue("window/geometry", self.saveGeometry())
229241
settings.setValue("splitter/main", self.main_splitter.sizes())
230242
settings.setValue("splitter/right", self.right_splitter.sizes())
231-
243+
232244
logging.info("ExplorerPro beendet")
233245
super().closeEvent(event)

src/core/file_index.py

Lines changed: 90 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -74,94 +74,96 @@ def __init__(self, db_path: Optional[str] = None):
7474
def _init_database(self):
7575
"""Initialisiert die Datenbankstruktur"""
7676
conn = sqlite3.connect(self.db_path)
77-
cursor = conn.cursor()
78-
79-
# Haupttabelle für Dateien
80-
cursor.execute('''
81-
CREATE TABLE IF NOT EXISTS files (
82-
id INTEGER PRIMARY KEY AUTOINCREMENT,
83-
path TEXT UNIQUE NOT NULL,
84-
filename TEXT NOT NULL,
85-
extension TEXT,
86-
size INTEGER,
87-
modified TIMESTAMP,
88-
created TIMESTAMP,
89-
hash TEXT,
90-
category TEXT,
91-
text_content TEXT,
92-
indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
93-
)
94-
''')
95-
96-
# Tags-Tabelle (Many-to-Many)
97-
cursor.execute('''
98-
CREATE TABLE IF NOT EXISTS tags (
99-
id INTEGER PRIMARY KEY AUTOINCREMENT,
100-
name TEXT UNIQUE NOT NULL
101-
)
102-
''')
103-
104-
cursor.execute('''
105-
CREATE TABLE IF NOT EXISTS file_tags (
106-
file_id INTEGER,
107-
tag_id INTEGER,
108-
PRIMARY KEY (file_id, tag_id),
109-
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE,
110-
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
111-
)
112-
''')
113-
114-
# Notizen-Tabelle
115-
cursor.execute('''
116-
CREATE TABLE IF NOT EXISTS notes (
117-
id INTEGER PRIMARY KEY AUTOINCREMENT,
118-
file_id INTEGER UNIQUE,
119-
content TEXT,
120-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
121-
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
122-
)
123-
''')
124-
125-
# FTS5 für Volltextsuche
126-
cursor.execute('''
127-
CREATE VIRTUAL TABLE IF NOT EXISTS files_fts USING fts5(
128-
path, filename, text_content,
129-
content='files',
130-
content_rowid='id'
131-
)
132-
''')
133-
134-
# Trigger für FTS-Synchronisation
135-
cursor.execute('''
136-
CREATE TRIGGER IF NOT EXISTS files_ai AFTER INSERT ON files BEGIN
137-
INSERT INTO files_fts(rowid, path, filename, text_content)
138-
VALUES (new.id, new.path, new.filename, new.text_content);
139-
END
140-
''')
141-
142-
cursor.execute('''
143-
CREATE TRIGGER IF NOT EXISTS files_ad AFTER DELETE ON files BEGIN
144-
INSERT INTO files_fts(files_fts, rowid, path, filename, text_content)
145-
VALUES ('delete', old.id, old.path, old.filename, old.text_content);
146-
END
147-
''')
148-
149-
cursor.execute('''
150-
CREATE TRIGGER IF NOT EXISTS files_au AFTER UPDATE ON files BEGIN
151-
INSERT INTO files_fts(files_fts, rowid, path, filename, text_content)
152-
VALUES ('delete', old.id, old.path, old.filename, old.text_content);
153-
INSERT INTO files_fts(rowid, path, filename, text_content)
154-
VALUES (new.id, new.path, new.filename, new.text_content);
155-
END
156-
''')
157-
158-
# Indizes
159-
cursor.execute('CREATE INDEX IF NOT EXISTS idx_files_hash ON files(hash)')
160-
cursor.execute('CREATE INDEX IF NOT EXISTS idx_files_category ON files(category)')
161-
cursor.execute('CREATE INDEX IF NOT EXISTS idx_files_extension ON files(extension)')
162-
163-
conn.commit()
164-
conn.close()
77+
try:
78+
cursor = conn.cursor()
79+
80+
# Haupttabelle für Dateien
81+
cursor.execute('''
82+
CREATE TABLE IF NOT EXISTS files (
83+
id INTEGER PRIMARY KEY AUTOINCREMENT,
84+
path TEXT UNIQUE NOT NULL,
85+
filename TEXT NOT NULL,
86+
extension TEXT,
87+
size INTEGER,
88+
modified TIMESTAMP,
89+
created TIMESTAMP,
90+
hash TEXT,
91+
category TEXT,
92+
text_content TEXT,
93+
indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
94+
)
95+
''')
96+
97+
# Tags-Tabelle (Many-to-Many)
98+
cursor.execute('''
99+
CREATE TABLE IF NOT EXISTS tags (
100+
id INTEGER PRIMARY KEY AUTOINCREMENT,
101+
name TEXT UNIQUE NOT NULL
102+
)
103+
''')
104+
105+
cursor.execute('''
106+
CREATE TABLE IF NOT EXISTS file_tags (
107+
file_id INTEGER,
108+
tag_id INTEGER,
109+
PRIMARY KEY (file_id, tag_id),
110+
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE,
111+
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
112+
)
113+
''')
114+
115+
# Notizen-Tabelle
116+
cursor.execute('''
117+
CREATE TABLE IF NOT EXISTS notes (
118+
id INTEGER PRIMARY KEY AUTOINCREMENT,
119+
file_id INTEGER UNIQUE,
120+
content TEXT,
121+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
122+
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
123+
)
124+
''')
125+
126+
# FTS5 für Volltextsuche
127+
cursor.execute('''
128+
CREATE VIRTUAL TABLE IF NOT EXISTS files_fts USING fts5(
129+
path, filename, text_content,
130+
content='files',
131+
content_rowid='id'
132+
)
133+
''')
134+
135+
# Trigger für FTS-Synchronisation
136+
cursor.execute('''
137+
CREATE TRIGGER IF NOT EXISTS files_ai AFTER INSERT ON files BEGIN
138+
INSERT INTO files_fts(rowid, path, filename, text_content)
139+
VALUES (new.id, new.path, new.filename, new.text_content);
140+
END
141+
''')
142+
143+
cursor.execute('''
144+
CREATE TRIGGER IF NOT EXISTS files_ad AFTER DELETE ON files BEGIN
145+
INSERT INTO files_fts(files_fts, rowid, path, filename, text_content)
146+
VALUES ('delete', old.id, old.path, old.filename, old.text_content);
147+
END
148+
''')
149+
150+
cursor.execute('''
151+
CREATE TRIGGER IF NOT EXISTS files_au AFTER UPDATE ON files BEGIN
152+
INSERT INTO files_fts(files_fts, rowid, path, filename, text_content)
153+
VALUES ('delete', old.id, old.path, old.filename, old.text_content);
154+
INSERT INTO files_fts(rowid, path, filename, text_content)
155+
VALUES (new.id, new.path, new.filename, new.text_content);
156+
END
157+
''')
158+
159+
# Indizes
160+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_files_hash ON files(hash)')
161+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_files_category ON files(category)')
162+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_files_extension ON files(extension)')
163+
164+
conn.commit()
165+
finally:
166+
conn.close()
165167

166168
def get_category(self, filename: str) -> str:
167169
"""Bestimmt die Kategorie einer Datei"""

src/gui/main_window.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,43 @@ def _setup_ui(self):
4040
self.back_action.setToolTip("Zurück (Alt+Left)")
4141
self.back_action.setShortcut(QKeySequence("Alt+Left"))
4242
self.addAction(self.back_action)
43+
self._set_action_accessibility(
44+
self.back_action,
45+
"Zurück",
46+
"Zum vorherigen Ordner wechseln.",
47+
)
4348

4449
self.forward_action = QAction("→", self)
4550
self.forward_action.setToolTip("Vorwärts (Alt+Right)")
4651
self.forward_action.setShortcut(QKeySequence("Alt+Right"))
4752
self.addAction(self.forward_action)
53+
self._set_action_accessibility(
54+
self.forward_action,
55+
"Vorwärts",
56+
"Zum nächsten Ordner in der Navigation wechseln.",
57+
)
4858

4959
self.up_action = QAction("↑", self)
5060
self.up_action.setToolTip("Übergeordneter Ordner (Alt+Up)")
5161
self.up_action.setShortcut(QKeySequence("Alt+Up"))
5262
self.addAction(self.up_action)
63+
self._set_action_accessibility(
64+
self.up_action,
65+
"Übergeordneter Ordner",
66+
"Eine Ebene nach oben im aktuellen Ordnerpfad wechseln.",
67+
)
5368

5469
self.addSeparator()
5570

5671
# Pfad-Anzeige
5772
self.path_edit = QLineEdit()
5873
self.path_edit.setPlaceholderText("Pfad eingeben...")
5974
self.path_edit.setMinimumWidth(300)
75+
self.path_edit.setToolTip("Aktuellen Ordnerpfad anzeigen oder direkt eingeben.")
76+
self.path_edit.setAccessibleName("Ordnerpfad")
77+
self.path_edit.setAccessibleDescription(
78+
"Zeigt den aktuellen Ordnerpfad und erlaubt direkte Navigation per Enter."
79+
)
6080
self.addWidget(self.path_edit)
6181

6282
self.addSeparator()
@@ -66,21 +86,46 @@ def _setup_ui(self):
6686
self.search_edit.setPlaceholderText("🔍 Suchen...")
6787
self.search_edit.setMinimumWidth(200)
6888
self.search_edit.setMaximumWidth(300)
89+
self.search_edit.setClearButtonEnabled(True)
90+
self.search_edit.setToolTip("Dateien im Index suchen. Enter startet die Suche.")
91+
self.search_edit.setAccessibleName("Dateisuche")
92+
self.search_edit.setAccessibleDescription(
93+
"Suchbegriff eingeben und mit Enter oder der Schaltfläche Suchen starten."
94+
)
6995
self.search_edit.returnPressed.connect(self._on_search)
7096
self.addWidget(self.search_edit)
7197

7298
# Suche-Button
73-
search_btn = QPushButton("Suchen")
74-
search_btn.clicked.connect(self._on_search)
75-
self.addWidget(search_btn)
99+
self.search_btn = QPushButton("Suchen")
100+
self.search_btn.setToolTip("Suche mit dem eingegebenen Begriff starten.")
101+
self.search_btn.setAccessibleName("Suche starten")
102+
self.search_btn.setAccessibleDescription(
103+
"Startet die Dateisuche mit dem aktuellen Suchbegriff."
104+
)
105+
self.search_btn.clicked.connect(self._on_search)
106+
self.addWidget(self.search_btn)
76107

77108
self.addSeparator()
78109

79110
# Ansicht-Umschalter
80111
self.view_btn = QToolButton()
81112
self.view_btn.setText("Ansicht")
82113
self.view_btn.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
114+
self.view_btn.setToolTip("Ansichten und Seitenleisten-Bereiche umschalten.")
115+
self.view_btn.setAccessibleName("Ansicht umschalten")
116+
self.view_btn.setAccessibleDescription(
117+
"Öffnet das Menü für Ansichtsoptionen sowie App-, Prompt- und Sync-Bereiche."
118+
)
83119
self.addWidget(self.view_btn)
120+
121+
def _set_action_accessibility(self, action: QAction, name: str, description: str) -> None:
122+
widget = self.widgetForAction(action)
123+
if widget is None:
124+
return
125+
widget.setAccessibleName(name)
126+
widget.setAccessibleDescription(description)
127+
widget.setToolTip(action.toolTip())
128+
widget.setWhatsThis(description)
84129

85130
def _on_search(self):
86131
query = self.search_edit.text()

src/gui/sidebar/advanced_search_dialog.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,12 @@ def _setup_ui(self):
269269
# Verbindungen
270270
self.results_table.itemSelectionChanged.connect(self._on_selection_changed)
271271

272+
def closeEvent(self, event):
273+
if self.search_worker and self.search_worker.isRunning():
274+
self.search_worker.cancel()
275+
self.search_worker.wait(3000)
276+
super().closeEvent(event)
277+
272278
def set_index(self, file_index):
273279
"""Setzt den Datei-Index"""
274280
self.file_index = file_index
@@ -383,8 +389,8 @@ def _on_results_ready(self, results: list):
383389
self.results_table.setRowCount(len(results))
384390

385391
for row, result in enumerate(results):
386-
# Name
387-
name_item = QTableWidgetItem(result.get('name', ''))
392+
# Name (DB-Spalte heißt 'filename', nicht 'name')
393+
name_item = QTableWidgetItem(result.get('filename', ''))
388394
name_item.setData(Qt.ItemDataRole.UserRole, result.get('path', ''))
389395
self.results_table.setItem(row, 0, name_item)
390396

src/gui/sidebar/search_panel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ def _on_results_ready(self, results: list):
290290
# Dict zu SearchResult konvertieren
291291
result = SearchResult(
292292
path=result_dict.get('path', ''),
293-
name=result_dict.get('name', ''),
293+
name=result_dict.get('filename', ''), # DB-Spalte heißt 'filename', nicht 'name'
294294
extension=result_dict.get('extension', ''),
295295
size=result_dict.get('size', 0),
296296
modified=result_dict.get('modified'),

src/modules/indexer/duplicate_finder.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,12 @@ def _setup_ui(self):
305305
self.tree.itemSelectionChanged.connect(self._on_selection_changed)
306306
self.tree.itemChanged.connect(self._on_item_changed)
307307

308+
def closeEvent(self, event):
309+
if self.scan_worker and self.scan_worker.isRunning():
310+
self.scan_worker.cancel()
311+
self.scan_worker.wait(3000)
312+
super().closeEvent(event)
313+
308314
def set_index(self, file_index):
309315
"""Setzt den Datei-Index"""
310316
self.file_index = file_index

src/modules/launcher/apps_panel.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,11 @@ def _save_apps(self):
306306
'arguments': a.arguments, 'working_dir': a.working_dir,
307307
'favorite': a.favorite
308308
} for a in self.apps]
309-
310-
with open(self.config_path, 'w', encoding='utf-8') as f:
311-
json.dump(data, f, indent=2, ensure_ascii=False)
309+
try:
310+
with open(self.config_path, 'w', encoding='utf-8') as f:
311+
json.dump(data, f, indent=2, ensure_ascii=False)
312+
except OSError as e:
313+
print(f"Fehler beim Speichern der Apps: {e}")
312314

313315
def _refresh_display(self):
314316
for cat_data in self.category_widgets.values():

src/modules/prompts/prompts_panel.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ def _add_default_prompts(self):
328328
def _save_prompts(self):
329329
"""Speichert Prompts nach JSON"""
330330
self.config_path.parent.mkdir(parents=True, exist_ok=True)
331-
331+
332332
data = []
333333
for p in self.prompts:
334334
data.append({
@@ -337,9 +337,12 @@ def _save_prompts(self):
337337
'created': p.created, 'modified': p.modified,
338338
'favorite': p.favorite, 'use_count': p.use_count
339339
})
340-
341-
with open(self.config_path, 'w', encoding='utf-8') as f:
342-
json.dump(data, f, indent=2, ensure_ascii=False)
340+
341+
try:
342+
with open(self.config_path, 'w', encoding='utf-8') as f:
343+
json.dump(data, f, indent=2, ensure_ascii=False)
344+
except OSError as e:
345+
print(f"Fehler beim Speichern der Prompts: {e}")
343346

344347
def _refresh_list(self, filter_text: str = "", category: str = None):
345348
"""Aktualisiert die Prompt-Liste"""

0 commit comments

Comments
 (0)