artiq/artiq/gui/models.py

419 lines
13 KiB
Python

from PyQt5 import QtCore
from artiq.protocols.sync_struct import Subscriber, process_mod
class ModelManager:
def __init__(self, model_factory):
self.model = None
self._model_factory = model_factory
self._setmodel_callbacks = []
def _create_model(self, init):
self.model = self._model_factory(init)
for cb in self._setmodel_callbacks:
cb(self.model)
return self.model
def add_setmodel_callback(self, cb):
self._setmodel_callbacks.append(cb)
if self.model is not None:
cb(self.model)
class ModelSubscriber(ModelManager, Subscriber):
def __init__(self, notifier_name, model_factory):
ModelManager.__init__(self, model_factory)
Subscriber.__init__(self, notifier_name, self._create_model)
class LocalModelManager(ModelManager):
def __init__(self, model_factory):
ModelManager.__init__(self, model_factory)
self.notify_cbs = []
def update(self, mod):
process_mod(self.model, mod)
for notify_cb in self.notify_cbs:
notify_cb(mod)
def init(self, struct):
self._create_model(struct)
mod = {"action": "init", "struct": struct}
for notify_cb in self.notify_cbs:
notify_cb(mod)
class _SyncSubstruct:
def __init__(self, update_cb, ref):
self.update_cb = update_cb
self.ref = ref
def append(self, x):
self.ref.append(x)
self.update_cb()
def insert(self, i, x):
self.ref.insert(i, x)
self.update_cb()
def pop(self, i=-1):
self.ref.pop(i)
self.update_cb()
def __setitem__(self, key, value):
self.ref[key] = value
self.update_cb()
def __delitem__(self, key):
self.ref.__delitem__(key)
self.update_cb()
def __getitem__(self, key):
return _SyncSubstruct(self.update_cb, self.ref[key])
class DictSyncModel(QtCore.QAbstractTableModel):
def __init__(self, headers, init):
self.headers = headers
self.backing_store = init
self.row_to_key = sorted(
self.backing_store.keys(),
key=lambda k: self.sort_key(k, self.backing_store[k]))
QtCore.QAbstractTableModel.__init__(self)
def rowCount(self, parent):
return len(self.backing_store)
def columnCount(self, parent):
return len(self.headers)
def data(self, index, role):
if not index.isValid() or role != QtCore.Qt.DisplayRole:
return None
else:
k = self.row_to_key[index.row()]
return self.convert(k, self.backing_store[k], index.column())
def headerData(self, col, orientation, role):
if (orientation == QtCore.Qt.Horizontal and
role == QtCore.Qt.DisplayRole):
return self.headers[col]
return None
def _find_row(self, k, v):
lo = 0
hi = len(self.row_to_key)
while lo < hi:
mid = (lo + hi)//2
if (self.sort_key(self.row_to_key[mid],
self.backing_store[self.row_to_key[mid]]) <
self.sort_key(k, v)):
lo = mid + 1
else:
hi = mid
return lo
def __setitem__(self, k, v):
if k in self.backing_store:
old_row = self.row_to_key.index(k)
new_row = self._find_row(k, v)
if old_row == new_row:
self.dataChanged.emit(self.index(old_row, 0),
self.index(old_row, len(self.headers)-1))
else:
self.beginMoveRows(QtCore.QModelIndex(), old_row, old_row,
QtCore.QModelIndex(), new_row)
self.backing_store[k] = v
self.row_to_key[old_row], self.row_to_key[new_row] = \
self.row_to_key[new_row], self.row_to_key[old_row]
if old_row != new_row:
self.endMoveRows()
else:
row = self._find_row(k, v)
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.backing_store[k] = v
self.row_to_key.insert(row, k)
self.endInsertRows()
def __delitem__(self, k):
row = self.row_to_key.index(k)
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
del self.row_to_key[row]
del self.backing_store[k]
self.endRemoveRows()
def __getitem__(self, k):
def update():
self[k] = self.backing_store[k]
return _SyncSubstruct(update, self.backing_store[k])
def sort_key(self, k, v):
raise NotImplementedError
def convert(self, k, v, column):
raise NotImplementedError
class ListSyncModel(QtCore.QAbstractTableModel):
def __init__(self, headers, init):
self.headers = headers
self.backing_store = init
QtCore.QAbstractTableModel.__init__(self)
def rowCount(self, parent):
return len(self.backing_store)
def columnCount(self, parent):
return len(self.headers)
def data(self, index, role):
if not index.isValid() or role != QtCore.Qt.DisplayRole:
return None
else:
return self.convert(self.backing_store[index.row()],
index.column())
def headerData(self, col, orientation, role):
if (orientation == QtCore.Qt.Horizontal and
role == QtCore.Qt.DisplayRole):
return self.headers[col]
return None
def __setitem__(self, k, v):
self.dataChanged.emit(self.index(k, 0),
self.index(k, len(self.headers)-1))
self.backing_store[k] = v
def __delitem__(self, k):
self.beginRemoveRows(QtCore.QModelIndex(), k, k)
del self.backing_store[k]
self.endRemoveRows()
def __getitem__(self, k):
def update():
self[k] = self.backing_store[k]
return _SyncSubstruct(update, self.backing_store[k])
def append(self, v):
row = len(self.backing_store)
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.backing_store.append(v)
self.endInsertRows()
def convert(self, v, column):
raise NotImplementedError
# An item is a node if it has children, a leaf if it does not.
# There can be a node and a leaf with the same name and different
# rows, e.g. foo/bar and foo.
class _DictSyncTreeSepItem:
def __init__(self, parent, row, name):
self.parent = parent
self.row = row
self.name = name
self.children_by_row = []
self.children_nodes_by_name = dict()
self.children_leaves_by_name = dict()
# is_node is permanently set when a child is added.
# This must be done instead of checking for the emptiness of
# children_by_row: in the middle of deletion operations, we remove
# items from children_by_row, and briefly turn nodes into leaves
# before they are removed.
# Of course, Qt sometimes happily calls data() on those invalid leaves,
# resulting in convert() being called for an invalid key if we hadn't
# permanently marked those items as nodes.
self.is_node = False
def __repr__(self):
return ("<DictSyncTreeSepItem {}, row={}, nchildren={}>".
format(self.name, self.row, len(self.children_by_row)))
def _bisect_item(a, name):
lo = 0
hi = len(a)
while lo < hi:
mid = (lo + hi)//2
if name < a[mid].name:
hi = mid
else:
lo = mid + 1
return lo
class DictSyncTreeSepModel(QtCore.QAbstractItemModel):
def __init__(self, separator, headers, init):
QtCore.QAbstractItemModel.__init__(self)
self.separator = separator
self.headers = headers
self.backing_store = dict()
self.children_by_row = []
self.children_nodes_by_name = dict()
self.children_leaves_by_name = dict()
for k, v in init.items():
self[k] = v
def rowCount(self, parent):
if parent.isValid():
item = parent.internalPointer()
return len(item.children_by_row)
else:
return len(self.children_by_row)
def columnCount(self, parent):
return len(self.headers)
def headerData(self, col, orientation, role):
if (orientation == QtCore.Qt.Horizontal and
role == QtCore.Qt.DisplayRole):
return self.headers[col]
return None
def index(self, row, column, parent):
if column >= len(self.headers):
return QtCore.QModelIndex()
if parent.isValid():
parent_item = parent.internalPointer()
try:
child = parent_item.children_by_row[row]
except IndexError:
# This can happen when the last row is selected
# and then deleted; Qt will attempt to select
# the non-existent next one.
return QtCore.QModelIndex()
return self.createIndex(row, column, child)
else:
try:
child = self.children_by_row[row]
except IndexError:
return QtCore.QModelIndex()
return self.createIndex(row, column, child)
def _index_item(self, item):
if item is self:
return QtCore.QModelIndex()
else:
return self.createIndex(item.row, 0, item)
def parent(self, index):
if index.isValid():
return self._index_item(index.internalPointer().parent)
else:
return QtCore.QModelIndex()
def _add_item(self, parent, name, leaf):
if leaf:
name_dict = parent.children_leaves_by_name
else:
name_dict = parent.children_nodes_by_name
if name in name_dict:
return name_dict[name]
row = _bisect_item(parent.children_by_row, name)
item = _DictSyncTreeSepItem(parent, row, name)
self.beginInsertRows(self._index_item(parent), row, row)
parent.is_node = True
parent.children_by_row.insert(row, item)
for next_item in parent.children_by_row[row+1:]:
next_item.row += 1
name_dict[name] = item
self.endInsertRows()
return item
def __setitem__(self, k, v):
*node_names, leaf_name = k.split(self.separator)
if k in self.backing_store:
parent = self
for node_name in node_names:
parent = parent.children_nodes_by_name[node_name]
item = parent.children_leaves_by_name[leaf_name]
index0 = self.createIndex(item.row, 0, item)
index1 = self.createIndex(item.row, len(self.headers)-1, item)
self.backing_store[k] = v
self.dataChanged.emit(index0, index1)
else:
self.backing_store[k] = v
parent = self
for node_name in node_names:
parent = self._add_item(parent, node_name, False)
self._add_item(parent, leaf_name, True)
def _del_item(self, parent, path):
if len(path) == 1:
# leaf
name = path[0]
item = parent.children_leaves_by_name[name]
row = item.row
self.beginRemoveRows(self._index_item(parent), row, row)
del parent.children_leaves_by_name[name]
del parent.children_by_row[row]
for next_item in parent.children_by_row[row:]:
next_item.row -= 1
self.endRemoveRows()
else:
# node
name, *rest = path
item = parent.children_nodes_by_name[name]
self._del_item(item, rest)
if not item.children_by_row:
row = item.row
self.beginRemoveRows(self._index_item(parent), row, row)
del parent.children_nodes_by_name[name]
del parent.children_by_row[row]
for next_item in parent.children_by_row[row:]:
next_item.row -= 1
self.endRemoveRows()
def __delitem__(self, k):
self._del_item(self, k.split(self.separator))
del self.backing_store[k]
def __getitem__(self, k):
def update():
self[k] = self.backing_store[k]
return _SyncSubstruct(update, self.backing_store[k])
def index_to_key(self, index):
item = index.internalPointer()
if item.is_node:
return None
key = item.name
item = item.parent
while item is not self:
key = item.name + self.separator + key
item = item.parent
return key
def data(self, index, role):
if not index.isValid() or (role != QtCore.Qt.DisplayRole
and role != QtCore.Qt.ToolTipRole):
return None
else:
column = index.column()
if column == 0 and role == QtCore.Qt.DisplayRole:
return index.internalPointer().name
else:
key = self.index_to_key(index)
if key is None:
return None
else:
if role == QtCore.Qt.DisplayRole:
convert = self.convert
else:
convert = self.convert_tooltip
return convert(key, self.backing_store[key],
column)
def convert(self, k, v, column):
raise NotImplementedError
def convert_tooltip(self, k, v, column):
return None