From 644c2508e11642038b3f5de540015494344df0d5 Mon Sep 17 00:00:00 2001 From: uermel Date: Fri, 9 Oct 2026 11:57:05 -0700 Subject: [PATCH] fix: compact Edit Object Types dialog with a larger table The object table was capped at about three rows, the form used three group boxes of one label/field column each, and every validation shrank the dialog (adjustSize), clipping the form's labels after "Edit Selected". The table now takes the dialog's extra height (above a splitter), the form is one grid of three label/field pairs per row with the filament properties as its last row and the form actions next to its status, and the dialog keeps its size. The radius is labelled "Tube radius" for filaments, and the "Save & Close" button shows its ampersand. Co-Authored-By: Claude Opus 5.5 --- .../ui/edit_object_types_dialog.py | 295 ++++++++---------- 1 file changed, 138 insertions(+), 157 deletions(-) diff --git a/src/copick_shared_ui/ui/edit_object_types_dialog.py b/src/copick_shared_ui/ui/edit_object_types_dialog.py index c594011..a17f740 100644 --- a/src/copick_shared_ui/ui/edit_object_types_dialog.py +++ b/src/copick_shared_ui/ui/edit_object_types_dialog.py @@ -7,7 +7,6 @@ from copick.models import PickableObject from qtpy.QtCore import Qt -from qtpy.QtGui import QFont from qtpy.QtWidgets import ( QCheckBox, QColorDialog, @@ -15,7 +14,6 @@ QDialog, QDialogButtonBox, QDoubleSpinBox, - QFormLayout, QGridLayout, QGroupBox, QHBoxLayout, @@ -25,6 +23,7 @@ QMessageBox, QPushButton, QSpinBox, + QSplitter, QTableWidget, QTableWidgetItem, QVBoxLayout, @@ -97,8 +96,8 @@ def __init__(self, parent=None, existing_objects=None): self.setModal(True) self.setWindowTitle("Edit Object Types") - self.setMinimumSize(800, 600) - self.resize(900, 700) + self.setMinimumSize(760, 560) + self.resize(900, 720) self._setup_ui() self._connect_signals() @@ -112,31 +111,26 @@ def _setup_ui(self): main_layout.setContentsMargins(12, 12, 12, 12) main_layout.setSpacing(12) - # Header - header = QLabel("✏️ Edit Object Types") - header_font = QFont() - header_font.setBold(True) - header_font.setPointSize(14) - header.setFont(header_font) - main_layout.addWidget(header) - - # Info label + # Info label (the window title names the dialog) info_label = QLabel("Select an object type from the table to edit, or create a new one using the form below.") info_label.setStyleSheet("color: gray; font-style: italic;") main_layout.addWidget(info_label) - # Objects table with management buttons + # Objects table above the form; the table takes the extra height, the form keeps its own self._create_objects_table() - main_layout.addWidget(self._objects_group) - - # Form for editing/adding self._create_object_form() - main_layout.addWidget(self._form_group) + splitter = QSplitter(Qt.Vertical) + splitter.addWidget(self._objects_group) + splitter.addWidget(self._form_group) + splitter.setStretchFactor(0, 1) + splitter.setStretchFactor(1, 0) + splitter.setChildrenCollapsible(False) + main_layout.addWidget(splitter, 1) # Dialog buttons self._button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self._ok_button = self._button_box.button(QDialogButtonBox.Ok) - self._ok_button.setText("Save & Close") + self._ok_button.setText("Save && Close") self._ok_button.setEnabled(False) # Initially disabled until changes are made self._ok_button.setStyleSheet( """ @@ -199,7 +193,7 @@ def _create_objects_table(self): self._objects_table.setSelectionBehavior(QTableWidget.SelectRows) self._objects_table.setSelectionMode(QTableWidget.SingleSelection) self._objects_table.setEditTriggers(QTableWidget.NoEditTriggers) - self._objects_table.setMaximumHeight(200) + self._objects_table.setMinimumHeight(150) # Configure column widths header = self._objects_table.horizontalHeader() @@ -215,35 +209,45 @@ def _create_objects_table(self): self._objects_group.setLayout(group_layout) def _create_object_form(self): - """Create the object form for editing/adding""" + """Create the object form for editing/adding: one grid of three label/field pairs per row.""" self._form_group = QGroupBox("Object Configuration") main_layout = QVBoxLayout() - # Form status label + # Status on the left, form actions on the right + actions = QHBoxLayout() self._form_status = QLabel("Ready to add new object") - self._form_status.setStyleSheet("font-weight: bold; color: #4A90E2; padding: 5px;") - main_layout.addWidget(self._form_status) - - # Grid layout for two columns - grid_layout = QGridLayout() - grid_layout.setColumnStretch(0, 1) - grid_layout.setColumnStretch(1, 1) - grid_layout.setHorizontalSpacing(20) + self._form_status.setStyleSheet("font-weight: bold; color: #4A90E2;") + actions.addWidget(self._form_status) + actions.addStretch() - # Left column - Basic properties - left_group = QGroupBox("Basic Properties") - left_layout = QFormLayout() + self._apply_button = QPushButton("✅ Save Object") + self._apply_button.setEnabled(False) + self._apply_button.setToolTip("Save the current object to the list") - # Object name - self._name_edit = QLineEdit() - self._name_edit.setPlaceholderText("Enter object name (e.g., 'ribosome', 'membrane')") - self._name_edit.setToolTip("Unique name for this pickable object type") - left_layout.addRow("Name*:", self._name_edit) + self._cancel_edit_button = QPushButton("❌ Cancel") + self._cancel_edit_button.setEnabled(False) + self._cancel_edit_button.setToolTip("Cancel current editing operation") - # Name validation label - self._name_validation = QLabel() - self._name_validation.setStyleSheet( - """ + actions.addWidget(self._apply_button) + actions.addWidget(self._cancel_edit_button) + main_layout.addLayout(actions) + + grid = QGridLayout() + grid.setHorizontalSpacing(8) + grid.setVerticalSpacing(6) + for col in (1, 3, 5): + grid.setColumnStretch(col, 1) + + def add_field(row: int, pair: int, text: str, widget: QWidget) -> QLabel: + label = QLabel(text) + label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + if pair: + label.setContentsMargins(12, 0, 0, 0) # separate the label/field pairs + grid.addWidget(label, row, 2 * pair) + grid.addWidget(widget, row, 2 * pair + 1) + return label + + error_style = """ QLabel { color: #d32f2f; font-size: 10px; @@ -252,123 +256,105 @@ def _create_object_form(self): border: 1px solid rgba(211, 47, 47, 0.3); border-radius: 3px; } - """, - ) + """ + + # Row 0: name, label, colour + self._name_edit = QLineEdit() + self._name_edit.setPlaceholderText("e.g. ribosome") + self._name_edit.setToolTip("Unique name for this pickable object type") + add_field(0, 0, "Name*:", self._name_edit) + + self._label_spin = QSpinBox() + self._label_spin.setRange(1, 9999) + self._label_spin.setValue(1) + self._label_spin.setToolTip("Unique numeric identifier for this object type") + add_field(0, 1, "Label*:", self._label_spin) + + color_widget = QWidget() + color_layout = QHBoxLayout(color_widget) + color_layout.setContentsMargins(0, 0, 0, 0) + self._color_button = ColorButton() + color_layout.addWidget(self._color_button) + color_layout.addStretch() + add_field(0, 2, "Color:", color_widget) + + # Row 1: validation messages (hidden unless a field is invalid) + self._name_validation = QLabel() + self._name_validation.setStyleSheet(error_style) self._name_validation.setWordWrap(True) self._name_validation.hide() - left_layout.addRow("", self._name_validation) + self._label_validation = QLabel() + self._label_validation.setStyleSheet(error_style) + self._label_validation.setWordWrap(True) + self._label_validation.hide() + validation = QHBoxLayout() + validation.setContentsMargins(0, 0, 0, 0) + validation.addWidget(self._name_validation, 1) + validation.addWidget(self._label_validation, 1) + grid.addLayout(validation, 1, 0, 1, 6) - # Is particle checkbox + # Row 2: type, radius, map threshold self._is_particle_cb = QCheckBox("Is Particle") self._is_particle_cb.setChecked(True) self._is_particle_cb.setToolTip( "Check if this object should be represented by points, uncheck for segmentation masks", ) - left_layout.addRow("Type:", self._is_particle_cb) - - # Is filament checkbox (a filament is a particle annotated with ordered points along its axis) + # A filament is a particle annotated with ordered points along its axis self._is_filament_cb = QCheckBox("Is Filament") self._is_filament_cb.setChecked(False) self._is_filament_cb.setToolTip( "Check for continuous assemblies (e.g. microtubules, actin) annotated with ordered points along their " "axis.\nPicks then carry the filament ID as instance ID. Requires 'Is Particle'.", ) - left_layout.addRow("", self._is_filament_cb) - - # Label (numeric ID) - self._label_spin = QSpinBox() - self._label_spin.setRange(1, 9999) - self._label_spin.setValue(1) - self._label_spin.setToolTip("Unique numeric identifier for this object type") - left_layout.addRow("Label*:", self._label_spin) + type_widget = QWidget() + type_layout = QHBoxLayout(type_widget) + type_layout.setContentsMargins(0, 0, 0, 0) + type_layout.addWidget(self._is_particle_cb) + type_layout.addWidget(self._is_filament_cb) + type_layout.addStretch() + add_field(2, 0, "Type:", type_widget) - # Label validation - self._label_validation = QLabel() - self._label_validation.setStyleSheet( - """ - QLabel { - color: #d32f2f; - font-size: 10px; - padding: 2px; - background-color: rgba(211, 47, 47, 0.1); - border: 1px solid rgba(211, 47, 47, 0.3); - border-radius: 3px; - } - """, - ) - self._label_validation.setWordWrap(True) - self._label_validation.hide() - left_layout.addRow("", self._label_validation) - - # Color selection - color_widget = QWidget() - color_layout = QHBoxLayout() - color_layout.setContentsMargins(0, 0, 0, 0) - color_layout.setSpacing(5) - - self._color_button = ColorButton() - color_layout.addWidget(self._color_button) - color_layout.addWidget(QLabel("Click to change color")) - color_layout.addStretch() - color_widget.setLayout(color_layout) - - left_layout.addRow("Color:", color_widget) - left_group.setLayout(left_layout) + self._radius_spin = QDoubleSpinBox() + self._radius_spin.setRange(0.1, 1000.0) + self._radius_spin.setDecimals(1) + self._radius_spin.setValue(10.0) + self._radius_spin.setSpecialValueText("None") + self._radius_spin.setToolTip("Radius for particle display (set to minimum for None)") + self._radius_label = add_field(2, 1, "Radius (Å):", self._radius_spin) - # Right column - Optional properties - right_group = QGroupBox("Optional Properties") - right_layout = QFormLayout() + self._threshold_spin = QDoubleSpinBox() + self._threshold_spin.setRange(-9999.0, 9999.0) + self._threshold_spin.setDecimals(3) + self._threshold_spin.setValue(0.0) + self._threshold_spin.setSpecialValueText("None") + self._threshold_spin.setToolTip("Threshold for isosurface rendering (set to minimum for None)") + add_field(2, 2, "Map Threshold:", self._threshold_spin) - # EMDB ID + # Row 3: database identifiers self._emdb_edit = QLineEdit() - self._emdb_edit.setPlaceholderText("e.g., EMD-1234") + self._emdb_edit.setPlaceholderText("e.g. EMD-1234") self._emdb_edit.setToolTip("EMDB ID for this object type") - right_layout.addRow("EMDB ID:", self._emdb_edit) + add_field(3, 0, "EMDB ID:", self._emdb_edit) - # PDB ID self._pdb_edit = QLineEdit() - self._pdb_edit.setPlaceholderText("e.g., 1ABC") + self._pdb_edit.setPlaceholderText("e.g. 1ABC") self._pdb_edit.setToolTip("PDB ID for this object type") - right_layout.addRow("PDB ID:", self._pdb_edit) + add_field(3, 1, "PDB ID:", self._pdb_edit) # Identifier (GO/UniProtKB/CHEBI/PDB/UBERON/CL/CDPO) self._identifier_edit = QLineEdit() - self._identifier_edit.setPlaceholderText("e.g., GO:0005840, UniProtKB:P0CX35, CHEBI:15986, PDB-1BXN") + self._identifier_edit.setPlaceholderText("e.g. GO:0005840") self._identifier_edit.setToolTip( - "Ontology/database identifier for this object type.\n" - "Supported namespaces: GO, UniProtKB, CHEBI, PDB (dash separator), UBERON, CL, CDPO.", + "Ontology/database identifier for this object type, e.g. GO:0005840, UniProtKB:P0CX35, CHEBI:15986, " + "PDB-1BXN.\nSupported namespaces: GO, UniProtKB, CHEBI, PDB (dash separator), UBERON, CL, CDPO.", ) - right_layout.addRow("Identifier:", self._identifier_edit) - - # Map threshold - self._threshold_spin = QDoubleSpinBox() - self._threshold_spin.setRange(-9999.0, 9999.0) - self._threshold_spin.setDecimals(3) - self._threshold_spin.setValue(0.0) - self._threshold_spin.setSpecialValueText("None") - self._threshold_spin.setToolTip("Threshold for isosurface rendering (set to minimum for None)") - right_layout.addRow("Map Threshold:", self._threshold_spin) - - # Radius - self._radius_spin = QDoubleSpinBox() - self._radius_spin.setRange(0.1, 1000.0) - self._radius_spin.setDecimals(1) - self._radius_spin.setValue(10.0) - self._radius_spin.setSpecialValueText("None") - self._radius_spin.setToolTip("Radius for particle display (set to minimum for None)") - right_layout.addRow("Radius (Å):", self._radius_spin) - - right_group.setLayout(right_layout) - - # Filament properties (stored as metadata["copick"]["filament"]) - self._filament_group = QGroupBox("Filament Properties") - filament_layout = QFormLayout() + add_field(3, 2, "Identifier:", self._identifier_edit) + # Row 4: filament properties (stored as metadata["copick"]["filament"]; enabled with "Is Filament") self._polarity_combo = QComboBox() for text, _value in POLARITY_CHOICES: self._polarity_combo.addItem(text) self._polarity_combo.setToolTip("Whether the structure has a polarity (microtubules and actin are polar)") - filament_layout.addRow("Polarity:", self._polarity_combo) self._rise_spin = QDoubleSpinBox() self._rise_spin.setRange(0.0, 10000.0) @@ -376,7 +362,6 @@ def _create_object_form(self): self._rise_spin.setValue(0.0) self._rise_spin.setSpecialValueText("Not set") self._rise_spin.setToolTip("Axial rise per subunit (descriptive only; never used as a sampling distance)") - filament_layout.addRow("Helical rise (Å):", self._rise_spin) self._twist_spin = QDoubleSpinBox() self._twist_spin.setRange(-361.0, 360.0) @@ -384,36 +369,33 @@ def _create_object_form(self): self._twist_spin.setValue(-361.0) self._twist_spin.setSpecialValueText("Not set") self._twist_spin.setToolTip("Twist per subunit (descriptive only)") - filament_layout.addRow("Helical twist (°):", self._twist_spin) - - self._filament_group.setLayout(filament_layout) - self._filament_group.setEnabled(False) - # Add groups to grid - grid_layout.addWidget(left_group, 0, 0) - grid_layout.addWidget(right_group, 0, 1) - grid_layout.addWidget(self._filament_group, 1, 0, 1, 2) - - # Form action buttons - form_buttons_layout = QHBoxLayout() - form_buttons_layout.setContentsMargins(0, 10, 0, 0) - - self._apply_button = QPushButton("✅ Save Object") - self._apply_button.setEnabled(False) - self._apply_button.setToolTip("Save the current object to the list") - - self._cancel_edit_button = QPushButton("❌ Cancel") - self._cancel_edit_button.setEnabled(False) - self._cancel_edit_button.setToolTip("Cancel current editing operation") - - form_buttons_layout.addWidget(self._apply_button) - form_buttons_layout.addWidget(self._cancel_edit_button) - form_buttons_layout.addStretch() - - main_layout.addLayout(grid_layout) - main_layout.addLayout(form_buttons_layout) + self._filament_widgets = [ + add_field(4, 0, "Polarity:", self._polarity_combo), + add_field(4, 1, "Helical rise (Å):", self._rise_spin), + add_field(4, 2, "Helical twist (°):", self._twist_spin), + self._polarity_combo, + self._rise_spin, + self._twist_spin, + ] + self._set_filament_enabled(False) + + main_layout.addLayout(grid) self._form_group.setLayout(main_layout) + def _set_filament_enabled(self, enabled: bool) -> None: + """Enable the filament properties, and call the radius what it means for a filament: the tube radius.""" + for widget in self._filament_widgets: + widget.setEnabled(enabled) + self._radius_label.setText("Tube radius (Å):" if enabled else "Radius (Å):") + self._radius_spin.setToolTip( + ( + "Tube radius of the filament (set to minimum for None)" + if enabled + else "Radius for particle display (set to minimum for None)" + ), + ) + def _connect_signals(self): """Connect widget signals""" # Dialog buttons @@ -447,7 +429,7 @@ def _on_particle_toggled(self, checked: bool): self._is_filament_cb.setChecked(False) def _on_filament_toggled(self, checked: bool): - self._filament_group.setEnabled(checked) + self._set_filament_enabled(checked) def _populate_objects_table(self): """Populate the objects table""" @@ -864,7 +846,6 @@ def _validate_form(self): ) self._update_button_states() - self.adjustSize() def _is_form_valid(self) -> bool: """Check if the current form state is valid"""