@@ -25,6 +25,146 @@ def paintEvent(self, event):
2525 self .codeEditor .lineNumberAreaPaintEvent (event )
2626
2727
28+ class Minimap (QWidget ):
29+ """Kompakte Code-Vorschau mit synchronisiertem Editor-Viewport.
30+
31+ Die Vorschau malt alle Dokumentzeilen in eine schmale, nicht editierbare
32+ Fläche. Ein Klick oder Ziehen positioniert die vertikale Scrollbar des
33+ Haupteditors an der entsprechenden Dokumentstelle.
34+ """
35+
36+ WIDTH = 110
37+
38+ def __init__ (self , editor : "CodeEditor" , parent = None ):
39+ super ().__init__ (parent or editor )
40+ self .editor = editor
41+ self .viewport_rect = QRect ()
42+ self ._dragging = False
43+ self .setFixedWidth (self .WIDTH )
44+ self .setFocusPolicy (Qt .FocusPolicy .NoFocus )
45+ self .setCursor (Qt .CursorShape .PointingHandCursor )
46+ self .setToolTip ("Minimap: klicken oder ziehen, um zu navigieren" )
47+ self .setStyleSheet (
48+ "QWidget { background-color: #1a1a1a; "
49+ "border-left: 1px solid #333; }"
50+ )
51+
52+ editor .textChanged .connect (self .update )
53+ editor .cursorPositionChanged .connect (self .update )
54+ editor .blockCountChanged .connect (lambda _count : self .update ())
55+ editor .updateRequest .connect (self ._editor_update_requested )
56+ editor .verticalScrollBar ().valueChanged .connect (lambda _value : self .update ())
57+ editor .document ().contentsChanged .connect (self .update )
58+
59+ def _editor_update_requested (self , _rect , _dy ):
60+ """Zeichnet die Vorschau nach Scroll- und Viewport-Updates neu."""
61+ self .update ()
62+
63+ def _document_lines (self ):
64+ document = self .editor .document ()
65+ block = document .firstBlock ()
66+ lines = []
67+ while block .isValid ():
68+ lines .append (block .text ())
69+ block = block .next ()
70+ return lines or ["" ]
71+
72+ def _visible_block_range (self , line_count ):
73+ first = self .editor .firstVisibleBlock ()
74+ if not first .isValid ():
75+ return 0 , min (1 , line_count )
76+
77+ start = max (0 , first .blockNumber ())
78+ end = min (line_count , start + 1 )
79+ block = first
80+ viewport_height = self .editor .viewport ().height ()
81+ while block .isValid ():
82+ top = int (self .editor .blockBoundingGeometry (block )
83+ .translated (self .editor .contentOffset ()).top ())
84+ bottom = top + int (self .editor .blockBoundingRect (block ).height ())
85+ if top > viewport_height :
86+ break
87+ if bottom >= 0 :
88+ end = min (line_count , block .blockNumber () + 1 )
89+ block = block .next ()
90+ return start , max (start + 1 , end )
91+
92+ def _update_viewport_rect (self , line_count ):
93+ start , end = self ._visible_block_range (line_count )
94+ height = max (1 , self .height ())
95+ top = int (start * height / line_count )
96+ bottom = int (end * height / line_count )
97+ self .viewport_rect = QRect (0 , top , self .width (), max (4 , bottom - top ))
98+
99+ def paintEvent (self , event ):
100+ painter = QPainter (self )
101+ painter .fillRect (event .rect (), QColor (26 , 26 , 26 ))
102+
103+ lines = self ._document_lines ()
104+ line_count = len (lines )
105+ self ._update_viewport_rect (line_count )
106+ height = max (1 , self .height ())
107+ width = max (1 , self .width () - 8 )
108+ max_chars = max (1 , max ((len (line ) for line in lines ), default = 1 ))
109+
110+ for index , line in enumerate (lines ):
111+ y = int (index * height / line_count )
112+ next_y = int ((index + 1 ) * height / line_count )
113+ line_height = max (1 , next_y - y )
114+ text = line .expandtabs (4 ).strip ()
115+ if not text :
116+ continue
117+ bar_width = max (2 , min (width , 3 + int (width * len (text ) / max_chars )))
118+ if text .startswith (("#" , "//" , "/*" , "*" )):
119+ color = QColor (82 , 120 , 82 )
120+ elif text .startswith (("def " , "class " , "function " )):
121+ color = QColor (90 , 140 , 205 )
122+ else :
123+ color = QColor (125 , 125 , 125 )
124+ painter .fillRect (4 , y , bar_width , max (1 , min (2 , line_height )), color )
125+
126+ painter .setPen (Qt .PenStyle .NoPen )
127+ painter .setBrush (QColor (100 , 100 , 200 , 70 ))
128+ painter .drawRect (self .viewport_rect )
129+ painter .setBrush (Qt .BrushStyle .NoBrush )
130+ painter .setPen (QColor (120 , 120 , 220 ))
131+ painter .drawRect (self .viewport_rect )
132+
133+ current_line = self .editor .textCursor ().blockNumber ()
134+ if 0 <= current_line < line_count :
135+ y = int (current_line * height / line_count )
136+ painter .fillRect (2 , y , self .width () - 4 , max (1 , int (height / line_count )),
137+ QColor (210 , 210 , 120 , 110 ))
138+
139+ def mousePressEvent (self , event ):
140+ if event .button () == Qt .MouseButton .LeftButton :
141+ self ._dragging = True
142+ self ._scroll_to_position (event .position ().y ())
143+ event .accept ()
144+ return
145+ super ().mousePressEvent (event )
146+
147+ def mouseMoveEvent (self , event ):
148+ if self ._dragging and event .buttons () & Qt .MouseButton .LeftButton :
149+ self ._scroll_to_position (event .position ().y ())
150+ event .accept ()
151+ return
152+ super ().mouseMoveEvent (event )
153+
154+ def mouseReleaseEvent (self , event ):
155+ if event .button () == Qt .MouseButton .LeftButton :
156+ self ._dragging = False
157+ event .accept ()
158+ return
159+ super ().mouseReleaseEvent (event )
160+
161+ def _scroll_to_position (self , y ):
162+ """Setzt die Editor-Scrollbar auf die geklickte Dokumentposition."""
163+ ratio = max (0.0 , min (1.0 , float (y ) / max (1 , self .height ())))
164+ scrollbar = self .editor .verticalScrollBar ()
165+ scrollbar .setValue (round (ratio * scrollbar .maximum ()))
166+
167+
28168class CodeEditor (QPlainTextEdit ):
29169 """Code-Editor mit Zeilennummern, Highlighting, Auto-Completion und Bracket Matching"""
30170
@@ -48,6 +188,8 @@ def __init__(self, parent=None):
48188 self .linter_errors : List [Dict ] = []
49189
50190 self .lineNumberArea = LineNumberArea (self )
191+ self .minimap = Minimap (self )
192+ self ._minimap_visible = True
51193
52194 self .blockCountChanged .connect (self .updateLineNumberAreaWidth )
53195 self .updateRequest .connect (self .updateLineNumberArea )
@@ -75,11 +217,21 @@ def apply_editor_settings(self, font_family: str = "Consolas", font_size: int =
75217 self .setTabStopWidth (space_width * tab_size )
76218 self .updateLineNumberAreaWidth (0 )
77219
78-
79220 # Auto-Completion
80221 self .completer = None
81222 self ._provider = None
82223
224+ def set_minimap_visible (self , visible : bool ):
225+ """Zeigt oder verbirgt die Minimap und aktualisiert den Randabstand."""
226+ self ._minimap_visible = bool (visible )
227+ self .minimap .setVisible (self ._minimap_visible )
228+ self .updateLineNumberAreaWidth (0 )
229+ self .minimap .update ()
230+
231+ def is_minimap_visible (self ) -> bool :
232+ """Gibt zurück, ob die Minimap für diesen Editor sichtbar sein soll."""
233+ return self ._minimap_visible
234+
83235 def set_completer_words (self , words : List [str ]):
84236 """Setzt die Completion-Wörter"""
85237 self .completer = QCompleter (sorted (set (words )), self )
@@ -271,7 +423,18 @@ def lineNumberAreaWidth(self):
271423 return 20 + char_width * digits
272424
273425 def updateLineNumberAreaWidth (self , _ ):
274- self .setViewportMargins (self .lineNumberAreaWidth (), 0 , 0 , 0 )
426+ right_margin = self .minimap .width () if self ._minimap_visible else 0
427+ self .setViewportMargins (self .lineNumberAreaWidth (), 0 , right_margin , 0 )
428+ self ._position_minimap ()
429+
430+ def _position_minimap (self ):
431+ if not hasattr (self , "minimap" ):
432+ return
433+ cr = self .contentsRect ()
434+ width = self .minimap .width ()
435+ self .minimap .setGeometry (
436+ QRect (cr .right () - width + 1 , cr .top (), width , cr .height ())
437+ )
275438
276439 def updateLineNumberArea (self , rect , dy ):
277440 if dy :
@@ -287,6 +450,7 @@ def resizeEvent(self, event):
287450 self .lineNumberArea .setGeometry (
288451 QRect (cr .left (), cr .top (), self .lineNumberAreaWidth (), cr .height ())
289452 )
453+ self ._position_minimap ()
290454
291455 def lineNumberAreaPaintEvent (self , event ):
292456 painter = QPainter (self .lineNumberArea )
0 commit comments