diff --git a/artiq/dashboard/schedule.py b/artiq/dashboard/schedule.py index 8ea213239..ac77398d7 100644 --- a/artiq/dashboard/schedule.py +++ b/artiq/dashboard/schedule.py @@ -6,6 +6,7 @@ import logging from PyQt6 import QtCore, QtWidgets, QtGui from artiq.gui.models import DictSyncModel +from artiq.gui.tools import SelectableColumnTableView from artiq.tools import elide @@ -66,7 +67,7 @@ class ScheduleDock(QtWidgets.QDockWidget): self.schedule_ctl = schedule_ctl - self.table = QtWidgets.QTableView() + self.table = SelectableColumnTableView() self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) self.table.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) self.table.verticalHeader().setSectionResizeMode( @@ -104,6 +105,9 @@ class ScheduleDock(QtWidgets.QDockWidget): h.resizeSection(6, 20 * cw) h.resizeSection(7, 20 * cw) + # Allow user to reorder or disable columns. + h.setSectionsMovable(True) + def set_model(self, model): self.table_model = model self.table.setModel(self.table_model) @@ -154,4 +158,9 @@ class ScheduleDock(QtWidgets.QDockWidget): return bytes(self.table.horizontalHeader().saveState()) def restore_state(self, state): - self.table.horizontalHeader().restoreState(QtCore.QByteArray(state)) + h = self.table.horizontalHeader() + h.restoreState(QtCore.QByteArray(state)) + + # The state includes the sectionsMovable property, so set it again to be able to + # deal with pre-existing save files from when we used not to enable it. + h.setSectionsMovable(True) diff --git a/artiq/gui/tools.py b/artiq/gui/tools.py index 0fc9e4615..63b228632 100644 --- a/artiq/gui/tools.py +++ b/artiq/gui/tools.py @@ -1,7 +1,7 @@ import asyncio import logging -from PyQt6 import QtCore, QtWidgets +from PyQt6 import QtCore, QtGui, QtWidgets class DoubleClickLineEdit(QtWidgets.QLineEdit): @@ -88,6 +88,51 @@ class LayoutWidget(QtWidgets.QWidget): self.layout.addWidget(item, row, col, rowspan, colspan) +class SelectableColumnTableView(QtWidgets.QTableView): + """A QTableView packaged up with a header row context menu that allows users to + show/hide columns using checkable entries. + + By default, all columns are shown. If only one shown column remains, the entry is + disabled to prevent a situation where no columns are shown, which might be confusing + to the user. + + Qt considers whether columns are shown to be part of the header state, i.e. it is + included in saveState()/restoreState(). + """ + + def __init__(self): + super().__init__() + + self.horizontalHeader().setContextMenuPolicy( + QtCore.Qt.ContextMenuPolicy.CustomContextMenu) + self.horizontalHeader().customContextMenuRequested.connect( + self.show_header_context_menu) + + def show_header_context_menu(self, pos): + menu = QtWidgets.QMenu(self) + + num_columns_total = self.model().columnCount() + num_columns_shown = sum( + (not self.isColumnHidden(i)) for i in range(num_columns_total)) + for i in range(num_columns_total): + name = self.model().headerData(i, QtCore.Qt.Orientation.Horizontal) + action = QtGui.QAction(name, self) + action.setCheckable(True) + + is_currently_hidden = self.isColumnHidden(i) + action.setChecked(not is_currently_hidden) + if not is_currently_hidden: + if num_columns_shown == 1: + # Don't allow hiding of the last visible column. + action.setEnabled(False) + + action.triggered.connect( + lambda checked, i=i: self.setColumnHidden(i, not checked)) + menu.addAction(action) + + menu.exec(self.horizontalHeader().mapToGlobal(pos)) + + async def get_open_file_name(parent, caption, dir, filter): """like QtWidgets.QFileDialog.getOpenFileName(), but a coroutine""" dialog = QtWidgets.QFileDialog(parent, caption, dir, filter)