Graphical User Interface

The following functions are classes are provided in the gui module. They are central in producing and managing the user interface as well as starting the worker threads responsible for producing plots from data.

Plot Manager

Orchestrate a plotting run in a background thread to keep the GUI responsive.

Workflow

  1. Collect the selected file labels from the main window.
  2. Build a reduced DataSet containing only the selected files, colours, device, structure type, and name.
  3. Collect plotting options from the active device widget (via alias properties) and from global GUI controls, storing them in PlotterOptions.
  4. Resolve and instantiate the appropriate device worker class from the implementations.devices.workers namespace.
  5. Configure the worker with the reduced dataset, selected plot function, and options.
  6. Move the worker to a QThread, wire up progress/finished signals, and start the thread.

The function logs a concise summary of the run (including a short run identifier) to the GUI console once the worker is started.

Parameters

window : QMainWindow Main application window providing access to the dataset, widgets (file selection, stacked options, checkboxes, etc.), and console API.

Source code in gui\plot_manager.py
 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
 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
@with_logging
def plot_manager(window, *args, **kwargs):
    """
        Orchestrate a plotting run in a background thread to keep the GUI responsive.

        Workflow
        --------
        1. Collect the selected file labels from the main window.
        2. Build a reduced `DataSet` containing only the selected files, colours,
           device, structure type, and name.
        3. Collect plotting options from the active device widget (via `alias`
           properties) and from global GUI controls, storing them in `PlotterOptions`.
        4. Resolve and instantiate the appropriate device worker class from the
           `implementations.devices.workers` namespace.
        5. Configure the worker with the reduced dataset, selected plot function,
           and options.
        6. Move the worker to a `QThread`, wire up progress/finished signals, and
           start the thread.

        The function logs a concise summary of the run (including a short run
        identifier) to the GUI console once the worker is started.

        Parameters
        ----------
        window : QMainWindow
            Main application window providing access to the dataset, widgets
            (file selection, stacked options, checkboxes, etc.), and console API.
        """

    # Grab the selected files for plotting and build a reduced dataset
    dataset_time = datetime.datetime.now().strftime(constants.DATETIME_FORMAT)
    experiment_time = window.dataset.get_experiment_date().strftime(constants.DATETIME_FORMAT)
    dataset_selection = dataset_manager.DataSet(dataset_time)
    dataset_selection.set_experiment_date(experiment_time)

    for item in window.selectedFilesList.selectedItems():
        lbl = item.text()

        structure = window.dataset.get_structure_type()
        dataset_selection.set_structure_type(structure)

        path = window.dataset.get_filepath(lbl)
        dataset_selection.add_filepath(path, lbl)

        colour = window.dataset.get_single_colour(lbl)
        dataset_selection.add_colour(colour, lbl)

    dataset_selection.set_device(window.dataset.get_device())
    dataset_selection.set_structure_type(window.dataset.get_structure_type())
    dataset_selection.set_name(window.dataset.get_name())

    # Recursively search for QWidget children with an alias to collect options and get their values
    options = PlotterOptions()
    for option in window.stackedWidget.currentWidget().findChildren(QtWidgets.QWidget):
        alias = option.property("alias")
        if alias is not None:
            option_value = get_qwidget_value(option)
            if option_value is not None:
                options.add_option(label=alias, value=option_value)
    options.add_option(label="presentation", value=get_qwidget_value(window.presentationCheckBox))
    options.add_option(label="legend_title", value=get_qwidget_value(window.legendTitleLineEdit))

    # Instantiate proper device class and set the data
    current_device_class = window.dataset.get_device()
    device_module = getattr(implementations.devices.workers, current_device_class.lower())
    device = getattr(device_module, current_device_class)

    # # Grab the correct plotting function and pass all options to it
    plot_function = window.get_current_plot_function()

    # Create a new thread for the device class to run in
    window.thread = QtCore.QThread()
    window.device_worker = device(current_device_class, dataset_selection, plot_function, options=options)
    window.device_worker.moveToThread(window.thread)

    # Connect signals and slots for the worker thread
    window.thread.started.connect(window.device_worker.run)

    # When the worker says "I'm done", stop the thread and schedule the worker for deletion
    window.device_worker.finished.connect(window.thread.quit)
    window.device_worker.finished.connect(window.device_worker.deleteLater)

    # When the thread is actually finished, clean up and reset GUI
    window.thread.finished.connect(window.thread.deleteLater)
    window.thread.finished.connect(window.on_plot_thread_finished)

    # Progress updates
    window.device_worker.progress.connect(window.report_progress)

    # Start the thread
    window.thread.start()
    window.console_print(
        f"(run {window.device_worker.identifier}) producing {current_device_class}-{plot_function} plot for {window.get_dataset_name()} with options {options}")

    window.plotBtn.setEnabled(False)

Utils

Clear

Fully reset the GUI state and remove all in-memory data.

This helper: - Clears the active DataSet and GUI fields via clear_data. - Empties the console widget. - Writes a confirmation message to the GUI console.

Parameters

window : QMainWindow Main application window whose state should be cleared.

Source code in gui\utils\clear\clear_all.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def clear_all(window: QtWidgets.QMainWindow):
    """
    Fully reset the GUI state and remove all in-memory data.

    This helper:
    - Clears the active `DataSet` and GUI fields via `clear_data`.
    - Empties the console widget.
    - Writes a confirmation message to the GUI console.

    Parameters
    ----------
    window : QMainWindow
        Main application window whose state should be cleared.
    """
    clear_data(window)
    window.consoleTextEdit.clear()
    window.console_print("Cleared memory")

Clear the currently loaded dataset and reset all GUI widgets tied to it.

Resets: - Stored DataSet object and its disk location. - Set name, device name, notes, and list widgets. - Plot type combobox and stacked widget view.

A console message is printed to confirm completion.

Parameters

window : QMainWindow Main GUI instance that holds dataset-related widgets.

Source code in gui\utils\clear\clear_data.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
def clear_data(window: QtWidgets.QMainWindow):
    """
    Clear the currently loaded dataset and reset all GUI widgets tied to it.

    Resets:
    - Stored `DataSet` object and its disk location.
    - Set name, device name, notes, and list widgets.
    - Plot type combobox and stacked widget view.

    A console message is printed to confirm completion.

    Parameters
    ----------
    window : QMainWindow
        Main GUI instance that holds dataset-related widgets.
    """
    window.dataset = None
    window.dataset_location = None

    window.currSetNameLineEdit.clear()
    window.currDeviceLineEdit.clear()
    window.notesPlainText.clear()
    window.console_print("Cleared dataset from memory")

    window.stackedWidget.setCurrentWidget(window.stackedWidget.widget(0))
    window.selectedFilesList.clear()
    window.plotTypeCombo.clear()

DataSet Tools

Launch the DataSet creation dialog and construct a new dataset from user input.

Workflow: - Opens the DataSet creator window populated with available devices. - On confirmation: * Clears existing state. * Stores the newly created dataset. * Loads it into the GUI and updates the header. * Immediately saves it to disk. - If cancelled, informs the user that no dataset was created.

Parameters

window : QMainWindow Main application window controlling dataset creation.

Source code in gui\utils\dataset_tools\create_dataset.py
 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
@with_logging
def create_dataset(window: QtWidgets.QMainWindow, *args, **kwargs):
    """
    Launch the DataSet creation dialog and construct a new dataset from user input.

    Workflow:
    - Opens the DataSet creator window populated with available devices.
    - On confirmation:
        * Clears existing state.
        * Stores the newly created dataset.
        * Loads it into the GUI and updates the header.
        * Immediately saves it to disk.
    - If cancelled, informs the user that no dataset was created.

    Parameters
    ----------
    window : QMainWindow
        Main application window controlling dataset creation.
    """
    window.set_dataset_window(gui.windows.DataSetCreatorWindow.UiDataCreatorWindow(devices = [k for k in window.devices]))
    window.get_dataset_window().show()

    if window.dataWindow.exec() == 1:
        # If the window was properly closed (Done button) then creation was successful
        #     Copy dataset_tools and print to console
        clear_data(window)
        window.set_dataset(window.get_dataset_window().get_dataset())
        window.console_print(f"DataSet file created")
        load_dataset(window)
        window.update_header()
        save_dataset(window)
    else:
        # Warn user that window was improperly closed and that no dataset_tools was created
        window.console_print("No DataSet file was created")

Populate the GUI with data from the currently loaded dataset.

Actions: - Adds all dataset labels to the file selection list. - Selects all items by default. - Populates the plot-type combobox with device-appropriate plotting functions.

Raises

IncompatibleDeviceTypeFound If the dataset’s device type does not match available plot handlers.

Parameters

window : QMainWindow GUI instance holding a loaded dataset.

Source code in gui\utils\dataset_tools\load_dataset.py
 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
@with_logging
def load_dataset(window: QtWidgets.QMainWindow):
    """
    Populate the GUI with data from the currently loaded dataset.

    Actions:
    - Adds all dataset labels to the file selection list.
    - Selects all items by default.
    - Populates the plot-type combobox with device-appropriate plotting functions.

    Raises
    ------
    IncompatibleDeviceTypeFound
        If the dataset’s device type does not match available plot handlers.

    Parameters
    ----------
    window : QMainWindow
        GUI instance holding a loaded dataset.
    """
    # Add all top level keys to the selection list of the gui
    for label in window.dataset.get_labels():
        window.selectedFilesList.addItem(label)

    # FEATURE REQUEST: Make this a setting
    # Select all items by default
    window.selectedFilesList.selectAll()

    # Edit combobox to show all available plot types
    try:
        for function in window.get_plot_functions(window.get_current_device()):
            window.plotTypeCombo.addItem(function)

    except KeyError:
        window.console_print(
            f"Incompatible device type [{window.get_current_device()}] found in {window.get_dataset_name()}, select another dataset or implement the device type. DataSet path: N/A")
        raise IncompatibleDeviceTypeFound

    window.console_print("DataSet loaded")

Save the currently loaded dataset to disk using a file dialog.

Behaviour: - Ensures a dataset is loaded before saving. - Opens a save-file dialog and writes the dataset via DataSetJSONEncoder. - Auto-appends a valid extension if necessary. - Updates the stored dataset location and logs status messages.

Parameters

window : QMainWindow GUI window containing the active dataset.

Source code in gui\utils\dataset_tools\save_dataset.py
 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
@with_logging
def save_dataset(window: QtWidgets.QMainWindow, *args, **kwargs):
    """
    Save the currently loaded dataset to disk using a file dialog.

    Behaviour:
    - Ensures a dataset is loaded before saving.
    - Opens a save-file dialog and writes the dataset via `DataSetJSONEncoder`.
    - Auto-appends a valid extension if necessary.
    - Updates the stored dataset location and logs status messages.

    Parameters
    ----------
    window : QMainWindow
        GUI window containing the active dataset.
    """

    # Make sure there is a dataset to save
    if window.get_dataset_name() is None:
        return window.console_print("Err: Must first load dataset", level="warning")

    # Run the file dialog
    # TODO: There's a bug in ubuntu 24 that has the filter reinitialised when navigating the path
    file_name = QtWidgets.QFileDialog.getSaveFileName(
        parent=window,
        caption="Save file to disk",
        filter="DataSets (*.json *.dataset *.ds);;All (*)",
        initialFilter="DataSets (*.json *.dataset *.ds)"
    )[0]

    if file_name != "":
        # Ensure the file name has a valid extension
        if file_name:
            if not any(file_name.endswith(ext) for ext in ('.json', '.dataset', '.ds')):
                # Default to .dataset if no valid extension
                file_name += '.ds'

        with open(file_name, "w") as json_file:
            current_dataset = window.get_dataset()
            json.dump(current_dataset, json_file, cls=fs.DataSetJSONEncoder)
            current_dataset.set_location(file_name)
        json_file.close()

        return window.console_print(f"Saved dataset file to {file_name}")
    else:
        # File dialog was exited without choosing a file
        return window.console_print(f"No file selected")

Other

Extract the current value from common Qt widget types.

Supported widgets: - QDoubleSpinBox / QSpinBox → numeric value - QCheckBox → boolean isChecked - QLineEdit / QComboBox → text, with "None"/"none" mapped to None

Raises NotImplementedError if the widget type is unsupported.

Parameters

widget : QWidget The widget whose value should be extracted.

Returns

Any The widget's value in a Python-friendly type.

Source code in gui\utils\get_qwidget_value.py
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
def get_qwidget_value(widget):
    """
    Extract the current value from common Qt widget types.

    Supported widgets:
    - QDoubleSpinBox / QSpinBox → numeric value
    - QCheckBox → boolean `isChecked`
    - QLineEdit / QComboBox → text, with "None"/"none" mapped to `None`

    Raises `NotImplementedError` if the widget type is unsupported.

    Parameters
    ----------
    widget : QWidget
        The widget whose value should be extracted.

    Returns
    -------
    Any
        The widget's value in a Python-friendly type.
    """
    if not isinstance(widget, QtWidgets.QWidget):
        raise ValueError("Input must be a QWidget instance")

    if isinstance(widget, QtWidgets.QDoubleSpinBox) or isinstance(widget, QtWidgets.QSpinBox):
        return widget.value()
    elif isinstance(widget, QtWidgets.QCheckBox):
        return widget.isChecked()
    elif isinstance(widget, QtWidgets.QLineEdit):
        return _cast_none_string_to_none_type(widget.text())
    elif isinstance(widget, QtWidgets.QComboBox):
        return _cast_none_string_to_none_type(widget.currentText())
    else:
        raise NotImplementedError(f"Widget type {type(widget)} not supported")

Find and return the checked QRadioButton within a dialog.

Scans all child radio buttons and returns the first active one.

Parameters

dialog : QDialog Container widget containing radio buttons.

Returns

QRadioButton or None The checked radio button, or None if no selection exists.

Source code in gui\utils\search_for_first_active_radio_button.py
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def search_for_first_active_radio_button(dialog: QtWidgets.QDialog) -> QtWidgets.QRadioButton:
    """
    Find and return the checked QRadioButton within a dialog.

    Scans all child radio buttons and returns the first active one.

    Parameters
    ----------
    dialog : QDialog
        Container widget containing radio buttons.

    Returns
    -------
    QRadioButton or None
        The checked radio button, or `None` if no selection exists.
    """
    for radio_button in dialog.findChildren(QtWidgets.QRadioButton):
        if radio_button.isChecked():
            return radio_button
    return None

Split a CamelCase string into its component words.

Examples

"MyPlotType" → ["My", "Plot", "Type"]

Parameters

camel_case : str Input CamelCase string.

Returns

list[str] List of lowercase/uppercase-correct word segments.

Source code in gui\utils\split_camelCase.py
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def split_camel_case(camel_case) -> list[str]:
    """
    Split a CamelCase string into its component words.

    Examples
    --------
    "MyPlotType" → ["My", "Plot", "Type"]

    Parameters
    ----------
    camel_case : str
        Input CamelCase string.

    Returns
    -------
    list[str]
        List of lowercase/uppercase-correct word segments.
    """
    return re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)', camel_case)

Windows and Dialogs

Bases: QMainWindow

Main GUI window for interactive, automated plotting of experimental data.

Responsibilities

  • Load the main QtDesigner-generated UI file and dynamically attach device- specific option panels to the central QStackedWidget.
  • Manage the currently loaded DataSet:
    • Creating, loading, saving, and autosaving datasets.
    • Displaying raw JSON content and console history in helper dialogs.
    • Updating header fields (set name, device type) when datasets change.
  • Integrate logging with a QTextEdit-based console for time-stamped messages.
  • Provide a thin controller layer for:
    • Launching the plotting pipeline via plot_manager.
    • Handling progress updates and console appends.
    • Adding notes and console history back into the dataset.

On construction, the window: - Discovers available devices from implementations.devices. - Loads and registers per-device widgets and their plot functions. - Wires menu actions and buttons to dataset, plotting, and utility actions. - Optionally auto-opens a demo dataset if a file name is supplied.

The class is intended to be the central hub of the GUI application, with device-specific logic pushed into worker classes and implementations.

Source code in gui\windows\MainWindow.py
 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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
class UiMainWindow(QtWidgets.QMainWindow):
    """
    Main GUI window for interactive, automated plotting of experimental data.

    Responsibilities
    ----------------
    - Load the main QtDesigner-generated UI file and dynamically attach device-
      specific option panels to the central `QStackedWidget`.
    - Manage the currently loaded `DataSet`:
        * Creating, loading, saving, and autosaving datasets.
        * Displaying raw JSON content and console history in helper dialogs.
        * Updating header fields (set name, device type) when datasets change.
    - Integrate logging with a QTextEdit-based console for time-stamped messages.
    - Provide a thin controller layer for:
        * Launching the plotting pipeline via `plot_manager`.
        * Handling progress updates and console appends.
        * Adding notes and console history back into the dataset.

    On construction, the window:
    - Discovers available devices from `implementations.devices`.
    - Loads and registers per-device widgets and their plot functions.
    - Wires menu actions and buttons to dataset, plotting, and utility actions.
    - Optionally auto-opens a demo dataset if a file name is supplied.

    The class is intended to be the central hub of the GUI application, with
    device-specific logic pushed into worker classes and implementations.
    """
    def __init__(self, demo_file_name: str = None):
        super(UiMainWindow, self).__init__()

        self.thread = None
        self.device_worker = None
        self.dataset = None
        self.dataset_location = None

        # Load the UI, Note that loadUI adds objects to 'self' using objectName
        self.dataWindow = None
        uic.loadUi(constant_paths.WINDOW_PATH, self)

        # Read the config file
        self.config = read_config(constant_paths.CONFIG_PATH)

        # Create/Get a logger with the desired settings
        self.logger = logging.getLogger(constants.LOG_NAME)
        self.consoleTextEdit.setFormatter(
            logging.Formatter(
                "%(asctime)s [%(levelname)8.8s] %(message)s",
                datefmt=f'{constants.DATETIME_FORMAT}: '
            )
        )
        self.logger.addHandler(self.consoleTextEdit)
        self.logger.setLevel(self.config['log_level'])

        self.plot_functions = {}
        self.devices = {}

        # Get list of devices as defined manually in the.devices __init__.py file
        for entry in devices.__all__:
            # Find and load the widget for any given device and add it to the stackedWidget
            entry_ui_file = entry.lower() + ".ui"
            entry_widget = uic.loadUi(constant_paths.WIDGET_PATH + entry_ui_file)
            entry_index = self.stackedWidget.addWidget(entry_widget)
            self.devices[entry] = entry_index

            # Import the corresponding module and get the class methods to set the plot_functions combobox when needed
            module = importlib.import_module(f"{devices.workers.__name__}.{entry.lower()}")
            entry_cls = getattr(module, entry)
            self.plot_functions[entry] = get_class_methods(entry_cls, ignore=["run"])

        # Reset stacked widget to empty page
        self.stackedWidget.setCurrentWidget(self.stackedWidget.widget(0))

        # Define menubar actions
        self.actionCreate_Set.triggered.connect(partial(create_dataset, self))
        self.actionSave_Set.triggered.connect(partial(save_dataset, self))
        self.actionLoad_Set.triggered.connect(partial(open_dataset_file, self))
        self.actionPreferences.triggered.connect(self.not_implemented)
        self.actionQuit.triggered.connect(self.quit)

        self.actionSave_format.triggered.connect(self.not_implemented)
        self.actionColour_scheme.triggered.connect(self.not_implemented)
        self.actionLine_width.triggered.connect(self.not_implemented)

        self.actionDocumentation.triggered.connect(self.navigate_to_docs)
        self.actionAbout.triggered.connect(self.show_about)

        # Define gui button actions
        self.showDataBtn.clicked.connect(self.display_data)
        self.showHistoryBtn.clicked.connect(self.display_history)
        self.addNotesBtn.clicked.connect(self.add_notes)

        self.appendBtn.clicked.connect(self.append_console_to_set)
        self.clearBtn.clicked.connect(partial(clear_data, window=self))
        self.clearAllBtn.clicked.connect(partial(clear_all, window=self))
        self.quitBtn.clicked.connect(self.quit)

        # Define stackedWidget widget actions
        self.plotBtn.clicked.connect(partial(plot_manager, self))

        # Make sure the progress bar is cleared
        self.progressBar.setValue(0)

        # Show the app
        self.show()
        self.console_print("Program Started")

    # Getters
    def get_plot_functions(self, device='Generic') -> list:
        return self.plot_functions[device]

    def get_current_plot_function(self) -> str:
        return self.plotTypeCombo.currentText()

    def get_current_device(self) -> str:
        return self.dataset.get_device()

    def get_dataset(self):
        return self.dataset

    def get_dataset_name(self) -> str:
        if self.dataset is None:
            return None
        return self.dataset.get_name()

    def get_dataset_window(self) -> QtWidgets.QDialog:
        return self.dataWindow

    # Setters
    def set_dataset_window(self, dataset_window: QtWidgets.QDialog):
        self.dataWindow = dataset_window

    def set_dataset(self, dataset: dataset_manager.dataset.DataSet):
        self.dataset = dataset

    # FUNCTIONALITY
    def autosave(self):
        file_name = self.dataset.get_location()
        if file_name is None:
            return self.console_print("Cannot autosave, no file location known. Open or create dataset first")

        with open(file_name, "w") as json_file:
            json.dump(self.dataset, json_file, cls=dataset_manager.DataSetJSONEncoder)
        json_file.close()

        return self.console_print(f"Saved dataset to {file_name}")

    def display_data(self):
        # Abort if no dataset was loaded
        if self.dataset is None:
            return self.console_print("Err: Must first load DataSet", level="warning")

        # Pretty print the dataset in a simple dialog
        pretty_json = json.dumps(
            self.dataset,
            indent=4,
            separators=(',', ': '),
            cls=dataset_manager.DataSetJSONEncoder
        )
        dialog_print(window=self, title=f"DataSet RAW: {self.dataset.get_name()}", contents=pretty_json)

        return None

    def display_history(self):
        if self.dataset is None:
            return self.console_print("Err: Must first load DataSet", level="warning")

        # Prints only the console history to a simple dialog
        pretty_history = ""
        for k, v in sorted(self.dataset.get_console().items()):
            line = f"{v}\n"
            pretty_history += line

        dialog_print(window=self, title=f"DataSet History: {self.dataset.get_name()}", contents=pretty_history)

        return None

    def add_notes(self):
        if self.dataset is None:
            return self.console_print("Err: Must first load DataSet", level="warning")

        # Add any notes to the dataset_manager with a trailing new line
        self.dataset.add_notes(self.notesPlainText.toPlainText() + "\n")
        self.console_print("Notes added to dataset_manager")
        self.autosave()

        return None

    def update_header(self):
        # Header should reflect opened dataset
        self.currSetNameLineEdit.setText(self.dataset.get_name())
        self.currDeviceLineEdit.setText(self.dataset.get_device())

        # Stacked widget should show the correct widget for the opened dataset
        new_page = self.stackedWidget.widget(self.devices[self.dataset.get_device()])
        self.stackedWidget.setCurrentWidget(new_page)

    def report_progress(self, progress: int):
        if not (isinstance(progress, int) and 0 <= progress <= 100):
            raise ValueError("Progress must be an integer between 0 and 100")

        self.progressBar.setValue(progress)

    def on_plot_thread_finished(self):
        # Reset UI elements
        self.progressBar.setValue(0)

        # Free button and log to console
        self.plotBtn.setEnabled(True)
        self.console_print(f"(run {self.device_worker.identifier}) finished")

        # Drop strong references so GC can do its thing
        self.device_worker = None
        self.thread = None

    def save_to_file(self, plaintext: str):
        file_dialog = QtWidgets.QFileDialog.getSaveFileName(self, "Save File", "", "Text Files (*.txt);;All Files (*)")
        if file_dialog[0]:  # Check if a file was selected
            file_path = file_dialog[0]
            with open(file_path, 'w') as file:
                file.write(plaintext)

    def console_print(self, fstring, level="normal"):
        # Print a message to the gui console
        now = datetime.datetime.now()
        fstring_to_print = now.strftime(f"{constants.DATETIME_FORMAT}: ") + fstring

        c = ConsoleColours()

        self.consoleTextEdit.setTextColor(c.get_colour(level))
        self.consoleTextEdit.append(fstring_to_print)
        self.consoleTextEdit.setTextColor(c.get_colour("normal"))

    def append_console_to_set(self):
        if self.dataset is None:
            return self.console_print("Err: Must first load DataSet", level="warning")

        # Append console contents to the dataset_manager
        console_text = self.consoleTextEdit.toPlainText()
        now = datetime.datetime.now()
        self.dataset.add_console(now.strftime(constants.DATETIME_FORMAT), console_text)
        self.console_print("Added console contents to set")
        self.autosave()

        return None

    def show_about(self):
        """
            Shows a simple window with licence, authorship and build information
        """
        # Grab the "about" info from about.txt
        with open(constant_paths.ABOUT_PATH) as about_file:
            about_contents = about_file.read()

        about_dialog = generate_about_dialog(about_contents, self.centralWidget(), constant_paths.LOGO_PATH)

        # Show the about dialog
        about_dialog.exec_()

    def navigate_to_docs(self):
        """
            Opens the default web browser and navigates to the documentation URL.
        """
        import webbrowser
        webbrowser.open(constant_paths.DOCS_URL)

    def not_implemented(self):
        """
            Shows the user a message that the current feature is planned but not yet implemented.
        """
        self.console_print("Feature not implemented", level='warning')

    # ESC now triggers a program exit
    def keyPressEvent(self, event) -> None:
        if event.key() == QtCore.Qt.Key.Key_Escape:
            self.quit()
        else:
            super(UiMainWindow, self).keyPressEvent(event)

    # CHECK: Program exit is not safe
    @staticmethod
    def quit():
        # Terminate the application
        sys.exit()

navigate_to_docs()

Opens the default web browser and navigates to the documentation URL.

Source code in gui\windows\MainWindow.py
315
316
317
318
319
320
def navigate_to_docs(self):
    """
        Opens the default web browser and navigates to the documentation URL.
    """
    import webbrowser
    webbrowser.open(constant_paths.DOCS_URL)

not_implemented()

Shows the user a message that the current feature is planned but not yet implemented.

Source code in gui\windows\MainWindow.py
322
323
324
325
326
def not_implemented(self):
    """
        Shows the user a message that the current feature is planned but not yet implemented.
    """
    self.console_print("Feature not implemented", level='warning')

show_about()

Shows a simple window with licence, authorship and build information

Source code in gui\windows\MainWindow.py
302
303
304
305
306
307
308
309
310
311
312
313
def show_about(self):
    """
        Shows a simple window with licence, authorship and build information
    """
    # Grab the "about" info from about.txt
    with open(constant_paths.ABOUT_PATH) as about_file:
        about_contents = about_file.read()

    about_dialog = generate_about_dialog(about_contents, self.centralWidget(), constant_paths.LOGO_PATH)

    # Show the about dialog
    about_dialog.exec_()

Bases: QDialog

Dialog for interactively creating new DataSet instances.

Overview

This window guides the user through constructing a dataset by: - Selecting a device type from a combobox. - Adding individual files with custom labels, or - Auto-generating filepaths from a directory according to a chosen structure. - Setting the experiment name and experiment date/time.

Behaviour

  • Maintains an internal DataSet object that is updated as the user adds files or generates sets from directories.
  • Displays the current file mapping as formatted JSON in a plain-text widget.
  • Validates that both a name and at least one file are present before enabling the “Done” button.
  • On completion (finish), writes name, device type, and experiment datetime into the dataset and closes with an accepted result.

Parameters

devices : list[str], optional List of available device names to present in the device selection combo box. Defaults to a single "N/A" entry when not specified.

Source code in gui\windows\DataSetCreatorWindow.py
 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
 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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
class UiDataCreatorWindow(QtWidgets.QDialog):
    """
       Dialog for interactively creating new `DataSet` instances.

       Overview
       --------
       This window guides the user through constructing a dataset by:
       - Selecting a device type from a combobox.
       - Adding individual files with custom labels, or
       - Auto-generating filepaths from a directory according to a chosen structure.
       - Setting the experiment name and experiment date/time.

       Behaviour
       ---------
       - Maintains an internal `DataSet` object that is updated as the user adds
         files or generates sets from directories.
       - Displays the current file mapping as formatted JSON in a plain-text widget.
       - Validates that both a name and at least one file are present before
         enabling the “Done” button.
       - On completion (`finish`), writes name, device type, and experiment
         datetime into the dataset and closes with an accepted result.

       Parameters
       ----------
       devices : list[str], optional
           List of available device names to present in the device selection combo
           box. Defaults to a single `"N/A"` entry when not specified.
       """
    def __init__(self, devices: list[str] = ["N/A"]):
        super(UiDataCreatorWindow, self).__init__()

        # Load the UI,
        # Note that loadUI adds objects to 'self' using objectName
        uic.loadUi("gui/windows/DataSetCreatorWindow.ui", self)

        self.dataset = dataset_manager.DataSet(datetime.datetime.now().strftime("%Y.%m.%d_%H.%M.%S"))

        # Add the correct devices to the experiment combo box
        self.dataTypeCombo.addItems(devices)

        # Set date to today by default
        self.dateTimeEdit.setDateTime(datetime.datetime.now())

        # Set starting tab to manual dataset creation
        self.tabWidget.setCurrentIndex(0)

        # Define widget action
        self.browseFilesBtn.clicked.connect(self.browse_files)
        self.browseDirBtn.clicked.connect(self.browse_dir)
        self.addLabelBtn.clicked.connect(self.add_file_to_set)
        self.generateBtn.clicked.connect(self.generate_set)
        self.resetBtn.clicked.connect(self.reset)
        self.doneBtn.clicked.connect(self.finish)

        # Enable button when all is filled
        self.showSetPlainText.textChanged.connect(self.finish_button_state)
        self.nameEdit.textChanged.connect(self.finish_button_state)
        self.browseFilesText.textChanged.connect(self.label_button_state)
        self.labelEdit.textChanged.connect(self.label_button_state)

        # Show the app
        self.show()

    def get_dataset(self) -> dataset_manager.dataset.DataSet:
        return self.dataset

    def browse_files(self):
        """
        # Open file selection dialog to get a file path and update gui when confirmed
        """
        file_name = QtWidgets.QFileDialog.getOpenFileName(self, "Open File")
        self.browseFilesText.setPlainText(file_name[0])

    def browse_dir(self):
        """
        # Open directory selection dialog to get a path and update gui when confirmed
        """
        dir_name = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select Directory')
        self.browseDirText.setPlainText(dir_name)

    def add_file_to_set(self):
        """
        # Gets the path and label and adds it to the current DataSet instance while updating GUI
        """
        # Read name and legend label from gui
        file_name = self.browseFilesText.toPlainText()
        file_label = self.labelEdit.text()

        # Must add a label and file
        if not (file_name and file_label):
            self.show_message(
                title="Label/Path Undefined",
                message="No label or path were defined, cannot proceed"
            )
            return None

        # Check for duplicate label
        if file_label in self.dataset.get_labels():
            self.show_message(
                title="Duplicate Label",
                message="""This label has already been used. Choose another label and try again."""
            )
        else:
            # Add the file to the dataset and update the gui
            self.dataset.set_structure_type("flat")
            self.dataset.add_filepath(file_name, file_label)
            self.showSetPlainText.setPlainText(
                json.dumps(
                    self.dataset.get_filepaths(),
                    indent=4,
                    separators=(',', ': ')
                )
            )
            self.browseFilesText.clear()

        # Empty label widget
        self.labelEdit.clear()

    def generate_set(self):
        """
        Automatically generate a set of filepaths based on a directory path. Will create nested structure if desired
        """
        path = self.browseDirText.toPlainText()

        # If path is not selected, show message and return None
        if not path:
            self.show_message(title="No directory selected", message="""No directory was selected, please select directory and try again""")
            return None

        # Construct the filepaths for this dataset
        active_button = split_camel_case(search_for_first_active_radio_button(self).objectName())[0]
        errors = self.dataset.construct_filepaths(root_dir=path, type=active_button)

        # Show the directories/files that were ignored to the user
        if errors != "":
            self.show_message(title="Files were ignored", message=errors)

        # Show the files in the gui
        self.showSetPlainText.setPlainText(
            json.dumps(
                self.dataset.get_filepaths(),
                indent=4,
                separators=(',', ': ')
            )
        )
        self.dataset.set_structure_type(active_button)

    def label_button_state(self):
        """Only enable the add button once a label and a path were selected """
        # Read name and legend label from gui
        file_name = self.browseFilesText.toPlainText()
        file_label = self.labelEdit.text()

        if (file_name != "") and (file_label != ""):
            self.addLabelBtn.setEnabled(True)
        else:
            self.addLabelBtn.setEnabled(False)


    def finish_button_state(self):
        """ Only enable closing when some data was included """
        # TODO: hmmmmmmmmmmmm, should I be able to close the window if I mistakenly opened it?
        nameTxt = self.nameEdit.text()
        files = self.showSetPlainText.toPlainText()
        if (files != "") and (nameTxt != ""):
            self.doneBtn.setEnabled(True)
        else:
            self.doneBtn.setEnabled(False)

    @staticmethod
    def show_message(title, message):
        msg = QtWidgets.QMessageBox()
        msg.setWindowTitle(title)
        msg.setText(message)
        x = msg.exec_()

    def reset(self):
        """ Completely reset this UI by clearing all elements """
        self.nameEdit.clear()
        self.labelEdit.clear()
        self.browseDirText.clear()
        self.browseFilesText.clear()
        self.showSetPlainText.clear()
        self.finish_button_state()

    def finish(self):
        """ Add name, device type,  and date and time dataset before exiting """
        self.dataset.set_name(self.nameEdit.text())
        self.dataset.set_device(self.dataTypeCombo.currentText())
        experiment_date_time = self.dateTimeEdit.dateTime().toPyDateTime().strftime("%Y.%m.%d_%H.%M.%S")
        self.dataset.set_experiment_date(experiment_date_time)

        self.done(1)

add_file_to_set()

Gets the path and label and adds it to the current DataSet instance while updating GUI

Source code in gui\windows\DataSetCreatorWindow.py
 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
def add_file_to_set(self):
    """
    # Gets the path and label and adds it to the current DataSet instance while updating GUI
    """
    # Read name and legend label from gui
    file_name = self.browseFilesText.toPlainText()
    file_label = self.labelEdit.text()

    # Must add a label and file
    if not (file_name and file_label):
        self.show_message(
            title="Label/Path Undefined",
            message="No label or path were defined, cannot proceed"
        )
        return None

    # Check for duplicate label
    if file_label in self.dataset.get_labels():
        self.show_message(
            title="Duplicate Label",
            message="""This label has already been used. Choose another label and try again."""
        )
    else:
        # Add the file to the dataset and update the gui
        self.dataset.set_structure_type("flat")
        self.dataset.add_filepath(file_name, file_label)
        self.showSetPlainText.setPlainText(
            json.dumps(
                self.dataset.get_filepaths(),
                indent=4,
                separators=(',', ': ')
            )
        )
        self.browseFilesText.clear()

    # Empty label widget
    self.labelEdit.clear()

browse_dir()

Open directory selection dialog to get a path and update gui when confirmed

Source code in gui\windows\DataSetCreatorWindow.py
84
85
86
87
88
89
def browse_dir(self):
    """
    # Open directory selection dialog to get a path and update gui when confirmed
    """
    dir_name = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select Directory')
    self.browseDirText.setPlainText(dir_name)

browse_files()

Open file selection dialog to get a file path and update gui when confirmed

Source code in gui\windows\DataSetCreatorWindow.py
77
78
79
80
81
82
def browse_files(self):
    """
    # Open file selection dialog to get a file path and update gui when confirmed
    """
    file_name = QtWidgets.QFileDialog.getOpenFileName(self, "Open File")
    self.browseFilesText.setPlainText(file_name[0])

finish()

Add name, device type, and date and time dataset before exiting

Source code in gui\windows\DataSetCreatorWindow.py
196
197
198
199
200
201
202
203
def finish(self):
    """ Add name, device type,  and date and time dataset before exiting """
    self.dataset.set_name(self.nameEdit.text())
    self.dataset.set_device(self.dataTypeCombo.currentText())
    experiment_date_time = self.dateTimeEdit.dateTime().toPyDateTime().strftime("%Y.%m.%d_%H.%M.%S")
    self.dataset.set_experiment_date(experiment_date_time)

    self.done(1)

finish_button_state()

Only enable closing when some data was included

Source code in gui\windows\DataSetCreatorWindow.py
170
171
172
173
174
175
176
177
178
def finish_button_state(self):
    """ Only enable closing when some data was included """
    # TODO: hmmmmmmmmmmmm, should I be able to close the window if I mistakenly opened it?
    nameTxt = self.nameEdit.text()
    files = self.showSetPlainText.toPlainText()
    if (files != "") and (nameTxt != ""):
        self.doneBtn.setEnabled(True)
    else:
        self.doneBtn.setEnabled(False)

generate_set()

Automatically generate a set of filepaths based on a directory path. Will create nested structure if desired

Source code in gui\windows\DataSetCreatorWindow.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def generate_set(self):
    """
    Automatically generate a set of filepaths based on a directory path. Will create nested structure if desired
    """
    path = self.browseDirText.toPlainText()

    # If path is not selected, show message and return None
    if not path:
        self.show_message(title="No directory selected", message="""No directory was selected, please select directory and try again""")
        return None

    # Construct the filepaths for this dataset
    active_button = split_camel_case(search_for_first_active_radio_button(self).objectName())[0]
    errors = self.dataset.construct_filepaths(root_dir=path, type=active_button)

    # Show the directories/files that were ignored to the user
    if errors != "":
        self.show_message(title="Files were ignored", message=errors)

    # Show the files in the gui
    self.showSetPlainText.setPlainText(
        json.dumps(
            self.dataset.get_filepaths(),
            indent=4,
            separators=(',', ': ')
        )
    )
    self.dataset.set_structure_type(active_button)

label_button_state()

Only enable the add button once a label and a path were selected

Source code in gui\windows\DataSetCreatorWindow.py
158
159
160
161
162
163
164
165
166
167
def label_button_state(self):
    """Only enable the add button once a label and a path were selected """
    # Read name and legend label from gui
    file_name = self.browseFilesText.toPlainText()
    file_label = self.labelEdit.text()

    if (file_name != "") and (file_label != ""):
        self.addLabelBtn.setEnabled(True)
    else:
        self.addLabelBtn.setEnabled(False)

reset()

Completely reset this UI by clearing all elements

Source code in gui\windows\DataSetCreatorWindow.py
187
188
189
190
191
192
193
194
def reset(self):
    """ Completely reset this UI by clearing all elements """
    self.nameEdit.clear()
    self.labelEdit.clear()
    self.browseDirText.clear()
    self.browseFilesText.clear()
    self.showSetPlainText.clear()
    self.finish_button_state()

Bases: Handler, QTextEdit

QTextEdit-based logging console widget for the GUI.

This class bridges the logging module with a Qt text widget by: - Subclassing both logging.Handler and QTextEdit. - Emitting formatted log messages through a dedicated Qt signal (appendTextEdit), which is connected to the widget's append slot. - Keeping the text area read-only so it behaves like a console.

Typical usage

  • Create an instance and add it as a handler to a logging.Logger.
  • Configure a formatter for the handler.
  • Logged messages will appear in the GUI with the configured format.

Parameters

parent : QWidget Parent widget that will own this console.

Source code in gui\windows\qtexteditconsole.py
 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
class QTextEditConsole(logging.Handler, QtWidgets.QTextEdit):
    """
    QTextEdit-based logging console widget for the GUI.

    This class bridges the `logging` module with a Qt text widget by:
    - Subclassing both `logging.Handler` and `QTextEdit`.
    - Emitting formatted log messages through a dedicated Qt signal
      (`appendTextEdit`), which is connected to the widget's `append` slot.
    - Keeping the text area read-only so it behaves like a console.

    Typical usage
    -------------
    - Create an instance and add it as a handler to a `logging.Logger`.
    - Configure a formatter for the handler.
    - Logged messages will appear in the GUI with the configured format.

    Parameters
    ----------
    parent : QWidget
        Parent widget that will own this console.
    """
    appendTextEdit = QtCore.pyqtSignal(str)

    def __init__(self, parent):
        logging.Handler.__init__(self)
        super(QtWidgets.QTextEdit, self).__init__(parent)

        self.setReadOnly(True)
        self.appendTextEdit.connect(self.append)

    def emit(self, record):
        msg = self.format(record)
        self.appendTextEdit.emit(msg)

Display text content inside a modal dialog with optional saving.

The dialog contains: - A read-only text editor showing contents. - “OK” to close the dialog. - “SAVE” to delegate saving via window.save_to_file.

Parameters

window : QMainWindow Parent window providing the save callback. title : str Dialog title bar text. contents : str Text content to display.

Source code in gui\windows\dialogs\dialog_print.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
def dialog_print(window: QtWidgets.QMainWindow, title, contents):
    """
    Display text content inside a modal dialog with optional saving.

    The dialog contains:
    - A read-only text editor showing `contents`.
    - “OK” to close the dialog.
    - “SAVE” to delegate saving via `window.save_to_file`.

    Parameters
    ----------
    window : QMainWindow
        Parent window providing the save callback.
    title : str
        Dialog title bar text.
    contents : str
        Text content to display.
    """
    # Prepare a text edit widget to host the contents
    history_text_edit = QtWidgets.QTextEdit(window)
    history_text_edit.setPlainText(contents)

    # Initialise the window
    dialog = QtWidgets.QDialog(window)
    dialog.setWindowTitle(title)

    # Set a default width and minimum height for the dialog
    dialog.resize(600, 400)

    # Create a QVBoxLayout for the dialog
    layout = QtWidgets.QVBoxLayout(dialog)

    # Add the QTextEdit widget to the layout
    layout.addWidget(history_text_edit)

    # Create a QHBoxLayout and host buttons
    button_layout = QtWidgets.QHBoxLayout()
    ok_button = QtWidgets.QPushButton("OK")
    save_button = QtWidgets.QPushButton("SAVE")
    button_layout.addWidget(ok_button)
    button_layout.addWidget(save_button)

    # Add the button layout to the main layout
    layout.addLayout(button_layout)

    # Connect the "OK" button to close the dialog
    ok_button.clicked.connect(dialog.accept)
    save_button.clicked.connect(
        lambda: window.save_to_file(history_text_edit.toPlainText())
    )

    # Show the dialog
    dialog.exec_()

Build and return an 'About' information dialog containing a logo and text.

Features: - Displays an application logo loaded via QPixmap. - Shows about text with HTML formatting support. - Uses a fixed-size vertical layout.

Parameters

about_contents : str HTML/markdown-like text describing the application. centralwidget : QWidget Parent widget for modal behavior. logo_path : str Directory path to the logo image file.

Returns

QDialog Configured dialog ready to be shown.

Source code in gui\windows\dialogs\generate_about_dialog.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
def generate_about_dialog(about_contents, centralwidget, logo_path):
    """
    Build and return an 'About' information dialog containing a logo and text.

    Features:
    - Displays an application logo loaded via QPixmap.
    - Shows about text with HTML formatting support.
    - Uses a fixed-size vertical layout.

    Parameters
    ----------
    about_contents : str
        HTML/markdown-like text describing the application.
    centralwidget : QWidget
        Parent widget for modal behavior.
    logo_path : str
        Directory path to the logo image file.

    Returns
    -------
    QDialog
        Configured dialog ready to be shown.
    """
    # Create a custom QDialog for the about information
    about_dialog = QtWidgets.QDialog(centralwidget)
    about_dialog.setWindowTitle("About")

    # Set the fixed size of the dialog
    about_dialog.setFixedSize(650, 700)  # Adjust the dimensions as needed

    # Load and set the image using QPixmap (make sure the path is correct)
    pixmap = QtGui.QPixmap(logo_path + "X_logo_x-lab_baseline_KL.png")
    pixmap = pixmap.scaled(600, 200, QtCore.Qt.KeepAspectRatio)
    image_label = QtWidgets.QLabel(about_dialog)
    image_label.setPixmap(pixmap)

    # Create a QLabel for the text (using HTML formatting)
    text_label = QtWidgets.QLabel(about_dialog)
    text_label.setWordWrap(True)
    text_label.setText(about_contents)

    # Create a QVBoxLayout for the dialog and add the image and text labels
    layout = QtWidgets.QVBoxLayout(about_dialog)
    layout.addWidget(image_label)
    layout.addWidget(text_label)

    about_dialog.setLayout(layout)

    return about_dialog