Skip to content

Commit a81a2f4

Browse files
committed
feat(a11y): enhance accessibility, tooltips, status tips, and UI diagnostics
1 parent e4e84b2 commit a81a2f4

10 files changed

Lines changed: 299 additions & 26 deletions

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/).
1313

1414
## [0.1.2] - 2026-08-16
1515

16-
### Bugfixes & Plugin-Resilienz (2026-08-20)
16+
### UX, Barrierefreiheit & Accessibility-Härtung (2026-08-21)
17+
18+
- `ui/main_window.py`: StatusTips auf allen Menü- und Toolbar-Aktionen (`Neu`, `Öffnen`, `Speichern`, `Beenden`, `Rückgängig`, `Wiederherstellen`, `Suchen`, `Gehe zu Zeile`, `Plugins`, `Einstellungen`, `Ausführen`, `Stoppen`, `Projektbaum`, `Terminal`, `Tastenkürzel`, `Über`); `setToolTip()`, `setWhatsThis()`, `setAccessibleName()` und `setAccessibleDescription()` für Hauptleiste, Sprach-Auswahl (`lang_combo`), Statusleisten-Widgets (`pos_label`, `lang_label`, `enc_label`) und Reiter im unteren Bedienpanel (`bottom_tabs`).
19+
- `core/tabs.py`: `TabWidget` mit `accessibleName` / `accessibleDescription` versehen und dynamische Tab-Tooltips mit absolutem Dateipfad beim Öffnen, Neuanlegen, Speichern und Drag-and-Drop-Umsortieren implementiert.
20+
- `ui/settings_dialog.py`: Tooltips und `accessibleName` / `accessibleDescription` für alle Einstellungsfelder (`font_combo`, `font_size_spin`, `tab_size_spin`, `theme_combo`, `auto_save_cb`, `minimap_cb`) integriert.
21+
- `ui/shortcuts_dialog.py` & `ui/plugins_dialog.py`: Barrierefreie Beschriftungen, Tooltips und Beschreibungen für Filter-Eingabefelder, Tabellen, Detailboxen und Aktions-Buttons ergänzt.
22+
- `tests/test_ui_ux_accessibility.py`: Neue automatisierte UX- und Accessibility-Vertragstestsuite mit 6 Tests für Toolbar, Tabs, Statusbar, Einstellungen, Shortcuts, Plugins und Panels (109 passed, 1 skipped).
1723

1824
- `core/highlighter.py`: Regex-Mustererstellung in `UniversalHighlighter` (`_keyword_pattern`) gehärtet, sodass Keywords und Builtins mit Satzzeichen/Metazeichen (z.B. Rubys `defined?` oder C++-Symbole) mit `re.escape()` maskiert und mit sicheren Wortgrenzen gematcht werden. Verhindert Fehl-Highlighting von Variablennamen (`define`) und unvollständiges Keyword-Highlighting.
1925
- `languages/declarative.py`: Parsing von `comment_style` in `DeclarativeLanguageProvider.from_dict` erweitert, sodass Einzelstring- (`#`), 1-Element-Listen (`["--"]`), 3-Element-Flachlisten (`["--", "--[[", "--]]"]`) und Dictionary-Formate deterministisch ausgewertet werden.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
[![Ecosystem: dev-bricks](https://img.shields.io/badge/ecosystem-dev--bricks-blue.svg)](https://github.com/dev-bricks)
99
[![Part of: open-bricks](https://img.shields.io/badge/part%20of-open--bricks-blue.svg)](https://github.com/open-bricks)
1010
[![LSP Ready](https://img.shields.io/badge/LSP-ready-purple.svg)]()
11-
[![Tests](https://img.shields.io/badge/tests-103%20passed-brightgreen.svg)]()
11+
[![Tests](https://img.shields.io/badge/tests-109%20passed-brightgreen.svg)]()
1212
[![llms.txt](https://img.shields.io/badge/llms.txt-available-green.svg)](llms.txt)
1313

1414
[Deutsch](README_de.md) | English

README_de.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
[![Ecosystem: dev-bricks](https://img.shields.io/badge/ecosystem-dev--bricks-blue.svg)](https://github.com/dev-bricks)
99
[![Part of: open-bricks](https://img.shields.io/badge/part%20of-open--bricks-blue.svg)](https://github.com/open-bricks)
1010
[![LSP Ready](https://img.shields.io/badge/LSP-ready-purple.svg)]()
11-
[![Tests](https://img.shields.io/badge/tests-103%20passed-brightgreen.svg)]()
11+
[![Tests](https://img.shields.io/badge/tests-109%20passed-brightgreen.svg)]()
1212
[![llms.txt](https://img.shields.io/badge/llms.txt-available-green.svg)](llms.txt)
1313

1414
[English](README.md) | Deutsch

core/tabs.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ class TabWidget(QTabWidget):
7272

7373
def __init__(self, parent=None):
7474
super().__init__(parent)
75+
self.setObjectName("editor_tab_widget")
76+
self.setAccessibleName("Editor-Tabs")
77+
self.setAccessibleDescription("Reiterleiste für geöffnete Code-Dateien")
7578
self.setTabsClosable(True)
7679
self.setMovable(True)
7780
self.tabs: dict = {} # index -> EditorTab
@@ -89,6 +92,7 @@ def _rebuild_tab_map(self):
8992
for tab in old_tabs:
9093
if tab.editor is widget:
9194
self.tabs[i] = tab
95+
self.setTabToolTip(i, str(tab.file_path) if tab.file_path else "Neues Dokument (ungespeichert)")
9296
break
9397

9498
def open_file(self, file_path: Path) -> EditorTab:
@@ -103,6 +107,7 @@ def open_file(self, file_path: Path) -> EditorTab:
103107
tab = EditorTab(file_path)
104108
idx = self.addTab(tab.editor, tab.title)
105109
self.tabs[idx] = tab
110+
self.setTabToolTip(idx, str(file_path))
106111
self.setCurrentIndex(idx)
107112

108113
tab.editor.modificationChanged.connect(
@@ -115,6 +120,7 @@ def new_tab(self) -> EditorTab:
115120
tab = EditorTab()
116121
idx = self.addTab(tab.editor, "Unbenannt")
117122
self.tabs[idx] = tab
123+
self.setTabToolTip(idx, "Neues Dokument (ungespeichert)")
118124
self.setCurrentIndex(idx)
119125
tab.editor.modificationChanged.connect(
120126
lambda _: self._update_tab_title(tab)
@@ -165,4 +171,5 @@ def _update_tab_title(self, tab: EditorTab):
165171
for idx in range(self.count()):
166172
if self.tabs.get(idx) is tab:
167173
self.setTabText(idx, tab.title)
174+
self.setTabToolTip(idx, str(tab.file_path) if tab.file_path else "Neues Dokument (ungespeichert)")
168175
break

llms.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# CodeBox - local PySide6 desktop code editor
22

3-
## Last-checked: 2026-08-20
3+
## Last-checked: 2026-08-21
44

55
> CodeBox is a local-first desktop IDE for Windows developers who want a lightweight PySide6 code editor with tabs, a project tree, an integrated terminal, Git helpers, syntax highlighting, Language Server Protocol diagnostics, and an extensible JSON/Python language plugin system.
66

@@ -31,7 +31,7 @@ Part of the dev-bricks family. Python, MIT.
3131
- `python main.py`: Launches the CodeBox desktop interface.
3232
- `python main.py --open <file>`: Launches and directly opens a target file path.
3333
- `build_exe.bat`: Uses PyInstaller to bundle the application into a standalone executable.
34-
- `python -m pytest`: Runs the automated test suite (103 passed, 1 skipped).
34+
- `python -m pytest`: Runs the automated test suite (109 passed, 1 skipped).
3535

3636
## Related Projects (dev-bricks & ellmos family)
3737

tests/test_ui_ux_accessibility.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
"""Unit- und Contract-Tests für UI-, UX-, Accessibility- und Sprach-Qualität in CodeBox."""
4+
5+
import pytest
6+
from PySide6.QtWidgets import QApplication
7+
8+
from ui.main_window import MainWindow
9+
from ui.settings_dialog import SettingsDialog
10+
from ui.shortcuts_dialog import ShortcutsDialog, SHORTCUTS_DATA
11+
from ui.plugins_dialog import PluginsDialog
12+
from ui.problems_panel import ProblemsPanel
13+
from core.tabs import TabWidget
14+
from core.output import OutputPanel
15+
16+
17+
@pytest.fixture(scope="module")
18+
def qapp():
19+
app = QApplication.instance()
20+
if app is None:
21+
app = QApplication([])
22+
yield app
23+
24+
25+
def test_main_window_toolbar_ux_and_a11y(qapp):
26+
"""Prüft Tooltips, StatusTips und Barrierefreiheit der Haupt-Toolbar."""
27+
window = MainWindow()
28+
try:
29+
# Toolbar Aktionen
30+
assert window.action_new.text() == "Neu"
31+
assert window.action_new.toolTip() == "Neue Datei erstellen (Ctrl+N)"
32+
assert window.action_new.statusTip() == "Erstellt eine neue leere Datei"
33+
assert len(window.action_new.whatsThis()) > 5
34+
35+
assert window.action_open.text() == "Öffnen"
36+
assert window.action_open.toolTip() == "Datei öffnen (Ctrl+O)"
37+
assert window.action_open.statusTip() == "Öffnet eine bestehende Datei von der Festplatte"
38+
assert len(window.action_open.whatsThis()) > 5
39+
40+
assert window.action_save.text() == "Speichern"
41+
assert window.action_save.toolTip() == "Aktuelle Datei speichern (Ctrl+S)"
42+
assert window.action_save.statusTip() == "Speichert die aktive Datei auf die Festplatte"
43+
assert len(window.action_save.whatsThis()) > 5
44+
45+
assert window.action_run.text() == "Ausführen"
46+
assert window.action_run.toolTip() == "Aktuelle Datei ausführen (F5)"
47+
assert window.action_run.statusTip() == "Führt das aktuelle Skript oder Programm aus"
48+
assert len(window.action_run.whatsThis()) > 5
49+
50+
# Sprach-Combo
51+
assert "Syntax-Highlighting" in window.lang_combo.toolTip()
52+
assert window.lang_combo.accessibleName() == "Programmiersprache"
53+
assert len(window.lang_combo.accessibleDescription()) > 10
54+
55+
# Statusbar
56+
assert window.pos_label.accessibleName() == "Cursorposition"
57+
assert "Cursor-Position" in window.pos_label.toolTip()
58+
assert window.lang_label.accessibleName() == "Aktive Sprache"
59+
assert window.enc_label.accessibleName() == "Dateikodierung"
60+
assert window.enc_label.text() == "UTF-8"
61+
62+
# Bottom Tabs
63+
assert window.bottom_tabs.accessibleName() == "Unteres Bedienpanel"
64+
assert window.bottom_tabs.tabToolTip(0) == "Programmausgabe und Fehlermeldungen anzeigen"
65+
assert window.bottom_tabs.tabToolTip(1) == "Integriertes Befehlszeilenterminal"
66+
assert window.bottom_tabs.tabToolTip(2) == "LSP- und Linter-Diagnosen und Fehlermeldungen"
67+
finally:
68+
window.close()
69+
70+
71+
def test_tab_widget_ux_tooltips_and_a11y(qapp, tmp_path):
72+
"""Prüft Tab-Tooltips mit echtem Pfad und Accessible-Attribute."""
73+
tabs = TabWidget()
74+
assert tabs.accessibleName() == "Editor-Tabs"
75+
assert "Reiterleiste" in tabs.accessibleDescription()
76+
77+
# Leerer neuer Tab
78+
tab1 = tabs.new_tab()
79+
assert tabs.tabToolTip(0) == "Neues Dokument (ungespeichert)"
80+
81+
# Datei-Tab mit Pfad
82+
test_file = tmp_path / "main.py"
83+
test_file.write_text("print('hello')", encoding="utf-8")
84+
tabs.open_file(test_file)
85+
assert tabs.tabToolTip(1) == str(test_file)
86+
87+
# Nach Speichern aktualisiert
88+
tab1.file_path = tmp_path / "neu.py"
89+
tab1.save()
90+
tabs._update_tab_title(tab1)
91+
assert tabs.tabToolTip(0) == str(tab1.file_path)
92+
93+
94+
def test_settings_dialog_ux_and_a11y(qapp):
95+
"""Prüft Tooltips und AccessibleNames aller Steuerelemente im Einstellungsdialog."""
96+
dialog = SettingsDialog()
97+
assert dialog.font_combo.accessibleName() == "Schriftart"
98+
assert len(dialog.font_combo.toolTip()) > 5
99+
assert dialog.font_size_spin.accessibleName() == "Schriftgröße"
100+
assert len(dialog.font_size_spin.toolTip()) > 5
101+
assert dialog.tab_size_spin.accessibleName() == "Tab-Breite"
102+
assert len(dialog.tab_size_spin.toolTip()) > 5
103+
assert dialog.theme_combo.accessibleName() == "Farbschema"
104+
assert len(dialog.theme_combo.toolTip()) > 5
105+
assert dialog.auto_save_cb.accessibleName() == "Dateien beim Ausführen automatisch speichern"
106+
assert len(dialog.auto_save_cb.toolTip()) > 5
107+
assert dialog.minimap_cb.accessibleName() == "Minimap anzeigen"
108+
assert len(dialog.minimap_cb.toolTip()) > 5
109+
110+
111+
def test_shortcuts_dialog_ux_and_a11y(qapp):
112+
"""Prüft Suchfilter, Tabelle und Accessible-Attribute im ShortcutsDialog."""
113+
dialog = ShortcutsDialog()
114+
assert dialog.search_input.accessibleName() == "Tastenkürzel filtern"
115+
assert len(dialog.search_input.toolTip()) > 5
116+
assert dialog.table.accessibleName() == "Tastenkürzel-Tabelle"
117+
assert dialog.btn_close.accessibleName() == "Tastenkürzel-Übersicht schließen"
118+
119+
# Filterung testen
120+
assert dialog.table.rowCount() == len(SHORTCUTS_DATA)
121+
dialog.filter_table("Ctrl+S")
122+
visible_rows = [r for r in range(dialog.table.rowCount()) if not dialog.table.isRowHidden(r)]
123+
assert len(visible_rows) >= 1
124+
125+
dialog.filter_table("")
126+
visible_rows_all = [r for r in range(dialog.table.rowCount()) if not dialog.table.isRowHidden(r)]
127+
assert len(visible_rows_all) == len(SHORTCUTS_DATA)
128+
129+
130+
def test_plugins_dialog_ux_and_a11y(qapp):
131+
"""Prüft Tabelle, Detailsfeld, Buttons und Accessible-Attribute im PluginsDialog."""
132+
dialog = PluginsDialog()
133+
assert dialog.table.accessibleName() == "Installierte Sprach-Plugins und Provider"
134+
assert dialog.details_text.accessibleName() == "Plugin-Details"
135+
assert dialog.btn_open_folder.accessibleName() == "Plugin-Ordner öffnen"
136+
assert dialog.btn_create.accessibleName() == "Neues JSON-Plugin erstellen"
137+
assert dialog.btn_reload.accessibleName() == "Plugins neu laden"
138+
assert dialog.btn_close.accessibleName() == "Plugin-Dialog schließen"
139+
140+
141+
def test_problems_and_output_panel_a11y(qapp):
142+
"""Prüft Accessible-Attribute von ProblemsPanel und OutputPanel."""
143+
problems = ProblemsPanel()
144+
assert problems.tree.accessibleName() == "Problems-Panel"
145+
146+
output = OutputPanel()
147+
assert output.status_label.accessibleName() == "Ausführungsstatus"
148+
assert output.run_btn.accessibleName() == "Aktuelle Datei ausführen"
149+
assert output.stop_btn.accessibleName() == "Ausführung stoppen"
150+
assert output.clear_btn.accessibleName() == "Ausgabe leeren"
151+
assert output.output.accessibleName() == "Programmausgabe"

0 commit comments

Comments
 (0)