d455da9aec
This namespace appears to be valid in PySide2/PyQt5 5.12+ so we may as well migrate to the newer namespace ourselves.
471 lines
16 KiB
Python
471 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from ..Qt import QtGui, QtCore, QT_LIB
|
|
from ..graphicsItems.ROI import ROI
|
|
from ..graphicsItems.ViewBox import ViewBox
|
|
from ..graphicsItems.GridItem import GridItem
|
|
|
|
import importlib
|
|
ui_template = importlib.import_module(
|
|
f'.CanvasTemplate_{QT_LIB.lower()}', package=__package__)
|
|
|
|
import numpy as np
|
|
from .. import debug
|
|
import weakref
|
|
import gc
|
|
from .CanvasManager import CanvasManager
|
|
from .CanvasItem import CanvasItem, GroupCanvasItem
|
|
|
|
translate = QtCore.QCoreApplication.translate
|
|
|
|
class Canvas(QtGui.QWidget):
|
|
|
|
sigSelectionChanged = QtCore.Signal(object, object)
|
|
sigItemTransformChanged = QtCore.Signal(object, object)
|
|
sigItemTransformChangeFinished = QtCore.Signal(object, object)
|
|
|
|
def __init__(self, parent=None, allowTransforms=True, hideCtrl=False, name=None):
|
|
QtGui.QWidget.__init__(self, parent)
|
|
self.ui = ui_template.Ui_Form()
|
|
self.ui.setupUi(self)
|
|
self.view = ViewBox()
|
|
self.ui.view.setCentralItem(self.view)
|
|
self.itemList = self.ui.itemList
|
|
self.itemList.setSelectionMode(self.itemList.SelectionMode.ExtendedSelection)
|
|
self.allowTransforms = allowTransforms
|
|
self.multiSelectBox = SelectBox()
|
|
self.view.addItem(self.multiSelectBox)
|
|
self.multiSelectBox.hide()
|
|
self.multiSelectBox.setZValue(1e6)
|
|
self.ui.mirrorSelectionBtn.hide()
|
|
self.ui.reflectSelectionBtn.hide()
|
|
self.ui.resetTransformsBtn.hide()
|
|
|
|
self.redirect = None ## which canvas to redirect items to
|
|
self.items = []
|
|
|
|
self.view.setAspectLocked(True)
|
|
|
|
grid = GridItem()
|
|
self.grid = CanvasItem(grid, name='Grid', movable=False)
|
|
self.addItem(self.grid)
|
|
|
|
self.hideBtn = QtGui.QPushButton('>', self)
|
|
self.hideBtn.setFixedWidth(20)
|
|
self.hideBtn.setFixedHeight(20)
|
|
self.ctrlSize = 200
|
|
self.sizeApplied = False
|
|
self.hideBtn.clicked.connect(self.hideBtnClicked)
|
|
self.ui.splitter.splitterMoved.connect(self.splitterMoved)
|
|
|
|
self.ui.itemList.itemChanged.connect(self.treeItemChanged)
|
|
self.ui.itemList.sigItemMoved.connect(self.treeItemMoved)
|
|
self.ui.itemList.itemSelectionChanged.connect(self.treeItemSelected)
|
|
self.ui.autoRangeBtn.clicked.connect(self.autoRange)
|
|
self.ui.redirectCheck.toggled.connect(self.updateRedirect)
|
|
self.ui.redirectCombo.currentIndexChanged.connect(self.updateRedirect)
|
|
self.multiSelectBox.sigRegionChanged.connect(self.multiSelectBoxChanged)
|
|
self.multiSelectBox.sigRegionChangeFinished.connect(self.multiSelectBoxChangeFinished)
|
|
self.ui.mirrorSelectionBtn.clicked.connect(self.mirrorSelectionClicked)
|
|
self.ui.reflectSelectionBtn.clicked.connect(self.reflectSelectionClicked)
|
|
self.ui.resetTransformsBtn.clicked.connect(self.resetTransformsClicked)
|
|
|
|
self.resizeEvent()
|
|
if hideCtrl:
|
|
self.hideBtnClicked()
|
|
|
|
if name is not None:
|
|
self.registeredName = CanvasManager.instance().registerCanvas(self, name)
|
|
self.ui.redirectCombo.setHostName(self.registeredName)
|
|
|
|
self.menu = QtGui.QMenu()
|
|
remAct = QtGui.QAction(translate("Context Menu", "Remove item"), self.menu)
|
|
remAct.triggered.connect(self.removeClicked)
|
|
self.menu.addAction(remAct)
|
|
self.menu.remAct = remAct
|
|
self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent
|
|
|
|
def splitterMoved(self):
|
|
self.resizeEvent()
|
|
|
|
def hideBtnClicked(self):
|
|
ctrlSize = self.ui.splitter.sizes()[1]
|
|
if ctrlSize == 0:
|
|
cs = self.ctrlSize
|
|
w = self.ui.splitter.size().width()
|
|
if cs > w:
|
|
cs = w - 20
|
|
self.ui.splitter.setSizes([w-cs, cs])
|
|
self.hideBtn.setText('>')
|
|
else:
|
|
self.ctrlSize = ctrlSize
|
|
self.ui.splitter.setSizes([100, 0])
|
|
self.hideBtn.setText('<')
|
|
self.resizeEvent()
|
|
|
|
def autoRange(self):
|
|
self.view.autoRange()
|
|
|
|
def resizeEvent(self, ev=None):
|
|
if ev is not None:
|
|
super().resizeEvent(ev)
|
|
self.hideBtn.move(self.ui.view.size().width() - self.hideBtn.width(), 0)
|
|
|
|
if not self.sizeApplied:
|
|
self.sizeApplied = True
|
|
s = int( min(self.width(), max(100, min(200, self.width()//4))) )
|
|
s2 = self.width()-s
|
|
self.ui.splitter.setSizes([s2, s])
|
|
|
|
def updateRedirect(self, *args):
|
|
### Decide whether/where to redirect items and make it so
|
|
cname = str(self.ui.redirectCombo.currentText())
|
|
man = CanvasManager.instance()
|
|
if self.ui.redirectCheck.isChecked() and cname != '':
|
|
redirect = man.getCanvas(cname)
|
|
else:
|
|
redirect = None
|
|
|
|
if self.redirect is redirect:
|
|
return
|
|
|
|
self.redirect = redirect
|
|
if redirect is None:
|
|
self.reclaimItems()
|
|
else:
|
|
self.redirectItems(redirect)
|
|
|
|
def redirectItems(self, canvas):
|
|
for i in self.items:
|
|
if i is self.grid:
|
|
continue
|
|
li = i.listItem
|
|
parent = li.parent()
|
|
if parent is None:
|
|
tree = li.treeWidget()
|
|
if tree is None:
|
|
print("Skipping item", i, i.name)
|
|
continue
|
|
tree.removeTopLevelItem(li)
|
|
else:
|
|
parent.removeChild(li)
|
|
canvas.addItem(i)
|
|
|
|
def reclaimItems(self):
|
|
items = self.items
|
|
self.items = [self.grid]
|
|
items.remove(self.grid)
|
|
|
|
for i in items:
|
|
i.canvas.removeItem(i)
|
|
self.addItem(i)
|
|
|
|
def treeItemChanged(self, item, col):
|
|
try:
|
|
citem = item.canvasItem()
|
|
except AttributeError:
|
|
return
|
|
if item.checkState(0) == QtCore.Qt.CheckState.Checked:
|
|
for i in range(item.childCount()):
|
|
item.child(i).setCheckState(0, QtCore.Qt.CheckState.Checked)
|
|
citem.show()
|
|
else:
|
|
for i in range(item.childCount()):
|
|
item.child(i).setCheckState(0, QtCore.Qt.CheckState.Unchecked)
|
|
citem.hide()
|
|
|
|
def treeItemSelected(self):
|
|
sel = self.selectedItems()
|
|
|
|
if len(sel) == 0:
|
|
return
|
|
|
|
multi = len(sel) > 1
|
|
for i in self.items:
|
|
## updated the selected state of every item
|
|
i.selectionChanged(i in sel, multi)
|
|
|
|
if len(sel)==1:
|
|
self.multiSelectBox.hide()
|
|
self.ui.mirrorSelectionBtn.hide()
|
|
self.ui.reflectSelectionBtn.hide()
|
|
self.ui.resetTransformsBtn.hide()
|
|
elif len(sel) > 1:
|
|
self.showMultiSelectBox()
|
|
|
|
self.sigSelectionChanged.emit(self, sel)
|
|
|
|
def selectedItems(self):
|
|
"""
|
|
Return list of all selected canvasItems
|
|
"""
|
|
return [item.canvasItem() for item in self.itemList.selectedItems() if item.canvasItem() is not None]
|
|
|
|
def selectItem(self, item):
|
|
li = item.listItem
|
|
self.itemList.setCurrentItem(li)
|
|
|
|
def showMultiSelectBox(self):
|
|
## Get list of selected canvas items
|
|
items = self.selectedItems()
|
|
|
|
rect = self.view.itemBoundingRect(items[0].graphicsItem())
|
|
for i in items:
|
|
if not i.isMovable(): ## all items in selection must be movable
|
|
return
|
|
br = self.view.itemBoundingRect(i.graphicsItem())
|
|
rect = rect|br
|
|
|
|
self.multiSelectBox.blockSignals(True)
|
|
self.multiSelectBox.setPos([rect.x(), rect.y()])
|
|
self.multiSelectBox.setSize(rect.size())
|
|
self.multiSelectBox.setAngle(0)
|
|
self.multiSelectBox.blockSignals(False)
|
|
|
|
self.multiSelectBox.show()
|
|
|
|
self.ui.mirrorSelectionBtn.show()
|
|
self.ui.reflectSelectionBtn.show()
|
|
self.ui.resetTransformsBtn.show()
|
|
|
|
def mirrorSelectionClicked(self):
|
|
for ci in self.selectedItems():
|
|
ci.mirrorY()
|
|
self.showMultiSelectBox()
|
|
|
|
def reflectSelectionClicked(self):
|
|
for ci in self.selectedItems():
|
|
ci.mirrorXY()
|
|
self.showMultiSelectBox()
|
|
|
|
def resetTransformsClicked(self):
|
|
for i in self.selectedItems():
|
|
i.resetTransformClicked()
|
|
self.showMultiSelectBox()
|
|
|
|
def multiSelectBoxChanged(self):
|
|
self.multiSelectBoxMoved()
|
|
|
|
def multiSelectBoxChangeFinished(self):
|
|
for ci in self.selectedItems():
|
|
ci.applyTemporaryTransform()
|
|
ci.sigTransformChangeFinished.emit(ci)
|
|
|
|
def multiSelectBoxMoved(self):
|
|
transform = self.multiSelectBox.getGlobalTransform()
|
|
for ci in self.selectedItems():
|
|
ci.setTemporaryTransform(transform)
|
|
ci.sigTransformChanged.emit(ci)
|
|
|
|
def addGraphicsItem(self, item, **opts):
|
|
"""Add a new GraphicsItem to the scene at pos.
|
|
Common options are name, pos, scale, and z
|
|
"""
|
|
citem = CanvasItem(item, **opts)
|
|
item._canvasItem = citem
|
|
self.addItem(citem)
|
|
return citem
|
|
|
|
def addGroup(self, name, **kargs):
|
|
group = GroupCanvasItem(name=name)
|
|
self.addItem(group, **kargs)
|
|
return group
|
|
|
|
def addItem(self, citem):
|
|
"""
|
|
Add an item to the canvas.
|
|
"""
|
|
|
|
## Check for redirections
|
|
if self.redirect is not None:
|
|
name = self.redirect.addItem(citem)
|
|
self.items.append(citem)
|
|
return name
|
|
|
|
if not self.allowTransforms:
|
|
citem.setMovable(False)
|
|
|
|
citem.sigTransformChanged.connect(self.itemTransformChanged)
|
|
citem.sigTransformChangeFinished.connect(self.itemTransformChangeFinished)
|
|
citem.sigVisibilityChanged.connect(self.itemVisibilityChanged)
|
|
|
|
|
|
## Determine name to use in the item list
|
|
name = citem.opts['name']
|
|
if name is None:
|
|
name = 'item'
|
|
newname = name
|
|
|
|
## If name already exists, append a number to the end
|
|
## NAH. Let items have the same name if they really want.
|
|
#c=0
|
|
#while newname in self.items:
|
|
#c += 1
|
|
#newname = name + '_%03d' %c
|
|
#name = newname
|
|
|
|
## find parent and add item to tree
|
|
insertLocation = 0
|
|
#print "Inserting node:", name
|
|
|
|
|
|
## determine parent list item where this item should be inserted
|
|
parent = citem.parentItem()
|
|
if parent in (None, self.view.childGroup):
|
|
parent = self.itemList.invisibleRootItem()
|
|
else:
|
|
parent = parent.listItem
|
|
|
|
## set Z value above all other siblings if none was specified
|
|
siblings = [parent.child(i).canvasItem() for i in range(parent.childCount())]
|
|
z = citem.zValue()
|
|
if z is None:
|
|
zvals = [i.zValue() for i in siblings]
|
|
if parent is self.itemList.invisibleRootItem():
|
|
if len(zvals) == 0:
|
|
z = 0
|
|
else:
|
|
z = max(zvals)+10
|
|
else:
|
|
if len(zvals) == 0:
|
|
z = parent.canvasItem().zValue()
|
|
else:
|
|
z = max(zvals)+1
|
|
citem.setZValue(z)
|
|
|
|
## determine location to insert item relative to its siblings
|
|
for i in range(parent.childCount()):
|
|
ch = parent.child(i)
|
|
zval = ch.canvasItem().graphicsItem().zValue() ## should we use CanvasItem.zValue here?
|
|
if zval < z:
|
|
insertLocation = i
|
|
break
|
|
else:
|
|
insertLocation = i+1
|
|
|
|
node = QtGui.QTreeWidgetItem([name])
|
|
flags = node.flags() | QtCore.Qt.ItemFlag.ItemIsUserCheckable | QtCore.Qt.ItemFlag.ItemIsDragEnabled
|
|
if not isinstance(citem, GroupCanvasItem):
|
|
flags = flags & ~QtCore.Qt.ItemFlag.ItemIsDropEnabled
|
|
node.setFlags(flags)
|
|
if citem.opts['visible']:
|
|
node.setCheckState(0, QtCore.Qt.CheckState.Checked)
|
|
else:
|
|
node.setCheckState(0, QtCore.Qt.CheckState.Unchecked)
|
|
|
|
node.name = name
|
|
parent.insertChild(insertLocation, node)
|
|
|
|
citem.name = name
|
|
citem.listItem = node
|
|
node.canvasItem = weakref.ref(citem)
|
|
self.items.append(citem)
|
|
|
|
ctrl = citem.ctrlWidget()
|
|
ctrl.hide()
|
|
self.ui.ctrlLayout.addWidget(ctrl)
|
|
|
|
## inform the canvasItem that its parent canvas has changed
|
|
citem.setCanvas(self)
|
|
|
|
## Autoscale to fit the first item added (not including the grid).
|
|
if len(self.items) == 2:
|
|
self.autoRange()
|
|
|
|
return citem
|
|
|
|
def treeItemMoved(self, item, parent, index):
|
|
##Item moved in tree; update Z values
|
|
if parent is self.itemList.invisibleRootItem():
|
|
item.canvasItem().setParentItem(self.view.childGroup)
|
|
else:
|
|
item.canvasItem().setParentItem(parent.canvasItem())
|
|
siblings = [parent.child(i).canvasItem() for i in range(parent.childCount())]
|
|
|
|
zvals = [i.zValue() for i in siblings]
|
|
zvals.sort(reverse=True)
|
|
|
|
for i in range(len(siblings)):
|
|
item = siblings[i]
|
|
item.setZValue(zvals[i])
|
|
|
|
def itemVisibilityChanged(self, item):
|
|
listItem = item.listItem
|
|
checked = listItem.checkState(0) == QtCore.Qt.CheckState.Checked
|
|
vis = item.isVisible()
|
|
if vis != checked:
|
|
if vis:
|
|
listItem.setCheckState(0, QtCore.Qt.CheckState.Checked)
|
|
else:
|
|
listItem.setCheckState(0, QtCore.Qt.CheckState.Unchecked)
|
|
|
|
def removeItem(self, item):
|
|
if isinstance(item, QtGui.QTreeWidgetItem):
|
|
item = item.canvasItem()
|
|
|
|
if isinstance(item, CanvasItem):
|
|
item.setCanvas(None)
|
|
listItem = item.listItem
|
|
listItem.canvasItem = None
|
|
item.listItem = None
|
|
self.itemList.removeTopLevelItem(listItem)
|
|
self.items.remove(item)
|
|
ctrl = item.ctrlWidget()
|
|
ctrl.hide()
|
|
self.ui.ctrlLayout.removeWidget(ctrl)
|
|
ctrl.setParent(None)
|
|
else:
|
|
if hasattr(item, '_canvasItem'):
|
|
self.removeItem(item._canvasItem)
|
|
else:
|
|
self.view.removeItem(item)
|
|
|
|
gc.collect()
|
|
|
|
def clear(self):
|
|
while len(self.items) > 0:
|
|
self.removeItem(self.items[0])
|
|
|
|
def addToScene(self, item):
|
|
self.view.addItem(item)
|
|
|
|
def removeFromScene(self, item):
|
|
self.view.removeItem(item)
|
|
|
|
def listItems(self):
|
|
"""Return a dictionary of name:item pairs"""
|
|
return self.items
|
|
|
|
def getListItem(self, name):
|
|
return self.items[name]
|
|
|
|
def itemTransformChanged(self, item):
|
|
self.sigItemTransformChanged.emit(self, item)
|
|
|
|
def itemTransformChangeFinished(self, item):
|
|
self.sigItemTransformChangeFinished.emit(self, item)
|
|
|
|
def itemListContextMenuEvent(self, ev):
|
|
self.menuItem = self.itemList.itemAt(ev.pos())
|
|
self.menu.popup(ev.globalPos())
|
|
|
|
def removeClicked(self):
|
|
for item in self.selectedItems():
|
|
self.removeItem(item)
|
|
self.menuItem = None
|
|
import gc
|
|
gc.collect()
|
|
|
|
|
|
class SelectBox(ROI):
|
|
def __init__(self, scalable=False):
|
|
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
|
|
ROI.__init__(self, [0,0], [1,1])
|
|
center = [0.5, 0.5]
|
|
|
|
if scalable:
|
|
self.addScaleHandle([1, 1], center, lockAspect=True)
|
|
self.addScaleHandle([0, 0], center, lockAspect=True)
|
|
self.addRotateHandle([0, 1], center)
|
|
self.addRotateHandle([1, 0], center)
|