Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 79 additions & 53 deletions src/silx/gui/plot/PlotInteraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def __init__(self, plot):
:param plot: The plot to apply modifications to.
"""
self._needReplot = False
self._selectionAreas = set()
self._legends = set()
self._plot = weakref.ref(plot) # Avoid cyclic-ref

@property
Expand Down Expand Up @@ -136,13 +136,57 @@ def setSelectionArea(self, points, fill, color, name="", shape="polygon"):
overlay=True,
)

self._selectionAreas.add(legend)
self._legends.add(legend)

def resetSelectionArea(self):
"""Remove all selection areas set by setSelectionArea."""
for legend in self._selectionAreas:
self.plot.remove(legend, kind="item")
self._selectionAreas = set()
def setMarker(
self,
x: float,
y: float,
name: str = "",
color: tuple[float, ...] | str | None = None,
) -> None:
"""
:param float x: The x data coord.
:param float y: The y data coord.
:param name: The name of the marker.
:param color: The name of the color.
"""
legend = "__MARKER__" + name

self.plot.addMarker(
x=x,
y=y,
legend=legend,
symbol="o",
color=color,
text=None,
selectable=False,
draggable=False,
)

self._legends.add(legend)

def clearItems(self):
"""
Remove all selection areas set by setSelectionArea
and markers set by setMarker.
"""
for legend in self._legends:
self.plot.remove(legend, kind=("item", "marker"))
self._legends = set()

def insideMarker(self, x: float, y: float, name="") -> bool:
"""
:param float x: The x pixel coord.
:param float y: The y pixel coord.
:returns: True when the marker is selected.
"""
legend = "__MARKER__" + name
marker = self.plot._getMarker(legend)
if marker is None:
return False

return marker.pick(x, y) is not None


# Zoom/Pan ####################################################################
Expand Down Expand Up @@ -450,11 +494,11 @@ def endDrag(self, startPos, endPos, btn):
# Avoid empty zoom area
self._zoom(x0, y0, x1, y1)

self.resetSelectionArea()
self.clearItems()

def cancel(self):
if isinstance(self.state, self.states["drag"]):
self.resetSelectionArea()
self.clearItems()


# Select ######################################################################
Expand Down Expand Up @@ -501,23 +545,10 @@ def enterState(self, x, y):
self.updateFirstPoint()

def updateFirstPoint(self):
"""Update drawing first point, using self._firstPos"""
x, y = self.machine.plot.dataToPixel(*self._firstPos, check=False)

offset = self.machine.getDragThreshold()
points = [
(x - offset, y - offset),
(x - offset, y + offset),
(x + offset, y + offset),
(x + offset, y - offset),
]
points = [
self.machine.plot.pixelToData(xpix, ypix, check=False)
for xpix, ypix in points
]
self.machine.setSelectionArea(
points, fill=None, color=self.machine.color, name="first_point"
)
"""Display first point as a fixed-size square marker."""
x, y = self._firstPos
r, g, b, _ = self.machine.color
self.machine.setMarker(x, y, "first_point", color=(r, g, b, 0.5))

def updateSelectionArea(self):
"""Update drawing selection area using self.points"""
Expand All @@ -538,7 +569,7 @@ def validate(self):
self.machine.cancel()

def closePolygon(self):
self.machine.resetSelectionArea()
self.machine.clearItems()
self.points[-1] = self.points[0]
eventDict = prepareDrawingSignal(
"drawingFinished", "polygon", self.points, self.machine.parameters
Expand All @@ -554,13 +585,10 @@ def onRelease(self, x, y, btn):
if btn == LEFT_BTN:
# checking if the position is close to the first point
# if yes : closing the "loop"
firstPos = self.machine.plot.dataToPixel(*self._firstPos, check=False)
dx, dy = abs(firstPos[0] - x), abs(firstPos[1] - y)

threshold = self.machine.getDragThreshold()
insideFirst = self.machine.insideMarker(x, y, "first_point")

# Only allow to close polygon after first point
if len(self.points) > 2 and dx <= threshold and dy <= threshold:
if len(self.points) > 2 and insideFirst:
self.closePolygon()
return False

Expand All @@ -580,6 +608,7 @@ def onRelease(self, x, y, btn):
*self.points[-2], check=False
)
dx, dy = abs(previousPos[0] - x), abs(previousPos[1] - y)
threshold = self.machine.getDragThreshold()
if dx >= threshold or dy >= threshold:
self.points.append(dataPos)
else:
Expand All @@ -589,12 +618,9 @@ def onRelease(self, x, y, btn):
return False

def onMove(self, x, y):
firstPos = self.machine.plot.dataToPixel(*self._firstPos, check=False)
dx, dy = abs(firstPos[0] - x), abs(firstPos[1] - y)
threshold = self.machine.getDragThreshold()

if dx <= threshold and dy <= threshold:
x, y = firstPos # Snap to first point
if self.machine.insideMarker(x, y, "first_point"):
# Snap to first point
x, y = self.machine.plot.dataToPixel(*self._firstPos, check=False)

dataPos = self.machine.plot.pixelToData(x, y)
assert dataPos is not None
Expand All @@ -607,7 +633,7 @@ def __init__(self, plot, parameters):

def cancel(self):
if isinstance(self.state, self.states["select"]):
self.resetSelectionArea()
self.clearItems()

def getDragThreshold(self):
"""Return dragging ratio with device to pixel ratio applied.
Expand Down Expand Up @@ -744,7 +770,7 @@ def select(self, x, y):
self.plot.notify(**eventDict)

def endSelect(self, x, y):
self.resetSelectionArea()
self.clearItems()

dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
Expand All @@ -759,7 +785,7 @@ def endSelect(self, x, y):
self.plot.notify(**eventDict)

def cancelSelect(self):
self.resetSelectionArea()
self.clearItems()


class SelectRectangle(Select2Points):
Expand Down Expand Up @@ -790,7 +816,7 @@ def select(self, x, y):
self.plot.notify(**eventDict)

def endSelect(self, x, y):
self.resetSelectionArea()
self.clearItems()

dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
Expand All @@ -801,7 +827,7 @@ def endSelect(self, x, y):
self.plot.notify(**eventDict)

def cancelSelect(self):
self.resetSelectionArea()
self.clearItems()


class SelectLine(Select2Points):
Expand All @@ -823,7 +849,7 @@ def select(self, x, y):
self.plot.notify(**eventDict)

def endSelect(self, x, y):
self.resetSelectionArea()
self.clearItems()

dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
Expand All @@ -834,7 +860,7 @@ def endSelect(self, x, y):
self.plot.notify(**eventDict)

def cancelSelect(self):
self.resetSelectionArea()
self.clearItems()


class Select1Point(Select):
Expand Down Expand Up @@ -904,15 +930,15 @@ def select(self, x, y):
self.plot.notify(**eventDict)

def endSelect(self, x, y):
self.resetSelectionArea()
self.clearItems()

eventDict = prepareDrawingSignal(
"drawingFinished", "hline", self._hLine(y), self.parameters
)
self.plot.notify(**eventDict)

def cancelSelect(self):
self.resetSelectionArea()
self.clearItems()


class SelectVLine(Select1Point):
Expand All @@ -939,15 +965,15 @@ def select(self, x, y):
self.plot.notify(**eventDict)

def endSelect(self, x, y):
self.resetSelectionArea()
self.clearItems()

eventDict = prepareDrawingSignal(
"drawingFinished", "vline", self._vLine(x), self.parameters
)
self.plot.notify(**eventDict)

def cancelSelect(self):
self.resetSelectionArea()
self.clearItems()


class DrawFreeHand(Select):
Expand Down Expand Up @@ -979,7 +1005,7 @@ def onMove(self, x, y):
def onRelease(self, x, y, btn):
if btn == LEFT_BTN:
if self.__isOut:
self.machine.resetSelectionArea()
self.machine.clearItems()
self.machine.endSelect(x, y)
self.goto("idle")

Expand Down Expand Up @@ -1040,10 +1066,10 @@ def endSelect(self, x, y):
self._points = None

def cancelSelect(self):
self.resetSelectionArea()
self.clearItems()

def cancel(self):
self.resetSelectionArea()
self.clearItems()


class SelectFreeLine(ClickOrDrag, _PlotInteraction):
Expand Down Expand Up @@ -1080,7 +1106,7 @@ def endDrag(self, startPos, endPos, btn):
self._processEvent(x, y, isLast=True)

def cancel(self):
self.resetSelectionArea()
self.clearItems()
self._points = []

def _processEvent(self, x, y, isLast):
Expand Down