2026-02-22 15:35:12 +00:00
|
|
|
from datetime import datetime
|
|
|
|
|
import os
|
|
|
|
|
import ntpath
|
|
|
|
|
from time import monotonic
|
|
|
|
|
|
|
|
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ScanSlicerStatusMixin: class definition for ScanSlicer behavior.
|
2026-02-22 15:40:12 +00:00
|
|
|
class ScanSlicerStatusMixin:
|
|
|
|
|
STATUS_EVENT_PUMP_INTERVAL_SEC = 0.05
|
|
|
|
|
STATUS_LOG_FLUSH_INTERVAL_SEC = 0.5
|
|
|
|
|
STATUS_LOG_BATCH_SIZE = 24
|
|
|
|
|
|
|
|
|
|
# update_status_filename: function definition for ScanSlicer behavior.
|
|
|
|
|
def update_status_filename(self, image_path=None):
|
|
|
|
|
label = getattr(self, "status_filename_label", None)
|
|
|
|
|
if label is None:
|
|
|
|
|
return
|
|
|
|
|
if image_path:
|
|
|
|
|
label.setText(ntpath.basename(str(image_path)))
|
|
|
|
|
else:
|
|
|
|
|
label.setText("No file")
|
|
|
|
|
|
|
|
|
|
# resolve_status_log_path: function definition for ScanSlicer behavior.
|
|
|
|
|
def resolve_status_log_path(self):
|
|
|
|
|
override = os.environ.get("SCANSLICER_STATUS_LOG_PATH")
|
|
|
|
|
if override:
|
|
|
|
|
return override
|
|
|
|
|
|
|
|
|
|
if os.path.exists("/.dockerenv") and os.path.isdir("/work"):
|
|
|
|
|
return os.path.join("/work", ".scanslicer", "status.log")
|
|
|
|
|
|
|
|
|
|
config_home = os.environ.get("XDG_CONFIG_HOME")
|
|
|
|
|
if not config_home:
|
|
|
|
|
config_home = os.path.join(os.path.expanduser("~"), ".config")
|
|
|
|
|
return os.path.join(config_home, "scanslicer", "status.log")
|
|
|
|
|
|
|
|
|
|
# initialize_status_log: function definition for ScanSlicer behavior.
|
|
|
|
|
def initialize_status_log(self):
|
|
|
|
|
self._status_log_buffer = []
|
|
|
|
|
self._status_log_last_flush_ts = monotonic()
|
|
|
|
|
self.status_log_path = self.resolve_status_log_path()
|
|
|
|
|
self._status_log_file = None
|
|
|
|
|
try:
|
|
|
|
|
directory = os.path.dirname(self.status_log_path)
|
|
|
|
|
if directory:
|
|
|
|
|
os.makedirs(directory, exist_ok=True)
|
|
|
|
|
# Start each app launch with a fresh log file.
|
|
|
|
|
self._status_log_file = open(self.status_log_path, "w", encoding="utf-8")
|
|
|
|
|
self._status_log_file.write("=== ScanSlicer session ")
|
|
|
|
|
self._status_log_file.write(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
|
|
|
|
self._status_log_file.write(" ===\n")
|
|
|
|
|
self._status_log_file.flush()
|
|
|
|
|
except OSError:
|
|
|
|
|
self.status_logging_enabled = False
|
|
|
|
|
self._status_log_file = None
|
|
|
|
|
|
|
|
|
|
# _flush_status_log_if_due: function definition for ScanSlicer behavior.
|
|
|
|
|
def _flush_status_log_if_due(self, force=False):
|
|
|
|
|
if not self.status_logging_enabled or not self.status_log_path:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
buffer = getattr(self, "_status_log_buffer", None)
|
|
|
|
|
if not buffer:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
now = monotonic()
|
|
|
|
|
last_flush = getattr(self, "_status_log_last_flush_ts", 0.0)
|
|
|
|
|
due_by_time = (now - last_flush) >= self.STATUS_LOG_FLUSH_INTERVAL_SEC
|
|
|
|
|
due_by_size = len(buffer) >= self.STATUS_LOG_BATCH_SIZE
|
|
|
|
|
if not force and not due_by_time and not due_by_size:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
payload = "".join(buffer)
|
|
|
|
|
try:
|
|
|
|
|
if self._status_log_file is None:
|
|
|
|
|
self._status_log_file = open(self.status_log_path, "a", encoding="utf-8")
|
|
|
|
|
self._status_log_file.write(payload)
|
|
|
|
|
self._status_log_file.flush()
|
|
|
|
|
except OSError:
|
|
|
|
|
self.status_logging_enabled = False
|
|
|
|
|
self._status_log_buffer = []
|
|
|
|
|
if self._status_log_file is not None:
|
|
|
|
|
try:
|
|
|
|
|
self._status_log_file.close()
|
|
|
|
|
except OSError:
|
|
|
|
|
pass
|
|
|
|
|
self._status_log_file = None
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self._status_log_buffer = []
|
|
|
|
|
self._status_log_last_flush_ts = now
|
|
|
|
|
|
|
|
|
|
# _close_status_log: function definition for ScanSlicer behavior.
|
|
|
|
|
def _close_status_log(self):
|
|
|
|
|
self._flush_status_log_if_due(force=True)
|
|
|
|
|
fh = getattr(self, "_status_log_file", None)
|
|
|
|
|
if fh is None:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
fh.close()
|
|
|
|
|
except OSError:
|
|
|
|
|
pass
|
|
|
|
|
self._status_log_file = None
|
|
|
|
|
|
|
|
|
|
# append_status_log: function definition for ScanSlicer behavior.
|
|
|
|
|
def append_status_log(self, text):
|
|
|
|
|
if not self.status_logging_enabled or not self.status_log_path:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
line = f"[{timestamp}] {text}\n"
|
|
|
|
|
buffer = getattr(self, "_status_log_buffer", None)
|
|
|
|
|
if buffer is None:
|
|
|
|
|
self._status_log_buffer = [line]
|
|
|
|
|
self._status_log_last_flush_ts = monotonic()
|
|
|
|
|
else:
|
|
|
|
|
buffer.append(line)
|
|
|
|
|
self._flush_status_log_if_due(force=False)
|
|
|
|
|
|
|
|
|
|
# log_detection_config: function definition for ScanSlicer behavior.
|
|
|
|
|
def log_detection_config(self):
|
|
|
|
|
preset_name = self.preset_dropdown.currentText()
|
|
|
|
|
values = self.params.to_dict()
|
|
|
|
|
values_text = ", ".join(f"{name}={values[name]}" for name in sorted(values.keys()))
|
|
|
|
|
self.append_status_log(f"Detection config: preset={preset_name}")
|
|
|
|
|
self.append_status_log(f"Detection params: {values_text}")
|
|
|
|
|
|
|
|
|
|
# _pump_ui_events_if_due: function definition for ScanSlicer behavior.
|
|
|
|
|
def _pump_ui_events_if_due(self, force=False):
|
|
|
|
|
now = monotonic()
|
|
|
|
|
last_pump = getattr(self, "_status_last_event_pump_ts", 0.0)
|
|
|
|
|
if force or (now - last_pump) >= self.STATUS_EVENT_PUMP_INTERVAL_SEC:
|
|
|
|
|
QApplication.processEvents()
|
|
|
|
|
self._status_last_event_pump_ts = now
|
|
|
|
|
|
|
|
|
|
# update_statusbar: function definition for ScanSlicer behavior.
|
|
|
|
|
def update_statusbar(self, text):
|
|
|
|
|
self.status_message_label.setText(text)
|
|
|
|
|
self.append_status_log(text)
|
|
|
|
|
self._pump_ui_events_if_due()
|
|
|
|
|
|
|
|
|
|
# set_processing_busy: function definition for ScanSlicer behavior.
|
|
|
|
|
def set_processing_busy(self, busy, status_text=None):
|
|
|
|
|
busy = bool(busy)
|
|
|
|
|
if self.processing_busy == busy:
|
|
|
|
|
if status_text is not None:
|
|
|
|
|
self.update_statusbar(status_text)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self.processing_busy = busy
|
|
|
|
|
enabled = not busy
|
|
|
|
|
|
|
|
|
|
if self.controls_scroll is not None:
|
|
|
|
|
self.controls_scroll.setEnabled(enabled)
|
|
|
|
|
if self.preview_toolbar is not None:
|
|
|
|
|
self.preview_toolbar.setEnabled(enabled)
|
|
|
|
|
self.slicing_canvas.setEnabled(enabled)
|
|
|
|
|
|
|
|
|
|
if status_text is not None:
|
|
|
|
|
self.update_statusbar(status_text)
|
|
|
|
|
|
|
|
|
|
if enabled:
|
|
|
|
|
self.update_quality_controls_state()
|
|
|
|
|
self.update_parameter_visibility()
|
|
|
|
|
self.refresh_active_box_toolbar()
|
|
|
|
|
|
|
|
|
|
self._pump_ui_events_if_due(force=True)
|
|
|
|
|
self._flush_status_log_if_due(force=True)
|