Skip to content

Commit 210d5fd

Browse files
Delete from header (#216)
* Improved that the same actions are used * Small review
1 parent 78cb182 commit 210d5fd

File tree

2 files changed

+203
-3
lines changed

2 files changed

+203
-3
lines changed

src/petab_gui/controllers/table_controllers.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,17 @@ def setup_connections(self):
117117
settings_manager.settings_changed.connect(self.update_defaults)
118118

119119
def setup_context_menu(self, actions):
120-
"""Setup context menu for this table."""
120+
"""Setup context menus for this table.
121+
122+
Sets up both the table body context menu and the header context menus
123+
using the same actions dictionary for consistency.
124+
125+
Args:
126+
actions: Dictionary of QAction objects
127+
"""
121128
view = self.view.table_view
122129
view.setup_context_menu(actions)
130+
view.setup_header_context_menus(actions)
123131

124132
def validate_changed_cell(self, row, column):
125133
"""Validate the changed cell and whether its linting is correct."""

src/petab_gui/views/table_view.py

Lines changed: 194 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import petab.v1 as petab
2-
from PySide6.QtCore import QPropertyAnimation, QRect, Qt
2+
from PySide6.QtCore import QItemSelectionModel, QPropertyAnimation, QRect, Qt
33
from PySide6.QtGui import QColor, QGuiApplication
44
from PySide6.QtWidgets import (
55
QComboBox,
66
QCompleter,
77
QDockWidget,
88
QHeaderView,
99
QLineEdit,
10+
QMenu,
1011
QStyledItemDelegate,
1112
QTableView,
1213
)
1314

14-
from ..utils import get_selected_rectangles
15+
from ..C import COLUMN
16+
from ..utils import get_selected, get_selected_rectangles
1517
from .context_menu_mananger import ContextMenuManager
1618

1719

@@ -321,6 +323,196 @@ def setup_context_menu(self, actions):
321323
self.context_menu_manager.create_context_menu
322324
)
323325

326+
def _get_source_model(self):
327+
"""Get the source model, handling proxy models if present.
328+
329+
Returns:
330+
The source model (unwraps proxy if present)
331+
"""
332+
model = self.model()
333+
return model.sourceModel() if hasattr(model, "sourceModel") else model
334+
335+
def _ensure_header_selected(self, index, mode=None):
336+
"""Ensure header at index is selected.
337+
338+
If not already selected, selects the entire row/column. Otherwise
339+
preserves existing multi-selection (Excel-like behavior).
340+
341+
Args:
342+
index: Row or column index to ensure is selected
343+
mode: COLUMN constant for column selection, None for row selection
344+
345+
Returns:
346+
Set of selected indices
347+
"""
348+
selection_model = self.selectionModel()
349+
selected = get_selected(self, mode=mode if mode == COLUMN else None)
350+
351+
if index not in selected:
352+
flag = (
353+
QItemSelectionModel.Columns
354+
if mode == COLUMN
355+
else QItemSelectionModel.Rows
356+
)
357+
row_idx = 0 if mode == COLUMN else index
358+
col_idx = index if mode == COLUMN else 0
359+
selection_model.select(
360+
self.model().index(row_idx, col_idx),
361+
QItemSelectionModel.Select | flag,
362+
)
363+
selected = {index}
364+
365+
return selected
366+
367+
def setup_header_context_menus(self, actions):
368+
"""Set up context menus for row and column deletion.
369+
370+
Enables right-click context menus on both the vertical (row) and
371+
horizontal (column) headers. The menus provide options to delete the
372+
clicked row or column using the same actions as the table body context
373+
menu.
374+
375+
Args:
376+
actions: Dictionary of QAction objects (same as setup_context_menu)
377+
"""
378+
# Store references to the delete actions for header menus
379+
self.delete_row_action = actions["delete_row"]
380+
self.delete_column_action = actions["delete_column"]
381+
382+
# Enable custom context menus on headers
383+
self.verticalHeader().setContextMenuPolicy(Qt.CustomContextMenu)
384+
self.horizontalHeader().setContextMenuPolicy(Qt.CustomContextMenu)
385+
386+
# Connect signals
387+
self.verticalHeader().customContextMenuRequested.connect(
388+
self._show_row_context_menu
389+
)
390+
self.horizontalHeader().customContextMenuRequested.connect(
391+
self._show_column_context_menu
392+
)
393+
394+
def _show_row_context_menu(self, position):
395+
"""Show context menu for row deletion.
396+
397+
Supports multi-row deletion. If the clicked row is not selected,
398+
it selects it. If multiple rows are already selected, it keeps
399+
the selection and deletes all selected rows (Excel-like behavior).
400+
401+
Uses the same delete_row action as the table body context menu.
402+
403+
Args:
404+
position: The position where the context menu was requested
405+
"""
406+
vertical_header = self.verticalHeader()
407+
row = vertical_header.logicalIndexAt(position)
408+
409+
source_model = self._get_source_model()
410+
411+
# Don't show menu for the "new row" at the end
412+
if row >= source_model.get_df().shape[0]:
413+
return
414+
415+
# Ensure clicked row is selected (or preserve multi-selection)
416+
selected_rows = self._ensure_header_selected(row)
417+
418+
# Create menu with count indicator using the existing action
419+
menu = QMenu()
420+
row_count = len(selected_rows)
421+
original_text = self.delete_row_action.text()
422+
423+
# Update action text based on selection count
424+
new_text = (
425+
"Delete Row" if row_count == 1 else f"Delete {row_count} Rows"
426+
)
427+
self.delete_row_action.setText(new_text)
428+
menu.addAction(self.delete_row_action)
429+
430+
try:
431+
menu.exec_(vertical_header.mapToGlobal(position))
432+
finally:
433+
# Always restore original action text
434+
self.delete_row_action.setText(original_text)
435+
436+
def _show_column_context_menu(self, position):
437+
"""Show context menu for column deletion.
438+
439+
Supports multi-column deletion. If the clicked column is not selected,
440+
it selects it. If multiple columns are already selected, it keeps
441+
the selection and deletes all selected columns (Excel-like behavior).
442+
443+
Shows informative text for required columns. Uses the same
444+
delete_column action as the table body context menu.
445+
446+
Args:
447+
position: The position where the context menu was requested
448+
"""
449+
horizontal_header = self.horizontalHeader()
450+
column = horizontal_header.logicalIndexAt(position)
451+
452+
source_model = self._get_source_model()
453+
454+
# Ensure clicked column is selected (or preserve multi-selection)
455+
selected_columns = self._ensure_header_selected(column, mode=COLUMN)
456+
457+
# Check which columns can be deleted and build required column list
458+
required_columns = []
459+
deletable_columns = []
460+
for col in selected_columns:
461+
can_delete, col_name = source_model.allow_column_deletion(col)
462+
if can_delete:
463+
deletable_columns.append(col)
464+
else:
465+
required_columns.append(col_name)
466+
467+
# Create menu with appropriate text using the existing action
468+
menu = QMenu()
469+
column_count = len(selected_columns)
470+
deletable_count = len(deletable_columns)
471+
472+
# Store original action text and enabled state
473+
original_text = self.delete_column_action.text()
474+
original_enabled = self.delete_column_action.isEnabled()
475+
476+
# Determine menu text and enabled state based on selection
477+
if column_count == 1:
478+
if deletable_count == 1:
479+
menu_text = "Delete Column"
480+
enabled = True
481+
else:
482+
# Single required column
483+
menu_text = (
484+
f"Delete Column (Required: '{required_columns[0]}')"
485+
)
486+
enabled = False
487+
else:
488+
# Multiple columns selected
489+
if deletable_count == column_count:
490+
menu_text = f"Delete {column_count} Columns"
491+
enabled = True
492+
elif deletable_count == 0:
493+
# All required
494+
menu_text = f"Delete {column_count} Columns (All required)"
495+
enabled = False
496+
else:
497+
# Some required, some deletable
498+
required_count = len(required_columns)
499+
menu_text = (
500+
f"Delete {deletable_count} Columns "
501+
f"({required_count} required will be skipped)"
502+
)
503+
enabled = True
504+
505+
self.delete_column_action.setText(menu_text)
506+
self.delete_column_action.setEnabled(enabled)
507+
menu.addAction(self.delete_column_action)
508+
509+
try:
510+
menu.exec_(horizontal_header.mapToGlobal(position))
511+
finally:
512+
# Always restore original action text and enabled state
513+
self.delete_column_action.setText(original_text)
514+
self.delete_column_action.setEnabled(original_enabled)
515+
324516
def setModel(self, model):
325517
"""Set the model for the table view.
326518

0 commit comments

Comments
 (0)