forked from M-Labs/artiq
1
0
Fork 0

Compare commits

...

10 Commits

3 changed files with 337 additions and 51 deletions

View File

@ -2,13 +2,15 @@ import asyncio
import logging import logging
import textwrap import textwrap
from collections import namedtuple from collections import namedtuple
from functools import partial
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets, QtGui
from artiq.coredevice.comm_moninj import CommMonInj, TTLOverride, TTLProbe from artiq.coredevice.comm_moninj import CommMonInj, TTLOverride, TTLProbe
from artiq.coredevice.ad9912_reg import AD9912_SER_CONF from artiq.coredevice.ad9912_reg import AD9912_SER_CONF
from artiq.gui.tools import LayoutWidget from artiq.gui.tools import LayoutWidget, QDockWidgetCloseDetect
from artiq.gui.flowlayout import FlowLayout from artiq.gui.models import DictSyncTreeSepModel
from artiq.gui.dndwidgets import VDragScrollArea, DragDropFlowLayoutWidget
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,6 +27,42 @@ class _CancellableLineEdit(QtWidgets.QLineEdit):
QtWidgets.QLineEdit.keyPressEvent(self, event) QtWidgets.QLineEdit.keyPressEvent(self, event)
# Cancellable and editable with double click.
# This class should not be used for programmatically changing text.
class _DoubleClickLineEdit(QtWidgets.QLineEdit):
finished = QtCore.pyqtSignal()
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
def mouseDoubleClickEvent(self, event):
if self.isReadOnly():
self.setReadOnly(False)
self.setFrame(True)
QtWidgets.QLineEdit.mouseDoubleClickEvent(self, event)
def _return_pressed(self):
self._text = self.text()
def _editing_finished(self):
self.setReadOnly(True)
self.setFrame(False)
self.setText(self._text)
self.finished.emit()
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)
class _TTLWidget(QtWidgets.QFrame): class _TTLWidget(QtWidgets.QFrame):
override_toggled = QtCore.pyqtSignal(bool) override_toggled = QtCore.pyqtSignal(bool)
level_toggled = QtCore.pyqtSignal(bool) level_toggled = QtCore.pyqtSignal(bool)
@ -34,6 +72,8 @@ class _TTLWidget(QtWidgets.QFrame):
self.setFrameShape(QtWidgets.QFrame.Box) self.setFrameShape(QtWidgets.QFrame.Box)
self.setFrameShadow(QtWidgets.QFrame.Raised) self.setFrameShadow(QtWidgets.QFrame.Raised)
self.uid = title
grid = QtWidgets.QGridLayout() grid = QtWidgets.QGridLayout()
grid.setContentsMargins(0, 0, 0, 0) grid.setContentsMargins(0, 0, 0, 0)
grid.setHorizontalSpacing(0) grid.setHorizontalSpacing(0)
@ -158,7 +198,10 @@ class _TTLHandler:
self.refresh_display() self.refresh_display()
def sort_key(self): def sort_key(self):
return self.channel return (0, self.channel, 0)
def to_model_path(self):
return "ttl/{}".format(self.title)
class _DDSWidget(QtWidgets.QFrame): class _DDSWidget(QtWidgets.QFrame):
@ -172,6 +215,8 @@ class _DDSWidget(QtWidgets.QFrame):
self.setFrameShape(QtWidgets.QFrame.Box) self.setFrameShape(QtWidgets.QFrame.Box)
self.setFrameShadow(QtWidgets.QFrame.Raised) self.setFrameShadow(QtWidgets.QFrame.Raised)
self.uid = title
grid = QtWidgets.QGridLayout() grid = QtWidgets.QGridLayout()
grid.setContentsMargins(0, 0, 0, 0) grid.setContentsMargins(0, 0, 0, 0)
grid.setHorizontalSpacing(0) grid.setHorizontalSpacing(0)
@ -354,7 +399,10 @@ class _DDSHandler:
self.dm.dds_channel_toggle(self.dds_name, sw=False) self.dm.dds_channel_toggle(self.dds_name, sw=False)
def sort_key(self): def sort_key(self):
return (self.bus_channel, self.channel) return (1, self.bus_channel, self.channel)
def to_model_path(self):
return "dds/{}".format(self.title)
class _DACWidget(QtWidgets.QFrame): class _DACWidget(QtWidgets.QFrame):
@ -364,6 +412,8 @@ class _DACWidget(QtWidgets.QFrame):
self.setFrameShape(QtWidgets.QFrame.Box) self.setFrameShape(QtWidgets.QFrame.Box)
self.setFrameShadow(QtWidgets.QFrame.Raised) self.setFrameShadow(QtWidgets.QFrame.Raised)
self.uid = (title, channel)
grid = QtWidgets.QGridLayout() grid = QtWidgets.QGridLayout()
grid.setContentsMargins(0, 0, 0, 0) grid.setContentsMargins(0, 0, 0, 0)
grid.setHorizontalSpacing(0) grid.setHorizontalSpacing(0)
@ -407,7 +457,62 @@ class _DACHandler:
self.widget.set_value(self.cur_value * 100 / 2**16) self.widget.set_value(self.cur_value * 100 / 2**16)
def sort_key(self): def sort_key(self):
return (self.spi_channel, self.channel) return (2, self.spi_channel, self.channel)
def to_model_path(self):
return "dac/{} ch{}".format(self.title, self.channel)
class Model(DictSyncTreeSepModel):
def __init__(self, init):
DictSyncTreeSepModel.__init__(self, "/", ["Channels"], init)
def clear(self):
for k in self.backing_store:
self._del_item(self, k.split(self.separator))
self.backing_store.clear()
def update(self, d):
for k, v in d.items():
self[v.to_model_path()] = v
class _AddChannelDialog(QtWidgets.QDialog):
def __init__(self, parent, model):
QtWidgets.QDialog.__init__(self, parent=parent)
self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
self.setWindowTitle("Add channels")
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
self._model = model
self._tree_view = QtWidgets.QTreeView()
self._tree_view.setHeaderHidden(True)
self._tree_view.setSelectionBehavior(
QtWidgets.QAbstractItemView.SelectItems)
self._tree_view.setSelectionMode(
QtWidgets.QAbstractItemView.ExtendedSelection)
self._tree_view.setModel(self._model)
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 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(self._model[key].ref)
self.channels = channels
self.accept()
_HandlerDesc = namedtuple("_HandlerDesc", "uid comment cls arguments") _HandlerDesc = namedtuple("_HandlerDesc", "uid comment cls arguments")
@ -487,12 +592,10 @@ class _DeviceManager:
self.handlers_by_uid = dict() self.handlers_by_uid = dict()
self.dds_sysclk = 0 self.dds_sysclk = 0
self.ttl_cb = lambda: None
self.ttl_handlers = dict() self.ttl_handlers = dict()
self.dds_cb = lambda: None
self.dds_handlers = dict() self.dds_handlers = dict()
self.dac_cb = lambda: None
self.dac_handlers = dict() self.dac_handlers = dict()
self.channels_cb = lambda: None
def init_ddb(self, ddb): def init_ddb(self, ddb):
self.ddb = ddb self.ddb = ddb
@ -513,17 +616,14 @@ class _DeviceManager:
self.setup_ttl_monitoring(False, handler.channel) self.setup_ttl_monitoring(False, handler.channel)
handler.delete_widget() handler.delete_widget()
del self.ttl_handlers[handler.channel] del self.ttl_handlers[handler.channel]
self.ttl_cb()
elif isinstance(handler, _DDSHandler): elif isinstance(handler, _DDSHandler):
self.setup_dds_monitoring(False, handler.bus_channel, handler.channel) self.setup_dds_monitoring(False, handler.bus_channel, handler.channel)
handler.delete_widget() handler.delete_widget()
del self.dds_handlers[(handler.bus_channel, handler.channel)] del self.dds_handlers[(handler.bus_channel, handler.channel)]
self.dds_cb()
elif isinstance(handler, _DACHandler): elif isinstance(handler, _DACHandler):
self.setup_dac_monitoring(False, handler.spi_channel, handler.channel) self.setup_dac_monitoring(False, handler.spi_channel, handler.channel)
handler.delete_widget() handler.delete_widget()
del self.dac_handlers[(handler.spi_channel, handler.channel)] del self.dac_handlers[(handler.spi_channel, handler.channel)]
self.dac_cb()
else: else:
raise ValueError raise ValueError
@ -536,19 +636,19 @@ class _DeviceManager:
if isinstance(handler, _TTLHandler): if isinstance(handler, _TTLHandler):
self.ttl_handlers[handler.channel] = handler self.ttl_handlers[handler.channel] = handler
self.ttl_cb()
self.setup_ttl_monitoring(True, handler.channel) self.setup_ttl_monitoring(True, handler.channel)
elif isinstance(handler, _DDSHandler): elif isinstance(handler, _DDSHandler):
self.dds_handlers[(handler.bus_channel, handler.channel)] = handler self.dds_handlers[(handler.bus_channel, handler.channel)] = handler
self.dds_cb()
self.setup_dds_monitoring(True, handler.bus_channel, handler.channel) self.setup_dds_monitoring(True, handler.bus_channel, handler.channel)
elif isinstance(handler, _DACHandler): elif isinstance(handler, _DACHandler):
self.dac_handlers[(handler.spi_channel, handler.channel)] = handler self.dac_handlers[(handler.spi_channel, handler.channel)] = handler
self.dac_cb()
self.setup_dac_monitoring(True, handler.spi_channel, handler.channel) self.setup_dac_monitoring(True, handler.spi_channel, handler.channel)
else: else:
raise ValueError raise ValueError
if self.description != description:
self.channels_cb()
self.description = description self.description = description
def ttl_set_mode(self, channel, mode): def ttl_set_mode(self, channel, mode):
@ -787,38 +887,156 @@ class _DeviceManager:
await self.mi_connection.close() await self.mi_connection.close()
class _MonInjDock(QtWidgets.QDockWidget): class _MonInjDock(QDockWidgetCloseDetect):
def __init__(self, name): def __init__(self, name, manager):
QtWidgets.QDockWidget.__init__(self, name) QtWidgets.QDockWidget.__init__(self, "MonInj")
self.setObjectName(name) self.setObjectName(name)
self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable | self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetFloatable) QtWidgets.QDockWidget.DockWidgetFloatable)
self.name = name
self.manager = manager
self.widget_uids = None
grid = LayoutWidget()
self.setWidget(grid)
self._channel_dialog = _AddChannelDialog(self, self.manager.channel_model)
self._channel_dialog.accepted.connect(self.add_channels)
add_channel_btn = QtWidgets.QToolButton()
add_channel_btn.setToolTip("Add channels...")
add_channel_btn.setIcon(
QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_FileDialogListView))
add_channel_btn.clicked.connect(self._channel_dialog.open)
grid.addWidget(add_channel_btn, 0, 0)
newdock = QtWidgets.QToolButton()
newdock.setToolTip("Create new moninj dock")
newdock.setIcon(QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_FileDialogNewFolder))
newdock.clicked.connect(lambda: manager.create_new_dock())
grid.addWidget(newdock, 0, 1)
self.display_name_edit = _DoubleClickLineEdit(name)
grid.addWidget(self.display_name_edit, 0, 2)
scroll_area = VDragScrollArea(self)
grid.addWidget(scroll_area, 1, 0, 1, 10)
self.flow = DragDropFlowLayoutWidget()
scroll_area.setWidgetResizable(True)
scroll_area.setWidget(self.flow)
def add_channels(self):
handlers = self._channel_dialog.channels
self.layout_widgets(handlers)
def layout_widgets(self, handlers): def layout_widgets(self, handlers):
scroll_area = QtWidgets.QScrollArea()
self.setWidget(scroll_area)
grid = FlowLayout()
grid_widget = QtWidgets.QWidget()
grid_widget.setLayout(grid)
for handler in sorted(handlers, key=lambda h: h.sort_key()): for handler in sorted(handlers, key=lambda h: h.sort_key()):
grid.addWidget(handler.widget) self.flow.addWidget(handler.widget)
scroll_area.setWidgetResizable(True) def restore_widgets(self):
scroll_area.setWidget(grid_widget) if self.widget_uids is not None:
uid2handler = self.manager.dm.handlers_by_uid
handlers = list()
for uid in self.widget_uids:
if uid in uid2handler:
handler = uid2handler[uid]
handler.create_widget()
handlers.append(handler)
else:
logger.warning("removing moninj widget {}".format(uid))
self.layout_widgets(handlers)
self.widget_uids = None
def _save_widget_uids(self):
uids = []
for i in range(self.flow.count()):
uids.append(self.flow.itemAt(i).widget().uid)
return uids
def save_state(self):
return {
"display_name": self.display_name_edit.text(),
"widget_uids": self._save_widget_uids()
}
def restore_state(self, state):
try:
display_name = state["display_name"]
except KeyError:
pass
else:
self.display_name_edit._text = display_name
self.display_name_edit.setText(display_name)
try:
self.widget_uids = state["widget_uids"]
except KeyError:
pass
class MonInj: class MonInj:
def __init__(self, schedule_ctl): def __init__(self, schedule_ctl, main_window):
self.ttl_dock = _MonInjDock("TTL") self.docks = dict()
self.dds_dock = _MonInjDock("DDS") self.main_window = main_window
self.dac_dock = _MonInjDock("DAC")
self.dm = _DeviceManager(schedule_ctl) self.dm = _DeviceManager(schedule_ctl)
self.dm.ttl_cb = lambda: self.ttl_dock.layout_widgets(self.dm.ttl_handlers.values()) self.dm.channels_cb = self.add_channels
self.dm.dds_cb = lambda: self.dds_dock.layout_widgets(self.dm.dds_handlers.values()) self.channel_model = Model({})
self.dm.dac_cb = lambda: self.dac_dock.layout_widgets(self.dm.dac_handlers.values())
def add_channels(self):
self.channel_model.clear()
self.channel_model.update(self.dm.handlers_by_uid)
for dock in self.docks.values():
dock.restore_widgets()
def create_new_dock(self, add_to_area=True):
n = 0
name = "moninj0"
while name in self.docks:
n += 1
name = "moninj" + str(n)
dock = _MonInjDock(name, self)
self.docks[name] = dock
if add_to_area:
self.main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock)
dock.setFloating(True)
dock.sigClosed.connect(partial(self.on_dock_closed, name))
self.update_closable()
return dock
def on_dock_closed(self, name):
dock = self.docks[name]
dock.deleteLater()
del self.docks[name]
self.update_closable()
def update_closable(self):
flags = (QtWidgets.QDockWidget.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetFloatable)
if len(self.docks) > 1:
flags |= QtWidgets.QDockWidget.DockWidgetClosable
for dock in self.docks.values():
dock.setFeatures(flags)
def save_state(self):
return {name: dock.save_state() for name, dock in self.docks.items()}
def restore_state(self, state):
if self.docks:
raise NotImplementedError
for name, dock_state in state.items():
dock = _MonInjDock(name, self)
self.docks[name] = dock
dock.restore_state(dock_state)
self.main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock)
dock.sigClosed.connect(partial(self.on_dock_closed, name))
self.update_closable()
def first_moninj_dock(self):
if self.docks:
return None
dock = self.create_new_dock(False)
return dock
async def stop(self): async def stop(self):
if self.dm is not None: if self.dm is not None:

View File

@ -226,8 +226,9 @@ def main():
smgr.register(d_applets) smgr.register(d_applets)
broadcast_clients["ccb"].notify_cbs.append(d_applets.ccb_notify) broadcast_clients["ccb"].notify_cbs.append(d_applets.ccb_notify)
d_ttl_dds = moninj.MonInj(rpc_clients["schedule"]) moninj_mgr = moninj.MonInj(rpc_clients["schedule"], main_window)
atexit_register_coroutine(d_ttl_dds.stop, loop=loop) smgr.register(moninj_mgr)
atexit_register_coroutine(moninj_mgr.stop, loop=loop)
d_waveform = waveform.WaveformDock( d_waveform = waveform.WaveformDock(
args.analyzer_proxy_timeout, args.analyzer_proxy_timeout,
@ -236,13 +237,6 @@ def main():
) )
atexit_register_coroutine(d_waveform.stop, loop=loop) atexit_register_coroutine(d_waveform.stop, loop=loop)
def init_cbs(ddb):
d_ttl_dds.dm.init_ddb(ddb)
d_waveform.init_ddb(ddb)
return ddb
devices_sub = Subscriber("devices", init_cbs, [d_ttl_dds.dm.notify_ddb, d_waveform.notify_ddb])
loop.run_until_complete(devices_sub.connect(args.server, args.port_notify))
atexit_register_coroutine(devices_sub.close, loop=loop)
d_interactive_args = interactive_args.InteractiveArgsDock( d_interactive_args = interactive_args.InteractiveArgsDock(
sub_clients["interactive_args"], sub_clients["interactive_args"],
@ -261,8 +255,8 @@ def main():
# lay out docks # lay out docks
right_docks = [ right_docks = [
d_explorer, d_shortcuts, d_explorer, d_shortcuts,
d_ttl_dds.ttl_dock, d_ttl_dds.dds_dock, d_ttl_dds.dac_dock, d_datasets, d_applets,
d_datasets, d_applets, d_waveform, d_interactive_args d_waveform, d_interactive_args
] ]
main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, right_docks[0]) main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, right_docks[0])
for d1, d2 in zip(right_docks, right_docks[1:]): for d1, d2 in zip(right_docks, right_docks[1:]):
@ -275,18 +269,28 @@ def main():
# Otherwise, the windows of those applets that are in detached # Otherwise, the windows of those applets that are in detached
# QDockWidgets fail to be embedded. # QDockWidgets fail to be embedded.
main_window.show() main_window.show()
smgr.load() smgr.load()
# connect devices_sub after loading state, else moninj widgets may not be created
def init_cbs(ddb):
moninj_mgr.dm.init_ddb(ddb)
d_waveform.init_ddb(ddb)
return ddb
devices_sub = Subscriber("devices", init_cbs, [moninj_mgr.dm.notify_ddb, d_waveform.notify_ddb])
loop.run_until_complete(devices_sub.connect(args.server, args.port_notify))
atexit_register_coroutine(devices_sub.close, loop=loop)
smgr.start(loop=loop) smgr.start(loop=loop)
atexit_register_coroutine(smgr.stop, loop=loop) atexit_register_coroutine(smgr.stop, loop=loop)
# work around for https://github.com/m-labs/artiq/issues/1307
d_ttl_dds.ttl_dock.show()
d_ttl_dds.dds_dock.show()
# create first log dock if not already in state # create first log dock if not already in state
d_log0 = logmgr.first_log_dock() d_log0 = logmgr.first_log_dock()
if d_log0 is not None: if d_log0 is not None:
main_window.tabifyDockWidget(d_schedule, d_log0) main_window.tabifyDockWidget(d_schedule, d_log0)
d_moninj0 = moninj_mgr.first_moninj_dock()
if d_moninj0 is not None:
main_window.tabifyDockWidget(right_docks[-1], d_moninj0)
if server_name is not None: if server_name is not None:
server_description = server_name + " ({})".format(args.server) server_description = server_name + " ({})".format(args.server)

View File

@ -1,4 +1,6 @@
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets, QtGui
from artiq.gui.flowlayout import FlowLayout
class VDragDropSplitter(QtWidgets.QSplitter): class VDragDropSplitter(QtWidgets.QSplitter):
@ -98,3 +100,65 @@ class VDragScrollArea(QtWidgets.QScrollArea):
dy = self._direction * self._speed dy = self._direction * self._speed
new_val = min(max_, max(min_, val + dy)) new_val = min(max_, max(min_, val + dy))
self.verticalScrollBar().setValue(new_val) self.verticalScrollBar().setValue(new_val)
# Widget with FlowLayout and drag and drop support between widgets
class DragDropFlowLayoutWidget(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.layout = FlowLayout()
self.setLayout(self.layout)
self.setAcceptDrops(True)
def _get_index(self, pos):
for i in range(self.layout.count()):
if self.itemAt(i).geometry().contains(pos):
return i
return -1
def mousePressEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton \
and event.modifiers() == QtCore.Qt.ShiftModifier:
index = self._get_index(event.pos())
if index == -1:
return
drag = QtGui.QDrag(self)
mime = QtCore.QMimeData()
mime.setData("index", str(index).encode())
drag.setMimeData(mime)
pixmapi = QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_FileIcon)
drag.setPixmap(pixmapi.pixmap(32))
drag.exec_(QtCore.Qt.MoveAction)
event.accept()
def dragEnterEvent(self, event):
event.accept()
def dropEvent(self, event):
index = self._get_index(event.pos())
source_layout = event.source()
source_index = int(bytes(event.mimeData().data("index")).decode())
if source_layout == self:
if index == source_index:
return
widget = self.layout.itemAt(source_index).widget()
self.layout.removeWidget(widget)
self.layout.addWidget(widget)
self.layout.itemList.insert(index, self.layout.itemList.pop())
else:
widget = source_layout.layout.itemAt(source_index).widget()
source_layout.layout.removeWidget(widget)
self.layout.addWidget(widget)
if index != -1:
self.layout.itemList.insert(index, self.layout.itemList.pop())
event.accept()
def addWidget(self, widget):
self.layout.addWidget(widget)
def count(self):
return self.layout.count()
def itemAt(self, i):
return self.layout.itemAt(i)