Sideband/sbapp/kivymd/uix/behaviors/hover_behavior.py

235 lines
7.4 KiB
Python

"""
Behaviors/Hover
===============
.. rubric:: Changing when the mouse is on the widget and the widget is visible.
To apply hover behavior, you must create a new class that is inherited from the
widget to which you apply the behavior and from the :attr:`HoverBehavior` class.
In `KV file`:
.. code-block:: kv
<HoverItem@MDBoxLayout+HoverBehavior>
In `python file`:
.. code-block:: python
class HoverItem(MDBoxLayout, HoverBehavior):
'''Custom item implementing hover behavior.'''
After creating a class, you must define two methods for it:
:attr:`HoverBehavior.on_enter` and :attr:`HoverBehavior.on_leave`, which will be automatically called
when the mouse cursor is over the widget and when the mouse cursor goes beyond
the widget.
.. note::
:class:`~HoverBehavior` will by default check to see if the current Widget is visible (i.e. not covered by a modal or popup and not a part of a Relative Layout, MDTab or Carousel that is not currently visible etc) and will only issue events if the widget is visible.
To get the legacy behavior that the events are always triggered, you can set `detect_visible` on the Widget to `False`.
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors import HoverBehavior
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
Screen
MDBoxLayout:
id: box
pos_hint: {'center_x': .5, 'center_y': .5}
size_hint: .8, .8
md_bg_color: app.theme_cls.bg_darkest
'''
class HoverItem(MDBoxLayout, HoverBehavior):
'''Custom item implementing hover behavior.'''
def on_enter(self, *args):
'''The method will be called when the mouse cursor
is within the borders of the current widget.'''
self.md_bg_color = (1, 1, 1, 1)
def on_leave(self, *args):
'''The method will be called when the mouse cursor goes beyond
the borders of the current widget.'''
self.md_bg_color = self.theme_cls.bg_darkest
class Test(MDApp):
def build(self):
self.screen = Builder.load_string(KV)
for i in range(5):
self.screen.ids.box.add_widget(HoverItem())
return self.screen
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hover-behavior.gif
:width: 250 px
:align: center
"""
__all__ = ("HoverBehavior",)
from kivy.core.window import Window
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.uix.widget import Widget
class HoverBehavior(object):
"""
:Events:
:attr:`on_enter`
Called when mouse enters the bbox of the widget AND the widget is visible
:attr:`on_leave`
Called when the mouse exits the widget AND the widget is visible
"""
hovering = BooleanProperty(False)
"""
`True`, if the mouse cursor is within the borders of the widget.
Note that this is set and cleared even if the widget is not visible
:attr:`hover` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
hover_visible = BooleanProperty(False)
"""
`True` if hovering is True AND is the current widget is visible
:attr:`hover_visible` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
enter_point = ObjectProperty(allownone=True)
"""
Holds the last position where the mouse pointer crossed into the Widget
if the Widget is visible and is currently in a hovering state
:attr:`enter_point` is a :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
detect_visible = BooleanProperty(True)
"""
Should this widget perform the visibility check?
:attr:`detect_visible` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
def __init__(self, **kwargs):
self.register_event_type("on_enter")
self.register_event_type("on_leave")
Window.bind(mouse_pos=self.on_mouse_update)
super(HoverBehavior, self).__init__(**kwargs)
def on_mouse_update(self, *args):
# If the Widget currently has no parent, do nothing
if not self.get_root_window():
return
pos = args[1]
#
# is the pointer in the same position as the widget?
# If not - then issue an on_exit event if needed
#
if not self.collide_point(*self.to_widget(*pos)):
self.hovering = False
self.enter_point = None
if self.hover_visible:
self.hover_visible = False
self.dispatch("on_leave")
return
#
# The pointer is in the same position as the widget
#
if self.hovering:
#
# nothing to do here. Not - this does not handle the case where
# a popup comes over an existing hover event.
# This seems reasonable
#
return
#
# Otherwise - set the hovering attribute
#
self.hovering = True
#
# We need to traverse the tree to see if the Widget is visible
#
# This is a two stage process:
# - first go up the tree to the root Window.
# At each stage - check that the Widget is actually visible
# - Second - At the root Window check that there is not another branch
# covering the Widget
#
self.hover_visible = True
if self.detect_visible:
widget: Widget = self
while True:
# Walk up the Widget tree from the target Widget
parent = widget.parent
try:
# See if the mouse point collides with the parent
# using both local and glabal coordinates to cover absoluet and relative layouts
pinside = parent.collide_point(
*parent.to_widget(*pos)
) or parent.collide_point(*pos)
except Exception:
# The collide_point will error when you reach the root Window
break
if not pinside:
self.hover_visible = False
break
# Iterate upwards
widget = parent
#
# parent = root window
# widget = first Widget on the current branch
#
children = parent.children
for child in children:
# For each top level widget - check if is current branch
# If it is - then break.
# If not then - since we start at 0 - this widget is visible
#
# Check to see if it should take the hover
#
if child == widget:
# this means that the current widget is visible
break
if child.collide_point(*pos):
# this means that the current widget is covered by a modal or popup
self.hover_visible = False
break
if self.hover_visible:
self.enter_point = pos
self.dispatch("on_enter")
def on_enter(self):
"""Called when mouse enters the bbox of the widget AND the widget is visible."""
def on_leave(self):
"""Called when the mouse exits the widget AND the widget is visible."""