Add paused log reading

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-09-18 23:15:05 +08:00
parent 34a079af0a
commit 5d41657bc9
3 changed files with 192 additions and 5 deletions
+18 -1
View File
@@ -1618,7 +1618,8 @@ TRANSLATION = {
},
"Search": {
"source": [
"Furious.Window.HomePage"
"Furious.Window.HomePage",
"Furious.Window.LogPage"
],
"RU": "Поиск",
"ZH": "查找",
@@ -3366,5 +3367,21 @@ TRANSLATION = {
"RU": "Остановить обновления",
"ZH": "停止更新",
"isReviewed": "True"
},
"Pause Updates": {
"source": [
"Furious.Window.LogPage"
],
"RU": "Приостановить обновление",
"ZH": "暂停刷新",
"isReviewed": "True"
},
"Resume Updates": {
"source": [
"Furious.Window.LogPage"
],
"RU": "Возобновить обновление",
"ZH": "继续刷新",
"isReviewed": "True"
}
}
+56 -4
View File
@@ -218,6 +218,7 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
)
self._searchRegex = None
self._paused = False
self.textBrowser = DraculaTextBrowser(
fontFamily=fontFamily,
@@ -384,6 +385,22 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
popupMenu=self._viewMenu,
)
self.pauseButton = AppQPushButton(
_('Pause Updates'), icon=bootstrapIcon('pause-circle.svg'), parent=self
)
self.pauseButton.setCheckable(True)
self.pauseButton.toggled.connect(self._pauseChanged)
self.findAction = AppQAction(
_('Search'),
parent=self,
callback=self.focusSearch,
shortcut=QtGui.QKeySequence.StandardKey.Find,
)
self.findAction.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
self.addAction(self.findAction)
self.autoScrollLabel = AppQLabel(_('Auto Scroll Down'))
self.autoScrollSwitch = AppQSwitch()
@@ -400,6 +417,7 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
actionLayout.addWidget(self.fileButton)
actionLayout.addWidget(self.editButton)
actionLayout.addWidget(self.viewButton)
actionLayout.addWidget(self.pauseButton)
actionLayout.addStretch(1)
actionLayout.addWidget(self.autoScrollLabel)
actionLayout.addWidget(self.autoScrollSwitch)
@@ -435,18 +453,50 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
self.retranslate()
def _registerMenuShortcuts(self, menu):
"""Associate popup actions with the page so shortcuts stay active."""
"""Keep document shortcuts local to the log browser and its children."""
for action in menu.actions():
if action.isSeparator():
continue
self.addAction(action)
action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
self.textBrowser.addAction(action)
submenu = action.menu() if hasattr(action, 'menu') else None
if submenu is not None:
self._registerMenuShortcuts(submenu)
@QtCore.Slot()
def focusSearch(self):
"""Focus the enabled search field without interrupting a paused view."""
if self.searchLineEdit.isEnabled():
self.searchLineEdit.setFocus(QtCore.Qt.ShortcutFocusReason)
self.searchLineEdit.selectAll()
@QtCore.Slot(bool)
def _pauseChanged(self, paused):
"""Freeze only presentation; the manager retains collection ownership."""
self._paused = paused
self.pauseButton.setText(_('Resume Updates') if paused else _('Pause Updates'))
self.pauseButton.setIcon(
bootstrapIcon('play-circle.svg' if paused else 'pause-circle.svg')
)
for widget in [self.searchLineEdit, self.filterComboBox, self.filterLabel]:
widget.setEnabled(not paused)
if paused:
self._updateTimer.stop()
self._highlightTimer.stop()
self._scrollTimer.stop()
self._followStateTimer.stop()
self._setHighlightBusy(False)
self._entriesDirty = True
else:
self._requestRefresh(invalidate=True, immediate=True)
def _categoryText(self, category) -> str:
"""Return a category's translated or literal display label."""
if category.id == APPLICATION_LOG_CATEGORY:
@@ -480,8 +530,10 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
"""Return whether document work can currently reach the screen."""
window = self.window()
return self.isVisible() and not (
hasattr(window, 'isMinimized') and window.isMinimized()
return (
not self._paused
and self.isVisible()
and not (hasattr(window, 'isMinimized') and window.isMinimized())
)
def _requestRefresh(self, *, invalidate=False, immediate=False):
+118
View File
@@ -1548,6 +1548,124 @@ class UnifiedLogPageTest(unittest.TestCase):
page.close()
page.deleteLater()
def testPausedViewPreservesSelectionAcrossEvictionClearAndHide(self):
"""Pause presentation while the shared bounded stream keeps collecting."""
with isolatedSettings():
manager = LogManager(maximumEntries=3)
page = LogPage(manager=manager)
self.addCleanup(self.disposePage, page)
manager.append('original text', APPLICATION_LOG_CATEGORY)
page.show()
self.assertRendered(page)
page.textBrowser.selectAll()
selected = page.textBrowser.textCursor().selectedText()
QTest.mouseClick(page.pauseButton, QtCore.Qt.LeftButton)
manager.clear()
for number in range(10):
manager.append(f'new {number}', APPLICATION_LOG_CATEGORY)
processQtEvents()
page.hide()
page.show()
processQtEvents()
self.assertEqual(page.plainText(), 'original text')
self.assertEqual(page.textBrowser.textCursor().selectedText(), selected)
self.assertFalse(page.searchLineEdit.isEnabled())
self.assertFalse(page.filterComboBox.isEnabled())
self.assertFalse(page._updateTimer.isActive())
self.assertFalse(page._highlightTimer.isActive())
self.assertFalse(page._scrollTimer.isActive())
QTest.mouseClick(page.pauseButton, QtCore.Qt.LeftButton)
self.assertRendered(page)
self.assertEqual(page.plainText().splitlines(), ['new 7', 'new 8', 'new 9'])
self.assertTrue(page.searchLineEdit.isEnabled())
self.assertTrue(page.filterComboBox.isEnabled())
def testPausedFilteredViewExportsItsSnapshotAndRetranslates(self):
"""Export the frozen filtered document, then resume the same filter."""
with isolatedSettings():
AppSettings.set('Language', 'EN')
manager = LogManager(maximumEntries=4)
page = LogPage(manager=manager)
self.addCleanup(self.disposePage, page)
manager.append('core match', CORE_LOG_CATEGORY)
manager.append('application match', APPLICATION_LOG_CATEGORY)
page.filterComboBox.setCurrentIndex(
page.filterComboBox.findData(CORE_LOG_CATEGORY)
)
page.searchLineEdit.setText('match')
page.show()
self.assertRendered(page)
QTest.mouseClick(page.pauseButton, QtCore.Qt.LeftButton)
manager.append('core later match', CORE_LOG_CATEGORY)
processQtEvents()
with mock.patch('Furious.Window.LogPage.saveAsFile') as save:
page._fileMenu.actions()[0].trigger()
save.assert_called_once_with('core match')
for language in ('ZH', 'RU', 'EN'):
AppSettings.set('Language', language)
page.pauseButton.retranslate()
self.assertEqual(page.pauseButton.text(), _('Resume Updates'))
QTest.mouseClick(page.pauseButton, QtCore.Qt.LeftButton)
self.assertRendered(page)
self.assertEqual(
page.plainText().splitlines(), ['core match', 'core later match']
)
self.assertEqual(page.pauseButton.text(), _('Pause Updates'))
def testSearchKeyboardKeepsEditingSeparateFromLogSelection(self):
"""Find focuses search; editing shortcuts act on the focused widget."""
with isolatedSettings():
manager = LogManager(maximumEntries=3)
page = LogPage(manager=manager)
self.addCleanup(self.disposePage, page)
self.addCleanup(QTest.keyRelease, page.textBrowser, QtCore.Qt.Key_Control)
manager.append('alpha log', APPLICATION_LOG_CATEGORY)
page.show()
page.activateWindow()
self.assertRendered(page)
page.textBrowser.setFocus()
processQtEvents()
QTest.keyClick(page.textBrowser, QtCore.Qt.Key_F, QtCore.Qt.ControlModifier)
self.assertTrue(waitFor(page.searchLineEdit.hasFocus))
QTest.keyClicks(page.searchLineEdit, 'alpha')
QTest.keyClick(
page.searchLineEdit, QtCore.Qt.Key_A, QtCore.Qt.ControlModifier
)
QTest.keyClick(
page.searchLineEdit, QtCore.Qt.Key_C, QtCore.Qt.ControlModifier
)
self.assertEqual(application().clipboard().text(), 'alpha')
self.assertFalse(page.textBrowser.textCursor().hasSelection())
self.assertRendered(page)
page.textBrowser.setFocus()
QTest.keyClick(page.textBrowser, QtCore.Qt.Key_A, QtCore.Qt.ControlModifier)
QTest.keyClick(page.textBrowser, QtCore.Qt.Key_C, QtCore.Qt.ControlModifier)
self.assertEqual(application().clipboard().text(), 'alpha log')
def testFindShortcutFollowsTheVisiblePage(self):
"""Two persistent pages share one window without ambiguous Find actions."""
with isolatedSettings():
stack = QStackedWidget()
pages = [LogPage(manager=LogManager(maximumEntries=3)) for _ in range(2)]
for page in pages:
stack.addWidget(page)
self.addCleanup(stack.deleteLater)
self.addCleanup(stack.close)
self.addCleanup(QTest.keyRelease, stack, QtCore.Qt.Key_Control)
stack.show()
stack.activateWindow()
for page in (pages[0], pages[1], pages[0]):
stack.setCurrentWidget(page)
page.textBrowser.setFocus()
processQtEvents()
QTest.keyClick(
page.textBrowser, QtCore.Qt.Key_F, QtCore.Qt.ControlModifier
)
self.assertTrue(waitFor(page.searchLineEdit.hasFocus))
def testLabelsOwnTheirDynamicTranslation(self):
"""Let each AppQLabel translate itself without page-level setters."""
with isolatedSettings():