summaryrefslogtreecommitdiff
path: root/silx/gui/plot/PlotInteraction.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/PlotInteraction.py')
-rw-r--r--silx/gui/plot/PlotInteraction.py396
1 files changed, 224 insertions, 172 deletions
diff --git a/silx/gui/plot/PlotInteraction.py b/silx/gui/plot/PlotInteraction.py
index abfcf79..d182a49 100644
--- a/silx/gui/plot/PlotInteraction.py
+++ b/silx/gui/plot/PlotInteraction.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -37,7 +37,7 @@ import weakref
from .. import colors
from .. import qt
from . import items
-from .Interaction import (ClickOrDrag, LEFT_BTN, RIGHT_BTN,
+from .Interaction import (ClickOrDrag, LEFT_BTN, RIGHT_BTN, MIDDLE_BTN,
State, StateMachine)
from .PlotEvents import (prepareCurveSignal, prepareDrawingSignal,
prepareHoverSignal, prepareImageSignal,
@@ -102,11 +102,11 @@ class _PlotInteraction(object):
else:
color2 = "black"
- self.plot.addItem(points[:, 0], points[:, 1], legend=legend,
- replace=False,
- shape=shape, fill=fill,
- color=color, linebgcolor=color2, linestyle="--",
- overlay=True)
+ self.plot.addShape(points[:, 0], points[:, 1], legend=legend,
+ replace=False,
+ shape=shape, fill=fill,
+ color=color, linebgcolor=color2, linestyle="--",
+ overlay=True)
self._selectionAreas.add(legend)
@@ -127,7 +127,7 @@ class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
_DOUBLE_CLICK_TIMEOUT = 0.4
- class ZoomIdle(ClickOrDrag.Idle):
+ class Idle(ClickOrDrag.Idle):
def onWheel(self, x, y, angle):
scaleF = 1.1 if angle > 0 else 1. / 1.1
applyZoomToPlot(self.machine.plot, scaleF, (x, y))
@@ -170,23 +170,16 @@ class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
x, y)
self.plot.notify(**eventDict)
- def __init__(self, plot):
+ def __init__(self, plot, **kwargs):
"""Init.
:param plot: The plot to apply modifications to.
"""
- _PlotInteraction.__init__(self, plot)
-
- states = {
- 'idle': _ZoomOnWheel.ZoomIdle,
- 'rightClick': ClickOrDrag.RightClick,
- 'clickOrDrag': ClickOrDrag.ClickOrDrag,
- 'drag': ClickOrDrag.Drag
- }
- StateMachine.__init__(self, states, 'idle')
-
self._lastClick = 0., None
+ _PlotInteraction.__init__(self, plot)
+ ClickOrDrag.__init__(self, **kwargs)
+
# Pan #########################################################################
@@ -198,10 +191,10 @@ class Pan(_ZoomOnWheel):
_, y2Data = self.plot.pixelToData(x, y, axis='right')
return xData, yData, y2Data
- def beginDrag(self, x, y):
+ def beginDrag(self, x, y, btn):
self._previousDataPos = self._pixelToData(x, y)
- def drag(self, x, y):
+ def drag(self, x, y, btn):
xData, yData, y2Data = self._pixelToData(x, y)
lastX, lastY, lastY2 = self._previousDataPos
@@ -266,7 +259,7 @@ class Pan(_ZoomOnWheel):
self._previousDataPos = self._pixelToData(x, y)
- def endDrag(self, startPos, endPos):
+ def endDrag(self, startPos, endPos, btn):
del self._previousDataPos
def cancel(self):
@@ -315,12 +308,12 @@ class Zoom(_ZoomOnWheel):
return areaX0, areaY0, areaX1, areaY1
- def beginDrag(self, x, y):
+ def beginDrag(self, x, y, btn):
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
self.x0, self.y0 = x, y
- def drag(self, x1, y1):
+ def drag(self, x1, y1, btn):
if self.color is None:
return # Do not draw zoom area
@@ -388,7 +381,7 @@ class Zoom(_ZoomOnWheel):
self.plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
- def endDrag(self, startPos, endPos):
+ def endDrag(self, startPos, endPos, btn):
x0, y0 = startPos
x1, y1 = endPos
@@ -475,6 +468,24 @@ class SelectPolygon(Select):
self.machine.parameters)
self.machine.plot.notify(**eventDict)
+ def validate(self):
+ if len(self.points) > 2:
+ self.closePolygon()
+ else:
+ # It would be nice to have a cancel event.
+ # The plot is not aware that the interaction was cancelled
+ self.machine.cancel()
+
+ def closePolygon(self):
+ self.machine.resetSelectionArea()
+ self.points[-1] = self.points[0]
+ eventDict = prepareDrawingSignal('drawingFinished',
+ 'polygon',
+ self.points,
+ self.machine.parameters)
+ self.machine.plot.notify(**eventDict)
+ self.goto('idle')
+
def onWheel(self, x, y, angle):
self.machine.onWheel(x, y, angle)
self.updateFirstPoint()
@@ -491,16 +502,7 @@ class SelectPolygon(Select):
# Only allow to close polygon after first point
if len(self.points) > 2 and dx <= threshold and dy <= threshold:
- self.machine.resetSelectionArea()
-
- self.points[-1] = self.points[0]
-
- eventDict = prepareDrawingSignal('drawingFinished',
- 'polygon',
- self.points,
- self.machine.parameters)
- self.machine.plot.notify(**eventDict)
- self.goto('idle')
+ self.closePolygon()
return False
# Update polygon last point not too close to previous one
@@ -1023,13 +1025,13 @@ class SelectFreeLine(ClickOrDrag, _PlotInteraction):
if btn == LEFT_BTN:
self._processEvent(x, y, isLast=True)
- def beginDrag(self, x, y):
+ def beginDrag(self, x, y, btn):
self._processEvent(x, y, isLast=False)
- def drag(self, x, y):
+ def drag(self, x, y, btn):
self._processEvent(x, y, isLast=False)
- def endDrag(self, startPos, endPos):
+ def endDrag(self, startPos, endPos, btn):
x, y = endPos
self._processEvent(x, y, isLast=True)
@@ -1079,15 +1081,13 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
applyZoomToPlot(self.machine.plot, scaleF, (x, y))
def onMove(self, x, y):
- result = self.machine.plot._pickTopMost(
- x, y, lambda item: isinstance(item, items.MarkerBase))
- marker = result.getItem() if result is not None else None
+ marker = self.machine.plot._getMarkerAt(x, y)
if marker is not None:
dataPos = self.machine.plot.pixelToData(x, y)
assert dataPos is not None
eventDict = prepareHoverSignal(
- marker.getLegend(), 'marker',
+ marker.getName(), 'marker',
dataPos, (x, y),
marker.isDraggable(),
marker.isSelectable())
@@ -1109,19 +1109,18 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
elif marker.isSelectable():
self.machine.plot.setGraphCursorShape(CURSOR_POINTING)
+ else:
+ self.machine.plot.setGraphCursorShape()
return True
def __init__(self, plot):
- _PlotInteraction.__init__(self, plot)
+ self._pan = Pan(plot)
- states = {
- 'idle': ItemsInteraction.Idle,
- 'rightClick': ClickOrDrag.RightClick,
- 'clickOrDrag': ClickOrDrag.ClickOrDrag,
- 'drag': ClickOrDrag.Drag
- }
- StateMachine.__init__(self, states, 'idle')
+ _PlotInteraction.__init__(self, plot)
+ ClickOrDrag.__init__(self,
+ clickButtons=(LEFT_BTN, RIGHT_BTN),
+ dragButtons=(LEFT_BTN, MIDDLE_BTN))
def click(self, x, y, btn):
"""Handle mouse click
@@ -1169,7 +1168,7 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
eventDict = prepareMarkerSignal('markerClicked',
'left',
- item.getLegend(),
+ item.getName(),
'marker',
item.isDraggable(),
item.isSelectable(),
@@ -1186,7 +1185,7 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
indices = result.getIndices(copy=False)
eventDict = prepareCurveSignal('left',
- item.getLegend(),
+ item.getName(),
'curve',
xData[indices],
yData[indices],
@@ -1201,7 +1200,7 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
indices = result.getIndices(copy=False)
row, column = indices[0][0], indices[1][0]
eventDict = prepareImageSignal('left',
- item.getLegend(),
+ item.getName(),
'image',
column, row,
dataPos[0], dataPos[1],
@@ -1224,7 +1223,7 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
eventDict = prepareMarkerSignal(eventType,
'left',
- marker.getLegend(),
+ marker.getName(),
'marker',
marker.isDraggable(),
marker.isSelectable(),
@@ -1242,65 +1241,79 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
self.plot.setGraphCursorShape()
self.draggedItemRef = None
- def beginDrag(self, x, y):
+ def beginDrag(self, x, y, btn):
"""Handle begining of drag interaction
:param x: X position of the mouse in pixels
:param y: Y position of the mouse in pixels
+ :param str btn: The mouse button for which a drag is starting.
:return: True if drag is catched by an item, False otherwise
"""
- self._lastPos = self.plot.pixelToData(x, y)
- assert self._lastPos is not None
+ if btn == LEFT_BTN:
+ self._lastPos = self.plot.pixelToData(x, y)
+ assert self._lastPos is not None
- result = self.plot._pickTopMost(x, y, self.__isDraggableItem)
- item = result.getItem() if result is not None else None
+ result = self.plot._pickTopMost(x, y, self.__isDraggableItem)
+ item = result.getItem() if result is not None else None
- self.draggedItemRef = None if item is None else weakref.ref(item)
+ self.draggedItemRef = None if item is None else weakref.ref(item)
- if item is None:
- self.__terminateDrag()
- return False
+ if item is None:
+ self.__terminateDrag()
+ return False
- if isinstance(item, items.MarkerBase):
- self._signalMarkerMovingEvent('markerMoving', item, x, y)
+ if isinstance(item, items.MarkerBase):
+ self._signalMarkerMovingEvent('markerMoving', item, x, y)
+ item._startDrag()
- return True
+ return True
+ elif btn == MIDDLE_BTN:
+ self._pan.beginDrag(x, y, btn)
+ return True
- def drag(self, x, y):
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
+ def drag(self, x, y, btn):
+ if btn == LEFT_BTN:
+ dataPos = self.plot.pixelToData(x, y)
+ assert dataPos is not None
- item = None if self.draggedItemRef is None else self.draggedItemRef()
- if item is not None:
- item.drag(self._lastPos, dataPos)
+ item = None if self.draggedItemRef is None else self.draggedItemRef()
+ if item is not None:
+ item.drag(self._lastPos, dataPos)
- if isinstance(item, items.MarkerBase):
- self._signalMarkerMovingEvent('markerMoving', item, x, y)
+ if isinstance(item, items.MarkerBase):
+ self._signalMarkerMovingEvent('markerMoving', item, x, y)
- self._lastPos = dataPos
-
- def endDrag(self, startPos, endPos):
- item = None if self.draggedItemRef is None else self.draggedItemRef()
- if item is not None and isinstance(item, items.MarkerBase):
- posData = list(item.getPosition())
- if posData[0] is None:
- posData[0] = 1.
- if posData[1] is None:
- posData[1] = 1.
-
- eventDict = prepareMarkerSignal(
- 'markerMoved',
- 'left',
- item.getLegend(),
- 'marker',
- item.isDraggable(),
- item.isSelectable(),
- posData)
- self.plot.notify(**eventDict)
+ self._lastPos = dataPos
+ elif btn == MIDDLE_BTN:
+ self._pan.drag(x, y, btn)
- self.__terminateDrag()
+ def endDrag(self, startPos, endPos, btn):
+ if btn == LEFT_BTN:
+ item = None if self.draggedItemRef is None else self.draggedItemRef()
+ if isinstance(item, items.MarkerBase):
+ posData = list(item.getPosition())
+ if posData[0] is None:
+ posData[0] = 1.
+ if posData[1] is None:
+ posData[1] = 1.
+
+ eventDict = prepareMarkerSignal(
+ 'markerMoved',
+ 'left',
+ item.getLegend(),
+ 'marker',
+ item.isDraggable(),
+ item.isSelectable(),
+ posData)
+ self.plot.notify(**eventDict)
+ item._endDrag()
+
+ self.__terminateDrag()
+ elif btn == MIDDLE_BTN:
+ self._pan.endDrag(startPos, endPos, btn)
def cancel(self):
+ self._pan.cancel()
self.__terminateDrag()
@@ -1319,25 +1332,12 @@ class ItemsInteractionForCombo(ItemsInteraction):
result = self.machine.plot._pickTopMost(
x, y, self.__isItemSelectableOrDraggable)
if result is not None: # Request focus and handle interaction
- self.goto('clickOrDrag', x, y)
+ self.goto('clickOrDrag', x, y, btn)
return True
else: # Do not request focus
return False
-
- elif btn == RIGHT_BTN:
- self.goto('rightClick', x, y)
- return True
-
- def __init__(self, plot):
- _PlotInteraction.__init__(self, plot)
-
- states = {
- 'idle': ItemsInteractionForCombo.Idle,
- 'rightClick': ClickOrDrag.RightClick,
- 'clickOrDrag': ClickOrDrag.ClickOrDrag,
- 'drag': ClickOrDrag.Drag
- }
- StateMachine.__init__(self, states, 'idle')
+ else:
+ return super().onPress(x, y, btn)
# FocusManager ################################################################
@@ -1350,11 +1350,12 @@ class FocusManager(StateMachine):
"""
class Idle(State):
def onPress(self, x, y, btn):
- for eventHandler in self.machine.eventHandlers:
- requestFocus = eventHandler.handleEvent('press', x, y, btn)
- if requestFocus:
- self.goto('focus', eventHandler, btn)
- break
+ if btn == LEFT_BTN:
+ for eventHandler in self.machine.eventHandlers:
+ requestFocus = eventHandler.handleEvent('press', x, y, btn)
+ if requestFocus:
+ self.goto('focus', eventHandler, btn)
+ break
def _processEvent(self, *args):
for eventHandler in self.machine.eventHandlers:
@@ -1366,7 +1367,8 @@ class FocusManager(StateMachine):
self._processEvent('move', x, y)
def onRelease(self, x, y, btn):
- self._processEvent('release', x, y, btn)
+ if btn == LEFT_BTN:
+ self._processEvent('release', x, y, btn)
def onWheel(self, x, y, angle):
self._processEvent('wheel', x, y, angle)
@@ -1376,18 +1378,24 @@ class FocusManager(StateMachine):
self.eventHandler = eventHandler
self.focusBtns = {btn}
+ def validate(self):
+ self.eventHandler.validate()
+ self.goto('idle')
+
def onPress(self, x, y, btn):
- self.focusBtns.add(btn)
- self.eventHandler.handleEvent('press', x, y, btn)
+ if btn == LEFT_BTN:
+ self.focusBtns.add(btn)
+ self.eventHandler.handleEvent('press', x, y, btn)
def onMove(self, x, y):
self.eventHandler.handleEvent('move', x, y)
def onRelease(self, x, y, btn):
- self.focusBtns.discard(btn)
- requestFocus = self.eventHandler.handleEvent('release', x, y, btn)
- if len(self.focusBtns) == 0 and not requestFocus:
- self.goto('idle')
+ if btn == LEFT_BTN:
+ self.focusBtns.discard(btn)
+ requestFocus = self.eventHandler.handleEvent('release', x, y, btn)
+ if len(self.focusBtns) == 0 and not requestFocus:
+ self.goto('idle')
def onWheel(self, x, y, angleInDegrees):
self.eventHandler.handleEvent('wheel', x, y, angleInDegrees)
@@ -1447,37 +1455,40 @@ class ZoomAndSelect(ItemsInteraction):
else:
self._zoom.click(x, y, btn)
- def beginDrag(self, x, y):
+ def beginDrag(self, x, y, btn):
"""Handle start drag and switching between zoom and item drag.
:param x: X position in pixels
:param y: Y position in pixels
+ :param str btn: The mouse button for which a drag is starting.
"""
- self._doZoom = not super(ZoomAndSelect, self).beginDrag(x, y)
+ self._doZoom = not super(ZoomAndSelect, self).beginDrag(x, y, btn)
if self._doZoom:
- self._zoom.beginDrag(x, y)
+ self._zoom.beginDrag(x, y, btn)
- def drag(self, x, y):
+ def drag(self, x, y, btn):
"""Handle drag, eventually forwarding to zoom.
:param x: X position in pixels
:param y: Y position in pixels
+ :param str btn: The mouse button for which a drag is in progress.
"""
if self._doZoom:
- return self._zoom.drag(x, y)
+ return self._zoom.drag(x, y, btn)
else:
- return super(ZoomAndSelect, self).drag(x, y)
+ return super(ZoomAndSelect, self).drag(x, y, btn)
- def endDrag(self, startPos, endPos):
+ def endDrag(self, startPos, endPos, btn):
"""Handle end of drag, eventually forwarding to zoom.
:param startPos: (x, y) position at the beginning of the drag
:param endPos: (x, y) position at the end of the drag
+ :param str btn: The mouse button for which a drag is done.
"""
if self._doZoom:
- return self._zoom.endDrag(startPos, endPos)
+ return self._zoom.endDrag(startPos, endPos, btn)
else:
- return super(ZoomAndSelect, self).endDrag(startPos, endPos)
+ return super(ZoomAndSelect, self).endDrag(startPos, endPos, btn)
class PanAndSelect(ItemsInteraction):
@@ -1515,41 +1526,101 @@ class PanAndSelect(ItemsInteraction):
else:
self._pan.click(x, y, btn)
- def beginDrag(self, x, y):
+ def beginDrag(self, x, y, btn):
"""Handle start drag and switching between zoom and item drag.
:param x: X position in pixels
:param y: Y position in pixels
+ :param str btn: The mouse button for which a drag is starting.
"""
- self._doPan = not super(PanAndSelect, self).beginDrag(x, y)
+ self._doPan = not super(PanAndSelect, self).beginDrag(x, y, btn)
if self._doPan:
- self._pan.beginDrag(x, y)
+ self._pan.beginDrag(x, y, btn)
- def drag(self, x, y):
+ def drag(self, x, y, btn):
"""Handle drag, eventually forwarding to zoom.
:param x: X position in pixels
:param y: Y position in pixels
+ :param str btn: The mouse button for which a drag is in progress.
"""
if self._doPan:
- return self._pan.drag(x, y)
+ return self._pan.drag(x, y, btn)
else:
- return super(PanAndSelect, self).drag(x, y)
+ return super(PanAndSelect, self).drag(x, y, btn)
- def endDrag(self, startPos, endPos):
+ def endDrag(self, startPos, endPos, btn):
"""Handle end of drag, eventually forwarding to zoom.
:param startPos: (x, y) position at the beginning of the drag
:param endPos: (x, y) position at the end of the drag
+ :param str btn: The mouse button for which a drag is done.
"""
if self._doPan:
- return self._pan.endDrag(startPos, endPos)
+ return self._pan.endDrag(startPos, endPos, btn)
else:
- return super(PanAndSelect, self).endDrag(startPos, endPos)
+ return super(PanAndSelect, self).endDrag(startPos, endPos, btn)
# Interaction mode control ####################################################
+# Mapping of draw modes: event handler
+_DRAW_MODES = {
+ 'polygon': SelectPolygon,
+ 'rectangle': SelectRectangle,
+ 'ellipse': SelectEllipse,
+ 'line': SelectLine,
+ 'vline': SelectVLine,
+ 'hline': SelectHLine,
+ 'polylines': SelectFreeLine,
+ 'pencil': DrawFreeHand,
+ }
+
+
+class DrawMode(FocusManager):
+ """Interactive mode for draw and select"""
+
+ def __init__(self, plot, shape, label, color, width):
+ eventHandlerClass = _DRAW_MODES[shape]
+ parameters = {
+ 'shape': shape,
+ 'label': label,
+ 'color': color,
+ 'width': width,
+ }
+ super().__init__((
+ Pan(plot, clickButtons=(), dragButtons=(MIDDLE_BTN,)),
+ eventHandlerClass(plot, parameters)))
+
+ def getDescription(self):
+ """Returns the dict describing this interactive mode"""
+ params = self.eventHandlers[1].parameters.copy()
+ params['mode'] = 'draw'
+ return params
+
+
+class DrawSelectMode(FocusManager):
+ """Interactive mode for draw and select"""
+
+ def __init__(self, plot, shape, label, color, width):
+ eventHandlerClass = _DRAW_MODES[shape]
+ parameters = {
+ 'shape': shape,
+ 'label': label,
+ 'color': color,
+ 'width': width,
+ }
+ super().__init__((
+ ItemsInteractionForCombo(plot),
+ eventHandlerClass(plot, parameters)))
+
+ def getDescription(self):
+ """Returns the dict describing this interactive mode"""
+ params = self.eventHandlers[1].parameters.copy()
+ params['mode'] = 'select-draw'
+ return params
+
+
class PlotInteraction(object):
"""Proxy to currently use state machine for interaction.
@@ -1582,26 +1653,15 @@ class PlotInteraction(object):
"""Returns the current interactive mode as a dict.
The returned dict contains at least the key 'mode'.
- Mode can be: 'draw', 'pan', 'select', 'zoom'.
+ Mode can be: 'draw', 'pan', 'select', 'select-draw', 'zoom'.
It can also contains extra keys (e.g., 'color') specific to a mode
as provided to :meth:`setInteractiveMode`.
"""
if isinstance(self._eventHandler, ZoomAndSelect):
return {'mode': 'zoom', 'color': self._eventHandler.color}
- elif isinstance(self._eventHandler, FocusManager):
- drawHandler = self._eventHandler.eventHandlers[1]
- if not isinstance(drawHandler, Select):
- raise RuntimeError('Unknown interactive mode')
-
- result = drawHandler.parameters.copy()
- result['mode'] = 'draw'
- return result
-
- elif isinstance(self._eventHandler, Select):
- result = self._eventHandler.parameters.copy()
- result['mode'] = 'draw'
- return result
+ elif isinstance(self._eventHandler, (DrawMode, DrawSelectMode)):
+ return self._eventHandler.getDescription()
elif isinstance(self._eventHandler, PanAndSelect):
return {'mode': 'pan'}
@@ -1609,6 +1669,13 @@ class PlotInteraction(object):
else:
return {'mode': 'select'}
+ def validate(self):
+ """Validate the current interaction if possible
+
+ If was designed to close the polygon interaction.
+ """
+ self._eventHandler.validate()
+
def setInteractiveMode(self, mode, color='black',
shape='polygon', label=None, width=None):
"""Switch the interactive mode.
@@ -1636,24 +1703,9 @@ class PlotInteraction(object):
color = colors.rgba(color)
if mode in ('draw', 'select-draw'):
- assert shape in self._DRAW_MODES
- eventHandlerClass = self._DRAW_MODES[shape]
- parameters = {
- 'shape': shape,
- 'label': label,
- 'color': color,
- 'width': width,
- }
- eventHandler = eventHandlerClass(plot, parameters)
-
self._eventHandler.cancel()
-
- if mode == 'draw':
- self._eventHandler = eventHandler
-
- else: # mode == 'select-draw'
- self._eventHandler = FocusManager(
- (ItemsInteractionForCombo(plot), eventHandler))
+ handlerClass = DrawMode if mode == 'draw' else DrawSelectMode
+ self._eventHandler = handlerClass(plot, shape, label, color, width)
elif mode == 'pan':
# Ignores color, shape and label