Skip to content

Commit a60daa9

Browse files
committed
fix: improve streaming text insertion and stop button functionality
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 Scroll Synchronization: - Keep simple pixel-based scroll sync (most stable) - Note: Minor drift can occur with word wrap when languages have different lengths - Alternative sync methods (line-based, percentage-based) had worse issues
1 parent 9042c76 commit a60daa9

2 files changed

Lines changed: 37 additions & 3 deletions

File tree

advanced.py

Lines changed: 27 additions & 2 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,6 +973,7 @@ def layout_review(self):
973973
self.review_splitter.setSizes(_size)
974974

975975
def synchronizeScrollbars(editors):
976+
"""Sync scrollbars between editors using simple pixel-based approach."""
976977
for editor in editors:
977978
for other_editor in editors:
978979
if editor != other_editor:
@@ -1139,7 +1140,31 @@ def streaming_translation(data):
11391140
elif isinstance(data, Paragraph):
11401141
self.table.setCurrentItem(self.table.item(data.row, 0))
11411142
else:
1142-
translation_text.insertPlainText(data)
1143+
# Check if user is at bottom (watching stream)
1144+
scrollbar = translation_text.verticalScrollBar()
1145+
was_at_bottom = scrollbar.value() >= scrollbar.maximum() - 10
1146+
1147+
# Disable updates to prevent auto-scroll/flicker
1148+
translation_text.setUpdatesEnabled(False)
1149+
saved_position = scrollbar.value()
1150+
1151+
# Append to document using cursor at end
1152+
doc = translation_text.document()
1153+
cursor = QTextCursor(doc)
1154+
end_position = getattr(QTextCursor.MoveOperation, 'End', None) or QTextCursor.End
1155+
cursor.movePosition(end_position)
1156+
cursor.insertText(data)
1157+
1158+
# Restore scroll position before re-enabling updates
1159+
if not was_at_bottom:
1160+
scrollbar.setValue(saved_position)
1161+
1162+
# Re-enable updates - this triggers repaint
1163+
translation_text.setUpdatesEnabled(True)
1164+
1165+
# Scroll to bottom only if user was watching
1166+
if was_at_bottom:
1167+
scrollbar.setValue(scrollbar.maximum())
11431168
self.trans_worker.streaming.connect(streaming_translation)
11441169

11451170
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)