import functools, time
from enum import Enum
from math import floor
from PySide6.QtCore import Qt, Property, QSize, QRect, QTimer, QPointF, QMimeData, QItemSelectionModel, QEvent
from PySide6.QtWidgets import QTreeView, QWidget, QApplication, QAbstractItemView, QItemDelegate, QStyle, QStyleOption, QStylePainter
from PySide6.QtGui import QStandardItemModel, QStandardItem, QPen, QBrush, QPainter, QColor, QDrag, QCursor, QPixmap, QMouseEvent, QRadialGradient
from editor.common.math import clamp, lerp
from editor.common.ease import easeInOutQuad, easeOutQuad
from editor.common.icon_cache import getThemePixmap
[docs]class TreeItemDelegate(QItemDelegate):
def __init__(self, view):
super().__init__()
self.view = view
[docs] def sizeHint(self, option, index):
w = option.rect.width()
h = index.data(Qt.SizeHintRole)
if h == None: h = self.view.itemHeight
return QSize(w, h)
[docs] def updateEditorGeometry(self, editor, option, index):
view = self.view
rect = view.visualRect(index)
left = rect.x() + view.treePaddingLeft
if index.data(Qt.DecorationRole): left += view.indentation() + view.itemPaddingLeft
rect.setLeft(left - 3)
editor.setGeometry(rect)
[docs] def paint(self, painter, option, index):
painter.setRenderHint(QPainter.Antialiasing, True)
view = self.view
rect = option.rect
rect.setHeight(view.itemHeight)
l, r = rect.left(), rect.right() + 1
t, h = rect.top(), rect.height()
ch = index.data(Qt.SizeHintRole)
if ch == None: ch = self.view.itemHeight
painter.setClipRect(QRect(0, t, r, ch))
bgRect = QRect(0, t, r, h)
branchRect = QRect(0, t, l, h)
self.drawBackground(painter, bgRect, option.state, index)
self.drawBranchLines(painter, branchRect, index)
self.drawBranchArrow(painter, branchRect, index)
self.drawContent(painter, rect, index)
[docs] def drawBackground(self, painter, rect, state, index):
view = self.view
selected = state & QStyle.State_Selected
hovered = index == view.hoveredIndex # option.state & QStyle.State_MouseOver
bgColor = None
if selected:
bgColor = view.backgroundSelected if view.viewFocused else view.backgroundSelectedUnfocused
elif hovered and view.dropIndicatorRect == None:
bgColor = view.backgroundHovered
else:
# alternate = option.features & QStyleOptionViewItem.Alternate
alternate = view.useAlternatingBackground and self.view._flatVisibleRowNumber(index, self.view.model()) % 2
bgColor = view.backgroundAlternate if alternate else view.background
painter.fillRect(rect, bgColor)
[docs] def drawContent(self, painter, rect, index):
view = self.view
textOffset = view.treePaddingLeft
decoration = index.data(Qt.DecorationRole)
if decoration:
indentation = view.indentation()
rectSize = view.itemHeight
textOffset += indentation + view.itemPaddingLeft
cx, cy = rect.left() + round(indentation / 2), rect.top() + round(rect.height() / 2)
iconSize = view.itemIconSize
halfSize = iconSize / 2
x, y = cx - halfSize + view.treePaddingLeft, cy - halfSize
painter.drawPixmap(x, y, iconSize, iconSize, decoration)
painter.drawText(rect.adjusted(textOffset, -1, 0, 0), Qt.AlignVCenter, index.data())
[docs] def drawBranchLines(self, painter, rect, index):
view = self.view
if not view.drawBranchLine: return
itemDepth = self._depth(index)
drawDepth = itemDepth - view.branchLineFilterDepth
if drawDepth >= 0:
painter.setOpacity(0.4)
indentation = view.indentation()
t, h = rect.top(), rect.height()
# print(index.siblingAtRow(index.row()+1).isValid(), index.data())
curr = index
for i in range(drawDepth):
x = int(indentation * (itemDepth - i - 0.5)) + view.treePaddingLeft
# painter.fillRect(x, t, indentation, h, Qt.green)
hasSiblings = curr.siblingAtRow(curr.row() + 1).isValid()
adjoinsItem = i == 0
if hasSiblings:
if not adjoinsItem:
painter.drawPixmap(x, t, indentation, h, getThemePixmap('icon_branch_I.png'))
else:
painter.drawPixmap(x, t, indentation, h, getThemePixmap('icon_branch_T.png'))
elif not hasSiblings and adjoinsItem:
painter.drawPixmap(x, t, indentation, h, getThemePixmap('icon_branch_L.png'))
curr = curr.parent()
painter.setOpacity(1)
[docs] def drawBranchArrow(self, painter, rect, index):
if not index.model().hasChildren(index): return
view = self.view
size = view.branchPixmapSize
sizeHalf = size / 2
cx = rect.right() - round(view.indentation() / 2) + 1 + view.treePaddingLeft
cy = rect.top() + (rect.height() / 2)
rect = QRect(cx - sizeHalf - view.branchArrowOffset, cy - sizeHalf, size, size)
branchArrow = view.isExpanded(index) and view.branchOpened or view.branchClosed
painter.drawPixmap(rect, branchArrow)
#################### UTILS ####################
def _depth(self, index):
depth = 0
parent = index.parent()
while parent.isValid():
depth += 1
parent = parent.parent()
return depth
[docs]class PingAnimPhase(Enum):
[docs]class TreeItemPingOverlay(QWidget):
@Property(int)
[docs] def pingZoomDuration(self):
return self._pingZoomDuration
@pingZoomDuration.setter
def pingZoomDuration(self, value):
self._pingZoomDuration = value
@Property(int)
[docs] def pingIdleDuration(self):
return self._pingIdleDuration
@pingIdleDuration.setter
def pingIdleDuration(self, value):
self._pingIdleDuration = value
@Property(int)
[docs] def pingFadeDuration(self):
return self._pingFadeDuration
@pingFadeDuration.setter
def pingFadeDuration(self, value):
self._pingFadeDuration = value
@Property(float)
[docs] def pingZoomScale(self):
return self._pingZoomScale
@pingZoomScale.setter
def pingZoomScale(self, value):
self._pingZoomScale = value
@Property(int)
[docs] def pingAnimTickInterval(self):
return self._pingAnimTickInterval
@pingAnimTickInterval.setter
def pingAnimTickInterval(self, value):
self._pingAnimTickInterval = value
@Property(QColor)
[docs] def pingOutlineColor(self):
return self._pingOutlinePen.color()
@pingOutlineColor.setter
def pingOutlineColor(self, value):
self._pingOutlinePen.setColor(value)
@Property(int)
[docs] def pingOutlineWidth(self):
return self._pingOutlinePen.width()
@pingOutlineWidth.setter
def pingOutlineWidth(self, value):
self._pingOutlinePen.setWidth(value)
@Property(int)
[docs] def pingOutlineRound(self):
return self._pingOutlineRound
@pingOutlineRound.setter
def pingOutlineRound(self, value):
self._pingOutlineRound = value
def __init__(self, treeview):
super().__init__(treeview)
self._pingZoomDuration = 100
self._pingIdleDuration = 2000
self._pingFadeDuration = 1000
self._pingZoomScale = 1.5
self._pingAnimTickInterval = 16
self._pingOutlinePen = QPen(QColor('#D7C11B'), 2)
self._pingOutlineRound = 6
self.setAttribute(Qt.WA_TransparentForMouseEvents, True)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.setAttribute(Qt.WA_DeleteOnClose, True)
treeview.installEventFilter(self)
[docs] def syncTreeViewRect(self):
treeview = self.parent()
viewport = treeview.viewport()
self.setGeometry(viewport.rect())
[docs] def eventFilter(self, obj, evt):
if evt.type() == QEvent.Resize: self.syncTreeViewRect()
return False
[docs] def startPing(self, index):
self.elapsed = 0
self.progress = 0
self.index = index
self.state = PingAnimPhase.Idle
list = TreeItemPingOverlay.InstanceList
instanceCount = len(list)
for i in range(instanceCount):
instance = list[instanceCount - i - 1]
if (instance.index == index): instance.stopPing()
self.InstanceList.append(self)
self.timer = QTimer()
self.timer.timeout.connect(self.tickPingAnim)
self.timer.start(self._pingAnimTickInterval)
self.prevTickTime = time.time()
self.syncTreeViewRect()
[docs] def stopPing(self):
self.InstanceList.remove(self)
self.timer.stop()
self.timer.deleteLater()
self.deleteLater()
@staticmethod
[docs] def stopAll():
list = TreeItemPingOverlay.InstanceList
instanceCount = len(list)
for i in range(instanceCount):
instance = list[instanceCount - i - 1]
instance.stopPing()
[docs] def tickPingAnim(self):
curr = time.time()
delta = (curr - self.prevTickTime) * 1000
self.prevTickTime = curr
self.elapsed += delta
zoomInDuration = self._pingZoomDuration
zoomOutDuration = zoomInDuration + self._pingZoomDuration
idleDuration = zoomInDuration + self._pingIdleDuration
fadeDuration = idleDuration + self._pingFadeDuration
if self.elapsed <= zoomInDuration:
self.state = PingAnimPhase.ZoomIn
self.progress = self.elapsed / self._pingZoomDuration
self.repaint()
elif self.elapsed <= zoomOutDuration:
self.state = PingAnimPhase.ZoomOut
self.progress = (self.elapsed - zoomInDuration) / self._pingZoomDuration
self.repaint()
elif self.elapsed <= idleDuration:
self.state = PingAnimPhase.Idle
self.repaint()
elif self.elapsed <= fadeDuration:
self.state = PingAnimPhase.Fade
k = (self.elapsed - idleDuration) / self._pingFadeDuration
self.progress = easeOutQuad(k)
self.repaint()
else:
return self.stopPing()
[docs] def paintEvent(self, event):
view = self.parent()
index = self.index
rect = view.visualRect(index)
painter = QPainter()
painter.begin(self)
painter.setRenderHints(QPainter.Antialiasing, True)
option = QStyleOption()
option.initFrom(self)
fm = option.fontMetrics
tp, ip = view.treePaddingLeft, view.itemPaddingLeft
width = option.fontMetrics.boundingRect(index.data()).width() + tp * 2
if index.data(Qt.DecorationRole): width += view.indentation() + ip + round((view.itemHeight - view.itemIconSize) / 2)
rect.setWidth(width)
zoom = None
if self.state == PingAnimPhase.ZoomIn:
zoom = lerp(1, self.pingZoomScale, self.progress)
elif self.state == PingAnimPhase.ZoomOut:
zoom = lerp(self.pingZoomScale, 1, self.progress)
elif self.state == PingAnimPhase.Fade:
painter.setOpacity(1 - self.progress)
if zoom != None:
tx, ty = rect.x() + width / 2, rect.y() + rect.height() / 2
painter.translate(tx, ty)
painter.scale(zoom, zoom)
painter.translate(-tx, -ty)
pen = painter.pen()
alternate = view._flatVisibleRowNumber(index, view.model()) % 2
bgColor = alternate and view.background or view.backgroundAlternate
painter.setBrush(bgColor)
painter.setPen(self._pingOutlinePen)
painter.drawRoundedRect(rect.adjusted(-12 + tp, 0, 12 - tp, 0), self._pingOutlineRound, self._pingOutlineRound)
painter.setPen(pen)
delegate = view.itemDelegate(index)
delegate.drawContent(painter, rect, index)
painter.end()
[docs]class TreeView(QTreeView):
@Property(int)
[docs] def treePaddingLeft(self):
return self._treePaddingLeft
@treePaddingLeft.setter
def treePaddingLeft(self, value):
if self._treePaddingLeft == value: return
self._treePaddingLeft = value
self.repaint()
@Property(int)
[docs] def itemPaddingLeft(self):
return self._itemPaddingLeft
@itemPaddingLeft.setter
def itemPaddingLeft(self, value):
if self._itemPaddingLeft == value: return
self._itemPaddingLeft = value
self.repaint()
@Property(int)
[docs] def itemIconSize(self):
return self._itemIconSize
@itemIconSize.setter
def itemIconSize(self, value):
if self._itemIconSize == value: return
self._itemIconSize = value
self.repaint()
@Property(int)
[docs] def itemHeight(self):
return self._itemHeight
@itemHeight.setter
def itemHeight(self, value):
if self._itemHeight == value: return
self._itemHeight = value
self.repaint()
@Property(int)
[docs] def branchPixmapSize(self):
return self._branchPixmapSize
@branchPixmapSize.setter
def branchPixmapSize(self, value):
if self._branchPixmapSize == value: return
self._branchPixmapSize = value
self.repaint()
@Property(QPixmap)
[docs] def branchOpened(self):
return self._pixmapBranchOpened
@branchOpened.setter
def branchOpened(self, value):
if self._pixmapBranchOpened == value: return
self._pixmapBranchOpened = value
self.repaint()
@Property(QPixmap)
[docs] def branchClosed(self):
return self._pixmapBranchClosed
@branchClosed.setter
def branchClosed(self, value):
if self._pixmapBranchClosed == value: return
self._pixmapBranchClosed = value
self.repaint()
@Property(int)
[docs] def branchArrowOffset(self):
return self._branchArrowOffset
@branchArrowOffset.setter
def branchArrowOffset(self, value):
self._branchArrowOffset = value
@Property(bool)
[docs] def customAnimated(self):
return self._customAnimated
@customAnimated.setter
def customAnimated(self, value):
self._customAnimated = value
@Property(int)
[docs] def customAnimDuration(self):
return self._customAnimDuration
@customAnimDuration.setter
def customAnimDuration(self, value):
self._customAnimDuration = value
@Property(int)
[docs] def customAnimTickInterval(self):
return self._customAnimTickInterval
@customAnimTickInterval.setter
def customAnimTickInterval(self, value):
self._customAnimTickInterval = value
@Property(int)
[docs] def dropIndicatorMargin(self):
return self._dropIndicatorMargin
@dropIndicatorMargin.setter
def dropIndicatorMargin(self, value):
self._dropIndicatorMargin = value
@Property(QColor)
[docs] def dropIndicatorColor(self):
return self._penDropIndicator.color()
@dropIndicatorColor.setter
def dropIndicatorColor(self, value):
if self._penDropIndicator.color() == value: return
self._penDropIndicator.setColor(value)
self.repaint()
@Property(int)
[docs] def dropIndicatorWidth(self):
return self._penDropIndicator.width()
@dropIndicatorWidth.setter
def dropIndicatorWidth(self, value):
if self._penDropIndicator.width() == value: return
self._penDropIndicator.setWidth(value)
self.repaint()
@Property(bool)
[docs] def drawBranchLine(self):
return self._drawBranchLine
@drawBranchLine.setter
def drawBranchLine(self, value):
self._drawBranchLine = value
@Property(int)
[docs] def branchLineFilterDepth(self):
return self._branchLineFilterDepth
@branchLineFilterDepth.setter
def branchLineFilterDepth(self, value):
self._branchLineFilterDepth = value
@Property(QColor)
[docs] def background(self):
return self._background
@background.setter
def background(self, value):
self._background = value
@Property(QColor)
[docs] def backgroundAlternate(self):
return self._backgroundAlternate
@backgroundAlternate.setter
def backgroundAlternate(self, value):
self._backgroundAlternate = value
@Property(QColor)
[docs] def backgroundSelected(self):
return self._backgroundSelected
@backgroundSelected.setter
def backgroundSelected(self, value):
self._backgroundSelected = value
@Property(QColor)
[docs] def backgroundSelectedUnfocused(self):
return self._backgroundSelectedUnfocused
@backgroundSelectedUnfocused.setter
def backgroundSelectedUnfocused(self, value):
self._backgroundSelectedUnfocused = value
@Property(QColor)
[docs] def backgroundHovered(self):
return self._backgroundHovered
@backgroundHovered.setter
def backgroundHovered(self, value):
self._backgroundHovered = value
@Property(bool)
[docs] def useAlternatingBackground(self):
return self._useAlternatingBackground
@useAlternatingBackground.setter
def useAlternatingBackground(self, value):
self._useAlternatingBackground = value
def __init__(self, parent = None):
super().__init__(parent)
self._treePaddingLeft = 2
self._itemPaddingLeft = 2
self._itemIconSize = 16
self._penDropIndicator = QPen(QColor('#8af'), 2)
self._branchPixmapSize = 12
self._pixmapBranchOpened = getThemePixmap('arrow_down.png')
self._pixmapBranchClosed = getThemePixmap('arrow_right.png')
self._branchArrowOffset = 0
self._customAnimated = True
self._customAnimDuration = 120
self._customAnimTickInterval = 16
self._dropIndicatorMargin = 5
self._itemHeight = 20
self._drawBranchLine = True
self._branchLineFilterDepth = 1
self.setIndentation(20)
self._background = QColor('#404040')
self._backgroundAlternate = QColor('#474747')
self._backgroundSelected = QColor('#515c84')
self._backgroundSelectedUnfocused = QColor('#6d7284')
self._backgroundHovered = QColor('#454768')
self._useAlternatingBackground = True
self.hoveredIndex = None
self.underAnimating = None
self.collapsingIndex = None
self.dropIndicatorRect = None
self.dropPosition = None # -1 - before, 0 - inside, 1 - after
self.viewFocused = None
self.autoExpandTimer = QTimer()
self.autoExpandTimer.timeout.connect(self.expandHovered)
self.autoExpandTimer.setSingleShot(True)
self.setMouseTracking(True)
self.setItemDelegate(TreeItemDelegate(self))
self.setAnimated(False)
self.setHeaderHidden(True)
self.setAlternatingRowColors(False)
self.setExpandsOnDoubleClick(False)
self.setItemsExpandable(False)
self.dropAccepted = None
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setDragDropMode(QAbstractItemView.InternalMove)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setEditTriggers(QAbstractItemView.SelectedClicked)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
# self.verticalScrollBar().setSingleStep(5)
self.verticalScrollBar().valueChanged.connect(self.onScrollerValueChange)
################# EVENTS #################
[docs] def updateViewFocused(self, focused):
if self.viewFocused == focused: return
self.viewFocused = focused
self.repaint()
[docs] def focusInEvent(self, evt):
self.updateViewFocused(True)
[docs] def focusOutEvent(self, evt):
# to do: handle context menu
self.updateViewFocused(False)
[docs] def testClickBranchArrow(self, evt, doubleClick):
# if self.underAnimating: return True
pos = evt.pos()
index = self.indexAt(pos)
if not self.model().hasChildren(index): return False
rect = self.visualRect(index)
l, t, h = rect.left(), rect.top(), rect.height()
indentation = self.indentation()
branchRect = QRect(l - indentation + self.treePaddingLeft - self.branchArrowOffset, t, indentation, h)
if branchRect.contains(pos):
recursive = evt.modifiers() == Qt.AltModifier
self.toggleExpand(index, recursive)
return True
if pos.x() < l + self.treePaddingLeft:
newPos = QPointF(l, pos.y())
newPosGlobal = self.mapToGlobal(newPos)
newEvt = QMouseEvent(evt.type(), newPos, newPosGlobal, evt.button(), evt.buttons(), evt.modifiers())
if doubleClick:
super().mouseDoubleClickEvent(newEvt)
else:
super().mousePressEvent(newEvt)
return True
[docs] def mousePressEvent(self, evt):
TreeItemPingOverlay.stopAll()
if self.testClickBranchArrow(evt, False): return
super().mousePressEvent(evt)
[docs] def mouseReleaseEvent(self, evt):
pos = evt.pos()
index = self.indexAt(pos)
rect = self.visualRect(index)
l, t, h = rect.left(), rect.top(), rect.height()
indentation = self.indentation()
branchRect = QRect(l - indentation + self.treePaddingLeft, t, indentation, h)
if branchRect.contains(pos): return
super().mouseReleaseEvent(evt)
[docs] def mouseDoubleClickEvent(self, evt):
if self.testClickBranchArrow(evt, True): return
super().mouseDoubleClickEvent(evt)
[docs] def mouseMoveEvent(self, evt):
self.updateHoveredIndex(self.indexAt(evt.pos()))
if self.dragEnabled(): super().mouseMoveEvent(evt)
[docs] def leaveEvent(self, evt):
super().leaveEvent(evt)
self.updateHoveredIndex(None)
def _getCollapsedParent(self, index):
parents = []
parent = index.parent()
while parent.isValid():
parents.insert(0, parent)
parent = parent.parent()
for idx in parents:
if not self.isExpanded(idx):
return idx
[docs] def keyPressEvent(self, evt):
key = evt.key()
mods = evt.modifiers()
if key == Qt.Key_Right:
# if self.underAnimating: return
model = self.model()
curr = self.currentIndex()
if not curr.isValid():
curr = model.index(0, 0, curr)
if curr.isValid():
self.toggleExpand(curr, mods & Qt.AltModifier)
self.setCurrentIndex(curr)
return
curr = self.currentIndex()
cp = self._getCollapsedParent(curr)
if cp: return self.setCurrentIndex(cp)
if not model.hasChildren(curr) or self.isExpanded(curr):
while True:
curr = self.indexBelow(curr)
if not curr.isValid(): return
if model.hasChildren(curr):
self.setCurrentIndex(curr)
return
else:
self.toggleExpand(curr, mods & Qt.AltModifier)
elif key == Qt.Key_Left:
# if self.underAnimating: return
if not self.selectedIndexes(): return super().keyPressEvent(evt)
model = self.model()
curr = self.currentIndex()
cp = self._getCollapsedParent(curr)
if cp: return self.setCurrentIndex(cp)
if not model.hasChildren(curr) or not self.isExpanded(curr):
parent = curr.parent()
if parent.isValid():
self.setCurrentIndex(parent)
else:
row = curr.row()
if row > 0: self.setCurrentIndex(curr.siblingAtRow(row - 1))
else:
self.toggleExpand(curr, mods & Qt.AltModifier)
elif key == Qt.Key_Delete or (key == Qt.Key_Backspace and mods & Qt.ControlModifier):
model = self.model()
selections = self.selectionModel().selectedIndexes()
selections.sort(key = functools.cmp_to_key(lambda e1, e2: self._checkIsAboveOf(e1, e2) and 1 or -1))
for idx in selections: model.removeRow(idx.row(), idx.parent())
elif key == Qt.Key_Escape:
self.selectionModel().clear()
elif key == Qt.Key_Space:
curr = self.currentIndex()
if not curr.isValid(): return
overlay = TreeItemPingOverlay(self)
overlay.startPing(curr)
overlay.show()
else:
super().keyPressEvent(evt)
[docs] def copySelection(self):
model = self.model()
selectionModel = self.selectionModel()
selections = selectionModel.selectedIndexes()
selections.sort(key = functools.cmp_to_key(lambda e1, e2: self._checkIsAboveOf(e1, e2) and -1 or 1))
# detect children and remove
for i in range(len(selections) - 1, 0, -1):
curr = selections[i]
for j in range(i - 1, -1, -1):
test = selections[j]
if self._checkIsChildOf(curr, test):
del selections[i]
break
clipboard = QApplication.clipboard()
clipboard.setMimeData(model.mimeData(selections))
[docs] def pasteSelection(self):
model = self.model()
selectionModel = self.selectionModel()
toSelections = []
parent = self.currentIndex().parent()
clipboard = QApplication.clipboard()
oldRowCount = model.rowCount(parent)
model.dropMimeData(clipboard.mimeData(), Qt.CopyAction, -1, -1, parent)
newRowCount = model.rowCount(parent)
dropedCount = newRowCount - oldRowCount
if dropedCount > 0:
selectionModel.clear()
self.setCurrentIndex(model.index(newRowCount - 1, 0, parent))
for i in range(newRowCount - dropedCount, newRowCount):
selection = model.index(i, 0, parent)
selectionModel.select(selection, QItemSelectionModel.Select)
while parent.isValid():
self.expandInstant(parent, False)
parent = parent.parent()
[docs] def duplicateSelection(self):
model = self.model()
selectionModel = self.selectionModel()
selections = selectionModel.selectedIndexes()
selections.sort(key = functools.cmp_to_key(lambda e1, e2: self._checkIsAboveOf(e1, e2) and -1 or 1))
# detect children and remove
for i in range(len(selections) - 1, 0, -1):
curr = selections[i]
for j in range(i - 1, -1, -1):
test = selections[j]
if self._checkIsChildOf(curr, test):
del selections[i]
break
selectionModel.clear()
toSelections = []
for selection in selections:
parent = selection.parent()
mimeData = model.mimeData([selection])
model.dropMimeData(mimeData, Qt.CopyAction, -1, -1, parent)
row = model.rowCount(parent) - 1
toSelections.append(model.index(row, 0, parent))
while parent.isValid():
self.expandInstant(parent, False)
parent = parent.parent()
self.setCurrentIndex(toSelections[-1])
for selection in toSelections:
selectionModel.select(selection, QItemSelectionModel.Select)
[docs] def event(self, evt):
super().event(evt)
if evt.type() == QEvent.ShortcutOverride:
key = evt.key()
mods = evt.modifiers()
if key == Qt.Key_A and mods & Qt.ControlModifier:
self.keyPressEvent(evt)
elif key == Qt.Key_C and mods & Qt.ControlModifier:
self.copySelection()
elif key == Qt.Key_V and mods & Qt.ControlModifier:
self.pasteSelection()
elif key == Qt.Key_D and mods & Qt.ControlModifier:
self.duplicateSelection()
################## DRAW ##################
[docs] def paintEvent(self, event):
painter = QPainter(self.viewport())
painter.setClipping(True)
self.drawTree(painter, event.region())
painter.setClipping(False)
self.drawDropIndicator(painter)
[docs] def drawBranches(self, painter, rect, index):
pass # skip default impl in QTreeView
[docs] def drawDropIndicator(self, painter):
if not self.dropAccepted: return
if not self.dropIndicatorRect: return
painter.setPen(self._penDropIndicator)
l = self.dropIndicatorRect.left() + self.treePaddingLeft + 1 # indicatorOffset
r = self.dropIndicatorRect.right()
y = self.dropIndicatorRect.top()
radius = 3
painter.drawEllipse(l-2*radius, y-radius, 2*radius, 2*radius)
painter.drawLine(l, y, r, y)
[docs] def updateHoveredIndex(self, new):
viewport = self.viewport()
old = self.hoveredIndex
if new != old:
self.hoveredIndex = new
if old:
rect = self.visualRect(old)
rect.setLeft(0)
viewport.update(rect)
if new:
rect = self.visualRect(new)
rect.setLeft(0)
viewport.update(rect)
[docs] def updateDropIndicatorRect(self, new):
viewport = self.viewport()
old = self.dropIndicatorRect
if new != old:
self.dropIndicatorRect = new
w, h = self.width(), self.itemHeight
if old:
t = old.top() - round(h / 2)
viewport.update(QRect(0, t, w, h))
if new:
t = new.top() - round(h / 2)
viewport.update(QRect(0, t, w, h))
################## DRAG ##################
[docs] def startDrag(self, supportedActions):
drag = QDrag(self)
selections = self.selectedIndexes()
drag.setMimeData(self.model().mimeData(selections))
drag.exec(Qt.MoveAction)
[docs] def dragEnterEvent(self, evt):
self.dropAccepted = True
evt.acceptProposedAction()
[docs] def dragLeaveEvent(self, evt):
self.updateDropIndicatorRect(None)
self.updateHoveredIndex(None)
[docs] def checkDropable(self, hovered, source):
if source != self: return False
if not hovered.isValid(): return True
selectionModel = self.selectionModel()
if selectionModel.isSelected(hovered): return False
for select in selectionModel.selectedIndexes():
# todo: check root index
if self._checkIsChildOf(hovered, select): return False
return True
[docs] def dragMoveEvent(self, evt):
pos = evt.pos()
hovered = self.indexAt(pos)
newDropIndicatorRect = None
if hovered.isValid():
rect = self.visualRect(hovered)
y, l, t, b = pos.y(), rect.left(), rect.top(), rect.bottom()
if y - t < self.dropIndicatorMargin: # and (y > margin and not self.hoveredIndex.isRoot())
newDropIndicatorRect = QRect(l, t, self.width()-l, 0)
self.dropPosition = -1
self.autoExpandTimer.stop()
elif b - y < self.dropIndicatorMargin:
newDropIndicatorRect = QRect(l, b + 1, self.width()-l, 0)
self.dropPosition = 1
self.autoExpandTimer.stop()
else:
self.autoExpandTimer.start(400)
self.dropPosition = 0
src = evt.source()
dropable = self.checkDropable(hovered, src)
if self.dropAccepted != dropable:
self.dropAccepted = dropable
if dropable: evt.accept()
else: evt.ignore()
if not dropable: newDropIndicatorRect = None
self.updateDropIndicatorRect(newDropIndicatorRect)
self.updateHoveredIndex(hovered)
if src != self: self.repaint()
super().dragMoveEvent(evt)
[docs] def dropEvent(self, evt):
evt.acceptProposedAction()
self.autoExpandTimer.stop()
idx = self.indexAt(evt.pos())
src = evt.source()
if src == self:
model = self.model()
selectionModel = self.selectionModel()
selections = selectionModel.selectedIndexes()
selections.sort(key = functools.cmp_to_key(lambda e1, e2: self._checkIsAboveOf(e1, e2) and 1 or -1))
selectionModel.clear()
dropInsertRow, dropParent = None, None
valid = idx.isValid()
if valid and self.dropPosition == -1:
dropInsertRow = idx.row()
dropParent = idx.parent()
elif valid and self.dropPosition == 1:
dropInsertRow = idx.row() + 1
dropParent = idx.parent()
else:
dropInsertRow = model.rowCount(idx)
dropParent = idx
delayRemoves = [selection for selection in selections if selection.parent() == dropParent and selection.row() < dropInsertRow]
for selection in selections:
mimeData = model.mimeData([selection])
if selection not in delayRemoves: model.removeRow(selection.row(), selection.parent())
model.dropMimeData(mimeData, Qt.CopyAction, dropInsertRow, -1, dropParent)
dropCount = len(selections)
self.setCurrentIndex(model.index(dropInsertRow + dropCount - 1, 0, dropParent))
for i in range(dropInsertRow, dropInsertRow + dropCount): selectionModel.select(model.index(i, 0, dropParent), QItemSelectionModel.Select)
for index in delayRemoves: model.removeRow(index.row(), index.parent())
if valid and self.dropPosition == 0: self.expandInstant(idx, False)
evt.acceptProposedAction()
else:
evt.ignore()
self.updateDropIndicatorRect(None)
################ ANIMATING ################
[docs] def toggleExpand(self, index, recursive):
if self.isExpanded(index):
def _collapse():
if recursive:
self._collapseRecursively(index)
else:
self.collapse(index)
if self.customAnimated:
self._playExpandAnim(index, True, recursive, _collapse)
else:
_collapse()
else:
if recursive:
self.expandRecursively(index)
else:
self.expand(index)
if self.customAnimated: self._playExpandAnim(index, False, recursive)
[docs] def expandInstant(self, index, recursive):
if self.isExpanded(index): return
model = self.model()
height = self.itemHeight
list = self._animChildrenList(index, recursive)
for idx in list: model.setData(idx, height, Qt.SizeHintRole)
if recursive:
self.expandRecursively(index)
else:
self.expand(index)
def _collapseRecursively(self, index):
self.collapse(index)
model = self.model()
count = model.rowCount(index)
for r in range(count): self._collapseRecursively(model.index(r, 0, index))
def _animChildrenList(self, index, includeHidden):
list = []
model = self.model()
count = model.rowCount(index)
for row in range(count):
child = model.index(row, 0, index)
list.append(child)
if self.isExpanded(child) or includeHidden:
list.extend(self._animChildrenList(child, includeHidden))
# if model.hasChildren(child): list.extend(self._animChildrenList)
return list
def _playExpandAnim(self, index, collapse, recursive, onFinish = None):
self.underAnimating = True
self.animElapsed = 0
self.prevIdxStop = 0
self.reverseExpanding = collapse
self.animIdxList = self._animChildrenList(index, recursive and not collapse)
self.onAnimFinish = onFinish
model = self.model()
initHeight = 0
if collapse:
self.collapsingIndex = index
self.animIdxList.reverse()
initHeight = self.itemHeight
for idx in self.animIdxList: model.setData(idx, initHeight, Qt.SizeHintRole)
self.animTimer = QTimer()
self.animTimer.timeout.connect(self._tickExpanding)
self.animTimer.start(self._customAnimTickInterval)
self.prevTickTime = time.time()
def _tickExpanding(self):
curr = time.time()
delta = (curr - self.prevTickTime) * 1000
self.prevTickTime = curr
self.animElapsed += delta
k = clamp(self.animElapsed / self.customAnimDuration, 0, 1)
progress = easeInOutQuad(k)
idxProgress = len(self.animIdxList) * progress
idxStop = floor(idxProgress)
resetHeight, currHeight = None, None
if self.reverseExpanding:
resetHeight, currHeight = 0, round((1 - idxProgress + idxStop) * self.itemHeight)
else:
resetHeight, currHeight = self.itemHeight, round((idxProgress - idxStop) * self.itemHeight)
model = self.model()
if progress < 1:
for i in range(self.prevIdxStop, idxStop): model.setData(self.animIdxList[i], resetHeight, Qt.SizeHintRole)
model.setData(self.animIdxList[idxStop], currHeight, Qt.SizeHintRole)
self.prevIdxStop = idxStop
# model.dataChanged.emit(self.animIdxList[self.prevIdxStop], self.animIdxList[self.idxStop], [Qt.SizeHintRole])
else:
for i in range(self.prevIdxStop, len(self.animIdxList)): model.setData(self.animIdxList[i], self.itemHeight, Qt.SizeHintRole)
# model.setData(self.animIdxList[-1], self.itemHeight, Qt.SizeHintRole)
self.underAnimating = False
self.animTimer.stop()
self.animTimer.deleteLater()
self.animElapsed = None
self.animIdxList = None
self.prevIdxStop = None
self.prevTickTime = None
self.collapsingIndex = None
if self.onAnimFinish:
self.onAnimFinish()
self.onAnimFinish = None
[docs] def expandHovered(self):
if not self.hoveredIndex or not self.hoveredIndex.isValid(): return
if not self.model().hasChildren(self.hoveredIndex): return
for selection in self.selectedIndexes():
if self.hoveredIndex == selection: return
if self._checkIsChildOf(self.hoveredIndex, selection): return
if self.isExpanded(self.hoveredIndex): return
self.toggleExpand(self.hoveredIndex, False)
# self.expandInstant(self.hoveredIndex, False)
[docs] def pingItem(self, index):
pass
#################### UTILS ####################
def _rowCountRecursive(self, index, model):
count = model.rowCount(index)
for row in range(count): count += self._rowCountRecursive(model.index(row, 0, index), model)
return count
def _flatRowNumber(self, index, model):
if not index.isValid(): return -1
result = index.row() + 1
parent = index.parent()
for row in range(result - 1): result += self._rowCountRecursive(model.index(row, 0, parent), model)
return result + self._flatRowNumber(parent, model)
def _visibleRowCountRecursive(self, index, model):
count = model.rowCount(index)
if not self.isExpanded(index) or self.collapsingIndex == index: return 0
for row in range(count): count += self._visibleRowCountRecursive(model.index(row, 0, index), model)
return count
def _flatVisibleRowNumber(self, index, model):
if not index.isValid(): return -1
result = index.row() + 1
parent = index.parent()
for row in range(result - 1): result += self._visibleRowCountRecursive(model.index(row, 0, parent), model)
return result + self._flatVisibleRowNumber(parent, model)
def _checkIsChildOf(self, index, test):
parent = index.parent()
while parent.isValid():
if parent == test: return True
parent = parent.parent()
return False
def _checkIsAboveOf(self, index, test):
seq1 = self._getRowSequence(index)
seq2 = self._getRowSequence(test)
len1 = len(seq1)
len2 = len(seq2)
for i in range(min(len1, len2)):
row1 = seq1[i]
row2 = seq2[i]
if row1 < row2: return True
if row1 > row2: return False
return len1 < len2
def _getRowSequence(self, index):
seq = []
curr = index
while curr.isValid():
seq.insert(0, curr.row())
curr = curr.parent()
return seq
[docs]def runTreeDemo():
view = TreeView()
view.setWindowFlags(Qt.Window)
model = QStandardItemModel()
for i in range(5):
n = QStandardItem(f'Item_{i}')
n.setData(getThemePixmap('entity.png').scaled(16, 16), Qt.DecorationRole)
model.appendRow(n)
for j in range(4):
c = QStandardItem(f'Child_{j}')
c.setData(getThemePixmap('entity.png').scaled(16, 16), Qt.DecorationRole)
n.appendRow(c)
for k in range(4):
s = QStandardItem(f'Subchild_{k}')
# s.setData(getThemePixmap('entity.png').scaled(16, 16), Qt.DecorationRole)
c.appendRow(s)
# model.dataChanged.connect(lambda i1, i2, r: print(r))
view.setModel(model)
view.show()