forked from M-Labs/artiq
1
0
Fork 0

Compare commits

...

10 Commits

4 changed files with 333 additions and 33 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
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, DoubleClickLineEdit
from artiq.gui.flowlayout import FlowLayout from artiq.gui.dndwidgets import VDragScrollArea, DragDropFlowLayoutWidget
from artiq.gui.models import DictSyncTreeSepModel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -149,6 +151,9 @@ class _TTLWidget(QtWidgets.QFrame):
def uid(self): def uid(self):
return self.title return self.title
def to_model_path(self):
return "ttl/{}".format(self.title)
class _DDSModel: class _DDSModel:
def __init__(self, dds_type, ref_clk, cpld=None, pll=1, clk_div=0): def __init__(self, dds_type, ref_clk, cpld=None, pll=1, clk_div=0):
@ -322,6 +327,9 @@ class _DDSWidget(QtWidgets.QFrame):
def uid(self): def uid(self):
return self.title return self.title
def to_model_path(self):
return "dds/{}".format(self.title)
class _DACWidget(QtWidgets.QFrame): class _DACWidget(QtWidgets.QFrame):
def __init__(self, dm, spi_channel, channel, title): def __init__(self, dm, spi_channel, channel, title):
@ -363,6 +371,9 @@ class _DACWidget(QtWidgets.QFrame):
def uid(self): def uid(self):
return (self.title, self.channel) return (self.title, self.channel)
def to_model_path(self):
return "dac/{} ch{}".format(self.title, self.channel)
_WidgetDesc = namedtuple("_WidgetDesc", "uid comment cls arguments") _WidgetDesc = namedtuple("_WidgetDesc", "uid comment cls arguments")
@ -735,34 +746,223 @@ class _DeviceManager:
await self.mi_connection.close() await self.mi_connection.close()
class _MonInjDock(QtWidgets.QDockWidget): class Model(DictSyncTreeSepModel):
def __init__(self, name): def __init__(self, init):
QtWidgets.QDockWidget.__init__(self, name) 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()
class _MonInjDock(QDockWidgetCloseDetect):
def __init__(self, name, manager):
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)
grid = LayoutWidget()
self.setWidget(grid)
self.manager = manager
self.widget_uids = None
newdock = QtWidgets.QToolButton()
newdock.setToolTip("Create new moninj dock")
newdock.setIcon(QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_FileDialogNewFolder))
newdock.clicked.connect(lambda: self.manager.create_new_dock())
grid.addWidget(newdock, 0, 0)
self.channel_dialog = _AddChannelDialog(self, self.manager.channel_model)
self.channel_dialog.accepted.connect(self.add_channels)
dialog_btn = QtWidgets.QToolButton()
dialog_btn.setToolTip("Add channels")
dialog_btn.setIcon(
QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_FileDialogListView))
dialog_btn.clicked.connect(self.channel_dialog.open)
grid.addWidget(dialog_btn, 0, 1)
self.label = DoubleClickLineEdit(name)
grid.addWidget(self.label, 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)
self.flow.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.flow.customContextMenuRequested.connect(self.custom_context_menu)
def custom_context_menu(self, pos):
index = self.flow._get_index(pos)
if index == -1:
return
menu = QtWidgets.QMenu()
delete_action = QtWidgets.QAction("Delete widget", menu)
delete_action.triggered.connect(partial(self.delete_widget, index))
menu.addAction(delete_action)
menu.exec_(self.flow.mapToGlobal(pos))
def delete_widget(self, index, checked):
widget = self.flow.itemAt(index).widget()
widget.hide()
self.flow.layout.takeAt(index)
def add_channels(self):
channels = self.channel_dialog.channels
self.layout_widgets(channels)
def layout_widgets(self, widgets): def layout_widgets(self, widgets):
scroll_area = QtWidgets.QScrollArea()
self.setWidget(scroll_area)
grid = FlowLayout()
grid_widget = QtWidgets.QWidget()
grid_widget.setLayout(grid)
for widget in sorted(widgets, key=lambda w: w.sort_key()): for widget in sorted(widgets, key=lambda w: w.sort_key()):
grid.addWidget(widget) widget.show()
self.flow.addWidget(widget)
scroll_area.setWidgetResizable(True) def restore_widgets(self):
scroll_area.setWidget(grid_widget) if self.widget_uids is not None:
widgets_by_uid = self.manager.dm.widgets_by_uid
widgets = list()
for uid in self.widget_uids:
if uid in widgets_by_uid:
widgets.append(widgets_by_uid[uid])
else:
logger.warning("removing moninj widget {}".format(uid))
self.layout_widgets(widgets)
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 {
"dock_label": self.label.text(),
"widget_uids": self._save_widget_uids()
}
def restore_state(self, state):
try:
label = state["dock_label"]
except KeyError:
pass
else:
self.label._text = label
self.label.setText(label)
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.dock = _MonInjDock("MonInj") self.docks = dict()
self.main_window = main_window
self.dm = _DeviceManager(schedule_ctl) self.dm = _DeviceManager(schedule_ctl)
self.dm.channels_cb = lambda: self.dock.layout_widgets(self.dm.widgets_by_uid.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.widgets_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.hide() # dock may be parent, only delete on exit
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 first_moninj_dock(self):
if self.docks:
return None
dock = self.create_new_dock(False)
return dock
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()
async def stop(self): async def stop(self):
if self.dm is not None: if self.dm is not None:

View File

@ -226,7 +226,8 @@ 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"]) d_ttl_dds = moninj.MonInj(rpc_clients["schedule"], main_window)
smgr.register(d_ttl_dds)
atexit_register_coroutine(d_ttl_dds.stop, loop=loop) atexit_register_coroutine(d_ttl_dds.stop, loop=loop)
d_waveform = waveform.WaveformDock( d_waveform = waveform.WaveformDock(
@ -236,14 +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"],
rpc_clients["interactive_arg_db"] rpc_clients["interactive_arg_db"]
@ -261,7 +254,7 @@ def main():
# lay out docks # lay out docks
right_docks = [ right_docks = [
d_explorer, d_shortcuts, d_explorer, d_shortcuts,
d_ttl_dds.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])
@ -276,16 +269,25 @@ def main():
# QDockWidgets fail to be embedded. # QDockWidgets fail to be embedded.
main_window.show() main_window.show()
smgr.load() smgr.load()
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)
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.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 = d_ttl_dds.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)

View File

@ -4,6 +4,40 @@ import logging
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
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)
def log_level_to_name(level): def log_level_to_name(level):
if level >= logging.CRITICAL: if level >= logging.CRITICAL:
return "CRITICAL" return "CRITICAL"