Skip to content

Commit f452962

Browse files
committed
Reverse scroll (scrolling the preview will also scroll source)
1 parent d55b1a2 commit f452962

2 files changed

Lines changed: 83 additions & 5 deletions

File tree

ReText/syncscroll.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

ReText/webenginepreview.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,13 @@ def __init__(self, tab,
150150

151151
self.setPage(webPage)
152152

153-
self.syncscroll = SyncScroll(webPage,
154-
editorPositionToSourceLineFunc,
155-
sourceLineToEditorPositionFunc)
156153
self.editBox = tab.editBox
154+
self.syncscroll = SyncScroll(
155+
webPage,
156+
editorPositionToSourceLineFunc,
157+
sourceLineToEditorPositionFunc,
158+
setEditorScrollValueFunc=self.editBox.verticalScrollBar().setValue,
159+
)
157160

158161
settings = self.settings()
159162
settings.setDefaultTextEncoding('utf-8')
@@ -172,6 +175,10 @@ def __init__(self, tab,
172175
self.editBox.verticalScrollBar().valueChanged.connect(self.syncscroll.handleEditorScrolled)
173176
self.editBox.resized.connect(self._handleEditorResized)
174177

178+
# When preview is scrolled, update the editor scroll accordingly
179+
if hasattr(webPage, 'scrollPositionChanged'):
180+
webPage.scrollPositionChanged.connect(self.syncscroll.handlePreviewScrolled)
181+
175182
# Scroll the preview when the mouse wheel is used to scroll
176183
# beyond the beginning/end of the editor
177184
self.editBox.scrollLimitReached.connect(self._handleWheelEvent)
@@ -225,6 +232,9 @@ def disconnectExternalSignals(self):
225232
self.editBox.resized.disconnect(self._handleEditorResized)
226233

227234
self.editBox.scrollLimitReached.disconnect(self._handleWheelEvent)
235+
# Disconnect preview scroll synchronization
236+
if hasattr(self.page(), 'scrollPositionChanged'):
237+
self.page().scrollPositionChanged.disconnect(self.syncscroll.handlePreviewScrolled)
228238

229239
def _handleCursorPositionChanged(self):
230240
editorCursorPosition = self.editBox.verticalScrollBar().value() + \

0 commit comments

Comments
 (0)