Source code for enable.tools.drag_tool
# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
""" Defines the base DragTool class.
"""
import warnings
# Enthought library imports
from enable.base_tool import BaseTool, KeySpec
from traits.api import Bool, Enum, List, Property, Str, Tuple, cached_property
[docs]class DragTool(BaseTool):
""" Base class for tools that are activated by a drag operation.
This tool insulates the drag operation from double clicks and the like, and
gracefully manages the transition into and out of drag mode.
"""
# The mouse button used for this drag operation.
drag_button = Enum("left", "right")
# Deprecated; on_drag_leave is the new, more flexible / intuitive means for
# providing this functionality
# Cancel the drag operation if the mouse leaves the associated component?
# NOTE: This behavior depends on "mouse_leave" events, which in general
# are not fired when `capture_mouse` is True (default).
end_drag_on_leave = Bool(False)
# Do nothing, cancel or end the drag operation if the mouse leaves the
# associated component?
# NOTE: This behavior depends on "mouse_leave" events, which in general
# are not fired when `capture_mouse` is True (default).
on_drag_leave = Enum(None, 'cancel', 'end')
# These keys, if pressed during drag, cause the drag operation to reset.
cancel_keys = List(Str, ["Esc"])
# The position of the initial mouse click that started the drag.
# Typically, tools that move things around use this
# position to do hit-testing to determine what object to "pick up".
mouse_down_position = Tuple(0.0, 0.0)
# The modifier key that must be used to activate the tool.
modifier_key = Enum("none", "shift", "alt", "control")
# Whether or not to capture the mouse during the drag operation. In effect,
# this routes mouse events back to this tool for dispatching, rather than
# allowing the event to be handled by the window. This may have effects
# surrounding "mouse_leave" events: see note on `on_drag_leave` flag.
capture_mouse = Bool(True)
# ------------------------------------------------------------------------
# Private traits used by DragTool
# ------------------------------------------------------------------------
# The possible states of this tool.
_drag_state = Enum("nondrag", "dragging")
# Records whether a mouse_down event has been received while in
# "nondrag" state. This is a safety check to prevent the tool from
# suddenly getting mouse focus while the mouse button is down (either from
# window_enter or programmatically) and erroneously
# initiating a drag.
_mouse_down_received = Bool(False)
# private property to hold the current list of KeySpec instances of the
# cancel keys
_cancel_keys = Property(List(KeySpec), observe="cancel_keys")
# ------------------------------------------------------------------------
# Interface for subclasses
# ------------------------------------------------------------------------
[docs] def is_draggable(self, x, y):
""" Returns whether the (x,y) position is in a region that is OK to
drag.
Used by the tool to determine when to start a drag.
"""
return True
[docs] def drag_start(self, event):
""" Called when the drag operation starts.
The *event* parameter is the mouse event that established the drag
operation; its **x** and **y** attributes correspond to the current
location of the mouse, and not to the position of the mouse when the
initial left_down or right_down event happened.
"""
pass
[docs] def dragging(self, event):
""" This method is called for every mouse_move event that the tool
receives while the user is dragging the mouse.
It is recommended that subclasses do most of their work in this method.
"""
pass
[docs] def drag_cancel(self, event):
""" Called when the drag is cancelled.
A drag is usually cancelled by receiving a mouse_leave event when
end_drag_on_leave is True, or on_drag_leave is 'cancel', or by the user
pressing any of the **cancel_keys**.
"""
pass
[docs] def drag_end(self, event):
""" Called when a mouse event causes the drag operation to end.
A drag is ended when a user releases the mouse, or by receiving a
mouse_leave event when on_drag_leave is 'end'.
"""
pass
# ------------------------------------------------------------------------
# Private methods for handling drag
# ------------------------------------------------------------------------
def _dispatch_stateful_event(self, event, suffix):
# We intercept a lot of the basic events and re-map them if
# necessary. "consume" indicates whether or not we should pass
# the event to the subclass's handlers.
consume = False
if suffix == self.drag_button + "_down":
consume = self._drag_button_down(event)
elif suffix == self.drag_button + "_up":
consume = self._drag_button_up(event)
elif suffix == "mouse_move":
consume = self._drag_mouse_move(event)
elif suffix == "mouse_leave":
consume = self._drag_mouse_leave(event)
elif suffix == "mouse_enter":
consume = self._drag_mouse_enter(event)
elif suffix == "key_pressed":
consume = self._drag_cancel_keypressed(event)
if not consume:
BaseTool._dispatch_stateful_event(self, event, suffix)
else:
event.handled = True
def _cancel_drag(self, event):
self._drag_state = "nondrag"
outcome = self.drag_cancel(event)
self._mouse_down_received = False
if event.window.mouse_owner is self:
event.window.set_mouse_owner(None)
return outcome
def _end_drag(self, event):
self._drag_state = "nondrag"
outcome = self.drag_end(event)
self._mouse_down_received = False
if event.window.mouse_owner is self:
event.window.set_mouse_owner(None)
return outcome
def _drag_cancel_keypressed(self, event):
if (self._drag_state != "nondrag"
and any(map(lambda x: x.match(event), self._cancel_keys))):
return self._cancel_drag(event)
else:
return False
def _drag_mouse_move(self, event):
state = self._drag_state
button_down = getattr(event, self.drag_button + "_down")
if state == "nondrag":
if (button_down
and self._mouse_down_received
and self.is_draggable(*self.mouse_down_position)):
self._drag_state = "dragging"
if self.capture_mouse:
event.window.set_mouse_owner(
self,
transform=event.net_transform(),
history=event.dispatch_history,
)
self.drag_start(event)
return self._drag_mouse_move(event)
return False
elif state == "dragging":
if button_down:
return self.dragging(event)
else:
return self._drag_button_up(event)
# If we don't invoke the subclass drag handler, then don't consume the
# event.
return False
def _drag_button_down(self, event):
if self._drag_state == "nondrag":
self.mouse_down_position = (event.x, event.y)
self._mouse_down_received = True
return False
def _drag_button_up(self, event):
self._mouse_down_received = False
state = self._drag_state
if event.window.mouse_owner == self:
event.window.set_mouse_owner(None)
if state == "dragging":
self._drag_state = "nondrag"
return self.drag_end(event)
# If we don't invoke the subclass drag handler, then don't consume the
# event.
return False
def _drag_mouse_leave(self, event):
if self.end_drag_on_leave:
# raise deprecation warning
msg = ("end_drag_on_leave is now deprecated as its name was "
"misleading. It triggers a drag_cancel not drag_end on "
"leave. Use new on_drag_end Enum trait instead.")
warnings.warn(
msg,
category=DeprecationWarning,
)
if self._drag_state == "dragging":
return self._cancel_drag(event)
return False
if self.on_drag_leave == "cancel" and self._drag_state == "dragging":
return self._cancel_drag(event)
elif self.on_drag_leave == "end" and self._drag_state == "dragging":
return self._end_drag(event)
return False
def _drag_mouse_enter(self, event):
state = self._drag_state
if state == "nondrag":
pass
elif state == "dragging":
pass
return False
# ------------------------------------------------------------------------
# Private methods for trait getter/setters
# ------------------------------------------------------------------------
@cached_property
def _get__cancel_keys(self):
return [KeySpec(key) for key in self.cancel_keys]