mirror of
https://github.com/Athemis/PyDSF.git
synced 2025-04-04 22:36:02 +00:00
Add pyqtgraph as submodule
This commit is contained in:
parent
f4c540a439
commit
ddb8394091
240 changed files with 50958 additions and 0 deletions
553
pyqtgraph/GraphicsScene/GraphicsScene.py
Normal file
553
pyqtgraph/GraphicsScene/GraphicsScene.py
Normal file
|
@ -0,0 +1,553 @@
|
|||
from ..Qt import QtCore, QtGui
|
||||
from ..python2_3 import sortList
|
||||
import weakref
|
||||
from ..Point import Point
|
||||
from .. import functions as fn
|
||||
from .. import ptime as ptime
|
||||
from .mouseEvents import *
|
||||
from .. import debug as debug
|
||||
|
||||
if hasattr(QtCore, 'PYQT_VERSION'):
|
||||
try:
|
||||
import sip
|
||||
HAVE_SIP = True
|
||||
except ImportError:
|
||||
HAVE_SIP = False
|
||||
else:
|
||||
HAVE_SIP = False
|
||||
|
||||
|
||||
__all__ = ['GraphicsScene']
|
||||
|
||||
class GraphicsScene(QtGui.QGraphicsScene):
|
||||
"""
|
||||
Extension of QGraphicsScene that implements a complete, parallel mouse event system.
|
||||
(It would have been preferred to just alter the way QGraphicsScene creates and delivers
|
||||
events, but this turned out to be impossible because the constructor for QGraphicsMouseEvent
|
||||
is private)
|
||||
|
||||
* Generates MouseClicked events in addition to the usual press/move/release events.
|
||||
(This works around a problem where it is impossible to have one item respond to a
|
||||
drag if another is watching for a click.)
|
||||
* Adjustable radius around click that will catch objects so you don't have to click *exactly* over small/thin objects
|
||||
* Global context menu--if an item implements a context menu, then its parent(s) may also add items to the menu.
|
||||
* Allows items to decide _before_ a mouse click which item will be the recipient of mouse events.
|
||||
This lets us indicate unambiguously to the user which item they are about to click/drag on
|
||||
* Eats mouseMove events that occur too soon after a mouse press.
|
||||
* Reimplements items() and itemAt() to circumvent PyQt bug
|
||||
|
||||
Mouse interaction is as follows:
|
||||
|
||||
1) Every time the mouse moves, the scene delivers both the standard hoverEnter/Move/LeaveEvents
|
||||
as well as custom HoverEvents.
|
||||
2) Items are sent HoverEvents in Z-order and each item may optionally call event.acceptClicks(button),
|
||||
acceptDrags(button) or both. If this method call returns True, this informs the item that _if_
|
||||
the user clicks/drags the specified mouse button, the item is guaranteed to be the
|
||||
recipient of click/drag events (the item may wish to change its appearance to indicate this).
|
||||
If the call to acceptClicks/Drags returns False, then the item is guaranteed to *not* receive
|
||||
the requested event (because another item has already accepted it).
|
||||
3) If the mouse is clicked, a mousePressEvent is generated as usual. If any items accept this press event, then
|
||||
No click/drag events will be generated and mouse interaction proceeds as defined by Qt. This allows
|
||||
items to function properly if they are expecting the usual press/move/release sequence of events.
|
||||
(It is recommended that items do NOT accept press events, and instead use click/drag events)
|
||||
Note: The default implementation of QGraphicsItem.mousePressEvent will *accept* the event if the
|
||||
item is has its Selectable or Movable flags enabled. You may need to override this behavior.
|
||||
4) If no item accepts the mousePressEvent, then the scene will begin delivering mouseDrag and/or mouseClick events.
|
||||
If the mouse is moved a sufficient distance (or moved slowly enough) before the button is released,
|
||||
then a mouseDragEvent is generated.
|
||||
If no drag events are generated before the button is released, then a mouseClickEvent is generated.
|
||||
5) Click/drag events are delivered to the item that called acceptClicks/acceptDrags on the HoverEvent
|
||||
in step 1. If no such items exist, then the scene attempts to deliver the events to items near the event.
|
||||
ClickEvents may be delivered in this way even if no
|
||||
item originally claimed it could accept the click. DragEvents may only be delivered this way if it is the initial
|
||||
move in a drag.
|
||||
"""
|
||||
|
||||
sigMouseHover = QtCore.Signal(object) ## emits a list of objects hovered over
|
||||
sigMouseMoved = QtCore.Signal(object) ## emits position of mouse on every move
|
||||
sigMouseClicked = QtCore.Signal(object) ## emitted when mouse is clicked. Check for event.isAccepted() to see whether the event has already been acted on.
|
||||
|
||||
sigPrepareForPaint = QtCore.Signal() ## emitted immediately before the scene is about to be rendered
|
||||
|
||||
_addressCache = weakref.WeakValueDictionary()
|
||||
|
||||
ExportDirectory = None
|
||||
|
||||
@classmethod
|
||||
def registerObject(cls, obj):
|
||||
"""
|
||||
Workaround for PyQt bug in qgraphicsscene.items()
|
||||
All subclasses of QGraphicsObject must register themselves with this function.
|
||||
(otherwise, mouse interaction with those objects will likely fail)
|
||||
"""
|
||||
if HAVE_SIP and isinstance(obj, sip.wrapper):
|
||||
cls._addressCache[sip.unwrapinstance(sip.cast(obj, QtGui.QGraphicsItem))] = obj
|
||||
|
||||
|
||||
def __init__(self, clickRadius=2, moveDistance=5, parent=None):
|
||||
QtGui.QGraphicsScene.__init__(self, parent)
|
||||
self.setClickRadius(clickRadius)
|
||||
self.setMoveDistance(moveDistance)
|
||||
self.exportDirectory = None
|
||||
|
||||
self.clickEvents = []
|
||||
self.dragButtons = []
|
||||
self.mouseGrabber = None
|
||||
self.dragItem = None
|
||||
self.lastDrag = None
|
||||
self.hoverItems = weakref.WeakKeyDictionary()
|
||||
self.lastHoverEvent = None
|
||||
|
||||
self.contextMenu = [QtGui.QAction("Export...", self)]
|
||||
self.contextMenu[0].triggered.connect(self.showExportDialog)
|
||||
|
||||
self.exportDialog = None
|
||||
|
||||
def render(self, *args):
|
||||
self.prepareForPaint()
|
||||
return QtGui.QGraphicsScene.render(self, *args)
|
||||
|
||||
def prepareForPaint(self):
|
||||
"""Called before every render. This method will inform items that the scene is about to
|
||||
be rendered by emitting sigPrepareForPaint.
|
||||
|
||||
This allows items to delay expensive processing until they know a paint will be required."""
|
||||
self.sigPrepareForPaint.emit()
|
||||
|
||||
|
||||
def setClickRadius(self, r):
|
||||
"""
|
||||
Set the distance away from mouse clicks to search for interacting items.
|
||||
When clicking, the scene searches first for items that directly intersect the click position
|
||||
followed by any other items that are within a rectangle that extends r pixels away from the
|
||||
click position.
|
||||
"""
|
||||
self._clickRadius = r
|
||||
|
||||
def setMoveDistance(self, d):
|
||||
"""
|
||||
Set the distance the mouse must move after a press before mouseMoveEvents will be delivered.
|
||||
This ensures that clicks with a small amount of movement are recognized as clicks instead of
|
||||
drags.
|
||||
"""
|
||||
self._moveDistance = d
|
||||
|
||||
def mousePressEvent(self, ev):
|
||||
#print 'scenePress'
|
||||
QtGui.QGraphicsScene.mousePressEvent(self, ev)
|
||||
if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events
|
||||
if self.lastHoverEvent is not None:
|
||||
# If the mouse has moved since the last hover event, send a new one.
|
||||
# This can happen if a context menu is open while the mouse is moving.
|
||||
if ev.scenePos() != self.lastHoverEvent.scenePos():
|
||||
self.sendHoverEvents(ev)
|
||||
|
||||
self.clickEvents.append(MouseClickEvent(ev))
|
||||
|
||||
## set focus on the topmost focusable item under this click
|
||||
items = self.items(ev.scenePos())
|
||||
for i in items:
|
||||
if i.isEnabled() and i.isVisible() and int(i.flags() & i.ItemIsFocusable) > 0:
|
||||
i.setFocus(QtCore.Qt.MouseFocusReason)
|
||||
break
|
||||
|
||||
def mouseMoveEvent(self, ev):
|
||||
self.sigMouseMoved.emit(ev.scenePos())
|
||||
|
||||
## First allow QGraphicsScene to deliver hoverEnter/Move/ExitEvents
|
||||
QtGui.QGraphicsScene.mouseMoveEvent(self, ev)
|
||||
|
||||
## Next deliver our own HoverEvents
|
||||
self.sendHoverEvents(ev)
|
||||
|
||||
if int(ev.buttons()) != 0: ## button is pressed; send mouseMoveEvents and mouseDragEvents
|
||||
QtGui.QGraphicsScene.mouseMoveEvent(self, ev)
|
||||
if self.mouseGrabberItem() is None:
|
||||
now = ptime.time()
|
||||
init = False
|
||||
## keep track of which buttons are involved in dragging
|
||||
for btn in [QtCore.Qt.LeftButton, QtCore.Qt.MidButton, QtCore.Qt.RightButton]:
|
||||
if int(ev.buttons() & btn) == 0:
|
||||
continue
|
||||
if int(btn) not in self.dragButtons: ## see if we've dragged far enough yet
|
||||
cev = [e for e in self.clickEvents if int(e.button()) == int(btn)][0]
|
||||
dist = Point(ev.screenPos() - cev.screenPos())
|
||||
if dist.length() < self._moveDistance and now - cev.time() < 0.5:
|
||||
continue
|
||||
init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True
|
||||
self.dragButtons.append(int(btn))
|
||||
|
||||
## If we have dragged buttons, deliver a drag event
|
||||
if len(self.dragButtons) > 0:
|
||||
if self.sendDragEvent(ev, init=init):
|
||||
ev.accept()
|
||||
|
||||
def leaveEvent(self, ev): ## inform items that mouse is gone
|
||||
if len(self.dragButtons) == 0:
|
||||
self.sendHoverEvents(ev, exitOnly=True)
|
||||
|
||||
|
||||
def mouseReleaseEvent(self, ev):
|
||||
#print 'sceneRelease'
|
||||
if self.mouseGrabberItem() is None:
|
||||
if ev.button() in self.dragButtons:
|
||||
if self.sendDragEvent(ev, final=True):
|
||||
#print "sent drag event"
|
||||
ev.accept()
|
||||
self.dragButtons.remove(ev.button())
|
||||
else:
|
||||
cev = [e for e in self.clickEvents if int(e.button()) == int(ev.button())]
|
||||
if self.sendClickEvent(cev[0]):
|
||||
#print "sent click event"
|
||||
ev.accept()
|
||||
self.clickEvents.remove(cev[0])
|
||||
|
||||
if int(ev.buttons()) == 0:
|
||||
self.dragItem = None
|
||||
self.dragButtons = []
|
||||
self.clickEvents = []
|
||||
self.lastDrag = None
|
||||
QtGui.QGraphicsScene.mouseReleaseEvent(self, ev)
|
||||
|
||||
self.sendHoverEvents(ev) ## let items prepare for next click/drag
|
||||
|
||||
def mouseDoubleClickEvent(self, ev):
|
||||
QtGui.QGraphicsScene.mouseDoubleClickEvent(self, ev)
|
||||
if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events
|
||||
self.clickEvents.append(MouseClickEvent(ev, double=True))
|
||||
|
||||
def sendHoverEvents(self, ev, exitOnly=False):
|
||||
## if exitOnly, then just inform all previously hovered items that the mouse has left.
|
||||
|
||||
if exitOnly:
|
||||
acceptable=False
|
||||
items = []
|
||||
event = HoverEvent(None, acceptable)
|
||||
else:
|
||||
acceptable = int(ev.buttons()) == 0 ## if we are in mid-drag, do not allow items to accept the hover event.
|
||||
event = HoverEvent(ev, acceptable)
|
||||
items = self.itemsNearEvent(event, hoverable=True)
|
||||
self.sigMouseHover.emit(items)
|
||||
|
||||
prevItems = list(self.hoverItems.keys())
|
||||
|
||||
#print "hover prev items:", prevItems
|
||||
#print "hover test items:", items
|
||||
for item in items:
|
||||
if hasattr(item, 'hoverEvent'):
|
||||
event.currentItem = item
|
||||
if item not in self.hoverItems:
|
||||
self.hoverItems[item] = None
|
||||
event.enter = True
|
||||
else:
|
||||
prevItems.remove(item)
|
||||
event.enter = False
|
||||
|
||||
try:
|
||||
item.hoverEvent(event)
|
||||
except:
|
||||
debug.printExc("Error sending hover event:")
|
||||
|
||||
event.enter = False
|
||||
event.exit = True
|
||||
#print "hover exit items:", prevItems
|
||||
for item in prevItems:
|
||||
event.currentItem = item
|
||||
try:
|
||||
item.hoverEvent(event)
|
||||
except:
|
||||
debug.printExc("Error sending hover exit event:")
|
||||
finally:
|
||||
del self.hoverItems[item]
|
||||
|
||||
# Update last hover event unless:
|
||||
# - mouse is dragging (move+buttons); in this case we want the dragged
|
||||
# item to continue receiving events until the drag is over
|
||||
# - event is not a mouse event (QEvent.Leave sometimes appears here)
|
||||
if (ev.type() == ev.GraphicsSceneMousePress or
|
||||
(ev.type() == ev.GraphicsSceneMouseMove and int(ev.buttons()) == 0)):
|
||||
self.lastHoverEvent = event ## save this so we can ask about accepted events later.
|
||||
|
||||
def sendDragEvent(self, ev, init=False, final=False):
|
||||
## Send a MouseDragEvent to the current dragItem or to
|
||||
## items near the beginning of the drag
|
||||
event = MouseDragEvent(ev, self.clickEvents[0], self.lastDrag, start=init, finish=final)
|
||||
#print "dragEvent: init=", init, 'final=', final, 'self.dragItem=', self.dragItem
|
||||
if init and self.dragItem is None:
|
||||
if self.lastHoverEvent is not None:
|
||||
acceptedItem = self.lastHoverEvent.dragItems().get(event.button(), None)
|
||||
else:
|
||||
acceptedItem = None
|
||||
|
||||
if acceptedItem is not None:
|
||||
#print "Drag -> pre-selected item:", acceptedItem
|
||||
self.dragItem = acceptedItem
|
||||
event.currentItem = self.dragItem
|
||||
try:
|
||||
self.dragItem.mouseDragEvent(event)
|
||||
except:
|
||||
debug.printExc("Error sending drag event:")
|
||||
|
||||
else:
|
||||
#print "drag -> new item"
|
||||
for item in self.itemsNearEvent(event):
|
||||
#print "check item:", item
|
||||
if not item.isVisible() or not item.isEnabled():
|
||||
continue
|
||||
if hasattr(item, 'mouseDragEvent'):
|
||||
event.currentItem = item
|
||||
try:
|
||||
item.mouseDragEvent(event)
|
||||
except:
|
||||
debug.printExc("Error sending drag event:")
|
||||
if event.isAccepted():
|
||||
#print " --> accepted"
|
||||
self.dragItem = item
|
||||
if int(item.flags() & item.ItemIsFocusable) > 0:
|
||||
item.setFocus(QtCore.Qt.MouseFocusReason)
|
||||
break
|
||||
elif self.dragItem is not None:
|
||||
event.currentItem = self.dragItem
|
||||
try:
|
||||
self.dragItem.mouseDragEvent(event)
|
||||
except:
|
||||
debug.printExc("Error sending hover exit event:")
|
||||
|
||||
self.lastDrag = event
|
||||
|
||||
return event.isAccepted()
|
||||
|
||||
|
||||
def sendClickEvent(self, ev):
|
||||
## if we are in mid-drag, click events may only go to the dragged item.
|
||||
if self.dragItem is not None and hasattr(self.dragItem, 'mouseClickEvent'):
|
||||
ev.currentItem = self.dragItem
|
||||
self.dragItem.mouseClickEvent(ev)
|
||||
|
||||
## otherwise, search near the cursor
|
||||
else:
|
||||
if self.lastHoverEvent is not None:
|
||||
acceptedItem = self.lastHoverEvent.clickItems().get(ev.button(), None)
|
||||
else:
|
||||
acceptedItem = None
|
||||
if acceptedItem is not None:
|
||||
ev.currentItem = acceptedItem
|
||||
try:
|
||||
acceptedItem.mouseClickEvent(ev)
|
||||
except:
|
||||
debug.printExc("Error sending click event:")
|
||||
else:
|
||||
for item in self.itemsNearEvent(ev):
|
||||
if not item.isVisible() or not item.isEnabled():
|
||||
continue
|
||||
if hasattr(item, 'mouseClickEvent'):
|
||||
ev.currentItem = item
|
||||
try:
|
||||
item.mouseClickEvent(ev)
|
||||
except:
|
||||
debug.printExc("Error sending click event:")
|
||||
|
||||
if ev.isAccepted():
|
||||
if int(item.flags() & item.ItemIsFocusable) > 0:
|
||||
item.setFocus(QtCore.Qt.MouseFocusReason)
|
||||
break
|
||||
self.sigMouseClicked.emit(ev)
|
||||
return ev.isAccepted()
|
||||
|
||||
def items(self, *args):
|
||||
#print 'args:', args
|
||||
items = QtGui.QGraphicsScene.items(self, *args)
|
||||
## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject,
|
||||
## then the object returned will be different than the actual item that was originally added to the scene
|
||||
items2 = list(map(self.translateGraphicsItem, items))
|
||||
#if HAVE_SIP and isinstance(self, sip.wrapper):
|
||||
#items2 = []
|
||||
#for i in items:
|
||||
#addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem))
|
||||
#i2 = GraphicsScene._addressCache.get(addr, i)
|
||||
##print i, "==>", i2
|
||||
#items2.append(i2)
|
||||
#print 'items:', items
|
||||
return items2
|
||||
|
||||
def selectedItems(self, *args):
|
||||
items = QtGui.QGraphicsScene.selectedItems(self, *args)
|
||||
## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject,
|
||||
## then the object returned will be different than the actual item that was originally added to the scene
|
||||
#if HAVE_SIP and isinstance(self, sip.wrapper):
|
||||
#items2 = []
|
||||
#for i in items:
|
||||
#addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem))
|
||||
#i2 = GraphicsScene._addressCache.get(addr, i)
|
||||
##print i, "==>", i2
|
||||
#items2.append(i2)
|
||||
items2 = list(map(self.translateGraphicsItem, items))
|
||||
|
||||
#print 'items:', items
|
||||
return items2
|
||||
|
||||
def itemAt(self, *args):
|
||||
item = QtGui.QGraphicsScene.itemAt(self, *args)
|
||||
|
||||
## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject,
|
||||
## then the object returned will be different than the actual item that was originally added to the scene
|
||||
#if HAVE_SIP and isinstance(self, sip.wrapper):
|
||||
#addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem))
|
||||
#item = GraphicsScene._addressCache.get(addr, item)
|
||||
#return item
|
||||
return self.translateGraphicsItem(item)
|
||||
|
||||
def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder, hoverable=False):
|
||||
"""
|
||||
Return an iterator that iterates first through the items that directly intersect point (in Z order)
|
||||
followed by any other items that are within the scene's click radius.
|
||||
"""
|
||||
#tr = self.getViewWidget(event.widget()).transform()
|
||||
view = self.views()[0]
|
||||
tr = view.viewportTransform()
|
||||
r = self._clickRadius
|
||||
rect = view.mapToScene(QtCore.QRect(0, 0, 2*r, 2*r)).boundingRect()
|
||||
|
||||
seen = set()
|
||||
if hasattr(event, 'buttonDownScenePos'):
|
||||
point = event.buttonDownScenePos()
|
||||
else:
|
||||
point = event.scenePos()
|
||||
w = rect.width()
|
||||
h = rect.height()
|
||||
rgn = QtCore.QRectF(point.x()-w, point.y()-h, 2*w, 2*h)
|
||||
#self.searchRect.setRect(rgn)
|
||||
|
||||
|
||||
items = self.items(point, selMode, sortOrder, tr)
|
||||
|
||||
## remove items whose shape does not contain point (scene.items() apparently sucks at this)
|
||||
items2 = []
|
||||
for item in items:
|
||||
if hoverable and not hasattr(item, 'hoverEvent'):
|
||||
continue
|
||||
shape = item.shape() # Note: default shape() returns boundingRect()
|
||||
if shape is None:
|
||||
continue
|
||||
if shape.contains(item.mapFromScene(point)):
|
||||
items2.append(item)
|
||||
|
||||
## Sort by descending Z-order (don't trust scene.itms() to do this either)
|
||||
## use 'absolute' z value, which is the sum of all item/parent ZValues
|
||||
def absZValue(item):
|
||||
if item is None:
|
||||
return 0
|
||||
return item.zValue() + absZValue(item.parentItem())
|
||||
|
||||
sortList(items2, lambda a,b: cmp(absZValue(b), absZValue(a)))
|
||||
|
||||
return items2
|
||||
|
||||
#for item in items:
|
||||
##seen.add(item)
|
||||
|
||||
#shape = item.mapToScene(item.shape())
|
||||
#if not shape.contains(point):
|
||||
#continue
|
||||
#yield item
|
||||
#for item in self.items(rgn, selMode, sortOrder, tr):
|
||||
##if item not in seen:
|
||||
#yield item
|
||||
|
||||
def getViewWidget(self):
|
||||
return self.views()[0]
|
||||
|
||||
#def getViewWidget(self, widget):
|
||||
### same pyqt bug -- mouseEvent.widget() doesn't give us the original python object.
|
||||
### [[doesn't seem to work correctly]]
|
||||
#if HAVE_SIP and isinstance(self, sip.wrapper):
|
||||
#addr = sip.unwrapinstance(sip.cast(widget, QtGui.QWidget))
|
||||
##print "convert", widget, addr
|
||||
#for v in self.views():
|
||||
#addr2 = sip.unwrapinstance(sip.cast(v, QtGui.QWidget))
|
||||
##print " check:", v, addr2
|
||||
#if addr2 == addr:
|
||||
#return v
|
||||
#else:
|
||||
#return widget
|
||||
|
||||
def addParentContextMenus(self, item, menu, event):
|
||||
"""
|
||||
Can be called by any item in the scene to expand its context menu to include parent context menus.
|
||||
Parents may implement getContextMenus to add new menus / actions to the existing menu.
|
||||
getContextMenus must accept 1 argument (the event that generated the original menu) and
|
||||
return a single QMenu or a list of QMenus.
|
||||
|
||||
The final menu will look like:
|
||||
|
||||
| Original Item 1
|
||||
| Original Item 2
|
||||
| ...
|
||||
| Original Item N
|
||||
| ------------------
|
||||
| Parent Item 1
|
||||
| Parent Item 2
|
||||
| ...
|
||||
| Grandparent Item 1
|
||||
| ...
|
||||
|
||||
|
||||
============== ==================================================
|
||||
**Arguments:**
|
||||
item The item that initially created the context menu
|
||||
(This is probably the item making the call to this function)
|
||||
menu The context menu being shown by the item
|
||||
event The original event that triggered the menu to appear.
|
||||
============== ==================================================
|
||||
"""
|
||||
|
||||
menusToAdd = []
|
||||
while item is not self:
|
||||
item = item.parentItem()
|
||||
if item is None:
|
||||
item = self
|
||||
if not hasattr(item, "getContextMenus"):
|
||||
continue
|
||||
subMenus = item.getContextMenus(event) or []
|
||||
if isinstance(subMenus, list): ## so that some items (like FlowchartViewBox) can return multiple menus
|
||||
menusToAdd.extend(subMenus)
|
||||
else:
|
||||
menusToAdd.append(subMenus)
|
||||
|
||||
if menusToAdd:
|
||||
menu.addSeparator()
|
||||
|
||||
for m in menusToAdd:
|
||||
if isinstance(m, QtGui.QMenu):
|
||||
menu.addMenu(m)
|
||||
elif isinstance(m, QtGui.QAction):
|
||||
menu.addAction(m)
|
||||
else:
|
||||
raise Exception("Cannot add object %s (type=%s) to QMenu." % (str(m), str(type(m))))
|
||||
|
||||
return menu
|
||||
|
||||
def getContextMenus(self, event):
|
||||
self.contextMenuItem = event.acceptedItem
|
||||
return self.contextMenu
|
||||
|
||||
def showExportDialog(self):
|
||||
if self.exportDialog is None:
|
||||
from . import exportDialog
|
||||
self.exportDialog = exportDialog.ExportDialog(self)
|
||||
self.exportDialog.show(self.contextMenuItem)
|
||||
|
||||
@staticmethod
|
||||
def translateGraphicsItem(item):
|
||||
## for fixing pyqt bugs where the wrong item is returned
|
||||
if HAVE_SIP and isinstance(item, sip.wrapper):
|
||||
addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem))
|
||||
item = GraphicsScene._addressCache.get(addr, item)
|
||||
return item
|
||||
|
||||
@staticmethod
|
||||
def translateGraphicsItems(items):
|
||||
return list(map(GraphicsScene.translateGraphicsItem, items))
|
||||
|
||||
|
||||
|
1
pyqtgraph/GraphicsScene/__init__.py
Normal file
1
pyqtgraph/GraphicsScene/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .GraphicsScene import *
|
143
pyqtgraph/GraphicsScene/exportDialog.py
Normal file
143
pyqtgraph/GraphicsScene/exportDialog.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYQT5
|
||||
from .. import exporters as exporters
|
||||
from .. import functions as fn
|
||||
from ..graphicsItems.ViewBox import ViewBox
|
||||
from ..graphicsItems.PlotItem import PlotItem
|
||||
|
||||
if USE_PYSIDE:
|
||||
from . import exportDialogTemplate_pyside as exportDialogTemplate
|
||||
elif USE_PYQT5:
|
||||
from . import exportDialogTemplate_pyqt5 as exportDialogTemplate
|
||||
else:
|
||||
from . import exportDialogTemplate_pyqt as exportDialogTemplate
|
||||
|
||||
|
||||
class ExportDialog(QtGui.QWidget):
|
||||
def __init__(self, scene):
|
||||
QtGui.QWidget.__init__(self)
|
||||
self.setVisible(False)
|
||||
self.setWindowTitle("Export")
|
||||
self.shown = False
|
||||
self.currentExporter = None
|
||||
self.scene = scene
|
||||
|
||||
self.selectBox = QtGui.QGraphicsRectItem()
|
||||
self.selectBox.setPen(fn.mkPen('y', width=3, style=QtCore.Qt.DashLine))
|
||||
self.selectBox.hide()
|
||||
self.scene.addItem(self.selectBox)
|
||||
|
||||
self.ui = exportDialogTemplate.Ui_Form()
|
||||
self.ui.setupUi(self)
|
||||
|
||||
self.ui.closeBtn.clicked.connect(self.close)
|
||||
self.ui.exportBtn.clicked.connect(self.exportClicked)
|
||||
self.ui.copyBtn.clicked.connect(self.copyClicked)
|
||||
self.ui.itemTree.currentItemChanged.connect(self.exportItemChanged)
|
||||
self.ui.formatList.currentItemChanged.connect(self.exportFormatChanged)
|
||||
|
||||
|
||||
def show(self, item=None):
|
||||
if item is not None:
|
||||
## Select next exportable parent of the item originally clicked on
|
||||
while not isinstance(item, ViewBox) and not isinstance(item, PlotItem) and item is not None:
|
||||
item = item.parentItem()
|
||||
## if this is a ViewBox inside a PlotItem, select the parent instead.
|
||||
if isinstance(item, ViewBox) and isinstance(item.parentItem(), PlotItem):
|
||||
item = item.parentItem()
|
||||
self.updateItemList(select=item)
|
||||
self.setVisible(True)
|
||||
self.activateWindow()
|
||||
self.raise_()
|
||||
self.selectBox.setVisible(True)
|
||||
|
||||
if not self.shown:
|
||||
self.shown = True
|
||||
vcenter = self.scene.getViewWidget().geometry().center()
|
||||
self.setGeometry(vcenter.x()-self.width()/2, vcenter.y()-self.height()/2, self.width(), self.height())
|
||||
|
||||
def updateItemList(self, select=None):
|
||||
self.ui.itemTree.clear()
|
||||
si = QtGui.QTreeWidgetItem(["Entire Scene"])
|
||||
si.gitem = self.scene
|
||||
self.ui.itemTree.addTopLevelItem(si)
|
||||
self.ui.itemTree.setCurrentItem(si)
|
||||
si.setExpanded(True)
|
||||
for child in self.scene.items():
|
||||
if child.parentItem() is None:
|
||||
self.updateItemTree(child, si, select=select)
|
||||
|
||||
def updateItemTree(self, item, treeItem, select=None):
|
||||
si = None
|
||||
if isinstance(item, ViewBox):
|
||||
si = QtGui.QTreeWidgetItem(['ViewBox'])
|
||||
elif isinstance(item, PlotItem):
|
||||
si = QtGui.QTreeWidgetItem(['Plot'])
|
||||
|
||||
if si is not None:
|
||||
si.gitem = item
|
||||
treeItem.addChild(si)
|
||||
treeItem = si
|
||||
if si.gitem is select:
|
||||
self.ui.itemTree.setCurrentItem(si)
|
||||
|
||||
for ch in item.childItems():
|
||||
self.updateItemTree(ch, treeItem, select=select)
|
||||
|
||||
|
||||
def exportItemChanged(self, item, prev):
|
||||
if item is None:
|
||||
return
|
||||
if item.gitem is self.scene:
|
||||
newBounds = self.scene.views()[0].viewRect()
|
||||
else:
|
||||
newBounds = item.gitem.sceneBoundingRect()
|
||||
self.selectBox.setRect(newBounds)
|
||||
self.selectBox.show()
|
||||
self.updateFormatList()
|
||||
|
||||
def updateFormatList(self):
|
||||
current = self.ui.formatList.currentItem()
|
||||
if current is not None:
|
||||
current = str(current.text())
|
||||
self.ui.formatList.clear()
|
||||
self.exporterClasses = {}
|
||||
gotCurrent = False
|
||||
for exp in exporters.listExporters():
|
||||
self.ui.formatList.addItem(exp.Name)
|
||||
self.exporterClasses[exp.Name] = exp
|
||||
if exp.Name == current:
|
||||
self.ui.formatList.setCurrentRow(self.ui.formatList.count()-1)
|
||||
gotCurrent = True
|
||||
|
||||
if not gotCurrent:
|
||||
self.ui.formatList.setCurrentRow(0)
|
||||
|
||||
def exportFormatChanged(self, item, prev):
|
||||
if item is None:
|
||||
self.currentExporter = None
|
||||
self.ui.paramTree.clear()
|
||||
return
|
||||
expClass = self.exporterClasses[str(item.text())]
|
||||
exp = expClass(item=self.ui.itemTree.currentItem().gitem)
|
||||
params = exp.parameters()
|
||||
if params is None:
|
||||
self.ui.paramTree.clear()
|
||||
else:
|
||||
self.ui.paramTree.setParameters(params)
|
||||
self.currentExporter = exp
|
||||
self.ui.copyBtn.setEnabled(exp.allowCopy)
|
||||
|
||||
def exportClicked(self):
|
||||
self.selectBox.hide()
|
||||
self.currentExporter.export()
|
||||
|
||||
def copyClicked(self):
|
||||
self.selectBox.hide()
|
||||
self.currentExporter.export(copy=True)
|
||||
|
||||
def close(self):
|
||||
self.selectBox.setVisible(False)
|
||||
self.setVisible(False)
|
||||
|
||||
|
||||
|
100
pyqtgraph/GraphicsScene/exportDialogTemplate.ui
Normal file
100
pyqtgraph/GraphicsScene/exportDialogTemplate.ui
Normal file
|
@ -0,0 +1,100 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>241</width>
|
||||
<height>367</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Export</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="0" colspan="3">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Item to export:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="3">
|
||||
<widget class="QTreeWidget" name="itemTree">
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string notr="true">1</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="3">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Export format</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="3">
|
||||
<widget class="QListWidget" name="formatList"/>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QPushButton" name="exportBtn">
|
||||
<property name="text">
|
||||
<string>Export</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="2">
|
||||
<widget class="QPushButton" name="closeBtn">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="3">
|
||||
<widget class="ParameterTree" name="paramTree">
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string notr="true">1</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="3">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Export options</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QPushButton" name="copyBtn">
|
||||
<property name="text">
|
||||
<string>Copy</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ParameterTree</class>
|
||||
<extends>QTreeWidget</extends>
|
||||
<header>..parametertree</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
77
pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py
Normal file
77
pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/GraphicsScene/exportDialogTemplate.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:52 2013
|
||||
# by: PyQt4 UI code generator 4.10
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
try:
|
||||
_fromUtf8 = QtCore.QString.fromUtf8
|
||||
except AttributeError:
|
||||
def _fromUtf8(s):
|
||||
return s
|
||||
|
||||
try:
|
||||
_encoding = QtGui.QApplication.UnicodeUTF8
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig, _encoding)
|
||||
except AttributeError:
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig)
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName(_fromUtf8("Form"))
|
||||
Form.resize(241, 367)
|
||||
self.gridLayout = QtGui.QGridLayout(Form)
|
||||
self.gridLayout.setSpacing(0)
|
||||
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
|
||||
self.label = QtGui.QLabel(Form)
|
||||
self.label.setObjectName(_fromUtf8("label"))
|
||||
self.gridLayout.addWidget(self.label, 0, 0, 1, 3)
|
||||
self.itemTree = QtGui.QTreeWidget(Form)
|
||||
self.itemTree.setObjectName(_fromUtf8("itemTree"))
|
||||
self.itemTree.headerItem().setText(0, _fromUtf8("1"))
|
||||
self.itemTree.header().setVisible(False)
|
||||
self.gridLayout.addWidget(self.itemTree, 1, 0, 1, 3)
|
||||
self.label_2 = QtGui.QLabel(Form)
|
||||
self.label_2.setObjectName(_fromUtf8("label_2"))
|
||||
self.gridLayout.addWidget(self.label_2, 2, 0, 1, 3)
|
||||
self.formatList = QtGui.QListWidget(Form)
|
||||
self.formatList.setObjectName(_fromUtf8("formatList"))
|
||||
self.gridLayout.addWidget(self.formatList, 3, 0, 1, 3)
|
||||
self.exportBtn = QtGui.QPushButton(Form)
|
||||
self.exportBtn.setObjectName(_fromUtf8("exportBtn"))
|
||||
self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1)
|
||||
self.closeBtn = QtGui.QPushButton(Form)
|
||||
self.closeBtn.setObjectName(_fromUtf8("closeBtn"))
|
||||
self.gridLayout.addWidget(self.closeBtn, 6, 2, 1, 1)
|
||||
self.paramTree = ParameterTree(Form)
|
||||
self.paramTree.setObjectName(_fromUtf8("paramTree"))
|
||||
self.paramTree.headerItem().setText(0, _fromUtf8("1"))
|
||||
self.paramTree.header().setVisible(False)
|
||||
self.gridLayout.addWidget(self.paramTree, 5, 0, 1, 3)
|
||||
self.label_3 = QtGui.QLabel(Form)
|
||||
self.label_3.setObjectName(_fromUtf8("label_3"))
|
||||
self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3)
|
||||
self.copyBtn = QtGui.QPushButton(Form)
|
||||
self.copyBtn.setObjectName(_fromUtf8("copyBtn"))
|
||||
self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(_translate("Form", "Export", None))
|
||||
self.label.setText(_translate("Form", "Item to export:", None))
|
||||
self.label_2.setText(_translate("Form", "Export format", None))
|
||||
self.exportBtn.setText(_translate("Form", "Export", None))
|
||||
self.closeBtn.setText(_translate("Form", "Close", None))
|
||||
self.label_3.setText(_translate("Form", "Export options", None))
|
||||
self.copyBtn.setText(_translate("Form", "Copy", None))
|
||||
|
||||
from ..parametertree import ParameterTree
|
64
pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt5.py
Normal file
64
pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt5.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/GraphicsScene/exportDialogTemplate.ui'
|
||||
#
|
||||
# Created: Wed Mar 26 15:09:29 2014
|
||||
# by: PyQt5 UI code generator 5.0.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName("Form")
|
||||
Form.resize(241, 367)
|
||||
self.gridLayout = QtWidgets.QGridLayout(Form)
|
||||
self.gridLayout.setSpacing(0)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.label = QtWidgets.QLabel(Form)
|
||||
self.label.setObjectName("label")
|
||||
self.gridLayout.addWidget(self.label, 0, 0, 1, 3)
|
||||
self.itemTree = QtWidgets.QTreeWidget(Form)
|
||||
self.itemTree.setObjectName("itemTree")
|
||||
self.itemTree.headerItem().setText(0, "1")
|
||||
self.itemTree.header().setVisible(False)
|
||||
self.gridLayout.addWidget(self.itemTree, 1, 0, 1, 3)
|
||||
self.label_2 = QtWidgets.QLabel(Form)
|
||||
self.label_2.setObjectName("label_2")
|
||||
self.gridLayout.addWidget(self.label_2, 2, 0, 1, 3)
|
||||
self.formatList = QtWidgets.QListWidget(Form)
|
||||
self.formatList.setObjectName("formatList")
|
||||
self.gridLayout.addWidget(self.formatList, 3, 0, 1, 3)
|
||||
self.exportBtn = QtWidgets.QPushButton(Form)
|
||||
self.exportBtn.setObjectName("exportBtn")
|
||||
self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1)
|
||||
self.closeBtn = QtWidgets.QPushButton(Form)
|
||||
self.closeBtn.setObjectName("closeBtn")
|
||||
self.gridLayout.addWidget(self.closeBtn, 6, 2, 1, 1)
|
||||
self.paramTree = ParameterTree(Form)
|
||||
self.paramTree.setObjectName("paramTree")
|
||||
self.paramTree.headerItem().setText(0, "1")
|
||||
self.paramTree.header().setVisible(False)
|
||||
self.gridLayout.addWidget(self.paramTree, 5, 0, 1, 3)
|
||||
self.label_3 = QtWidgets.QLabel(Form)
|
||||
self.label_3.setObjectName("label_3")
|
||||
self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3)
|
||||
self.copyBtn = QtWidgets.QPushButton(Form)
|
||||
self.copyBtn.setObjectName("copyBtn")
|
||||
self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
Form.setWindowTitle(_translate("Form", "Export"))
|
||||
self.label.setText(_translate("Form", "Item to export:"))
|
||||
self.label_2.setText(_translate("Form", "Export format"))
|
||||
self.exportBtn.setText(_translate("Form", "Export"))
|
||||
self.closeBtn.setText(_translate("Form", "Close"))
|
||||
self.label_3.setText(_translate("Form", "Export options"))
|
||||
self.copyBtn.setText(_translate("Form", "Copy"))
|
||||
|
||||
from ..parametertree import ParameterTree
|
63
pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py
Normal file
63
pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/GraphicsScene/exportDialogTemplate.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:53 2013
|
||||
# by: pyside-uic 0.2.14 running on PySide 1.1.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName("Form")
|
||||
Form.resize(241, 367)
|
||||
self.gridLayout = QtGui.QGridLayout(Form)
|
||||
self.gridLayout.setSpacing(0)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.label = QtGui.QLabel(Form)
|
||||
self.label.setObjectName("label")
|
||||
self.gridLayout.addWidget(self.label, 0, 0, 1, 3)
|
||||
self.itemTree = QtGui.QTreeWidget(Form)
|
||||
self.itemTree.setObjectName("itemTree")
|
||||
self.itemTree.headerItem().setText(0, "1")
|
||||
self.itemTree.header().setVisible(False)
|
||||
self.gridLayout.addWidget(self.itemTree, 1, 0, 1, 3)
|
||||
self.label_2 = QtGui.QLabel(Form)
|
||||
self.label_2.setObjectName("label_2")
|
||||
self.gridLayout.addWidget(self.label_2, 2, 0, 1, 3)
|
||||
self.formatList = QtGui.QListWidget(Form)
|
||||
self.formatList.setObjectName("formatList")
|
||||
self.gridLayout.addWidget(self.formatList, 3, 0, 1, 3)
|
||||
self.exportBtn = QtGui.QPushButton(Form)
|
||||
self.exportBtn.setObjectName("exportBtn")
|
||||
self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1)
|
||||
self.closeBtn = QtGui.QPushButton(Form)
|
||||
self.closeBtn.setObjectName("closeBtn")
|
||||
self.gridLayout.addWidget(self.closeBtn, 6, 2, 1, 1)
|
||||
self.paramTree = ParameterTree(Form)
|
||||
self.paramTree.setObjectName("paramTree")
|
||||
self.paramTree.headerItem().setText(0, "1")
|
||||
self.paramTree.header().setVisible(False)
|
||||
self.gridLayout.addWidget(self.paramTree, 5, 0, 1, 3)
|
||||
self.label_3 = QtGui.QLabel(Form)
|
||||
self.label_3.setObjectName("label_3")
|
||||
self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3)
|
||||
self.copyBtn = QtGui.QPushButton(Form)
|
||||
self.copyBtn.setObjectName("copyBtn")
|
||||
self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.label.setText(QtGui.QApplication.translate("Form", "Item to export:", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.label_2.setText(QtGui.QApplication.translate("Form", "Export format", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.exportBtn.setText(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.closeBtn.setText(QtGui.QApplication.translate("Form", "Close", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.copyBtn.setText(QtGui.QApplication.translate("Form", "Copy", None, QtGui.QApplication.UnicodeUTF8))
|
||||
|
||||
from ..parametertree import ParameterTree
|
382
pyqtgraph/GraphicsScene/mouseEvents.py
Normal file
382
pyqtgraph/GraphicsScene/mouseEvents.py
Normal file
|
@ -0,0 +1,382 @@
|
|||
from ..Point import Point
|
||||
from ..Qt import QtCore, QtGui
|
||||
import weakref
|
||||
from .. import ptime as ptime
|
||||
|
||||
class MouseDragEvent(object):
|
||||
"""
|
||||
Instances of this class are delivered to items in a :class:`GraphicsScene <pyqtgraph.GraphicsScene>` via their mouseDragEvent() method when the item is being mouse-dragged.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
def __init__(self, moveEvent, pressEvent, lastEvent, start=False, finish=False):
|
||||
self.start = start
|
||||
self.finish = finish
|
||||
self.accepted = False
|
||||
self.currentItem = None
|
||||
self._buttonDownScenePos = {}
|
||||
self._buttonDownScreenPos = {}
|
||||
for btn in [QtCore.Qt.LeftButton, QtCore.Qt.MidButton, QtCore.Qt.RightButton]:
|
||||
self._buttonDownScenePos[int(btn)] = moveEvent.buttonDownScenePos(btn)
|
||||
self._buttonDownScreenPos[int(btn)] = moveEvent.buttonDownScreenPos(btn)
|
||||
self._scenePos = moveEvent.scenePos()
|
||||
self._screenPos = moveEvent.screenPos()
|
||||
if lastEvent is None:
|
||||
self._lastScenePos = pressEvent.scenePos()
|
||||
self._lastScreenPos = pressEvent.screenPos()
|
||||
else:
|
||||
self._lastScenePos = lastEvent.scenePos()
|
||||
self._lastScreenPos = lastEvent.screenPos()
|
||||
self._buttons = moveEvent.buttons()
|
||||
self._button = pressEvent.button()
|
||||
self._modifiers = moveEvent.modifiers()
|
||||
self.acceptedItem = None
|
||||
|
||||
def accept(self):
|
||||
"""An item should call this method if it can handle the event. This will prevent the event being delivered to any other items."""
|
||||
self.accepted = True
|
||||
self.acceptedItem = self.currentItem
|
||||
|
||||
def ignore(self):
|
||||
"""An item should call this method if it cannot handle the event. This will allow the event to be delivered to other items."""
|
||||
self.accepted = False
|
||||
|
||||
def isAccepted(self):
|
||||
return self.accepted
|
||||
|
||||
def scenePos(self):
|
||||
"""Return the current scene position of the mouse."""
|
||||
return Point(self._scenePos)
|
||||
|
||||
def screenPos(self):
|
||||
"""Return the current screen position (pixels relative to widget) of the mouse."""
|
||||
return Point(self._screenPos)
|
||||
|
||||
def buttonDownScenePos(self, btn=None):
|
||||
"""
|
||||
Return the scene position of the mouse at the time *btn* was pressed.
|
||||
If *btn* is omitted, then the button that initiated the drag is assumed.
|
||||
"""
|
||||
if btn is None:
|
||||
btn = self.button()
|
||||
return Point(self._buttonDownScenePos[int(btn)])
|
||||
|
||||
def buttonDownScreenPos(self, btn=None):
|
||||
"""
|
||||
Return the screen position (pixels relative to widget) of the mouse at the time *btn* was pressed.
|
||||
If *btn* is omitted, then the button that initiated the drag is assumed.
|
||||
"""
|
||||
if btn is None:
|
||||
btn = self.button()
|
||||
return Point(self._buttonDownScreenPos[int(btn)])
|
||||
|
||||
def lastScenePos(self):
|
||||
"""
|
||||
Return the scene position of the mouse immediately prior to this event.
|
||||
"""
|
||||
return Point(self._lastScenePos)
|
||||
|
||||
def lastScreenPos(self):
|
||||
"""
|
||||
Return the screen position of the mouse immediately prior to this event.
|
||||
"""
|
||||
return Point(self._lastScreenPos)
|
||||
|
||||
def buttons(self):
|
||||
"""
|
||||
Return the buttons currently pressed on the mouse.
|
||||
(see QGraphicsSceneMouseEvent::buttons in the Qt documentation)
|
||||
"""
|
||||
return self._buttons
|
||||
|
||||
def button(self):
|
||||
"""Return the button that initiated the drag (may be different from the buttons currently pressed)
|
||||
(see QGraphicsSceneMouseEvent::button in the Qt documentation)
|
||||
|
||||
"""
|
||||
return self._button
|
||||
|
||||
def pos(self):
|
||||
"""
|
||||
Return the current position of the mouse in the coordinate system of the item
|
||||
that the event was delivered to.
|
||||
"""
|
||||
return Point(self.currentItem.mapFromScene(self._scenePos))
|
||||
|
||||
def lastPos(self):
|
||||
"""
|
||||
Return the previous position of the mouse in the coordinate system of the item
|
||||
that the event was delivered to.
|
||||
"""
|
||||
return Point(self.currentItem.mapFromScene(self._lastScenePos))
|
||||
|
||||
def buttonDownPos(self, btn=None):
|
||||
"""
|
||||
Return the position of the mouse at the time the drag was initiated
|
||||
in the coordinate system of the item that the event was delivered to.
|
||||
"""
|
||||
if btn is None:
|
||||
btn = self.button()
|
||||
return Point(self.currentItem.mapFromScene(self._buttonDownScenePos[int(btn)]))
|
||||
|
||||
def isStart(self):
|
||||
"""Returns True if this event is the first since a drag was initiated."""
|
||||
return self.start
|
||||
|
||||
def isFinish(self):
|
||||
"""Returns False if this is the last event in a drag. Note that this
|
||||
event will have the same position as the previous one."""
|
||||
return self.finish
|
||||
|
||||
def __repr__(self):
|
||||
if self.currentItem is None:
|
||||
lp = self._lastScenePos
|
||||
p = self._scenePos
|
||||
else:
|
||||
lp = self.lastPos()
|
||||
p = self.pos()
|
||||
return "<MouseDragEvent (%g,%g)->(%g,%g) buttons=%d start=%s finish=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isStart()), str(self.isFinish()))
|
||||
|
||||
def modifiers(self):
|
||||
"""Return any keyboard modifiers currently pressed.
|
||||
(see QGraphicsSceneMouseEvent::modifiers in the Qt documentation)
|
||||
|
||||
"""
|
||||
return self._modifiers
|
||||
|
||||
|
||||
|
||||
class MouseClickEvent(object):
|
||||
"""
|
||||
Instances of this class are delivered to items in a :class:`GraphicsScene <pyqtgraph.GraphicsScene>` via their mouseClickEvent() method when the item is clicked.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, pressEvent, double=False):
|
||||
self.accepted = False
|
||||
self.currentItem = None
|
||||
self._double = double
|
||||
self._scenePos = pressEvent.scenePos()
|
||||
self._screenPos = pressEvent.screenPos()
|
||||
self._button = pressEvent.button()
|
||||
self._buttons = pressEvent.buttons()
|
||||
self._modifiers = pressEvent.modifiers()
|
||||
self._time = ptime.time()
|
||||
self.acceptedItem = None
|
||||
|
||||
def accept(self):
|
||||
"""An item should call this method if it can handle the event. This will prevent the event being delivered to any other items."""
|
||||
self.accepted = True
|
||||
self.acceptedItem = self.currentItem
|
||||
|
||||
def ignore(self):
|
||||
"""An item should call this method if it cannot handle the event. This will allow the event to be delivered to other items."""
|
||||
self.accepted = False
|
||||
|
||||
def isAccepted(self):
|
||||
return self.accepted
|
||||
|
||||
def scenePos(self):
|
||||
"""Return the current scene position of the mouse."""
|
||||
return Point(self._scenePos)
|
||||
|
||||
def screenPos(self):
|
||||
"""Return the current screen position (pixels relative to widget) of the mouse."""
|
||||
return Point(self._screenPos)
|
||||
|
||||
def buttons(self):
|
||||
"""
|
||||
Return the buttons currently pressed on the mouse.
|
||||
(see QGraphicsSceneMouseEvent::buttons in the Qt documentation)
|
||||
"""
|
||||
return self._buttons
|
||||
|
||||
def button(self):
|
||||
"""Return the mouse button that generated the click event.
|
||||
(see QGraphicsSceneMouseEvent::button in the Qt documentation)
|
||||
"""
|
||||
return self._button
|
||||
|
||||
def double(self):
|
||||
"""Return True if this is a double-click."""
|
||||
return self._double
|
||||
|
||||
def pos(self):
|
||||
"""
|
||||
Return the current position of the mouse in the coordinate system of the item
|
||||
that the event was delivered to.
|
||||
"""
|
||||
return Point(self.currentItem.mapFromScene(self._scenePos))
|
||||
|
||||
def lastPos(self):
|
||||
"""
|
||||
Return the previous position of the mouse in the coordinate system of the item
|
||||
that the event was delivered to.
|
||||
"""
|
||||
return Point(self.currentItem.mapFromScene(self._lastScenePos))
|
||||
|
||||
def modifiers(self):
|
||||
"""Return any keyboard modifiers currently pressed.
|
||||
(see QGraphicsSceneMouseEvent::modifiers in the Qt documentation)
|
||||
"""
|
||||
return self._modifiers
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
if self.currentItem is None:
|
||||
p = self._scenePos
|
||||
else:
|
||||
p = self.pos()
|
||||
return "<MouseClickEvent (%g,%g) button=%d>" % (p.x(), p.y(), int(self.button()))
|
||||
except:
|
||||
return "<MouseClickEvent button=%d>" % (int(self.button()))
|
||||
|
||||
def time(self):
|
||||
return self._time
|
||||
|
||||
|
||||
|
||||
class HoverEvent(object):
|
||||
"""
|
||||
Instances of this class are delivered to items in a :class:`GraphicsScene <pyqtgraph.GraphicsScene>` via their hoverEvent() method when the mouse is hovering over the item.
|
||||
This event class both informs items that the mouse cursor is nearby and allows items to
|
||||
communicate with one another about whether each item will accept *potential* mouse events.
|
||||
|
||||
It is common for multiple overlapping items to receive hover events and respond by changing
|
||||
their appearance. This can be misleading to the user since, in general, only one item will
|
||||
respond to mouse events. To avoid this, items make calls to event.acceptClicks(button)
|
||||
and/or acceptDrags(button).
|
||||
|
||||
Each item may make multiple calls to acceptClicks/Drags, each time for a different button.
|
||||
If the method returns True, then the item is guaranteed to be
|
||||
the recipient of the claimed event IF the user presses the specified mouse button before
|
||||
moving. If claimEvent returns False, then this item is guaranteed NOT to get the specified
|
||||
event (because another has already claimed it) and the item should change its appearance
|
||||
accordingly.
|
||||
|
||||
event.isEnter() returns True if the mouse has just entered the item's shape;
|
||||
event.isExit() returns True if the mouse has just left.
|
||||
"""
|
||||
def __init__(self, moveEvent, acceptable):
|
||||
self.enter = False
|
||||
self.acceptable = acceptable
|
||||
self.exit = False
|
||||
self.__clickItems = weakref.WeakValueDictionary()
|
||||
self.__dragItems = weakref.WeakValueDictionary()
|
||||
self.currentItem = None
|
||||
if moveEvent is not None:
|
||||
self._scenePos = moveEvent.scenePos()
|
||||
self._screenPos = moveEvent.screenPos()
|
||||
self._lastScenePos = moveEvent.lastScenePos()
|
||||
self._lastScreenPos = moveEvent.lastScreenPos()
|
||||
self._buttons = moveEvent.buttons()
|
||||
self._modifiers = moveEvent.modifiers()
|
||||
else:
|
||||
self.exit = True
|
||||
|
||||
|
||||
|
||||
def isEnter(self):
|
||||
"""Returns True if the mouse has just entered the item's shape"""
|
||||
return self.enter
|
||||
|
||||
def isExit(self):
|
||||
"""Returns True if the mouse has just exited the item's shape"""
|
||||
return self.exit
|
||||
|
||||
def acceptClicks(self, button):
|
||||
"""Inform the scene that the item (that the event was delivered to)
|
||||
would accept a mouse click event if the user were to click before
|
||||
moving the mouse again.
|
||||
|
||||
Returns True if the request is successful, otherwise returns False (indicating
|
||||
that some other item would receive an incoming click).
|
||||
"""
|
||||
if not self.acceptable:
|
||||
return False
|
||||
if button not in self.__clickItems:
|
||||
self.__clickItems[button] = self.currentItem
|
||||
return True
|
||||
return False
|
||||
|
||||
def acceptDrags(self, button):
|
||||
"""Inform the scene that the item (that the event was delivered to)
|
||||
would accept a mouse drag event if the user were to drag before
|
||||
the next hover event.
|
||||
|
||||
Returns True if the request is successful, otherwise returns False (indicating
|
||||
that some other item would receive an incoming drag event).
|
||||
"""
|
||||
if not self.acceptable:
|
||||
return False
|
||||
if button not in self.__dragItems:
|
||||
self.__dragItems[button] = self.currentItem
|
||||
return True
|
||||
return False
|
||||
|
||||
def scenePos(self):
|
||||
"""Return the current scene position of the mouse."""
|
||||
return Point(self._scenePos)
|
||||
|
||||
def screenPos(self):
|
||||
"""Return the current screen position of the mouse."""
|
||||
return Point(self._screenPos)
|
||||
|
||||
def lastScenePos(self):
|
||||
"""Return the previous scene position of the mouse."""
|
||||
return Point(self._lastScenePos)
|
||||
|
||||
def lastScreenPos(self):
|
||||
"""Return the previous screen position of the mouse."""
|
||||
return Point(self._lastScreenPos)
|
||||
|
||||
def buttons(self):
|
||||
"""
|
||||
Return the buttons currently pressed on the mouse.
|
||||
(see QGraphicsSceneMouseEvent::buttons in the Qt documentation)
|
||||
"""
|
||||
return self._buttons
|
||||
|
||||
def pos(self):
|
||||
"""
|
||||
Return the current position of the mouse in the coordinate system of the item
|
||||
that the event was delivered to.
|
||||
"""
|
||||
return Point(self.currentItem.mapFromScene(self._scenePos))
|
||||
|
||||
def lastPos(self):
|
||||
"""
|
||||
Return the previous position of the mouse in the coordinate system of the item
|
||||
that the event was delivered to.
|
||||
"""
|
||||
return Point(self.currentItem.mapFromScene(self._lastScenePos))
|
||||
|
||||
def __repr__(self):
|
||||
if self.exit:
|
||||
return "<HoverEvent exit=True>"
|
||||
|
||||
if self.currentItem is None:
|
||||
lp = self._lastScenePos
|
||||
p = self._scenePos
|
||||
else:
|
||||
lp = self.lastPos()
|
||||
p = self.pos()
|
||||
return "<HoverEvent (%g,%g)->(%g,%g) buttons=%d enter=%s exit=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isEnter()), str(self.isExit()))
|
||||
|
||||
def modifiers(self):
|
||||
"""Return any keyboard modifiers currently pressed.
|
||||
(see QGraphicsSceneMouseEvent::modifiers in the Qt documentation)
|
||||
"""
|
||||
return self._modifiers
|
||||
|
||||
def clickItems(self):
|
||||
return self.__clickItems
|
||||
|
||||
def dragItems(self):
|
||||
return self.__dragItems
|
||||
|
||||
|
||||
|
56
pyqtgraph/PlotData.py
Normal file
56
pyqtgraph/PlotData.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
|
||||
|
||||
class PlotData(object):
|
||||
"""
|
||||
Class used for managing plot data
|
||||
- allows data sharing between multiple graphics items (curve, scatter, graph..)
|
||||
- each item may define the columns it needs
|
||||
- column groupings ('pos' or x, y, z)
|
||||
- efficiently appendable
|
||||
- log, fft transformations
|
||||
- color mode conversion (float/byte/qcolor)
|
||||
- pen/brush conversion
|
||||
- per-field cached masking
|
||||
- allows multiple masking fields (different graphics need to mask on different criteria)
|
||||
- removal of nan/inf values
|
||||
- option for single value shared by entire column
|
||||
- cached downsampling
|
||||
- cached min / max / hasnan / isuniform
|
||||
"""
|
||||
def __init__(self):
|
||||
self.fields = {}
|
||||
|
||||
self.maxVals = {} ## cache for max/min
|
||||
self.minVals = {}
|
||||
|
||||
def addFields(self, **fields):
|
||||
for f in fields:
|
||||
if f not in self.fields:
|
||||
self.fields[f] = None
|
||||
|
||||
def hasField(self, f):
|
||||
return f in self.fields
|
||||
|
||||
def __getitem__(self, field):
|
||||
return self.fields[field]
|
||||
|
||||
def __setitem__(self, field, val):
|
||||
self.fields[field] = val
|
||||
|
||||
def max(self, field):
|
||||
mx = self.maxVals.get(field, None)
|
||||
if mx is None:
|
||||
mx = np.max(self[field])
|
||||
self.maxVals[field] = mx
|
||||
return mx
|
||||
|
||||
def min(self, field):
|
||||
mn = self.minVals.get(field, None)
|
||||
if mn is None:
|
||||
mn = np.min(self[field])
|
||||
self.minVals[field] = mn
|
||||
return mn
|
||||
|
||||
|
||||
|
||||
|
155
pyqtgraph/Point.py
Normal file
155
pyqtgraph/Point.py
Normal file
|
@ -0,0 +1,155 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Point.py - Extension of QPointF which adds a few missing methods.
|
||||
Copyright 2010 Luke Campagnola
|
||||
Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
"""
|
||||
|
||||
from .Qt import QtCore
|
||||
import numpy as np
|
||||
|
||||
def clip(x, mn, mx):
|
||||
if x > mx:
|
||||
return mx
|
||||
if x < mn:
|
||||
return mn
|
||||
return x
|
||||
|
||||
class Point(QtCore.QPointF):
|
||||
"""Extension of QPointF which adds a few missing methods."""
|
||||
|
||||
def __init__(self, *args):
|
||||
if len(args) == 1:
|
||||
if isinstance(args[0], QtCore.QSizeF):
|
||||
QtCore.QPointF.__init__(self, float(args[0].width()), float(args[0].height()))
|
||||
return
|
||||
elif isinstance(args[0], float) or isinstance(args[0], int):
|
||||
QtCore.QPointF.__init__(self, float(args[0]), float(args[0]))
|
||||
return
|
||||
elif hasattr(args[0], '__getitem__'):
|
||||
QtCore.QPointF.__init__(self, float(args[0][0]), float(args[0][1]))
|
||||
return
|
||||
elif len(args) == 2:
|
||||
QtCore.QPointF.__init__(self, args[0], args[1])
|
||||
return
|
||||
QtCore.QPointF.__init__(self, *args)
|
||||
|
||||
def __len__(self):
|
||||
return 2
|
||||
|
||||
def __reduce__(self):
|
||||
return (Point, (self.x(), self.y()))
|
||||
|
||||
def __getitem__(self, i):
|
||||
if i == 0:
|
||||
return self.x()
|
||||
elif i == 1:
|
||||
return self.y()
|
||||
else:
|
||||
raise IndexError("Point has no index %s" % str(i))
|
||||
|
||||
def __setitem__(self, i, x):
|
||||
if i == 0:
|
||||
return self.setX(x)
|
||||
elif i == 1:
|
||||
return self.setY(x)
|
||||
else:
|
||||
raise IndexError("Point has no index %s" % str(i))
|
||||
|
||||
def __radd__(self, a):
|
||||
return self._math_('__radd__', a)
|
||||
|
||||
def __add__(self, a):
|
||||
return self._math_('__add__', a)
|
||||
|
||||
def __rsub__(self, a):
|
||||
return self._math_('__rsub__', a)
|
||||
|
||||
def __sub__(self, a):
|
||||
return self._math_('__sub__', a)
|
||||
|
||||
def __rmul__(self, a):
|
||||
return self._math_('__rmul__', a)
|
||||
|
||||
def __mul__(self, a):
|
||||
return self._math_('__mul__', a)
|
||||
|
||||
def __rdiv__(self, a):
|
||||
return self._math_('__rdiv__', a)
|
||||
|
||||
def __div__(self, a):
|
||||
return self._math_('__div__', a)
|
||||
|
||||
def __truediv__(self, a):
|
||||
return self._math_('__truediv__', a)
|
||||
|
||||
def __rtruediv__(self, a):
|
||||
return self._math_('__rtruediv__', a)
|
||||
|
||||
def __rpow__(self, a):
|
||||
return self._math_('__rpow__', a)
|
||||
|
||||
def __pow__(self, a):
|
||||
return self._math_('__pow__', a)
|
||||
|
||||
def _math_(self, op, x):
|
||||
#print "point math:", op
|
||||
#try:
|
||||
#fn = getattr(QtCore.QPointF, op)
|
||||
#pt = fn(self, x)
|
||||
#print fn, pt, self, x
|
||||
#return Point(pt)
|
||||
#except AttributeError:
|
||||
x = Point(x)
|
||||
return Point(getattr(self[0], op)(x[0]), getattr(self[1], op)(x[1]))
|
||||
|
||||
def length(self):
|
||||
"""Returns the vector length of this Point."""
|
||||
return (self[0]**2 + self[1]**2) ** 0.5
|
||||
|
||||
def norm(self):
|
||||
"""Returns a vector in the same direction with unit length."""
|
||||
return self / self.length()
|
||||
|
||||
def angle(self, a):
|
||||
"""Returns the angle in degrees between this vector and the vector a."""
|
||||
n1 = self.length()
|
||||
n2 = a.length()
|
||||
if n1 == 0. or n2 == 0.:
|
||||
return None
|
||||
## Probably this should be done with arctan2 instead..
|
||||
ang = np.arccos(clip(self.dot(a) / (n1 * n2), -1.0, 1.0)) ### in radians
|
||||
c = self.cross(a)
|
||||
if c > 0:
|
||||
ang *= -1.
|
||||
return ang * 180. / np.pi
|
||||
|
||||
def dot(self, a):
|
||||
"""Returns the dot product of a and this Point."""
|
||||
a = Point(a)
|
||||
return self[0]*a[0] + self[1]*a[1]
|
||||
|
||||
def cross(self, a):
|
||||
a = Point(a)
|
||||
return self[0]*a[1] - self[1]*a[0]
|
||||
|
||||
def proj(self, b):
|
||||
"""Return the projection of this vector onto the vector b"""
|
||||
b1 = b / b.length()
|
||||
return self.dot(b1) * b1
|
||||
|
||||
def __repr__(self):
|
||||
return "Point(%f, %f)" % (self[0], self[1])
|
||||
|
||||
|
||||
def min(self):
|
||||
return min(self[0], self[1])
|
||||
|
||||
def max(self):
|
||||
return max(self[0], self[1])
|
||||
|
||||
def copy(self):
|
||||
return Point(self)
|
||||
|
||||
def toQPoint(self):
|
||||
return QtCore.QPoint(*self)
|
206
pyqtgraph/Qt.py
Normal file
206
pyqtgraph/Qt.py
Normal file
|
@ -0,0 +1,206 @@
|
|||
"""
|
||||
This module exists to smooth out some of the differences between PySide and PyQt4:
|
||||
|
||||
* Automatically import either PyQt4 or PySide depending on availability
|
||||
* Allow to import QtCore/QtGui pyqtgraph.Qt without specifying which Qt wrapper
|
||||
you want to use.
|
||||
* Declare QtCore.Signal, .Slot in PyQt4
|
||||
* Declare loadUiType function for Pyside
|
||||
|
||||
"""
|
||||
|
||||
import sys, re
|
||||
|
||||
from .python2_3 import asUnicode
|
||||
|
||||
PYSIDE = 'PySide'
|
||||
PYQT4 = 'PyQt4'
|
||||
PYQT5 = 'PyQt5'
|
||||
|
||||
QT_LIB = None
|
||||
|
||||
## Automatically determine whether to use PyQt or PySide.
|
||||
## This is done by first checking to see whether one of the libraries
|
||||
## is already imported. If not, then attempt to import PyQt4, then PySide.
|
||||
libOrder = [PYQT4, PYSIDE, PYQT5]
|
||||
|
||||
for lib in libOrder:
|
||||
if lib in sys.modules:
|
||||
QT_LIB = lib
|
||||
break
|
||||
|
||||
if QT_LIB is None:
|
||||
for lib in libOrder:
|
||||
try:
|
||||
__import__(lib)
|
||||
QT_LIB = lib
|
||||
break
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if QT_LIB == None:
|
||||
raise Exception("PyQtGraph requires one of PyQt4, PyQt5 or PySide; none of these packages could be imported.")
|
||||
|
||||
if QT_LIB == PYSIDE:
|
||||
from PySide import QtGui, QtCore, QtOpenGL, QtSvg
|
||||
try:
|
||||
from PySide import QtTest
|
||||
except ImportError:
|
||||
pass
|
||||
import PySide
|
||||
try:
|
||||
from PySide import shiboken
|
||||
isQObjectAlive = shiboken.isValid
|
||||
except ImportError:
|
||||
def isQObjectAlive(obj):
|
||||
try:
|
||||
if hasattr(obj, 'parent'):
|
||||
obj.parent()
|
||||
elif hasattr(obj, 'parentItem'):
|
||||
obj.parentItem()
|
||||
else:
|
||||
raise Exception("Cannot determine whether Qt object %s is still alive." % obj)
|
||||
except RuntimeError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
VERSION_INFO = 'PySide ' + PySide.__version__
|
||||
|
||||
# Make a loadUiType function like PyQt has
|
||||
|
||||
# Credit:
|
||||
# http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313
|
||||
|
||||
class StringIO(object):
|
||||
"""Alternative to built-in StringIO needed to circumvent unicode/ascii issues"""
|
||||
def __init__(self):
|
||||
self.data = []
|
||||
|
||||
def write(self, data):
|
||||
self.data.append(data)
|
||||
|
||||
def getvalue(self):
|
||||
return ''.join(map(asUnicode, self.data)).encode('utf8')
|
||||
|
||||
def loadUiType(uiFile):
|
||||
"""
|
||||
Pyside "loadUiType" command like PyQt4 has one, so we have to convert the ui file to py code in-memory first and then execute it in a special frame to retrieve the form_class.
|
||||
"""
|
||||
import pysideuic
|
||||
import xml.etree.ElementTree as xml
|
||||
#from io import StringIO
|
||||
|
||||
parsed = xml.parse(uiFile)
|
||||
widget_class = parsed.find('widget').get('class')
|
||||
form_class = parsed.find('class').text
|
||||
|
||||
with open(uiFile, 'r') as f:
|
||||
o = StringIO()
|
||||
frame = {}
|
||||
|
||||
pysideuic.compileUi(f, o, indent=0)
|
||||
pyc = compile(o.getvalue(), '<string>', 'exec')
|
||||
exec(pyc, frame)
|
||||
|
||||
#Fetch the base_class and form class based on their type in the xml from designer
|
||||
form_class = frame['Ui_%s'%form_class]
|
||||
base_class = eval('QtGui.%s'%widget_class)
|
||||
|
||||
return form_class, base_class
|
||||
|
||||
elif QT_LIB == PYQT4:
|
||||
|
||||
from PyQt4 import QtGui, QtCore, uic
|
||||
try:
|
||||
from PyQt4 import QtSvg
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from PyQt4 import QtOpenGL
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from PyQt4 import QtTest
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR
|
||||
|
||||
elif QT_LIB == PYQT5:
|
||||
|
||||
# We're using PyQt5 which has a different structure so we're going to use a shim to
|
||||
# recreate the Qt4 structure for Qt5
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets, Qt, uic
|
||||
try:
|
||||
from PyQt5 import QtSvg
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from PyQt5 import QtOpenGL
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Re-implement deprecated APIs
|
||||
def scale(self, sx, sy):
|
||||
tr = self.transform()
|
||||
tr.scale(sx, sy)
|
||||
self.setTransform(tr)
|
||||
QtWidgets.QGraphicsItem.scale = scale
|
||||
|
||||
def rotate(self, angle):
|
||||
tr = self.transform()
|
||||
tr.rotate(angle)
|
||||
self.setTransform(tr)
|
||||
QtWidgets.QGraphicsItem.rotate = rotate
|
||||
|
||||
def translate(self, dx, dy):
|
||||
tr = self.transform()
|
||||
tr.translate(dx, dy)
|
||||
self.setTransform(tr)
|
||||
QtWidgets.QGraphicsItem.translate = translate
|
||||
|
||||
def setMargin(self, i):
|
||||
self.setContentsMargins(i, i, i, i)
|
||||
QtWidgets.QGridLayout.setMargin = setMargin
|
||||
|
||||
def setResizeMode(self, mode):
|
||||
self.setSectionResizeMode(mode)
|
||||
QtWidgets.QHeaderView.setResizeMode = setResizeMode
|
||||
|
||||
|
||||
QtGui.QApplication = QtWidgets.QApplication
|
||||
QtGui.QGraphicsScene = QtWidgets.QGraphicsScene
|
||||
QtGui.QGraphicsObject = QtWidgets.QGraphicsObject
|
||||
QtGui.QGraphicsWidget = QtWidgets.QGraphicsWidget
|
||||
|
||||
QtGui.QApplication.setGraphicsSystem = None
|
||||
|
||||
# Import all QtWidgets objects into QtGui
|
||||
for o in dir(QtWidgets):
|
||||
if o.startswith('Q'):
|
||||
setattr(QtGui, o, getattr(QtWidgets,o) )
|
||||
|
||||
VERSION_INFO = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR
|
||||
|
||||
# Common to PyQt4 and 5
|
||||
if QT_LIB.startswith('PyQt'):
|
||||
import sip
|
||||
def isQObjectAlive(obj):
|
||||
return not sip.isdeleted(obj)
|
||||
loadUiType = uic.loadUiType
|
||||
|
||||
QtCore.Signal = QtCore.pyqtSignal
|
||||
|
||||
|
||||
|
||||
## Make sure we have Qt >= 4.7
|
||||
versionReq = [4, 7]
|
||||
USE_PYSIDE = QT_LIB == PYSIDE
|
||||
USE_PYQT4 = QT_LIB == PYQT4
|
||||
USE_PYQT5 = QT_LIB == PYQT5
|
||||
QtVersion = PySide.QtCore.__version__ if QT_LIB == PYSIDE else QtCore.QT_VERSION_STR
|
||||
m = re.match(r'(\d+)\.(\d+).*', QtVersion)
|
||||
if m is not None and list(map(int, m.groups())) < versionReq:
|
||||
print(list(map(int, m.groups())))
|
||||
raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion))
|
258
pyqtgraph/SRTTransform.py
Normal file
258
pyqtgraph/SRTTransform.py
Normal file
|
@ -0,0 +1,258 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .Qt import QtCore, QtGui
|
||||
from .Point import Point
|
||||
import numpy as np
|
||||
|
||||
class SRTTransform(QtGui.QTransform):
|
||||
"""Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate
|
||||
This transform has no shear; angles are always preserved.
|
||||
"""
|
||||
def __init__(self, init=None):
|
||||
QtGui.QTransform.__init__(self)
|
||||
self.reset()
|
||||
|
||||
if init is None:
|
||||
return
|
||||
elif isinstance(init, dict):
|
||||
self.restoreState(init)
|
||||
elif isinstance(init, SRTTransform):
|
||||
self._state = {
|
||||
'pos': Point(init._state['pos']),
|
||||
'scale': Point(init._state['scale']),
|
||||
'angle': init._state['angle']
|
||||
}
|
||||
self.update()
|
||||
elif isinstance(init, QtGui.QTransform):
|
||||
self.setFromQTransform(init)
|
||||
elif isinstance(init, QtGui.QMatrix4x4):
|
||||
self.setFromMatrix4x4(init)
|
||||
else:
|
||||
raise Exception("Cannot create SRTTransform from input type: %s" % str(type(init)))
|
||||
|
||||
|
||||
def getScale(self):
|
||||
return self._state['scale']
|
||||
|
||||
def getAngle(self):
|
||||
## deprecated; for backward compatibility
|
||||
return self.getRotation()
|
||||
|
||||
def getRotation(self):
|
||||
return self._state['angle']
|
||||
|
||||
def getTranslation(self):
|
||||
return self._state['pos']
|
||||
|
||||
def reset(self):
|
||||
self._state = {
|
||||
'pos': Point(0,0),
|
||||
'scale': Point(1,1),
|
||||
'angle': 0.0 ## in degrees
|
||||
}
|
||||
self.update()
|
||||
|
||||
def setFromQTransform(self, tr):
|
||||
p1 = Point(tr.map(0., 0.))
|
||||
p2 = Point(tr.map(1., 0.))
|
||||
p3 = Point(tr.map(0., 1.))
|
||||
|
||||
dp2 = Point(p2-p1)
|
||||
dp3 = Point(p3-p1)
|
||||
|
||||
## detect flipped axes
|
||||
if dp2.angle(dp3) > 0:
|
||||
#da = 180
|
||||
da = 0
|
||||
sy = -1.0
|
||||
else:
|
||||
da = 0
|
||||
sy = 1.0
|
||||
|
||||
self._state = {
|
||||
'pos': Point(p1),
|
||||
'scale': Point(dp2.length(), dp3.length() * sy),
|
||||
'angle': (np.arctan2(dp2[1], dp2[0]) * 180. / np.pi) + da
|
||||
}
|
||||
self.update()
|
||||
|
||||
def setFromMatrix4x4(self, m):
|
||||
m = SRTTransform3D(m)
|
||||
angle, axis = m.getRotation()
|
||||
if angle != 0 and (axis[0] != 0 or axis[1] != 0 or axis[2] != 1):
|
||||
print("angle: %s axis: %s" % (str(angle), str(axis)))
|
||||
raise Exception("Can only convert 4x4 matrix to 3x3 if rotation is around Z-axis.")
|
||||
self._state = {
|
||||
'pos': Point(m.getTranslation()),
|
||||
'scale': Point(m.getScale()),
|
||||
'angle': angle
|
||||
}
|
||||
self.update()
|
||||
|
||||
def translate(self, *args):
|
||||
"""Acceptable arguments are:
|
||||
x, y
|
||||
[x, y]
|
||||
Point(x,y)"""
|
||||
t = Point(*args)
|
||||
self.setTranslate(self._state['pos']+t)
|
||||
|
||||
def setTranslate(self, *args):
|
||||
"""Acceptable arguments are:
|
||||
x, y
|
||||
[x, y]
|
||||
Point(x,y)"""
|
||||
self._state['pos'] = Point(*args)
|
||||
self.update()
|
||||
|
||||
def scale(self, *args):
|
||||
"""Acceptable arguments are:
|
||||
x, y
|
||||
[x, y]
|
||||
Point(x,y)"""
|
||||
s = Point(*args)
|
||||
self.setScale(self._state['scale'] * s)
|
||||
|
||||
def setScale(self, *args):
|
||||
"""Acceptable arguments are:
|
||||
x, y
|
||||
[x, y]
|
||||
Point(x,y)"""
|
||||
self._state['scale'] = Point(*args)
|
||||
self.update()
|
||||
|
||||
def rotate(self, angle):
|
||||
"""Rotate the transformation by angle (in degrees)"""
|
||||
self.setRotate(self._state['angle'] + angle)
|
||||
|
||||
def setRotate(self, angle):
|
||||
"""Set the transformation rotation to angle (in degrees)"""
|
||||
self._state['angle'] = angle
|
||||
self.update()
|
||||
|
||||
def __truediv__(self, t):
|
||||
"""A / B == B^-1 * A"""
|
||||
dt = t.inverted()[0] * self
|
||||
return SRTTransform(dt)
|
||||
|
||||
def __div__(self, t):
|
||||
return self.__truediv__(t)
|
||||
|
||||
def __mul__(self, t):
|
||||
return SRTTransform(QtGui.QTransform.__mul__(self, t))
|
||||
|
||||
def saveState(self):
|
||||
p = self._state['pos']
|
||||
s = self._state['scale']
|
||||
#if s[0] == 0:
|
||||
#raise Exception('Invalid scale: %s' % str(s))
|
||||
return {'pos': (p[0], p[1]), 'scale': (s[0], s[1]), 'angle': self._state['angle']}
|
||||
|
||||
def restoreState(self, state):
|
||||
self._state['pos'] = Point(state.get('pos', (0,0)))
|
||||
self._state['scale'] = Point(state.get('scale', (1.,1.)))
|
||||
self._state['angle'] = state.get('angle', 0)
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
QtGui.QTransform.reset(self)
|
||||
## modifications to the transform are multiplied on the right, so we need to reverse order here.
|
||||
QtGui.QTransform.translate(self, *self._state['pos'])
|
||||
QtGui.QTransform.rotate(self, self._state['angle'])
|
||||
QtGui.QTransform.scale(self, *self._state['scale'])
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.saveState())
|
||||
|
||||
def matrix(self):
|
||||
return np.array([[self.m11(), self.m12(), self.m13()],[self.m21(), self.m22(), self.m23()],[self.m31(), self.m32(), self.m33()]])
|
||||
|
||||
if __name__ == '__main__':
|
||||
from . import widgets
|
||||
import GraphicsView
|
||||
from .functions import *
|
||||
app = QtGui.QApplication([])
|
||||
win = QtGui.QMainWindow()
|
||||
win.show()
|
||||
cw = GraphicsView.GraphicsView()
|
||||
#cw.enableMouse()
|
||||
win.setCentralWidget(cw)
|
||||
s = QtGui.QGraphicsScene()
|
||||
cw.setScene(s)
|
||||
win.resize(600,600)
|
||||
cw.enableMouse()
|
||||
cw.setRange(QtCore.QRectF(-100., -100., 200., 200.))
|
||||
|
||||
class Item(QtGui.QGraphicsItem):
|
||||
def __init__(self):
|
||||
QtGui.QGraphicsItem.__init__(self)
|
||||
self.b = QtGui.QGraphicsRectItem(20, 20, 20, 20, self)
|
||||
self.b.setPen(QtGui.QPen(mkPen('y')))
|
||||
self.t1 = QtGui.QGraphicsTextItem(self)
|
||||
self.t1.setHtml('<span style="color: #F00">R</span>')
|
||||
self.t1.translate(20, 20)
|
||||
self.l1 = QtGui.QGraphicsLineItem(10, 0, -10, 0, self)
|
||||
self.l2 = QtGui.QGraphicsLineItem(0, 10, 0, -10, self)
|
||||
self.l1.setPen(QtGui.QPen(mkPen('y')))
|
||||
self.l2.setPen(QtGui.QPen(mkPen('y')))
|
||||
def boundingRect(self):
|
||||
return QtCore.QRectF()
|
||||
def paint(self, *args):
|
||||
pass
|
||||
|
||||
#s.addItem(b)
|
||||
#s.addItem(t1)
|
||||
item = Item()
|
||||
s.addItem(item)
|
||||
l1 = QtGui.QGraphicsLineItem(10, 0, -10, 0)
|
||||
l2 = QtGui.QGraphicsLineItem(0, 10, 0, -10)
|
||||
l1.setPen(QtGui.QPen(mkPen('r')))
|
||||
l2.setPen(QtGui.QPen(mkPen('r')))
|
||||
s.addItem(l1)
|
||||
s.addItem(l2)
|
||||
|
||||
tr1 = SRTTransform()
|
||||
tr2 = SRTTransform()
|
||||
tr3 = QtGui.QTransform()
|
||||
tr3.translate(20, 0)
|
||||
tr3.rotate(45)
|
||||
print("QTransform -> Transform:", SRTTransform(tr3))
|
||||
|
||||
print("tr1:", tr1)
|
||||
|
||||
tr2.translate(20, 0)
|
||||
tr2.rotate(45)
|
||||
print("tr2:", tr2)
|
||||
|
||||
dt = tr2/tr1
|
||||
print("tr2 / tr1 = ", dt)
|
||||
|
||||
print("tr2 * tr1 = ", tr2*tr1)
|
||||
|
||||
tr4 = SRTTransform()
|
||||
tr4.scale(-1, 1)
|
||||
tr4.rotate(30)
|
||||
print("tr1 * tr4 = ", tr1*tr4)
|
||||
|
||||
w1 = widgets.TestROI((19,19), (22, 22), invertible=True)
|
||||
#w2 = widgets.TestROI((0,0), (150, 150))
|
||||
w1.setZValue(10)
|
||||
s.addItem(w1)
|
||||
#s.addItem(w2)
|
||||
w1Base = w1.getState()
|
||||
#w2Base = w2.getState()
|
||||
def update():
|
||||
tr1 = w1.getGlobalTransform(w1Base)
|
||||
#tr2 = w2.getGlobalTransform(w2Base)
|
||||
item.setTransform(tr1)
|
||||
|
||||
#def update2():
|
||||
#tr1 = w1.getGlobalTransform(w1Base)
|
||||
#tr2 = w2.getGlobalTransform(w2Base)
|
||||
#t1.setTransform(tr1)
|
||||
#w1.setState(w1Base)
|
||||
#w1.applyGlobalTransform(tr2)
|
||||
|
||||
w1.sigRegionChanged.connect(update)
|
||||
#w2.sigRegionChanged.connect(update2)
|
||||
|
||||
from .SRTTransform3D import SRTTransform3D
|
315
pyqtgraph/SRTTransform3D.py
Normal file
315
pyqtgraph/SRTTransform3D.py
Normal file
|
@ -0,0 +1,315 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .Qt import QtCore, QtGui
|
||||
from .Vector import Vector
|
||||
from .Transform3D import Transform3D
|
||||
from .Vector import Vector
|
||||
import numpy as np
|
||||
|
||||
class SRTTransform3D(Transform3D):
|
||||
"""4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate
|
||||
This transform has no shear; angles are always preserved.
|
||||
"""
|
||||
def __init__(self, init=None):
|
||||
Transform3D.__init__(self)
|
||||
self.reset()
|
||||
if init is None:
|
||||
return
|
||||
if init.__class__ is QtGui.QTransform:
|
||||
init = SRTTransform(init)
|
||||
|
||||
if isinstance(init, dict):
|
||||
self.restoreState(init)
|
||||
elif isinstance(init, SRTTransform3D):
|
||||
self._state = {
|
||||
'pos': Vector(init._state['pos']),
|
||||
'scale': Vector(init._state['scale']),
|
||||
'angle': init._state['angle'],
|
||||
'axis': Vector(init._state['axis']),
|
||||
}
|
||||
self.update()
|
||||
elif isinstance(init, SRTTransform):
|
||||
self._state = {
|
||||
'pos': Vector(init._state['pos']),
|
||||
'scale': Vector(init._state['scale']),
|
||||
'angle': init._state['angle'],
|
||||
'axis': Vector(0, 0, 1),
|
||||
}
|
||||
self._state['scale'][2] = 1.0
|
||||
self.update()
|
||||
elif isinstance(init, QtGui.QMatrix4x4):
|
||||
self.setFromMatrix(init)
|
||||
else:
|
||||
raise Exception("Cannot build SRTTransform3D from argument type:", type(init))
|
||||
|
||||
|
||||
def getScale(self):
|
||||
return Vector(self._state['scale'])
|
||||
|
||||
def getRotation(self):
|
||||
"""Return (angle, axis) of rotation"""
|
||||
return self._state['angle'], Vector(self._state['axis'])
|
||||
|
||||
def getTranslation(self):
|
||||
return Vector(self._state['pos'])
|
||||
|
||||
def reset(self):
|
||||
self._state = {
|
||||
'pos': Vector(0,0,0),
|
||||
'scale': Vector(1,1,1),
|
||||
'angle': 0.0, ## in degrees
|
||||
'axis': (0, 0, 1)
|
||||
}
|
||||
self.update()
|
||||
|
||||
def translate(self, *args):
|
||||
"""Adjust the translation of this transform"""
|
||||
t = Vector(*args)
|
||||
self.setTranslate(self._state['pos']+t)
|
||||
|
||||
def setTranslate(self, *args):
|
||||
"""Set the translation of this transform"""
|
||||
self._state['pos'] = Vector(*args)
|
||||
self.update()
|
||||
|
||||
def scale(self, *args):
|
||||
"""adjust the scale of this transform"""
|
||||
## try to prevent accidentally setting 0 scale on z axis
|
||||
if len(args) == 1 and hasattr(args[0], '__len__'):
|
||||
args = args[0]
|
||||
if len(args) == 2:
|
||||
args = args + (1,)
|
||||
|
||||
s = Vector(*args)
|
||||
self.setScale(self._state['scale'] * s)
|
||||
|
||||
def setScale(self, *args):
|
||||
"""Set the scale of this transform"""
|
||||
if len(args) == 1 and hasattr(args[0], '__len__'):
|
||||
args = args[0]
|
||||
if len(args) == 2:
|
||||
args = args + (1,)
|
||||
self._state['scale'] = Vector(*args)
|
||||
self.update()
|
||||
|
||||
def rotate(self, angle, axis=(0,0,1)):
|
||||
"""Adjust the rotation of this transform"""
|
||||
origAxis = self._state['axis']
|
||||
if axis[0] == origAxis[0] and axis[1] == origAxis[1] and axis[2] == origAxis[2]:
|
||||
self.setRotate(self._state['angle'] + angle)
|
||||
else:
|
||||
m = QtGui.QMatrix4x4()
|
||||
m.translate(*self._state['pos'])
|
||||
m.rotate(self._state['angle'], *self._state['axis'])
|
||||
m.rotate(angle, *axis)
|
||||
m.scale(*self._state['scale'])
|
||||
self.setFromMatrix(m)
|
||||
|
||||
def setRotate(self, angle, axis=(0,0,1)):
|
||||
"""Set the transformation rotation to angle (in degrees)"""
|
||||
|
||||
self._state['angle'] = angle
|
||||
self._state['axis'] = Vector(axis)
|
||||
self.update()
|
||||
|
||||
def setFromMatrix(self, m):
|
||||
"""
|
||||
Set this transform mased on the elements of *m*
|
||||
The input matrix must be affine AND have no shear,
|
||||
otherwise the conversion will most likely fail.
|
||||
"""
|
||||
import numpy.linalg
|
||||
for i in range(4):
|
||||
self.setRow(i, m.row(i))
|
||||
m = self.matrix().reshape(4,4)
|
||||
## translation is 4th column
|
||||
self._state['pos'] = m[:3,3]
|
||||
|
||||
## scale is vector-length of first three columns
|
||||
scale = (m[:3,:3]**2).sum(axis=0)**0.5
|
||||
## see whether there is an inversion
|
||||
z = np.cross(m[0, :3], m[1, :3])
|
||||
if np.dot(z, m[2, :3]) < 0:
|
||||
scale[1] *= -1 ## doesn't really matter which axis we invert
|
||||
self._state['scale'] = scale
|
||||
|
||||
## rotation axis is the eigenvector with eigenvalue=1
|
||||
r = m[:3, :3] / scale[np.newaxis, :]
|
||||
try:
|
||||
evals, evecs = numpy.linalg.eig(r)
|
||||
except:
|
||||
print("Rotation matrix: %s" % str(r))
|
||||
print("Scale: %s" % str(scale))
|
||||
print("Original matrix: %s" % str(m))
|
||||
raise
|
||||
eigIndex = np.argwhere(np.abs(evals-1) < 1e-6)
|
||||
if len(eigIndex) < 1:
|
||||
print("eigenvalues: %s" % str(evals))
|
||||
print("eigenvectors: %s" % str(evecs))
|
||||
print("index: %s, %s" % (str(eigIndex), str(evals-1)))
|
||||
raise Exception("Could not determine rotation axis.")
|
||||
axis = evecs[:,eigIndex[0,0]].real
|
||||
axis /= ((axis**2).sum())**0.5
|
||||
self._state['axis'] = axis
|
||||
|
||||
## trace(r) == 2 cos(angle) + 1, so:
|
||||
cos = (r.trace()-1)*0.5 ## this only gets us abs(angle)
|
||||
|
||||
## The off-diagonal values can be used to correct the angle ambiguity,
|
||||
## but we need to figure out which element to use:
|
||||
axisInd = np.argmax(np.abs(axis))
|
||||
rInd,sign = [((1,2), -1), ((0,2), 1), ((0,1), -1)][axisInd]
|
||||
|
||||
## Then we have r-r.T = sin(angle) * 2 * sign * axis[axisInd];
|
||||
## solve for sin(angle)
|
||||
sin = (r-r.T)[rInd] / (2. * sign * axis[axisInd])
|
||||
|
||||
## finally, we get the complete angle from arctan(sin/cos)
|
||||
self._state['angle'] = np.arctan2(sin, cos) * 180 / np.pi
|
||||
if self._state['angle'] == 0:
|
||||
self._state['axis'] = (0,0,1)
|
||||
|
||||
def as2D(self):
|
||||
"""Return a QTransform representing the x,y portion of this transform (if possible)"""
|
||||
return SRTTransform(self)
|
||||
|
||||
#def __div__(self, t):
|
||||
#"""A / B == B^-1 * A"""
|
||||
#dt = t.inverted()[0] * self
|
||||
#return SRTTransform(dt)
|
||||
|
||||
#def __mul__(self, t):
|
||||
#return SRTTransform(QtGui.QTransform.__mul__(self, t))
|
||||
|
||||
def saveState(self):
|
||||
p = self._state['pos']
|
||||
s = self._state['scale']
|
||||
ax = self._state['axis']
|
||||
#if s[0] == 0:
|
||||
#raise Exception('Invalid scale: %s' % str(s))
|
||||
return {
|
||||
'pos': (p[0], p[1], p[2]),
|
||||
'scale': (s[0], s[1], s[2]),
|
||||
'angle': self._state['angle'],
|
||||
'axis': (ax[0], ax[1], ax[2])
|
||||
}
|
||||
|
||||
def restoreState(self, state):
|
||||
self._state['pos'] = Vector(state.get('pos', (0.,0.,0.)))
|
||||
scale = state.get('scale', (1.,1.,1.))
|
||||
scale = tuple(scale) + (1.,) * (3-len(scale))
|
||||
self._state['scale'] = Vector(scale)
|
||||
self._state['angle'] = state.get('angle', 0.)
|
||||
self._state['axis'] = state.get('axis', (0, 0, 1))
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
Transform3D.setToIdentity(self)
|
||||
## modifications to the transform are multiplied on the right, so we need to reverse order here.
|
||||
Transform3D.translate(self, *self._state['pos'])
|
||||
Transform3D.rotate(self, self._state['angle'], *self._state['axis'])
|
||||
Transform3D.scale(self, *self._state['scale'])
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.saveState())
|
||||
|
||||
def matrix(self, nd=3):
|
||||
if nd == 3:
|
||||
return np.array(self.copyDataTo()).reshape(4,4)
|
||||
elif nd == 2:
|
||||
m = np.array(self.copyDataTo()).reshape(4,4)
|
||||
m[2] = m[3]
|
||||
m[:,2] = m[:,3]
|
||||
return m[:3,:3]
|
||||
else:
|
||||
raise Exception("Argument 'nd' must be 2 or 3")
|
||||
|
||||
if __name__ == '__main__':
|
||||
import widgets
|
||||
import GraphicsView
|
||||
from functions import *
|
||||
app = QtGui.QApplication([])
|
||||
win = QtGui.QMainWindow()
|
||||
win.show()
|
||||
cw = GraphicsView.GraphicsView()
|
||||
#cw.enableMouse()
|
||||
win.setCentralWidget(cw)
|
||||
s = QtGui.QGraphicsScene()
|
||||
cw.setScene(s)
|
||||
win.resize(600,600)
|
||||
cw.enableMouse()
|
||||
cw.setRange(QtCore.QRectF(-100., -100., 200., 200.))
|
||||
|
||||
class Item(QtGui.QGraphicsItem):
|
||||
def __init__(self):
|
||||
QtGui.QGraphicsItem.__init__(self)
|
||||
self.b = QtGui.QGraphicsRectItem(20, 20, 20, 20, self)
|
||||
self.b.setPen(QtGui.QPen(mkPen('y')))
|
||||
self.t1 = QtGui.QGraphicsTextItem(self)
|
||||
self.t1.setHtml('<span style="color: #F00">R</span>')
|
||||
self.t1.translate(20, 20)
|
||||
self.l1 = QtGui.QGraphicsLineItem(10, 0, -10, 0, self)
|
||||
self.l2 = QtGui.QGraphicsLineItem(0, 10, 0, -10, self)
|
||||
self.l1.setPen(QtGui.QPen(mkPen('y')))
|
||||
self.l2.setPen(QtGui.QPen(mkPen('y')))
|
||||
def boundingRect(self):
|
||||
return QtCore.QRectF()
|
||||
def paint(self, *args):
|
||||
pass
|
||||
|
||||
#s.addItem(b)
|
||||
#s.addItem(t1)
|
||||
item = Item()
|
||||
s.addItem(item)
|
||||
l1 = QtGui.QGraphicsLineItem(10, 0, -10, 0)
|
||||
l2 = QtGui.QGraphicsLineItem(0, 10, 0, -10)
|
||||
l1.setPen(QtGui.QPen(mkPen('r')))
|
||||
l2.setPen(QtGui.QPen(mkPen('r')))
|
||||
s.addItem(l1)
|
||||
s.addItem(l2)
|
||||
|
||||
tr1 = SRTTransform()
|
||||
tr2 = SRTTransform()
|
||||
tr3 = QtGui.QTransform()
|
||||
tr3.translate(20, 0)
|
||||
tr3.rotate(45)
|
||||
print("QTransform -> Transform: %s" % str(SRTTransform(tr3)))
|
||||
|
||||
print("tr1: %s" % str(tr1))
|
||||
|
||||
tr2.translate(20, 0)
|
||||
tr2.rotate(45)
|
||||
print("tr2: %s" % str(tr2))
|
||||
|
||||
dt = tr2/tr1
|
||||
print("tr2 / tr1 = %s" % str(dt))
|
||||
|
||||
print("tr2 * tr1 = %s" % str(tr2*tr1))
|
||||
|
||||
tr4 = SRTTransform()
|
||||
tr4.scale(-1, 1)
|
||||
tr4.rotate(30)
|
||||
print("tr1 * tr4 = %s" % str(tr1*tr4))
|
||||
|
||||
w1 = widgets.TestROI((19,19), (22, 22), invertible=True)
|
||||
#w2 = widgets.TestROI((0,0), (150, 150))
|
||||
w1.setZValue(10)
|
||||
s.addItem(w1)
|
||||
#s.addItem(w2)
|
||||
w1Base = w1.getState()
|
||||
#w2Base = w2.getState()
|
||||
def update():
|
||||
tr1 = w1.getGlobalTransform(w1Base)
|
||||
#tr2 = w2.getGlobalTransform(w2Base)
|
||||
item.setTransform(tr1)
|
||||
|
||||
#def update2():
|
||||
#tr1 = w1.getGlobalTransform(w1Base)
|
||||
#tr2 = w2.getGlobalTransform(w2Base)
|
||||
#t1.setTransform(tr1)
|
||||
#w1.setState(w1Base)
|
||||
#w1.applyGlobalTransform(tr2)
|
||||
|
||||
w1.sigRegionChanged.connect(update)
|
||||
#w2.sigRegionChanged.connect(update2)
|
||||
|
||||
from .SRTTransform import SRTTransform
|
119
pyqtgraph/SignalProxy.py
Normal file
119
pyqtgraph/SignalProxy.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .Qt import QtCore
|
||||
from .ptime import time
|
||||
from . import ThreadsafeTimer
|
||||
import weakref
|
||||
|
||||
__all__ = ['SignalProxy']
|
||||
|
||||
class SignalProxy(QtCore.QObject):
|
||||
"""Object which collects rapid-fire signals and condenses them
|
||||
into a single signal or a rate-limited stream of signals.
|
||||
Used, for example, to prevent a SpinBox from generating multiple
|
||||
signals when the mouse wheel is rolled over it.
|
||||
|
||||
Emits sigDelayed after input signals have stopped for a certain period of time.
|
||||
"""
|
||||
|
||||
sigDelayed = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, signal, delay=0.3, rateLimit=0, slot=None):
|
||||
"""Initialization arguments:
|
||||
signal - a bound Signal or pyqtSignal instance
|
||||
delay - Time (in seconds) to wait for signals to stop before emitting (default 0.3s)
|
||||
slot - Optional function to connect sigDelayed to.
|
||||
rateLimit - (signals/sec) if greater than 0, this allows signals to stream out at a
|
||||
steady rate while they are being received.
|
||||
"""
|
||||
|
||||
QtCore.QObject.__init__(self)
|
||||
signal.connect(self.signalReceived)
|
||||
self.signal = signal
|
||||
self.delay = delay
|
||||
self.rateLimit = rateLimit
|
||||
self.args = None
|
||||
self.timer = ThreadsafeTimer.ThreadsafeTimer()
|
||||
self.timer.timeout.connect(self.flush)
|
||||
self.block = False
|
||||
self.slot = weakref.ref(slot)
|
||||
self.lastFlushTime = None
|
||||
if slot is not None:
|
||||
self.sigDelayed.connect(slot)
|
||||
|
||||
def setDelay(self, delay):
|
||||
self.delay = delay
|
||||
|
||||
def signalReceived(self, *args):
|
||||
"""Received signal. Cancel previous timer and store args to be forwarded later."""
|
||||
if self.block:
|
||||
return
|
||||
self.args = args
|
||||
if self.rateLimit == 0:
|
||||
self.timer.stop()
|
||||
self.timer.start((self.delay*1000)+1)
|
||||
else:
|
||||
now = time()
|
||||
if self.lastFlushTime is None:
|
||||
leakTime = 0
|
||||
else:
|
||||
lastFlush = self.lastFlushTime
|
||||
leakTime = max(0, (lastFlush + (1.0 / self.rateLimit)) - now)
|
||||
|
||||
self.timer.stop()
|
||||
self.timer.start((min(leakTime, self.delay)*1000)+1)
|
||||
|
||||
|
||||
def flush(self):
|
||||
"""If there is a signal queued up, send it now."""
|
||||
if self.args is None or self.block:
|
||||
return False
|
||||
#self.emit(self.signal, *self.args)
|
||||
self.sigDelayed.emit(self.args)
|
||||
self.args = None
|
||||
self.timer.stop()
|
||||
self.lastFlushTime = time()
|
||||
return True
|
||||
|
||||
def disconnect(self):
|
||||
self.block = True
|
||||
try:
|
||||
self.signal.disconnect(self.signalReceived)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
self.sigDelayed.disconnect(self.slot())
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
#def proxyConnect(source, signal, slot, delay=0.3):
|
||||
#"""Connect a signal to a slot with delay. Returns the SignalProxy
|
||||
#object that was created. Be sure to store this object so it is not
|
||||
#garbage-collected immediately."""
|
||||
#sp = SignalProxy(source, signal, delay)
|
||||
#if source is None:
|
||||
#sp.connect(sp, QtCore.SIGNAL('signal'), slot)
|
||||
#else:
|
||||
#sp.connect(sp, signal, slot)
|
||||
#return sp
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from .Qt import QtGui
|
||||
app = QtGui.QApplication([])
|
||||
win = QtGui.QMainWindow()
|
||||
spin = QtGui.QSpinBox()
|
||||
win.setCentralWidget(spin)
|
||||
win.show()
|
||||
|
||||
def fn(*args):
|
||||
print("Raw signal:", args)
|
||||
def fn2(*args):
|
||||
print("Delayed signal:", args)
|
||||
|
||||
|
||||
spin.valueChanged.connect(fn)
|
||||
#proxy = proxyConnect(spin, QtCore.SIGNAL('valueChanged(int)'), fn)
|
||||
proxy = SignalProxy(spin.valueChanged, delay=0.5, slot=fn2)
|
||||
|
41
pyqtgraph/ThreadsafeTimer.py
Normal file
41
pyqtgraph/ThreadsafeTimer.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
from .Qt import QtCore, QtGui
|
||||
|
||||
class ThreadsafeTimer(QtCore.QObject):
|
||||
"""
|
||||
Thread-safe replacement for QTimer.
|
||||
"""
|
||||
|
||||
timeout = QtCore.Signal()
|
||||
sigTimerStopRequested = QtCore.Signal()
|
||||
sigTimerStartRequested = QtCore.Signal(object)
|
||||
|
||||
def __init__(self):
|
||||
QtCore.QObject.__init__(self)
|
||||
self.timer = QtCore.QTimer()
|
||||
self.timer.timeout.connect(self.timerFinished)
|
||||
self.timer.moveToThread(QtCore.QCoreApplication.instance().thread())
|
||||
self.moveToThread(QtCore.QCoreApplication.instance().thread())
|
||||
self.sigTimerStopRequested.connect(self.stop, QtCore.Qt.QueuedConnection)
|
||||
self.sigTimerStartRequested.connect(self.start, QtCore.Qt.QueuedConnection)
|
||||
|
||||
|
||||
def start(self, timeout):
|
||||
isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
|
||||
if isGuiThread:
|
||||
#print "start timer", self, "from gui thread"
|
||||
self.timer.start(timeout)
|
||||
else:
|
||||
#print "start timer", self, "from remote thread"
|
||||
self.sigTimerStartRequested.emit(timeout)
|
||||
|
||||
def stop(self):
|
||||
isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
|
||||
if isGuiThread:
|
||||
#print "stop timer", self, "from gui thread"
|
||||
self.timer.stop()
|
||||
else:
|
||||
#print "stop timer", self, "from remote thread"
|
||||
self.sigTimerStopRequested.emit()
|
||||
|
||||
def timerFinished(self):
|
||||
self.timeout.emit()
|
35
pyqtgraph/Transform3D.py
Normal file
35
pyqtgraph/Transform3D.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .Qt import QtCore, QtGui
|
||||
from . import functions as fn
|
||||
import numpy as np
|
||||
|
||||
class Transform3D(QtGui.QMatrix4x4):
|
||||
"""
|
||||
Extension of QMatrix4x4 with some helpful methods added.
|
||||
"""
|
||||
def __init__(self, *args):
|
||||
QtGui.QMatrix4x4.__init__(self, *args)
|
||||
|
||||
def matrix(self, nd=3):
|
||||
if nd == 3:
|
||||
return np.array(self.copyDataTo()).reshape(4,4)
|
||||
elif nd == 2:
|
||||
m = np.array(self.copyDataTo()).reshape(4,4)
|
||||
m[2] = m[3]
|
||||
m[:,2] = m[:,3]
|
||||
return m[:3,:3]
|
||||
else:
|
||||
raise Exception("Argument 'nd' must be 2 or 3")
|
||||
|
||||
def map(self, obj):
|
||||
"""
|
||||
Extends QMatrix4x4.map() to allow mapping (3, ...) arrays of coordinates
|
||||
"""
|
||||
if isinstance(obj, np.ndarray) and obj.ndim >= 2 and obj.shape[0] in (2,3):
|
||||
return fn.transformCoordinates(self, obj)
|
||||
else:
|
||||
return QtGui.QMatrix4x4.map(self, obj)
|
||||
|
||||
def inverted(self):
|
||||
inv, b = QtGui.QMatrix4x4.inverted(self)
|
||||
return Transform3D(inv), b
|
87
pyqtgraph/Vector.py
Normal file
87
pyqtgraph/Vector.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Vector.py - Extension of QVector3D which adds a few missing methods.
|
||||
Copyright 2010 Luke Campagnola
|
||||
Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
"""
|
||||
|
||||
from .Qt import QtGui, QtCore, USE_PYSIDE
|
||||
import numpy as np
|
||||
|
||||
class Vector(QtGui.QVector3D):
|
||||
"""Extension of QVector3D which adds a few helpful methods."""
|
||||
|
||||
def __init__(self, *args):
|
||||
if len(args) == 1:
|
||||
if isinstance(args[0], QtCore.QSizeF):
|
||||
QtGui.QVector3D.__init__(self, float(args[0].width()), float(args[0].height()), 0)
|
||||
return
|
||||
elif isinstance(args[0], QtCore.QPoint) or isinstance(args[0], QtCore.QPointF):
|
||||
QtGui.QVector3D.__init__(self, float(args[0].x()), float(args[0].y()), 0)
|
||||
elif hasattr(args[0], '__getitem__'):
|
||||
vals = list(args[0])
|
||||
if len(vals) == 2:
|
||||
vals.append(0)
|
||||
if len(vals) != 3:
|
||||
raise Exception('Cannot init Vector with sequence of length %d' % len(args[0]))
|
||||
QtGui.QVector3D.__init__(self, *vals)
|
||||
return
|
||||
elif len(args) == 2:
|
||||
QtGui.QVector3D.__init__(self, args[0], args[1], 0)
|
||||
return
|
||||
QtGui.QVector3D.__init__(self, *args)
|
||||
|
||||
def __len__(self):
|
||||
return 3
|
||||
|
||||
def __add__(self, b):
|
||||
# workaround for pyside bug. see https://bugs.launchpad.net/pyqtgraph/+bug/1223173
|
||||
if USE_PYSIDE and isinstance(b, QtGui.QVector3D):
|
||||
b = Vector(b)
|
||||
return QtGui.QVector3D.__add__(self, b)
|
||||
|
||||
#def __reduce__(self):
|
||||
#return (Point, (self.x(), self.y()))
|
||||
|
||||
def __getitem__(self, i):
|
||||
if i == 0:
|
||||
return self.x()
|
||||
elif i == 1:
|
||||
return self.y()
|
||||
elif i == 2:
|
||||
return self.z()
|
||||
else:
|
||||
raise IndexError("Point has no index %s" % str(i))
|
||||
|
||||
def __setitem__(self, i, x):
|
||||
if i == 0:
|
||||
return self.setX(x)
|
||||
elif i == 1:
|
||||
return self.setY(x)
|
||||
elif i == 2:
|
||||
return self.setZ(x)
|
||||
else:
|
||||
raise IndexError("Point has no index %s" % str(i))
|
||||
|
||||
def __iter__(self):
|
||||
yield(self.x())
|
||||
yield(self.y())
|
||||
yield(self.z())
|
||||
|
||||
def angle(self, a):
|
||||
"""Returns the angle in degrees between this vector and the vector a."""
|
||||
n1 = self.length()
|
||||
n2 = a.length()
|
||||
if n1 == 0. or n2 == 0.:
|
||||
return None
|
||||
## Probably this should be done with arctan2 instead..
|
||||
ang = np.arccos(np.clip(QtGui.QVector3D.dotProduct(self, a) / (n1 * n2), -1.0, 1.0)) ### in radians
|
||||
# c = self.crossProduct(a)
|
||||
# if c > 0:
|
||||
# ang *= -1.
|
||||
return ang * 180. / np.pi
|
||||
|
||||
def __abs__(self):
|
||||
return Vector(abs(self.x()), abs(self.y()), abs(self.z()))
|
||||
|
||||
|
286
pyqtgraph/WidgetGroup.py
Normal file
286
pyqtgraph/WidgetGroup.py
Normal file
|
@ -0,0 +1,286 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
WidgetGroup.py - WidgetGroup class for easily managing lots of Qt widgets
|
||||
Copyright 2010 Luke Campagnola
|
||||
Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
|
||||
This class addresses the problem of having to save and restore the state
|
||||
of a large group of widgets.
|
||||
"""
|
||||
|
||||
from .Qt import QtCore, QtGui, USE_PYQT5
|
||||
import weakref, inspect
|
||||
from .python2_3 import asUnicode
|
||||
|
||||
|
||||
__all__ = ['WidgetGroup']
|
||||
|
||||
def splitterState(w):
|
||||
s = str(w.saveState().toPercentEncoding())
|
||||
return s
|
||||
|
||||
def restoreSplitter(w, s):
|
||||
if type(s) is list:
|
||||
w.setSizes(s)
|
||||
elif type(s) is str:
|
||||
w.restoreState(QtCore.QByteArray.fromPercentEncoding(s))
|
||||
else:
|
||||
print("Can't configure QSplitter using object of type", type(s))
|
||||
if w.count() > 0: ## make sure at least one item is not collapsed
|
||||
for i in w.sizes():
|
||||
if i > 0:
|
||||
return
|
||||
w.setSizes([50] * w.count())
|
||||
|
||||
def comboState(w):
|
||||
ind = w.currentIndex()
|
||||
data = w.itemData(ind)
|
||||
#if not data.isValid():
|
||||
if data is not None:
|
||||
try:
|
||||
if not data.isValid():
|
||||
data = None
|
||||
else:
|
||||
data = data.toInt()[0]
|
||||
except AttributeError:
|
||||
pass
|
||||
if data is None:
|
||||
return asUnicode(w.itemText(ind))
|
||||
else:
|
||||
return data
|
||||
|
||||
def setComboState(w, v):
|
||||
if type(v) is int:
|
||||
#ind = w.findData(QtCore.QVariant(v))
|
||||
ind = w.findData(v)
|
||||
if ind > -1:
|
||||
w.setCurrentIndex(ind)
|
||||
return
|
||||
w.setCurrentIndex(w.findText(str(v)))
|
||||
|
||||
|
||||
class WidgetGroup(QtCore.QObject):
|
||||
"""This class takes a list of widgets and keeps an internal record of their
|
||||
state that is always up to date.
|
||||
|
||||
Allows reading and writing from groups of widgets simultaneously.
|
||||
"""
|
||||
|
||||
## List of widget types that can be handled by WidgetGroup.
|
||||
## The value for each type is a tuple (change signal function, get function, set function, [auto-add children])
|
||||
## The change signal function that takes an object and returns a signal that is emitted any time the state of the widget changes, not just
|
||||
## when it is changed by user interaction. (for example, 'clicked' is not a valid signal here)
|
||||
## If the change signal is None, the value of the widget is not cached.
|
||||
## Custom widgets not in this list can be made to work with WidgetGroup by giving them a 'widgetGroupInterface' method
|
||||
## which returns the tuple.
|
||||
classes = {
|
||||
QtGui.QSpinBox:
|
||||
(lambda w: w.valueChanged,
|
||||
QtGui.QSpinBox.value,
|
||||
QtGui.QSpinBox.setValue),
|
||||
QtGui.QDoubleSpinBox:
|
||||
(lambda w: w.valueChanged,
|
||||
QtGui.QDoubleSpinBox.value,
|
||||
QtGui.QDoubleSpinBox.setValue),
|
||||
QtGui.QSplitter:
|
||||
(None,
|
||||
splitterState,
|
||||
restoreSplitter,
|
||||
True),
|
||||
QtGui.QCheckBox:
|
||||
(lambda w: w.stateChanged,
|
||||
QtGui.QCheckBox.isChecked,
|
||||
QtGui.QCheckBox.setChecked),
|
||||
QtGui.QComboBox:
|
||||
(lambda w: w.currentIndexChanged,
|
||||
comboState,
|
||||
setComboState),
|
||||
QtGui.QGroupBox:
|
||||
(lambda w: w.toggled,
|
||||
QtGui.QGroupBox.isChecked,
|
||||
QtGui.QGroupBox.setChecked,
|
||||
True),
|
||||
QtGui.QLineEdit:
|
||||
(lambda w: w.editingFinished,
|
||||
lambda w: str(w.text()),
|
||||
QtGui.QLineEdit.setText),
|
||||
QtGui.QRadioButton:
|
||||
(lambda w: w.toggled,
|
||||
QtGui.QRadioButton.isChecked,
|
||||
QtGui.QRadioButton.setChecked),
|
||||
QtGui.QSlider:
|
||||
(lambda w: w.valueChanged,
|
||||
QtGui.QSlider.value,
|
||||
QtGui.QSlider.setValue),
|
||||
}
|
||||
|
||||
sigChanged = QtCore.Signal(str, object)
|
||||
|
||||
|
||||
def __init__(self, widgetList=None):
|
||||
"""Initialize WidgetGroup, adding specified widgets into this group.
|
||||
widgetList can be:
|
||||
- a list of widget specifications (widget, [name], [scale])
|
||||
- a dict of name: widget pairs
|
||||
- any QObject, and all compatible child widgets will be added recursively.
|
||||
|
||||
The 'scale' parameter for each widget allows QSpinBox to display a different value than the value recorded
|
||||
in the group state (for example, the program may set a spin box value to 100e-6 and have it displayed as 100 to the user)
|
||||
"""
|
||||
QtCore.QObject.__init__(self)
|
||||
self.widgetList = weakref.WeakKeyDictionary() # Make sure widgets don't stick around just because they are listed here
|
||||
self.scales = weakref.WeakKeyDictionary()
|
||||
self.cache = {} ## name:value pairs
|
||||
self.uncachedWidgets = weakref.WeakKeyDictionary()
|
||||
if isinstance(widgetList, QtCore.QObject):
|
||||
self.autoAdd(widgetList)
|
||||
elif isinstance(widgetList, list):
|
||||
for w in widgetList:
|
||||
self.addWidget(*w)
|
||||
elif isinstance(widgetList, dict):
|
||||
for name, w in widgetList.items():
|
||||
self.addWidget(w, name)
|
||||
elif widgetList is None:
|
||||
return
|
||||
else:
|
||||
raise Exception("Wrong argument type %s" % type(widgetList))
|
||||
|
||||
def addWidget(self, w, name=None, scale=None):
|
||||
if not self.acceptsType(w):
|
||||
raise Exception("Widget type %s not supported by WidgetGroup" % type(w))
|
||||
if name is None:
|
||||
name = str(w.objectName())
|
||||
if name == '':
|
||||
raise Exception("Cannot add widget '%s' without a name." % str(w))
|
||||
self.widgetList[w] = name
|
||||
self.scales[w] = scale
|
||||
self.readWidget(w)
|
||||
|
||||
if type(w) in WidgetGroup.classes:
|
||||
signal = WidgetGroup.classes[type(w)][0]
|
||||
else:
|
||||
signal = w.widgetGroupInterface()[0]
|
||||
|
||||
if signal is not None:
|
||||
if inspect.isfunction(signal) or inspect.ismethod(signal):
|
||||
signal = signal(w)
|
||||
signal.connect(self.mkChangeCallback(w))
|
||||
else:
|
||||
self.uncachedWidgets[w] = None
|
||||
|
||||
def findWidget(self, name):
|
||||
for w in self.widgetList:
|
||||
if self.widgetList[w] == name:
|
||||
return w
|
||||
return None
|
||||
|
||||
def interface(self, obj):
|
||||
t = type(obj)
|
||||
if t in WidgetGroup.classes:
|
||||
return WidgetGroup.classes[t]
|
||||
else:
|
||||
return obj.widgetGroupInterface()
|
||||
|
||||
def checkForChildren(self, obj):
|
||||
"""Return true if we should automatically search the children of this object for more."""
|
||||
iface = self.interface(obj)
|
||||
return (len(iface) > 3 and iface[3])
|
||||
|
||||
def autoAdd(self, obj):
|
||||
## Find all children of this object and add them if possible.
|
||||
accepted = self.acceptsType(obj)
|
||||
if accepted:
|
||||
#print "%s auto add %s" % (self.objectName(), obj.objectName())
|
||||
self.addWidget(obj)
|
||||
|
||||
if not accepted or self.checkForChildren(obj):
|
||||
for c in obj.children():
|
||||
self.autoAdd(c)
|
||||
|
||||
def acceptsType(self, obj):
|
||||
for c in WidgetGroup.classes:
|
||||
if isinstance(obj, c):
|
||||
return True
|
||||
if hasattr(obj, 'widgetGroupInterface'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def setScale(self, widget, scale):
|
||||
val = self.readWidget(widget)
|
||||
self.scales[widget] = scale
|
||||
self.setWidget(widget, val)
|
||||
|
||||
def mkChangeCallback(self, w):
|
||||
return lambda *args: self.widgetChanged(w, *args)
|
||||
|
||||
def widgetChanged(self, w, *args):
|
||||
n = self.widgetList[w]
|
||||
v1 = self.cache[n]
|
||||
v2 = self.readWidget(w)
|
||||
if v1 != v2:
|
||||
if not USE_PYQT5:
|
||||
# Old signal kept for backward compatibility.
|
||||
self.emit(QtCore.SIGNAL('changed'), self.widgetList[w], v2)
|
||||
self.sigChanged.emit(self.widgetList[w], v2)
|
||||
|
||||
def state(self):
|
||||
for w in self.uncachedWidgets:
|
||||
self.readWidget(w)
|
||||
return self.cache.copy()
|
||||
|
||||
def setState(self, s):
|
||||
for w in self.widgetList:
|
||||
n = self.widgetList[w]
|
||||
if n not in s:
|
||||
continue
|
||||
self.setWidget(w, s[n])
|
||||
|
||||
def readWidget(self, w):
|
||||
if type(w) in WidgetGroup.classes:
|
||||
getFunc = WidgetGroup.classes[type(w)][1]
|
||||
else:
|
||||
getFunc = w.widgetGroupInterface()[1]
|
||||
|
||||
if getFunc is None:
|
||||
return None
|
||||
|
||||
## if the getter function provided in the interface is a bound method,
|
||||
## then just call the method directly. Otherwise, pass in the widget as the first arg
|
||||
## to the function.
|
||||
if inspect.ismethod(getFunc) and getFunc.__self__ is not None:
|
||||
val = getFunc()
|
||||
else:
|
||||
val = getFunc(w)
|
||||
|
||||
if self.scales[w] is not None:
|
||||
val /= self.scales[w]
|
||||
#if isinstance(val, QtCore.QString):
|
||||
#val = str(val)
|
||||
n = self.widgetList[w]
|
||||
self.cache[n] = val
|
||||
return val
|
||||
|
||||
def setWidget(self, w, v):
|
||||
v1 = v
|
||||
if self.scales[w] is not None:
|
||||
v *= self.scales[w]
|
||||
|
||||
if type(w) in WidgetGroup.classes:
|
||||
setFunc = WidgetGroup.classes[type(w)][2]
|
||||
else:
|
||||
setFunc = w.widgetGroupInterface()[2]
|
||||
|
||||
## if the setter function provided in the interface is a bound method,
|
||||
## then just call the method directly. Otherwise, pass in the widget as the first arg
|
||||
## to the function.
|
||||
if inspect.ismethod(setFunc) and setFunc.__self__ is not None:
|
||||
setFunc(v)
|
||||
else:
|
||||
setFunc(w, v)
|
||||
|
||||
#name = self.widgetList[w]
|
||||
#if name in self.cache and (self.cache[name] != v1):
|
||||
#print "%s: Cached value %s != set value %s" % (name, str(self.cache[name]), str(v1))
|
||||
|
||||
|
||||
|
439
pyqtgraph/__init__.py
Normal file
439
pyqtgraph/__init__.py
Normal file
|
@ -0,0 +1,439 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PyQtGraph - Scientific Graphics and GUI Library for Python
|
||||
www.pyqtgraph.org
|
||||
"""
|
||||
|
||||
__version__ = '0.9.10'
|
||||
|
||||
### import all the goodies and add some helper functions for easy CLI use
|
||||
|
||||
## 'Qt' is a local module; it is intended mainly to cover up the differences
|
||||
## between PyQt4 and PySide.
|
||||
from .Qt import QtGui
|
||||
|
||||
## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause)
|
||||
#if QtGui.QApplication.instance() is None:
|
||||
#app = QtGui.QApplication([])
|
||||
|
||||
import numpy ## pyqtgraph requires numpy
|
||||
## (import here to avoid massive error dump later on if numpy is not available)
|
||||
|
||||
import os, sys
|
||||
|
||||
## check python version
|
||||
## Allow anything >= 2.7
|
||||
if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1] < 6):
|
||||
raise Exception("Pyqtgraph requires Python version 2.6 or greater (this is %d.%d)" % (sys.version_info[0], sys.version_info[1]))
|
||||
|
||||
## helpers for 2/3 compatibility
|
||||
from . import python2_3
|
||||
|
||||
## install workarounds for numpy bugs
|
||||
from . import numpy_fix
|
||||
|
||||
## in general openGL is poorly supported with Qt+GraphicsView.
|
||||
## we only enable it where the performance benefit is critical.
|
||||
## Note this only applies to 2D graphics; 3D graphics always use OpenGL.
|
||||
if 'linux' in sys.platform: ## linux has numerous bugs in opengl implementation
|
||||
useOpenGL = False
|
||||
elif 'darwin' in sys.platform: ## openGL can have a major impact on mac, but also has serious bugs
|
||||
useOpenGL = False
|
||||
if QtGui.QApplication.instance() is not None:
|
||||
print('Warning: QApplication was created before pyqtgraph was imported; there may be problems (to avoid bugs, call QApplication.setGraphicsSystem("raster") before the QApplication is created).')
|
||||
if QtGui.QApplication.setGraphicsSystem:
|
||||
QtGui.QApplication.setGraphicsSystem('raster') ## work around a variety of bugs in the native graphics system
|
||||
else:
|
||||
useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff.
|
||||
|
||||
CONFIG_OPTIONS = {
|
||||
'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl.
|
||||
'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox
|
||||
'foreground': 'd', ## default foreground color for axes, labels, etc.
|
||||
'background': 'k', ## default background for GraphicsWidget
|
||||
'antialias': False,
|
||||
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
|
||||
'useWeave': False, ## Use weave to speed up some operations, if it is available
|
||||
'weaveDebug': False, ## Print full error message if weave compile fails
|
||||
'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide
|
||||
'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code)
|
||||
'crashWarning': False, # If True, print warnings about situations that may result in a crash
|
||||
}
|
||||
|
||||
|
||||
def setConfigOption(opt, value):
|
||||
CONFIG_OPTIONS[opt] = value
|
||||
|
||||
def setConfigOptions(**opts):
|
||||
CONFIG_OPTIONS.update(opts)
|
||||
|
||||
def getConfigOption(opt):
|
||||
return CONFIG_OPTIONS[opt]
|
||||
|
||||
|
||||
def systemInfo():
|
||||
print("sys.platform: %s" % sys.platform)
|
||||
print("sys.version: %s" % sys.version)
|
||||
from .Qt import VERSION_INFO
|
||||
print("qt bindings: %s" % VERSION_INFO)
|
||||
|
||||
global __version__
|
||||
rev = None
|
||||
if __version__ is None: ## this code was probably checked out from bzr; look up the last-revision file
|
||||
lastRevFile = os.path.join(os.path.dirname(__file__), '..', '.bzr', 'branch', 'last-revision')
|
||||
if os.path.exists(lastRevFile):
|
||||
rev = open(lastRevFile, 'r').read().strip()
|
||||
|
||||
print("pyqtgraph: %s; %s" % (__version__, rev))
|
||||
print("config:")
|
||||
import pprint
|
||||
pprint.pprint(CONFIG_OPTIONS)
|
||||
|
||||
## Rename orphaned .pyc files. This is *probably* safe :)
|
||||
## We only do this if __version__ is None, indicating the code was probably pulled
|
||||
## from the repository.
|
||||
def renamePyc(startDir):
|
||||
### Used to rename orphaned .pyc files
|
||||
### When a python file changes its location in the repository, usually the .pyc file
|
||||
### is left behind, possibly causing mysterious and difficult to track bugs.
|
||||
|
||||
### Note that this is no longer necessary for python 3.2; from PEP 3147:
|
||||
### "If the py source file is missing, the pyc file inside __pycache__ will be ignored.
|
||||
### This eliminates the problem of accidental stale pyc file imports."
|
||||
|
||||
printed = False
|
||||
startDir = os.path.abspath(startDir)
|
||||
for path, dirs, files in os.walk(startDir):
|
||||
if '__pycache__' in path:
|
||||
continue
|
||||
for f in files:
|
||||
fileName = os.path.join(path, f)
|
||||
base, ext = os.path.splitext(fileName)
|
||||
py = base + ".py"
|
||||
if ext == '.pyc' and not os.path.isfile(py):
|
||||
if not printed:
|
||||
print("NOTE: Renaming orphaned .pyc files:")
|
||||
printed = True
|
||||
n = 1
|
||||
while True:
|
||||
name2 = fileName + ".renamed%d" % n
|
||||
if not os.path.exists(name2):
|
||||
break
|
||||
n += 1
|
||||
print(" " + fileName + " ==>")
|
||||
print(" " + name2)
|
||||
os.rename(fileName, name2)
|
||||
|
||||
path = os.path.split(__file__)[0]
|
||||
if __version__ is None and not hasattr(sys, 'frozen') and sys.version_info[0] == 2: ## If we are frozen, there's a good chance we don't have the original .py files anymore.
|
||||
renamePyc(path)
|
||||
|
||||
|
||||
## Import almost everything to make it available from a single namespace
|
||||
## don't import the more complex systems--canvas, parametertree, flowchart, dockarea
|
||||
## these must be imported separately.
|
||||
#from . import frozenSupport
|
||||
#def importModules(path, globals, locals, excludes=()):
|
||||
#"""Import all modules residing within *path*, return a dict of name: module pairs.
|
||||
|
||||
#Note that *path* MUST be relative to the module doing the import.
|
||||
#"""
|
||||
#d = os.path.join(os.path.split(globals['__file__'])[0], path)
|
||||
#files = set()
|
||||
#for f in frozenSupport.listdir(d):
|
||||
#if frozenSupport.isdir(os.path.join(d, f)) and f not in ['__pycache__', 'tests']:
|
||||
#files.add(f)
|
||||
#elif f[-3:] == '.py' and f != '__init__.py':
|
||||
#files.add(f[:-3])
|
||||
#elif f[-4:] == '.pyc' and f != '__init__.pyc':
|
||||
#files.add(f[:-4])
|
||||
|
||||
#mods = {}
|
||||
#path = path.replace(os.sep, '.')
|
||||
#for modName in files:
|
||||
#if modName in excludes:
|
||||
#continue
|
||||
#try:
|
||||
#if len(path) > 0:
|
||||
#modName = path + '.' + modName
|
||||
#print( "from .%s import * " % modName)
|
||||
#mod = __import__(modName, globals, locals, ['*'], 1)
|
||||
#mods[modName] = mod
|
||||
#except:
|
||||
#import traceback
|
||||
#traceback.print_stack()
|
||||
#sys.excepthook(*sys.exc_info())
|
||||
#print("[Error importing module: %s]" % modName)
|
||||
|
||||
#return mods
|
||||
|
||||
#def importAll(path, globals, locals, excludes=()):
|
||||
#"""Given a list of modules, import all names from each module into the global namespace."""
|
||||
#mods = importModules(path, globals, locals, excludes)
|
||||
#for mod in mods.values():
|
||||
#if hasattr(mod, '__all__'):
|
||||
#names = mod.__all__
|
||||
#else:
|
||||
#names = [n for n in dir(mod) if n[0] != '_']
|
||||
#for k in names:
|
||||
#if hasattr(mod, k):
|
||||
#globals[k] = getattr(mod, k)
|
||||
|
||||
# Dynamic imports are disabled. This causes too many problems.
|
||||
#importAll('graphicsItems', globals(), locals())
|
||||
#importAll('widgets', globals(), locals(),
|
||||
#excludes=['MatplotlibWidget', 'RawImageWidget', 'RemoteGraphicsView'])
|
||||
|
||||
from .graphicsItems.VTickGroup import *
|
||||
from .graphicsItems.GraphicsWidget import *
|
||||
from .graphicsItems.ScaleBar import *
|
||||
from .graphicsItems.PlotDataItem import *
|
||||
from .graphicsItems.GraphItem import *
|
||||
from .graphicsItems.TextItem import *
|
||||
from .graphicsItems.GraphicsLayout import *
|
||||
from .graphicsItems.UIGraphicsItem import *
|
||||
from .graphicsItems.GraphicsObject import *
|
||||
from .graphicsItems.PlotItem import *
|
||||
from .graphicsItems.ROI import *
|
||||
from .graphicsItems.InfiniteLine import *
|
||||
from .graphicsItems.HistogramLUTItem import *
|
||||
from .graphicsItems.GridItem import *
|
||||
from .graphicsItems.GradientLegend import *
|
||||
from .graphicsItems.GraphicsItem import *
|
||||
from .graphicsItems.BarGraphItem import *
|
||||
from .graphicsItems.ViewBox import *
|
||||
from .graphicsItems.ArrowItem import *
|
||||
from .graphicsItems.ImageItem import *
|
||||
from .graphicsItems.AxisItem import *
|
||||
from .graphicsItems.LabelItem import *
|
||||
from .graphicsItems.CurvePoint import *
|
||||
from .graphicsItems.GraphicsWidgetAnchor import *
|
||||
from .graphicsItems.PlotCurveItem import *
|
||||
from .graphicsItems.ButtonItem import *
|
||||
from .graphicsItems.GradientEditorItem import *
|
||||
from .graphicsItems.MultiPlotItem import *
|
||||
from .graphicsItems.ErrorBarItem import *
|
||||
from .graphicsItems.IsocurveItem import *
|
||||
from .graphicsItems.LinearRegionItem import *
|
||||
from .graphicsItems.FillBetweenItem import *
|
||||
from .graphicsItems.LegendItem import *
|
||||
from .graphicsItems.ScatterPlotItem import *
|
||||
from .graphicsItems.ItemGroup import *
|
||||
|
||||
from .widgets.MultiPlotWidget import *
|
||||
from .widgets.ScatterPlotWidget import *
|
||||
from .widgets.ColorMapWidget import *
|
||||
from .widgets.FileDialog import *
|
||||
from .widgets.ValueLabel import *
|
||||
from .widgets.HistogramLUTWidget import *
|
||||
from .widgets.CheckTable import *
|
||||
from .widgets.BusyCursor import *
|
||||
from .widgets.PlotWidget import *
|
||||
from .widgets.ComboBox import *
|
||||
from .widgets.GradientWidget import *
|
||||
from .widgets.DataFilterWidget import *
|
||||
from .widgets.SpinBox import *
|
||||
from .widgets.JoystickButton import *
|
||||
from .widgets.GraphicsLayoutWidget import *
|
||||
from .widgets.TreeWidget import *
|
||||
from .widgets.PathButton import *
|
||||
from .widgets.VerticalLabel import *
|
||||
from .widgets.FeedbackButton import *
|
||||
from .widgets.ColorButton import *
|
||||
from .widgets.DataTreeWidget import *
|
||||
from .widgets.GraphicsView import *
|
||||
from .widgets.LayoutWidget import *
|
||||
from .widgets.TableWidget import *
|
||||
from .widgets.ProgressDialog import *
|
||||
|
||||
from .imageview import *
|
||||
from .WidgetGroup import *
|
||||
from .Point import Point
|
||||
from .Vector import Vector
|
||||
from .SRTTransform import SRTTransform
|
||||
from .Transform3D import Transform3D
|
||||
from .SRTTransform3D import SRTTransform3D
|
||||
from .functions import *
|
||||
from .graphicsWindows import *
|
||||
from .SignalProxy import *
|
||||
from .colormap import *
|
||||
from .ptime import time
|
||||
from .Qt import isQObjectAlive
|
||||
|
||||
|
||||
##############################################################
|
||||
## PyQt and PySide both are prone to crashing on exit.
|
||||
## There are two general approaches to dealing with this:
|
||||
## 1. Install atexit handlers that assist in tearing down to avoid crashes.
|
||||
## This helps, but is never perfect.
|
||||
## 2. Terminate the process before python starts tearing down
|
||||
## This is potentially dangerous
|
||||
|
||||
## Attempts to work around exit crashes:
|
||||
import atexit
|
||||
_cleanupCalled = False
|
||||
def cleanup():
|
||||
global _cleanupCalled
|
||||
if _cleanupCalled:
|
||||
return
|
||||
|
||||
if not getConfigOption('exitCleanup'):
|
||||
return
|
||||
|
||||
ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore.
|
||||
|
||||
## Workaround for Qt exit crash:
|
||||
## ALL QGraphicsItems must have a scene before they are deleted.
|
||||
## This is potentially very expensive, but preferred over crashing.
|
||||
## Note: this appears to be fixed in PySide as of 2012.12, but it should be left in for a while longer..
|
||||
if QtGui.QApplication.instance() is None:
|
||||
return
|
||||
import gc
|
||||
s = QtGui.QGraphicsScene()
|
||||
for o in gc.get_objects():
|
||||
try:
|
||||
if isinstance(o, QtGui.QGraphicsItem) and isQObjectAlive(o) and o.scene() is None:
|
||||
if getConfigOption('crashWarning'):
|
||||
sys.stderr.write('Error: graphics item without scene. '
|
||||
'Make sure ViewBox.close() and GraphicsView.close() '
|
||||
'are properly called before app shutdown (%s)\n' % (o,))
|
||||
|
||||
s.addItem(o)
|
||||
except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object
|
||||
continue
|
||||
_cleanupCalled = True
|
||||
|
||||
atexit.register(cleanup)
|
||||
|
||||
# Call cleanup when QApplication quits. This is necessary because sometimes
|
||||
# the QApplication will quit before the atexit callbacks are invoked.
|
||||
# Note: cannot connect this function until QApplication has been created, so
|
||||
# instead we have GraphicsView.__init__ call this for us.
|
||||
_cleanupConnected = False
|
||||
def _connectCleanup():
|
||||
global _cleanupConnected
|
||||
if _cleanupConnected:
|
||||
return
|
||||
QtGui.QApplication.instance().aboutToQuit.connect(cleanup)
|
||||
_cleanupConnected = True
|
||||
|
||||
|
||||
## Optional function for exiting immediately (with some manual teardown)
|
||||
def exit():
|
||||
"""
|
||||
Causes python to exit without garbage-collecting any objects, and thus avoids
|
||||
calling object destructor methods. This is a sledgehammer workaround for
|
||||
a variety of bugs in PyQt and Pyside that cause crashes on exit.
|
||||
|
||||
This function does the following in an attempt to 'safely' terminate
|
||||
the process:
|
||||
|
||||
* Invoke atexit callbacks
|
||||
* Close all open file handles
|
||||
* os._exit()
|
||||
|
||||
Note: there is some potential for causing damage with this function if you
|
||||
are using objects that _require_ their destructors to be called (for example,
|
||||
to properly terminate log files, disconnect from devices, etc). Situations
|
||||
like this are probably quite rare, but use at your own risk.
|
||||
"""
|
||||
|
||||
## first disable our own cleanup function; won't be needing it.
|
||||
setConfigOptions(exitCleanup=False)
|
||||
|
||||
## invoke atexit callbacks
|
||||
atexit._run_exitfuncs()
|
||||
|
||||
## close file handles
|
||||
if sys.platform == 'darwin':
|
||||
for fd in xrange(3, 4096):
|
||||
if fd not in [7]: # trying to close 7 produces an illegal instruction on the Mac.
|
||||
os.close(fd)
|
||||
else:
|
||||
os.closerange(3, 4096) ## just guessing on the maximum descriptor count..
|
||||
|
||||
os._exit(0)
|
||||
|
||||
|
||||
|
||||
## Convenience functions for command-line use
|
||||
|
||||
plots = []
|
||||
images = []
|
||||
QAPP = None
|
||||
|
||||
def plot(*args, **kargs):
|
||||
"""
|
||||
Create and return a :class:`PlotWindow <pyqtgraph.PlotWindow>`
|
||||
(this is just a window with :class:`PlotWidget <pyqtgraph.PlotWidget>` inside), plot data in it.
|
||||
Accepts a *title* argument to set the title of the window.
|
||||
All other arguments are used to plot data. (see :func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`)
|
||||
"""
|
||||
mkQApp()
|
||||
#if 'title' in kargs:
|
||||
#w = PlotWindow(title=kargs['title'])
|
||||
#del kargs['title']
|
||||
#else:
|
||||
#w = PlotWindow()
|
||||
#if len(args)+len(kargs) > 0:
|
||||
#w.plot(*args, **kargs)
|
||||
|
||||
pwArgList = ['title', 'labels', 'name', 'left', 'right', 'top', 'bottom', 'background']
|
||||
pwArgs = {}
|
||||
dataArgs = {}
|
||||
for k in kargs:
|
||||
if k in pwArgList:
|
||||
pwArgs[k] = kargs[k]
|
||||
else:
|
||||
dataArgs[k] = kargs[k]
|
||||
|
||||
w = PlotWindow(**pwArgs)
|
||||
if len(args) > 0 or len(dataArgs) > 0:
|
||||
w.plot(*args, **dataArgs)
|
||||
plots.append(w)
|
||||
w.show()
|
||||
return w
|
||||
|
||||
def image(*args, **kargs):
|
||||
"""
|
||||
Create and return an :class:`ImageWindow <pyqtgraph.ImageWindow>`
|
||||
(this is just a window with :class:`ImageView <pyqtgraph.ImageView>` widget inside), show image data inside.
|
||||
Will show 2D or 3D image data.
|
||||
Accepts a *title* argument to set the title of the window.
|
||||
All other arguments are used to show data. (see :func:`ImageView.setImage() <pyqtgraph.ImageView.setImage>`)
|
||||
"""
|
||||
mkQApp()
|
||||
w = ImageWindow(*args, **kargs)
|
||||
images.append(w)
|
||||
w.show()
|
||||
return w
|
||||
show = image ## for backward compatibility
|
||||
|
||||
def dbg(*args, **kwds):
|
||||
"""
|
||||
Create a console window and begin watching for exceptions.
|
||||
|
||||
All arguments are passed to :func:`ConsoleWidget.__init__() <pyqtgraph.console.ConsoleWidget.__init__>`.
|
||||
"""
|
||||
mkQApp()
|
||||
from . import console
|
||||
c = console.ConsoleWidget(*args, **kwds)
|
||||
c.catchAllExceptions()
|
||||
c.show()
|
||||
global consoles
|
||||
try:
|
||||
consoles.append(c)
|
||||
except NameError:
|
||||
consoles = [c]
|
||||
return c
|
||||
|
||||
|
||||
def mkQApp():
|
||||
global QAPP
|
||||
inst = QtGui.QApplication.instance()
|
||||
if inst is None:
|
||||
QAPP = QtGui.QApplication([])
|
||||
else:
|
||||
QAPP = inst
|
||||
return QAPP
|
||||
|
604
pyqtgraph/canvas/Canvas.py
Normal file
604
pyqtgraph/canvas/Canvas.py
Normal file
|
@ -0,0 +1,604 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
if __name__ == '__main__':
|
||||
import sys, os
|
||||
md = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path = [os.path.dirname(md), os.path.join(md, '..', '..', '..')] + sys.path
|
||||
|
||||
from ..Qt import QtGui, QtCore, USE_PYSIDE
|
||||
from ..graphicsItems.ROI import ROI
|
||||
from ..graphicsItems.ViewBox import ViewBox
|
||||
from ..graphicsItems.GridItem import GridItem
|
||||
|
||||
if USE_PYSIDE:
|
||||
from .CanvasTemplate_pyside import *
|
||||
else:
|
||||
from .CanvasTemplate_pyqt import *
|
||||
|
||||
import numpy as np
|
||||
from .. import debug
|
||||
import weakref
|
||||
from .CanvasManager import CanvasManager
|
||||
from .CanvasItem import CanvasItem, GroupCanvasItem
|
||||
|
||||
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_Form()
|
||||
self.ui.setupUi(self)
|
||||
#self.view = self.ui.view
|
||||
self.view = ViewBox()
|
||||
self.ui.view.setCentralItem(self.view)
|
||||
self.itemList = self.ui.itemList
|
||||
self.itemList.setSelectionMode(self.itemList.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.enableMouse()
|
||||
self.view.setAspectLocked(True)
|
||||
#self.view.invertY()
|
||||
|
||||
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.storeSvgBtn.clicked.connect(self.storeSvg)
|
||||
#self.ui.storePngBtn.clicked.connect(self.storePng)
|
||||
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()
|
||||
#self.menu.setTitle("Image")
|
||||
remAct = QtGui.QAction("Remove item", self.menu)
|
||||
remAct.triggered.connect(self.removeClicked)
|
||||
self.menu.addAction(remAct)
|
||||
self.menu.remAct = remAct
|
||||
self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent
|
||||
|
||||
|
||||
#def storeSvg(self):
|
||||
#from pyqtgraph.GraphicsScene.exportDialog import ExportDialog
|
||||
#ex = ExportDialog(self.ui.view)
|
||||
#ex.show()
|
||||
|
||||
#def storePng(self):
|
||||
#self.ui.view.writeImage()
|
||||
|
||||
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:
|
||||
QtGui.QWidget.resizeEvent(self, ev)
|
||||
self.hideBtn.move(self.ui.view.size().width() - self.hideBtn.width(), 0)
|
||||
|
||||
if not self.sizeApplied:
|
||||
self.sizeApplied = True
|
||||
s = min(self.width(), max(100, min(200, self.width()*0.25)))
|
||||
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 = {'Grid': items['Grid']}
|
||||
#del items['Grid']
|
||||
self.items = [self.grid]
|
||||
items.remove(self.grid)
|
||||
|
||||
for i in items:
|
||||
i.canvas.removeItem(i)
|
||||
self.addItem(i)
|
||||
|
||||
def treeItemChanged(self, item, col):
|
||||
#gi = self.items.get(item.name, None)
|
||||
#if gi is None:
|
||||
#return
|
||||
try:
|
||||
citem = item.canvasItem()
|
||||
except AttributeError:
|
||||
return
|
||||
if item.checkState(0) == QtCore.Qt.Checked:
|
||||
for i in range(item.childCount()):
|
||||
item.child(i).setCheckState(0, QtCore.Qt.Checked)
|
||||
citem.show()
|
||||
else:
|
||||
for i in range(item.childCount()):
|
||||
item.child(i).setCheckState(0, QtCore.Qt.Unchecked)
|
||||
citem.hide()
|
||||
|
||||
def treeItemSelected(self):
|
||||
sel = self.selectedItems()
|
||||
#sel = []
|
||||
#for listItem in self.itemList.selectedItems():
|
||||
#if hasattr(listItem, 'canvasItem') and listItem.canvasItem is not None:
|
||||
#sel.append(listItem.canvasItem)
|
||||
#sel = [self.items[item.name] for item in sel]
|
||||
|
||||
if len(sel) == 0:
|
||||
#self.selectWidget.hide()
|
||||
return
|
||||
|
||||
multi = len(sel) > 1
|
||||
for i in self.items:
|
||||
#i.ctrlWidget().hide()
|
||||
## updated the selected state of every item
|
||||
i.selectionChanged(i in sel, multi)
|
||||
|
||||
if len(sel)==1:
|
||||
#item = sel[0]
|
||||
#item.ctrlWidget().show()
|
||||
self.multiSelectBox.hide()
|
||||
self.ui.mirrorSelectionBtn.hide()
|
||||
self.ui.reflectSelectionBtn.hide()
|
||||
self.ui.resetTransformsBtn.hide()
|
||||
elif len(sel) > 1:
|
||||
self.showMultiSelectBox()
|
||||
|
||||
#if item.isMovable():
|
||||
#self.selectBox.setPos(item.item.pos())
|
||||
#self.selectBox.setSize(item.item.sceneBoundingRect().size())
|
||||
#self.selectBox.show()
|
||||
#else:
|
||||
#self.selectBox.hide()
|
||||
|
||||
#self.emit(QtCore.SIGNAL('itemSelected'), self, item)
|
||||
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 selectedItem(self):
|
||||
#sel = self.itemList.selectedItems()
|
||||
#if sel is None or len(sel) < 1:
|
||||
#return
|
||||
#return self.items.get(sel[0].name, None)
|
||||
|
||||
def selectItem(self, item):
|
||||
li = item.listItem
|
||||
#li = self.getListItem(item.name())
|
||||
#print "select", li
|
||||
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()
|
||||
#self.multiSelectBoxBase = self.multiSelectBox.getState().copy()
|
||||
|
||||
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
|
||||
#currentNode = self.itemList.invisibleRootItem()
|
||||
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 == 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.ItemIsUserCheckable | QtCore.Qt.ItemIsDragEnabled
|
||||
if not isinstance(citem, GroupCanvasItem):
|
||||
flags = flags & ~QtCore.Qt.ItemIsDropEnabled
|
||||
node.setFlags(flags)
|
||||
if citem.opts['visible']:
|
||||
node.setCheckState(0, QtCore.Qt.Checked)
|
||||
else:
|
||||
node.setCheckState(0, QtCore.Qt.Unchecked)
|
||||
|
||||
node.name = name
|
||||
#if citem.opts['parent'] != None:
|
||||
## insertLocation is incorrect in this case
|
||||
parent.insertChild(insertLocation, node)
|
||||
#else:
|
||||
#root.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()
|
||||
|
||||
|
||||
#for n in name:
|
||||
#nextnode = None
|
||||
#for x in range(currentNode.childCount()):
|
||||
#ch = currentNode.child(x)
|
||||
#if hasattr(ch, 'name'): ## check Z-value of current item to determine insert location
|
||||
#zval = ch.canvasItem.zValue()
|
||||
#if zval > z:
|
||||
###print " ->", x
|
||||
#insertLocation = x+1
|
||||
#if n == ch.text(0):
|
||||
#nextnode = ch
|
||||
#break
|
||||
#if nextnode is None: ## If name doesn't exist, create it
|
||||
#nextnode = QtGui.QTreeWidgetItem([n])
|
||||
#nextnode.setFlags((nextnode.flags() | QtCore.Qt.ItemIsUserCheckable) & ~QtCore.Qt.ItemIsDropEnabled)
|
||||
#nextnode.setCheckState(0, QtCore.Qt.Checked)
|
||||
### Add node to correct position in list by Z-value
|
||||
###print " ==>", insertLocation
|
||||
#currentNode.insertChild(insertLocation, nextnode)
|
||||
|
||||
#if n == name[-1]: ## This is the leaf; add some extra properties.
|
||||
#nextnode.name = name
|
||||
|
||||
#if n == name[0]: ## This is the root; make the item movable
|
||||
#nextnode.setFlags(nextnode.flags() | QtCore.Qt.ItemIsDragEnabled)
|
||||
#else:
|
||||
#nextnode.setFlags(nextnode.flags() & ~QtCore.Qt.ItemIsDragEnabled)
|
||||
|
||||
#currentNode = nextnode
|
||||
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])
|
||||
#item = self.itemList.topLevelItem(i)
|
||||
|
||||
##ci = self.items[item.name]
|
||||
#ci = item.canvasItem
|
||||
#if ci is None:
|
||||
#continue
|
||||
#if ci.zValue() != zvals[i]:
|
||||
#ci.setZValue(zvals[i])
|
||||
|
||||
#if self.itemList.topLevelItemCount() < 2:
|
||||
#return
|
||||
#name = item.name
|
||||
#gi = self.items[name]
|
||||
#if index == 0:
|
||||
#next = self.itemList.topLevelItem(1)
|
||||
#z = self.items[next.name].zValue()+1
|
||||
#else:
|
||||
#prev = self.itemList.topLevelItem(index-1)
|
||||
#z = self.items[prev.name].zValue()-1
|
||||
#gi.setZValue(z)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def itemVisibilityChanged(self, item):
|
||||
listItem = item.listItem
|
||||
checked = listItem.checkState(0) == QtCore.Qt.Checked
|
||||
vis = item.isVisible()
|
||||
if vis != checked:
|
||||
if vis:
|
||||
listItem.setCheckState(0, QtCore.Qt.Checked)
|
||||
else:
|
||||
listItem.setCheckState(0, QtCore.Qt.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)
|
||||
else:
|
||||
if hasattr(item, '_canvasItem'):
|
||||
self.removeItem(item._canvasItem)
|
||||
else:
|
||||
self.view.removeItem(item)
|
||||
|
||||
## disconnect signals, remove from list, etc..
|
||||
|
||||
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 scene(self):
|
||||
#return self.view.scene()
|
||||
|
||||
def itemTransformChanged(self, item):
|
||||
#self.emit(QtCore.SIGNAL('itemTransformChanged'), self, item)
|
||||
self.sigItemTransformChanged.emit(self, item)
|
||||
|
||||
def itemTransformChangeFinished(self, item):
|
||||
#self.emit(QtCore.SIGNAL('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):
|
||||
#self.removeItem(self.menuItem)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
512
pyqtgraph/canvas/CanvasItem.py
Normal file
512
pyqtgraph/canvas/CanvasItem.py
Normal file
|
@ -0,0 +1,512 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE
|
||||
from ..graphicsItems.ROI import ROI
|
||||
from .. import SRTTransform, ItemGroup
|
||||
if USE_PYSIDE:
|
||||
from . import TransformGuiTemplate_pyside as TransformGuiTemplate
|
||||
else:
|
||||
from . import TransformGuiTemplate_pyqt as TransformGuiTemplate
|
||||
|
||||
from .. import debug
|
||||
|
||||
class SelectBox(ROI):
|
||||
def __init__(self, scalable=False, rotatable=True):
|
||||
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
|
||||
ROI.__init__(self, [0,0], [1,1], invertible=True)
|
||||
center = [0.5, 0.5]
|
||||
|
||||
if scalable:
|
||||
self.addScaleHandle([1, 1], center, lockAspect=True)
|
||||
self.addScaleHandle([0, 0], center, lockAspect=True)
|
||||
if rotatable:
|
||||
self.addRotateHandle([0, 1], center)
|
||||
self.addRotateHandle([1, 0], center)
|
||||
|
||||
class CanvasItem(QtCore.QObject):
|
||||
|
||||
sigResetUserTransform = QtCore.Signal(object)
|
||||
sigTransformChangeFinished = QtCore.Signal(object)
|
||||
sigTransformChanged = QtCore.Signal(object)
|
||||
|
||||
"""CanvasItem takes care of managing an item's state--alpha, visibility, z-value, transformations, etc. and
|
||||
provides a control widget"""
|
||||
|
||||
sigVisibilityChanged = QtCore.Signal(object)
|
||||
transformCopyBuffer = None
|
||||
|
||||
def __init__(self, item, **opts):
|
||||
defOpts = {'name': None, 'z': None, 'movable': True, 'scalable': False, 'rotatable': True, 'visible': True, 'parent':None} #'pos': [0,0], 'scale': [1,1], 'angle':0,
|
||||
defOpts.update(opts)
|
||||
self.opts = defOpts
|
||||
self.selectedAlone = False ## whether this item is the only one selected
|
||||
|
||||
QtCore.QObject.__init__(self)
|
||||
self.canvas = None
|
||||
self._graphicsItem = item
|
||||
|
||||
parent = self.opts['parent']
|
||||
if parent is not None:
|
||||
self._graphicsItem.setParentItem(parent.graphicsItem())
|
||||
self._parentItem = parent
|
||||
else:
|
||||
self._parentItem = None
|
||||
|
||||
z = self.opts['z']
|
||||
if z is not None:
|
||||
item.setZValue(z)
|
||||
|
||||
self.ctrl = QtGui.QWidget()
|
||||
self.layout = QtGui.QGridLayout()
|
||||
self.layout.setSpacing(0)
|
||||
self.layout.setContentsMargins(0,0,0,0)
|
||||
self.ctrl.setLayout(self.layout)
|
||||
|
||||
self.alphaLabel = QtGui.QLabel("Alpha")
|
||||
self.alphaSlider = QtGui.QSlider()
|
||||
self.alphaSlider.setMaximum(1023)
|
||||
self.alphaSlider.setOrientation(QtCore.Qt.Horizontal)
|
||||
self.alphaSlider.setValue(1023)
|
||||
self.layout.addWidget(self.alphaLabel, 0, 0)
|
||||
self.layout.addWidget(self.alphaSlider, 0, 1)
|
||||
self.resetTransformBtn = QtGui.QPushButton('Reset Transform')
|
||||
self.copyBtn = QtGui.QPushButton('Copy')
|
||||
self.pasteBtn = QtGui.QPushButton('Paste')
|
||||
|
||||
self.transformWidget = QtGui.QWidget()
|
||||
self.transformGui = TransformGuiTemplate.Ui_Form()
|
||||
self.transformGui.setupUi(self.transformWidget)
|
||||
self.layout.addWidget(self.transformWidget, 3, 0, 1, 2)
|
||||
self.transformGui.mirrorImageBtn.clicked.connect(self.mirrorY)
|
||||
self.transformGui.reflectImageBtn.clicked.connect(self.mirrorXY)
|
||||
|
||||
self.layout.addWidget(self.resetTransformBtn, 1, 0, 1, 2)
|
||||
self.layout.addWidget(self.copyBtn, 2, 0, 1, 1)
|
||||
self.layout.addWidget(self.pasteBtn, 2, 1, 1, 1)
|
||||
self.alphaSlider.valueChanged.connect(self.alphaChanged)
|
||||
self.alphaSlider.sliderPressed.connect(self.alphaPressed)
|
||||
self.alphaSlider.sliderReleased.connect(self.alphaReleased)
|
||||
#self.canvas.sigSelectionChanged.connect(self.selectionChanged)
|
||||
self.resetTransformBtn.clicked.connect(self.resetTransformClicked)
|
||||
self.copyBtn.clicked.connect(self.copyClicked)
|
||||
self.pasteBtn.clicked.connect(self.pasteClicked)
|
||||
|
||||
self.setMovable(self.opts['movable']) ## update gui to reflect this option
|
||||
|
||||
|
||||
if 'transform' in self.opts:
|
||||
self.baseTransform = self.opts['transform']
|
||||
else:
|
||||
self.baseTransform = SRTTransform()
|
||||
if 'pos' in self.opts and self.opts['pos'] is not None:
|
||||
self.baseTransform.translate(self.opts['pos'])
|
||||
if 'angle' in self.opts and self.opts['angle'] is not None:
|
||||
self.baseTransform.rotate(self.opts['angle'])
|
||||
if 'scale' in self.opts and self.opts['scale'] is not None:
|
||||
self.baseTransform.scale(self.opts['scale'])
|
||||
|
||||
## create selection box (only visible when selected)
|
||||
tr = self.baseTransform.saveState()
|
||||
if 'scalable' not in opts and tr['scale'] == (1,1):
|
||||
self.opts['scalable'] = True
|
||||
|
||||
## every CanvasItem implements its own individual selection box
|
||||
## so that subclasses are free to make their own.
|
||||
self.selectBox = SelectBox(scalable=self.opts['scalable'], rotatable=self.opts['rotatable'])
|
||||
#self.canvas.scene().addItem(self.selectBox)
|
||||
self.selectBox.hide()
|
||||
self.selectBox.setZValue(1e6)
|
||||
self.selectBox.sigRegionChanged.connect(self.selectBoxChanged) ## calls selectBoxMoved
|
||||
self.selectBox.sigRegionChangeFinished.connect(self.selectBoxChangeFinished)
|
||||
|
||||
## set up the transformations that will be applied to the item
|
||||
## (It is not safe to use item.setTransform, since the item might count on that not changing)
|
||||
self.itemRotation = QtGui.QGraphicsRotation()
|
||||
self.itemScale = QtGui.QGraphicsScale()
|
||||
self._graphicsItem.setTransformations([self.itemRotation, self.itemScale])
|
||||
|
||||
self.tempTransform = SRTTransform() ## holds the additional transform that happens during a move - gets added to the userTransform when move is done.
|
||||
self.userTransform = SRTTransform() ## stores the total transform of the object
|
||||
self.resetUserTransform()
|
||||
|
||||
## now happens inside resetUserTransform -> selectBoxToItem
|
||||
# self.selectBoxBase = self.selectBox.getState().copy()
|
||||
|
||||
|
||||
#print "Created canvas item", self
|
||||
#print " base:", self.baseTransform
|
||||
#print " user:", self.userTransform
|
||||
#print " temp:", self.tempTransform
|
||||
#print " bounds:", self.item.sceneBoundingRect()
|
||||
|
||||
def setMovable(self, m):
|
||||
self.opts['movable'] = m
|
||||
|
||||
if m:
|
||||
self.resetTransformBtn.show()
|
||||
self.copyBtn.show()
|
||||
self.pasteBtn.show()
|
||||
else:
|
||||
self.resetTransformBtn.hide()
|
||||
self.copyBtn.hide()
|
||||
self.pasteBtn.hide()
|
||||
|
||||
def setCanvas(self, canvas):
|
||||
## Called by canvas whenever the item is added.
|
||||
## It is our responsibility to add all graphicsItems to the canvas's scene
|
||||
## The canvas will automatically add our graphicsitem,
|
||||
## so we just need to take care of the selectbox.
|
||||
if canvas is self.canvas:
|
||||
return
|
||||
|
||||
if canvas is None:
|
||||
self.canvas.removeFromScene(self._graphicsItem)
|
||||
self.canvas.removeFromScene(self.selectBox)
|
||||
else:
|
||||
canvas.addToScene(self._graphicsItem)
|
||||
canvas.addToScene(self.selectBox)
|
||||
self.canvas = canvas
|
||||
|
||||
def graphicsItem(self):
|
||||
"""Return the graphicsItem for this canvasItem."""
|
||||
return self._graphicsItem
|
||||
|
||||
def parentItem(self):
|
||||
return self._parentItem
|
||||
|
||||
def setParentItem(self, parent):
|
||||
self._parentItem = parent
|
||||
if parent is not None:
|
||||
if isinstance(parent, CanvasItem):
|
||||
parent = parent.graphicsItem()
|
||||
self.graphicsItem().setParentItem(parent)
|
||||
|
||||
#def name(self):
|
||||
#return self.opts['name']
|
||||
|
||||
def copyClicked(self):
|
||||
CanvasItem.transformCopyBuffer = self.saveTransform()
|
||||
|
||||
def pasteClicked(self):
|
||||
t = CanvasItem.transformCopyBuffer
|
||||
if t is None:
|
||||
return
|
||||
else:
|
||||
self.restoreTransform(t)
|
||||
|
||||
def mirrorY(self):
|
||||
if not self.isMovable():
|
||||
return
|
||||
|
||||
#flip = self.transformGui.mirrorImageCheck.isChecked()
|
||||
#tr = self.userTransform.saveState()
|
||||
|
||||
inv = SRTTransform()
|
||||
inv.scale(-1, 1)
|
||||
self.userTransform = self.userTransform * inv
|
||||
self.updateTransform()
|
||||
self.selectBoxFromUser()
|
||||
self.sigTransformChangeFinished.emit(self)
|
||||
#if flip:
|
||||
#if tr['scale'][0] < 0 xor tr['scale'][1] < 0:
|
||||
#return
|
||||
#else:
|
||||
#self.userTransform.setScale([-tr['scale'][0], tr['scale'][1]])
|
||||
#self.userTransform.setTranslate([-tr['pos'][0], tr['pos'][1]])
|
||||
#self.userTransform.setRotate(-tr['angle'])
|
||||
#self.updateTransform()
|
||||
#self.selectBoxFromUser()
|
||||
#return
|
||||
#elif not flip:
|
||||
#if tr['scale'][0] > 0 and tr['scale'][1] > 0:
|
||||
#return
|
||||
#else:
|
||||
#self.userTransform.setScale([-tr['scale'][0], tr['scale'][1]])
|
||||
#self.userTransform.setTranslate([-tr['pos'][0], tr['pos'][1]])
|
||||
#self.userTransform.setRotate(-tr['angle'])
|
||||
#self.updateTransform()
|
||||
#self.selectBoxFromUser()
|
||||
#return
|
||||
|
||||
def mirrorXY(self):
|
||||
if not self.isMovable():
|
||||
return
|
||||
self.rotate(180.)
|
||||
# inv = SRTTransform()
|
||||
# inv.scale(-1, -1)
|
||||
# self.userTransform = self.userTransform * inv #flip lr/ud
|
||||
# s=self.updateTransform()
|
||||
# self.setTranslate(-2*s['pos'][0], -2*s['pos'][1])
|
||||
# self.selectBoxFromUser()
|
||||
|
||||
|
||||
def hasUserTransform(self):
|
||||
#print self.userRotate, self.userTranslate
|
||||
return not self.userTransform.isIdentity()
|
||||
|
||||
def ctrlWidget(self):
|
||||
return self.ctrl
|
||||
|
||||
def alphaChanged(self, val):
|
||||
alpha = val / 1023.
|
||||
self._graphicsItem.setOpacity(alpha)
|
||||
|
||||
def isMovable(self):
|
||||
return self.opts['movable']
|
||||
|
||||
|
||||
def selectBoxMoved(self):
|
||||
"""The selection box has moved; get its transformation information and pass to the graphics item"""
|
||||
self.userTransform = self.selectBox.getGlobalTransform(relativeTo=self.selectBoxBase)
|
||||
self.updateTransform()
|
||||
|
||||
def scale(self, x, y):
|
||||
self.userTransform.scale(x, y)
|
||||
self.selectBoxFromUser()
|
||||
self.updateTransform()
|
||||
|
||||
def rotate(self, ang):
|
||||
self.userTransform.rotate(ang)
|
||||
self.selectBoxFromUser()
|
||||
self.updateTransform()
|
||||
|
||||
def translate(self, x, y):
|
||||
self.userTransform.translate(x, y)
|
||||
self.selectBoxFromUser()
|
||||
self.updateTransform()
|
||||
|
||||
def setTranslate(self, x, y):
|
||||
self.userTransform.setTranslate(x, y)
|
||||
self.selectBoxFromUser()
|
||||
self.updateTransform()
|
||||
|
||||
def setRotate(self, angle):
|
||||
self.userTransform.setRotate(angle)
|
||||
self.selectBoxFromUser()
|
||||
self.updateTransform()
|
||||
|
||||
def setScale(self, x, y):
|
||||
self.userTransform.setScale(x, y)
|
||||
self.selectBoxFromUser()
|
||||
self.updateTransform()
|
||||
|
||||
|
||||
def setTemporaryTransform(self, transform):
|
||||
self.tempTransform = transform
|
||||
self.updateTransform()
|
||||
|
||||
def applyTemporaryTransform(self):
|
||||
"""Collapses tempTransform into UserTransform, resets tempTransform"""
|
||||
self.userTransform = self.userTransform * self.tempTransform ## order is important!
|
||||
self.resetTemporaryTransform()
|
||||
self.selectBoxFromUser() ## update the selection box to match the new userTransform
|
||||
|
||||
#st = self.userTransform.saveState()
|
||||
|
||||
#self.userTransform = self.userTransform * self.tempTransform ## order is important!
|
||||
|
||||
#### matrix multiplication affects the scale factors, need to reset
|
||||
#if st['scale'][0] < 0 or st['scale'][1] < 0:
|
||||
#nst = self.userTransform.saveState()
|
||||
#self.userTransform.setScale([-nst['scale'][0], -nst['scale'][1]])
|
||||
|
||||
#self.resetTemporaryTransform()
|
||||
#self.selectBoxFromUser()
|
||||
#self.selectBoxChangeFinished()
|
||||
|
||||
|
||||
|
||||
def resetTemporaryTransform(self):
|
||||
self.tempTransform = SRTTransform() ## don't use Transform.reset()--this transform might be used elsewhere.
|
||||
self.updateTransform()
|
||||
|
||||
def transform(self):
|
||||
return self._graphicsItem.transform()
|
||||
|
||||
def updateTransform(self):
|
||||
"""Regenerate the item position from the base, user, and temp transforms"""
|
||||
transform = self.baseTransform * self.userTransform * self.tempTransform ## order is important
|
||||
s = transform.saveState()
|
||||
self._graphicsItem.setPos(*s['pos'])
|
||||
|
||||
self.itemRotation.setAngle(s['angle'])
|
||||
self.itemScale.setXScale(s['scale'][0])
|
||||
self.itemScale.setYScale(s['scale'][1])
|
||||
|
||||
self.displayTransform(transform)
|
||||
return(s) # return the transform state
|
||||
|
||||
def displayTransform(self, transform):
|
||||
"""Updates transform numbers in the ctrl widget."""
|
||||
|
||||
tr = transform.saveState()
|
||||
|
||||
self.transformGui.translateLabel.setText("Translate: (%f, %f)" %(tr['pos'][0], tr['pos'][1]))
|
||||
self.transformGui.rotateLabel.setText("Rotate: %f degrees" %tr['angle'])
|
||||
self.transformGui.scaleLabel.setText("Scale: (%f, %f)" %(tr['scale'][0], tr['scale'][1]))
|
||||
#self.transformGui.mirrorImageCheck.setChecked(False)
|
||||
#if tr['scale'][0] < 0:
|
||||
# self.transformGui.mirrorImageCheck.setChecked(True)
|
||||
|
||||
|
||||
def resetUserTransform(self):
|
||||
#self.userRotate = 0
|
||||
#self.userTranslate = pg.Point(0,0)
|
||||
self.userTransform.reset()
|
||||
self.updateTransform()
|
||||
|
||||
self.selectBox.blockSignals(True)
|
||||
self.selectBoxToItem()
|
||||
self.selectBox.blockSignals(False)
|
||||
self.sigTransformChanged.emit(self)
|
||||
self.sigTransformChangeFinished.emit(self)
|
||||
|
||||
def resetTransformClicked(self):
|
||||
self.resetUserTransform()
|
||||
self.sigResetUserTransform.emit(self)
|
||||
|
||||
def restoreTransform(self, tr):
|
||||
try:
|
||||
#self.userTranslate = pg.Point(tr['trans'])
|
||||
#self.userRotate = tr['rot']
|
||||
self.userTransform = SRTTransform(tr)
|
||||
self.updateTransform()
|
||||
|
||||
self.selectBoxFromUser() ## move select box to match
|
||||
self.sigTransformChanged.emit(self)
|
||||
self.sigTransformChangeFinished.emit(self)
|
||||
except:
|
||||
#self.userTranslate = pg.Point([0,0])
|
||||
#self.userRotate = 0
|
||||
self.userTransform = SRTTransform()
|
||||
debug.printExc("Failed to load transform:")
|
||||
#print "set transform", self, self.userTranslate
|
||||
|
||||
def saveTransform(self):
|
||||
"""Return a dict containing the current user transform"""
|
||||
#print "save transform", self, self.userTranslate
|
||||
#return {'trans': list(self.userTranslate), 'rot': self.userRotate}
|
||||
return self.userTransform.saveState()
|
||||
|
||||
def selectBoxFromUser(self):
|
||||
"""Move the selection box to match the current userTransform"""
|
||||
## user transform
|
||||
#trans = QtGui.QTransform()
|
||||
#trans.translate(*self.userTranslate)
|
||||
#trans.rotate(-self.userRotate)
|
||||
|
||||
#x2, y2 = trans.map(*self.selectBoxBase['pos'])
|
||||
|
||||
self.selectBox.blockSignals(True)
|
||||
self.selectBox.setState(self.selectBoxBase)
|
||||
self.selectBox.applyGlobalTransform(self.userTransform)
|
||||
#self.selectBox.setAngle(self.userRotate)
|
||||
#self.selectBox.setPos([x2, y2])
|
||||
self.selectBox.blockSignals(False)
|
||||
|
||||
|
||||
def selectBoxToItem(self):
|
||||
"""Move/scale the selection box so it fits the item's bounding rect. (assumes item is not rotated)"""
|
||||
self.itemRect = self._graphicsItem.boundingRect()
|
||||
rect = self._graphicsItem.mapRectToParent(self.itemRect)
|
||||
self.selectBox.blockSignals(True)
|
||||
self.selectBox.setPos([rect.x(), rect.y()])
|
||||
self.selectBox.setSize(rect.size())
|
||||
self.selectBox.setAngle(0)
|
||||
self.selectBoxBase = self.selectBox.getState().copy()
|
||||
self.selectBox.blockSignals(False)
|
||||
|
||||
def zValue(self):
|
||||
return self.opts['z']
|
||||
|
||||
def setZValue(self, z):
|
||||
self.opts['z'] = z
|
||||
if z is not None:
|
||||
self._graphicsItem.setZValue(z)
|
||||
|
||||
#def selectionChanged(self, canvas, items):
|
||||
#self.selected = len(items) == 1 and (items[0] is self)
|
||||
#self.showSelectBox()
|
||||
|
||||
|
||||
def selectionChanged(self, sel, multi):
|
||||
"""
|
||||
Inform the item that its selection state has changed.
|
||||
============== =========================================================
|
||||
**Arguments:**
|
||||
sel (bool) whether the item is currently selected
|
||||
multi (bool) whether there are multiple items currently
|
||||
selected
|
||||
============== =========================================================
|
||||
"""
|
||||
self.selectedAlone = sel and not multi
|
||||
self.showSelectBox()
|
||||
if self.selectedAlone:
|
||||
self.ctrlWidget().show()
|
||||
else:
|
||||
self.ctrlWidget().hide()
|
||||
|
||||
def showSelectBox(self):
|
||||
"""Display the selection box around this item if it is selected and movable"""
|
||||
if self.selectedAlone and self.isMovable() and self.isVisible(): #and len(self.canvas.itemList.selectedItems())==1:
|
||||
self.selectBox.show()
|
||||
else:
|
||||
self.selectBox.hide()
|
||||
|
||||
def hideSelectBox(self):
|
||||
self.selectBox.hide()
|
||||
|
||||
|
||||
def selectBoxChanged(self):
|
||||
self.selectBoxMoved()
|
||||
#self.updateTransform(self.selectBox)
|
||||
#self.emit(QtCore.SIGNAL('transformChanged'), self)
|
||||
self.sigTransformChanged.emit(self)
|
||||
|
||||
def selectBoxChangeFinished(self):
|
||||
#self.emit(QtCore.SIGNAL('transformChangeFinished'), self)
|
||||
self.sigTransformChangeFinished.emit(self)
|
||||
|
||||
def alphaPressed(self):
|
||||
"""Hide selection box while slider is moving"""
|
||||
self.hideSelectBox()
|
||||
|
||||
def alphaReleased(self):
|
||||
self.showSelectBox()
|
||||
|
||||
def show(self):
|
||||
if self.opts['visible']:
|
||||
return
|
||||
self.opts['visible'] = True
|
||||
self._graphicsItem.show()
|
||||
self.showSelectBox()
|
||||
self.sigVisibilityChanged.emit(self)
|
||||
|
||||
def hide(self):
|
||||
if not self.opts['visible']:
|
||||
return
|
||||
self.opts['visible'] = False
|
||||
self._graphicsItem.hide()
|
||||
self.hideSelectBox()
|
||||
self.sigVisibilityChanged.emit(self)
|
||||
|
||||
def setVisible(self, vis):
|
||||
if vis:
|
||||
self.show()
|
||||
else:
|
||||
self.hide()
|
||||
|
||||
def isVisible(self):
|
||||
return self.opts['visible']
|
||||
|
||||
|
||||
class GroupCanvasItem(CanvasItem):
|
||||
"""
|
||||
Canvas item used for grouping others
|
||||
"""
|
||||
|
||||
def __init__(self, **opts):
|
||||
defOpts = {'movable': False, 'scalable': False}
|
||||
defOpts.update(opts)
|
||||
item = ItemGroup()
|
||||
CanvasItem.__init__(self, item, **defOpts)
|
||||
|
76
pyqtgraph/canvas/CanvasManager.py
Normal file
76
pyqtgraph/canvas/CanvasManager.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ..Qt import QtCore, QtGui
|
||||
if not hasattr(QtCore, 'Signal'):
|
||||
QtCore.Signal = QtCore.pyqtSignal
|
||||
import weakref
|
||||
|
||||
class CanvasManager(QtCore.QObject):
|
||||
SINGLETON = None
|
||||
|
||||
sigCanvasListChanged = QtCore.Signal()
|
||||
|
||||
def __init__(self):
|
||||
if CanvasManager.SINGLETON is not None:
|
||||
raise Exception("Can only create one canvas manager.")
|
||||
CanvasManager.SINGLETON = self
|
||||
QtCore.QObject.__init__(self)
|
||||
self.canvases = weakref.WeakValueDictionary()
|
||||
|
||||
@classmethod
|
||||
def instance(cls):
|
||||
return CanvasManager.SINGLETON
|
||||
|
||||
def registerCanvas(self, canvas, name):
|
||||
n2 = name
|
||||
i = 0
|
||||
while n2 in self.canvases:
|
||||
n2 = "%s_%03d" % (name, i)
|
||||
i += 1
|
||||
self.canvases[n2] = canvas
|
||||
self.sigCanvasListChanged.emit()
|
||||
return n2
|
||||
|
||||
def unregisterCanvas(self, name):
|
||||
c = self.canvases[name]
|
||||
del self.canvases[name]
|
||||
self.sigCanvasListChanged.emit()
|
||||
|
||||
def listCanvases(self):
|
||||
return list(self.canvases.keys())
|
||||
|
||||
def getCanvas(self, name):
|
||||
return self.canvases[name]
|
||||
|
||||
|
||||
manager = CanvasManager()
|
||||
|
||||
|
||||
class CanvasCombo(QtGui.QComboBox):
|
||||
def __init__(self, parent=None):
|
||||
QtGui.QComboBox.__init__(self, parent)
|
||||
man = CanvasManager.instance()
|
||||
man.sigCanvasListChanged.connect(self.updateCanvasList)
|
||||
self.hostName = None
|
||||
self.updateCanvasList()
|
||||
|
||||
def updateCanvasList(self):
|
||||
canvases = CanvasManager.instance().listCanvases()
|
||||
canvases.insert(0, "")
|
||||
if self.hostName in canvases:
|
||||
canvases.remove(self.hostName)
|
||||
|
||||
sel = self.currentText()
|
||||
if sel in canvases:
|
||||
self.blockSignals(True) ## change does not affect current selection; block signals during update
|
||||
self.clear()
|
||||
for i in canvases:
|
||||
self.addItem(i)
|
||||
if i == sel:
|
||||
self.setCurrentIndex(self.count())
|
||||
|
||||
self.blockSignals(False)
|
||||
|
||||
def setHostName(self, name):
|
||||
self.hostName = name
|
||||
self.updateCanvasList()
|
||||
|
135
pyqtgraph/canvas/CanvasTemplate.ui
Normal file
135
pyqtgraph/canvas/CanvasTemplate.ui
Normal file
|
@ -0,0 +1,135 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>490</width>
|
||||
<height>414</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="GraphicsView" name="view"/>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QPushButton" name="autoRangeBtn">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>1</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Auto Range</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="redirectCheck">
|
||||
<property name="toolTip">
|
||||
<string>Check to display all local items in a remote canvas.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Redirect</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="CanvasCombo" name="redirectCombo"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="2">
|
||||
<widget class="TreeWidget" name="itemList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="headerHidden">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string notr="true">1</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="0" colspan="2">
|
||||
<layout class="QGridLayout" name="ctrlLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QPushButton" name="resetTransformsBtn">
|
||||
<property name="text">
|
||||
<string>Reset Transforms</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QPushButton" name="mirrorSelectionBtn">
|
||||
<property name="text">
|
||||
<string>Mirror Selection</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QPushButton" name="reflectSelectionBtn">
|
||||
<property name="text">
|
||||
<string>MirrorXY</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>TreeWidget</class>
|
||||
<extends>QTreeWidget</extends>
|
||||
<header>..widgets.TreeWidget</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>GraphicsView</class>
|
||||
<extends>QGraphicsView</extends>
|
||||
<header>..widgets.GraphicsView</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>CanvasCombo</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>CanvasManager</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
92
pyqtgraph/canvas/CanvasTemplate_pyqt.py
Normal file
92
pyqtgraph/canvas/CanvasTemplate_pyqt.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file 'acq4/pyqtgraph/canvas/CanvasTemplate.ui'
|
||||
#
|
||||
# Created: Thu Jan 2 11:13:07 2014
|
||||
# by: PyQt4 UI code generator 4.9
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
try:
|
||||
_fromUtf8 = QtCore.QString.fromUtf8
|
||||
except AttributeError:
|
||||
_fromUtf8 = lambda s: s
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName(_fromUtf8("Form"))
|
||||
Form.resize(490, 414)
|
||||
self.gridLayout = QtGui.QGridLayout(Form)
|
||||
self.gridLayout.setMargin(0)
|
||||
self.gridLayout.setSpacing(0)
|
||||
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
|
||||
self.splitter = QtGui.QSplitter(Form)
|
||||
self.splitter.setOrientation(QtCore.Qt.Horizontal)
|
||||
self.splitter.setObjectName(_fromUtf8("splitter"))
|
||||
self.view = GraphicsView(self.splitter)
|
||||
self.view.setObjectName(_fromUtf8("view"))
|
||||
self.layoutWidget = QtGui.QWidget(self.splitter)
|
||||
self.layoutWidget.setObjectName(_fromUtf8("layoutWidget"))
|
||||
self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget)
|
||||
self.gridLayout_2.setMargin(0)
|
||||
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
|
||||
self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(1)
|
||||
sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth())
|
||||
self.autoRangeBtn.setSizePolicy(sizePolicy)
|
||||
self.autoRangeBtn.setObjectName(_fromUtf8("autoRangeBtn"))
|
||||
self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2)
|
||||
self.horizontalLayout = QtGui.QHBoxLayout()
|
||||
self.horizontalLayout.setSpacing(0)
|
||||
self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
|
||||
self.redirectCheck = QtGui.QCheckBox(self.layoutWidget)
|
||||
self.redirectCheck.setObjectName(_fromUtf8("redirectCheck"))
|
||||
self.horizontalLayout.addWidget(self.redirectCheck)
|
||||
self.redirectCombo = CanvasCombo(self.layoutWidget)
|
||||
self.redirectCombo.setObjectName(_fromUtf8("redirectCombo"))
|
||||
self.horizontalLayout.addWidget(self.redirectCombo)
|
||||
self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2)
|
||||
self.itemList = TreeWidget(self.layoutWidget)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(100)
|
||||
sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth())
|
||||
self.itemList.setSizePolicy(sizePolicy)
|
||||
self.itemList.setHeaderHidden(True)
|
||||
self.itemList.setObjectName(_fromUtf8("itemList"))
|
||||
self.itemList.headerItem().setText(0, _fromUtf8("1"))
|
||||
self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2)
|
||||
self.ctrlLayout = QtGui.QGridLayout()
|
||||
self.ctrlLayout.setSpacing(0)
|
||||
self.ctrlLayout.setObjectName(_fromUtf8("ctrlLayout"))
|
||||
self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2)
|
||||
self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn"))
|
||||
self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1)
|
||||
self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn"))
|
||||
self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1)
|
||||
self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.reflectSelectionBtn.setObjectName(_fromUtf8("reflectSelectionBtn"))
|
||||
self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1)
|
||||
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.redirectCheck.setToolTip(QtGui.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.redirectCheck.setText(QtGui.QApplication.translate("Form", "Redirect", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.resetTransformsBtn.setText(QtGui.QApplication.translate("Form", "Reset Transforms", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8))
|
||||
|
||||
from ..widgets.TreeWidget import TreeWidget
|
||||
from CanvasManager import CanvasCombo
|
||||
from ..widgets.GraphicsView import GraphicsView
|
96
pyqtgraph/canvas/CanvasTemplate_pyqt5.py
Normal file
96
pyqtgraph/canvas/CanvasTemplate_pyqt5.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/canvas/CanvasTemplate.ui'
|
||||
#
|
||||
# Created: Wed Mar 26 15:09:28 2014
|
||||
# by: PyQt5 UI code generator 5.0.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName("Form")
|
||||
Form.resize(490, 414)
|
||||
self.gridLayout = QtWidgets.QGridLayout(Form)
|
||||
self.gridLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout.setSpacing(0)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.splitter = QtWidgets.QSplitter(Form)
|
||||
self.splitter.setOrientation(QtCore.Qt.Horizontal)
|
||||
self.splitter.setObjectName("splitter")
|
||||
self.view = GraphicsView(self.splitter)
|
||||
self.view.setObjectName("view")
|
||||
self.layoutWidget = QtWidgets.QWidget(self.splitter)
|
||||
self.layoutWidget.setObjectName("layoutWidget")
|
||||
self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget)
|
||||
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
self.storeSvgBtn = QtWidgets.QPushButton(self.layoutWidget)
|
||||
self.storeSvgBtn.setObjectName("storeSvgBtn")
|
||||
self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1)
|
||||
self.storePngBtn = QtWidgets.QPushButton(self.layoutWidget)
|
||||
self.storePngBtn.setObjectName("storePngBtn")
|
||||
self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1)
|
||||
self.autoRangeBtn = QtWidgets.QPushButton(self.layoutWidget)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(1)
|
||||
sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth())
|
||||
self.autoRangeBtn.setSizePolicy(sizePolicy)
|
||||
self.autoRangeBtn.setObjectName("autoRangeBtn")
|
||||
self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2)
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout.setSpacing(0)
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.redirectCheck = QtWidgets.QCheckBox(self.layoutWidget)
|
||||
self.redirectCheck.setObjectName("redirectCheck")
|
||||
self.horizontalLayout.addWidget(self.redirectCheck)
|
||||
self.redirectCombo = CanvasCombo(self.layoutWidget)
|
||||
self.redirectCombo.setObjectName("redirectCombo")
|
||||
self.horizontalLayout.addWidget(self.redirectCombo)
|
||||
self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2)
|
||||
self.itemList = TreeWidget(self.layoutWidget)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(100)
|
||||
sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth())
|
||||
self.itemList.setSizePolicy(sizePolicy)
|
||||
self.itemList.setHeaderHidden(True)
|
||||
self.itemList.setObjectName("itemList")
|
||||
self.itemList.headerItem().setText(0, "1")
|
||||
self.gridLayout_2.addWidget(self.itemList, 7, 0, 1, 2)
|
||||
self.ctrlLayout = QtWidgets.QGridLayout()
|
||||
self.ctrlLayout.setSpacing(0)
|
||||
self.ctrlLayout.setObjectName("ctrlLayout")
|
||||
self.gridLayout_2.addLayout(self.ctrlLayout, 11, 0, 1, 2)
|
||||
self.resetTransformsBtn = QtWidgets.QPushButton(self.layoutWidget)
|
||||
self.resetTransformsBtn.setObjectName("resetTransformsBtn")
|
||||
self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 0, 1, 1)
|
||||
self.mirrorSelectionBtn = QtWidgets.QPushButton(self.layoutWidget)
|
||||
self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn")
|
||||
self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1)
|
||||
self.reflectSelectionBtn = QtWidgets.QPushButton(self.layoutWidget)
|
||||
self.reflectSelectionBtn.setObjectName("reflectSelectionBtn")
|
||||
self.gridLayout_2.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1)
|
||||
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
Form.setWindowTitle(_translate("Form", "Form"))
|
||||
self.storeSvgBtn.setText(_translate("Form", "Store SVG"))
|
||||
self.storePngBtn.setText(_translate("Form", "Store PNG"))
|
||||
self.autoRangeBtn.setText(_translate("Form", "Auto Range"))
|
||||
self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas."))
|
||||
self.redirectCheck.setText(_translate("Form", "Redirect"))
|
||||
self.resetTransformsBtn.setText(_translate("Form", "Reset Transforms"))
|
||||
self.mirrorSelectionBtn.setText(_translate("Form", "Mirror Selection"))
|
||||
self.reflectSelectionBtn.setText(_translate("Form", "MirrorXY"))
|
||||
|
||||
from ..widgets.GraphicsView import GraphicsView
|
||||
from ..widgets.TreeWidget import TreeWidget
|
||||
from CanvasManager import CanvasCombo
|
95
pyqtgraph/canvas/CanvasTemplate_pyside.py
Normal file
95
pyqtgraph/canvas/CanvasTemplate_pyside.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/canvas/CanvasTemplate.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:52 2013
|
||||
# by: pyside-uic 0.2.14 running on PySide 1.1.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName("Form")
|
||||
Form.resize(490, 414)
|
||||
self.gridLayout = QtGui.QGridLayout(Form)
|
||||
self.gridLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout.setSpacing(0)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.splitter = QtGui.QSplitter(Form)
|
||||
self.splitter.setOrientation(QtCore.Qt.Horizontal)
|
||||
self.splitter.setObjectName("splitter")
|
||||
self.view = GraphicsView(self.splitter)
|
||||
self.view.setObjectName("view")
|
||||
self.layoutWidget = QtGui.QWidget(self.splitter)
|
||||
self.layoutWidget.setObjectName("layoutWidget")
|
||||
self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget)
|
||||
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
self.storeSvgBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.storeSvgBtn.setObjectName("storeSvgBtn")
|
||||
self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1)
|
||||
self.storePngBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.storePngBtn.setObjectName("storePngBtn")
|
||||
self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1)
|
||||
self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(1)
|
||||
sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth())
|
||||
self.autoRangeBtn.setSizePolicy(sizePolicy)
|
||||
self.autoRangeBtn.setObjectName("autoRangeBtn")
|
||||
self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2)
|
||||
self.horizontalLayout = QtGui.QHBoxLayout()
|
||||
self.horizontalLayout.setSpacing(0)
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.redirectCheck = QtGui.QCheckBox(self.layoutWidget)
|
||||
self.redirectCheck.setObjectName("redirectCheck")
|
||||
self.horizontalLayout.addWidget(self.redirectCheck)
|
||||
self.redirectCombo = CanvasCombo(self.layoutWidget)
|
||||
self.redirectCombo.setObjectName("redirectCombo")
|
||||
self.horizontalLayout.addWidget(self.redirectCombo)
|
||||
self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2)
|
||||
self.itemList = TreeWidget(self.layoutWidget)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(100)
|
||||
sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth())
|
||||
self.itemList.setSizePolicy(sizePolicy)
|
||||
self.itemList.setHeaderHidden(True)
|
||||
self.itemList.setObjectName("itemList")
|
||||
self.itemList.headerItem().setText(0, "1")
|
||||
self.gridLayout_2.addWidget(self.itemList, 7, 0, 1, 2)
|
||||
self.ctrlLayout = QtGui.QGridLayout()
|
||||
self.ctrlLayout.setSpacing(0)
|
||||
self.ctrlLayout.setObjectName("ctrlLayout")
|
||||
self.gridLayout_2.addLayout(self.ctrlLayout, 11, 0, 1, 2)
|
||||
self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.resetTransformsBtn.setObjectName("resetTransformsBtn")
|
||||
self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 0, 1, 1)
|
||||
self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn")
|
||||
self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1)
|
||||
self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.reflectSelectionBtn.setObjectName("reflectSelectionBtn")
|
||||
self.gridLayout_2.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1)
|
||||
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.storeSvgBtn.setText(QtGui.QApplication.translate("Form", "Store SVG", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.storePngBtn.setText(QtGui.QApplication.translate("Form", "Store PNG", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.redirectCheck.setToolTip(QtGui.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.redirectCheck.setText(QtGui.QApplication.translate("Form", "Redirect", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.resetTransformsBtn.setText(QtGui.QApplication.translate("Form", "Reset Transforms", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8))
|
||||
|
||||
from ..widgets.TreeWidget import TreeWidget
|
||||
from CanvasManager import CanvasCombo
|
||||
from ..widgets.GraphicsView import GraphicsView
|
75
pyqtgraph/canvas/TransformGuiTemplate.ui
Normal file
75
pyqtgraph/canvas/TransformGuiTemplate.ui
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>224</width>
|
||||
<height>117</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="translateLabel">
|
||||
<property name="text">
|
||||
<string>Translate:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="rotateLabel">
|
||||
<property name="text">
|
||||
<string>Rotate:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="scaleLabel">
|
||||
<property name="text">
|
||||
<string>Scale:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="mirrorImageBtn">
|
||||
<property name="toolTip">
|
||||
<string extracomment="Mirror the item across the global Y axis"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Mirror</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="reflectImageBtn">
|
||||
<property name="text">
|
||||
<string>Reflect</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
69
pyqtgraph/canvas/TransformGuiTemplate_pyqt.py
Normal file
69
pyqtgraph/canvas/TransformGuiTemplate_pyqt.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/canvas/TransformGuiTemplate.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:52 2013
|
||||
# by: PyQt4 UI code generator 4.10
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
try:
|
||||
_fromUtf8 = QtCore.QString.fromUtf8
|
||||
except AttributeError:
|
||||
def _fromUtf8(s):
|
||||
return s
|
||||
|
||||
try:
|
||||
_encoding = QtGui.QApplication.UnicodeUTF8
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig, _encoding)
|
||||
except AttributeError:
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig)
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName(_fromUtf8("Form"))
|
||||
Form.resize(224, 117)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth())
|
||||
Form.setSizePolicy(sizePolicy)
|
||||
self.verticalLayout = QtGui.QVBoxLayout(Form)
|
||||
self.verticalLayout.setSpacing(1)
|
||||
self.verticalLayout.setMargin(0)
|
||||
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
|
||||
self.translateLabel = QtGui.QLabel(Form)
|
||||
self.translateLabel.setObjectName(_fromUtf8("translateLabel"))
|
||||
self.verticalLayout.addWidget(self.translateLabel)
|
||||
self.rotateLabel = QtGui.QLabel(Form)
|
||||
self.rotateLabel.setObjectName(_fromUtf8("rotateLabel"))
|
||||
self.verticalLayout.addWidget(self.rotateLabel)
|
||||
self.scaleLabel = QtGui.QLabel(Form)
|
||||
self.scaleLabel.setObjectName(_fromUtf8("scaleLabel"))
|
||||
self.verticalLayout.addWidget(self.scaleLabel)
|
||||
self.horizontalLayout = QtGui.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
|
||||
self.mirrorImageBtn = QtGui.QPushButton(Form)
|
||||
self.mirrorImageBtn.setToolTip(_fromUtf8(""))
|
||||
self.mirrorImageBtn.setObjectName(_fromUtf8("mirrorImageBtn"))
|
||||
self.horizontalLayout.addWidget(self.mirrorImageBtn)
|
||||
self.reflectImageBtn = QtGui.QPushButton(Form)
|
||||
self.reflectImageBtn.setObjectName(_fromUtf8("reflectImageBtn"))
|
||||
self.horizontalLayout.addWidget(self.reflectImageBtn)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(_translate("Form", "Form", None))
|
||||
self.translateLabel.setText(_translate("Form", "Translate:", None))
|
||||
self.rotateLabel.setText(_translate("Form", "Rotate:", None))
|
||||
self.scaleLabel.setText(_translate("Form", "Scale:", None))
|
||||
self.mirrorImageBtn.setText(_translate("Form", "Mirror", None))
|
||||
self.reflectImageBtn.setText(_translate("Form", "Reflect", None))
|
||||
|
56
pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py
Normal file
56
pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/canvas/TransformGuiTemplate.ui'
|
||||
#
|
||||
# Created: Wed Mar 26 15:09:28 2014
|
||||
# by: PyQt5 UI code generator 5.0.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName("Form")
|
||||
Form.resize(224, 117)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth())
|
||||
Form.setSizePolicy(sizePolicy)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(Form)
|
||||
self.verticalLayout.setSpacing(1)
|
||||
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.translateLabel = QtWidgets.QLabel(Form)
|
||||
self.translateLabel.setObjectName("translateLabel")
|
||||
self.verticalLayout.addWidget(self.translateLabel)
|
||||
self.rotateLabel = QtWidgets.QLabel(Form)
|
||||
self.rotateLabel.setObjectName("rotateLabel")
|
||||
self.verticalLayout.addWidget(self.rotateLabel)
|
||||
self.scaleLabel = QtWidgets.QLabel(Form)
|
||||
self.scaleLabel.setObjectName("scaleLabel")
|
||||
self.verticalLayout.addWidget(self.scaleLabel)
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.mirrorImageBtn = QtWidgets.QPushButton(Form)
|
||||
self.mirrorImageBtn.setToolTip("")
|
||||
self.mirrorImageBtn.setObjectName("mirrorImageBtn")
|
||||
self.horizontalLayout.addWidget(self.mirrorImageBtn)
|
||||
self.reflectImageBtn = QtWidgets.QPushButton(Form)
|
||||
self.reflectImageBtn.setObjectName("reflectImageBtn")
|
||||
self.horizontalLayout.addWidget(self.reflectImageBtn)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
Form.setWindowTitle(_translate("Form", "Form"))
|
||||
self.translateLabel.setText(_translate("Form", "Translate:"))
|
||||
self.rotateLabel.setText(_translate("Form", "Rotate:"))
|
||||
self.scaleLabel.setText(_translate("Form", "Scale:"))
|
||||
self.mirrorImageBtn.setText(_translate("Form", "Mirror"))
|
||||
self.reflectImageBtn.setText(_translate("Form", "Reflect"))
|
||||
|
55
pyqtgraph/canvas/TransformGuiTemplate_pyside.py
Normal file
55
pyqtgraph/canvas/TransformGuiTemplate_pyside.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/canvas/TransformGuiTemplate.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:52 2013
|
||||
# by: pyside-uic 0.2.14 running on PySide 1.1.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName("Form")
|
||||
Form.resize(224, 117)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth())
|
||||
Form.setSizePolicy(sizePolicy)
|
||||
self.verticalLayout = QtGui.QVBoxLayout(Form)
|
||||
self.verticalLayout.setSpacing(1)
|
||||
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.translateLabel = QtGui.QLabel(Form)
|
||||
self.translateLabel.setObjectName("translateLabel")
|
||||
self.verticalLayout.addWidget(self.translateLabel)
|
||||
self.rotateLabel = QtGui.QLabel(Form)
|
||||
self.rotateLabel.setObjectName("rotateLabel")
|
||||
self.verticalLayout.addWidget(self.rotateLabel)
|
||||
self.scaleLabel = QtGui.QLabel(Form)
|
||||
self.scaleLabel.setObjectName("scaleLabel")
|
||||
self.verticalLayout.addWidget(self.scaleLabel)
|
||||
self.horizontalLayout = QtGui.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.mirrorImageBtn = QtGui.QPushButton(Form)
|
||||
self.mirrorImageBtn.setToolTip("")
|
||||
self.mirrorImageBtn.setObjectName("mirrorImageBtn")
|
||||
self.horizontalLayout.addWidget(self.mirrorImageBtn)
|
||||
self.reflectImageBtn = QtGui.QPushButton(Form)
|
||||
self.reflectImageBtn.setObjectName("reflectImageBtn")
|
||||
self.horizontalLayout.addWidget(self.reflectImageBtn)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.translateLabel.setText(QtGui.QApplication.translate("Form", "Translate:", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.rotateLabel.setText(QtGui.QApplication.translate("Form", "Rotate:", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.scaleLabel.setText(QtGui.QApplication.translate("Form", "Scale:", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.mirrorImageBtn.setText(QtGui.QApplication.translate("Form", "Mirror", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.reflectImageBtn.setText(QtGui.QApplication.translate("Form", "Reflect", None, QtGui.QApplication.UnicodeUTF8))
|
||||
|
3
pyqtgraph/canvas/__init__.py
Normal file
3
pyqtgraph/canvas/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .Canvas import *
|
||||
from .CanvasItem import *
|
250
pyqtgraph/colormap.py
Normal file
250
pyqtgraph/colormap.py
Normal file
|
@ -0,0 +1,250 @@
|
|||
import numpy as np
|
||||
from .Qt import QtGui, QtCore
|
||||
|
||||
class ColorMap(object):
|
||||
"""
|
||||
A ColorMap defines a relationship between a scalar value and a range of colors.
|
||||
ColorMaps are commonly used for false-coloring monochromatic images, coloring
|
||||
scatter-plot points, and coloring surface plots by height.
|
||||
|
||||
Each color map is defined by a set of colors, each corresponding to a
|
||||
particular scalar value. For example:
|
||||
|
||||
| 0.0 -> black
|
||||
| 0.2 -> red
|
||||
| 0.6 -> yellow
|
||||
| 1.0 -> white
|
||||
|
||||
The colors for intermediate values are determined by interpolating between
|
||||
the two nearest colors in either RGB or HSV color space.
|
||||
|
||||
To provide user-defined color mappings, see :class:`GradientWidget <pyqtgraph.GradientWidget>`.
|
||||
"""
|
||||
|
||||
|
||||
## color interpolation modes
|
||||
RGB = 1
|
||||
HSV_POS = 2
|
||||
HSV_NEG = 3
|
||||
|
||||
## boundary modes
|
||||
CLIP = 1
|
||||
REPEAT = 2
|
||||
MIRROR = 3
|
||||
|
||||
## return types
|
||||
BYTE = 1
|
||||
FLOAT = 2
|
||||
QCOLOR = 3
|
||||
|
||||
enumMap = {
|
||||
'rgb': RGB,
|
||||
'hsv+': HSV_POS,
|
||||
'hsv-': HSV_NEG,
|
||||
'clip': CLIP,
|
||||
'repeat': REPEAT,
|
||||
'mirror': MIRROR,
|
||||
'byte': BYTE,
|
||||
'float': FLOAT,
|
||||
'qcolor': QCOLOR,
|
||||
}
|
||||
|
||||
def __init__(self, pos, color, mode=None):
|
||||
"""
|
||||
=============== ==============================================================
|
||||
**Arguments:**
|
||||
pos Array of positions where each color is defined
|
||||
color Array of RGBA colors.
|
||||
Integer data types are interpreted as 0-255; float data types
|
||||
are interpreted as 0.0-1.0
|
||||
mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG)
|
||||
indicating the color space that should be used when
|
||||
interpolating between stops. Note that the last mode value is
|
||||
ignored. By default, the mode is entirely RGB.
|
||||
=============== ==============================================================
|
||||
"""
|
||||
self.pos = np.array(pos)
|
||||
self.color = np.array(color)
|
||||
if mode is None:
|
||||
mode = np.ones(len(pos))
|
||||
self.mode = mode
|
||||
self.stopsCache = {}
|
||||
|
||||
def map(self, data, mode='byte'):
|
||||
"""
|
||||
Return an array of colors corresponding to the values in *data*.
|
||||
Data must be either a scalar position or an array (any shape) of positions.
|
||||
|
||||
The *mode* argument determines the type of data returned:
|
||||
|
||||
=========== ===============================================================
|
||||
byte (default) Values are returned as 0-255 unsigned bytes.
|
||||
float Values are returned as 0.0-1.0 floats.
|
||||
qcolor Values are returned as an array of QColor objects.
|
||||
=========== ===============================================================
|
||||
"""
|
||||
if isinstance(mode, basestring):
|
||||
mode = self.enumMap[mode.lower()]
|
||||
|
||||
if mode == self.QCOLOR:
|
||||
pos, color = self.getStops(self.BYTE)
|
||||
else:
|
||||
pos, color = self.getStops(mode)
|
||||
|
||||
# don't need this--np.interp takes care of it.
|
||||
#data = np.clip(data, pos.min(), pos.max())
|
||||
|
||||
# Interpolate
|
||||
# TODO: is griddata faster?
|
||||
# interp = scipy.interpolate.griddata(pos, color, data)
|
||||
if np.isscalar(data):
|
||||
interp = np.empty((color.shape[1],), dtype=color.dtype)
|
||||
else:
|
||||
if not isinstance(data, np.ndarray):
|
||||
data = np.array(data)
|
||||
interp = np.empty(data.shape + (color.shape[1],), dtype=color.dtype)
|
||||
for i in range(color.shape[1]):
|
||||
interp[...,i] = np.interp(data, pos, color[:,i])
|
||||
|
||||
# Convert to QColor if requested
|
||||
if mode == self.QCOLOR:
|
||||
if np.isscalar(data):
|
||||
return QtGui.QColor(*interp)
|
||||
else:
|
||||
return [QtGui.QColor(*x) for x in interp]
|
||||
else:
|
||||
return interp
|
||||
|
||||
def mapToQColor(self, data):
|
||||
"""Convenience function; see :func:`map() <pyqtgraph.ColorMap.map>`."""
|
||||
return self.map(data, mode=self.QCOLOR)
|
||||
|
||||
def mapToByte(self, data):
|
||||
"""Convenience function; see :func:`map() <pyqtgraph.ColorMap.map>`."""
|
||||
return self.map(data, mode=self.BYTE)
|
||||
|
||||
def mapToFloat(self, data):
|
||||
"""Convenience function; see :func:`map() <pyqtgraph.ColorMap.map>`."""
|
||||
return self.map(data, mode=self.FLOAT)
|
||||
|
||||
def getGradient(self, p1=None, p2=None):
|
||||
"""Return a QLinearGradient object spanning from QPoints p1 to p2."""
|
||||
if p1 == None:
|
||||
p1 = QtCore.QPointF(0,0)
|
||||
if p2 == None:
|
||||
p2 = QtCore.QPointF(self.pos.max()-self.pos.min(),0)
|
||||
g = QtGui.QLinearGradient(p1, p2)
|
||||
|
||||
pos, color = self.getStops(mode=self.BYTE)
|
||||
color = [QtGui.QColor(*x) for x in color]
|
||||
g.setStops(zip(pos, color))
|
||||
|
||||
#if self.colorMode == 'rgb':
|
||||
#ticks = self.listTicks()
|
||||
#g.setStops([(x, QtGui.QColor(t.color)) for t,x in ticks])
|
||||
#elif self.colorMode == 'hsv': ## HSV mode is approximated for display by interpolating 10 points between each stop
|
||||
#ticks = self.listTicks()
|
||||
#stops = []
|
||||
#stops.append((ticks[0][1], ticks[0][0].color))
|
||||
#for i in range(1,len(ticks)):
|
||||
#x1 = ticks[i-1][1]
|
||||
#x2 = ticks[i][1]
|
||||
#dx = (x2-x1) / 10.
|
||||
#for j in range(1,10):
|
||||
#x = x1 + dx*j
|
||||
#stops.append((x, self.getColor(x)))
|
||||
#stops.append((x2, self.getColor(x2)))
|
||||
#g.setStops(stops)
|
||||
return g
|
||||
|
||||
def getColors(self, mode=None):
|
||||
"""Return list of all color stops converted to the specified mode.
|
||||
If mode is None, then no conversion is done."""
|
||||
if isinstance(mode, basestring):
|
||||
mode = self.enumMap[mode.lower()]
|
||||
|
||||
color = self.color
|
||||
if mode in [self.BYTE, self.QCOLOR] and color.dtype.kind == 'f':
|
||||
color = (color * 255).astype(np.ubyte)
|
||||
elif mode == self.FLOAT and color.dtype.kind != 'f':
|
||||
color = color.astype(float) / 255.
|
||||
|
||||
if mode == self.QCOLOR:
|
||||
color = [QtGui.QColor(*x) for x in color]
|
||||
|
||||
return color
|
||||
|
||||
def getStops(self, mode):
|
||||
## Get fully-expanded set of RGBA stops in either float or byte mode.
|
||||
if mode not in self.stopsCache:
|
||||
color = self.color
|
||||
if mode == self.BYTE and color.dtype.kind == 'f':
|
||||
color = (color * 255).astype(np.ubyte)
|
||||
elif mode == self.FLOAT and color.dtype.kind != 'f':
|
||||
color = color.astype(float) / 255.
|
||||
|
||||
## to support HSV mode, we need to do a little more work..
|
||||
#stops = []
|
||||
#for i in range(len(self.pos)):
|
||||
#pos = self.pos[i]
|
||||
#color = color[i]
|
||||
|
||||
#imode = self.mode[i]
|
||||
#if imode == self.RGB:
|
||||
#stops.append((x,color))
|
||||
#else:
|
||||
#ns =
|
||||
self.stopsCache[mode] = (self.pos, color)
|
||||
return self.stopsCache[mode]
|
||||
|
||||
def getLookupTable(self, start=0.0, stop=1.0, nPts=512, alpha=None, mode='byte'):
|
||||
"""
|
||||
Return an RGB(A) lookup table (ndarray).
|
||||
|
||||
=============== =============================================================================
|
||||
**Arguments:**
|
||||
start The starting value in the lookup table (default=0.0)
|
||||
stop The final value in the lookup table (default=1.0)
|
||||
nPts The number of points in the returned lookup table.
|
||||
alpha True, False, or None - Specifies whether or not alpha values are included
|
||||
in the table. If alpha is None, it will be automatically determined.
|
||||
mode Determines return type: 'byte' (0-255), 'float' (0.0-1.0), or 'qcolor'.
|
||||
See :func:`map() <pyqtgraph.ColorMap.map>`.
|
||||
=============== =============================================================================
|
||||
"""
|
||||
if isinstance(mode, basestring):
|
||||
mode = self.enumMap[mode.lower()]
|
||||
|
||||
if alpha is None:
|
||||
alpha = self.usesAlpha()
|
||||
|
||||
x = np.linspace(start, stop, nPts)
|
||||
table = self.map(x, mode)
|
||||
|
||||
if not alpha:
|
||||
return table[:,:3]
|
||||
else:
|
||||
return table
|
||||
|
||||
def usesAlpha(self):
|
||||
"""Return True if any stops have an alpha < 255"""
|
||||
max = 1.0 if self.color.dtype.kind == 'f' else 255
|
||||
return np.any(self.color[:,3] != max)
|
||||
|
||||
def isMapTrivial(self):
|
||||
"""
|
||||
Return True if the gradient has exactly two stops in it: black at 0.0 and white at 1.0.
|
||||
"""
|
||||
if len(self.pos) != 2:
|
||||
return False
|
||||
if self.pos[0] != 0.0 or self.pos[1] != 1.0:
|
||||
return False
|
||||
if self.color.dtype.kind == 'f':
|
||||
return np.all(self.color == np.array([[0.,0.,0.,1.], [1.,1.,1.,1.]]))
|
||||
else:
|
||||
return np.all(self.color == np.array([[0,0,0,255], [255,255,255,255]]))
|
||||
|
||||
def __repr__(self):
|
||||
pos = repr(self.pos).replace('\n', '')
|
||||
color = repr(self.color).replace('\n', '')
|
||||
return "ColorMap(%s, %s)" % (pos, color)
|
217
pyqtgraph/configfile.py
Normal file
217
pyqtgraph/configfile.py
Normal file
|
@ -0,0 +1,217 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
configfile.py - Human-readable text configuration file library
|
||||
Copyright 2010 Luke Campagnola
|
||||
Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
|
||||
Used for reading and writing dictionary objects to a python-like configuration
|
||||
file format. Data structures may be nested and contain any data type as long
|
||||
as it can be converted to/from a string using repr and eval.
|
||||
"""
|
||||
|
||||
import re, os, sys
|
||||
from .pgcollections import OrderedDict
|
||||
GLOBAL_PATH = None # so not thread safe.
|
||||
from . import units
|
||||
from .python2_3 import asUnicode
|
||||
from .Qt import QtCore
|
||||
from .Point import Point
|
||||
from .colormap import ColorMap
|
||||
import numpy
|
||||
|
||||
class ParseError(Exception):
|
||||
def __init__(self, message, lineNum, line, fileName=None):
|
||||
self.lineNum = lineNum
|
||||
self.line = line
|
||||
#self.message = message
|
||||
self.fileName = fileName
|
||||
Exception.__init__(self, message)
|
||||
|
||||
def __str__(self):
|
||||
if self.fileName is None:
|
||||
msg = "Error parsing string at line %d:\n" % self.lineNum
|
||||
else:
|
||||
msg = "Error parsing config file '%s' at line %d:\n" % (self.fileName, self.lineNum)
|
||||
msg += "%s\n%s" % (self.line, self.message)
|
||||
return msg
|
||||
#raise Exception()
|
||||
|
||||
|
||||
def writeConfigFile(data, fname):
|
||||
s = genString(data)
|
||||
fd = open(fname, 'w')
|
||||
fd.write(s)
|
||||
fd.close()
|
||||
|
||||
def readConfigFile(fname):
|
||||
#cwd = os.getcwd()
|
||||
global GLOBAL_PATH
|
||||
if GLOBAL_PATH is not None:
|
||||
fname2 = os.path.join(GLOBAL_PATH, fname)
|
||||
if os.path.exists(fname2):
|
||||
fname = fname2
|
||||
|
||||
GLOBAL_PATH = os.path.dirname(os.path.abspath(fname))
|
||||
|
||||
try:
|
||||
#os.chdir(newDir) ## bad.
|
||||
fd = open(fname)
|
||||
s = asUnicode(fd.read())
|
||||
fd.close()
|
||||
s = s.replace("\r\n", "\n")
|
||||
s = s.replace("\r", "\n")
|
||||
data = parseString(s)[1]
|
||||
except ParseError:
|
||||
sys.exc_info()[1].fileName = fname
|
||||
raise
|
||||
except:
|
||||
print("Error while reading config file %s:"% fname)
|
||||
raise
|
||||
#finally:
|
||||
#os.chdir(cwd)
|
||||
return data
|
||||
|
||||
def appendConfigFile(data, fname):
|
||||
s = genString(data)
|
||||
fd = open(fname, 'a')
|
||||
fd.write(s)
|
||||
fd.close()
|
||||
|
||||
|
||||
def genString(data, indent=''):
|
||||
s = ''
|
||||
for k in data:
|
||||
sk = str(k)
|
||||
if len(sk) == 0:
|
||||
print(data)
|
||||
raise Exception('blank dict keys not allowed (see data above)')
|
||||
if sk[0] == ' ' or ':' in sk:
|
||||
print(data)
|
||||
raise Exception('dict keys must not contain ":" or start with spaces [offending key is "%s"]' % sk)
|
||||
if isinstance(data[k], dict):
|
||||
s += indent + sk + ':\n'
|
||||
s += genString(data[k], indent + ' ')
|
||||
else:
|
||||
s += indent + sk + ': ' + repr(data[k]) + '\n'
|
||||
return s
|
||||
|
||||
def parseString(lines, start=0):
|
||||
|
||||
data = OrderedDict()
|
||||
if isinstance(lines, basestring):
|
||||
lines = lines.split('\n')
|
||||
lines = [l for l in lines if re.search(r'\S', l) and not re.match(r'\s*#', l)] ## remove empty lines
|
||||
|
||||
indent = measureIndent(lines[start])
|
||||
ln = start - 1
|
||||
|
||||
try:
|
||||
while True:
|
||||
ln += 1
|
||||
#print ln
|
||||
if ln >= len(lines):
|
||||
break
|
||||
|
||||
l = lines[ln]
|
||||
|
||||
## Skip blank lines or lines starting with #
|
||||
if re.match(r'\s*#', l) or not re.search(r'\S', l):
|
||||
continue
|
||||
|
||||
## Measure line indentation, make sure it is correct for this level
|
||||
lineInd = measureIndent(l)
|
||||
if lineInd < indent:
|
||||
ln -= 1
|
||||
break
|
||||
if lineInd > indent:
|
||||
#print lineInd, indent
|
||||
raise ParseError('Indentation is incorrect. Expected %d, got %d' % (indent, lineInd), ln+1, l)
|
||||
|
||||
|
||||
if ':' not in l:
|
||||
raise ParseError('Missing colon', ln+1, l)
|
||||
|
||||
(k, p, v) = l.partition(':')
|
||||
k = k.strip()
|
||||
v = v.strip()
|
||||
|
||||
## set up local variables to use for eval
|
||||
local = units.allUnits.copy()
|
||||
local['OrderedDict'] = OrderedDict
|
||||
local['readConfigFile'] = readConfigFile
|
||||
local['Point'] = Point
|
||||
local['QtCore'] = QtCore
|
||||
local['ColorMap'] = ColorMap
|
||||
# Needed for reconstructing numpy arrays
|
||||
local['array'] = numpy.array
|
||||
for dtype in ['int8', 'uint8',
|
||||
'int16', 'uint16', 'float16',
|
||||
'int32', 'uint32', 'float32',
|
||||
'int64', 'uint64', 'float64']:
|
||||
local[dtype] = getattr(numpy, dtype)
|
||||
|
||||
if len(k) < 1:
|
||||
raise ParseError('Missing name preceding colon', ln+1, l)
|
||||
if k[0] == '(' and k[-1] == ')': ## If the key looks like a tuple, try evaluating it.
|
||||
try:
|
||||
k1 = eval(k, local)
|
||||
if type(k1) is tuple:
|
||||
k = k1
|
||||
except:
|
||||
pass
|
||||
if re.search(r'\S', v) and v[0] != '#': ## eval the value
|
||||
try:
|
||||
val = eval(v, local)
|
||||
except:
|
||||
ex = sys.exc_info()[1]
|
||||
raise ParseError("Error evaluating expression '%s': [%s: %s]" % (v, ex.__class__.__name__, str(ex)), (ln+1), l)
|
||||
else:
|
||||
if ln+1 >= len(lines) or measureIndent(lines[ln+1]) <= indent:
|
||||
#print "blank dict"
|
||||
val = {}
|
||||
else:
|
||||
#print "Going deeper..", ln+1
|
||||
(ln, val) = parseString(lines, start=ln+1)
|
||||
data[k] = val
|
||||
#print k, repr(val)
|
||||
except ParseError:
|
||||
raise
|
||||
except:
|
||||
ex = sys.exc_info()[1]
|
||||
raise ParseError("%s: %s" % (ex.__class__.__name__, str(ex)), ln+1, l)
|
||||
#print "Returning shallower..", ln+1
|
||||
return (ln, data)
|
||||
|
||||
def measureIndent(s):
|
||||
n = 0
|
||||
while n < len(s) and s[n] == ' ':
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import tempfile
|
||||
fn = tempfile.mktemp()
|
||||
tf = open(fn, 'w')
|
||||
cf = """
|
||||
key: 'value'
|
||||
key2: ##comment
|
||||
##comment
|
||||
key21: 'value' ## comment
|
||||
##comment
|
||||
key22: [1,2,3]
|
||||
key23: 234 #comment
|
||||
"""
|
||||
tf.write(cf)
|
||||
tf.close()
|
||||
print("=== Test:===")
|
||||
num = 1
|
||||
for line in cf.split('\n'):
|
||||
print("%02d %s" % (num, line))
|
||||
num += 1
|
||||
print(cf)
|
||||
print("============")
|
||||
data = readConfigFile(fn)
|
||||
print(data)
|
||||
os.remove(fn)
|
62
pyqtgraph/console/CmdInput.py
Normal file
62
pyqtgraph/console/CmdInput.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
from ..Qt import QtCore, QtGui
|
||||
from ..python2_3 import asUnicode
|
||||
|
||||
class CmdInput(QtGui.QLineEdit):
|
||||
|
||||
sigExecuteCmd = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, parent):
|
||||
QtGui.QLineEdit.__init__(self, parent)
|
||||
self.history = [""]
|
||||
self.ptr = 0
|
||||
#self.lastCmd = None
|
||||
#self.setMultiline(False)
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
#print "press:", ev.key(), QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_Enter
|
||||
if ev.key() == QtCore.Qt.Key_Up and self.ptr < len(self.history) - 1:
|
||||
self.setHistory(self.ptr+1)
|
||||
ev.accept()
|
||||
return
|
||||
elif ev.key() == QtCore.Qt.Key_Down and self.ptr > 0:
|
||||
self.setHistory(self.ptr-1)
|
||||
ev.accept()
|
||||
return
|
||||
elif ev.key() == QtCore.Qt.Key_Return:
|
||||
self.execCmd()
|
||||
else:
|
||||
QtGui.QLineEdit.keyPressEvent(self, ev)
|
||||
self.history[0] = asUnicode(self.text())
|
||||
|
||||
def execCmd(self):
|
||||
cmd = asUnicode(self.text())
|
||||
if len(self.history) == 1 or cmd != self.history[1]:
|
||||
self.history.insert(1, cmd)
|
||||
#self.lastCmd = cmd
|
||||
self.history[0] = ""
|
||||
self.setHistory(0)
|
||||
self.sigExecuteCmd.emit(cmd)
|
||||
|
||||
def setHistory(self, num):
|
||||
self.ptr = num
|
||||
self.setText(self.history[self.ptr])
|
||||
|
||||
#def setMultiline(self, m):
|
||||
#height = QtGui.QFontMetrics(self.font()).lineSpacing()
|
||||
#if m:
|
||||
#self.setFixedHeight(height*5)
|
||||
#else:
|
||||
#self.setFixedHeight(height+15)
|
||||
#self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
#self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
|
||||
|
||||
#def sizeHint(self):
|
||||
#hint = QtGui.QPlainTextEdit.sizeHint(self)
|
||||
#height = QtGui.QFontMetrics(self.font()).lineSpacing()
|
||||
#hint.setHeight(height)
|
||||
#return hint
|
||||
|
||||
|
||||
|
||||
|
388
pyqtgraph/console/Console.py
Normal file
388
pyqtgraph/console/Console.py
Normal file
|
@ -0,0 +1,388 @@
|
|||
|
||||
from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYQT5
|
||||
import sys, re, os, time, traceback, subprocess
|
||||
if USE_PYSIDE:
|
||||
from . import template_pyside as template
|
||||
elif USE_PYQT5:
|
||||
from . import template_pyqt5 as template
|
||||
else:
|
||||
from . import template_pyqt as template
|
||||
|
||||
from .. import exceptionHandling as exceptionHandling
|
||||
import pickle
|
||||
from .. import getConfigOption
|
||||
|
||||
class ConsoleWidget(QtGui.QWidget):
|
||||
"""
|
||||
Widget displaying console output and accepting command input.
|
||||
Implements:
|
||||
|
||||
- eval python expressions / exec python statements
|
||||
- storable history of commands
|
||||
- exception handling allowing commands to be interpreted in the context of any level in the exception stack frame
|
||||
|
||||
Why not just use python in an interactive shell (or ipython) ? There are a few reasons:
|
||||
|
||||
- pyside does not yet allow Qt event processing and interactive shell at the same time
|
||||
- on some systems, typing in the console _blocks_ the qt event loop until the user presses enter. This can
|
||||
be baffling and frustrating to users since it would appear the program has frozen.
|
||||
- some terminals (eg windows cmd.exe) have notoriously unfriendly interfaces
|
||||
- ability to add extra features like exception stack introspection
|
||||
- ability to have multiple interactive prompts, including for spawned sub-processes
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None):
|
||||
"""
|
||||
============== ============================================================================
|
||||
**Arguments:**
|
||||
namespace dictionary containing the initial variables present in the default namespace
|
||||
historyFile optional file for storing command history
|
||||
text initial text to display in the console window
|
||||
editor optional string for invoking code editor (called when stack trace entries are
|
||||
double-clicked). May contain {fileName} and {lineNum} format keys. Example::
|
||||
|
||||
editorCommand --loadfile {fileName} --gotoline {lineNum}
|
||||
============== =============================================================================
|
||||
"""
|
||||
QtGui.QWidget.__init__(self, parent)
|
||||
if namespace is None:
|
||||
namespace = {}
|
||||
self.localNamespace = namespace
|
||||
self.editor = editor
|
||||
self.multiline = None
|
||||
self.inCmd = False
|
||||
|
||||
self.ui = template.Ui_Form()
|
||||
self.ui.setupUi(self)
|
||||
self.output = self.ui.output
|
||||
self.input = self.ui.input
|
||||
self.input.setFocus()
|
||||
|
||||
if text is not None:
|
||||
self.output.setPlainText(text)
|
||||
|
||||
self.historyFile = historyFile
|
||||
|
||||
history = self.loadHistory()
|
||||
if history is not None:
|
||||
self.input.history = [""] + history
|
||||
self.ui.historyList.addItems(history[::-1])
|
||||
self.ui.historyList.hide()
|
||||
self.ui.exceptionGroup.hide()
|
||||
|
||||
self.input.sigExecuteCmd.connect(self.runCmd)
|
||||
self.ui.historyBtn.toggled.connect(self.ui.historyList.setVisible)
|
||||
self.ui.historyList.itemClicked.connect(self.cmdSelected)
|
||||
self.ui.historyList.itemDoubleClicked.connect(self.cmdDblClicked)
|
||||
self.ui.exceptionBtn.toggled.connect(self.ui.exceptionGroup.setVisible)
|
||||
|
||||
self.ui.catchAllExceptionsBtn.toggled.connect(self.catchAllExceptions)
|
||||
self.ui.catchNextExceptionBtn.toggled.connect(self.catchNextException)
|
||||
self.ui.clearExceptionBtn.clicked.connect(self.clearExceptionClicked)
|
||||
self.ui.exceptionStackList.itemClicked.connect(self.stackItemClicked)
|
||||
self.ui.exceptionStackList.itemDoubleClicked.connect(self.stackItemDblClicked)
|
||||
self.ui.onlyUncaughtCheck.toggled.connect(self.updateSysTrace)
|
||||
|
||||
self.currentTraceback = None
|
||||
|
||||
def loadHistory(self):
|
||||
"""Return the list of previously-invoked command strings (or None)."""
|
||||
if self.historyFile is not None:
|
||||
return pickle.load(open(self.historyFile, 'rb'))
|
||||
|
||||
def saveHistory(self, history):
|
||||
"""Store the list of previously-invoked command strings."""
|
||||
if self.historyFile is not None:
|
||||
pickle.dump(open(self.historyFile, 'wb'), history)
|
||||
|
||||
def runCmd(self, cmd):
|
||||
#cmd = str(self.input.lastCmd)
|
||||
self.stdout = sys.stdout
|
||||
self.stderr = sys.stderr
|
||||
encCmd = re.sub(r'>', '>', re.sub(r'<', '<', cmd))
|
||||
encCmd = re.sub(r' ', ' ', encCmd)
|
||||
|
||||
self.ui.historyList.addItem(cmd)
|
||||
self.saveHistory(self.input.history[1:100])
|
||||
|
||||
try:
|
||||
sys.stdout = self
|
||||
sys.stderr = self
|
||||
if self.multiline is not None:
|
||||
self.write("<br><b>%s</b>\n"%encCmd, html=True)
|
||||
self.execMulti(cmd)
|
||||
else:
|
||||
self.write("<br><div style='background-color: #CCF'><b>%s</b>\n"%encCmd, html=True)
|
||||
self.inCmd = True
|
||||
self.execSingle(cmd)
|
||||
|
||||
if not self.inCmd:
|
||||
self.write("</div>\n", html=True)
|
||||
|
||||
finally:
|
||||
sys.stdout = self.stdout
|
||||
sys.stderr = self.stderr
|
||||
|
||||
sb = self.output.verticalScrollBar()
|
||||
sb.setValue(sb.maximum())
|
||||
sb = self.ui.historyList.verticalScrollBar()
|
||||
sb.setValue(sb.maximum())
|
||||
|
||||
def globals(self):
|
||||
frame = self.currentFrame()
|
||||
if frame is not None and self.ui.runSelectedFrameCheck.isChecked():
|
||||
return self.currentFrame().tb_frame.f_globals
|
||||
else:
|
||||
return globals()
|
||||
|
||||
def locals(self):
|
||||
frame = self.currentFrame()
|
||||
if frame is not None and self.ui.runSelectedFrameCheck.isChecked():
|
||||
return self.currentFrame().tb_frame.f_locals
|
||||
else:
|
||||
return self.localNamespace
|
||||
|
||||
def currentFrame(self):
|
||||
## Return the currently selected exception stack frame (or None if there is no exception)
|
||||
if self.currentTraceback is None:
|
||||
return None
|
||||
index = self.ui.exceptionStackList.currentRow()
|
||||
tb = self.currentTraceback
|
||||
for i in range(index):
|
||||
tb = tb.tb_next
|
||||
return tb
|
||||
|
||||
def execSingle(self, cmd):
|
||||
try:
|
||||
output = eval(cmd, self.globals(), self.locals())
|
||||
self.write(repr(output) + '\n')
|
||||
except SyntaxError:
|
||||
try:
|
||||
exec(cmd, self.globals(), self.locals())
|
||||
except SyntaxError as exc:
|
||||
if 'unexpected EOF' in exc.msg:
|
||||
self.multiline = cmd
|
||||
else:
|
||||
self.displayException()
|
||||
except:
|
||||
self.displayException()
|
||||
except:
|
||||
self.displayException()
|
||||
|
||||
|
||||
def execMulti(self, nextLine):
|
||||
#self.stdout.write(nextLine+"\n")
|
||||
if nextLine.strip() != '':
|
||||
self.multiline += "\n" + nextLine
|
||||
return
|
||||
else:
|
||||
cmd = self.multiline
|
||||
|
||||
try:
|
||||
output = eval(cmd, self.globals(), self.locals())
|
||||
self.write(str(output) + '\n')
|
||||
self.multiline = None
|
||||
except SyntaxError:
|
||||
try:
|
||||
exec(cmd, self.globals(), self.locals())
|
||||
self.multiline = None
|
||||
except SyntaxError as exc:
|
||||
if 'unexpected EOF' in exc.msg:
|
||||
self.multiline = cmd
|
||||
else:
|
||||
self.displayException()
|
||||
self.multiline = None
|
||||
except:
|
||||
self.displayException()
|
||||
self.multiline = None
|
||||
except:
|
||||
self.displayException()
|
||||
self.multiline = None
|
||||
|
||||
def write(self, strn, html=False):
|
||||
self.output.moveCursor(QtGui.QTextCursor.End)
|
||||
if html:
|
||||
self.output.textCursor().insertHtml(strn)
|
||||
else:
|
||||
if self.inCmd:
|
||||
self.inCmd = False
|
||||
self.output.textCursor().insertHtml("</div><br><div style='font-weight: normal; background-color: #FFF;'>")
|
||||
#self.stdout.write("</div><br><div style='font-weight: normal; background-color: #FFF;'>")
|
||||
self.output.insertPlainText(strn)
|
||||
#self.stdout.write(strn)
|
||||
|
||||
def displayException(self):
|
||||
"""
|
||||
Display the current exception and stack.
|
||||
"""
|
||||
tb = traceback.format_exc()
|
||||
lines = []
|
||||
indent = 4
|
||||
prefix = ''
|
||||
for l in tb.split('\n'):
|
||||
lines.append(" "*indent + prefix + l)
|
||||
self.write('\n'.join(lines))
|
||||
self.exceptionHandler(*sys.exc_info())
|
||||
|
||||
def cmdSelected(self, item):
|
||||
index = -(self.ui.historyList.row(item)+1)
|
||||
self.input.setHistory(index)
|
||||
self.input.setFocus()
|
||||
|
||||
def cmdDblClicked(self, item):
|
||||
index = -(self.ui.historyList.row(item)+1)
|
||||
self.input.setHistory(index)
|
||||
self.input.execCmd()
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
def catchAllExceptions(self, catch=True):
|
||||
"""
|
||||
If True, the console will catch all unhandled exceptions and display the stack
|
||||
trace. Each exception caught clears the last.
|
||||
"""
|
||||
self.ui.catchAllExceptionsBtn.setChecked(catch)
|
||||
if catch:
|
||||
self.ui.catchNextExceptionBtn.setChecked(False)
|
||||
self.enableExceptionHandling()
|
||||
self.ui.exceptionBtn.setChecked(True)
|
||||
else:
|
||||
self.disableExceptionHandling()
|
||||
|
||||
def catchNextException(self, catch=True):
|
||||
"""
|
||||
If True, the console will catch the next unhandled exception and display the stack
|
||||
trace.
|
||||
"""
|
||||
self.ui.catchNextExceptionBtn.setChecked(catch)
|
||||
if catch:
|
||||
self.ui.catchAllExceptionsBtn.setChecked(False)
|
||||
self.enableExceptionHandling()
|
||||
self.ui.exceptionBtn.setChecked(True)
|
||||
else:
|
||||
self.disableExceptionHandling()
|
||||
|
||||
def enableExceptionHandling(self):
|
||||
exceptionHandling.register(self.exceptionHandler)
|
||||
self.updateSysTrace()
|
||||
|
||||
def disableExceptionHandling(self):
|
||||
exceptionHandling.unregister(self.exceptionHandler)
|
||||
self.updateSysTrace()
|
||||
|
||||
def clearExceptionClicked(self):
|
||||
self.currentTraceback = None
|
||||
self.ui.exceptionInfoLabel.setText("[No current exception]")
|
||||
self.ui.exceptionStackList.clear()
|
||||
self.ui.clearExceptionBtn.setEnabled(False)
|
||||
|
||||
def stackItemClicked(self, item):
|
||||
pass
|
||||
|
||||
def stackItemDblClicked(self, item):
|
||||
editor = self.editor
|
||||
if editor is None:
|
||||
editor = getConfigOption('editorCommand')
|
||||
if editor is None:
|
||||
return
|
||||
tb = self.currentFrame()
|
||||
lineNum = tb.tb_lineno
|
||||
fileName = tb.tb_frame.f_code.co_filename
|
||||
subprocess.Popen(self.editor.format(fileName=fileName, lineNum=lineNum), shell=True)
|
||||
|
||||
|
||||
#def allExceptionsHandler(self, *args):
|
||||
#self.exceptionHandler(*args)
|
||||
|
||||
#def nextExceptionHandler(self, *args):
|
||||
#self.ui.catchNextExceptionBtn.setChecked(False)
|
||||
#self.exceptionHandler(*args)
|
||||
|
||||
def updateSysTrace(self):
|
||||
## Install or uninstall sys.settrace handler
|
||||
|
||||
if not self.ui.catchNextExceptionBtn.isChecked() and not self.ui.catchAllExceptionsBtn.isChecked():
|
||||
if sys.gettrace() == self.systrace:
|
||||
sys.settrace(None)
|
||||
return
|
||||
|
||||
if self.ui.onlyUncaughtCheck.isChecked():
|
||||
if sys.gettrace() == self.systrace:
|
||||
sys.settrace(None)
|
||||
else:
|
||||
if sys.gettrace() is not None and sys.gettrace() != self.systrace:
|
||||
self.ui.onlyUncaughtCheck.setChecked(False)
|
||||
raise Exception("sys.settrace is in use; cannot monitor for caught exceptions.")
|
||||
else:
|
||||
sys.settrace(self.systrace)
|
||||
|
||||
def exceptionHandler(self, excType, exc, tb):
|
||||
if self.ui.catchNextExceptionBtn.isChecked():
|
||||
self.ui.catchNextExceptionBtn.setChecked(False)
|
||||
elif not self.ui.catchAllExceptionsBtn.isChecked():
|
||||
return
|
||||
|
||||
self.ui.clearExceptionBtn.setEnabled(True)
|
||||
self.currentTraceback = tb
|
||||
|
||||
excMessage = ''.join(traceback.format_exception_only(excType, exc))
|
||||
self.ui.exceptionInfoLabel.setText(excMessage)
|
||||
self.ui.exceptionStackList.clear()
|
||||
for index, line in enumerate(traceback.extract_tb(tb)):
|
||||
self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line)
|
||||
|
||||
def systrace(self, frame, event, arg):
|
||||
if event == 'exception' and self.checkException(*arg):
|
||||
self.exceptionHandler(*arg)
|
||||
return self.systrace
|
||||
|
||||
def checkException(self, excType, exc, tb):
|
||||
## Return True if the exception is interesting; False if it should be ignored.
|
||||
|
||||
filename = tb.tb_frame.f_code.co_filename
|
||||
function = tb.tb_frame.f_code.co_name
|
||||
|
||||
filterStr = str(self.ui.filterText.text())
|
||||
if filterStr != '':
|
||||
if isinstance(exc, Exception):
|
||||
msg = exc.message
|
||||
elif isinstance(exc, basestring):
|
||||
msg = exc
|
||||
else:
|
||||
msg = repr(exc)
|
||||
match = re.search(filterStr, "%s:%s:%s" % (filename, function, msg))
|
||||
return match is not None
|
||||
|
||||
## Go through a list of common exception points we like to ignore:
|
||||
if excType is GeneratorExit or excType is StopIteration:
|
||||
return False
|
||||
if excType is KeyError:
|
||||
if filename.endswith('python2.7/weakref.py') and function in ('__contains__', 'get'):
|
||||
return False
|
||||
if filename.endswith('python2.7/copy.py') and function == '_keep_alive':
|
||||
return False
|
||||
if excType is AttributeError:
|
||||
if filename.endswith('python2.7/collections.py') and function == '__init__':
|
||||
return False
|
||||
if filename.endswith('numpy/core/fromnumeric.py') and function in ('all', '_wrapit', 'transpose', 'sum'):
|
||||
return False
|
||||
if filename.endswith('numpy/core/arrayprint.py') and function in ('_array2string'):
|
||||
return False
|
||||
if filename.endswith('MetaArray.py') and function == '__getattr__':
|
||||
for name in ('__array_interface__', '__array_struct__', '__array__'): ## numpy looks for these when converting objects to array
|
||||
if name in exc:
|
||||
return False
|
||||
if filename.endswith('flowchart/eq.py'):
|
||||
return False
|
||||
if filename.endswith('pyqtgraph/functions.py') and function == 'makeQImage':
|
||||
return False
|
||||
if excType is TypeError:
|
||||
if filename.endswith('numpy/lib/function_base.py') and function == 'iterable':
|
||||
return False
|
||||
if excType is ZeroDivisionError:
|
||||
if filename.endswith('python2.7/traceback.py'):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
1
pyqtgraph/console/__init__.py
Normal file
1
pyqtgraph/console/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .Console import ConsoleWidget
|
194
pyqtgraph/console/template.ui
Normal file
194
pyqtgraph/console/template.ui
Normal file
|
@ -0,0 +1,194 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>694</width>
|
||||
<height>497</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Console</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="output">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Monospace</family>
|
||||
</font>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="CmdInput" name="input"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="historyBtn">
|
||||
<property name="text">
|
||||
<string>History..</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="exceptionBtn">
|
||||
<property name="text">
|
||||
<string>Exceptions..</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QListWidget" name="historyList">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Monospace</family>
|
||||
</font>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="exceptionGroup">
|
||||
<property name="title">
|
||||
<string>Exception Handling</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="6">
|
||||
<widget class="QPushButton" name="clearExceptionBtn">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Clear Exception</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="catchAllExceptionsBtn">
|
||||
<property name="text">
|
||||
<string>Show All Exceptions</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QPushButton" name="catchNextExceptionBtn">
|
||||
<property name="text">
|
||||
<string>Show Next Exception</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QCheckBox" name="onlyUncaughtCheck">
|
||||
<property name="text">
|
||||
<string>Only Uncaught Exceptions</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="7">
|
||||
<widget class="QListWidget" name="exceptionStackList">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="7">
|
||||
<widget class="QCheckBox" name="runSelectedFrameCheck">
|
||||
<property name="text">
|
||||
<string>Run commands in selected stack frame</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="7">
|
||||
<widget class="QLabel" name="exceptionInfoLabel">
|
||||
<property name="text">
|
||||
<string>Exception Info</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="5">
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Filter (regex):</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QLineEdit" name="filterText"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>CmdInput</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>.CmdInput</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
127
pyqtgraph/console/template_pyqt.py
Normal file
127
pyqtgraph/console/template_pyqt.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file 'template.ui'
|
||||
#
|
||||
# Created: Fri May 02 18:55:28 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
try:
|
||||
_fromUtf8 = QtCore.QString.fromUtf8
|
||||
except AttributeError:
|
||||
def _fromUtf8(s):
|
||||
return s
|
||||
|
||||
try:
|
||||
_encoding = QtGui.QApplication.UnicodeUTF8
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig, _encoding)
|
||||
except AttributeError:
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig)
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName(_fromUtf8("Form"))
|
||||
Form.resize(694, 497)
|
||||
self.gridLayout = QtGui.QGridLayout(Form)
|
||||
self.gridLayout.setMargin(0)
|
||||
self.gridLayout.setSpacing(0)
|
||||
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
|
||||
self.splitter = QtGui.QSplitter(Form)
|
||||
self.splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
self.splitter.setObjectName(_fromUtf8("splitter"))
|
||||
self.layoutWidget = QtGui.QWidget(self.splitter)
|
||||
self.layoutWidget.setObjectName(_fromUtf8("layoutWidget"))
|
||||
self.verticalLayout = QtGui.QVBoxLayout(self.layoutWidget)
|
||||
self.verticalLayout.setMargin(0)
|
||||
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
|
||||
self.output = QtGui.QPlainTextEdit(self.layoutWidget)
|
||||
font = QtGui.QFont()
|
||||
font.setFamily(_fromUtf8("Monospace"))
|
||||
self.output.setFont(font)
|
||||
self.output.setReadOnly(True)
|
||||
self.output.setObjectName(_fromUtf8("output"))
|
||||
self.verticalLayout.addWidget(self.output)
|
||||
self.horizontalLayout = QtGui.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
|
||||
self.input = CmdInput(self.layoutWidget)
|
||||
self.input.setObjectName(_fromUtf8("input"))
|
||||
self.horizontalLayout.addWidget(self.input)
|
||||
self.historyBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.historyBtn.setCheckable(True)
|
||||
self.historyBtn.setObjectName(_fromUtf8("historyBtn"))
|
||||
self.horizontalLayout.addWidget(self.historyBtn)
|
||||
self.exceptionBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.exceptionBtn.setCheckable(True)
|
||||
self.exceptionBtn.setObjectName(_fromUtf8("exceptionBtn"))
|
||||
self.horizontalLayout.addWidget(self.exceptionBtn)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||
self.historyList = QtGui.QListWidget(self.splitter)
|
||||
font = QtGui.QFont()
|
||||
font.setFamily(_fromUtf8("Monospace"))
|
||||
self.historyList.setFont(font)
|
||||
self.historyList.setObjectName(_fromUtf8("historyList"))
|
||||
self.exceptionGroup = QtGui.QGroupBox(self.splitter)
|
||||
self.exceptionGroup.setObjectName(_fromUtf8("exceptionGroup"))
|
||||
self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup)
|
||||
self.gridLayout_2.setSpacing(0)
|
||||
self.gridLayout_2.setContentsMargins(-1, 0, -1, 0)
|
||||
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
|
||||
self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
|
||||
self.clearExceptionBtn.setEnabled(False)
|
||||
self.clearExceptionBtn.setObjectName(_fromUtf8("clearExceptionBtn"))
|
||||
self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1)
|
||||
self.catchAllExceptionsBtn = QtGui.QPushButton(self.exceptionGroup)
|
||||
self.catchAllExceptionsBtn.setCheckable(True)
|
||||
self.catchAllExceptionsBtn.setObjectName(_fromUtf8("catchAllExceptionsBtn"))
|
||||
self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1)
|
||||
self.catchNextExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
|
||||
self.catchNextExceptionBtn.setCheckable(True)
|
||||
self.catchNextExceptionBtn.setObjectName(_fromUtf8("catchNextExceptionBtn"))
|
||||
self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1)
|
||||
self.onlyUncaughtCheck = QtGui.QCheckBox(self.exceptionGroup)
|
||||
self.onlyUncaughtCheck.setChecked(True)
|
||||
self.onlyUncaughtCheck.setObjectName(_fromUtf8("onlyUncaughtCheck"))
|
||||
self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1)
|
||||
self.exceptionStackList = QtGui.QListWidget(self.exceptionGroup)
|
||||
self.exceptionStackList.setAlternatingRowColors(True)
|
||||
self.exceptionStackList.setObjectName(_fromUtf8("exceptionStackList"))
|
||||
self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7)
|
||||
self.runSelectedFrameCheck = QtGui.QCheckBox(self.exceptionGroup)
|
||||
self.runSelectedFrameCheck.setChecked(True)
|
||||
self.runSelectedFrameCheck.setObjectName(_fromUtf8("runSelectedFrameCheck"))
|
||||
self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7)
|
||||
self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup)
|
||||
self.exceptionInfoLabel.setObjectName(_fromUtf8("exceptionInfoLabel"))
|
||||
self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7)
|
||||
spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1)
|
||||
self.label = QtGui.QLabel(self.exceptionGroup)
|
||||
self.label.setObjectName(_fromUtf8("label"))
|
||||
self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1)
|
||||
self.filterText = QtGui.QLineEdit(self.exceptionGroup)
|
||||
self.filterText.setObjectName(_fromUtf8("filterText"))
|
||||
self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1)
|
||||
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(_translate("Form", "Console", None))
|
||||
self.historyBtn.setText(_translate("Form", "History..", None))
|
||||
self.exceptionBtn.setText(_translate("Form", "Exceptions..", None))
|
||||
self.exceptionGroup.setTitle(_translate("Form", "Exception Handling", None))
|
||||
self.clearExceptionBtn.setText(_translate("Form", "Clear Exception", None))
|
||||
self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions", None))
|
||||
self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception", None))
|
||||
self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions", None))
|
||||
self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame", None))
|
||||
self.exceptionInfoLabel.setText(_translate("Form", "Exception Info", None))
|
||||
self.label.setText(_translate("Form", "Filter (regex):", None))
|
||||
|
||||
from .CmdInput import CmdInput
|
107
pyqtgraph/console/template_pyqt5.py
Normal file
107
pyqtgraph/console/template_pyqt5.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/console/template.ui'
|
||||
#
|
||||
# Created: Wed Mar 26 15:09:29 2014
|
||||
# by: PyQt5 UI code generator 5.0.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName("Form")
|
||||
Form.resize(710, 497)
|
||||
self.gridLayout = QtWidgets.QGridLayout(Form)
|
||||
self.gridLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout.setSpacing(0)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.splitter = QtWidgets.QSplitter(Form)
|
||||
self.splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
self.splitter.setObjectName("splitter")
|
||||
self.layoutWidget = QtWidgets.QWidget(self.splitter)
|
||||
self.layoutWidget.setObjectName("layoutWidget")
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget)
|
||||
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.output = QtWidgets.QPlainTextEdit(self.layoutWidget)
|
||||
font = QtGui.QFont()
|
||||
font.setFamily("Monospace")
|
||||
self.output.setFont(font)
|
||||
self.output.setReadOnly(True)
|
||||
self.output.setObjectName("output")
|
||||
self.verticalLayout.addWidget(self.output)
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.input = CmdInput(self.layoutWidget)
|
||||
self.input.setObjectName("input")
|
||||
self.horizontalLayout.addWidget(self.input)
|
||||
self.historyBtn = QtWidgets.QPushButton(self.layoutWidget)
|
||||
self.historyBtn.setCheckable(True)
|
||||
self.historyBtn.setObjectName("historyBtn")
|
||||
self.horizontalLayout.addWidget(self.historyBtn)
|
||||
self.exceptionBtn = QtWidgets.QPushButton(self.layoutWidget)
|
||||
self.exceptionBtn.setCheckable(True)
|
||||
self.exceptionBtn.setObjectName("exceptionBtn")
|
||||
self.horizontalLayout.addWidget(self.exceptionBtn)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||
self.historyList = QtWidgets.QListWidget(self.splitter)
|
||||
font = QtGui.QFont()
|
||||
font.setFamily("Monospace")
|
||||
self.historyList.setFont(font)
|
||||
self.historyList.setObjectName("historyList")
|
||||
self.exceptionGroup = QtWidgets.QGroupBox(self.splitter)
|
||||
self.exceptionGroup.setObjectName("exceptionGroup")
|
||||
self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup)
|
||||
self.gridLayout_2.setSpacing(0)
|
||||
self.gridLayout_2.setContentsMargins(-1, 0, -1, 0)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup)
|
||||
self.catchAllExceptionsBtn.setCheckable(True)
|
||||
self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn")
|
||||
self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1)
|
||||
self.catchNextExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup)
|
||||
self.catchNextExceptionBtn.setCheckable(True)
|
||||
self.catchNextExceptionBtn.setObjectName("catchNextExceptionBtn")
|
||||
self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1)
|
||||
self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup)
|
||||
self.onlyUncaughtCheck.setChecked(True)
|
||||
self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck")
|
||||
self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1)
|
||||
self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup)
|
||||
self.exceptionStackList.setAlternatingRowColors(True)
|
||||
self.exceptionStackList.setObjectName("exceptionStackList")
|
||||
self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5)
|
||||
self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup)
|
||||
self.runSelectedFrameCheck.setChecked(True)
|
||||
self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck")
|
||||
self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5)
|
||||
self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup)
|
||||
self.exceptionInfoLabel.setObjectName("exceptionInfoLabel")
|
||||
self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5)
|
||||
self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup)
|
||||
self.clearExceptionBtn.setEnabled(False)
|
||||
self.clearExceptionBtn.setObjectName("clearExceptionBtn")
|
||||
self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1)
|
||||
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1)
|
||||
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
Form.setWindowTitle(_translate("Form", "Console"))
|
||||
self.historyBtn.setText(_translate("Form", "History.."))
|
||||
self.exceptionBtn.setText(_translate("Form", "Exceptions.."))
|
||||
self.exceptionGroup.setTitle(_translate("Form", "Exception Handling"))
|
||||
self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions"))
|
||||
self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception"))
|
||||
self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions"))
|
||||
self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame"))
|
||||
self.exceptionInfoLabel.setText(_translate("Form", "Exception Info"))
|
||||
self.clearExceptionBtn.setText(_translate("Form", "Clear Exception"))
|
||||
|
||||
from .CmdInput import CmdInput
|
106
pyqtgraph/console/template_pyside.py
Normal file
106
pyqtgraph/console/template_pyside.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/console/template.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:53 2013
|
||||
# by: pyside-uic 0.2.14 running on PySide 1.1.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName("Form")
|
||||
Form.resize(710, 497)
|
||||
self.gridLayout = QtGui.QGridLayout(Form)
|
||||
self.gridLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout.setSpacing(0)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.splitter = QtGui.QSplitter(Form)
|
||||
self.splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
self.splitter.setObjectName("splitter")
|
||||
self.layoutWidget = QtGui.QWidget(self.splitter)
|
||||
self.layoutWidget.setObjectName("layoutWidget")
|
||||
self.verticalLayout = QtGui.QVBoxLayout(self.layoutWidget)
|
||||
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.output = QtGui.QPlainTextEdit(self.layoutWidget)
|
||||
font = QtGui.QFont()
|
||||
font.setFamily("Monospace")
|
||||
self.output.setFont(font)
|
||||
self.output.setReadOnly(True)
|
||||
self.output.setObjectName("output")
|
||||
self.verticalLayout.addWidget(self.output)
|
||||
self.horizontalLayout = QtGui.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.input = CmdInput(self.layoutWidget)
|
||||
self.input.setObjectName("input")
|
||||
self.horizontalLayout.addWidget(self.input)
|
||||
self.historyBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.historyBtn.setCheckable(True)
|
||||
self.historyBtn.setObjectName("historyBtn")
|
||||
self.horizontalLayout.addWidget(self.historyBtn)
|
||||
self.exceptionBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.exceptionBtn.setCheckable(True)
|
||||
self.exceptionBtn.setObjectName("exceptionBtn")
|
||||
self.horizontalLayout.addWidget(self.exceptionBtn)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||
self.historyList = QtGui.QListWidget(self.splitter)
|
||||
font = QtGui.QFont()
|
||||
font.setFamily("Monospace")
|
||||
self.historyList.setFont(font)
|
||||
self.historyList.setObjectName("historyList")
|
||||
self.exceptionGroup = QtGui.QGroupBox(self.splitter)
|
||||
self.exceptionGroup.setObjectName("exceptionGroup")
|
||||
self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup)
|
||||
self.gridLayout_2.setSpacing(0)
|
||||
self.gridLayout_2.setContentsMargins(-1, 0, -1, 0)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
self.catchAllExceptionsBtn = QtGui.QPushButton(self.exceptionGroup)
|
||||
self.catchAllExceptionsBtn.setCheckable(True)
|
||||
self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn")
|
||||
self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1)
|
||||
self.catchNextExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
|
||||
self.catchNextExceptionBtn.setCheckable(True)
|
||||
self.catchNextExceptionBtn.setObjectName("catchNextExceptionBtn")
|
||||
self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1)
|
||||
self.onlyUncaughtCheck = QtGui.QCheckBox(self.exceptionGroup)
|
||||
self.onlyUncaughtCheck.setChecked(True)
|
||||
self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck")
|
||||
self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1)
|
||||
self.exceptionStackList = QtGui.QListWidget(self.exceptionGroup)
|
||||
self.exceptionStackList.setAlternatingRowColors(True)
|
||||
self.exceptionStackList.setObjectName("exceptionStackList")
|
||||
self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5)
|
||||
self.runSelectedFrameCheck = QtGui.QCheckBox(self.exceptionGroup)
|
||||
self.runSelectedFrameCheck.setChecked(True)
|
||||
self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck")
|
||||
self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5)
|
||||
self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup)
|
||||
self.exceptionInfoLabel.setObjectName("exceptionInfoLabel")
|
||||
self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5)
|
||||
self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
|
||||
self.clearExceptionBtn.setEnabled(False)
|
||||
self.clearExceptionBtn.setObjectName("clearExceptionBtn")
|
||||
self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1)
|
||||
spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1)
|
||||
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Console", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.exceptionGroup.setTitle(QtGui.QApplication.translate("Form", "Exception Handling", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.catchAllExceptionsBtn.setText(QtGui.QApplication.translate("Form", "Show All Exceptions", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.catchNextExceptionBtn.setText(QtGui.QApplication.translate("Form", "Show Next Exception", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.onlyUncaughtCheck.setText(QtGui.QApplication.translate("Form", "Only Uncaught Exceptions", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Exception Info", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Exception", None, QtGui.QApplication.UnicodeUTF8))
|
||||
|
||||
from .CmdInput import CmdInput
|
1169
pyqtgraph/debug.py
Normal file
1169
pyqtgraph/debug.py
Normal file
File diff suppressed because it is too large
Load diff
277
pyqtgraph/dockarea/Container.py
Normal file
277
pyqtgraph/dockarea/Container.py
Normal file
|
@ -0,0 +1,277 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ..Qt import QtCore, QtGui
|
||||
import weakref
|
||||
|
||||
class Container(object):
|
||||
#sigStretchChanged = QtCore.Signal() ## can't do this here; not a QObject.
|
||||
|
||||
def __init__(self, area):
|
||||
object.__init__(self)
|
||||
self.area = area
|
||||
self._container = None
|
||||
self._stretch = (10, 10)
|
||||
self.stretches = weakref.WeakKeyDictionary()
|
||||
|
||||
def container(self):
|
||||
return self._container
|
||||
|
||||
def containerChanged(self, c):
|
||||
self._container = c
|
||||
|
||||
def type(self):
|
||||
return None
|
||||
|
||||
def insert(self, new, pos=None, neighbor=None):
|
||||
# remove from existing parent first
|
||||
new.setParent(None)
|
||||
|
||||
if not isinstance(new, list):
|
||||
new = [new]
|
||||
if neighbor is None:
|
||||
if pos == 'before':
|
||||
index = 0
|
||||
else:
|
||||
index = self.count()
|
||||
else:
|
||||
index = self.indexOf(neighbor)
|
||||
if index == -1:
|
||||
index = 0
|
||||
if pos == 'after':
|
||||
index += 1
|
||||
|
||||
for n in new:
|
||||
#print "change container", n, " -> ", self
|
||||
n.containerChanged(self)
|
||||
#print "insert", n, " -> ", self, index
|
||||
self._insertItem(n, index)
|
||||
index += 1
|
||||
n.sigStretchChanged.connect(self.childStretchChanged)
|
||||
#print "child added", self
|
||||
self.updateStretch()
|
||||
|
||||
def apoptose(self, propagate=True):
|
||||
##if there is only one (or zero) item in this container, disappear.
|
||||
cont = self._container
|
||||
c = self.count()
|
||||
if c > 1:
|
||||
return
|
||||
if self.count() == 1: ## if there is one item, give it to the parent container (unless this is the top)
|
||||
if self is self.area.topContainer:
|
||||
return
|
||||
self.container().insert(self.widget(0), 'before', self)
|
||||
#print "apoptose:", self
|
||||
self.close()
|
||||
if propagate and cont is not None:
|
||||
cont.apoptose()
|
||||
|
||||
def close(self):
|
||||
self.area = None
|
||||
self._container = None
|
||||
self.setParent(None)
|
||||
|
||||
def childEvent(self, ev):
|
||||
ch = ev.child()
|
||||
if ev.removed() and hasattr(ch, 'sigStretchChanged'):
|
||||
#print "Child", ev.child(), "removed, updating", self
|
||||
try:
|
||||
ch.sigStretchChanged.disconnect(self.childStretchChanged)
|
||||
except:
|
||||
pass
|
||||
self.updateStretch()
|
||||
|
||||
def childStretchChanged(self):
|
||||
#print "child", QtCore.QObject.sender(self), "changed shape, updating", self
|
||||
self.updateStretch()
|
||||
|
||||
def setStretch(self, x=None, y=None):
|
||||
#print "setStretch", self, x, y
|
||||
self._stretch = (x, y)
|
||||
self.sigStretchChanged.emit()
|
||||
|
||||
def updateStretch(self):
|
||||
###Set the stretch values for this container to reflect its contents
|
||||
pass
|
||||
|
||||
|
||||
def stretch(self):
|
||||
"""Return the stretch factors for this container"""
|
||||
return self._stretch
|
||||
|
||||
|
||||
class SplitContainer(Container, QtGui.QSplitter):
|
||||
"""Horizontal or vertical splitter with some changes:
|
||||
- save/restore works correctly
|
||||
"""
|
||||
sigStretchChanged = QtCore.Signal()
|
||||
|
||||
def __init__(self, area, orientation):
|
||||
QtGui.QSplitter.__init__(self)
|
||||
self.setOrientation(orientation)
|
||||
Container.__init__(self, area)
|
||||
#self.splitterMoved.connect(self.restretchChildren)
|
||||
|
||||
def _insertItem(self, item, index):
|
||||
self.insertWidget(index, item)
|
||||
item.show() ## need to show since it may have been previously hidden by tab
|
||||
|
||||
def saveState(self):
|
||||
sizes = self.sizes()
|
||||
if all([x == 0 for x in sizes]):
|
||||
sizes = [10] * len(sizes)
|
||||
return {'sizes': sizes}
|
||||
|
||||
def restoreState(self, state):
|
||||
sizes = state['sizes']
|
||||
self.setSizes(sizes)
|
||||
for i in range(len(sizes)):
|
||||
self.setStretchFactor(i, sizes[i])
|
||||
|
||||
def childEvent(self, ev):
|
||||
QtGui.QSplitter.childEvent(self, ev)
|
||||
Container.childEvent(self, ev)
|
||||
|
||||
#def restretchChildren(self):
|
||||
#sizes = self.sizes()
|
||||
#tot = sum(sizes)
|
||||
|
||||
|
||||
|
||||
|
||||
class HContainer(SplitContainer):
|
||||
def __init__(self, area):
|
||||
SplitContainer.__init__(self, area, QtCore.Qt.Horizontal)
|
||||
|
||||
def type(self):
|
||||
return 'horizontal'
|
||||
|
||||
def updateStretch(self):
|
||||
##Set the stretch values for this container to reflect its contents
|
||||
#print "updateStretch", self
|
||||
x = 0
|
||||
y = 0
|
||||
sizes = []
|
||||
for i in range(self.count()):
|
||||
wx, wy = self.widget(i).stretch()
|
||||
x += wx
|
||||
y = max(y, wy)
|
||||
sizes.append(wx)
|
||||
#print " child", self.widget(i), wx, wy
|
||||
self.setStretch(x, y)
|
||||
#print sizes
|
||||
|
||||
tot = float(sum(sizes))
|
||||
if tot == 0:
|
||||
scale = 1.0
|
||||
else:
|
||||
scale = self.width() / tot
|
||||
self.setSizes([int(s*scale) for s in sizes])
|
||||
|
||||
|
||||
|
||||
class VContainer(SplitContainer):
|
||||
def __init__(self, area):
|
||||
SplitContainer.__init__(self, area, QtCore.Qt.Vertical)
|
||||
|
||||
def type(self):
|
||||
return 'vertical'
|
||||
|
||||
def updateStretch(self):
|
||||
##Set the stretch values for this container to reflect its contents
|
||||
#print "updateStretch", self
|
||||
x = 0
|
||||
y = 0
|
||||
sizes = []
|
||||
for i in range(self.count()):
|
||||
wx, wy = self.widget(i).stretch()
|
||||
y += wy
|
||||
x = max(x, wx)
|
||||
sizes.append(wy)
|
||||
#print " child", self.widget(i), wx, wy
|
||||
self.setStretch(x, y)
|
||||
|
||||
#print sizes
|
||||
tot = float(sum(sizes))
|
||||
if tot == 0:
|
||||
scale = 1.0
|
||||
else:
|
||||
scale = self.height() / tot
|
||||
self.setSizes([int(s*scale) for s in sizes])
|
||||
|
||||
|
||||
class TContainer(Container, QtGui.QWidget):
|
||||
sigStretchChanged = QtCore.Signal()
|
||||
def __init__(self, area):
|
||||
QtGui.QWidget.__init__(self)
|
||||
Container.__init__(self, area)
|
||||
self.layout = QtGui.QGridLayout()
|
||||
self.layout.setSpacing(0)
|
||||
self.layout.setContentsMargins(0,0,0,0)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.hTabLayout = QtGui.QHBoxLayout()
|
||||
self.hTabBox = QtGui.QWidget()
|
||||
self.hTabBox.setLayout(self.hTabLayout)
|
||||
self.hTabLayout.setSpacing(2)
|
||||
self.hTabLayout.setContentsMargins(0,0,0,0)
|
||||
self.layout.addWidget(self.hTabBox, 0, 1)
|
||||
|
||||
self.stack = QtGui.QStackedWidget()
|
||||
self.layout.addWidget(self.stack, 1, 1)
|
||||
self.stack.childEvent = self.stackChildEvent
|
||||
|
||||
|
||||
self.setLayout(self.layout)
|
||||
for n in ['count', 'widget', 'indexOf']:
|
||||
setattr(self, n, getattr(self.stack, n))
|
||||
|
||||
|
||||
def _insertItem(self, item, index):
|
||||
if not isinstance(item, Dock.Dock):
|
||||
raise Exception("Tab containers may hold only docks, not other containers.")
|
||||
self.stack.insertWidget(index, item)
|
||||
self.hTabLayout.insertWidget(index, item.label)
|
||||
#QtCore.QObject.connect(item.label, QtCore.SIGNAL('clicked'), self.tabClicked)
|
||||
item.label.sigClicked.connect(self.tabClicked)
|
||||
self.tabClicked(item.label)
|
||||
|
||||
def tabClicked(self, tab, ev=None):
|
||||
if ev is None or ev.button() == QtCore.Qt.LeftButton:
|
||||
for i in range(self.count()):
|
||||
w = self.widget(i)
|
||||
if w is tab.dock:
|
||||
w.label.setDim(False)
|
||||
self.stack.setCurrentIndex(i)
|
||||
else:
|
||||
w.label.setDim(True)
|
||||
|
||||
def raiseDock(self, dock):
|
||||
"""Move *dock* to the top of the stack"""
|
||||
self.stack.currentWidget().label.setDim(True)
|
||||
self.stack.setCurrentWidget(dock)
|
||||
dock.label.setDim(False)
|
||||
|
||||
|
||||
def type(self):
|
||||
return 'tab'
|
||||
|
||||
def saveState(self):
|
||||
return {'index': self.stack.currentIndex()}
|
||||
|
||||
def restoreState(self, state):
|
||||
self.stack.setCurrentIndex(state['index'])
|
||||
|
||||
def updateStretch(self):
|
||||
##Set the stretch values for this container to reflect its contents
|
||||
x = 0
|
||||
y = 0
|
||||
for i in range(self.count()):
|
||||
wx, wy = self.widget(i).stretch()
|
||||
x = max(x, wx)
|
||||
y = max(y, wy)
|
||||
self.setStretch(x, y)
|
||||
|
||||
def stackChildEvent(self, ev):
|
||||
QtGui.QStackedWidget.childEvent(self.stack, ev)
|
||||
Container.childEvent(self, ev)
|
||||
|
||||
from . import Dock
|
361
pyqtgraph/dockarea/Dock.py
Normal file
361
pyqtgraph/dockarea/Dock.py
Normal file
|
@ -0,0 +1,361 @@
|
|||
from ..Qt import QtCore, QtGui
|
||||
|
||||
from .DockDrop import *
|
||||
from ..widgets.VerticalLabel import VerticalLabel
|
||||
from ..python2_3 import asUnicode
|
||||
|
||||
class Dock(QtGui.QWidget, DockDrop):
|
||||
|
||||
sigStretchChanged = QtCore.Signal()
|
||||
sigClosed = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closable=False):
|
||||
QtGui.QWidget.__init__(self)
|
||||
DockDrop.__init__(self)
|
||||
self._container = None
|
||||
self._name = name
|
||||
self.area = area
|
||||
self.label = DockLabel(name, self, closable)
|
||||
if closable:
|
||||
self.label.sigCloseClicked.connect(self.close)
|
||||
self.labelHidden = False
|
||||
self.moveLabel = True ## If false, the dock is no longer allowed to move the label.
|
||||
self.autoOrient = autoOrientation
|
||||
self.orientation = 'horizontal'
|
||||
#self.label.setAlignment(QtCore.Qt.AlignHCenter)
|
||||
self.topLayout = QtGui.QGridLayout()
|
||||
self.topLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.topLayout.setSpacing(0)
|
||||
self.setLayout(self.topLayout)
|
||||
self.topLayout.addWidget(self.label, 0, 1)
|
||||
self.widgetArea = QtGui.QWidget()
|
||||
self.topLayout.addWidget(self.widgetArea, 1, 1)
|
||||
self.layout = QtGui.QGridLayout()
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.setSpacing(0)
|
||||
self.widgetArea.setLayout(self.layout)
|
||||
self.widgetArea.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
|
||||
self.widgets = []
|
||||
self.currentRow = 0
|
||||
#self.titlePos = 'top'
|
||||
self.raiseOverlay()
|
||||
self.hStyle = """
|
||||
Dock > QWidget {
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
border-top-width: 0px;
|
||||
}"""
|
||||
self.vStyle = """
|
||||
Dock > QWidget {
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-left-width: 0px;
|
||||
}"""
|
||||
self.nStyle = """
|
||||
Dock > QWidget {
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
}"""
|
||||
self.dragStyle = """
|
||||
Dock > QWidget {
|
||||
border: 4px solid #00F;
|
||||
border-radius: 5px;
|
||||
}"""
|
||||
self.setAutoFillBackground(False)
|
||||
self.widgetArea.setStyleSheet(self.hStyle)
|
||||
|
||||
self.setStretch(*size)
|
||||
|
||||
if widget is not None:
|
||||
self.addWidget(widget)
|
||||
|
||||
if hideTitle:
|
||||
self.hideTitleBar()
|
||||
|
||||
def implements(self, name=None):
|
||||
if name is None:
|
||||
return ['dock']
|
||||
else:
|
||||
return name == 'dock'
|
||||
|
||||
def setStretch(self, x=None, y=None):
|
||||
"""
|
||||
Set the 'target' size for this Dock.
|
||||
The actual size will be determined by comparing this Dock's
|
||||
stretch value to the rest of the docks it shares space with.
|
||||
"""
|
||||
#print "setStretch", self, x, y
|
||||
#self._stretch = (x, y)
|
||||
if x is None:
|
||||
x = 0
|
||||
if y is None:
|
||||
y = 0
|
||||
#policy = self.sizePolicy()
|
||||
#policy.setHorizontalStretch(x)
|
||||
#policy.setVerticalStretch(y)
|
||||
#self.setSizePolicy(policy)
|
||||
self._stretch = (x, y)
|
||||
self.sigStretchChanged.emit()
|
||||
#print "setStretch", self, x, y, self.stretch()
|
||||
|
||||
def stretch(self):
|
||||
#policy = self.sizePolicy()
|
||||
#return policy.horizontalStretch(), policy.verticalStretch()
|
||||
return self._stretch
|
||||
|
||||
#def stretch(self):
|
||||
#return self._stretch
|
||||
|
||||
def hideTitleBar(self):
|
||||
"""
|
||||
Hide the title bar for this Dock.
|
||||
This will prevent the Dock being moved by the user.
|
||||
"""
|
||||
self.label.hide()
|
||||
self.labelHidden = True
|
||||
if 'center' in self.allowedAreas:
|
||||
self.allowedAreas.remove('center')
|
||||
self.updateStyle()
|
||||
|
||||
def showTitleBar(self):
|
||||
"""
|
||||
Show the title bar for this Dock.
|
||||
"""
|
||||
self.label.show()
|
||||
self.labelHidden = False
|
||||
self.allowedAreas.add('center')
|
||||
self.updateStyle()
|
||||
|
||||
def title(self):
|
||||
"""
|
||||
Gets the text displayed in the title bar for this dock.
|
||||
"""
|
||||
return asUnicode(self.label.text())
|
||||
|
||||
def setTitle(self, text):
|
||||
"""
|
||||
Sets the text displayed in title bar for this Dock.
|
||||
"""
|
||||
self.label.setText(text)
|
||||
|
||||
def setOrientation(self, o='auto', force=False):
|
||||
"""
|
||||
Sets the orientation of the title bar for this Dock.
|
||||
Must be one of 'auto', 'horizontal', or 'vertical'.
|
||||
By default ('auto'), the orientation is determined
|
||||
based on the aspect ratio of the Dock.
|
||||
"""
|
||||
#print self.name(), "setOrientation", o, force
|
||||
if o == 'auto' and self.autoOrient:
|
||||
if self.container().type() == 'tab':
|
||||
o = 'horizontal'
|
||||
elif self.width() > self.height()*1.5:
|
||||
o = 'vertical'
|
||||
else:
|
||||
o = 'horizontal'
|
||||
if force or self.orientation != o:
|
||||
self.orientation = o
|
||||
self.label.setOrientation(o)
|
||||
self.updateStyle()
|
||||
|
||||
def updateStyle(self):
|
||||
## updates orientation and appearance of title bar
|
||||
#print self.name(), "update style:", self.orientation, self.moveLabel, self.label.isVisible()
|
||||
if self.labelHidden:
|
||||
self.widgetArea.setStyleSheet(self.nStyle)
|
||||
elif self.orientation == 'vertical':
|
||||
self.label.setOrientation('vertical')
|
||||
if self.moveLabel:
|
||||
#print self.name(), "reclaim label"
|
||||
self.topLayout.addWidget(self.label, 1, 0)
|
||||
self.widgetArea.setStyleSheet(self.vStyle)
|
||||
else:
|
||||
self.label.setOrientation('horizontal')
|
||||
if self.moveLabel:
|
||||
#print self.name(), "reclaim label"
|
||||
self.topLayout.addWidget(self.label, 0, 1)
|
||||
self.widgetArea.setStyleSheet(self.hStyle)
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
self.setOrientation()
|
||||
self.resizeOverlay(self.size())
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def container(self):
|
||||
return self._container
|
||||
|
||||
def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1):
|
||||
"""
|
||||
Add a new widget to the interior of this Dock.
|
||||
Each Dock uses a QGridLayout to arrange widgets within.
|
||||
"""
|
||||
if row is None:
|
||||
row = self.currentRow
|
||||
self.currentRow = max(row+1, self.currentRow)
|
||||
self.widgets.append(widget)
|
||||
self.layout.addWidget(widget, row, col, rowspan, colspan)
|
||||
self.raiseOverlay()
|
||||
|
||||
|
||||
def startDrag(self):
|
||||
self.drag = QtGui.QDrag(self)
|
||||
mime = QtCore.QMimeData()
|
||||
#mime.setPlainText("asd")
|
||||
self.drag.setMimeData(mime)
|
||||
self.widgetArea.setStyleSheet(self.dragStyle)
|
||||
self.update()
|
||||
action = self.drag.exec_()
|
||||
self.updateStyle()
|
||||
|
||||
def float(self):
|
||||
self.area.floatDock(self)
|
||||
|
||||
def containerChanged(self, c):
|
||||
#print self.name(), "container changed"
|
||||
self._container = c
|
||||
if c.type() != 'tab':
|
||||
self.moveLabel = True
|
||||
self.label.setDim(False)
|
||||
else:
|
||||
self.moveLabel = False
|
||||
|
||||
self.setOrientation(force=True)
|
||||
|
||||
def raiseDock(self):
|
||||
"""If this Dock is stacked underneath others, raise it to the top."""
|
||||
self.container().raiseDock(self)
|
||||
|
||||
|
||||
def close(self):
|
||||
"""Remove this dock from the DockArea it lives inside."""
|
||||
self.setParent(None)
|
||||
self.label.setParent(None)
|
||||
self._container.apoptose()
|
||||
self._container = None
|
||||
self.sigClosed.emit(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Dock %s %s>" % (self.name(), self.stretch())
|
||||
|
||||
## PySide bug: We need to explicitly redefine these methods
|
||||
## or else drag/drop events will not be delivered.
|
||||
def dragEnterEvent(self, *args):
|
||||
DockDrop.dragEnterEvent(self, *args)
|
||||
|
||||
def dragMoveEvent(self, *args):
|
||||
DockDrop.dragMoveEvent(self, *args)
|
||||
|
||||
def dragLeaveEvent(self, *args):
|
||||
DockDrop.dragLeaveEvent(self, *args)
|
||||
|
||||
def dropEvent(self, *args):
|
||||
DockDrop.dropEvent(self, *args)
|
||||
|
||||
|
||||
class DockLabel(VerticalLabel):
|
||||
|
||||
sigClicked = QtCore.Signal(object, object)
|
||||
sigCloseClicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, text, dock, showCloseButton):
|
||||
self.dim = False
|
||||
self.fixedWidth = False
|
||||
VerticalLabel.__init__(self, text, orientation='horizontal', forceWidth=False)
|
||||
self.setAlignment(QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter)
|
||||
self.dock = dock
|
||||
self.updateStyle()
|
||||
self.setAutoFillBackground(False)
|
||||
self.startedDrag = False
|
||||
|
||||
self.closeButton = None
|
||||
if showCloseButton:
|
||||
self.closeButton = QtGui.QToolButton(self)
|
||||
self.closeButton.clicked.connect(self.sigCloseClicked)
|
||||
self.closeButton.setIcon(QtGui.QApplication.style().standardIcon(QtGui.QStyle.SP_TitleBarCloseButton))
|
||||
|
||||
def updateStyle(self):
|
||||
r = '3px'
|
||||
if self.dim:
|
||||
fg = '#aaa'
|
||||
bg = '#44a'
|
||||
border = '#339'
|
||||
else:
|
||||
fg = '#fff'
|
||||
bg = '#66c'
|
||||
border = '#55B'
|
||||
|
||||
if self.orientation == 'vertical':
|
||||
self.vStyle = """DockLabel {
|
||||
background-color : %s;
|
||||
color : %s;
|
||||
border-top-right-radius: 0px;
|
||||
border-top-left-radius: %s;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: %s;
|
||||
border-width: 0px;
|
||||
border-right: 2px solid %s;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}""" % (bg, fg, r, r, border)
|
||||
self.setStyleSheet(self.vStyle)
|
||||
else:
|
||||
self.hStyle = """DockLabel {
|
||||
background-color : %s;
|
||||
color : %s;
|
||||
border-top-right-radius: %s;
|
||||
border-top-left-radius: %s;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-width: 0px;
|
||||
border-bottom: 2px solid %s;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
}""" % (bg, fg, r, r, border)
|
||||
self.setStyleSheet(self.hStyle)
|
||||
|
||||
def setDim(self, d):
|
||||
if self.dim != d:
|
||||
self.dim = d
|
||||
self.updateStyle()
|
||||
|
||||
def setOrientation(self, o):
|
||||
VerticalLabel.setOrientation(self, o)
|
||||
self.updateStyle()
|
||||
|
||||
def mousePressEvent(self, ev):
|
||||
if ev.button() == QtCore.Qt.LeftButton:
|
||||
self.pressPos = ev.pos()
|
||||
self.startedDrag = False
|
||||
ev.accept()
|
||||
|
||||
def mouseMoveEvent(self, ev):
|
||||
if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance():
|
||||
self.dock.startDrag()
|
||||
ev.accept()
|
||||
|
||||
def mouseReleaseEvent(self, ev):
|
||||
if not self.startedDrag:
|
||||
self.sigClicked.emit(self, ev)
|
||||
ev.accept()
|
||||
|
||||
def mouseDoubleClickEvent(self, ev):
|
||||
if ev.button() == QtCore.Qt.LeftButton:
|
||||
self.dock.float()
|
||||
|
||||
def resizeEvent (self, ev):
|
||||
if self.closeButton:
|
||||
if self.orientation == 'vertical':
|
||||
size = ev.size().width()
|
||||
pos = QtCore.QPoint(0, 0)
|
||||
else:
|
||||
size = ev.size().height()
|
||||
pos = QtCore.QPoint(ev.size().width() - size, 0)
|
||||
self.closeButton.setFixedSize(QtCore.QSize(size, size))
|
||||
self.closeButton.move(pos)
|
||||
super(DockLabel,self).resizeEvent(ev)
|
339
pyqtgraph/dockarea/DockArea.py
Normal file
339
pyqtgraph/dockarea/DockArea.py
Normal file
|
@ -0,0 +1,339 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ..Qt import QtCore, QtGui
|
||||
from .Container import *
|
||||
from .DockDrop import *
|
||||
from .Dock import Dock
|
||||
from .. import debug as debug
|
||||
import weakref
|
||||
|
||||
## TODO:
|
||||
# - containers should be drop areas, not docks. (but every slot within a container must have its own drop areas?)
|
||||
# - drop between tabs
|
||||
# - nest splitters inside tab boxes, etc.
|
||||
|
||||
|
||||
|
||||
|
||||
class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
def __init__(self, temporary=False, home=None):
|
||||
Container.__init__(self, self)
|
||||
QtGui.QWidget.__init__(self)
|
||||
DockDrop.__init__(self, allowedAreas=['left', 'right', 'top', 'bottom'])
|
||||
self.layout = QtGui.QVBoxLayout()
|
||||
self.layout.setContentsMargins(0,0,0,0)
|
||||
self.layout.setSpacing(0)
|
||||
self.setLayout(self.layout)
|
||||
self.docks = weakref.WeakValueDictionary()
|
||||
self.topContainer = None
|
||||
self.raiseOverlay()
|
||||
self.temporary = temporary
|
||||
self.tempAreas = []
|
||||
self.home = home
|
||||
|
||||
def type(self):
|
||||
return "top"
|
||||
|
||||
def addDock(self, dock=None, position='bottom', relativeTo=None, **kwds):
|
||||
"""Adds a dock to this area.
|
||||
|
||||
============== =================================================================
|
||||
**Arguments:**
|
||||
dock The new Dock object to add. If None, then a new Dock will be
|
||||
created.
|
||||
position 'bottom', 'top', 'left', 'right', 'above', or 'below'
|
||||
relativeTo If relativeTo is None, then the new Dock is added to fill an
|
||||
entire edge of the window. If relativeTo is another Dock, then
|
||||
the new Dock is placed adjacent to it (or in a tabbed
|
||||
configuration for 'above' and 'below').
|
||||
============== =================================================================
|
||||
|
||||
All extra keyword arguments are passed to Dock.__init__() if *dock* is
|
||||
None.
|
||||
"""
|
||||
if dock is None:
|
||||
dock = Dock(**kwds)
|
||||
|
||||
|
||||
## Determine the container to insert this dock into.
|
||||
## If there is no neighbor, then the container is the top.
|
||||
if relativeTo is None or relativeTo is self:
|
||||
if self.topContainer is None:
|
||||
container = self
|
||||
neighbor = None
|
||||
else:
|
||||
container = self.topContainer
|
||||
neighbor = None
|
||||
else:
|
||||
if isinstance(relativeTo, basestring):
|
||||
relativeTo = self.docks[relativeTo]
|
||||
container = self.getContainer(relativeTo)
|
||||
neighbor = relativeTo
|
||||
|
||||
## what container type do we need?
|
||||
neededContainer = {
|
||||
'bottom': 'vertical',
|
||||
'top': 'vertical',
|
||||
'left': 'horizontal',
|
||||
'right': 'horizontal',
|
||||
'above': 'tab',
|
||||
'below': 'tab'
|
||||
}[position]
|
||||
|
||||
## Can't insert new containers into a tab container; insert outside instead.
|
||||
if neededContainer != container.type() and container.type() == 'tab':
|
||||
neighbor = container
|
||||
container = container.container()
|
||||
|
||||
## Decide if the container we have is suitable.
|
||||
## If not, insert a new container inside.
|
||||
if neededContainer != container.type():
|
||||
if neighbor is None:
|
||||
container = self.addContainer(neededContainer, self.topContainer)
|
||||
else:
|
||||
container = self.addContainer(neededContainer, neighbor)
|
||||
|
||||
## Insert the new dock before/after its neighbor
|
||||
insertPos = {
|
||||
'bottom': 'after',
|
||||
'top': 'before',
|
||||
'left': 'before',
|
||||
'right': 'after',
|
||||
'above': 'before',
|
||||
'below': 'after'
|
||||
}[position]
|
||||
#print "request insert", dock, insertPos, neighbor
|
||||
old = dock.container()
|
||||
container.insert(dock, insertPos, neighbor)
|
||||
dock.area = self
|
||||
self.docks[dock.name()] = dock
|
||||
if old is not None:
|
||||
old.apoptose()
|
||||
|
||||
return dock
|
||||
|
||||
def moveDock(self, dock, position, neighbor):
|
||||
"""
|
||||
Move an existing Dock to a new location.
|
||||
"""
|
||||
## Moving to the edge of a tabbed dock causes a drop outside the tab box
|
||||
if position in ['left', 'right', 'top', 'bottom'] and neighbor is not None and neighbor.container() is not None and neighbor.container().type() == 'tab':
|
||||
neighbor = neighbor.container()
|
||||
self.addDock(dock, position, neighbor)
|
||||
|
||||
def getContainer(self, obj):
|
||||
if obj is None:
|
||||
return self
|
||||
return obj.container()
|
||||
|
||||
def makeContainer(self, typ):
|
||||
if typ == 'vertical':
|
||||
new = VContainer(self)
|
||||
elif typ == 'horizontal':
|
||||
new = HContainer(self)
|
||||
elif typ == 'tab':
|
||||
new = TContainer(self)
|
||||
return new
|
||||
|
||||
def addContainer(self, typ, obj):
|
||||
"""Add a new container around obj"""
|
||||
new = self.makeContainer(typ)
|
||||
|
||||
container = self.getContainer(obj)
|
||||
container.insert(new, 'before', obj)
|
||||
#print "Add container:", new, " -> ", container
|
||||
if obj is not None:
|
||||
new.insert(obj)
|
||||
self.raiseOverlay()
|
||||
return new
|
||||
|
||||
def insert(self, new, pos=None, neighbor=None):
|
||||
if self.topContainer is not None:
|
||||
self.topContainer.containerChanged(None)
|
||||
self.layout.addWidget(new)
|
||||
self.topContainer = new
|
||||
#print self, "set top:", new
|
||||
new._container = self
|
||||
self.raiseOverlay()
|
||||
#print "Insert top:", new
|
||||
|
||||
def count(self):
|
||||
if self.topContainer is None:
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
#def paintEvent(self, ev):
|
||||
#self.drawDockOverlay()
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
self.resizeOverlay(self.size())
|
||||
|
||||
def addTempArea(self):
|
||||
if self.home is None:
|
||||
area = DockArea(temporary=True, home=self)
|
||||
self.tempAreas.append(area)
|
||||
win = TempAreaWindow(area)
|
||||
area.win = win
|
||||
win.show()
|
||||
else:
|
||||
area = self.home.addTempArea()
|
||||
#print "added temp area", area, area.window()
|
||||
return area
|
||||
|
||||
def floatDock(self, dock):
|
||||
"""Removes *dock* from this DockArea and places it in a new window."""
|
||||
area = self.addTempArea()
|
||||
area.win.resize(dock.size())
|
||||
area.moveDock(dock, 'top', None)
|
||||
|
||||
|
||||
def removeTempArea(self, area):
|
||||
self.tempAreas.remove(area)
|
||||
#print "close window", area.window()
|
||||
area.window().close()
|
||||
|
||||
def saveState(self):
|
||||
"""
|
||||
Return a serialized (storable) representation of the state of
|
||||
all Docks in this DockArea."""
|
||||
|
||||
if self.topContainer is None:
|
||||
main = None
|
||||
else:
|
||||
main = self.childState(self.topContainer)
|
||||
|
||||
state = {'main': main, 'float': []}
|
||||
for a in self.tempAreas:
|
||||
geo = a.win.geometry()
|
||||
geo = (geo.x(), geo.y(), geo.width(), geo.height())
|
||||
state['float'].append((a.saveState(), geo))
|
||||
return state
|
||||
|
||||
def childState(self, obj):
|
||||
if isinstance(obj, Dock):
|
||||
return ('dock', obj.name(), {})
|
||||
else:
|
||||
childs = []
|
||||
for i in range(obj.count()):
|
||||
childs.append(self.childState(obj.widget(i)))
|
||||
return (obj.type(), childs, obj.saveState())
|
||||
|
||||
|
||||
def restoreState(self, state):
|
||||
"""
|
||||
Restore Dock configuration as generated by saveState.
|
||||
|
||||
Note that this function does not create any Docks--it will only
|
||||
restore the arrangement of an existing set of Docks.
|
||||
|
||||
"""
|
||||
|
||||
## 1) make dict of all docks and list of existing containers
|
||||
containers, docks = self.findAll()
|
||||
oldTemps = self.tempAreas[:]
|
||||
#print "found docks:", docks
|
||||
|
||||
## 2) create container structure, move docks into new containers
|
||||
if state['main'] is not None:
|
||||
self.buildFromState(state['main'], docks, self)
|
||||
|
||||
## 3) create floating areas, populate
|
||||
for s in state['float']:
|
||||
a = self.addTempArea()
|
||||
a.buildFromState(s[0]['main'], docks, a)
|
||||
a.win.setGeometry(*s[1])
|
||||
|
||||
## 4) Add any remaining docks to the bottom
|
||||
for d in docks.values():
|
||||
self.moveDock(d, 'below', None)
|
||||
|
||||
#print "\nKill old containers:"
|
||||
## 5) kill old containers
|
||||
for c in containers:
|
||||
c.close()
|
||||
for a in oldTemps:
|
||||
a.apoptose()
|
||||
|
||||
|
||||
def buildFromState(self, state, docks, root, depth=0):
|
||||
typ, contents, state = state
|
||||
pfx = " " * depth
|
||||
if typ == 'dock':
|
||||
try:
|
||||
obj = docks[contents]
|
||||
del docks[contents]
|
||||
except KeyError:
|
||||
raise Exception('Cannot restore dock state; no dock with name "%s"' % contents)
|
||||
else:
|
||||
obj = self.makeContainer(typ)
|
||||
|
||||
root.insert(obj, 'after')
|
||||
#print pfx+"Add:", obj, " -> ", root
|
||||
|
||||
if typ != 'dock':
|
||||
for o in contents:
|
||||
self.buildFromState(o, docks, obj, depth+1)
|
||||
obj.apoptose(propagate=False)
|
||||
obj.restoreState(state) ## this has to be done later?
|
||||
|
||||
|
||||
def findAll(self, obj=None, c=None, d=None):
|
||||
if obj is None:
|
||||
obj = self.topContainer
|
||||
|
||||
## check all temp areas first
|
||||
if c is None:
|
||||
c = []
|
||||
d = {}
|
||||
for a in self.tempAreas:
|
||||
c1, d1 = a.findAll()
|
||||
c.extend(c1)
|
||||
d.update(d1)
|
||||
|
||||
if isinstance(obj, Dock):
|
||||
d[obj.name()] = obj
|
||||
elif obj is not None:
|
||||
c.append(obj)
|
||||
for i in range(obj.count()):
|
||||
o2 = obj.widget(i)
|
||||
c2, d2 = self.findAll(o2)
|
||||
c.extend(c2)
|
||||
d.update(d2)
|
||||
return (c, d)
|
||||
|
||||
def apoptose(self):
|
||||
#print "apoptose area:", self.temporary, self.topContainer, self.topContainer.count()
|
||||
if self.topContainer.count() == 0:
|
||||
self.topContainer = None
|
||||
if self.temporary:
|
||||
self.home.removeTempArea(self)
|
||||
#self.close()
|
||||
|
||||
def clear(self):
|
||||
docks = self.findAll()[1]
|
||||
for dock in docks.values():
|
||||
dock.close()
|
||||
|
||||
## PySide bug: We need to explicitly redefine these methods
|
||||
## or else drag/drop events will not be delivered.
|
||||
def dragEnterEvent(self, *args):
|
||||
DockDrop.dragEnterEvent(self, *args)
|
||||
|
||||
def dragMoveEvent(self, *args):
|
||||
DockDrop.dragMoveEvent(self, *args)
|
||||
|
||||
def dragLeaveEvent(self, *args):
|
||||
DockDrop.dragLeaveEvent(self, *args)
|
||||
|
||||
def dropEvent(self, *args):
|
||||
DockDrop.dropEvent(self, *args)
|
||||
|
||||
|
||||
class TempAreaWindow(QtGui.QMainWindow):
|
||||
def __init__(self, area, **kwargs):
|
||||
QtGui.QMainWindow.__init__(self, **kwargs)
|
||||
self.setCentralWidget(area)
|
||||
|
||||
def closeEvent(self, *args, **kwargs):
|
||||
self.centralWidget().clear()
|
||||
QtGui.QMainWindow.closeEvent(self, *args, **kwargs)
|
128
pyqtgraph/dockarea/DockDrop.py
Normal file
128
pyqtgraph/dockarea/DockDrop.py
Normal file
|
@ -0,0 +1,128 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ..Qt import QtCore, QtGui
|
||||
|
||||
class DockDrop(object):
|
||||
"""Provides dock-dropping methods"""
|
||||
def __init__(self, allowedAreas=None):
|
||||
object.__init__(self)
|
||||
if allowedAreas is None:
|
||||
allowedAreas = ['center', 'right', 'left', 'top', 'bottom']
|
||||
self.allowedAreas = set(allowedAreas)
|
||||
self.setAcceptDrops(True)
|
||||
self.dropArea = None
|
||||
self.overlay = DropAreaOverlay(self)
|
||||
self.overlay.raise_()
|
||||
|
||||
def resizeOverlay(self, size):
|
||||
self.overlay.resize(size)
|
||||
|
||||
def raiseOverlay(self):
|
||||
self.overlay.raise_()
|
||||
|
||||
def dragEnterEvent(self, ev):
|
||||
src = ev.source()
|
||||
if hasattr(src, 'implements') and src.implements('dock'):
|
||||
#print "drag enter accept"
|
||||
ev.accept()
|
||||
else:
|
||||
#print "drag enter ignore"
|
||||
ev.ignore()
|
||||
|
||||
def dragMoveEvent(self, ev):
|
||||
#print "drag move"
|
||||
ld = ev.pos().x()
|
||||
rd = self.width() - ld
|
||||
td = ev.pos().y()
|
||||
bd = self.height() - td
|
||||
|
||||
mn = min(ld, rd, td, bd)
|
||||
if mn > 30:
|
||||
self.dropArea = "center"
|
||||
elif (ld == mn or td == mn) and mn > self.height()/3.:
|
||||
self.dropArea = "center"
|
||||
elif (rd == mn or ld == mn) and mn > self.width()/3.:
|
||||
self.dropArea = "center"
|
||||
|
||||
elif rd == mn:
|
||||
self.dropArea = "right"
|
||||
elif ld == mn:
|
||||
self.dropArea = "left"
|
||||
elif td == mn:
|
||||
self.dropArea = "top"
|
||||
elif bd == mn:
|
||||
self.dropArea = "bottom"
|
||||
|
||||
if ev.source() is self and self.dropArea == 'center':
|
||||
#print " no self-center"
|
||||
self.dropArea = None
|
||||
ev.ignore()
|
||||
elif self.dropArea not in self.allowedAreas:
|
||||
#print " not allowed"
|
||||
self.dropArea = None
|
||||
ev.ignore()
|
||||
else:
|
||||
#print " ok"
|
||||
ev.accept()
|
||||
self.overlay.setDropArea(self.dropArea)
|
||||
|
||||
def dragLeaveEvent(self, ev):
|
||||
self.dropArea = None
|
||||
self.overlay.setDropArea(self.dropArea)
|
||||
|
||||
def dropEvent(self, ev):
|
||||
area = self.dropArea
|
||||
if area is None:
|
||||
return
|
||||
if area == 'center':
|
||||
area = 'above'
|
||||
self.area.moveDock(ev.source(), area, self)
|
||||
self.dropArea = None
|
||||
self.overlay.setDropArea(self.dropArea)
|
||||
|
||||
|
||||
|
||||
class DropAreaOverlay(QtGui.QWidget):
|
||||
"""Overlay widget that draws drop areas during a drag-drop operation"""
|
||||
|
||||
def __init__(self, parent):
|
||||
QtGui.QWidget.__init__(self, parent)
|
||||
self.dropArea = None
|
||||
self.hide()
|
||||
self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
|
||||
|
||||
def setDropArea(self, area):
|
||||
self.dropArea = area
|
||||
if area is None:
|
||||
self.hide()
|
||||
else:
|
||||
## Resize overlay to just the region where drop area should be displayed.
|
||||
## This works around a Qt bug--can't display transparent widgets over QGLWidget
|
||||
prgn = self.parent().rect()
|
||||
rgn = QtCore.QRect(prgn)
|
||||
w = min(30, prgn.width()/3.)
|
||||
h = min(30, prgn.height()/3.)
|
||||
|
||||
if self.dropArea == 'left':
|
||||
rgn.setWidth(w)
|
||||
elif self.dropArea == 'right':
|
||||
rgn.setLeft(rgn.left() + prgn.width() - w)
|
||||
elif self.dropArea == 'top':
|
||||
rgn.setHeight(h)
|
||||
elif self.dropArea == 'bottom':
|
||||
rgn.setTop(rgn.top() + prgn.height() - h)
|
||||
elif self.dropArea == 'center':
|
||||
rgn.adjust(w, h, -w, -h)
|
||||
self.setGeometry(rgn)
|
||||
self.show()
|
||||
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, ev):
|
||||
if self.dropArea is None:
|
||||
return
|
||||
p = QtGui.QPainter(self)
|
||||
rgn = self.rect()
|
||||
|
||||
p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 255, 50)))
|
||||
p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 150), 3))
|
||||
p.drawRect(rgn)
|
2
pyqtgraph/dockarea/__init__.py
Normal file
2
pyqtgraph/dockarea/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .DockArea import DockArea
|
||||
from .Dock import Dock
|
16
pyqtgraph/dockarea/tests/test_dock.py
Normal file
16
pyqtgraph/dockarea/tests/test_dock.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#import sip
|
||||
#sip.setapi('QString', 1)
|
||||
|
||||
import pyqtgraph as pg
|
||||
pg.mkQApp()
|
||||
|
||||
import pyqtgraph.dockarea as da
|
||||
|
||||
def test_dock():
|
||||
name = pg.asUnicode("évènts_zàhéér")
|
||||
dock = da.Dock(name=name)
|
||||
# make sure unicode names work correctly
|
||||
assert dock.name() == name
|
||||
# no surprises in return type.
|
||||
assert type(dock.name()) == type(name)
|
106
pyqtgraph/exceptionHandling.py
Normal file
106
pyqtgraph/exceptionHandling.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""This module installs a wrapper around sys.excepthook which allows multiple
|
||||
new exception handlers to be registered.
|
||||
|
||||
Optionally, the wrapper also stops exceptions from causing long-term storage
|
||||
of local stack frames. This has two major effects:
|
||||
- Unhandled exceptions will no longer cause memory leaks
|
||||
(If an exception occurs while a lot of data is present on the stack,
|
||||
such as when loading large files, the data would ordinarily be kept
|
||||
until the next exception occurs. We would rather release this memory
|
||||
as soon as possible.)
|
||||
- Some debuggers may have a hard time handling uncaught exceptions
|
||||
|
||||
The module also provides a callback mechanism allowing others to respond
|
||||
to exceptions.
|
||||
"""
|
||||
|
||||
import sys, time
|
||||
#from lib.Manager import logMsg
|
||||
import traceback
|
||||
#from log import *
|
||||
|
||||
#logging = False
|
||||
|
||||
callbacks = []
|
||||
clear_tracebacks = False
|
||||
|
||||
def register(fn):
|
||||
"""
|
||||
Register a callable to be invoked when there is an unhandled exception.
|
||||
The callback will be passed the output of sys.exc_info(): (exception type, exception, traceback)
|
||||
Multiple callbacks will be invoked in the order they were registered.
|
||||
"""
|
||||
callbacks.append(fn)
|
||||
|
||||
def unregister(fn):
|
||||
"""Unregister a previously registered callback."""
|
||||
callbacks.remove(fn)
|
||||
|
||||
def setTracebackClearing(clear=True):
|
||||
"""
|
||||
Enable or disable traceback clearing.
|
||||
By default, clearing is disabled and Python will indefinitely store unhandled exception stack traces.
|
||||
This function is provided since Python's default behavior can cause unexpected retention of
|
||||
large memory-consuming objects.
|
||||
"""
|
||||
global clear_tracebacks
|
||||
clear_tracebacks = clear
|
||||
|
||||
class ExceptionHandler(object):
|
||||
def __call__(self, *args):
|
||||
## Start by extending recursion depth just a bit.
|
||||
## If the error we are catching is due to recursion, we don't want to generate another one here.
|
||||
recursionLimit = sys.getrecursionlimit()
|
||||
try:
|
||||
sys.setrecursionlimit(recursionLimit+100)
|
||||
|
||||
|
||||
## call original exception handler first (prints exception)
|
||||
global original_excepthook, callbacks, clear_tracebacks
|
||||
try:
|
||||
print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time()))))
|
||||
except Exception:
|
||||
sys.stderr.write("Warning: stdout is broken! Falling back to stderr.\n")
|
||||
sys.stdout = sys.stderr
|
||||
|
||||
ret = original_excepthook(*args)
|
||||
|
||||
for cb in callbacks:
|
||||
try:
|
||||
cb(*args)
|
||||
except Exception:
|
||||
print(" --------------------------------------------------------------")
|
||||
print(" Error occurred during exception callback %s" % str(cb))
|
||||
print(" --------------------------------------------------------------")
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
|
||||
|
||||
## Clear long-term storage of last traceback to prevent memory-hogging.
|
||||
## (If an exception occurs while a lot of data is present on the stack,
|
||||
## such as when loading large files, the data would ordinarily be kept
|
||||
## until the next exception occurs. We would rather release this memory
|
||||
## as soon as possible.)
|
||||
if clear_tracebacks is True:
|
||||
sys.last_traceback = None
|
||||
|
||||
finally:
|
||||
sys.setrecursionlimit(recursionLimit)
|
||||
|
||||
|
||||
def implements(self, interface=None):
|
||||
## this just makes it easy for us to detect whether an ExceptionHook is already installed.
|
||||
if interface is None:
|
||||
return ['ExceptionHandler']
|
||||
else:
|
||||
return interface == 'ExceptionHandler'
|
||||
|
||||
|
||||
|
||||
## replace built-in excepthook only if this has not already been done
|
||||
if not (hasattr(sys.excepthook, 'implements') and sys.excepthook.implements('ExceptionHandler')):
|
||||
original_excepthook = sys.excepthook
|
||||
sys.excepthook = ExceptionHandler()
|
||||
|
||||
|
||||
|
83
pyqtgraph/exporters/CSVExporter.py
Normal file
83
pyqtgraph/exporters/CSVExporter.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from .Exporter import Exporter
|
||||
from ..parametertree import Parameter
|
||||
from .. import PlotItem
|
||||
|
||||
__all__ = ['CSVExporter']
|
||||
|
||||
|
||||
class CSVExporter(Exporter):
|
||||
Name = "CSV from plot data"
|
||||
windows = []
|
||||
def __init__(self, item):
|
||||
Exporter.__init__(self, item)
|
||||
self.params = Parameter(name='params', type='group', children=[
|
||||
{'name': 'separator', 'type': 'list', 'value': 'comma', 'values': ['comma', 'tab']},
|
||||
{'name': 'precision', 'type': 'int', 'value': 10, 'limits': [0, None]},
|
||||
{'name': 'columnMode', 'type': 'list', 'values': ['(x,y) per plot', '(x,y,y,y) for all plots']}
|
||||
])
|
||||
|
||||
def parameters(self):
|
||||
return self.params
|
||||
|
||||
def export(self, fileName=None):
|
||||
|
||||
if not isinstance(self.item, PlotItem):
|
||||
raise Exception("Must have a PlotItem selected for CSV export.")
|
||||
|
||||
if fileName is None:
|
||||
self.fileSaveDialog(filter=["*.csv", "*.tsv"])
|
||||
return
|
||||
|
||||
fd = open(fileName, 'w')
|
||||
data = []
|
||||
header = []
|
||||
|
||||
appendAllX = self.params['columnMode'] == '(x,y) per plot'
|
||||
|
||||
for i, c in enumerate(self.item.curves):
|
||||
cd = c.getData()
|
||||
if cd[0] is None:
|
||||
continue
|
||||
data.append(cd)
|
||||
if hasattr(c, 'implements') and c.implements('plotData') and c.name() is not None:
|
||||
name = c.name().replace('"', '""') + '_'
|
||||
xName, yName = '"'+name+'x"', '"'+name+'y"'
|
||||
else:
|
||||
xName = 'x%04d' % i
|
||||
yName = 'y%04d' % i
|
||||
if appendAllX or i == 0:
|
||||
header.extend([xName, yName])
|
||||
else:
|
||||
header.extend([yName])
|
||||
|
||||
if self.params['separator'] == 'comma':
|
||||
sep = ','
|
||||
else:
|
||||
sep = '\t'
|
||||
|
||||
fd.write(sep.join(header) + '\n')
|
||||
i = 0
|
||||
numFormat = '%%0.%dg' % self.params['precision']
|
||||
numRows = max([len(d[0]) for d in data])
|
||||
for i in range(numRows):
|
||||
for j, d in enumerate(data):
|
||||
# write x value if this is the first column, or if we want x
|
||||
# for all rows
|
||||
if appendAllX or j == 0:
|
||||
if d is not None and i < len(d[0]):
|
||||
fd.write(numFormat % d[0][i] + sep)
|
||||
else:
|
||||
fd.write(' %s' % sep)
|
||||
|
||||
# write y value
|
||||
if d is not None and i < len(d[1]):
|
||||
fd.write(numFormat % d[1][i] + sep)
|
||||
else:
|
||||
fd.write(' %s' % sep)
|
||||
fd.write('\n')
|
||||
fd.close()
|
||||
|
||||
CSVExporter.register()
|
||||
|
||||
|
139
pyqtgraph/exporters/Exporter.py
Normal file
139
pyqtgraph/exporters/Exporter.py
Normal file
|
@ -0,0 +1,139 @@
|
|||
from ..widgets.FileDialog import FileDialog
|
||||
from ..Qt import QtGui, QtCore, QtSvg
|
||||
from ..python2_3 import asUnicode
|
||||
from ..GraphicsScene import GraphicsScene
|
||||
import os, re
|
||||
LastExportDirectory = None
|
||||
|
||||
|
||||
class Exporter(object):
|
||||
"""
|
||||
Abstract class used for exporting graphics to file / printer / whatever.
|
||||
"""
|
||||
allowCopy = False # subclasses set this to True if they can use the copy buffer
|
||||
Exporters = []
|
||||
|
||||
@classmethod
|
||||
def register(cls):
|
||||
"""
|
||||
Used to register Exporter classes to appear in the export dialog.
|
||||
"""
|
||||
Exporter.Exporters.append(cls)
|
||||
|
||||
def __init__(self, item):
|
||||
"""
|
||||
Initialize with the item to be exported.
|
||||
Can be an individual graphics item or a scene.
|
||||
"""
|
||||
object.__init__(self)
|
||||
self.item = item
|
||||
|
||||
def parameters(self):
|
||||
"""Return the parameters used to configure this exporter."""
|
||||
raise Exception("Abstract method must be overridden in subclass.")
|
||||
|
||||
def export(self, fileName=None, toBytes=False, copy=False):
|
||||
"""
|
||||
If *fileName* is None, pop-up a file dialog.
|
||||
If *toBytes* is True, return a bytes object rather than writing to file.
|
||||
If *copy* is True, export to the copy buffer rather than writing to file.
|
||||
"""
|
||||
raise Exception("Abstract method must be overridden in subclass.")
|
||||
|
||||
def fileSaveDialog(self, filter=None, opts=None):
|
||||
## Show a file dialog, call self.export(fileName) when finished.
|
||||
if opts is None:
|
||||
opts = {}
|
||||
self.fileDialog = FileDialog()
|
||||
self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
|
||||
self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
|
||||
if filter is not None:
|
||||
if isinstance(filter, basestring):
|
||||
self.fileDialog.setNameFilter(filter)
|
||||
elif isinstance(filter, list):
|
||||
self.fileDialog.setNameFilters(filter)
|
||||
global LastExportDirectory
|
||||
exportDir = LastExportDirectory
|
||||
if exportDir is not None:
|
||||
self.fileDialog.setDirectory(exportDir)
|
||||
self.fileDialog.show()
|
||||
self.fileDialog.opts = opts
|
||||
self.fileDialog.fileSelected.connect(self.fileSaveFinished)
|
||||
return
|
||||
|
||||
def fileSaveFinished(self, fileName):
|
||||
fileName = asUnicode(fileName)
|
||||
global LastExportDirectory
|
||||
LastExportDirectory = os.path.split(fileName)[0]
|
||||
|
||||
## If file name does not match selected extension, append it now
|
||||
ext = os.path.splitext(fileName)[1].lower().lstrip('.')
|
||||
selectedExt = re.search(r'\*\.(\w+)\b', asUnicode(self.fileDialog.selectedNameFilter()))
|
||||
if selectedExt is not None:
|
||||
selectedExt = selectedExt.groups()[0].lower()
|
||||
if ext != selectedExt:
|
||||
fileName = fileName + '.' + selectedExt.lstrip('.')
|
||||
|
||||
self.export(fileName=fileName, **self.fileDialog.opts)
|
||||
|
||||
def getScene(self):
|
||||
if isinstance(self.item, GraphicsScene):
|
||||
return self.item
|
||||
else:
|
||||
return self.item.scene()
|
||||
|
||||
def getSourceRect(self):
|
||||
if isinstance(self.item, GraphicsScene):
|
||||
w = self.item.getViewWidget()
|
||||
return w.viewportTransform().inverted()[0].mapRect(w.rect())
|
||||
else:
|
||||
return self.item.sceneBoundingRect()
|
||||
|
||||
def getTargetRect(self):
|
||||
if isinstance(self.item, GraphicsScene):
|
||||
return self.item.getViewWidget().rect()
|
||||
else:
|
||||
return self.item.mapRectToDevice(self.item.boundingRect())
|
||||
|
||||
def setExportMode(self, export, opts=None):
|
||||
"""
|
||||
Call setExportMode(export, opts) on all items that will
|
||||
be painted during the export. This informs the item
|
||||
that it is about to be painted for export, allowing it to
|
||||
alter its appearance temporarily
|
||||
|
||||
|
||||
*export* - bool; must be True before exporting and False afterward
|
||||
*opts* - dict; common parameters are 'antialias' and 'background'
|
||||
"""
|
||||
if opts is None:
|
||||
opts = {}
|
||||
for item in self.getPaintItems():
|
||||
if hasattr(item, 'setExportMode'):
|
||||
item.setExportMode(export, opts)
|
||||
|
||||
def getPaintItems(self, root=None):
|
||||
"""Return a list of all items that should be painted in the correct order."""
|
||||
if root is None:
|
||||
root = self.item
|
||||
preItems = []
|
||||
postItems = []
|
||||
if isinstance(root, QtGui.QGraphicsScene):
|
||||
childs = [i for i in root.items() if i.parentItem() is None]
|
||||
rootItem = []
|
||||
else:
|
||||
childs = root.childItems()
|
||||
rootItem = [root]
|
||||
childs.sort(key=lambda a: a.zValue())
|
||||
while len(childs) > 0:
|
||||
ch = childs.pop(0)
|
||||
tree = self.getPaintItems(ch)
|
||||
if int(ch.flags() & ch.ItemStacksBehindParent) > 0 or (ch.zValue() < 0 and int(ch.flags() & ch.ItemNegativeZStacksBehindParent) > 0):
|
||||
preItems.extend(tree)
|
||||
else:
|
||||
postItems.extend(tree)
|
||||
|
||||
return preItems + rootItem + postItems
|
||||
|
||||
def render(self, painter, targetRect, sourceRect, item=None):
|
||||
self.getScene().render(painter, QtCore.QRectF(targetRect), QtCore.QRectF(sourceRect))
|
58
pyqtgraph/exporters/HDF5Exporter.py
Normal file
58
pyqtgraph/exporters/HDF5Exporter.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from .Exporter import Exporter
|
||||
from ..parametertree import Parameter
|
||||
from .. import PlotItem
|
||||
|
||||
import numpy
|
||||
try:
|
||||
import h5py
|
||||
HAVE_HDF5 = True
|
||||
except ImportError:
|
||||
HAVE_HDF5 = False
|
||||
|
||||
__all__ = ['HDF5Exporter']
|
||||
|
||||
|
||||
class HDF5Exporter(Exporter):
|
||||
Name = "HDF5 Export: plot (x,y)"
|
||||
windows = []
|
||||
allowCopy = False
|
||||
|
||||
def __init__(self, item):
|
||||
Exporter.__init__(self, item)
|
||||
self.params = Parameter(name='params', type='group', children=[
|
||||
{'name': 'Name', 'type': 'str', 'value': 'Export',},
|
||||
{'name': 'columnMode', 'type': 'list', 'values': ['(x,y) per plot', '(x,y,y,y) for all plots']},
|
||||
])
|
||||
|
||||
def parameters(self):
|
||||
return self.params
|
||||
|
||||
def export(self, fileName=None):
|
||||
if not HAVE_HDF5:
|
||||
raise RuntimeError("This exporter requires the h5py package, "
|
||||
"but it was not importable.")
|
||||
|
||||
if not isinstance(self.item, PlotItem):
|
||||
raise Exception("Must have a PlotItem selected for HDF5 export.")
|
||||
|
||||
if fileName is None:
|
||||
self.fileSaveDialog(filter=["*.h5", "*.hdf", "*.hd5"])
|
||||
return
|
||||
dsname = self.params['Name']
|
||||
fd = h5py.File(fileName, 'a') # forces append to file... 'w' doesn't seem to "delete/overwrite"
|
||||
data = []
|
||||
|
||||
appendAllX = self.params['columnMode'] == '(x,y) per plot'
|
||||
for i,c in enumerate(self.item.curves):
|
||||
d = c.getData()
|
||||
if appendAllX or i == 0:
|
||||
data.append(d[0])
|
||||
data.append(d[1])
|
||||
|
||||
fdata = numpy.array(data).astype('double')
|
||||
dset = fd.create_dataset(dsname, data=fdata)
|
||||
fd.close()
|
||||
|
||||
if HAVE_HDF5:
|
||||
HDF5Exporter.register()
|
102
pyqtgraph/exporters/ImageExporter.py
Normal file
102
pyqtgraph/exporters/ImageExporter.py
Normal file
|
@ -0,0 +1,102 @@
|
|||
from .Exporter import Exporter
|
||||
from ..parametertree import Parameter
|
||||
from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE
|
||||
from .. import functions as fn
|
||||
import numpy as np
|
||||
|
||||
__all__ = ['ImageExporter']
|
||||
|
||||
class ImageExporter(Exporter):
|
||||
Name = "Image File (PNG, TIF, JPG, ...)"
|
||||
allowCopy = True
|
||||
|
||||
def __init__(self, item):
|
||||
Exporter.__init__(self, item)
|
||||
tr = self.getTargetRect()
|
||||
if isinstance(item, QtGui.QGraphicsItem):
|
||||
scene = item.scene()
|
||||
else:
|
||||
scene = item
|
||||
bgbrush = scene.views()[0].backgroundBrush()
|
||||
bg = bgbrush.color()
|
||||
if bgbrush.style() == QtCore.Qt.NoBrush:
|
||||
bg.setAlpha(0)
|
||||
|
||||
self.params = Parameter(name='params', type='group', children=[
|
||||
{'name': 'width', 'type': 'int', 'value': tr.width(), 'limits': (0, None)},
|
||||
{'name': 'height', 'type': 'int', 'value': tr.height(), 'limits': (0, None)},
|
||||
{'name': 'antialias', 'type': 'bool', 'value': True},
|
||||
{'name': 'background', 'type': 'color', 'value': bg},
|
||||
])
|
||||
self.params.param('width').sigValueChanged.connect(self.widthChanged)
|
||||
self.params.param('height').sigValueChanged.connect(self.heightChanged)
|
||||
|
||||
def widthChanged(self):
|
||||
sr = self.getSourceRect()
|
||||
ar = float(sr.height()) / sr.width()
|
||||
self.params.param('height').setValue(self.params['width'] * ar, blockSignal=self.heightChanged)
|
||||
|
||||
def heightChanged(self):
|
||||
sr = self.getSourceRect()
|
||||
ar = float(sr.width()) / sr.height()
|
||||
self.params.param('width').setValue(self.params['height'] * ar, blockSignal=self.widthChanged)
|
||||
|
||||
def parameters(self):
|
||||
return self.params
|
||||
|
||||
def export(self, fileName=None, toBytes=False, copy=False):
|
||||
if fileName is None and not toBytes and not copy:
|
||||
if USE_PYSIDE:
|
||||
filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()]
|
||||
else:
|
||||
filter = ["*."+bytes(f).decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()]
|
||||
preferred = ['*.png', '*.tif', '*.jpg']
|
||||
for p in preferred[::-1]:
|
||||
if p in filter:
|
||||
filter.remove(p)
|
||||
filter.insert(0, p)
|
||||
self.fileSaveDialog(filter=filter)
|
||||
return
|
||||
|
||||
targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height'])
|
||||
sourceRect = self.getSourceRect()
|
||||
|
||||
|
||||
#self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32)
|
||||
#self.png.fill(pyqtgraph.mkColor(self.params['background']))
|
||||
w, h = self.params['width'], self.params['height']
|
||||
if w == 0 or h == 0:
|
||||
raise Exception("Cannot export image with size=0 (requested export size is %dx%d)" % (w,h))
|
||||
bg = np.empty((self.params['width'], self.params['height'], 4), dtype=np.ubyte)
|
||||
color = self.params['background']
|
||||
bg[:,:,0] = color.blue()
|
||||
bg[:,:,1] = color.green()
|
||||
bg[:,:,2] = color.red()
|
||||
bg[:,:,3] = color.alpha()
|
||||
self.png = fn.makeQImage(bg, alpha=True)
|
||||
|
||||
## set resolution of image:
|
||||
origTargetRect = self.getTargetRect()
|
||||
resolutionScale = targetRect.width() / origTargetRect.width()
|
||||
#self.png.setDotsPerMeterX(self.png.dotsPerMeterX() * resolutionScale)
|
||||
#self.png.setDotsPerMeterY(self.png.dotsPerMeterY() * resolutionScale)
|
||||
|
||||
painter = QtGui.QPainter(self.png)
|
||||
#dtr = painter.deviceTransform()
|
||||
try:
|
||||
self.setExportMode(True, {'antialias': self.params['antialias'], 'background': self.params['background'], 'painter': painter, 'resolutionScale': resolutionScale})
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing, self.params['antialias'])
|
||||
self.getScene().render(painter, QtCore.QRectF(targetRect), QtCore.QRectF(sourceRect))
|
||||
finally:
|
||||
self.setExportMode(False)
|
||||
painter.end()
|
||||
|
||||
if copy:
|
||||
QtGui.QApplication.clipboard().setImage(self.png)
|
||||
elif toBytes:
|
||||
return self.png
|
||||
else:
|
||||
self.png.save(fileName)
|
||||
|
||||
ImageExporter.register()
|
||||
|
128
pyqtgraph/exporters/Matplotlib.py
Normal file
128
pyqtgraph/exporters/Matplotlib.py
Normal file
|
@ -0,0 +1,128 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from .Exporter import Exporter
|
||||
from .. import PlotItem
|
||||
from .. import functions as fn
|
||||
|
||||
__all__ = ['MatplotlibExporter']
|
||||
|
||||
"""
|
||||
It is helpful when using the matplotlib Exporter if your
|
||||
.matplotlib/matplotlibrc file is configured appropriately.
|
||||
The following are suggested for getting usable PDF output that
|
||||
can be edited in Illustrator, etc.
|
||||
|
||||
backend : Qt4Agg
|
||||
text.usetex : True # Assumes you have a findable LaTeX installation
|
||||
interactive : False
|
||||
font.family : sans-serif
|
||||
font.sans-serif : 'Arial' # (make first in list)
|
||||
mathtext.default : sf
|
||||
figure.facecolor : white # personal preference
|
||||
# next setting allows pdf font to be readable in Adobe Illustrator
|
||||
pdf.fonttype : 42 # set fonts to TrueType (otherwise it will be 3
|
||||
# and the text will be vectorized.
|
||||
text.dvipnghack : True # primarily to clean up font appearance on Mac
|
||||
|
||||
The advantage is that there is less to do to get an exported file cleaned and ready for
|
||||
publication. Fonts are not vectorized (outlined), and window colors are white.
|
||||
|
||||
"""
|
||||
|
||||
class MatplotlibExporter(Exporter):
|
||||
Name = "Matplotlib Window"
|
||||
windows = []
|
||||
def __init__(self, item):
|
||||
Exporter.__init__(self, item)
|
||||
|
||||
def parameters(self):
|
||||
return None
|
||||
|
||||
def cleanAxes(self, axl):
|
||||
if type(axl) is not list:
|
||||
axl = [axl]
|
||||
for ax in axl:
|
||||
if ax is None:
|
||||
continue
|
||||
for loc, spine in ax.spines.iteritems():
|
||||
if loc in ['left', 'bottom']:
|
||||
pass
|
||||
elif loc in ['right', 'top']:
|
||||
spine.set_color('none')
|
||||
# do not draw the spine
|
||||
else:
|
||||
raise ValueError('Unknown spine location: %s' % loc)
|
||||
# turn off ticks when there is no spine
|
||||
ax.xaxis.set_ticks_position('bottom')
|
||||
|
||||
def export(self, fileName=None):
|
||||
|
||||
if isinstance(self.item, PlotItem):
|
||||
mpw = MatplotlibWindow()
|
||||
MatplotlibExporter.windows.append(mpw)
|
||||
|
||||
stdFont = 'Arial'
|
||||
|
||||
fig = mpw.getFigure()
|
||||
|
||||
# get labels from the graphic item
|
||||
xlabel = self.item.axes['bottom']['item'].label.toPlainText()
|
||||
ylabel = self.item.axes['left']['item'].label.toPlainText()
|
||||
title = self.item.titleLabel.text
|
||||
|
||||
ax = fig.add_subplot(111, title=title)
|
||||
ax.clear()
|
||||
self.cleanAxes(ax)
|
||||
#ax.grid(True)
|
||||
for item in self.item.curves:
|
||||
x, y = item.getData()
|
||||
opts = item.opts
|
||||
pen = fn.mkPen(opts['pen'])
|
||||
if pen.style() == QtCore.Qt.NoPen:
|
||||
linestyle = ''
|
||||
else:
|
||||
linestyle = '-'
|
||||
color = tuple([c/255. for c in fn.colorTuple(pen.color())])
|
||||
symbol = opts['symbol']
|
||||
if symbol == 't':
|
||||
symbol = '^'
|
||||
symbolPen = fn.mkPen(opts['symbolPen'])
|
||||
symbolBrush = fn.mkBrush(opts['symbolBrush'])
|
||||
markeredgecolor = tuple([c/255. for c in fn.colorTuple(symbolPen.color())])
|
||||
markerfacecolor = tuple([c/255. for c in fn.colorTuple(symbolBrush.color())])
|
||||
markersize = opts['symbolSize']
|
||||
|
||||
if opts['fillLevel'] is not None and opts['fillBrush'] is not None:
|
||||
fillBrush = fn.mkBrush(opts['fillBrush'])
|
||||
fillcolor = tuple([c/255. for c in fn.colorTuple(fillBrush.color())])
|
||||
ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor)
|
||||
|
||||
pl = ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(),
|
||||
linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor,
|
||||
markersize=markersize)
|
||||
xr, yr = self.item.viewRange()
|
||||
ax.set_xbound(*xr)
|
||||
ax.set_ybound(*yr)
|
||||
ax.set_xlabel(xlabel) # place the labels.
|
||||
ax.set_ylabel(ylabel)
|
||||
mpw.draw()
|
||||
else:
|
||||
raise Exception("Matplotlib export currently only works with plot items")
|
||||
|
||||
MatplotlibExporter.register()
|
||||
|
||||
|
||||
class MatplotlibWindow(QtGui.QMainWindow):
|
||||
def __init__(self):
|
||||
from ..widgets import MatplotlibWidget
|
||||
QtGui.QMainWindow.__init__(self)
|
||||
self.mpl = MatplotlibWidget.MatplotlibWidget()
|
||||
self.setCentralWidget(self.mpl)
|
||||
self.show()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.mpl, attr)
|
||||
|
||||
def closeEvent(self, ev):
|
||||
MatplotlibExporter.windows.remove(self)
|
||||
|
||||
|
68
pyqtgraph/exporters/PrintExporter.py
Normal file
68
pyqtgraph/exporters/PrintExporter.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
from .Exporter import Exporter
|
||||
from ..parametertree import Parameter
|
||||
from ..Qt import QtGui, QtCore, QtSvg
|
||||
import re
|
||||
|
||||
__all__ = ['PrintExporter']
|
||||
#__all__ = [] ## Printer is disabled for now--does not work very well.
|
||||
|
||||
class PrintExporter(Exporter):
|
||||
Name = "Printer"
|
||||
def __init__(self, item):
|
||||
Exporter.__init__(self, item)
|
||||
tr = self.getTargetRect()
|
||||
self.params = Parameter(name='params', type='group', children=[
|
||||
{'name': 'width', 'type': 'float', 'value': 0.1, 'limits': (0, None), 'suffix': 'm', 'siPrefix': True},
|
||||
{'name': 'height', 'type': 'float', 'value': (0.1 * tr.height()) / tr.width(), 'limits': (0, None), 'suffix': 'm', 'siPrefix': True},
|
||||
])
|
||||
self.params.param('width').sigValueChanged.connect(self.widthChanged)
|
||||
self.params.param('height').sigValueChanged.connect(self.heightChanged)
|
||||
|
||||
def widthChanged(self):
|
||||
sr = self.getSourceRect()
|
||||
ar = sr.height() / sr.width()
|
||||
self.params.param('height').setValue(self.params['width'] * ar, blockSignal=self.heightChanged)
|
||||
|
||||
def heightChanged(self):
|
||||
sr = self.getSourceRect()
|
||||
ar = sr.width() / sr.height()
|
||||
self.params.param('width').setValue(self.params['height'] * ar, blockSignal=self.widthChanged)
|
||||
|
||||
def parameters(self):
|
||||
return self.params
|
||||
|
||||
def export(self, fileName=None):
|
||||
printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution)
|
||||
dialog = QtGui.QPrintDialog(printer)
|
||||
dialog.setWindowTitle("Print Document")
|
||||
if dialog.exec_() != QtGui.QDialog.Accepted:
|
||||
return
|
||||
|
||||
#dpi = QtGui.QDesktopWidget().physicalDpiX()
|
||||
|
||||
#self.svg.setSize(QtCore.QSize(100,100))
|
||||
#self.svg.setResolution(600)
|
||||
#res = printer.resolution()
|
||||
sr = self.getSourceRect()
|
||||
#res = sr.width() * .4 / (self.params['width'] * 100 / 2.54)
|
||||
res = QtGui.QDesktopWidget().physicalDpiX()
|
||||
printer.setResolution(res)
|
||||
rect = printer.pageRect()
|
||||
center = rect.center()
|
||||
h = self.params['height'] * res * 100. / 2.54
|
||||
w = self.params['width'] * res * 100. / 2.54
|
||||
x = center.x() - w/2.
|
||||
y = center.y() - h/2.
|
||||
|
||||
targetRect = QtCore.QRect(x, y, w, h)
|
||||
sourceRect = self.getSourceRect()
|
||||
painter = QtGui.QPainter(printer)
|
||||
try:
|
||||
self.setExportMode(True, {'painter': painter})
|
||||
self.getScene().render(painter, QtCore.QRectF(targetRect), QtCore.QRectF(sourceRect))
|
||||
finally:
|
||||
self.setExportMode(False)
|
||||
painter.end()
|
||||
|
||||
|
||||
#PrintExporter.register()
|
442
pyqtgraph/exporters/SVGExporter.py
Normal file
442
pyqtgraph/exporters/SVGExporter.py
Normal file
|
@ -0,0 +1,442 @@
|
|||
from .Exporter import Exporter
|
||||
from ..python2_3 import asUnicode
|
||||
from ..parametertree import Parameter
|
||||
from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE
|
||||
from .. import debug
|
||||
from .. import functions as fn
|
||||
import re
|
||||
import xml.dom.minidom as xml
|
||||
import numpy as np
|
||||
|
||||
|
||||
__all__ = ['SVGExporter']
|
||||
|
||||
class SVGExporter(Exporter):
|
||||
Name = "Scalable Vector Graphics (SVG)"
|
||||
allowCopy=True
|
||||
|
||||
def __init__(self, item):
|
||||
Exporter.__init__(self, item)
|
||||
#tr = self.getTargetRect()
|
||||
self.params = Parameter(name='params', type='group', children=[
|
||||
#{'name': 'width', 'type': 'float', 'value': tr.width(), 'limits': (0, None)},
|
||||
#{'name': 'height', 'type': 'float', 'value': tr.height(), 'limits': (0, None)},
|
||||
#{'name': 'viewbox clipping', 'type': 'bool', 'value': True},
|
||||
#{'name': 'normalize coordinates', 'type': 'bool', 'value': True},
|
||||
#{'name': 'normalize line width', 'type': 'bool', 'value': True},
|
||||
])
|
||||
#self.params.param('width').sigValueChanged.connect(self.widthChanged)
|
||||
#self.params.param('height').sigValueChanged.connect(self.heightChanged)
|
||||
|
||||
def widthChanged(self):
|
||||
sr = self.getSourceRect()
|
||||
ar = sr.height() / sr.width()
|
||||
self.params.param('height').setValue(self.params['width'] * ar, blockSignal=self.heightChanged)
|
||||
|
||||
def heightChanged(self):
|
||||
sr = self.getSourceRect()
|
||||
ar = sr.width() / sr.height()
|
||||
self.params.param('width').setValue(self.params['height'] * ar, blockSignal=self.widthChanged)
|
||||
|
||||
def parameters(self):
|
||||
return self.params
|
||||
|
||||
def export(self, fileName=None, toBytes=False, copy=False):
|
||||
if toBytes is False and copy is False and fileName is None:
|
||||
self.fileSaveDialog(filter="Scalable Vector Graphics (*.svg)")
|
||||
return
|
||||
|
||||
## Qt's SVG generator is not complete. (notably, it lacks clipping)
|
||||
## Instead, we will use Qt to generate SVG for each item independently,
|
||||
## then manually reconstruct the entire document.
|
||||
xml = generateSvg(self.item)
|
||||
|
||||
if toBytes:
|
||||
return xml.encode('UTF-8')
|
||||
elif copy:
|
||||
md = QtCore.QMimeData()
|
||||
md.setData('image/svg+xml', QtCore.QByteArray(xml.encode('UTF-8')))
|
||||
QtGui.QApplication.clipboard().setMimeData(md)
|
||||
else:
|
||||
with open(fileName, 'wb') as fh:
|
||||
fh.write(asUnicode(xml).encode('utf-8'))
|
||||
|
||||
|
||||
xmlHeader = """\
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny">
|
||||
<title>pyqtgraph SVG export</title>
|
||||
<desc>Generated with Qt and pyqtgraph</desc>
|
||||
"""
|
||||
|
||||
def generateSvg(item):
|
||||
global xmlHeader
|
||||
try:
|
||||
node, defs = _generateItemSvg(item)
|
||||
finally:
|
||||
## reset export mode for all items in the tree
|
||||
if isinstance(item, QtGui.QGraphicsScene):
|
||||
items = item.items()
|
||||
else:
|
||||
items = [item]
|
||||
for i in items:
|
||||
items.extend(i.childItems())
|
||||
for i in items:
|
||||
if hasattr(i, 'setExportMode'):
|
||||
i.setExportMode(False)
|
||||
|
||||
cleanXml(node)
|
||||
|
||||
defsXml = "<defs>\n"
|
||||
for d in defs:
|
||||
defsXml += d.toprettyxml(indent=' ')
|
||||
defsXml += "</defs>\n"
|
||||
return xmlHeader + defsXml + node.toprettyxml(indent=' ') + "\n</svg>\n"
|
||||
|
||||
|
||||
def _generateItemSvg(item, nodes=None, root=None):
|
||||
## This function is intended to work around some issues with Qt's SVG generator
|
||||
## and SVG in general.
|
||||
## 1) Qt SVG does not implement clipping paths. This is absurd.
|
||||
## The solution is to let Qt generate SVG for each item independently,
|
||||
## then glue them together manually with clipping.
|
||||
##
|
||||
## The format Qt generates for all items looks like this:
|
||||
##
|
||||
## <g>
|
||||
## <g transform="matrix(...)">
|
||||
## one or more of: <path/> or <polyline/> or <text/>
|
||||
## </g>
|
||||
## <g transform="matrix(...)">
|
||||
## one or more of: <path/> or <polyline/> or <text/>
|
||||
## </g>
|
||||
## . . .
|
||||
## </g>
|
||||
##
|
||||
## 2) There seems to be wide disagreement over whether path strokes
|
||||
## should be scaled anisotropically.
|
||||
## see: http://web.mit.edu/jonas/www/anisotropy/
|
||||
## Given that both inkscape and illustrator seem to prefer isotropic
|
||||
## scaling, we will optimize for those cases.
|
||||
##
|
||||
## 3) Qt generates paths using non-scaling-stroke from SVG 1.2, but
|
||||
## inkscape only supports 1.1.
|
||||
##
|
||||
## Both 2 and 3 can be addressed by drawing all items in world coordinates.
|
||||
|
||||
profiler = debug.Profiler()
|
||||
|
||||
if nodes is None: ## nodes maps all node IDs to their XML element.
|
||||
## this allows us to ensure all elements receive unique names.
|
||||
nodes = {}
|
||||
|
||||
if root is None:
|
||||
root = item
|
||||
|
||||
## Skip hidden items
|
||||
if hasattr(item, 'isVisible') and not item.isVisible():
|
||||
return None
|
||||
|
||||
## If this item defines its own SVG generator, use that.
|
||||
if hasattr(item, 'generateSvg'):
|
||||
return item.generateSvg(nodes)
|
||||
|
||||
|
||||
## Generate SVG text for just this item (exclude its children; we'll handle them later)
|
||||
tr = QtGui.QTransform()
|
||||
if isinstance(item, QtGui.QGraphicsScene):
|
||||
xmlStr = "<g>\n</g>\n"
|
||||
doc = xml.parseString(xmlStr)
|
||||
childs = [i for i in item.items() if i.parentItem() is None]
|
||||
elif item.__class__.paint == QtGui.QGraphicsItem.paint:
|
||||
xmlStr = "<g>\n</g>\n"
|
||||
doc = xml.parseString(xmlStr)
|
||||
childs = item.childItems()
|
||||
else:
|
||||
childs = item.childItems()
|
||||
tr = itemTransform(item, item.scene())
|
||||
|
||||
## offset to corner of root item
|
||||
if isinstance(root, QtGui.QGraphicsScene):
|
||||
rootPos = QtCore.QPoint(0,0)
|
||||
else:
|
||||
rootPos = root.scenePos()
|
||||
tr2 = QtGui.QTransform()
|
||||
tr2.translate(-rootPos.x(), -rootPos.y())
|
||||
tr = tr * tr2
|
||||
|
||||
arr = QtCore.QByteArray()
|
||||
buf = QtCore.QBuffer(arr)
|
||||
svg = QtSvg.QSvgGenerator()
|
||||
svg.setOutputDevice(buf)
|
||||
dpi = QtGui.QDesktopWidget().physicalDpiX()
|
||||
svg.setResolution(dpi)
|
||||
|
||||
p = QtGui.QPainter()
|
||||
p.begin(svg)
|
||||
if hasattr(item, 'setExportMode'):
|
||||
item.setExportMode(True, {'painter': p})
|
||||
try:
|
||||
p.setTransform(tr)
|
||||
item.paint(p, QtGui.QStyleOptionGraphicsItem(), None)
|
||||
finally:
|
||||
p.end()
|
||||
## Can't do this here--we need to wait until all children have painted as well.
|
||||
## this is taken care of in generateSvg instead.
|
||||
#if hasattr(item, 'setExportMode'):
|
||||
#item.setExportMode(False)
|
||||
|
||||
if USE_PYSIDE:
|
||||
xmlStr = str(arr)
|
||||
else:
|
||||
xmlStr = bytes(arr).decode('utf-8')
|
||||
doc = xml.parseString(xmlStr)
|
||||
|
||||
try:
|
||||
## Get top-level group for this item
|
||||
g1 = doc.getElementsByTagName('g')[0]
|
||||
## get list of sub-groups
|
||||
g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g']
|
||||
|
||||
defs = doc.getElementsByTagName('defs')
|
||||
if len(defs) > 0:
|
||||
defs = [n for n in defs[0].childNodes if isinstance(n, xml.Element)]
|
||||
except:
|
||||
print(doc.toxml())
|
||||
raise
|
||||
|
||||
profiler('render')
|
||||
|
||||
## Get rid of group transformation matrices by applying
|
||||
## transformation to inner coordinates
|
||||
correctCoordinates(g1, defs, item)
|
||||
profiler('correct')
|
||||
## make sure g1 has the transformation matrix
|
||||
#m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32())
|
||||
#g1.setAttribute('transform', "matrix(%f,%f,%f,%f,%f,%f)" % m)
|
||||
|
||||
#print "=================",item,"====================="
|
||||
#print g1.toprettyxml(indent=" ", newl='')
|
||||
|
||||
## Inkscape does not support non-scaling-stroke (this is SVG 1.2, inkscape supports 1.1)
|
||||
## So we need to correct anything attempting to use this.
|
||||
#correctStroke(g1, item, root)
|
||||
|
||||
## decide on a name for this item
|
||||
baseName = item.__class__.__name__
|
||||
i = 1
|
||||
while True:
|
||||
name = baseName + "_%d" % i
|
||||
if name not in nodes:
|
||||
break
|
||||
i += 1
|
||||
nodes[name] = g1
|
||||
g1.setAttribute('id', name)
|
||||
|
||||
## If this item clips its children, we need to take care of that.
|
||||
childGroup = g1 ## add children directly to this node unless we are clipping
|
||||
if not isinstance(item, QtGui.QGraphicsScene):
|
||||
## See if this item clips its children
|
||||
if int(item.flags() & item.ItemClipsChildrenToShape) > 0:
|
||||
## Generate svg for just the path
|
||||
#if isinstance(root, QtGui.QGraphicsScene):
|
||||
#path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape()))
|
||||
#else:
|
||||
#path = QtGui.QGraphicsPathItem(root.mapToParent(item.mapToItem(root, item.shape())))
|
||||
path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape()))
|
||||
item.scene().addItem(path)
|
||||
try:
|
||||
#pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0]
|
||||
pathNode = _generateItemSvg(path, root=root)[0].getElementsByTagName('path')[0]
|
||||
# assume <defs> for this path is empty.. possibly problematic.
|
||||
finally:
|
||||
item.scene().removeItem(path)
|
||||
|
||||
## and for the clipPath element
|
||||
clip = name + '_clip'
|
||||
clipNode = g1.ownerDocument.createElement('clipPath')
|
||||
clipNode.setAttribute('id', clip)
|
||||
clipNode.appendChild(pathNode)
|
||||
g1.appendChild(clipNode)
|
||||
|
||||
childGroup = g1.ownerDocument.createElement('g')
|
||||
childGroup.setAttribute('clip-path', 'url(#%s)' % clip)
|
||||
g1.appendChild(childGroup)
|
||||
profiler('clipping')
|
||||
|
||||
## Add all child items as sub-elements.
|
||||
childs.sort(key=lambda c: c.zValue())
|
||||
for ch in childs:
|
||||
csvg = _generateItemSvg(ch, nodes, root)
|
||||
if csvg is None:
|
||||
continue
|
||||
cg, cdefs = csvg
|
||||
childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now)
|
||||
defs.extend(cdefs)
|
||||
|
||||
profiler('children')
|
||||
return g1, defs
|
||||
|
||||
def correctCoordinates(node, defs, item):
|
||||
# TODO: correct gradient coordinates inside defs
|
||||
|
||||
## Remove transformation matrices from <g> tags by applying matrix to coordinates inside.
|
||||
## Each item is represented by a single top-level group with one or more groups inside.
|
||||
## Each inner group contains one or more drawing primitives, possibly of different types.
|
||||
groups = node.getElementsByTagName('g')
|
||||
|
||||
## Since we leave text unchanged, groups which combine text and non-text primitives must be split apart.
|
||||
## (if at some point we start correcting text transforms as well, then it should be safe to remove this)
|
||||
groups2 = []
|
||||
for grp in groups:
|
||||
subGroups = [grp.cloneNode(deep=False)]
|
||||
textGroup = None
|
||||
for ch in grp.childNodes[:]:
|
||||
if isinstance(ch, xml.Element):
|
||||
if textGroup is None:
|
||||
textGroup = ch.tagName == 'text'
|
||||
if ch.tagName == 'text':
|
||||
if textGroup is False:
|
||||
subGroups.append(grp.cloneNode(deep=False))
|
||||
textGroup = True
|
||||
else:
|
||||
if textGroup is True:
|
||||
subGroups.append(grp.cloneNode(deep=False))
|
||||
textGroup = False
|
||||
subGroups[-1].appendChild(ch)
|
||||
groups2.extend(subGroups)
|
||||
for sg in subGroups:
|
||||
node.insertBefore(sg, grp)
|
||||
node.removeChild(grp)
|
||||
groups = groups2
|
||||
|
||||
|
||||
for grp in groups:
|
||||
matrix = grp.getAttribute('transform')
|
||||
match = re.match(r'matrix\((.*)\)', matrix)
|
||||
if match is None:
|
||||
vals = [1,0,0,1,0,0]
|
||||
else:
|
||||
vals = [float(a) for a in match.groups()[0].split(',')]
|
||||
tr = np.array([[vals[0], vals[2], vals[4]], [vals[1], vals[3], vals[5]]])
|
||||
|
||||
removeTransform = False
|
||||
for ch in grp.childNodes:
|
||||
if not isinstance(ch, xml.Element):
|
||||
continue
|
||||
if ch.tagName == 'polyline':
|
||||
removeTransform = True
|
||||
coords = np.array([[float(a) for a in c.split(',')] for c in ch.getAttribute('points').strip().split(' ')])
|
||||
coords = fn.transformCoordinates(tr, coords, transpose=True)
|
||||
ch.setAttribute('points', ' '.join([','.join([str(a) for a in c]) for c in coords]))
|
||||
elif ch.tagName == 'path':
|
||||
removeTransform = True
|
||||
newCoords = ''
|
||||
oldCoords = ch.getAttribute('d').strip()
|
||||
if oldCoords == '':
|
||||
continue
|
||||
for c in oldCoords.split(' '):
|
||||
x,y = c.split(',')
|
||||
if x[0].isalpha():
|
||||
t = x[0]
|
||||
x = x[1:]
|
||||
else:
|
||||
t = ''
|
||||
nc = fn.transformCoordinates(tr, np.array([[float(x),float(y)]]), transpose=True)
|
||||
newCoords += t+str(nc[0,0])+','+str(nc[0,1])+' '
|
||||
ch.setAttribute('d', newCoords)
|
||||
elif ch.tagName == 'text':
|
||||
removeTransform = False
|
||||
## leave text alone for now. Might need this later to correctly render text with outline.
|
||||
#c = np.array([
|
||||
#[float(ch.getAttribute('x')), float(ch.getAttribute('y'))],
|
||||
#[float(ch.getAttribute('font-size')), 0],
|
||||
#[0,0]])
|
||||
#c = fn.transformCoordinates(tr, c, transpose=True)
|
||||
#ch.setAttribute('x', str(c[0,0]))
|
||||
#ch.setAttribute('y', str(c[0,1]))
|
||||
#fs = c[1]-c[2]
|
||||
#fs = (fs**2).sum()**0.5
|
||||
#ch.setAttribute('font-size', str(fs))
|
||||
|
||||
## Correct some font information
|
||||
families = ch.getAttribute('font-family').split(',')
|
||||
if len(families) == 1:
|
||||
font = QtGui.QFont(families[0].strip('" '))
|
||||
if font.style() == font.SansSerif:
|
||||
families.append('sans-serif')
|
||||
elif font.style() == font.Serif:
|
||||
families.append('serif')
|
||||
elif font.style() == font.Courier:
|
||||
families.append('monospace')
|
||||
ch.setAttribute('font-family', ', '.join([f if ' ' not in f else '"%s"'%f for f in families]))
|
||||
|
||||
## correct line widths if needed
|
||||
if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke':
|
||||
w = float(grp.getAttribute('stroke-width'))
|
||||
s = fn.transformCoordinates(tr, np.array([[w,0], [0,0]]), transpose=True)
|
||||
w = ((s[0]-s[1])**2).sum()**0.5
|
||||
ch.setAttribute('stroke-width', str(w))
|
||||
|
||||
if removeTransform:
|
||||
grp.removeAttribute('transform')
|
||||
|
||||
|
||||
SVGExporter.register()
|
||||
|
||||
|
||||
def itemTransform(item, root):
|
||||
## Return the transformation mapping item to root
|
||||
## (actually to parent coordinate system of root)
|
||||
|
||||
if item is root:
|
||||
tr = QtGui.QTransform()
|
||||
tr.translate(*item.pos())
|
||||
tr = tr * item.transform()
|
||||
return tr
|
||||
|
||||
|
||||
if int(item.flags() & item.ItemIgnoresTransformations) > 0:
|
||||
pos = item.pos()
|
||||
parent = item.parentItem()
|
||||
if parent is not None:
|
||||
pos = itemTransform(parent, root).map(pos)
|
||||
tr = QtGui.QTransform()
|
||||
tr.translate(pos.x(), pos.y())
|
||||
tr = item.transform() * tr
|
||||
else:
|
||||
## find next parent that is either the root item or
|
||||
## an item that ignores its transformation
|
||||
nextRoot = item
|
||||
while True:
|
||||
nextRoot = nextRoot.parentItem()
|
||||
if nextRoot is None:
|
||||
nextRoot = root
|
||||
break
|
||||
if nextRoot is root or int(nextRoot.flags() & nextRoot.ItemIgnoresTransformations) > 0:
|
||||
break
|
||||
|
||||
if isinstance(nextRoot, QtGui.QGraphicsScene):
|
||||
tr = item.sceneTransform()
|
||||
else:
|
||||
tr = itemTransform(nextRoot, root) * item.itemTransform(nextRoot)[0]
|
||||
|
||||
return tr
|
||||
|
||||
|
||||
def cleanXml(node):
|
||||
## remove extraneous text; let the xml library do the formatting.
|
||||
hasElement = False
|
||||
nonElement = []
|
||||
for ch in node.childNodes:
|
||||
if isinstance(ch, xml.Element):
|
||||
hasElement = True
|
||||
cleanXml(ch)
|
||||
else:
|
||||
nonElement.append(ch)
|
||||
|
||||
if hasElement:
|
||||
for ch in nonElement:
|
||||
node.removeChild(ch)
|
||||
elif node.tagName == 'g': ## remove childless groups
|
||||
node.parentNode.removeChild(node)
|
11
pyqtgraph/exporters/__init__.py
Normal file
11
pyqtgraph/exporters/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from .Exporter import Exporter
|
||||
from .ImageExporter import *
|
||||
from .SVGExporter import *
|
||||
from .Matplotlib import *
|
||||
from .CSVExporter import *
|
||||
from .PrintExporter import *
|
||||
from .HDF5Exporter import *
|
||||
|
||||
def listExporters():
|
||||
return Exporter.Exporters[:]
|
||||
|
49
pyqtgraph/exporters/tests/test_csv.py
Normal file
49
pyqtgraph/exporters/tests/test_csv.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
"""
|
||||
SVG export test
|
||||
"""
|
||||
import pyqtgraph as pg
|
||||
import pyqtgraph.exporters
|
||||
import csv
|
||||
|
||||
app = pg.mkQApp()
|
||||
|
||||
def approxeq(a, b):
|
||||
return (a-b) <= ((a + b) * 1e-6)
|
||||
|
||||
def test_CSVExporter():
|
||||
plt = pg.plot()
|
||||
y1 = [1,3,2,3,1,6,9,8,4,2]
|
||||
plt.plot(y=y1, name='myPlot')
|
||||
|
||||
y2 = [3,4,6,1,2,4,2,3,5,3,5,1,3]
|
||||
x2 = pg.np.linspace(0, 1.0, len(y2))
|
||||
plt.plot(x=x2, y=y2)
|
||||
|
||||
y3 = [1,5,2,3,4,6,1,2,4,2,3,5,3]
|
||||
x3 = pg.np.linspace(0, 1.0, len(y3)+1)
|
||||
plt.plot(x=x3, y=y3, stepMode=True)
|
||||
|
||||
ex = pg.exporters.CSVExporter(plt.plotItem)
|
||||
ex.export(fileName='test.csv')
|
||||
|
||||
r = csv.reader(open('test.csv', 'r'))
|
||||
lines = [line for line in r]
|
||||
header = lines.pop(0)
|
||||
assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002']
|
||||
|
||||
i = 0
|
||||
for vals in lines:
|
||||
vals = list(map(str.strip, vals))
|
||||
assert (i >= len(y1) and vals[0] == '') or approxeq(float(vals[0]), i)
|
||||
assert (i >= len(y1) and vals[1] == '') or approxeq(float(vals[1]), y1[i])
|
||||
|
||||
assert (i >= len(x2) and vals[2] == '') or approxeq(float(vals[2]), x2[i])
|
||||
assert (i >= len(y2) and vals[3] == '') or approxeq(float(vals[3]), y2[i])
|
||||
|
||||
assert (i >= len(x3) and vals[4] == '') or approxeq(float(vals[4]), x3[i])
|
||||
assert (i >= len(y3) and vals[5] == '') or approxeq(float(vals[5]), y3[i])
|
||||
i += 1
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_CSVExporter()
|
||||
|
67
pyqtgraph/exporters/tests/test_svg.py
Normal file
67
pyqtgraph/exporters/tests/test_svg.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
"""
|
||||
SVG export test
|
||||
"""
|
||||
import pyqtgraph as pg
|
||||
import pyqtgraph.exporters
|
||||
app = pg.mkQApp()
|
||||
|
||||
def test_plotscene():
|
||||
pg.setConfigOption('foreground', (0,0,0))
|
||||
w = pg.GraphicsWindow()
|
||||
w.show()
|
||||
p1 = w.addPlot()
|
||||
p2 = w.addPlot()
|
||||
p1.plot([1,3,2,3,1,6,9,8,4,2,3,5,3], pen={'color':'k'})
|
||||
p1.setXRange(0,5)
|
||||
p2.plot([1,5,2,3,4,6,1,2,4,2,3,5,3], pen={'color':'k', 'cosmetic':False, 'width': 0.3})
|
||||
app.processEvents()
|
||||
app.processEvents()
|
||||
|
||||
ex = pg.exporters.SVGExporter(w.scene())
|
||||
ex.export(fileName='test.svg')
|
||||
|
||||
|
||||
def test_simple():
|
||||
scene = pg.QtGui.QGraphicsScene()
|
||||
#rect = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100)
|
||||
#scene.addItem(rect)
|
||||
#rect.setPos(20,20)
|
||||
#rect.translate(50, 50)
|
||||
#rect.rotate(30)
|
||||
#rect.scale(0.5, 0.5)
|
||||
|
||||
#rect1 = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100)
|
||||
#rect1.setParentItem(rect)
|
||||
#rect1.setFlag(rect1.ItemIgnoresTransformations)
|
||||
#rect1.setPos(20, 20)
|
||||
#rect1.scale(2,2)
|
||||
|
||||
#el1 = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 100)
|
||||
#el1.setParentItem(rect1)
|
||||
##grp = pg.ItemGroup()
|
||||
#grp.setParentItem(rect)
|
||||
#grp.translate(200,0)
|
||||
##grp.rotate(30)
|
||||
|
||||
#rect2 = pg.QtGui.QGraphicsRectItem(0, 0, 100, 25)
|
||||
#rect2.setFlag(rect2.ItemClipsChildrenToShape)
|
||||
#rect2.setParentItem(grp)
|
||||
#rect2.setPos(0,25)
|
||||
#rect2.rotate(30)
|
||||
#el = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 50)
|
||||
#el.translate(10,-5)
|
||||
#el.scale(0.5,2)
|
||||
#el.setParentItem(rect2)
|
||||
|
||||
grp2 = pg.ItemGroup()
|
||||
scene.addItem(grp2)
|
||||
grp2.scale(100,100)
|
||||
|
||||
rect3 = pg.QtGui.QGraphicsRectItem(0,0,2,2)
|
||||
rect3.setPen(pg.mkPen(width=1, cosmetic=False))
|
||||
grp2.addItem(rect3)
|
||||
|
||||
ex = pg.exporters.SVGExporter(scene)
|
||||
ex.export(fileName='test.svg')
|
||||
|
||||
|
938
pyqtgraph/flowchart/Flowchart.py
Normal file
938
pyqtgraph/flowchart/Flowchart.py
Normal file
|
@ -0,0 +1,938 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYQT5
|
||||
from .Node import *
|
||||
from ..pgcollections import OrderedDict
|
||||
from ..widgets.TreeWidget import *
|
||||
from .. import FileDialog, DataTreeWidget
|
||||
|
||||
## pyside and pyqt use incompatible ui files.
|
||||
if USE_PYSIDE:
|
||||
from . import FlowchartTemplate_pyside as FlowchartTemplate
|
||||
from . import FlowchartCtrlTemplate_pyside as FlowchartCtrlTemplate
|
||||
elif USE_PYQT5:
|
||||
from . import FlowchartTemplate_pyqt5 as FlowchartTemplate
|
||||
from . import FlowchartCtrlTemplate_pyqt5 as FlowchartCtrlTemplate
|
||||
else:
|
||||
from . import FlowchartTemplate_pyqt as FlowchartTemplate
|
||||
from . import FlowchartCtrlTemplate_pyqt as FlowchartCtrlTemplate
|
||||
|
||||
from .Terminal import Terminal
|
||||
from numpy import ndarray
|
||||
from .library import LIBRARY
|
||||
from ..debug import printExc
|
||||
from .. import configfile as configfile
|
||||
from .. import dockarea as dockarea
|
||||
from . import FlowchartGraphicsView
|
||||
from .. import functions as fn
|
||||
|
||||
def strDict(d):
|
||||
return dict([(str(k), v) for k, v in d.items()])
|
||||
|
||||
|
||||
|
||||
|
||||
class Flowchart(Node):
|
||||
sigFileLoaded = QtCore.Signal(object)
|
||||
sigFileSaved = QtCore.Signal(object)
|
||||
|
||||
|
||||
#sigOutputChanged = QtCore.Signal() ## inherited from Node
|
||||
sigChartLoaded = QtCore.Signal()
|
||||
sigStateChanged = QtCore.Signal() # called when output is expected to have changed
|
||||
sigChartChanged = QtCore.Signal(object, object, object) # called when nodes are added, removed, or renamed.
|
||||
# (self, action, node)
|
||||
|
||||
def __init__(self, terminals=None, name=None, filePath=None, library=None):
|
||||
self.library = library or LIBRARY
|
||||
if name is None:
|
||||
name = "Flowchart"
|
||||
if terminals is None:
|
||||
terminals = {}
|
||||
self.filePath = filePath
|
||||
Node.__init__(self, name, allowAddInput=True, allowAddOutput=True) ## create node without terminals; we'll add these later
|
||||
|
||||
|
||||
self.inputWasSet = False ## flag allows detection of changes in the absence of input change.
|
||||
self._nodes = {}
|
||||
self.nextZVal = 10
|
||||
#self.connects = []
|
||||
#self._chartGraphicsItem = FlowchartGraphicsItem(self)
|
||||
self._widget = None
|
||||
self._scene = None
|
||||
self.processing = False ## flag that prevents recursive node updates
|
||||
|
||||
self.widget()
|
||||
|
||||
self.inputNode = Node('Input', allowRemove=False, allowAddOutput=True)
|
||||
self.outputNode = Node('Output', allowRemove=False, allowAddInput=True)
|
||||
self.addNode(self.inputNode, 'Input', [-150, 0])
|
||||
self.addNode(self.outputNode, 'Output', [300, 0])
|
||||
|
||||
self.outputNode.sigOutputChanged.connect(self.outputChanged)
|
||||
self.outputNode.sigTerminalRenamed.connect(self.internalTerminalRenamed)
|
||||
self.inputNode.sigTerminalRenamed.connect(self.internalTerminalRenamed)
|
||||
self.outputNode.sigTerminalRemoved.connect(self.internalTerminalRemoved)
|
||||
self.inputNode.sigTerminalRemoved.connect(self.internalTerminalRemoved)
|
||||
self.outputNode.sigTerminalAdded.connect(self.internalTerminalAdded)
|
||||
self.inputNode.sigTerminalAdded.connect(self.internalTerminalAdded)
|
||||
|
||||
self.viewBox.autoRange(padding = 0.04)
|
||||
|
||||
for name, opts in terminals.items():
|
||||
self.addTerminal(name, **opts)
|
||||
|
||||
def setLibrary(self, lib):
|
||||
self.library = lib
|
||||
self.widget().chartWidget.buildMenu()
|
||||
|
||||
def setInput(self, **args):
|
||||
"""Set the input values of the flowchart. This will automatically propagate
|
||||
the new values throughout the flowchart, (possibly) causing the output to change.
|
||||
"""
|
||||
#print "setInput", args
|
||||
#Node.setInput(self, **args)
|
||||
#print " ....."
|
||||
self.inputWasSet = True
|
||||
self.inputNode.setOutput(**args)
|
||||
|
||||
def outputChanged(self):
|
||||
## called when output of internal node has changed
|
||||
vals = self.outputNode.inputValues()
|
||||
self.widget().outputChanged(vals)
|
||||
self.setOutput(**vals)
|
||||
#self.sigOutputChanged.emit(self)
|
||||
|
||||
def output(self):
|
||||
"""Return a dict of the values on the Flowchart's output terminals.
|
||||
"""
|
||||
return self.outputNode.inputValues()
|
||||
|
||||
def nodes(self):
|
||||
return self._nodes
|
||||
|
||||
def addTerminal(self, name, **opts):
|
||||
term = Node.addTerminal(self, name, **opts)
|
||||
name = term.name()
|
||||
if opts['io'] == 'in': ## inputs to the flowchart become outputs on the input node
|
||||
opts['io'] = 'out'
|
||||
opts['multi'] = False
|
||||
self.inputNode.sigTerminalAdded.disconnect(self.internalTerminalAdded)
|
||||
try:
|
||||
term2 = self.inputNode.addTerminal(name, **opts)
|
||||
finally:
|
||||
self.inputNode.sigTerminalAdded.connect(self.internalTerminalAdded)
|
||||
|
||||
else:
|
||||
opts['io'] = 'in'
|
||||
#opts['multi'] = False
|
||||
self.outputNode.sigTerminalAdded.disconnect(self.internalTerminalAdded)
|
||||
try:
|
||||
term2 = self.outputNode.addTerminal(name, **opts)
|
||||
finally:
|
||||
self.outputNode.sigTerminalAdded.connect(self.internalTerminalAdded)
|
||||
return term
|
||||
|
||||
def removeTerminal(self, name):
|
||||
#print "remove:", name
|
||||
term = self[name]
|
||||
inTerm = self.internalTerminal(term)
|
||||
Node.removeTerminal(self, name)
|
||||
inTerm.node().removeTerminal(inTerm.name())
|
||||
|
||||
def internalTerminalRenamed(self, term, oldName):
|
||||
self[oldName].rename(term.name())
|
||||
|
||||
def internalTerminalAdded(self, node, term):
|
||||
if term._io == 'in':
|
||||
io = 'out'
|
||||
else:
|
||||
io = 'in'
|
||||
Node.addTerminal(self, term.name(), io=io, renamable=term.isRenamable(), removable=term.isRemovable(), multiable=term.isMultiable())
|
||||
|
||||
def internalTerminalRemoved(self, node, term):
|
||||
try:
|
||||
Node.removeTerminal(self, term.name())
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def terminalRenamed(self, term, oldName):
|
||||
newName = term.name()
|
||||
#print "flowchart rename", newName, oldName
|
||||
#print self.terminals
|
||||
Node.terminalRenamed(self, self[oldName], oldName)
|
||||
#print self.terminals
|
||||
for n in [self.inputNode, self.outputNode]:
|
||||
if oldName in n.terminals:
|
||||
n[oldName].rename(newName)
|
||||
|
||||
def createNode(self, nodeType, name=None, pos=None):
|
||||
if name is None:
|
||||
n = 0
|
||||
while True:
|
||||
name = "%s.%d" % (nodeType, n)
|
||||
if name not in self._nodes:
|
||||
break
|
||||
n += 1
|
||||
|
||||
node = self.library.getNodeType(nodeType)(name)
|
||||
self.addNode(node, name, pos)
|
||||
return node
|
||||
|
||||
def addNode(self, node, name, pos=None):
|
||||
if pos is None:
|
||||
pos = [0, 0]
|
||||
if type(pos) in [QtCore.QPoint, QtCore.QPointF]:
|
||||
pos = [pos.x(), pos.y()]
|
||||
item = node.graphicsItem()
|
||||
item.setZValue(self.nextZVal*2)
|
||||
self.nextZVal += 1
|
||||
self.viewBox.addItem(item)
|
||||
item.moveBy(*pos)
|
||||
self._nodes[name] = node
|
||||
self.widget().addNode(node)
|
||||
node.sigClosed.connect(self.nodeClosed)
|
||||
node.sigRenamed.connect(self.nodeRenamed)
|
||||
node.sigOutputChanged.connect(self.nodeOutputChanged)
|
||||
self.sigChartChanged.emit(self, 'add', node)
|
||||
|
||||
def removeNode(self, node):
|
||||
node.close()
|
||||
|
||||
def nodeClosed(self, node):
|
||||
del self._nodes[node.name()]
|
||||
self.widget().removeNode(node)
|
||||
for signal in ['sigClosed', 'sigRenamed', 'sigOutputChanged']:
|
||||
try:
|
||||
getattr(node, signal).disconnect(self.nodeClosed)
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
self.sigChartChanged.emit(self, 'remove', node)
|
||||
|
||||
def nodeRenamed(self, node, oldName):
|
||||
del self._nodes[oldName]
|
||||
self._nodes[node.name()] = node
|
||||
self.widget().nodeRenamed(node, oldName)
|
||||
self.sigChartChanged.emit(self, 'rename', node)
|
||||
|
||||
def arrangeNodes(self):
|
||||
pass
|
||||
|
||||
def internalTerminal(self, term):
|
||||
"""If the terminal belongs to the external Node, return the corresponding internal terminal"""
|
||||
if term.node() is self:
|
||||
if term.isInput():
|
||||
return self.inputNode[term.name()]
|
||||
else:
|
||||
return self.outputNode[term.name()]
|
||||
else:
|
||||
return term
|
||||
|
||||
def connectTerminals(self, term1, term2):
|
||||
"""Connect two terminals together within this flowchart."""
|
||||
term1 = self.internalTerminal(term1)
|
||||
term2 = self.internalTerminal(term2)
|
||||
term1.connectTo(term2)
|
||||
|
||||
|
||||
def process(self, **args):
|
||||
"""
|
||||
Process data through the flowchart, returning the output.
|
||||
|
||||
Keyword arguments must be the names of input terminals.
|
||||
The return value is a dict with one key per output terminal.
|
||||
|
||||
"""
|
||||
data = {} ## Stores terminal:value pairs
|
||||
|
||||
## determine order of operations
|
||||
## order should look like [('p', node1), ('p', node2), ('d', terminal1), ...]
|
||||
## Each tuple specifies either (p)rocess this node or (d)elete the result from this terminal
|
||||
order = self.processOrder()
|
||||
#print "ORDER:", order
|
||||
|
||||
## Record inputs given to process()
|
||||
for n, t in self.inputNode.outputs().items():
|
||||
# if n not in args:
|
||||
# raise Exception("Parameter %s required to process this chart." % n)
|
||||
if n in args:
|
||||
data[t] = args[n]
|
||||
|
||||
ret = {}
|
||||
|
||||
## process all in order
|
||||
for c, arg in order:
|
||||
|
||||
if c == 'p': ## Process a single node
|
||||
#print "===> process:", arg
|
||||
node = arg
|
||||
if node is self.inputNode:
|
||||
continue ## input node has already been processed.
|
||||
|
||||
|
||||
## get input and output terminals for this node
|
||||
outs = list(node.outputs().values())
|
||||
ins = list(node.inputs().values())
|
||||
|
||||
## construct input value dictionary
|
||||
args = {}
|
||||
for inp in ins:
|
||||
inputs = inp.inputTerminals()
|
||||
if len(inputs) == 0:
|
||||
continue
|
||||
if inp.isMultiValue(): ## multi-input terminals require a dict of all inputs
|
||||
args[inp.name()] = dict([(i, data[i]) for i in inputs if i in data])
|
||||
else: ## single-inputs terminals only need the single input value available
|
||||
args[inp.name()] = data[inputs[0]]
|
||||
|
||||
if node is self.outputNode:
|
||||
ret = args ## we now have the return value, but must keep processing in case there are other endpoint nodes in the chart
|
||||
else:
|
||||
try:
|
||||
if node.isBypassed():
|
||||
result = node.processBypassed(args)
|
||||
else:
|
||||
result = node.process(display=False, **args)
|
||||
except:
|
||||
print("Error processing node %s. Args are: %s" % (str(node), str(args)))
|
||||
raise
|
||||
for out in outs:
|
||||
#print " Output:", out, out.name()
|
||||
#print out.name()
|
||||
try:
|
||||
data[out] = result[out.name()]
|
||||
except KeyError:
|
||||
pass
|
||||
elif c == 'd': ## delete a terminal result (no longer needed; may be holding a lot of memory)
|
||||
#print "===> delete", arg
|
||||
if arg in data:
|
||||
del data[arg]
|
||||
|
||||
return ret
|
||||
|
||||
def processOrder(self):
|
||||
"""Return the order of operations required to process this chart.
|
||||
The order returned should look like [('p', node1), ('p', node2), ('d', terminal1), ...]
|
||||
where each tuple specifies either (p)rocess this node or (d)elete the result from this terminal
|
||||
"""
|
||||
|
||||
## first collect list of nodes/terminals and their dependencies
|
||||
deps = {}
|
||||
tdeps = {} ## {terminal: [nodes that depend on terminal]}
|
||||
for name, node in self._nodes.items():
|
||||
deps[node] = node.dependentNodes()
|
||||
for t in node.outputs().values():
|
||||
tdeps[t] = t.dependentNodes()
|
||||
|
||||
#print "DEPS:", deps
|
||||
## determine correct node-processing order
|
||||
#deps[self] = []
|
||||
order = fn.toposort(deps)
|
||||
#print "ORDER1:", order
|
||||
|
||||
## construct list of operations
|
||||
ops = [('p', n) for n in order]
|
||||
|
||||
## determine when it is safe to delete terminal values
|
||||
dels = []
|
||||
for t, nodes in tdeps.items():
|
||||
lastInd = 0
|
||||
lastNode = None
|
||||
for n in nodes: ## determine which node is the last to be processed according to order
|
||||
if n is self:
|
||||
lastInd = None
|
||||
break
|
||||
else:
|
||||
try:
|
||||
ind = order.index(n)
|
||||
except ValueError:
|
||||
continue
|
||||
if lastNode is None or ind > lastInd:
|
||||
lastNode = n
|
||||
lastInd = ind
|
||||
#tdeps[t] = lastNode
|
||||
if lastInd is not None:
|
||||
dels.append((lastInd+1, t))
|
||||
#dels.sort(lambda a,b: cmp(b[0], a[0]))
|
||||
dels.sort(key=lambda a: a[0], reverse=True)
|
||||
for i, t in dels:
|
||||
ops.insert(i, ('d', t))
|
||||
return ops
|
||||
|
||||
|
||||
def nodeOutputChanged(self, startNode):
|
||||
"""Triggered when a node's output values have changed. (NOT called during process())
|
||||
Propagates new data forward through network."""
|
||||
## first collect list of nodes/terminals and their dependencies
|
||||
|
||||
if self.processing:
|
||||
return
|
||||
self.processing = True
|
||||
try:
|
||||
deps = {}
|
||||
for name, node in self._nodes.items():
|
||||
deps[node] = []
|
||||
for t in node.outputs().values():
|
||||
deps[node].extend(t.dependentNodes())
|
||||
|
||||
## determine order of updates
|
||||
order = fn.toposort(deps, nodes=[startNode])
|
||||
order.reverse()
|
||||
|
||||
## keep track of terminals that have been updated
|
||||
terms = set(startNode.outputs().values())
|
||||
|
||||
#print "======= Updating", startNode
|
||||
#print "Order:", order
|
||||
for node in order[1:]:
|
||||
#print "Processing node", node
|
||||
for term in list(node.inputs().values()):
|
||||
#print " checking terminal", term
|
||||
deps = list(term.connections().keys())
|
||||
update = False
|
||||
for d in deps:
|
||||
if d in terms:
|
||||
#print " ..input", d, "changed"
|
||||
update = True
|
||||
term.inputChanged(d, process=False)
|
||||
if update:
|
||||
#print " processing.."
|
||||
node.update()
|
||||
terms |= set(node.outputs().values())
|
||||
|
||||
finally:
|
||||
self.processing = False
|
||||
if self.inputWasSet:
|
||||
self.inputWasSet = False
|
||||
else:
|
||||
self.sigStateChanged.emit()
|
||||
|
||||
|
||||
|
||||
def chartGraphicsItem(self):
|
||||
"""Return the graphicsItem which displays the internals of this flowchart.
|
||||
(graphicsItem() still returns the external-view item)"""
|
||||
#return self._chartGraphicsItem
|
||||
return self.viewBox
|
||||
|
||||
def widget(self):
|
||||
if self._widget is None:
|
||||
self._widget = FlowchartCtrlWidget(self)
|
||||
self.scene = self._widget.scene()
|
||||
self.viewBox = self._widget.viewBox()
|
||||
#self._scene = QtGui.QGraphicsScene()
|
||||
#self._widget.setScene(self._scene)
|
||||
#self.scene.addItem(self.chartGraphicsItem())
|
||||
|
||||
#ci = self.chartGraphicsItem()
|
||||
#self.viewBox.addItem(ci)
|
||||
#self.viewBox.autoRange()
|
||||
return self._widget
|
||||
|
||||
def listConnections(self):
|
||||
conn = set()
|
||||
for n in self._nodes.values():
|
||||
terms = n.outputs()
|
||||
for n, t in terms.items():
|
||||
for c in t.connections():
|
||||
conn.add((t, c))
|
||||
return conn
|
||||
|
||||
def saveState(self):
|
||||
state = Node.saveState(self)
|
||||
state['nodes'] = []
|
||||
state['connects'] = []
|
||||
#state['terminals'] = self.saveTerminals()
|
||||
|
||||
for name, node in self._nodes.items():
|
||||
cls = type(node)
|
||||
if hasattr(cls, 'nodeName'):
|
||||
clsName = cls.nodeName
|
||||
pos = node.graphicsItem().pos()
|
||||
ns = {'class': clsName, 'name': name, 'pos': (pos.x(), pos.y()), 'state': node.saveState()}
|
||||
state['nodes'].append(ns)
|
||||
|
||||
conn = self.listConnections()
|
||||
for a, b in conn:
|
||||
state['connects'].append((a.node().name(), a.name(), b.node().name(), b.name()))
|
||||
|
||||
state['inputNode'] = self.inputNode.saveState()
|
||||
state['outputNode'] = self.outputNode.saveState()
|
||||
|
||||
return state
|
||||
|
||||
def restoreState(self, state, clear=False):
|
||||
self.blockSignals(True)
|
||||
try:
|
||||
if clear:
|
||||
self.clear()
|
||||
Node.restoreState(self, state)
|
||||
nodes = state['nodes']
|
||||
#nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0]))
|
||||
nodes.sort(key=lambda a: a['pos'][0])
|
||||
for n in nodes:
|
||||
if n['name'] in self._nodes:
|
||||
#self._nodes[n['name']].graphicsItem().moveBy(*n['pos'])
|
||||
self._nodes[n['name']].restoreState(n['state'])
|
||||
continue
|
||||
try:
|
||||
node = self.createNode(n['class'], name=n['name'])
|
||||
node.restoreState(n['state'])
|
||||
except:
|
||||
printExc("Error creating node %s: (continuing anyway)" % n['name'])
|
||||
#node.graphicsItem().moveBy(*n['pos'])
|
||||
|
||||
self.inputNode.restoreState(state.get('inputNode', {}))
|
||||
self.outputNode.restoreState(state.get('outputNode', {}))
|
||||
|
||||
#self.restoreTerminals(state['terminals'])
|
||||
for n1, t1, n2, t2 in state['connects']:
|
||||
try:
|
||||
self.connectTerminals(self._nodes[n1][t1], self._nodes[n2][t2])
|
||||
except:
|
||||
print(self._nodes[n1].terminals)
|
||||
print(self._nodes[n2].terminals)
|
||||
printExc("Error connecting terminals %s.%s - %s.%s:" % (n1, t1, n2, t2))
|
||||
|
||||
|
||||
finally:
|
||||
self.blockSignals(False)
|
||||
|
||||
self.sigChartLoaded.emit()
|
||||
self.outputChanged()
|
||||
self.sigStateChanged.emit()
|
||||
#self.sigOutputChanged.emit()
|
||||
|
||||
def loadFile(self, fileName=None, startDir=None):
|
||||
if fileName is None:
|
||||
if startDir is None:
|
||||
startDir = self.filePath
|
||||
if startDir is None:
|
||||
startDir = '.'
|
||||
self.fileDialog = FileDialog(None, "Load Flowchart..", startDir, "Flowchart (*.fc)")
|
||||
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
|
||||
#self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
|
||||
self.fileDialog.show()
|
||||
self.fileDialog.fileSelected.connect(self.loadFile)
|
||||
return
|
||||
## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs..
|
||||
#fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)")
|
||||
fileName = unicode(fileName)
|
||||
state = configfile.readConfigFile(fileName)
|
||||
self.restoreState(state, clear=True)
|
||||
self.viewBox.autoRange()
|
||||
#self.emit(QtCore.SIGNAL('fileLoaded'), fileName)
|
||||
self.sigFileLoaded.emit(fileName)
|
||||
|
||||
def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'):
|
||||
if fileName is None:
|
||||
if startDir is None:
|
||||
startDir = self.filePath
|
||||
if startDir is None:
|
||||
startDir = '.'
|
||||
self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
|
||||
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
|
||||
self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
|
||||
#self.fileDialog.setDirectory(startDir)
|
||||
self.fileDialog.show()
|
||||
self.fileDialog.fileSelected.connect(self.saveFile)
|
||||
return
|
||||
#fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
|
||||
fileName = unicode(fileName)
|
||||
configfile.writeConfigFile(self.saveState(), fileName)
|
||||
self.sigFileSaved.emit(fileName)
|
||||
|
||||
def clear(self):
|
||||
for n in list(self._nodes.values()):
|
||||
if n is self.inputNode or n is self.outputNode:
|
||||
continue
|
||||
n.close() ## calls self.nodeClosed(n) by signal
|
||||
#self.clearTerminals()
|
||||
self.widget().clear()
|
||||
|
||||
def clearTerminals(self):
|
||||
Node.clearTerminals(self)
|
||||
self.inputNode.clearTerminals()
|
||||
self.outputNode.clearTerminals()
|
||||
|
||||
#class FlowchartGraphicsItem(QtGui.QGraphicsItem):
|
||||
class FlowchartGraphicsItem(GraphicsObject):
|
||||
|
||||
def __init__(self, chart):
|
||||
#print "FlowchartGraphicsItem.__init__"
|
||||
#QtGui.QGraphicsItem.__init__(self)
|
||||
GraphicsObject.__init__(self)
|
||||
self.chart = chart ## chart is an instance of Flowchart()
|
||||
self.updateTerminals()
|
||||
|
||||
def updateTerminals(self):
|
||||
#print "FlowchartGraphicsItem.updateTerminals"
|
||||
self.terminals = {}
|
||||
bounds = self.boundingRect()
|
||||
inp = self.chart.inputs()
|
||||
dy = bounds.height() / (len(inp)+1)
|
||||
y = dy
|
||||
for n, t in inp.items():
|
||||
item = t.graphicsItem()
|
||||
self.terminals[n] = item
|
||||
item.setParentItem(self)
|
||||
item.setAnchor(bounds.width(), y)
|
||||
y += dy
|
||||
out = self.chart.outputs()
|
||||
dy = bounds.height() / (len(out)+1)
|
||||
y = dy
|
||||
for n, t in out.items():
|
||||
item = t.graphicsItem()
|
||||
self.terminals[n] = item
|
||||
item.setParentItem(self)
|
||||
item.setAnchor(0, y)
|
||||
y += dy
|
||||
|
||||
def boundingRect(self):
|
||||
#print "FlowchartGraphicsItem.boundingRect"
|
||||
return QtCore.QRectF()
|
||||
|
||||
def paint(self, p, *args):
|
||||
#print "FlowchartGraphicsItem.paint"
|
||||
pass
|
||||
#p.drawRect(self.boundingRect())
|
||||
|
||||
|
||||
class FlowchartCtrlWidget(QtGui.QWidget):
|
||||
"""The widget that contains the list of all the nodes in a flowchart and their controls, as well as buttons for loading/saving flowcharts."""
|
||||
|
||||
def __init__(self, chart):
|
||||
self.items = {}
|
||||
#self.loadDir = loadDir ## where to look initially for chart files
|
||||
self.currentFileName = None
|
||||
QtGui.QWidget.__init__(self)
|
||||
self.chart = chart
|
||||
self.ui = FlowchartCtrlTemplate.Ui_Form()
|
||||
self.ui.setupUi(self)
|
||||
self.ui.ctrlList.setColumnCount(2)
|
||||
#self.ui.ctrlList.setColumnWidth(0, 200)
|
||||
self.ui.ctrlList.setColumnWidth(1, 20)
|
||||
self.ui.ctrlList.setVerticalScrollMode(self.ui.ctrlList.ScrollPerPixel)
|
||||
self.ui.ctrlList.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
|
||||
self.chartWidget = FlowchartWidget(chart, self)
|
||||
#self.chartWidget.viewBox().autoRange()
|
||||
self.cwWin = QtGui.QMainWindow()
|
||||
self.cwWin.setWindowTitle('Flowchart')
|
||||
self.cwWin.setCentralWidget(self.chartWidget)
|
||||
self.cwWin.resize(1000,800)
|
||||
|
||||
h = self.ui.ctrlList.header()
|
||||
if not USE_PYQT5:
|
||||
h.setResizeMode(0, h.Stretch)
|
||||
else:
|
||||
h.setSectionResizeMode(0, h.Stretch)
|
||||
|
||||
self.ui.ctrlList.itemChanged.connect(self.itemChanged)
|
||||
self.ui.loadBtn.clicked.connect(self.loadClicked)
|
||||
self.ui.saveBtn.clicked.connect(self.saveClicked)
|
||||
self.ui.saveAsBtn.clicked.connect(self.saveAsClicked)
|
||||
self.ui.showChartBtn.toggled.connect(self.chartToggled)
|
||||
self.chart.sigFileLoaded.connect(self.setCurrentFile)
|
||||
self.ui.reloadBtn.clicked.connect(self.reloadClicked)
|
||||
self.chart.sigFileSaved.connect(self.fileSaved)
|
||||
|
||||
|
||||
|
||||
#def resizeEvent(self, ev):
|
||||
#QtGui.QWidget.resizeEvent(self, ev)
|
||||
#self.ui.ctrlList.setColumnWidth(0, self.ui.ctrlList.viewport().width()-20)
|
||||
|
||||
def chartToggled(self, b):
|
||||
if b:
|
||||
self.cwWin.show()
|
||||
else:
|
||||
self.cwWin.hide()
|
||||
|
||||
def reloadClicked(self):
|
||||
try:
|
||||
self.chartWidget.reloadLibrary()
|
||||
self.ui.reloadBtn.success("Reloaded.")
|
||||
except:
|
||||
self.ui.reloadBtn.success("Error.")
|
||||
raise
|
||||
|
||||
|
||||
def loadClicked(self):
|
||||
newFile = self.chart.loadFile()
|
||||
#self.setCurrentFile(newFile)
|
||||
|
||||
def fileSaved(self, fileName):
|
||||
self.setCurrentFile(unicode(fileName))
|
||||
self.ui.saveBtn.success("Saved.")
|
||||
|
||||
def saveClicked(self):
|
||||
if self.currentFileName is None:
|
||||
self.saveAsClicked()
|
||||
else:
|
||||
try:
|
||||
self.chart.saveFile(self.currentFileName)
|
||||
#self.ui.saveBtn.success("Saved.")
|
||||
except:
|
||||
self.ui.saveBtn.failure("Error")
|
||||
raise
|
||||
|
||||
def saveAsClicked(self):
|
||||
try:
|
||||
if self.currentFileName is None:
|
||||
newFile = self.chart.saveFile()
|
||||
else:
|
||||
newFile = self.chart.saveFile(suggestedFileName=self.currentFileName)
|
||||
#self.ui.saveAsBtn.success("Saved.")
|
||||
#print "Back to saveAsClicked."
|
||||
except:
|
||||
self.ui.saveBtn.failure("Error")
|
||||
raise
|
||||
|
||||
#self.setCurrentFile(newFile)
|
||||
|
||||
def setCurrentFile(self, fileName):
|
||||
self.currentFileName = unicode(fileName)
|
||||
if fileName is None:
|
||||
self.ui.fileNameLabel.setText("<b>[ new ]</b>")
|
||||
else:
|
||||
self.ui.fileNameLabel.setText("<b>%s</b>" % os.path.split(self.currentFileName)[1])
|
||||
self.resizeEvent(None)
|
||||
|
||||
def itemChanged(self, *args):
|
||||
pass
|
||||
|
||||
def scene(self):
|
||||
return self.chartWidget.scene() ## returns the GraphicsScene object
|
||||
|
||||
def viewBox(self):
|
||||
return self.chartWidget.viewBox()
|
||||
|
||||
def nodeRenamed(self, node, oldName):
|
||||
self.items[node].setText(0, node.name())
|
||||
|
||||
def addNode(self, node):
|
||||
ctrl = node.ctrlWidget()
|
||||
#if ctrl is None:
|
||||
#return
|
||||
item = QtGui.QTreeWidgetItem([node.name(), '', ''])
|
||||
self.ui.ctrlList.addTopLevelItem(item)
|
||||
byp = QtGui.QPushButton('X')
|
||||
byp.setCheckable(True)
|
||||
byp.setFixedWidth(20)
|
||||
item.bypassBtn = byp
|
||||
self.ui.ctrlList.setItemWidget(item, 1, byp)
|
||||
byp.node = node
|
||||
node.bypassButton = byp
|
||||
byp.setChecked(node.isBypassed())
|
||||
byp.clicked.connect(self.bypassClicked)
|
||||
|
||||
if ctrl is not None:
|
||||
item2 = QtGui.QTreeWidgetItem()
|
||||
item.addChild(item2)
|
||||
self.ui.ctrlList.setItemWidget(item2, 0, ctrl)
|
||||
|
||||
self.items[node] = item
|
||||
|
||||
def removeNode(self, node):
|
||||
if node in self.items:
|
||||
item = self.items[node]
|
||||
#self.disconnect(item.bypassBtn, QtCore.SIGNAL('clicked()'), self.bypassClicked)
|
||||
try:
|
||||
item.bypassBtn.clicked.disconnect(self.bypassClicked)
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
self.ui.ctrlList.removeTopLevelItem(item)
|
||||
|
||||
def bypassClicked(self):
|
||||
btn = QtCore.QObject.sender(self)
|
||||
btn.node.bypass(btn.isChecked())
|
||||
|
||||
def chartWidget(self):
|
||||
return self.chartWidget
|
||||
|
||||
def outputChanged(self, data):
|
||||
pass
|
||||
#self.ui.outputTree.setData(data, hideRoot=True)
|
||||
|
||||
def clear(self):
|
||||
self.chartWidget.clear()
|
||||
|
||||
def select(self, node):
|
||||
item = self.items[node]
|
||||
self.ui.ctrlList.setCurrentItem(item)
|
||||
|
||||
class FlowchartWidget(dockarea.DockArea):
|
||||
"""Includes the actual graphical flowchart and debugging interface"""
|
||||
def __init__(self, chart, ctrl):
|
||||
#QtGui.QWidget.__init__(self)
|
||||
dockarea.DockArea.__init__(self)
|
||||
self.chart = chart
|
||||
self.ctrl = ctrl
|
||||
self.hoverItem = None
|
||||
#self.setMinimumWidth(250)
|
||||
#self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding))
|
||||
|
||||
#self.ui = FlowchartTemplate.Ui_Form()
|
||||
#self.ui.setupUi(self)
|
||||
|
||||
## build user interface (it was easier to do it here than via developer)
|
||||
self.view = FlowchartGraphicsView.FlowchartGraphicsView(self)
|
||||
self.viewDock = dockarea.Dock('view', size=(1000,600))
|
||||
self.viewDock.addWidget(self.view)
|
||||
self.viewDock.hideTitleBar()
|
||||
self.addDock(self.viewDock)
|
||||
|
||||
|
||||
self.hoverText = QtGui.QTextEdit()
|
||||
self.hoverText.setReadOnly(True)
|
||||
self.hoverDock = dockarea.Dock('Hover Info', size=(1000,20))
|
||||
self.hoverDock.addWidget(self.hoverText)
|
||||
self.addDock(self.hoverDock, 'bottom')
|
||||
|
||||
self.selInfo = QtGui.QWidget()
|
||||
self.selInfoLayout = QtGui.QGridLayout()
|
||||
self.selInfo.setLayout(self.selInfoLayout)
|
||||
self.selDescLabel = QtGui.QLabel()
|
||||
self.selNameLabel = QtGui.QLabel()
|
||||
self.selDescLabel.setWordWrap(True)
|
||||
self.selectedTree = DataTreeWidget()
|
||||
#self.selectedTree.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||
#self.selInfoLayout.addWidget(self.selNameLabel)
|
||||
self.selInfoLayout.addWidget(self.selDescLabel)
|
||||
self.selInfoLayout.addWidget(self.selectedTree)
|
||||
self.selDock = dockarea.Dock('Selected Node', size=(1000,200))
|
||||
self.selDock.addWidget(self.selInfo)
|
||||
self.addDock(self.selDock, 'bottom')
|
||||
|
||||
self._scene = self.view.scene()
|
||||
self._viewBox = self.view.viewBox()
|
||||
#self._scene = QtGui.QGraphicsScene()
|
||||
#self._scene = FlowchartGraphicsView.FlowchartGraphicsScene()
|
||||
#self.view.setScene(self._scene)
|
||||
|
||||
self.buildMenu()
|
||||
#self.ui.addNodeBtn.mouseReleaseEvent = self.addNodeBtnReleased
|
||||
|
||||
self._scene.selectionChanged.connect(self.selectionChanged)
|
||||
self._scene.sigMouseHover.connect(self.hoverOver)
|
||||
#self.view.sigClicked.connect(self.showViewMenu)
|
||||
#self._scene.sigSceneContextMenu.connect(self.showViewMenu)
|
||||
#self._viewBox.sigActionPositionChanged.connect(self.menuPosChanged)
|
||||
|
||||
|
||||
def reloadLibrary(self):
|
||||
#QtCore.QObject.disconnect(self.nodeMenu, QtCore.SIGNAL('triggered(QAction*)'), self.nodeMenuTriggered)
|
||||
self.nodeMenu.triggered.disconnect(self.nodeMenuTriggered)
|
||||
self.nodeMenu = None
|
||||
self.subMenus = []
|
||||
self.chart.library.reload()
|
||||
self.buildMenu()
|
||||
|
||||
def buildMenu(self, pos=None):
|
||||
def buildSubMenu(node, rootMenu, subMenus, pos=None):
|
||||
for section, node in node.items():
|
||||
menu = QtGui.QMenu(section)
|
||||
rootMenu.addMenu(menu)
|
||||
if isinstance(node, OrderedDict):
|
||||
buildSubMenu(node, menu, subMenus, pos=pos)
|
||||
subMenus.append(menu)
|
||||
else:
|
||||
act = rootMenu.addAction(section)
|
||||
act.nodeType = section
|
||||
act.pos = pos
|
||||
self.nodeMenu = QtGui.QMenu()
|
||||
self.subMenus = []
|
||||
buildSubMenu(self.chart.library.getNodeTree(), self.nodeMenu, self.subMenus, pos=pos)
|
||||
self.nodeMenu.triggered.connect(self.nodeMenuTriggered)
|
||||
return self.nodeMenu
|
||||
|
||||
def menuPosChanged(self, pos):
|
||||
self.menuPos = pos
|
||||
|
||||
def showViewMenu(self, ev):
|
||||
#QtGui.QPushButton.mouseReleaseEvent(self.ui.addNodeBtn, ev)
|
||||
#if ev.button() == QtCore.Qt.RightButton:
|
||||
#self.menuPos = self.view.mapToScene(ev.pos())
|
||||
#self.nodeMenu.popup(ev.globalPos())
|
||||
#print "Flowchart.showViewMenu called"
|
||||
|
||||
#self.menuPos = ev.scenePos()
|
||||
self.buildMenu(ev.scenePos())
|
||||
self.nodeMenu.popup(ev.screenPos())
|
||||
|
||||
def scene(self):
|
||||
return self._scene ## the GraphicsScene item
|
||||
|
||||
def viewBox(self):
|
||||
return self._viewBox ## the viewBox that items should be added to
|
||||
|
||||
def nodeMenuTriggered(self, action):
|
||||
nodeType = action.nodeType
|
||||
if action.pos is not None:
|
||||
pos = action.pos
|
||||
else:
|
||||
pos = self.menuPos
|
||||
pos = self.viewBox().mapSceneToView(pos)
|
||||
|
||||
self.chart.createNode(nodeType, pos=pos)
|
||||
|
||||
|
||||
def selectionChanged(self):
|
||||
#print "FlowchartWidget.selectionChanged called."
|
||||
items = self._scene.selectedItems()
|
||||
#print " scene.selectedItems: ", items
|
||||
if len(items) == 0:
|
||||
data = None
|
||||
else:
|
||||
item = items[0]
|
||||
if hasattr(item, 'node') and isinstance(item.node, Node):
|
||||
n = item.node
|
||||
self.ctrl.select(n)
|
||||
data = {'outputs': n.outputValues(), 'inputs': n.inputValues()}
|
||||
self.selNameLabel.setText(n.name())
|
||||
if hasattr(n, 'nodeName'):
|
||||
self.selDescLabel.setText("<b>%s</b>: %s" % (n.nodeName, n.__class__.__doc__))
|
||||
else:
|
||||
self.selDescLabel.setText("")
|
||||
if n.exception is not None:
|
||||
data['exception'] = n.exception
|
||||
else:
|
||||
data = None
|
||||
self.selectedTree.setData(data, hideRoot=True)
|
||||
|
||||
def hoverOver(self, items):
|
||||
#print "FlowchartWidget.hoverOver called."
|
||||
term = None
|
||||
for item in items:
|
||||
if item is self.hoverItem:
|
||||
return
|
||||
self.hoverItem = item
|
||||
if hasattr(item, 'term') and isinstance(item.term, Terminal):
|
||||
term = item.term
|
||||
break
|
||||
if term is None:
|
||||
self.hoverText.setPlainText("")
|
||||
else:
|
||||
val = term.value()
|
||||
if isinstance(val, ndarray):
|
||||
val = "%s %s %s" % (type(val).__name__, str(val.shape), str(val.dtype))
|
||||
else:
|
||||
val = str(val)
|
||||
if len(val) > 400:
|
||||
val = val[:400] + "..."
|
||||
self.hoverText.setPlainText("%s.%s = %s" % (term.node().name(), term.name(), val))
|
||||
#self.hoverLabel.setCursorPosition(0)
|
||||
|
||||
|
||||
|
||||
def clear(self):
|
||||
#self.outputTree.setData(None)
|
||||
self.selectedTree.setData(None)
|
||||
self.hoverText.setPlainText('')
|
||||
self.selNameLabel.setText('')
|
||||
self.selDescLabel.setText('')
|
||||
|
||||
|
||||
class FlowchartNode(Node):
|
||||
pass
|
||||
|
120
pyqtgraph/flowchart/FlowchartCtrlTemplate.ui
Normal file
120
pyqtgraph/flowchart/FlowchartCtrlTemplate.ui
Normal file
|
@ -0,0 +1,120 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>217</width>
|
||||
<height>499</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="verticalSpacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="loadBtn">
|
||||
<property name="text">
|
||||
<string>Load..</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1" colspan="2">
|
||||
<widget class="FeedbackButton" name="saveBtn">
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="FeedbackButton" name="saveAsBtn">
|
||||
<property name="text">
|
||||
<string>As..</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="FeedbackButton" name="reloadBtn">
|
||||
<property name="text">
|
||||
<string>Reload Libs</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="flat">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2" colspan="2">
|
||||
<widget class="QPushButton" name="showChartBtn">
|
||||
<property name="text">
|
||||
<string>Flowchart</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="4">
|
||||
<widget class="TreeWidget" name="ctrlList">
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="headerStretchLastSection">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="headerStretchLastSection">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string notr="true">1</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="fileNameLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>TreeWidget</class>
|
||||
<extends>QTreeWidget</extends>
|
||||
<header>..widgets.TreeWidget</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>FeedbackButton</class>
|
||||
<extends>QPushButton</extends>
|
||||
<header>..widgets.FeedbackButton</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
80
pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py
Normal file
80
pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartCtrlTemplate.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:50 2013
|
||||
# by: PyQt4 UI code generator 4.10
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
try:
|
||||
_fromUtf8 = QtCore.QString.fromUtf8
|
||||
except AttributeError:
|
||||
def _fromUtf8(s):
|
||||
return s
|
||||
|
||||
try:
|
||||
_encoding = QtGui.QApplication.UnicodeUTF8
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig, _encoding)
|
||||
except AttributeError:
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig)
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName(_fromUtf8("Form"))
|
||||
Form.resize(217, 499)
|
||||
self.gridLayout = QtGui.QGridLayout(Form)
|
||||
self.gridLayout.setMargin(0)
|
||||
self.gridLayout.setVerticalSpacing(0)
|
||||
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
|
||||
self.loadBtn = QtGui.QPushButton(Form)
|
||||
self.loadBtn.setObjectName(_fromUtf8("loadBtn"))
|
||||
self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1)
|
||||
self.saveBtn = FeedbackButton(Form)
|
||||
self.saveBtn.setObjectName(_fromUtf8("saveBtn"))
|
||||
self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2)
|
||||
self.saveAsBtn = FeedbackButton(Form)
|
||||
self.saveAsBtn.setObjectName(_fromUtf8("saveAsBtn"))
|
||||
self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1)
|
||||
self.reloadBtn = FeedbackButton(Form)
|
||||
self.reloadBtn.setCheckable(False)
|
||||
self.reloadBtn.setFlat(False)
|
||||
self.reloadBtn.setObjectName(_fromUtf8("reloadBtn"))
|
||||
self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2)
|
||||
self.showChartBtn = QtGui.QPushButton(Form)
|
||||
self.showChartBtn.setCheckable(True)
|
||||
self.showChartBtn.setObjectName(_fromUtf8("showChartBtn"))
|
||||
self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2)
|
||||
self.ctrlList = TreeWidget(Form)
|
||||
self.ctrlList.setObjectName(_fromUtf8("ctrlList"))
|
||||
self.ctrlList.headerItem().setText(0, _fromUtf8("1"))
|
||||
self.ctrlList.header().setVisible(False)
|
||||
self.ctrlList.header().setStretchLastSection(False)
|
||||
self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4)
|
||||
self.fileNameLabel = QtGui.QLabel(Form)
|
||||
font = QtGui.QFont()
|
||||
font.setBold(True)
|
||||
font.setWeight(75)
|
||||
self.fileNameLabel.setFont(font)
|
||||
self.fileNameLabel.setText(_fromUtf8(""))
|
||||
self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.fileNameLabel.setObjectName(_fromUtf8("fileNameLabel"))
|
||||
self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(_translate("Form", "Form", None))
|
||||
self.loadBtn.setText(_translate("Form", "Load..", None))
|
||||
self.saveBtn.setText(_translate("Form", "Save", None))
|
||||
self.saveAsBtn.setText(_translate("Form", "As..", None))
|
||||
self.reloadBtn.setText(_translate("Form", "Reload Libs", None))
|
||||
self.showChartBtn.setText(_translate("Form", "Flowchart", None))
|
||||
|
||||
from ..widgets.TreeWidget import TreeWidget
|
||||
from ..widgets.FeedbackButton import FeedbackButton
|
67
pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py
Normal file
67
pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartCtrlTemplate.ui'
|
||||
#
|
||||
# Created: Wed Mar 26 15:09:28 2014
|
||||
# by: PyQt5 UI code generator 5.0.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName("Form")
|
||||
Form.resize(217, 499)
|
||||
self.gridLayout = QtWidgets.QGridLayout(Form)
|
||||
self.gridLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout.setVerticalSpacing(0)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.loadBtn = QtWidgets.QPushButton(Form)
|
||||
self.loadBtn.setObjectName("loadBtn")
|
||||
self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1)
|
||||
self.saveBtn = FeedbackButton(Form)
|
||||
self.saveBtn.setObjectName("saveBtn")
|
||||
self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2)
|
||||
self.saveAsBtn = FeedbackButton(Form)
|
||||
self.saveAsBtn.setObjectName("saveAsBtn")
|
||||
self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1)
|
||||
self.reloadBtn = FeedbackButton(Form)
|
||||
self.reloadBtn.setCheckable(False)
|
||||
self.reloadBtn.setFlat(False)
|
||||
self.reloadBtn.setObjectName("reloadBtn")
|
||||
self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2)
|
||||
self.showChartBtn = QtWidgets.QPushButton(Form)
|
||||
self.showChartBtn.setCheckable(True)
|
||||
self.showChartBtn.setObjectName("showChartBtn")
|
||||
self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2)
|
||||
self.ctrlList = TreeWidget(Form)
|
||||
self.ctrlList.setObjectName("ctrlList")
|
||||
self.ctrlList.headerItem().setText(0, "1")
|
||||
self.ctrlList.header().setVisible(False)
|
||||
self.ctrlList.header().setStretchLastSection(False)
|
||||
self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4)
|
||||
self.fileNameLabel = QtWidgets.QLabel(Form)
|
||||
font = QtGui.QFont()
|
||||
font.setBold(True)
|
||||
font.setWeight(75)
|
||||
self.fileNameLabel.setFont(font)
|
||||
self.fileNameLabel.setText("")
|
||||
self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.fileNameLabel.setObjectName("fileNameLabel")
|
||||
self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
Form.setWindowTitle(_translate("Form", "Form"))
|
||||
self.loadBtn.setText(_translate("Form", "Load.."))
|
||||
self.saveBtn.setText(_translate("Form", "Save"))
|
||||
self.saveAsBtn.setText(_translate("Form", "As.."))
|
||||
self.reloadBtn.setText(_translate("Form", "Reload Libs"))
|
||||
self.showChartBtn.setText(_translate("Form", "Flowchart"))
|
||||
|
||||
from ..widgets.FeedbackButton import FeedbackButton
|
||||
from ..widgets.TreeWidget import TreeWidget
|
66
pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py
Normal file
66
pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartCtrlTemplate.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:51 2013
|
||||
# by: pyside-uic 0.2.14 running on PySide 1.1.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName("Form")
|
||||
Form.resize(217, 499)
|
||||
self.gridLayout = QtGui.QGridLayout(Form)
|
||||
self.gridLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout.setVerticalSpacing(0)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.loadBtn = QtGui.QPushButton(Form)
|
||||
self.loadBtn.setObjectName("loadBtn")
|
||||
self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1)
|
||||
self.saveBtn = FeedbackButton(Form)
|
||||
self.saveBtn.setObjectName("saveBtn")
|
||||
self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2)
|
||||
self.saveAsBtn = FeedbackButton(Form)
|
||||
self.saveAsBtn.setObjectName("saveAsBtn")
|
||||
self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1)
|
||||
self.reloadBtn = FeedbackButton(Form)
|
||||
self.reloadBtn.setCheckable(False)
|
||||
self.reloadBtn.setFlat(False)
|
||||
self.reloadBtn.setObjectName("reloadBtn")
|
||||
self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2)
|
||||
self.showChartBtn = QtGui.QPushButton(Form)
|
||||
self.showChartBtn.setCheckable(True)
|
||||
self.showChartBtn.setObjectName("showChartBtn")
|
||||
self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2)
|
||||
self.ctrlList = TreeWidget(Form)
|
||||
self.ctrlList.setObjectName("ctrlList")
|
||||
self.ctrlList.headerItem().setText(0, "1")
|
||||
self.ctrlList.header().setVisible(False)
|
||||
self.ctrlList.header().setStretchLastSection(False)
|
||||
self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4)
|
||||
self.fileNameLabel = QtGui.QLabel(Form)
|
||||
font = QtGui.QFont()
|
||||
font.setWeight(75)
|
||||
font.setBold(True)
|
||||
self.fileNameLabel.setFont(font)
|
||||
self.fileNameLabel.setText("")
|
||||
self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.fileNameLabel.setObjectName("fileNameLabel")
|
||||
self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load..", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.saveBtn.setText(QtGui.QApplication.translate("Form", "Save", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.saveAsBtn.setText(QtGui.QApplication.translate("Form", "As..", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.reloadBtn.setText(QtGui.QApplication.translate("Form", "Reload Libs", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.showChartBtn.setText(QtGui.QApplication.translate("Form", "Flowchart", None, QtGui.QApplication.UnicodeUTF8))
|
||||
|
||||
from ..widgets.TreeWidget import TreeWidget
|
||||
from ..widgets.FeedbackButton import FeedbackButton
|
109
pyqtgraph/flowchart/FlowchartGraphicsView.py
Normal file
109
pyqtgraph/flowchart/FlowchartGraphicsView.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ..Qt import QtGui, QtCore
|
||||
from ..widgets.GraphicsView import GraphicsView
|
||||
from ..GraphicsScene import GraphicsScene
|
||||
from ..graphicsItems.ViewBox import ViewBox
|
||||
|
||||
#class FlowchartGraphicsView(QtGui.QGraphicsView):
|
||||
class FlowchartGraphicsView(GraphicsView):
|
||||
|
||||
sigHoverOver = QtCore.Signal(object)
|
||||
sigClicked = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, widget, *args):
|
||||
#QtGui.QGraphicsView.__init__(self, *args)
|
||||
GraphicsView.__init__(self, *args, useOpenGL=False)
|
||||
#self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(255,255,255)))
|
||||
self._vb = FlowchartViewBox(widget, lockAspect=True, invertY=True)
|
||||
self.setCentralItem(self._vb)
|
||||
#self.scene().addItem(self.vb)
|
||||
#self.setMouseTracking(True)
|
||||
#self.lastPos = None
|
||||
#self.setTransformationAnchor(self.AnchorViewCenter)
|
||||
#self.setRenderHints(QtGui.QPainter.Antialiasing)
|
||||
self.setRenderHint(QtGui.QPainter.Antialiasing, True)
|
||||
#self.setDragMode(QtGui.QGraphicsView.RubberBandDrag)
|
||||
#self.setRubberBandSelectionMode(QtCore.Qt.ContainsItemBoundingRect)
|
||||
|
||||
def viewBox(self):
|
||||
return self._vb
|
||||
|
||||
|
||||
#def mousePressEvent(self, ev):
|
||||
#self.moved = False
|
||||
#self.lastPos = ev.pos()
|
||||
#return QtGui.QGraphicsView.mousePressEvent(self, ev)
|
||||
|
||||
#def mouseMoveEvent(self, ev):
|
||||
#self.moved = True
|
||||
#callSuper = False
|
||||
#if ev.buttons() & QtCore.Qt.RightButton:
|
||||
#if self.lastPos is not None:
|
||||
#dif = ev.pos() - self.lastPos
|
||||
#self.scale(1.01**-dif.y(), 1.01**-dif.y())
|
||||
#elif ev.buttons() & QtCore.Qt.MidButton:
|
||||
#if self.lastPos is not None:
|
||||
#dif = ev.pos() - self.lastPos
|
||||
#self.translate(dif.x(), -dif.y())
|
||||
#else:
|
||||
##self.emit(QtCore.SIGNAL('hoverOver'), self.items(ev.pos()))
|
||||
#self.sigHoverOver.emit(self.items(ev.pos()))
|
||||
#callSuper = True
|
||||
#self.lastPos = ev.pos()
|
||||
|
||||
#if callSuper:
|
||||
#QtGui.QGraphicsView.mouseMoveEvent(self, ev)
|
||||
|
||||
#def mouseReleaseEvent(self, ev):
|
||||
#if not self.moved:
|
||||
##self.emit(QtCore.SIGNAL('clicked'), ev)
|
||||
#self.sigClicked.emit(ev)
|
||||
#return QtGui.QGraphicsView.mouseReleaseEvent(self, ev)
|
||||
|
||||
class FlowchartViewBox(ViewBox):
|
||||
|
||||
def __init__(self, widget, *args, **kwargs):
|
||||
ViewBox.__init__(self, *args, **kwargs)
|
||||
self.widget = widget
|
||||
#self.menu = None
|
||||
#self._subMenus = None ## need a place to store the menus otherwise they dissappear (even though they've been added to other menus) ((yes, it doesn't make sense))
|
||||
|
||||
|
||||
|
||||
|
||||
def getMenu(self, ev):
|
||||
## called by ViewBox to create a new context menu
|
||||
self._fc_menu = QtGui.QMenu()
|
||||
self._subMenus = self.getContextMenus(ev)
|
||||
for menu in self._subMenus:
|
||||
self._fc_menu.addMenu(menu)
|
||||
return self._fc_menu
|
||||
|
||||
def getContextMenus(self, ev):
|
||||
## called by scene to add menus on to someone else's context menu
|
||||
menu = self.widget.buildMenu(ev.scenePos())
|
||||
menu.setTitle("Add node")
|
||||
return [menu, ViewBox.getMenu(self, ev)]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
##class FlowchartGraphicsScene(QtGui.QGraphicsScene):
|
||||
#class FlowchartGraphicsScene(GraphicsScene):
|
||||
|
||||
#sigContextMenuEvent = QtCore.Signal(object)
|
||||
|
||||
#def __init__(self, *args):
|
||||
##QtGui.QGraphicsScene.__init__(self, *args)
|
||||
#GraphicsScene.__init__(self, *args)
|
||||
|
||||
#def mouseClickEvent(self, ev):
|
||||
##QtGui.QGraphicsScene.contextMenuEvent(self, ev)
|
||||
#if not ev.button() in [QtCore.Qt.RightButton]:
|
||||
#self.sigContextMenuEvent.emit(ev)
|
98
pyqtgraph/flowchart/FlowchartTemplate.ui
Normal file
98
pyqtgraph/flowchart/FlowchartTemplate.ui
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>529</width>
|
||||
<height>329</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="selInfoWidget" native="true">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>260</x>
|
||||
<y>10</y>
|
||||
<width>264</width>
|
||||
<height>222</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="selDescLabel">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="selNameLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="DataTreeWidget" name="selectedTree">
|
||||
<column>
|
||||
<property name="text">
|
||||
<string notr="true">1</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="hoverText">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>240</y>
|
||||
<width>521</width>
|
||||
<height>81</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="FlowchartGraphicsView" name="view">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>256</width>
|
||||
<height>192</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>DataTreeWidget</class>
|
||||
<extends>QTreeWidget</extends>
|
||||
<header>..widgets.DataTreeWidget</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>FlowchartGraphicsView</class>
|
||||
<extends>QGraphicsView</extends>
|
||||
<header>..flowchart.FlowchartGraphicsView</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
68
pyqtgraph/flowchart/FlowchartTemplate_pyqt.py
Normal file
68
pyqtgraph/flowchart/FlowchartTemplate_pyqt.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartTemplate.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:51 2013
|
||||
# by: PyQt4 UI code generator 4.10
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
try:
|
||||
_fromUtf8 = QtCore.QString.fromUtf8
|
||||
except AttributeError:
|
||||
def _fromUtf8(s):
|
||||
return s
|
||||
|
||||
try:
|
||||
_encoding = QtGui.QApplication.UnicodeUTF8
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig, _encoding)
|
||||
except AttributeError:
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig)
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName(_fromUtf8("Form"))
|
||||
Form.resize(529, 329)
|
||||
self.selInfoWidget = QtGui.QWidget(Form)
|
||||
self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222))
|
||||
self.selInfoWidget.setObjectName(_fromUtf8("selInfoWidget"))
|
||||
self.gridLayout = QtGui.QGridLayout(self.selInfoWidget)
|
||||
self.gridLayout.setMargin(0)
|
||||
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
|
||||
self.selDescLabel = QtGui.QLabel(self.selInfoWidget)
|
||||
self.selDescLabel.setText(_fromUtf8(""))
|
||||
self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
|
||||
self.selDescLabel.setWordWrap(True)
|
||||
self.selDescLabel.setObjectName(_fromUtf8("selDescLabel"))
|
||||
self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1)
|
||||
self.selNameLabel = QtGui.QLabel(self.selInfoWidget)
|
||||
font = QtGui.QFont()
|
||||
font.setBold(True)
|
||||
font.setWeight(75)
|
||||
self.selNameLabel.setFont(font)
|
||||
self.selNameLabel.setText(_fromUtf8(""))
|
||||
self.selNameLabel.setObjectName(_fromUtf8("selNameLabel"))
|
||||
self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1)
|
||||
self.selectedTree = DataTreeWidget(self.selInfoWidget)
|
||||
self.selectedTree.setObjectName(_fromUtf8("selectedTree"))
|
||||
self.selectedTree.headerItem().setText(0, _fromUtf8("1"))
|
||||
self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2)
|
||||
self.hoverText = QtGui.QTextEdit(Form)
|
||||
self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81))
|
||||
self.hoverText.setObjectName(_fromUtf8("hoverText"))
|
||||
self.view = FlowchartGraphicsView(Form)
|
||||
self.view.setGeometry(QtCore.QRect(0, 0, 256, 192))
|
||||
self.view.setObjectName(_fromUtf8("view"))
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(_translate("Form", "Form", None))
|
||||
|
||||
from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView
|
||||
from ..widgets.DataTreeWidget import DataTreeWidget
|
55
pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py
Normal file
55
pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartTemplate.ui'
|
||||
#
|
||||
# Created: Wed Mar 26 15:09:28 2014
|
||||
# by: PyQt5 UI code generator 5.0.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName("Form")
|
||||
Form.resize(529, 329)
|
||||
self.selInfoWidget = QtWidgets.QWidget(Form)
|
||||
self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222))
|
||||
self.selInfoWidget.setObjectName("selInfoWidget")
|
||||
self.gridLayout = QtWidgets.QGridLayout(self.selInfoWidget)
|
||||
self.gridLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.selDescLabel = QtWidgets.QLabel(self.selInfoWidget)
|
||||
self.selDescLabel.setText("")
|
||||
self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
|
||||
self.selDescLabel.setWordWrap(True)
|
||||
self.selDescLabel.setObjectName("selDescLabel")
|
||||
self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1)
|
||||
self.selNameLabel = QtWidgets.QLabel(self.selInfoWidget)
|
||||
font = QtGui.QFont()
|
||||
font.setBold(True)
|
||||
font.setWeight(75)
|
||||
self.selNameLabel.setFont(font)
|
||||
self.selNameLabel.setText("")
|
||||
self.selNameLabel.setObjectName("selNameLabel")
|
||||
self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1)
|
||||
self.selectedTree = DataTreeWidget(self.selInfoWidget)
|
||||
self.selectedTree.setObjectName("selectedTree")
|
||||
self.selectedTree.headerItem().setText(0, "1")
|
||||
self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2)
|
||||
self.hoverText = QtWidgets.QTextEdit(Form)
|
||||
self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81))
|
||||
self.hoverText.setObjectName("hoverText")
|
||||
self.view = FlowchartGraphicsView(Form)
|
||||
self.view.setGeometry(QtCore.QRect(0, 0, 256, 192))
|
||||
self.view.setObjectName("view")
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
Form.setWindowTitle(_translate("Form", "Form"))
|
||||
|
||||
from ..widgets.DataTreeWidget import DataTreeWidget
|
||||
from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView
|
54
pyqtgraph/flowchart/FlowchartTemplate_pyside.py
Normal file
54
pyqtgraph/flowchart/FlowchartTemplate_pyside.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartTemplate.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:51 2013
|
||||
# by: pyside-uic 0.2.14 running on PySide 1.1.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName("Form")
|
||||
Form.resize(529, 329)
|
||||
self.selInfoWidget = QtGui.QWidget(Form)
|
||||
self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222))
|
||||
self.selInfoWidget.setObjectName("selInfoWidget")
|
||||
self.gridLayout = QtGui.QGridLayout(self.selInfoWidget)
|
||||
self.gridLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.selDescLabel = QtGui.QLabel(self.selInfoWidget)
|
||||
self.selDescLabel.setText("")
|
||||
self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
|
||||
self.selDescLabel.setWordWrap(True)
|
||||
self.selDescLabel.setObjectName("selDescLabel")
|
||||
self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1)
|
||||
self.selNameLabel = QtGui.QLabel(self.selInfoWidget)
|
||||
font = QtGui.QFont()
|
||||
font.setWeight(75)
|
||||
font.setBold(True)
|
||||
self.selNameLabel.setFont(font)
|
||||
self.selNameLabel.setText("")
|
||||
self.selNameLabel.setObjectName("selNameLabel")
|
||||
self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1)
|
||||
self.selectedTree = DataTreeWidget(self.selInfoWidget)
|
||||
self.selectedTree.setObjectName("selectedTree")
|
||||
self.selectedTree.headerItem().setText(0, "1")
|
||||
self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2)
|
||||
self.hoverText = QtGui.QTextEdit(Form)
|
||||
self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81))
|
||||
self.hoverText.setObjectName("hoverText")
|
||||
self.view = FlowchartGraphicsView(Form)
|
||||
self.view.setGeometry(QtCore.QRect(0, 0, 256, 192))
|
||||
self.view.setObjectName("view")
|
||||
|
||||
self.retranslateUi(Form)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
|
||||
|
||||
from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView
|
||||
from ..widgets.DataTreeWidget import DataTreeWidget
|
644
pyqtgraph/flowchart/Node.py
Normal file
644
pyqtgraph/flowchart/Node.py
Normal file
|
@ -0,0 +1,644 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ..Qt import QtCore, QtGui
|
||||
from ..graphicsItems.GraphicsObject import GraphicsObject
|
||||
from .. import functions as fn
|
||||
from .Terminal import *
|
||||
from ..pgcollections import OrderedDict
|
||||
from ..debug import *
|
||||
import numpy as np
|
||||
from .eq import *
|
||||
|
||||
|
||||
def strDict(d):
|
||||
return dict([(str(k), v) for k, v in d.items()])
|
||||
|
||||
class Node(QtCore.QObject):
|
||||
"""
|
||||
Node represents the basic processing unit of a flowchart.
|
||||
A Node subclass implements at least:
|
||||
|
||||
1) A list of input / ouptut terminals and their properties
|
||||
2) a process() function which takes the names of input terminals as keyword arguments and returns a dict with the names of output terminals as keys.
|
||||
|
||||
A flowchart thus consists of multiple instances of Node subclasses, each of which is connected
|
||||
to other by wires between their terminals. A flowchart is, itself, also a special subclass of Node.
|
||||
This allows Nodes within the flowchart to connect to the input/output nodes of the flowchart itself.
|
||||
|
||||
Optionally, a node class can implement the ctrlWidget() method, which must return a QWidget (usually containing other widgets) that will be displayed in the flowchart control panel. Some nodes implement fairly complex control widgets, but most nodes follow a simple form-like pattern: a list of parameter names and a single value (represented as spin box, check box, etc..) for each parameter. To make this easier, the CtrlNode subclass allows you to instead define a simple data structure that CtrlNode will use to automatically generate the control widget. """
|
||||
|
||||
sigOutputChanged = QtCore.Signal(object) # self
|
||||
sigClosed = QtCore.Signal(object)
|
||||
sigRenamed = QtCore.Signal(object, object)
|
||||
sigTerminalRenamed = QtCore.Signal(object, object) # term, oldName
|
||||
sigTerminalAdded = QtCore.Signal(object, object) # self, term
|
||||
sigTerminalRemoved = QtCore.Signal(object, object) # self, term
|
||||
|
||||
|
||||
def __init__(self, name, terminals=None, allowAddInput=False, allowAddOutput=False, allowRemove=True):
|
||||
"""
|
||||
============== ============================================================
|
||||
**Arguments:**
|
||||
name The name of this specific node instance. It can be any
|
||||
string, but must be unique within a flowchart. Usually,
|
||||
we simply let the flowchart decide on a name when calling
|
||||
Flowchart.addNode(...)
|
||||
terminals Dict-of-dicts specifying the terminals present on this Node.
|
||||
Terminal specifications look like::
|
||||
|
||||
'inputTerminalName': {'io': 'in'}
|
||||
'outputTerminalName': {'io': 'out'}
|
||||
|
||||
There are a number of optional parameters for terminals:
|
||||
multi, pos, renamable, removable, multiable, bypass. See
|
||||
the Terminal class for more information.
|
||||
allowAddInput bool; whether the user is allowed to add inputs by the
|
||||
context menu.
|
||||
allowAddOutput bool; whether the user is allowed to add outputs by the
|
||||
context menu.
|
||||
allowRemove bool; whether the user is allowed to remove this node by the
|
||||
context menu.
|
||||
============== ============================================================
|
||||
|
||||
"""
|
||||
QtCore.QObject.__init__(self)
|
||||
self._name = name
|
||||
self._bypass = False
|
||||
self.bypassButton = None ## this will be set by the flowchart ctrl widget..
|
||||
self._graphicsItem = None
|
||||
self.terminals = OrderedDict()
|
||||
self._inputs = OrderedDict()
|
||||
self._outputs = OrderedDict()
|
||||
self._allowAddInput = allowAddInput ## flags to allow the user to add/remove terminals
|
||||
self._allowAddOutput = allowAddOutput
|
||||
self._allowRemove = allowRemove
|
||||
|
||||
self.exception = None
|
||||
if terminals is None:
|
||||
return
|
||||
for name, opts in terminals.items():
|
||||
self.addTerminal(name, **opts)
|
||||
|
||||
|
||||
def nextTerminalName(self, name):
|
||||
"""Return an unused terminal name"""
|
||||
name2 = name
|
||||
i = 1
|
||||
while name2 in self.terminals:
|
||||
name2 = "%s.%d" % (name, i)
|
||||
i += 1
|
||||
return name2
|
||||
|
||||
def addInput(self, name="Input", **args):
|
||||
"""Add a new input terminal to this Node with the given name. Extra
|
||||
keyword arguments are passed to Terminal.__init__.
|
||||
|
||||
This is a convenience function that just calls addTerminal(io='in', ...)"""
|
||||
#print "Node.addInput called."
|
||||
return self.addTerminal(name, io='in', **args)
|
||||
|
||||
def addOutput(self, name="Output", **args):
|
||||
"""Add a new output terminal to this Node with the given name. Extra
|
||||
keyword arguments are passed to Terminal.__init__.
|
||||
|
||||
This is a convenience function that just calls addTerminal(io='out', ...)"""
|
||||
return self.addTerminal(name, io='out', **args)
|
||||
|
||||
def removeTerminal(self, term):
|
||||
"""Remove the specified terminal from this Node. May specify either the
|
||||
terminal's name or the terminal itself.
|
||||
|
||||
Causes sigTerminalRemoved to be emitted."""
|
||||
if isinstance(term, Terminal):
|
||||
name = term.name()
|
||||
else:
|
||||
name = term
|
||||
term = self.terminals[name]
|
||||
|
||||
#print "remove", name
|
||||
#term.disconnectAll()
|
||||
term.close()
|
||||
del self.terminals[name]
|
||||
if name in self._inputs:
|
||||
del self._inputs[name]
|
||||
if name in self._outputs:
|
||||
del self._outputs[name]
|
||||
self.graphicsItem().updateTerminals()
|
||||
self.sigTerminalRemoved.emit(self, term)
|
||||
|
||||
|
||||
def terminalRenamed(self, term, oldName):
|
||||
"""Called after a terminal has been renamed
|
||||
|
||||
Causes sigTerminalRenamed to be emitted."""
|
||||
newName = term.name()
|
||||
for d in [self.terminals, self._inputs, self._outputs]:
|
||||
if oldName not in d:
|
||||
continue
|
||||
d[newName] = d[oldName]
|
||||
del d[oldName]
|
||||
|
||||
self.graphicsItem().updateTerminals()
|
||||
self.sigTerminalRenamed.emit(term, oldName)
|
||||
|
||||
def addTerminal(self, name, **opts):
|
||||
"""Add a new terminal to this Node with the given name. Extra
|
||||
keyword arguments are passed to Terminal.__init__.
|
||||
|
||||
Causes sigTerminalAdded to be emitted."""
|
||||
name = self.nextTerminalName(name)
|
||||
term = Terminal(self, name, **opts)
|
||||
self.terminals[name] = term
|
||||
if term.isInput():
|
||||
self._inputs[name] = term
|
||||
elif term.isOutput():
|
||||
self._outputs[name] = term
|
||||
self.graphicsItem().updateTerminals()
|
||||
self.sigTerminalAdded.emit(self, term)
|
||||
return term
|
||||
|
||||
|
||||
def inputs(self):
|
||||
"""Return dict of all input terminals.
|
||||
Warning: do not modify."""
|
||||
return self._inputs
|
||||
|
||||
def outputs(self):
|
||||
"""Return dict of all output terminals.
|
||||
Warning: do not modify."""
|
||||
return self._outputs
|
||||
|
||||
def process(self, **kargs):
|
||||
"""Process data through this node. This method is called any time the flowchart
|
||||
wants the node to process data. It will be called with one keyword argument
|
||||
corresponding to each input terminal, and must return a dict mapping the name
|
||||
of each output terminal to its new value.
|
||||
|
||||
This method is also called with a 'display' keyword argument, which indicates
|
||||
whether the node should update its display (if it implements any) while processing
|
||||
this data. This is primarily used to disable expensive display operations
|
||||
during batch processing.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def graphicsItem(self):
|
||||
"""Return the GraphicsItem for this node. Subclasses may re-implement
|
||||
this method to customize their appearance in the flowchart."""
|
||||
if self._graphicsItem is None:
|
||||
self._graphicsItem = NodeGraphicsItem(self)
|
||||
return self._graphicsItem
|
||||
|
||||
## this is just bad planning. Causes too many bugs.
|
||||
def __getattr__(self, attr):
|
||||
"""Return the terminal with the given name"""
|
||||
if attr not in self.terminals:
|
||||
raise AttributeError(attr)
|
||||
else:
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
print("Warning: use of node.terminalName is deprecated; use node['terminalName'] instead.")
|
||||
return self.terminals[attr]
|
||||
|
||||
def __getitem__(self, item):
|
||||
#return getattr(self, item)
|
||||
"""Return the terminal with the given name"""
|
||||
if item not in self.terminals:
|
||||
raise KeyError(item)
|
||||
else:
|
||||
return self.terminals[item]
|
||||
|
||||
def name(self):
|
||||
"""Return the name of this node."""
|
||||
return self._name
|
||||
|
||||
def rename(self, name):
|
||||
"""Rename this node. This will cause sigRenamed to be emitted."""
|
||||
oldName = self._name
|
||||
self._name = name
|
||||
#self.emit(QtCore.SIGNAL('renamed'), self, oldName)
|
||||
self.sigRenamed.emit(self, oldName)
|
||||
|
||||
def dependentNodes(self):
|
||||
"""Return the list of nodes which provide direct input to this node"""
|
||||
nodes = set()
|
||||
for t in self.inputs().values():
|
||||
nodes |= set([i.node() for i in t.inputTerminals()])
|
||||
return nodes
|
||||
#return set([t.inputTerminals().node() for t in self.listInputs().itervalues()])
|
||||
|
||||
def __repr__(self):
|
||||
return "<Node %s @%x>" % (self.name(), id(self))
|
||||
|
||||
def ctrlWidget(self):
|
||||
"""Return this Node's control widget.
|
||||
|
||||
By default, Nodes have no control widget. Subclasses may reimplement this
|
||||
method to provide a custom widget. This method is called by Flowcharts
|
||||
when they are constructing their Node list."""
|
||||
return None
|
||||
|
||||
def bypass(self, byp):
|
||||
"""Set whether this node should be bypassed.
|
||||
|
||||
When bypassed, a Node's process() method is never called. In some cases,
|
||||
data is automatically copied directly from specific input nodes to
|
||||
output nodes instead (see the bypass argument to Terminal.__init__).
|
||||
This is usually called when the user disables a node from the flowchart
|
||||
control panel.
|
||||
"""
|
||||
self._bypass = byp
|
||||
if self.bypassButton is not None:
|
||||
self.bypassButton.setChecked(byp)
|
||||
self.update()
|
||||
|
||||
def isBypassed(self):
|
||||
"""Return True if this Node is currently bypassed."""
|
||||
return self._bypass
|
||||
|
||||
def setInput(self, **args):
|
||||
"""Set the values on input terminals. For most nodes, this will happen automatically through Terminal.inputChanged.
|
||||
This is normally only used for nodes with no connected inputs."""
|
||||
changed = False
|
||||
for k, v in args.items():
|
||||
term = self._inputs[k]
|
||||
oldVal = term.value()
|
||||
if not eq(oldVal, v):
|
||||
changed = True
|
||||
term.setValue(v, process=False)
|
||||
if changed and '_updatesHandled_' not in args:
|
||||
self.update()
|
||||
|
||||
def inputValues(self):
|
||||
"""Return a dict of all input values currently assigned to this node."""
|
||||
vals = {}
|
||||
for n, t in self.inputs().items():
|
||||
vals[n] = t.value()
|
||||
return vals
|
||||
|
||||
def outputValues(self):
|
||||
"""Return a dict of all output values currently generated by this node."""
|
||||
vals = {}
|
||||
for n, t in self.outputs().items():
|
||||
vals[n] = t.value()
|
||||
return vals
|
||||
|
||||
def connected(self, localTerm, remoteTerm):
|
||||
"""Called whenever one of this node's terminals is connected elsewhere."""
|
||||
pass
|
||||
|
||||
def disconnected(self, localTerm, remoteTerm):
|
||||
"""Called whenever one of this node's terminals is disconnected from another."""
|
||||
pass
|
||||
|
||||
def update(self, signal=True):
|
||||
"""Collect all input values, attempt to process new output values, and propagate downstream.
|
||||
Subclasses should call update() whenever thir internal state has changed
|
||||
(such as when the user interacts with the Node's control widget). Update
|
||||
is automatically called when the inputs to the node are changed.
|
||||
"""
|
||||
vals = self.inputValues()
|
||||
#print " inputs:", vals
|
||||
try:
|
||||
if self.isBypassed():
|
||||
out = self.processBypassed(vals)
|
||||
else:
|
||||
out = self.process(**strDict(vals))
|
||||
#print " output:", out
|
||||
if out is not None:
|
||||
if signal:
|
||||
self.setOutput(**out)
|
||||
else:
|
||||
self.setOutputNoSignal(**out)
|
||||
for n,t in self.inputs().items():
|
||||
t.setValueAcceptable(True)
|
||||
self.clearException()
|
||||
except:
|
||||
#printExc( "Exception while processing %s:" % self.name())
|
||||
for n,t in self.outputs().items():
|
||||
t.setValue(None)
|
||||
self.setException(sys.exc_info())
|
||||
|
||||
if signal:
|
||||
#self.emit(QtCore.SIGNAL('outputChanged'), self) ## triggers flowchart to propagate new data
|
||||
self.sigOutputChanged.emit(self) ## triggers flowchart to propagate new data
|
||||
|
||||
def processBypassed(self, args):
|
||||
"""Called when the flowchart would normally call Node.process, but this node is currently bypassed.
|
||||
The default implementation looks for output terminals with a bypass connection and returns the
|
||||
corresponding values. Most Node subclasses will _not_ need to reimplement this method."""
|
||||
result = {}
|
||||
for term in list(self.outputs().values()):
|
||||
byp = term.bypassValue()
|
||||
if byp is None:
|
||||
result[term.name()] = None
|
||||
else:
|
||||
result[term.name()] = args.get(byp, None)
|
||||
return result
|
||||
|
||||
def setOutput(self, **vals):
|
||||
self.setOutputNoSignal(**vals)
|
||||
#self.emit(QtCore.SIGNAL('outputChanged'), self) ## triggers flowchart to propagate new data
|
||||
self.sigOutputChanged.emit(self) ## triggers flowchart to propagate new data
|
||||
|
||||
def setOutputNoSignal(self, **vals):
|
||||
for k, v in vals.items():
|
||||
term = self.outputs()[k]
|
||||
term.setValue(v)
|
||||
#targets = term.connections()
|
||||
#for t in targets: ## propagate downstream
|
||||
#if t is term:
|
||||
#continue
|
||||
#t.inputChanged(term)
|
||||
term.setValueAcceptable(True)
|
||||
|
||||
def setException(self, exc):
|
||||
self.exception = exc
|
||||
self.recolor()
|
||||
|
||||
def clearException(self):
|
||||
self.setException(None)
|
||||
|
||||
def recolor(self):
|
||||
if self.exception is None:
|
||||
self.graphicsItem().setPen(QtGui.QPen(QtGui.QColor(0, 0, 0)))
|
||||
else:
|
||||
self.graphicsItem().setPen(QtGui.QPen(QtGui.QColor(150, 0, 0), 3))
|
||||
|
||||
def saveState(self):
|
||||
"""Return a dictionary representing the current state of this node
|
||||
(excluding input / output values). This is used for saving/reloading
|
||||
flowcharts. The default implementation returns this Node's position,
|
||||
bypass state, and information about each of its terminals.
|
||||
|
||||
Subclasses may want to extend this method, adding extra keys to the returned
|
||||
dict."""
|
||||
pos = self.graphicsItem().pos()
|
||||
state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()}
|
||||
termsEditable = self._allowAddInput | self._allowAddOutput
|
||||
for term in self._inputs.values() + self._outputs.values():
|
||||
termsEditable |= term._renamable | term._removable | term._multiable
|
||||
if termsEditable:
|
||||
state['terminals'] = self.saveTerminals()
|
||||
return state
|
||||
|
||||
def restoreState(self, state):
|
||||
"""Restore the state of this node from a structure previously generated
|
||||
by saveState(). """
|
||||
pos = state.get('pos', (0,0))
|
||||
self.graphicsItem().setPos(*pos)
|
||||
self.bypass(state.get('bypass', False))
|
||||
if 'terminals' in state:
|
||||
self.restoreTerminals(state['terminals'])
|
||||
|
||||
def saveTerminals(self):
|
||||
terms = OrderedDict()
|
||||
for n, t in self.terminals.items():
|
||||
terms[n] = (t.saveState())
|
||||
return terms
|
||||
|
||||
def restoreTerminals(self, state):
|
||||
for name in list(self.terminals.keys()):
|
||||
if name not in state:
|
||||
self.removeTerminal(name)
|
||||
for name, opts in state.items():
|
||||
if name in self.terminals:
|
||||
term = self[name]
|
||||
term.setOpts(**opts)
|
||||
continue
|
||||
try:
|
||||
opts = strDict(opts)
|
||||
self.addTerminal(name, **opts)
|
||||
except:
|
||||
printExc("Error restoring terminal %s (%s):" % (str(name), str(opts)))
|
||||
|
||||
|
||||
def clearTerminals(self):
|
||||
for t in self.terminals.values():
|
||||
t.close()
|
||||
self.terminals = OrderedDict()
|
||||
self._inputs = OrderedDict()
|
||||
self._outputs = OrderedDict()
|
||||
|
||||
def close(self):
|
||||
"""Cleans up after the node--removes terminals, graphicsItem, widget"""
|
||||
self.disconnectAll()
|
||||
self.clearTerminals()
|
||||
item = self.graphicsItem()
|
||||
if item.scene() is not None:
|
||||
item.scene().removeItem(item)
|
||||
self._graphicsItem = None
|
||||
w = self.ctrlWidget()
|
||||
if w is not None:
|
||||
w.setParent(None)
|
||||
#self.emit(QtCore.SIGNAL('closed'), self)
|
||||
self.sigClosed.emit(self)
|
||||
|
||||
def disconnectAll(self):
|
||||
for t in self.terminals.values():
|
||||
t.disconnectAll()
|
||||
|
||||
|
||||
#class NodeGraphicsItem(QtGui.QGraphicsItem):
|
||||
class NodeGraphicsItem(GraphicsObject):
|
||||
def __init__(self, node):
|
||||
#QtGui.QGraphicsItem.__init__(self)
|
||||
GraphicsObject.__init__(self)
|
||||
#QObjectWorkaround.__init__(self)
|
||||
|
||||
#self.shadow = QtGui.QGraphicsDropShadowEffect()
|
||||
#self.shadow.setOffset(5,5)
|
||||
#self.shadow.setBlurRadius(10)
|
||||
#self.setGraphicsEffect(self.shadow)
|
||||
|
||||
self.pen = fn.mkPen(0,0,0)
|
||||
self.selectPen = fn.mkPen(200,200,200,width=2)
|
||||
self.brush = fn.mkBrush(200, 200, 200, 150)
|
||||
self.hoverBrush = fn.mkBrush(200, 200, 200, 200)
|
||||
self.selectBrush = fn.mkBrush(200, 200, 255, 200)
|
||||
self.hovered = False
|
||||
|
||||
self.node = node
|
||||
flags = self.ItemIsMovable | self.ItemIsSelectable | self.ItemIsFocusable |self.ItemSendsGeometryChanges
|
||||
#flags = self.ItemIsFocusable |self.ItemSendsGeometryChanges
|
||||
|
||||
self.setFlags(flags)
|
||||
self.bounds = QtCore.QRectF(0, 0, 100, 100)
|
||||
self.nameItem = QtGui.QGraphicsTextItem(self.node.name(), self)
|
||||
self.nameItem.setDefaultTextColor(QtGui.QColor(50, 50, 50))
|
||||
self.nameItem.moveBy(self.bounds.width()/2. - self.nameItem.boundingRect().width()/2., 0)
|
||||
self.nameItem.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
|
||||
self.updateTerminals()
|
||||
#self.setZValue(10)
|
||||
|
||||
self.nameItem.focusOutEvent = self.labelFocusOut
|
||||
self.nameItem.keyPressEvent = self.labelKeyPress
|
||||
|
||||
self.menu = None
|
||||
self.buildMenu()
|
||||
|
||||
#self.node.sigTerminalRenamed.connect(self.updateActionMenu)
|
||||
|
||||
#def setZValue(self, z):
|
||||
#for t, item in self.terminals.itervalues():
|
||||
#item.setZValue(z+1)
|
||||
#GraphicsObject.setZValue(self, z)
|
||||
|
||||
def labelFocusOut(self, ev):
|
||||
QtGui.QGraphicsTextItem.focusOutEvent(self.nameItem, ev)
|
||||
self.labelChanged()
|
||||
|
||||
def labelKeyPress(self, ev):
|
||||
if ev.key() == QtCore.Qt.Key_Enter or ev.key() == QtCore.Qt.Key_Return:
|
||||
self.labelChanged()
|
||||
else:
|
||||
QtGui.QGraphicsTextItem.keyPressEvent(self.nameItem, ev)
|
||||
|
||||
def labelChanged(self):
|
||||
newName = str(self.nameItem.toPlainText())
|
||||
if newName != self.node.name():
|
||||
self.node.rename(newName)
|
||||
|
||||
### re-center the label
|
||||
bounds = self.boundingRect()
|
||||
self.nameItem.setPos(bounds.width()/2. - self.nameItem.boundingRect().width()/2., 0)
|
||||
|
||||
def setPen(self, *args, **kwargs):
|
||||
self.pen = fn.mkPen(*args, **kwargs)
|
||||
self.update()
|
||||
|
||||
def setBrush(self, brush):
|
||||
self.brush = brush
|
||||
self.update()
|
||||
|
||||
|
||||
def updateTerminals(self):
|
||||
bounds = self.bounds
|
||||
self.terminals = {}
|
||||
inp = self.node.inputs()
|
||||
dy = bounds.height() / (len(inp)+1)
|
||||
y = dy
|
||||
for i, t in inp.items():
|
||||
item = t.graphicsItem()
|
||||
item.setParentItem(self)
|
||||
#item.setZValue(self.zValue()+1)
|
||||
br = self.bounds
|
||||
item.setAnchor(0, y)
|
||||
self.terminals[i] = (t, item)
|
||||
y += dy
|
||||
|
||||
out = self.node.outputs()
|
||||
dy = bounds.height() / (len(out)+1)
|
||||
y = dy
|
||||
for i, t in out.items():
|
||||
item = t.graphicsItem()
|
||||
item.setParentItem(self)
|
||||
item.setZValue(self.zValue())
|
||||
br = self.bounds
|
||||
item.setAnchor(bounds.width(), y)
|
||||
self.terminals[i] = (t, item)
|
||||
y += dy
|
||||
|
||||
#self.buildMenu()
|
||||
|
||||
|
||||
def boundingRect(self):
|
||||
return self.bounds.adjusted(-5, -5, 5, 5)
|
||||
|
||||
def paint(self, p, *args):
|
||||
|
||||
p.setPen(self.pen)
|
||||
if self.isSelected():
|
||||
p.setPen(self.selectPen)
|
||||
p.setBrush(self.selectBrush)
|
||||
else:
|
||||
p.setPen(self.pen)
|
||||
if self.hovered:
|
||||
p.setBrush(self.hoverBrush)
|
||||
else:
|
||||
p.setBrush(self.brush)
|
||||
|
||||
p.drawRect(self.bounds)
|
||||
|
||||
|
||||
def mousePressEvent(self, ev):
|
||||
ev.ignore()
|
||||
|
||||
|
||||
def mouseClickEvent(self, ev):
|
||||
#print "Node.mouseClickEvent called."
|
||||
if int(ev.button()) == int(QtCore.Qt.LeftButton):
|
||||
ev.accept()
|
||||
#print " ev.button: left"
|
||||
sel = self.isSelected()
|
||||
#ret = QtGui.QGraphicsItem.mousePressEvent(self, ev)
|
||||
self.setSelected(True)
|
||||
if not sel and self.isSelected():
|
||||
#self.setBrush(QtGui.QBrush(QtGui.QColor(200, 200, 255)))
|
||||
#self.emit(QtCore.SIGNAL('selected'))
|
||||
#self.scene().selectionChanged.emit() ## for some reason this doesn't seem to be happening automatically
|
||||
self.update()
|
||||
#return ret
|
||||
|
||||
elif int(ev.button()) == int(QtCore.Qt.RightButton):
|
||||
#print " ev.button: right"
|
||||
ev.accept()
|
||||
#pos = ev.screenPos()
|
||||
self.raiseContextMenu(ev)
|
||||
#self.menu.popup(QtCore.QPoint(pos.x(), pos.y()))
|
||||
|
||||
def mouseDragEvent(self, ev):
|
||||
#print "Node.mouseDrag"
|
||||
if ev.button() == QtCore.Qt.LeftButton:
|
||||
ev.accept()
|
||||
self.setPos(self.pos()+self.mapToParent(ev.pos())-self.mapToParent(ev.lastPos()))
|
||||
|
||||
def hoverEvent(self, ev):
|
||||
if not ev.isExit() and ev.acceptClicks(QtCore.Qt.LeftButton):
|
||||
ev.acceptDrags(QtCore.Qt.LeftButton)
|
||||
self.hovered = True
|
||||
else:
|
||||
self.hovered = False
|
||||
self.update()
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace:
|
||||
ev.accept()
|
||||
if not self.node._allowRemove:
|
||||
return
|
||||
self.node.close()
|
||||
else:
|
||||
ev.ignore()
|
||||
|
||||
def itemChange(self, change, val):
|
||||
if change == self.ItemPositionHasChanged:
|
||||
for k, t in self.terminals.items():
|
||||
t[1].nodeMoved()
|
||||
return GraphicsObject.itemChange(self, change, val)
|
||||
|
||||
|
||||
def getMenu(self):
|
||||
return self.menu
|
||||
|
||||
def raiseContextMenu(self, ev):
|
||||
menu = self.scene().addParentContextMenus(self, self.getMenu(), ev)
|
||||
pos = ev.screenPos()
|
||||
menu.popup(QtCore.QPoint(pos.x(), pos.y()))
|
||||
|
||||
def buildMenu(self):
|
||||
self.menu = QtGui.QMenu()
|
||||
self.menu.setTitle("Node")
|
||||
a = self.menu.addAction("Add input", self.addInputFromMenu)
|
||||
if not self.node._allowAddInput:
|
||||
a.setEnabled(False)
|
||||
a = self.menu.addAction("Add output", self.addOutputFromMenu)
|
||||
if not self.node._allowAddOutput:
|
||||
a.setEnabled(False)
|
||||
a = self.menu.addAction("Remove node", self.node.close)
|
||||
if not self.node._allowRemove:
|
||||
a.setEnabled(False)
|
||||
|
||||
def addInputFromMenu(self): ## called when add input is clicked in context menu
|
||||
self.node.addInput(renamable=True, removable=True, multiable=True)
|
||||
|
||||
def addOutputFromMenu(self): ## called when add output is clicked in context menu
|
||||
self.node.addOutput(renamable=True, removable=True, multiable=False)
|
||||
|
86
pyqtgraph/flowchart/NodeLibrary.py
Normal file
86
pyqtgraph/flowchart/NodeLibrary.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
from ..pgcollections import OrderedDict
|
||||
from .Node import Node
|
||||
|
||||
def isNodeClass(cls):
|
||||
try:
|
||||
if not issubclass(cls, Node):
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
return hasattr(cls, 'nodeName')
|
||||
|
||||
|
||||
|
||||
class NodeLibrary:
|
||||
"""
|
||||
A library of flowchart Node types. Custom libraries may be built to provide
|
||||
each flowchart with a specific set of allowed Node types.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.nodeList = OrderedDict()
|
||||
self.nodeTree = OrderedDict()
|
||||
|
||||
def addNodeType(self, nodeClass, paths, override=False):
|
||||
"""
|
||||
Register a new node type. If the type's name is already in use,
|
||||
an exception will be raised (unless override=True).
|
||||
|
||||
============== =========================================================
|
||||
**Arguments:**
|
||||
|
||||
nodeClass a subclass of Node (must have typ.nodeName)
|
||||
paths list of tuples specifying the location(s) this
|
||||
type will appear in the library tree.
|
||||
override if True, overwrite any class having the same name
|
||||
============== =========================================================
|
||||
"""
|
||||
if not isNodeClass(nodeClass):
|
||||
raise Exception("Object %s is not a Node subclass" % str(nodeClass))
|
||||
|
||||
name = nodeClass.nodeName
|
||||
if not override and name in self.nodeList:
|
||||
raise Exception("Node type name '%s' is already registered." % name)
|
||||
|
||||
self.nodeList[name] = nodeClass
|
||||
for path in paths:
|
||||
root = self.nodeTree
|
||||
for n in path:
|
||||
if n not in root:
|
||||
root[n] = OrderedDict()
|
||||
root = root[n]
|
||||
root[name] = nodeClass
|
||||
|
||||
def getNodeType(self, name):
|
||||
try:
|
||||
return self.nodeList[name]
|
||||
except KeyError:
|
||||
raise Exception("No node type called '%s'" % name)
|
||||
|
||||
def getNodeTree(self):
|
||||
return self.nodeTree
|
||||
|
||||
def copy(self):
|
||||
"""
|
||||
Return a copy of this library.
|
||||
"""
|
||||
lib = NodeLibrary()
|
||||
lib.nodeList = self.nodeList.copy()
|
||||
lib.nodeTree = self.treeCopy(self.nodeTree)
|
||||
return lib
|
||||
|
||||
@staticmethod
|
||||
def treeCopy(tree):
|
||||
copy = OrderedDict()
|
||||
for k,v in tree.items():
|
||||
if isNodeClass(v):
|
||||
copy[k] = v
|
||||
else:
|
||||
copy[k] = NodeLibrary.treeCopy(v)
|
||||
return copy
|
||||
|
||||
def reload(self):
|
||||
"""
|
||||
Reload Node classes in this library.
|
||||
"""
|
||||
raise NotImplementedError()
|
634
pyqtgraph/flowchart/Terminal.py
Normal file
634
pyqtgraph/flowchart/Terminal.py
Normal file
|
@ -0,0 +1,634 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ..Qt import QtCore, QtGui
|
||||
import weakref
|
||||
from ..graphicsItems.GraphicsObject import GraphicsObject
|
||||
from .. import functions as fn
|
||||
from ..Point import Point
|
||||
#from PySide import QtCore, QtGui
|
||||
from .eq import *
|
||||
|
||||
class Terminal(object):
|
||||
def __init__(self, node, name, io, optional=False, multi=False, pos=None, renamable=False, removable=False, multiable=False, bypass=None):
|
||||
"""
|
||||
Construct a new terminal.
|
||||
|
||||
============== =================================================================================
|
||||
**Arguments:**
|
||||
node the node to which this terminal belongs
|
||||
name string, the name of the terminal
|
||||
io 'in' or 'out'
|
||||
optional bool, whether the node may process without connection to this terminal
|
||||
multi bool, for inputs: whether this terminal may make multiple connections
|
||||
for outputs: whether this terminal creates a different value for each connection
|
||||
pos [x, y], the position of the terminal within its node's boundaries
|
||||
renamable (bool) Whether the terminal can be renamed by the user
|
||||
removable (bool) Whether the terminal can be removed by the user
|
||||
multiable (bool) Whether the user may toggle the *multi* option for this terminal
|
||||
bypass (str) Name of the terminal from which this terminal's value is derived
|
||||
when the Node is in bypass mode.
|
||||
============== =================================================================================
|
||||
"""
|
||||
self._io = io
|
||||
#self._isOutput = opts[0] in ['out', 'io']
|
||||
#self._isInput = opts[0]] in ['in', 'io']
|
||||
#self._isIO = opts[0]=='io'
|
||||
self._optional = optional
|
||||
self._multi = multi
|
||||
self._node = weakref.ref(node)
|
||||
self._name = name
|
||||
self._renamable = renamable
|
||||
self._removable = removable
|
||||
self._multiable = multiable
|
||||
self._connections = {}
|
||||
self._graphicsItem = TerminalGraphicsItem(self, parent=self._node().graphicsItem())
|
||||
self._bypass = bypass
|
||||
|
||||
if multi:
|
||||
self._value = {} ## dictionary of terminal:value pairs.
|
||||
else:
|
||||
self._value = None
|
||||
|
||||
self.valueOk = None
|
||||
self.recolor()
|
||||
|
||||
def value(self, term=None):
|
||||
"""Return the value this terminal provides for the connected terminal"""
|
||||
if term is None:
|
||||
return self._value
|
||||
|
||||
if self.isMultiValue():
|
||||
return self._value.get(term, None)
|
||||
else:
|
||||
return self._value
|
||||
|
||||
def bypassValue(self):
|
||||
return self._bypass
|
||||
|
||||
def setValue(self, val, process=True):
|
||||
"""If this is a single-value terminal, val should be a single value.
|
||||
If this is a multi-value terminal, val should be a dict of terminal:value pairs"""
|
||||
if not self.isMultiValue():
|
||||
if eq(val, self._value):
|
||||
return
|
||||
self._value = val
|
||||
else:
|
||||
if not isinstance(self._value, dict):
|
||||
self._value = {}
|
||||
if val is not None:
|
||||
self._value.update(val)
|
||||
|
||||
self.setValueAcceptable(None) ## by default, input values are 'unchecked' until Node.update().
|
||||
if self.isInput() and process:
|
||||
self.node().update()
|
||||
|
||||
## Let the flowchart handle this.
|
||||
#if self.isOutput():
|
||||
#for c in self.connections():
|
||||
#if c.isInput():
|
||||
#c.inputChanged(self)
|
||||
self.recolor()
|
||||
|
||||
def setOpts(self, **opts):
|
||||
self._renamable = opts.get('renamable', self._renamable)
|
||||
self._removable = opts.get('removable', self._removable)
|
||||
self._multiable = opts.get('multiable', self._multiable)
|
||||
if 'multi' in opts:
|
||||
self.setMultiValue(opts['multi'])
|
||||
|
||||
|
||||
def connected(self, term):
|
||||
"""Called whenever this terminal has been connected to another. (note--this function is called on both terminals)"""
|
||||
if self.isInput() and term.isOutput():
|
||||
self.inputChanged(term)
|
||||
if self.isOutput() and self.isMultiValue():
|
||||
self.node().update()
|
||||
self.node().connected(self, term)
|
||||
|
||||
def disconnected(self, term):
|
||||
"""Called whenever this terminal has been disconnected from another. (note--this function is called on both terminals)"""
|
||||
if self.isMultiValue() and term in self._value:
|
||||
del self._value[term]
|
||||
self.node().update()
|
||||
#self.recolor()
|
||||
else:
|
||||
if self.isInput():
|
||||
self.setValue(None)
|
||||
self.node().disconnected(self, term)
|
||||
#self.node().update()
|
||||
|
||||
def inputChanged(self, term, process=True):
|
||||
"""Called whenever there is a change to the input value to this terminal.
|
||||
It may often be useful to override this function."""
|
||||
if self.isMultiValue():
|
||||
self.setValue({term: term.value(self)}, process=process)
|
||||
else:
|
||||
self.setValue(term.value(self), process=process)
|
||||
|
||||
def valueIsAcceptable(self):
|
||||
"""Returns True->acceptable None->unknown False->Unacceptable"""
|
||||
return self.valueOk
|
||||
|
||||
def setValueAcceptable(self, v=True):
|
||||
self.valueOk = v
|
||||
self.recolor()
|
||||
|
||||
def connections(self):
|
||||
return self._connections
|
||||
|
||||
def node(self):
|
||||
return self._node()
|
||||
|
||||
def isInput(self):
|
||||
return self._io == 'in'
|
||||
|
||||
def isMultiValue(self):
|
||||
return self._multi
|
||||
|
||||
def setMultiValue(self, multi):
|
||||
"""Set whether this is a multi-value terminal."""
|
||||
self._multi = multi
|
||||
if not multi and len(self.inputTerminals()) > 1:
|
||||
self.disconnectAll()
|
||||
|
||||
for term in self.inputTerminals():
|
||||
self.inputChanged(term)
|
||||
|
||||
def isOutput(self):
|
||||
return self._io == 'out'
|
||||
|
||||
def isRenamable(self):
|
||||
return self._renamable
|
||||
|
||||
def isRemovable(self):
|
||||
return self._removable
|
||||
|
||||
def isMultiable(self):
|
||||
return self._multiable
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def graphicsItem(self):
|
||||
return self._graphicsItem
|
||||
|
||||
def isConnected(self):
|
||||
return len(self.connections()) > 0
|
||||
|
||||
def connectedTo(self, term):
|
||||
return term in self.connections()
|
||||
|
||||
def hasInput(self):
|
||||
#conn = self.extendedConnections()
|
||||
for t in self.connections():
|
||||
if t.isOutput():
|
||||
return True
|
||||
return False
|
||||
|
||||
def inputTerminals(self):
|
||||
"""Return the terminal(s) that give input to this one."""
|
||||
#terms = self.extendedConnections()
|
||||
#for t in terms:
|
||||
#if t.isOutput():
|
||||
#return t
|
||||
return [t for t in self.connections() if t.isOutput()]
|
||||
|
||||
|
||||
def dependentNodes(self):
|
||||
"""Return the list of nodes which receive input from this terminal."""
|
||||
#conn = self.extendedConnections()
|
||||
#del conn[self]
|
||||
return set([t.node() for t in self.connections() if t.isInput()])
|
||||
|
||||
def connectTo(self, term, connectionItem=None):
|
||||
try:
|
||||
if self.connectedTo(term):
|
||||
raise Exception('Already connected')
|
||||
if term is self:
|
||||
raise Exception('Not connecting terminal to self')
|
||||
if term.node() is self.node():
|
||||
raise Exception("Can't connect to terminal on same node.")
|
||||
for t in [self, term]:
|
||||
if t.isInput() and not t._multi and len(t.connections()) > 0:
|
||||
raise Exception("Cannot connect %s <-> %s: Terminal %s is already connected to %s (and does not allow multiple connections)" % (self, term, t, list(t.connections().keys())))
|
||||
#if self.hasInput() and term.hasInput():
|
||||
#raise Exception('Target terminal already has input')
|
||||
|
||||
#if term in self.node().terminals.values():
|
||||
#if self.isOutput() or term.isOutput():
|
||||
#raise Exception('Can not connect an output back to the same node.')
|
||||
except:
|
||||
if connectionItem is not None:
|
||||
connectionItem.close()
|
||||
raise
|
||||
|
||||
if connectionItem is None:
|
||||
connectionItem = ConnectionItem(self.graphicsItem(), term.graphicsItem())
|
||||
#self.graphicsItem().scene().addItem(connectionItem)
|
||||
self.graphicsItem().getViewBox().addItem(connectionItem)
|
||||
#connectionItem.setParentItem(self.graphicsItem().parent().parent())
|
||||
self._connections[term] = connectionItem
|
||||
term._connections[self] = connectionItem
|
||||
|
||||
self.recolor()
|
||||
|
||||
#if self.isOutput() and term.isInput():
|
||||
#term.inputChanged(self)
|
||||
#if term.isInput() and term.isOutput():
|
||||
#self.inputChanged(term)
|
||||
self.connected(term)
|
||||
term.connected(self)
|
||||
|
||||
return connectionItem
|
||||
|
||||
def disconnectFrom(self, term):
|
||||
if not self.connectedTo(term):
|
||||
return
|
||||
item = self._connections[term]
|
||||
#print "removing connection", item
|
||||
#item.scene().removeItem(item)
|
||||
item.close()
|
||||
del self._connections[term]
|
||||
del term._connections[self]
|
||||
self.recolor()
|
||||
term.recolor()
|
||||
|
||||
self.disconnected(term)
|
||||
term.disconnected(self)
|
||||
#if self.isOutput() and term.isInput():
|
||||
#term.inputChanged(self)
|
||||
#if term.isInput() and term.isOutput():
|
||||
#self.inputChanged(term)
|
||||
|
||||
|
||||
def disconnectAll(self):
|
||||
for t in list(self._connections.keys()):
|
||||
self.disconnectFrom(t)
|
||||
|
||||
def recolor(self, color=None, recurse=True):
|
||||
if color is None:
|
||||
if not self.isConnected(): ## disconnected terminals are black
|
||||
color = QtGui.QColor(0,0,0)
|
||||
elif self.isInput() and not self.hasInput(): ## input terminal with no connected output terminals
|
||||
color = QtGui.QColor(200,200,0)
|
||||
elif self._value is None or eq(self._value, {}): ## terminal is connected but has no data (possibly due to processing error)
|
||||
color = QtGui.QColor(255,255,255)
|
||||
elif self.valueIsAcceptable() is None: ## terminal has data, but it is unknown if the data is ok
|
||||
color = QtGui.QColor(200, 200, 0)
|
||||
elif self.valueIsAcceptable() is True: ## terminal has good input, all ok
|
||||
color = QtGui.QColor(0, 200, 0)
|
||||
else: ## terminal has bad input
|
||||
color = QtGui.QColor(200, 0, 0)
|
||||
self.graphicsItem().setBrush(QtGui.QBrush(color))
|
||||
|
||||
if recurse:
|
||||
for t in self.connections():
|
||||
t.recolor(color, recurse=False)
|
||||
|
||||
|
||||
def rename(self, name):
|
||||
oldName = self._name
|
||||
self._name = name
|
||||
self.node().terminalRenamed(self, oldName)
|
||||
self.graphicsItem().termRenamed(name)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Terminal %s.%s>" % (str(self.node().name()), str(self.name()))
|
||||
|
||||
#def extendedConnections(self, terms=None):
|
||||
#"""Return list of terminals (including this one) that are directly or indirectly wired to this."""
|
||||
#if terms is None:
|
||||
#terms = {}
|
||||
#terms[self] = None
|
||||
#for t in self._connections:
|
||||
#if t in terms:
|
||||
#continue
|
||||
#terms.update(t.extendedConnections(terms))
|
||||
#return terms
|
||||
|
||||
def __hash__(self):
|
||||
return id(self)
|
||||
|
||||
def close(self):
|
||||
self.disconnectAll()
|
||||
item = self.graphicsItem()
|
||||
if item.scene() is not None:
|
||||
item.scene().removeItem(item)
|
||||
|
||||
def saveState(self):
|
||||
return {'io': self._io, 'multi': self._multi, 'optional': self._optional, 'renamable': self._renamable, 'removable': self._removable, 'multiable': self._multiable}
|
||||
|
||||
|
||||
#class TerminalGraphicsItem(QtGui.QGraphicsItem):
|
||||
class TerminalGraphicsItem(GraphicsObject):
|
||||
|
||||
def __init__(self, term, parent=None):
|
||||
self.term = term
|
||||
#QtGui.QGraphicsItem.__init__(self, parent)
|
||||
GraphicsObject.__init__(self, parent)
|
||||
self.brush = fn.mkBrush(0,0,0)
|
||||
self.box = QtGui.QGraphicsRectItem(0, 0, 10, 10, self)
|
||||
self.label = QtGui.QGraphicsTextItem(self.term.name(), self)
|
||||
self.label.scale(0.7, 0.7)
|
||||
#self.setAcceptHoverEvents(True)
|
||||
self.newConnection = None
|
||||
self.setFiltersChildEvents(True) ## to pick up mouse events on the rectitem
|
||||
if self.term.isRenamable():
|
||||
self.label.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
|
||||
self.label.focusOutEvent = self.labelFocusOut
|
||||
self.label.keyPressEvent = self.labelKeyPress
|
||||
self.setZValue(1)
|
||||
self.menu = None
|
||||
|
||||
|
||||
def labelFocusOut(self, ev):
|
||||
QtGui.QGraphicsTextItem.focusOutEvent(self.label, ev)
|
||||
self.labelChanged()
|
||||
|
||||
def labelKeyPress(self, ev):
|
||||
if ev.key() == QtCore.Qt.Key_Enter or ev.key() == QtCore.Qt.Key_Return:
|
||||
self.labelChanged()
|
||||
else:
|
||||
QtGui.QGraphicsTextItem.keyPressEvent(self.label, ev)
|
||||
|
||||
def labelChanged(self):
|
||||
newName = str(self.label.toPlainText())
|
||||
if newName != self.term.name():
|
||||
self.term.rename(newName)
|
||||
|
||||
def termRenamed(self, name):
|
||||
self.label.setPlainText(name)
|
||||
|
||||
def setBrush(self, brush):
|
||||
self.brush = brush
|
||||
self.box.setBrush(brush)
|
||||
|
||||
def disconnect(self, target):
|
||||
self.term.disconnectFrom(target.term)
|
||||
|
||||
def boundingRect(self):
|
||||
br = self.box.mapRectToParent(self.box.boundingRect())
|
||||
lr = self.label.mapRectToParent(self.label.boundingRect())
|
||||
return br | lr
|
||||
|
||||
def paint(self, p, *args):
|
||||
pass
|
||||
|
||||
def setAnchor(self, x, y):
|
||||
pos = QtCore.QPointF(x, y)
|
||||
self.anchorPos = pos
|
||||
br = self.box.mapRectToParent(self.box.boundingRect())
|
||||
lr = self.label.mapRectToParent(self.label.boundingRect())
|
||||
|
||||
|
||||
if self.term.isInput():
|
||||
self.box.setPos(pos.x(), pos.y()-br.height()/2.)
|
||||
self.label.setPos(pos.x() + br.width(), pos.y() - lr.height()/2.)
|
||||
else:
|
||||
self.box.setPos(pos.x()-br.width(), pos.y()-br.height()/2.)
|
||||
self.label.setPos(pos.x()-br.width()-lr.width(), pos.y()-lr.height()/2.)
|
||||
self.updateConnections()
|
||||
|
||||
def updateConnections(self):
|
||||
for t, c in self.term.connections().items():
|
||||
c.updateLine()
|
||||
|
||||
def mousePressEvent(self, ev):
|
||||
#ev.accept()
|
||||
ev.ignore() ## necessary to allow click/drag events to process correctly
|
||||
|
||||
def mouseClickEvent(self, ev):
|
||||
if ev.button() == QtCore.Qt.LeftButton:
|
||||
ev.accept()
|
||||
self.label.setFocus(QtCore.Qt.MouseFocusReason)
|
||||
elif ev.button() == QtCore.Qt.RightButton:
|
||||
ev.accept()
|
||||
self.raiseContextMenu(ev)
|
||||
|
||||
def raiseContextMenu(self, ev):
|
||||
## only raise menu if this terminal is removable
|
||||
menu = self.getMenu()
|
||||
menu = self.scene().addParentContextMenus(self, menu, ev)
|
||||
pos = ev.screenPos()
|
||||
menu.popup(QtCore.QPoint(pos.x(), pos.y()))
|
||||
|
||||
def getMenu(self):
|
||||
if self.menu is None:
|
||||
self.menu = QtGui.QMenu()
|
||||
self.menu.setTitle("Terminal")
|
||||
remAct = QtGui.QAction("Remove terminal", self.menu)
|
||||
remAct.triggered.connect(self.removeSelf)
|
||||
self.menu.addAction(remAct)
|
||||
self.menu.remAct = remAct
|
||||
if not self.term.isRemovable():
|
||||
remAct.setEnabled(False)
|
||||
multiAct = QtGui.QAction("Multi-value", self.menu)
|
||||
multiAct.setCheckable(True)
|
||||
multiAct.setChecked(self.term.isMultiValue())
|
||||
multiAct.setEnabled(self.term.isMultiable())
|
||||
|
||||
multiAct.triggered.connect(self.toggleMulti)
|
||||
self.menu.addAction(multiAct)
|
||||
self.menu.multiAct = multiAct
|
||||
if self.term.isMultiable():
|
||||
multiAct.setEnabled = False
|
||||
return self.menu
|
||||
|
||||
def toggleMulti(self):
|
||||
multi = self.menu.multiAct.isChecked()
|
||||
self.term.setMultiValue(multi)
|
||||
|
||||
def removeSelf(self):
|
||||
self.term.node().removeTerminal(self.term)
|
||||
|
||||
def mouseDragEvent(self, ev):
|
||||
if ev.button() != QtCore.Qt.LeftButton:
|
||||
ev.ignore()
|
||||
return
|
||||
|
||||
ev.accept()
|
||||
if ev.isStart():
|
||||
if self.newConnection is None:
|
||||
self.newConnection = ConnectionItem(self)
|
||||
#self.scene().addItem(self.newConnection)
|
||||
self.getViewBox().addItem(self.newConnection)
|
||||
#self.newConnection.setParentItem(self.parent().parent())
|
||||
|
||||
self.newConnection.setTarget(self.mapToView(ev.pos()))
|
||||
elif ev.isFinish():
|
||||
if self.newConnection is not None:
|
||||
items = self.scene().items(ev.scenePos())
|
||||
gotTarget = False
|
||||
for i in items:
|
||||
if isinstance(i, TerminalGraphicsItem):
|
||||
self.newConnection.setTarget(i)
|
||||
try:
|
||||
self.term.connectTo(i.term, self.newConnection)
|
||||
gotTarget = True
|
||||
except:
|
||||
self.scene().removeItem(self.newConnection)
|
||||
self.newConnection = None
|
||||
raise
|
||||
break
|
||||
|
||||
if not gotTarget:
|
||||
#print "remove unused connection"
|
||||
#self.scene().removeItem(self.newConnection)
|
||||
self.newConnection.close()
|
||||
self.newConnection = None
|
||||
else:
|
||||
if self.newConnection is not None:
|
||||
self.newConnection.setTarget(self.mapToView(ev.pos()))
|
||||
|
||||
def hoverEvent(self, ev):
|
||||
if not ev.isExit() and ev.acceptDrags(QtCore.Qt.LeftButton):
|
||||
ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it.
|
||||
ev.acceptClicks(QtCore.Qt.RightButton)
|
||||
self.box.setBrush(fn.mkBrush('w'))
|
||||
else:
|
||||
self.box.setBrush(self.brush)
|
||||
self.update()
|
||||
|
||||
#def hoverEnterEvent(self, ev):
|
||||
#self.hover = True
|
||||
|
||||
#def hoverLeaveEvent(self, ev):
|
||||
#self.hover = False
|
||||
|
||||
def connectPoint(self):
|
||||
## return the connect position of this terminal in view coords
|
||||
return self.mapToView(self.mapFromItem(self.box, self.box.boundingRect().center()))
|
||||
|
||||
def nodeMoved(self):
|
||||
for t, item in self.term.connections().items():
|
||||
item.updateLine()
|
||||
|
||||
|
||||
#class ConnectionItem(QtGui.QGraphicsItem):
|
||||
class ConnectionItem(GraphicsObject):
|
||||
|
||||
def __init__(self, source, target=None):
|
||||
#QtGui.QGraphicsItem.__init__(self)
|
||||
GraphicsObject.__init__(self)
|
||||
self.setFlags(
|
||||
self.ItemIsSelectable |
|
||||
self.ItemIsFocusable
|
||||
)
|
||||
self.source = source
|
||||
self.target = target
|
||||
self.length = 0
|
||||
self.hovered = False
|
||||
self.path = None
|
||||
self.shapePath = None
|
||||
self.style = {
|
||||
'shape': 'line',
|
||||
'color': (100, 100, 250),
|
||||
'width': 1.0,
|
||||
'hoverColor': (150, 150, 250),
|
||||
'hoverWidth': 1.0,
|
||||
'selectedColor': (200, 200, 0),
|
||||
'selectedWidth': 3.0,
|
||||
}
|
||||
#self.line = QtGui.QGraphicsLineItem(self)
|
||||
self.source.getViewBox().addItem(self)
|
||||
self.updateLine()
|
||||
self.setZValue(0)
|
||||
|
||||
def close(self):
|
||||
if self.scene() is not None:
|
||||
#self.scene().removeItem(self.line)
|
||||
self.scene().removeItem(self)
|
||||
|
||||
def setTarget(self, target):
|
||||
self.target = target
|
||||
self.updateLine()
|
||||
|
||||
def setStyle(self, **kwds):
|
||||
self.style.update(kwds)
|
||||
if 'shape' in kwds:
|
||||
self.updateLine()
|
||||
else:
|
||||
self.update()
|
||||
|
||||
def updateLine(self):
|
||||
start = Point(self.source.connectPoint())
|
||||
if isinstance(self.target, TerminalGraphicsItem):
|
||||
stop = Point(self.target.connectPoint())
|
||||
elif isinstance(self.target, QtCore.QPointF):
|
||||
stop = Point(self.target)
|
||||
else:
|
||||
return
|
||||
self.prepareGeometryChange()
|
||||
|
||||
self.path = self.generatePath(start, stop)
|
||||
self.shapePath = None
|
||||
self.update()
|
||||
|
||||
def generatePath(self, start, stop):
|
||||
path = QtGui.QPainterPath()
|
||||
path.moveTo(start)
|
||||
if self.style['shape'] == 'line':
|
||||
path.lineTo(stop)
|
||||
elif self.style['shape'] == 'cubic':
|
||||
path.cubicTo(Point(stop.x(), start.y()), Point(start.x(), stop.y()), Point(stop.x(), stop.y()))
|
||||
else:
|
||||
raise Exception('Invalid shape "%s"; options are "line" or "cubic"' % self.style['shape'])
|
||||
return path
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace:
|
||||
#if isinstance(self.target, TerminalGraphicsItem):
|
||||
self.source.disconnect(self.target)
|
||||
ev.accept()
|
||||
else:
|
||||
ev.ignore()
|
||||
|
||||
def mousePressEvent(self, ev):
|
||||
ev.ignore()
|
||||
|
||||
def mouseClickEvent(self, ev):
|
||||
if ev.button() == QtCore.Qt.LeftButton:
|
||||
ev.accept()
|
||||
sel = self.isSelected()
|
||||
self.setSelected(True)
|
||||
if not sel and self.isSelected():
|
||||
self.update()
|
||||
|
||||
def hoverEvent(self, ev):
|
||||
if (not ev.isExit()) and ev.acceptClicks(QtCore.Qt.LeftButton):
|
||||
self.hovered = True
|
||||
else:
|
||||
self.hovered = False
|
||||
self.update()
|
||||
|
||||
|
||||
def boundingRect(self):
|
||||
return self.shape().boundingRect()
|
||||
##return self.line.boundingRect()
|
||||
#px = self.pixelWidth()
|
||||
#return QtCore.QRectF(-5*px, 0, 10*px, self.length)
|
||||
def viewRangeChanged(self):
|
||||
self.shapePath = None
|
||||
self.prepareGeometryChange()
|
||||
|
||||
def shape(self):
|
||||
if self.shapePath is None:
|
||||
if self.path is None:
|
||||
return QtGui.QPainterPath()
|
||||
stroker = QtGui.QPainterPathStroker()
|
||||
px = self.pixelWidth()
|
||||
stroker.setWidth(px*8)
|
||||
self.shapePath = stroker.createStroke(self.path)
|
||||
return self.shapePath
|
||||
|
||||
def paint(self, p, *args):
|
||||
if self.isSelected():
|
||||
p.setPen(fn.mkPen(self.style['selectedColor'], width=self.style['selectedWidth']))
|
||||
else:
|
||||
if self.hovered:
|
||||
p.setPen(fn.mkPen(self.style['hoverColor'], width=self.style['hoverWidth']))
|
||||
else:
|
||||
p.setPen(fn.mkPen(self.style['color'], width=self.style['width']))
|
||||
|
||||
#p.drawLine(0, 0, 0, self.length)
|
||||
|
||||
p.drawPath(self.path)
|
4
pyqtgraph/flowchart/__init__.py
Normal file
4
pyqtgraph/flowchart/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .Flowchart import *
|
||||
|
||||
from .library import getNodeType, registerNodeType, getNodeTree
|
36
pyqtgraph/flowchart/eq.py
Normal file
36
pyqtgraph/flowchart/eq.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from numpy import ndarray, bool_
|
||||
from ..metaarray import MetaArray
|
||||
|
||||
def eq(a, b):
|
||||
"""The great missing equivalence function: Guaranteed evaluation to a single bool value."""
|
||||
if a is b:
|
||||
return True
|
||||
|
||||
try:
|
||||
e = a==b
|
||||
except ValueError:
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
except:
|
||||
print("a:", str(type(a)), str(a))
|
||||
print("b:", str(type(b)), str(b))
|
||||
raise
|
||||
t = type(e)
|
||||
if t is bool:
|
||||
return e
|
||||
elif t is bool_:
|
||||
return bool(e)
|
||||
elif isinstance(e, ndarray) or (hasattr(e, 'implements') and e.implements('MetaArray')):
|
||||
try: ## disaster: if a is an empty array and b is not, then e.all() is True
|
||||
if a.shape != b.shape:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
if (hasattr(e, 'implements') and e.implements('MetaArray')):
|
||||
return e.asarray().all()
|
||||
else:
|
||||
return e.all()
|
||||
else:
|
||||
raise Exception("== operator returned type %s" % str(type(e)))
|
356
pyqtgraph/flowchart/library/Data.py
Normal file
356
pyqtgraph/flowchart/library/Data.py
Normal file
|
@ -0,0 +1,356 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ..Node import Node
|
||||
from ...Qt import QtGui, QtCore
|
||||
import numpy as np
|
||||
from .common import *
|
||||
from ...SRTTransform import SRTTransform
|
||||
from ...Point import Point
|
||||
from ...widgets.TreeWidget import TreeWidget
|
||||
from ...graphicsItems.LinearRegionItem import LinearRegionItem
|
||||
|
||||
from . import functions
|
||||
|
||||
class ColumnSelectNode(Node):
|
||||
"""Select named columns from a record array or MetaArray."""
|
||||
nodeName = "ColumnSelect"
|
||||
def __init__(self, name):
|
||||
Node.__init__(self, name, terminals={'In': {'io': 'in'}})
|
||||
self.columns = set()
|
||||
self.columnList = QtGui.QListWidget()
|
||||
self.axis = 0
|
||||
self.columnList.itemChanged.connect(self.itemChanged)
|
||||
|
||||
def process(self, In, display=True):
|
||||
if display:
|
||||
self.updateList(In)
|
||||
|
||||
out = {}
|
||||
if hasattr(In, 'implements') and In.implements('MetaArray'):
|
||||
for c in self.columns:
|
||||
out[c] = In[self.axis:c]
|
||||
elif isinstance(In, np.ndarray) and In.dtype.fields is not None:
|
||||
for c in self.columns:
|
||||
out[c] = In[c]
|
||||
else:
|
||||
self.In.setValueAcceptable(False)
|
||||
raise Exception("Input must be MetaArray or ndarray with named fields")
|
||||
|
||||
return out
|
||||
|
||||
def ctrlWidget(self):
|
||||
return self.columnList
|
||||
|
||||
def updateList(self, data):
|
||||
if hasattr(data, 'implements') and data.implements('MetaArray'):
|
||||
cols = data.listColumns()
|
||||
for ax in cols: ## find first axis with columns
|
||||
if len(cols[ax]) > 0:
|
||||
self.axis = ax
|
||||
cols = set(cols[ax])
|
||||
break
|
||||
else:
|
||||
cols = list(data.dtype.fields.keys())
|
||||
|
||||
rem = set()
|
||||
for c in self.columns:
|
||||
if c not in cols:
|
||||
self.removeTerminal(c)
|
||||
rem.add(c)
|
||||
self.columns -= rem
|
||||
|
||||
self.columnList.blockSignals(True)
|
||||
self.columnList.clear()
|
||||
for c in cols:
|
||||
item = QtGui.QListWidgetItem(c)
|
||||
item.setFlags(QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsUserCheckable)
|
||||
if c in self.columns:
|
||||
item.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.columnList.addItem(item)
|
||||
self.columnList.blockSignals(False)
|
||||
|
||||
|
||||
def itemChanged(self, item):
|
||||
col = str(item.text())
|
||||
if item.checkState() == QtCore.Qt.Checked:
|
||||
if col not in self.columns:
|
||||
self.columns.add(col)
|
||||
self.addOutput(col)
|
||||
else:
|
||||
if col in self.columns:
|
||||
self.columns.remove(col)
|
||||
self.removeTerminal(col)
|
||||
self.update()
|
||||
|
||||
def saveState(self):
|
||||
state = Node.saveState(self)
|
||||
state['columns'] = list(self.columns)
|
||||
return state
|
||||
|
||||
def restoreState(self, state):
|
||||
Node.restoreState(self, state)
|
||||
self.columns = set(state.get('columns', []))
|
||||
for c in self.columns:
|
||||
self.addOutput(c)
|
||||
|
||||
|
||||
|
||||
class RegionSelectNode(CtrlNode):
|
||||
"""Returns a slice from a 1-D array. Connect the 'widget' output to a plot to display a region-selection widget."""
|
||||
nodeName = "RegionSelect"
|
||||
uiTemplate = [
|
||||
('start', 'spin', {'value': 0, 'step': 0.1}),
|
||||
('stop', 'spin', {'value': 0.1, 'step': 0.1}),
|
||||
('display', 'check', {'value': True}),
|
||||
('movable', 'check', {'value': True}),
|
||||
]
|
||||
|
||||
def __init__(self, name):
|
||||
self.items = {}
|
||||
CtrlNode.__init__(self, name, terminals={
|
||||
'data': {'io': 'in'},
|
||||
'selected': {'io': 'out'},
|
||||
'region': {'io': 'out'},
|
||||
'widget': {'io': 'out', 'multi': True}
|
||||
})
|
||||
self.ctrls['display'].toggled.connect(self.displayToggled)
|
||||
self.ctrls['movable'].toggled.connect(self.movableToggled)
|
||||
|
||||
def displayToggled(self, b):
|
||||
for item in self.items.values():
|
||||
item.setVisible(b)
|
||||
|
||||
def movableToggled(self, b):
|
||||
for item in self.items.values():
|
||||
item.setMovable(b)
|
||||
|
||||
|
||||
def process(self, data=None, display=True):
|
||||
#print "process.."
|
||||
s = self.stateGroup.state()
|
||||
region = [s['start'], s['stop']]
|
||||
|
||||
if display:
|
||||
conn = self['widget'].connections()
|
||||
for c in conn:
|
||||
plot = c.node().getPlot()
|
||||
if plot is None:
|
||||
continue
|
||||
if c in self.items:
|
||||
item = self.items[c]
|
||||
item.setRegion(region)
|
||||
#print " set rgn:", c, region
|
||||
#item.setXVals(events)
|
||||
else:
|
||||
item = LinearRegionItem(values=region)
|
||||
self.items[c] = item
|
||||
#item.connect(item, QtCore.SIGNAL('regionChanged'), self.rgnChanged)
|
||||
item.sigRegionChanged.connect(self.rgnChanged)
|
||||
item.setVisible(s['display'])
|
||||
item.setMovable(s['movable'])
|
||||
#print " new rgn:", c, region
|
||||
#self.items[c].setYRange([0., 0.2], relative=True)
|
||||
|
||||
if self['selected'].isConnected():
|
||||
if data is None:
|
||||
sliced = None
|
||||
elif (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
sliced = data[0:s['start']:s['stop']]
|
||||
else:
|
||||
mask = (data['time'] >= s['start']) * (data['time'] < s['stop'])
|
||||
sliced = data[mask]
|
||||
else:
|
||||
sliced = None
|
||||
|
||||
return {'selected': sliced, 'widget': self.items, 'region': region}
|
||||
|
||||
|
||||
def rgnChanged(self, item):
|
||||
region = item.getRegion()
|
||||
self.stateGroup.setState({'start': region[0], 'stop': region[1]})
|
||||
self.update()
|
||||
|
||||
|
||||
class EvalNode(Node):
|
||||
"""Return the output of a string evaluated/executed by the python interpreter.
|
||||
The string may be either an expression or a python script, and inputs are accessed as the name of the terminal.
|
||||
For expressions, a single value may be evaluated for a single output, or a dict for multiple outputs.
|
||||
For a script, the text will be executed as the body of a function."""
|
||||
nodeName = 'PythonEval'
|
||||
|
||||
def __init__(self, name):
|
||||
Node.__init__(self, name,
|
||||
terminals = {
|
||||
'input': {'io': 'in', 'renamable': True, 'multiable': True},
|
||||
'output': {'io': 'out', 'renamable': True, 'multiable': True},
|
||||
},
|
||||
allowAddInput=True, allowAddOutput=True)
|
||||
|
||||
self.ui = QtGui.QWidget()
|
||||
self.layout = QtGui.QGridLayout()
|
||||
#self.addInBtn = QtGui.QPushButton('+Input')
|
||||
#self.addOutBtn = QtGui.QPushButton('+Output')
|
||||
self.text = QtGui.QTextEdit()
|
||||
self.text.setTabStopWidth(30)
|
||||
self.text.setPlainText("# Access inputs as args['input_name']\nreturn {'output': None} ## one key per output terminal")
|
||||
#self.layout.addWidget(self.addInBtn, 0, 0)
|
||||
#self.layout.addWidget(self.addOutBtn, 0, 1)
|
||||
self.layout.addWidget(self.text, 1, 0, 1, 2)
|
||||
self.ui.setLayout(self.layout)
|
||||
|
||||
#QtCore.QObject.connect(self.addInBtn, QtCore.SIGNAL('clicked()'), self.addInput)
|
||||
#self.addInBtn.clicked.connect(self.addInput)
|
||||
#QtCore.QObject.connect(self.addOutBtn, QtCore.SIGNAL('clicked()'), self.addOutput)
|
||||
#self.addOutBtn.clicked.connect(self.addOutput)
|
||||
self.text.focusOutEvent = self.focusOutEvent
|
||||
self.lastText = None
|
||||
|
||||
def ctrlWidget(self):
|
||||
return self.ui
|
||||
|
||||
#def addInput(self):
|
||||
#Node.addInput(self, 'input', renamable=True)
|
||||
|
||||
#def addOutput(self):
|
||||
#Node.addOutput(self, 'output', renamable=True)
|
||||
|
||||
def focusOutEvent(self, ev):
|
||||
text = str(self.text.toPlainText())
|
||||
if text != self.lastText:
|
||||
self.lastText = text
|
||||
self.update()
|
||||
return QtGui.QTextEdit.focusOutEvent(self.text, ev)
|
||||
|
||||
def process(self, display=True, **args):
|
||||
l = locals()
|
||||
l.update(args)
|
||||
## try eval first, then exec
|
||||
try:
|
||||
text = str(self.text.toPlainText()).replace('\n', ' ')
|
||||
output = eval(text, globals(), l)
|
||||
except SyntaxError:
|
||||
fn = "def fn(**args):\n"
|
||||
run = "\noutput=fn(**args)\n"
|
||||
text = fn + "\n".join([" "+l for l in str(self.text.toPlainText()).split('\n')]) + run
|
||||
exec(text)
|
||||
except:
|
||||
print("Error processing node: %s" % self.name())
|
||||
raise
|
||||
return output
|
||||
|
||||
def saveState(self):
|
||||
state = Node.saveState(self)
|
||||
state['text'] = str(self.text.toPlainText())
|
||||
#state['terminals'] = self.saveTerminals()
|
||||
return state
|
||||
|
||||
def restoreState(self, state):
|
||||
Node.restoreState(self, state)
|
||||
self.text.clear()
|
||||
self.text.insertPlainText(state['text'])
|
||||
self.restoreTerminals(state['terminals'])
|
||||
self.update()
|
||||
|
||||
class ColumnJoinNode(Node):
|
||||
"""Concatenates record arrays and/or adds new columns"""
|
||||
nodeName = 'ColumnJoin'
|
||||
|
||||
def __init__(self, name):
|
||||
Node.__init__(self, name, terminals = {
|
||||
'output': {'io': 'out'},
|
||||
})
|
||||
|
||||
#self.items = []
|
||||
|
||||
self.ui = QtGui.QWidget()
|
||||
self.layout = QtGui.QGridLayout()
|
||||
self.ui.setLayout(self.layout)
|
||||
|
||||
self.tree = TreeWidget()
|
||||
self.addInBtn = QtGui.QPushButton('+ Input')
|
||||
self.remInBtn = QtGui.QPushButton('- Input')
|
||||
|
||||
self.layout.addWidget(self.tree, 0, 0, 1, 2)
|
||||
self.layout.addWidget(self.addInBtn, 1, 0)
|
||||
self.layout.addWidget(self.remInBtn, 1, 1)
|
||||
|
||||
self.addInBtn.clicked.connect(self.addInput)
|
||||
self.remInBtn.clicked.connect(self.remInput)
|
||||
self.tree.sigItemMoved.connect(self.update)
|
||||
|
||||
def ctrlWidget(self):
|
||||
return self.ui
|
||||
|
||||
def addInput(self):
|
||||
#print "ColumnJoinNode.addInput called."
|
||||
term = Node.addInput(self, 'input', renamable=True, removable=True, multiable=True)
|
||||
#print "Node.addInput returned. term:", term
|
||||
item = QtGui.QTreeWidgetItem([term.name()])
|
||||
item.term = term
|
||||
term.joinItem = item
|
||||
#self.items.append((term, item))
|
||||
self.tree.addTopLevelItem(item)
|
||||
|
||||
def remInput(self):
|
||||
sel = self.tree.currentItem()
|
||||
term = sel.term
|
||||
term.joinItem = None
|
||||
sel.term = None
|
||||
self.tree.removeTopLevelItem(sel)
|
||||
self.removeTerminal(term)
|
||||
self.update()
|
||||
|
||||
def process(self, display=True, **args):
|
||||
order = self.order()
|
||||
vals = []
|
||||
for name in order:
|
||||
if name not in args:
|
||||
continue
|
||||
val = args[name]
|
||||
if isinstance(val, np.ndarray) and len(val.dtype) > 0:
|
||||
vals.append(val)
|
||||
else:
|
||||
vals.append((name, None, val))
|
||||
return {'output': functions.concatenateColumns(vals)}
|
||||
|
||||
def order(self):
|
||||
return [str(self.tree.topLevelItem(i).text(0)) for i in range(self.tree.topLevelItemCount())]
|
||||
|
||||
def saveState(self):
|
||||
state = Node.saveState(self)
|
||||
state['order'] = self.order()
|
||||
return state
|
||||
|
||||
def restoreState(self, state):
|
||||
Node.restoreState(self, state)
|
||||
inputs = self.inputs()
|
||||
|
||||
## Node.restoreState should have created all of the terminals we need
|
||||
## However: to maintain support for some older flowchart files, we need
|
||||
## to manually add any terminals that were not taken care of.
|
||||
for name in [n for n in state['order'] if n not in inputs]:
|
||||
Node.addInput(self, name, renamable=True, removable=True, multiable=True)
|
||||
inputs = self.inputs()
|
||||
|
||||
order = [name for name in state['order'] if name in inputs]
|
||||
for name in inputs:
|
||||
if name not in order:
|
||||
order.append(name)
|
||||
|
||||
self.tree.clear()
|
||||
for name in order:
|
||||
term = self[name]
|
||||
item = QtGui.QTreeWidgetItem([name])
|
||||
item.term = term
|
||||
term.joinItem = item
|
||||
#self.items.append((term, item))
|
||||
self.tree.addTopLevelItem(item)
|
||||
|
||||
def terminalRenamed(self, term, oldName):
|
||||
Node.terminalRenamed(self, term, oldName)
|
||||
item = term.joinItem
|
||||
item.setText(0, term.name())
|
||||
self.update()
|
||||
|
||||
|
312
pyqtgraph/flowchart/library/Display.py
Normal file
312
pyqtgraph/flowchart/library/Display.py
Normal file
|
@ -0,0 +1,312 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ..Node import Node
|
||||
import weakref
|
||||
from ...Qt import QtCore, QtGui
|
||||
from ...graphicsItems.ScatterPlotItem import ScatterPlotItem
|
||||
from ...graphicsItems.PlotCurveItem import PlotCurveItem
|
||||
from ... import PlotDataItem, ComboBox
|
||||
|
||||
from .common import *
|
||||
import numpy as np
|
||||
|
||||
class PlotWidgetNode(Node):
|
||||
"""Connection to PlotWidget. Will plot arrays, metaarrays, and display event lists."""
|
||||
nodeName = 'PlotWidget'
|
||||
sigPlotChanged = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, name):
|
||||
Node.__init__(self, name, terminals={'In': {'io': 'in', 'multi': True}})
|
||||
self.plot = None # currently selected plot
|
||||
self.plots = {} # list of available plots user may select from
|
||||
self.ui = None
|
||||
self.items = {}
|
||||
|
||||
def disconnected(self, localTerm, remoteTerm):
|
||||
if localTerm is self['In'] and remoteTerm in self.items:
|
||||
self.plot.removeItem(self.items[remoteTerm])
|
||||
del self.items[remoteTerm]
|
||||
|
||||
def setPlot(self, plot):
|
||||
#print "======set plot"
|
||||
if plot == self.plot:
|
||||
return
|
||||
|
||||
# clear data from previous plot
|
||||
if self.plot is not None:
|
||||
for vid in list(self.items.keys()):
|
||||
self.plot.removeItem(self.items[vid])
|
||||
del self.items[vid]
|
||||
|
||||
self.plot = plot
|
||||
self.updateUi()
|
||||
self.update()
|
||||
self.sigPlotChanged.emit(self)
|
||||
|
||||
def getPlot(self):
|
||||
return self.plot
|
||||
|
||||
def process(self, In, display=True):
|
||||
if display and self.plot is not None:
|
||||
items = set()
|
||||
# Add all new input items to selected plot
|
||||
for name, vals in In.items():
|
||||
if vals is None:
|
||||
continue
|
||||
if type(vals) is not list:
|
||||
vals = [vals]
|
||||
|
||||
for val in vals:
|
||||
vid = id(val)
|
||||
if vid in self.items and self.items[vid].scene() is self.plot.scene():
|
||||
# Item is already added to the correct scene
|
||||
# possible bug: what if two plots occupy the same scene? (should
|
||||
# rarely be a problem because items are removed from a plot before
|
||||
# switching).
|
||||
items.add(vid)
|
||||
else:
|
||||
# Add the item to the plot, or generate a new item if needed.
|
||||
if isinstance(val, QtGui.QGraphicsItem):
|
||||
self.plot.addItem(val)
|
||||
item = val
|
||||
else:
|
||||
item = self.plot.plot(val)
|
||||
self.items[vid] = item
|
||||
items.add(vid)
|
||||
|
||||
# Any left-over items that did not appear in the input must be removed
|
||||
for vid in list(self.items.keys()):
|
||||
if vid not in items:
|
||||
self.plot.removeItem(self.items[vid])
|
||||
del self.items[vid]
|
||||
|
||||
def processBypassed(self, args):
|
||||
if self.plot is None:
|
||||
return
|
||||
for item in list(self.items.values()):
|
||||
self.plot.removeItem(item)
|
||||
self.items = {}
|
||||
|
||||
def ctrlWidget(self):
|
||||
if self.ui is None:
|
||||
self.ui = ComboBox()
|
||||
self.ui.currentIndexChanged.connect(self.plotSelected)
|
||||
self.updateUi()
|
||||
return self.ui
|
||||
|
||||
def plotSelected(self, index):
|
||||
self.setPlot(self.ui.value())
|
||||
|
||||
def setPlotList(self, plots):
|
||||
"""
|
||||
Specify the set of plots (PlotWidget or PlotItem) that the user may
|
||||
select from.
|
||||
|
||||
*plots* must be a dictionary of {name: plot} pairs.
|
||||
"""
|
||||
self.plots = plots
|
||||
self.updateUi()
|
||||
|
||||
def updateUi(self):
|
||||
# sets list and automatically preserves previous selection
|
||||
self.ui.setItems(self.plots)
|
||||
try:
|
||||
self.ui.setValue(self.plot)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
class CanvasNode(Node):
|
||||
"""Connection to a Canvas widget."""
|
||||
nodeName = 'CanvasWidget'
|
||||
|
||||
def __init__(self, name):
|
||||
Node.__init__(self, name, terminals={'In': {'io': 'in', 'multi': True}})
|
||||
self.canvas = None
|
||||
self.items = {}
|
||||
|
||||
def disconnected(self, localTerm, remoteTerm):
|
||||
if localTerm is self.In and remoteTerm in self.items:
|
||||
self.canvas.removeItem(self.items[remoteTerm])
|
||||
del self.items[remoteTerm]
|
||||
|
||||
def setCanvas(self, canvas):
|
||||
self.canvas = canvas
|
||||
|
||||
def getCanvas(self):
|
||||
return self.canvas
|
||||
|
||||
def process(self, In, display=True):
|
||||
if display:
|
||||
items = set()
|
||||
for name, vals in In.items():
|
||||
if vals is None:
|
||||
continue
|
||||
if type(vals) is not list:
|
||||
vals = [vals]
|
||||
|
||||
for val in vals:
|
||||
vid = id(val)
|
||||
if vid in self.items:
|
||||
items.add(vid)
|
||||
else:
|
||||
self.canvas.addItem(val)
|
||||
item = val
|
||||
self.items[vid] = item
|
||||
items.add(vid)
|
||||
for vid in list(self.items.keys()):
|
||||
if vid not in items:
|
||||
#print "remove", self.items[vid]
|
||||
self.canvas.removeItem(self.items[vid])
|
||||
del self.items[vid]
|
||||
|
||||
|
||||
class PlotCurve(CtrlNode):
|
||||
"""Generates a plot curve from x/y data"""
|
||||
nodeName = 'PlotCurve'
|
||||
uiTemplate = [
|
||||
('color', 'color'),
|
||||
]
|
||||
|
||||
def __init__(self, name):
|
||||
CtrlNode.__init__(self, name, terminals={
|
||||
'x': {'io': 'in'},
|
||||
'y': {'io': 'in'},
|
||||
'plot': {'io': 'out'}
|
||||
})
|
||||
self.item = PlotDataItem()
|
||||
|
||||
def process(self, x, y, display=True):
|
||||
#print "scatterplot process"
|
||||
if not display:
|
||||
return {'plot': None}
|
||||
|
||||
self.item.setData(x, y, pen=self.ctrls['color'].color())
|
||||
return {'plot': self.item}
|
||||
|
||||
|
||||
|
||||
|
||||
class ScatterPlot(CtrlNode):
|
||||
"""Generates a scatter plot from a record array or nested dicts"""
|
||||
nodeName = 'ScatterPlot'
|
||||
uiTemplate = [
|
||||
('x', 'combo', {'values': [], 'index': 0}),
|
||||
('y', 'combo', {'values': [], 'index': 0}),
|
||||
('sizeEnabled', 'check', {'value': False}),
|
||||
('size', 'combo', {'values': [], 'index': 0}),
|
||||
('absoluteSize', 'check', {'value': False}),
|
||||
('colorEnabled', 'check', {'value': False}),
|
||||
('color', 'colormap', {}),
|
||||
('borderEnabled', 'check', {'value': False}),
|
||||
('border', 'colormap', {}),
|
||||
]
|
||||
|
||||
def __init__(self, name):
|
||||
CtrlNode.__init__(self, name, terminals={
|
||||
'input': {'io': 'in'},
|
||||
'plot': {'io': 'out'}
|
||||
})
|
||||
self.item = ScatterPlotItem()
|
||||
self.keys = []
|
||||
|
||||
#self.ui = QtGui.QWidget()
|
||||
#self.layout = QtGui.QGridLayout()
|
||||
#self.ui.setLayout(self.layout)
|
||||
|
||||
#self.xCombo = QtGui.QComboBox()
|
||||
#self.yCombo = QtGui.QComboBox()
|
||||
|
||||
|
||||
|
||||
def process(self, input, display=True):
|
||||
#print "scatterplot process"
|
||||
if not display:
|
||||
return {'plot': None}
|
||||
|
||||
self.updateKeys(input[0])
|
||||
|
||||
x = str(self.ctrls['x'].currentText())
|
||||
y = str(self.ctrls['y'].currentText())
|
||||
size = str(self.ctrls['size'].currentText())
|
||||
pen = QtGui.QPen(QtGui.QColor(0,0,0,0))
|
||||
points = []
|
||||
for i in input:
|
||||
pt = {'pos': (i[x], i[y])}
|
||||
if self.ctrls['sizeEnabled'].isChecked():
|
||||
pt['size'] = i[size]
|
||||
if self.ctrls['borderEnabled'].isChecked():
|
||||
pt['pen'] = QtGui.QPen(self.ctrls['border'].getColor(i))
|
||||
else:
|
||||
pt['pen'] = pen
|
||||
if self.ctrls['colorEnabled'].isChecked():
|
||||
pt['brush'] = QtGui.QBrush(self.ctrls['color'].getColor(i))
|
||||
points.append(pt)
|
||||
self.item.setPxMode(not self.ctrls['absoluteSize'].isChecked())
|
||||
|
||||
self.item.setPoints(points)
|
||||
|
||||
return {'plot': self.item}
|
||||
|
||||
|
||||
|
||||
def updateKeys(self, data):
|
||||
if isinstance(data, dict):
|
||||
keys = list(data.keys())
|
||||
elif isinstance(data, list) or isinstance(data, tuple):
|
||||
keys = data
|
||||
elif isinstance(data, np.ndarray) or isinstance(data, np.void):
|
||||
keys = data.dtype.names
|
||||
else:
|
||||
print("Unknown data type:", type(data), data)
|
||||
return
|
||||
|
||||
for c in self.ctrls.values():
|
||||
c.blockSignals(True)
|
||||
for c in [self.ctrls['x'], self.ctrls['y'], self.ctrls['size']]:
|
||||
cur = str(c.currentText())
|
||||
c.clear()
|
||||
for k in keys:
|
||||
c.addItem(k)
|
||||
if k == cur:
|
||||
c.setCurrentIndex(c.count()-1)
|
||||
for c in [self.ctrls['color'], self.ctrls['border']]:
|
||||
c.setArgList(keys)
|
||||
for c in self.ctrls.values():
|
||||
c.blockSignals(False)
|
||||
|
||||
self.keys = keys
|
||||
|
||||
|
||||
def saveState(self):
|
||||
state = CtrlNode.saveState(self)
|
||||
return {'keys': self.keys, 'ctrls': state}
|
||||
|
||||
def restoreState(self, state):
|
||||
self.updateKeys(state['keys'])
|
||||
CtrlNode.restoreState(self, state['ctrls'])
|
||||
|
||||
#class ImageItem(Node):
|
||||
#"""Creates an ImageItem for display in a canvas from a file handle."""
|
||||
#nodeName = 'Image'
|
||||
|
||||
#def __init__(self, name):
|
||||
#Node.__init__(self, name, terminals={
|
||||
#'file': {'io': 'in'},
|
||||
#'image': {'io': 'out'}
|
||||
#})
|
||||
#self.imageItem = graphicsItems.ImageItem()
|
||||
#self.handle = None
|
||||
|
||||
#def process(self, file, display=True):
|
||||
#if not display:
|
||||
#return {'image': None}
|
||||
|
||||
#if file != self.handle:
|
||||
#self.handle = file
|
||||
#data = file.read()
|
||||
#self.imageItem.updateImage(data)
|
||||
|
||||
#pos = file.
|
||||
|
||||
|
||||
|
346
pyqtgraph/flowchart/library/Filters.py
Normal file
346
pyqtgraph/flowchart/library/Filters.py
Normal file
|
@ -0,0 +1,346 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ...Qt import QtCore, QtGui
|
||||
from ..Node import Node
|
||||
from . import functions
|
||||
from ... import functions as pgfn
|
||||
from .common import *
|
||||
import numpy as np
|
||||
|
||||
from ... import PolyLineROI
|
||||
from ... import Point
|
||||
from ... import metaarray as metaarray
|
||||
|
||||
|
||||
class Downsample(CtrlNode):
|
||||
"""Downsample by averaging samples together."""
|
||||
nodeName = 'Downsample'
|
||||
uiTemplate = [
|
||||
('n', 'intSpin', {'min': 1, 'max': 1000000})
|
||||
]
|
||||
|
||||
def processData(self, data):
|
||||
return functions.downsample(data, self.ctrls['n'].value(), axis=0)
|
||||
|
||||
|
||||
class Subsample(CtrlNode):
|
||||
"""Downsample by selecting every Nth sample."""
|
||||
nodeName = 'Subsample'
|
||||
uiTemplate = [
|
||||
('n', 'intSpin', {'min': 1, 'max': 1000000})
|
||||
]
|
||||
|
||||
def processData(self, data):
|
||||
return data[::self.ctrls['n'].value()]
|
||||
|
||||
|
||||
class Bessel(CtrlNode):
|
||||
"""Bessel filter. Input data must have time values."""
|
||||
nodeName = 'BesselFilter'
|
||||
uiTemplate = [
|
||||
('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}),
|
||||
('cutoff', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
|
||||
('order', 'intSpin', {'value': 4, 'min': 1, 'max': 16}),
|
||||
('bidir', 'check', {'checked': True})
|
||||
]
|
||||
|
||||
def processData(self, data):
|
||||
s = self.stateGroup.state()
|
||||
if s['band'] == 'lowpass':
|
||||
mode = 'low'
|
||||
else:
|
||||
mode = 'high'
|
||||
return functions.besselFilter(data, bidir=s['bidir'], btype=mode, cutoff=s['cutoff'], order=s['order'])
|
||||
|
||||
|
||||
class Butterworth(CtrlNode):
|
||||
"""Butterworth filter"""
|
||||
nodeName = 'ButterworthFilter'
|
||||
uiTemplate = [
|
||||
('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}),
|
||||
('wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
|
||||
('wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
|
||||
('gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
|
||||
('gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
|
||||
('bidir', 'check', {'checked': True})
|
||||
]
|
||||
|
||||
def processData(self, data):
|
||||
s = self.stateGroup.state()
|
||||
if s['band'] == 'lowpass':
|
||||
mode = 'low'
|
||||
else:
|
||||
mode = 'high'
|
||||
ret = functions.butterworthFilter(data, bidir=s['bidir'], btype=mode, wPass=s['wPass'], wStop=s['wStop'], gPass=s['gPass'], gStop=s['gStop'])
|
||||
return ret
|
||||
|
||||
|
||||
class ButterworthNotch(CtrlNode):
|
||||
"""Butterworth notch filter"""
|
||||
nodeName = 'ButterworthNotchFilter'
|
||||
uiTemplate = [
|
||||
('low_wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
|
||||
('low_wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
|
||||
('low_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
|
||||
('low_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
|
||||
('high_wPass', 'spin', {'value': 3000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
|
||||
('high_wStop', 'spin', {'value': 4000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
|
||||
('high_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
|
||||
('high_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
|
||||
('bidir', 'check', {'checked': True})
|
||||
]
|
||||
|
||||
def processData(self, data):
|
||||
s = self.stateGroup.state()
|
||||
|
||||
low = functions.butterworthFilter(data, bidir=s['bidir'], btype='low', wPass=s['low_wPass'], wStop=s['low_wStop'], gPass=s['low_gPass'], gStop=s['low_gStop'])
|
||||
high = functions.butterworthFilter(data, bidir=s['bidir'], btype='high', wPass=s['high_wPass'], wStop=s['high_wStop'], gPass=s['high_gPass'], gStop=s['high_gStop'])
|
||||
return low + high
|
||||
|
||||
|
||||
class Mean(CtrlNode):
|
||||
"""Filters data by taking the mean of a sliding window"""
|
||||
nodeName = 'MeanFilter'
|
||||
uiTemplate = [
|
||||
('n', 'intSpin', {'min': 1, 'max': 1000000})
|
||||
]
|
||||
|
||||
@metaArrayWrapper
|
||||
def processData(self, data):
|
||||
n = self.ctrls['n'].value()
|
||||
return functions.rollingSum(data, n) / n
|
||||
|
||||
|
||||
class Median(CtrlNode):
|
||||
"""Filters data by taking the median of a sliding window"""
|
||||
nodeName = 'MedianFilter'
|
||||
uiTemplate = [
|
||||
('n', 'intSpin', {'min': 1, 'max': 1000000})
|
||||
]
|
||||
|
||||
@metaArrayWrapper
|
||||
def processData(self, data):
|
||||
try:
|
||||
import scipy.ndimage
|
||||
except ImportError:
|
||||
raise Exception("MedianFilter node requires the package scipy.ndimage.")
|
||||
return scipy.ndimage.median_filter(data, self.ctrls['n'].value())
|
||||
|
||||
class Mode(CtrlNode):
|
||||
"""Filters data by taking the mode (histogram-based) of a sliding window"""
|
||||
nodeName = 'ModeFilter'
|
||||
uiTemplate = [
|
||||
('window', 'intSpin', {'value': 500, 'min': 1, 'max': 1000000}),
|
||||
]
|
||||
|
||||
@metaArrayWrapper
|
||||
def processData(self, data):
|
||||
return functions.modeFilter(data, self.ctrls['window'].value())
|
||||
|
||||
|
||||
class Denoise(CtrlNode):
|
||||
"""Removes anomalous spikes from data, replacing with nearby values"""
|
||||
nodeName = 'DenoiseFilter'
|
||||
uiTemplate = [
|
||||
('radius', 'intSpin', {'value': 2, 'min': 0, 'max': 1000000}),
|
||||
('threshold', 'doubleSpin', {'value': 4.0, 'min': 0, 'max': 1000})
|
||||
]
|
||||
|
||||
def processData(self, data):
|
||||
#print "DENOISE"
|
||||
s = self.stateGroup.state()
|
||||
return functions.denoise(data, **s)
|
||||
|
||||
|
||||
class Gaussian(CtrlNode):
|
||||
"""Gaussian smoothing filter."""
|
||||
nodeName = 'GaussianFilter'
|
||||
uiTemplate = [
|
||||
('sigma', 'doubleSpin', {'min': 0, 'max': 1000000})
|
||||
]
|
||||
|
||||
@metaArrayWrapper
|
||||
def processData(self, data):
|
||||
try:
|
||||
import scipy.ndimage
|
||||
except ImportError:
|
||||
raise Exception("GaussianFilter node requires the package scipy.ndimage.")
|
||||
return pgfn.gaussianFilter(data, self.ctrls['sigma'].value())
|
||||
|
||||
|
||||
class Derivative(CtrlNode):
|
||||
"""Returns the pointwise derivative of the input"""
|
||||
nodeName = 'DerivativeFilter'
|
||||
|
||||
def processData(self, data):
|
||||
if hasattr(data, 'implements') and data.implements('MetaArray'):
|
||||
info = data.infoCopy()
|
||||
if 'values' in info[0]:
|
||||
info[0]['values'] = info[0]['values'][:-1]
|
||||
return metaarray.MetaArray(data[1:] - data[:-1], info=info)
|
||||
else:
|
||||
return data[1:] - data[:-1]
|
||||
|
||||
|
||||
class Integral(CtrlNode):
|
||||
"""Returns the pointwise integral of the input"""
|
||||
nodeName = 'IntegralFilter'
|
||||
|
||||
@metaArrayWrapper
|
||||
def processData(self, data):
|
||||
data[1:] += data[:-1]
|
||||
return data
|
||||
|
||||
|
||||
class Detrend(CtrlNode):
|
||||
"""Removes linear trend from the data"""
|
||||
nodeName = 'DetrendFilter'
|
||||
|
||||
@metaArrayWrapper
|
||||
def processData(self, data):
|
||||
try:
|
||||
from scipy.signal import detrend
|
||||
except ImportError:
|
||||
raise Exception("DetrendFilter node requires the package scipy.signal.")
|
||||
return detrend(data)
|
||||
|
||||
class RemoveBaseline(PlottingCtrlNode):
|
||||
"""Remove an arbitrary, graphically defined baseline from the data."""
|
||||
nodeName = 'RemoveBaseline'
|
||||
|
||||
def __init__(self, name):
|
||||
## define inputs and outputs (one output needs to be a plot)
|
||||
PlottingCtrlNode.__init__(self, name)
|
||||
self.line = PolyLineROI([[0,0],[1,0]])
|
||||
self.line.sigRegionChanged.connect(self.changed)
|
||||
|
||||
## create a PolyLineROI, add it to a plot -- actually, I think we want to do this after the node is connected to a plot (look at EventDetection.ThresholdEvents node for ideas), and possible after there is data. We will need to update the end positions of the line each time the input data changes
|
||||
#self.line = None ## will become a PolyLineROI
|
||||
|
||||
def connectToPlot(self, node):
|
||||
"""Define what happens when the node is connected to a plot"""
|
||||
|
||||
if node.plot is None:
|
||||
return
|
||||
node.getPlot().addItem(self.line)
|
||||
|
||||
def disconnectFromPlot(self, plot):
|
||||
"""Define what happens when the node is disconnected from a plot"""
|
||||
plot.removeItem(self.line)
|
||||
|
||||
def processData(self, data):
|
||||
## get array of baseline (from PolyLineROI)
|
||||
h0 = self.line.getHandles()[0]
|
||||
h1 = self.line.getHandles()[-1]
|
||||
|
||||
timeVals = data.xvals(0)
|
||||
h0.setPos(timeVals[0], h0.pos()[1])
|
||||
h1.setPos(timeVals[-1], h1.pos()[1])
|
||||
|
||||
pts = self.line.listPoints() ## lists line handles in same coordinates as data
|
||||
pts, indices = self.adjustXPositions(pts, timeVals) ## maxe sure x positions match x positions of data points
|
||||
|
||||
## construct an array that represents the baseline
|
||||
arr = np.zeros(len(data), dtype=float)
|
||||
n = 1
|
||||
arr[0] = pts[0].y()
|
||||
for i in range(len(pts)-1):
|
||||
x1 = pts[i].x()
|
||||
x2 = pts[i+1].x()
|
||||
y1 = pts[i].y()
|
||||
y2 = pts[i+1].y()
|
||||
m = (y2-y1)/(x2-x1)
|
||||
b = y1
|
||||
|
||||
times = timeVals[(timeVals > x1)*(timeVals <= x2)]
|
||||
arr[n:n+len(times)] = (m*(times-times[0]))+b
|
||||
n += len(times)
|
||||
|
||||
return data - arr ## subract baseline from data
|
||||
|
||||
def adjustXPositions(self, pts, data):
|
||||
"""Return a list of Point() where the x position is set to the nearest x value in *data* for each point in *pts*."""
|
||||
points = []
|
||||
timeIndices = []
|
||||
for p in pts:
|
||||
x = np.argwhere(abs(data - p.x()) == abs(data - p.x()).min())
|
||||
points.append(Point(data[x], p.y()))
|
||||
timeIndices.append(x)
|
||||
|
||||
return points, timeIndices
|
||||
|
||||
|
||||
|
||||
class AdaptiveDetrend(CtrlNode):
|
||||
"""Removes baseline from data, ignoring anomalous events"""
|
||||
nodeName = 'AdaptiveDetrend'
|
||||
uiTemplate = [
|
||||
('threshold', 'doubleSpin', {'value': 3.0, 'min': 0, 'max': 1000000})
|
||||
]
|
||||
|
||||
def processData(self, data):
|
||||
return functions.adaptiveDetrend(data, threshold=self.ctrls['threshold'].value())
|
||||
|
||||
class HistogramDetrend(CtrlNode):
|
||||
"""Removes baseline from data by computing mode (from histogram) of beginning and end of data."""
|
||||
nodeName = 'HistogramDetrend'
|
||||
uiTemplate = [
|
||||
('windowSize', 'intSpin', {'value': 500, 'min': 10, 'max': 1000000, 'suffix': 'pts'}),
|
||||
('numBins', 'intSpin', {'value': 50, 'min': 3, 'max': 1000000}),
|
||||
('offsetOnly', 'check', {'checked': False}),
|
||||
]
|
||||
|
||||
def processData(self, data):
|
||||
s = self.stateGroup.state()
|
||||
#ws = self.ctrls['windowSize'].value()
|
||||
#bn = self.ctrls['numBins'].value()
|
||||
#offset = self.ctrls['offsetOnly'].checked()
|
||||
return functions.histogramDetrend(data, window=s['windowSize'], bins=s['numBins'], offsetOnly=s['offsetOnly'])
|
||||
|
||||
|
||||
|
||||
class RemovePeriodic(CtrlNode):
|
||||
nodeName = 'RemovePeriodic'
|
||||
uiTemplate = [
|
||||
#('windowSize', 'intSpin', {'value': 500, 'min': 10, 'max': 1000000, 'suffix': 'pts'}),
|
||||
#('numBins', 'intSpin', {'value': 50, 'min': 3, 'max': 1000000})
|
||||
('f0', 'spin', {'value': 60, 'suffix': 'Hz', 'siPrefix': True, 'min': 0, 'max': None}),
|
||||
('harmonics', 'intSpin', {'value': 30, 'min': 0}),
|
||||
('samples', 'intSpin', {'value': 1, 'min': 1}),
|
||||
]
|
||||
|
||||
def processData(self, data):
|
||||
times = data.xvals('Time')
|
||||
dt = times[1]-times[0]
|
||||
|
||||
data1 = data.asarray()
|
||||
ft = np.fft.fft(data1)
|
||||
|
||||
## determine frequencies in fft data
|
||||
df = 1.0 / (len(data1) * dt)
|
||||
freqs = np.linspace(0.0, (len(ft)-1) * df, len(ft))
|
||||
|
||||
## flatten spikes at f0 and harmonics
|
||||
f0 = self.ctrls['f0'].value()
|
||||
for i in xrange(1, self.ctrls['harmonics'].value()+2):
|
||||
f = f0 * i # target frequency
|
||||
|
||||
## determine index range to check for this frequency
|
||||
ind1 = int(np.floor(f / df))
|
||||
ind2 = int(np.ceil(f / df)) + (self.ctrls['samples'].value()-1)
|
||||
if ind1 > len(ft)/2.:
|
||||
break
|
||||
mag = (abs(ft[ind1-1]) + abs(ft[ind2+1])) * 0.5
|
||||
for j in range(ind1, ind2+1):
|
||||
phase = np.angle(ft[j]) ## Must preserve the phase of each point, otherwise any transients in the trace might lead to large artifacts.
|
||||
re = mag * np.cos(phase)
|
||||
im = mag * np.sin(phase)
|
||||
ft[j] = re + im*1j
|
||||
ft[len(ft)-j] = re - im*1j
|
||||
|
||||
data2 = np.fft.ifft(ft).real
|
||||
|
||||
ma = metaarray.MetaArray(data2, info=data.infoCopy())
|
||||
return ma
|
||||
|
||||
|
||||
|
74
pyqtgraph/flowchart/library/Operators.py
Normal file
74
pyqtgraph/flowchart/library/Operators.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ..Node import Node
|
||||
|
||||
class UniOpNode(Node):
|
||||
"""Generic node for performing any operation like Out = In.fn()"""
|
||||
def __init__(self, name, fn):
|
||||
self.fn = fn
|
||||
Node.__init__(self, name, terminals={
|
||||
'In': {'io': 'in'},
|
||||
'Out': {'io': 'out', 'bypass': 'In'}
|
||||
})
|
||||
|
||||
def process(self, **args):
|
||||
return {'Out': getattr(args['In'], self.fn)()}
|
||||
|
||||
class BinOpNode(Node):
|
||||
"""Generic node for performing any operation like A.fn(B)"""
|
||||
def __init__(self, name, fn):
|
||||
self.fn = fn
|
||||
Node.__init__(self, name, terminals={
|
||||
'A': {'io': 'in'},
|
||||
'B': {'io': 'in'},
|
||||
'Out': {'io': 'out', 'bypass': 'A'}
|
||||
})
|
||||
|
||||
def process(self, **args):
|
||||
if isinstance(self.fn, tuple):
|
||||
for name in self.fn:
|
||||
try:
|
||||
fn = getattr(args['A'], name)
|
||||
break
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
fn = getattr(args['A'], self.fn)
|
||||
out = fn(args['B'])
|
||||
if out is NotImplemented:
|
||||
raise Exception("Operation %s not implemented between %s and %s" % (fn, str(type(args['A'])), str(type(args['B']))))
|
||||
#print " ", fn, out
|
||||
return {'Out': out}
|
||||
|
||||
|
||||
class AbsNode(UniOpNode):
|
||||
"""Returns abs(Inp). Does not check input types."""
|
||||
nodeName = 'Abs'
|
||||
def __init__(self, name):
|
||||
UniOpNode.__init__(self, name, '__abs__')
|
||||
|
||||
class AddNode(BinOpNode):
|
||||
"""Returns A + B. Does not check input types."""
|
||||
nodeName = 'Add'
|
||||
def __init__(self, name):
|
||||
BinOpNode.__init__(self, name, '__add__')
|
||||
|
||||
class SubtractNode(BinOpNode):
|
||||
"""Returns A - B. Does not check input types."""
|
||||
nodeName = 'Subtract'
|
||||
def __init__(self, name):
|
||||
BinOpNode.__init__(self, name, '__sub__')
|
||||
|
||||
class MultiplyNode(BinOpNode):
|
||||
"""Returns A * B. Does not check input types."""
|
||||
nodeName = 'Multiply'
|
||||
def __init__(self, name):
|
||||
BinOpNode.__init__(self, name, '__mul__')
|
||||
|
||||
class DivideNode(BinOpNode):
|
||||
"""Returns A / B. Does not check input types."""
|
||||
nodeName = 'Divide'
|
||||
def __init__(self, name):
|
||||
# try truediv first, followed by div
|
||||
BinOpNode.__init__(self, name, ('__truediv__', '__div__'))
|
||||
|
||||
|
28
pyqtgraph/flowchart/library/__init__.py
Normal file
28
pyqtgraph/flowchart/library/__init__.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ...pgcollections import OrderedDict
|
||||
import os, types
|
||||
from ...debug import printExc
|
||||
from ..NodeLibrary import NodeLibrary, isNodeClass
|
||||
from ... import reload as reload
|
||||
|
||||
|
||||
# Build default library
|
||||
LIBRARY = NodeLibrary()
|
||||
|
||||
# For backward compatibility, expose the default library's properties here:
|
||||
NODE_LIST = LIBRARY.nodeList
|
||||
NODE_TREE = LIBRARY.nodeTree
|
||||
registerNodeType = LIBRARY.addNodeType
|
||||
getNodeTree = LIBRARY.getNodeTree
|
||||
getNodeType = LIBRARY.getNodeType
|
||||
|
||||
# Add all nodes to the default library
|
||||
from . import Data, Display, Filters, Operators
|
||||
for mod in [Data, Display, Filters, Operators]:
|
||||
nodes = [getattr(mod, name) for name in dir(mod) if isNodeClass(getattr(mod, name))]
|
||||
for node in nodes:
|
||||
LIBRARY.addNodeType(node, [(mod.__name__.split('.')[-1],)])
|
||||
|
||||
|
||||
|
||||
|
184
pyqtgraph/flowchart/library/common.py
Normal file
184
pyqtgraph/flowchart/library/common.py
Normal file
|
@ -0,0 +1,184 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ...Qt import QtCore, QtGui
|
||||
from ...widgets.SpinBox import SpinBox
|
||||
#from ...SignalProxy import SignalProxy
|
||||
from ...WidgetGroup import WidgetGroup
|
||||
#from ColorMapper import ColorMapper
|
||||
from ..Node import Node
|
||||
import numpy as np
|
||||
from ...widgets.ColorButton import ColorButton
|
||||
try:
|
||||
import metaarray
|
||||
HAVE_METAARRAY = True
|
||||
except:
|
||||
HAVE_METAARRAY = False
|
||||
|
||||
|
||||
def generateUi(opts):
|
||||
"""Convenience function for generating common UI types"""
|
||||
widget = QtGui.QWidget()
|
||||
l = QtGui.QFormLayout()
|
||||
l.setSpacing(0)
|
||||
widget.setLayout(l)
|
||||
ctrls = {}
|
||||
row = 0
|
||||
for opt in opts:
|
||||
if len(opt) == 2:
|
||||
k, t = opt
|
||||
o = {}
|
||||
elif len(opt) == 3:
|
||||
k, t, o = opt
|
||||
else:
|
||||
raise Exception("Widget specification must be (name, type) or (name, type, {opts})")
|
||||
if t == 'intSpin':
|
||||
w = QtGui.QSpinBox()
|
||||
if 'max' in o:
|
||||
w.setMaximum(o['max'])
|
||||
if 'min' in o:
|
||||
w.setMinimum(o['min'])
|
||||
if 'value' in o:
|
||||
w.setValue(o['value'])
|
||||
elif t == 'doubleSpin':
|
||||
w = QtGui.QDoubleSpinBox()
|
||||
if 'max' in o:
|
||||
w.setMaximum(o['max'])
|
||||
if 'min' in o:
|
||||
w.setMinimum(o['min'])
|
||||
if 'value' in o:
|
||||
w.setValue(o['value'])
|
||||
elif t == 'spin':
|
||||
w = SpinBox()
|
||||
w.setOpts(**o)
|
||||
elif t == 'check':
|
||||
w = QtGui.QCheckBox()
|
||||
if 'checked' in o:
|
||||
w.setChecked(o['checked'])
|
||||
elif t == 'combo':
|
||||
w = QtGui.QComboBox()
|
||||
for i in o['values']:
|
||||
w.addItem(i)
|
||||
#elif t == 'colormap':
|
||||
#w = ColorMapper()
|
||||
elif t == 'color':
|
||||
w = ColorButton()
|
||||
else:
|
||||
raise Exception("Unknown widget type '%s'" % str(t))
|
||||
if 'tip' in o:
|
||||
w.setToolTip(o['tip'])
|
||||
w.setObjectName(k)
|
||||
l.addRow(k, w)
|
||||
if o.get('hidden', False):
|
||||
w.hide()
|
||||
label = l.labelForField(w)
|
||||
label.hide()
|
||||
|
||||
ctrls[k] = w
|
||||
w.rowNum = row
|
||||
row += 1
|
||||
group = WidgetGroup(widget)
|
||||
return widget, group, ctrls
|
||||
|
||||
|
||||
class CtrlNode(Node):
|
||||
"""Abstract class for nodes with auto-generated control UI"""
|
||||
|
||||
sigStateChanged = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, name, ui=None, terminals=None):
|
||||
if ui is None:
|
||||
if hasattr(self, 'uiTemplate'):
|
||||
ui = self.uiTemplate
|
||||
else:
|
||||
ui = []
|
||||
if terminals is None:
|
||||
terminals = {'In': {'io': 'in'}, 'Out': {'io': 'out', 'bypass': 'In'}}
|
||||
Node.__init__(self, name=name, terminals=terminals)
|
||||
|
||||
self.ui, self.stateGroup, self.ctrls = generateUi(ui)
|
||||
self.stateGroup.sigChanged.connect(self.changed)
|
||||
|
||||
def ctrlWidget(self):
|
||||
return self.ui
|
||||
|
||||
def changed(self):
|
||||
self.update()
|
||||
self.sigStateChanged.emit(self)
|
||||
|
||||
def process(self, In, display=True):
|
||||
out = self.processData(In)
|
||||
return {'Out': out}
|
||||
|
||||
def saveState(self):
|
||||
state = Node.saveState(self)
|
||||
state['ctrl'] = self.stateGroup.state()
|
||||
return state
|
||||
|
||||
def restoreState(self, state):
|
||||
Node.restoreState(self, state)
|
||||
if self.stateGroup is not None:
|
||||
self.stateGroup.setState(state.get('ctrl', {}))
|
||||
|
||||
def hideRow(self, name):
|
||||
w = self.ctrls[name]
|
||||
l = self.ui.layout().labelForField(w)
|
||||
w.hide()
|
||||
l.hide()
|
||||
|
||||
def showRow(self, name):
|
||||
w = self.ctrls[name]
|
||||
l = self.ui.layout().labelForField(w)
|
||||
w.show()
|
||||
l.show()
|
||||
|
||||
|
||||
class PlottingCtrlNode(CtrlNode):
|
||||
"""Abstract class for CtrlNodes that can connect to plots."""
|
||||
|
||||
def __init__(self, name, ui=None, terminals=None):
|
||||
#print "PlottingCtrlNode.__init__ called."
|
||||
CtrlNode.__init__(self, name, ui=ui, terminals=terminals)
|
||||
self.plotTerminal = self.addOutput('plot', optional=True)
|
||||
|
||||
def connected(self, term, remote):
|
||||
CtrlNode.connected(self, term, remote)
|
||||
if term is not self.plotTerminal:
|
||||
return
|
||||
node = remote.node()
|
||||
node.sigPlotChanged.connect(self.connectToPlot)
|
||||
self.connectToPlot(node)
|
||||
|
||||
def disconnected(self, term, remote):
|
||||
CtrlNode.disconnected(self, term, remote)
|
||||
if term is not self.plotTerminal:
|
||||
return
|
||||
remote.node().sigPlotChanged.disconnect(self.connectToPlot)
|
||||
self.disconnectFromPlot(remote.node().getPlot())
|
||||
|
||||
def connectToPlot(self, node):
|
||||
"""Define what happens when the node is connected to a plot"""
|
||||
raise Exception("Must be re-implemented in subclass")
|
||||
|
||||
def disconnectFromPlot(self, plot):
|
||||
"""Define what happens when the node is disconnected from a plot"""
|
||||
raise Exception("Must be re-implemented in subclass")
|
||||
|
||||
def process(self, In, display=True):
|
||||
out = CtrlNode.process(self, In, display)
|
||||
out['plot'] = None
|
||||
return out
|
||||
|
||||
|
||||
def metaArrayWrapper(fn):
|
||||
def newFn(self, data, *args, **kargs):
|
||||
if HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
d1 = fn(self, data.view(np.ndarray), *args, **kargs)
|
||||
info = data.infoCopy()
|
||||
if d1.shape != data.shape:
|
||||
for i in range(data.ndim):
|
||||
if 'values' in info[i]:
|
||||
info[i]['values'] = info[i]['values'][:d1.shape[i]]
|
||||
return metaarray.MetaArray(d1, info=info)
|
||||
else:
|
||||
return fn(self, data, *args, **kargs)
|
||||
return newFn
|
||||
|
355
pyqtgraph/flowchart/library/functions.py
Normal file
355
pyqtgraph/flowchart/library/functions.py
Normal file
|
@ -0,0 +1,355 @@
|
|||
import numpy as np
|
||||
from ...metaarray import MetaArray
|
||||
|
||||
def downsample(data, n, axis=0, xvals='subsample'):
|
||||
"""Downsample by averaging points together across axis.
|
||||
If multiple axes are specified, runs once per axis.
|
||||
If a metaArray is given, then the axis values can be either subsampled
|
||||
or downsampled to match.
|
||||
"""
|
||||
ma = None
|
||||
if (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
ma = data
|
||||
data = data.view(np.ndarray)
|
||||
|
||||
|
||||
if hasattr(axis, '__len__'):
|
||||
if not hasattr(n, '__len__'):
|
||||
n = [n]*len(axis)
|
||||
for i in range(len(axis)):
|
||||
data = downsample(data, n[i], axis[i])
|
||||
return data
|
||||
|
||||
nPts = int(data.shape[axis] / n)
|
||||
s = list(data.shape)
|
||||
s[axis] = nPts
|
||||
s.insert(axis+1, n)
|
||||
sl = [slice(None)] * data.ndim
|
||||
sl[axis] = slice(0, nPts*n)
|
||||
d1 = data[tuple(sl)]
|
||||
#print d1.shape, s
|
||||
d1.shape = tuple(s)
|
||||
d2 = d1.mean(axis+1)
|
||||
|
||||
if ma is None:
|
||||
return d2
|
||||
else:
|
||||
info = ma.infoCopy()
|
||||
if 'values' in info[axis]:
|
||||
if xvals == 'subsample':
|
||||
info[axis]['values'] = info[axis]['values'][::n][:nPts]
|
||||
elif xvals == 'downsample':
|
||||
info[axis]['values'] = downsample(info[axis]['values'], n)
|
||||
return MetaArray(d2, info=info)
|
||||
|
||||
|
||||
def applyFilter(data, b, a, padding=100, bidir=True):
|
||||
"""Apply a linear filter with coefficients a, b. Optionally pad the data before filtering
|
||||
and/or run the filter in both directions."""
|
||||
try:
|
||||
import scipy.signal
|
||||
except ImportError:
|
||||
raise Exception("applyFilter() requires the package scipy.signal.")
|
||||
|
||||
d1 = data.view(np.ndarray)
|
||||
|
||||
if padding > 0:
|
||||
d1 = np.hstack([d1[:padding], d1, d1[-padding:]])
|
||||
|
||||
if bidir:
|
||||
d1 = scipy.signal.lfilter(b, a, scipy.signal.lfilter(b, a, d1)[::-1])[::-1]
|
||||
else:
|
||||
d1 = scipy.signal.lfilter(b, a, d1)
|
||||
|
||||
if padding > 0:
|
||||
d1 = d1[padding:-padding]
|
||||
|
||||
if (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
return MetaArray(d1, info=data.infoCopy())
|
||||
else:
|
||||
return d1
|
||||
|
||||
def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True):
|
||||
"""return data passed through bessel filter"""
|
||||
try:
|
||||
import scipy.signal
|
||||
except ImportError:
|
||||
raise Exception("besselFilter() requires the package scipy.signal.")
|
||||
|
||||
if dt is None:
|
||||
try:
|
||||
tvals = data.xvals('Time')
|
||||
dt = (tvals[-1]-tvals[0]) / (len(tvals)-1)
|
||||
except:
|
||||
dt = 1.0
|
||||
|
||||
b,a = scipy.signal.bessel(order, cutoff * dt, btype=btype)
|
||||
|
||||
return applyFilter(data, b, a, bidir=bidir)
|
||||
#base = data.mean()
|
||||
#d1 = scipy.signal.lfilter(b, a, data.view(ndarray)-base) + base
|
||||
#if (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
#return MetaArray(d1, info=data.infoCopy())
|
||||
#return d1
|
||||
|
||||
def butterworthFilter(data, wPass, wStop=None, gPass=2.0, gStop=20.0, order=1, dt=None, btype='low', bidir=True):
|
||||
"""return data passed through bessel filter"""
|
||||
try:
|
||||
import scipy.signal
|
||||
except ImportError:
|
||||
raise Exception("butterworthFilter() requires the package scipy.signal.")
|
||||
|
||||
if dt is None:
|
||||
try:
|
||||
tvals = data.xvals('Time')
|
||||
dt = (tvals[-1]-tvals[0]) / (len(tvals)-1)
|
||||
except:
|
||||
dt = 1.0
|
||||
|
||||
if wStop is None:
|
||||
wStop = wPass * 2.0
|
||||
ord, Wn = scipy.signal.buttord(wPass*dt*2., wStop*dt*2., gPass, gStop)
|
||||
#print "butterworth ord %f Wn %f c %f sc %f" % (ord, Wn, cutoff, stopCutoff)
|
||||
b,a = scipy.signal.butter(ord, Wn, btype=btype)
|
||||
|
||||
return applyFilter(data, b, a, bidir=bidir)
|
||||
|
||||
|
||||
def rollingSum(data, n):
|
||||
d1 = data.copy()
|
||||
d1[1:] += d1[:-1] # integrate
|
||||
d2 = np.empty(len(d1) - n + 1, dtype=data.dtype)
|
||||
d2[0] = d1[n-1] # copy first point
|
||||
d2[1:] = d1[n:] - d1[:-n] # subtract
|
||||
return d2
|
||||
|
||||
|
||||
def mode(data, bins=None):
|
||||
"""Returns location max value from histogram."""
|
||||
if bins is None:
|
||||
bins = int(len(data)/10.)
|
||||
if bins < 2:
|
||||
bins = 2
|
||||
y, x = np.histogram(data, bins=bins)
|
||||
ind = np.argmax(y)
|
||||
mode = 0.5 * (x[ind] + x[ind+1])
|
||||
return mode
|
||||
|
||||
def modeFilter(data, window=500, step=None, bins=None):
|
||||
"""Filter based on histogram-based mode function"""
|
||||
d1 = data.view(np.ndarray)
|
||||
vals = []
|
||||
l2 = int(window/2.)
|
||||
if step is None:
|
||||
step = l2
|
||||
i = 0
|
||||
while True:
|
||||
if i > len(data)-step:
|
||||
break
|
||||
vals.append(mode(d1[i:i+window], bins))
|
||||
i += step
|
||||
|
||||
chunks = [np.linspace(vals[0], vals[0], l2)]
|
||||
for i in range(len(vals)-1):
|
||||
chunks.append(np.linspace(vals[i], vals[i+1], step))
|
||||
remain = len(data) - step*(len(vals)-1) - l2
|
||||
chunks.append(np.linspace(vals[-1], vals[-1], remain))
|
||||
d2 = np.hstack(chunks)
|
||||
|
||||
if (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
return MetaArray(d2, info=data.infoCopy())
|
||||
return d2
|
||||
|
||||
def denoise(data, radius=2, threshold=4):
|
||||
"""Very simple noise removal function. Compares a point to surrounding points,
|
||||
replaces with nearby values if the difference is too large."""
|
||||
|
||||
|
||||
r2 = radius * 2
|
||||
d1 = data.view(np.ndarray)
|
||||
d2 = d1[radius:] - d1[:-radius] #a derivative
|
||||
#d3 = data[r2:] - data[:-r2]
|
||||
#d4 = d2 - d3
|
||||
stdev = d2.std()
|
||||
#print "denoise: stdev of derivative:", stdev
|
||||
mask1 = d2 > stdev*threshold #where derivative is large and positive
|
||||
mask2 = d2 < -stdev*threshold #where derivative is large and negative
|
||||
maskpos = mask1[:-radius] * mask2[radius:] #both need to be true
|
||||
maskneg = mask1[radius:] * mask2[:-radius]
|
||||
mask = maskpos + maskneg
|
||||
d5 = np.where(mask, d1[:-r2], d1[radius:-radius]) #where both are true replace the value with the value from 2 points before
|
||||
d6 = np.empty(d1.shape, dtype=d1.dtype) #add points back to the ends
|
||||
d6[radius:-radius] = d5
|
||||
d6[:radius] = d1[:radius]
|
||||
d6[-radius:] = d1[-radius:]
|
||||
|
||||
if (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
return MetaArray(d6, info=data.infoCopy())
|
||||
return d6
|
||||
|
||||
def adaptiveDetrend(data, x=None, threshold=3.0):
|
||||
"""Return the signal with baseline removed. Discards outliers from baseline measurement."""
|
||||
try:
|
||||
import scipy.signal
|
||||
except ImportError:
|
||||
raise Exception("adaptiveDetrend() requires the package scipy.signal.")
|
||||
|
||||
if x is None:
|
||||
x = data.xvals(0)
|
||||
|
||||
d = data.view(np.ndarray)
|
||||
|
||||
d2 = scipy.signal.detrend(d)
|
||||
|
||||
stdev = d2.std()
|
||||
mask = abs(d2) < stdev*threshold
|
||||
#d3 = where(mask, 0, d2)
|
||||
#d4 = d2 - lowPass(d3, cutoffs[1], dt=dt)
|
||||
|
||||
lr = scipy.stats.linregress(x[mask], d[mask])
|
||||
base = lr[1] + lr[0]*x
|
||||
d4 = d - base
|
||||
|
||||
if (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
return MetaArray(d4, info=data.infoCopy())
|
||||
return d4
|
||||
|
||||
|
||||
def histogramDetrend(data, window=500, bins=50, threshold=3.0, offsetOnly=False):
|
||||
"""Linear detrend. Works by finding the most common value at the beginning and end of a trace, excluding outliers.
|
||||
If offsetOnly is True, then only the offset from the beginning of the trace is subtracted.
|
||||
"""
|
||||
|
||||
d1 = data.view(np.ndarray)
|
||||
d2 = [d1[:window], d1[-window:]]
|
||||
v = [0, 0]
|
||||
for i in [0, 1]:
|
||||
d3 = d2[i]
|
||||
stdev = d3.std()
|
||||
mask = abs(d3-np.median(d3)) < stdev*threshold
|
||||
d4 = d3[mask]
|
||||
y, x = np.histogram(d4, bins=bins)
|
||||
ind = np.argmax(y)
|
||||
v[i] = 0.5 * (x[ind] + x[ind+1])
|
||||
|
||||
if offsetOnly:
|
||||
d3 = data.view(np.ndarray) - v[0]
|
||||
else:
|
||||
base = np.linspace(v[0], v[1], len(data))
|
||||
d3 = data.view(np.ndarray) - base
|
||||
|
||||
if (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
return MetaArray(d3, info=data.infoCopy())
|
||||
return d3
|
||||
|
||||
def concatenateColumns(data):
|
||||
"""Returns a single record array with columns taken from the elements in data.
|
||||
data should be a list of elements, which can be either record arrays or tuples (name, type, data)
|
||||
"""
|
||||
|
||||
## first determine dtype
|
||||
dtype = []
|
||||
names = set()
|
||||
maxLen = 0
|
||||
for element in data:
|
||||
if isinstance(element, np.ndarray):
|
||||
## use existing columns
|
||||
for i in range(len(element.dtype)):
|
||||
name = element.dtype.names[i]
|
||||
dtype.append((name, element.dtype[i]))
|
||||
maxLen = max(maxLen, len(element))
|
||||
else:
|
||||
name, type, d = element
|
||||
if type is None:
|
||||
type = suggestDType(d)
|
||||
dtype.append((name, type))
|
||||
if isinstance(d, list) or isinstance(d, np.ndarray):
|
||||
maxLen = max(maxLen, len(d))
|
||||
if name in names:
|
||||
raise Exception('Name "%s" repeated' % name)
|
||||
names.add(name)
|
||||
|
||||
|
||||
|
||||
## create empty array
|
||||
out = np.empty(maxLen, dtype)
|
||||
|
||||
## fill columns
|
||||
for element in data:
|
||||
if isinstance(element, np.ndarray):
|
||||
for i in range(len(element.dtype)):
|
||||
name = element.dtype.names[i]
|
||||
try:
|
||||
out[name] = element[name]
|
||||
except:
|
||||
print("Column:", name)
|
||||
print("Input shape:", element.shape, element.dtype)
|
||||
print("Output shape:", out.shape, out.dtype)
|
||||
raise
|
||||
else:
|
||||
name, type, d = element
|
||||
out[name] = d
|
||||
|
||||
return out
|
||||
|
||||
def suggestDType(x):
|
||||
"""Return a suitable dtype for x"""
|
||||
if isinstance(x, list) or isinstance(x, tuple):
|
||||
if len(x) == 0:
|
||||
raise Exception('can not determine dtype for empty list')
|
||||
x = x[0]
|
||||
|
||||
if hasattr(x, 'dtype'):
|
||||
return x.dtype
|
||||
elif isinstance(x, float):
|
||||
return float
|
||||
elif isinstance(x, int):
|
||||
return int
|
||||
#elif isinstance(x, basestring): ## don't try to guess correct string length; use object instead.
|
||||
#return '<U%d' % len(x)
|
||||
else:
|
||||
return object
|
||||
|
||||
def removePeriodic(data, f0=60.0, dt=None, harmonics=10, samples=4):
|
||||
if (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
data1 = data.asarray()
|
||||
if dt is None:
|
||||
times = data.xvals('Time')
|
||||
dt = times[1]-times[0]
|
||||
else:
|
||||
data1 = data
|
||||
if dt is None:
|
||||
raise Exception('Must specify dt for this data')
|
||||
|
||||
ft = np.fft.fft(data1)
|
||||
|
||||
## determine frequencies in fft data
|
||||
df = 1.0 / (len(data1) * dt)
|
||||
freqs = np.linspace(0.0, (len(ft)-1) * df, len(ft))
|
||||
|
||||
## flatten spikes at f0 and harmonics
|
||||
for i in xrange(1, harmonics + 2):
|
||||
f = f0 * i # target frequency
|
||||
|
||||
## determine index range to check for this frequency
|
||||
ind1 = int(np.floor(f / df))
|
||||
ind2 = int(np.ceil(f / df)) + (samples-1)
|
||||
if ind1 > len(ft)/2.:
|
||||
break
|
||||
mag = (abs(ft[ind1-1]) + abs(ft[ind2+1])) * 0.5
|
||||
for j in range(ind1, ind2+1):
|
||||
phase = np.angle(ft[j]) ## Must preserve the phase of each point, otherwise any transients in the trace might lead to large artifacts.
|
||||
re = mag * np.cos(phase)
|
||||
im = mag * np.sin(phase)
|
||||
ft[j] = re + im*1j
|
||||
ft[len(ft)-j] = re - im*1j
|
||||
|
||||
data2 = np.fft.ifft(ft).real
|
||||
|
||||
if (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
return metaarray.MetaArray(data2, info=data.infoCopy())
|
||||
else:
|
||||
return data2
|
||||
|
||||
|
||||
|
52
pyqtgraph/frozenSupport.py
Normal file
52
pyqtgraph/frozenSupport.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
## Definitions helpful in frozen environments (eg py2exe)
|
||||
import os, sys, zipfile
|
||||
|
||||
def listdir(path):
|
||||
"""Replacement for os.listdir that works in frozen environments."""
|
||||
if not hasattr(sys, 'frozen'):
|
||||
return os.listdir(path)
|
||||
|
||||
(zipPath, archivePath) = splitZip(path)
|
||||
if archivePath is None:
|
||||
return os.listdir(path)
|
||||
|
||||
with zipfile.ZipFile(zipPath, "r") as zipobj:
|
||||
contents = zipobj.namelist()
|
||||
results = set()
|
||||
for name in contents:
|
||||
# components in zip archive paths are always separated by forward slash
|
||||
if name.startswith(archivePath) and len(name) > len(archivePath):
|
||||
name = name[len(archivePath):].split('/')[0]
|
||||
results.add(name)
|
||||
return list(results)
|
||||
|
||||
def isdir(path):
|
||||
"""Replacement for os.path.isdir that works in frozen environments."""
|
||||
if not hasattr(sys, 'frozen'):
|
||||
return os.path.isdir(path)
|
||||
|
||||
(zipPath, archivePath) = splitZip(path)
|
||||
if archivePath is None:
|
||||
return os.path.isdir(path)
|
||||
with zipfile.ZipFile(zipPath, "r") as zipobj:
|
||||
contents = zipobj.namelist()
|
||||
archivePath = archivePath.rstrip('/') + '/' ## make sure there's exactly one '/' at the end
|
||||
for c in contents:
|
||||
if c.startswith(archivePath):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def splitZip(path):
|
||||
"""Splits a path containing a zip file into (zipfile, subpath).
|
||||
If there is no zip file, returns (path, None)"""
|
||||
components = os.path.normpath(path).split(os.sep)
|
||||
for index, component in enumerate(components):
|
||||
if component.endswith('.zip'):
|
||||
zipPath = os.sep.join(components[0:index+1])
|
||||
archivePath = ''.join([x+'/' for x in components[index+1:]])
|
||||
return (zipPath, archivePath)
|
||||
else:
|
||||
return (path, None)
|
||||
|
||||
|
2253
pyqtgraph/functions.py
Normal file
2253
pyqtgraph/functions.py
Normal file
File diff suppressed because it is too large
Load diff
126
pyqtgraph/graphicsItems/ArrowItem.py
Normal file
126
pyqtgraph/graphicsItems/ArrowItem.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from .. import functions as fn
|
||||
import numpy as np
|
||||
__all__ = ['ArrowItem']
|
||||
|
||||
class ArrowItem(QtGui.QGraphicsPathItem):
|
||||
"""
|
||||
For displaying scale-invariant arrows.
|
||||
For arrows pointing to a location on a curve, see CurveArrow
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, **opts):
|
||||
"""
|
||||
Arrows can be initialized with any keyword arguments accepted by
|
||||
the setStyle() method.
|
||||
"""
|
||||
self.opts = {}
|
||||
QtGui.QGraphicsPathItem.__init__(self, opts.get('parent', None))
|
||||
|
||||
if 'size' in opts:
|
||||
opts['headLen'] = opts['size']
|
||||
if 'width' in opts:
|
||||
opts['headWidth'] = opts['width']
|
||||
defaultOpts = {
|
||||
'pxMode': True,
|
||||
'angle': -150, ## If the angle is 0, the arrow points left
|
||||
'pos': (0,0),
|
||||
'headLen': 20,
|
||||
'tipAngle': 25,
|
||||
'baseAngle': 0,
|
||||
'tailLen': None,
|
||||
'tailWidth': 3,
|
||||
'pen': (200,200,200),
|
||||
'brush': (50,50,200),
|
||||
}
|
||||
defaultOpts.update(opts)
|
||||
|
||||
self.setStyle(**defaultOpts)
|
||||
|
||||
self.rotate(self.opts['angle'])
|
||||
self.moveBy(*self.opts['pos'])
|
||||
|
||||
def setStyle(self, **opts):
|
||||
"""
|
||||
Changes the appearance of the arrow.
|
||||
All arguments are optional:
|
||||
|
||||
====================== =================================================
|
||||
**Keyword Arguments:**
|
||||
angle Orientation of the arrow in degrees. Default is
|
||||
0; arrow pointing to the left.
|
||||
headLen Length of the arrow head, from tip to base.
|
||||
default=20
|
||||
headWidth Width of the arrow head at its base.
|
||||
tipAngle Angle of the tip of the arrow in degrees. Smaller
|
||||
values make a 'sharper' arrow. If tipAngle is
|
||||
specified, ot overrides headWidth. default=25
|
||||
baseAngle Angle of the base of the arrow head. Default is
|
||||
0, which means that the base of the arrow head
|
||||
is perpendicular to the arrow tail.
|
||||
tailLen Length of the arrow tail, measured from the base
|
||||
of the arrow head to the end of the tail. If
|
||||
this value is None, no tail will be drawn.
|
||||
default=None
|
||||
tailWidth Width of the tail. default=3
|
||||
pen The pen used to draw the outline of the arrow.
|
||||
brush The brush used to fill the arrow.
|
||||
====================== =================================================
|
||||
"""
|
||||
self.opts.update(opts)
|
||||
|
||||
opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']])
|
||||
self.path = fn.makeArrowPath(**opt)
|
||||
self.setPath(self.path)
|
||||
|
||||
self.setPen(fn.mkPen(self.opts['pen']))
|
||||
self.setBrush(fn.mkBrush(self.opts['brush']))
|
||||
|
||||
if self.opts['pxMode']:
|
||||
self.setFlags(self.flags() | self.ItemIgnoresTransformations)
|
||||
else:
|
||||
self.setFlags(self.flags() & ~self.ItemIgnoresTransformations)
|
||||
|
||||
def paint(self, p, *args):
|
||||
p.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
QtGui.QGraphicsPathItem.paint(self, p, *args)
|
||||
|
||||
#p.setPen(fn.mkPen('r'))
|
||||
#p.setBrush(fn.mkBrush(None))
|
||||
#p.drawRect(self.boundingRect())
|
||||
|
||||
def shape(self):
|
||||
#if not self.opts['pxMode']:
|
||||
#return QtGui.QGraphicsPathItem.shape(self)
|
||||
return self.path
|
||||
|
||||
## dataBounds and pixelPadding methods are provided to ensure ViewBox can
|
||||
## properly auto-range
|
||||
def dataBounds(self, ax, frac, orthoRange=None):
|
||||
pw = 0
|
||||
pen = self.pen()
|
||||
if not pen.isCosmetic():
|
||||
pw = pen.width() * 0.7072
|
||||
if self.opts['pxMode']:
|
||||
return [0,0]
|
||||
else:
|
||||
br = self.boundingRect()
|
||||
if ax == 0:
|
||||
return [br.left()-pw, br.right()+pw]
|
||||
else:
|
||||
return [br.top()-pw, br.bottom()+pw]
|
||||
|
||||
def pixelPadding(self):
|
||||
pad = 0
|
||||
if self.opts['pxMode']:
|
||||
br = self.boundingRect()
|
||||
pad += (br.width()**2 + br.height()**2) ** 0.5
|
||||
pen = self.pen()
|
||||
if pen.isCosmetic():
|
||||
pad += max(1, pen.width()) * 0.7072
|
||||
return pad
|
||||
|
||||
|
||||
|
1076
pyqtgraph/graphicsItems/AxisItem.py
Normal file
1076
pyqtgraph/graphicsItems/AxisItem.py
Normal file
File diff suppressed because it is too large
Load diff
168
pyqtgraph/graphicsItems/BarGraphItem.py
Normal file
168
pyqtgraph/graphicsItems/BarGraphItem.py
Normal file
|
@ -0,0 +1,168 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from .GraphicsObject import GraphicsObject
|
||||
from .. import getConfigOption
|
||||
from .. import functions as fn
|
||||
import numpy as np
|
||||
|
||||
|
||||
__all__ = ['BarGraphItem']
|
||||
|
||||
class BarGraphItem(GraphicsObject):
|
||||
def __init__(self, **opts):
|
||||
"""
|
||||
Valid keyword options are:
|
||||
x, x0, x1, y, y0, y1, width, height, pen, brush
|
||||
|
||||
x specifies the x-position of the center of the bar.
|
||||
x0, x1 specify left and right edges of the bar, respectively.
|
||||
width specifies distance from x0 to x1.
|
||||
You may specify any combination:
|
||||
|
||||
x, width
|
||||
x0, width
|
||||
x1, width
|
||||
x0, x1
|
||||
|
||||
Likewise y, y0, y1, and height.
|
||||
If only height is specified, then y0 will be set to 0
|
||||
|
||||
Example uses:
|
||||
|
||||
BarGraphItem(x=range(5), height=[1,5,2,4,3], width=0.5)
|
||||
|
||||
|
||||
"""
|
||||
GraphicsObject.__init__(self)
|
||||
self.opts = dict(
|
||||
x=None,
|
||||
y=None,
|
||||
x0=None,
|
||||
y0=None,
|
||||
x1=None,
|
||||
y1=None,
|
||||
height=None,
|
||||
width=None,
|
||||
pen=None,
|
||||
brush=None,
|
||||
pens=None,
|
||||
brushes=None,
|
||||
)
|
||||
self._shape = None
|
||||
self.picture = None
|
||||
self.setOpts(**opts)
|
||||
|
||||
def setOpts(self, **opts):
|
||||
self.opts.update(opts)
|
||||
self.picture = None
|
||||
self._shape = None
|
||||
self.update()
|
||||
self.informViewBoundsChanged()
|
||||
|
||||
def drawPicture(self):
|
||||
self.picture = QtGui.QPicture()
|
||||
self._shape = QtGui.QPainterPath()
|
||||
p = QtGui.QPainter(self.picture)
|
||||
|
||||
pen = self.opts['pen']
|
||||
pens = self.opts['pens']
|
||||
|
||||
if pen is None and pens is None:
|
||||
pen = getConfigOption('foreground')
|
||||
|
||||
brush = self.opts['brush']
|
||||
brushes = self.opts['brushes']
|
||||
if brush is None and brushes is None:
|
||||
brush = (128, 128, 128)
|
||||
|
||||
def asarray(x):
|
||||
if x is None or np.isscalar(x) or isinstance(x, np.ndarray):
|
||||
return x
|
||||
return np.array(x)
|
||||
|
||||
|
||||
x = asarray(self.opts.get('x'))
|
||||
x0 = asarray(self.opts.get('x0'))
|
||||
x1 = asarray(self.opts.get('x1'))
|
||||
width = asarray(self.opts.get('width'))
|
||||
|
||||
if x0 is None:
|
||||
if width is None:
|
||||
raise Exception('must specify either x0 or width')
|
||||
if x1 is not None:
|
||||
x0 = x1 - width
|
||||
elif x is not None:
|
||||
x0 = x - width/2.
|
||||
else:
|
||||
raise Exception('must specify at least one of x, x0, or x1')
|
||||
if width is None:
|
||||
if x1 is None:
|
||||
raise Exception('must specify either x1 or width')
|
||||
width = x1 - x0
|
||||
|
||||
y = asarray(self.opts.get('y'))
|
||||
y0 = asarray(self.opts.get('y0'))
|
||||
y1 = asarray(self.opts.get('y1'))
|
||||
height = asarray(self.opts.get('height'))
|
||||
|
||||
if y0 is None:
|
||||
if height is None:
|
||||
y0 = 0
|
||||
elif y1 is not None:
|
||||
y0 = y1 - height
|
||||
elif y is not None:
|
||||
y0 = y - height/2.
|
||||
else:
|
||||
y0 = 0
|
||||
if height is None:
|
||||
if y1 is None:
|
||||
raise Exception('must specify either y1 or height')
|
||||
height = y1 - y0
|
||||
|
||||
p.setPen(fn.mkPen(pen))
|
||||
p.setBrush(fn.mkBrush(brush))
|
||||
for i in range(len(x0)):
|
||||
if pens is not None:
|
||||
p.setPen(fn.mkPen(pens[i]))
|
||||
if brushes is not None:
|
||||
p.setBrush(fn.mkBrush(brushes[i]))
|
||||
|
||||
if np.isscalar(x0):
|
||||
x = x0
|
||||
else:
|
||||
x = x0[i]
|
||||
if np.isscalar(y0):
|
||||
y = y0
|
||||
else:
|
||||
y = y0[i]
|
||||
if np.isscalar(width):
|
||||
w = width
|
||||
else:
|
||||
w = width[i]
|
||||
if np.isscalar(height):
|
||||
h = height
|
||||
else:
|
||||
h = height[i]
|
||||
|
||||
|
||||
rect = QtCore.QRectF(x, y, w, h)
|
||||
p.drawRect(rect)
|
||||
self._shape.addRect(rect)
|
||||
|
||||
p.end()
|
||||
self.prepareGeometryChange()
|
||||
|
||||
|
||||
def paint(self, p, *args):
|
||||
if self.picture is None:
|
||||
self.drawPicture()
|
||||
self.picture.play(p)
|
||||
|
||||
def boundingRect(self):
|
||||
if self.picture is None:
|
||||
self.drawPicture()
|
||||
return QtCore.QRectF(self.picture.boundingRect())
|
||||
|
||||
def shape(self):
|
||||
if self.picture is None:
|
||||
self.drawPicture()
|
||||
return self._shape
|
58
pyqtgraph/graphicsItems/ButtonItem.py
Normal file
58
pyqtgraph/graphicsItems/ButtonItem.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from .GraphicsObject import GraphicsObject
|
||||
|
||||
__all__ = ['ButtonItem']
|
||||
class ButtonItem(GraphicsObject):
|
||||
"""Button graphicsItem displaying an image."""
|
||||
|
||||
clicked = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, imageFile=None, width=None, parentItem=None, pixmap=None):
|
||||
self.enabled = True
|
||||
GraphicsObject.__init__(self)
|
||||
if imageFile is not None:
|
||||
self.setImageFile(imageFile)
|
||||
elif pixmap is not None:
|
||||
self.setPixmap(pixmap)
|
||||
|
||||
if width is not None:
|
||||
s = float(width) / self.pixmap.width()
|
||||
self.scale(s, s)
|
||||
if parentItem is not None:
|
||||
self.setParentItem(parentItem)
|
||||
self.setOpacity(0.7)
|
||||
|
||||
def setImageFile(self, imageFile):
|
||||
self.setPixmap(QtGui.QPixmap(imageFile))
|
||||
|
||||
def setPixmap(self, pixmap):
|
||||
self.pixmap = pixmap
|
||||
self.update()
|
||||
|
||||
def mouseClickEvent(self, ev):
|
||||
if self.enabled:
|
||||
self.clicked.emit(self)
|
||||
|
||||
def mouseHoverEvent(self, ev):
|
||||
if not self.enabled:
|
||||
return
|
||||
if ev.isEnter():
|
||||
self.setOpacity(1.0)
|
||||
else:
|
||||
self.setOpacity(0.7)
|
||||
|
||||
def disable(self):
|
||||
self.enabled = False
|
||||
self.setOpacity(0.4)
|
||||
|
||||
def enable(self):
|
||||
self.enabled = True
|
||||
self.setOpacity(0.7)
|
||||
|
||||
def paint(self, p, *args):
|
||||
p.setRenderHint(p.Antialiasing)
|
||||
p.drawPixmap(0, 0, self.pixmap)
|
||||
|
||||
def boundingRect(self):
|
||||
return QtCore.QRectF(self.pixmap.rect())
|
||||
|
117
pyqtgraph/graphicsItems/CurvePoint.py
Normal file
117
pyqtgraph/graphicsItems/CurvePoint.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from . import ArrowItem
|
||||
import numpy as np
|
||||
from ..Point import Point
|
||||
import weakref
|
||||
from .GraphicsObject import GraphicsObject
|
||||
|
||||
__all__ = ['CurvePoint', 'CurveArrow']
|
||||
class CurvePoint(GraphicsObject):
|
||||
"""A GraphicsItem that sets its location to a point on a PlotCurveItem.
|
||||
Also rotates to be tangent to the curve.
|
||||
The position along the curve is a Qt property, and thus can be easily animated.
|
||||
|
||||
Note: This class does not display anything; see CurveArrow for an applied example
|
||||
"""
|
||||
|
||||
def __init__(self, curve, index=0, pos=None, rotate=True):
|
||||
"""Position can be set either as an index referring to the sample number or
|
||||
the position 0.0 - 1.0
|
||||
If *rotate* is True, then the item rotates to match the tangent of the curve.
|
||||
"""
|
||||
|
||||
GraphicsObject.__init__(self)
|
||||
#QObjectWorkaround.__init__(self)
|
||||
self._rotate = rotate
|
||||
self.curve = weakref.ref(curve)
|
||||
self.setParentItem(curve)
|
||||
self.setProperty('position', 0.0)
|
||||
self.setProperty('index', 0)
|
||||
|
||||
if hasattr(self, 'ItemHasNoContents'):
|
||||
self.setFlags(self.flags() | self.ItemHasNoContents)
|
||||
|
||||
if pos is not None:
|
||||
self.setPos(pos)
|
||||
else:
|
||||
self.setIndex(index)
|
||||
|
||||
def setPos(self, pos):
|
||||
self.setProperty('position', float(pos))## cannot use numpy types here, MUST be python float.
|
||||
|
||||
def setIndex(self, index):
|
||||
self.setProperty('index', int(index)) ## cannot use numpy types here, MUST be python int.
|
||||
|
||||
def event(self, ev):
|
||||
if not isinstance(ev, QtCore.QDynamicPropertyChangeEvent) or self.curve() is None:
|
||||
return False
|
||||
|
||||
if ev.propertyName() == 'index':
|
||||
index = self.property('index')
|
||||
if 'QVariant' in repr(index):
|
||||
index = index.toInt()[0]
|
||||
elif ev.propertyName() == 'position':
|
||||
index = None
|
||||
else:
|
||||
return False
|
||||
|
||||
(x, y) = self.curve().getData()
|
||||
if index is None:
|
||||
#print ev.propertyName(), self.property('position').toDouble()[0], self.property('position').typeName()
|
||||
pos = self.property('position')
|
||||
if 'QVariant' in repr(pos): ## need to support 2 APIs :(
|
||||
pos = pos.toDouble()[0]
|
||||
index = (len(x)-1) * np.clip(pos, 0.0, 1.0)
|
||||
|
||||
if index != int(index): ## interpolate floating-point values
|
||||
i1 = int(index)
|
||||
i2 = np.clip(i1+1, 0, len(x)-1)
|
||||
s2 = index-i1
|
||||
s1 = 1.0-s2
|
||||
newPos = (x[i1]*s1+x[i2]*s2, y[i1]*s1+y[i2]*s2)
|
||||
else:
|
||||
index = int(index)
|
||||
i1 = np.clip(index-1, 0, len(x)-1)
|
||||
i2 = np.clip(index+1, 0, len(x)-1)
|
||||
newPos = (x[index], y[index])
|
||||
|
||||
p1 = self.parentItem().mapToScene(QtCore.QPointF(x[i1], y[i1]))
|
||||
p2 = self.parentItem().mapToScene(QtCore.QPointF(x[i2], y[i2]))
|
||||
ang = np.arctan2(p2.y()-p1.y(), p2.x()-p1.x()) ## returns radians
|
||||
self.resetTransform()
|
||||
if self._rotate:
|
||||
self.rotate(180+ ang * 180 / np.pi) ## takes degrees
|
||||
QtGui.QGraphicsItem.setPos(self, *newPos)
|
||||
return True
|
||||
|
||||
def boundingRect(self):
|
||||
return QtCore.QRectF()
|
||||
|
||||
def paint(self, *args):
|
||||
pass
|
||||
|
||||
def makeAnimation(self, prop='position', start=0.0, end=1.0, duration=10000, loop=1):
|
||||
anim = QtCore.QPropertyAnimation(self, prop)
|
||||
anim.setDuration(duration)
|
||||
anim.setStartValue(start)
|
||||
anim.setEndValue(end)
|
||||
anim.setLoopCount(loop)
|
||||
return anim
|
||||
|
||||
|
||||
class CurveArrow(CurvePoint):
|
||||
"""Provides an arrow that points to any specific sample on a PlotCurveItem.
|
||||
Provides properties that can be animated."""
|
||||
|
||||
def __init__(self, curve, index=0, pos=None, **opts):
|
||||
CurvePoint.__init__(self, curve, index=index, pos=pos)
|
||||
if opts.get('pxMode', True):
|
||||
opts['pxMode'] = False
|
||||
self.setFlags(self.flags() | self.ItemIgnoresTransformations)
|
||||
opts['angle'] = 0
|
||||
self.arrow = ArrowItem.ArrowItem(**opts)
|
||||
self.arrow.setParentItem(self)
|
||||
|
||||
def setStyle(self, **opts):
|
||||
return self.arrow.setStyle(**opts)
|
||||
|
149
pyqtgraph/graphicsItems/ErrorBarItem.py
Normal file
149
pyqtgraph/graphicsItems/ErrorBarItem.py
Normal file
|
@ -0,0 +1,149 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from .GraphicsObject import GraphicsObject
|
||||
from .. import getConfigOption
|
||||
from .. import functions as fn
|
||||
|
||||
__all__ = ['ErrorBarItem']
|
||||
|
||||
class ErrorBarItem(GraphicsObject):
|
||||
def __init__(self, **opts):
|
||||
"""
|
||||
All keyword arguments are passed to setData().
|
||||
"""
|
||||
GraphicsObject.__init__(self)
|
||||
self.opts = dict(
|
||||
x=None,
|
||||
y=None,
|
||||
height=None,
|
||||
width=None,
|
||||
top=None,
|
||||
bottom=None,
|
||||
left=None,
|
||||
right=None,
|
||||
beam=None,
|
||||
pen=None
|
||||
)
|
||||
self.setData(**opts)
|
||||
|
||||
def setData(self, **opts):
|
||||
"""
|
||||
Update the data in the item. All arguments are optional.
|
||||
|
||||
Valid keyword options are:
|
||||
x, y, height, width, top, bottom, left, right, beam, pen
|
||||
|
||||
* x and y must be numpy arrays specifying the coordinates of data points.
|
||||
* height, width, top, bottom, left, right, and beam may be numpy arrays,
|
||||
single values, or None to disable. All values should be positive.
|
||||
* top, bottom, left, and right specify the lengths of bars extending
|
||||
in each direction.
|
||||
* If height is specified, it overrides top and bottom.
|
||||
* If width is specified, it overrides left and right.
|
||||
* beam specifies the width of the beam at the end of each bar.
|
||||
* pen may be any single argument accepted by pg.mkPen().
|
||||
|
||||
This method was added in version 0.9.9. For prior versions, use setOpts.
|
||||
"""
|
||||
self.opts.update(opts)
|
||||
self.path = None
|
||||
self.update()
|
||||
self.prepareGeometryChange()
|
||||
self.informViewBoundsChanged()
|
||||
|
||||
def setOpts(self, **opts):
|
||||
# for backward compatibility
|
||||
self.setData(**opts)
|
||||
|
||||
def drawPath(self):
|
||||
p = QtGui.QPainterPath()
|
||||
|
||||
x, y = self.opts['x'], self.opts['y']
|
||||
if x is None or y is None:
|
||||
return
|
||||
|
||||
beam = self.opts['beam']
|
||||
|
||||
|
||||
height, top, bottom = self.opts['height'], self.opts['top'], self.opts['bottom']
|
||||
if height is not None or top is not None or bottom is not None:
|
||||
## draw vertical error bars
|
||||
if height is not None:
|
||||
y1 = y - height/2.
|
||||
y2 = y + height/2.
|
||||
else:
|
||||
if bottom is None:
|
||||
y1 = y
|
||||
else:
|
||||
y1 = y - bottom
|
||||
if top is None:
|
||||
y2 = y
|
||||
else:
|
||||
y2 = y + top
|
||||
|
||||
for i in range(len(x)):
|
||||
p.moveTo(x[i], y1[i])
|
||||
p.lineTo(x[i], y2[i])
|
||||
|
||||
if beam is not None and beam > 0:
|
||||
x1 = x - beam/2.
|
||||
x2 = x + beam/2.
|
||||
if height is not None or top is not None:
|
||||
for i in range(len(x)):
|
||||
p.moveTo(x1[i], y2[i])
|
||||
p.lineTo(x2[i], y2[i])
|
||||
if height is not None or bottom is not None:
|
||||
for i in range(len(x)):
|
||||
p.moveTo(x1[i], y1[i])
|
||||
p.lineTo(x2[i], y1[i])
|
||||
|
||||
width, right, left = self.opts['width'], self.opts['right'], self.opts['left']
|
||||
if width is not None or right is not None or left is not None:
|
||||
## draw vertical error bars
|
||||
if width is not None:
|
||||
x1 = x - width/2.
|
||||
x2 = x + width/2.
|
||||
else:
|
||||
if left is None:
|
||||
x1 = x
|
||||
else:
|
||||
x1 = x - left
|
||||
if right is None:
|
||||
x2 = x
|
||||
else:
|
||||
x2 = x + right
|
||||
|
||||
for i in range(len(x)):
|
||||
p.moveTo(x1[i], y[i])
|
||||
p.lineTo(x2[i], y[i])
|
||||
|
||||
if beam is not None and beam > 0:
|
||||
y1 = y - beam/2.
|
||||
y2 = y + beam/2.
|
||||
if width is not None or right is not None:
|
||||
for i in range(len(x)):
|
||||
p.moveTo(x2[i], y1[i])
|
||||
p.lineTo(x2[i], y2[i])
|
||||
if width is not None or left is not None:
|
||||
for i in range(len(x)):
|
||||
p.moveTo(x1[i], y1[i])
|
||||
p.lineTo(x1[i], y2[i])
|
||||
|
||||
self.path = p
|
||||
self.prepareGeometryChange()
|
||||
|
||||
|
||||
def paint(self, p, *args):
|
||||
if self.path is None:
|
||||
self.drawPath()
|
||||
pen = self.opts['pen']
|
||||
if pen is None:
|
||||
pen = getConfigOption('foreground')
|
||||
p.setPen(fn.mkPen(pen))
|
||||
p.drawPath(self.path)
|
||||
|
||||
def boundingRect(self):
|
||||
if self.path is None:
|
||||
self.drawPath()
|
||||
return self.path.boundingRect()
|
||||
|
||||
|
80
pyqtgraph/graphicsItems/FillBetweenItem.py
Normal file
80
pyqtgraph/graphicsItems/FillBetweenItem.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
from ..Qt import QtGui, USE_PYQT5, USE_PYQT4, USE_PYSIDE
|
||||
from .. import functions as fn
|
||||
from .PlotDataItem import PlotDataItem
|
||||
from .PlotCurveItem import PlotCurveItem
|
||||
|
||||
class FillBetweenItem(QtGui.QGraphicsPathItem):
|
||||
"""
|
||||
GraphicsItem filling the space between two PlotDataItems.
|
||||
"""
|
||||
def __init__(self, curve1=None, curve2=None, brush=None, pen=None):
|
||||
QtGui.QGraphicsPathItem.__init__(self)
|
||||
self.curves = None
|
||||
if curve1 is not None and curve2 is not None:
|
||||
self.setCurves(curve1, curve2)
|
||||
elif curve1 is not None or curve2 is not None:
|
||||
raise Exception("Must specify two curves to fill between.")
|
||||
|
||||
if brush is not None:
|
||||
self.setBrush(brush)
|
||||
self.setPen(pen)
|
||||
self.updatePath()
|
||||
|
||||
def setBrush(self, *args, **kwds):
|
||||
QtGui.QGraphicsPathItem.setBrush(self, fn.mkBrush(*args, **kwds))
|
||||
|
||||
def setPen(self, *args, **kwds):
|
||||
QtGui.QGraphicsPathItem.setPen(self, fn.mkPen(*args, **kwds))
|
||||
|
||||
def setCurves(self, curve1, curve2):
|
||||
"""Set the curves to fill between.
|
||||
|
||||
Arguments must be instances of PlotDataItem or PlotCurveItem.
|
||||
|
||||
Added in version 0.9.9
|
||||
"""
|
||||
if self.curves is not None:
|
||||
for c in self.curves:
|
||||
try:
|
||||
c.sigPlotChanged.disconnect(self.curveChanged)
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
|
||||
curves = [curve1, curve2]
|
||||
for c in curves:
|
||||
if not isinstance(c, PlotDataItem) and not isinstance(c, PlotCurveItem):
|
||||
raise TypeError("Curves must be PlotDataItem or PlotCurveItem.")
|
||||
self.curves = curves
|
||||
curve1.sigPlotChanged.connect(self.curveChanged)
|
||||
curve2.sigPlotChanged.connect(self.curveChanged)
|
||||
self.setZValue(min(curve1.zValue(), curve2.zValue())-1)
|
||||
self.curveChanged()
|
||||
|
||||
def setBrush(self, *args, **kwds):
|
||||
"""Change the fill brush. Acceps the same arguments as pg.mkBrush()"""
|
||||
QtGui.QGraphicsPathItem.setBrush(self, fn.mkBrush(*args, **kwds))
|
||||
|
||||
def curveChanged(self):
|
||||
self.updatePath()
|
||||
|
||||
def updatePath(self):
|
||||
if self.curves is None:
|
||||
self.setPath(QtGui.QPainterPath())
|
||||
return
|
||||
paths = []
|
||||
for c in self.curves:
|
||||
if isinstance(c, PlotDataItem):
|
||||
paths.append(c.curve.getPath())
|
||||
elif isinstance(c, PlotCurveItem):
|
||||
paths.append(c.getPath())
|
||||
|
||||
path = QtGui.QPainterPath()
|
||||
transform = QtGui.QTransform()
|
||||
p1 = paths[0].toSubpathPolygons(transform)
|
||||
p2 = paths[1].toReversed().toSubpathPolygons(transform)
|
||||
if len(p1) == 0 or len(p2) == 0:
|
||||
self.setPath(QtGui.QPainterPath())
|
||||
return
|
||||
|
||||
path.addPolygon(p1[0] + p2[0])
|
||||
self.setPath(path)
|
935
pyqtgraph/graphicsItems/GradientEditorItem.py
Normal file
935
pyqtgraph/graphicsItems/GradientEditorItem.py
Normal file
|
@ -0,0 +1,935 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from ..python2_3 import sortList
|
||||
from .. import functions as fn
|
||||
from .GraphicsObject import GraphicsObject
|
||||
from .GraphicsWidget import GraphicsWidget
|
||||
from ..widgets.SpinBox import SpinBox
|
||||
import weakref
|
||||
from ..pgcollections import OrderedDict
|
||||
from ..colormap import ColorMap
|
||||
|
||||
import numpy as np
|
||||
|
||||
__all__ = ['TickSliderItem', 'GradientEditorItem']
|
||||
|
||||
|
||||
Gradients = OrderedDict([
|
||||
('thermal', {'ticks': [(0.3333, (185, 0, 0, 255)), (0.6666, (255, 220, 0, 255)), (1, (255, 255, 255, 255)), (0, (0, 0, 0, 255))], 'mode': 'rgb'}),
|
||||
('flame', {'ticks': [(0.2, (7, 0, 220, 255)), (0.5, (236, 0, 134, 255)), (0.8, (246, 246, 0, 255)), (1.0, (255, 255, 255, 255)), (0.0, (0, 0, 0, 255))], 'mode': 'rgb'}),
|
||||
('yellowy', {'ticks': [(0.0, (0, 0, 0, 255)), (0.2328863796753704, (32, 0, 129, 255)), (0.8362738179251941, (255, 255, 0, 255)), (0.5257586450247, (115, 15, 255, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'} ),
|
||||
('bipolar', {'ticks': [(0.0, (0, 255, 255, 255)), (1.0, (255, 255, 0, 255)), (0.5, (0, 0, 0, 255)), (0.25, (0, 0, 255, 255)), (0.75, (255, 0, 0, 255))], 'mode': 'rgb'}),
|
||||
('spectrum', {'ticks': [(1.0, (255, 0, 255, 255)), (0.0, (255, 0, 0, 255))], 'mode': 'hsv'}),
|
||||
('cyclic', {'ticks': [(0.0, (255, 0, 4, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'hsv'}),
|
||||
('greyclip', {'ticks': [(0.0, (0, 0, 0, 255)), (0.99, (255, 255, 255, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'rgb'}),
|
||||
('grey', {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'}),
|
||||
])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class TickSliderItem(GraphicsWidget):
|
||||
## public class
|
||||
"""**Bases:** :class:`GraphicsWidget <pyqtgraph.GraphicsWidget>`
|
||||
|
||||
A rectangular item with tick marks along its length that can (optionally) be moved by the user."""
|
||||
|
||||
def __init__(self, orientation='bottom', allowAdd=True, **kargs):
|
||||
"""
|
||||
============== =================================================================================
|
||||
**Arguments:**
|
||||
orientation Set the orientation of the gradient. Options are: 'left', 'right'
|
||||
'top', and 'bottom'.
|
||||
allowAdd Specifies whether ticks can be added to the item by the user.
|
||||
tickPen Default is white. Specifies the color of the outline of the ticks.
|
||||
Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>`
|
||||
============== =================================================================================
|
||||
"""
|
||||
## public
|
||||
GraphicsWidget.__init__(self)
|
||||
self.orientation = orientation
|
||||
self.length = 100
|
||||
self.tickSize = 15
|
||||
self.ticks = {}
|
||||
self.maxDim = 20
|
||||
self.allowAdd = allowAdd
|
||||
if 'tickPen' in kargs:
|
||||
self.tickPen = fn.mkPen(kargs['tickPen'])
|
||||
else:
|
||||
self.tickPen = fn.mkPen('w')
|
||||
|
||||
self.orientations = {
|
||||
'left': (90, 1, 1),
|
||||
'right': (90, 1, 1),
|
||||
'top': (0, 1, -1),
|
||||
'bottom': (0, 1, 1)
|
||||
}
|
||||
|
||||
self.setOrientation(orientation)
|
||||
#self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain)
|
||||
#self.setBackgroundRole(QtGui.QPalette.NoRole)
|
||||
#self.setMouseTracking(True)
|
||||
|
||||
#def boundingRect(self):
|
||||
#return self.mapRectFromParent(self.geometry()).normalized()
|
||||
|
||||
#def shape(self): ## No idea why this is necessary, but rotated items do not receive clicks otherwise.
|
||||
#p = QtGui.QPainterPath()
|
||||
#p.addRect(self.boundingRect())
|
||||
#return p
|
||||
|
||||
def paint(self, p, opt, widget):
|
||||
#p.setPen(fn.mkPen('g', width=3))
|
||||
#p.drawRect(self.boundingRect())
|
||||
return
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
ev.ignore()
|
||||
|
||||
def setMaxDim(self, mx=None):
|
||||
if mx is None:
|
||||
mx = self.maxDim
|
||||
else:
|
||||
self.maxDim = mx
|
||||
|
||||
if self.orientation in ['bottom', 'top']:
|
||||
self.setFixedHeight(mx)
|
||||
self.setMaximumWidth(16777215)
|
||||
else:
|
||||
self.setFixedWidth(mx)
|
||||
self.setMaximumHeight(16777215)
|
||||
|
||||
|
||||
def setOrientation(self, orientation):
|
||||
## public
|
||||
"""Set the orientation of the TickSliderItem.
|
||||
|
||||
============== ===================================================================
|
||||
**Arguments:**
|
||||
orientation Options are: 'left', 'right', 'top', 'bottom'
|
||||
The orientation option specifies which side of the slider the
|
||||
ticks are on, as well as whether the slider is vertical ('right'
|
||||
and 'left') or horizontal ('top' and 'bottom').
|
||||
============== ===================================================================
|
||||
"""
|
||||
self.orientation = orientation
|
||||
self.setMaxDim()
|
||||
self.resetTransform()
|
||||
ort = orientation
|
||||
if ort == 'top':
|
||||
transform = QtGui.QTransform.fromScale(1, -1)
|
||||
transform.translate(0, -self.height())
|
||||
self.setTransform(transform)
|
||||
elif ort == 'left':
|
||||
transform = QtGui.QTransform()
|
||||
transform.rotate(270)
|
||||
transform.scale(1, -1)
|
||||
transform.translate(-self.height(), -self.maxDim)
|
||||
self.setTransform(transform)
|
||||
elif ort == 'right':
|
||||
transform = QtGui.QTransform()
|
||||
transform.rotate(270)
|
||||
transform.translate(-self.height(), 0)
|
||||
self.setTransform(transform)
|
||||
elif ort != 'bottom':
|
||||
raise Exception("%s is not a valid orientation. Options are 'left', 'right', 'top', and 'bottom'" %str(ort))
|
||||
|
||||
self.translate(self.tickSize/2., 0)
|
||||
|
||||
def addTick(self, x, color=None, movable=True):
|
||||
## public
|
||||
"""
|
||||
Add a tick to the item.
|
||||
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
x Position where tick should be added.
|
||||
color Color of added tick. If color is not specified, the color will be
|
||||
white.
|
||||
movable Specifies whether the tick is movable with the mouse.
|
||||
============== ==================================================================
|
||||
"""
|
||||
|
||||
if color is None:
|
||||
color = QtGui.QColor(255,255,255)
|
||||
tick = Tick(self, [x*self.length, 0], color, movable, self.tickSize, pen=self.tickPen)
|
||||
self.ticks[tick] = x
|
||||
tick.setParentItem(self)
|
||||
return tick
|
||||
|
||||
def removeTick(self, tick):
|
||||
## public
|
||||
"""
|
||||
Removes the specified tick.
|
||||
"""
|
||||
del self.ticks[tick]
|
||||
tick.setParentItem(None)
|
||||
if self.scene() is not None:
|
||||
self.scene().removeItem(tick)
|
||||
|
||||
def tickMoved(self, tick, pos):
|
||||
#print "tick changed"
|
||||
## Correct position of tick if it has left bounds.
|
||||
newX = min(max(0, pos.x()), self.length)
|
||||
pos.setX(newX)
|
||||
tick.setPos(pos)
|
||||
self.ticks[tick] = float(newX) / self.length
|
||||
|
||||
def tickMoveFinished(self, tick):
|
||||
pass
|
||||
|
||||
def tickClicked(self, tick, ev):
|
||||
if ev.button() == QtCore.Qt.RightButton:
|
||||
self.removeTick(tick)
|
||||
|
||||
def widgetLength(self):
|
||||
if self.orientation in ['bottom', 'top']:
|
||||
return self.width()
|
||||
else:
|
||||
return self.height()
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
wlen = max(40, self.widgetLength())
|
||||
self.setLength(wlen-self.tickSize-2)
|
||||
self.setOrientation(self.orientation)
|
||||
#bounds = self.scene().itemsBoundingRect()
|
||||
#bounds.setLeft(min(-self.tickSize*0.5, bounds.left()))
|
||||
#bounds.setRight(max(self.length + self.tickSize, bounds.right()))
|
||||
#self.setSceneRect(bounds)
|
||||
#self.fitInView(bounds, QtCore.Qt.KeepAspectRatio)
|
||||
|
||||
def setLength(self, newLen):
|
||||
#private
|
||||
for t, x in list(self.ticks.items()):
|
||||
t.setPos(x * newLen + 1, t.pos().y())
|
||||
self.length = float(newLen)
|
||||
|
||||
#def mousePressEvent(self, ev):
|
||||
#QtGui.QGraphicsView.mousePressEvent(self, ev)
|
||||
#self.ignoreRelease = False
|
||||
#for i in self.items(ev.pos()):
|
||||
#if isinstance(i, Tick):
|
||||
#self.ignoreRelease = True
|
||||
#break
|
||||
##if len(self.items(ev.pos())) > 0: ## Let items handle their own clicks
|
||||
##self.ignoreRelease = True
|
||||
|
||||
#def mouseReleaseEvent(self, ev):
|
||||
#QtGui.QGraphicsView.mouseReleaseEvent(self, ev)
|
||||
#if self.ignoreRelease:
|
||||
#return
|
||||
|
||||
#pos = self.mapToScene(ev.pos())
|
||||
|
||||
#if ev.button() == QtCore.Qt.LeftButton and self.allowAdd:
|
||||
#if pos.x() < 0 or pos.x() > self.length:
|
||||
#return
|
||||
#if pos.y() < 0 or pos.y() > self.tickSize:
|
||||
#return
|
||||
#pos.setX(min(max(pos.x(), 0), self.length))
|
||||
#self.addTick(pos.x()/self.length)
|
||||
#elif ev.button() == QtCore.Qt.RightButton:
|
||||
#self.showMenu(ev)
|
||||
|
||||
def mouseClickEvent(self, ev):
|
||||
if ev.button() == QtCore.Qt.LeftButton and self.allowAdd:
|
||||
pos = ev.pos()
|
||||
if pos.x() < 0 or pos.x() > self.length:
|
||||
return
|
||||
if pos.y() < 0 or pos.y() > self.tickSize:
|
||||
return
|
||||
pos.setX(min(max(pos.x(), 0), self.length))
|
||||
self.addTick(pos.x()/self.length)
|
||||
elif ev.button() == QtCore.Qt.RightButton:
|
||||
self.showMenu(ev)
|
||||
|
||||
#if ev.button() == QtCore.Qt.RightButton:
|
||||
#if self.moving:
|
||||
#ev.accept()
|
||||
#self.setPos(self.startPosition)
|
||||
#self.moving = False
|
||||
#self.sigMoving.emit(self)
|
||||
#self.sigMoved.emit(self)
|
||||
#else:
|
||||
#pass
|
||||
#self.view().tickClicked(self, ev)
|
||||
###remove
|
||||
|
||||
def hoverEvent(self, ev):
|
||||
if (not ev.isExit()) and ev.acceptClicks(QtCore.Qt.LeftButton):
|
||||
ev.acceptClicks(QtCore.Qt.RightButton)
|
||||
## show ghost tick
|
||||
#self.currentPen = fn.mkPen(255, 0,0)
|
||||
#else:
|
||||
#self.currentPen = self.pen
|
||||
#self.update()
|
||||
|
||||
def showMenu(self, ev):
|
||||
pass
|
||||
|
||||
def setTickColor(self, tick, color):
|
||||
"""Set the color of the specified tick.
|
||||
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
tick Can be either an integer corresponding to the index of the tick
|
||||
or a Tick object. Ex: if you had a slider with 3 ticks and you
|
||||
wanted to change the middle tick, the index would be 1.
|
||||
color The color to make the tick. Can be any argument that is valid for
|
||||
:func:`mkBrush <pyqtgraph.mkBrush>`
|
||||
============== ==================================================================
|
||||
"""
|
||||
tick = self.getTick(tick)
|
||||
tick.color = color
|
||||
tick.update()
|
||||
#tick.setBrush(QtGui.QBrush(QtGui.QColor(tick.color)))
|
||||
|
||||
def setTickValue(self, tick, val):
|
||||
## public
|
||||
"""
|
||||
Set the position (along the slider) of the tick.
|
||||
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
tick Can be either an integer corresponding to the index of the tick
|
||||
or a Tick object. Ex: if you had a slider with 3 ticks and you
|
||||
wanted to change the middle tick, the index would be 1.
|
||||
val The desired position of the tick. If val is < 0, position will be
|
||||
set to 0. If val is > 1, position will be set to 1.
|
||||
============== ==================================================================
|
||||
"""
|
||||
tick = self.getTick(tick)
|
||||
val = min(max(0.0, val), 1.0)
|
||||
x = val * self.length
|
||||
pos = tick.pos()
|
||||
pos.setX(x)
|
||||
tick.setPos(pos)
|
||||
self.ticks[tick] = val
|
||||
self.updateGradient()
|
||||
|
||||
def tickValue(self, tick):
|
||||
## public
|
||||
"""Return the value (from 0.0 to 1.0) of the specified tick.
|
||||
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
tick Can be either an integer corresponding to the index of the tick
|
||||
or a Tick object. Ex: if you had a slider with 3 ticks and you
|
||||
wanted the value of the middle tick, the index would be 1.
|
||||
============== ==================================================================
|
||||
"""
|
||||
tick = self.getTick(tick)
|
||||
return self.ticks[tick]
|
||||
|
||||
def getTick(self, tick):
|
||||
## public
|
||||
"""Return the Tick object at the specified index.
|
||||
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
tick An integer corresponding to the index of the desired tick. If the
|
||||
argument is not an integer it will be returned unchanged.
|
||||
============== ==================================================================
|
||||
"""
|
||||
if type(tick) is int:
|
||||
tick = self.listTicks()[tick][0]
|
||||
return tick
|
||||
|
||||
#def mouseMoveEvent(self, ev):
|
||||
#QtGui.QGraphicsView.mouseMoveEvent(self, ev)
|
||||
|
||||
def listTicks(self):
|
||||
"""Return a sorted list of all the Tick objects on the slider."""
|
||||
## public
|
||||
ticks = list(self.ticks.items())
|
||||
sortList(ticks, lambda a,b: cmp(a[1], b[1])) ## see pyqtgraph.python2_3.sortList
|
||||
return ticks
|
||||
|
||||
|
||||
class GradientEditorItem(TickSliderItem):
|
||||
"""
|
||||
**Bases:** :class:`TickSliderItem <pyqtgraph.TickSliderItem>`
|
||||
|
||||
An item that can be used to define a color gradient. Implements common pre-defined gradients that are
|
||||
customizable by the user. :class: `GradientWidget <pyqtgraph.GradientWidget>` provides a widget
|
||||
with a GradientEditorItem that can be added to a GUI.
|
||||
|
||||
================================ ===========================================================
|
||||
**Signals:**
|
||||
sigGradientChanged(self) Signal is emitted anytime the gradient changes. The signal
|
||||
is emitted in real time while ticks are being dragged or
|
||||
colors are being changed.
|
||||
sigGradientChangeFinished(self) Signal is emitted when the gradient is finished changing.
|
||||
================================ ===========================================================
|
||||
|
||||
"""
|
||||
|
||||
sigGradientChanged = QtCore.Signal(object)
|
||||
sigGradientChangeFinished = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, *args, **kargs):
|
||||
"""
|
||||
Create a new GradientEditorItem.
|
||||
All arguments are passed to :func:`TickSliderItem.__init__ <pyqtgraph.TickSliderItem.__init__>`
|
||||
|
||||
=============== =================================================================================
|
||||
**Arguments:**
|
||||
orientation Set the orientation of the gradient. Options are: 'left', 'right'
|
||||
'top', and 'bottom'.
|
||||
allowAdd Default is True. Specifies whether ticks can be added to the item.
|
||||
tickPen Default is white. Specifies the color of the outline of the ticks.
|
||||
Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>`
|
||||
=============== =================================================================================
|
||||
"""
|
||||
self.currentTick = None
|
||||
self.currentTickColor = None
|
||||
self.rectSize = 15
|
||||
self.gradRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, self.rectSize, 100, self.rectSize))
|
||||
self.backgroundRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, -self.rectSize, 100, self.rectSize))
|
||||
self.backgroundRect.setBrush(QtGui.QBrush(QtCore.Qt.DiagCrossPattern))
|
||||
self.colorMode = 'rgb'
|
||||
|
||||
TickSliderItem.__init__(self, *args, **kargs)
|
||||
|
||||
self.colorDialog = QtGui.QColorDialog()
|
||||
self.colorDialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True)
|
||||
self.colorDialog.setOption(QtGui.QColorDialog.DontUseNativeDialog, True)
|
||||
|
||||
self.colorDialog.currentColorChanged.connect(self.currentColorChanged)
|
||||
self.colorDialog.rejected.connect(self.currentColorRejected)
|
||||
self.colorDialog.accepted.connect(self.currentColorAccepted)
|
||||
|
||||
self.backgroundRect.setParentItem(self)
|
||||
self.gradRect.setParentItem(self)
|
||||
|
||||
self.setMaxDim(self.rectSize + self.tickSize)
|
||||
|
||||
self.rgbAction = QtGui.QAction('RGB', self)
|
||||
self.rgbAction.setCheckable(True)
|
||||
self.rgbAction.triggered.connect(lambda: self.setColorMode('rgb'))
|
||||
self.hsvAction = QtGui.QAction('HSV', self)
|
||||
self.hsvAction.setCheckable(True)
|
||||
self.hsvAction.triggered.connect(lambda: self.setColorMode('hsv'))
|
||||
|
||||
self.menu = QtGui.QMenu()
|
||||
|
||||
## build context menu of gradients
|
||||
l = self.length
|
||||
self.length = 100
|
||||
global Gradients
|
||||
for g in Gradients:
|
||||
px = QtGui.QPixmap(100, 15)
|
||||
p = QtGui.QPainter(px)
|
||||
self.restoreState(Gradients[g])
|
||||
grad = self.getGradient()
|
||||
brush = QtGui.QBrush(grad)
|
||||
p.fillRect(QtCore.QRect(0, 0, 100, 15), brush)
|
||||
p.end()
|
||||
label = QtGui.QLabel()
|
||||
label.setPixmap(px)
|
||||
label.setContentsMargins(1, 1, 1, 1)
|
||||
act = QtGui.QWidgetAction(self)
|
||||
act.setDefaultWidget(label)
|
||||
act.triggered.connect(self.contextMenuClicked)
|
||||
act.name = g
|
||||
self.menu.addAction(act)
|
||||
self.length = l
|
||||
self.menu.addSeparator()
|
||||
self.menu.addAction(self.rgbAction)
|
||||
self.menu.addAction(self.hsvAction)
|
||||
|
||||
|
||||
for t in list(self.ticks.keys()):
|
||||
self.removeTick(t)
|
||||
self.addTick(0, QtGui.QColor(0,0,0), True)
|
||||
self.addTick(1, QtGui.QColor(255,0,0), True)
|
||||
self.setColorMode('rgb')
|
||||
self.updateGradient()
|
||||
|
||||
def setOrientation(self, orientation):
|
||||
## public
|
||||
"""
|
||||
Set the orientation of the GradientEditorItem.
|
||||
|
||||
============== ===================================================================
|
||||
**Arguments:**
|
||||
orientation Options are: 'left', 'right', 'top', 'bottom'
|
||||
The orientation option specifies which side of the gradient the
|
||||
ticks are on, as well as whether the gradient is vertical ('right'
|
||||
and 'left') or horizontal ('top' and 'bottom').
|
||||
============== ===================================================================
|
||||
"""
|
||||
TickSliderItem.setOrientation(self, orientation)
|
||||
self.translate(0, self.rectSize)
|
||||
|
||||
def showMenu(self, ev):
|
||||
#private
|
||||
self.menu.popup(ev.screenPos().toQPoint())
|
||||
|
||||
def contextMenuClicked(self, b=None):
|
||||
#private
|
||||
#global Gradients
|
||||
act = self.sender()
|
||||
self.loadPreset(act.name)
|
||||
|
||||
def loadPreset(self, name):
|
||||
"""
|
||||
Load a predefined gradient.
|
||||
|
||||
""" ## TODO: provide image with names of defined gradients
|
||||
#global Gradients
|
||||
self.restoreState(Gradients[name])
|
||||
|
||||
def setColorMode(self, cm):
|
||||
"""
|
||||
Set the color mode for the gradient. Options are: 'hsv', 'rgb'
|
||||
|
||||
"""
|
||||
|
||||
## public
|
||||
if cm not in ['rgb', 'hsv']:
|
||||
raise Exception("Unknown color mode %s. Options are 'rgb' and 'hsv'." % str(cm))
|
||||
|
||||
try:
|
||||
self.rgbAction.blockSignals(True)
|
||||
self.hsvAction.blockSignals(True)
|
||||
self.rgbAction.setChecked(cm == 'rgb')
|
||||
self.hsvAction.setChecked(cm == 'hsv')
|
||||
finally:
|
||||
self.rgbAction.blockSignals(False)
|
||||
self.hsvAction.blockSignals(False)
|
||||
self.colorMode = cm
|
||||
self.updateGradient()
|
||||
|
||||
def colorMap(self):
|
||||
"""Return a ColorMap object representing the current state of the editor."""
|
||||
if self.colorMode == 'hsv':
|
||||
raise NotImplementedError('hsv colormaps not yet supported')
|
||||
pos = []
|
||||
color = []
|
||||
for t,x in self.listTicks():
|
||||
pos.append(x)
|
||||
c = t.color
|
||||
color.append([c.red(), c.green(), c.blue(), c.alpha()])
|
||||
return ColorMap(np.array(pos), np.array(color, dtype=np.ubyte))
|
||||
|
||||
def updateGradient(self):
|
||||
#private
|
||||
self.gradient = self.getGradient()
|
||||
self.gradRect.setBrush(QtGui.QBrush(self.gradient))
|
||||
self.sigGradientChanged.emit(self)
|
||||
|
||||
def setLength(self, newLen):
|
||||
#private (but maybe public)
|
||||
TickSliderItem.setLength(self, newLen)
|
||||
self.backgroundRect.setRect(1, -self.rectSize, newLen, self.rectSize)
|
||||
self.gradRect.setRect(1, -self.rectSize, newLen, self.rectSize)
|
||||
self.updateGradient()
|
||||
|
||||
def currentColorChanged(self, color):
|
||||
#private
|
||||
if color.isValid() and self.currentTick is not None:
|
||||
self.setTickColor(self.currentTick, color)
|
||||
self.updateGradient()
|
||||
|
||||
def currentColorRejected(self):
|
||||
#private
|
||||
self.setTickColor(self.currentTick, self.currentTickColor)
|
||||
self.updateGradient()
|
||||
|
||||
def currentColorAccepted(self):
|
||||
self.sigGradientChangeFinished.emit(self)
|
||||
|
||||
def tickClicked(self, tick, ev):
|
||||
#private
|
||||
if ev.button() == QtCore.Qt.LeftButton:
|
||||
self.raiseColorDialog(tick)
|
||||
elif ev.button() == QtCore.Qt.RightButton:
|
||||
self.raiseTickContextMenu(tick, ev)
|
||||
|
||||
def raiseColorDialog(self, tick):
|
||||
if not tick.colorChangeAllowed:
|
||||
return
|
||||
self.currentTick = tick
|
||||
self.currentTickColor = tick.color
|
||||
self.colorDialog.setCurrentColor(tick.color)
|
||||
self.colorDialog.open()
|
||||
|
||||
def raiseTickContextMenu(self, tick, ev):
|
||||
self.tickMenu = TickMenu(tick, self)
|
||||
self.tickMenu.popup(ev.screenPos().toQPoint())
|
||||
|
||||
def tickMoved(self, tick, pos):
|
||||
#private
|
||||
TickSliderItem.tickMoved(self, tick, pos)
|
||||
self.updateGradient()
|
||||
|
||||
def tickMoveFinished(self, tick):
|
||||
self.sigGradientChangeFinished.emit(self)
|
||||
|
||||
|
||||
def getGradient(self):
|
||||
"""Return a QLinearGradient object."""
|
||||
g = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(self.length,0))
|
||||
if self.colorMode == 'rgb':
|
||||
ticks = self.listTicks()
|
||||
g.setStops([(x, QtGui.QColor(t.color)) for t,x in ticks])
|
||||
elif self.colorMode == 'hsv': ## HSV mode is approximated for display by interpolating 10 points between each stop
|
||||
ticks = self.listTicks()
|
||||
stops = []
|
||||
stops.append((ticks[0][1], ticks[0][0].color))
|
||||
for i in range(1,len(ticks)):
|
||||
x1 = ticks[i-1][1]
|
||||
x2 = ticks[i][1]
|
||||
dx = (x2-x1) / 10.
|
||||
for j in range(1,10):
|
||||
x = x1 + dx*j
|
||||
stops.append((x, self.getColor(x)))
|
||||
stops.append((x2, self.getColor(x2)))
|
||||
g.setStops(stops)
|
||||
return g
|
||||
|
||||
def getColor(self, x, toQColor=True):
|
||||
"""
|
||||
Return a color for a given value.
|
||||
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
x Value (position on gradient) of requested color.
|
||||
toQColor If true, returns a QColor object, else returns a (r,g,b,a) tuple.
|
||||
============== ==================================================================
|
||||
"""
|
||||
ticks = self.listTicks()
|
||||
if x <= ticks[0][1]:
|
||||
c = ticks[0][0].color
|
||||
if toQColor:
|
||||
return QtGui.QColor(c) # always copy colors before handing them out
|
||||
else:
|
||||
return (c.red(), c.green(), c.blue(), c.alpha())
|
||||
if x >= ticks[-1][1]:
|
||||
c = ticks[-1][0].color
|
||||
if toQColor:
|
||||
return QtGui.QColor(c) # always copy colors before handing them out
|
||||
else:
|
||||
return (c.red(), c.green(), c.blue(), c.alpha())
|
||||
|
||||
x2 = ticks[0][1]
|
||||
for i in range(1,len(ticks)):
|
||||
x1 = x2
|
||||
x2 = ticks[i][1]
|
||||
if x1 <= x and x2 >= x:
|
||||
break
|
||||
|
||||
dx = (x2-x1)
|
||||
if dx == 0:
|
||||
f = 0.
|
||||
else:
|
||||
f = (x-x1) / dx
|
||||
c1 = ticks[i-1][0].color
|
||||
c2 = ticks[i][0].color
|
||||
if self.colorMode == 'rgb':
|
||||
r = c1.red() * (1.-f) + c2.red() * f
|
||||
g = c1.green() * (1.-f) + c2.green() * f
|
||||
b = c1.blue() * (1.-f) + c2.blue() * f
|
||||
a = c1.alpha() * (1.-f) + c2.alpha() * f
|
||||
if toQColor:
|
||||
return QtGui.QColor(int(r), int(g), int(b), int(a))
|
||||
else:
|
||||
return (r,g,b,a)
|
||||
elif self.colorMode == 'hsv':
|
||||
h1,s1,v1,_ = c1.getHsv()
|
||||
h2,s2,v2,_ = c2.getHsv()
|
||||
h = h1 * (1.-f) + h2 * f
|
||||
s = s1 * (1.-f) + s2 * f
|
||||
v = v1 * (1.-f) + v2 * f
|
||||
c = QtGui.QColor()
|
||||
c.setHsv(h,s,v)
|
||||
if toQColor:
|
||||
return c
|
||||
else:
|
||||
return (c.red(), c.green(), c.blue(), c.alpha())
|
||||
|
||||
def getLookupTable(self, nPts, alpha=None):
|
||||
"""
|
||||
Return an RGB(A) lookup table (ndarray).
|
||||
|
||||
============== ============================================================================
|
||||
**Arguments:**
|
||||
nPts The number of points in the returned lookup table.
|
||||
alpha True, False, or None - Specifies whether or not alpha values are included
|
||||
in the table.If alpha is None, alpha will be automatically determined.
|
||||
============== ============================================================================
|
||||
"""
|
||||
if alpha is None:
|
||||
alpha = self.usesAlpha()
|
||||
if alpha:
|
||||
table = np.empty((nPts,4), dtype=np.ubyte)
|
||||
else:
|
||||
table = np.empty((nPts,3), dtype=np.ubyte)
|
||||
|
||||
for i in range(nPts):
|
||||
x = float(i)/(nPts-1)
|
||||
color = self.getColor(x, toQColor=False)
|
||||
table[i] = color[:table.shape[1]]
|
||||
|
||||
return table
|
||||
|
||||
def usesAlpha(self):
|
||||
"""Return True if any ticks have an alpha < 255"""
|
||||
|
||||
ticks = self.listTicks()
|
||||
for t in ticks:
|
||||
if t[0].color.alpha() < 255:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def isLookupTrivial(self):
|
||||
"""Return True if the gradient has exactly two stops in it: black at 0.0 and white at 1.0"""
|
||||
ticks = self.listTicks()
|
||||
if len(ticks) != 2:
|
||||
return False
|
||||
if ticks[0][1] != 0.0 or ticks[1][1] != 1.0:
|
||||
return False
|
||||
c1 = fn.colorTuple(ticks[0][0].color)
|
||||
c2 = fn.colorTuple(ticks[1][0].color)
|
||||
if c1 != (0,0,0,255) or c2 != (255,255,255,255):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def mouseReleaseEvent(self, ev):
|
||||
#private
|
||||
TickSliderItem.mouseReleaseEvent(self, ev)
|
||||
self.updateGradient()
|
||||
|
||||
def addTick(self, x, color=None, movable=True, finish=True):
|
||||
"""
|
||||
Add a tick to the gradient. Return the tick.
|
||||
|
||||
============== ==================================================================
|
||||
**Arguments:**
|
||||
x Position where tick should be added.
|
||||
color Color of added tick. If color is not specified, the color will be
|
||||
the color of the gradient at the specified position.
|
||||
movable Specifies whether the tick is movable with the mouse.
|
||||
============== ==================================================================
|
||||
"""
|
||||
|
||||
|
||||
if color is None:
|
||||
color = self.getColor(x)
|
||||
t = TickSliderItem.addTick(self, x, color=color, movable=movable)
|
||||
t.colorChangeAllowed = True
|
||||
t.removeAllowed = True
|
||||
|
||||
if finish:
|
||||
self.sigGradientChangeFinished.emit(self)
|
||||
return t
|
||||
|
||||
|
||||
def removeTick(self, tick, finish=True):
|
||||
TickSliderItem.removeTick(self, tick)
|
||||
if finish:
|
||||
self.updateGradient()
|
||||
self.sigGradientChangeFinished.emit(self)
|
||||
|
||||
|
||||
def saveState(self):
|
||||
"""
|
||||
Return a dictionary with parameters for rebuilding the gradient. Keys will include:
|
||||
|
||||
- 'mode': hsv or rgb
|
||||
- 'ticks': a list of tuples (pos, (r,g,b,a))
|
||||
"""
|
||||
## public
|
||||
ticks = []
|
||||
for t in self.ticks:
|
||||
c = t.color
|
||||
ticks.append((self.ticks[t], (c.red(), c.green(), c.blue(), c.alpha())))
|
||||
state = {'mode': self.colorMode, 'ticks': ticks}
|
||||
return state
|
||||
|
||||
def restoreState(self, state):
|
||||
"""
|
||||
Restore the gradient specified in state.
|
||||
|
||||
============== ====================================================================
|
||||
**Arguments:**
|
||||
state A dictionary with same structure as those returned by
|
||||
:func:`saveState <pyqtgraph.GradientEditorItem.saveState>`
|
||||
|
||||
Keys must include:
|
||||
|
||||
- 'mode': hsv or rgb
|
||||
- 'ticks': a list of tuples (pos, (r,g,b,a))
|
||||
============== ====================================================================
|
||||
"""
|
||||
## public
|
||||
self.setColorMode(state['mode'])
|
||||
for t in list(self.ticks.keys()):
|
||||
self.removeTick(t, finish=False)
|
||||
for t in state['ticks']:
|
||||
c = QtGui.QColor(*t[1])
|
||||
self.addTick(t[0], c, finish=False)
|
||||
self.updateGradient()
|
||||
self.sigGradientChangeFinished.emit(self)
|
||||
|
||||
def setColorMap(self, cm):
|
||||
self.setColorMode('rgb')
|
||||
for t in list(self.ticks.keys()):
|
||||
self.removeTick(t, finish=False)
|
||||
colors = cm.getColors(mode='qcolor')
|
||||
for i in range(len(cm.pos)):
|
||||
x = cm.pos[i]
|
||||
c = colors[i]
|
||||
self.addTick(x, c, finish=False)
|
||||
self.updateGradient()
|
||||
self.sigGradientChangeFinished.emit(self)
|
||||
|
||||
|
||||
class Tick(QtGui.QGraphicsWidget): ## NOTE: Making this a subclass of GraphicsObject instead results in
|
||||
## activating this bug: https://bugreports.qt-project.org/browse/PYSIDE-86
|
||||
## private class
|
||||
|
||||
# When making Tick a subclass of QtGui.QGraphicsObject as origin,
|
||||
# ..GraphicsScene.items(self, *args) will get Tick object as a
|
||||
# class of QtGui.QMultimediaWidgets.QGraphicsVideoItem in python2.7-PyQt5(5.4.0)
|
||||
|
||||
sigMoving = QtCore.Signal(object)
|
||||
sigMoved = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, view, pos, color, movable=True, scale=10, pen='w'):
|
||||
self.movable = movable
|
||||
self.moving = False
|
||||
self.view = weakref.ref(view)
|
||||
self.scale = scale
|
||||
self.color = color
|
||||
self.pen = fn.mkPen(pen)
|
||||
self.hoverPen = fn.mkPen(255,255,0)
|
||||
self.currentPen = self.pen
|
||||
self.pg = QtGui.QPainterPath(QtCore.QPointF(0,0))
|
||||
self.pg.lineTo(QtCore.QPointF(-scale/3**0.5, scale))
|
||||
self.pg.lineTo(QtCore.QPointF(scale/3**0.5, scale))
|
||||
self.pg.closeSubpath()
|
||||
|
||||
QtGui.QGraphicsObject.__init__(self)
|
||||
self.setPos(pos[0], pos[1])
|
||||
if self.movable:
|
||||
self.setZValue(1)
|
||||
else:
|
||||
self.setZValue(0)
|
||||
|
||||
def boundingRect(self):
|
||||
return self.pg.boundingRect()
|
||||
|
||||
def shape(self):
|
||||
return self.pg
|
||||
|
||||
def paint(self, p, *args):
|
||||
p.setRenderHints(QtGui.QPainter.Antialiasing)
|
||||
p.fillPath(self.pg, fn.mkBrush(self.color))
|
||||
|
||||
p.setPen(self.currentPen)
|
||||
p.drawPath(self.pg)
|
||||
|
||||
|
||||
def mouseDragEvent(self, ev):
|
||||
if self.movable and ev.button() == QtCore.Qt.LeftButton:
|
||||
if ev.isStart():
|
||||
self.moving = True
|
||||
self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos())
|
||||
self.startPosition = self.pos()
|
||||
ev.accept()
|
||||
|
||||
if not self.moving:
|
||||
return
|
||||
|
||||
newPos = self.cursorOffset + self.mapToParent(ev.pos())
|
||||
newPos.setY(self.pos().y())
|
||||
|
||||
self.setPos(newPos)
|
||||
self.view().tickMoved(self, newPos)
|
||||
self.sigMoving.emit(self)
|
||||
if ev.isFinish():
|
||||
self.moving = False
|
||||
self.sigMoved.emit(self)
|
||||
self.view().tickMoveFinished(self)
|
||||
|
||||
def mouseClickEvent(self, ev):
|
||||
if ev.button() == QtCore.Qt.RightButton and self.moving:
|
||||
ev.accept()
|
||||
self.setPos(self.startPosition)
|
||||
self.view().tickMoved(self, self.startPosition)
|
||||
self.moving = False
|
||||
self.sigMoving.emit(self)
|
||||
self.sigMoved.emit(self)
|
||||
else:
|
||||
self.view().tickClicked(self, ev)
|
||||
##remove
|
||||
|
||||
def hoverEvent(self, ev):
|
||||
if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
|
||||
ev.acceptClicks(QtCore.Qt.LeftButton)
|
||||
ev.acceptClicks(QtCore.Qt.RightButton)
|
||||
self.currentPen = self.hoverPen
|
||||
else:
|
||||
self.currentPen = self.pen
|
||||
self.update()
|
||||
|
||||
|
||||
class TickMenu(QtGui.QMenu):
|
||||
|
||||
def __init__(self, tick, sliderItem):
|
||||
QtGui.QMenu.__init__(self)
|
||||
|
||||
self.tick = weakref.ref(tick)
|
||||
self.sliderItem = weakref.ref(sliderItem)
|
||||
|
||||
self.removeAct = self.addAction("Remove Tick", lambda: self.sliderItem().removeTick(tick))
|
||||
if (not self.tick().removeAllowed) or len(self.sliderItem().ticks) < 3:
|
||||
self.removeAct.setEnabled(False)
|
||||
|
||||
positionMenu = self.addMenu("Set Position")
|
||||
w = QtGui.QWidget()
|
||||
l = QtGui.QGridLayout()
|
||||
w.setLayout(l)
|
||||
|
||||
value = sliderItem.tickValue(tick)
|
||||
self.fracPosSpin = SpinBox()
|
||||
self.fracPosSpin.setOpts(value=value, bounds=(0.0, 1.0), step=0.01, decimals=2)
|
||||
#self.dataPosSpin = SpinBox(value=dataVal)
|
||||
#self.dataPosSpin.setOpts(decimals=3, siPrefix=True)
|
||||
|
||||
l.addWidget(QtGui.QLabel("Position:"), 0,0)
|
||||
l.addWidget(self.fracPosSpin, 0, 1)
|
||||
#l.addWidget(QtGui.QLabel("Position (data units):"), 1, 0)
|
||||
#l.addWidget(self.dataPosSpin, 1,1)
|
||||
|
||||
#if self.sliderItem().dataParent is None:
|
||||
# self.dataPosSpin.setEnabled(False)
|
||||
|
||||
a = QtGui.QWidgetAction(self)
|
||||
a.setDefaultWidget(w)
|
||||
positionMenu.addAction(a)
|
||||
|
||||
self.fracPosSpin.sigValueChanging.connect(self.fractionalValueChanged)
|
||||
#self.dataPosSpin.valueChanged.connect(self.dataValueChanged)
|
||||
|
||||
colorAct = self.addAction("Set Color", lambda: self.sliderItem().raiseColorDialog(self.tick()))
|
||||
if not self.tick().colorChangeAllowed:
|
||||
colorAct.setEnabled(False)
|
||||
|
||||
def fractionalValueChanged(self, x):
|
||||
self.sliderItem().setTickValue(self.tick(), self.fracPosSpin.value())
|
||||
#if self.sliderItem().dataParent is not None:
|
||||
# self.dataPosSpin.blockSignals(True)
|
||||
# self.dataPosSpin.setValue(self.sliderItem().tickDataValue(self.tick()))
|
||||
# self.dataPosSpin.blockSignals(False)
|
||||
|
||||
#def dataValueChanged(self, val):
|
||||
# self.sliderItem().setTickValue(self.tick(), val, dataUnits=True)
|
||||
# self.fracPosSpin.blockSignals(True)
|
||||
# self.fracPosSpin.setValue(self.sliderItem().tickValue(self.tick()))
|
||||
# self.fracPosSpin.blockSignals(False)
|
||||
|
114
pyqtgraph/graphicsItems/GradientLegend.py
Normal file
114
pyqtgraph/graphicsItems/GradientLegend.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from .UIGraphicsItem import *
|
||||
from .. import functions as fn
|
||||
|
||||
__all__ = ['GradientLegend']
|
||||
|
||||
class GradientLegend(UIGraphicsItem):
|
||||
"""
|
||||
Draws a color gradient rectangle along with text labels denoting the value at specific
|
||||
points along the gradient.
|
||||
"""
|
||||
|
||||
def __init__(self, size, offset):
|
||||
self.size = size
|
||||
self.offset = offset
|
||||
UIGraphicsItem.__init__(self)
|
||||
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
|
||||
self.brush = QtGui.QBrush(QtGui.QColor(200,0,0))
|
||||
self.pen = QtGui.QPen(QtGui.QColor(0,0,0))
|
||||
self.labels = {'max': 1, 'min': 0}
|
||||
self.gradient = QtGui.QLinearGradient()
|
||||
self.gradient.setColorAt(0, QtGui.QColor(0,0,0))
|
||||
self.gradient.setColorAt(1, QtGui.QColor(255,0,0))
|
||||
|
||||
def setGradient(self, g):
|
||||
self.gradient = g
|
||||
self.update()
|
||||
|
||||
def setIntColorScale(self, minVal, maxVal, *args, **kargs):
|
||||
colors = [fn.intColor(i, maxVal-minVal, *args, **kargs) for i in range(minVal, maxVal)]
|
||||
g = QtGui.QLinearGradient()
|
||||
for i in range(len(colors)):
|
||||
x = float(i)/len(colors)
|
||||
g.setColorAt(x, colors[i])
|
||||
self.setGradient(g)
|
||||
if 'labels' not in kargs:
|
||||
self.setLabels({str(minVal/10.): 0, str(maxVal): 1})
|
||||
else:
|
||||
self.setLabels({kargs['labels'][0]:0, kargs['labels'][1]:1})
|
||||
|
||||
def setLabels(self, l):
|
||||
"""Defines labels to appear next to the color scale. Accepts a dict of {text: value} pairs"""
|
||||
self.labels = l
|
||||
self.update()
|
||||
|
||||
def paint(self, p, opt, widget):
|
||||
UIGraphicsItem.paint(self, p, opt, widget)
|
||||
rect = self.boundingRect() ## Boundaries of visible area in scene coords.
|
||||
unit = self.pixelSize() ## Size of one view pixel in scene coords.
|
||||
if unit[0] is None:
|
||||
return
|
||||
|
||||
## determine max width of all labels
|
||||
labelWidth = 0
|
||||
labelHeight = 0
|
||||
for k in self.labels:
|
||||
b = p.boundingRect(QtCore.QRectF(0, 0, 0, 0), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(k))
|
||||
labelWidth = max(labelWidth, b.width())
|
||||
labelHeight = max(labelHeight, b.height())
|
||||
|
||||
labelWidth *= unit[0]
|
||||
labelHeight *= unit[1]
|
||||
|
||||
textPadding = 2 # in px
|
||||
|
||||
if self.offset[0] < 0:
|
||||
x3 = rect.right() + unit[0] * self.offset[0]
|
||||
x2 = x3 - labelWidth - unit[0]*textPadding*2
|
||||
x1 = x2 - unit[0] * self.size[0]
|
||||
else:
|
||||
x1 = rect.left() + unit[0] * self.offset[0]
|
||||
x2 = x1 + unit[0] * self.size[0]
|
||||
x3 = x2 + labelWidth + unit[0]*textPadding*2
|
||||
if self.offset[1] < 0:
|
||||
y2 = rect.top() - unit[1] * self.offset[1]
|
||||
y1 = y2 + unit[1] * self.size[1]
|
||||
else:
|
||||
y1 = rect.bottom() - unit[1] * self.offset[1]
|
||||
y2 = y1 - unit[1] * self.size[1]
|
||||
self.b = [x1,x2,x3,y1,y2,labelWidth]
|
||||
|
||||
## Draw background
|
||||
p.setPen(self.pen)
|
||||
p.setBrush(QtGui.QBrush(QtGui.QColor(255,255,255,100)))
|
||||
rect = QtCore.QRectF(
|
||||
QtCore.QPointF(x1 - unit[0]*textPadding, y1 + labelHeight/2 + unit[1]*textPadding),
|
||||
QtCore.QPointF(x3, y2 - labelHeight/2 - unit[1]*textPadding)
|
||||
)
|
||||
p.drawRect(rect)
|
||||
|
||||
|
||||
## Have to scale painter so that text and gradients are correct size. Bleh.
|
||||
p.scale(unit[0], unit[1])
|
||||
|
||||
## Draw color bar
|
||||
self.gradient.setStart(0, y1/unit[1])
|
||||
self.gradient.setFinalStop(0, y2/unit[1])
|
||||
p.setBrush(self.gradient)
|
||||
rect = QtCore.QRectF(
|
||||
QtCore.QPointF(x1/unit[0], y1/unit[1]),
|
||||
QtCore.QPointF(x2/unit[0], y2/unit[1])
|
||||
)
|
||||
p.drawRect(rect)
|
||||
|
||||
|
||||
## draw labels
|
||||
p.setPen(QtGui.QPen(QtGui.QColor(0,0,0)))
|
||||
tx = x2 + unit[0]*textPadding
|
||||
lh = labelHeight/unit[1]
|
||||
for k in self.labels:
|
||||
y = y1 + self.labels[k] * (y2-y1)
|
||||
p.drawText(QtCore.QRectF(tx/unit[0], y/unit[1] - lh/2.0, 1000, lh), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(k))
|
||||
|
||||
|
147
pyqtgraph/graphicsItems/GraphItem.py
Normal file
147
pyqtgraph/graphicsItems/GraphItem.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
from .. import functions as fn
|
||||
from .GraphicsObject import GraphicsObject
|
||||
from .ScatterPlotItem import ScatterPlotItem
|
||||
from ..Qt import QtGui, QtCore
|
||||
import numpy as np
|
||||
from .. import getConfigOption
|
||||
|
||||
__all__ = ['GraphItem']
|
||||
|
||||
|
||||
class GraphItem(GraphicsObject):
|
||||
"""A GraphItem displays graph information as
|
||||
a set of nodes connected by lines (as in 'graph theory', not 'graphics').
|
||||
Useful for drawing networks, trees, etc.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwds):
|
||||
GraphicsObject.__init__(self)
|
||||
self.scatter = ScatterPlotItem()
|
||||
self.scatter.setParentItem(self)
|
||||
self.adjacency = None
|
||||
self.pos = None
|
||||
self.picture = None
|
||||
self.pen = 'default'
|
||||
self.setData(**kwds)
|
||||
|
||||
def setData(self, **kwds):
|
||||
"""
|
||||
Change the data displayed by the graph.
|
||||
|
||||
============== =======================================================================
|
||||
**Arguments:**
|
||||
pos (N,2) array of the positions of each node in the graph.
|
||||
adj (M,2) array of connection data. Each row contains indexes
|
||||
of two nodes that are connected.
|
||||
pen The pen to use when drawing lines between connected
|
||||
nodes. May be one of:
|
||||
|
||||
* QPen
|
||||
* a single argument to pass to pg.mkPen
|
||||
* a record array of length M
|
||||
with fields (red, green, blue, alpha, width). Note
|
||||
that using this option may have a significant performance
|
||||
cost.
|
||||
* None (to disable connection drawing)
|
||||
* 'default' to use the default foreground color.
|
||||
|
||||
symbolPen The pen(s) used for drawing nodes.
|
||||
symbolBrush The brush(es) used for drawing nodes.
|
||||
``**opts`` All other keyword arguments are given to
|
||||
:func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>`
|
||||
to affect the appearance of nodes (symbol, size, brush,
|
||||
etc.)
|
||||
============== =======================================================================
|
||||
"""
|
||||
if 'adj' in kwds:
|
||||
self.adjacency = kwds.pop('adj')
|
||||
if self.adjacency.dtype.kind not in 'iu':
|
||||
raise Exception("adjacency array must have int or unsigned type.")
|
||||
self._update()
|
||||
if 'pos' in kwds:
|
||||
self.pos = kwds['pos']
|
||||
self._update()
|
||||
if 'pen' in kwds:
|
||||
self.setPen(kwds.pop('pen'))
|
||||
self._update()
|
||||
|
||||
if 'symbolPen' in kwds:
|
||||
kwds['pen'] = kwds.pop('symbolPen')
|
||||
if 'symbolBrush' in kwds:
|
||||
kwds['brush'] = kwds.pop('symbolBrush')
|
||||
self.scatter.setData(**kwds)
|
||||
self.informViewBoundsChanged()
|
||||
|
||||
def _update(self):
|
||||
self.picture = None
|
||||
self.prepareGeometryChange()
|
||||
self.update()
|
||||
|
||||
def setPen(self, *args, **kwargs):
|
||||
"""
|
||||
Set the pen used to draw graph lines.
|
||||
May be:
|
||||
|
||||
* None to disable line drawing
|
||||
* Record array with fields (red, green, blue, alpha, width)
|
||||
* Any set of arguments and keyword arguments accepted by
|
||||
:func:`mkPen <pyqtgraph.mkPen>`.
|
||||
* 'default' to use the default foreground color.
|
||||
"""
|
||||
if len(args) == 1 and len(kwargs) == 0:
|
||||
self.pen = args[0]
|
||||
else:
|
||||
self.pen = fn.mkPen(*args, **kwargs)
|
||||
self.picture = None
|
||||
self.update()
|
||||
|
||||
def generatePicture(self):
|
||||
self.picture = QtGui.QPicture()
|
||||
if self.pen is None or self.pos is None or self.adjacency is None:
|
||||
return
|
||||
|
||||
p = QtGui.QPainter(self.picture)
|
||||
try:
|
||||
pts = self.pos[self.adjacency]
|
||||
pen = self.pen
|
||||
if isinstance(pen, np.ndarray):
|
||||
lastPen = None
|
||||
for i in range(pts.shape[0]):
|
||||
pen = self.pen[i]
|
||||
if np.any(pen != lastPen):
|
||||
lastPen = pen
|
||||
if pen.dtype.fields is None:
|
||||
p.setPen(fn.mkPen(color=(pen[0], pen[1], pen[2], pen[3]), width=1))
|
||||
else:
|
||||
p.setPen(fn.mkPen(color=(pen['red'], pen['green'], pen['blue'], pen['alpha']), width=pen['width']))
|
||||
p.drawLine(QtCore.QPointF(*pts[i][0]), QtCore.QPointF(*pts[i][1]))
|
||||
else:
|
||||
if pen == 'default':
|
||||
pen = getConfigOption('foreground')
|
||||
p.setPen(fn.mkPen(pen))
|
||||
pts = pts.reshape((pts.shape[0]*pts.shape[1], pts.shape[2]))
|
||||
path = fn.arrayToQPath(x=pts[:,0], y=pts[:,1], connect='pairs')
|
||||
p.drawPath(path)
|
||||
finally:
|
||||
p.end()
|
||||
|
||||
def paint(self, p, *args):
|
||||
if self.picture == None:
|
||||
self.generatePicture()
|
||||
if getConfigOption('antialias') is True:
|
||||
p.setRenderHint(p.Antialiasing)
|
||||
self.picture.play(p)
|
||||
|
||||
def boundingRect(self):
|
||||
return self.scatter.boundingRect()
|
||||
|
||||
def dataBounds(self, *args, **kwds):
|
||||
return self.scatter.dataBounds(*args, **kwds)
|
||||
|
||||
def pixelPadding(self):
|
||||
return self.scatter.pixelPadding()
|
||||
|
||||
|
||||
|
||||
|
||||
|
585
pyqtgraph/graphicsItems/GraphicsItem.py
Normal file
585
pyqtgraph/graphicsItems/GraphicsItem.py
Normal file
|
@ -0,0 +1,585 @@
|
|||
from ..Qt import QtGui, QtCore, isQObjectAlive
|
||||
from ..GraphicsScene import GraphicsScene
|
||||
from ..Point import Point
|
||||
from .. import functions as fn
|
||||
import weakref
|
||||
import operator
|
||||
from ..util.lru_cache import LRUCache
|
||||
|
||||
|
||||
class GraphicsItem(object):
|
||||
"""
|
||||
**Bases:** :class:`object`
|
||||
|
||||
Abstract class providing useful methods to GraphicsObject and GraphicsWidget.
|
||||
(This is required because we cannot have multiple inheritance with QObject subclasses.)
|
||||
|
||||
A note about Qt's GraphicsView framework:
|
||||
|
||||
The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task.
|
||||
"""
|
||||
_pixelVectorGlobalCache = LRUCache(100, 70)
|
||||
|
||||
def __init__(self, register=True):
|
||||
if not hasattr(self, '_qtBaseClass'):
|
||||
for b in self.__class__.__bases__:
|
||||
if issubclass(b, QtGui.QGraphicsItem):
|
||||
self.__class__._qtBaseClass = b
|
||||
break
|
||||
if not hasattr(self, '_qtBaseClass'):
|
||||
raise Exception('Could not determine Qt base class for GraphicsItem: %s' % str(self))
|
||||
|
||||
self._pixelVectorCache = [None, None]
|
||||
self._viewWidget = None
|
||||
self._viewBox = None
|
||||
self._connectedView = None
|
||||
self._exportOpts = False ## If False, not currently exporting. Otherwise, contains dict of export options.
|
||||
if register:
|
||||
GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items()
|
||||
|
||||
|
||||
|
||||
|
||||
def getViewWidget(self):
|
||||
"""
|
||||
Return the view widget for this item.
|
||||
|
||||
If the scene has multiple views, only the first view is returned.
|
||||
The return value is cached; clear the cached value with forgetViewWidget().
|
||||
If the view has been deleted by Qt, return None.
|
||||
"""
|
||||
if self._viewWidget is None:
|
||||
scene = self.scene()
|
||||
if scene is None:
|
||||
return None
|
||||
views = scene.views()
|
||||
if len(views) < 1:
|
||||
return None
|
||||
self._viewWidget = weakref.ref(self.scene().views()[0])
|
||||
|
||||
v = self._viewWidget()
|
||||
if v is not None and not isQObjectAlive(v):
|
||||
return None
|
||||
|
||||
return v
|
||||
|
||||
def forgetViewWidget(self):
|
||||
self._viewWidget = None
|
||||
|
||||
def getViewBox(self):
|
||||
"""
|
||||
Return the first ViewBox or GraphicsView which bounds this item's visible space.
|
||||
If this item is not contained within a ViewBox, then the GraphicsView is returned.
|
||||
If the item is contained inside nested ViewBoxes, then the inner-most ViewBox is returned.
|
||||
The result is cached; clear the cache with forgetViewBox()
|
||||
"""
|
||||
if self._viewBox is None:
|
||||
p = self
|
||||
while True:
|
||||
try:
|
||||
p = p.parentItem()
|
||||
except RuntimeError: ## sometimes happens as items are being removed from a scene and collected.
|
||||
return None
|
||||
if p is None:
|
||||
vb = self.getViewWidget()
|
||||
if vb is None:
|
||||
return None
|
||||
else:
|
||||
self._viewBox = weakref.ref(vb)
|
||||
break
|
||||
if hasattr(p, 'implements') and p.implements('ViewBox'):
|
||||
self._viewBox = weakref.ref(p)
|
||||
break
|
||||
return self._viewBox() ## If we made it this far, _viewBox is definitely not None
|
||||
|
||||
def forgetViewBox(self):
|
||||
self._viewBox = None
|
||||
|
||||
|
||||
def deviceTransform(self, viewportTransform=None):
|
||||
"""
|
||||
Return the transform that converts local item coordinates to device coordinates (usually pixels).
|
||||
Extends deviceTransform to automatically determine the viewportTransform.
|
||||
"""
|
||||
if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different.
|
||||
return self._exportOpts['painter'].deviceTransform() * self.sceneTransform()
|
||||
|
||||
if viewportTransform is None:
|
||||
view = self.getViewWidget()
|
||||
if view is None:
|
||||
return None
|
||||
viewportTransform = view.viewportTransform()
|
||||
dt = self._qtBaseClass.deviceTransform(self, viewportTransform)
|
||||
|
||||
#xmag = abs(dt.m11())+abs(dt.m12())
|
||||
#ymag = abs(dt.m21())+abs(dt.m22())
|
||||
#if xmag * ymag == 0:
|
||||
if dt.determinant() == 0: ## occurs when deviceTransform is invalid because widget has not been displayed
|
||||
return None
|
||||
else:
|
||||
return dt
|
||||
|
||||
def viewTransform(self):
|
||||
"""Return the transform that maps from local coordinates to the item's ViewBox coordinates
|
||||
If there is no ViewBox, return the scene transform.
|
||||
Returns None if the item does not have a view."""
|
||||
view = self.getViewBox()
|
||||
if view is None:
|
||||
return None
|
||||
if hasattr(view, 'implements') and view.implements('ViewBox'):
|
||||
tr = self.itemTransform(view.innerSceneItem())
|
||||
if isinstance(tr, tuple):
|
||||
tr = tr[0] ## difference between pyside and pyqt
|
||||
return tr
|
||||
else:
|
||||
return self.sceneTransform()
|
||||
#return self.deviceTransform(view.viewportTransform())
|
||||
|
||||
|
||||
|
||||
def getBoundingParents(self):
|
||||
"""Return a list of parents to this item that have child clipping enabled."""
|
||||
p = self
|
||||
parents = []
|
||||
while True:
|
||||
p = p.parentItem()
|
||||
if p is None:
|
||||
break
|
||||
if p.flags() & self.ItemClipsChildrenToShape:
|
||||
parents.append(p)
|
||||
return parents
|
||||
|
||||
def viewRect(self):
|
||||
"""Return the bounds (in item coordinates) of this item's ViewBox or GraphicsWidget"""
|
||||
view = self.getViewBox()
|
||||
if view is None:
|
||||
return None
|
||||
bounds = self.mapRectFromView(view.viewRect())
|
||||
if bounds is None:
|
||||
return None
|
||||
|
||||
bounds = bounds.normalized()
|
||||
|
||||
## nah.
|
||||
#for p in self.getBoundingParents():
|
||||
#bounds &= self.mapRectFromScene(p.sceneBoundingRect())
|
||||
|
||||
return bounds
|
||||
|
||||
|
||||
|
||||
def pixelVectors(self, direction=None):
|
||||
"""Return vectors in local coordinates representing the width and height of a view pixel.
|
||||
If direction is specified, then return vectors parallel and orthogonal to it.
|
||||
|
||||
Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed)
|
||||
or if pixel size is below floating-point precision limit.
|
||||
"""
|
||||
|
||||
## This is an expensive function that gets called very frequently.
|
||||
## We have two levels of cache to try speeding things up.
|
||||
|
||||
dt = self.deviceTransform()
|
||||
if dt is None:
|
||||
return None, None
|
||||
|
||||
## Ignore translation. If the translation is much larger than the scale
|
||||
## (such as when looking at unix timestamps), we can get floating-point errors.
|
||||
dt.setMatrix(dt.m11(), dt.m12(), 0, dt.m21(), dt.m22(), 0, 0, 0, 1)
|
||||
|
||||
## check local cache
|
||||
if direction is None and dt == self._pixelVectorCache[0]:
|
||||
return tuple(map(Point, self._pixelVectorCache[1])) ## return a *copy*
|
||||
|
||||
## check global cache
|
||||
#key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32())
|
||||
key = (dt.m11(), dt.m21(), dt.m12(), dt.m22())
|
||||
pv = self._pixelVectorGlobalCache.get(key, None)
|
||||
if direction is None and pv is not None:
|
||||
self._pixelVectorCache = [dt, pv]
|
||||
return tuple(map(Point,pv)) ## return a *copy*
|
||||
|
||||
|
||||
if direction is None:
|
||||
direction = QtCore.QPointF(1, 0)
|
||||
if direction.manhattanLength() == 0:
|
||||
raise Exception("Cannot compute pixel length for 0-length vector.")
|
||||
|
||||
## attempt to re-scale direction vector to fit within the precision of the coordinate system
|
||||
## Here's the problem: we need to map the vector 'direction' from the item to the device, via transform 'dt'.
|
||||
## In some extreme cases, this mapping can fail unless the length of 'direction' is cleverly chosen.
|
||||
## Example:
|
||||
## dt = [ 1, 0, 2
|
||||
## 0, 2, 1e20
|
||||
## 0, 0, 1 ]
|
||||
## Then we map the origin (0,0) and direction (0,1) and get:
|
||||
## o' = 2,1e20
|
||||
## d' = 2,1e20 <-- should be 1e20+2, but this can't be represented with a 32-bit float
|
||||
##
|
||||
## |o' - d'| == 0 <-- this is the problem.
|
||||
|
||||
## Perhaps the easiest solution is to exclude the transformation column from dt. Does this cause any other problems?
|
||||
|
||||
#if direction.x() == 0:
|
||||
#r = abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))
|
||||
##r = 1.0/(abs(dt.m12()) + abs(dt.m22()))
|
||||
#elif direction.y() == 0:
|
||||
#r = abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))
|
||||
##r = 1.0/(abs(dt.m11()) + abs(dt.m21()))
|
||||
#else:
|
||||
#r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5
|
||||
#if r == 0:
|
||||
#r = 1. ## shouldn't need to do this; probably means the math above is wrong?
|
||||
#directionr = direction * r
|
||||
directionr = direction
|
||||
|
||||
## map direction vector onto device
|
||||
#viewDir = Point(dt.map(directionr) - dt.map(Point(0,0)))
|
||||
#mdirection = dt.map(directionr)
|
||||
dirLine = QtCore.QLineF(QtCore.QPointF(0,0), directionr)
|
||||
viewDir = dt.map(dirLine)
|
||||
if viewDir.length() == 0:
|
||||
return None, None ## pixel size cannot be represented on this scale
|
||||
|
||||
## get unit vector and orthogonal vector (length of pixel)
|
||||
#orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space
|
||||
try:
|
||||
normView = viewDir.unitVector()
|
||||
#normView = viewDir.norm() ## direction of one pixel orthogonal to line
|
||||
normOrtho = normView.normalVector()
|
||||
#normOrtho = orthoDir.norm()
|
||||
except:
|
||||
raise Exception("Invalid direction %s" %directionr)
|
||||
|
||||
## map back to item
|
||||
dti = fn.invertQTransform(dt)
|
||||
#pv = Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0)))
|
||||
pv = Point(dti.map(normView).p2()), Point(dti.map(normOrtho).p2())
|
||||
self._pixelVectorCache[1] = pv
|
||||
self._pixelVectorCache[0] = dt
|
||||
self._pixelVectorGlobalCache[key] = pv
|
||||
return self._pixelVectorCache[1]
|
||||
|
||||
|
||||
def pixelLength(self, direction, ortho=False):
|
||||
"""Return the length of one pixel in the direction indicated (in local coordinates)
|
||||
If ortho=True, then return the length of one pixel orthogonal to the direction indicated.
|
||||
|
||||
Return None if pixel size is not yet defined (usually because the item has not yet been displayed).
|
||||
"""
|
||||
normV, orthoV = self.pixelVectors(direction)
|
||||
if normV == None or orthoV == None:
|
||||
return None
|
||||
if ortho:
|
||||
return orthoV.length()
|
||||
return normV.length()
|
||||
|
||||
|
||||
def pixelSize(self):
|
||||
## deprecated
|
||||
v = self.pixelVectors()
|
||||
if v == (None, None):
|
||||
return None, None
|
||||
return (v[0].x()**2+v[0].y()**2)**0.5, (v[1].x()**2+v[1].y()**2)**0.5
|
||||
|
||||
def pixelWidth(self):
|
||||
## deprecated
|
||||
vt = self.deviceTransform()
|
||||
if vt is None:
|
||||
return 0
|
||||
vt = fn.invertQTransform(vt)
|
||||
return vt.map(QtCore.QLineF(0, 0, 1, 0)).length()
|
||||
|
||||
def pixelHeight(self):
|
||||
## deprecated
|
||||
vt = self.deviceTransform()
|
||||
if vt is None:
|
||||
return 0
|
||||
vt = fn.invertQTransform(vt)
|
||||
return vt.map(QtCore.QLineF(0, 0, 0, 1)).length()
|
||||
#return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length()
|
||||
|
||||
|
||||
def mapToDevice(self, obj):
|
||||
"""
|
||||
Return *obj* mapped from local coordinates to device coordinates (pixels).
|
||||
If there is no device mapping available, return None.
|
||||
"""
|
||||
vt = self.deviceTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
return vt.map(obj)
|
||||
|
||||
def mapFromDevice(self, obj):
|
||||
"""
|
||||
Return *obj* mapped from device coordinates (pixels) to local coordinates.
|
||||
If there is no device mapping available, return None.
|
||||
"""
|
||||
vt = self.deviceTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
if isinstance(obj, QtCore.QPoint):
|
||||
obj = QtCore.QPointF(obj)
|
||||
vt = fn.invertQTransform(vt)
|
||||
return vt.map(obj)
|
||||
|
||||
def mapRectToDevice(self, rect):
|
||||
"""
|
||||
Return *rect* mapped from local coordinates to device coordinates (pixels).
|
||||
If there is no device mapping available, return None.
|
||||
"""
|
||||
vt = self.deviceTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
return vt.mapRect(rect)
|
||||
|
||||
def mapRectFromDevice(self, rect):
|
||||
"""
|
||||
Return *rect* mapped from device coordinates (pixels) to local coordinates.
|
||||
If there is no device mapping available, return None.
|
||||
"""
|
||||
vt = self.deviceTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
vt = fn.invertQTransform(vt)
|
||||
return vt.mapRect(rect)
|
||||
|
||||
def mapToView(self, obj):
|
||||
vt = self.viewTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
return vt.map(obj)
|
||||
|
||||
def mapRectToView(self, obj):
|
||||
vt = self.viewTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
return vt.mapRect(obj)
|
||||
|
||||
def mapFromView(self, obj):
|
||||
vt = self.viewTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
vt = fn.invertQTransform(vt)
|
||||
return vt.map(obj)
|
||||
|
||||
def mapRectFromView(self, obj):
|
||||
vt = self.viewTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
vt = fn.invertQTransform(vt)
|
||||
return vt.mapRect(obj)
|
||||
|
||||
def pos(self):
|
||||
return Point(self._qtBaseClass.pos(self))
|
||||
|
||||
def viewPos(self):
|
||||
return self.mapToView(self.mapFromParent(self.pos()))
|
||||
|
||||
def parentItem(self):
|
||||
## PyQt bug -- some items are returned incorrectly.
|
||||
return GraphicsScene.translateGraphicsItem(self._qtBaseClass.parentItem(self))
|
||||
|
||||
def setParentItem(self, parent):
|
||||
## Workaround for Qt bug: https://bugreports.qt-project.org/browse/QTBUG-18616
|
||||
if parent is not None:
|
||||
pscene = parent.scene()
|
||||
if pscene is not None and self.scene() is not pscene:
|
||||
pscene.addItem(self)
|
||||
return self._qtBaseClass.setParentItem(self, parent)
|
||||
|
||||
def childItems(self):
|
||||
## PyQt bug -- some child items are returned incorrectly.
|
||||
return list(map(GraphicsScene.translateGraphicsItem, self._qtBaseClass.childItems(self)))
|
||||
|
||||
|
||||
def sceneTransform(self):
|
||||
## Qt bug: do no allow access to sceneTransform() until
|
||||
## the item has a scene.
|
||||
|
||||
if self.scene() is None:
|
||||
return self.transform()
|
||||
else:
|
||||
return self._qtBaseClass.sceneTransform(self)
|
||||
|
||||
|
||||
def transformAngle(self, relativeItem=None):
|
||||
"""Return the rotation produced by this item's transform (this assumes there is no shear in the transform)
|
||||
If relativeItem is given, then the angle is determined relative to that item.
|
||||
"""
|
||||
if relativeItem is None:
|
||||
relativeItem = self.parentItem()
|
||||
|
||||
|
||||
tr = self.itemTransform(relativeItem)
|
||||
if isinstance(tr, tuple): ## difference between pyside and pyqt
|
||||
tr = tr[0]
|
||||
#vec = tr.map(Point(1,0)) - tr.map(Point(0,0))
|
||||
vec = tr.map(QtCore.QLineF(0,0,1,0))
|
||||
#return Point(vec).angle(Point(1,0))
|
||||
return vec.angleTo(QtCore.QLineF(vec.p1(), vec.p1()+QtCore.QPointF(1,0)))
|
||||
|
||||
#def itemChange(self, change, value):
|
||||
#ret = self._qtBaseClass.itemChange(self, change, value)
|
||||
#if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged:
|
||||
#print "Item scene changed:", self
|
||||
#self.setChildScene(self) ## This is bizarre.
|
||||
#return ret
|
||||
|
||||
#def setChildScene(self, ch):
|
||||
#scene = self.scene()
|
||||
#for ch2 in ch.childItems():
|
||||
#if ch2.scene() is not scene:
|
||||
#print "item", ch2, "has different scene:", ch2.scene(), scene
|
||||
#scene.addItem(ch2)
|
||||
#QtGui.QApplication.processEvents()
|
||||
#print " --> ", ch2.scene()
|
||||
#self.setChildScene(ch2)
|
||||
|
||||
def parentChanged(self):
|
||||
"""Called when the item's parent has changed.
|
||||
This method handles connecting / disconnecting from ViewBox signals
|
||||
to make sure viewRangeChanged works properly. It should generally be
|
||||
extended, not overridden."""
|
||||
self._updateView()
|
||||
|
||||
|
||||
def _updateView(self):
|
||||
## called to see whether this item has a new view to connect to
|
||||
## NOTE: This is called from GraphicsObject.itemChange or GraphicsWidget.itemChange.
|
||||
|
||||
## It is possible this item has moved to a different ViewBox or widget;
|
||||
## clear out previously determined references to these.
|
||||
self.forgetViewBox()
|
||||
self.forgetViewWidget()
|
||||
|
||||
## check for this item's current viewbox or view widget
|
||||
view = self.getViewBox()
|
||||
#if view is None:
|
||||
##print " no view"
|
||||
#return
|
||||
|
||||
oldView = None
|
||||
if self._connectedView is not None:
|
||||
oldView = self._connectedView()
|
||||
|
||||
if view is oldView:
|
||||
#print " already have view", view
|
||||
return
|
||||
|
||||
## disconnect from previous view
|
||||
if oldView is not None:
|
||||
for signal, slot in [('sigRangeChanged', self.viewRangeChanged),
|
||||
('sigDeviceRangeChanged', self.viewRangeChanged),
|
||||
('sigTransformChanged', self.viewTransformChanged),
|
||||
('sigDeviceTransformChanged', self.viewTransformChanged)]:
|
||||
try:
|
||||
getattr(oldView, signal).disconnect(slot)
|
||||
except (TypeError, AttributeError, RuntimeError):
|
||||
# TypeError and RuntimeError are from pyqt and pyside, respectively
|
||||
pass
|
||||
|
||||
self._connectedView = None
|
||||
|
||||
## connect to new view
|
||||
if view is not None:
|
||||
#print "connect:", self, view
|
||||
if hasattr(view, 'sigDeviceRangeChanged'):
|
||||
# connect signals from GraphicsView
|
||||
view.sigDeviceRangeChanged.connect(self.viewRangeChanged)
|
||||
view.sigDeviceTransformChanged.connect(self.viewTransformChanged)
|
||||
else:
|
||||
# connect signals from ViewBox
|
||||
view.sigRangeChanged.connect(self.viewRangeChanged)
|
||||
view.sigTransformChanged.connect(self.viewTransformChanged)
|
||||
self._connectedView = weakref.ref(view)
|
||||
self.viewRangeChanged()
|
||||
self.viewTransformChanged()
|
||||
|
||||
## inform children that their view might have changed
|
||||
self._replaceView(oldView)
|
||||
|
||||
self.viewChanged(view, oldView)
|
||||
|
||||
def viewChanged(self, view, oldView):
|
||||
"""Called when this item's view has changed
|
||||
(ie, the item has been added to or removed from a ViewBox)"""
|
||||
pass
|
||||
|
||||
def _replaceView(self, oldView, item=None):
|
||||
if item is None:
|
||||
item = self
|
||||
for child in item.childItems():
|
||||
if isinstance(child, GraphicsItem):
|
||||
if child.getViewBox() is oldView:
|
||||
child._updateView()
|
||||
#self._replaceView(oldView, child)
|
||||
else:
|
||||
self._replaceView(oldView, child)
|
||||
|
||||
|
||||
|
||||
def viewRangeChanged(self):
|
||||
"""
|
||||
Called whenever the view coordinates of the ViewBox containing this item have changed.
|
||||
"""
|
||||
pass
|
||||
|
||||
def viewTransformChanged(self):
|
||||
"""
|
||||
Called whenever the transformation matrix of the view has changed.
|
||||
(eg, the view range has changed or the view was resized)
|
||||
"""
|
||||
pass
|
||||
|
||||
#def prepareGeometryChange(self):
|
||||
#self._qtBaseClass.prepareGeometryChange(self)
|
||||
#self.informViewBoundsChanged()
|
||||
|
||||
def informViewBoundsChanged(self):
|
||||
"""
|
||||
Inform this item's container ViewBox that the bounds of this item have changed.
|
||||
This is used by ViewBox to react if auto-range is enabled.
|
||||
"""
|
||||
view = self.getViewBox()
|
||||
if view is not None and hasattr(view, 'implements') and view.implements('ViewBox'):
|
||||
view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
|
||||
|
||||
def childrenShape(self):
|
||||
"""Return the union of the shapes of all descendants of this item in local coordinates."""
|
||||
childs = self.allChildItems()
|
||||
shapes = [self.mapFromItem(c, c.shape()) for c in self.allChildItems()]
|
||||
return reduce(operator.add, shapes)
|
||||
|
||||
def allChildItems(self, root=None):
|
||||
"""Return list of the entire item tree descending from this item."""
|
||||
if root is None:
|
||||
root = self
|
||||
tree = []
|
||||
for ch in root.childItems():
|
||||
tree.append(ch)
|
||||
tree.extend(self.allChildItems(ch))
|
||||
return tree
|
||||
|
||||
|
||||
def setExportMode(self, export, opts=None):
|
||||
"""
|
||||
This method is called by exporters to inform items that they are being drawn for export
|
||||
with a specific set of options. Items access these via self._exportOptions.
|
||||
When exporting is complete, _exportOptions is set to False.
|
||||
"""
|
||||
if opts is None:
|
||||
opts = {}
|
||||
if export:
|
||||
self._exportOpts = opts
|
||||
#if 'antialias' not in opts:
|
||||
#self._exportOpts['antialias'] = True
|
||||
else:
|
||||
self._exportOpts = False
|
||||
|
||||
#def update(self):
|
||||
#self._qtBaseClass.update(self)
|
||||
#print "Update:", self
|
||||
|
||||
def getContextMenus(self, event):
|
||||
return [self.getMenu()] if hasattr(self, "getMenu") else []
|
171
pyqtgraph/graphicsItems/GraphicsLayout.py
Normal file
171
pyqtgraph/graphicsItems/GraphicsLayout.py
Normal file
|
@ -0,0 +1,171 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from .. import functions as fn
|
||||
from .GraphicsWidget import GraphicsWidget
|
||||
## Must be imported at the end to avoid cyclic-dependency hell:
|
||||
from .ViewBox import ViewBox
|
||||
from .PlotItem import PlotItem
|
||||
from .LabelItem import LabelItem
|
||||
|
||||
__all__ = ['GraphicsLayout']
|
||||
class GraphicsLayout(GraphicsWidget):
|
||||
"""
|
||||
Used for laying out GraphicsWidgets in a grid.
|
||||
This is usually created automatically as part of a :class:`GraphicsWindow <pyqtgraph.GraphicsWindow>` or :class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>`.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, parent=None, border=None):
|
||||
GraphicsWidget.__init__(self, parent)
|
||||
if border is True:
|
||||
border = (100,100,100)
|
||||
self.border = border
|
||||
self.layout = QtGui.QGraphicsGridLayout()
|
||||
self.setLayout(self.layout)
|
||||
self.items = {} ## item: [(row, col), (row, col), ...] lists all cells occupied by the item
|
||||
self.rows = {} ## row: {col1: item1, col2: item2, ...} maps cell location to item
|
||||
self.currentRow = 0
|
||||
self.currentCol = 0
|
||||
self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding))
|
||||
|
||||
#def resizeEvent(self, ev):
|
||||
#ret = GraphicsWidget.resizeEvent(self, ev)
|
||||
#print self.pos(), self.mapToDevice(self.rect().topLeft())
|
||||
#return ret
|
||||
|
||||
def setBorder(self, *args, **kwds):
|
||||
"""
|
||||
Set the pen used to draw border between cells.
|
||||
|
||||
See :func:`mkPen <pyqtgraph.mkPen>` for arguments.
|
||||
"""
|
||||
self.border = fn.mkPen(*args, **kwds)
|
||||
self.update()
|
||||
|
||||
def nextRow(self):
|
||||
"""Advance to next row for automatic item placement"""
|
||||
self.currentRow += 1
|
||||
self.currentCol = -1
|
||||
self.nextColumn()
|
||||
|
||||
def nextColumn(self):
|
||||
"""Advance to next available column
|
||||
(generally only for internal use--called by addItem)"""
|
||||
self.currentCol += 1
|
||||
while self.getItem(self.currentRow, self.currentCol) is not None:
|
||||
self.currentCol += 1
|
||||
|
||||
def nextCol(self, *args, **kargs):
|
||||
"""Alias of nextColumn"""
|
||||
return self.nextColumn(*args, **kargs)
|
||||
|
||||
def addPlot(self, row=None, col=None, rowspan=1, colspan=1, **kargs):
|
||||
"""
|
||||
Create a PlotItem and place it in the next available cell (or in the cell specified)
|
||||
All extra keyword arguments are passed to :func:`PlotItem.__init__ <pyqtgraph.PlotItem.__init__>`
|
||||
Returns the created item.
|
||||
"""
|
||||
plot = PlotItem(**kargs)
|
||||
self.addItem(plot, row, col, rowspan, colspan)
|
||||
return plot
|
||||
|
||||
def addViewBox(self, row=None, col=None, rowspan=1, colspan=1, **kargs):
|
||||
"""
|
||||
Create a ViewBox and place it in the next available cell (or in the cell specified)
|
||||
All extra keyword arguments are passed to :func:`ViewBox.__init__ <pyqtgraph.ViewBox.__init__>`
|
||||
Returns the created item.
|
||||
"""
|
||||
vb = ViewBox(**kargs)
|
||||
self.addItem(vb, row, col, rowspan, colspan)
|
||||
return vb
|
||||
|
||||
def addLabel(self, text=' ', row=None, col=None, rowspan=1, colspan=1, **kargs):
|
||||
"""
|
||||
Create a LabelItem with *text* and place it in the next available cell (or in the cell specified)
|
||||
All extra keyword arguments are passed to :func:`LabelItem.__init__ <pyqtgraph.LabelItem.__init__>`
|
||||
Returns the created item.
|
||||
|
||||
To create a vertical label, use *angle* = -90.
|
||||
"""
|
||||
text = LabelItem(text, **kargs)
|
||||
self.addItem(text, row, col, rowspan, colspan)
|
||||
return text
|
||||
|
||||
def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs):
|
||||
"""
|
||||
Create an empty GraphicsLayout and place it in the next available cell (or in the cell specified)
|
||||
All extra keyword arguments are passed to :func:`GraphicsLayout.__init__ <pyqtgraph.GraphicsLayout.__init__>`
|
||||
Returns the created item.
|
||||
"""
|
||||
layout = GraphicsLayout(**kargs)
|
||||
self.addItem(layout, row, col, rowspan, colspan)
|
||||
return layout
|
||||
|
||||
def addItem(self, item, row=None, col=None, rowspan=1, colspan=1):
|
||||
"""
|
||||
Add an item to the layout and place it in the next available cell (or in the cell specified).
|
||||
The item must be an instance of a QGraphicsWidget subclass.
|
||||
"""
|
||||
if row is None:
|
||||
row = self.currentRow
|
||||
if col is None:
|
||||
col = self.currentCol
|
||||
|
||||
self.items[item] = []
|
||||
for i in range(rowspan):
|
||||
for j in range(colspan):
|
||||
row2 = row + i
|
||||
col2 = col + j
|
||||
if row2 not in self.rows:
|
||||
self.rows[row2] = {}
|
||||
self.rows[row2][col2] = item
|
||||
self.items[item].append((row2, col2))
|
||||
|
||||
self.layout.addItem(item, row, col, rowspan, colspan)
|
||||
self.nextColumn()
|
||||
|
||||
def getItem(self, row, col):
|
||||
"""Return the item in (*row*, *col*). If the cell is empty, return None."""
|
||||
return self.rows.get(row, {}).get(col, None)
|
||||
|
||||
def boundingRect(self):
|
||||
return self.rect()
|
||||
|
||||
def paint(self, p, *args):
|
||||
if self.border is None:
|
||||
return
|
||||
p.setPen(fn.mkPen(self.border))
|
||||
for i in self.items:
|
||||
r = i.mapRectToParent(i.boundingRect())
|
||||
p.drawRect(r)
|
||||
|
||||
def itemIndex(self, item):
|
||||
for i in range(self.layout.count()):
|
||||
if self.layout.itemAt(i).graphicsItem() is item:
|
||||
return i
|
||||
raise Exception("Could not determine index of item " + str(item))
|
||||
|
||||
def removeItem(self, item):
|
||||
"""Remove *item* from the layout."""
|
||||
ind = self.itemIndex(item)
|
||||
self.layout.removeAt(ind)
|
||||
self.scene().removeItem(item)
|
||||
|
||||
for r,c in self.items[item]:
|
||||
del self.rows[r][c]
|
||||
del self.items[item]
|
||||
self.update()
|
||||
|
||||
def clear(self):
|
||||
items = []
|
||||
for i in list(self.items.keys()):
|
||||
self.removeItem(i)
|
||||
|
||||
def setContentsMargins(self, *args):
|
||||
# Wrap calls to layout. This should happen automatically, but there
|
||||
# seems to be a Qt bug:
|
||||
# http://stackoverflow.com/questions/27092164/margins-in-pyqtgraphs-graphicslayout
|
||||
self.layout.setContentsMargins(*args)
|
||||
|
||||
def setSpacing(self, *args):
|
||||
self.layout.setSpacing(*args)
|
||||
|
39
pyqtgraph/graphicsItems/GraphicsObject.py
Normal file
39
pyqtgraph/graphicsItems/GraphicsObject.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from ..Qt import QtGui, QtCore, USE_PYSIDE
|
||||
if not USE_PYSIDE:
|
||||
import sip
|
||||
from .GraphicsItem import GraphicsItem
|
||||
|
||||
__all__ = ['GraphicsObject']
|
||||
class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
|
||||
"""
|
||||
**Bases:** :class:`GraphicsItem <pyqtgraph.graphicsItems.GraphicsItem>`, :class:`QtGui.QGraphicsObject`
|
||||
|
||||
Extension of QGraphicsObject with some useful methods (provided by :class:`GraphicsItem <pyqtgraph.graphicsItems.GraphicsItem>`)
|
||||
"""
|
||||
_qtBaseClass = QtGui.QGraphicsObject
|
||||
def __init__(self, *args):
|
||||
self.__inform_view_on_changes = True
|
||||
QtGui.QGraphicsObject.__init__(self, *args)
|
||||
self.setFlag(self.ItemSendsGeometryChanges)
|
||||
GraphicsItem.__init__(self)
|
||||
|
||||
def itemChange(self, change, value):
|
||||
ret = QtGui.QGraphicsObject.itemChange(self, change, value)
|
||||
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
|
||||
self.parentChanged()
|
||||
try:
|
||||
inform_view_on_change = self.__inform_view_on_changes
|
||||
except AttributeError:
|
||||
# It's possible that the attribute was already collected when the itemChange happened
|
||||
# (if it was triggered during the gc of the object).
|
||||
pass
|
||||
else:
|
||||
if inform_view_on_change and change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
|
||||
self.informViewBoundsChanged()
|
||||
|
||||
## workaround for pyqt bug:
|
||||
## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html
|
||||
if not USE_PYSIDE and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem):
|
||||
ret = sip.cast(ret, QtGui.QGraphicsItem)
|
||||
|
||||
return ret
|
59
pyqtgraph/graphicsItems/GraphicsWidget.py
Normal file
59
pyqtgraph/graphicsItems/GraphicsWidget.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from ..GraphicsScene import GraphicsScene
|
||||
from .GraphicsItem import GraphicsItem
|
||||
|
||||
__all__ = ['GraphicsWidget']
|
||||
|
||||
class GraphicsWidget(GraphicsItem, QtGui.QGraphicsWidget):
|
||||
|
||||
_qtBaseClass = QtGui.QGraphicsWidget
|
||||
def __init__(self, *args, **kargs):
|
||||
"""
|
||||
**Bases:** :class:`GraphicsItem <pyqtgraph.GraphicsItem>`, :class:`QtGui.QGraphicsWidget`
|
||||
|
||||
Extends QGraphicsWidget with several helpful methods and workarounds for PyQt bugs.
|
||||
Most of the extra functionality is inherited from :class:`GraphicsItem <pyqtgraph.GraphicsItem>`.
|
||||
"""
|
||||
QtGui.QGraphicsWidget.__init__(self, *args, **kargs)
|
||||
GraphicsItem.__init__(self)
|
||||
|
||||
## done by GraphicsItem init
|
||||
#GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items()
|
||||
|
||||
# Removed due to https://bugreports.qt-project.org/browse/PYSIDE-86
|
||||
#def itemChange(self, change, value):
|
||||
## BEWARE: Calling QGraphicsWidget.itemChange can lead to crashing!
|
||||
##ret = QtGui.QGraphicsWidget.itemChange(self, change, value) ## segv occurs here
|
||||
## The default behavior is just to return the value argument, so we'll do that
|
||||
## without calling the original method.
|
||||
#ret = value
|
||||
#if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
|
||||
#self._updateView()
|
||||
#return ret
|
||||
|
||||
def setFixedHeight(self, h):
|
||||
self.setMaximumHeight(h)
|
||||
self.setMinimumHeight(h)
|
||||
|
||||
def setFixedWidth(self, h):
|
||||
self.setMaximumWidth(h)
|
||||
self.setMinimumWidth(h)
|
||||
|
||||
def height(self):
|
||||
return self.geometry().height()
|
||||
|
||||
def width(self):
|
||||
return self.geometry().width()
|
||||
|
||||
def boundingRect(self):
|
||||
br = self.mapRectFromParent(self.geometry()).normalized()
|
||||
#print "bounds:", br
|
||||
return br
|
||||
|
||||
def shape(self): ## No idea why this is necessary, but rotated items do not receive clicks otherwise.
|
||||
p = QtGui.QPainterPath()
|
||||
p.addRect(self.boundingRect())
|
||||
#print "shape:", p.boundingRect()
|
||||
return p
|
||||
|
||||
|
110
pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py
Normal file
110
pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from ..Point import Point
|
||||
|
||||
|
||||
class GraphicsWidgetAnchor(object):
|
||||
"""
|
||||
Class used to allow GraphicsWidgets to anchor to a specific position on their
|
||||
parent. The item will be automatically repositioned if the parent is resized.
|
||||
This is used, for example, to anchor a LegendItem to a corner of its parent
|
||||
PlotItem.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.__parent = None
|
||||
self.__parentAnchor = None
|
||||
self.__itemAnchor = None
|
||||
self.__offset = (0,0)
|
||||
if hasattr(self, 'geometryChanged'):
|
||||
self.geometryChanged.connect(self.__geometryChanged)
|
||||
|
||||
def anchor(self, itemPos, parentPos, offset=(0,0)):
|
||||
"""
|
||||
Anchors the item at its local itemPos to the item's parent at parentPos.
|
||||
Both positions are expressed in values relative to the size of the item or parent;
|
||||
a value of 0 indicates left or top edge, while 1 indicates right or bottom edge.
|
||||
|
||||
Optionally, offset may be specified to introduce an absolute offset.
|
||||
|
||||
Example: anchor a box such that its upper-right corner is fixed 10px left
|
||||
and 10px down from its parent's upper-right corner::
|
||||
|
||||
box.anchor(itemPos=(1,0), parentPos=(1,0), offset=(-10,10))
|
||||
"""
|
||||
parent = self.parentItem()
|
||||
if parent is None:
|
||||
raise Exception("Cannot anchor; parent is not set.")
|
||||
|
||||
if self.__parent is not parent:
|
||||
if self.__parent is not None:
|
||||
self.__parent.geometryChanged.disconnect(self.__geometryChanged)
|
||||
|
||||
self.__parent = parent
|
||||
parent.geometryChanged.connect(self.__geometryChanged)
|
||||
|
||||
self.__itemAnchor = itemPos
|
||||
self.__parentAnchor = parentPos
|
||||
self.__offset = offset
|
||||
self.__geometryChanged()
|
||||
|
||||
|
||||
def autoAnchor(self, pos, relative=True):
|
||||
"""
|
||||
Set the position of this item relative to its parent by automatically
|
||||
choosing appropriate anchor settings.
|
||||
|
||||
If relative is True, one corner of the item will be anchored to
|
||||
the appropriate location on the parent with no offset. The anchored
|
||||
corner will be whichever is closest to the parent's boundary.
|
||||
|
||||
If relative is False, one corner of the item will be anchored to the same
|
||||
corner of the parent, with an absolute offset to achieve the correct
|
||||
position.
|
||||
"""
|
||||
pos = Point(pos)
|
||||
br = self.mapRectToParent(self.boundingRect()).translated(pos - self.pos())
|
||||
pbr = self.parentItem().boundingRect()
|
||||
anchorPos = [0,0]
|
||||
parentPos = Point()
|
||||
itemPos = Point()
|
||||
if abs(br.left() - pbr.left()) < abs(br.right() - pbr.right()):
|
||||
anchorPos[0] = 0
|
||||
parentPos[0] = pbr.left()
|
||||
itemPos[0] = br.left()
|
||||
else:
|
||||
anchorPos[0] = 1
|
||||
parentPos[0] = pbr.right()
|
||||
itemPos[0] = br.right()
|
||||
|
||||
if abs(br.top() - pbr.top()) < abs(br.bottom() - pbr.bottom()):
|
||||
anchorPos[1] = 0
|
||||
parentPos[1] = pbr.top()
|
||||
itemPos[1] = br.top()
|
||||
else:
|
||||
anchorPos[1] = 1
|
||||
parentPos[1] = pbr.bottom()
|
||||
itemPos[1] = br.bottom()
|
||||
|
||||
if relative:
|
||||
relPos = [(itemPos[0]-pbr.left()) / pbr.width(), (itemPos[1]-pbr.top()) / pbr.height()]
|
||||
self.anchor(anchorPos, relPos)
|
||||
else:
|
||||
offset = itemPos - parentPos
|
||||
self.anchor(anchorPos, anchorPos, offset)
|
||||
|
||||
def __geometryChanged(self):
|
||||
if self.__parent is None:
|
||||
return
|
||||
if self.__itemAnchor is None:
|
||||
return
|
||||
|
||||
o = self.mapToParent(Point(0,0))
|
||||
a = self.boundingRect().bottomRight() * Point(self.__itemAnchor)
|
||||
a = self.mapToParent(a)
|
||||
p = self.__parent.boundingRect().bottomRight() * Point(self.__parentAnchor)
|
||||
off = Point(self.__offset)
|
||||
pos = p + (o-a) + off
|
||||
self.setPos(pos)
|
||||
|
||||
|
120
pyqtgraph/graphicsItems/GridItem.py
Normal file
120
pyqtgraph/graphicsItems/GridItem.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
from ..Qt import QtGui, QtCore
|
||||
from .UIGraphicsItem import *
|
||||
import numpy as np
|
||||
from ..Point import Point
|
||||
from .. import functions as fn
|
||||
|
||||
__all__ = ['GridItem']
|
||||
class GridItem(UIGraphicsItem):
|
||||
"""
|
||||
**Bases:** :class:`UIGraphicsItem <pyqtgraph.UIGraphicsItem>`
|
||||
|
||||
Displays a rectangular grid of lines indicating major divisions within a coordinate system.
|
||||
Automatically determines what divisions to use.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
UIGraphicsItem.__init__(self)
|
||||
#QtGui.QGraphicsItem.__init__(self, *args)
|
||||
#self.setFlag(QtGui.QGraphicsItem.ItemClipsToShape)
|
||||
#self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
|
||||
|
||||
self.picture = None
|
||||
|
||||
|
||||
def viewRangeChanged(self):
|
||||
UIGraphicsItem.viewRangeChanged(self)
|
||||
self.picture = None
|
||||
#UIGraphicsItem.viewRangeChanged(self)
|
||||
#self.update()
|
||||
|
||||
def paint(self, p, opt, widget):
|
||||
#p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100)))
|
||||
#p.drawRect(self.boundingRect())
|
||||
#UIGraphicsItem.paint(self, p, opt, widget)
|
||||
### draw picture
|
||||
if self.picture is None:
|
||||
#print "no pic, draw.."
|
||||
self.generatePicture()
|
||||
p.drawPicture(QtCore.QPointF(0, 0), self.picture)
|
||||
#p.setPen(QtGui.QPen(QtGui.QColor(255,0,0)))
|
||||
#p.drawLine(0, -100, 0, 100)
|
||||
#p.drawLine(-100, 0, 100, 0)
|
||||
#print "drawing Grid."
|
||||
|
||||
|
||||
def generatePicture(self):
|
||||
self.picture = QtGui.QPicture()
|
||||
p = QtGui.QPainter()
|
||||
p.begin(self.picture)
|
||||
|
||||
dt = fn.invertQTransform(self.viewTransform())
|
||||
vr = self.getViewWidget().rect()
|
||||
unit = self.pixelWidth(), self.pixelHeight()
|
||||
dim = [vr.width(), vr.height()]
|
||||
lvr = self.boundingRect()
|
||||
ul = np.array([lvr.left(), lvr.top()])
|
||||
br = np.array([lvr.right(), lvr.bottom()])
|
||||
|
||||
texts = []
|
||||
|
||||
if ul[1] > br[1]:
|
||||
x = ul[1]
|
||||
ul[1] = br[1]
|
||||
br[1] = x
|
||||
for i in [2,1,0]: ## Draw three different scales of grid
|
||||
dist = br-ul
|
||||
nlTarget = 10.**i
|
||||
d = 10. ** np.floor(np.log10(abs(dist/nlTarget))+0.5)
|
||||
ul1 = np.floor(ul / d) * d
|
||||
br1 = np.ceil(br / d) * d
|
||||
dist = br1-ul1
|
||||
nl = (dist / d) + 0.5
|
||||
#print "level", i
|
||||
#print " dim", dim
|
||||
#print " dist", dist
|
||||
#print " d", d
|
||||
#print " nl", nl
|
||||
for ax in range(0,2): ## Draw grid for both axes
|
||||
ppl = dim[ax] / nl[ax]
|
||||
c = np.clip(3.*(ppl-3), 0., 30.)
|
||||
linePen = QtGui.QPen(QtGui.QColor(255, 255, 255, c))
|
||||
textPen = QtGui.QPen(QtGui.QColor(255, 255, 255, c*2))
|
||||
#linePen.setCosmetic(True)
|
||||
#linePen.setWidth(1)
|
||||
bx = (ax+1) % 2
|
||||
for x in range(0, int(nl[ax])):
|
||||
linePen.setCosmetic(False)
|
||||
if ax == 0:
|
||||
linePen.setWidthF(self.pixelWidth())
|
||||
#print "ax 0 height", self.pixelHeight()
|
||||
else:
|
||||
linePen.setWidthF(self.pixelHeight())
|
||||
#print "ax 1 width", self.pixelWidth()
|
||||
p.setPen(linePen)
|
||||
p1 = np.array([0.,0.])
|
||||
p2 = np.array([0.,0.])
|
||||
p1[ax] = ul1[ax] + x * d[ax]
|
||||
p2[ax] = p1[ax]
|
||||
p1[bx] = ul[bx]
|
||||
p2[bx] = br[bx]
|
||||
## don't draw lines that are out of bounds.
|
||||
if p1[ax] < min(ul[ax], br[ax]) or p1[ax] > max(ul[ax], br[ax]):
|
||||
continue
|
||||
p.drawLine(QtCore.QPointF(p1[0], p1[1]), QtCore.QPointF(p2[0], p2[1]))
|
||||
if i < 2:
|
||||
p.setPen(textPen)
|
||||
if ax == 0:
|
||||
x = p1[0] + unit[0]
|
||||
y = ul[1] + unit[1] * 8.
|
||||
else:
|
||||
x = ul[0] + unit[0]*3
|
||||
y = p1[1] + unit[1]
|
||||
texts.append((QtCore.QPointF(x, y), "%g"%p1[ax]))
|
||||
tr = self.deviceTransform()
|
||||
#tr.scale(1.5, 1.5)
|
||||
p.setWorldTransform(fn.invertQTransform(tr))
|
||||
for t in texts:
|
||||
x = tr.map(t[0]) + Point(0.5, 0.5)
|
||||
p.drawText(x, t[1])
|
||||
p.end()
|
215
pyqtgraph/graphicsItems/HistogramLUTItem.py
Normal file
215
pyqtgraph/graphicsItems/HistogramLUTItem.py
Normal file
|
@ -0,0 +1,215 @@
|
|||
"""
|
||||
GraphicsWidget displaying an image histogram along with gradient editor. Can be used to adjust the appearance of images.
|
||||
"""
|
||||
|
||||
|
||||
from ..Qt import QtGui, QtCore
|
||||
from .. import functions as fn
|
||||
from .GraphicsWidget import GraphicsWidget
|
||||
from .ViewBox import *
|
||||
from .GradientEditorItem import *
|
||||
from .LinearRegionItem import *
|
||||
from .PlotDataItem import *
|
||||
from .AxisItem import *
|
||||
from .GridItem import *
|
||||
from ..Point import Point
|
||||
from .. import functions as fn
|
||||
import numpy as np
|
||||
from .. import debug as debug
|
||||
|
||||
import weakref
|
||||
|
||||
__all__ = ['HistogramLUTItem']
|
||||
|
||||
|
||||
class HistogramLUTItem(GraphicsWidget):
|
||||
"""
|
||||
This is a graphicsWidget which provides controls for adjusting the display of an image.
|
||||
Includes:
|
||||
|
||||
- Image histogram
|
||||
- Movable region over histogram to select black/white levels
|
||||
- Gradient editor to define color lookup table for single-channel images
|
||||
"""
|
||||
|
||||
sigLookupTableChanged = QtCore.Signal(object)
|
||||
sigLevelsChanged = QtCore.Signal(object)
|
||||
sigLevelChangeFinished = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, image=None, fillHistogram=True):
|
||||
"""
|
||||
If *image* (ImageItem) is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance.
|
||||
By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False.
|
||||
"""
|
||||
GraphicsWidget.__init__(self)
|
||||
self.lut = None
|
||||
self.imageItem = lambda: None # fake a dead weakref
|
||||
|
||||
self.layout = QtGui.QGraphicsGridLayout()
|
||||
self.setLayout(self.layout)
|
||||
self.layout.setContentsMargins(1,1,1,1)
|
||||
self.layout.setSpacing(0)
|
||||
self.vb = ViewBox(parent=self)
|
||||
self.vb.setMaximumWidth(152)
|
||||
self.vb.setMinimumWidth(45)
|
||||
self.vb.setMouseEnabled(x=False, y=True)
|
||||
self.gradient = GradientEditorItem()
|
||||
self.gradient.setOrientation('right')
|
||||
self.gradient.loadPreset('grey')
|
||||
self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal)
|
||||
self.region.setZValue(1000)
|
||||
self.vb.addItem(self.region)
|
||||
self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self)
|
||||
self.layout.addItem(self.axis, 0, 0)
|
||||
self.layout.addItem(self.vb, 0, 1)
|
||||
self.layout.addItem(self.gradient, 0, 2)
|
||||
self.range = None
|
||||
self.gradient.setFlag(self.gradient.ItemStacksBehindParent)
|
||||
self.vb.setFlag(self.gradient.ItemStacksBehindParent)
|
||||
|
||||
#self.grid = GridItem()
|
||||
#self.vb.addItem(self.grid)
|
||||
|
||||
self.gradient.sigGradientChanged.connect(self.gradientChanged)
|
||||
self.region.sigRegionChanged.connect(self.regionChanging)
|
||||
self.region.sigRegionChangeFinished.connect(self.regionChanged)
|
||||
self.vb.sigRangeChanged.connect(self.viewRangeChanged)
|
||||
self.plot = PlotDataItem()
|
||||
self.plot.rotate(90)
|
||||
self.fillHistogram(fillHistogram)
|
||||
|
||||
self.vb.addItem(self.plot)
|
||||
self.autoHistogramRange()
|
||||
|
||||
if image is not None:
|
||||
self.setImageItem(image)
|
||||
#self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
|
||||
|
||||
def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)):
|
||||
if fill:
|
||||
self.plot.setFillLevel(level)
|
||||
self.plot.setFillBrush(color)
|
||||
else:
|
||||
self.plot.setFillLevel(None)
|
||||
|
||||
#def sizeHint(self, *args):
|
||||
#return QtCore.QSizeF(115, 200)
|
||||
|
||||
def paint(self, p, *args):
|
||||
pen = self.region.lines[0].pen
|
||||
rgn = self.getLevels()
|
||||
p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0]))
|
||||
p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1]))
|
||||
gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect())
|
||||
for pen in [fn.mkPen('k', width=3), pen]:
|
||||
p.setPen(pen)
|
||||
p.drawLine(p1, gradRect.bottomLeft())
|
||||
p.drawLine(p2, gradRect.topLeft())
|
||||
p.drawLine(gradRect.topLeft(), gradRect.topRight())
|
||||
p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight())
|
||||
#p.drawRect(self.boundingRect())
|
||||
|
||||
|
||||
def setHistogramRange(self, mn, mx, padding=0.1):
|
||||
"""Set the Y range on the histogram plot. This disables auto-scaling."""
|
||||
self.vb.enableAutoRange(self.vb.YAxis, False)
|
||||
self.vb.setYRange(mn, mx, padding)
|
||||
|
||||
#d = mx-mn
|
||||
#mn -= d*padding
|
||||
#mx += d*padding
|
||||
#self.range = [mn,mx]
|
||||
#self.updateRange()
|
||||
#self.vb.setMouseEnabled(False, True)
|
||||
#self.region.setBounds([mn,mx])
|
||||
|
||||
def autoHistogramRange(self):
|
||||
"""Enable auto-scaling on the histogram plot."""
|
||||
self.vb.enableAutoRange(self.vb.XYAxes)
|
||||
#self.range = None
|
||||
#self.updateRange()
|
||||
#self.vb.setMouseEnabled(False, False)
|
||||
|
||||
#def updateRange(self):
|
||||
#self.vb.autoRange()
|
||||
#if self.range is not None:
|
||||
#self.vb.setYRange(*self.range)
|
||||
#vr = self.vb.viewRect()
|
||||
|
||||
#self.region.setBounds([vr.top(), vr.bottom()])
|
||||
|
||||
def setImageItem(self, img):
|
||||
"""Set an ImageItem to have its levels and LUT automatically controlled
|
||||
by this HistogramLUTItem.
|
||||
"""
|
||||
self.imageItem = weakref.ref(img)
|
||||
img.sigImageChanged.connect(self.imageChanged)
|
||||
img.setLookupTable(self.getLookupTable) ## send function pointer, not the result
|
||||
#self.gradientChanged()
|
||||
self.regionChanged()
|
||||
self.imageChanged(autoLevel=True)
|
||||
#self.vb.autoRange()
|
||||
|
||||
def viewRangeChanged(self):
|
||||
self.update()
|
||||
|
||||
def gradientChanged(self):
|
||||
if self.imageItem() is not None:
|
||||
if self.gradient.isLookupTrivial():
|
||||
self.imageItem().setLookupTable(None) #lambda x: x.astype(np.uint8))
|
||||
else:
|
||||
self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result
|
||||
|
||||
self.lut = None
|
||||
#if self.imageItem is not None:
|
||||
#self.imageItem.setLookupTable(self.gradient.getLookupTable(512))
|
||||
self.sigLookupTableChanged.emit(self)
|
||||
|
||||
def getLookupTable(self, img=None, n=None, alpha=None):
|
||||
"""Return a lookup table from the color gradient defined by this
|
||||
HistogramLUTItem.
|
||||
"""
|
||||
if n is None:
|
||||
if img.dtype == np.uint8:
|
||||
n = 256
|
||||
else:
|
||||
n = 512
|
||||
if self.lut is None:
|
||||
self.lut = self.gradient.getLookupTable(n, alpha=alpha)
|
||||
return self.lut
|
||||
|
||||
def regionChanged(self):
|
||||
#if self.imageItem is not None:
|
||||
#self.imageItem.setLevels(self.region.getRegion())
|
||||
self.sigLevelChangeFinished.emit(self)
|
||||
#self.update()
|
||||
|
||||
def regionChanging(self):
|
||||
if self.imageItem() is not None:
|
||||
self.imageItem().setLevels(self.region.getRegion())
|
||||
self.sigLevelsChanged.emit(self)
|
||||
self.update()
|
||||
|
||||
def imageChanged(self, autoLevel=False, autoRange=False):
|
||||
profiler = debug.Profiler()
|
||||
h = self.imageItem().getHistogram()
|
||||
profiler('get histogram')
|
||||
if h[0] is None:
|
||||
return
|
||||
self.plot.setData(*h)
|
||||
profiler('set plot')
|
||||
if autoLevel:
|
||||
mn = h[0][0]
|
||||
mx = h[0][-1]
|
||||
self.region.setRegion([mn, mx])
|
||||
profiler('set region')
|
||||
|
||||
def getLevels(self):
|
||||
"""Return the min and max levels.
|
||||
"""
|
||||
return self.region.getRegion()
|
||||
|
||||
def setLevels(self, mn, mx):
|
||||
"""Set the min and max levels.
|
||||
"""
|
||||
self.region.setRegion([mn, mx])
|
528
pyqtgraph/graphicsItems/ImageItem.py
Normal file
528
pyqtgraph/graphicsItems/ImageItem.py
Normal file
|
@ -0,0 +1,528 @@
|
|||
from __future__ import division
|
||||
|
||||
from ..Qt import QtGui, QtCore
|
||||
import numpy as np
|
||||
import collections
|
||||
from .. import functions as fn
|
||||
from .. import debug as debug
|
||||
from .GraphicsObject import GraphicsObject
|
||||
from ..Point import Point
|
||||
|
||||
__all__ = ['ImageItem']
|
||||
|
||||
|
||||
class ImageItem(GraphicsObject):
|
||||
"""
|
||||
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
|
||||
|
||||
GraphicsObject displaying an image. Optimized for rapid update (ie video display).
|
||||
This item displays either a 2D numpy array (height, width) or
|
||||
a 3D array (height, width, RGBa). This array is optionally scaled (see
|
||||
:func:`setLevels <pyqtgraph.ImageItem.setLevels>`) and/or colored
|
||||
with a lookup table (see :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`)
|
||||
before being displayed.
|
||||
|
||||
ImageItem is frequently used in conjunction with
|
||||
:class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` or
|
||||
:class:`HistogramLUTWidget <pyqtgraph.HistogramLUTWidget>` to provide a GUI
|
||||
for controlling the levels and lookup table used to display the image.
|
||||
"""
|
||||
|
||||
|
||||
sigImageChanged = QtCore.Signal()
|
||||
sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu
|
||||
|
||||
def __init__(self, image=None, **kargs):
|
||||
"""
|
||||
See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments.
|
||||
"""
|
||||
GraphicsObject.__init__(self)
|
||||
self.menu = None
|
||||
self.image = None ## original image data
|
||||
self.qimage = None ## rendered image for display
|
||||
|
||||
self.paintMode = None
|
||||
|
||||
self.levels = None ## [min, max] or [[redMin, redMax], ...]
|
||||
self.lut = None
|
||||
self.autoDownsample = False
|
||||
|
||||
self.drawKernel = None
|
||||
self.border = None
|
||||
self.removable = False
|
||||
|
||||
if image is not None:
|
||||
self.setImage(image, **kargs)
|
||||
else:
|
||||
self.setOpts(**kargs)
|
||||
|
||||
def setCompositionMode(self, mode):
|
||||
"""Change the composition mode of the item (see QPainter::CompositionMode
|
||||
in the Qt documentation). This is useful when overlaying multiple ImageItems.
|
||||
|
||||
============================================ ============================================================
|
||||
**Most common arguments:**
|
||||
QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it
|
||||
is opaque. Otherwise, it uses the alpha channel to blend
|
||||
the image with the background.
|
||||
QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to
|
||||
reflect the lightness or darkness of the background.
|
||||
QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels
|
||||
are added together.
|
||||
QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background.
|
||||
============================================ ============================================================
|
||||
"""
|
||||
self.paintMode = mode
|
||||
self.update()
|
||||
|
||||
## use setOpacity instead.
|
||||
#def setAlpha(self, alpha):
|
||||
#self.setOpacity(alpha)
|
||||
#self.updateImage()
|
||||
|
||||
def setBorder(self, b):
|
||||
self.border = fn.mkPen(b)
|
||||
self.update()
|
||||
|
||||
def width(self):
|
||||
if self.image is None:
|
||||
return None
|
||||
return self.image.shape[0]
|
||||
|
||||
def height(self):
|
||||
if self.image is None:
|
||||
return None
|
||||
return self.image.shape[1]
|
||||
|
||||
def boundingRect(self):
|
||||
if self.image is None:
|
||||
return QtCore.QRectF(0., 0., 0., 0.)
|
||||
return QtCore.QRectF(0., 0., float(self.width()), float(self.height()))
|
||||
|
||||
#def setClipLevel(self, level=None):
|
||||
#self.clipLevel = level
|
||||
#self.updateImage()
|
||||
|
||||
#def paint(self, p, opt, widget):
|
||||
#pass
|
||||
#if self.pixmap is not None:
|
||||
#p.drawPixmap(0, 0, self.pixmap)
|
||||
#print "paint"
|
||||
|
||||
def setLevels(self, levels, update=True):
|
||||
"""
|
||||
Set image scaling levels. Can be one of:
|
||||
|
||||
* [blackLevel, whiteLevel]
|
||||
* [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]]
|
||||
|
||||
Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>`
|
||||
for more details on how levels are applied.
|
||||
"""
|
||||
self.levels = levels
|
||||
if update:
|
||||
self.updateImage()
|
||||
|
||||
def getLevels(self):
|
||||
return self.levels
|
||||
#return self.whiteLevel, self.blackLevel
|
||||
|
||||
def setLookupTable(self, lut, update=True):
|
||||
"""
|
||||
Set the lookup table (numpy array) to use for this image. (see
|
||||
:func:`makeARGB <pyqtgraph.makeARGB>` for more information on how this is used).
|
||||
Optionally, lut can be a callable that accepts the current image as an
|
||||
argument and returns the lookup table to use.
|
||||
|
||||
Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>`
|
||||
or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`.
|
||||
"""
|
||||
self.lut = lut
|
||||
if update:
|
||||
self.updateImage()
|
||||
|
||||
def setAutoDownsample(self, ads):
|
||||
"""
|
||||
Set the automatic downsampling mode for this ImageItem.
|
||||
|
||||
Added in version 0.9.9
|
||||
"""
|
||||
self.autoDownsample = ads
|
||||
self.qimage = None
|
||||
self.update()
|
||||
|
||||
def setOpts(self, update=True, **kargs):
|
||||
|
||||
if 'lut' in kargs:
|
||||
self.setLookupTable(kargs['lut'], update=update)
|
||||
if 'levels' in kargs:
|
||||
self.setLevels(kargs['levels'], update=update)
|
||||
#if 'clipLevel' in kargs:
|
||||
#self.setClipLevel(kargs['clipLevel'])
|
||||
if 'opacity' in kargs:
|
||||
self.setOpacity(kargs['opacity'])
|
||||
if 'compositionMode' in kargs:
|
||||
self.setCompositionMode(kargs['compositionMode'])
|
||||
if 'border' in kargs:
|
||||
self.setBorder(kargs['border'])
|
||||
if 'removable' in kargs:
|
||||
self.removable = kargs['removable']
|
||||
self.menu = None
|
||||
if 'autoDownsample' in kargs:
|
||||
self.setAutoDownsample(kargs['autoDownsample'])
|
||||
if update:
|
||||
self.update()
|
||||
|
||||
def setRect(self, rect):
|
||||
"""Scale and translate the image to fit within rect (must be a QRect or QRectF)."""
|
||||
self.resetTransform()
|
||||
self.translate(rect.left(), rect.top())
|
||||
self.scale(rect.width() / self.width(), rect.height() / self.height())
|
||||
|
||||
def clear(self):
|
||||
self.image = None
|
||||
self.prepareGeometryChange()
|
||||
self.informViewBoundsChanged()
|
||||
self.update()
|
||||
|
||||
def setImage(self, image=None, autoLevels=None, **kargs):
|
||||
"""
|
||||
Update the image displayed by this item. For more information on how the image
|
||||
is processed before displaying, see :func:`makeARGB <pyqtgraph.makeARGB>`
|
||||
|
||||
================= =========================================================================
|
||||
**Arguments:**
|
||||
image (numpy array) Specifies the image data. May be 2D (width, height) or
|
||||
3D (width, height, RGBa). The array dtype must be integer or floating
|
||||
point of any bit depth. For 3D arrays, the third dimension must
|
||||
be of length 3 (RGB) or 4 (RGBA).
|
||||
autoLevels (bool) If True, this forces the image to automatically select
|
||||
levels based on the maximum and minimum values in the data.
|
||||
By default, this argument is true unless the levels argument is
|
||||
given.
|
||||
lut (numpy array) The color lookup table to use when displaying the image.
|
||||
See :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`.
|
||||
levels (min, max) The minimum and maximum values to use when rescaling the image
|
||||
data. By default, this will be set to the minimum and maximum values
|
||||
in the image. If the image array has dtype uint8, no rescaling is necessary.
|
||||
opacity (float 0.0-1.0)
|
||||
compositionMode see :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>`
|
||||
border Sets the pen used when drawing the image border. Default is None.
|
||||
autoDownsample (bool) If True, the image is automatically downsampled to match the
|
||||
screen resolution. This improves performance for large images and
|
||||
reduces aliasing.
|
||||
================= =========================================================================
|
||||
"""
|
||||
profile = debug.Profiler()
|
||||
|
||||
gotNewData = False
|
||||
if image is None:
|
||||
if self.image is None:
|
||||
return
|
||||
else:
|
||||
gotNewData = True
|
||||
shapeChanged = (self.image is None or image.shape != self.image.shape)
|
||||
self.image = image.view(np.ndarray)
|
||||
if self.image.shape[0] > 2**15-1 or self.image.shape[1] > 2**15-1:
|
||||
if 'autoDownsample' not in kargs:
|
||||
kargs['autoDownsample'] = True
|
||||
if shapeChanged:
|
||||
self.prepareGeometryChange()
|
||||
self.informViewBoundsChanged()
|
||||
|
||||
profile()
|
||||
|
||||
if autoLevels is None:
|
||||
if 'levels' in kargs:
|
||||
autoLevels = False
|
||||
else:
|
||||
autoLevels = True
|
||||
if autoLevels:
|
||||
img = self.image
|
||||
while img.size > 2**16:
|
||||
img = img[::2, ::2]
|
||||
mn, mx = img.min(), img.max()
|
||||
if mn == mx:
|
||||
mn = 0
|
||||
mx = 255
|
||||
kargs['levels'] = [mn,mx]
|
||||
|
||||
profile()
|
||||
|
||||
self.setOpts(update=False, **kargs)
|
||||
|
||||
profile()
|
||||
|
||||
self.qimage = None
|
||||
self.update()
|
||||
|
||||
profile()
|
||||
|
||||
if gotNewData:
|
||||
self.sigImageChanged.emit()
|
||||
|
||||
|
||||
def updateImage(self, *args, **kargs):
|
||||
## used for re-rendering qimage from self.image.
|
||||
|
||||
## can we make any assumptions here that speed things up?
|
||||
## dtype, range, size are all the same?
|
||||
defaults = {
|
||||
'autoLevels': False,
|
||||
}
|
||||
defaults.update(kargs)
|
||||
return self.setImage(*args, **defaults)
|
||||
|
||||
def render(self):
|
||||
# Convert data to QImage for display.
|
||||
|
||||
profile = debug.Profiler()
|
||||
if self.image is None or self.image.size == 0:
|
||||
return
|
||||
if isinstance(self.lut, collections.Callable):
|
||||
lut = self.lut(self.image)
|
||||
else:
|
||||
lut = self.lut
|
||||
|
||||
if self.autoDownsample:
|
||||
# reduce dimensions of image based on screen resolution
|
||||
o = self.mapToDevice(QtCore.QPointF(0,0))
|
||||
x = self.mapToDevice(QtCore.QPointF(1,0))
|
||||
y = self.mapToDevice(QtCore.QPointF(0,1))
|
||||
w = Point(x-o).length()
|
||||
h = Point(y-o).length()
|
||||
xds = max(1, int(1/w))
|
||||
yds = max(1, int(1/h))
|
||||
image = fn.downsample(self.image, xds, axis=0)
|
||||
image = fn.downsample(image, yds, axis=1)
|
||||
else:
|
||||
image = self.image
|
||||
|
||||
argb, alpha = fn.makeARGB(image.transpose((1, 0, 2)[:image.ndim]), lut=lut, levels=self.levels)
|
||||
self.qimage = fn.makeQImage(argb, alpha, transpose=False)
|
||||
|
||||
def paint(self, p, *args):
|
||||
profile = debug.Profiler()
|
||||
if self.image is None:
|
||||
return
|
||||
if self.qimage is None:
|
||||
self.render()
|
||||
if self.qimage is None:
|
||||
return
|
||||
profile('render QImage')
|
||||
if self.paintMode is not None:
|
||||
p.setCompositionMode(self.paintMode)
|
||||
profile('set comp mode')
|
||||
|
||||
p.drawImage(QtCore.QRectF(0,0,self.image.shape[0],self.image.shape[1]), self.qimage)
|
||||
profile('p.drawImage')
|
||||
if self.border is not None:
|
||||
p.setPen(self.border)
|
||||
p.drawRect(self.boundingRect())
|
||||
|
||||
def save(self, fileName, *args):
|
||||
"""Save this image to file. Note that this saves the visible image (after scale/color changes), not the original data."""
|
||||
if self.qimage is None:
|
||||
self.render()
|
||||
self.qimage.save(fileName, *args)
|
||||
|
||||
def getHistogram(self, bins='auto', step='auto', targetImageSize=200, targetHistogramSize=500, **kwds):
|
||||
"""Returns x and y arrays containing the histogram values for the current image.
|
||||
For an explanation of the return format, see numpy.histogram().
|
||||
|
||||
The *step* argument causes pixels to be skipped when computing the histogram to save time.
|
||||
If *step* is 'auto', then a step is chosen such that the analyzed data has
|
||||
dimensions roughly *targetImageSize* for each axis.
|
||||
|
||||
The *bins* argument and any extra keyword arguments are passed to
|
||||
np.histogram(). If *bins* is 'auto', then a bin number is automatically
|
||||
chosen based on the image characteristics:
|
||||
|
||||
* Integer images will have approximately *targetHistogramSize* bins,
|
||||
with each bin having an integer width.
|
||||
* All other types will have *targetHistogramSize* bins.
|
||||
|
||||
This method is also used when automatically computing levels.
|
||||
"""
|
||||
if self.image is None:
|
||||
return None,None
|
||||
if step == 'auto':
|
||||
step = (np.ceil(self.image.shape[0] / targetImageSize),
|
||||
np.ceil(self.image.shape[1] / targetImageSize))
|
||||
if np.isscalar(step):
|
||||
step = (step, step)
|
||||
stepData = self.image[::step[0], ::step[1]]
|
||||
|
||||
if bins == 'auto':
|
||||
if stepData.dtype.kind in "ui":
|
||||
mn = stepData.min()
|
||||
mx = stepData.max()
|
||||
step = np.ceil((mx-mn) / 500.)
|
||||
bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
|
||||
if len(bins) == 0:
|
||||
bins = [mn, mx]
|
||||
else:
|
||||
bins = 500
|
||||
|
||||
kwds['bins'] = bins
|
||||
hist = np.histogram(stepData, **kwds)
|
||||
|
||||
return hist[1][:-1], hist[0]
|
||||
|
||||
def setPxMode(self, b):
|
||||
"""
|
||||
Set whether the item ignores transformations and draws directly to screen pixels.
|
||||
If True, the item will not inherit any scale or rotation transformations from its
|
||||
parent items, but its position will be transformed as usual.
|
||||
(see GraphicsItem::ItemIgnoresTransformations in the Qt documentation)
|
||||
"""
|
||||
self.setFlag(self.ItemIgnoresTransformations, b)
|
||||
|
||||
def setScaledMode(self):
|
||||
self.setPxMode(False)
|
||||
|
||||
def getPixmap(self):
|
||||
if self.qimage is None:
|
||||
self.render()
|
||||
if self.qimage is None:
|
||||
return None
|
||||
return QtGui.QPixmap.fromImage(self.qimage)
|
||||
|
||||
def pixelSize(self):
|
||||
"""return scene-size of a single pixel in the image"""
|
||||
br = self.sceneBoundingRect()
|
||||
if self.image is None:
|
||||
return 1,1
|
||||
return br.width()/self.width(), br.height()/self.height()
|
||||
|
||||
def viewTransformChanged(self):
|
||||
if self.autoDownsample:
|
||||
self.qimage = None
|
||||
self.update()
|
||||
|
||||
#def mousePressEvent(self, ev):
|
||||
#if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton:
|
||||
#self.drawAt(ev.pos(), ev)
|
||||
#ev.accept()
|
||||
#else:
|
||||
#ev.ignore()
|
||||
|
||||
#def mouseMoveEvent(self, ev):
|
||||
##print "mouse move", ev.pos()
|
||||
#if self.drawKernel is not None:
|
||||
#self.drawAt(ev.pos(), ev)
|
||||
|
||||
#def mouseReleaseEvent(self, ev):
|
||||
#pass
|
||||
|
||||
def mouseDragEvent(self, ev):
|
||||
if ev.button() != QtCore.Qt.LeftButton:
|
||||
ev.ignore()
|
||||
return
|
||||
elif self.drawKernel is not None:
|
||||
ev.accept()
|
||||
self.drawAt(ev.pos(), ev)
|
||||
|
||||
def mouseClickEvent(self, ev):
|
||||
if ev.button() == QtCore.Qt.RightButton:
|
||||
if self.raiseContextMenu(ev):
|
||||
ev.accept()
|
||||
if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton:
|
||||
self.drawAt(ev.pos(), ev)
|
||||
|
||||
def raiseContextMenu(self, ev):
|
||||
menu = self.getMenu()
|
||||
if menu is None:
|
||||
return False
|
||||
menu = self.scene().addParentContextMenus(self, menu, ev)
|
||||
pos = ev.screenPos()
|
||||
menu.popup(QtCore.QPoint(pos.x(), pos.y()))
|
||||
return True
|
||||
|
||||
def getMenu(self):
|
||||
if self.menu is None:
|
||||
if not self.removable:
|
||||
return None
|
||||
self.menu = QtGui.QMenu()
|
||||
self.menu.setTitle("Image")
|
||||
remAct = QtGui.QAction("Remove image", self.menu)
|
||||
remAct.triggered.connect(self.removeClicked)
|
||||
self.menu.addAction(remAct)
|
||||
self.menu.remAct = remAct
|
||||
return self.menu
|
||||
|
||||
|
||||
def hoverEvent(self, ev):
|
||||
if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton):
|
||||
ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it.
|
||||
ev.acceptClicks(QtCore.Qt.RightButton)
|
||||
#self.box.setBrush(fn.mkBrush('w'))
|
||||
elif not ev.isExit() and self.removable:
|
||||
ev.acceptClicks(QtCore.Qt.RightButton) ## accept context menu clicks
|
||||
#else:
|
||||
#self.box.setBrush(self.brush)
|
||||
#self.update()
|
||||
|
||||
|
||||
|
||||
def tabletEvent(self, ev):
|
||||
print(ev.device())
|
||||
print(ev.pointerType())
|
||||
print(ev.pressure())
|
||||
|
||||
def drawAt(self, pos, ev=None):
|
||||
pos = [int(pos.x()), int(pos.y())]
|
||||
dk = self.drawKernel
|
||||
kc = self.drawKernelCenter
|
||||
sx = [0,dk.shape[0]]
|
||||
sy = [0,dk.shape[1]]
|
||||
tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]]
|
||||
ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]]
|
||||
|
||||
for i in [0,1]:
|
||||
dx1 = -min(0, tx[i])
|
||||
dx2 = min(0, self.image.shape[0]-tx[i])
|
||||
tx[i] += dx1+dx2
|
||||
sx[i] += dx1+dx2
|
||||
|
||||
dy1 = -min(0, ty[i])
|
||||
dy2 = min(0, self.image.shape[1]-ty[i])
|
||||
ty[i] += dy1+dy2
|
||||
sy[i] += dy1+dy2
|
||||
|
||||
ts = (slice(tx[0],tx[1]), slice(ty[0],ty[1]))
|
||||
ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1]))
|
||||
mask = self.drawMask
|
||||
src = dk
|
||||
|
||||
if isinstance(self.drawMode, collections.Callable):
|
||||
self.drawMode(dk, self.image, mask, ss, ts, ev)
|
||||
else:
|
||||
src = src[ss]
|
||||
if self.drawMode == 'set':
|
||||
if mask is not None:
|
||||
mask = mask[ss]
|
||||
self.image[ts] = self.image[ts] * (1-mask) + src * mask
|
||||
else:
|
||||
self.image[ts] = src
|
||||
elif self.drawMode == 'add':
|
||||
self.image[ts] += src
|
||||
else:
|
||||
raise Exception("Unknown draw mode '%s'" % self.drawMode)
|
||||
self.updateImage()
|
||||
|
||||
def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'):
|
||||
self.drawKernel = kernel
|
||||
self.drawKernelCenter = center
|
||||
self.drawMode = mode
|
||||
self.drawMask = mask
|
||||
|
||||
def removeClicked(self):
|
||||
## Send remove event only after we have exited the menu event handler
|
||||
self.removeTimer = QtCore.QTimer()
|
||||
self.removeTimer.timeout.connect(self.emitRemoveRequested)
|
||||
self.removeTimer.start(0)
|
||||
|
||||
def emitRemoveRequested(self):
|
||||
self.removeTimer.timeout.disconnect(self.emitRemoveRequested)
|
||||
self.sigRemoveRequested.emit(self)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue