from PyQt5 import QtCore from sipyco.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, disconnect_cb=None): ModelManager.__init__(self, model_factory) Subscriber.__init__(self, notifier_name, self._create_model, disconnect_cb=disconnect_cb) 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 ("". 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