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