Skip to content

Commit 07ca56d

Browse files
committed
Automatically display prints even while running blocking code
When writing to the console, check if more than X seconds have passed from the last refresh triggered by a write and force a display update if so. This only updates the consoles and does not call QApplication.processEvents.
1 parent 9347d83 commit 07ca56d

File tree

4 files changed

+225
-39
lines changed

4 files changed

+225
-39
lines changed

preditor/gui/console.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,11 @@ def setCommand(self):
136136

137137
def clear(self):
138138
"""clears the text in the editor"""
139-
super().clear()
139+
super(ConsoleBase, self).clear()
140140
self.startInputLine()
141+
# Note: Don't use the regular `super()` call here as it would result
142+
# in multiple calls to repaint, just call the base Qt class's clear.
143+
self.maybeRepaint(force=True)
141144

142145
def clearToLastPrompt(self):
143146
# store the current cursor position so we can restore when we are done

preditor/gui/console_base.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import re
33
import string
44
import subprocess
5+
import time
56
import traceback
67
from fractions import Fraction
78
from typing import Optional
@@ -34,6 +35,13 @@ def __init__(self, parent: QWidget, controller: Optional[LoggerWindow] = None):
3435
super().__init__(parent)
3536
self.controller = controller
3637
self._first_show = True
38+
# The last time a repaint call was made when writing. Used to limit the
39+
# number of refreshes to once per X.X seconds.
40+
self._last_repaint_time = 0
41+
# The last time `QApplication.processEvents()` was called while writing.
42+
# Optionally used to prevent the app from being marked as not responding
43+
# while processing blocking code and ensure _last_repaint_time processes.
44+
self._last_process_events_time = 0
3745

3846
# Create the highlighter
3947
highlight = CodeHighlighter(self, 'Python')
@@ -104,6 +112,13 @@ def add_separator(self):
104112
# Add a horizontal rule
105113
self.insertHtml("<hr><br>")
106114

115+
def clear(self):
116+
"""clears the text in the editor"""
117+
super().clear()
118+
# Ensure the console is refreshed in case the user is clearing the console
119+
# as part of a blocking call.
120+
self.maybeRepaint(force=True)
121+
107122
def contextMenuEvent(self, event):
108123
"""Builds a custom right click menu to show."""
109124
# Create the standard menu and allow subclasses to customize it
@@ -310,6 +325,65 @@ def init_excepthook(self, attrName=None, value=None):
310325
if self.write_error in PreditorExceptHook.callbacks:
311326
PreditorExceptHook.callbacks.remove(self.write_error)
312327

328+
def maybeRepaint(self, force=False):
329+
"""Forces the console to repaint if enough time has elapsed from the
330+
last repaint.
331+
332+
This method is called every time a print is written to the console. So if
333+
more than `self.controller.repaintConsolesDelay` seconds has elapsed after
334+
the last time maybeRepaint updated the display it will update the display
335+
again, showing all new output.
336+
337+
This prefers calling `self.repaint()` so it doesn't add extra processing
338+
of the Qt event loop. However if enabled it will call
339+
`QApplication.processEvents` periodically. This ensures the repaint is
340+
shown for writes if the Qt app looses focus. On windows this appears to
341+
happen after ~5 seconds.
342+
343+
`self.controller.uiRepaintConsolesOnWriteCHK` can be used to disable
344+
the entire `maybeRepaint` method. To disable only the processEvents calls
345+
`self.controller.uiRepaintProcessEventsOccasionallyCHK` can be disabled.
346+
The repaint delay is controlled by `self.controller.repaintConsolesDelay`.
347+
"""
348+
if not self.controller:
349+
return
350+
if not self.controller.uiRepaintConsolesOnWriteCHK.isChecked():
351+
return
352+
353+
if force:
354+
if self.controller.uiRepaintProcessEventsOccasionallyCHK.isChecked():
355+
QApplication.processEvents()
356+
self._last_process_events_time = time.time_ns()
357+
self._last_repaint_time = self._last_process_events_time
358+
else:
359+
self.repaint()
360+
self._last_repaint_time = time.time_ns()
361+
return
362+
363+
# NOTE: All numbers here should remain int values. This method is can be
364+
# called multiple times per write/print call so it should be optimized
365+
# as much as possible
366+
current_time = time.time_ns()
367+
if (
368+
self.controller.uiRepaintProcessEventsOccasionallyCHK.isChecked()
369+
and current_time - self._last_process_events_time > 5 * 1e9 # seconds
370+
):
371+
# NOTE: On windows 10 this seems to only need called once after the
372+
# app looses focus for the repaint to work on each call. However if
373+
# the app regains focus while processEvents is called it will require
374+
# another processEvents call. Calling this every X seconds also
375+
# ensures the app doesn't stay (Not Responding for long).
376+
QApplication.processEvents()
377+
self._last_process_events_time = time.time_ns()
378+
self._last_repaint_time = self._last_process_events_time
379+
elif (
380+
current_time - self._last_repaint_time
381+
> self.controller.repaintConsolesDelay
382+
):
383+
# Enough time has elapsed, repaint the widget and reset the delay
384+
self.repaint()
385+
self._last_repaint_time = time.time_ns()
386+
313387
def mouseMoveEvent(self, event):
314388
"""Overload of mousePressEvent to change mouse pointer to indicate it is
315389
over a clickable error hyperlink.
@@ -704,6 +778,9 @@ def _write(self, msg, stream_type=StreamType.STDOUT):
704778
# Non-hyperlink output
705779
self.insertPlainText(msg)
706780

781+
# Update the display of the console if enough time has passed and enabled
782+
self.maybeRepaint()
783+
707784
# These Qt Properties can be customized using style sheets.
708785
commentColor = QtPropertyInit('_commentColor', QColor(0, 206, 52))
709786
errorMessageColor = QtPropertyInit('_errorMessageColor', QColor(Qt.GlobalColor.red))

preditor/gui/loggerwindow.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,14 @@ def connectSignals(self):
345345
self.setConsoleHighlightEnabled
346346
)
347347

348+
# Pre-cache the refresh on Write value for speed when writing
349+
self.uiRepaintConsolesPerSecondSPIN.valueChanged.connect(
350+
self.updateRepaintDelay
351+
)
352+
self.uiRepaintConsolesOnWriteCHK.toggled.connect(
353+
self.uiRepaintProcessEventsOccasionallyCHK.setEnabled
354+
)
355+
348356
def setIcons(self):
349357
"""Set various icons"""
350358
self.uiClearLogACT.setIcon(QIcon(resourcePath('img/close-thick.png')))
@@ -1420,6 +1428,11 @@ def recordPrefs(self, manual=False, disableFileMonitoring=False):
14201428
'consoleHighlightEnabled': (
14211429
self.uiConsoleHighlightEnabledCHK.isChecked()
14221430
),
1431+
'repaintConsolesOnWrite': self.uiRepaintConsolesOnWriteCHK.isChecked(),
1432+
'repaintProcessEventsOccasionally': (
1433+
self.uiRepaintProcessEventsOccasionallyCHK.isChecked()
1434+
),
1435+
'repaintConsolesperSecond': self.uiRepaintConsolesPerSecondSPIN.value(),
14231436
}
14241437
)
14251438

@@ -1737,6 +1750,21 @@ def restorePrefs(self, skip_geom=False):
17371750
confirmBeforeClose = pref.get('confirmBeforeClose', True)
17381751
self.uiConfirmBeforeCloseCHK.setChecked(confirmBeforeClose)
17391752

1753+
# Repaint on write configuration
1754+
self.uiRepaintConsolesOnWriteCHK.setChecked(
1755+
pref.get('repaintConsolesOnWrite', True)
1756+
)
1757+
self.uiRepaintConsolesPerSecondSPIN.setValue(
1758+
pref.get('repaintConsolesperSecond', 0.2)
1759+
)
1760+
self.uiRepaintProcessEventsOccasionallyCHK.setChecked(
1761+
pref.get('repaintProcessEventsOccasionally', True)
1762+
)
1763+
self.uiRepaintProcessEventsOccasionallyCHK.setEnabled(
1764+
self.uiRepaintConsolesOnWriteCHK.isChecked()
1765+
)
1766+
self.updateRepaintDelay()
1767+
17401768
# Ensure the correct workbox stack page is shown
17411769
self.update_workbox_stack()
17421770

@@ -2199,6 +2227,18 @@ def updateIndentationsUseTabs(self):
21992227
self.uiIndentationsTabsCHK.isChecked()
22002228
)
22012229

2230+
@Slot()
2231+
def updateRepaintDelay(self):
2232+
"""Update write repaint delay for change to uiRepaintConsolesPerSecondSPIN.
2233+
2234+
`repaintConsolesDelay` is stored as an int nanosecond value so we can use
2235+
`time.time_ns()` without converting to floats which adds a small but
2236+
cumulative time to each write call. Pre-converting this helps limit the
2237+
total delay time.
2238+
"""
2239+
secs = self.uiRepaintConsolesPerSecondSPIN.value()
2240+
self.repaintConsolesDelay = round(round(secs * 1e9))
2241+
22022242
@Slot()
22032243
def update_workbox_stack(self):
22042244
if self.uiWorkboxTAB.editor_cls:

preditor/gui/ui/loggerwindow.ui

Lines changed: 104 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@
6868
<rect>
6969
<x>0</x>
7070
<y>0</y>
71-
<width>938</width>
72-
<height>447</height>
71+
<width>156</width>
72+
<height>29</height>
7373
</rect>
7474
</property>
7575
<layout class="QVBoxLayout" name="verticalLayout_7">
@@ -619,20 +619,6 @@
619619
<property name="spacing">
620620
<number>3</number>
621621
</property>
622-
<item row="0" column="0">
623-
<widget class="QLabel" name="label_3">
624-
<property name="toolTip">
625-
<string>Set the maximun number of backup files on disk per workbox.
626-
Must be at least 1</string>
627-
</property>
628-
<property name="text">
629-
<string>Max recently closed workboxes</string>
630-
</property>
631-
<property name="alignment">
632-
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
633-
</property>
634-
</widget>
635-
</item>
636622
<item row="0" column="1">
637623
<widget class="QSpinBox" name="uiMaxNumRecentWorkboxesSPIN">
638624
<property name="sizePolicy">
@@ -656,6 +642,44 @@ Must be at least 1</string>
656642
</property>
657643
</widget>
658644
</item>
645+
<item row="2" column="0">
646+
<widget class="QLabel" name="label">
647+
<property name="toolTip">
648+
<string>'If running code in the logger takes X seconds or longer,
649+
the window will flash if it is not in focus.
650+
Setting the value to zero will disable flashing.'</string>
651+
</property>
652+
<property name="text">
653+
<string>Flash Interval</string>
654+
</property>
655+
<property name="alignment">
656+
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
657+
</property>
658+
</widget>
659+
</item>
660+
<item row="2" column="1">
661+
<widget class="QDoubleSpinBox" name="uiFlashTimeSPIN">
662+
<property name="toolTip">
663+
<string>'If running code in the logger takes X seconds or longer,
664+
the window will flash if it is not in focus.
665+
Setting the value to zero will disable flashing.'</string>
666+
</property>
667+
</widget>
668+
</item>
669+
<item row="0" column="0">
670+
<widget class="QLabel" name="label_3">
671+
<property name="toolTip">
672+
<string>Set the maximun number of backup files on disk per workbox.
673+
Must be at least 1</string>
674+
</property>
675+
<property name="text">
676+
<string>Max recently closed workboxes</string>
677+
</property>
678+
<property name="alignment">
679+
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
680+
</property>
681+
</widget>
682+
</item>
659683
<item row="1" column="0">
660684
<widget class="QLabel" name="label_2">
661685
<property name="toolTip">
@@ -684,27 +708,69 @@ Must be at least 1</string>
684708
</property>
685709
</widget>
686710
</item>
687-
<item row="2" column="0">
688-
<widget class="QLabel" name="label">
711+
</layout>
712+
</widget>
713+
</item>
714+
<item>
715+
<widget class="QGroupBox" name="uiCodeExecutionGRP">
716+
<property name="title">
717+
<string>Code Execution</string>
718+
</property>
719+
<layout class="QGridLayout" name="gridLayout_4">
720+
<property name="leftMargin">
721+
<number>3</number>
722+
</property>
723+
<property name="topMargin">
724+
<number>3</number>
725+
</property>
726+
<property name="rightMargin">
727+
<number>3</number>
728+
</property>
729+
<property name="bottomMargin">
730+
<number>3</number>
731+
</property>
732+
<property name="spacing">
733+
<number>3</number>
734+
</property>
735+
<item row="0" column="0">
736+
<widget class="QCheckBox" name="uiRepaintConsolesOnWriteCHK">
689737
<property name="toolTip">
690-
<string>'If running code in the logger takes X seconds or longer,
691-
the window will flash if it is not in focus.
692-
Setting the value to zero will disable flashing.'</string>
738+
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enables refreshing of consoles when prints are written. Enabling seeing output when running blocking code would normally prevent Qt from displaying that output until the blocking code finishes.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
693739
</property>
694740
<property name="text">
695-
<string>Flash Interval</string>
741+
<string>Repaint on writes per sec.</string>
696742
</property>
697-
<property name="alignment">
698-
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
743+
<property name="checked">
744+
<bool>true</bool>
699745
</property>
700746
</widget>
701747
</item>
702-
<item row="2" column="1">
703-
<widget class="QDoubleSpinBox" name="uiFlashTimeSPIN">
748+
<item row="0" column="1">
749+
<widget class="QDoubleSpinBox" name="uiRepaintConsolesPerSecondSPIN">
704750
<property name="toolTip">
705-
<string>'If running code in the logger takes X seconds or longer,
706-
the window will flash if it is not in focus.
707-
Setting the value to zero will disable flashing.'</string>
751+
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Only refresh consoles if more than X seconds has elapsed from the previous refresh. Useful for seeing the output from blocking code while its running, but could potentially slow down code execution time if set to short.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
752+
</property>
753+
<property name="maximum">
754+
<double>600.000000000000000</double>
755+
</property>
756+
<property name="singleStep">
757+
<double>0.100000000000000</double>
758+
</property>
759+
<property name="value">
760+
<double>0.200000000000000</double>
761+
</property>
762+
</widget>
763+
</item>
764+
<item row="1" column="0" colspan="2">
765+
<widget class="QCheckBox" name="uiRepaintProcessEventsOccasionallyCHK">
766+
<property name="toolTip">
767+
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When &amp;quot;Repaint on writes per sec.&amp;quot; is enabled it calls `repaint` instead of `QApplication.processEvents()`. This only updates the consoles and not all of Qt when running blocking code. However if the Qt app looses focus this will stop updating after a few seconds. Enabling this will enable calling `QApplication.processEvents()` every few seconds.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
768+
</property>
769+
<property name="text">
770+
<string>Process Events occasionally</string>
771+
</property>
772+
<property name="checked">
773+
<bool>true</bool>
708774
</property>
709775
</widget>
710776
</item>
@@ -1727,17 +1793,17 @@ This button removes those (very old) workboxes.</string>
17271793
</action>
17281794
</widget>
17291795
<customwidgets>
1796+
<customwidget>
1797+
<class>ConsolePrEdit</class>
1798+
<extends>QTextEdit</extends>
1799+
<header>preditor.gui.console</header>
1800+
</customwidget>
17301801
<customwidget>
17311802
<class>GroupTabWidget</class>
17321803
<extends>QTabWidget</extends>
17331804
<header>preditor.gui.group_tab_widget.group_tab_widget.h</header>
17341805
<container>1</container>
17351806
</customwidget>
1736-
<customwidget>
1737-
<class>ConsolePrEdit</class>
1738-
<extends>QTextEdit</extends>
1739-
<header>preditor.gui.console.h</header>
1740-
</customwidget>
17411807
<customwidget>
17421808
<class>EditorChooser</class>
17431809
<extends>QWidget</extends>
@@ -1759,8 +1825,8 @@ This button removes those (very old) workboxes.</string>
17591825
<slot>apply_options()</slot>
17601826
<hints>
17611827
<hint type="sourcelabel">
1762-
<x>558</x>
1763-
<y>367</y>
1828+
<x>165</x>
1829+
<y>363</y>
17641830
</hint>
17651831
<hint type="destinationlabel">
17661832
<x>586</x>
@@ -1807,8 +1873,8 @@ This button removes those (very old) workboxes.</string>
18071873
<slot>update_workbox_stack()</slot>
18081874
<hints>
18091875
<hint type="sourcelabel">
1810-
<x>763</x>
1811-
<y>371</y>
1876+
<x>165</x>
1877+
<y>363</y>
18121878
</hint>
18131879
<hint type="destinationlabel">
18141880
<x>747</x>

0 commit comments

Comments
 (0)