""" Components/Tooltip ================== .. seealso:: `Material Design spec, Tooltips `_ .. rubric:: Tooltips display informative text when users hover over, focus on, or tap an element. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tooltip.png :align: center To use the :class:`~MDTooltip` class, you must create a new class inherited from the :class:`~MDTooltip` class: In Kv-language: .. code-block:: kv In Python code: .. code-block:: python class TooltipMDIconButton(MDIconButton, MDTooltip): pass .. Warning:: :class:`~MDTooltip` only works correctly with button and label classes. .. code-block:: python from kivy.lang import Builder from kivymd.app import MDApp KV = ''' MDScreen: TooltipMDIconButton: icon: "language-python" tooltip_text: self.icon pos_hint: {"center_x": .5, "center_y": .5} ''' class Test(MDApp): def build(self): return Builder.load_string(KV) Test().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tooltip.gif :align: center .. Note:: The behavior of tooltips on desktop and mobile devices is different. For more detailed information, `click here `_. """ __all__ = ("MDTooltip", "MDTooltipViewClass") import os from typing import Union from kivy.animation import Animation from kivy.clock import Clock from kivy.core.window import Window from kivy.lang import Builder from kivy.metrics import dp from kivy.properties import ( BoundedNumericProperty, ColorProperty, ListProperty, NumericProperty, OptionProperty, StringProperty, ) from kivy.uix.boxlayout import BoxLayout from kivymd import uix_path from kivymd.font_definitions import theme_font_styles from kivymd.material_resources import DEVICE_TYPE from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import HoverBehavior, TouchBehavior with open( os.path.join(uix_path, "tooltip", "tooltip.kv"), encoding="utf-8" ) as kv_file: Builder.load_string(kv_file.read()) class MDTooltip(ThemableBehavior, HoverBehavior, TouchBehavior): tooltip_bg_color = ColorProperty(None) """ Tooltip background color in ``rgba`` format. :attr:`tooltip_bg_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ tooltip_text_color = ColorProperty(None) """ Tooltip text color in ``rgba`` format. :attr:`tooltip_text_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ tooltip_text = StringProperty() """ Tooltip text. :attr:`tooltip_text` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ tooltip_font_style = OptionProperty("Caption", options=theme_font_styles) """ Tooltip font style. Available options are: `'H1'`, `'H2'`, `'H3'`, `'H4'`, `'H5'`, `'H6'`, `'Subtitle1'`, `'Subtitle2'`, `'Body1'`, `'Body2'`, `'Button'`, `'Caption'`, `'Overline'`, `'Icon'`. :attr:`tooltip_font_style` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Caption'`. """ tooltip_radius = ListProperty( [ dp(7), ] ) """ Corner radius values. :attr:`radius` is an :class:`~kivy.properties.ListProperty` and defaults to `[dp(7),]`. """ tooltip_display_delay = BoundedNumericProperty(0, min=0, max=4) """ Tooltip dsiplay delay. :attr:`tooltip_display_delay` is an :class:`~kivy.properties.BoundedNumericProperty` and defaults to `0`, min of `0` & max of `4`. This property only works on desktop. """ shift_y = NumericProperty() """ Y-offset of tooltip text. :attr:`shift_y` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ shift_right = NumericProperty() """ Shifting the tooltip text to the right. .. versionadded:: 1.0.0 :attr:`shift_right` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ shift_left = NumericProperty() """ Shifting the tooltip text to the left. .. versionadded:: 1.0.0 :attr:`shift_left` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ _tooltip = None def __init__(self, **kwargs): super().__init__(**kwargs) self.register_event_type("on_show") self.register_event_type("on_dismiss") def delete_clock(self, widget, touch, *args): if self.collide_point(touch.x, touch.y) and touch.grab_current: try: Clock.unschedule(touch.ud["event"]) except KeyError: pass self.on_leave() def adjust_tooltip_position(self, x: float, y: float) -> tuple: """ Returns the coordinates of the tooltip that fit into the borders of the screen. """ # If the position of the tooltip is outside the right border # of the screen. if x + self._tooltip.width > Window.width: x = Window.width - (self._tooltip.width + dp(10)) else: # If the position of the tooltip is outside the left border # of the screen. if x < 0: x = "10dp" # If the tooltip position is below bottom the screen border. if y < 0: y = dp(10) # If the tooltip position is below top the screen border. else: if Window.height - self._tooltip.height < y: y = Window.height - (self._tooltip.height + dp(10)) return x, y def display_tooltip(self, interval: Union[int, float]) -> None: if not self._tooltip or self._tooltip.parent: return Window.add_widget(self._tooltip) pos = self.to_window(self.center_x, self.center_y) if not self.shift_right and not self.shift_left: x = pos[0] - (self._tooltip.width / 2) else: if self.shift_right: x = pos[0] - (self._tooltip.width / 2) + self.shift_right if self.shift_left: x = pos[0] - (self._tooltip.width / 2) - self.shift_left if not self.shift_y: y = pos[1] - self._tooltip.height / 2 - self.height / 2 - dp(20) else: y = pos[1] - self._tooltip.height / 2 - self.height + self.shift_y x, y = self.adjust_tooltip_position(x, y) self._tooltip.pos = (x, y) if DEVICE_TYPE == "desktop": Clock.schedule_once( self.animation_tooltip_show, self.tooltip_display_delay ) else: Clock.schedule_once(self.animation_tooltip_show, 0) def animation_tooltip_show(self, interval: Union[int, float]) -> None: """Animation of opening tooltip on the screen.""" if self._tooltip: ( Animation(_scale_x=1, _scale_y=1, d=0.1) + Animation(opacity=1, d=0.2) ).start(self._tooltip) self.dispatch("on_show") def animation_tooltip_dismiss(self, interval: Union[int, float]) -> None: """ .. versionadded:: 1.0.0 Animation of closing tooltip on the screen. """ if self._tooltip: anim = Animation(_scale_x=0, _scale_y=0, d=0.1) + Animation( opacity=0, d=0.2 ) anim.bind(on_complete=self._on_dismiss_anim_complete) anim.start(self._tooltip) def remove_tooltip(self, *args) -> None: """Removes the tooltip widget from the screen.""" Window.remove_widget(self._tooltip) def on_long_touch(self, touch, *args) -> None: if DEVICE_TYPE != "desktop": self.on_enter() def on_enter(self, *args) -> None: """ See :attr:`~kivymd.uix.behaviors.hover_behavior.HoverBehavior.on_enter` method in :class:`~kivymd.uix.behaviors.hover_behavior.HoverBehavior` class. """ if self.tooltip_text: if self._tooltip: self.remove_tooltip() self._tooltip = MDTooltipViewClass( tooltip_bg_color=self.tooltip_bg_color, tooltip_text_color=self.tooltip_text_color, tooltip_text=self.tooltip_text, tooltip_font_style=self.tooltip_font_style, tooltip_radius=self.tooltip_radius, ) Clock.schedule_once(self.display_tooltip, -1) def on_leave(self) -> None: """ See :attr:`~kivymd.uix.behaviors.hover_behavior.HoverBehavior.on_leave` method in :class:`~kivymd.uix.behaviors.hover_behavior.HoverBehavior` class. """ if self._tooltip: Clock.schedule_once(self.animation_tooltip_dismiss) def on_show(self) -> None: """Default dismiss event handler.""" def on_dismiss(self) -> None: """ .. versionadded:: 1.0.0 Default dismiss event handler. """ def _on_dismiss_anim_complete(self, *args): self.dispatch("on_dismiss") self.remove_tooltip() self._tooltip = None class MDTooltipViewClass(ThemableBehavior, BoxLayout): tooltip_bg_color = ColorProperty(None) """ See :attr:`~MDTooltip.tooltip_bg_color`. """ tooltip_text_color = ColorProperty(None) """ See :attr:`~MDTooltip.tooltip_text_color`. """ tooltip_text = StringProperty() """ See :attr:`~MDTooltip.tooltip_text`. """ tooltip_font_style = OptionProperty("Caption", options=theme_font_styles) """ See :attr:`~MDTooltip.tooltip_font_style`. """ tooltip_radius = ListProperty() """ See :attr:`~MDTooltip.tooltip_radius`. """ _scale_x = NumericProperty(0) _scale_y = NumericProperty(0) def __init__(self, **kwargs): super().__init__(**kwargs) self.padding = [ dp(8) if DEVICE_TYPE == "desktop" else dp(16), dp(4), dp(8) if DEVICE_TYPE == "desktop" else dp(16), dp(4), ]