diff --git a/artiq/dashboard/alt_moninj.py b/artiq/dashboard/alt_moninj.py index fc706ae37..872f4cf31 100644 --- a/artiq/dashboard/alt_moninj.py +++ b/artiq/dashboard/alt_moninj.py @@ -3,6 +3,55 @@ from PyQt5 import QtWidgets, QtCore from artiq.gui.models import DictSyncTreeSepModel +from artiq.gui.tools import QRecursiveFilterProxyModel + + +# Not tested with validators or partial editing (textChanged etc). Intended for simple usage. +class BetterLineEdit(QtWidgets.QLineEdit): + finished = QtCore.pyqtSignal(bool) + + def __init__(self, init): + QtWidgets.QLineEdit.__init__(self, init) + self.setFrame(False) + self.setReadOnly(True) + self.returnPressed.connect(self._return_pressed) + self.editingFinished.connect(self._editing_finished) + self._text = init + self._candidate = None + + def mouseDoubleClickEvent(self, event): + if self.isReadOnly(): + self.setReadOnly(False) + self.setFrame(True) + QtWidgets.QLineEdit.mouseDoubleClickEvent(self, event) + + def _return_pressed(self): + self._candidate = self.text() + + def _editing_finished(self): + self.setReadOnly(True) + self.setFrame(False) + if self._candidate is not None: + changed = self._candidate != self._text + self._text = self._candidate + self.setText(self._text) + self._candidate = None + self.finished.emit(changed) + else: + self.setText(self._text) + + def set_text(self, text): + self.blockSignals(True) + self._text = text + self.setText(self._text) + self.blockSignals(False) + + def keyPressEvent(self, event): + key = event.key() + if key == QtCore.Qt.Key_Escape and not self.isReadOnly(): + self.editingFinished.emit() + else: + QtWidgets.QLineEdit.keyPressEvent(self, event) # MoninjView contains these and adds according to the proper type @@ -12,48 +61,162 @@ from artiq.gui.models import DictSyncTreeSepModel # -> this is passed directly to the device manager # suboptimally multiple signals could be exposed class _MoninjWidget(QtWidgets.QWidget): - inject = QtCore.pyqtSignal(tuple) + inject = QtCore.pyqtSignal(str, str) + nameChanged = QtCore.pyqtSignal() - def __init__(self): + def __init__(self, name, channel): QtWidgets.QWidget.__init__(self) + self.name = name + self.channel = channel + self.setMaximumHeight(100) + self.layout = QtWidgets.QGridLayout() + self.setLayout(self.layout) + self.name_label = BetterLineEdit(self.name) # TODO: make this a cancellable line edit -> add cancellable line edit to gui tools + self.name_label.finished.connect(self.name_changed) + self.layout.addWidget(self.name_label, 0, 0) + + def name_changed(self, changed): + if changed: + self.name = self.name_label._text + self.nameChanged.emit() def refresh(self, value): raise NotImplementedError class _TTLWidget(_MoninjWidget): - pass + def __init__(self, name, channel): + _MoninjWidget.__init__(self, name, channel) + self.label = QtWidgets.QLabel("0") + self.layout.addWidget(self.label, 0, 1) + self.button_group = QtWidgets.QButtonGroup() + self.button_group.setExclusive(False) + self.lvl = QtWidgets.QPushButton("LVL") + self.lvl.setCheckable(True) + self.layout.addWidget(self.lvl, 0, 2) + self.button_group.addButton(self.lvl, 0) + self.ovr = QtWidgets.QPushButton("OVR") + self.ovr.setCheckable(True) + self.layout.addWidget(self.ovr, 0, 3) + self.button_group.addButton(self.ovr, 1) + self.button_group.idClicked.connect(self._button_clicked) + + def _button_clicked(self, id): + lvl = self.lvl.isChecked() + ovr = self.ovr.isChecked() + if lvl and ovr: + self.inject.emit("ttl", "1") + elif not lvl and ovr: + self.inject.emit("ttl", "0") + elif id == 1: + self.inject.emit("ttl", "exp") + + def refresh(self, value): + self.label.setText(value) class _DDSWidget(_MoninjWidget): - pass + def __init__(self, name, channel): + _MoninjWidget.__init__(self, name, channel) + + def refresh(self, value): + raise NotImplementedError class _DACWidget(_MoninjWidget): - pass + def __init__(self, name, channel): + _MoninjWidget.__init__(self, name, channel) # the channel is the actual uid + + def refresh(self, value): + raise NotImplementedError # The main tree / table view of the dock -> probably inherits from QTreeView and connects with the -# 'MoninjModel' MoninjModel and View could be merged as QTreeWidget but probably not a good idea.. -class MoninjView: - pass +# 'MoninjModel' MoninjModel and View could be merged as QTreeWidget <-- this is what we will do... +# dont allow nested groups for now +# strongly resembles and borrows from EntryTreeWidget +class MoninjTreeWidget(QtWidgets.QTreeWidget): + def __init__(self): + QtWidgets.QTreeWidget.__init__(self) + self.setDragDropMode(QtWidgets.QTreeWidget.InternalMove) + + def add_group(self): + pass + + def add_channel(self, channel_args): + pass + + def add_channels(self, channels): + pass + + def remove_channel(self, id): + pass + + def get_configuration(self): + pass + + def set_configuration(self, config): + pass # Like the _AddChannelDialog but should have search function enabled with QRecursiveFilterProxyModel class MoninjAddChannelDialog(QtWidgets.QDialog): - pass + def __init__(self, parent): + QtWidgets.QDialog.__init__(self, parent=parent) + self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) + self.setWindowTitle("Add channels") + + layout = QtWidgets.QVBoxLayout() + self.setLayout(layout) + + self._search = QtWidgets.QLineEdit() + self._search.setPlaceholderText("search...") + self._search.editingFinished.connect(self._search_datasets) + layout.addWidget(self._search) + + self._model = None + self._tree_view = QtWidgets.QTreeView() + self._tree_view.setHeaderHidden(True) + self._tree_view.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectItems) + self._tree_view.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection) + layout.addWidget(self._tree_view) + + self._button_box = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel + ) + self._button_box.setCenterButtons(True) + self._button_box.accepted.connect(self.add_channels) + self._button_box.rejected.connect(self.reject) + layout.addWidget(self._button_box) + + def setModel(self, model): + self._model = model + self._model_filter = QRecursiveFilterProxyModel() + self._model_filter.setSourceModel(model) + self._tree_view.setModel(self._model_filter) + + def _search_datasets(self): + if hasattr(self, "_table_filter"): + self.table_model_filter.setFilterFixedString(self._search.displayText()) + + def add_channels(self): + selection = self._tree_view.selectedIndexes() + channels = [] + for select in selection: + key = self._model.index_to_key(select) + if key is not None: + channels.append([key, *self._model[key].ref, []]) + self.channels = channels + self.accept() # Contains the necessary identifiers for listing all available channels, organized by type and name # does not do any moninj itself, only exists as reference to add channels to 'MoninjModel' class MoninjChannelModel(DictSyncTreeSepModel): - pass - - -# Currently displayed data, should either be AbstractTableModel -# extends features to connect widgets with device manager -class MoninjModel: - pass + def __init__(self, init): + DictSyncTreeSepModel.__init__(self, "/", ["Channels"], init) # Retains most functionality of old DeviceManager (contain any device interface here) @@ -69,5 +232,80 @@ class DeviceManager: # + search bar (sort options / if not in view itself) # + add channels dialog window class MoninjDock(QtWidgets.QDockWidget): - pass + def __init__(self, schedule_ctl): + QtWidgets.QDockWidget.__init__(self, "MonInj") + self.setObjectName("MonInj") + self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable | + QtWidgets.QDockWidget.DockWidgetFloatable) + # connect the device manager (it manages ddb + moninj) + self.dm = DeviceManager(schedule_ctl) + + # GridLayout + layout = QtWidgets.QGridLayout() + self.setLayout(layout) + + # Options (borrow from waveform) + self._menu_btn = QtWidgets.QPushButton() + self._menu_btn.setIcon( + QtWidgets.QApplication.style().standardIcon( + QtWidgets.QStyle.SP_FileDialogStart)) + layout.addWidget(self._menu_btn, 0, 0) + + self._file_menu = QtWidgets.QMenu() + # self._add_async_action("Open configuration...", self.load_configuration) + # self._add_async_action("Save configuration...", self.save_configuration) + self._menu_btn.setMenu(self._file_menu) + + # Add channels + self.channel_model = MoninjChannelModel() + self.add_channel_dialog = MoninjAddChannelDialog() + self.add_channel_dialog.setModel(self.channel_model) + + self._add_btn = QtWidgets.QToolButton() + self._add_btn.setToolTip("Add channels...") + self._add_btn.setIcon( + QtWidgets.QApplication.style().standardIcon( + QtWidgets.QStyle.SP_FileDialogListView)) + self._add_btn.clicked.connect(self.add_channel_dialog.open) + layout.addWidget(self._add_btn, 0, 2) + + # Add new group + self._add_group_btn = QtWidgets.QToolButton() + self._add_group_btn.setToolTip("Add group...") + self._add_group_btn.setIcon( + QtWidgets.QApplication.style().standardIcon( + QtWidgets.QStyle.SP_FileDialogListView)) + + # connect device manager to channel model (potentially separate out this) + self.dm.update_channels.connect(self.channel_model.update_channels) + + # MoninjView and model + self.moninj_model = MoninjModel() + self.moninj_view = MoninjView() + self.moninj_view.setModel(self.moninj_model) + layout.addWidget(self.moninj_view, 1, 0) + + # connect device manager to moninj model / view + self.dm.monitor.connect(self.moninj_view.monitor) + self.moninj_view.inject.connect(self.dm.inject) + + # add group + self._add_group_btn.clicked.connect(self.moninj_model.new_group) + + # is it acceptable for open/closed knowledge to be dropped? only maintained in state + def save_configuration(self): + pass + + def open_configuration(self): + pass + + def save_state(self): + pass + + def restore_state(self): + pass + + async def stop(self): + if self.dm is not None: + await self.dm.close()