Skip to content

API Reference

Bases: QMainWindow

A QMainWindow subclass that runs a function on a background thread and streams its stdout/stderr live into a QTextEdit.

Subclasses must call set_output_field() with a QTextEdit before calling _started(), and should override _started's argument collection to pass whatever inputs the wrapped function needs.

Parameters:

Name Type Description Default
func Callable

The function to be run

required
Source code in src/qtwrap/window/script_capture_window.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
class ScriptCaptureWindow(QMainWindow):
    """
    A QMainWindow subclass that runs a function on a background thread
    and streams its stdout/stderr live into a QTextEdit.

    Subclasses must call `set_output_field()` with a QTextEdit before
    calling `_started()`, and should override `_started`'s argument
    collection to pass whatever inputs the wrapped function needs.

    Args:
        func (Callable): The function to be run
    """

    def __init__(self, func):
        super().__init__()

        self.__func = func
        self.__thread = None
        self.__worker = None
        self.__output = None
        self.__old_stdout = None

        self.__redirector = StreamRedirector()
        self.__redirector.text_written.connect(
            self.__append_text, type=Qt.ConnectionType.QueuedConnection
        )

        self.__status_bar = StatusBar()
        self.setStatusBar(self.__status_bar)

        self.__menu_bar = QMenuBar()
        self.setMenuBar(self.__menu_bar)

        self.__save_menu = self.__menu_bar.addMenu("&Save")
        self.__save_menu.addAction("Save Output", self.__save_output)

    def _started(self, args: tuple = (), kwargs: dict | None = None):
        """
        Run the wrapped function on a background QThread.

        Disables all input widgets in the central widget for the
        duration of the run and re-enables them once finished, whether
        the function completes normally or raises.

        Args:
            args (tuple): Positional arguments forwarded to the wrapped function.
            kwargs (dict): Keyword arguments forwarded to the wrapped function.

        Raises:
            RuntimeError: If `set_output_field()` was never called.
        """

        if kwargs is None:
            kwargs = {}
        if self.__output is None: raise RuntimeError("The output field must be set.")
        if self.__thread is not None and self.__thread.isRunning():
            return

        self.__status_bar.clearMessage()

        self.__output.clear()
        self.__set_inputs_enabled(False)
        self.__output.setTextColor(QColor(*LogLevel.INFO.value))

        self.__old_stdout = sys.stdout
        sys.stdout = self.__redirector

        self.__thread = QThread()
        self.__worker = OutputCaptureWorker(self.__func, args, kwargs)
        self.__worker.moveToThread(self.__thread)

        self.__thread.started.connect(self.__worker.run)
        self.__worker.error_signal.connect(
            lambda text: self.__append_text(text=text, level=LogLevel.ERROR),
            type=Qt.ConnectionType.QueuedConnection,
        )
        self.__worker.finished_signal.connect(self.__finished)

        self.__thread.start()

    def __finished(self):
        sys.stdout = self.__old_stdout
        self.__thread.quit()
        self.__thread.wait()
        self.__worker.deleteLater()
        self.__thread.deleteLater()
        self.__worker = None
        self.__thread = None
        self.__set_inputs_enabled(True)

    def __append_text(self, text: str, level: LogLevel = LogLevel.INFO):
        if text and self.__output:
            self.__output.setTextColor(QColor(*level.value))
            self.__output.insertPlainText(text)
            self.__output.ensureCursorVisible()

            if level == LogLevel.ERROR:
                self.status(msg=f"Script Error: \"{text.split(chr(10))[-2]}\"", level=level)

    def __set_inputs_enabled(self, enabled: bool = True):
        central = self.centralWidget()
        if central is None:
            return
        for obj in central.findChildren(QWidget):
            widget = cast(QWidget, obj)
            if widget is not self.__output:
                widget.setEnabled(enabled)

    def __save_output(self):
        file_path, _ = QFileDialog.getSaveFileName(self, 'Save Script Output', log_file_path())
        if file_path:
            with open(file_path, 'w') as save_file:
                save_file.write(self.__output.toPlainText())

    def get_output_field(self) -> QTextEdit:
        return self.__output

    def set_output_field(self, output_field: QTextEdit):
        self.__output = output_field
        self.__output.setReadOnly(True)

    def status(self, msg: str, level: LogLevel = LogLevel.INFO):
        self.__status_bar.log_status(msg, level)

Bases: _BrowsingSelector

A path-selection widget for choosing a single directory.

Displays a label, an editable text field showing the selected path, and a browse button that opens a native directory-picker dialog.

Parameters:

Name Type Description Default
field_label str

Text displayed in the label next to the path field.

'Select Directory:'
btn_label str

Text displayed on the browse button.

'Browse'
inline bool

If True, arranges the label, path field, and button in a single horizontal row. If False, places the label on its own row above the path field and button.

True
Source code in src/qtwrap/widgets/directory_selector.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class DirectorySelector(_BrowsingSelector):
    """
    A path-selection widget for choosing a single directory.

    Displays a label, an editable text field showing the selected path,
    and a browse button that opens a native directory-picker dialog.

    Args:
        field_label: Text displayed in the label next to the path field.
        btn_label: Text displayed on the browse button.
        inline: If True, arranges the label, path field, and button in a
            single horizontal row. If False, places the label on its own
            row above the path field and button.
    """

    def __init__(self, field_label: str = "Select Directory:", btn_label: str = "Browse", inline: bool = True):
        super().__init__(field_label=field_label, btn_label=btn_label, inline=inline)

    def _selector_dialog(self):
        """
        Open a directory-picker dialog and update the path field with the chosen directory.

        Does nothing if the user cancels the dialog.
        """
        dir_path = QFileDialog.getExistingDirectory(
            self,
            self._label.text(),
            QDir.currentPath()
        )
        if dir_path: self._path_input.setText(str(Path(dir_path)))

Bases: _BrowsingSelector

A path-selection widget for choosing a single file.

Displays a label, an editable text field showing the selected path, and a browse button that opens a native file-picker dialog.

Parameters:

Name Type Description Default
field_label str

Text displayed in the label next to the path field.

'Select File:'
btn_label str

Text displayed on the browse button.

'Browse'
inline bool

If True, arranges the label, path field, and button in a single horizontal row. If False, places the label on its own row above the path field and button.

True
file_filter str

A Qt file-dialog filter string restricting which files are shown (e.g. ".json"). Defaults to "", allowing any file.

'*'
Source code in src/qtwrap/widgets/file_selector.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class FileSelector(_BrowsingSelector):
    """
    A path-selection widget for choosing a single file.

    Displays a label, an editable text field showing the selected path,
    and a browse button that opens a native file-picker dialog.

    Args:
        field_label: Text displayed in the label next to the path field.
        btn_label: Text displayed on the browse button.
        inline: If True, arranges the label, path field, and button in a
            single horizontal row. If False, places the label on its own
            row above the path field and button.
        file_filter: A Qt file-dialog filter string restricting which
            files are shown (e.g. "*.json"). Defaults to "*", allowing
            any file.
    """

    def __init__(self, field_label: str = "Select File:", btn_label: str = "Browse", inline: bool = True, file_filter: str = '*'):
        super().__init__(field_label=field_label, btn_label=btn_label, inline=inline)
        self.__file_filter = file_filter

    def _selector_dialog(self):
        """
        Open a file-picker dialog and update the path field with the chosen file.

        Does nothing if the user cancels the dialog.
        """
        file_path, _ = QFileDialog.getOpenFileName(
            self,
            self._label.text(),
            QDir.currentPath(),
            self.__file_filter
        )
        if file_path: self._path_input.setText(str(Path(file_path)))

Bases: QWidget

A widget for selecting one or more options from a set of radio buttons.

Displays a title label followed by a row (or column) of radio buttons, one per option. Options can be added at construction time or later via add_options().

Parameters:

Name Type Description Default
*options

Labels for the radio buttons to display initially.

()
title str

Text displayed above the options.

'ChoiceSelector:'
inline bool

If True, arranges the options in a single horizontal row. If False, stacks them vertically.

True
exclusive bool

If True, only one option can be selected at a time (standard radio-button behavior). If False, multiple options can be selected simultaneously.

True
Source code in src/qtwrap/widgets/choice_selector.py
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class ChoiceSelector(QWidget):
    """
    A widget for selecting one or more options from a set of radio buttons.

    Displays a title label followed by a row (or column) of radio
    buttons, one per option. Options can be added at construction time
    or later via `add_options()`.

    Args:
        *options: Labels for the radio buttons to display initially.
        title: Text displayed above the options.
        inline: If True, arranges the options in a single horizontal
            row. If False, stacks them vertically.
        exclusive: If True, only one option can be selected at a time
            (standard radio-button behavior). If False, multiple
            options can be selected simultaneously.
    """

    def __init__(self, *options, title: str = "ChoiceSelector:", inline: bool = True, exclusive: bool = True):
        super().__init__()

        self.__parent_layout = QVBoxLayout()
        self.__child_layout = QHBoxLayout() if inline else QVBoxLayout()
        self.__child_widget = QWidget()

        self.__title = QLabel(title)
        self.__btn_group = QButtonGroup()
        self.__btn_group.setExclusive(exclusive)

        self.add_options(*options)
        self.__child_widget.setLayout(self.__child_layout)

        self.__parent_layout.addWidget(self.__title)
        self.__parent_layout.addWidget(self.__child_widget)
        self.setLayout(self.__parent_layout)

    def add_options(self, *options):
        """
        Add additional radio-button options to the widget.

        Args:
            *options: Labels for the new radio buttons to append.
        """
        for option in options:
            btn = QRadioButton(option)
            self.__btn_group.addButton(btn)
            self.__child_layout.addWidget(btn)

    def choice(self) -> list[str]:
        """
        Return the currently selected option(s).

        Returns:
            The labels of all currently checked radio buttons. Contains
            at most one element if `exclusive` was True at construction,
            or an empty list if nothing has been selected yet.
        """
        return [btn.text() for btn in self.__btn_group.buttons() if btn.isChecked()]

add_options(*options)

Add additional radio-button options to the widget.

Parameters:

Name Type Description Default
*options

Labels for the new radio buttons to append.

()
Source code in src/qtwrap/widgets/choice_selector.py
40
41
42
43
44
45
46
47
48
49
50
def add_options(self, *options):
    """
    Add additional radio-button options to the widget.

    Args:
        *options: Labels for the new radio buttons to append.
    """
    for option in options:
        btn = QRadioButton(option)
        self.__btn_group.addButton(btn)
        self.__child_layout.addWidget(btn)

choice()

Return the currently selected option(s).

Returns:

Type Description
list[str]

The labels of all currently checked radio buttons. Contains

list[str]

at most one element if exclusive was True at construction,

list[str]

or an empty list if nothing has been selected yet.

Source code in src/qtwrap/widgets/choice_selector.py
52
53
54
55
56
57
58
59
60
61
def choice(self) -> list[str]:
    """
    Return the currently selected option(s).

    Returns:
        The labels of all currently checked radio buttons. Contains
        at most one element if `exclusive` was True at construction,
        or an empty list if nothing has been selected yet.
    """
    return [btn.text() for btn in self.__btn_group.buttons() if btn.isChecked()]

Bases: QStatusBar

A QStatusBar subclass that adds a logging mechanic. The status text color is automatically determined from the given :class:qtwrap.enums.LogLevel.

Source code in src/qtwrap/window/tools/status_bar.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class StatusBar(QStatusBar):
    """
    A QStatusBar subclass that adds a logging mechanic.
    The status text color is automatically determined from the given :class:`qtwrap.enums.LogLevel`.
    """

    def __init__(self):
        super().__init__()

    def log_status(self, status: str, level: LogLevel = LogLevel.INFO):
        self.setStyleSheet(f"""
            color: rgb{str(level.value)}
        """)
        self.showMessage(status)

Bases: Enum

Enum holding logging levels constants and their associated output color represented as RGB tuple.

Example

LogLevel.INFO.value equals (255, 255, 255) so INFO log events will be displayed in white.

Source code in src/qtwrap/enums/log_level.py
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class LogLevel(Enum):
    """
    Enum holding logging levels constants and their associated output color represented as RGB tuple.

    Example:
        LogLevel.INFO.value equals (255, 255, 255) so INFO log events will be displayed in white.
    """

    INFO = (255, 255, 255)
    WARNING = (255, 255, 0)
    ERROR = (255, 0, 0)