Skip to content

Allow Selectors to be dragged from anywhere within their patch #19657

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 16, 2021
Merged
Show file tree
Hide file tree
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
9 changes: 9 additions & 0 deletions doc/users/next_whats_new/widget_dragging.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Dragging selectors
------------------

The `~matplotlib.widgets.RectangleSelector` and
`~matplotlib.widgets.EllipseSelector` have a new keyword argument,
*drag_from_anywhere*, which when set to `True` allows you to click and drag
from anywhere inside the selector to move it. Previously it was only possible
to move it by either activating the move modifier button, or clicking on the
central handle.
35 changes: 35 additions & 0 deletions lib/matplotlib/tests/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,41 @@ def test_rectangle_selector():
check_rectangle(rectprops=dict(fill=True))


@pytest.mark.parametrize('drag_from_anywhere, new_center',
[[True, (60, 75)],
[False, (30, 20)]])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're not dragging from the center, and you didn't enable the new option, why is the center now (30, 20) instead of remaining at (50, 65)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because dragging with drag_from_anywhere=False creates a new rectangle, which has a center (30, 20). I'll add some more comments to the test to make it clearer what's going on.

def test_rectangle_drag(drag_from_anywhere, new_center):
ax = get_ax()

def onselect(epress, erelease):
pass

tool = widgets.RectangleSelector(ax, onselect, interactive=True,
drag_from_anywhere=drag_from_anywhere)
# Create rectangle
do_event(tool, 'press', xdata=0, ydata=10, button=1)
do_event(tool, 'onmove', xdata=100, ydata=120, button=1)
do_event(tool, 'release', xdata=100, ydata=120, button=1)
assert tool.center == (50, 65)
# Drag inside rectangle, but away from centre handle
#
# If drag_from_anywhere == True, this will move the rectangle by (10, 10),
# giving it a new center of (60, 75)
#
# If drag_from_anywhere == False, this will create a new rectangle with
# center (30, 20)
do_event(tool, 'press', xdata=25, ydata=15, button=1)
do_event(tool, 'onmove', xdata=35, ydata=25, button=1)
do_event(tool, 'release', xdata=35, ydata=25, button=1)
assert tool.center == new_center
# Check that in both cases, dragging outside the rectangle draws a new
# rectangle
do_event(tool, 'press', xdata=175, ydata=185, button=1)
do_event(tool, 'onmove', xdata=185, ydata=195, button=1)
do_event(tool, 'release', xdata=185, ydata=195, button=1)
assert tool.center == (180, 190)


def test_ellipse():
"""For ellipse, test out the key modifiers"""
ax = get_ax()
Expand Down
31 changes: 25 additions & 6 deletions lib/matplotlib/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2189,7 +2189,8 @@ def __init__(self, ax, onselect, drawtype='box',
minspanx=0, minspany=0, useblit=False,
lineprops=None, rectprops=None, spancoords='data',
button=None, maxdist=10, marker_props=None,
interactive=False, state_modifier_keys=None):
interactive=False, state_modifier_keys=None,
drag_from_anywhere=False):
r"""
Parameters
----------
Expand Down Expand Up @@ -2261,13 +2262,18 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent)
default: "ctrl".

"square" and "center" can be combined.

drag_from_anywhere : bool, optional
If `True`, the widget can be moved by clicking anywhere within
its bounds.
"""
super().__init__(ax, onselect, useblit=useblit, button=button,
state_modifier_keys=state_modifier_keys)

self.to_draw = None
self.visible = True
self.interactive = interactive
self.drag_from_anywhere = drag_from_anywhere

if drawtype == 'none': # draw a line but make it invisible
drawtype = 'line'
Expand Down Expand Up @@ -2407,8 +2413,9 @@ def _onmove(self, event):
y1 = event.ydata

# move existing shape
elif (('move' in self.state or self.active_handle == 'C')
and self._extents_on_press is not None):
elif (('move' in self.state or self.active_handle == 'C' or
(self.drag_from_anywhere and self._contains(event))) and
self._extents_on_press is not None):
x0, x1, y0, y1 = self._extents_on_press
dx = event.xdata - self.eventpress.xdata
dy = event.ydata - self.eventpress.ydata
Expand Down Expand Up @@ -2539,16 +2546,24 @@ def _set_active_handle(self, event):
if 'move' in self.state:
self.active_handle = 'C'
self._extents_on_press = self.extents

# Set active handle as closest handle, if mouse click is close enough.
elif m_dist < self.maxdist * 2:
# Prioritise center handle over other handles
self.active_handle = 'C'
elif c_dist > self.maxdist and e_dist > self.maxdist:
self.active_handle = None
return
# Not close to any handles
if self.drag_from_anywhere and self._contains(event):
# Check if we've clicked inside the region
self.active_handle = 'C'
self._extents_on_press = self.extents
else:
self.active_handle = None
return
elif c_dist < e_dist:
# Closest to a corner handle
self.active_handle = self._corner_order[c_idx]
else:
# Closest to an edge handle
self.active_handle = self._edge_order[e_idx]

# Save coordinates of rectangle at the start of handle movement.
Expand All @@ -2560,6 +2575,10 @@ def _set_active_handle(self, event):
y0, y1 = y1, event.ydata
self._extents_on_press = x0, x1, y0, y1

def _contains(self, event):
"""Return True if event is within the patch."""
return self.to_draw.contains(event, radius=0)[0]

@property
def geometry(self):
"""
Expand Down