Cleaned up and centralized export functionality

Moved GraphicsScene to its own directory, added exportDialog
Removed old export options from PlotItem / ViewBox (will re-enable once they are working again)
This commit is contained in:
Luke Campagnola 2012-03-11 11:59:45 -04:00
parent 920fd9333e
commit 81a32b0d1e
19 changed files with 955 additions and 344 deletions

View File

@ -3,7 +3,9 @@ import weakref
from pyqtgraph.Point import Point
import pyqtgraph.functions as fn
import pyqtgraph.ptime as ptime
from mouseEvents import *
import debug
import exportDialog
try:
import sip
@ -63,6 +65,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
sigMouseMoved = QtCore.Signal(object) ## emits position of mouse on every move
sigMouseClicked = QtCore.Signal(object) ## emitted when MouseClickEvent is not accepted by any items under the click.
ExportDirectory = None
@classmethod
def registerObject(cls, obj):
"""
@ -78,6 +82,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
QtGui.QGraphicsScene.__init__(self)
self.setClickRadius(clickRadius)
self.setMoveDistance(moveDistance)
self.exportDirectory = None
self.clickEvents = []
self.dragButtons = []
self.mouseGrabber = None
@ -89,6 +95,11 @@ class GraphicsScene(QtGui.QGraphicsScene):
#self.searchRect.setPen(fn.mkPen(200,0,0))
#self.addItem(self.searchRect)
self.contextMenu = [QtGui.QAction("Export...", self)]
self.contextMenu[0].triggered.connect(self.showExportDialog)
self.exportDialog = None
def setClickRadius(self, r):
"""
@ -420,19 +431,22 @@ class GraphicsScene(QtGui.QGraphicsScene):
##if item not in seen:
#yield item
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 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):
"""
@ -464,11 +478,12 @@ class GraphicsScene(QtGui.QGraphicsScene):
#items = self.itemsNearEvent(ev)
menusToAdd = []
while item.parentItem() is not None:
while item is not self:
item = item.parentItem()
#for item in items:
#if item is sender:
#continue
if item is None:
item = self
if not hasattr(item, "getContextMenus"):
continue
@ -484,10 +499,24 @@ class GraphicsScene(QtGui.QGraphicsScene):
menu.addSeparator()
for m in menusToAdd:
menu.addMenu(m)
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:
self.exportDialog = exportDialog.ExportDialog(self)
self.exportDialog.show(self.contextMenuItem)
@staticmethod
def translateGraphicsItem(item):
## for fixing pyqt bugs where the wrong item is returned
@ -501,247 +530,4 @@ class GraphicsScene(QtGui.QGraphicsScene):
return map(GraphicsScene.translateGraphicsItem, items)
class MouseDragEvent:
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()
def accept(self):
self.accepted = True
self.acceptedItem = self.currentItem
def ignore(self):
self.accepted = False
def isAccepted(self):
return self.accepted
def scenePos(self):
return Point(self._scenePos)
def screenPos(self):
return Point(self._screenPos)
def buttonDownScenePos(self, btn=None):
if btn is None:
btn = self.button()
return Point(self._buttonDownScenePos[int(btn)])
def buttonDownScreenPos(self, btn=None):
if btn is None:
btn = self.button()
return Point(self._buttonDownScreenPos[int(btn)])
def lastScenePos(self):
return Point(self._lastScenePos)
def lastScreenPos(self):
return Point(self._lastScreenPos)
def buttons(self):
return self._buttons
def button(self):
"""Return the button that initiated the drag (may be different from the buttons currently pressed)"""
return self._button
def pos(self):
return Point(self.currentItem.mapFromScene(self._scenePos))
def lastPos(self):
return Point(self.currentItem.mapFromScene(self._lastScenePos))
def buttonDownPos(self, btn=None):
if btn is None:
btn = self.button()
return Point(self.currentItem.mapFromScene(self._buttonDownScenePos[int(btn)]))
def isStart(self):
return self.start
def isFinish(self):
return self.finish
def __repr__(self):
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 self._modifiers
class MouseClickEvent:
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()
def accept(self):
self.accepted = True
self.acceptedItem = self.currentItem
def ignore(self):
self.accepted = False
def isAccepted(self):
return self.accepted
def scenePos(self):
return Point(self._scenePos)
def screenPos(self):
return Point(self._screenPos)
def buttons(self):
return self._buttons
def button(self):
return self._button
def double(self):
return self._double
def pos(self):
return Point(self.currentItem.mapFromScene(self._scenePos))
def lastPos(self):
return Point(self.currentItem.mapFromScene(self._lastScenePos))
def modifiers(self):
return self._modifiers
def __repr__(self):
p = self.pos()
return "<MouseClickEvent (%g,%g) button=%d>" % (p.x(), p.y(), int(self.button()))
def time(self):
return self._time
class HoverEvent:
"""
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):
return self.enter
def isExit(self):
return self.exit
def acceptClicks(self, button):
""""""
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):
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 Point(self._scenePos)
def screenPos(self):
return Point(self._screenPos)
def lastScenePos(self):
return Point(self._lastScenePos)
def lastScreenPos(self):
return Point(self._lastScreenPos)
def buttons(self):
return self._buttons
def pos(self):
return Point(self.currentItem.mapFromScene(self._scenePos))
def lastPos(self):
return Point(self.currentItem.mapFromScene(self._lastScenePos))
def __repr__(self):
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 self._modifiers
def clickItems(self):
return self.__clickItems
def dragItems(self):
return self.__dragItems

View File

@ -0,0 +1 @@
from GraphicsScene import *

View File

@ -0,0 +1,120 @@
import exportDialogTemplate
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
import pyqtgraph.exporters as exporters
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(pg.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.itemTree.currentItemChanged.connect(self.exportItemChanged)
self.ui.formatList.currentItemChanged.connect(self.exportFormatChanged)
def show(self, item=None):
if item is not None:
while not isinstance(item, pg.ViewBox) and not isinstance(item, pg.PlotItem) and item is not None:
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, pg.ViewBox):
si = QtGui.QTreeWidgetItem(['ViewBox'])
elif isinstance(item, pg.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.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()
self.ui.paramTree.setParameters(params)
self.currentExporter = exp
def exportClicked(self):
self.selectBox.hide()
self.currentExporter.export()
def close(self):
self.selectBox.setVisible(False)
self.setVisible(False)

View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'exportDialogTemplate.ui'
#
# Created: Sat Mar 10 17:54:53 2012
# by: PyQt4 UI code generator 4.8.5
#
# 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(241, 367)
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
self.gridLayout = QtGui.QGridLayout(Form)
self.gridLayout.setSpacing(0)
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
self.label = QtGui.QLabel(Form)
self.label.setText(QtGui.QApplication.translate("Form", "Item to export:", None, QtGui.QApplication.UnicodeUTF8))
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.setText(QtGui.QApplication.translate("Form", "Export format", None, QtGui.QApplication.UnicodeUTF8))
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.setText(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8))
self.exportBtn.setObjectName(_fromUtf8("exportBtn"))
self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1)
self.closeBtn = QtGui.QPushButton(Form)
self.closeBtn.setText(QtGui.QApplication.translate("Form", "Close", None, QtGui.QApplication.UnicodeUTF8))
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.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8))
self.label_3.setObjectName(_fromUtf8("label_3"))
self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
pass
from pyqtgraph.parametertree import ParameterTree

View File

@ -0,0 +1,93 @@
<?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>Form</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>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ParameterTree</class>
<extends>QTreeWidget</extends>
<header>pyqtgraph.parametertree</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,249 @@
from pyqtgraph.Point import Point
from pyqtgraph.Qt import QtCore, QtGui
import weakref
import pyqtgraph.ptime as ptime
class MouseDragEvent:
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()
def accept(self):
self.accepted = True
self.acceptedItem = self.currentItem
def ignore(self):
self.accepted = False
def isAccepted(self):
return self.accepted
def scenePos(self):
return Point(self._scenePos)
def screenPos(self):
return Point(self._screenPos)
def buttonDownScenePos(self, btn=None):
if btn is None:
btn = self.button()
return Point(self._buttonDownScenePos[int(btn)])
def buttonDownScreenPos(self, btn=None):
if btn is None:
btn = self.button()
return Point(self._buttonDownScreenPos[int(btn)])
def lastScenePos(self):
return Point(self._lastScenePos)
def lastScreenPos(self):
return Point(self._lastScreenPos)
def buttons(self):
return self._buttons
def button(self):
"""Return the button that initiated the drag (may be different from the buttons currently pressed)"""
return self._button
def pos(self):
return Point(self.currentItem.mapFromScene(self._scenePos))
def lastPos(self):
return Point(self.currentItem.mapFromScene(self._lastScenePos))
def buttonDownPos(self, btn=None):
if btn is None:
btn = self.button()
return Point(self.currentItem.mapFromScene(self._buttonDownScenePos[int(btn)]))
def isStart(self):
return self.start
def isFinish(self):
return self.finish
def __repr__(self):
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 self._modifiers
class MouseClickEvent:
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()
def accept(self):
self.accepted = True
self.acceptedItem = self.currentItem
def ignore(self):
self.accepted = False
def isAccepted(self):
return self.accepted
def scenePos(self):
return Point(self._scenePos)
def screenPos(self):
return Point(self._screenPos)
def buttons(self):
return self._buttons
def button(self):
return self._button
def double(self):
return self._double
def pos(self):
return Point(self.currentItem.mapFromScene(self._scenePos))
def lastPos(self):
return Point(self.currentItem.mapFromScene(self._lastScenePos))
def modifiers(self):
return self._modifiers
def __repr__(self):
p = self.pos()
return "<MouseClickEvent (%g,%g) button=%d>" % (p.x(), p.y(), int(self.button()))
def time(self):
return self._time
class HoverEvent:
"""
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):
return self.enter
def isExit(self):
return self.exit
def acceptClicks(self, button):
""""""
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):
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 Point(self._scenePos)
def screenPos(self):
return Point(self._screenPos)
def lastScenePos(self):
return Point(self._lastScenePos)
def lastScreenPos(self):
return Point(self._lastScreenPos)
def buttons(self):
return self._buttons
def pos(self):
return Point(self.currentItem.mapFromScene(self._scenePos))
def lastPos(self):
return Point(self.currentItem.mapFromScene(self._lastScenePos))
def __repr__(self):
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 self._modifiers
def clickItems(self):
return self.__clickItems
def dragItems(self):
return self.__dragItems

View File

@ -19,11 +19,44 @@ def setConfigOption(opt, value):
def getConfigOption(opt):
return CONFIG_OPTIONS[opt]
## Rename orphaned .pyc files. This is *probably* safe :)
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.
printed = False
startDir = os.path.abspath(startDir)
for path, dirs, files in os.walk(startDir):
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)
import os
path = os.path.split(__file__)[0]
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.
import os
def importAll(path):
d = os.path.join(os.path.split(__file__)[0], path)
files = []
@ -101,4 +134,6 @@ show = image ## for backward compatibility
def mkQApp():
if QtGui.QApplication.instance() is None:
global QAPP
QAPP = QtGui.QApplication([])
QAPP = QtGui.QApplication([])

View File

@ -12,6 +12,11 @@ For these function arguments, the following values may be used:
* QColor
* QPen / QBrush where appropriate
Notably, more complex pens and brushes can be easily built using the mkPen() / mkBrush() functions or with Qt's QPen and QBrush classes.
Notably, more complex pens and brushes can be easily built using the mkPen() / mkBrush() functions or with Qt's QPen and QBrush classes::
mkPen('y', width=3, style=QtCore.Qt.DashLine) ## Make a dashed yellow line 2px wide
mkPen(0.5) ## solid grey line 1px wide
mkPen(color=(200, 200, 255), style=QtCore.Qt.DotLine) ## Dotted pale-blue line
See the Qt documentation for 'QPen' and 'PenStyle' for more options.
Colors can also be built using mkColor(), intColor(), hsvColor(), or Qt's QColor class

94
exporters/Exporter.py Normal file
View File

@ -0,0 +1,94 @@
from pyqtgraph.widgets.FileDialog import FileDialog
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore, QtSvg
import os
LastExportDirectory = None
class Exporter(object):
"""
Abstract class used for exporting graphics to file / printer / whatever.
"""
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 item(self):
return self.item
def parameters(self):
"""Return the parameters used to configure this exporter."""
raise Exception("Abstract method must be overridden in subclass.")
def export(self):
""""""
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 = str(fileName)
global LastExportDirectory
LastExportDirectory = os.path.split(fileName)[0]
self.export(fileName=fileName, **self.fileDialog.opts)
def getScene(self):
if isinstance(self.item, pg.GraphicsScene):
return self.item
else:
return self.item.scene()
def getSourceRect(self):
if isinstance(self.item, pg.GraphicsScene):
return self.item.getViewWidget().viewRect()
else:
return self.item.sceneBoundingRect()
def getTargetRect(self):
if isinstance(self.item, pg.GraphicsScene):
return self.item.getViewWidget().rect()
else:
return self.item.mapRectToDevice(self.item.boundingRect())
#def writePs(self, fileName=None, item=None):
#if fileName is None:
#self.fileSaveDialog(self.writeSvg, filter="PostScript (*.ps)")
#return
#if item is None:
#item = self
#printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution)
#printer.setOutputFileName(fileName)
#painter = QtGui.QPainter(printer)
#self.render(painter)
#painter.end()
#def writeToPrinter(self):
#pass

View File

@ -0,0 +1,62 @@
from Exporter import Exporter
from pyqtgraph.parametertree import Parameter
from pyqtgraph.Qt import QtGui, QtCore, QtSvg
import pyqtgraph as pg
import numpy as np
__all__ = ['ImageExporter']
class ImageExporter(Exporter):
Name = "Image File (PNG, TIF, JPG, ...)"
def __init__(self, item):
Exporter.__init__(self, item)
tr = self.getTargetRect()
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': (0,0,0,255)},
])
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):
if fileName is None:
filter = ["*."+str(f) 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']))
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 = pg.makeQImage(bg, alpha=True)
painter = QtGui.QPainter(self.png)
self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect)
self.png.save(fileName)
painter.end()

64
exporters/SVGExporter.py Normal file
View File

@ -0,0 +1,64 @@
from Exporter import Exporter
from pyqtgraph.parametertree import Parameter
from pyqtgraph.Qt import QtGui, QtCore, QtSvg
import re
__all__ = ['SVGExporter']
class SVGExporter(Exporter):
Name = "Scalable Vector Graphics (SVG)"
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)},
])
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):
if fileName is None:
self.fileSaveDialog(filter="Scalable Vector Graphics (*.svg)")
return
self.svg = QtSvg.QSvgGenerator()
self.svg.setFileName(fileName)
self.svg.setSize(QtCore.QSize(100,100))
#self.svg.setResolution(600)
#self.svg.setViewBox()
targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height'])
sourceRect = self.getSourceRect()
painter = QtGui.QPainter(self.svg)
self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect)
painter.end()
## Workaround to set pen widths correctly
data = open(fileName).readlines()
for i in range(len(data)):
line = data[i]
m = re.match(r'(<g .*)stroke-width="1"(.*transform="matrix\(([^\)]+)\)".*)', line)
if m is not None:
#print "Matched group:", line
g = m.groups()
matrix = map(float, g[2].split(','))
#print "matrix:", matrix
scale = max(abs(matrix[0]), abs(matrix[3]))
if scale == 0 or scale == 1.0:
continue
data[i] = g[0] + ' stroke-width="%0.2g" ' % (1.0/scale) + g[1] + '\n'
#print "old line:", line
#print "new line:", data[i]
open(fileName, 'w').write(''.join(data))

7
exporters/__init__.py Normal file
View File

@ -0,0 +1,7 @@
from SVGExporter import *
from ImageExporter import *
Exporters = [SVGExporter, ImageExporter]
def listExporters():
return Exporters[:]

View File

@ -160,6 +160,7 @@ class GraphicsItemMethods(object):
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:
@ -169,6 +170,7 @@ class GraphicsItemMethods(object):
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:
@ -176,6 +178,27 @@ class GraphicsItemMethods(object):
vt = vt.inverted()[0]
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 = vt.inverted()[0]
return vt.mapRect(rect)
def mapToView(self, obj):
vt = self.viewTransform()
if vt is None:

View File

@ -173,19 +173,16 @@ class PlotItem(GraphicsWidget):
self.subMenus.append(sm)
self.ctrlMenu.addMenu(sm)
## exporting is handled by GraphicsScene now
#exportOpts = collections.OrderedDict([
#('SVG - Full Plot', self.saveSvgClicked),
#('SVG - Curves Only', self.saveSvgCurvesClicked),
#('Image', self.saveImgClicked),
#('CSV', self.saveCsvClicked),
#])
exportOpts = collections.OrderedDict([
('SVG - Full Plot', self.saveSvgClicked),
('SVG - Curves Only', self.saveSvgCurvesClicked),
('Image', self.saveImgClicked),
('CSV', self.saveCsvClicked),
])
#self.vb.menu.setExportMethods(exportOpts)
self.vb.menu.setExportMethods(exportOpts)
#self.menuAction = QtGui.QWidgetAction(self)
#self.menuAction.setDefaultWidget(w)
#self.ctrlMenu.addAction(self.menuAction)
#if HAVE_WIDGETGROUP:
self.stateGroup = WidgetGroup()

View File

@ -46,9 +46,10 @@ class ViewBoxMenu(QtGui.QMenu):
for sig, fn in connects:
sig.connect(getattr(self, axis.lower()+fn))
self.export = QtGui.QMenu("Export")
self.setExportMethods(view.exportMethods)
self.addMenu(self.export)
## exporting is handled by GraphicsScene now
#self.export = QtGui.QMenu("Export")
#self.setExportMethods(view.exportMethods)
#self.addMenu(self.export)
self.leftMenu = QtGui.QMenu("Mouse Mode")
group = QtGui.QActionGroup(self)
@ -78,7 +79,7 @@ class ViewBoxMenu(QtGui.QMenu):
def subMenus(self):
if not self.valid:
self.updateState()
return [self.viewAll] + self.axes + [self.export, self.leftMenu]
return [self.viewAll] + self.axes + [self.leftMenu]
def setExportMethods(self, methods):

View File

@ -23,7 +23,11 @@ class ParameterTree(TreeWidget):
self.lastSel = None
self.setRootIsDecorated(False)
def setParameters(self, param, root=None, depth=0, showTop=True):
def setParameters(self, param, showTop=True):
self.clear()
self.addParameters(param, showTop=showTop)
def addParameters(self, param, root=None, depth=0, showTop=True):
item = param.makeTreeItem(depth=depth)
if root is None:
root = self.invisibleRootItem()
@ -37,7 +41,11 @@ class ParameterTree(TreeWidget):
item.treeWidgetChanged()
for ch in param:
self.setParameters(ch, root=item, depth=depth+1)
self.addParameters(ch, root=item, depth=depth+1)
def clear(self):
self.invisibleRootItem().takeChildren()
def focusNext(self, item, forward=True):
## Give input focus to the next (or previous) item after 'item'

View File

@ -28,18 +28,10 @@ class ComplexParameter(Parameter):
self.b.sigValueChanged.connect(self.bChanged)
def aChanged(self):
try:
self.b.sigValueChanged.disconnect(self.bChanged)
self.b.setValue(1.0 / self.a.value())
finally:
self.b.sigValueChanged.connect(self.bChanged)
self.b.setValue(1.0 / self.a.value(), blockSignal=self.bChanged)
def bChanged(self):
try:
self.a.sigValueChanged.disconnect(self.aChanged)
self.a.setValue(1.0 / self.b.value())
finally:
self.a.sigValueChanged.connect(self.aChanged)
self.a.setValue(1.0 / self.b.value(), blockSignal=self.aChanged)
## test add/remove

View File

@ -3,6 +3,7 @@ from Parameter import Parameter, registerParameterType
from ParameterItem import ParameterItem
from pyqtgraph.widgets.SpinBox import SpinBox
from pyqtgraph.widgets.ColorButton import ColorButton
import pyqtgraph as pg
import os, collections
class WidgetParameterItem(ParameterItem):
@ -263,6 +264,14 @@ class EventProxy(QtCore.QObject):
class SimpleParameter(Parameter):
itemClass = WidgetParameterItem
def __init__(self, *args, **kargs):
Parameter.__init__(self, *args, **kargs)
if self.opts['type'] == 'color':
self.value = self.colorValue
def colorValue(self):
return pg.mkColor(Parameter.value(self))
registerParameterType('int', SimpleParameter, override=True)
registerParameterType('float', SimpleParameter, override=True)
registerParameterType('bool', SimpleParameter, override=True)

View File

@ -411,60 +411,60 @@ class GraphicsView(QtGui.QGraphicsView):
return Point(p11 - p01)
def writeSvg(self, fileName=None):
if fileName is None:
self.fileDialog = FileDialog()
self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
if GraphicsView.lastFileDir is not None:
self.fileDialog.setDirectory(GraphicsView.lastFileDir)
self.fileDialog.show()
self.fileDialog.fileSelected.connect(self.writeSvg)
return
fileName = str(fileName)
GraphicsView.lastFileDir = os.path.split(fileName)[0]
self.svg = QtSvg.QSvgGenerator()
self.svg.setFileName(fileName)
self.svg.setSize(self.size())
self.svg.setResolution(600)
painter = QtGui.QPainter(self.svg)
self.render(painter)
def writeImage(self, fileName=None):
if fileName is None:
self.fileDialog = FileDialog()
self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) ## this is the line that makes the fileDialog not show on mac
if GraphicsView.lastFileDir is not None:
self.fileDialog.setDirectory(GraphicsView.lastFileDir)
self.fileDialog.show()
self.fileDialog.fileSelected.connect(self.writeImage)
return
fileName = str(fileName)
GraphicsView.lastFileDir = os.path.split(fileName)[0]
self.png = QtGui.QImage(self.size(), QtGui.QImage.Format_ARGB32)
painter = QtGui.QPainter(self.png)
rh = self.renderHints()
self.setRenderHints(QtGui.QPainter.Antialiasing)
self.render(painter)
self.setRenderHints(rh)
self.png.save(fileName)
def writePs(self, fileName=None):
if fileName is None:
self.fileDialog = FileDialog()
self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
self.fileDialog.show()
self.fileDialog.fileSelected.connect(self.writePs)
return
#def writeSvg(self, fileName=None):
#if fileName is None:
# fileName = str(QtGui.QFileDialog.getSaveFileName())
printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution)
printer.setOutputFileName(fileName)
painter = QtGui.QPainter(printer)
self.render(painter)
painter.end()
#self.fileDialog = FileDialog()
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
#self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
#if GraphicsView.lastFileDir is not None:
#self.fileDialog.setDirectory(GraphicsView.lastFileDir)
#self.fileDialog.show()
#self.fileDialog.fileSelected.connect(self.writeSvg)
#return
#fileName = str(fileName)
#GraphicsView.lastFileDir = os.path.split(fileName)[0]
#self.svg = QtSvg.QSvgGenerator()
#self.svg.setFileName(fileName)
#self.svg.setSize(self.size())
#self.svg.setResolution(600)
#painter = QtGui.QPainter(self.svg)
#self.render(painter)
#def writeImage(self, fileName=None):
#if fileName is None:
#self.fileDialog = FileDialog()
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
#self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) ## this is the line that makes the fileDialog not show on mac
#if GraphicsView.lastFileDir is not None:
#self.fileDialog.setDirectory(GraphicsView.lastFileDir)
#self.fileDialog.show()
#self.fileDialog.fileSelected.connect(self.writeImage)
#return
#fileName = str(fileName)
#GraphicsView.lastFileDir = os.path.split(fileName)[0]
#self.png = QtGui.QImage(self.size(), QtGui.QImage.Format_ARGB32)
#painter = QtGui.QPainter(self.png)
#rh = self.renderHints()
#self.setRenderHints(QtGui.QPainter.Antialiasing)
#self.render(painter)
#self.setRenderHints(rh)
#self.png.save(fileName)
#def writePs(self, fileName=None):
#if fileName is None:
#self.fileDialog = FileDialog()
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
#self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
#self.fileDialog.show()
#self.fileDialog.fileSelected.connect(self.writePs)
#return
##if fileName is None:
## fileName = str(QtGui.QFileDialog.getSaveFileName())
#printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution)
#printer.setOutputFileName(fileName)
#painter = QtGui.QPainter(printer)
#self.render(painter)
#painter.end()
def dragEnterEvent(self, ev):
ev.ignore() ## not sure why, but for some reason this class likes to consume drag events