55
66Zeigt eine Baumstruktur des geöffneten Projektordners an.
77Unterstützt Doppelklick zum Öffnen, Kontextmenü und Filter.
8+ Git-Status-Indikatoren (M/S/U/D) werden rechts neben dem Dateinamen eingeblendet.
89"""
910
1011from pathlib import Path
12+ from typing import Optional , Dict , TYPE_CHECKING
1113from PySide6 .QtWidgets import (
1214 QWidget , QVBoxLayout , QHBoxLayout , QTreeView , QFileSystemModel ,
1315 QPushButton , QLabel , QLineEdit , QMenu , QFileDialog ,
14- QMessageBox
16+ QMessageBox , QStyledItemDelegate
1517)
1618from PySide6 .QtCore import Qt , QDir , Signal , QSortFilterProxyModel , QModelIndex
17- from PySide6 .QtGui import QFont
19+ from PySide6 .QtGui import QFont , QColor
20+
21+ if TYPE_CHECKING :
22+ from features .git_integration import GitFileStatus
1823
1924
2025# Dateien/Ordner, die standardmäßig ausgeblendet werden
2530}
2631
2732
33+ def status_for_path (
34+ abs_path : str ,
35+ repo_root : str ,
36+ status_dict : Dict [str , "GitFileStatus" ],
37+ ) -> Optional ["GitFileStatus" ]:
38+ """Sucht GitFileStatus für einen absoluten Dateipfad anhand eines Status-Dicts.
39+
40+ Normalisiert Windows-Backslashes zu Forward-Slashes (Porcelain-Format von
41+ ``git status --porcelain``). Gibt None zurück, wenn der Pfad außerhalb des
42+ Repos liegt oder kein Status-Eintrag existiert.
43+
44+ Args:
45+ abs_path: Absoluter Dateipfad (Windows- oder Unix-Trennzeichen).
46+ repo_root: Absoluter Pfad zum Repo-Wurzelordner.
47+ status_dict: Dict aus ``GitRepo.get_status()`` (Keys: Slash-relativ).
48+
49+ Returns:
50+ GitFileStatus oder None.
51+ """
52+ try :
53+ rel = Path (abs_path ).relative_to (repo_root )
54+ except ValueError :
55+ return None
56+ return status_dict .get (rel .as_posix ())
57+
58+
59+ class GitStatusDelegate (QStyledItemDelegate ):
60+ """Zeichnet einen farbigen Git-Status-Badge rechts neben dem Dateinamen.
61+
62+ Holt sich den absoluten Dateipfad über ``QFileSystemModel``/Proxy und
63+ delegiert den Lookup an die Qt-freie ``status_for_path``-Funktion.
64+ Ordner werden nicht beschriftet.
65+ """
66+
67+ _BADGE_FONT_SIZE = 9
68+
69+ def __init__ (self , fs_model : QFileSystemModel , proxy : QSortFilterProxyModel ,
70+ parent = None ):
71+ super ().__init__ (parent )
72+ self ._fs_model = fs_model
73+ self ._proxy = proxy
74+ self ._status_dict : Dict [str , "GitFileStatus" ] = {}
75+ self ._repo_root : str = ""
76+
77+ def set_status (self , repo_root : str , status_dict : Dict [str , "GitFileStatus" ]):
78+ """Setzt den aktuellen Git-Status-Cache (wird bei set_root/refresh aufgerufen)."""
79+ self ._repo_root = repo_root
80+ self ._status_dict = status_dict
81+
82+ def _status_for_index (self , index : QModelIndex ) -> Optional ["GitFileStatus" ]:
83+ source = self ._proxy .mapToSource (index )
84+ if not source .isValid ():
85+ return None
86+ abs_path = self ._fs_model .filePath (source )
87+ if not abs_path or not self ._repo_root :
88+ return None
89+ return status_for_path (abs_path , self ._repo_root , self ._status_dict )
90+
91+ def paint (self , painter , option , index ):
92+ super ().paint (painter , option , index )
93+ # Ordner nicht beschriften
94+ source = self ._proxy .mapToSource (index )
95+ if source .isValid () and self ._fs_model .isDir (source ):
96+ return
97+ status = self ._status_for_index (index )
98+ if not (status and status .status_icon ):
99+ return
100+ painter .save ()
101+ color = status .color_hint
102+ if color :
103+ painter .setPen (QColor (color ))
104+ badge_font = QFont ("Consolas" , self ._BADGE_FONT_SIZE )
105+ badge_font .setBold (True )
106+ painter .setFont (badge_font )
107+ rect = option .rect .adjusted (0 , 0 , - 4 , 0 )
108+ painter .drawText (
109+ rect ,
110+ Qt .AlignmentFlag .AlignRight | Qt .AlignmentFlag .AlignVCenter ,
111+ status .status_icon ,
112+ )
113+ painter .restore ()
114+
115+
28116class FileFilterProxy (QSortFilterProxyModel ):
29117 """Filtert versteckte Dateien und Ordner."""
30118
@@ -183,6 +271,10 @@ def _setup_ui(self):
183271 self .tree .setContextMenuPolicy (Qt .ContextMenuPolicy .CustomContextMenu )
184272 self .tree .customContextMenuRequested .connect (self ._on_context_menu )
185273
274+ # Git-Status-Delegate (zeigt M/S/U/D-Badges neben Dateinamen)
275+ self .git_delegate = GitStatusDelegate (self .fs_model , self .proxy , self )
276+ self .tree .setItemDelegateForColumn (0 , self .git_delegate )
277+
186278 layout .addWidget (self .tree )
187279
188280 def set_root (self , path : str ):
@@ -192,6 +284,7 @@ def set_root(self, path: str):
192284 proxy_root = self .proxy .mapFromSource (root_index )
193285 self .tree .setRootIndex (proxy_root )
194286 self ._update_git_info ()
287+ self ._load_git_status ()
195288
196289 def _update_git_info (self ):
197290 """Aktualisiert die Git-Branch-Anzeige im Titel."""
@@ -206,13 +299,29 @@ def _update_git_info(self):
206299 else :
207300 self .title_label .setText (self ._root_path .name )
208301
302+ def _load_git_status (self ):
303+ """Lädt den Git-Status aller geänderten Dateien für den aktuellen Root.
304+
305+ Befüllt den Cache des GitStatusDelegate, damit dieser beim Paint-Aufruf
306+ sofort die Badges rendern kann.
307+ """
308+ if not self ._root_path :
309+ self .git_delegate .set_status ("" , {})
310+ return
311+ from features .git_integration import GitRepo
312+ repo = GitRepo (str (self ._root_path ))
313+ if repo .is_git_repo ():
314+ self .git_delegate .set_status (str (self ._root_path ), repo .get_status ())
315+ else :
316+ self .git_delegate .set_status ("" , {})
317+
209318 def _open_folder_dialog (self ):
210319 path = QFileDialog .getExistingDirectory (self , "Projektordner wählen" )
211320 if path :
212321 self .set_root (path )
213322
214323 def _refresh (self ):
215- """Aktualisiert die Ansicht."""
324+ """Aktualisiert die Ansicht und den Git-Status-Cache ."""
216325 if self ._root_path :
217326 self .set_root (str (self ._root_path ))
218327
0 commit comments