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
- Collect the selected file labels from the main window.
- Build a reduced
DataSetcontaining only the selected files, colours, device, structure type, and name. - Collect plotting options from the active device widget (via
aliasproperties) and from global GUI controls, storing them inPlotterOptions. - Resolve and instantiate the appropriate device worker class from the
implementations.devices.workersnamespace. - Configure the worker with the reduced dataset, selected plot function, and options.
- 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
@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())
dataset_selection.set_location(window.dataset.get_location())
# 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)
# Connect console printing
window.device_worker.console_print.connect(window.console_print)
# When the thread is actually finished, clean up and reset GUI
window.device_worker.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}")
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
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
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
@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
@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
@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
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
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
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.
- Launching the plotting pipeline via
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
DataSetobject 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