Skip to content

UI Dialogs

karcytics_sdk.plugin.dialogs

Dialog utilities for Karcytics SDK.

Provides convenient wrappers for common PySide6 file dialogs and message boxes that work with the current theme.

SaveWorkflowDialog

Bases: QDialog

Dialog to capture metadata before saving a workflow.

Source code in src/karcytics_sdk/plugin/dialogs.py
class SaveWorkflowDialog(QDialog):
    """Dialog to capture metadata before saving a workflow."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Save Workflow")
        self.setMinimumWidth(400)
        self.layout = QVBoxLayout(self)

        form = QFormLayout()

        self.name_input = QLineEdit()
        self.name_input.setPlaceholderText("e.g., WT vs KO Replicate 1")

        self.desc_input = QTextEdit()
        self.desc_input.setPlaceholderText("Briefly describe the analysis...")
        self.desc_input.setMaximumHeight(80)

        self.tags_input = QLineEdit()
        self.tags_input.setPlaceholderText("e.g., ponceau, ko_mice, week_4")

        form.addRow("Workflow Name:", self.name_input)
        form.addRow("Description:", self.desc_input)
        form.addRow("Tags (comma separated):", self.tags_input)

        self.layout.addLayout(form)

        # Standard Save/Cancel buttons
        self.buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
        self.buttons.accepted.connect(self.accept)
        self.buttons.rejected.connect(self.reject)

        # Disable save if name is empty
        self.buttons.button(QDialogButtonBox.StandardButton.Save).setEnabled(False)
        self.name_input.textChanged.connect(
            lambda text: self.buttons.button(QDialogButtonBox.StandardButton.Save).setEnabled(bool(text.strip()))
        )

        self.layout.addWidget(self.buttons)

    def get_metadata(self) -> dict:
        """Extracts and formats the user input."""
        tags = [t.strip().lower() for t in self.tags_input.text().split(",") if t.strip()]
        return {
            "name": self.name_input.text().strip(),
            "description": self.desc_input.toPlainText().strip(),
            "tags": tags,
            "timestamp": datetime.now().isoformat(),
        }

get_metadata()

Extracts and formats the user input.

Source code in src/karcytics_sdk/plugin/dialogs.py
def get_metadata(self) -> dict:
    """Extracts and formats the user input."""
    tags = [t.strip().lower() for t in self.tags_input.text().split(",") if t.strip()]
    return {
        "name": self.name_input.text().strip(),
        "description": self.desc_input.toPlainText().strip(),
        "tags": tags,
        "timestamp": datetime.now().isoformat(),
    }

ask_ok_cancel(parent=None, title='', message='')

Show OK/Cancel dialog.

Parameters:

Name Type Description Default
parent

Parent widget

None
title str

Dialog title

''
message str

Dialog message

''

Returns:

Type Description
bool

True if user clicks OK, False otherwise

Source code in src/karcytics_sdk/plugin/dialogs.py
def ask_ok_cancel(parent=None, title: str = "", message: str = "") -> bool:
    """Show OK/Cancel dialog.

    Args:
        parent: Parent widget
        title: Dialog title
        message: Dialog message

    Returns:
        True if user clicks OK, False otherwise
    """
    reply = QMessageBox.question(
        parent, title, message, QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel
    )
    return reply == QMessageBox.StandardButton.Ok

ask_yes_no(parent=None, title='', message='')

Show yes/no question dialog.

Parameters:

Name Type Description Default
parent

Parent widget

None
title str

Dialog title

''
message str

Dialog message

''

Returns:

Type Description
bool

True if user clicks Yes, False otherwise

Source code in src/karcytics_sdk/plugin/dialogs.py
def ask_yes_no(parent=None, title: str = "", message: str = "") -> bool:
    """Show yes/no question dialog.

    Args:
        parent: Parent widget
        title: Dialog title
        message: Dialog message

    Returns:
        True if user clicks Yes, False otherwise
    """
    msg_box = QMessageBox(
        QMessageBox.Icon.Question,
        title,
        message,
        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
        parent,
    )
    if parent and (parent.windowFlags() & Qt.WindowType.WindowStaysOnTopHint):
        msg_box.setWindowFlags(msg_box.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
    reply = msg_box.exec()
    return reply == QMessageBox.StandardButton.Yes

bring_into_project(parent, path)

path, registered with the open project after offering to copy it into its assets.

A file chosen from outside the project would be left behind when the project moves to another computer, so the user is asked whether to copy it into the project's 'assets' folder. It is registered with the project either way (copied, or referenced where it is).

Returns the path to use (the copy's when it was copied), path itself when no project is open, or None when the project couldn't take it (a warning says why), so the caller can stop.

Source code in src/karcytics_sdk/plugin/dialogs.py
def bring_into_project(parent, path: str) -> str | None:
    """`path`, registered with the open project after offering to copy it into its assets.

    A file chosen from outside the project would be left behind when the
    project moves to another computer, so the user is asked whether to copy
    it into the project's 'assets' folder. It is registered with the
    project either way (copied, or referenced where it is).

    Returns the path to use (the copy's when it was copied), `path` itself
    when no project is open, or None when the project couldn't take it (a
    warning says why), so the caller can stop.
    """
    window = parent.window() if parent is not None else None
    pm = getattr(window, "project_manager", None)
    if pm is None:
        return path
    try:
        inside = Path(pm.assets_dir).resolve() in Path(path).resolve().parents
        copy = not inside and ask_yes_no(
            parent,
            "Copy to Workspace?",
            f"The image '{Path(path).name}' is outside the project folder.\n\n"
            "Would you like to copy it into the project's 'assets' folder for safe keeping "
            "and portability?",
        )
        resolved = pm.get_asset_path(pm.add_image(Path(path), copy))
    except Exception as exc:  # the project manager reports any failure as an exception
        show_warning(parent, "Asset Error", f"Failed to add asset to project:\n{exc}")
        return None
    return str(resolved) if resolved else path

confirm_destructive(parent, title, message, items=())

Ask before an action that also affects other things, listing them.

For "deleting X also deletes its 3 children" or "rename in 5 samples": the message, then the affected items as a bulleted list (the first six and a count of the rest), with No as the default answer.

Returns:

Type Description
bool

True if the user clicks Yes.

Source code in src/karcytics_sdk/plugin/dialogs.py
def confirm_destructive(parent, title: str, message: str, items: Sequence[str] = ()) -> bool:
    """Ask before an action that also affects other things, listing them.

    For "deleting X also deletes its 3 children" or "rename in 5 samples":
    the message, then the affected items as a bulleted list (the first six
    and a count of the rest), with No as the default answer.

    Returns:
        True if the user clicks Yes.
    """
    listed = [f"• {item}" for item in items[:_MAX_LISTED]]
    if len(items) > _MAX_LISTED:
        listed.append(f"... and {len(items) - _MAX_LISTED} more")
    msg_box = QMessageBox(
        QMessageBox.Icon.Warning,
        title,
        message,
        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
        parent,
    )
    if listed:
        msg_box.setInformativeText("\n".join(listed))
    msg_box.setDefaultButton(QMessageBox.StandardButton.No)
    return msg_box.exec() == QMessageBox.StandardButton.Yes

get_directory(parent=None, title='Select Directory', start_dir='')

Show directory selection dialog.

Parameters:

Name Type Description Default
parent

Parent widget

None
title str

Dialog title

'Select Directory'
start_dir str

Initial directory

''

Returns:

Type Description
str | None

Selected directory path or None if cancelled

Source code in src/karcytics_sdk/plugin/dialogs.py
def get_directory(parent=None, title: str = "Select Directory", start_dir: str = "") -> str | None:
    """Show directory selection dialog.

    Args:
        parent: Parent widget
        title: Dialog title
        start_dir: Initial directory

    Returns:
        Selected directory path or None if cancelled
    """
    dir_path = QFileDialog.getExistingDirectory(parent, title, start_dir)
    return dir_path if dir_path else None

get_double(parent=None, title='', label='', value=0.0, min_val=-999.99, max_val=999.99, decimals=2)

Show float input dialog.

Parameters:

Name Type Description Default
parent

Parent widget

None
title str

Dialog title

''
label str

Label text

''
value float

Default value

0.0
min_val float

Minimum allowed value

-999.99
max_val float

Maximum allowed value

999.99
decimals int

Number of decimal places

2

Returns:

Type Description
float | None

User input or None if cancelled

Source code in src/karcytics_sdk/plugin/dialogs.py
def get_double(  # noqa: PLR0913, PLR0917
    parent=None,
    title: str = "",
    label: str = "",
    value: float = 0.0,
    min_val: float = -999.99,
    max_val: float = 999.99,
    decimals: int = 2,
) -> float | None:
    """Show float input dialog.

    Args:
        parent: Parent widget
        title: Dialog title
        label: Label text
        value: Default value
        min_val: Minimum allowed value
        max_val: Maximum allowed value
        decimals: Number of decimal places

    Returns:
        User input or None if cancelled
    """
    num, ok = QInputDialog.getDouble(parent, title, label, value, min_val, max_val, decimals)
    return num if ok else None

get_image_path(parent=None, title='Select Image', start_dir='')

Show file dialog to select an image.

Parameters:

Name Type Description Default
parent

Parent widget

None
title str

Dialog title

'Select Image'
start_dir str

Initial directory (default: the open project's assets folder)

''

Returns:

Type Description
str | None

Selected file path or None if cancelled

Source code in src/karcytics_sdk/plugin/dialogs.py
def get_image_path(parent=None, title: str = "Select Image", start_dir: str = "") -> str | None:
    """Show file dialog to select an image.

    Args:
        parent: Parent widget
        title: Dialog title
        start_dir: Initial directory (default: the open project's assets folder)

    Returns:
        Selected file path or None if cancelled
    """
    file_path, _ = QFileDialog.getOpenFileName(
        parent,
        title,
        start_dir or project_assets_dir(parent),
        "Images (*.png *.jpg *.jpeg *.tiff *.tif);;All Files (*)",
    )
    return file_path if file_path else None

get_image_paths(parent=None, title='Select Images', start_dir='')

Show file dialog to select multiple images.

Parameters:

Name Type Description Default
parent

Parent widget

None
title str

Dialog title

'Select Images'
start_dir str

Initial directory (default: the open project's assets folder)

''

Returns:

Type Description
list[str]

List of selected file paths

Source code in src/karcytics_sdk/plugin/dialogs.py
def get_image_paths(parent=None, title: str = "Select Images", start_dir: str = "") -> list[str]:
    """Show file dialog to select multiple images.

    Args:
        parent: Parent widget
        title: Dialog title
        start_dir: Initial directory (default: the open project's assets folder)

    Returns:
        List of selected file paths
    """
    file_paths, _ = QFileDialog.getOpenFileNames(
        parent,
        title,
        start_dir or project_assets_dir(parent),
        "Images (*.png *.jpg *.jpeg *.tiff *.tif);;All Files (*)",
    )
    return file_paths if file_paths else []

get_number(parent=None, title='', label='', value=0, min_val=-999999, max_val=999999)

Show integer input dialog.

Parameters:

Name Type Description Default
parent

Parent widget

None
title str

Dialog title

''
label str

Label text

''
value int

Default value

0
min_val int

Minimum allowed value

-999999
max_val int

Maximum allowed value

999999

Returns:

Type Description
int | None

User input or None if cancelled

Source code in src/karcytics_sdk/plugin/dialogs.py
def get_number(  # noqa: PLR0913, PLR0917
    parent=None,
    title: str = "",
    label: str = "",
    value: int = 0,
    min_val: int = -999999,
    max_val: int = 999999,
) -> int | None:
    """Show integer input dialog.

    Args:
        parent: Parent widget
        title: Dialog title
        label: Label text
        value: Default value
        min_val: Minimum allowed value
        max_val: Maximum allowed value

    Returns:
        User input or None if cancelled
    """
    num, ok = QInputDialog.getInt(parent, title, label, value, min_val, max_val)
    return num if ok else None

get_save_path(parent=None, title='Save As', start_dir='', file_filter='')

Show save file dialog.

Parameters:

Name Type Description Default
parent

Parent widget

None
title str

Dialog title

'Save As'
start_dir str

Initial directory

''
file_filter str

Qt file filter string (e.g. "CSV Files (.csv);;All Files ()")

''

Returns:

Type Description
str | None

Selected file path or None if cancelled

Source code in src/karcytics_sdk/plugin/dialogs.py
def get_save_path(parent=None, title: str = "Save As", start_dir: str = "", file_filter: str = "") -> str | None:
    """Show save file dialog.

    Args:
        parent: Parent widget
        title: Dialog title
        start_dir: Initial directory
        file_filter: Qt file filter string (e.g. "CSV Files (*.csv);;All Files (*)")

    Returns:
        Selected file path or None if cancelled
    """
    if not file_filter:
        file_filter = "All Files (*)"

    file_path, _ = QFileDialog.getSaveFileName(parent, title, start_dir, file_filter)
    return file_path if file_path else None

get_text(parent=None, title='', label='', default='')

Show text input dialog.

Parameters:

Name Type Description Default
parent

Parent widget

None
title str

Dialog title

''
label str

Label text

''
default str

Default value in text field

''

Returns:

Type Description
str | None

User input or None if cancelled

Source code in src/karcytics_sdk/plugin/dialogs.py
def get_text(parent=None, title: str = "", label: str = "", default: str = "") -> str | None:
    """Show text input dialog.

    Args:
        parent: Parent widget
        title: Dialog title
        label: Label text
        default: Default value in text field

    Returns:
        User input or None if cancelled
    """
    text, ok = QInputDialog.getText(parent, title, label, text=default)
    return text if ok else None

import_assets_workflow(parent, project_manager, file_paths)

Orchestrates the multi-file import workflow as requested by the user.

  1. If multiple files, asks if they should be grouped in a subdirectory.
  2. Asks once for 'Copy to Workspace' for the entire batch.
  3. Executes the batch import via ProjectManager.

Returns:

Type Description
list[str]

List of file hashes for the imported assets.

Source code in src/karcytics_sdk/plugin/dialogs.py
def import_assets_workflow(parent, project_manager, file_paths: list[str]) -> list[str]:
    """Orchestrates the multi-file import workflow as requested by the user.

    1. If multiple files, asks if they should be grouped in a subdirectory.
    2. Asks once for 'Copy to Workspace' for the entire batch.
    3. Executes the batch import via ProjectManager.

    Returns:
        List of file hashes for the imported assets.
    """
    if not file_paths:
        return []

    subfolder = None
    if len(file_paths) > 1 and ask_yes_no(
        parent,
        "Group Files?",
        "Would you like to create a subdirectory in 'assets' for this collected set of files?",
    ):
        subfolder = get_text(parent, "Subdirectory Name", "Enter folder name:", default="experiment_data")
        if not subfolder or not subfolder.strip():
            subfolder = None

    copy_to_workspace = ask_yes_no(
        parent,
        "Copy to Workspace?",
        f"Copy these {len(file_paths)} files into the project assets folder?\n\n"
        "This ensures the project remains portable if moved to another computer.",
    )

    from pathlib import Path

    path_objs = [Path(p) for p in file_paths]

    return project_manager.batch_add_images(path_objs, copy_to_workspace, subfolder)

project_assets_dir(widget=None)

The open project's assets folder, as widget's window knows it ("" with no project).

The Hub (and an isolated plugin's runtime) sets project_manager on the plugin's window; its assets_dir is where a project's images live.

Source code in src/karcytics_sdk/plugin/dialogs.py
def project_assets_dir(widget=None) -> str:
    """The open project's assets folder, as `widget`'s window knows it ("" with no project).

    The Hub (and an isolated plugin's runtime) sets `project_manager` on the
    plugin's window; its `assets_dir` is where a project's images live.
    """
    window = widget.window() if widget is not None else None
    folder = getattr(getattr(window, "project_manager", None), "assets_dir", None)
    return str(folder) if folder and Path(folder).is_dir() else ""

show_error(parent=None, title='Error', message='')

Show error dialog.

Parameters:

Name Type Description Default
parent

Parent widget

None
title str

Dialog title

'Error'
message str

Dialog message

''
Source code in src/karcytics_sdk/plugin/dialogs.py
def show_error(parent=None, title: str = "Error", message: str = "") -> None:
    """Show error dialog.

    Args:
        parent: Parent widget
        title: Dialog title
        message: Dialog message
    """
    QMessageBox.critical(parent, title, message)

show_info(parent=None, title='Information', message='')

Show information dialog.

Parameters:

Name Type Description Default
parent

Parent widget

None
title str

Dialog title

'Information'
message str

Dialog message

''
Source code in src/karcytics_sdk/plugin/dialogs.py
def show_info(parent=None, title: str = "Information", message: str = "") -> None:
    """Show information dialog.

    Args:
        parent: Parent widget
        title: Dialog title
        message: Dialog message
    """
    QMessageBox.information(parent, title, message)

show_warning(parent=None, title='Warning', message='')

Show warning dialog.

Parameters:

Name Type Description Default
parent

Parent widget

None
title str

Dialog title

'Warning'
message str

Dialog message

''
Source code in src/karcytics_sdk/plugin/dialogs.py
def show_warning(parent=None, title: str = "Warning", message: str = "") -> None:
    """Show warning dialog.

    Args:
        parent: Parent widget
        title: Dialog title
        message: Dialog message
    """
    QMessageBox.warning(parent, title, message)