Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 132 additions & 3 deletions res/design.ui
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@
<x>0</x>
<y>0</y>
<width>786</width>
<height>426</height>
<height>598</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>786</width>
<height>426</height>
<height>450</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>786</width>
<height>426</height>
<height>598</height>
</size>
</property>
<property name="font">
Expand All @@ -35,7 +35,22 @@
<iconset resource="resources.qrc">
<normaloff>:/resources/icon.ico</normaloff>:/resources/icon.ico</iconset>
</property>
<property name="styleSheet">
<string notr="true">QMainWindow::separator { height: 0px; width: 0px; }</string>
</property>
<widget class="QWidget" name="central_widget">
<property name="minimumSize">
<size>
<width>0</width>
<height>404</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>404</height>
</size>
</property>
<widget class="QLabel" name="x_label">
<property name="geometry">
<rect>
Expand Down Expand Up @@ -930,9 +945,105 @@
<addaction name="action_save_profile_as"/>
<addaction name="action_load_profile"/>
</widget>
<widget class="QMenu" name="menu_view">
<property name="title">
<string>View</string>
</property>
<addaction name="action_toggle_logs"/>
</widget>
<addaction name="menu_file"/>
<addaction name="menu_view"/>
<addaction name="menu_help"/>
</widget>
<widget class="QStatusBar" name="status_bar">
<property name="styleSheet">
<string notr="true">QStatusBar { border-top: 1px solid palette(mid); }
ClickableLabel { padding: 1px 16px 1px 6px; }
ClickableLabel:hover { background-color: palette(midlight); }</string>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<widget class="ClickableLabel" name="log_footer_label">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>100</width>
<height>30</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Last log message. Click to show the full log.</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</widget>
<widget class="QDockWidget" name="log_dock">
<property name="styleSheet">
<string notr="true">QDockWidget::title { padding: 6px; }</string>
</property>
<property name="features">
<set>QDockWidget::DockWidgetFeature::NoDockWidgetFeatures</set>
</property>
<property name="windowTitle">
<string>1 (docker header can't be hidden in editor, removed at runtime)
2 (line count height is approximate, making header appear about 2 lines)</string>
</property>
<attribute name="dockWidgetArea">
<number>8</number>
</attribute>
<widget class="QWidget" name="log_dock_contents">
<layout class="QVBoxLayout" name="log_dock_layout">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPlainTextEdit" name="log_history_view">
<property name="font">
<font>
<family>monospace</family>
</font>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="plainText">
<string>3
4
5
6
7
8
9
10
11</string>
</property>
<property name="textInteractionFlags">
<set>Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<action name="action_view_help">
<property name="text">
<string>View Help</string>
Expand Down Expand Up @@ -1031,7 +1142,25 @@
<enum>QAction::MenuRole::AboutQtRole</enum>
</property>
</action>
<action name="action_toggle_logs">
<property name="text">
<string>Toggle Logs</string>
</property>
<property name="shortcut">
<string>Ctrl+L</string>
</property>
<property name="shortcutContext">
<enum>Qt::ShortcutContext::ApplicationShortcut</enum>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>ClickableLabel</class>
<extends>QLabel</extends>
<header>log_capture</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>split_image_folder_input</tabstop>
<tabstop>x_spinbox</tabstop>
Expand Down
110 changes: 109 additions & 1 deletion src/AutoSplit.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3

import os
import re
import sys
import warnings

Expand Down Expand Up @@ -33,6 +34,13 @@ def do_nothing(*_): ...
# os.environ.setdefault("QT_DEBUG_PLUGINS", "1")

# ruff: disable[E402] # https://github.com/astral-sh/ruff/issues/21423

# Tee stdout/stderr as soon as possible, so import-time warnings and errors are also caught.
# This must run after the platform-specific setup above (which has to happen before any Qt import).
import log_capture

log_capture.install()

import signal
from collections.abc import Callable
from copy import deepcopy
Expand All @@ -43,7 +51,7 @@ def do_nothing(*_): ...
import cv2
from PySide6 import QtCore, QtGui
from PySide6.QtTest import QTest
from PySide6.QtWidgets import QApplication, QFileDialog, QLabel, QMainWindow, QMessageBox
from PySide6.QtWidgets import QApplication, QFileDialog, QLabel, QMainWindow, QMessageBox, QWidget

import error_messages
import user_profile
Expand Down Expand Up @@ -100,6 +108,12 @@ def do_nothing(*_): ...

CHECK_FPS_ITERATIONS = 10

LOG_STDERR_COLOR = QtGui.QColor("#c0392b")
"""Color for log lines that came from stderr (warnings, errors, tracebacks).
Source: Pomegranate from https://flatuicolors.com/palette/defo"""
LOG_LOCATION_PREFIX_RE = re.compile(r"^\S+:\d+:\s+")
"""Matches a leading `path:lineno:` (e.g. from a warning) to drop it from the footer preview."""


class AutoSplit(QMainWindow, design.Ui_MainWindow):
# Parse command line args
Expand Down Expand Up @@ -173,6 +187,7 @@ def _show_error_signal_slot(error_message_box: Callable[..., object]):
f"AutoSplit v{AUTOSPLIT_VERSION}"
+ (" (externally controlled)" if self.is_auto_controlled else "")
)
self._setup_log_footer()

# Hotkeys need to be initialized to be passed as thread arguments in hotkeys.py
for hotkey in HOTKEYS:
Expand All @@ -197,6 +212,7 @@ def _show_error_signal_slot(error_message_box: Callable[..., object]):
self.update_auto_control.start()

# Connecting menu actions
self.action_toggle_logs.triggered.connect(self._toggle_log_panel)
self.action_view_help.triggered.connect(view_help)
self.action_about.triggered.connect(lambda: open_about(self))
self.action_about_qt.triggered.connect(about_qt)
Expand Down Expand Up @@ -283,6 +299,98 @@ def _update_checker_widget_signal_slot(latest_version: str, check_on_open: bool)

# FUNCTIONS

# region Log footer panel
_last_log_footer_entry: log_capture.LogLine | None = None
"""Last (timestamp, text, is_stderr) shown in the footer, kept so it can be re-rendered."""

_collapsed_height = 0
"""
Window height with the log panel collapsed; captured from the .ui to fix the height per state.
"""

_log_panel_height = 0
"""Window growth when the panel opens; derived from the .ui (expanded - collapsed height)."""

def _setup_log_footer(self):
"""Wire up the clickable log footer and the expandable log history panel."""
# Both come from the .ui: collapsed = minimumSize height, panel = designed (expanded)
# geometry height minus that. Captured before setFixedHeight() overwrites minimumHeight().
self._collapsed_height = self.minimumHeight()
self._log_panel_height = self.height() - self._collapsed_height
# The status bar doesn't add() its .ui child, and stretch isn't expressible there.
self.status_bar.addWidget(self.log_footer_label, 1)
self.status_bar.setContentsMargins(0, 0, 0, 0) # Let the label's padding define the insets.
# An empty title bar (collapses to no height) makes the dock read as an inline panel.
self.log_dock.setTitleBarWidget(QWidget())
self.log_history_view.setFont( # Use the system's monospace font.
QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.SystemFont.FixedFont)
)
# Drop the .ui's Designer-only placeholders (kept there as layout/line-count notes).
self.log_history_view.clear()
self.log_dock.setWindowTitle("")
self.log_footer_label.clicked.connect(self._toggle_log_panel)

# Include logs already emitted before connecting live.
history = log_capture.LOG_EMITTER.history()
for log_line in history:
self._append_log_line(log_line)
if history:
self._update_log_footer(history[-1])
# Each live line goes into the panel and updates the footer
# (append first, so the footer's single-line preview reflects the line that was just added).
log_capture.LOG_EMITTER.line_logged.connect(self._append_log_line)
log_capture.LOG_EMITTER.line_logged.connect(self._update_log_footer)

# Restore the panel's last expanded/collapsed state.
self._set_log_panel_visible(
show=cast("bool", user_profile.QT_SETTINGS.value("log_panel_visible", False, type=bool))
)

def _set_log_panel_visible(self, show: bool): # noqa: FBT001 # boolean value setter, not an arbitrary flag
self.log_dock.setVisible(show)
# Fix the height per state so it can't be dragged to over-expand or hide content.
self.setFixedHeight(self._collapsed_height + (self._log_panel_height if show else 0))
self._refresh_log_footer() # Flip the chevron to match the new state.

def _toggle_log_panel(self):
self._set_log_panel_visible(not self.log_dock.isVisible())
user_profile.QT_SETTINGS.setValue("log_panel_visible", self.log_dock.isVisible())

def _append_log_line(self, log_line: log_capture.LogLine):
timestamp, text, is_stderr = log_line
cursor = self.log_history_view.textCursor()
cursor.movePosition(QtGui.QTextCursor.MoveOperation.End)
char_format = QtGui.QTextCharFormat()
if is_stderr:
char_format.setForeground(LOG_STDERR_COLOR)
# Newline before each entry (not after) so there is no trailing blank line.
prefix = "" if self.log_history_view.document().isEmpty() else "\n"
# Align a multi-line entry's continuation lines under the text (past the timestamp).
body = text.replace("\n", "\n" + " " * (len(timestamp) + 1))
cursor.insertText(f"{prefix}{timestamp} {body}", char_format)
scrollbar = self.log_history_view.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())

def _update_log_footer(self, log_line: log_capture.LogLine):
self._last_log_footer_entry = log_line
self._refresh_log_footer()

def _refresh_log_footer(self):
if self._last_log_footer_entry is None:
return
timestamp, text, is_stderr = self._last_log_footer_entry
# Footer is single-line and shows the message directly (no path prefix).
# First line of an (already-relativized) entry; drops a leading `path:lineno:` prefix.
first_line = LOG_LOCATION_PREFIX_RE.sub("", text.split("\n", 1)[0])
# Footer affordances hinting it expands a log panel: a chevron and hover/border styling.
# Not using isVisible()) as it's incorrect during startup restore, before window is shown
chevron = "▶" if self.log_dock.isHidden() else "▲"
stderr_color = f"color: {LOG_STDERR_COLOR.name()};" if is_stderr else ""
self.log_footer_label.setStyleSheet(stderr_color)
self.log_footer_label.set_elided_text(f"{chevron} {timestamp} {first_line}")

# endregion

def __browse(self):
# User selects the file with the split images in it.
new_split_image_directory = QFileDialog.getExistingDirectory(
Expand Down
6 changes: 5 additions & 1 deletion src/error_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from types import TracebackType
from typing import TYPE_CHECKING, NoReturn

from PySide6 import QtCore, QtWidgets
from PySide6 import QtCore, QtGui, QtWidgets

from utils import FROZEN, GITHUB_REPOSITORY

Expand Down Expand Up @@ -41,6 +41,10 @@ def _set_text_message(
kill_button: str = "",
accept_button: str = "",
):
# Also surface the error message in the logs
plain_message = QtGui.QTextDocumentFragment.fromHtml(message).toPlainText()
sys.stderr.write(f"{plain_message}\n{details}\n" if details else f"{plain_message}\n")

message_box = QtWidgets.QMessageBox()
message_box.setWindowTitle("Error")
message_box.setTextFormat(QtCore.Qt.TextFormat.RichText)
Expand Down
Loading
Loading