From 2e61be739fee2e0947fe4d8c0b9f86f8a3262979 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 17 Nov 2013 14:17:01 -0800 Subject: [PATCH 1/4] Don't copy the context menu of ViewBoxes. This allows customization of the context menu of a ViewBox simply by calling viewbox.menu.addAction(...). See issue #13. Also some cleanup. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 19 ++++--------------- .../graphicsItems/ViewBox/ViewBoxMenu.py | 2 +- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 5ab118f7..6e0a20d2 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1054,26 +1054,15 @@ class ViewBox(GraphicsWidget): if ev.button() == QtCore.Qt.RightButton and self.menuEnabled(): ev.accept() self.raiseContextMenu(ev) - + def raiseContextMenu(self, ev): - #print "viewbox.raiseContextMenu called." - - #menu = self.getMenu(ev) menu = self.getMenu(ev) self.scene().addParentContextMenus(self, menu, ev) - #print "2:", [str(a.text()) for a in self.menu.actions()] - pos = ev.screenPos() - #pos2 = ev.scenePos() - #print "3:", [str(a.text()) for a in self.menu.actions()] - #self.sigActionPositionChanged.emit(pos2) + menu.popup(ev.screenPos().toPoint()) - menu.popup(QtCore.QPoint(pos.x(), pos.y())) - #print "4:", [str(a.text()) for a in self.menu.actions()] - def getMenu(self, ev): - self._menuCopy = self.menu.copy() ## temporary storage to prevent menu disappearing - return self._menuCopy - + return self.menu + def getContextMenus(self, event): if self.menuEnabled(): return self.menu.subMenus() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index 5242ecdd..b508fd4b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -275,4 +275,4 @@ class ViewBoxMenu(QtGui.QMenu): from .ViewBox import ViewBox - \ No newline at end of file + From 5b905cde8b71c1a940c1ef1214ea356b4c8d9fc4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 19 Nov 2013 07:46:17 -0500 Subject: [PATCH 2/4] Override ViewBox.popup() to update menu before showing Extend ViewBox menu in examples/contextMenu --- examples/contextMenu.py | 142 ++++++++++++++++++ .../graphicsItems/ViewBox/ViewBoxMenu.py | 22 +-- 2 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 examples/contextMenu.py diff --git a/examples/contextMenu.py b/examples/contextMenu.py new file mode 100644 index 00000000..c2c5918d --- /dev/null +++ b/examples/contextMenu.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +""" +Demonstrates adding a custom context menu to a GraphicsItem +and extending the context menu of a ViewBox. + +PyQtGraph implements a system that allows each item in a scene to implement its +own context menu, and for the menus of its parent items to be automatically +displayed as well. + +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +win = pg.GraphicsWindow() +win.setWindowTitle('pyqtgraph example: context menu') + + +view = win.addViewBox() + +# add two new actions to the ViewBox context menu: +zoom1 = view.menu.addAction('Zoom to box 1') +zoom2 = view.menu.addAction('Zoom to box 2') + +# define callbacks for these actions +def zoomTo1(): + # note that box1 is defined below + view.autoRange(items=[box1]) +zoom1.triggered.connect(zoomTo1) + +def zoomTo2(): + # note that box1 is defined below + view.autoRange(items=[box2]) +zoom2.triggered.connect(zoomTo2) + + + +class MenuBox(pg.GraphicsObject): + """ + This class draws a rectangular area. Right-clicking inside the area will + raise a custom context menu which also includes the context menus of + its parents. + """ + def __init__(self, name): + self.name = name + self.pen = pg.mkPen('r') + + # menu creation is deferred because it is expensive and often + # the user will never see the menu anyway. + self.menu = None + + # note that the use of super() is often avoided because Qt does not + # allow to inherit from multiple QObject subclasses. + pg.GraphicsObject.__init__(self) + + + # All graphics items must have paint() and boundingRect() defined. + def boundingRect(self): + return QtCore.QRectF(0, 0, 10, 10) + + def paint(self, p, *args): + p.setPen(self.pen) + p.drawRect(self.boundingRect()) + + + # On right-click, raise the context menu + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + if self.raiseContextMenu(ev): + ev.accept() + + def raiseContextMenu(self, ev): + menu = self.getContextMenus() + + # Let the scene add on to the end of our context menu + # (this is optional) + menu = self.scene().addParentContextMenus(self, menu, ev) + + pos = ev.screenPos() + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + return True + + # This method will be called when this item's _children_ want to raise + # a context menu that includes their parents' menus. + def getContextMenus(self, event=None): + if self.menu is None: + self.menu = QtGui.QMenu() + self.menu.setTitle(self.name+ " options..") + + green = QtGui.QAction("Turn green", self.menu) + green.triggered.connect(self.setGreen) + self.menu.addAction(green) + self.menu.green = green + + blue = QtGui.QAction("Turn blue", self.menu) + blue.triggered.connect(self.setBlue) + self.menu.addAction(blue) + self.menu.green = blue + + alpha = QtGui.QWidgetAction(self.menu) + alphaSlider = QtGui.QSlider() + alphaSlider.setOrientation(QtCore.Qt.Horizontal) + alphaSlider.setMaximum(255) + alphaSlider.setValue(255) + alphaSlider.valueChanged.connect(self.setAlpha) + alpha.setDefaultWidget(alphaSlider) + self.menu.addAction(alpha) + self.menu.alpha = alpha + self.menu.alphaSlider = alphaSlider + return self.menu + + # Define context menu callbacks + def setGreen(self): + self.pen = pg.mkPen('g') + # inform Qt that this item must be redrawn. + self.update() + + def setBlue(self): + self.pen = pg.mkPen('b') + self.update() + + def setAlpha(self, a): + self.setOpacity(a/255.) + + +# This box's context menu will include the ViewBox's menu +box1 = MenuBox("Menu Box #1") +view.addItem(box1) + +# This box's context menu will include both the ViewBox's menu and box1's menu +box2 = MenuBox("Menu Box #2") +box2.setParentItem(box1) +box2.setPos(5, 5) +box2.scale(0.2, 0.2) + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index b508fd4b..99c3c3fb 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -88,15 +88,15 @@ class ViewBoxMenu(QtGui.QMenu): self.updateState() - def copy(self): - m = QtGui.QMenu() - for sm in self.subMenus(): - if isinstance(sm, QtGui.QMenu): - m.addMenu(sm) - else: - m.addAction(sm) - m.setTitle(self.title()) - return m + #def copy(self): + #m = QtGui.QMenu() + #for sm in self.subMenus(): + #if isinstance(sm, QtGui.QMenu): + #m.addMenu(sm) + #else: + #m.addAction(sm) + #m.setTitle(self.title()) + #return m def subMenus(self): if not self.valid: @@ -159,6 +159,10 @@ class ViewBoxMenu(QtGui.QMenu): self.ctrl[1].invertCheck.setChecked(state['yInverted']) self.valid = True + def popup(self, *args): + if not self.valid: + self.updateState() + QtGui.QMenu.popup(self, *args) def autoRange(self): self.view().autoRange() ## don't let signal call this directly--it'll add an unwanted argument From 23a0d6d7c07aba4c45e14d72b9e3647eaa52d8bd Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 20 Nov 2013 12:13:35 -0800 Subject: [PATCH 3/4] Use actions of ViewBox's contextMenu in full menu. The main change is on `ViewBox.getContextMenus`, which now returns an up-to-date of actions that `GraphicsScene.addParentContextMenus` can use. Also, `getContextMenus` was given a default implementation in the base class (falling back on `getMenu` if defined), and some cleanup was done. --- pyqtgraph/GraphicsScene/GraphicsScene.py | 29 +++++++------------ pyqtgraph/flowchart/Node.py | 3 -- pyqtgraph/flowchart/Terminal.py | 4 --- pyqtgraph/graphicsItems/GraphicsItem.py | 3 ++ pyqtgraph/graphicsItems/ROI.py | 6 +--- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 7 +---- .../graphicsItems/ViewBox/ViewBoxMenu.py | 16 ---------- 7 files changed, 15 insertions(+), 53 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 8729d085..6850371a 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -489,7 +489,7 @@ class GraphicsScene(QtGui.QGraphicsScene): #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. @@ -519,30 +519,21 @@ class GraphicsScene(QtGui.QGraphicsScene): event The original event that triggered the menu to appear. ============== ================================================== """ - - #items = self.itemsNearEvent(ev) + 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) - if subMenus is None: - continue - if type(subMenus) is not list: ## so that some items (like FlowchartViewBox) can return multiple menus - subMenus = [subMenus] - - for sm in subMenus: - menusToAdd.append(sm) - - if len(menusToAdd) > 0: + 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) diff --git a/pyqtgraph/flowchart/Node.py b/pyqtgraph/flowchart/Node.py index cd73b42b..f1de40d6 100644 --- a/pyqtgraph/flowchart/Node.py +++ b/pyqtgraph/flowchart/Node.py @@ -617,9 +617,6 @@ class NodeGraphicsItem(GraphicsObject): def getMenu(self): return self.menu - - def getContextMenus(self, event): - return [self.menu] def raiseContextMenu(self, ev): menu = self.scene().addParentContextMenus(self, self.getMenu(), ev) diff --git a/pyqtgraph/flowchart/Terminal.py b/pyqtgraph/flowchart/Terminal.py index 45805cd8..fea60dee 100644 --- a/pyqtgraph/flowchart/Terminal.py +++ b/pyqtgraph/flowchart/Terminal.py @@ -436,10 +436,6 @@ class TerminalGraphicsItem(GraphicsObject): def toggleMulti(self): multi = self.menu.multiAct.isChecked() self.term.setMultiValue(multi) - - ## probably never need this - #def getContextMenus(self, ev): - #return [self.getMenu()] def removeSelf(self): self.term.node().removeTerminal(self.term) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index a129436e..19cddd8a 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -585,3 +585,6 @@ class GraphicsItem(object): #def update(self): #self._qtBaseClass.update(self) #print "Update:", self + + def getContextMenus(self, event): + return [self.getMenu()] if hasattr(self, "getMenu") else [] diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index f6ce4680..cb5f4f30 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1202,11 +1202,7 @@ class Handle(UIGraphicsItem): def getMenu(self): return self.menu - - - def getContextMenus(self, event): - return [self.menu] - + def raiseContextMenu(self, ev): menu = self.scene().addParentContextMenus(self, self.getMenu(), ev) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 6e0a20d2..c60923b0 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1064,12 +1064,7 @@ class ViewBox(GraphicsWidget): return self.menu def getContextMenus(self, event): - if self.menuEnabled(): - return self.menu.subMenus() - else: - return None - #return [self.getMenu(event)] - + return self.menu.actions() if self.menuEnabled() else [] def mouseDragEvent(self, ev, axis=None): ## if axis is specified, event will only affect that axis. diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index 99c3c3fb..15d0be06 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -88,22 +88,6 @@ class ViewBoxMenu(QtGui.QMenu): self.updateState() - #def copy(self): - #m = QtGui.QMenu() - #for sm in self.subMenus(): - #if isinstance(sm, QtGui.QMenu): - #m.addMenu(sm) - #else: - #m.addAction(sm) - #m.setTitle(self.title()) - #return m - - def subMenus(self): - if not self.valid: - self.updateState() - return [self.viewAll] + self.axes + [self.leftMenu] - - def setExportMethods(self, methods): self.exportMethods = methods self.export.clear() From 19cf49bc7d5286eb8b0d9a826c1293ba97a120fc Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 21 Nov 2013 23:29:03 -0500 Subject: [PATCH 4/4] fixed context menu handling for non-GraphicsItems --- pyqtgraph/GraphicsScene/GraphicsScene.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 6850371a..3fdd5924 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -525,6 +525,8 @@ class GraphicsScene(QtGui.QGraphicsScene): 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)