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 textwrap
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.ad9912_reg import AD9912_SER_CONF
from artiq.gui.tools import LayoutWidget
from artiq.gui.flowlayout import FlowLayout
from artiq.gui.tools import LayoutWidget, QDockWidgetCloseDetect
from artiq.gui.models import DictSyncTreeSepModel
from artiq.gui.dndwidgets import VDragScrollArea, DragDropFlowLayoutWidget
logger = logging.getLogger(__name__)
@ -25,6 +27,42 @@ class _CancellableLineEdit(QtWidgets.QLineEdit):
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):
override_toggled = QtCore.pyqtSignal(bool)
level_toggled = QtCore.pyqtSignal(bool)
@ -34,6 +72,8 @@ class _TTLWidget(QtWidgets.QFrame):
self.setFrameShape(QtWidgets.QFrame.Box)
self.setFrameShadow(QtWidgets.QFrame.Raised)
self.uid = title
grid = QtWidgets.QGridLayout()
grid.setContentsMargins(0, 0, 0, 0)
grid.setHorizontalSpacing(0)
@ -158,7 +198,10 @@ class _TTLHandler:
self.refresh_display()
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):
@ -172,6 +215,8 @@ class _DDSWidget(QtWidgets.QFrame):
self.setFrameShape(QtWidgets.QFrame.Box)
self.setFrameShadow(QtWidgets.QFrame.Raised)
self.uid = title
grid = QtWidgets.QGridLayout()
grid.setContentsMargins(0, 0, 0, 0)
grid.setHorizontalSpacing(0)
@ -354,7 +399,10 @@ class _DDSHandler:
self.dm.dds_channel_toggle(self.dds_name, sw=False)
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):
@ -364,6 +412,8 @@ class _DACWidget(QtWidgets.QFrame):
self.setFrameShape(QtWidgets.QFrame.Box)
self.setFrameShadow(QtWidgets.QFrame.Raised)
self.uid = (title, channel)
grid = QtWidgets.QGridLayout()
grid.setContentsMargins(0, 0, 0, 0)
grid.setHorizontalSpacing(0)
@ -407,7 +457,62 @@ class _DACHandler:
self.widget.set_value(self.cur_value * 100 / 2**16)
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")
@ -487,12 +592,10 @@ class _DeviceManager:
self.handlers_by_uid = dict()
self.dds_sysclk = 0
self.ttl_cb = lambda: None
self.ttl_handlers = dict()
self.dds_cb = lambda: None
self.dds_handlers = dict()
self.dac_cb = lambda: None
self.dac_handlers = dict()
self.channels_cb = lambda: None
def init_ddb(self, ddb):
self.ddb = ddb
@ -513,17 +616,14 @@ class _DeviceManager:
self.setup_ttl_monitoring(False, handler.channel)
handler.delete_widget()
del self.ttl_handlers[handler.channel]
self.ttl_cb()
elif isinstance(handler, _DDSHandler):
self.setup_dds_monitoring(False, handler.bus_channel, handler.channel)
handler.delete_widget()
del self.dds_handlers[(handler.bus_channel, handler.channel)]
self.dds_cb()
elif isinstance(handler, _DACHandler):
self.setup_dac_monitoring(False, handler.spi_channel, handler.channel)
handler.delete_widget()
del self.dac_handlers[(handler.spi_channel, handler.channel)]
self.dac_cb()
else:
raise ValueError
@ -536,19 +636,19 @@ class _DeviceManager:
if isinstance(handler, _TTLHandler):
self.ttl_handlers[handler.channel] = handler
self.ttl_cb()
self.setup_ttl_monitoring(True, handler.channel)
elif isinstance(handler, _DDSHandler):
self.dds_handlers[(handler.bus_channel, handler.channel)] = handler
self.dds_cb()
self.setup_dds_monitoring(True, handler.bus_channel, handler.channel)
elif isinstance(handler, _DACHandler):
self.dac_handlers[(handler.spi_channel, handler.channel)] = handler
self.dac_cb()
self.setup_dac_monitoring(True, handler.spi_channel, handler.channel)
else:
raise ValueError
if self.description != description:
self.channels_cb()
self.description = description
def ttl_set_mode(self, channel, mode):
@ -787,38 +887,156 @@ class _DeviceManager:
await self.mi_connection.close()
class _MonInjDock(QtWidgets.QDockWidget):
def __init__(self, name):
QtWidgets.QDockWidget.__init__(self, name)
class _MonInjDock(QDockWidgetCloseDetect):
def __init__(self, name, manager):
QtWidgets.QDockWidget.__init__(self, "MonInj")
self.setObjectName(name)
self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
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):
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()):
grid.addWidget(handler.widget)
self.flow.addWidget(handler.widget)
scroll_area.setWidgetResizable(True)
scroll_area.setWidget(grid_widget)
def restore_widgets(self):
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:
def __init__(self, schedule_ctl):
self.ttl_dock = _MonInjDock("TTL")
self.dds_dock = _MonInjDock("DDS")
self.dac_dock = _MonInjDock("DAC")
def __init__(self, schedule_ctl, main_window):
self.docks = dict()
self.main_window = main_window
self.dm = _DeviceManager(schedule_ctl)
self.dm.ttl_cb = lambda: self.ttl_dock.layout_widgets(self.dm.ttl_handlers.values())
self.dm.dds_cb = lambda: self.dds_dock.layout_widgets(self.dm.dds_handlers.values())
self.dm.dac_cb = lambda: self.dac_dock.layout_widgets(self.dm.dac_handlers.values())
self.dm.channels_cb = self.add_channels
self.channel_model = Model({})
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):
if self.dm is not None:

View File

@ -226,8 +226,9 @@ def main():
smgr.register(d_applets)
broadcast_clients["ccb"].notify_cbs.append(d_applets.ccb_notify)
d_ttl_dds = moninj.MonInj(rpc_clients["schedule"])
atexit_register_coroutine(d_ttl_dds.stop, loop=loop)
moninj_mgr = moninj.MonInj(rpc_clients["schedule"], main_window)
smgr.register(moninj_mgr)
atexit_register_coroutine(moninj_mgr.stop, loop=loop)
d_waveform = waveform.WaveformDock(
args.analyzer_proxy_timeout,
@ -236,13 +237,6 @@ def main():
)
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(
sub_clients["interactive_args"],
@ -261,8 +255,8 @@ def main():
# lay out docks
right_docks = [
d_explorer, d_shortcuts,
d_ttl_dds.ttl_dock, d_ttl_dds.dds_dock, d_ttl_dds.dac_dock,
d_datasets, d_applets, d_waveform, d_interactive_args
d_datasets, d_applets,
d_waveform, d_interactive_args
]
main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, right_docks[0])
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
# QDockWidgets fail to be embedded.
main_window.show()
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)
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
d_log0 = logmgr.first_log_dock()
if d_log0 is not None:
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:
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):
@ -98,3 +100,65 @@ class VDragScrollArea(QtWidgets.QScrollArea):
dy = self._direction * self._speed
new_val = min(max_, max(min_, val + dy))
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)