class DeltaEmittingMixin: """base class mixin for objects emitting deltas when their state changes 2 signals: - deltaEmitted : fired when a new semantic delta is added - up to user to define what counts here - stateChanged : fired when the absolute final state of object changes, as result of delta being applied or rolled back, or anything else suggested to also emit a representative delta of the final change (though these deltas should not be tracked or captured) """ def __init__(self): self.deltaEmitted = Signal(name="deltaEmitted") self.stateChanged = Signal(name="stateChanged") # def applyDelta(self, delta:DeltaAtom): # """FOR NOW I'm leaving the behaviour to apply / revert deltas in the Delta # objects themselves - I think it should be defined by compatible # classes in this method, but the only real advantage we get out of that # (aside from reducing code fragmentation) is the ability to reuse delta objects # across multiple types of objects (just be interpreting them differently) - # but only if this interpretation has to be MORE different across objects than # can be captured in the Delta doDelta() method anyway""" # pass def emitDelta(self, delta:DeltaAtom, apply=True): self.deltaEmitted.emit(delta) if apply: delta.doDelta(self)
class DataRef(object): """ wrapper for consistent references to primitive data types """ def __init__(self, val): self._val = val self.onDataSet = Signal(name="onDataSet") def __repr__(self): return self._val def __call__(self, *args, **kwargs): """set new value for wrapper""" if args: self._val = args[0] return self.value() def __str__(self): return str(self._val) def __bool__(self): return bool(self._val) def value(self): return self._val def set(self, newVal): if newVal == self._val: return oldVal = self._val self._val = newVal self.onDataSet.emit(oldVal, newVal)
def __init__(self, instance, name:str, defaultValue, desc=""): super(ToolFieldBase, self).__init__(instance) self.name = name self.desc = desc self.defaultValue = defaultValue # self.valueProxy = ToolFieldProxy.getProxy(self.baseValue) self.valueChanged = Signal() self.valueChanged.connect(self.onValueChanged) self.valueProxy = self.newProxy()
def __init__(self, transactionCls=Transaction): self._transactionCls = transactionCls self._currentTransaction: transactionCls = None # integer count to track current frames - # can only ever have 1 master top-level transaction open self._transactionFrameDepth = 0 self.transactionOpened = Signal() self.transactionClosed = Signal()
class ToolFieldBase(InstanceDescriptorInterface): """uses descriptor syntax but creates and stores dynamic attributes on the instance object I didn't want to make it this complicated I swear really, really suffering to make this instance specific signals need to be per-instance as well """ def __init__(self, instance, name:str, defaultValue, desc=""): super(ToolFieldBase, self).__init__(instance) self.name = name self.desc = desc self.defaultValue = defaultValue # self.valueProxy = ToolFieldProxy.getProxy(self.baseValue) self.valueChanged = Signal() self.valueChanged.connect(self.onValueChanged) self.valueProxy = self.newProxy() def newProxy(self)->ToolFieldProxy: """create a new proxy object and connect it to this object's signals""" valueProxy: ToolFieldProxy = ToolFieldProxy.getProxy( copy.deepcopy(self.defaultValue)) valueProxy._proxyData["objectChanged"].connect(self.valueChanged) return valueProxy def delegatedGet(self, instance, owner): return self.valueProxy def delegatedSet(self, instance, value): self.setValue(value) # def __get__(self, instance, objType=None): # """returns instance-specific proxy managed by this # tool field""" # return self._getInstanceProxy(instance) # # def __set__(self, instance, value): # """if attribute is directly set, update # value and proxy references""" # self.setValue(value) def setValue(self, value): """only emit changed value if value has actually changed""" oldValue = copy.deepcopy(self.valueProxy._proxyResult()) self.valueProxy._proxyLink.setProxyTarget(value) if value != oldValue: self.valueChanged.emit() def onValueChanged(self, *args, **kwargs): """fired when core object is modified""" print("tool field value changed")
def __init__(self, name: str, value=None): # signals emitting DeltaAtom objects self.deltasChanged = Signal() # emitted on caller request self.stateChanged = Signal( ) # emitted when absolute final result of tree changes self.gatheringDeltas = True # internal tree attrs self._name = name self._value = value self._parent = None
def __init__(self, parent=None, nameDelegateCls=TreeNameDelegate, valueDelegateCls=TreeValueDelegate, contextMenu: TreeMenu = None, showValues=True, showRoot=False, scanForWidgets=False): super(TreeView, self).__init__(parent) self.root = None self.nameDelegateCls = nameDelegateCls self.valueDelegateCls = valueDelegateCls self.highlights = {} # dict of tree addresses to highlight self.valuesShown = showValues self.showRoot = showRoot self.scanForWidgets = scanForWidgets # either pass this at init or assign dynamically in subclasses self.context = contextMenu self.contentChanged = Signal() self.sizeChanged = Signal() self.currentSelected = None self.editedIndex = None # stored on editing self.scrollPos = self.verticalScrollBar().value() self.actions = {} # set delegate classes self.setItemDelegateForColumn(0, self.nameDelegateCls(self)) self.setItemDelegateForColumn(1, self.valueDelegateCls(self)) # convenience wrappers self.keyState = KeyState() self.sel = SelectionModelContainer(self.selectionModel(), parent=self) # internal stores for drawing overrides self._liveInputRoots = weakref.WeakSet() self._deltaTrackingRoots = weakref.WeakSet() self._deltaCreatedBranches = weakref.WeakSet() self._deltaModifiedBranches = weakref.WeakSet() self._deltaDeletedBranches = weakref.WeakSet() self._widgetBranches = weakref.WeakKeyDictionary() self.savedSelectedTrees = [] self.savedExpandedTrees = [] self.makeBehaviour() self.makeAppearance() self.makeConnections() self.makeBaseActions() self.expandAll() self.showValues(showValues)
class DeltaTracker: """base class for component tracking atomic deltas of the format shown above""" def __init__(self): self._deltaStack : T.List[DeltaAtom] = [] self._deltaIndex = -1 # which is "current" active delta self.deltasChanged = Signal(name="deltasChanged") @property def deltaStack(self)->T.Tuple[DeltaAtom]: """return read-only view of deltas""" return tuple(self._deltaStack) @property def currentDelta(self)->DeltaAtom: return self.deltaStack[self._deltaIndex] def addDelta(self, delta:DeltaAtom): """append delta to the stack if index is not at last, discard deltas beyond it""" self._deltaStack = self._deltaStack[ :len(self._deltaStack) + self._deltaIndex + 1] self._deltaStack.append(delta) self._deltaIndex = -1 self.deltasChanged.emit() def applyDeltas(self, targetObj): """apply contained deltas to the target object up to current delta index""" for i in self.deltaStack[ :len(self._deltaStack) + self._deltaIndex + 1]: i.doDelta(targetObj, ) def clearDeltas(self): self._deltaStack.clear() self.deltasChanged.emit() def undo(self, targetObj, moveIndex=True): """roll back a single delta""" self.deltaStack[self._deltaIndex].undoDelta(targetObj) if moveIndex: self._deltaIndex = self._deltaIndex - 1 self.deltasChanged.emit() def redo(self, targetObj, moveIndex=True): """reapply a single delta""" redoIndex = self._deltaIndex + 1 if redoIndex >= len(self.deltaStack): raise RuntimeError("no deltas left to redo") self.deltaStack[self._deltaIndex + 1].doDelta(targetObj) if moveIndex: self._deltaIndex = self._deltaIndex + 1 self.deltasChanged.emit()
def __init__(self, baseTree: "Tree", uid=""): super(TransformChain, self).__init__(uid) self.baseTree: "Tree" = None self.transformers: Ty.List[Transformation] = [] self.dirty = True # self.deltaTracker : TreeDeltaTracker = None self._resultTree = None self.resultChanged = Signal() self.markedDirty = Signal() self.transformChanged = Signal() self.transformChanged.connect(self.onTransformChanged) self.setBaseTree(baseTree)
class TransformChain(UidElement): """holds a sequence of transforms to apply to a copy of the base object here it's a tree all the messing around with coredata is dumb - just operate on trees and match() the result SPECIAL CASE delta transform, always comes last in chain """ uidInstanceMap: Ty.Dict[str, "TransformChain"] = {} def __init__(self, baseTree: "Tree", uid=""): super(TransformChain, self).__init__(uid) self.baseTree: "Tree" = None self.transformers: Ty.List[Transformation] = [] self.dirty = True # self.deltaTracker : TreeDeltaTracker = None self._resultTree = None self.resultChanged = Signal() self.markedDirty = Signal() self.transformChanged = Signal() self.transformChanged.connect(self.onTransformChanged) self.setBaseTree(baseTree) def addTransform(self, transformObj: Transformation): self.transformers.append(transformObj) def removeTransform(self, transformObj: Transformation): self.transformers.remove(transformObj) def setBaseTree(self, baseTree: "Tree"): self.baseTree = baseTree self.onBaseTreeChanged() def setDirty(self, state=True): self.dirty = state self.markedDirty.emit() def onBaseTreeChanged(self, *args, **kwargs): """very inefficient for now, only need apply when struture changes""" for branch in self.baseTree.allBranches(includeSelf=True): for i in branch.signals(): i.connect(self.onBaseTreeChanged) self.setDirty(True) def onTransformChanged(self, *args, **kwargs): self.setDirty(True) def deltaTransforms(self): deltaTransforms = [ i for i in self.transformers if isinstance(i, DeltaTransformation) ] return deltaTransforms def getDeltaTransform(self): """if no tracker is found, create one as a transform look up the latest one in the chain """ if not self.deltaTransforms(): tf = DeltaTransformation() self.addTransform(tf) tf.deltasChanged.connect(self.transformChanged) return self.deltaTransforms()[-1] def resultTree(self) -> "Tree": """return result tree of all known transforms""" if self.dirty or self._resultTree is None: #resultTree = self.baseTree.copy() resultTree = self.baseTree._copyAndXorUids(self.baseTree, saltUid=self.uid, andCoreData=True) resultTree.muteSignals(recursive=True) self.markedDirty.mute() # apply transform objects for i in self.transformers: resultTree = i.transformTree(resultTree) assert isinstance(resultTree, TreeBase), \ f"Incorrect type {resultTree} returned from transformation {i}" self._resultTree = resultTree self.dirty = False resultTree.activateSignals(recursive=True) self.markedDirty.activate() self.resultChanged.emit() return self._resultTree
class TreeInterface(TreeBase): keyType = (str, T.Sequence[str]) separatorChar = "/" # string char allowing splitting string keys parentChar = "^" # string char denoting direct parent # behaviour for generating branches branchesInheritType = False StructureEvents = StructureEvents LookupMode = LookupMode # properties and descriptors TreePropertyDescriptor = TreePropertyDescriptor TreeBranchDescriptor = TreeBranchDescriptor # strings used for properties LOOKUP_CREATE_KEY = "_lookupCreate" DESCRIPTION_KEY = "_desc" OPTION_KEY = "_options" READ_ONLY_KEY = "_readOnly" DEFAULT_PROP_KEY = "_default" default = TreePropertyDescriptor(DEFAULT_PROP_KEY, default=None, inherited=True) lookupCreate = TreePropertyDescriptor(LOOKUP_CREATE_KEY, default=False, inherited=True) description = TreePropertyDescriptor(DESCRIPTION_KEY, default="", inherited=False) readOnly = TreePropertyDescriptor(OPTION_KEY, default=False, inherited=True) # discrete values for tree, like enum, True/False, etc options: optionType = TreePropertyDescriptor(READ_ONLY_KEY, default=False, inherited=False) def __init__(self, name: str, value=None): # signals emitting DeltaAtom objects self.deltasChanged = Signal() # emitted on caller request self.stateChanged = Signal( ) # emitted when absolute final result of tree changes self.gatheringDeltas = True # internal tree attrs self._name = name self._value = value self._parent = None def signals(self): return (self.deltasChanged, self.stateChanged) @classmethod def defaultBranchCls(cls): """might be an idea to have an instance version of this, to define per-branch areas of the tree to inherit types, while other areas stay as defaults""" if cls.branchesInheritType: return cls raise NotImplementedError @property def name(self) -> str: return self._name @name.setter def name(self, val: str): self.setName(val) def _evalDefault(self): if callable(self.default): return self.default(self) return self.default @property def value(self) -> T: if self._value is None and self.default is not None: self._value = self._evalDefault() return self._value @value.setter def value(self, val: T): self.setValue(val) v = value # connected nodes @property def parent(self) -> (TreeType): """return this tree's parent object""" raise NotImplementedError @property def root(self) -> TreeType: branch = self while branch.parent is not None: branch = branch.parent return branch @property def branchMap(self) -> dict[str, TreeType]: """return a nice view of {tree name : tree} generated from uid map""" raise NotImplementedError @property def isLeaf(self) -> bool: return not self.branches @property def leaves(self) -> list[TreeType]: """returns branches under this branch which do not have branches of their own""" return [i for i in self.allBranches(False) if i.isLeaf] def keys(self) -> tuple[str]: return tuple(self.branchMap.keys()) @property def branches(self) -> list[TreeType]: """return a list of immediate branches of this tree""" return list(self.branchMap.values()) @property def siblings(self) -> list[TreeType]: if self.parent: return [i for i in self.parent.branches if i is not self] return [] def allBranches(self, includeSelf=True, depthFirst=True) -> list[TreeType]: """ returns list of all tree objects depth first""" found = [self] if includeSelf else [] if depthFirst: for i in self.branches: found.extend(i.allBranches(includeSelf=True, depthFirst=True)) else: found.extend(self.branches) for i in self.branches: found.extend(i.allBranches(includeSelf=False, depthFirst=False)) return found def _ownIndex(self) -> int: if self.parent: return self.parent.index(self.name) else: return -1 def index(self, lookup=None, *args, **kwargs) -> int: if lookup is None: # get tree's own index return self._ownIndex() if lookup in self.branchMap.keys(): return list(self.branchMap.keys()).index(lookup, *args, **kwargs) else: return -1 def flattenedIndex(self) -> int: """ return the index of this branch if entire tree were flattened """ index = self.index() if self.parent: index += self.parent.flattenedIndex() + 1 return index def trunk(self, includeSelf=True) -> list[TreeType]: """return sequence of ancestor trees in descending order to this tree""" baseList = [self] if includeSelf else [] if not self.parent: return baseList return self.parent.trunk(includeSelf=False) + [self.parent] + baseList def depth(self) -> int: """return int depth of this tree from root""" return len(self.trunk(includeSelf=False)) # addresses def address(self, includeSelf=False, _prev=None) -> list[str]: """if uid, return path by uids else return nice string paths""" prev = _prev or [] token = self.name root = self.root if not prev and includeSelf: prev = [token] if root is self: return prev prev.insert(0, token) return self.parent.address(_prev=prev) def stringAddress(self, includeRoot=False) -> str: """ returns the address sequence joined by the tree separator """ base = self.separatorChar.join(self.address()) if includeRoot: base = self.root.name + self.separatorChar + base return base def commonParent(self, otherBranch: TreeType) -> TreeType: """ return the lowest common parent between given branches or None if one branch is direct parent of the other, that branch will be returned this uses absolute parents, ignoring breakpoints """ # print("commonParent") if self.root is not otherBranch.root: return None otherTrunk = set(otherBranch.trunk(includeSelf=True)) # otherTrunk.add(otherBranch) test = self while test not in otherTrunk: test = test.parent return test def relAddress(self, fromBranch=None): """ retrieve the relative path from the given branch to this one""" # check that branches share a common tree (root) common = self.commonParent(fromBranch) if not common: raise LookupError("Branches {} and {} " "do not share a common root".format( self, fromBranch)) addr = [] commonDepth = common.depth # parent tokens to navigate up from other for i in range(commonDepth - fromBranch.depth): addr.append(self.root.parentToken) # add address to this node addr.extend(self.address(includeSelf=False)[commonDepth - 1:]) return addr # calling - main features of "tree syntax" @classmethod def parseAddressTokens(cls, *tokenArgs: keyType) -> list[str]: """ address may be arbitrarily nested strings, each potentially containing separators return uniform flat list of tokens :type tokenArgs : tuple""" # first flatten address flat = list(flatten(tokenArgs)) # check for any string tokens that need splitting for i, token in enumerate(tuple(flat)): if isinstance(token, str): flat[i] = token.split(cls.separatorChar) # flatten any tuples that the string formatting produced return flatten(flat) def _branchFromToken(self, token: keyType) -> (TreeType, None): """ given single address token, return a known branch or none """ if token == self.parentChar: return self.parent if token not in self.branchMap.keys(): return None return self.branchMap[token] def _getBranchInternal(self, lookup: keyType) -> (TreeType, None): """look up a single layer of branch from first of given tokens""" if not lookup: return self name = lookup.pop(0) found = self._branchFromToken(name) if not found: return None return found._getBranchInternal(lookup) def getBranch(self, key: keyType) -> (TreeType, None): key = self.parseAddressTokens((key, )) return self._getBranchInternal(key) def get(self, key: keyType, default=None): """return branch value""" result = self.getBranch(key) if result: return result.value return default def __call__(self, *address: keyType, create=None, **kwargs) -> TreeType: """ index into tree hierarchy via address sequence, return matching branch""" address = self.parseAddressTokens(address) if not address: return self # all input coerced to list first = str(address.pop(0)) found = self.getBranch(first) if found: return found(address, create=create, **kwargs) # if create is passed directly, use it - # else use lookupcreate default activeCreate = create if create is not None else self.lookupCreate # if branch should not be created, lookup is invalid if not activeCreate: raise LookupError("tree {} has no child {}".format(self, first)) # create new child branch for lookup obj = self._createChildBranch(first, kwargs) # add it to this tree self.addChild(obj) branch = self.getBranch(first) return branch(*address, create=create, **kwargs) def __setitem__(self, key: (str, tuple), value: T, **kwargs): """ assuming that setting tree values is far more frequent than setting actual tree objects """ self(key, **kwargs).value = value def __getitem__(self, address: (str, tuple), **kwargs) -> T: """ returns direct value of lookup branch :rtype T """ return self(address, **kwargs).value # signal systems def emitStackDelta(self, delta: TreeDeltaAtom, apply=True): """emit a semantic delta after a caller modification to this data structure """ if not self.gatheringDeltas: return self.deltasChanged.emit(delta) if apply: delta.doDelta(self.root) def emitStateDelta(self, delta: TreeDeltaAtom): """emit a raw delta for any end change to this tree's state """ self.stateChanged.emit(delta) # referencing def getRef(self) -> TreeReference: """return a persistent reference to this branch""" return TreeReference(self) # writing functions # _private set() functions called from within deltas def setName(self, name: str): delta = TreeNameDelta(self, oldValue=self.name, newValue=name) self.emitStackDelta(delta, apply=True) def _setName(self, name: str): """override here """ self._name = name def setValue(self, value): delta = TreeValueDelta(self, oldValue=self.value, newValue=value) self.emitStackDelta(delta, apply=True) def _setValue(self, value): self._value = value # final value state delta # delta = TreeValueDelta(self, oldValue=self.value, newValue=value) # self.emitStateDelta(delta) # parenting def setIndex(self, index: int): """ reorders tree branch to given index negative indices not yet supported """ if not self.parent: return if index < 0: # ? index = len(self.siblings) + index delta = TreeStructureDelta( self, self.parent, self.parent, oldIndex=self.index(), newIndex=index, eventCode=StructureEvents.branchIndexChanged) self.emitStackDelta(delta, apply=True) self.parent.coreData().branchDatas.remove(self.coreData()) self.parent.coreData().branchDatas.insert(index, self.coreData()) # emit signal of parent if self._signalsActive: self.parent.structureChanged( self, self.parent, self.parent, self.StructureEvents.branchIndexChanged) def _setIndex(self, index: int): raise NotImplementedError def _getRemoveParentAndTarget( self, address: keyType = None, ) -> tuple[TreeType, TreeType]: """return parent, child targets for removal if no target is given, remove the branch this method is called on, eg branchA.remove() -> removes branchA from its parent""" if not address: if not self.parent: return None, self return self.parent, self if isinstance(address, TreeBase): branch = address else: branch = self(address, create=False) return branch.parent, branch def remove( self, address: (keyType, TreeInterface, None) = None, ): """removes address, or just removes the tree if no address is given""" parent, removeTarget = self._getRemoveParentAndTarget(address) if parent is not self: return parent.remove(removeTarget) elif parent is None: # branch had no parent to begin with return delta = TreeDeletionDelta(removeTarget, self, removeTarget.name, removeTarget.value, removeTarget.properties) self.emitStackDelta(delta, apply=True) def _remove(self, branch: TreeType): """ pass a branch in this tree to remove""" raise NotImplementedError def addChild(self, newBranch: TreeInterface, index: int = None) -> TreeType: """called on parent to add new child node determine if branch was already known to current root - if so, branch is effectively moved if not, branch is effectively created """ # get correct index index = index if index is not None else len(self.branches) currentRoot = newBranch.root delta = TreeStructureDelta(newBranch, newBranch.parent, self, oldIndex=newBranch.index(), newIndex=index, eventCode=self.StructureEvents.branchAdded) self.emitStackDelta(delta, apply=True) def _addChild(self, newBranch: TreeInterface, index: int) -> TreeType: raise NotImplementedError def _setParent(self, parentBranch: TreeInterface): """only internal, not to be used in client code""" # disconnect and reconnect signal to root for currentRootSignal, newRootSignal, selfSignal in zip( self.root.signals(), parentBranch.root.signals(), self.signals()): selfSignal.disconnect(currentRootSignal) selfSignal.connect(newRootSignal) def _createChildBranch(self, name, kwargs) -> TreeType: """called internally when a branch is created on lookup""" # check if branch should inherit directly if self.branchesInheritType: obj = self.__class__(name, None) else: obj = self.defaultBranchCls()(name=name) return obj # other behaviour @property def properties(self) -> dict: raise NotImplementedError def getProperty(self, key: str, default=None): return self.properties.get(key, default) def setProperty(self, key: str, value): oldValue = self.getProperty(key, default=FailToFind) delta = TreePropertyDelta(self, key, oldValue, value) self.emitStackDelta(delta, apply=True) def _setProperty(self, key: str, value): self.properties[key] = value def _removeProperty(self, key): if key in self.properties: self.properties.pop(key) def getInherited(self, key, default=None, returnBranch=False): """return first instance of key found in trunk properties if returnBranch, returns the first node in trunk that includes key """ lookup = self.getProperty(key, FailToFind) if lookup is not FailToFind: return lookup if self.parent: return lookup if lookup is not FailToFind else self.parent.getInherited( key, default=default, returnBranch=returnBranch) return default #region breakpoints # for defining distinct regions within contiguous hierarchy @property def breakTags(self) -> set[str]: return set(self.getProperty("breakTags", set())) def setBreakTags(self, val=set()): if isinstance(val, str): val = {val} self.setProperty("breakTags", set(val)) @property def isBreakRoot(self): """checks if this branch is a "main" breakpoint""" return "main" in self.breakTags def setBreakRoot(self): self.setBreakTags(self.breakTags.union({"main"})) def breakRoot(self) -> TreeType: """return the first parent with 'main' in its break tags""" return self.getBreak(("main", )) @property def absoluteRoot(self) -> TreeType: """returns physical top of current hierarchy, ignoring breakpoints""" return self.parent.absoluteRoot if self.parent else self breakTagType = T.Union[T.Sequence[str], str] def isBreak(self, breakTags: breakTagType = ("main", )): """check if this tree is a breakpoint for any of the given tags""" breakTags = (breakTags, ) if isinstance(breakTags, str) else breakTags return set(breakTags) <= self.breakTags def getBreak(self, breakTags: breakTagType = ("main", )) -> TreeType: """return the first matching breakpoint in this tree's trunk """ if not breakTags: return self.absoluteRoot branch = self while branch is not self.absoluteRoot: if branch.isBreak(breakTags): return branch branch = branch.parent return None #endregion
class TreeView(QtWidgets.QTreeView): """widget for viewing and editing an Tree display values in columns, branches in rows""" highlightKind = { "error": QtCore.Qt.red, "warning": QtCore.Qt.yellow, "success": QtCore.Qt.green, } background = QtGui.QColor(100, 100, 128) # {oldBranch, newBranch} currentBranchChanged = QtCore.Signal(dict) def __init__(self, parent=None, nameDelegateCls=TreeNameDelegate, valueDelegateCls=TreeValueDelegate, contextMenu: TreeMenu = None, showValues=True, showRoot=False, scanForWidgets=False): super(TreeView, self).__init__(parent) self.root = None self.nameDelegateCls = nameDelegateCls self.valueDelegateCls = valueDelegateCls self.highlights = {} # dict of tree addresses to highlight self.valuesShown = showValues self.showRoot = showRoot self.scanForWidgets = scanForWidgets # either pass this at init or assign dynamically in subclasses self.context = contextMenu self.contentChanged = Signal() self.sizeChanged = Signal() self.currentSelected = None self.editedIndex = None # stored on editing self.scrollPos = self.verticalScrollBar().value() self.actions = {} # set delegate classes self.setItemDelegateForColumn(0, self.nameDelegateCls(self)) self.setItemDelegateForColumn(1, self.valueDelegateCls(self)) # convenience wrappers self.keyState = KeyState() self.sel = SelectionModelContainer(self.selectionModel(), parent=self) # internal stores for drawing overrides self._liveInputRoots = weakref.WeakSet() self._deltaTrackingRoots = weakref.WeakSet() self._deltaCreatedBranches = weakref.WeakSet() self._deltaModifiedBranches = weakref.WeakSet() self._deltaDeletedBranches = weakref.WeakSet() self._widgetBranches = weakref.WeakKeyDictionary() self.savedSelectedTrees = [] self.savedExpandedTrees = [] self.makeBehaviour() self.makeAppearance() self.makeConnections() self.makeBaseActions() self.expandAll() self.showValues(showValues) def makeBehaviour(self): self.setDragEnabled(True) self.setAcceptDrops(True) self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) self.setSelectionMode(self.ExtendedSelection) self.setSelectionBehavior(self.SelectRows) #self.setDragDropMode() #self.setDropIndicatorShown() self.setAutoScroll(False) self.setFocusPolicy(QtCore.Qt.ClickFocus) self.setDefaultDropAction(QtCore.Qt.CopyAction) def makeAppearance(self): # appearance if doIcons: self.setWindowIcon(QtGui.QIcon(squareCentre)) self.setStyleSheet(styleSheet) self.setSizePolicy(expandingPolicy) header = self.header() header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) header.setStretchLastSection(True) self.setSizeAdjustPolicy( QtWidgets.QAbstractScrollArea.AdjustToContents) self.setUniformRowHeights(True) self.setIndentation(10) #self.setAlternatingRowColors(True) self.showDropIndicator() def makeConnections(self): self.contentChanged.connect(self.onKeyVisibilitySet) self.contentChanged.connect(self.resizeToTree) self.expanded.connect(self.onExpanded) self.collapsed.connect(self.onCollapsed) @property def tree(self): return self.parent().tree def model(self) -> TreeModel: return super(TreeView, self).model() def setModel(self, model: QtCore.QAbstractItemModel) -> None: super(TreeView, self).setModel(model) self.model().layoutAboutToBeChanged.connect(self.saveAppearance) self.model().layoutChanged.connect(self.restoreAppearance) self.model().itemChanged.connect(self.onModelItemChanged) self.setSelectionModel(QtCore.QItemSelectionModel(self.model())) self.sel.setSelectionModel(self.selectionModel()) self.selectionModel().currentChanged.connect(self.onCurrentChanged) for i in (self.model().layoutChanged, self.model().itemChanged, self.model().dataChanged): i.connect(self.onTreeChanged) def setTree(self, tree): """associates widget with Tree object""" #self.root = tree.root self.root = self.tree self.expandAll() if self.showRoot: self.setRootIndex(self.model().invisibleRootItem().index()) else: self.setRootIndex(self.model().invisibleRootItem().child( 0, 0).index()) # no direct tree signal connections - # only connect to signals from model to ensure # UI fires after model changes # # connect tree signals # for i in self.root.signals(): # i.connect(self.onTreeChanged) self.onTreeChanged() return def data(self, index, role=QtCore.Qt.DisplayRole): """ convenience """ #self.viewport() return self.model().data(index, role) def onTreeChanged(self, *args, **kwargs): #print("on tree changed") self.refreshDrawingInternals() self.onKeyVisibilitySet() self.resizeToTree() def refreshDrawingInternals(self): for i in (self._liveInputRoots, self._deltaTrackingRoots, self._deltaCreatedBranches, self._deltaModifiedBranches, self._deltaDeletedBranches): i.clear() # for i in self.tree.allBranches(): #type:Tree # if i.liveInput: # self._liveInputRoots.add(i) # if i._deltaTracker: # self._deltaTrackingRoots.add(i) # # if i.isDeltaCreated(): # self._deltaCreatedBranches.add(i) # elif i.deltaBaseName(): # self._deltaModifiedBranches.add(i) if self.scanForWidgets: self.generateWidgetsByProperties() def boundingRectForBranch(self, branch, includeBranches=True) -> QtCore.QRect: index = self.model().rowFromTree(branch) rect = self.visualRect(index) if includeBranches: for i in self.model().allChildren(index): rect = rect.united(self.visualRect(i)) return rect # # def paintEvent(self, event:QtGui.QPaintEvent) -> None: # """draw overlays as post process""" # painter = QtGui.QPainter(self.viewport()) # refExpand = 3 # result = super(TreeView, self).paintEvent(event) # # for branch in self._liveInputRoots: # rect = self.boundingRectForBranch(branch, includeBranches=True) # rect = rect.marginsAdded(QtCore.QMargins( # refExpand, refExpand, refExpand, refExpand)) # # brush = QtGui.QBrush(QtGui.QColor(150, 250, 150, 32)) # painter.setBrush(brush) # #painter.setPen(QtCore.Qt.PenStyle.NoPen) # pen = QtGui.QPen(QtGui.QColor(150, 200, 150, 64)) # painter.setPen(pen) # painter.drawRoundedRect(rect, 4.0, 4.0) # # # check for delta tracking # deltaExpand = 1 # for branch in self._deltaTrackingRoots: # rect = self.boundingRectForBranch(branch, includeBranches=True) # rect = rect.marginsAdded(QtCore.QMargins( # deltaExpand, deltaExpand, deltaExpand, deltaExpand)) # # # purple dots # pen = QtGui.QPen(QtGui.QColor(160, 80, 240)) # pen.setWidth(1) # pen.setDashOffset(5) # pen.setStyle(QtCore.Qt.DashLine) # painter.setPen(pen) # painter.drawRoundedRect(rect, 1.0, 1.0) # # self._deltaModifiedBranches.difference_update(self._deltaCreatedBranches) # # # paint rectangles for each delta-affected branch # for branch in self._deltaCreatedBranches: # rect = self.boundingRectForBranch(branch, includeBranches=True) # col = QtGui.QColor(*createColour) # pen = QtGui.QPen(col) # brush = QtGui.QBrush(col) # #brush.color().setAlpha(128) # painter.setPen(pen) # painter.setBrush(brush) # painter.drawRoundedRect(rect, 1.0, 1.0) # for branch in self._deltaModifiedBranches: # rect = self.boundingRectForBranch(branch, includeBranches=False) # rect = rect.marginsRemoved(QtCore.QMargins(1, 1, 1, 1)) # col = QtGui.QColor(*modifyColour) # pen = QtGui.QPen(col) # #col.setAlpha(128) # col = col.lighter(120) # brush = QtGui.QBrush(col) # #brush.color().setAlpha(128) # painter.setPen(pen) # painter.setBrush(brush) # painter.drawRoundedRect(rect, 1.0, 1.0) # # # result = super(TreeView, self).paintEvent(event) def makeBaseActions(self): """sets up copy, add, delete etc actions for branch entries""" def showValues(self, state=True): """tracks if value column is shown or not""" self.setColumnHidden(1, not state) self.valuesShown = state # region events @catchAll def mousePressEvent(self, event): #print("tree mousePress") # if event.isAccepted(): # print("tree event accepted") # return True self.keyState.mousePressed(event) # only pass event on editing, # need to manage selection separately if not (self.keyState.ctrl or self.keyState.shift)\ or event.button() == QtCore.Qt.RightButton: return super(TreeView, self).mousePressEvent(event) pass index = self.indexAt(event.pos()) self.onClicked(index) event.accept() def onClicked(self, index): """ manage selection manually """ # if ctrl, toggle selection if self.keyState.ctrl and not self.keyState.shift: self.sel.toggle(index) self.sel.setCurrent(index) return elif self.keyState.shift: # contiguous span clickRow = self.model().rowFromIndex(index) currentRow = self.model().rowFromIndex(self.sel.current()) # find physically lowest on screen if self.visualRect(clickRow).y() < \ self.visualRect(currentRow).y(): fn = self.indexAbove else: lowest = clickRow highest = currentRow fn = self.indexBelow targets = [] selStatuses = [] checkIdx = currentRow selRows = self.selectionModel().selectedRows() count = 0 while checkIdx != clickRow and count < 4: count += 1 checkIdx = fn(checkIdx) targets.append(checkIdx) selStatuses.append(checkIdx in selRows) addOrRemove = sum(selStatuses) < len(selStatuses) / 2 for row in targets: self.sel.add(row) # set previous selection self.sel.setCurrent(index) self.currentSelected = index def selectedBranches(self) -> Ty.List[Tree]: """returns branches for all name and value items selected in ui""" branchList = [] for i in self.sel.rows: branch = self.model().branchFromIndex(i) if branch in branchList: continue branchList.append(branch) return branchList def selectedValues(self) -> Ty.List[Tree]: """old return branches whose values ONLY have been selected, not names used to allow people to set tree branch values""" indices = [i for i in self.sel.indices if i.column() == 1] return list(map(self.model().branchFromIndex, indices)) def getContextMenu(self) -> TreeMenu: """override however is easiest""" return self.context def contextMenuEvent(self, event): if self.getContextMenu() is None: print("No context menu assigned, deferring") return super(TreeView, self).contextMenuEvent(event) self.context.onContextRequested() # pos = event.localPos() pos = event.globalPos() pos = event.pos() # pos = self.viewport().mapFromGlobal( self.mapToGlobal( event.pos())) pos = self.mapToGlobal(event.pos()) menu = self.context.exec_(pos) event.accept() def onCurrentChanged(self, currentIdx: QtCore.QModelIndex, prevIdx: QtCore.QModelIndex): """connected to selection model - convert model indices to branches, then emit top-level signal""" newBranch = currentIdx.data(treeObjRole) prevBranch = prevIdx.data(treeObjRole) self.currentBranchChanged.emit({ "oldBranch": prevBranch, "newBranch": newBranch }) def copyEntries(self): clip = QtGui.QGuiApplication.clipboard() indices = self.selectionModel().selectedRows() if not indices: # nothing to copy return mime = self.model().mimeData(indices) clip.setMimeData(mime) def pasteEntries(self): indices = self.selectedIndexes() # i strongly hate if not indices: return index = indices[0] clip = QtGui.QGuiApplication.clipboard() data = clip.mimeData() self.model().dropMimeData(data, QtCore.Qt.CopyAction, 0, 0, index) self.sync() def dropEvent(self, event): super(TreeView, self).dropEvent(event) def keyPressEvent(self, event): """ bulk of navigation operations, for hierarchy navigation aim to emulate maya outliner ctrl+D - duplicate del - delete left/right - select siblings up / down - select child / parent p - parent selected branches to last selected shiftP - parent selected branches to root ctrl + shift + left / right - shuffle selected among siblings events modify the core tree data structure - model and view are rebuilt atop it not sure if there is an elegant way to structure this going with disgusting battery of if statements """ self.keyState.keyPressed(event) sel = self.selectionModel().selectedRows() key = event.key() # don't override anything if editing is in progress if self.state() == QtWidgets.QTreeView.EditingState or len(sel) == 0: #print("tree editing, skipping") event.accept() return True # editing entry if key in [QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return]: # shift-enter begins editing on value if self.keyState.shift: idx = sel[0].siblingAtColumn(1) else: # edit name idx = sel[0] self.editedIndex = idx self.edit(idx) event.accept() return True if self.keyState.ctrl and key in \ (QtCore.Qt.Key_D, QtCore.Qt.Key_C, QtCore.Qt.Key_V, QtCore.Qt.Key_X): if key == QtCore.Qt.Key_D: # duplicate for row in sel: self.model().duplicateRow(row) elif key == QtCore.Qt.Key_C: # copy self.copyEntries() elif key == QtCore.Qt.Key_V: self.copyEntries() for row in sel: self.model().deleteRow(row) elif key == QtCore.Qt.Key_V: # paste self.pasteEntries() event.accept() return True # shifting row up or down if self.keyState.shift and self.keyState.ctrl: self.saveAppearance() if key in [QtCore.Qt.Key_Up, QtCore.Qt.Key_Left]: for row in sel: self.model().shiftRow(row, up=True) elif key in [QtCore.Qt.Key_Down, QtCore.Qt.Key_Right]: for row in sel: self.model().shiftRow(row, up=False) self.restoreAppearance() event.accept() #self.sync() return True # deleting if key == QtCore.Qt.Key_Delete: #print("deleting") for row in sel: self.model().deleteRow(row) #self.sync() event.accept() return True # reparenting if key == QtCore.Qt.Key_P: if self.keyState.shift: for row in sel: # unparent row self.model().unParentRow(row) elif len(sel) > 1: # parent self.model().parentRows(sel[:-1], sel[-1]) event.accept() return True if key in (QtCore.Qt.Key_Tab, QtCore.Qt.Key_Backtab): if self.keyState.shift: # unparent row print(sel) for row in sel: self.model().unParentRow(row) else: # parent to row directly above for row in reversed(sel): adj = self.model().connectedIndices(row) if adj["prev"] and not adj["prev"] == row: self.model().parentRows([row], target=adj["prev"]) #self.sync() event.accept() return True # direction keys to move cursor if key in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Right, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down): self.sel.clear() if self.sel.current(): sel.append(self.sel.current()) for i in sel: adj = self.model().connectedIndices(i) target = None if key == QtCore.Qt.Key_Left: # back one index if adj["prev"]: target = adj["prev"] elif key == QtCore.Qt.Key_Right: # forwards one index if adj["next"]: target = adj["next"] elif key == QtCore.Qt.Key_Up: # up to parent if adj["parent"]: target = (i.parent()) else: target = i else: # down to child if i.child(0, 0).isValid(): target = i.child(0, 0) else: target = i if target: self.sel.add(target) if i == self.sel.current(): self.sel.setCurrent(target) event.accept() return True return super(TreeView, self).keyPressEvent(event) #endregion # region appearance def resizeToTree(self, *args, **kwargs): colWidth = self.sizeHintForColumn(0) + self.sizeHintForColumn(1) + 10 self.setMinimumWidth(colWidth) self.setGeometry( 0, 0, #baseRect.width() + baseRect.x(), colWidth, self.viewportSizeHint().height() + self.header().rect().height()) pass def saveAppearance(self): """ saves expansion and selection state """ self.savedSelectedTrees = [] self.savedExpandedTrees = [] self.currentSelected = None for i in self.selectionModel().selectedRows(): branch = self.model().treeFromRow(i) self.savedSelectedTrees.append(branch) for i in self.model().allRows(): if not self.model().checkIndex(i): print("index {} is not valid, skipping".format(i)) if self.isExpanded(i): branch = self.model().treeFromRow(i) if branch: self.savedExpandedTrees.append(branch) if self.selectionModel().currentIndex().isValid(): self.currentSelected = self.model().treeFromRow( self.selectionModel().currentIndex()) # save viewport scroll position self.scrollPos = self.verticalScrollBar().value() def restoreAppearance(self): """ restores expansion and selection state """ self.setRootIndex(self.model().invisibleRootItem().child(0, 0).index()) self.resizeToTree() print(self.savedExpandedTrees, self.savedExpandedTrees) for i in self.savedSelectedTrees: if not self.model().rowFromTree(i): continue self.selectionModel().select( self.model().rowFromTree(i), QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows) for i in self.savedExpandedTrees: if not self.model().rowFromTree(i): print("expanded tree not found ") continue self.expand(self.model().rowFromTree(i)) pass if self.currentSelected: #print("setting current") row = self.model().rowFromTree(self.currentSelected) self.sel.setCurrent(row) self.verticalScrollBar().setValue(self.scrollPos) self.resizeToTree() def sync(self, *args, **kwargs): print("widget sync") pass self.saveAppearance() sel = self.selectionModel() # self.model().sync() # self.setRootIndex(self.model().invisibleRootItem().child(0, 0).index()) self.setSelectionModel(sel) self.restoreAppearance() self.expandAll() self.onKeyVisibilitySet() self.resizeToTree() def addHighlight(self, address, kind): """adds a highlight to TreeView line, depending on reason""" colour = QtCore.QColor(self.highlightKind[kind]) self.highlights[address] = kind def _recursiveApply( self, fnName, startIndex, callSuper=True, ): """apply a method recursively to all child indices""" if callSuper: result = getattr(super(TreeView, self), fnName)(startIndex) else: result = None for i in range(self.model().rowCount(startIndex)): childIdx = startIndex.child(i, 0) getattr(self, fnName)(childIdx) return result def onExpanded(self, index): """ check for shift - recursively expand children if so """ if self.keyState.shift: self._recursiveApply("expand", index) def onCollapsed(self, index): """ check for shift - recursively expand children if so """ if self.keyState.shift: self._recursiveApply("collapse", index) def display(self): self.sync() print(self.tree.displayStr()) #endregion # # def commitData(self, editor): # print("commit", self.editedIndex) # # if self.editedIndex: # # #print(self.editedIndex.isValid()) # # self.sel.setCurrent(self.editedIndex) # # self.sel.add(self.editedIndex) # return super(TreeView, self).commitData(editor) # # if self.editedIndex: # # print(self.editedIndex.isValid()) # # self.sel.setCurrent(self.editedIndex) # # self.sel.add(self.editedIndex) def select(self, branch=None, path=None, clear=True): """ main user selection method """ if clear: self.sel.clear() if not (branch or path): # select -cl 1 return if path: result = self.tree.getBranch(path) if result is None: return None branch = [result] else: if not isinstance(branch, (list, tuple)): branch = branch for b in branch: item = self.model().rowFromTree(b) self.sel.add(item.index()) def focusNextPrevChild(self, direction): return False # widget generation def addValueWidgetForBranchParams(self, branch: Tree): """if possible, adds an AtomicWidget for the value field of the given branch track widgets per tree branch, per view """ # print("branch", branch, "index", self.model().rowFromTree(branch), # self.model().allRows()) itemIdx = self.model().rowFromTree(branch) # type:QtCore.QModelIndex valueIdx = itemIdx.sibling(itemIdx.row(), 1) if self.indexWidget(valueIdx) is not None: """for now do not support changing the type of widget for an entry live""" # print("found existing widget", self.indexWidget(valueIdx), "for", branch) return value = branch.value params: AtomicWidgetParams = branch.getProperty(UI_PROPERTY_KEY) params.value = value newWidg = atomicWidgetForParams(params) newWidg.setParent(self) branchItem: TreeBranchItem = self.model().itemFromIndex( self.model().rowFromTree(branch)) valueItem = branchItem.valueItem() newWidg.atomValueChanged.connect(branch.setValue) self.setIndexWidget(valueIdx, newWidg) def onModelItemChanged(self, item: (TreeBranchItem, TreeValueItem)): """when data on model item changes, check if it has an index widget - if so, sync its state""" if not isinstance(item, TreeValueItem): return if not isinstance(self.indexWidget(item.index()), AtomicWidget): return print("index widget", self.indexWidget(item.index())) # connect widget signals to UI elements only widg: AtomicWidget = self.indexWidget(item.index()) widg.setAtomValue(item.data(role=2)) def generateWidgetsByProperties(self, *args, **kwargs): """iterate over ui tree adding widgets where uiData property is used, set to AtomicWidgetParams""" for branch in self.tree.allBranches(): # print("branch ui params", branch.name, branch.getProperty(UI_PROPERTY_KEY)) if branch.getProperty(UI_PROPERTY_KEY) is None: continue if isinstance(branch.getProperty(UI_PROPERTY_KEY), AtomicWidgetParams): self.addValueWidgetForBranchParams(branch) if T.TYPE_CHECKING: def parent(self) -> TreeWidget: pass def onKeyVisibilitySet(self): """update item visibility based on parent widget vis map""" print("view onKeyVisibilitySet", self.parent().keyVisibilityPresetMap) visMap = self.parent().keyVisibilityPresetMap for k, v in visMap.items(): print("k", self.tree.getBranch(k)) if self.tree.getBranch(k) is not None: if not v: # hide branch item item = self.model().itemFromIndex(self.model().rowFromTree( self.tree.getBranch(k))) self.setRowHidden(item.row(), item.parent().index(), True)
def __init__(self, obj, _proxyLink): super(ToolFieldProxy, self).__init__(obj, _proxyLink) self._proxyData["objectChanged"] = Signal() self._proxyData["cacheValue"] = None
def __init__(self): self._deltaStack : T.List[DeltaAtom] = [] self._deltaIndex = -1 # which is "current" active delta self.deltasChanged = Signal(name="deltasChanged")
def __init__(self): self.deltaEmitted = Signal(name="deltaEmitted") self.stateChanged = Signal(name="stateChanged")
def __init__(self, val): self._val = val self.onDataSet = Signal(name="onDataSet")