# -*- python -*-
#
# OpenAlea.Visualea: OpenAlea graphical user interface
#
# Copyright 2006-2023 INRIA - CIRAD - INRA
1#
# File author(s): Daniel Barbeau <daniel.barbeau@sophia.inria.fr>
#
# Distributed under the Cecill-C License.
# See accompanying file LICENSE.txt or copy at
# http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.html
#
# OpenAlea WebSite : http://openalea.rtfd.io
#
###############################################################################
__license__ = "Cecill-C"
__revision__ = " $Id$ "
import weakref
from qtpy import QtCore, QtGui, QtWidgets
##################################################################################
# Some PYQT versions don't know about some QGraphicsItem flags or enums yet #
# even though the underlying Qt knows about it (.sip files not up-to-date #
# when building PyQt). The differences between PYQT_VERSION 4.6.2 and 4.7.3 are: #
##################################################################################
unportableFlags = ['ItemSendsGeometryChanges', 'ItemUsesExtendedStyleOption',
'ItemScenePositionHasChanged', 'ItemAcceptsInputMethod', 'ItemSendsScenePositionChanges',
'ItemHasNoContents', 'ItemNegativeZStacksBehindParent', 'ItemIsPanel']
unportableEnums = ["ItemScenePositionHasChanged", "ItemPositionHasChanged"]
__dict__ = globals()
__badsymbols = []
for f in unportableFlags+unportableEnums:
try:
__dict__[f] = getattr(QtWidgets.QGraphicsItem, f)
except Exception as e:
__badsymbols.append(f)
continue
if len(__badsymbols):
print("""
The following QtWidgets.QGraphicsItem enums and flags were not found.
These are probably used by a graph view. They might exist but your version
of PyQt is too old so openalea.grapheditor.qtutils will try to compensate
them:
%s
""" % (__badsymbols))
# if it's just the PyQt Version that is too old we have a hack as
# the qt flag exists but is simply not exposed.
# this is not bug free: if the Qt guys change the enum order, we're wrecked.
"""
if hasattr(QtCore, 'PYQT_VERSION') and QtCore.PYQT_VERSION < 0x040703 and QtCore.PYQT_VERSION >= 0x040600:
# -- flags --
ItemSendsGeometryChanges = 0x800
ItemSendsScenePositionChanges = 0xffff
# -- enums --
ItemScenePositionHasChanged = 0x1b
ItemPositionHasChanged = 0x9
"""
#####################################################
# A Global to know if using QGraphicsEffect is safe #
#####################################################
safeEffects = False #QtCore.QT_VERSION >= 0x40600 and QtCore.PYQT_VERSION > 0x40704
#######################################
# A very simple signal implementation #
#######################################
[docs]
class AleaSignal(object):
def __init__(self, *types):
self.types = types
self.callbacks = weakref.WeakKeyDictionary()
[docs]
def connect(self, callback):
self.callbacks[callback] = callback
[docs]
def disconnect(self, callback=None):
if callback is not None:
del self.callbacks[callback]
else:
self.callbacks.clear()
[docs]
def emit(self, *args):
# TODO: do type checking?
callbacks = list(self.callbacks.values())[:]
for c in callbacks:
c(*args)
###############################################
# A QGraphicsWidget that looks like a post-it #
###############################################
[docs]
class MemoRects(QtWidgets.QGraphicsRectItem):
__handleSize = 7.5
__defaultColor = QtGui.QColor(250, 250, 100)
def __init__(self, rect, parent=None):
QtWidgets.QGraphicsRectItem.__init__(self, rect, parent)
self.__resizing = False
self.__handlePoly = QtGui.QPolygonF([QtCore.QPointF(0, -self.__handleSize),
QtCore.QPointF(0, 0),
QtCore.QPointF(-self.__handleSize,0)])
self.setFlag(QtWidgets.QGraphicsItem.ItemStacksBehindParent)
# -- handle --
self.__handlePos = QtCore.QPointF(0,0)
# -- header --
self.__headerContentRect = None
self.__headerRect = None
# -- color --
self.__color = None
self.__darkerColor = None
self.__shadowColor = None
self.setColor(self.__defaultColor.darker(110))
# -- optionnal cosmetics --
if safeEffects:
fx = QtWidgets.QGraphicsDropShadowEffect()
fx.setOffset(2,2)
fx.setBlurRadius(5)
self.setGraphicsEffect(fx)
[docs]
def setColor(self, color):
self.__color = color
self.__darkerColor = color.darker(140)
self.__shadowColor = color.darker(200)
self.update()
def __moveHandleBottomRightTo(self, point):
delta = point - self.__handlePos
self.__handlePoly.translate(delta.x(), delta.y())
self.__handlePos = point
[docs]
def mousePressEvent(self, event):
pos = event.pos()
bottomRight = self.boundingRect().bottomRight()
x, y = bottomRight.x(), bottomRight.y()
rect = QtCore.QRectF(x-5, y-5, x+5, y+5)
if self.__handlePoly.containsPoint(pos, QtCore.Qt.OddEvenFill):
self.__resizing = True
else:
QtWidgets.QGraphicsRectItem.mousePressEvent(self, event)
[docs]
def mouseMoveEvent(self, event):
if self.__resizing:
delta = event.pos() - event.lastPos()
bottomRight = self.boundingRect().bottomRight() + delta
newRect = QtCore.QRectF(0.,0., bottomRight.x(), bottomRight.y())
if newRect.contains(self.__headerContentRect.adjusted(0,0,0,self.__handleSize)):
self.setRect(newRect)
self.__headerRect = QtCore.QRectF(0.,0.,
bottomRight.x(),
self.__headerContentRect.height())
self.__moveHandleBottomRightTo(newRect.bottomRight())
else:
QtWidgets.QGraphicsRectItem.mouseMoveEvent(self, event)
[docs]
def mouseReleaseEvent(self, event):
if self.__resizing:
self.__resizing = False
else:
QtWidgets.QGraphicsRectItem.mouseReleaseEvent(self, event)
[docs]
def paint(self, painter, paintOptions, widget):
myRect = self.boundingRect()
painter.fillRect(self.__headerRect, self.__darkerColor)
gradTop = self.__headerRect.bottomLeft()
gradBot = gradTop + QtCore.QPointF(0,4)
gradient = QtGui.QLinearGradient(gradTop,gradBot)
gradient.setColorAt(0, self.__shadowColor)
gradient.setColorAt(1, self.__color)
brush = QtGui.QBrush(gradient)
bottomRect = myRect.adjusted(0,self.__headerRect.bottom(),0,0)
painter.fillRect(bottomRect, brush)
if not safeEffects:
oldPen = painter.pen()
pen = QtGui.QPen()
pen.setColor(QtGui.QColor(10,10,10,100))
pen.setWidth(1)
painter.setPen(pen)
painter.drawRect(myRect.adjusted(0.5,0.5,-0.5,-0.5))
painter.setPen(oldPen)
painter.setBrush(QtGui.QBrush(self.__darkerColor))
painter.drawConvexPolygon(self.__handlePoly)
###############################################
# A Vanishing GraphicsItem mixing. Appears on #
# hover in and vanishes on hover out #
###############################################
[docs]
class AleaQGraphicsVanishingMixin:
__baseOpacity = 0.01
__numFrames = 24
def __init__(self, vanishingTime=500, baseOpacity=__baseOpacity):
self.setAcceptHoverEvents(True)
self.setOpacity(self.__baseOpacity)
self.__vanEnabled = True
self.__vanishingTime = vanishingTime
self.__timer = QtCore.QTimeLine(vanishingTime)
self.__timer.setFrameRange(0, self.__numFrames)
self.__timer.frameChanged.connect(self.__onFrameChanged)
self.__lowOpacity = baseOpacity
self.__toSleep = False
self.__sleeping = False
self.setOpacity(self.__lowOpacity)
[docs]
def setBaseOpactity(self, opacity):
self.__lowOpacity = opacity
def __onFrameChanged(self, frame):
opacity = frame*(1.0-self.__lowOpacity)/self.__numFrames + self.__lowOpacity
if opacity == self.__lowOpacity and self.__toSleep:
self.__sleeping = True
self.setOpacity(opacity)
[docs]
def setVanishingEnabled(self, val):
self.__vanEnabled = val
if val == True:
if self.__timer.state() == QtCore.QTimeLine.Running:
if self.__timer.direction == QtCore.QTimeLine.Backward:
self.appear()
else:
self.disappear()
else:
self.setOpacity(1)
[docs]
def setVanishingTime(self, time):
self.__vanishingTime = time
self.__timer.setDuration(time)
[docs]
def vanishingTime(self):
return self.__vanishingTime
[docs]
def wakeup(self):
self.__sleeping = False
[docs]
def sleep(self):
self.__sleeping = True
[docs]
def setSleepOnDisappear(self, val):
self.__toSleep = val
[docs]
def disappear(self):
if not self.__vanEnabled:
return
if self.opacity() == self.__lowOpacity:
return
state = self.__timer.state()
self.__timer.setDuration(self.__vanishingTime)
if ((state == QtCore.QTimeLine.Running) and
(self.__timer.direction() == QtCore.QTimeLine.Forward)):
self.__timer.setDirection(QtCore.QTimeLine.Backward)
elif (state == QtCore.QTimeLine.NotRunning):
self.__timer.setCurrentTime(self.__timer.duration())
self.__timer.setDirection(QtCore.QTimeLine.Backward)
self.__timer.start()
[docs]
def appear(self):
if self.__sleeping:
return
if not self.__vanEnabled:
return
if self.opacity() == 1.0:
return
state = self.__timer.state()
self.__timer.setDuration(self.__vanishingTime//2)
if state == QtCore.QTimeLine.Running and \
self.__timer.direction() == QtCore.QTimeLine.Backward:
self.__timer.setDirection(QtCore.QTimeLine.Forward)
elif state == QtCore.QTimeLine.NotRunning:
self.__timer.setCurrentTime(0)
self.__timer.setDirection(QtCore.QTimeLine.Forward)
self.__timer.start()
[docs]
def hoverEnterEvent(self, event):
self.appear()
[docs]
def hoverLeaveEvent(self, event):
self.disappear()
###########################################
# A button like mixin for qgraphics items #
###########################################
########################
# A horizontal toolbar #
########################
#############################################################
# Customized Qt Classes that can be reused in other places. #
#############################################################
[docs]
class AleaQGraphicsRoundedRectItem(QtWidgets.QGraphicsRectItem):
def __init__(self, radius, cache=False, *args):
QtWidgets.QGraphicsRectItem.__init__(self, *args)
self.__radius = radius
self.__useCachedPath = cache
self.__cachedPath = None
if cache:
self.refresh_cached_shape()
[docs]
def shape(self):
if self.__useCachedPath:
return self.__cachedPath
return self._make_path()
def _make_path(self, pen=None):
if pen == None:
pen = self.pen()
else:
self.setPen(pen)
penWidth = pen.widthF()
path = QtGui.QPainterPath()
path.addRoundedRect(self.rect().adjusted(penWidth, penWidth,
-penWidth, -penWidth),
self.__radius, self.__radius)
return path
[docs]
def refresh_cached_shape(self):
self.__cachedPath = self._make_path()
[docs]
def paint(self, painter, options, widget):
painter.setPen(self.pen())
painter.setBrush(self.brush())
painter.drawPath(self.shape())
[docs]
class AleaQGraphicsEmitingTextItem(QtWidgets.QGraphicsTextItem):
"""A QtWidgets.QGraphicsTextItem that emits geometryModified whenever
its geometry can have changed."""
######################
# The Missing Signal #
######################
geometryModified = QtCore.Signal(QtCore.QRectF)
def __init__(self, *args, **kwargs):
QtWidgets.QGraphicsTextItem.__init__(self, *args, **kwargs)
self.document().contentsChanged.connect(self.__onDocumentChanged)
self.hoveredIn = AleaSignal()
self.hoveredOut = AleaSignal()
def __onDocumentChanged(self):
self.geometryModified.emit(self.boundingRect())
[docs]
def hoverEnterEvent(self, event):
QtWidgets.QGraphicsTextItem.hoverEnterEvent(self, event)
self.hoveredIn.emit(event)
[docs]
def hoverLeaveEvent(self, event):
QtWidgets.QGraphicsTextItem.hoverLeaveEvent(self, event)
self.hoveredOut.emit(event)
###########
# Buttons #
###########
[docs]
class AleaQGraphicsColorWheel(AleaQGraphicsVanishingMixin, AleaQGraphicsButtonMixin, QtWidgets.QGraphicsEllipseItem):
_stopHues = range(0,360,360//12)
_stopPos = [i*1.0/12 for i in range(12)]
######################
# The Missing Signal #
######################
def __init__(self, radius=3.0, parent=None):
QtWidgets.QGraphicsEllipseItem.__init__(self, 0,0,radius*2, radius*2, parent)
AleaQGraphicsVanishingMixin.__init__(self)
AleaQGraphicsButtonMixin.__init__(self)
self.colorChanged = AleaSignal(QtGui.QColor)
gradient = QtGui.QConicalGradient()
gradient.setCenter(radius, radius)
for hue, pos in zip(self._stopHues, self._stopPos):
gradient.setColorAt(pos, QtGui.QColor.fromHsv(hue, 255, 255, 255))
self.setBrush(QtGui.QBrush(gradient))
def _onButtonPressed(self, event):
color = QtWidgets.QColorDialog.getColor(QtCore.Qt.white, event.widget())
if color.isValid():
self.colorChanged.emit(color)
#####################################
# Simple layouts for QGraphicsItems #
#####################################
[docs]
class Layout:
def __init__(self, parent=None, final=None, margins=(0.,0.,0.,0.),
innerMargins=(0.,0.), center=False, mins=(0.,0.)):
self._parent = parent
self._final = final
self._x1, self._x2, self._y1, self._y2 = margins
self._ix1, self._iy1 = innerMargins
self._center = center
self._minWidth, self._minHeight = mins
self._boundCache = None
self._items = []
if parent:
parent.addItem(self)
[docs]
def clear(self):
self._items = []
self._final = None
def __add__(self, other):
return self._items + other._items
def __iter__(self):
return self._items.__iter__()
def __contains__(self, item):
return self._items.__contains__(item)
[docs]
def center(self, val):
self._center = val
[docs]
def sort(self, cmp):
self._items.sort(cmp)
[docs]
def addFinalItem(self, item):
self._final = item
[docs]
def addItem(self, item):
self._items.append(item)
[docs]
def removeItem(self, item):
self._items.remove(item)
[docs]
def boundingRect(self, force=True):
if force or self._boundCache is None:
x, y, w, h = self._boundingRect()
w = max(w, self._minWidth)
h = max(h, self._minHeight)
self._boundCache = QtCore.QRectF(x, y, w, h)
if self._parent:
self._parent.boundCache = None
return self._boundCache
def _boundingRect(self):
raise NotImplementedError
[docs]
def setPos(self, pos):
raise NotImplementedError
[docs]
def setMinimumSize(self, width=None, height=None):
if width: self._minWidth = width
if height: self._minHeight = height
[docs]
def setMargins(self, x1=None, x2=None,
y1=None, y2=None):
if x1: self._x1=x1
if x2: self._x2=x2,
if y1: self._y1=y1
if y2: self._y2=y2
[docs]
def setInnerMargins(self, ix1=None,
iy1=None):
if ix1: self._ix1=ix1
if iy1: self._iy1=iy1
[docs]
def isVisible(self):
return True
[docs]
def visibleItems(self, subcall=None):
return [ i if subcall is None else subcall(i) for i in self._items if i.isVisible() ]
[docs]
class HorizontalLayout(Layout):
def _boundingRect(self):
width = self._x1 + self._x2
height = self._y1 + self._y2
geoms = self.visibleItems(lambda x:x.boundingRect())
lenGeoms = len(geoms)
if lenGeoms>0:
width += sum( g.width() for g in geoms ) + (lenGeoms-1)*self._ix1
height += max( g.height() for g in geoms )
if self._final and self._final.isVisible():
width += self._final.boundingRect().width()
height = max( height, self._final.boundingRect().height() )
return 0., 0., width, height
[docs]
def setPos(self, pos):
offset = pos + QtCore.QPointF(self._x1, self._y1)
innerOffset = QtCore.QPointF(self._ix1, self._iy1)
items = self.visibleItems()
selfHeight = self.boundingRect(force=False).height()
for it in items:
itRect = it.boundingRect()
if self._center:
offset.setY(pos.y()+(selfHeight - itRect.height())/2.)
it.setPos(offset)
offset += QtCore.QPointF(itRect.width(), 0.) + innerOffset
if self._final and self._final.isVisible():
if self._center:
offset.setY(pos.y()+(selfHeight - self._final.boundingRect().height())/2.)
self._final.setPos(offset)
[docs]
class VerticalLayout(Layout):
def _boundingRect(self):
width = self._x1 + self._x2
height = self._y1 + self._y2
geoms = self.visibleItems(lambda x:x.boundingRect())
lenGeoms = len(geoms)
if lenGeoms>0:
width += max( g.width() for g in geoms )
height += sum( g.height() for g in geoms ) + (lenGeoms-1)*self._iy1
if self._final and self._final.isVisible():
width = max(width, self._final.boundingRect().width())
height += self._final.boundingRect().height()
return 0., 0., width, height
[docs]
def setPos(self, pos):
offset = pos + QtCore.QPointF(self._x1, self._y1)
innerOffset = QtCore.QPointF(self._ix1, self._iy1)
items = self.visibleItems()
selfWidth = self.boundingRect(force=False).width()
for it in items:
itRect = it.boundingRect()
if self._center:
offset.setX(pos.x()+(selfWidth - itRect.width())/2.)
it.setPos(offset)
offset += QtCore.QPointF(0., itRect.height()) + innerOffset
if self._final and self._final.isVisible():
if self._center:
offset.setX(pos.x()+(selfWidth - self._final.boundingRect().width())/2.)
self._final.setPos(offset)
#########################################################################################################
# class AleaQGraphicsLabelWidget(QtWidgets.QGraphicsWidget): #
# def __init__(self, label, parent=None): #
# QtWidgets.QGraphicsWidget.__init__(self, parent) #
# self.__label = QtWidgets.QGraphicsSimpleTextItem(self) #
# font = self.__label.font() #
# font.setBold(True) #
# self.__label.setText(label) #
# #
# def boundingRect(self): #
# return self.__label.boundingRect() #
# #
# def shape(self): #
# return self.__label.shape() #
# #
# def size(self): #
# return self.boundingRect().size() #
# #
# def sizeHint(self, blop, blip): #
# return self.size() #
# #
# def setText(self, text): #
# self.__label.setText(text) #
# self.updateGeometry() #
# #
# def paint(self, painter, paintOpts, widget): #
# self.__label.paint(painter, paintOpts, widget) #
# #
# class AleaQGraphicsProxyWidget(QtWidgets.QGraphicsProxyWidget): #
# """Embed a QWidget in a QGraphicsItem without the ugly background. #
# #
# When embedding for ex. a QLabel in a QGraphicsLayout using the normal #
# QGraphicsProxyWidget, the QLabel is rendered with its ugly background #
# and the custom drawing of the QGraphicsItem is hidden. #
# This class overrides the painting routine or the QGraphicsProxyWidget #
# to paint the child widget without the background. #
# """ #
# def __init__(self, widget, parent=None): #
# """ #
# Ctor. #
# #
# :Parameters: #
# - widget (QtWidgets.QWidget) - The QWidget to embed #
# - parent (QtWidgets.QGraphicsItem) - Reference to the parent. #
# #
# """ #
# QtWidgets.QGraphicsProxyWidget.__init__(self, parent) #
# self.setWidget(widget) #
# self.__noMouseEventForward = True #
# #
# def event(self, event): #
# #needed or else it catches events before getting to the nodes in dataflowviews and #
# #makes tooltips invisible. #
# if(event.type()==QtCore.QEvent.GraphicsSceneHoverMove and self.__noMouseEventForward): #
# event.ignore() #
# return True #
# return QtWidgets.QGraphicsProxyWidget.event(self, event) #
# #
# def setMouseEventForward(self, val): #
# self.__noMouseEventForward = val #
# #
# def setWidget(self, widget): #
# widget.setBackgroundRole(QtGui.QPalette.Background) #
# widget.setAutoFillBackground(True) #
# widget.setStyleSheet("background-color: transparent") #
# QtWidgets.QGraphicsProxyWidget.setWidget(self, widget) #
#########################################################################################################
[docs]
def qrect_contains(r1, r2, proper):
assert r1 is not None
assert r2 is not None
dl,dr,dt,db = [0]*4
contains = False
if r1.contains(r2, proper):
contains = True
elif r1.intersects(r2):
if r2.left() < r1.left():
dl = r1.left() - r2.left()
if r2.right() > r1.right():
dr = r1.right() - r2.right()
if r2.top() < r1.top():
dt = r1.top() - r2.top()
if r2.bottom() > r1.bottom():
db = r1.bottom() - r2.bottom()
return contains, (dl,dr,dt,db)
[docs]
def mixin_method(mixinOne, mixinTwo, methodname, firstWins = False, invert=False, caller=None):
"""A function that returns a method calling method \"methodname\"
from mixinOne and then from mixinTwo, or the reverse order
if invert is True.
Can be used to quickly reimplement simple overloads.
"""
first = None
second = None
if(not invert):
first = None if (mixinOne is None) else getattr(mixinOne, methodname, None)
second = None if (mixinTwo is None) else getattr(mixinTwo, methodname, None)
else:
second = None if (mixinOne is None) else getattr(mixinOne, methodname, None)
first = None if (mixinTwo is None) else getattr(mixinTwo, methodname, None)
def simple_call(self, *args, **kwargs):
return first(self, *args, **kwargs)
def mixin_call(self, *args, **kwargs):
v1 = first(self, *args, **kwargs)
v2 = second(self, *args, **kwargs)
if(firstWins): return v1
else: return v2
if(second and first is None):
first = second
second = None
if(first and second is None):
return simple_call
else:
return mixin_call
[docs]
def extend_qt_scene_event(qtcls):
def event_handler(self, event):
t = event.type()
if t == QtCore.QEvent.GraphicsSceneMouseMove:
self.moveEvent(event)
elif t == QtCore.QEvent.Show:
self.polishEvent()
return qtcls.sceneEvent(self, event)
return event_handler