artiq/artiq/gui/applets.py

353 lines
13 KiB
Python
Raw Normal View History

2016-01-07 20:23:17 +08:00
import logging
import asyncio
2016-01-16 01:15:06 +08:00
import sys
import shlex
from functools import partial
2016-01-07 20:23:17 +08:00
2016-02-15 07:23:47 +08:00
from PyQt5 import QtCore, QtGui, QtWidgets
2016-01-07 20:23:17 +08:00
2016-02-08 16:59:15 +08:00
from artiq.protocols.pipe_ipc import AsyncioParentComm
2016-02-08 21:35:37 +08:00
from artiq.protocols import pyon
2016-02-15 05:22:07 +08:00
from artiq.gui.tools import QDockWidgetCloseDetect
2016-02-08 16:59:15 +08:00
2016-01-07 20:23:17 +08:00
logger = logging.getLogger(__name__)
2016-02-08 16:59:15 +08:00
class AppletIPCServer(AsyncioParentComm):
2016-02-09 02:20:07 +08:00
def __init__(self, datasets_sub):
AsyncioParentComm.__init__(self)
self.datasets_sub = datasets_sub
self.datasets = set()
2016-02-08 16:59:15 +08:00
def write_pyon(self, obj):
self.write(pyon.encode(obj).encode() + b"\n")
async def read_pyon(self):
line = await self.readline()
return pyon.decode(line.decode())
2016-02-09 02:20:07 +08:00
def _synthesize_init(self, data):
struct = {k: v for k, v in data.items() if k in self.datasets}
return {"action": "init",
"struct": struct}
def _on_mod(self, mod):
if mod["action"] == "init":
mod = self._synthesize_init(mod["struct"])
else:
if mod["path"]:
if mod["path"][0] not in self.datasets:
return
elif mod["action"] in {"setitem", "delitem"}:
if mod["key"] not in self.datasets:
return
self.write_pyon({"action": "mod", "mod": mod})
async def serve(self, embed_cb, fix_initial_size_cb):
2016-02-09 02:20:07 +08:00
self.datasets_sub.notify_cbs.append(self._on_mod)
2016-02-08 21:35:37 +08:00
try:
while True:
obj = await self.read_pyon()
try:
action = obj["action"]
if action == "embed":
embed_cb(obj["win_id"])
self.write_pyon({"action": "embed_done"})
elif action == "fix_initial_size":
fix_initial_size_cb()
2016-02-08 21:35:37 +08:00
elif action == "subscribe":
2016-02-09 02:20:07 +08:00
self.datasets = obj["datasets"]
if self.datasets_sub.model is not None:
mod = self._synthesize_init(
self.datasets_sub.model.backing_store)
self.write_pyon({"action": "mod", "mod": mod})
2016-02-08 21:35:37 +08:00
else:
2016-02-09 02:23:50 +08:00
raise ValueError("unknown action in applet message")
2016-02-08 21:35:37 +08:00
except:
2016-02-09 02:23:50 +08:00
logger.warning("error processing applet message",
2016-02-08 21:35:37 +08:00
exc_info=True)
self.write_pyon({"action": "error"})
except asyncio.CancelledError:
pass
except:
logger.error("error processing data from applet, "
"server stopped", exc_info=True)
2016-02-09 02:20:07 +08:00
finally:
self.datasets_sub.notify_cbs.remove(self._on_mod)
2016-02-08 21:35:37 +08:00
def start(self, embed_cb, fix_initial_size_cb):
self.server_task = asyncio.ensure_future(
self.serve(embed_cb, fix_initial_size_cb))
2016-02-08 21:35:37 +08:00
async def stop(self):
self.server_task.cancel()
await asyncio.wait([self.server_task])
2016-02-08 16:59:15 +08:00
2016-02-15 05:22:07 +08:00
class AppletDock(QDockWidgetCloseDetect):
2016-02-09 02:20:07 +08:00
def __init__(self, datasets_sub, uid, name, command):
2016-02-15 05:22:07 +08:00
QDockWidgetCloseDetect.__init__(self, "Applet: " + name)
self.setObjectName("applet" + str(uid))
self.setMinimumSize(QtCore.QSize(100, 100))
2016-02-09 02:20:07 +08:00
self.datasets_sub = datasets_sub
self.applet_name = name
self.command = command
self.starting_stopping = False
def rename(self, name):
self.applet_name = name
self.setWindowTitle("Applet: " + name)
async def start(self):
if self.starting_stopping:
return
self.starting_stopping = True
2016-02-09 02:20:07 +08:00
self.ipc = AppletIPCServer(self.datasets_sub)
if "{ipc_address}" not in self.command:
logger.warning("IPC address missing from command for %s",
self.applet_name)
command = self.command.format(
python=sys.executable.replace("\\", "\\\\"),
ipc_address=self.ipc.get_address().replace("\\", "\\\\")
)
logger.debug("starting command %s for %s", command, self.applet_name)
try:
await self.ipc.create_subprocess(*shlex.split(command),
start_new_session=True)
except:
logger.warning("Applet %s failed to start", self.applet_name,
exc_info=True)
self.ipc.start(self.embed, self.fix_initial_size)
2016-01-07 20:23:17 +08:00
self.starting_stopping = False
2016-02-08 21:35:37 +08:00
def embed(self, win_id):
logger.debug("capturing window 0x%x for %s", win_id, self.applet_name)
self.embed_window = QtGui.QWindow.fromWinId(win_id)
self.embed_widget = QtWidgets.QWidget.createWindowContainer(
self.embed_window)
self.setWidget(self.embed_widget)
2016-01-07 20:23:17 +08:00
# HACK: This function would not be needed if Qt window embedding
# worked correctly.
def fix_initial_size(self):
self.embed_window.resize(self.embed_widget.size())
async def terminate(self):
if self.starting_stopping:
return
self.starting_stopping = True
2016-02-08 21:35:37 +08:00
if hasattr(self, "ipc"):
await self.ipc.stop()
self.ipc.write_pyon({"action": "terminate"})
try:
2016-02-08 16:59:15 +08:00
await asyncio.wait_for(self.ipc.process.wait(), 2.0)
except:
logger.warning("Applet %s failed to exit, killing",
self.applet_name)
try:
2016-02-08 16:59:15 +08:00
self.ipc.process.kill()
except ProcessLookupError:
pass
2016-02-08 16:59:15 +08:00
await self.ipc.process.wait()
del self.ipc
if hasattr(self, "embed_widget"):
self.embed_widget.deleteLater()
del self.embed_widget
self.starting_stopping = False
async def restart(self):
await self.terminate()
await self.start()
2016-01-07 20:23:17 +08:00
2016-01-16 01:15:06 +08:00
_templates = [
("Big number", "{python} -m artiq.applets.big_number "
2016-02-09 05:25:02 +08:00
"--embed {ipc_address} NUMBER_DATASET"),
2016-01-16 01:15:06 +08:00
("Histogram", "{python} -m artiq.applets.plot_hist "
2016-02-09 05:25:02 +08:00
"--embed {ipc_address} COUNTS_DATASET "
2016-01-16 01:15:06 +08:00
"--x BIN_BOUNDARIES_DATASET"),
("XY", "{python} -m artiq.applets.plot_xy "
2016-02-09 05:25:02 +08:00
"--embed {ipc_address} Y_DATASET --x X_DATASET "
2016-01-16 01:15:06 +08:00
"--error ERROR_DATASET --fit FIT_DATASET"),
("XY + Histogram", "{python} -m artiq.applets.plot_xy_hist "
2016-02-09 05:25:02 +08:00
"--embed {ipc_address} X_DATASET "
2016-01-16 01:15:06 +08:00
"HIST_BIN_BOUNDARIES_DATASET "
"HISTS_COUNTS_DATASET"),
]
class AppletsDock(QtWidgets.QDockWidget):
def __init__(self, main_window, datasets_sub):
QtWidgets.QDockWidget.__init__(self, "Applets")
2016-02-14 20:46:15 +08:00
self.setObjectName("Applets")
self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetFloatable)
self.main_window = main_window
2016-02-09 02:20:07 +08:00
self.datasets_sub = datasets_sub
2016-02-08 16:59:15 +08:00
self.dock_to_checkbox = dict()
self.applet_uids = set()
2016-01-07 20:23:17 +08:00
self.table = QtWidgets.QTableWidget(0, 3)
self.table.setHorizontalHeaderLabels(["Enable", "Name", "Command"])
2016-02-15 07:23:47 +08:00
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
2016-01-07 20:23:17 +08:00
self.table.horizontalHeader().setStretchLastSection(True)
2016-02-15 07:23:47 +08:00
self.table.horizontalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeToContents)
self.table.verticalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeToContents)
2016-01-07 20:23:17 +08:00
self.table.verticalHeader().hide()
self.table.setTextElideMode(QtCore.Qt.ElideNone)
self.setWidget(self.table)
2016-01-07 20:23:17 +08:00
self.table.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
2016-02-15 07:23:47 +08:00
new_action = QtWidgets.QAction("New applet", self.table)
2016-01-07 20:23:17 +08:00
new_action.triggered.connect(self.new)
self.table.addAction(new_action)
2016-02-15 07:23:47 +08:00
templates_menu = QtWidgets.QMenu()
2016-01-16 01:15:06 +08:00
for name, template in _templates:
2016-02-15 07:23:47 +08:00
action = QtWidgets.QAction(name, self.table)
2016-01-16 01:15:06 +08:00
action.triggered.connect(partial(self.new_template, template))
templates_menu.addAction(action)
2016-02-15 07:23:47 +08:00
restart_action = QtWidgets.QAction("New applet from template", self.table)
2016-01-16 01:15:06 +08:00
restart_action.setMenu(templates_menu)
self.table.addAction(restart_action)
2016-02-15 07:23:47 +08:00
restart_action = QtWidgets.QAction("Restart selected applet", self.table)
2016-01-17 04:12:16 +08:00
restart_action.setShortcut("CTRL+R")
restart_action.setShortcutContext(QtCore.Qt.WidgetShortcut)
restart_action.triggered.connect(self.restart)
2016-01-07 20:23:17 +08:00
self.table.addAction(restart_action)
2016-02-15 07:23:47 +08:00
delete_action = QtWidgets.QAction("Delete selected applet", self.table)
2016-01-17 04:12:16 +08:00
delete_action.setShortcut("DELETE")
delete_action.setShortcutContext(QtCore.Qt.WidgetShortcut)
2016-01-07 20:23:17 +08:00
delete_action.triggered.connect(self.delete)
self.table.addAction(delete_action)
self.table.cellChanged.connect(self.cell_changed)
def create(self, uid, name, command):
2016-02-09 02:20:07 +08:00
dock = AppletDock(self.datasets_sub, uid, name, command)
self.main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock)
dock.setFloating(True)
2016-02-08 16:59:15 +08:00
asyncio.ensure_future(dock.start())
dock.sigClosed.connect(partial(self.on_dock_closed, dock))
return dock
2016-01-07 20:23:17 +08:00
def cell_changed(self, row, column):
if column == 0:
item = self.table.item(row, column)
if item.checkState() == QtCore.Qt.Checked:
command = self.table.item(row, 2)
if command:
command = command.text()
name = self.table.item(row, 1)
if name is None:
name = ""
else:
name = name.text()
dock = self.create(item.applet_uid, name, command)
2016-02-08 16:59:15 +08:00
item.applet_dock = dock
self.dock_to_checkbox[dock] = item
2016-01-07 20:23:17 +08:00
else:
dock = item.applet_dock
2016-02-08 16:59:15 +08:00
if dock is not None:
# This calls self.on_dock_closed
dock.close()
elif column == 1 or column == 2:
new_value = self.table.item(row, column).text()
dock = self.table.item(row, 0).applet_dock
2016-02-08 16:59:15 +08:00
if dock is not None:
if column == 1:
2016-02-08 16:59:15 +08:00
dock.rename(new_value)
else:
2016-02-08 16:59:15 +08:00
dock.command = new_value
2016-02-08 16:59:15 +08:00
def on_dock_closed(self, dock):
asyncio.ensure_future(dock.terminate())
checkbox_item = self.dock_to_checkbox[dock]
checkbox_item.applet_dock = None
del self.dock_to_checkbox[dock]
checkbox_item.setCheckState(QtCore.Qt.Unchecked)
2016-01-07 20:23:17 +08:00
2016-02-09 04:57:07 +08:00
def new(self, uid=None):
if uid is None:
uid = next(iter(set(range(len(self.applet_uids) + 1))
- self.applet_uids))
self.applet_uids.add(uid)
2016-01-07 20:23:17 +08:00
row = self.table.rowCount()
self.table.insertRow(row)
checkbox = QtWidgets.QTableWidgetItem()
checkbox.setFlags(QtCore.Qt.ItemIsSelectable |
QtCore.Qt.ItemIsUserCheckable |
QtCore.Qt.ItemIsEnabled)
checkbox.setCheckState(QtCore.Qt.Unchecked)
checkbox.applet_uid = uid
checkbox.applet_dock = None
2016-01-07 20:23:17 +08:00
self.table.setItem(row, 0, checkbox)
2016-01-16 01:15:06 +08:00
self.table.setItem(row, 1, QtWidgets.QTableWidgetItem())
self.table.setItem(row, 2, QtWidgets.QTableWidgetItem())
2016-01-09 19:29:20 +08:00
return row
2016-01-07 20:23:17 +08:00
2016-01-16 01:15:06 +08:00
def new_template(self, template):
row = self.new()
self.table.item(row, 2).setText(template)
def restart(self):
selection = self.table.selectedRanges()
if selection:
row = selection[0].topRow()
dock = self.table.item(row, 0).applet_dock
2016-02-08 16:59:15 +08:00
if dock is not None:
asyncio.ensure_future(dock.restart())
2016-01-07 20:23:17 +08:00
def delete(self):
selection = self.table.selectedRanges()
if selection:
row = selection[0].topRow()
item = self.table.item(row, 0)
dock = item.applet_dock
2016-02-08 16:59:15 +08:00
if dock is not None:
# This calls self.on_dock_closed
dock.close()
self.applet_uids.remove(item.applet_uid)
2016-01-16 01:11:13 +08:00
self.table.removeRow(row)
2016-01-07 20:23:17 +08:00
2016-02-08 16:59:15 +08:00
async def stop(self):
for row in range(self.table.rowCount()):
dock = self.table.item(row, 0).applet_dock
2016-02-08 16:59:15 +08:00
if dock is not None:
await dock.terminate()
2016-01-09 19:29:20 +08:00
def save_state(self):
state = []
for row in range(self.table.rowCount()):
2016-02-09 04:57:07 +08:00
uid = self.table.item(row, 0).applet_uid
2016-01-09 19:29:20 +08:00
enabled = self.table.item(row, 0).checkState() == QtCore.Qt.Checked
name = self.table.item(row, 1).text()
command = self.table.item(row, 2).text()
2016-02-09 04:57:07 +08:00
state.append((uid, enabled, name, command))
2016-01-09 19:29:20 +08:00
return state
def restore_state(self, state):
2016-02-09 04:57:07 +08:00
for uid, enabled, name, command in state:
row = self.new(uid)
2016-01-09 19:29:20 +08:00
item = QtWidgets.QTableWidgetItem()
item.setText(name)
self.table.setItem(row, 1, item)
item = QtWidgets.QTableWidgetItem()
item.setText(command)
self.table.setItem(row, 2, item)
if enabled:
self.table.item(row, 0).setCheckState(QtCore.Qt.Checked)