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
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, thread_data):
        # Reset UI elements
        self.progressBar.setValue(0)

        # Free button and log to console
        self.plotBtn.setEnabled(True)

        if thread_data['ok']:
            self.console_print(f"(run {self.device_worker.identifier}) finished succesfully")
        else:
            self.console_print(message=thread_data["message"], level="alert")
            print(thread_data["traceback"])

        # Cleanup the worker and the thread after showing the message
        self.thread.quit()
        self.thread.wait()
        self.thread.deleteLater()

        self.device_worker = None
        self.thread = None

        self.console_print(message="Disposed of Thread and Worker")

    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, message, level="normal"):
        # Print a message to the gui console
        now = datetime.datetime.now()
        fstring_to_print = now.strftime(f"{constants.DATETIME_FORMAT}: ") + message

        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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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