22
33import os
44from collections import deque
5+ from contextlib import suppress
56from typing import TYPE_CHECKING
67
78from pymmcore_plus import CMMCorePlus
8- from qtpy .QtCore import QFileSystemWatcher , QObject , QTimer , QUrl , Signal
9+ from qtpy .QtCore import (
10+ QFileSystemWatcher ,
11+ QObject ,
12+ QSize ,
13+ QTimer ,
14+ QTimerEvent ,
15+ QUrl ,
16+ Signal ,
17+ )
918from qtpy .QtGui import QCloseEvent , QDesktopServices , QFontDatabase , QPalette
1019from qtpy .QtWidgets import (
1120 QApplication ,
@@ -43,26 +52,38 @@ def __init__(
4352 # unless the file is flushed from cache to disk. This does NOT happen
4453 # when CMMCorePlus.logMessage() is called. So we need to poll the file for
4554 # Windows' sake.
46- self ._timer = QTimer (self )
47- self ._timer .setInterval (self ._interval )
48- self ._timer .timeout .connect (self ._read_new )
55+ self ._timer_id : int | None = None
4956
5057 # Watcher for rotation/truncate events
5158 self ._watcher = QFileSystemWatcher (self )
5259 self ._watcher .addPath (self ._path )
5360 self ._watcher .fileChanged .connect (self ._on_file_changed )
5461
62+ def __del__ (self ) -> None :
63+ """Ensure file is closed when object is deleted."""
64+ with suppress (RuntimeError ):
65+ self ._stop ()
66+
67+ def timerEvent (self , event : QTimerEvent | None ) -> None :
68+ if event and event .timerId () == self ._timer_id :
69+ self ._read_new ()
70+
5571 def start (self ) -> None :
5672 """Open the file and start polling."""
57- self ._file = open (self ._path , encoding = "utf-8" , errors = "replace" )
58- self ._file .seek (0 , os .SEEK_END )
59- self ._timer .start ()
73+ if self ._timer_id is None :
74+ self ._file = open (self ._path , encoding = "utf-8" , errors = "replace" )
75+ self ._file .seek (0 , os .SEEK_END )
76+ self ._timer_id = self .startTimer (self ._interval )
6077
6178 def _stop (self ) -> None :
6279 """Stop polling and close the file."""
63- self ._timer .stop ()
64- if self ._file :
65- self ._file .close ()
80+ if self ._timer_id is not None :
81+ self .killTimer (self ._timer_id )
82+ self ._timer_id = None
83+ if self ._file is not None :
84+ with suppress (Exception ):
85+ self ._file .close ()
86+ self ._file = None
6687 self .finished .emit ()
6788
6889 def _on_file_changed (self , path : str ) -> None :
@@ -151,17 +172,27 @@ def __init__(
151172 layout .setContentsMargins (0 , 0 , 0 , 0 )
152173 layout .addLayout (file_layout )
153174 layout .addWidget (self ._log_view )
154- self .setLayout (layout )
155175
156176 # --- Connections ---
157177 self ._reader .new_lines .connect (self ._append_line )
158- self ._clear_btn .clicked .connect (self ._log_view . clear )
178+ self ._clear_btn .clicked .connect (self .clear )
159179 self ._log_btn .clicked .connect (self ._open_native )
160180 self ._reader .start ()
161181
162- def __del__ (self ) -> None :
163- """Stop reader before deletion."""
164- self ._reader ._stop ()
182+ # scroll left to begin
183+ def _scroll_left () -> None :
184+ if sb := self ._log_view .horizontalScrollBar ():
185+ sb .setValue (0 )
186+
187+ QTimer .singleShot (0 , _scroll_left )
188+
189+ def clear (self ) -> None :
190+ """Clear the log view."""
191+ self ._log_view .clear ()
192+
193+ def sizeHint (self ) -> QSize :
194+ hint = super ().sizeHint ()
195+ return hint .expandedTo (QSize (1000 , 800 ))
165196
166197 def _append_line (self , line : str ) -> None :
167198 """Append a line, respecting pause/follow settings."""
@@ -170,8 +201,6 @@ def _append_line(self, line: str) -> None:
170201 def closeEvent (self , event : QCloseEvent | None ) -> None :
171202 """Clean up thread on close."""
172203 self ._reader ._stop ()
173- # self._thread.quit()
174- # self._thread.wait()
175204 super ().closeEvent (event )
176205
177206 def _open_native (self ) -> None :
0 commit comments