@@ -21,11 +21,14 @@ class SyncScroll:
2121
2222 def __init__ (self , previewFrame ,
2323 editorPositionToSourceLineFunc ,
24- sourceLineToEditorPositionFunc ):
24+ sourceLineToEditorPositionFunc ,
25+ setEditorScrollValueFunc = None ):
2526 self .posmap = {}
2627 self .frame = previewFrame
2728 self .editorPositionToSourceLine = editorPositionToSourceLineFunc
2829 self .sourceLineToEditorPosition = sourceLineToEditorPositionFunc
30+ # Optional callback to set the editor vertical scroll value (in pixels)
31+ self ._setEditorScrollValue = setEditorScrollValueFunc
2932
3033 self .previewPositionBeforeLoad = QPoint ()
3134 self .contentIsLoading = False
@@ -34,6 +37,10 @@ def __init__(self, previewFrame,
3437 self .editorViewportOffset = 0
3538 self .editorCursorPosition = 0
3639
40+ # Guards to prevent recursive scroll feedback loops
41+ self ._updating_preview = False
42+ self ._updating_editor = False
43+
3744 self .frame .contentsSizeChanged .connect (self ._handlePreviewResized )
3845 self .frame .loadStarted .connect (self ._handleLoadStarted )
3946 self .frame .loadFinished .connect (self ._handleLoadFinished )
@@ -46,6 +53,10 @@ def handleEditorResized(self, editorViewportHeight):
4653 self ._updatePreviewScrollPosition ()
4754
4855 def handleEditorScrolled (self , editorViewportOffset ):
56+ # If we are programmatically updating the editor due to preview scroll,
57+ # ignore this event to avoid feedback loops.
58+ if self ._updating_editor :
59+ return
4960 self .editorViewportOffset = editorViewportOffset
5061 return self ._updatePreviewScrollPosition ()
5162
@@ -137,9 +148,66 @@ def _updatePreviewScrollPosition(self):
137148
138149 pos = self .frame .scrollPosition ()
139150 pos .setY (preview_scroll_offset )
140- self .frame .setScrollPosition (pos )
151+ # Prevent preview→editor feedback while we adjust preview scroll
152+ self ._updating_preview = True
153+ try :
154+ self .frame .setScrollPosition (pos )
155+ finally :
156+ self ._updating_preview = False
141157
142158 def _setPositionMap (self , posmap ):
143159 self .posmap = posmap
144160 if posmap :
145161 self .posmap [0 ] = 0
162+
163+ def handlePreviewScrolled (self , previewScrollPosition ):
164+ """
165+ Update editor scroll position based on preview scroll position.
166+
167+ previewScrollPosition can be either a QPointF/QPoint or a numeric Y value.
168+ """
169+ if not self ._setEditorScrollValue :
170+ return
171+ # Avoid reacting to our own preview updates
172+ if self ._updating_preview :
173+ return
174+ if not self .posmap :
175+ return
176+
177+ # Extract Y coordinate
178+ try :
179+ preview_y = previewScrollPosition .y ()
180+ except AttributeError :
181+ preview_y = float (previewScrollPosition )
182+
183+ # Binary search using line numbers to find nearest posmap values
184+ posmap_lines = [0 ] + sorted (self .posmap .keys ())
185+ min_index = 0
186+ max_index = len (posmap_lines ) - 1
187+ while max_index - min_index > 1 :
188+ current_index = int ((min_index + max_index ) / 2 )
189+ current_line = posmap_lines [current_index ]
190+ if self .posmap [current_line ] > preview_y :
191+ max_index = current_index
192+ else :
193+ min_index = current_index
194+
195+ min_line = posmap_lines [min_index ]
196+ max_line = posmap_lines [max_index ]
197+
198+ min_preview_pos = self .posmap [min_line ]
199+ max_preview_pos = self .posmap [max_line ]
200+
201+ min_textedit_pos = self .sourceLineToEditorPosition (min_line )
202+ max_textedit_pos = self .sourceLineToEditorPosition (max_line )
203+
204+ editor_scroll_to = self ._linearScale (preview_y ,
205+ min_preview_pos , max_preview_pos ,
206+ min_textedit_pos , max_textedit_pos )
207+
208+ # Apply editor scroll with guard to avoid triggering editor→preview update
209+ self ._updating_editor = True
210+ try :
211+ self ._setEditorScrollValue (int (editor_scroll_to ))
212+ finally :
213+ self ._updating_editor = False
0 commit comments