Skip to content

Commit 5787f52

Browse files
committed
fix: improve streaming UX, stop button, and scroll synchronization
Streaming Text Insertion: - Disable widget updates during text insertion to prevent auto-scroll - Append using separate QTextCursor on document (doesn't affect visible cursor) - Only auto-scroll to bottom if user was already at bottom (watching stream) - Preserve user's scroll position when they scroll up to read earlier content - Qt version compatibility for QTextCursor.End enum (Qt5/Qt6) Stop Button Fix: - Add cancellation check inside streaming generator consumption loop - Check cancel_request() on each character/chunk during streaming - Raise TranslationCanceled immediately when stop is requested - Apply to both single translation and batch mode streaming - Fixes infinite 'Stopping...' state with large merged translations - Stops immediately instead of waiting for paragraph completion Scroll Synchronization: - Replace pixel-based scroll sync with line-number (block) synchronization - Get first visible block number and sync to same block in other editors - Works correctly during streaming when translation is incomplete - Works with word wrap enabled/disabled (syncs wrapped lines properly) - If target line doesn't exist yet during streaming, skip sync gracefully - Keeps original and translation aligned by actual content position
1 parent 9042c76 commit 5787f52

2 files changed

Lines changed: 57 additions & 4 deletions

File tree

advanced.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
QPlainTextEdit, QPushButton, QSplitter, QLabel, QThread, QLineEdit,
77
QGridLayout, QProgressBar, pyqtSignal, pyqtSlot, QPixmap, QEvent,
88
QStackedWidget, QSpacerItem, QTabWidget, QCheckBox,
9-
QComboBox, QSizePolicy)
9+
QComboBox, QSizePolicy, QTextCursor)
1010
from calibre.constants import __version__ # type: ignore
1111
from calibre.gui2 import I # type: ignore
1212
from calibre.utils.localization import _ # type: ignore
@@ -973,11 +973,31 @@ def layout_review(self):
973973
self.review_splitter.setSizes(_size)
974974

975975
def synchronizeScrollbars(editors):
976+
"""Sync scroll position by line number, not pixels or percentage.
977+
Works correctly during streaming and with word wrap."""
976978
for editor in editors:
977979
for other_editor in editors:
978980
if editor != other_editor:
981+
def create_sync_handler(source, target):
982+
def sync_by_line_number():
983+
# Get first visible line number in source
984+
source_block = source.firstVisibleBlock()
985+
line_number = source_block.blockNumber()
986+
987+
# Get the block at same line number in target
988+
target_doc = target.document()
989+
target_block = target_doc.findBlockByNumber(line_number)
990+
991+
if target_block.isValid():
992+
# Move cursor to that block and ensure it's visible
993+
cursor = target.textCursor()
994+
cursor.setPosition(target_block.position())
995+
target.setTextCursor(cursor)
996+
target.ensureCursorVisible()
997+
return sync_by_line_number
998+
979999
editor.verticalScrollBar().valueChanged.connect(
980-
other_editor.verticalScrollBar().setValue)
1000+
create_sync_handler(editor, other_editor))
9811001
synchronizeScrollbars((raw_text, original_text, translation_text))
9821002

9831003
translation_text.cursorPositionChanged.connect(
@@ -1139,7 +1159,31 @@ def streaming_translation(data):
11391159
elif isinstance(data, Paragraph):
11401160
self.table.setCurrentItem(self.table.item(data.row, 0))
11411161
else:
1142-
translation_text.insertPlainText(data)
1162+
# Check if user is at bottom (watching stream)
1163+
scrollbar = translation_text.verticalScrollBar()
1164+
was_at_bottom = scrollbar.value() >= scrollbar.maximum() - 10
1165+
1166+
# Disable updates to prevent auto-scroll/flicker
1167+
translation_text.setUpdatesEnabled(False)
1168+
saved_position = scrollbar.value()
1169+
1170+
# Append to document using cursor at end
1171+
doc = translation_text.document()
1172+
cursor = QTextCursor(doc)
1173+
end_position = getattr(QTextCursor.MoveOperation, 'End', None) or QTextCursor.End
1174+
cursor.movePosition(end_position)
1175+
cursor.insertText(data)
1176+
1177+
# Restore scroll position before re-enabling updates
1178+
if not was_at_bottom:
1179+
scrollbar.setValue(saved_position)
1180+
1181+
# Re-enable updates - this triggers repaint
1182+
translation_text.setUpdatesEnabled(True)
1183+
1184+
# Scroll to bottom only if user was watching
1185+
if was_at_bottom:
1186+
scrollbar.setValue(scrollbar.maximum())
11431187
self.trans_worker.streaming.connect(streaming_translation)
11441188

11451189
def modify_translation():

lib/translation.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,23 @@ def translate_paragraph(self, paragraph):
169169
temp = ''
170170
clear = True
171171
for char in translation:
172+
# Check for cancellation during streaming
173+
if self.cancel_request():
174+
raise TranslationCanceled(_('Translation canceled.'))
172175
if clear:
173176
self.streaming('')
174177
clear = False
175178
self.streaming(char)
176179
time.sleep(0.05)
177180
temp += char
178181
else:
179-
temp = ''.join([char for char in translation])
182+
# For batch mode, still check cancellation periodically
183+
temp_chars = []
184+
for char in translation:
185+
if self.cancel_request():
186+
raise TranslationCanceled(_('Translation canceled.'))
187+
temp_chars.append(char)
188+
temp = ''.join(temp_chars)
180189
translation = temp
181190
translation = self.glossary.restore(translation)
182191
paragraph.translation = translation.strip()

0 commit comments

Comments
 (0)