Exemplo n.º 1
0
class MDTextFieldRound(ThemableBehavior, TextInput):
    icon_left = StringProperty()
    """
    Left icon.

    :attr:`icon_left` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    icon_left_color = ColorProperty((0, 0, 0, 1))
    """
    Color of left icon in ``rgba`` format.

    :attr:`icon_left_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `(0, 0, 0, 1)`.
    """

    icon_right = StringProperty()
    """
    Right icon.

    :attr:`icon_right` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    icon_right_color = ColorProperty((0, 0, 0, 1))
    """
    Color of right icon.

    :attr:`icon_right_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `(0, 0, 0, 1)`.
    """

    line_color = ColorProperty(None)
    """
    Field line color.

    :attr:`line_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    normal_color = ColorProperty(None)
    """
    Field color if `focus` is `False`.

    :attr:`normal_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    color_active = ColorProperty(None)
    """
    Field color if `focus` is `True`.

    :attr:`color_active` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    _color_active = ColorProperty(None)
    _icon_left_color_copy = ColorProperty(None)
    _icon_right_color_copy = ColorProperty(None)

    def __init__(self, **kwargs):
        self._lbl_icon_left = MDIcon(theme_text_color="Custom")
        self._lbl_icon_right = MDIcon(theme_text_color="Custom")
        super().__init__(**kwargs)
        self.cursor_color = self.theme_cls.primary_color
        self.icon_left_color = self.theme_cls.text_color
        self.icon_right_color = self.theme_cls.text_color

        if not self.normal_color:
            self.normal_color = self.theme_cls.primary_light
        if not self.line_color:
            self.line_color = self.theme_cls.primary_dark
        if not self.color_active:
            self._color_active = (0.5, 0.5, 0.5, 0.5)

    def on_focus(self, instance, value):
        if value:
            self.icon_left_color = self.theme_cls.primary_color
            self.icon_right_color = self.theme_cls.primary_color
        else:
            self.icon_left_color = (self._icon_left_color_copy
                                    or self.theme_cls.text_color)
            self.icon_right_color = (self._icon_right_color_copy
                                     or self.theme_cls.text_color)

    def on_icon_left(self, instance, value):
        self._lbl_icon_left.icon = value

    def on_icon_left_color(self, instance, value):
        self._lbl_icon_left.text_color = value
        if (not self._icon_left_color_copy
                and value != self.theme_cls.text_color
                and value != self.theme_cls.primary_color):
            self._icon_left_color_copy = value

    def on_icon_right(self, instance, value):
        self._lbl_icon_right.icon = value

    def on_icon_right_color(self, instance, value):
        self._lbl_icon_right.text_color = value
        if (not self._icon_right_color_copy
                and value != self.theme_cls.text_color
                and value != self.theme_cls.primary_color):
            self._icon_right_color_copy = value

    def on_color_active(self, instance, value):
        if value != [0, 0, 0, 0.5]:
            self._color_active = value
            self._color_active[-1] = 0.5
        else:
            self._color_active = value
Exemplo n.º 2
0
class DecentraListItem(ThemableBehavior, RectangularRippleBehavior, MDBoxLayout):
    text = StringProperty()
    secondary_text = StringProperty()
    tertiary_text = StringProperty()
    bar_color = ColorProperty((1, 0, 0, 1))
class InventoryScreen(Screen, BetterLogger):
    merge_layout_cover_color = ColorProperty([0, 0, 0, 0])

    merge_option: str = None

    current_merge_output_button_id = "unknown_item"
    current_recipe_button_id = "unknown_item"

    def __init__(self, **kwargs):
        BetterLogger.__init__(self)
        Screen.__init__(self, **kwargs)

    def on_pre_enter(self, *args):
        self.ids["inventory_items_holder"].bind(
            size=self.on_inventory_items_holder_size)
        self.ids["merge_gui"].bind(
            on_items=self.update_merge_option_gui_output)
        self.update_inventory()

        self.merge_option_button_clicked(
            graphicsConfig.get("InventoryScreen", "default_merge_option_id"))

    def update_inventory(self):
        self.ids["inventory_items_holder"].clear_widgets()

        if self.merge_option == "recipes":
            unordered_items: dict[str, int] = dict(
                GameConfig.get("Items", "recipes"))
            # TODO: order items
            items = unordered_items

            for item in items.keys():
                b = TextBetterButton(button_id=str(item) + "_item",
                                     size_type="big",
                                     show_amount_text=True)
                b.bind(on_release=ignore_args(self.item_pressed, b))
                b.button_storage = str(item)
                self.ids["inventory_items_holder"].add_widget(b)

                self.log_deep_debug("Added button -", b)

        else:
            unordered_items: dict[str, int] = dict(gameData.get("inventory"))
            # TODO: order items
            items = unordered_items

            for item, amount in items.items():
                b = TextBetterButton(button_id=str(item) + "_item",
                                     size_type="big",
                                     show_amount_text=True,
                                     amount=amount)
                b.bind(on_release=ignore_args(self.item_pressed, b))
                b.button_storage = str(item)
                self.ids["inventory_items_holder"].add_widget(b)

                self.log_deep_debug("Added button -", b)

    def item_pressed(self, button: TextBetterButton):
        if self.merge_option == "place":
            building_type = str(button.button_storage)

            if building_type in GameConfig.get("Buildings", "list"):
                gameData.move_to_placed_buildings(building_type)

                self.update_inventory()

            else:
                self.log_deep_debug("Item", building_type,
                                    "was clicked on but is not a building")

        elif self.merge_option == "recipes":
            item = str(button.button_storage)

            if item in GameConfig.get("Items", "recipes"):
                recipe = GameConfig.get("Items", "recipes", item)
                self.log_deep_debug("Creating GUI for recipe of item", item,
                                    "| Recipe is", recipe)

                self.ids["recipe_gui"].set_all(recipe)
                self.current_recipe_button_id = item + "_item"
                self.ids["merge_output_button"].button_id = item + "_item"

            else:
                self.log_deep_debug(
                    "Item", item,
                    "was clicked on but is doesnt have a merge recipe")

        elif self.merge_option == "merge":
            item = str(button.button_storage)
            touch: MotionEvent = button.last_touch
            item_large_move_amount = graphicsConfig.getint(
                "InventoryScreen", "item_large_move_amount")

            if (touch.is_double_tap or touch.is_triple_tap) and \
                    (self.ids["merge_gui"].get_moved_amount(item) < gameData.get("inventory")[item] -
                     (item_large_move_amount - 1)):
                self.ids["merge_gui"].add(item, item_large_move_amount)

            elif self.ids["merge_gui"].get_moved_amount(item) < gameData.get(
                    "inventory")[item]:
                self.ids["merge_gui"].add(item, 1)

            else:
                self.log_deep_debug(
                    "Item was pressed while merge mode active cant move anymore because all "
                    "have already been moved")

        else:
            self.log_critical("No know merge option", self.merge_option)

    def update_merge_option_gui_output(
            self, instance):  # TODO: Show how many you can make
        items = list(instance.get_all())
        self.log_deep_debug("Updating merge option gui output with items -",
                            items)
        has_changed_image = False

        for recipe_product, recipe in GameConfig.get("Items",
                                                     "recipes").items():
            correct = True

            for i2 in items:
                if i2[0] not in recipe.keys():
                    correct = False

            for i1 in recipe.items():
                matched = False
                for i2 in items:
                    if i1[0] == i2[0]:
                        if i1[1] <= i2[1]:
                            self.log_deep_debug(
                                "Correct match for merge recipe part", i1, i2)
                            matched = True

                if not matched:
                    try:
                        # noinspection PyUnboundLocalVariable
                        self.log_deep_debug("No merge recipe part", i1, i2)
                    except UnboundLocalError:  # Nothing in items dict
                        pass

                    correct = False

            if correct:
                self.log_deep_debug(
                    "All merge recipe part matches found, item is",
                    recipe_product)
                self.ids["merge_output_button"].button_id = str(
                    recipe_product) + "_item"
                self.ids["merge_output_button"].button_storage = str(
                    recipe_product)
                has_changed_image = True
                break

        if not has_changed_image:
            self.ids["merge_output_button"].button_id = "unknown_item"
            self.ids["merge_output_button"].button_storage = None

    def do_merge(self, product):
        if self.merge_option == "merge":
            if product is not None:

                for item, amount in GameConfig.get("Items", "recipes",
                                                   product).items():
                    self.ids["merge_gui"].remove(item, amount)
                    gameData.set("inventory",
                                 item,
                                 to=gameData.getint("inventory", item) -
                                 amount)

                    if gameData.getint("inventory", item) == 0:
                        gameData.remove("inventory", item)

                try:
                    gameData.set("inventory",
                                 product,
                                 to=gameData.getint("inventory", product) + 1)

                except KeyError:
                    gameData.set("inventory", product, to=1)

            else:
                self.log_deep_debug(
                    "merge_output button was pressed but nothing inside, ignoring it"
                )

        else:
            self.log_deep_debug(
                "merge_output button was pressed while merge_mode is not \"merge\", ignoring it"
            )

        self.update_inventory()

    def on_touch_down(self, touch):
        Screen.on_touch_down(self, touch)

    def on_inventory_items_holder_size(self, _instance, _size):
        holder_width = self.ids["inventory_items_holder"].width

        button_size = height() * graphicsConfig.getfloat(
            "Buttons", "size_hint_y_big")

        buildings_per_row = int(holder_width / button_size)
        extra_space = holder_width - (buildings_per_row * button_size)

        for building in self.ids["inventory_items_holder"].children:
            building.width = (height() * graphicsConfig.getfloat("Buttons", "size_hint_y_big")) + \
                             (extra_space / buildings_per_row)
            building.height = height() * graphicsConfig.getfloat(
                "Buttons", "size_hint_y_big")

            # TODO: fix bug where not proper sizing on first open

    def merge_option_button_clicked(self, id_of_clicked: str):
        outer_color = graphicsConfig.getdict("Buttons", "flat_color")
        label_color = graphicsConfig.getdict("Buttons", "flat_label_color")

        button: FlatBetterButton
        for button_id in self.ids:
            if button_id != "merge_option_buttons_holder" and str(
                    button_id).startswith("merge_option_"):
                button = self.ids[button_id]

                if button_id == id_of_clicked:  # TODO: Fix selected text coloring, for some reason its the wrong blue
                    button.bg_color = label_color
                    button.label_color = outer_color

                else:
                    button.bg_color = outer_color
                    button.label_color = label_color

        merge_gui: MergeGUI = self.ids["merge_gui"]
        recipe_gui: MergeGUI = self.ids["recipe_gui"]
        handled = False

        if id_of_clicked == "merge_option_place":
            handled = True

            self.merge_layout_cover_color = graphicsConfig.getdict(
                "InventoryScreen", "merge_cover_active_color")
            self.merge_option = "place"

            merge_gui.active = False
            recipe_gui.active = False

            self.ids["merge_output_button"].button_id = "unknown_item"

        else:
            self.merge_layout_cover_color = 0, 0, 0, 0

        if id_of_clicked == "merge_option_recipes":
            handled = True
            self.log_deep_debug("Switched merge option to recipes")

            self.merge_option = "recipes"

            merge_gui.active = False
            recipe_gui.active = True

            self.ids[
                "merge_output_button"].button_id = self.current_recipe_button_id

        if id_of_clicked == "merge_option_merge":
            handled = True
            self.log_deep_debug("Switched merge option to merge")

            self.merge_option = "merge"

            merge_gui.active = True
            recipe_gui.active = False

            self.ids[
                "merge_output_button"].button_id = self.current_merge_output_button_id

        if not handled:
            self.log_critical("No know merge option", id_of_clicked)

        self.update_inventory()
Exemplo n.º 4
0
class MasterColour(object):
    pseudo_bind_master_colour_attribute = StringProperty('foreground_colour',
                                                         allownone=True)
    _master_colour = ColorProperty()

    def get_master_colour(self):
        """ I could also get it from the shape list, in concentric shapes.
            and for some other widgets this could be self.background_color or even maybe self.color """
        return self._master_colour

    def set_master_colour(self, value):
        """ This function is 'continued' in concentricshapes """

        if value == None:
            return

        if all((True if type(x) == str else False for x in value)):
            colour_attribute_for_master_colour = ''.join(value)
        else:
            colour_attribute_for_master_colour = None
        if colour_attribute_for_master_colour:
            if colour_attribute_for_master_colour[0] == '#':
                self._master_colour = rgba(colour_attribute_for_master_colour)
                self.pseudo_bind_master_colour_attribute = None
            elif hasattr(self, colour_attribute_for_master_colour):
                self.pseudo_bind_master_colour_attribute = colour_attribute_for_master_colour
            else:
                raise Exception(
                    "{} doesnt start with a '#' to denote its an rgba value"
                    ", nor is the value found as an attribute of this class {}"
                    .format(value, self))
        elif type(value) in (list, tuple, ColorProperty):
            self._master_colour = tuple(value)
            self.pseudo_bind_master_colour_attribute = None
        elif issubclass(type(value), list):
            self._master_colour = tuple(value)
            self.pseudo_bind_master_colour_attribute = None
        else:
            raise Exception("Couldn't set value {} as master colour")
        """ This function is 'continued' in concentricshapes """

        self.do_colour_update()

    def do_colour_update(self, *args):
        pass

    def set_master_to_colour_attribute(self,
                                       colour_attribute_for_master_colour,
                                       *args):
        self.master_colour = getattr(self, colour_attribute_for_master_colour)

    master_colour = AliasProperty(get_master_colour, set_master_colour)

    def pseudo_bind_master_colour(self, attribute, wid, value):
        if self.pseudo_bind_master_colour_attribute == attribute:
            self.master_colour = value
            self.pass_master_colour_to_children(wid, value)

    """ the bellow function could use a list of widgets to apply the scheme to """

    def pass_master_colour_to_children(self, wid, colour):
        """ this function is expanded on in subclasses of colourwidget """
        pass
Exemplo n.º 5
0
class MDDropdownMenu(ThemableBehavior, FloatLayout):
    """
    :Events:
        :attr:`on_enter`
            Call when mouse enter the bbox of item menu.
        :attr:`on_leave`
            Call when the mouse exit the item menu.
        :attr:`on_dismiss`
            Call when closes menu.
        :attr:`on_release`
            The method that will be called when you click menu items.
    """

    selected_color = ColorProperty(None)
    """Custom color (``rgba`` format) for list item when hover behavior occurs.

    :attr:`selected_color` is a :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    items = ListProperty()
    """
    See :attr:`~kivy.uix.recycleview.RecycleView.data`.

    :attr:`items` is a :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    width_mult = NumericProperty(1)
    """
    This number multiplied by the standard increment (56dp on mobile,
    64dp on desktop, determines the width of the menu items.

    If the resulting number were to be too big for the application Window,
    the multiplier will be adjusted for the biggest possible one.

    :attr:`width_mult` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `1`.
    """

    max_height = NumericProperty()
    """
    The menu will grow no bigger than this number. Set to 0 for no limit.

    :attr:`max_height` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    border_margin = NumericProperty("4dp")
    """
    Margin between Window border and menu.

    :attr:`border_margin` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `4dp`.
    """

    ver_growth = OptionProperty(None, allownone=True, options=["up", "down"])
    """
    Where the menu will grow vertically to when opening. Set to None to let
    the widget pick for you. Available options are: `'up'`, `'down'`.

    :attr:`ver_growth` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `None`.
    """

    hor_growth = OptionProperty(None,
                                allownone=True,
                                options=["left", "right"])
    """
    Where the menu will grow horizontally to when opening. Set to None to let
    the widget pick for you. Available options are: `'left'`, `'right'`.

    :attr:`hor_growth` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `None`.
    """

    background_color = ColorProperty(None)
    """
    Color of the background of the menu.

    :attr:`background_color` is a :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    opening_transition = StringProperty("out_cubic")
    """
    Type of animation for opening a menu window.

    :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'out_cubic'`.
    """

    opening_time = NumericProperty(0.2)
    """
    Menu window opening animation time and you can set it to 0
    if you don't want animation of menu opening.

    :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `0.2`.
    """

    caller = ObjectProperty()
    """
    The widget object that caller the menu window.

    :attr:`caller` is a :class:`~kivy.properties.ObjectProperty`
    and defaults to `None`.
    """

    position = OptionProperty("auto", options=["auto", "center", "bottom"])
    """
    Menu window position relative to parent element.
    Available options are: `'auto'`, `'center'`, `'bottom'`.

    :attr:`position` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `'auto'`.
    """

    radius = ListProperty([
        dp(7),
    ])
    """
    Menu radius.

    :attr:`radius` is a :class:`~kivy.properties.ListProperty`
    and defaults to `'[dp(7),]'`.
    """

    _start_coords = []
    _calculate_complete = False
    _calculate_process = False

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Window.bind(on_resize=self.check_position_caller)
        Window.bind(on_maximize=self.set_menu_properties)
        Window.bind(on_restore=self.set_menu_properties)
        self.register_event_type("on_dismiss")
        self.register_event_type("on_enter")
        self.register_event_type("on_leave")
        self.register_event_type("on_release")
        self.menu = self.ids.md_menu
        self.target_height = 0

    def check_position_caller(self, instance, width, height):
        self.set_menu_properties(0)

    def set_bg_color_items(self, instance_selected_item):
        """Called when a Hover Behavior event occurs for a list item.

        :type instance_selected_item: <kivymd.uix.menu.MDMenuItemIcon object>
        """

        if self.selected_color:
            for item in self.menu.ids.box.children:
                if item is not instance_selected_item:
                    item.bg_color = (0, 0, 0, 0)
                else:
                    instance_selected_item.bg_color = self.selected_color

    def create_menu_items(self):
        """Creates menu items."""

        for data in self.items:
            if data.get("icon") and data.get("right_content_cls", None):

                item = MDMenuItemIcon(
                    text=data.get("text", ""),
                    divider=data.get("divider", "Full"),
                    _txt_top_pad=data.get("top_pad", "20dp"),
                    _txt_bot_pad=data.get("bot_pad", "20dp"),
                )

            elif data.get("icon"):
                item = MDMenuItemIcon(
                    text=data.get("text", ""),
                    divider=data.get("divider", "Full"),
                    _txt_top_pad=data.get("top_pad", "20dp"),
                    _txt_bot_pad=data.get("bot_pad", "20dp"),
                )

            elif data.get("right_content_cls", None):
                item = MDMenuItemRight(
                    text=data.get("text", ""),
                    divider=data.get("divider", "Full"),
                    _txt_top_pad=data.get("top_pad", "20dp"),
                    _txt_bot_pad=data.get("bot_pad", "20dp"),
                )

            else:

                item = MDMenuItem(
                    text=data.get("text", ""),
                    divider=data.get("divider", "Full"),
                    _txt_top_pad=data.get("top_pad", "20dp"),
                    _txt_bot_pad=data.get("bot_pad", "20dp"),
                )

            # Set height item.
            if data.get("height", ""):
                item.height = data.get("height")
            # Compensate icon area by some left padding.
            if not data.get("icon"):
                item._txt_left_pad = data.get("left_pad", "32dp")
            # Set left icon.
            else:
                item.icon = data.get("icon", "")
            item.bind(on_release=lambda x=item: self.dispatch("on_release", x))
            right_content_cls = data.get("right_content_cls", None)
            # Set right content.
            if isinstance(right_content_cls, RightContent):
                item.ids._right_container.width = right_content_cls.width + dp(
                    20)
                item.ids._right_container.padding = ("10dp", 0, 0, 0)
                item.add_widget(right_content_cls)
            else:
                if "_right_container" in item.ids:
                    item.ids._right_container.width = 0
            self.menu.ids.box.add_widget(item)

    def set_menu_properties(self, interval=0):
        """Sets the size and position for the menu window."""

        if self.caller:
            if not self.menu.ids.box.children:
                self.create_menu_items()
            # We need to pick a starting point, see how big we need to be,
            # and where to grow to.
            self._start_coords = self.caller.to_window(self.caller.center_x,
                                                       self.caller.center_y)
            self.target_width = self.width_mult * m_res.STANDARD_INCREMENT

            # If we're wider than the Window...
            if self.target_width > Window.width:
                # ...reduce our multiplier to max allowed.
                self.target_width = (
                    int(Window.width / m_res.STANDARD_INCREMENT) *
                    m_res.STANDARD_INCREMENT)

            # Set the target_height of the menu depending on the size of
            # each MDMenuItem or MDMenuItemIcon
            self.target_height = 0
            for item in self.menu.ids.box.children:
                self.target_height += item.height

            # If we're over max_height...
            if 0 < self.max_height < self.target_height:
                self.target_height = self.max_height

            # Establish vertical growth direction.
            if self.ver_growth is not None:
                ver_growth = self.ver_growth
            else:
                # If there's enough space below us:
                if (self.target_height <=
                        self._start_coords[1] - self.border_margin):
                    ver_growth = "down"
                # if there's enough space above us:
                elif (self.target_height < Window.height -
                      self._start_coords[1] - self.border_margin):
                    ver_growth = "up"
                # Otherwise, let's pick the one with more space and adjust ourselves.
                else:
                    # If there"s more space below us:
                    if (self._start_coords[1] >=
                            Window.height - self._start_coords[1]):
                        ver_growth = "down"
                        self.target_height = (self._start_coords[1] -
                                              self.border_margin)
                    # If there's more space above us:
                    else:
                        ver_growth = "up"
                        self.target_height = (Window.height -
                                              self._start_coords[1] -
                                              self.border_margin)

            if self.hor_growth is not None:
                hor_growth = self.hor_growth
            else:
                # If there's enough space to the right:
                if (self.target_width <= Window.width - self._start_coords[0] -
                        self.border_margin):
                    hor_growth = "right"
                # if there's enough space to the left:
                elif (self.target_width <
                      self._start_coords[0] - self.border_margin):
                    hor_growth = "left"
                # Otherwise, let's pick the one with more space and adjust ourselves.
                else:
                    # if there"s more space to the right:
                    if (Window.width - self._start_coords[0] >=
                            self._start_coords[0]):
                        hor_growth = "right"
                        self.target_width = (Window.width -
                                             self._start_coords[0] -
                                             self.border_margin)
                    # if there"s more space to the left:
                    else:
                        hor_growth = "left"
                        self.target_width = (self._start_coords[0] -
                                             self.border_margin)

            if ver_growth == "down":
                self.tar_y = self._start_coords[1] - self.target_height
            else:  # should always be "up"
                self.tar_y = self._start_coords[1]

            if hor_growth == "right":
                self.tar_x = self._start_coords[0]
            else:  # should always be "left"
                self.tar_x = self._start_coords[0] - self.target_width
            self._calculate_complete = True

    def open(self):
        """Animate the opening of a menu window."""
        def open(interval):
            if not self._calculate_complete:
                return
            if self.position == "auto":
                self.menu.pos = self._start_coords
                anim = Animation(
                    x=self.tar_x,
                    y=self.tar_y,
                    width=self.target_width,
                    height=self.target_height,
                    duration=self.opening_time,
                    opacity=1,
                    transition=self.opening_transition,
                )
                anim.start(self.menu)
            else:
                if self.position == "center":
                    self.menu.pos = (
                        self._start_coords[0] - self.target_width / 2,
                        self._start_coords[1] - self.target_height / 2,
                    )
                elif self.position == "bottom":
                    self.menu.pos = (
                        self._start_coords[0] - self.target_width / 2,
                        self.caller.pos[1] - self.target_height,
                    )
                anim = Animation(
                    width=self.target_width,
                    height=self.target_height,
                    duration=self.opening_time,
                    opacity=1,
                    transition=self.opening_transition,
                )
            anim.start(self.menu)
            Window.add_widget(self)
            Clock.unschedule(open)
            self._calculate_process = False

        self.set_menu_properties()
        if not self._calculate_process:
            self._calculate_process = True
            Clock.schedule_interval(open, 0)

    def on_touch_down(self, touch):
        if not self.menu.collide_point(*touch.pos):
            self.dispatch("on_dismiss")
            return True
        super().on_touch_down(touch)
        return True

    def on_touch_move(self, touch):
        super().on_touch_move(touch)
        return True

    def on_touch_up(self, touch):
        super().on_touch_up(touch)
        return True

    def on_enter(self, instance):
        """Call when mouse enter the bbox of the item of menu."""

    def on_leave(self, instance):
        """Call when the mouse exit the item of menu."""

    def on_release(self, *args):
        """The method that will be called when you click menu items."""

    def on_dismiss(self):
        """Called when the menu is closed."""

        Window.remove_widget(self)
        self.menu.width = 0
        self.menu.height = 0
        self.menu.opacity = 0

    def dismiss(self):
        """Closes the menu."""

        self.on_dismiss()
Exemplo n.º 6
0
class ThemeManager(EventDispatcher):
    primary_palette = OptionProperty("Blue", options=palette)
    """
    The name of the color scheme that the application will use.
    All major `material` components will have the color
    of the specified color theme.

    Available options are: `'Red'`, `'Pink'`, `'Purple'`, `'DeepPurple'`,
    `'Indigo'`, `'Blue'`, `'LightBlue'`, `'Cyan'`, `'Teal'`, `'Green'`,
    `'LightGreen'`, `'Lime'`, `'Yellow'`, `'Amber'`, `'Orange'`, `'DeepOrange'`,
    `'Brown'`, `'Gray'`, `'BlueGray'`.

    To change the color scheme of an application:

    .. code-block:: python

        from kivy.uix.screenmanager import Screen

        from kivymd.app import MDApp
        from kivymd.uix.button import MDRectangleFlatButton


        class MainApp(MDApp):
            def build(self):
                self.theme_cls.primary_palette = "Green"  # "Purple", "Red"

                screen = Screen()
                screen.add_widget(
                    MDRectangleFlatButton(
                        text="Hello, World",
                        pos_hint={"center_x": 0.5, "center_y": 0.5},
                    )
                )
                return screen


        MainApp().run()

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-palette.png

    :attr:`primary_palette` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'Blue'`.
    """

    primary_hue = OptionProperty("500", options=hue)
    """
    The color hue of the application.

    Available options are: `'50'`, `'100'`, `'200'`, `'300'`, `'400'`, `'500'`,
    `'600'`, `'700'`, `'800'`, `'900'`, `'A100'`, `'A200'`, `'A400'`, `'A700'`.

    To change the hue color scheme of an application:

    .. code-block:: python

        from kivy.uix.screenmanager import Screen

        from kivymd.app import MDApp
        from kivymd.uix.button import MDRectangleFlatButton


        class MainApp(MDApp):
            def build(self):
                self.theme_cls.primary_palette = "Green"  # "Purple", "Red"
                self.theme_cls.primary_hue = "200"  # "500"

                screen = Screen()
                screen.add_widget(
                    MDRectangleFlatButton(
                        text="Hello, World",
                        pos_hint={"center_x": 0.5, "center_y": 0.5},
                    )
                )
                return screen


        MainApp().run()

    With a value of ``self.theme_cls.primary_hue = "500"``:

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-palette.png

    With a value of ``self.theme_cls.primary_hue = "200"``:

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-hue.png

    :attr:`primary_hue` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'500'`.
    """

    primary_light_hue = OptionProperty("200", options=hue)
    """
    Hue value for :attr:`primary_light`.

    :attr:`primary_light_hue` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'200'`.
    """

    primary_dark_hue = OptionProperty("700", options=hue)
    """
    Hue value for :attr:`primary_dark`.

    :attr:`primary_light_hue` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'700'`.
    """

    def _get_primary_color(self):
        return get_color_from_hex(
            colors[self.primary_palette][self.primary_hue]
        )

    primary_color = AliasProperty(
        _get_primary_color, bind=("primary_palette", "primary_hue")
    )
    """
    The color of the current application theme in ``rgba`` format.

    :attr:`primary_color` is an :class:`~kivy.properties.AliasProperty` that
    returns the value of the current application theme, property is readonly.
    """

    def _get_primary_light(self):
        return get_color_from_hex(
            colors[self.primary_palette][self.primary_light_hue]
        )

    primary_light = AliasProperty(
        _get_primary_light, bind=("primary_palette", "primary_light_hue")
    )
    """
    Colors of the current application color theme in ``rgba`` format
    (in lighter color).

    .. code-block:: python

        from kivy.lang import Builder

        from kivymd.app import MDApp


        KV = '''
        Screen:

            MDRaisedButton:
                text: "primary_light"
                pos_hint: {"center_x": 0.5, "center_y": 0.7}
                md_bg_color: app.theme_cls.primary_light

            MDRaisedButton:
                text: "primary_color"
                pos_hint: {"center_x": 0.5, "center_y": 0.5}

            MDRaisedButton:
                text: "primary_dark"
                pos_hint: {"center_x": 0.5, "center_y": 0.3}
                md_bg_color: app.theme_cls.primary_dark
        '''


        class MainApp(MDApp):
            def build(self):
                self.theme_cls.primary_palette = "Green"
                return Builder.load_string(KV)


        MainApp().run()

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-colors-light-dark.png
        :align: center

    :attr:`primary_light` is an :class:`~kivy.properties.AliasProperty` that
    returns the value of the current application theme (in lighter color),
    property is readonly.
    """

    def _get_primary_dark(self):
        return get_color_from_hex(
            colors[self.primary_palette][self.primary_dark_hue]
        )

    primary_dark = AliasProperty(
        _get_primary_dark, bind=("primary_palette", "primary_dark_hue")
    )
    """
    Colors of the current application color theme
    in ``rgba`` format (in darker color).

    :attr:`primary_dark` is an :class:`~kivy.properties.AliasProperty` that
    returns the value of the current application theme (in darker color),
    property is readonly.
    """

    accent_palette = OptionProperty("Amber", options=palette)
    """
    The application color palette used for items such as the tab indicator
    in the :attr:`MDTabsBar` class and so on...

    The image below shows the color schemes with the values
    ``self.theme_cls.accent_palette = 'Blue'``, ``Red'`` and​​ ``Yellow'``:

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/accent-palette.png

    :attr:`accent_palette` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'Amber'`.
    """

    accent_hue = OptionProperty("500", options=hue)
    """Similar to :attr:`primary_hue`,
    but returns a value for :attr:`accent_palette`.

    :attr:`accent_hue` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'500'`.
    """

    accent_light_hue = OptionProperty("200", options=hue)
    """
    Hue value for :attr:`accent_light`.

    :attr:`accent_light_hue` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'200'`.
    """

    accent_dark_hue = OptionProperty("700", options=hue)
    """
    Hue value for :attr:`accent_dark`.

    :attr:`accent_dark_hue` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'700'`.
    """

    def _get_accent_color(self):
        return get_color_from_hex(colors[self.accent_palette][self.accent_hue])

    accent_color = AliasProperty(
        _get_accent_color, bind=["accent_palette", "accent_hue"]
    )
    """Similar to :attr:`primary_color`,
    but returns a value for :attr:`accent_color`.

    :attr:`accent_color` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`accent_color`,
    property is readonly.
    """

    def _get_accent_light(self):
        return get_color_from_hex(
            colors[self.accent_palette][self.accent_light_hue]
        )

    accent_light = AliasProperty(
        _get_accent_light, bind=["accent_palette", "accent_light_hue"]
    )
    """Similar to :attr:`primary_light`,
    but returns a value for :attr:`accent_light`.

    :attr:`accent_light` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`accent_light`,
    property is readonly.
    """

    def _get_accent_dark(self):
        return get_color_from_hex(
            colors[self.accent_palette][self.accent_dark_hue]
        )

    accent_dark = AliasProperty(
        _get_accent_dark, bind=["accent_palette", "accent_dark_hue"]
    )
    """Similar to :attr:`primary_dark`,
    but returns a value for :attr:`accent_dark`.

    :attr:`accent_dark` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`accent_dark`,
    property is readonly.
    """

    theme_style = OptionProperty("Light", options=["Light", "Dark"])
    """App theme style.

    .. code-block:: python

        from kivy.uix.screenmanager import Screen

        from kivymd.app import MDApp
        from kivymd.uix.button import MDRectangleFlatButton


        class MainApp(MDApp):
            def build(self):
                self.theme_cls.theme_style = "Dark"  # "Light"

                screen = Screen()
                screen.add_widget(
                    MDRectangleFlatButton(
                        text="Hello, World",
                        pos_hint={"center_x": 0.5, "center_y": 0.5},
                    )
                )
                return screen


        MainApp().run()

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/theme-style.png

    :attr:`theme_style` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'Light'`.
    """

    def _get_theme_style(self, opposite):
        if opposite:
            return "Light" if self.theme_style == "Dark" else "Dark"
        else:
            return self.theme_style

    def _get_bg_darkest(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            return get_color_from_hex(colors["Light"]["StatusBar"])
        elif theme_style == "Dark":
            return get_color_from_hex(colors["Dark"]["StatusBar"])

    bg_darkest = AliasProperty(_get_bg_darkest, bind=["theme_style"])
    """
    Similar to :attr:`bg_dark`,
    but the color values ​​are a tone lower (darker) than :attr:`bg_dark`.

    .. code-block:: python

        KV = '''
        <Box@BoxLayout>:
            bg: 0, 0, 0, 0

            canvas:
                Color:
                    rgba: root.bg
                Rectangle:
                    pos: self.pos
                    size: self.size

        BoxLayout:

            Box:
                bg: app.theme_cls.bg_light
            Box:
                bg: app.theme_cls.bg_normal
            Box:
                bg: app.theme_cls.bg_dark
            Box:
                bg: app.theme_cls.bg_darkest
        '''

        from kivy.lang import Builder

        from kivymd.app import MDApp


        class MainApp(MDApp):
            def build(self):
                self.theme_cls.theme_style = "Dark"  # "Light"
                return Builder.load_string(KV)


        MainApp().run()

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bg-normal-dark-darkest.png

    :attr:`bg_darkest` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`bg_darkest`,
    property is readonly.
    """

    def _get_op_bg_darkest(self):
        return self._get_bg_darkest(True)

    opposite_bg_darkest = AliasProperty(
        _get_op_bg_darkest, bind=["theme_style"]
    )
    """
    The opposite value of color in the :attr:`bg_darkest`.

    :attr:`opposite_bg_darkest` is an :class:`~kivy.properties.AliasProperty`
    that returns the value in ``rgba`` format for :attr:`opposite_bg_darkest`,
    property is readonly.
    """

    def _get_bg_dark(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            return get_color_from_hex(colors["Light"]["AppBar"])
        elif theme_style == "Dark":
            return get_color_from_hex(colors["Dark"]["AppBar"])

    bg_dark = AliasProperty(_get_bg_dark, bind=["theme_style"])
    """
    Similar to :attr:`bg_normal`,
    but the color values ​​are one tone lower (darker) than :attr:`bg_normal`.

    :attr:`bg_dark` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`bg_dark`,
    property is readonly.
    """

    def _get_op_bg_dark(self):
        return self._get_bg_dark(True)

    opposite_bg_dark = AliasProperty(_get_op_bg_dark, bind=["theme_style"])
    """
    The opposite value of color in the :attr:`bg_dark`.

    :attr:`opposite_bg_dark` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`opposite_bg_dark`,
    property is readonly.
    """

    def _get_bg_normal(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            return get_color_from_hex(colors["Light"]["Background"])
        elif theme_style == "Dark":
            return get_color_from_hex(colors["Dark"]["Background"])

    bg_normal = AliasProperty(_get_bg_normal, bind=["theme_style"])
    """
    Similar to :attr:`bg_light`,
    but the color values ​​are one tone lower (darker) than :attr:`bg_light`.

    :attr:`bg_normal` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`bg_normal`,
    property is readonly.
    """

    def _get_op_bg_normal(self):
        return self._get_bg_normal(True)

    opposite_bg_normal = AliasProperty(_get_op_bg_normal, bind=["theme_style"])
    """
    The opposite value of color in the :attr:`bg_normal`.

    :attr:`opposite_bg_normal` is an :class:`~kivy.properties.AliasProperty`
    that returns the value in ``rgba`` format for :attr:`opposite_bg_normal`,
    property is readonly.
    """

    def _get_bg_light(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            return get_color_from_hex(colors["Light"]["CardsDialogs"])
        elif theme_style == "Dark":
            return get_color_from_hex(colors["Dark"]["CardsDialogs"])

    bg_light = AliasProperty(_get_bg_light, bind=["theme_style"])
    """"
    Depending on the style of the theme (`'Dark'` or `'Light`')
    that the application uses, :attr:`bg_light` contains the color value
    in ``rgba`` format for the widgets background.

    :attr:`bg_light` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`bg_light`,
    property is readonly.
    """

    def _get_op_bg_light(self):
        return self._get_bg_light(True)

    opposite_bg_light = AliasProperty(_get_op_bg_light, bind=["theme_style"])
    """
    The opposite value of color in the :attr:`bg_light`.

    :attr:`opposite_bg_light` is an :class:`~kivy.properties.AliasProperty`
    that returns the value in ``rgba`` format for :attr:`opposite_bg_light`,
    property is readonly.
    """

    def _get_divider_color(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            color = get_color_from_hex("000000")
        elif theme_style == "Dark":
            color = get_color_from_hex("FFFFFF")
        color[3] = 0.12
        return color

    divider_color = AliasProperty(_get_divider_color, bind=["theme_style"])
    """
    Color for dividing lines such as  :class:`~kivymd.uix.card.MDSeparator`.

    :attr:`divider_color` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`divider_color`,
    property is readonly.
    """

    def _get_op_divider_color(self):
        return self._get_divider_color(True)

    opposite_divider_color = AliasProperty(
        _get_op_divider_color, bind=["theme_style"]
    )
    """
    The opposite value of color in the :attr:`divider_color`.

    :attr:`opposite_divider_color` is an :class:`~kivy.properties.AliasProperty`
    that returns the value in ``rgba`` format for :attr:`opposite_divider_color`,
    property is readonly.
    """

    def _get_text_color(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            color = get_color_from_hex("000000")
            color[3] = 0.87
        elif theme_style == "Dark":
            color = get_color_from_hex("FFFFFF")
        return color

    text_color = AliasProperty(_get_text_color, bind=["theme_style"])
    """
    Color of the text used in the :class:`~kivymd.uix.label.MDLabel`.

    :attr:`text_color` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`text_color`,
    property is readonly.
    """

    def _get_op_text_color(self):
        return self._get_text_color(True)

    opposite_text_color = AliasProperty(
        _get_op_text_color, bind=["theme_style"]
    )
    """
    The opposite value of color in the :attr:`text_color`.

    :attr:`opposite_text_color` is an :class:`~kivy.properties.AliasProperty`
    that returns the value in ``rgba`` format for :attr:`opposite_text_color`,
    property is readonly.
    """

    def _get_secondary_text_color(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            color = get_color_from_hex("000000")
            color[3] = 0.54
        elif theme_style == "Dark":
            color = get_color_from_hex("FFFFFF")
            color[3] = 0.70
        return color

    secondary_text_color = AliasProperty(
        _get_secondary_text_color, bind=["theme_style"]
    )
    """
    The color for the secondary text that is used in classes
    from the module :class:`~kivymd/uix/list.TwoLineListItem`.

    :attr:`secondary_text_color` is an :class:`~kivy.properties.AliasProperty`
    that returns the value in ``rgba`` format for :attr:`secondary_text_color`,
    property is readonly.
    """

    def _get_op_secondary_text_color(self):
        return self._get_secondary_text_color(True)

    opposite_secondary_text_color = AliasProperty(
        _get_op_secondary_text_color, bind=["theme_style"]
    )
    """
    The opposite value of color in the :attr:`secondary_text_color`.

    :attr:`opposite_secondary_text_color`
    is an :class:`~kivy.properties.AliasProperty` that returns the value
    in ``rgba`` format for :attr:`opposite_secondary_text_color`,
    property is readonly.
    """

    def _get_icon_color(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            color = get_color_from_hex("000000")
            color[3] = 0.54
        elif theme_style == "Dark":
            color = get_color_from_hex("FFFFFF")
        return color

    icon_color = AliasProperty(_get_icon_color, bind=["theme_style"])
    """
    Color of the icon used in the :class:`~kivymd.uix.button.MDIconButton`.

    :attr:`icon_color` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`icon_color`,
    property is readonly.
    """

    def _get_op_icon_color(self):
        return self._get_icon_color(True)

    opposite_icon_color = AliasProperty(
        _get_op_icon_color, bind=["theme_style"]
    )
    """
    The opposite value of color in the :attr:`icon_color`.

    :attr:`opposite_icon_color` is an :class:`~kivy.properties.AliasProperty`
    that returns the value in ``rgba`` format for :attr:`opposite_icon_color`,
    property is readonly.
    """

    def _get_disabled_hint_text_color(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            color = get_color_from_hex("000000")
            color[3] = 0.38
        elif theme_style == "Dark":
            color = get_color_from_hex("FFFFFF")
            color[3] = 0.50
        return color

    disabled_hint_text_color = AliasProperty(
        _get_disabled_hint_text_color, bind=["theme_style"]
    )
    """
    Color of the disabled text used in the :class:`~kivymd.uix.textfield.MDTextField`.

    :attr:`disabled_hint_text_color`
    is an :class:`~kivy.properties.AliasProperty` that returns the value
    in ``rgba`` format for :attr:`disabled_hint_text_color`,
    property is readonly.
    """

    def _get_op_disabled_hint_text_color(self):
        return self._get_disabled_hint_text_color(True)

    opposite_disabled_hint_text_color = AliasProperty(
        _get_op_disabled_hint_text_color, bind=["theme_style"]
    )
    """
    The opposite value of color in the :attr:`disabled_hint_text_color`.

    :attr:`opposite_disabled_hint_text_color`
    is an :class:`~kivy.properties.AliasProperty` that returns the value
    in ``rgba`` format for :attr:`opposite_disabled_hint_text_color`,
    property is readonly.
    """

    # Hardcoded because muh standard
    def _get_error_color(self):
        return get_color_from_hex(colors["Red"]["A700"])

    error_color = AliasProperty(_get_error_color)
    """
    Color of the error text used
    in the :class:`~kivymd.uix.textfield.MDTextField`.

    :attr:`error_color` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`error_color`,
    property is readonly.
    """

    def _get_ripple_color(self):
        return self._ripple_color

    def _set_ripple_color(self, value):
        self._ripple_color = value

    _ripple_color = ColorProperty(get_color_from_hex(colors["Gray"]["400"]))
    """Private value."""

    ripple_color = AliasProperty(
        _get_ripple_color, _set_ripple_color, bind=["_ripple_color"]
    )
    """
    Color of ripple effects.

    :attr:`ripple_color` is an :class:`~kivy.properties.AliasProperty` that
    returns the value in ``rgba`` format for :attr:`ripple_color`,
    property is readonly.
    """

    def _determine_device_orientation(self, _, window_size):
        if window_size[0] > window_size[1]:
            self.device_orientation = "landscape"
        elif window_size[1] >= window_size[0]:
            self.device_orientation = "portrait"

    device_orientation = StringProperty("")
    """
    Device orientation.

    :attr:`device_orientation` is an :class:`~kivy.properties.StringProperty`.
    """

    def _get_standard_increment(self):
        if DEVICE_TYPE == "mobile":
            if self.device_orientation == "landscape":
                return dp(48)
            else:
                return dp(56)
        else:
            return dp(64)

    standard_increment = AliasProperty(
        _get_standard_increment, bind=["device_orientation"]
    )
    """
    Value of standard increment.

    :attr:`standard_increment` is an :class:`~kivy.properties.AliasProperty`
    that returns the value in ``rgba`` format for :attr:`standard_increment`,
    property is readonly.
    """

    def _get_horizontal_margins(self):
        if DEVICE_TYPE == "mobile":
            return dp(16)
        else:
            return dp(24)

    horizontal_margins = AliasProperty(_get_horizontal_margins)
    """
    Value of horizontal margins.

    :attr:`horizontal_margins` is an :class:`~kivy.properties.AliasProperty`
    that returns the value in ``rgba`` format for :attr:`horizontal_margins`,
    property is readonly.
    """

    def on_theme_style(self, instance, value):
        if (
            hasattr(App.get_running_app(), "theme_cls")
            and App.get_running_app().theme_cls == self
        ):
            self.set_clearcolor_by_theme_style(value)

    set_clearcolor = BooleanProperty(True)

    def set_clearcolor_by_theme_style(self, theme_style):
        if not self.set_clearcolor:
            return
        Window.clearcolor = get_color_from_hex(
            colors[theme_style]["Background"]
        )

    # font name, size (sp), always caps, letter spacing (sp)
    font_styles = DictProperty(
        {
            "H1": ["RobotoLight", 96, False, -1.5],
            "H2": ["RobotoLight", 60, False, -0.5],
            "H3": ["Roboto", 48, False, 0],
            "H4": ["Roboto", 34, False, 0.25],
            "H5": ["Roboto", 24, False, 0],
            "H6": ["RobotoMedium", 20, False, 0.15],
            "Subtitle1": ["Roboto", 16, False, 0.15],
            "Subtitle2": ["RobotoMedium", 14, False, 0.1],
            "Body1": ["Roboto", 16, False, 0.5],
            "Body2": ["Roboto", 14, False, 0.25],
            "Button": ["RobotoMedium", 14, True, 1.25],
            "Caption": ["Roboto", 12, False, 0.4],
            "Overline": ["Roboto", 10, True, 1.5],
            "Icon": ["Icons", 24, False, 0],
        }
    )
    """
    Data of default font styles.

    Add custom font:

    .. code-block:: python

        KV = '''
        Screen:

            MDLabel:
                text: "JetBrainsMono"
                halign: "center"
                font_style: "JetBrainsMono"
        '''

        from kivy.core.text import LabelBase

        from kivy.lang import Builder

        from kivymd.app import MDApp
        from kivymd.font_definitions import theme_font_styles


        class MainApp(MDApp):
            def build(self):
                LabelBase.register(
                    name="JetBrainsMono",
                    fn_regular="JetBrainsMono-Regular.ttf")

                theme_font_styles.append('JetBrainsMono')
                self.theme_cls.font_styles["JetBrainsMono"] = [
                    "JetBrainsMono",
                    16,
                    False,
                    0.15,
                ]
                return Builder.load_string(KV)


        MainApp().run()

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/font-styles.png

    :attr:`font_styles` is an :class:`~kivy.properties.DictProperty`.
    """

    def set_colors(
        self,
        primary_palette,
        primary_hue,
        primary_light_hue,
        primary_dark_hue,
        accent_palette,
        accent_hue,
        accent_light_hue,
        accent_dark_hue,
    ):
        self.primary_palette = primary_palette
        self.primary_hue = primary_hue
        self.primary_light_hue = primary_light_hue
        self.primary_dark_hue = primary_dark_hue
        self.accent_palette = accent_palette
        self.accent_hue = accent_hue
        self.accent_light_hue = accent_light_hue
        self.accent_dark_hue = accent_dark_hue

    """
    Courtesy method to allow all of the theme color attributes to be set in one call.

    :attr:`set_colors` allows all of the following to be set in one method call:

    * primary palette color,
    * primary hue,
    * primary light hue,
    * primary dark hue,
    * accent palette color,
    * accent hue,
    * accent ligth hue, and
    * accent dark hue.

    Note that all values *must* be provided. If you only want to set one or two values
    use the appropriate method call for that.

    .. code-block:: python

        from kivy.uix.screenmanager import Screen

        from kivymd.app import MDApp
        from kivymd.uix.button import MDRectangleFlatButton


        class MainApp(MDApp):
            def build(self):
                self.theme_cls.set_colors(
                    "Blue", "600", "50", "800", "Teal", "600", "100", "800"
                )

                screen = Screen()
                screen.add_widget(
                    MDRectangleFlatButton(
                        text="Hello, World",
                        pos_hint={"center_x": 0.5, "center_y": 0.5},
                    )
                )
                return screen


        MainApp().run()

    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.rec_shadow = Atlas(f"{images_path}rec_shadow.atlas")
        self.rec_st_shadow = Atlas(f"{images_path}rec_st_shadow.atlas")
        self.quad_shadow = Atlas(f"{images_path}quad_shadow.atlas")
        self.round_shadow = Atlas(f"{images_path}round_shadow.atlas")
        Clock.schedule_once(lambda x: self.on_theme_style(0, self.theme_style))
        self._determine_device_orientation(None, Window.size)
        Window.bind(size=self._determine_device_orientation)
Exemplo n.º 7
0
class ShaderTransition(TransitionBase):
    '''Transition class that uses a Shader for animating the transition between
    2 screens. By default, this class doesn't assign any fragment/vertex
    shader. If you want to create your own fragment shader for the transition,
    you need to declare the header yourself and include the "t", "tex_in" and
    "tex_out" uniform::

        # Create your own transition. This shader implements a "fading"
        # transition.
        fs = """$HEADER
            uniform float t;
            uniform sampler2D tex_in;
            uniform sampler2D tex_out;

            void main(void) {
                vec4 cin = texture2D(tex_in, tex_coord0);
                vec4 cout = texture2D(tex_out, tex_coord0);
                gl_FragColor = mix(cout, cin, t);
            }
        """

        # And create your transition
        tr = ShaderTransition(fs=fs)
        sm = ScreenManager(transition=tr)

    '''

    fs = StringProperty(None)
    '''Fragment shader to use.

    :attr:`fs` is a :class:`~kivy.properties.StringProperty` and defaults to
    None.'''

    vs = StringProperty(None)
    '''Vertex shader to use.

    :attr:`vs` is a :class:`~kivy.properties.StringProperty` and defaults to
    None.'''

    clearcolor = ColorProperty([0, 0, 0, 1])
    '''Sets the color of Fbo ClearColor.

    .. versionadded:: 1.9.0

    :attr:`clearcolor` is a :class:`~kivy.properties.ColorProperty`
    and defaults to [0, 0, 0, 1].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    '''
    def make_screen_fbo(self, screen):
        fbo = Fbo(size=screen.size, with_stencilbuffer=True)
        with fbo:
            ClearColor(*self.clearcolor)
            ClearBuffers()
        fbo.add(screen.canvas)
        with fbo.before:
            PushMatrix()
            Translate(-screen.x, -screen.y, 0)
        with fbo.after:
            PopMatrix()
        return fbo

    def on_progress(self, progress):
        self.render_ctx['t'] = progress

    def on_complete(self):
        self.render_ctx['t'] = 1.
        super(ShaderTransition, self).on_complete()

    def _remove_out_canvas(self, *args):
        if (self.screen_out
                and self.screen_out.canvas in self.manager.canvas.children
                and self.screen_out not in self.manager.children):
            self.manager.canvas.remove(self.screen_out.canvas)

    def add_screen(self, screen):
        self.screen_in.pos = self.screen_out.pos
        self.screen_in.size = self.screen_out.size
        self.manager.real_remove_widget(self.screen_out)
        self.manager.canvas.add(self.screen_out.canvas)

        def remove_screen_out(instr):
            Clock.schedule_once(self._remove_out_canvas, -1)
            self.render_ctx.remove(instr)

        self.fbo_in = self.make_screen_fbo(self.screen_in)
        self.fbo_out = self.make_screen_fbo(self.screen_out)
        self.manager.canvas.add(self.fbo_in)
        self.manager.canvas.add(self.fbo_out)

        self.render_ctx = RenderContext(fs=self.fs,
                                        vs=self.vs,
                                        use_parent_modelview=True,
                                        use_parent_projection=True)
        with self.render_ctx:
            BindTexture(texture=self.fbo_out.texture, index=1)
            BindTexture(texture=self.fbo_in.texture, index=2)
            x, y = self.screen_in.pos
            w, h = self.fbo_in.texture.size
            Rectangle(size=(w, h),
                      pos=(x, y),
                      tex_coords=self.fbo_in.texture.tex_coords)
            Callback(remove_screen_out)
        self.render_ctx['tex_out'] = 1
        self.render_ctx['tex_in'] = 2
        self.manager.canvas.add(self.render_ctx)

    def remove_screen(self, screen):
        self.manager.canvas.remove(self.fbo_in)
        self.manager.canvas.remove(self.fbo_out)
        self.manager.canvas.remove(self.render_ctx)
        self._remove_out_canvas()
        self.manager.real_add_widget(self.screen_in)

    def stop(self):
        self._remove_out_canvas()
        super(ShaderTransition, self).stop()
Exemplo n.º 8
0
class BaseDialogPicker(
        BaseDialog,
        FakeRectangularElevationBehavior,
        SpecificBackgroundColorBehavior,
):
    """
    Base class for :class:`~kivymd.uix.picker.MDDatePicker` and
    :class:`~kivymd.uix.picker.MDTimePicker` classes.

    :Events:
        `on_save`
            Events called when the "OK" dialog box button is clicked.
        `on_cancel`
            Events called when the "CANCEL" dialog box button is clicked.
    """

    title_input = StringProperty("INPUT DATE")
    """
    Dialog title fot input date.

    .. code-block:: python

        MDDatePicker(title_input="INPUT DATE")

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-date-picker-input-date.png
        :align: center

    :attr:`title_input` is an :class:`~kivy.properties.StringProperty`
    and defaults to `INPUT DATE`.
    """

    title = StringProperty("SELECT DATE")
    """
    Dialog title fot select date.

    .. code-block:: python

        MDDatePicker(title="SELECT DATE")

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-date-picker-select-date.png
        :align: center

    :attr:`title` is an :class:`~kivy.properties.StringProperty`
    and defaults to `SELECT DATE`.
    """

    radius = ListProperty([7, 7, 7, 7])
    """
    Radius list for the four corners of the dialog.

    .. code-block:: python

        MDDatePicker(radius=[7, 7, 7, 26])

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-date-picker-radius.png
        :align: center

    :attr:`radius` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[7, 7, 7, 7]`.
    """

    primary_color = ColorProperty(None)
    """
    Background color of toolbar in (r, g, b, a) format.

    .. code-block:: python

        MDDatePicker(primary_color=get_color_from_hex("#72225b"))

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-color-date.png
        :align: center

    :attr:`primary_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    accent_color = ColorProperty(None)
    """
    Background color of calendar/clock face in (r, g, b, a) format.

    .. code-block:: python

        MDDatePicker(
            primary_color=get_color_from_hex("#72225b"),
            accent_color=get_color_from_hex("#5d1a4a"),
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/accent-color-date.png
        :align: center

    :attr:`accent_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    selector_color = ColorProperty(None)
    """
    Background color of the selected day of the month or hour in (r, g, b, a) format.

    .. code-block:: python

        MDDatePicker(
            primary_color=get_color_from_hex("#72225b"),
            accent_color=get_color_from_hex("#5d1a4a"),
            selector_color=get_color_from_hex("#e93f39"),
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selector-color-date.png
        :align: center

    :attr:`selector_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    text_toolbar_color = ColorProperty(None)
    """
    Color of labels for text on a toolbar in (r, g, b, a) format.

    .. code-block:: python

        MDDatePicker(
            primary_color=get_color_from_hex("#72225b"),
            accent_color=get_color_from_hex("#5d1a4a"),
            selector_color=get_color_from_hex("#e93f39"),
            text_toolbar_color=get_color_from_hex("#cccccc"),
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-toolbar-color-date.png
        :align: center

    :attr:`text_toolbar_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    text_color = ColorProperty(None)
    """
    Color of text labels in calendar/clock face in (r, g, b, a) format.

    .. code-block:: python

        MDDatePicker(
            primary_color=get_color_from_hex("#72225b"),
            accent_color=get_color_from_hex("#5d1a4a"),
            selector_color=get_color_from_hex("#e93f39"),
            text_toolbar_color=get_color_from_hex("#cccccc"),
            text_color=("#ffffff"),
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-color-date.png
        :align: center

    :attr:`text_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    text_current_color = ColorProperty(None)
    """
    Color of the text of the current day of the month/hour in (r, g, b, a) format.

    .. code-block:: python

        MDDatePicker(
            primary_color=get_color_from_hex("#72225b"),
            accent_color=get_color_from_hex("#5d1a4a"),
            selector_color=get_color_from_hex("#e93f39"),
            text_toolbar_color=get_color_from_hex("#cccccc"),
            text_color=("#ffffff"),
            text_current_color=get_color_from_hex("#e93f39"),
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-current-color-date.png
        :align: center

    :attr:`text_current_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    text_button_color = ColorProperty(None)
    """
    Text button color in (r, g, b, a) format.

    .. code-block:: python

        MDDatePicker(
            primary_color=get_color_from_hex("#72225b"),
            accent_color=get_color_from_hex("#5d1a4a"),
            selector_color=get_color_from_hex("#e93f39"),
            text_toolbar_color=get_color_from_hex("#cccccc"),
            text_color=("#ffffff"),
            text_current_color=get_color_from_hex("#e93f39"),
            text_button_color=(1, 1, 1, .5),
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-button-color-date.png
        :align: center

    :attr:`text_button_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    input_field_background_color = ColorProperty(None)
    """
    Background color of input fields in (r, g, b, a) format.

    .. code-block:: python

        MDDatePicker(
            primary_color=get_color_from_hex("#72225b"),
            accent_color=get_color_from_hex("#5d1a4a"),
            selector_color=get_color_from_hex("#e93f39"),
            text_toolbar_color=get_color_from_hex("#cccccc"),
            text_color=("#ffffff"),
            text_current_color=get_color_from_hex("#e93f39"),
            input_field_background_color=(1, 1, 1, 0.2),
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/input-field-background-color-date.png
        :align: center

    :attr:`input_field_background_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    input_field_text_color = ColorProperty(None)
    """
    Text color of input fields in (r, g, b, a) format.

    Background color of input fields.

    .. code-block:: python

        MDDatePicker(
            primary_color=get_color_from_hex("#72225b"),
            accent_color=get_color_from_hex("#5d1a4a"),
            selector_color=get_color_from_hex("#e93f39"),
            text_toolbar_color=get_color_from_hex("#cccccc"),
            text_color=("#ffffff"),
            text_current_color=get_color_from_hex("#e93f39"),
            input_field_background_color=(1, 1, 1, 0.2),
            input_field_text_color=(1, 1, 1, 1),
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/input-field-background-color-date.png
        :align: center

    :attr:`input_field_text_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    font_name = StringProperty("Roboto")
    """
    Font name for dialog window text.

    .. code-block:: python

        MDDatePicker(
            primary_color=get_color_from_hex("#72225b"),
            accent_color=get_color_from_hex("#5d1a4a"),
            selector_color=get_color_from_hex("#e93f39"),
            text_toolbar_color=get_color_from_hex("#cccccc"),
            text_color=("#ffffff"),
            text_current_color=get_color_from_hex("#e93f39"),
            input_field_background_color=(1, 1, 1, 0.2),
            input_field_text_color=(1, 1, 1, 1),
            font_name="Weather.ttf",

        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/font-name-date.png
        :align: center

    :attr:`font_name` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'Roboto'`.
    """
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.register_event_type("on_save")
        self.register_event_type("on_cancel")

    def on_save(self, *args) -> None:
        """Events called when the "OK" dialog box button is clicked."""

        self.dismiss()

    def on_cancel(self, *args) -> None:
        """Events called when the "CANCEL" dialog box button is clicked."""

        self.dismiss()
Exemplo n.º 9
0
class CupertinoSwitch(ButtonBehavior, Widget):
    """
    iOS style Switch. To comply with iOS standard, keep the width to height ratio of
    :class:`CupertinoSwitch` at 2:1

    .. image:: ../_static/switch/demo.gif
    """

    toggled = BooleanProperty(False)
    """
    If :class:`CupertinoSwitch` is on
    
    .. image:: ../_static/switch/toggled.png
    
    **Python**
    
    .. code-block:: python
    
       CupertinoSwitch(toggled=True)
    
    **KV**
    
    .. code-block::
    
       CupertinoSwitch:
           toggled: True
    """

    thumb_color = ColorProperty([1, 1, 1, 1])
    """
    Color of thumb of :class:`CupertinoSwitch`
    
    .. image:: ../_static/switch/thumb_color.png
    
    **Python**
    
    .. code-block:: python
    
       CupertinoSwitch(thumb_color=(1, 0, 0, 1))
    
    **KV**
    
    .. code-block::
    
       CupertinoSwitch:
           thumb_color: 1, 0, 0, 1
    """

    color_toggled = ColorProperty([0.3, 0.85, 0.4, 1])
    """
    Background color of :class:`CupertinoSwitch` when on
    
    .. image:: ../_static/switch/color_toggled.gif
    
    **Python**
    
    .. code-block:: python
    
       CupertinoSwitch(color_toggled=(1, 0, 0, 1))
    
    **KV**
    
    .. code-block::
    
       CupertinoSwitch:
           color_toggled: 1, 0, 0, 1
    """

    color_untoggled = ColorProperty([0.95, 0.95, 0.95, 1])
    """
Exemplo n.º 10
0
class GlobalContentArea(AnchorLayout):
    """Base window for the application, hosts context buttons, status bar and content area.

    Some properties copied from Kivy's TabbedPanel
    """

    background_color = ColorProperty([0, 0, 0, 1])
    """Background color, in the format (r, g, b, a).

    :attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and
    defaults to [1, 1, 1, 1].
    """
    border_color = ColorProperty([77 / 256, 77 / 256, 76 / 256, 1])
    """Border color, in the format (r, g, b, a).

    :attr:`border_color` is a :class:`~kivy.properties.ColorProperty` and
    defaults to [77/256, 177/256, 76/256, 1].
    """

    _current_page = ObjectProperty(None)

    def get_current_page(self):
        return self._current_page

    current_page = AliasProperty(get_current_page,
                                 None,
                                 bind=('_current_page', ))
    """Links to the currently selected or active page.

    :attr:`current_page` is an :class:`~kivy.AliasProperty`, read-only.
    """

    tab_height = NumericProperty('64px')
    '''Specifies the height of the tab header.

    :attr:`tab_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 64.
    '''

    tab_width = NumericProperty('64px', allownone=True)
    '''Specifies the width of the tab header.

    :attr:`tab_width` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 64.
    '''

    status_height = NumericProperty('50px')
    '''Specifies the height of the status bar.

    :attr:`status_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 100.
    '''

    def __init__(self, **kwargs):
        self._pages = []
        self._statusbar = None

        super(GlobalContentArea, self).__init__(**kwargs)

    def set_page(self, page):
        if self._current_page is not None:
            self.ids.ContentPanel.remove_widget(self._current_page)
            self._current_page.active = False

        self._current_page = self._pages[page]

        self._current_page.active = True
        self.ids.ContentPanel.add_widget(self._current_page)

    def register_content(self, page):
        index = len(self._pages)
        self._pages.append(page)

        cbtn = page.create_context_button(length=self.tab_height,
                                          cb=lambda inst: self.set_page(index))
        self.ids.ContextButtons.add_widget(cbtn)

        if self._current_page is None:
            self.set_page(index)

    def register_status_bar(self, statusbar):
        # Remove a status bar if it already exists
        if self._statusbar is not None:
            self.ids.StatusBar.remove_widget(self._statusbar)

        self._statusbar = statusbar

        # Register status bar if one was given
        if self._statusbar is not None:
            self.ids.StatusBar.add_widget(self._statusbar)

    @property
    def status_bar(self):
        return self._statusbar
Exemplo n.º 11
0
class AKCardStack(ThemableBehavior, RelativeLayout):
    """
    Represents a stack of cards. Call the `change()` method on this class to
    change the stack of cards.

    Use the :attr:`current_card` to get front most card to add widgets to it.

    """

    radius = ListProperty([dp(20), dp(20), dp(20), dp(20)])
    """Sets the radius for all the cards

    :attr:`radius` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[dp(20), dp(20), dp(20), dp(20)]`.

    """

    first_color = ColorProperty(None)
    """Sets the color of the front most card

    :attr:`first_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `'app.theme_cls.primary_light'`.

    """

    second_color = ColorProperty(None)
    """Sets the color of the second most card

    :attr:`second_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `'app.theme_cls.primary_color'`.

    """

    third_color = ColorProperty(None)
    """Sets the color of the last card

    :attr:`third_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `'app.theme_cls.primary_dark'`.

    """

    current_card = ObjectProperty(None)
    """A read only property that returns the object of the front most card

    :attr:`current_card` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to `None`.

    """

    transition = StringProperty("in_out_circ")
    """Type of animation interpolation to be used

    :attr:`transition` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'in_out_circ'`.

    """

    card_out_direction = OptionProperty(
        "down", options=["down", "up", "left", "right"])
    """Direction in which the front most card is animated out of the screen.
    Can be 'down', 'up', 'left' or 'right'.

    :attr:`card_out_direction` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'down'`.

    """

    card_in_direction = OptionProperty("side",
                                       options=["bottom", "top", "side"])
    """Direction in which the new card to be added comes from.
    Can be 'side', 'bottom', or 'top'

    :attr:`card_in_direction` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'side'`.

    """

    elevation = NumericProperty(0)
    """The elevation of the front most card
    :attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.

    """
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.counter = 1
        if not self.first_color:
            self.first_color = self.theme_cls.primary_light
        if not self.second_color:
            self.second_color = self.theme_cls.primary_color
        if not self.third_color:
            self.third_color = self.theme_cls.primary_dark
        Clock.schedule_once(self.initial_current_card, -1)

    def initial_current_card(self, time):
        """
        Sets the :attr:'current_card' at the start of the program
        """
        self.current_card = self.ids.card1.children[0]

    def change(self, *args):
        """
        Causes the CardStack to change to the next card
        """
        card_to_drop = "card" + str(self.counter)
        if self.card_out_direction == "down":
            Animation(
                pos_hint={
                    "center_x": 0.5,
                    "center_y": -1
                },
                duration=0.3,
                t=self.transition,
            ).start(self.ids[card_to_drop].children[0])
        elif self.card_out_direction == "up":
            Animation(
                pos_hint={
                    "center_x": 0.5,
                    "center_y": 1.5
                },
                duration=0.3,
                t=self.transition,
            ).start(self.ids[card_to_drop].children[0])
        elif self.card_out_direction == "right":
            Animation(
                pos_hint={
                    "center_x": 1.5,
                    "center_y": 0.5
                },
                duration=0.3,
                t=self.transition,
            ).start(self.ids[card_to_drop].children[0])
        else:
            Animation(
                pos_hint={
                    "center_x": -1,
                    "center_y": 0.5
                },
                duration=0.3,
                t=self.transition,
            ).start(self.ids[card_to_drop].children[0])
        # Update the var current_card to reflect the new card brought to front
        if self.counter + 1 == 4:
            self.current_card = self.ids["card1"].children[0]
        else:
            self.current_card = self.ids["card" +
                                         str(self.counter + 1)].children[0]
        Clock.schedule_once(self.card_changer, 0.2)

    def card_changer(self, *args):
        """
        Internal function. Do not call this function use `change()` instead
        """
        # Rotate second card into palce
        if self.counter + 1 == 4:
            card2 = "card1"
        else:
            card2 = "card" + str(self.counter + 1)
        Animation(
            pos_hint={
                "center_x": 0.5,
                "center_y": 0.5
            },
            elevation=self.elevation,
            md_bg_color=self.first_color,
            duration=0.3,
        ).start(self.ids[card2].children[0])
        Animation(angle=0, duration=0.4).start(
            self.ids[card2].canvas.before.children[-1])

        # Rotate last card into place
        if self.counter + 2 == 4:
            card3 = "card1"
        elif self.counter + 2 == 5:
            card3 = "card2"
        else:
            card3 = "card3"
        Animation(
            pos_hint={
                "center_x": 0.45,
                "center_y": 0.5
            },
            elevation=0,
            md_bg_color=self.second_color,
            duration=0.5,
        ).start(self.ids[card3].children[0])
        Animation(angle=3, duration=0.5).start(
            self.ids[card3].canvas.before.children[-1])

        # Remove the first card and add it back at the back of the stack as a new_card
        new_card = self.ids["card" + str(self.counter)]
        # Clear this card of its widgets
        new_card.children[0].clear_widgets()
        self.remove_widget(new_card)
        if self.card_in_direction == "side":
            new_card.children[0].pos_hint = {
                "center_x": 0.35,
                "center_y": 0.55
            }
        elif self.card_in_direction == "top":
            new_card.children[0].pos_hint = {
                "center_x": 0.42,
                "center_y": 0.60
            }
        else:
            new_card.children[0].pos_hint = {"center_x": 0.42, "center_y": 0.4}
        new_card.children[0].md_bg_color = self.third_color
        new_card.children[0].opacity = 0
        self.add_widget(new_card, 4)
        Animation(
            opacity=1,
            pos_hint={
                "center_x": 0.42,
                "center_y": 0.5
            },
            elevation=0,
            duration=0.4,
        ).start(new_card.children[0])
        Animation(angle=5,
                  duration=0.2).start(new_card.canvas.before.children[-1])

        # Increment the counter variable and if passes 3 resest counter
        if self.counter != 3:
            self.counter += 1
        else:
            self.counter = 1
Exemplo n.º 12
0
class MDTabs(ThemableBehavior, SpecificBackgroundColorBehavior, AnchorLayout):
    """
    You can use this class to create your own tabbed panel.

    :Events:
        `on_tab_switch`
            Called when switching tabs.
        `on_slide_progress`
            Called while the slide is scrolling.
        `on_ref_press`
            The method will be called when the ``on_ref_press`` event
            occurs when you, for example, use markup text for tabs.
    """

    default_tab = NumericProperty(0)
    """
    Index of the default tab.

    :attr:`default_tab` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    tab_bar_height = NumericProperty("48dp")
    """
    Height of the tab bar.

    :attr:`tab_bar_height` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `'48dp'`.
    """

    tab_indicator_anim = BooleanProperty(False)
    """
    Tab indicator animation. If you want use animation set it to ``True``.

    :attr:`tab_indicator_anim` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    tab_indicator_height = NumericProperty("2dp")
    """
    Height of the tab indicator.

    :attr:`tab_indicator_height` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `'2dp'`.
    """

    tab_indicator_type = OptionProperty(
        "line", options=["line", "fill", "round", "line-round", "line-rect"])
    """
    Type of tab indicator. Available options are: `'line'`, `'fill'`,
    `'round'`, `'line-rect'` and `'line-round'`.

    :attr:`tab_indicator_type` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'line'`.
    """

    tab_hint_x = BooleanProperty(False)
    """
    This option affects the size of each child. if it's `True`, the size of
    each tab will be ignored and will use the size available by the container.

    :attr:`tab_hint_x` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    anim_duration = NumericProperty(0.2)
    """
    Duration of the slide animation.

    :attr:`anim_duration` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0.2`.
    """

    anim_threshold = BoundedNumericProperty(0.8,
                                            min=0.0,
                                            max=1.0,
                                            errorhandler=lambda x: 0.0
                                            if x < 0.0 else 1.0)
    """
    Animation threshold allow you to change the tab indicator animation effect.

    :attr:`anim_threshold` is an :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to `0.8`.
    """

    allow_stretch = BooleanProperty(True)
    """
    If `True`, The tab will update dynamically to it's content width, and wrap
    any text if the widget is wider than `"360dp"`.

    If `False`, the tab won't update to it's maximum texture width.
    this means that the `fixed_tab_label_width` will be used as the label
    width. this will wrap any text inside to fit the fixed value.

    :attr:`allow_stretch` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    fixed_tab_label_width = NumericProperty("140dp")
    """
    If `allow_stretch` is `False`, the class will set this value as the width
    to all the tabs title label.

    :attr:`fixed_tab_label_width` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `140dp`.
    """

    background_color = ColorProperty(None)
    """
    Background color of tabs in ``rgba`` format.

    :attr:`background_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    text_color_normal = ColorProperty(None)
    """
    Text color of the label when it is not selected.

    :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    text_color_active = ColorProperty(None)
    """
    Text color of the label when it is selected.

    :attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    elevation = NumericProperty(0)
    """
    Tab value elevation.

    .. seealso::

        `Behaviors/Elevation <https://kivymd.readthedocs.io/en/latest/behaviors/elevation/index.html>`_

    :attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    indicator_color = ColorProperty(None)
    """
    Color indicator in ``rgba`` format.

    :attr:`indicator_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    lock_swiping = BooleanProperty(False)
    """
    If True - disable switching tabs by swipe.

    :attr:`lock_swiping` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    font_name = StringProperty("Roboto")
    """
    Font name for tab text.

    :attr:`font_name` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'Roboto'`.
    """

    ripple_duration = NumericProperty(2)
    """
    Ripple duration when long touching to tab.

    :attr:`ripple_duration` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `2`.
    """

    no_ripple_effect = BooleanProperty(True)
    """
    Whether to use the ripple effect when tapping on a tab.

    :attr:`no_ripple_effect` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    title_icon_mode = OptionProperty("Lead", options=["Lead", "Top"])
    """
    This property sets the mode in wich the tab's title and icon are shown.

    :attr:`title_icon_mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'Lead'`.
    """

    force_title_icon_mode = BooleanProperty(True)
    """
    If this property is se to `True`, it will force the class to update every
    tab inside the scroll view to the current `title_icon_mode`

    :attr:`force_title_icon_mode` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.register_event_type("on_tab_switch")
        self.register_event_type("on_ref_press")
        self.register_event_type("on_slide_progress")
        Clock.schedule_once(self._carousel_bind, 1)
        self.theme_cls.bind(
            primary_palette=self.update_icon_color,
            theme_style=self.update_icon_color,
        )
        self.bind(
            force_title_icon_mode=self._parse_icon_mode,
            title_icon_mode=self._parse_icon_mode,
        )
        self.bind(tab_hint_x=self._update_tab_hint_x)

    def _update_tab_hint_x(self, *args):
        if not self.ids.layout.children:
            return
        if self.tab_hint_x is True:
            self.fixed_tab_label_width = self.width // len(
                self.ids.layout.children)
            self.allow_stretch = False
        else:
            self.allow_stretch = True

    def _parse_icon_mode(self, *args):
        if self.force_title_icon_mode is True:
            for slide in self.carousel.slides:
                slide.title_icon_mode = self.title_icon_mode
                if self.title_icon_mode == "Top":
                    self.tab_bar_height = dp(72)
                else:
                    self.tab_bar_height = dp(48)

    def update_icon_color(self, instance, value):
        for tab_label in self.get_tab_list():
            if not self.text_color_normal:
                tab_label.text_color_normal = self.theme_cls.text_color
            if not self.text_color_active:
                tab_label.text_color_active = self.specific_secondary_text_color

    def switch_tab(self, name_tab, search_by="text"):
        """
        This funciont switch between tabs
        name_tab can be either a String or a MDTabsBase.

        `search_by` will look up through the properties of every tab.

        If the value doesnt match, it will raise a ValueError.

        Search_by options:
            text : will search by the raw text of the label (`tab_label_text`)
            icon : will search by the `icon` property
            title : will search by the `title` property
        """

        if isinstance(name_tab, str):
            if search_by == "title":
                for tab_instance in self.tab_bar.parent.carousel.slides:
                    if tab_instance.title_is_capital is True:
                        _name_tab = name_tab.upper()
                    else:
                        _name_tab = name_tab
                    if tab_instance.title == _name_tab:
                        self.carousel.load_slide(tab_instance)
                        return
            # Search by icon.
            elif search_by == "icon":
                for tab_instance in self.tab_bar.parent.carousel.slides:
                    if tab_instance.icon == name_tab:
                        self.carousel.load_slide(tab_instance)
                        return
            # Search by title.
            else:
                for tab_instance in self.tab_bar.parent.carousel.slides:
                    if tab_instance.tab_label_text == name_tab:
                        self.carousel.load_slide(tab_instance)
                        return
            raise ValueError("switch_tab:\n\t"
                             "name_tab not found in the tab list\n\t"
                             f"search_by = {repr(search_by)} \n\t"
                             f"name_tab = {repr(name_tab)} \n\t")
        else:
            self.carousel.load_slide(name_tab.tab)

    def get_tab_list(self):
        """Returns a list of tab objects."""

        return self.tab_bar.layout.children[::-1]

    def get_slides(self):
        return self.carousel.slides

    def add_widget(self, widget, index=0, canvas=None):
        # You can add only subclass of MDTabsBase.
        if not isinstance(widget, (MDTabsBase, MDTabsMain, MDTabsBar)):
            raise ValueError(
                f"MDTabs[{self.uid}].add_widget:\n\t"
                "The widget provided is not a subclass of MDTabsBase.")
        if len(self.children) >= 2:
            try:
                # FIXME: Can't set the value of the `no_ripple_effect`
                #  and `ripple_duration` properties for widget.tab_label.
                widget.tab_label._no_ripple_effect = self.no_ripple_effect
                widget.tab_label.ripple_duration_in_slow = self.ripple_duration
                widget.tab_label.group = str(self)
                widget.tab_label.tab_bar = self.tab_bar
                widget.tab_label.text_color_normal = (
                    self.text_color_normal if self.text_color_normal else
                    self.specific_secondary_text_color)
                widget.tab_label.text_color_active = (
                    self.text_color_active
                    if self.text_color_active else self.specific_text_color)
                self.bind(
                    allow_stretch=widget.tab_label._update_text_size,
                    fixed_tab_label_width=widget.tab_label._update_text_size,
                    font_name=widget.tab_label.setter("font_name"),
                    text_color_active=widget.tab_label.setter(
                        "text_color_active"),
                    text_color_normal=widget.tab_label.setter(
                        "text_color_normal"),
                )
                Clock.schedule_once(widget.tab_label._update_text_size, 0)
                self.tab_bar.layout.add_widget(widget.tab_label)
                self.carousel.add_widget(widget)
                if self.force_title_icon_mode is True:
                    widget.title_icon_mode = self.title_icon_mode
                Clock.schedule_once(
                    self.tab_bar._label_request_indicator_update, 0)
                return
            except AttributeError:
                pass
        if isinstance(widget, (MDTabsMain, MDTabsBar)):
            return super().add_widget(widget)

    def remove_widget(self, widget):
        if len(self.carousel.slides) < 2:
            return
        # You can remove only subclass of MDTabsLabel or MDTabsBase.
        if not issubclass(widget.__class__, (MDTabsLabel, MDTabsBase)):
            raise MDTabsException(
                "MDTabs can remove only subclass of MDTabsLabel or MDTabsBase")
        # If the widget is an instance of MDTabsBase, then the widget is
        # set as the widget's tab_label object.
        if issubclass(widget.__class__, MDTabsBase):
            slide = widget
            title_label = widget.tab_label
        else:
            # We already got the label, so we set the slide reference.
            slide = widget.tab
            title_label = widget
        # Set memory.
        # Search object next tab.
        # Clean all bindings to allow the widget to be collected.
        self.unbind(
            allow_stretch=title_label._update_text_size,
            fixed_tab_label_width=title_label._update_text_size,
            font_name=title_label.setter("font_name"),
            text_color_active=title_label.setter("text_color_active"),
            text_color_normal=title_label.setter("text_color_normal"),
        )
        self.carousel.remove_widget(slide)
        self.tab_bar.layout.remove_widget(title_label)
        # Clean the references.
        slide = None
        title_label = None
        widget = None

    def on_slide_progress(self, *args):
        """
        This event is deployed every available frame while the tab is scrolling.
        """

    def on_carousel_index(self, carousel, index):
        """Called when the Tab index have changed.

        This event is deployed by the built in carousel of the class.
        """

        # When the index of the carousel change, update tab indicator,
        # select the current tab and reset threshold data.
        if carousel.current_slide:
            current_tab_label = carousel.current_slide.tab_label
            if current_tab_label.state == "normal":
                # current_tab_label._do_press()
                current_tab_label.dispatch("on_release")
                current_tab_label._release_group(self)
                current_tab_label.state = "down"

            if self.tab_indicator_type == "round":
                self.tab_indicator_height = self.tab_bar_height
                if index == 0:
                    radius = [
                        0,
                        self.tab_bar_height / 2,
                        self.tab_bar_height / 2,
                        0,
                    ]
                    self.tab_bar.update_indicator(current_tab_label.x,
                                                  current_tab_label.width,
                                                  radius)
                elif index == len(self.get_tab_list()) - 1:
                    radius = [
                        self.tab_bar_height / 2,
                        0,
                        0,
                        self.tab_bar_height / 2,
                    ]
                    self.tab_bar.update_indicator(current_tab_label.x,
                                                  current_tab_label.width,
                                                  radius)
                else:
                    radius = [
                        self.tab_bar_height / 2,
                    ]
                    self.tab_bar.update_indicator(current_tab_label.x,
                                                  current_tab_label.width,
                                                  radius)
            elif (self.tab_indicator_type == "fill"
                  or self.tab_indicator_type == "line-round"
                  or self.tab_indicator_type == "line-rect"):
                self.tab_indicator_height = self.tab_bar_height
                self.tab_bar.update_indicator(current_tab_label.x,
                                              current_tab_label.width)
            else:
                self.tab_bar.update_indicator(current_tab_label.x,
                                              current_tab_label.width)

    def on_ref_press(self, *args):
        """
        This event will be launched every time the user press a markup enabled
        label with a link or reference inside.
        """

    def on_tab_switch(self, *args):
        """This event is launched every time the current tab is changed."""

    def on_size(self, *args):
        if self.carousel.current_slide:
            self._update_indicator(self.carousel.current_slide.tab_label)

    def _carousel_bind(self, interval):
        self.carousel.bind(on_slide_progress=self._on_slide_progress)

    def _on_slide_progress(self, *args):
        self.dispatch("on_slide_progress", args)

    def _update_indicator(self, current_tab_label):
        def update_indicator(interval):
            self.tab_bar.update_indicator(current_tab_label.x,
                                          current_tab_label.width)

        if not current_tab_label:
            current_tab_label = self.tab_bar.layout.children[-1]
        Clock.schedule_once(update_indicator)

    def _update_padding(self, layout, *args):
        if self.tab_hint_x is True:
            layout.padding = [0, 0]
            Clock.schedule_once(self._update_tab_hint_x)
            return True
        padding = [0, 0]
        # This is more efficient than to use sum([layout.children]).
        width = layout.width - (layout.padding[0] * 2)
        # Forces the padding of the tab_bar when the tab_bar is scrollable.
        if width > self.width:
            padding = [dp(52), 0]
        # Set the new padding.
        layout.padding = padding
        # Update the indicator.
        if self.carousel.current_slide:
            self._update_indicator(self.carousel.current_slide.tab_label)
            Clock.schedule_once(
                lambda x: setattr(self.carousel.current_slide.tab_label,
                                  "state", "down"),
                -1,
            )
        return True
Exemplo n.º 13
0
class ScrollView(StencilView):
    '''ScrollView class. See module documentation for more information.

    :Events:
        `on_scroll_start`
            Generic event fired when scrolling starts from touch.
        `on_scroll_move`
            Generic event fired when scrolling move from touch.
        `on_scroll_stop`
            Generic event fired when scrolling stops from touch.

    .. versionchanged:: 1.9.0
        `on_scroll_start`, `on_scroll_move` and `on_scroll_stop` events are
        now dispatched when scrolling to handle nested ScrollViews.

    .. versionchanged:: 1.7.0
        `auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has
        been deprecated, use :attr:`effect_cls` instead.
    '''

    scroll_distance = NumericProperty(_scroll_distance)
    '''Distance to move before scrolling the :class:`ScrollView`, in pixels. As
    soon as the distance has been traveled, the :class:`ScrollView` will start
    to scroll, and no touch event will go to children.
    It is advisable that you base this value on the dpi of your target device's
    screen.

    :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 20 (pixels), according to the default value in user
    configuration.
    '''

    scroll_wheel_distance = NumericProperty('20sp')
    '''Distance to move when scrolling with a mouse wheel.
    It is advisable that you base this value on the dpi of your target device's
    screen.

    .. versionadded:: 1.8.0

    :attr:`scroll_wheel_distance` is a
    :class:`~kivy.properties.NumericProperty` , defaults to 20 pixels.
    '''

    scroll_timeout = NumericProperty(_scroll_timeout)
    '''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds.
    If the user has not moved :attr:`scroll_distance` within the timeout,
    the scrolling will be disabled, and the touch event will go to the
    children.

    :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 55 (milliseconds) according to the default value in user
    configuration.

    .. versionchanged:: 1.5.0
        Default value changed from 250 to 55.
    '''

    scroll_x = NumericProperty(0.)
    '''X scrolling value, between 0 and 1. If 0, the content's left side will
    touch the left side of the ScrollView. If 1, the content's right side will
    touch the right side.

    This property is controlled by :class:`ScrollView` only if
    :attr:`do_scroll_x` is True.

    :attr:`scroll_x` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.
    '''

    scroll_y = NumericProperty(1.)
    '''Y scrolling value, between 0 and 1. If 0, the content's bottom side will
    touch the bottom side of the ScrollView. If 1, the content's top side will
    touch the top side.

    This property is controlled by :class:`ScrollView` only if
    :attr:`do_scroll_y` is True.

    :attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.
    '''

    do_scroll_x = BooleanProperty(True)
    '''Allow scroll on X axis.

    :attr:`do_scroll_x` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to True.
    '''

    do_scroll_y = BooleanProperty(True)
    '''Allow scroll on Y axis.

    :attr:`do_scroll_y` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to True.
    '''
    def _get_do_scroll(self):
        return (self.do_scroll_x, self.do_scroll_y)

    def _set_do_scroll(self, value):
        if isinstance(value, (list, tuple)):
            self.do_scroll_x, self.do_scroll_y = value
        else:
            self.do_scroll_x = self.do_scroll_y = bool(value)

    do_scroll = AliasProperty(_get_do_scroll,
                              _set_do_scroll,
                              bind=('do_scroll_x', 'do_scroll_y'),
                              cache=True)
    '''Allow scroll on X or Y axis.

    :attr:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of
    (:attr:`do_scroll_x` + :attr:`do_scroll_y`)
    '''

    always_overscroll = BooleanProperty(True)
    '''Make sure user can overscroll even if there is not enough content
    to require scrolling.

    This is useful if you want to trigger some action on overscroll, but
    there is not always enough content to trigger it.

    :attr:`always_overscroll` is a
    :class:`~kivy.properties.BooleanProperty` and defaults to `True`.

    .. versionadded:: 2.0.0

    The option was added and enabled by default, set to False to get the
    previous behavior of only allowing to overscroll when there is
    enough content to allow scrolling.
    '''

    def _get_vbar(self):
        # must return (y, height) in %
        # calculate the viewport size / scrollview size %
        if self._viewport is None:
            return 0, 1.
        vh = self._viewport.height
        h = self.height
        if vh < h or vh == 0:
            return 0, 1.
        ph = max(0.01, h / float(vh))
        sy = min(1.0, max(0.0, self.scroll_y))
        py = (1. - ph) * sy
        return (py, ph)

    vbar = AliasProperty(_get_vbar,
                         bind=('scroll_y', '_viewport', 'viewport_size',
                               'height'),
                         cache=True)
    '''Return a tuple of (position, size) of the vertical scrolling bar.

    .. versionadded:: 1.2.0

    The position and size are normalized between 0-1, and represent a
    proportion of the current scrollview height. This property is used
    internally for drawing the little vertical bar when you're scrolling.

    :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly.
    '''

    def _get_hbar(self):
        # must return (x, width) in %
        # calculate the viewport size / scrollview size %
        if self._viewport is None:
            return 0, 1.
        vw = self._viewport.width
        w = self.width
        if vw < w or vw == 0:
            return 0, 1.
        pw = max(0.01, w / float(vw))
        sx = min(1.0, max(0.0, self.scroll_x))
        px = (1. - pw) * sx
        return (px, pw)

    hbar = AliasProperty(_get_hbar,
                         bind=('scroll_x', '_viewport', 'viewport_size',
                               'width'),
                         cache=True)
    '''Return a tuple of (position, size) of the horizontal scrolling bar.

    .. versionadded:: 1.2.0

    The position and size are normalized between 0-1, and represent a
    proportion of the current scrollview height. This property is used
    internally for drawing the little horizontal bar when you're scrolling.

    :attr:`hbar` is a :class:`~kivy.properties.AliasProperty`, readonly.
    '''

    bar_color = ColorProperty([.7, .7, .7, .9])
    '''Color of horizontal / vertical scroll bar, in RGBA format.

    .. versionadded:: 1.2.0

    :attr:`bar_color` is a :class:`~kivy.properties.ColorProperty` and defaults
    to [.7, .7, .7, .9].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    '''

    bar_inactive_color = ColorProperty([.7, .7, .7, .2])
    '''Color of horizontal / vertical scroll bar (in RGBA format), when no
    scroll is happening.

    .. versionadded:: 1.9.0

    :attr:`bar_inactive_color` is a
    :class:`~kivy.properties.ColorProperty` and defaults to [.7, .7, .7, .2].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    '''

    bar_width = NumericProperty('2dp')
    '''Width of the horizontal / vertical scroll bar. The width is interpreted
    as a height for the horizontal bar.

    .. versionadded:: 1.2.0

    :attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 2.
    '''

    bar_pos_x = OptionProperty('bottom', options=('top', 'bottom'))
    '''Which side of the ScrollView the horizontal scroll bar should go
    on. Possible values are 'top' and 'bottom'.

    .. versionadded:: 1.8.0

    :attr:`bar_pos_x` is an :class:`~kivy.properties.OptionProperty`,
    defaults to 'bottom'.

    '''

    bar_pos_y = OptionProperty('right', options=('left', 'right'))
    '''Which side of the ScrollView the vertical scroll bar should go
    on. Possible values are 'left' and 'right'.

    .. versionadded:: 1.8.0

    :attr:`bar_pos_y` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'right'.

    '''

    bar_pos = ReferenceListProperty(bar_pos_x, bar_pos_y)
    '''Which side of the scroll view to place each of the bars on.

    :attr:`bar_pos` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`bar_pos_x`, :attr:`bar_pos_y`)
    '''

    bar_margin = NumericProperty(0)
    '''Margin between the bottom / right side of the scrollview when drawing
    the horizontal / vertical scroll bar.

    .. versionadded:: 1.2.0

    :attr:`bar_margin` is a :class:`~kivy.properties.NumericProperty`, default
    to 0
    '''

    effect_cls = ObjectProperty(DampedScrollEffect, allownone=True)
    '''Class effect to instantiate for X and Y axis.

    .. versionadded:: 1.7.0

    :attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to :class:`DampedScrollEffect`.

    .. versionchanged:: 1.8.0
        If you set a string, the :class:`~kivy.factory.Factory` will be used to
        resolve the class.

    '''

    effect_x = ObjectProperty(None, allownone=True)
    '''Effect to apply for the X axis. If None is set, an instance of
    :attr:`effect_cls` will be created.

    .. versionadded:: 1.7.0

    :attr:`effect_x` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None.
    '''

    effect_y = ObjectProperty(None, allownone=True)
    '''Effect to apply for the Y axis. If None is set, an instance of
    :attr:`effect_cls` will be created.

    .. versionadded:: 1.7.0

    :attr:`effect_y` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None, read-only.
    '''

    viewport_size = ListProperty([0, 0])
    '''(internal) Size of the internal viewport. This is the size of your only
    child in the scrollview.
    '''

    scroll_type = OptionProperty(['content'],
                                 options=(['content'], ['bars'],
                                          ['bars',
                                           'content'], ['content', 'bars']))
    '''Sets the type of scrolling to use for the content of the scrollview.
    Available options are: ['content'], ['bars'], ['bars', 'content'].

    +---------------------+------------------------------------------------+
    | ['content']         | Content is scrolled by dragging or swiping the |
    |                     | content directly.                              |
    +---------------------+------------------------------------------------+
    | ['bars']            | Content is scrolled by dragging or swiping the |
    |                     | scroll bars.                                   |
    +---------------------+------------------------------------------------+
    | ['bars', 'content'] | Content is scrolled by either of the above     |
    |                     | methods.                                       |
    +---------------------+------------------------------------------------+

    .. versionadded:: 1.8.0

    :attr:`scroll_type` is an :class:`~kivy.properties.OptionProperty` and
    defaults to ['content'].
    '''

    smooth_scroll_end = NumericProperty(None, allownone=True)
    '''Whether smooth scroll end should be used when scrolling with the
    mouse-wheel and the factor of transforming the scroll distance to
    velocity. This option also enables velocity addition meaning if you
    scroll more, you will scroll faster and further. The recommended value
    is `10`. The velocity is calculated as :attr:`scroll_wheel_distance` *
    :attr:`smooth_scroll_end`.

    .. versionadded:: 1.11.0

    :attr:`smooth_scroll_end` is a :class:`~kivy.properties.NumericProperty`
    and defaults to None.
    '''

    # private, for internal use only

    _viewport = ObjectProperty(None, allownone=True)
    _bar_color = ListProperty([0, 0, 0, 0])
    _effect_x_start_width = None
    _effect_y_start_height = None
    _update_effect_bounds_ev = None
    _bind_inactive_bar_color_ev = None

    def _set_viewport_size(self, instance, value):
        self.viewport_size = value

    def on__viewport(self, instance, value):
        if value:
            value.bind(size=self._set_viewport_size)
            self.viewport_size = value.size

    __events__ = ('on_scroll_start', 'on_scroll_move', 'on_scroll_stop')

    def __init__(self, **kwargs):
        self._touch = None
        self._trigger_update_from_scroll = Clock.create_trigger(
            self.update_from_scroll, -1)
        # create a specific canvas for the viewport
        from kivy.graphics import PushMatrix, Translate, PopMatrix, Canvas
        self.canvas_viewport = Canvas()
        self.canvas = Canvas()
        with self.canvas_viewport.before:
            PushMatrix()
            self.g_translate = Translate(0, 0)
        with self.canvas_viewport.after:
            PopMatrix()

        super(ScrollView, self).__init__(**kwargs)

        self.register_event_type('on_scroll_start')
        self.register_event_type('on_scroll_move')
        self.register_event_type('on_scroll_stop')

        # now add the viewport canvas to our canvas
        self.canvas.add(self.canvas_viewport)

        effect_cls = self.effect_cls
        if isinstance(effect_cls, string_types):
            effect_cls = Factory.get(effect_cls)
        if self.effect_x is None and effect_cls is not None:
            self.effect_x = effect_cls(target_widget=self._viewport)
        if self.effect_y is None and effect_cls is not None:
            self.effect_y = effect_cls(target_widget=self._viewport)

        trigger_update_from_scroll = self._trigger_update_from_scroll
        update_effect_widget = self._update_effect_widget
        update_effect_x_bounds = self._update_effect_x_bounds
        update_effect_y_bounds = self._update_effect_y_bounds
        fbind = self.fbind
        fbind('width', update_effect_x_bounds)
        fbind('height', update_effect_y_bounds)
        fbind('viewport_size', self._update_effect_bounds)
        fbind('_viewport', update_effect_widget)
        fbind('scroll_x', trigger_update_from_scroll)
        fbind('scroll_y', trigger_update_from_scroll)
        fbind('pos', trigger_update_from_scroll)
        fbind('size', trigger_update_from_scroll)

        trigger_update_from_scroll()
        update_effect_widget()
        update_effect_x_bounds()
        update_effect_y_bounds()

    def on_effect_x(self, instance, value):
        if value:
            value.bind(scroll=self._update_effect_x)
            value.target_widget = self._viewport

    def on_effect_y(self, instance, value):
        if value:
            value.bind(scroll=self._update_effect_y)
            value.target_widget = self._viewport

    def on_effect_cls(self, instance, cls):
        if isinstance(cls, string_types):
            cls = Factory.get(cls)
        self.effect_x = cls(target_widget=self._viewport)
        self.effect_x.bind(scroll=self._update_effect_x)
        self.effect_y = cls(target_widget=self._viewport)
        self.effect_y.bind(scroll=self._update_effect_y)

    def _update_effect_widget(self, *args):
        if self.effect_x:
            self.effect_x.target_widget = self._viewport
        if self.effect_y:
            self.effect_y.target_widget = self._viewport

    def _update_effect_x_bounds(self, *args):
        if not self._viewport or not self.effect_x:
            return
        scrollable_width = self.width - self.viewport_size[0]
        self.effect_x.min = 0
        self.effect_x.max = min(0, scrollable_width)
        self.effect_x.value = scrollable_width * self.scroll_x

    def _update_effect_y_bounds(self, *args):
        if not self._viewport or not self.effect_y:
            return
        scrollable_height = self.height - self.viewport_size[1]
        self.effect_y.min = 0 if scrollable_height < 0 else scrollable_height
        self.effect_y.max = scrollable_height
        self.effect_y.value = self.effect_y.max * self.scroll_y

    def _update_effect_bounds(self, *args):
        self._update_effect_x_bounds()
        self._update_effect_y_bounds()

    def _update_effect_x(self, *args):
        vp = self._viewport
        if not vp or not self.effect_x:
            return

        if self.effect_x.is_manual:
            sw = vp.width - self._effect_x_start_width
        else:
            sw = vp.width - self.width
        if sw < 1 and not (self.always_overscroll and self.do_scroll_x):
            return
        if sw != 0:
            sx = self.effect_x.scroll / sw
            self.scroll_x = -sx
        self._trigger_update_from_scroll()

    def _update_effect_y(self, *args):
        vp = self._viewport
        if not vp or not self.effect_y:
            return
        if self.effect_y.is_manual:
            sh = vp.height - self._effect_y_start_height
        else:
            sh = vp.height - self.height

        if sh < 1 and not (self.always_overscroll and self.do_scroll_y):
            return
        if sh != 0:
            sy = self.effect_y.scroll / sh
            self.scroll_y = -sy
        self._trigger_update_from_scroll()

    def to_local(self, x, y, **k):
        tx, ty = self.g_translate.xy
        return x - tx, y - ty

    def to_parent(self, x, y, **k):
        tx, ty = self.g_translate.xy
        return x + tx, y + ty

    def _apply_transform(self, m, pos=None):
        tx, ty = self.g_translate.xy
        m.translate(tx, ty, 0)
        return super(ScrollView, self)._apply_transform(m, (0, 0))

    def simulate_touch_down(self, touch):
        # at this point the touch is in parent coords
        touch.push()
        touch.apply_transform_2d(self.to_local)
        ret = super(ScrollView, self).on_touch_down(touch)
        touch.pop()
        return ret

    def on_touch_down(self, touch):
        if self.dispatch('on_scroll_start', touch):
            self._touch = touch
            touch.grab(self)
            return True

    def _touch_in_handle(self, pos, size, touch):
        x, y = pos
        width, height = size
        return x <= touch.x <= x + width and y <= touch.y <= y + height

    def on_scroll_start(self, touch, check_children=True):
        if check_children:
            touch.push()
            touch.apply_transform_2d(self.to_local)
            if self.dispatch_children('on_scroll_start', touch):
                touch.pop()
                return True
            touch.pop()

        if not self.collide_point(*touch.pos):
            touch.ud[self._get_uid('svavoid')] = True
            return
        if self.disabled:
            return True
        if self._touch or (not (self.do_scroll_x or self.do_scroll_y)):
            return self.simulate_touch_down(touch)

        # handle mouse scrolling, only if the viewport size is bigger than the
        # scrollview size, and if the user allowed to do it
        vp = self._viewport
        if not vp:
            return True
        scroll_type = self.scroll_type
        ud = touch.ud
        scroll_bar = 'bars' in scroll_type

        # check if touch is in bar_x(horizontal) or bar_y(vertical)
        # width_enable_overscroll or vp.width > self.width
        width_scrollable = self.always_overscroll or vp.width > self.width
        height_scrollable = self.always_overscroll or vp.height > self.height

        d = {
            'bottom': touch.y - self.y - self.bar_margin,
            'top': self.top - touch.y - self.bar_margin,
            'left': touch.x - self.x - self.bar_margin,
            'right': self.right - touch.x - self.bar_margin
        }

        ud['in_bar_x'] = (scroll_bar and width_scrollable
                          and (0 <= d[self.bar_pos_x] <= self.bar_width))
        ud['in_bar_y'] = (scroll_bar and height_scrollable
                          and (0 <= d[self.bar_pos_y] <= self.bar_width))

        if 'button' in touch.profile and touch.button.startswith('scroll'):
            btn = touch.button
            m = self.scroll_wheel_distance
            e = None

            if ((btn == 'scrolldown' and self.scroll_y >= 1)
                    or (btn == 'scrollup' and self.scroll_y <= 0)
                    or (btn == 'scrollleft' and self.scroll_x >= 1)
                    or (btn == 'scrollright' and self.scroll_x <= 0)):
                return False

            if (self.effect_x and self.do_scroll_y and height_scrollable
                    and btn in ('scrolldown', 'scrollup')):
                e = self.effect_x if ud['in_bar_x'] else self.effect_y

            elif (self.effect_y and self.do_scroll_x and width_scrollable
                  and btn in ('scrollleft', 'scrollright')):
                e = self.effect_y if ud['in_bar_y'] else self.effect_x

            if e:
                # make sure the effect's value is synced to scroll value
                self._update_effect_bounds()
                if btn in ('scrolldown', 'scrollleft'):
                    if self.smooth_scroll_end:
                        e.velocity -= m * self.smooth_scroll_end
                    else:
                        if self.always_overscroll:
                            e.value = e.value - m
                        else:
                            e.value = max(e.value - m, e.max)
                        e.velocity = 0
                elif btn in ('scrollup', 'scrollright'):
                    if self.smooth_scroll_end:
                        e.velocity += m * self.smooth_scroll_end
                    else:
                        if self.always_overscroll:
                            e.value = e.value + m
                        else:
                            e.value = min(e.value + m, e.min)
                        e.velocity = 0
                touch.ud[self._get_uid('svavoid')] = True
                e.trigger_velocity_update()
            return True

        in_bar = ud['in_bar_x'] or ud['in_bar_y']
        if scroll_type == ['bars'] and not in_bar:
            return self.simulate_touch_down(touch)

        if in_bar:
            if (ud['in_bar_y'] and not self._touch_in_handle(
                    self._handle_y_pos, self._handle_y_size, touch)):
                self.scroll_y = (touch.y - self.y) / self.height
            elif (ud['in_bar_x'] and not self._touch_in_handle(
                    self._handle_x_pos, self._handle_x_size, touch)):
                self.scroll_x = (touch.x - self.x) / self.width

        # no mouse scrolling, so the user is going to drag the scrollview with
        # this touch.
        self._touch = touch
        uid = self._get_uid()

        ud[uid] = {
            'mode': 'unknown',
            'dx': 0,
            'dy': 0,
            'user_stopped': in_bar,
            'frames': Clock.frames,
            'time': touch.time_start,
        }

        if (self.do_scroll_x and self.effect_x and not ud['in_bar_x']
                and not ud['in_bar_y']):
            # make sure the effect's value is synced to scroll value
            self._update_effect_bounds()

            self._effect_x_start_width = self.width
            self.effect_x.start(touch.x)
            self._scroll_x_mouse = self.scroll_x

        if (self.do_scroll_y and self.effect_y and not ud['in_bar_x']
                and not ud['in_bar_y']):
            # make sure the effect's value is synced to scroll value
            self._update_effect_bounds()

            self._effect_y_start_height = self.height
            self.effect_y.start(touch.y)
            self._scroll_y_mouse = self.scroll_y

        if not in_bar:
            Clock.schedule_once(self._change_touch_mode,
                                self.scroll_timeout / 1000.)
        return True

    def on_touch_move(self, touch):
        if self._touch is not touch:
            # don't pass on touch to children if outside the sv
            if self.collide_point(*touch.pos):
                # touch is in parent
                touch.push()
                touch.apply_transform_2d(self.to_local)
                super(ScrollView, self).on_touch_move(touch)
                touch.pop()
            return self._get_uid() in touch.ud
        if touch.grab_current is not self:
            return True

        if not any(
                isinstance(key, str) and key.startswith('sv.')
                for key in touch.ud):
            # don't pass on touch to children if outside the sv
            if self.collide_point(*touch.pos):
                # touch is in window coordinates
                touch.push()
                touch.apply_transform_2d(self.to_local)
                res = super(ScrollView, self).on_touch_move(touch)
                touch.pop()
                return res
            return False

        touch.ud['sv.handled'] = {'x': False, 'y': False}
        if self.dispatch('on_scroll_move', touch):
            return True

    def on_scroll_move(self, touch):
        if self._get_uid('svavoid') in touch.ud:
            return False

        touch.push()
        touch.apply_transform_2d(self.to_local)
        if self.dispatch_children('on_scroll_move', touch):
            touch.pop()
            return True
        touch.pop()

        rv = True

        # By default this touch can be used to defocus currently focused
        # widget, like any touch outside of ScrollView.
        touch.ud['sv.can_defocus'] = True

        uid = self._get_uid()
        if uid not in touch.ud:
            self._touch = False
            return self.on_scroll_start(touch, False)
        ud = touch.ud[uid]

        # check if the minimum distance has been travelled
        if ud['mode'] == 'unknown':
            if not (self.do_scroll_x or self.do_scroll_y):
                # touch is in parent, but _change expects window coords
                touch.push()
                touch.apply_transform_2d(self.to_local)
                touch.apply_transform_2d(self.to_window)
                self._change_touch_mode()
                touch.pop()
                return
            ud['dx'] += abs(touch.dx)
            ud['dy'] += abs(touch.dy)
            if ((ud['dx'] > self.scroll_distance and self.do_scroll_x)
                    or (ud['dy'] > self.scroll_distance and self.do_scroll_y)):
                ud['mode'] = 'scroll'

        if ud['mode'] == 'scroll':
            not_in_bar = not touch.ud.get('in_bar_x', False) and \
                not touch.ud.get('in_bar_y', False)

            if not touch.ud['sv.handled']['x'] and self.do_scroll_x \
                    and self.effect_x:
                width = self.width
                if touch.ud.get('in_bar_x', False):
                    if self.hbar[1] != 1:
                        dx = touch.dx / float(width - width * self.hbar[1])
                        self.scroll_x = min(max(self.scroll_x + dx, 0.), 1.)
                        self._trigger_update_from_scroll()
                elif not_in_bar:
                    self.effect_x.update(touch.x)

                if self.scroll_x < 0 or self.scroll_x > 1:
                    rv = False
                else:
                    touch.ud['sv.handled']['x'] = True
                # Touch resulted in scroll should not defocus focused widget
                touch.ud['sv.can_defocus'] = False
            if not touch.ud['sv.handled']['y'] and self.do_scroll_y \
                    and self.effect_y:
                height = self.height
                if touch.ud.get('in_bar_y', False):
                    dy = touch.dy / float(height - height * self.vbar[1])
                    self.scroll_y = min(max(self.scroll_y + dy, 0.), 1.)
                    self._trigger_update_from_scroll()
                elif not_in_bar:
                    self.effect_y.update(touch.y)

                if self.scroll_y < 0 or self.scroll_y > 1:
                    rv = False
                else:
                    touch.ud['sv.handled']['y'] = True
                # Touch resulted in scroll should not defocus focused widget
                touch.ud['sv.can_defocus'] = False
            ud['dt'] = touch.time_update - ud['time']
            ud['time'] = touch.time_update
            ud['user_stopped'] = True
        return rv

    def on_touch_up(self, touch):
        uid = self._get_uid('svavoid')
        if self._touch is not touch and uid not in touch.ud:
            # don't pass on touch to children if outside the sv
            if self.collide_point(*touch.pos):
                # touch is in parents
                touch.push()
                touch.apply_transform_2d(self.to_local)
                if super(ScrollView, self).on_touch_up(touch):
                    touch.pop()
                    return True
                touch.pop()
            return False

        if self.dispatch('on_scroll_stop', touch):
            touch.ungrab(self)
            if not touch.ud.get('sv.can_defocus', True):
                # Focused widget should stay focused
                FocusBehavior.ignored_touch.append(touch)
            return True

    def on_scroll_stop(self, touch, check_children=True):
        self._touch = None

        if check_children:
            touch.push()
            touch.apply_transform_2d(self.to_local)
            if self.dispatch_children('on_scroll_stop', touch):
                touch.pop()
                return True
            touch.pop()

        if self._get_uid('svavoid') in touch.ud:
            return
        if self._get_uid() not in touch.ud:
            return False

        self._touch = None
        uid = self._get_uid()
        ud = touch.ud[uid]
        not_in_bar = not touch.ud.get('in_bar_x', False) and \
            not touch.ud.get('in_bar_y', False)
        if self.do_scroll_x and self.effect_x and not_in_bar:
            self.effect_x.stop(touch.x)
        if self.do_scroll_y and self.effect_y and not_in_bar:
            self.effect_y.stop(touch.y)
        if ud['mode'] == 'unknown':
            # we must do the click at least..
            # only send the click if it was not a click to stop
            # autoscrolling
            if not ud['user_stopped']:
                self.simulate_touch_down(touch)
            Clock.schedule_once(partial(self._do_touch_up, touch), .2)

        ev = self._update_effect_bounds_ev
        if ev is None:
            ev = self._update_effect_bounds_ev = Clock.create_trigger(
                self._update_effect_bounds)
        ev()

        # if we do mouse scrolling, always accept it
        if 'button' in touch.profile and touch.button.startswith('scroll'):
            return True

        return self._get_uid() in touch.ud

    def scroll_to(self, widget, padding=10, animate=True):
        '''Scrolls the viewport to ensure that the given widget is visible,
        optionally with padding and animation. If animate is True (the
        default), then the default animation parameters will be used.
        Otherwise, it should be a dict containing arguments to pass to
        :class:`~kivy.animation.Animation` constructor.

        .. versionadded:: 1.9.1
        '''
        if not self.parent:
            return

        # if _viewport is layout and has pending operation, reschedule
        if hasattr(self._viewport, 'do_layout'):
            if self._viewport._trigger_layout.is_triggered:
                Clock.schedule_once(
                    lambda *dt: self.scroll_to(widget, padding, animate))
                return

        if isinstance(padding, (int, float)):
            padding = (padding, padding)

        pos = self.parent.to_widget(*widget.to_window(*widget.pos))
        cor = self.parent.to_widget(
            *widget.to_window(widget.right, widget.top))

        dx = dy = 0

        if pos[1] < self.y:
            dy = self.y - pos[1] + dp(padding[1])
        elif cor[1] > self.top:
            dy = self.top - cor[1] - dp(padding[1])

        if pos[0] < self.x:
            dx = self.x - pos[0] + dp(padding[0])
        elif cor[0] > self.right:
            dx = self.right - cor[0] - dp(padding[0])

        dsx, dsy = self.convert_distance_to_scroll(dx, dy)
        sxp = min(1, max(0, self.scroll_x - dsx))
        syp = min(1, max(0, self.scroll_y - dsy))

        if animate:
            if animate is True:
                animate = {'d': 0.2, 't': 'out_quad'}
            Animation.stop_all(self, 'scroll_x', 'scroll_y')
            Animation(scroll_x=sxp, scroll_y=syp, **animate).start(self)
        else:
            self.scroll_x = sxp
            self.scroll_y = syp

    def convert_distance_to_scroll(self, dx, dy):
        '''Convert a distance in pixels to a scroll distance, depending on the
        content size and the scrollview size.

        The result will be a tuple of scroll distance that can be added to
        :data:`scroll_x` and :data:`scroll_y`
        '''
        if not self._viewport:
            return 0, 0
        vp = self._viewport
        if vp.width > self.width:
            sw = vp.width - self.width
            sx = dx / float(sw)
        else:
            sx = 0
        if vp.height > self.height:
            sh = vp.height - self.height
            sy = dy / float(sh)
        else:
            sy = 1
        return sx, sy

    def update_from_scroll(self, *largs):
        '''Force the reposition of the content, according to current value of
        :attr:`scroll_x` and :attr:`scroll_y`.

        This method is automatically called when one of the :attr:`scroll_x`,
        :attr:`scroll_y`, :attr:`pos` or :attr:`size` properties change, or
        if the size of the content changes.
        '''
        if not self._viewport:
            self.g_translate.xy = self.pos
            return
        vp = self._viewport

        # update from size_hint
        if vp.size_hint_x is not None:
            w = vp.size_hint_x * self.width
            if vp.size_hint_min_x is not None:
                w = max(w, vp.size_hint_min_x)
            if vp.size_hint_max_x is not None:
                w = min(w, vp.size_hint_max_x)
            vp.width = w

        if vp.size_hint_y is not None:
            h = vp.size_hint_y * self.height
            if vp.size_hint_min_y is not None:
                h = max(h, vp.size_hint_min_y)
            if vp.size_hint_max_y is not None:
                h = min(h, vp.size_hint_max_y)
            vp.height = h

        if vp.width > self.width or self.always_overscroll:
            sw = vp.width - self.width
            x = self.x - self.scroll_x * sw
        else:
            x = self.x

        if vp.height > self.height or self.always_overscroll:
            sh = vp.height - self.height
            y = self.y - self.scroll_y * sh
        else:
            y = self.top - vp.height

        # from 1.8.0, we now use a matrix by default, instead of moving the
        # widget position behind. We set it here, but it will be a no-op most
        # of the time.
        vp.pos = 0, 0
        self.g_translate.xy = x, y

        # New in 1.2.0, show bar when scrolling happens and (changed in 1.9.0)
        # fade to bar_inactive_color when no scroll is happening.
        ev = self._bind_inactive_bar_color_ev
        if ev is None:
            ev = self._bind_inactive_bar_color_ev = Clock.create_trigger(
                self._bind_inactive_bar_color, .5)
        self.funbind('bar_inactive_color', self._change_bar_color)
        Animation.stop_all(self, '_bar_color')
        self.fbind('bar_color', self._change_bar_color)
        self._bar_color = self.bar_color
        ev()

    def _bind_inactive_bar_color(self, *l):
        self.funbind('bar_color', self._change_bar_color)
        self.fbind('bar_inactive_color', self._change_bar_color)
        Animation(_bar_color=self.bar_inactive_color, d=.5,
                  t='out_quart').start(self)

    def _change_bar_color(self, inst, value):
        self._bar_color = value

    def add_widget(self, widget, *args, **kwargs):
        if self._viewport:
            raise Exception('ScrollView accept only one widget')
        canvas = self.canvas
        self.canvas = self.canvas_viewport
        super(ScrollView, self).add_widget(widget, *args, **kwargs)
        self.canvas = canvas
        self._viewport = widget
        widget.bind(size=self._trigger_update_from_scroll,
                    size_hint_min=self._trigger_update_from_scroll)
        self._trigger_update_from_scroll()

    def remove_widget(self, widget, *args, **kwargs):
        canvas = self.canvas
        self.canvas = self.canvas_viewport
        super(ScrollView, self).remove_widget(widget, *args, **kwargs)
        self.canvas = canvas
        if widget is self._viewport:
            self._viewport = None

    def _get_uid(self, prefix='sv'):
        return '{0}.{1}'.format(prefix, self.uid)

    def _change_touch_mode(self, *largs):
        if not self._touch:
            return
        uid = self._get_uid()
        touch = self._touch
        if uid not in touch.ud:
            self._touch = False
            return
        ud = touch.ud[uid]
        if ud['mode'] != 'unknown' or ud['user_stopped']:
            return
        diff_frames = Clock.frames - ud['frames']

        # in order to be able to scroll on very slow devices, let at least 3
        # frames displayed to accumulate some velocity. And then, change the
        # touch mode. Otherwise, we might never be able to compute velocity,
        # and no way to scroll it. See #1464 and #1499
        if diff_frames < 3:
            Clock.schedule_once(self._change_touch_mode, 0)
            return

        if self.do_scroll_x and self.effect_x:
            self.effect_x.cancel()
        if self.do_scroll_y and self.effect_y:
            self.effect_y.cancel()
        # XXX the next line was in the condition. But this stop
        # the possibility to "drag" an object out of the scrollview in the
        # non-used direction: if you have an horizontal scrollview, a
        # vertical gesture will not "stop" the scroll view to look for an
        # horizontal gesture, until the timeout is done.
        # and touch.dx + touch.dy == 0:
        touch.ungrab(self)
        self._touch = None
        # touch is in window coords
        touch.push()
        touch.apply_transform_2d(self.to_widget)
        touch.apply_transform_2d(self.to_parent)
        self.simulate_touch_down(touch)
        touch.pop()
        return

    def _do_touch_up(self, touch, *largs):
        # touch is in window coords
        touch.push()
        touch.apply_transform_2d(self.to_widget)
        super(ScrollView, self).on_touch_up(touch)
        touch.pop()
        # don't forget about grab event!
        for x in touch.grab_list[:]:
            touch.grab_list.remove(x)
            x = x()
            if not x:
                continue
            touch.grab_current = x
            # touch is in window coords
            touch.push()
            touch.apply_transform_2d(self.to_widget)
            super(ScrollView, self).on_touch_up(touch)
            touch.pop()
        touch.grab_current = None
Exemplo n.º 14
0
class MDSlider(ThemableBehavior, Slider):
    active = BooleanProperty(False)
    """
    If the slider is clicked.

    :attr:`active` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    hint = BooleanProperty(True)
    """
    If True, then the current value is displayed above the slider.

    :attr:`hint` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    hint_bg_color = ColorProperty(None)
    """
    Hint rectangle color in ``rgba`` format.

    :attr:`hint_bg_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    hint_text_color = ColorProperty(None)
    """
    Hint text color in ``rgba`` format.

    :attr:`hint_text_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    hint_radius = NumericProperty(4)
    """
    Hint radius.

    :attr:`hint_radius` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `4`.
    """

    show_off = BooleanProperty(True)
    """
    Show the `'off'` ring when set to minimum value.

    :attr:`show_off` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    color = ColorProperty([0, 0, 0, 0])
    """
    Color slider in ``rgba`` format.

    :attr:`color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    _track_color_active = ColorProperty([0, 0, 0, 0])
    _track_color_normal = ColorProperty([0, 0, 0, 0])
    _track_color_disabled = ColorProperty([0, 0, 0, 0])
    _thumb_pos = ListProperty([0, 0])
    _thumb_color_disabled = ColorProperty(
        get_color_from_hex(colors["Gray"]["400"])
    )
    # Internal state of ring
    _is_off = BooleanProperty(False)
    # Internal adjustment to reposition sliders for ring
    _offset = ListProperty((0, 0))

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.theme_cls.bind(
            theme_style=self._set_colors,
            primary_color=self._set_colors,
            primary_palette=self._set_colors,
        )
        self._set_colors()

    def on_hint(self, instance, value):
        if not value:
            self.remove_widget(self.ids.hint_box)

    def on_value_normalized(self, *args):
        """When the ``value == min`` set it to `'off'` state and make slider
        a ring.
        """

        self._update_is_off()

    def on_show_off(self, *args):
        self._update_is_off()

    def on__is_off(self, *args):
        self._update_offset()

    def on_active(self, *args):
        self._update_offset()

    def on_touch_down(self, touch):
        if super().on_touch_down(touch):
            self.active = True

    def on_touch_up(self, touch):
        if super().on_touch_up(touch):
            self.active = False

    def _update_offset(self):
        """Offset is used to shift the sliders so the background color
        shows through the off circle.
        """

        d = 2 if self.active else 0
        self._offset = (dp(11 + d), dp(11 + d)) if self._is_off else (0, 0)

    def _update_is_off(self):
        self._is_off = self.show_off and (self.value_normalized == 0)

    def _set_colors(self, *args):
        if self.theme_cls.theme_style == "Dark":
            self._track_color_normal = get_color_from_hex("FFFFFF")
            self._track_color_normal[3] = 0.3
            self._track_color_active = self._track_color_normal
            self._track_color_disabled = self._track_color_normal
            if self.color == [0, 0, 0, 0]:
                self.color = get_color_from_hex(
                    colors[self.theme_cls.primary_palette]["200"]
                )
            self.thumb_color_disabled = get_color_from_hex(
                colors["Gray"]["800"]
            )
        else:
            self._track_color_normal = get_color_from_hex("000000")
            self._track_color_normal[3] = 0.26
            self._track_color_active = get_color_from_hex("000000")
            self._track_color_active[3] = 0.38
            self._track_color_disabled = get_color_from_hex("000000")
            self._track_color_disabled[3] = 0.26
            if self.color == [0, 0, 0, 0]:
                self.color = self.theme_cls.primary_color
Exemplo n.º 15
0
class HotReloadViewer(ThemableBehavior, MDBoxLayout):
    """
    :Events:
        :attr:`on_error`
            Called when an error occurs in the KV-file that the user is editing.
    """

    path = StringProperty()
    """Path to KV file.

    :attr:`path` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    errors = BooleanProperty(False)
    """
    Show errors while editing KV-file.

    :attr:`errors` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    errors_background_color = ColorProperty(None)
    """
    Error background color.

    :attr:`errors_background_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    errors_text_color = ColorProperty(None)
    """
    Error text color.

    :attr:`errors_text_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    _temp_widget = None

    def __init__(self, **kwargs):
        self.observer = Observer()
        self.error_text = HotReloadErrorText()
        super().__init__(**kwargs)
        self.register_event_type("on_error")

    @mainthread
    def update(self, *args):
        """Updates and displays the KV-file that the user edits."""

        Builder.unload_file(self.path)
        self.clear_widgets()
        try:
            self.padding = (0, 0, 0, 0)
            self.md_bg_color = (0, 0, 0, 0)
            self._temp_widget = Builder.load_file(self.path)
            self.add_widget(self._temp_widget)
        except Exception as error:
            self.show_error(error)
            self.dispatch("on_error", error)

    def show_error(self, error):
        """Displays text with a current error."""

        if self._temp_widget and not self.errors:
            self.add_widget(self._temp_widget)
            return
        else:
            if self.errors_background_color:
                self.md_bg_color = self.errors_background_color
            self.padding = ("4dp", "4dp", "4dp", "4dp")
            self.error_text.text = (
                error.message
                if getattr(error, r"message", None)
                else str(error)
            )
            self.add_widget(self.error_text)

    def on_error(self, *args):
        """
        Called when an error occurs in the KV-file that the user is editing.
        """

    def on_errors_text_color(self, instance, value):
        self.error_text.errors_text_color = value

    def on_path(self, instance, value):
        value = os.path.abspath(value)
        self.observer.schedule(
            HotReloadHandler(self.update, value), os.path.dirname(value)
        )
        self.observer.start()
        Clock.schedule_once(self.update, 1)
Exemplo n.º 16
0
class Bubble(GridLayout):
    '''Bubble class. See module documentation for more information.
    '''

    background_color = ColorProperty([1, 1, 1, 1])
    '''Background color, in the format (r, g, b, a). To use it you have to set
    either :attr:`background_image` or :attr:`arrow_image` first.

    :attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and
    defaults to [1, 1, 1, 1].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    '''

    border = ListProperty([16, 16, 16, 16])
    '''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
    graphics instruction. Used with the :attr:`background_image`.
    It should be used when using custom backgrounds.

    It must be a list of 4 values: (bottom, right, top, left). Read the
    BorderImage instructions for more information about how to use it.

    :attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to
    (16, 16, 16, 16)
    '''

    background_image = StringProperty(
        'atlas://data/images/defaulttheme/bubble')
    '''Background image of the bubble.

    :attr:`background_image` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/bubble'.
    '''

    arrow_image = StringProperty(
        'atlas://data/images/defaulttheme/bubble_arrow')
    ''' Image of the arrow pointing to the bubble.

    :attr:`arrow_image` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/bubble_arrow'.
    '''

    show_arrow = BooleanProperty(True)
    ''' Indicates whether to show arrow.

    .. versionadded:: 1.8.0

    :attr:`show_arrow` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to `True`.
    '''

    arrow_pos = OptionProperty(
        'bottom_mid',
        options=('left_top', 'left_mid', 'left_bottom', 'top_left', 'top_mid',
                 'top_right', 'right_top', 'right_mid', 'right_bottom',
                 'bottom_left', 'bottom_mid', 'bottom_right'))
    '''Specifies the position of the arrow relative to the bubble.
    Can be one of: left_top, left_mid, left_bottom top_left, top_mid, top_right
    right_top, right_mid, right_bottom bottom_left, bottom_mid, bottom_right.

    :attr:`arrow_pos` is a :class:`~kivy.properties.OptionProperty` and
    defaults to 'bottom_mid'.
    '''

    content = ObjectProperty(None)
    '''This is the object where the main content of the bubble is held.

    :attr:`content` is a :class:`~kivy.properties.ObjectProperty` and
    defaults to 'None'.
    '''

    orientation = OptionProperty('horizontal',
                                 options=('horizontal', 'vertical'))
    '''This specifies the manner in which the children inside bubble
    are arranged. Can be one of 'vertical' or 'horizontal'.

    :attr:`orientation` is a :class:`~kivy.properties.OptionProperty` and
    defaults to 'horizontal'.
    '''

    limit_to = ObjectProperty(None, allownone=True)
    '''Specifies the widget to which the bubbles position is restricted.

    .. versionadded:: 1.6.0

    :attr:`limit_to` is a :class:`~kivy.properties.ObjectProperty` and
    defaults to 'None'.
    '''

    border_auto_scale = OptionProperty('both_lower',
                                       options=[
                                           'off', 'both', 'x_only', 'y_only',
                                           'y_full_x_lower', 'x_full_y_lower',
                                           'both_lower'
                                       ])
    '''Specifies the :attr:`kivy.graphics.BorderImage.auto_scale`
    value on the background BorderImage.

    .. versionadded:: 1.11.0

    :attr:`border_auto_scale` is a
    :class:`~kivy.properties.OptionProperty` and defaults to
    'both_lower'.
    '''
    def __init__(self, **kwargs):
        self._prev_arrow_pos = None
        self._arrow_layout = BoxLayout()
        self._bk_img = Image(source=self.background_image,
                             allow_stretch=True,
                             keep_ratio=False,
                             color=self.background_color)
        self.background_texture = self._bk_img.texture
        self._arrow_img = Image(source=self.arrow_image,
                                allow_stretch=True,
                                color=self.background_color)
        self.content = content = BubbleContent(parent=self)
        super(Bubble, self).__init__(**kwargs)
        content.parent = None
        self.add_widget(content)
        self.on_arrow_pos()

    def add_widget(self, widget, *args, **kwargs):
        content = self.content
        if content is None:
            return
        if widget == content or widget == self._arrow_img\
                or widget == self._arrow_layout:
            super(Bubble, self).add_widget(widget, *args, **kwargs)
        else:
            content.add_widget(widget, *args, **kwargs)

    def remove_widget(self, widget, *args, **kwargs):
        content = self.content
        if not content:
            return
        if widget == content or widget == self._arrow_img\
                or widget == self._arrow_layout:
            super(Bubble, self).remove_widget(widget, *args, **kwargs)
        else:
            content.remove_widget(widget, *args, **kwargs)

    def clear_widgets(self, *args, **kwargs):
        if self.content:
            self.content.clear_widgets(*args, **kwargs)

    def on_show_arrow(self, instance, value):
        self._arrow_img.opacity = int(value)

    def on_parent(self, instance, value):
        Clock.schedule_once(self._update_arrow)

    def on_pos(self, instance, pos):
        lt = self.limit_to

        if lt:
            self.limit_to = None
            if lt is EventLoop.window:
                x = y = 0
                top = lt.height
                right = lt.width
            else:
                x, y = lt.x, lt.y
                top, right = lt.top, lt.right

            self.x = max(self.x, x)
            self.right = min(self.right, right)
            self.top = min(self.top, top)
            self.y = max(self.y, y)
            self.limit_to = lt

    def on_background_image(self, *l):
        self._bk_img.source = self.background_image

    def on_background_color(self, *l):
        if self.content is None:
            return
        self._arrow_img.color = self._bk_img.color = self.background_color

    def on_orientation(self, *l):
        content = self.content
        if not content:
            return
        if self.orientation[0] == 'v':
            content.cols = 1
            content.rows = 99
        else:
            content.cols = 99
            content.rows = 1

    def on_arrow_image(self, *l):
        self._arrow_img.source = self.arrow_image

    def on_arrow_pos(self, *l):
        self_content = self.content
        if not self_content:
            Clock.schedule_once(self.on_arrow_pos)
            return
        if self_content not in self.children:
            Clock.schedule_once(self.on_arrow_pos)
            return
        self_arrow_pos = self.arrow_pos
        if self._prev_arrow_pos == self_arrow_pos:
            return
        self._prev_arrow_pos = self_arrow_pos

        self_arrow_layout = self._arrow_layout
        self_arrow_layout.clear_widgets()
        self_arrow_img = self._arrow_img
        self._sctr = self._arrow_img
        super(Bubble, self).clear_widgets()
        self_content.parent = None

        self_arrow_img.size_hint = (1, None)
        self_arrow_img.height = dp(self_arrow_img.texture_size[1])
        self_arrow_img.pos = 0, 0
        widget_list = []
        arrow_list = []
        parent = self_arrow_img.parent
        if parent:
            parent.remove_widget(self_arrow_img)

        if self_arrow_pos[0] == 'b' or self_arrow_pos[0] == 't':
            self.cols = 1
            self.rows = 3
            self_arrow_layout.orientation = 'horizontal'
            self_arrow_img.width = self.width / 3
            self_arrow_layout.size_hint = (1, None)
            self_arrow_layout.height = self_arrow_img.height
            if self_arrow_pos[0] == 'b':
                if self_arrow_pos == 'bottom_mid':
                    widget_list = (self_content, self_arrow_img)
                else:
                    if self_arrow_pos == 'bottom_left':
                        arrow_list = (self_arrow_img, Widget(), Widget())
                    elif self_arrow_pos == 'bottom_right':
                        # add two dummy widgets
                        arrow_list = (Widget(), Widget(), self_arrow_img)
                    widget_list = (self_content, self_arrow_layout)
            else:
                sctr = Scatter(do_translation=False,
                               rotation=180,
                               do_rotation=False,
                               do_scale=False,
                               size_hint=(None, None),
                               size=self_arrow_img.size)
                sctr.add_widget(self_arrow_img)
                if self_arrow_pos == 'top_mid':
                    # add two dummy widgets
                    arrow_list = (Widget(), sctr, Widget())
                elif self_arrow_pos == 'top_left':
                    arrow_list = (sctr, Widget(), Widget())
                elif self_arrow_pos == 'top_right':
                    arrow_list = (Widget(), Widget(), sctr)
                widget_list = (self_arrow_layout, self_content)
        elif self_arrow_pos[0] == 'l' or self_arrow_pos[0] == 'r':
            self.cols = 3
            self.rows = 1
            self_arrow_img.width = self.height / 3
            self_arrow_layout.orientation = 'vertical'
            self_arrow_layout.cols = 1
            self_arrow_layout.size_hint = (None, 1)
            self_arrow_layout.width = self_arrow_img.height

            rotation = -90 if self_arrow_pos[0] == 'l' else 90
            self._sctr = sctr = Scatter(do_translation=False,
                                        rotation=rotation,
                                        do_rotation=False,
                                        do_scale=False,
                                        size_hint=(None, None),
                                        size=(self_arrow_img.size))
            sctr.add_widget(self_arrow_img)

            if self_arrow_pos[-4:] == '_top':
                arrow_list = (Widget(size_hint=(1, .07)), sctr,
                              Widget(size_hint=(1, .3)))
            elif self_arrow_pos[-4:] == '_mid':
                arrow_list = (Widget(), sctr, Widget())
                Clock.schedule_once(self._update_arrow)
            elif self_arrow_pos[-7:] == '_bottom':
                arrow_list = (Widget(), Widget(), sctr)

            if self_arrow_pos[0] == 'l':
                widget_list = (self_arrow_layout, self_content)
            else:
                widget_list = (self_content, self_arrow_layout)

        # add widgets to arrow_layout
        add = self_arrow_layout.add_widget
        for widg in arrow_list:
            add(widg)

        # add widgets to self
        add = self.add_widget
        for widg in widget_list:
            add(widg)

    def _update_arrow(self, *dt):
        if self.arrow_pos in ('left_mid', 'right_mid'):
            self._sctr.center_y = self._arrow_layout.center_y

    @property
    def _fills_row_first(self):
        return True

    @property
    def _fills_from_left_to_right(self):
        return True

    @property
    def _fills_from_top_to_bottom(self):
        return True
Exemplo n.º 17
0
class Label(Widget):
    '''Label class, see module documentation for more information.

    :Events:
        `on_ref_press`
            Fired when the user clicks on a word referenced with a
            ``[ref]`` tag in a text markup.
    '''

    __events__ = ['on_ref_press']

    _font_properties = ('text', 'font_size', 'font_name', 'bold', 'italic',
                        'underline', 'strikethrough', 'font_family', 'color',
                        'disabled_color', 'halign', 'valign', 'padding_x',
                        'padding_y', 'outline_width', 'disabled_outline_color',
                        'outline_color', 'text_size', 'shorten', 'mipmap',
                        'line_height', 'max_lines', 'strip', 'shorten_from',
                        'split_str', 'ellipsis_options', 'unicode_errors',
                        'markup', 'font_hinting', 'font_kerning',
                        'font_blended', 'font_context', 'font_features',
                        'base_direction', 'text_language')

    def __init__(self, **kwargs):
        self._trigger_texture = Clock.create_trigger(self.texture_update, -1)
        super(Label, self).__init__(**kwargs)

        # bind all the property for recreating the texture
        d = Label._font_properties
        fbind = self.fbind
        update = self._trigger_texture_update
        fbind('disabled', update, 'disabled')
        for x in d:
            fbind(x, update, x)

        self._label = None
        self._create_label()

        # force the texture creation
        self._trigger_texture()

    def _create_label(self):
        # create the core label class according to markup value
        if self._label is not None:
            cls = self._label.__class__
        else:
            cls = None
        markup = self.markup
        if (markup and cls is not CoreMarkupLabel) or \
           (not markup and cls is not CoreLabel):
            # markup have change, we need to change our rendering method.
            d = Label._font_properties
            dkw = dict(list(zip(d, [getattr(self, x) for x in d])))
            if markup:
                self._label = CoreMarkupLabel(**dkw)
            else:
                self._label = CoreLabel(**dkw)

    def _trigger_texture_update(self, name=None, source=None, value=None):
        # check if the label core class need to be switch to a new one
        if name == 'markup':
            self._create_label()
        if source:
            if name == 'text':
                self._label.text = value
            elif name == 'text_size':
                self._label.usersize = value
            elif name == 'font_size':
                self._label.options[name] = value
            elif name == 'disabled_color' and self.disabled:
                self._label.options['color'] = value
            elif name == 'disabled_outline_color' and self.disabled:
                self._label.options['outline_color'] = value
            elif name == 'disabled':
                self._label.options['color'] = self.disabled_color if value \
                    else self.color
                self._label.options['outline_color'] = (
                    self.disabled_outline_color
                    if value else self.outline_color)
            else:
                self._label.options[name] = value
        self._trigger_texture()

    def texture_update(self, *largs):
        '''Force texture recreation with the current Label properties.

        After this function call, the :attr:`texture` and :attr:`texture_size`
        will be updated in this order.
        '''
        mrkup = self._label.__class__ is CoreMarkupLabel
        self.texture = None

        if (not self._label.text or (self.halign == 'justify' or self.strip)
                and not self._label.text.strip()):
            self.texture_size = (0, 0)
            self.is_shortened = False
            if mrkup:
                self.refs, self._label._refs = {}, {}
                self.anchors, self._label._anchors = {}, {}
        else:
            if mrkup:
                text = self.text
                # we must strip here, otherwise, if the last line is empty,
                # markup will retain the last empty line since it only strips
                # line by line within markup
                if self.halign == 'justify' or self.strip:
                    text = text.strip()
                self._label.text = ''.join(
                    ('[color=',
                     get_hex_from_color(
                         self.disabled_color if self.disabled else self.color),
                     ']', text, '[/color]'))
                self._label.refresh()
                # force the rendering to get the references
                if self._label.texture:
                    self._label.texture.bind()
                self.refs = self._label.refs
                self.anchors = self._label.anchors
            else:
                self._label.refresh()
            texture = self._label.texture
            if texture is not None:
                self.texture = self._label.texture
                self.texture_size = list(self.texture.size)
            self.is_shortened = self._label.is_shortened

    def on_touch_down(self, touch):
        if super(Label, self).on_touch_down(touch):
            return True
        if not len(self.refs):
            return False
        tx, ty = touch.pos
        tx -= self.center_x - self.texture_size[0] / 2.
        ty -= self.center_y - self.texture_size[1] / 2.
        ty = self.texture_size[1] - ty
        for uid, zones in self.refs.items():
            for zone in zones:
                x, y, w, h = zone
                if x <= tx <= w and y <= ty <= h:
                    self.dispatch('on_ref_press', uid)
                    return True
        return False

    def on_ref_press(self, ref):
        pass

    #
    # Properties
    #

    disabled_color = ColorProperty([1, 1, 1, .3])
    '''The color of the text when the widget is disabled, in the (r, g, b, a)
    format.

    .. versionadded:: 1.8.0

    :attr:`disabled_color` is a :class:`~kivy.properties.ColorProperty` and
    defaults to [1, 1, 1, .3].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    '''

    text = StringProperty('')
    '''Text of the label.

    Creation of a simple hello world::

        widget = Label(text='Hello world')

    If you want to create the widget with an unicode string, use::

        widget = Label(text=u'My unicode string')

    :attr:`text` is a :class:`~kivy.properties.StringProperty` and defaults to
    ''.
    '''

    text_size = ListProperty([None, None])
    '''By default, the label is not constrained to any bounding box.
    You can set the size constraint of the label with this property.
    The text will autoflow into the constraints. So although the font size
    will not be reduced, the text will be arranged to fit into the box as best
    as possible, with any text still outside the box clipped.

    This sets and clips :attr:`texture_size` to text_size if not None.

    .. versionadded:: 1.0.4

    For example, whatever your current widget size is, if you want the label to
    be created in a box with width=200 and unlimited height::

        Label(text='Very big big line', text_size=(200, None))

    .. note::

        This text_size property is the same as the
        :attr:`~kivy.core.text.Label.usersize` property in the
        :class:`~kivy.core.text.Label` class. (It is named size= in the
        constructor.)

    :attr:`text_size` is a :class:`~kivy.properties.ListProperty` and
    defaults to (None, None), meaning no size restriction by default.
    '''

    base_direction = OptionProperty(
        None,
        options=['ltr', 'rtl', 'weak_rtl', 'weak_ltr', None],
        allownone=True)
    '''Base direction of text, this impacts horizontal alignment when
    :attr:`halign` is `auto` (the default). Available options are: None,
    "ltr" (left to right), "rtl" (right to left) plus "weak_ltr" and
    "weak_rtl".

    .. note::
        This feature requires the Pango text provider.

    .. note::
        Weak modes are currently not implemented in Kivy text layout, and
        have the same effect as setting strong mode.

    .. versionadded:: 1.11.0

    :attr:`base_direction` is an :class:`~kivy.properties.OptionProperty` and
    defaults to None (autodetect RTL if possible, otherwise LTR).
    '''

    text_language = StringProperty(None, allownone=True)
    '''Language of the text, if None Pango will determine it from locale.
    This is an RFC-3066 format language tag (as a string), for example
    "en_US", "zh_CN", "fr" or "ja". This can impact font selection, metrics
    and rendering. For example, the same bytes of text can look different
    for `ur` and `ar` languages, though both use Arabic script.

    .. note::
        This feature requires the Pango text provider.

    .. versionadded:: 1.11.0

    :attr:`text_language` is a :class:`~kivy.properties.StringProperty` and
    defaults to None.
    '''

    font_context = StringProperty(None, allownone=True)
    '''Font context. `None` means the font is used in isolation, so you are
    guaranteed to be drawing with the TTF file resolved by :attr:`font_name`.
    Specifying a value here will load the font file into a named context,
    enabling fallback between all fonts in the same context. If a font
    context is set, you are not guaranteed that rendering will actually use
    the specified TTF file for all glyphs (Pango will pick the one it
    thinks is best).

    If Kivy is linked against a system-wide installation of FontConfig,
    you can load the system fonts by specifying a font context starting
    with the special string `system://`. This will load the system
    fontconfig configuration, and add your application-specific fonts on
    top of it (this imposes a signifficant risk of family name collision,
    Pango may not use your custom font file, but pick one from the system)

    .. note::
        This feature requires the Pango text provider.

    .. versionadded:: 1.11.0

    :attr:`font_context` is a :class:`~kivy.properties.StringProperty` and
    defaults to None.
    '''

    font_family = StringProperty(None, allownone=True)
    '''Font family, this is only applicable when using :attr:`font_context`
    option. The specified font family will be requested, but note that it may
    not be available, or there could be multiple fonts registered with the
    same family. The value can be a family name (string) available in the
    font context (for example a system font in a `system://` context, or a
    custom font file added using :class:`kivy.core.text.FontContextManager`).
    If set to `None`, font selection is controlled by the :attr:`font_name`
    setting.

    .. note::
        If using :attr:`font_name` to reference a custom font file, you
        should leave this as `None`. The family name is managed automatically
        in this case.

    .. note::
        This feature requires the Pango text provider.

    .. versionadded:: 1.11.0

    :attr:`font_family` is a :class:`~kivy.properties.StringProperty` and
    defaults to None.
    '''

    font_name = StringProperty(DEFAULT_FONT)
    '''Filename of the font to use. The path can be absolute or relative.
    Relative paths are resolved by the :func:`~kivy.resources.resource_find`
    function.

    .. warning::

        Depending of your text provider, the font file can be ignored. However,
        you can mostly use this without problems.

        If the font used lacks the glyphs for the particular language/symbols
        you are using, you will see '[]' blank box characters instead of the
        actual glyphs. The solution is to use a font that has the glyphs you
        need to display. For example, to display |unicodechar|, use a font such
        as freesans.ttf that has the glyph.

        .. |unicodechar| image:: images/unicode-char.png

    :attr:`font_name` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'Roboto'. This value is taken
    from :class:`~kivy.config.Config`.
    '''

    font_size = NumericProperty('15sp')
    '''Font size of the text, in pixels.

    :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 15sp.
    '''

    font_features = StringProperty()
    '''OpenType font features, in CSS format, this is passed straight
    through to Pango. The effects of requesting a feature depends on loaded
    fonts, library versions, etc. For a complete list of features, see:

    https://en.wikipedia.org/wiki/List_of_typographic_features

    .. note::
        This feature requires the Pango text provider, and Pango library
        v1.38 or later.

    .. versionadded:: 1.11.0

    :attr:`font_features` is a :class:`~kivy.properties.StringProperty` and
    defaults to an empty string.
    '''

    line_height = NumericProperty(1.0)
    '''Line Height for the text. e.g. line_height = 2 will cause the spacing
    between lines to be twice the size.

    :attr:`line_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.0.

    .. versionadded:: 1.5.0
    '''

    bold = BooleanProperty(False)
    '''Indicates use of the bold version of your font.

    .. note::

        Depending of your font, the bold attribute may have no impact on your
        text rendering.

    :attr:`bold` is a :class:`~kivy.properties.BooleanProperty` and defaults to
    False.
    '''

    italic = BooleanProperty(False)
    '''Indicates use of the italic version of your font.

    .. note::

        Depending of your font, the italic attribute may have no impact on your
        text rendering.

    :attr:`italic` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    underline = BooleanProperty(False)
    '''Adds an underline to the text.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`underline` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    strikethrough = BooleanProperty(False)
    '''Adds a strikethrough line to the text.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`strikethrough` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    padding_x = NumericProperty(0)
    '''Horizontal padding of the text inside the widget box.

    :attr:`padding_x` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.

    .. versionchanged:: 1.9.0
        `padding_x` has been fixed to work as expected.
        In the past, the text was padded by the negative of its values.
    '''

    padding_y = NumericProperty(0)
    '''Vertical padding of the text inside the widget box.

    :attr:`padding_y` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.

    .. versionchanged:: 1.9.0
        `padding_y` has been fixed to work as expected.
        In the past, the text was padded by the negative of its values.
    '''

    padding = ReferenceListProperty(padding_x, padding_y)
    '''Padding of the text in the format (padding_x, padding_y)

    :attr:`padding` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`padding_x`, :attr:`padding_y`) properties.
    '''

    halign = OptionProperty(
        'auto', options=['left', 'center', 'right', 'justify', 'auto'])
    '''Horizontal alignment of the text.

    :attr:`halign` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'auto'. Available options are : auto, left, center, right and
    justify. Auto will attempt to autodetect horizontal alignment for RTL text
    (Pango only), otherwise it behaves like `left`.

    .. warning::

        This doesn't change the position of the text texture of the Label
        (centered), only the position of the text in this texture. You probably
        want to bind the size of the Label to the :attr:`texture_size` or set a
        :attr:`text_size`.

    .. versionchanged:: 1.10.1
        Added `auto` option

    .. versionchanged:: 1.6.0
        A new option was added to :attr:`halign`, namely `justify`.
    '''

    valign = OptionProperty('bottom',
                            options=['bottom', 'middle', 'center', 'top'])
    '''Vertical alignment of the text.

    :attr:`valign` is an :class:`~kivy.properties.OptionProperty` and defaults
    to 'bottom'. Available options are : `'bottom'`,
    `'middle'` (or `'center'`) and `'top'`.

    .. versionchanged:: 1.10.0
        The `'center'` option has been added as an alias of `'middle'`.

    .. warning::

        This doesn't change the position of the text texture of the Label
        (centered), only the position of the text within this texture. You
        probably want to bind the size of the Label to the :attr:`texture_size`
        or set a :attr:`text_size` to change this behavior.
    '''

    color = ColorProperty([1, 1, 1, 1])
    '''Text color, in the format (r, g, b, a).

    :attr:`color` is a :class:`~kivy.properties.ColorProperty` and defaults to
    [1, 1, 1, 1].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    '''

    outline_width = NumericProperty(None, allownone=True)
    '''Width in pixels for the outline around the text. No outline will be
    rendered if the value is None.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`outline_width` is a :class:`~kivy.properties.NumericProperty` and
    defaults to None.
    '''

    outline_color = ColorProperty([0, 0, 0, 1])
    '''The color of the text outline, in the (r, g, b) format.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`outline_color` is a :class:`~kivy.properties.ColorProperty` and
    defaults to [0, 0, 0, 1].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`. Alpha component is ignored
        and assigning value to it has no effect.
    '''

    disabled_outline_color = ColorProperty([0, 0, 0, 1])
    '''The color of the text outline when the widget is disabled, in the
    (r, g, b) format.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`disabled_outline_color` is a :class:`~kivy.properties.ColorProperty`
    and defaults to [0, 0, 0].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`. Alpha component is ignored
        and assigning value to it has no effect.
    '''

    texture = ObjectProperty(None, allownone=True)
    '''Texture object of the text.
    The text is rendered automatically when a property changes. The OpenGL
    texture created in this operation is stored in this property. You can use
    this :attr:`texture` for any graphics elements.

    Depending on the texture creation, the value will be a
    :class:`~kivy.graphics.texture.Texture` or
    :class:`~kivy.graphics.texture.TextureRegion` object.

    .. warning::

        The :attr:`texture` update is scheduled for the next frame. If you need
        the texture immediately after changing a property, you have to call
        the :meth:`texture_update` method before accessing :attr:`texture`::

            l = Label(text='Hello world')
            # l.texture is good
            l.font_size = '50sp'
            # l.texture is not updated yet
            l.texture_update()
            # l.texture is good now.

    :attr:`texture` is an :class:`~kivy.properties.ObjectProperty` and defaults
    to None.
    '''

    texture_size = ListProperty([0, 0])
    '''Texture size of the text. The size is determined by the font size and
    text. If :attr:`text_size` is [None, None], the texture will be the size
    required to fit the text, otherwise it's clipped to fit :attr:`text_size`.

    When :attr:`text_size` is [None, None], one can bind to texture_size
    and rescale it proportionally to fit the size of the label in order to
    make the text fit maximally in the label.

    .. warning::

        The :attr:`texture_size` is set after the :attr:`texture`
        property. If you listen for changes to :attr:`texture`,
        :attr:`texture_size` will not be up-to-date in your callback.
        Bind to :attr:`texture_size` instead.
    '''

    mipmap = BooleanProperty(False)
    '''Indicates whether OpenGL mipmapping is applied to the texture or not.
    Read :ref:`mipmap` for more information.

    .. versionadded:: 1.0.7

    :attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    shorten = BooleanProperty(False)
    '''
    Indicates whether the label should attempt to shorten its textual contents
    as much as possible if a :attr:`text_size` is given. Setting this to True
    without an appropriately set :attr:`text_size` will lead to unexpected
    results.

    :attr:`shorten_from` and :attr:`split_str` control the direction from
    which the :attr:`text` is split, as well as where in the :attr:`text` we
    are allowed to split.

    :attr:`shorten` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    shorten_from = OptionProperty('center',
                                  options=['left', 'center', 'right'])
    '''The side from which we should shorten the text from, can be left,
    right, or center.

    For example, if left, the ellipsis will appear towards the left side and we
    will display as much text starting from the right as possible. Similar to
    :attr:`shorten`, this option only applies when :attr:`text_size` [0] is
    not None, In this case, the string is shortened to fit within the specified
    width.

    .. versionadded:: 1.9.0

    :attr:`shorten_from` is a :class:`~kivy.properties.OptionProperty` and
    defaults to `center`.
    '''

    is_shortened = BooleanProperty(False)
    '''This property indicates if :attr:`text` was rendered with or without
    shortening when :attr:`shorten` is True.

    .. versionadded:: 1.10.0

    :attr:`is_shortened` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    split_str = StringProperty('')
    '''The string used to split the :attr:`text` while shortening the string
    when :attr:`shorten` is True.

    For example, if it's a space, the string will be broken into words and as
    many whole words that can fit into a single line will be displayed. If
    :attr:`split_str` is the empty string, `''`, we split on every character
    fitting as much text as possible into the line.

    .. versionadded:: 1.9.0

    :attr:`split_str` is a :class:`~kivy.properties.StringProperty` and
    defaults to `''` (the empty string).
    '''

    ellipsis_options = DictProperty({})
    '''Font options for the ellipsis string('...') used to split the text.

    Accepts a dict as option name with the value. Only applied when
    :attr:`markup` is true and text is shortened. All font options which work
    for :class:`Label` will work for :attr:`ellipsis_options`. Defaults for
    the options not specified are taken from the surronding text.

    .. code-block:: kv

        Label:
            text: 'Some very long line which will be cut'
            markup: True
            shorten: True
            ellipsis_options: {'color':(1,0.5,0.5,1),'underline':True}

    .. versionadded:: 2.0.0

    :attr:`ellipsis_options` is a :class:`~kivy.properties.DictProperty` and
    defaults to `{}` (the empty dict).
    '''

    unicode_errors = OptionProperty('replace',
                                    options=('strict', 'replace', 'ignore'))
    '''How to handle unicode decode errors. Can be `'strict'`, `'replace'` or
    `'ignore'`.

    .. versionadded:: 1.9.0

    :attr:`unicode_errors` is an :class:`~kivy.properties.OptionProperty` and
    defaults to `'replace'`.
    '''

    markup = BooleanProperty(False)
    '''
    .. versionadded:: 1.1.0

    If True, the text will be rendered using the
    :class:`~kivy.core.text.markup.MarkupLabel`: you can change the
    style of the text using tags. Check the
    :doc:`api-kivy.core.text.markup` documentation for more information.

    :attr:`markup` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    refs = DictProperty({})
    '''
    .. versionadded:: 1.1.0

    List of ``[ref=xxx]`` markup items in the text with the bounding box of
    all the words contained in a ref, available only after rendering.

    For example, if you wrote::

        Check out my [ref=hello]link[/ref]

    The refs will be set with::

        {'hello': ((64, 0, 78, 16), )}

    The references marked "hello" have a bounding box at (x1, y1, x2, y2).
    These co-ordinates are relative to the top left corner of the text, with
    the y value increasing downwards. You can define multiple refs with the
    same name: each occurrence will be added as another (x1, y1, x2, y2) tuple
    to this list.

    The current Label implementation uses these references if they exist in
    your markup text, automatically doing the collision with the touch and
    dispatching an `on_ref_press` event.

    You can bind a ref event like this::

        def print_it(instance, value):
            print('User click on', value)
        widget = Label(text='Hello [ref=world]World[/ref]', markup=True)
        widget.bind(on_ref_press=print_it)

    .. note::

        This works only with markup text. You need :attr:`markup` set to
        True.
    '''

    anchors = DictProperty({})
    '''
    .. versionadded:: 1.1.0

    Position of all the ``[anchor=xxx]`` markup in the text.
    These co-ordinates are relative to the top left corner of the text, with
    the y value increasing downwards. Anchors names should be unique and only
    the first occurrence of any duplicate anchors will be recorded.


    You can place anchors in your markup text as follows::

        text = """
            [anchor=title1][size=24]This is my Big title.[/size]
            [anchor=content]Hello world
        """

    Then, all the ``[anchor=]`` references will be removed and you'll get all
    the anchor positions in this property (only after rendering)::

        >>> widget = Label(text=text, markup=True)
        >>> widget.texture_update()
        >>> widget.anchors
        {"content": (20, 32), "title1": (20, 16)}

    .. note::

        This works only with markup text. You need :attr:`markup` set to
        True.

    '''

    max_lines = NumericProperty(0)
    '''Maximum number of lines to use, defaults to 0, which means unlimited.
    Please note that :attr:`shorten` take over this property. (with
    shorten, the text is always one line.)

    .. versionadded:: 1.8.0

    :attr:`max_lines` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.
    '''

    strip = BooleanProperty(False)
    '''Whether leading and trailing spaces and newlines should be stripped from
    each displayed line. If True, every line will start at the right or left
    edge, depending on :attr:`halign`. If :attr:`halign` is `justify` it is
    implicitly True.

    .. versionadded:: 1.9.0

    :attr:`strip` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    font_hinting = OptionProperty('normal',
                                  options=[None, 'normal', 'light', 'mono'],
                                  allownone=True)
    '''What hinting option to use for font rendering.
    Can be one of `'normal'`, `'light'`, `'mono'` or None.

    .. note::
        This feature requires SDL2 or Pango text provider.

    .. versionadded:: 1.10.0

    :attr:`font_hinting` is an :class:`~kivy.properties.OptionProperty` and
    defaults to `'normal'`.
    '''

    font_kerning = BooleanProperty(True)
    '''Whether kerning is enabled for font rendering. You should normally
    only disable this if rendering is broken with a particular font file.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`font_kerning` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to True.
    '''

    font_blended = BooleanProperty(True)
    '''Whether blended or solid font rendering should be used.
Exemplo n.º 18
0
class MDDatePicker(BaseDialogPicker):
    text_weekday_color = ColorProperty(None)
    """
    Text color of weekday names in (r, g, b, a) format.

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-date-picker-text-weekday-color.png
        :align: center

    :attr:`text_weekday_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    helper_text = StringProperty("Wrong date")
    """
    Helper text when entering an invalid date.

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-date-picker-helper-text.png
        :align: center

    :attr:`helper_text` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'Wrong date'`.
    """

    day = NumericProperty()
    """
    The day of the month to be opened by default. If not specified,
    the current number will be used.

    See `Open date dialog with the specified date <https://kivymd.readthedocs.io/en/latest/components/datepicker/#open-date-dialog-with-the-specified-date>`_ for more information.

    :attr:`day` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    month = NumericProperty()
    """
    The number of month to be opened by default. If not specified,
    the current number will be used.

    See `Open date dialog with the specified date <https://kivymd.readthedocs.io/en/latest/components/datepicker/#open-date-dialog-with-the-specified-date>`_ for more information.

    :attr:`month` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    year = NumericProperty()
    """
    The year of month to be opened by default. If not specified,
    the current number will be used.

    See `Open date dialog with the specified date <https://kivymd.readthedocs.io/en/latest/components/datepicker/#open-date-dialog-with-the-specified-date>`_ for more information.

    :attr:`year` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    min_year = NumericProperty(1914)
    """
    The year of month to be opened by default. If not specified,
    the current number will be used.

    :attr:`min_year` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `1914`.
    """

    max_year = NumericProperty(2121)
    """
    The year of month to be opened by default. If not specified,
    the current number will be used.

    :attr:`max_year` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `2121`.
    """

    mode = OptionProperty("picker", options=["picker", "range"])
    """
    Dialog type:`'picker'` type allows you to select one date;
                 `'range'` type allows to set a range of dates from which the
                 user can select a date.
    Available options are: [`'picker'`, `'range'`].

    :attr:`mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `picker`.
    """

    min_date = ObjectProperty()
    """
    The minimum value of the date range for the `'mode`' parameter.
    Must be an object <class 'datetime.date'>.

    See `Open date dialog with the specified date <https://kivymd.readthedocs.io/en/latest/components/datepicker/#interval-date>`_ for more information.

    :attr:`min_date` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to `None`.
    """

    max_date = ObjectProperty()
    """
    The minimum value of the date range for the `'mode`' parameter.
    Must be an object <class 'datetime.date'>.

    See `Open date dialog with the specified date <https://kivymd.readthedocs.io/en/latest/components/datepicker/#interval-date>`_ for more information.

    :attr:`max_date` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to `None`.
    """

    date_range_text_error = StringProperty("Error date range")
    """
    Error text that will be shown on the screen in the form of a toast if the
    minimum date range exceeds the maximum.

    :attr:`date_range_text_error` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'Error date range'`.
    """

    input_field_cls = ObjectProperty(DatePickerInputField)
    """
    A class that will implement date input in the format dd/mm/yyyy.
    See :class:`~DatePickerInputField` class for more information.

    .. code-block:: python

        class CustomInputField(MDTextField):
            owner = ObjectProperty()  # required attribute

            # Required method.
            def set_error(self):
                [...]

            # Required method.
            def get_list_date(self):
                [...]

            # Required method.
            def input_filter(self):
                [...]

        def show_date_picker(self):
            date_dialog = MDDatePicker(input_field_cls=CustomInputField)

    :attr:`input_field_cls` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to :class:`~DatePickerInputField`.
    """

    sel_year = NumericProperty()
    sel_month = NumericProperty()
    sel_day = NumericProperty()

    _calendar_layout = ObjectProperty()
    _calendar_list = None
    _enter_data_field = None
    _enter_data_field_two = None
    _enter_data_field_container = None
    _date_range = []
    _sel_day_widget = ObjectProperty()
    _scale_calendar_layout = NumericProperty(1)
    _scale_year_layout = NumericProperty(0)
    _shift_dialog_height = NumericProperty(0)
    _input_date_dialog_open = BooleanProperty(False)
    _select_year_dialog_open = False
    _start_range_date = 0
    _end_range_date = 0

    def __init__(
        self,
        year=None,
        month=None,
        day=None,
        firstweekday=0,
        **kwargs,
    ):
        self.today = date.today()
        self.calendar = calendar.Calendar(firstweekday)
        self.sel_year = year if year else self.today.year
        self.sel_month = month if month else self.today.month
        self.sel_day = day if day else self.today.day
        self.month = self.sel_month
        self.year = self.sel_year
        self.day = self.sel_day
        self._current_selected_date = (
            self.sel_day,
            self.sel_month,
            self.sel_year,
        )
        super().__init__(**kwargs)
        self.theme_cls.bind(device_orientation=self.on_device_orientation)

        if self.max_date and self.min_date:
            if self.min_date and not isinstance(self.min_date, date):
                raise DatePickerTypeDateError(
                    "'min_date' must be of class <class 'datetime.date'>")
            if self.max_date and not isinstance(self.max_date, date):
                raise DatePickerTypeDateError(
                    "'max_date' must be of class <class 'datetime.date'>")
            self.compare_date_range()
            self._date_range = self.get_date_range()

        self.generate_list_widgets_days()
        self.update_calendar(self.sel_year, self.sel_month)

        if (not self.max_date and not self.min_date and not self._date_range
                and self.mode != "range"):
            # Mark the current day.
            self.set_month_day(self.sel_day)
            self._sel_day_widget.dispatch("on_release")

    def on_device_orientation(self, instance_theme_manager: ThemeManager,
                              orientation_value: str) -> None:
        """Called when the device's screen orientation changes."""

        if self._input_date_dialog_open:
            if orientation_value == "portrait":
                self._shift_dialog_height = dp(250)
            if orientation_value == "landscape":
                self._shift_dialog_height = dp(138)

    def on_ok_button_pressed(self) -> None:
        """
        Called when the 'OK' button is pressed to confirm the date entered.
        """

        if self._enter_data_field and not self.is_date_valaid(
                self._enter_data_field.text):
            self._enter_data_field.set_error()
            return
        if self._enter_data_field_two and not self.is_date_valaid(
                self._enter_data_field_two.text):
            self._enter_data_field_two.set_error()
            return

        self.dispatch(
            "on_save",
            date(self.sel_year, self.sel_month, self.sel_day),
            self._date_range,
        )

    def is_date_valaid(self, date: str) -> bool:
        """Checks the valid of the currently entered date."""

        try:
            time.strptime(date, "%d/%m/%Y")
            return True
        except ValueError:
            return False

    def transformation_from_dialog_select_year(self) -> None:
        self.ids.chevron_left.disabled = False
        self.ids.chevron_right.disabled = False
        self.ids._year_layout.disabled = True
        self.ids.triangle.disabled = False
        self._select_year_dialog_open = False
        self.ids.triangle.icon = "menu-down"

        Animation(opacity=1, d=0.15).start(self.ids.chevron_left)
        Animation(opacity=1, d=0.15).start(self.ids.chevron_right)
        Animation(_scale_year_layout=0, d=0.15).start(self)
        Animation(_shift_dialog_height=dp(0), _scale_calendar_layout=1,
                  d=0.15).start(self)

        self._calendar_layout.clear_widgets()
        self.generate_list_widgets_days()
        self.update_calendar(self.year, self.month)

        if self.mode != "range":
            self.set_month_day(self.day)
            self._sel_day_widget.dispatch("on_release")

    def transformation_to_dialog_select_year(self) -> None:
        def disabled_chevron_buttons(*args):
            self.ids.chevron_left.disabled = True
            self.ids.chevron_right.disabled = True

        self._select_year_dialog_open = True
        self.ids._year_layout.disabled = False
        self._scale_calendar_layout = 0
        Animation(opacity=0, d=0.15).start(self.ids.chevron_left)
        Animation(opacity=0, d=0.15).start(self.ids.chevron_right)
        anim = Animation(_scale_year_layout=1, d=0.15)
        anim.bind(on_complete=disabled_chevron_buttons)
        anim.start(self)
        self.ids.triangle.icon = "menu-up"
        self.generate_list_widgets_years()
        self.set_position_to_current_year()

    def transformation_to_dialog_input_date(self) -> None:
        def set_date_to_input_field():
            if not self._enter_data_field_two:
                # Date of current day.
                self._enter_data_field.text = (
                    f"{'' if self.sel_day >= 10 else '0'}"
                    f"{self.sel_day}/"
                    f"{'' if self.sel_month >= 10 else '0'}"
                    f"{self.sel_month}/{self.sel_year}")
            else:
                # Range start date.
                self._enter_data_field.text = (
                    f"{'' if self.min_date.day >= 10 else '0'}"
                    f"{self.min_date.day}/"
                    f"{'' if self.min_date.month >= 10 else '0'}"
                    f"{self.min_date.month}/{self.min_date.year}")

        def set_date_to_input_field_two() -> None:
            # Range end date.
            self._enter_data_field_two.text = (
                f"{'' if self.max_date.day >= 10 else '0'}"
                f"{self.max_date.day}/"
                f"{'' if self.max_date.month >= 10 else '0'}"
                f"{self.max_date.month}/{self.max_date.year}")

        self.ids.triangle.disabled = True
        if self._select_year_dialog_open:
            self.transformation_from_dialog_select_year()
        self._input_date_dialog_open = True

        self._enter_data_field_container = DatePickerInputFieldContainer(
            owner=self)
        self._enter_data_field = self.get_field()
        if self.min_date and self.max_date:
            self._enter_data_field_two = self.get_field()
            set_date_to_input_field_two()
        set_date_to_input_field()
        self._enter_data_field_container.add_widget(self._enter_data_field)
        if self._enter_data_field_two:
            self._enter_data_field_container.add_widget(
                self._enter_data_field_two)

        self.ids.container.add_widget(self._enter_data_field_container)
        self.ids.edit_icon.icon = "calendar"
        self.ids.label_title.text = self.title_input

        Animation(
            _shift_dialog_height=dp(250)
            if self.theme_cls.device_orientation == "portrait" else dp(138),
            _scale_calendar_layout=0,
            d=0.15,
        ).start(self)
        Animation(
            opacity=0,
            d=0.15 if self.theme_cls.device_orientation == "portrait" else 0,
        ).start(self.ids.chevron_left)
        Animation(
            opacity=0,
            d=0.15 if self.theme_cls.device_orientation == "portrait" else 0,
        ).start(self.ids.chevron_right)
        Animation(opacity=0, d=0.15).start(self.ids.label_month_selector)
        Animation(opacity=0, d=0.15).start(self.ids.triangle)
        Animation(opacity=1, d=0.15).start(self._enter_data_field)
        if self._enter_data_field_two:
            Animation(opacity=1, d=0.15).start(self._enter_data_field_two)
        self.ids.label_full_date.text = self.set_text_full_date(
            self.sel_year,
            self.sel_month,
            self.sel_day,
            self.theme_cls.device_orientation,
        )

    def transformation_from_dialog_input_date(
            self, interval: Union[int, float]) -> None:
        self._input_date_dialog_open = False
        self.ids.label_full_date.text = self.set_text_full_date(
            self.sel_year,
            self.sel_month,
            self.sel_day,
            self.theme_cls.device_orientation,
        )
        self.ids.triangle.disabled = False
        self.ids.container.remove_widget(self._enter_data_field_container)
        Animation(_shift_dialog_height=dp(0), _scale_calendar_layout=1,
                  d=0.15).start(self)
        Animation(
            opacity=1,
            d=0.15
            if self.theme_cls.device_orientation == "portrait" else 0.65,
        ).start(self.ids.chevron_left)
        Animation(
            opacity=1,
            d=0.15
            if self.theme_cls.device_orientation == "portrait" else 0.65,
        ).start(self.ids.chevron_right)
        Animation(opacity=1, d=0.15).start(self.ids.label_month_selector)
        Animation(opacity=1, d=0.15).start(self.ids.triangle)
        Animation(opacity=0, d=0.15).start(self._enter_data_field)
        self.ids.edit_icon.icon = "pencil"
        self.ids.label_title.text = self.title

        if not self.min_date and not self.max_date:
            list_date = self._enter_data_field.get_list_date()
            if len(list_date) == 3 and len(list_date[2]) == 4:
                # self._sel_day_widget.is_selected = False
                self.update_calendar(int(list_date[2]), int(list_date[1]))
                self.set_month_day(int(list_date[0]))
                # self._sel_day_widget.dispatch("on_release")
                if self.mode != "range":
                    self._sel_day_widget.is_selected = False
                    self._sel_day_widget.dispatch("on_release")
        elif self.min_date and self.max_date:
            list_min_date = self._enter_data_field.get_list_date()
            list_max_date = self._enter_data_field_two.get_list_date()

            if len(list_min_date) == 3 and len(list_min_date[2]) == 4:
                self.min_date = date(
                    int(list_min_date[2]),
                    int(list_min_date[1]),
                    int(list_min_date[0]),
                )
            if len(list_max_date) == 3 and len(list_max_date[2]) == 4:
                self.max_date = date(
                    int(list_max_date[2]),
                    int(list_max_date[1]),
                    int(list_max_date[0]),
                )

            self.update_calendar_for_date_range()
            self.ids.label_full_date.text = self.set_text_full_date(
                int(list_max_date[2]),
                int(list_max_date[1]),
                int(list_max_date[0]),
                self.theme_cls.device_orientation,
            )

    def compare_date_range(self) -> None:
        # TODO: Add behavior if the minimum date range exceeds the maximum
        #  date range. Use toast?
        if self.max_date <= self.min_date:
            raise DatePickerTypeDateError(
                "`max_date` value cannot be less than or equal "
                "to 'min_date' value")

    def update_calendar_for_date_range(self) -> None:
        # self.compare_date_range()
        self._date_range = self.get_date_range()
        self._calendar_layout.clear_widgets()
        self.generate_list_widgets_days()
        self.update_calendar(self.year, self.month)

    def update_text_full_date(self, list_date) -> None:
        """
        Updates the title of the week, month and number day name
        in an open date input dialog.
        """

        if len(list_date) == 1 and len(list_date[0]) == 2:
            self.ids.label_full_date.text = self.set_text_full_date(
                self.sel_year,
                self.sel_month,
                list_date[0],
                self.theme_cls.device_orientation,
            )
        if len(list_date) == 2 and len(list_date[1]) == 2:
            self.ids.label_full_date.text = self.set_text_full_date(
                self.sel_year,
                int(list_date[1]),
                int(list_date[0]),
                self.theme_cls.device_orientation,
            )
        if len(list_date) == 3 and len(list_date[2]) == 4:
            self.ids.label_full_date.text = self.set_text_full_date(
                int(list_date[2]),
                int(list_date[1]),
                int(list_date[0]),
                self.theme_cls.device_orientation,
            )

    def update_calendar(self, year, month) -> None:
        try:
            dates = [x for x in self.calendar.itermonthdates(year, month)]
        except ValueError as e:
            if str(e) == "year is out of range":
                pass
        else:
            self.year = year
            self.month = month
            for idx in range(len(self._calendar_list)):
                self._calendar_list[idx].current_month = int(self.month)
                self._calendar_list[idx].current_year = int(self.year)

                # Dates of the month not in the range 1-31.
                if idx >= len(dates) or dates[idx].month != month:
                    # self._calendar_list[idx].disabled = True
                    self._calendar_list[idx].text = ""
                # Dates of the month in the range 1-31.
                else:
                    self._calendar_list[idx].disabled = False
                    self._calendar_list[idx].text = str(dates[idx].day)
                    self._calendar_list[idx].is_today = dates[
                        idx] == self.today
                # The marked date widget has a True value in the `is_selected`
                # attribute. In the KV file it is checked if the date widget
                # (DatePickerDaySelectableItem) has the `is_selected = False`
                # attribute value, then the date widget is not highlighted.
                if (
                        0 if not self._calendar_list[idx].text else int(
                            self._calendar_list[idx].text),
                        self._calendar_list[idx].current_month,
                        self._calendar_list[idx].current_year,
                ) == self._current_selected_date:
                    self._calendar_list[idx].is_selected = True
                else:
                    self._calendar_list[idx].is_selected = False
                # Dates outside the set range - disabled.
                if (self.mode == "picker" and self._date_range
                        and self._calendar_list[idx].text) or (
                            self.mode == "range" and self._start_range_date
                            and self._end_range_date
                            and self._calendar_list[idx].text):
                    if (date(
                            self._calendar_list[idx].current_year,
                            self._calendar_list[idx].current_month,
                            int(self._calendar_list[idx].text),
                    ) not in self._date_range):
                        self._calendar_list[idx].disabled = True

    def get_field(self) -> MDTextField:
        """Creates and returns a text field object used to enter dates."""

        if issubclass(self.input_field_cls, MDTextField):
            field = self.input_field_cls(
                owner=self,
                helper_text=self.helper_text,
                line_color_normal=self.theme_cls.divider_color,
            )
            field.color_mode = "custom"
            field.line_color_focus = (self.theme_cls.primary_color
                                      if not self.input_field_text_color else
                                      self.input_field_text_color)
            field.current_hint_text_color = field.line_color_focus
            field._current_hint_text_color = field.line_color_focus
            return field
        else:
            raise TypeError(
                "The `input_field_cls` parameter must be an object of the "
                "`kivymd.uix.textfield.MDTextField class`")

    def get_date_range(self) -> list:
        date_range = [
            self.min_date + datetime.timedelta(days=x)
            for x in range((self.max_date - self.min_date).days + 1)
        ]
        return date_range

    def set_text_full_date(self, year, month, day, orientation):
        """
        Returns a string of type "Tue, Feb 2" or "Tue,\nFeb 2" for a date
        choose and a string like "Feb 15 - Mar 23" or "Feb 15,\nMar 23" for
        a date range.
        """

        if 12 < int(month) < 0:
            raise ValueError("set_text_full_date:\n\t"
                             f"Month [{month}] out of range.")
        if int(day) > calendar.monthrange(int(year), (month))[1]:
            raise ValueError("set_text_full_date:\n\t"
                             f"Day [{day}] out of range for the month {month}")
        date = datetime.date(int(year), int(month), int(day))
        separator = ("\n" if (orientation == "landscape"
                              and not self._input_date_dialog_open) else " ")

        if self.mode == "picker":
            if not self.min_date and not self.max_date:
                return (date.strftime("%a,").capitalize() + separator +
                        date.strftime("%b ").capitalize() +
                        str(day).lstrip("0"))
            else:
                return (
                    self.min_date.strftime("%b ").capitalize() +
                    str(self.min_date.day).lstrip("0") +
                    (" - " if orientation == "portrait" else
                     (",\n" if not self._input_date_dialog_open else ", ")) +
                    self.max_date.strftime("%b ").capitalize() +
                    str(self.max_date.day).lstrip("0"))
        elif self.mode == "range":
            if self._start_range_date and self._end_range_date:
                if (orientation == "landscape"
                        and "-" in self.ids.label_full_date.text):
                    return (
                        self.ids.label_full_date.text.split("-")[0].strip() +
                        (",\n" if not self._input_date_dialog_open else " - ")
                        + date.strftime("%b ").capitalize() +
                        str(day).lstrip("0"))
                else:
                    if (orientation == "landscape"
                            and "," in self.ids.label_full_date.text):
                        return (self.ids.label_full_date.text.split(
                            ",")[0].strip() +
                                (",\n" if not self._input_date_dialog_open else
                                 "-") + date.strftime("%b ").capitalize() +
                                str(day).lstrip("0"))
                    if (orientation == "portrait"
                            and "," in self.ids.label_full_date.text):
                        return (self.ids.label_full_date.text.split(
                            ",")[0].strip() + "-" +
                                date.strftime("%b ").capitalize() +
                                str(day).lstrip("0"))
                    if (orientation == "portrait"
                            and "-" in self.ids.label_full_date.text):
                        return (self.ids.label_full_date.text.split(
                            "-")[0].strip() + " - " +
                                date.strftime("%b ").capitalize() +
                                str(day).lstrip("0"))
            elif self._start_range_date and not self._end_range_date:
                return ((date.strftime("%b ").capitalize() +
                         str(day).lstrip("0") +
                         " - End") if orientation != "landscape" else
                        (date.strftime("%b ").capitalize() +
                         str(day).lstrip("0") +
                         "{}End".format(",\n" if not self.
                                        _input_date_dialog_open else " - ")))
            elif not self._start_range_date and not self._end_range_date:
                return (
                    "Start - End"
                    if orientation != "landscape" else "Start{}End".format(
                        ",\n" if not self._input_date_dialog_open else " - "))

    def set_selected_widget(self, widget) -> None:
        if self._sel_day_widget:
            self._sel_day_widget.is_selected = False

        widget.is_selected = True
        self.sel_month = int(self.month)
        self.sel_year = int(self.year)
        self.sel_day = int(widget.text)
        self._current_selected_date = (
            self.sel_day,
            self.sel_month,
            self.sel_year,
        )
        self._sel_day_widget = widget

    def set_month_day(self, day) -> None:
        for idx in range(len(self._calendar_list)):
            if str(day) == str(self._calendar_list[idx].text):
                self._sel_day_widget = self._calendar_list[idx]
                self.sel_day = int(self._calendar_list[idx].text)
                if self._sel_day_widget:
                    self._sel_day_widget.is_selected = False
                self._sel_day_widget = self._calendar_list[idx]

    def set_position_to_current_year(self) -> None:
        # TODO: Add the feature to set the position of the list of years
        #  for the current year. This is not currently possible because the
        #  ``RecycleView`` class does not support this functionality.
        #  There is a solution to this problem
        #  - https://github.com/Bakterija/log_fruit/blob/dev/src/app_modules/widgets/app_recycleview/recycleview.py.
        #  But I have not been able to get it to work.
        pass

    def generate_list_widgets_years(self) -> None:
        for i, number_year in enumerate(range(self.min_year, self.max_year)):
            self.ids._year_layout.data.append({
                "owner":
                self,
                "text":
                str(number_year),
                "index":
                i,
                "selectable":
                True,
                "viewclass":
                "DatePickerYearSelectableItem",
            })

    def generate_list_widgets_days(self) -> None:
        calendar_list = []

        for day in self.calendar.iterweekdays():
            weekday_label = DatePickerWeekdayLabel(
                text=calendar.day_name[day][0].upper(),
                owner=self,
                hint_text=calendar.day_name[day],
            )
            weekday_label.font_name = self.font_name
            self._calendar_layout.add_widget(weekday_label)
        for i, j in enumerate(range(6 * 7)):  # 6 weeks, 7 days a week
            day_selectable_item = DatePickerDaySelectableItem(
                index=i,
                owner=self,
                current_month=int(self.month),
                current_year=int(self.year),
            )
            calendar_list.append(day_selectable_item)
            self._calendar_layout.add_widget(day_selectable_item)
        self._calendar_list = calendar_list

    def change_month(self, operation: str) -> None:
        """
        Called when "chevron-left" and "chevron-right" buttons are pressed.
        Switches the calendar to the previous/next month.
        """

        operation = 1 if operation == "next" else -1
        month = (12 if self.month + operation == 0 else 1 if self.month +
                 operation == 13 else self.month + operation)
        year = (self.year - 1 if self.month + operation == 0 else self.year +
                1 if self.month + operation == 13 else self.year)
        self.update_calendar(year, month)
        if self.sel_day:
            x = calendar.monthrange(year, month)[1]
            if x < self.sel_day:
                self.sel_day = (x if year <= self.sel_year
                                and month <= self.sel_year else 1)
Exemplo n.º 19
0
class FloatButton(AnchorLayout):
    callback = ObjectProperty()
    md_bg_color = ColorProperty([1, 1, 1, 1])
    icon = StringProperty()
Exemplo n.º 20
0
class MDToolbar(NotchedBox):
    """
    :Events:
        `on_action_button`
            Method for the button used for the :class:`~MDBottomAppBar` class.
    """

    left_action_items = ListProperty()
    """
    The icons on the left of the toolbar.
    To add one, append a list like the following:

    .. code-block:: kv

        left_action_items: [`'icon_name'`, callback, tooltip text]

    where `'icon_name'` is a string that corresponds to an icon definition,
    ``callback`` is the function called on a touch release event and
    ``tooltip text` is the text to be displayed in the tooltip. Both the
    ``callback`` and ``tooltip text`` are optional but the order must be
    preserved.

    :attr:`left_action_items` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    right_action_items = ListProperty()
    """
    The icons on the left of the toolbar.
    Works the same way as :attr:`left_action_items`.

    :attr:`right_action_items` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    title = StringProperty()
    """
    Text toolbar.

    :attr:`title` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    anchor_title = OptionProperty("left", options=["left", "center", "right"])
    """
    Position toolbar title.
    Available options are: `'left'`, `'center'`, `'right'`.

    :attr:`anchor_title` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'left'`.
    """

    mode = OptionProperty(
        "center", options=["free-end", "free-center", "end", "center"]
    )
    """
    Floating button position. Only for :class:`~MDBottomAppBar` class.
    Available options are: `'free-end'`, `'free-center'`, `'end'`, `'center'`.

    :attr:`mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'center'`.
    """

    round = NumericProperty("10dp")
    """
    Rounding the corners at the notch for a button.
    Onle for :class:`~MDBottomAppBar` class.

    :attr:`round` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `'10dp'`.
    """

    icon = StringProperty("android")
    """
    Floating button. Onle for :class:`~MDBottomAppBar` class.

    :attr:`icon` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'android'`.
    """

    icon_color = ColorProperty()
    """
    Color action button. Onle for :class:`~MDBottomAppBar` class.

    :attr:`icon_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `[]`.
    """

    type = OptionProperty("top", options=["top", "bottom"])
    """
    When using the :class:`~MDBottomAppBar` class, the parameter ``type``
    must be set to `'bottom'`:

    .. code-block:: kv

        MDBottomAppBar:

            MDToolbar:
                type: "bottom"

    Available options are: `'top'`, `'bottom'`.

    :attr:`type` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'top'`.
    """

    opposite_colors = BooleanProperty(False)

    _shift = NumericProperty("3.5dp")

    def __init__(self, **kwargs):
        self.action_button = MDActionBottomAppBarButton()
        super().__init__(**kwargs)
        self.register_event_type("on_action_button")
        if not self.icon_color:
            self.icon_color = self.theme_cls.primary_color
        Window.bind(on_resize=self._on_resize)
        self.bind(specific_text_color=self.update_action_bar_text_colors)
        # self.bind(opposite_colors=self.update_opposite_colors)
        self.theme_cls.bind(primary_palette=self.update_md_bg_color)
        Clock.schedule_once(
            lambda x: self.on_left_action_items(0, self.left_action_items)
        )
        Clock.schedule_once(
            lambda x: self.on_right_action_items(0, self.right_action_items)
        )
        Clock.schedule_once(lambda x: self.set_md_bg_color(0, self.md_bg_color))

    def on_type(self, instance, value):
        if value == "bottom":
            self.action_button.bind(center_x=self.setter("notch_center_x"))
            self.action_button.bind(
                on_release=lambda x: self.dispatch("on_action_button")
            )
            self.action_button.x = (
                Window.width / 2 - self.action_button.width / 2
            )
            self.action_button.y = (
                (self.center[1] - self.height / 2)
                + self.theme_cls.standard_increment / 2
                + self._shift
            )
            self.on_mode(None, self.mode)

    def on_action_button(self, *args):
        pass

    def on_md_bg_color(self, instance, value):
        if self.type == "bottom":
            self.md_bg_color = [0, 0, 0, 0]

    def on_left_action_items(self, instance, value):
        self.update_action_bar(self.ids["left_actions"], value)

    def on_right_action_items(self, instance, value):
        self.update_action_bar(self.ids["right_actions"], value)

    def set_md_bg_color(self, instance, value):
        if value == [1.0, 1.0, 1.0, 0.0]:
            self.md_bg_color = self.theme_cls.primary_color

    def update_action_bar(self, action_bar, action_bar_items):
        action_bar.clear_widgets()
        new_width = 0
        for item in action_bar_items:
            new_width += dp(48)
            if len(item) == 1:
                item.append(lambda x: None)
            if len(item) > 1 and not item[1]:
                item[1] = lambda x: None
            if len(item) == 2:
                if type(item[1]) is str:
                    item.insert(1, lambda x: None)
                else:
                    item.append("")
            action_bar.add_widget(
                MDActionTopAppBarButton(
                    icon=item[0],
                    on_release=item[1],
                    tooltip_text=item[2],
                    theme_text_color="Custom"
                    if not self.opposite_colors
                    else "Primary",
                    text_color=self.specific_text_color,
                    opposite_colors=self.opposite_colors,
                )
            )
        action_bar.width = new_width

    def update_md_bg_color(self, *args):
        self.md_bg_color = self.theme_cls._get_primary_color()

    def update_opposite_colors(self, instance, value):
        if value:
            self.ids.label_title.theme_text_color = ""

    def update_action_bar_text_colors(self, *args):
        for child in self.ids["left_actions"].children:
            child.text_color = self.specific_text_color
        for child in self.ids["right_actions"].children:
            child.text_color = self.specific_text_color

    def on_icon(self, instance, value):
        self.action_button.icon = value

    def on_icon_color(self, instance, value):
        self.action_button.md_bg_color = value

    def on_mode(self, instance, value):
        if self.type == "top":
            return

        def set_button_pos(*args):
            self.action_button.x = x
            self.action_button.y = y - self._rounded_rectangle_height / 2
            self.action_button._hard_shadow_size = (0, 0)
            self.action_button._soft_shadow_size = (0, 0)
            anim = Animation(_scale_x=1, _scale_y=1, d=0.05)
            anim.bind(on_complete=self.set_shadow)
            anim.start(self.action_button)

        if value == "center":
            self.set_notch()
            x = Window.width / 2 - self.action_button.width / 2
            y = (
                (self.center[1] - self.height / 2)
                + self.theme_cls.standard_increment / 2
                + self._shift
            )
        elif value == "end":

            self.set_notch()
            x = Window.width - self.action_button.width * 2
            y = (
                (self.center[1] - self.height / 2)
                + self.theme_cls.standard_increment / 2
                + self._shift
            )
            self.right_action_items = []
        elif value == "free-end":
            self.remove_notch()
            x = Window.width - self.action_button.width - dp(10)
            y = self.action_button.height + self.action_button.height / 2
        elif value == "free-center":
            self.remove_notch()
            x = Window.width / 2 - self.action_button.width / 2
            y = self.action_button.height + self.action_button.height / 2
        self.remove_shadow()
        anim = Animation(_scale_x=0, _scale_y=0, d=0.1)
        anim.bind(on_complete=set_button_pos)
        anim.start(self.action_button)

    def remove_notch(self):
        anim = Animation(d=0.1) + Animation(
            notch_radius=0,
            d=0.1,
        )
        anim.start(self)

    def set_notch(self):
        anim = Animation(d=0.1) + Animation(
            notch_radius=self.action_button.width / 2 + dp(8),
            d=0.1,
        )
        anim.start(self)

    def remove_shadow(self):
        self.action_button._elevation = 0

    def set_shadow(self, *args):
        self.action_button._elevation = self.action_button.elevation

    def _on_resize(self, instance, width, height):
        if self.mode == "center":
            self.action_button.x = width / 2 - self.action_button.width / 2
        else:
            self.action_button.x = width - self.action_button.width * 2

    def _update_specific_text_color(self, instance, value):
        if self.specific_text_color in (
            [0.0, 0.0, 0.0, 0.87],
            [0.0, 0.0, 0.0, 1.0],
            [1.0, 1.0, 1.0, 1.0],
        ):
            self.specific_text_color = text_colors[
                self.theme_cls.primary_palette
            ][self.theme_cls.primary_hue]
Exemplo n.º 21
0
import re
import time

from kivy.app import App
from kivy.clock import Clock
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.properties import ColorProperty
from kivy.utils import get_random_color

Window.clearcolor = get_random_color()
from Client.client import Client

FG_Color = ColorProperty(list(map(
    lambda x: min(x + 0.5, 1) if max(Window.clearcolor[:-1]) < 0.5 else max(x - 0.5, 0) if max(
        Window.clearcolor[:-1]) > 0.5 else x, Window.clearcolor[:-1])) + [1.0])
BG_Color = ColorProperty(Window.clearcolor)

network = Client()


class MainScreen(Screen):
    fgcolor = FG_Color
    bgcolor = BG_Color

    def refresh(self, dt):
        try:
            if self.parent.current == "MainScreen":
                network.messages = network.get('messages', {"token": network.token})['data']['messages']
                children = self.children[0].children[::-1]
Exemplo n.º 22
0
class MDTabs(ThemableBehavior, SpecificBackgroundColorBehavior, AnchorLayout):
    """
    You can use this class to create your own tabbed panel..

    :Events:
        `on_tab_switch`
            Called when switching tabs.
        `on_slide_progress`
            Called while the slide is scrolling.
        `on_ref_press`
            The method will be called when the ``on_ref_press`` event
            occurs when you, for example, use markup text for tabs.
    """

    default_tab = NumericProperty(0)
    """
    Index of the default tab.

    :attr:`default_tab` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    tab_bar_height = NumericProperty("48dp")
    """
    Height of the tab bar.

    :attr:`tab_bar_height` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `'48dp'`.
    """

    tab_indicator_anim = BooleanProperty(False)
    """
    Tab indicator animation. If you want use animation set it to ``True``.

    :attr:`tab_indicator_anim` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    tab_indicator_height = NumericProperty("2dp")
    """
    Height of the tab indicator.

    :attr:`tab_indicator_height` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `'2dp'`.
    """

    tab_indicator_type = OptionProperty(
        "line", options=["line", "fill", "round", "line-round", "line-rect"]
    )
    """
    Type of tab indicator. Available options are: `'line'`, `'fill'`,
    `'round'`, `'line-rect'` and `'line-round'`.

    :attr:`tab_indicator_type` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'line'`.
    """

    anim_duration = NumericProperty(0.2)
    """
    Duration of the slide animation.

    :attr:`anim_duration` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0.2`.
    """

    anim_threshold = BoundedNumericProperty(
        0.8, min=0.0, max=1.0, errorhandler=lambda x: 0.0 if x < 0.0 else 1.0
    )
    """
    Animation threshold allow you to change the tab indicator animation effect.

    :attr:`anim_threshold` is an :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to `0.8`.
    """

    allow_stretch = BooleanProperty(True)
    """
    If False - tabs will not stretch to full screen.

    :attr:`allow_stretch` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    background_color = ColorProperty(None)
    """
    Background color of tabs in ``rgba`` format.

    :attr:`background_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    text_color_normal = ColorProperty((1, 1, 1, 1))
    """
    Text color of the label when it is not selected.

    :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `(1, 1, 1, 1)`.
    """

    text_color_active = ColorProperty((1, 1, 1, 1))
    """
    Text color of the label when it is selected.

    :attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `(1, 1, 1, 1)`.
    """

    elevation = NumericProperty(0)
    """
    Tab value elevation.

    .. seealso::

        `Behaviors/Elevation <https://kivymd.readthedocs.io/en/latest/behaviors/elevation/index.html>`_

    :attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    indicator_color = ColorProperty(None)
    """
    Color indicator in ``rgba`` format.

    :attr:`indicator_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    lock_swiping = BooleanProperty(False)
    """
    If True - disable switching tabs by swipe.

    :attr:`lock_swiping` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    font_name = StringProperty("Roboto")
    """
    Font name for tab text.

    :attr:`font_name` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'Roboto'`.
    """

    ripple_duration = NumericProperty(2)
    """
    Ripple duration when long touching to tab.

    :attr:`ripple_duration` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `2`.
    """

    no_ripple_effect = BooleanProperty(True)
    """
    Whether to use the ripple effect when tapping on a tab.

    :attr:`no_ripple_effect` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.register_event_type("on_tab_switch")
        self.register_event_type("on_ref_press")
        self.register_event_type("on_slide_progress")
        Clock.schedule_once(self._carousel_bind, 1)

    def switch_tab(self, name_tab):
        """Switching the tab by name."""

        for instance_tab in self.tab_bar.parent.carousel.slides:
            if instance_tab.text == name_tab:
                self.tab_bar.parent.carousel.load_slide(instance_tab)
                break

    def get_tab_list(self):
        """Returns a list of tab objects."""

        return self.tab_bar.layout.children

    def add_widget(self, widget, index=0, canvas=None):
        # You can add only subclass of MDTabsBase.
        if len(self.children) >= 2:
            try:
                # FIXME: Can't set the value of the `no_ripple_effect`
                #  and `ripple_duration` properties for widget.tab_label.
                widget.tab_label._no_ripple_effect = self.no_ripple_effect
                widget.tab_label.ripple_duration_in_slow = self.ripple_duration

                widget.tab_label.group = str(self)
                widget.tab_label.tab_bar = self.tab_bar
                self.bind(
                    text_color_normal=widget.tab_label.setter(
                        "text_color_normal"
                    )
                )
                self.bind(
                    text_color_active=widget.tab_label.setter(
                        "text_color_active"
                    )
                )
                self.bind(font_name=widget.tab_label.setter("font_name"))
                self.tab_bar.layout.add_widget(widget.tab_label)
                self.carousel.add_widget(widget)
                return
            except AttributeError:
                pass
        return super().add_widget(widget)

    def remove_widget(self, widget):
        # You can remove only subclass of MDTabsLabel.
        if not issubclass(widget.__class__, MDTabsLabel):
            raise MDTabsException(
                "MDTabs can remove only subclass of MDTabsLabel"
            )
        # The last tab is not deleted.
        if len(self.tab_bar.layout.children) == 1:
            return

        self.tab_bar.layout.remove_widget(widget)

        for tab in self.carousel.slides:
            if tab.text == widget.text:
                self.carousel.remove_widget(tab)
                break

    def on_slide_progress(self, *args):
        """Called while the slide is scrolling."""

    def on_carousel_index(self, carousel, index):
        """Called when the carousel index changes."""

        # When the index of the carousel change, update tab indicator,
        # select the current tab and reset threshold data.
        if carousel.current_slide:
            current_tab_label = carousel.current_slide.tab_label
            if current_tab_label.state == "normal":
                # current_tab_label._do_press()
                current_tab_label.dispatch("on_release")

            if self.tab_indicator_type == "round":
                self.tab_indicator_height = self.tab_bar_height
                if index == 0:
                    radius = [
                        0,
                        self.tab_bar_height / 2,
                        self.tab_bar_height / 2,
                        0,
                    ]
                    self.tab_bar.update_indicator(
                        current_tab_label.x, current_tab_label.width, radius
                    )
                elif index == len(self.get_tab_list()) - 1:
                    radius = [
                        self.tab_bar_height / 2,
                        0,
                        0,
                        self.tab_bar_height / 2,
                    ]
                    self.tab_bar.update_indicator(
                        current_tab_label.x, current_tab_label.width, radius
                    )
                else:
                    radius = [
                        self.tab_bar_height / 2,
                    ]
                    self.tab_bar.update_indicator(
                        current_tab_label.x, current_tab_label.width, radius
                    )
            elif (
                self.tab_indicator_type == "fill"
                or self.tab_indicator_type == "line-round"
                or self.tab_indicator_type == "line-rect"
            ):
                self.tab_indicator_height = self.tab_bar_height
                self.tab_bar.update_indicator(
                    current_tab_label.x, current_tab_label.width
                )
            else:
                self.tab_bar.update_indicator(
                    current_tab_label.x, current_tab_label.width
                )

    def on_ref_press(self, *args):
        """The method will be called when the ``on_ref_press`` event
        occurs when you, for example, use markup text for tabs."""

    def on_tab_switch(self, *args):
        """Called when switching tabs."""

    def on_size(self, *args):
        if self.carousel.current_slide:
            self._update_indicator(self.carousel.current_slide.tab_label)

    def _carousel_bind(self, i):
        self.carousel.bind(on_slide_progress=self._on_slide_progress)

    def _on_slide_progress(self, *args):
        self.dispatch("on_slide_progress", args)

    def _update_indicator(self, current_tab_label):
        if not current_tab_label:
            current_tab_label = self.tab_bar.layout.children[-1]
        self.tab_bar.update_indicator(
            current_tab_label.x, current_tab_label.width
        )
Exemplo n.º 23
0
class Button(ButtonBehavior, Label):
    '''Button class, see module documentation for more information.

    .. versionchanged:: 1.8.0
        The behavior / logic of the button has been moved to
        :class:`~kivy.uix.behaviors.ButtonBehaviors`.

    '''

    background_color = ColorProperty([1, 1, 1, 1])
    '''Background color, in the format (r, g, b, a).

    This acts as a *multiplier* to the texture colour. The default
    texture is grey, so just setting the background color will give
    a darker result. To set a plain color, set the
    :attr:`background_normal` to ``''``.

    .. versionadded:: 1.0.8

    The :attr:`background_color` is a
    :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, 1].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    '''

    background_normal = StringProperty(
        'atlas://data/images/defaulttheme/button')
    '''Background image of the button used for the default graphical
    representation when the button is not pressed.

    .. versionadded:: 1.0.4

    :attr:`background_normal` is a :class:`~kivy.properties.StringProperty`
    and defaults to 'atlas://data/images/defaulttheme/button'.
    '''

    background_down = StringProperty(
        'atlas://data/images/defaulttheme/button_pressed')
    '''Background image of the button used for the default graphical
    representation when the button is pressed.

    .. versionadded:: 1.0.4

    :attr:`background_down` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/button_pressed'.
    '''

    background_disabled_normal = StringProperty(
        'atlas://data/images/defaulttheme/button_disabled')
    '''Background image of the button used for the default graphical
    representation when the button is disabled and not pressed.

    .. versionadded:: 1.8.0

    :attr:`background_disabled_normal` is a
    :class:`~kivy.properties.StringProperty` and defaults to
    'atlas://data/images/defaulttheme/button_disabled'.
    '''

    background_disabled_down = StringProperty(
        'atlas://data/images/defaulttheme/button_disabled_pressed')
    '''Background image of the button used for the default graphical
    representation when the button is disabled and pressed.

    .. versionadded:: 1.8.0

    :attr:`background_disabled_down` is a
    :class:`~kivy.properties.StringProperty` and defaults to
    'atlas://data/images/defaulttheme/button_disabled_pressed'.
    '''

    border = ListProperty([16, 16, 16, 16])
    '''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
Exemplo n.º 24
0
class TreeViewNode(object):
    '''TreeViewNode class, used to build a node class for a TreeView object.
    '''
    def __init__(self, **kwargs):
        if self.__class__ is TreeViewNode:
            raise TreeViewException('You cannot use directly TreeViewNode.')
        super(TreeViewNode, self).__init__(**kwargs)

    is_leaf = BooleanProperty(True)
    '''Boolean to indicate whether this node is a leaf or not. Used to adjust
    the graphical representation.

    :attr:`is_leaf` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to True. It is automatically set to False when child is added.
    '''

    is_open = BooleanProperty(False)
    '''Boolean to indicate whether this node is opened or not, in case there
    are child nodes. This is used to adjust the graphical representation.

    .. warning::

        This property is automatically set by the :class:`TreeView`. You can
        read but not write it.

    :attr:`is_open` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    is_loaded = BooleanProperty(False)
    '''Boolean to indicate whether this node is already loaded or not. This
    property is used only if the :class:`TreeView` uses asynchronous loading.

    :attr:`is_loaded` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    is_selected = BooleanProperty(False)
    '''Boolean to indicate whether this node is selected or not. This is used
    adjust the graphical representation.

    .. warning::

        This property is automatically set by the :class:`TreeView`. You can
        read but not write it.

    :attr:`is_selected` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    no_selection = BooleanProperty(False)
    '''Boolean used to indicate whether selection of the node is allowed or
     not.

    :attr:`no_selection` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    nodes = ListProperty([])
    '''List of nodes. The nodes list is different than the children list. A
    node in the nodes list represents a node on the tree. An item in the
    children list represents the widget associated with the node.

    .. warning::

        This property is automatically set by the :class:`TreeView`. You can
        read but not write it.

    :attr:`nodes` is a :class:`~kivy.properties.ListProperty` and defaults to
    [].
    '''

    parent_node = ObjectProperty(None, allownone=True)
    '''Parent node. This attribute is needed because the :attr:`parent` can be
    None when the node is not displayed.

    .. versionadded:: 1.0.7

    :attr:`parent_node` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None.
    '''

    level = NumericProperty(-1)
    '''Level of the node.

    :attr:`level` is a :class:`~kivy.properties.NumericProperty` and defaults
    to -1.
    '''

    color_selected = ColorProperty([.3, .3, .3, 1.])
    '''Background color of the node when the node is selected.

    :attr:`color_selected` is a :class:`~kivy.properties.ColorProperty` and
    defaults to [.1, .1, .1, 1].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    '''

    odd = BooleanProperty(False)
    '''
    This property is set by the TreeView widget automatically and is read-only.

    :attr:`odd` is a :class:`~kivy.properties.BooleanProperty` and defaults to
    False.
    '''

    odd_color = ColorProperty([1., 1., 1., .0])
    '''Background color of odd nodes when the node is not selected.

    :attr:`odd_color` is a :class:`~kivy.properties.ColorProperty` and defaults
    to [1., 1., 1., 0.].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    '''

    even_color = ColorProperty([0.5, 0.5, 0.5, 0.1])
    '''Background color of even nodes when the node is not selected.
Exemplo n.º 25
0
class MDChip(ThemableBehavior, ButtonBehavior, BoxLayout):
    text = StringProperty()
    """
    Chip text.

    :attr:`text` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    icon = StringProperty("checkbox-blank-circle")
    """
    Chip icon.

    :attr:`icon` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'checkbox-blank-circle'`.
    """

    color = ColorProperty(None)
    """
    Chip color in ``rgba`` format.

    :attr:`color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    text_color = ColorProperty(None)
    """
    Chip's text color in ``rgba`` format.

    :attr:`text_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    icon_color = ColorProperty(None)
    """
    Chip's icon color in ``rgba`` format.

    :attr:`icon_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    check = BooleanProperty(False)
    """
    If True, a checkmark is added to the left when touch to the chip.

    :attr:`check` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    radius = ListProperty([
        dp(12),
    ])
    """
    Corner radius values.

    :attr:`radius` is an :class:`~kivy.properties.ListProperty`
    and defaults to `'[dp(12),]'`.
    """

    selected_chip_color = ColorProperty(None)
    """
    The color of the chip that is currently selected in ``rgba`` format.

    :attr:`selected_chip_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    _color = ColorProperty(None)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Clock.schedule_once(self.set_color)

    def set_color(self, interval):
        if not self.color:
            self.color = self.theme_cls.primary_color
        else:
            self._color = self.color

    def on_icon(self, instance, value):
        def remove_icon(interval):
            self.remove_widget(self.ids.icon)

        if value == "":
            self.icon = "checkbox-blank-circle"
            Clock.schedule_once(remove_icon)

    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            self.dispatch("on_press")
            self.dispatch("on_release")
            md_choose_chip = self.parent

            if self.selected_chip_color:
                Animation(
                    color=self.theme_cls.primary_dark if
                    not self.selected_chip_color else self.selected_chip_color,
                    d=0.3,
                ).start(self)

            if issubclass(md_choose_chip.__class__, MDChooseChip):
                for chip in md_choose_chip.children:
                    if chip is not self:
                        chip.color = (self.theme_cls.primary_color
                                      if not chip._color else chip._color)
                    else:
                        chip.color = self.theme_cls.primary_color

            if self.check:
                if not len(self.ids.box_check.children):
                    self.ids.box_check.add_widget(
                        MDIcon(
                            icon="check",
                            size_hint=(None, None),
                            size=("26dp", "26dp"),
                            font_size=sp(20),
                        ))
                else:
                    check = self.ids.box_check.children[0]
                    self.ids.box_check.remove_widget(check)
Exemplo n.º 26
0
class ColourProperties(MasterColour):  # , MasterTextColour, MasterTrimColour):
    background_colour = ColorProperty()
    foreground_colour = ColorProperty()
    trim_colour = ColorProperty()
    text_colour = ColorProperty()
Exemplo n.º 27
0
class HomeMenuCard(MDCard):
    icon = StringProperty()
    bg_color = ColorProperty()
    icon_color = ColorProperty(None, allownone=True)
    text = StringProperty()
Exemplo n.º 28
0
class MDTextField(ThemableBehavior, TextInput):
    helper_text = StringProperty("This field is required")
    """
    Text for ``helper_text`` mode.

    :attr:`helper_text` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'This field is required'`.
    """

    helper_text_mode = OptionProperty(
        "none", options=["none", "on_error", "persistent", "on_focus"])
    """
    Helper text mode. Available options are: `'on_error'`, `'persistent'`,
    `'on_focus'`.

    :attr:`helper_text_mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'none'`.
    """

    max_text_length = NumericProperty(None)
    """
    Maximum allowed value of characters in a text field.

    :attr:`max_text_length` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `None`.
    """

    required = BooleanProperty(False)
    """
    Required text. If True then the text field requires text.

    :attr:`required` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    color_mode = OptionProperty("primary",
                                options=["primary", "accent", "custom"])
    """
    Color text mode. Available options are: `'primary'`, `'accent'`,
    `'custom'`.

    :attr:`color_mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'primary'`.
    """

    mode = OptionProperty("line", options=["rectangle", "fill"])
    """
    Text field mode. Available options are: `'line'`, `'rectangle'`, `'fill'`.

    :attr:`mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'line'`.
    """

    line_color_normal = ColorProperty(None)
    """
    Line color normal in ``rgba`` format.

    :attr:`line_color_normal` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    line_color_focus = ColorProperty(None)
    """
    Line color focus in ``rgba`` format.

    :attr:`line_color_focus` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    line_anim = BooleanProperty(True)
    """
    If True, then text field shows animated line when on focus.

    :attr:`line_anim` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    error_color = ColorProperty(None)
    """
    Error color in ``rgba`` format for ``required = True``.

    :attr:`error_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    fill_color = ColorProperty((0, 0, 0, 0))
    """
    The background color of the fill in rgba format when the ``mode`` parameter
    is "fill".

    :attr:`fill_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `(0, 0, 0, 0)`.
    """

    active_line = BooleanProperty(True)
    """
    Show active line or not.

    :attr:`active_line` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    error = BooleanProperty(False)
    """
    If True, then the text field goes into ``error`` mode.

    :attr:`error` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    current_hint_text_color = ColorProperty(None)
    """
    ``hint_text`` text color.

    :attr:`current_hint_text_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    icon_right = StringProperty()
    """
    Right icon.

    :attr:`icon_right` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    icon_right_color = ColorProperty((0, 0, 0, 1))
    """
    Color of right icon in ``rgba`` format.

    :attr:`icon_right_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `(0, 0, 0, 1)`.
    """

    text_color = ColorProperty(None)
    """
    Text color in ``rgba`` format.

    :attr:`text_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    font_size = NumericProperty("16sp")
    """
    Font size of the text in pixels.

    :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and
    defaults to `'16sp'`.
    """

    # TODO: Add minimum allowed height. Otherwise, if the value is,
    #  for example, 20, the text field will simply be lessened.
    max_height = NumericProperty(0)
    """
    Maximum height of the text box when `multiline = True`.

    :attr:`max_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to `0`.
    """

    _text_len_error = BooleanProperty(False)
    _hint_lbl_font_size = NumericProperty("16sp")
    _line_blank_space_right_point = NumericProperty(0)
    _line_blank_space_left_point = NumericProperty(0)
    _hint_y = NumericProperty("38dp")
    _line_width = NumericProperty(0)
    _current_line_color = ColorProperty((0, 0, 0, 0))
    _current_error_color = ColorProperty((0, 0, 0, 0))
    _current_hint_text_color = ColorProperty((0, 0, 0, 0))
    _current_right_lbl_color = ColorProperty((0, 0, 0, 0))
    _msg_lbl = None
    _right_msg_lbl = None
    _hint_lbl = None
    _lbl_icon_right = None

    def __init__(self, **kwargs):
        self.set_objects_labels()
        super().__init__(**kwargs)
        # Sets default colors.
        self.line_color_normal = self.theme_cls.divider_color
        self.line_color_focus = self.theme_cls.primary_color
        self.error_color = self.theme_cls.error_color
        self._current_hint_text_color = self.theme_cls.disabled_hint_text_color
        self._current_line_color = self.theme_cls.primary_color

        self.bind(
            helper_text=self._set_msg,
            hint_text=self.on_hint_text,
            _hint_lbl_font_size=self._hint_lbl.setter("font_size"),
            helper_text_mode=self._set_message_mode,
            max_text_length=self._set_max_text_length,
            text=self.on_text,
        )
        self.theme_cls.bind(
            primary_color=self._update_primary_color,
            theme_style=self._update_theme_style,
            accent_color=self._update_accent_color,
        )
        self.has_had_text = False
        self._better_texture_size = None

    def set_objects_labels(self):
        """Creates labels objects for the parameters
        `helper_text`,`hint_text`, etc."""

        # Label object for `helper_text` parameter.
        self._msg_lbl = TextfieldLabel(
            font_style="Caption",
            halign="left",
            valign="middle",
            text=self.helper_text,
            field=self,
        )
        # Label object for `max_text_length` parameter.
        self._right_msg_lbl = TextfieldLabel(
            font_style="Caption",
            halign="right",
            valign="middle",
            text="",
            field=self,
        )
        # Label object for `hint_text` parameter.
        self._hint_lbl = TextfieldLabel(font_style="Subtitle1",
                                        halign="left",
                                        valign="middle",
                                        field=self)
        # MDIcon object for the icon on the right.
        self._lbl_icon_right = MDIcon(theme_text_color="Custom")

    def on_icon_right(self, instance, value):
        self._lbl_icon_right.icon = value

    def on_icon_right_color(self, instance, value):
        self._lbl_icon_right.text_color = value

    def on_width(self, instance, width):
        """Called when the application window is resized."""

        if (any((self.focus, self.error, self._text_len_error))
                and instance is not None):
            # Bottom line width when active focus.
            self._line_width = width
        self._msg_lbl.width = self.width
        self._right_msg_lbl.width = self.width

    def on_focus(self, *args):
        disabled_hint_text_color = self.theme_cls.disabled_hint_text_color
        Animation.cancel_all(self, "_line_width", "_hint_y",
                             "_hint_lbl_font_size")
        self._set_text_len_error()

        if self.focus:
            _fill_color = self.fill_color
            _fill_color[3] = self.fill_color[3] - 0.1
            if not self._get_has_error():

                def on_progress(*args):
                    self._line_blank_space_right_point = (
                        self._hint_lbl.width + dp(5))

                animation = Animation(
                    _line_blank_space_left_point=self._hint_lbl.x - dp(5),
                    _current_hint_text_color=self.line_color_focus,
                    fill_color=_fill_color,
                    duration=0.2,
                    t="out_quad",
                )
                animation.bind(on_progress=on_progress)
                animation.start(self)
            self.has_had_text = True
            Animation.cancel_all(self, "_line_width", "_hint_y",
                                 "_hint_lbl_font_size")
            if not self.text:
                self._anim_lbl_font_size(dp(14), sp(12))
            Animation(
                _line_width=self.width,
                duration=(0.2 if self.line_anim else 0),
                t="out_quad",
            ).start(self)
            if self._get_has_error():
                self._anim_current_error_color(self.error_color)
                if self.helper_text_mode == "on_error" and (
                        self.error or self._text_len_error):
                    self._anim_current_error_color(self.error_color)
                elif (self.helper_text_mode == "on_error" and not self.error
                      and not self._text_len_error):
                    self._anim_current_error_color((0, 0, 0, 0))
                elif self.helper_text_mode in ("persistent", "on_focus"):
                    self._anim_current_error_color(disabled_hint_text_color)
            else:
                self._anim_current_right_lbl_color(disabled_hint_text_color)
                Animation(
                    duration=0.2,
                    _current_hint_text_color=self.line_color_focus).start(self)
                if self.helper_text_mode == "on_error":
                    self._anim_current_error_color((0, 0, 0, 0))
                if self.helper_text_mode in ("persistent", "on_focus"):
                    self._anim_current_error_color(disabled_hint_text_color)
        else:
            _fill_color = self.fill_color
            _fill_color[3] = self.fill_color[3] + 0.1
            Animation(fill_color=_fill_color, duration=0.2,
                      t="out_quad").start(self)
            if not self.text:
                self._anim_lbl_font_size(dp(38), sp(16))
                Animation(
                    _line_blank_space_right_point=0,
                    _line_blank_space_left_point=0,
                    duration=0.2,
                    t="out_quad",
                ).start(self)
            if self._get_has_error():
                self._anim_get_has_error_color(self.error_color)
                if self.helper_text_mode == "on_error" and (
                        self.error or self._text_len_error):
                    self._anim_current_error_color(self.error_color)
                elif (self.helper_text_mode == "on_error" and not self.error
                      and not self._text_len_error):
                    self._anim_current_error_color((0, 0, 0, 0))
                elif self.helper_text_mode == "persistent":
                    self._anim_current_error_color(disabled_hint_text_color)
                elif self.helper_text_mode == "on_focus":
                    self._anim_current_error_color((0, 0, 0, 0))
            else:
                Animation(duration=0.2,
                          color=(1, 1, 1, 1)).start(self._hint_lbl)
                self._anim_get_has_error_color()
                if self.helper_text_mode == "on_error":
                    self._anim_current_error_color((0, 0, 0, 0))
                elif self.helper_text_mode == "persistent":
                    self._anim_current_error_color(disabled_hint_text_color)
                elif self.helper_text_mode == "on_focus":
                    self._anim_current_error_color((0, 0, 0, 0))
                Animation(
                    _line_width=0,
                    duration=(0.2 if self.line_anim else 0),
                    t="out_quad",
                ).start(self)

    def on_disabled(self, *args):
        if self.disabled:
            self._update_colors(self.theme_cls.disabled_hint_text_color)
        elif not self.disabled:
            if self.color_mode == "primary":
                self._update_primary_color()
            elif self.color_mode == "accent":
                self._update_accent_color()
            elif self.color_mode == "custom":
                self._update_colors(self.line_color_focus)

    def on_text(self, instance, text):
        self.text = re.sub("\n", " ", text) if not self.multiline else text
        if len(text) > 0:
            self.has_had_text = True
        if self.max_text_length is not None:
            self._right_msg_lbl.text = f"{len(text)}/{self.max_text_length}"
        self._set_text_len_error()
        if self.error or self._text_len_error:
            if self.focus:
                self._anim_current_line_color(self.error_color)
                if self.helper_text_mode == "on_error" and (
                        self.error or self._text_len_error):
                    self._anim_current_error_color(self.error_color)
                if self._text_len_error:
                    self._anim_current_right_lbl_color(self.error_color)
        else:
            if self.focus:
                self._anim_current_right_lbl_color(
                    self.theme_cls.disabled_hint_text_color)
                self._anim_current_line_color(self.line_color_focus)
                if self.helper_text_mode == "on_error":
                    self._anim_current_error_color((0, 0, 0, 0))
        if len(self.text) != 0 and not self.focus:
            self._hint_y = dp(14)
            self._hint_lbl_font_size = sp(12)

    def on_text_validate(self):
        self.has_had_text = True
        self._set_text_len_error()

    def on_color_mode(self, instance, mode):
        if mode == "primary":
            self._update_primary_color()
        elif mode == "accent":
            self._update_accent_color()
        elif mode == "custom":
            self._update_colors(self.line_color_focus)

    def on_line_color_focus(self, *args):
        if self.color_mode == "custom":
            self._update_colors(self.line_color_focus)

    def on__hint_text(self, instance, value):
        pass

    def on_hint_text(self, instance, value):
        self._hint_lbl.text = value
        self._hint_lbl.font_size = sp(16)

    def on_height(self, instance, value):
        if value >= self.max_height and self.max_height:
            self.height = self.max_height

    def _anim_get_has_error_color(self, color=None):
        # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_get_has_error.png
        if not color:
            line_color = self.line_color_focus
            hint_text_color = self.theme_cls.disabled_hint_text_color
            right_lbl_color = (0, 0, 0, 0)
        else:
            line_color = color
            hint_text_color = color
            right_lbl_color = color
        Animation(
            duration=0.2,
            _current_line_color=line_color,
            _current_hint_text_color=hint_text_color,
            _current_right_lbl_color=right_lbl_color,
        ).start(self)

    def _anim_current_line_color(self, color):
        # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_anim_current_line_color.gif
        Animation(
            duration=0.2,
            _current_hint_text_color=color,
            _current_line_color=color,
        ).start(self)

    def _anim_lbl_font_size(self, hint_y, font_size):
        Animation(
            _hint_y=hint_y,
            _hint_lbl_font_size=font_size,
            duration=0.2,
            t="out_quad",
        ).start(self)

    def _anim_current_right_lbl_color(self, color, duration=0.2):
        # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_anim_current_right_lbl_color.png
        Animation(duration=duration,
                  _current_right_lbl_color=color).start(self)

    def _anim_current_error_color(self, color, duration=0.2):
        # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_anim_current_error_color_to_disabled_color.gif
        # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_anim_current_error_color_to_fade.gif
        Animation(duration=duration, _current_error_color=color).start(self)

    def _update_colors(self, color):
        self.line_color_focus = color
        if not self.error and not self._text_len_error:
            self._current_line_color = color
            if self.focus:
                self._current_line_color = color

    def _update_accent_color(self, *args):
        if self.color_mode == "accent":
            self._update_colors(self.theme_cls.accent_color)

    def _update_primary_color(self, *args):
        if self.color_mode == "primary":
            self._update_colors(self.theme_cls.primary_color)

    def _update_theme_style(self, *args):
        self.line_color_normal = self.theme_cls.divider_color
        if not any([self.error, self._text_len_error]):
            if not self.focus:
                self._current_hint_text_color = (
                    self.theme_cls.disabled_hint_text_color)
                self._current_right_lbl_color = (
                    self.theme_cls.disabled_hint_text_color)
                if self.helper_text_mode == "persistent":
                    self._current_error_color = (
                        self.theme_cls.disabled_hint_text_color)

    def _get_has_error(self):
        if self.error or all([
                self.max_text_length is not None
                and len(self.text) > self.max_text_length
        ]):
            has_error = True
        else:
            if all((self.required, len(self.text) == 0, self.has_had_text)):
                has_error = True
            else:
                has_error = False
        return has_error

    def _get_max_text_length(self):
        """Returns the maximum number of characters that can be entered in a
        text field."""

        return (sys.maxsize
                if self.max_text_length is None else self.max_text_length)

    def _set_text_len_error(self):
        if len(self.text) > self._get_max_text_length() or all(
            (self.required, len(self.text) == 0, self.has_had_text)):
            self._text_len_error = True
        else:
            self._text_len_error = False

    def _set_msg(self, instance, text):
        self._msg_lbl.text = text
        self.helper_text = text

    def _set_message_mode(self, instance, text):
        self.helper_text_mode = text
        if self.helper_text_mode == "persistent":
            self._anim_current_error_color(
                self.theme_cls.disabled_hint_text_color, 0.1)

    def _set_max_text_length(self, instance, length):
        self.max_text_length = length
        self._right_msg_lbl.text = f"{len(self.text)}/{length}"

    def _refresh_hint_text(self):
        pass
Exemplo n.º 29
0
class MDDialog(BaseDialog):
    title = StringProperty()
    """
    Title dialog.

    .. code-block:: python

        [...]
            self.dialog = MDDialog(
                title="Reset settings?",
                buttons=[
                    MDFlatButton(
                        text="CANCEL", text_color=self.theme_cls.primary_color
                    ),
                    MDFlatButton(
                        text="ACCEPT", text_color=self.theme_cls.primary_color
                    ),
                ],
            )
            [...]

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-title.png
        :align: center

    :attr:`title` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    text = StringProperty()
    """
    Text dialog.

    .. code-block:: python

        [...]
            self.dialog = MDDialog(
                title="Reset settings?",
                text="This will reset your device to its default factory settings.",
                buttons=[
                    MDFlatButton(
                        text="CANCEL", text_color=self.theme_cls.primary_color
                    ),
                    MDFlatButton(
                        text="ACCEPT", text_color=self.theme_cls.primary_color
                    ),
                ],
            )
            [...]

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-text.png
        :align: center

    :attr:`text` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    buttons = ListProperty()
    """
    List of button objects for dialog.
    Objects must be inherited from :class:`~kivymd.uix.button.BaseButton` class.

    .. code-block:: python

        [...]
            self.dialog = MDDialog(
                text="Discard draft?",
                buttons=[
                    MDFlatButton(text="CANCEL"), MDRaisedButton(text="DISCARD"),
                ],
            )
            [...]

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-buttons.png
        :align: center

    :attr:`buttons` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    items = ListProperty()
    """
    List of items objects for dialog.
    Objects must be inherited from :class:`~kivymd.uix.list.BaseListItem` class.

    With type 'simple'
    ~~~~~~~~~~~~~~~~~~

    .. code-block:: python

        from kivy.lang import Builder
        from kivy.properties import StringProperty

        from kivymd.app import MDApp
        from kivymd.uix.dialog import MDDialog
        from kivymd.uix.list import OneLineAvatarListItem

        KV = '''
        <Item>

            ImageLeftWidget:
                source: root.source


        FloatLayout:

            MDFlatButton:
                text: "ALERT DIALOG"
                pos_hint: {'center_x': .5, 'center_y': .5}
                on_release: app.show_simple_dialog()
        '''


        class Item(OneLineAvatarListItem):
            divider = None
            source = StringProperty()


        class Example(MDApp):
            dialog = None

            def build(self):
                return Builder.load_string(KV)

            def show_simple_dialog(self):
                if not self.dialog:
                    self.dialog = MDDialog(
                        title="Set backup account",
                        type="simple",
                        items=[
                            Item(text="*****@*****.**", source="user-1.png"),
                            Item(text="*****@*****.**", source="user-2.png"),
                            Item(text="Add account", source="add-icon.png"),
                        ],
                    )
                self.dialog.open()


        Example().run()

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-items.png
        :align: center

    With type 'confirmation'
    ~~~~~~~~~~~~~~~~~~~~~~~~

    .. code-block:: python

        from kivy.lang import Builder

        from kivymd.app import MDApp
        from kivymd.uix.button import MDFlatButton
        from kivymd.uix.dialog import MDDialog
        from kivymd.uix.list import OneLineAvatarIconListItem

        KV = '''
        <ItemConfirm>
            on_release: root.set_icon(check)

            CheckboxLeftWidget:
                id: check
                group: "check"


        FloatLayout:

            MDFlatButton:
                text: "ALERT DIALOG"
                pos_hint: {'center_x': .5, 'center_y': .5}
                on_release: app.show_confirmation_dialog()
        '''


        class ItemConfirm(OneLineAvatarIconListItem):
            divider = None

            def set_icon(self, instance_check):
                instance_check.active = True
                check_list = instance_check.get_widgets(instance_check.group)
                for check in check_list:
                    if check != instance_check:
                        check.active = False


        class Example(MDApp):
            dialog = None

            def build(self):
                return Builder.load_string(KV)

            def show_confirmation_dialog(self):
                if not self.dialog:
                    self.dialog = MDDialog(
                        title="Phone ringtone",
                        type="confirmation",
                        items=[
                            ItemConfirm(text="Callisto"),
                            ItemConfirm(text="Luna"),
                            ItemConfirm(text="Night"),
                            ItemConfirm(text="Solo"),
                            ItemConfirm(text="Phobos"),
                            ItemConfirm(text="Diamond"),
                            ItemConfirm(text="Sirena"),
                            ItemConfirm(text="Red music"),
                            ItemConfirm(text="Allergio"),
                            ItemConfirm(text="Magic"),
                            ItemConfirm(text="Tic-tac"),
                        ],
                        buttons=[
                            MDFlatButton(
                                text="CANCEL", text_color=self.theme_cls.primary_color
                            ),
                            MDFlatButton(
                                text="OK", text_color=self.theme_cls.primary_color
                            ),
                        ],
                    )
                self.dialog.open()


        Example().run()

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-confirmation.png
        :align: center

    :attr:`items` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    width_offset = NumericProperty(dp(48))
    """
    Dialog offset from device width.

    :attr:`width_offset` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `dp(48)`.
    """

    type = OptionProperty(
        "alert", options=["alert", "simple", "confirmation", "custom"]
    )
    """
    Dialog type.
    Available option are `'alert'`, `'simple'`, `'confirmation'`, `'custom'`.

    :attr:`type` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'alert'`.
    """

    content_cls = ObjectProperty()
    """
    Custom content class.

    .. code-block:: python

        from kivy.lang import Builder
        from kivy.uix.boxlayout import BoxLayout

        from kivymd.app import MDApp
        from kivymd.uix.button import MDFlatButton
        from kivymd.uix.dialog import MDDialog

        KV = '''
        <Content>
            orientation: "vertical"
            spacing: "12dp"
            size_hint_y: None
            height: "120dp"

            MDTextField:
                hint_text: "City"

            MDTextField:
                hint_text: "Street"


        FloatLayout:

            MDFlatButton:
                text: "ALERT DIALOG"
                pos_hint: {'center_x': .5, 'center_y': .5}
                on_release: app.show_confirmation_dialog()
        '''


        class Content(BoxLayout):
            pass


        class Example(MDApp):
            dialog = None

            def build(self):
                return Builder.load_string(KV)

            def show_confirmation_dialog(self):
                if not self.dialog:
                    self.dialog = MDDialog(
                        title="Address:",
                        type="custom",
                        content_cls=Content(),
                        buttons=[
                            MDFlatButton(
                                text="CANCEL", text_color=self.theme_cls.primary_color
                            ),
                            MDFlatButton(
                                text="OK", text_color=self.theme_cls.primary_color
                            ),
                        ],
                    )
                self.dialog.open()


        Example().run()

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-custom.png
        :align: center

    :attr:`content_cls` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to `'None'`.
    """

    md_bg_color = ColorProperty(None)
    """
    Background color in the format (r, g, b, a).

    :attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    _scroll_height = NumericProperty("28dp")
    _spacer_top = NumericProperty("24dp")

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Window.bind(on_resize=self.update_width)

        self.md_bg_color = (
            self.theme_cls.bg_dark if not self.md_bg_color else self.md_bg_color
        )

        if self.size_hint == [1, 1] and (
            DEVICE_TYPE == "desktop" or DEVICE_TYPE == "tablet"
        ):
            self.size_hint = (None, None)
            self.width = min(dp(560), Window.width - self.width_offset)
        elif self.size_hint == [1, 1] and DEVICE_TYPE == "mobile":
            self.size_hint = (None, None)
            self.width = min(dp(280), Window.width - self.width_offset)

        if not self.title:
            self._spacer_top = 0

        if not self.buttons:
            self.ids.root_button_box.height = 0
        else:
            self.create_buttons()

        update_height = False
        if self.type in ("simple", "confirmation"):
            if self.type == "confirmation":
                self.ids.spacer_top_box.add_widget(MDSeparator())
                self.ids.spacer_bottom_box.add_widget(MDSeparator())
            self.create_items()
        if self.type == "custom":
            if self.content_cls:
                self.ids.container.remove_widget(self.ids.scroll)
                self.ids.container.remove_widget(self.ids.text)
                self.ids.spacer_top_box.add_widget(self.content_cls)
                self.ids.spacer_top_box.padding = (0, "24dp", "16dp", 0)
                update_height = True
        if self.type == "alert":
            self.ids.scroll.bar_width = 0

        if update_height:
            Clock.schedule_once(self.update_height)

    def update_width(self, *args):
        self.width = max(
            self.height + self.width_offset,
            min(
                dp(560) if DEVICE_TYPE != "mobile" else dp(280),
                Window.width - self.width_offset,
            ),
        )

    def update_height(self, *_):
        self._spacer_top = self.content_cls.height + dp(24)

    def on_open(self):
        # TODO: Add scrolling text.
        self.height = self.ids.container.height

    def set_normal_height(self):
        self.size_hint_y = 0.8

    def get_normal_height(self):
        return (
            (Window.height * 80 / 100)
            - self._spacer_top
            - dp(52)
            - self.ids.container.padding[1]
            - self.ids.container.padding[-1]
            - 100
        )

    def edit_padding_for_item(self, instance_item):
        instance_item.ids._left_container.x = 0
        instance_item._txt_left_pad = "56dp"

    def create_items(self):
        if not self.text:
            self.ids.container.remove_widget(self.ids.text)
            height = 0
        else:
            height = self.ids.text.height

        for item in self.items:
            if issubclass(item.__class__, BaseListItem):
                height += item.height  # calculate height contents
                self.edit_padding_for_item(item)
                self.ids.box_items.add_widget(item)

        if height > Window.height:
            self.set_normal_height()
            self.ids.scroll.height = self.get_normal_height()
        else:
            self.ids.scroll.height = height

    def create_buttons(self):
        for button in self.buttons:
            if issubclass(button.__class__, BaseButton):
                self.ids.button_box.add_widget(button)
Exemplo n.º 30
0
    def test_color_property(self):
        from kivy.properties import ColorProperty

        color = ColorProperty()
        color.link(wid, 'color')
        color.link_deps(wid, 'color')
        self.assertEqual(color.get(wid), [1, 1, 1, 1])

        color.set(wid, "#00ff00")
        self.assertEqual(color.get(wid), [0, 1, 0, 1])

        color.set(wid, "#7f7fff7f")
        self.assertEqual(color.get(wid)[0], 127 / 255.)
        self.assertEqual(color.get(wid)[1], 127 / 255.)
        self.assertEqual(color.get(wid)[2], 1)
        self.assertEqual(color.get(wid)[3], 127 / 255.)

        color.set(wid, (1, 1, 0))
        self.assertEqual(color.get(wid), [1, 1, 0, 1])
        color.set(wid, (1, 1, 0, 0))
        self.assertEqual(color.get(wid), [1, 1, 0, 0])
Exemplo n.º 31
0
class MDBackdrop(ThemableBehavior, FloatLayout):
    """
    :Events:
        :attr:`on_open`
            When the front layer drops.
        :attr:`on_close`
            When the front layer rises.
    """

    padding = ListProperty([0, 0, 0, 0])
    """
    Padding for contents of the front layer.

    :attr:`padding` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0, 0, 0]`.
    """

    left_action_items = ListProperty()
    """
    The icons and methods left of the :class:`kivymd.uix.toolbar.MDToolbar`
    in back layer. For more information, see the :class:`kivymd.uix.toolbar.MDToolbar` module
    and :attr:`left_action_items` parameter.

    :attr:`left_action_items` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    right_action_items = ListProperty()
    """
    Works the same way as :attr:`left_action_items`.

    :attr:`right_action_items` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    title = StringProperty()
    """
    See the :class:`kivymd.uix.toolbar.MDToolbar.title` parameter.

    :attr:`title` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    back_layer_color = ColorProperty(None)
    """
    Background color of back layer.

    :attr:`back_layer_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    front_layer_color = ColorProperty(None)
    """
    Background color of front layer.

    :attr:`front_layer_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    radius_left = NumericProperty("16dp")
    """
    The value of the rounding radius of the upper left corner
    of the front layer.

    :attr:`radius_left` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `16dp`.
    """

    radius_right = NumericProperty("16dp")
    """
    The value of the rounding radius of the upper right corner
    of the front layer.

    :attr:`radius_right` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `16dp`.
    """

    header = BooleanProperty(True)
    """
    Whether to use a header above the contents of the front layer.

    :attr:`header` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    header_text = StringProperty("Header")
    """
    Text of header.

    :attr:`header_text` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'Header'`.
    """

    close_icon = StringProperty("close")
    """
    The name of the icon that will be installed on the toolbar
    on the left when opening the front layer.

    :attr:`close_icon` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'close'`.
    """

    _open_icon = ""
    _front_layer_open = False

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.register_event_type("on_open")
        self.register_event_type("on_close")
        Clock.schedule_once(
            lambda x: self.on_left_action_items(self, self.left_action_items))

    def on_open(self):
        """When the front layer drops."""

    def on_close(self):
        """When the front layer rises."""

    def on_left_action_items(self, instance, value):
        if value:
            self.left_action_items = [value[0]]
        else:
            self.left_action_items = [["menu", lambda x: self.open()]]
        self._open_icon = self.left_action_items[0][0]

    def on_header(self, instance, value):
        if not value:
            self.ids._front_layer.remove_widget(self.ids.header_button)

    def open(self, open_up_to=0):
        """
        Opens the front layer.

        :open_up_to:
            the height to which the front screen will be lowered;
            if equal to zero - falls to the bottom of the screen;
        """

        self.animtion_icon_menu()
        if self._front_layer_open:
            self.close()
            return

        if open_up_to:
            if open_up_to < (self.ids.header_button.height -
                             self.ids._front_layer.height):
                y = self.ids.header_button.height - self.ids._front_layer.height
            elif open_up_to > 0:
                y = 0
            else:
                y = open_up_to
        else:
            y = self.ids.header_button.height - self.ids._front_layer.height

        Animation(y=y, d=0.2, t="out_quad").start(self.ids._front_layer)
        self._front_layer_open = True
        self.dispatch("on_open")

    def close(self):
        """Opens the front layer."""

        Animation(y=0, d=0.2, t="out_quad").start(self.ids._front_layer)
        self._front_layer_open = False
        self.dispatch("on_close")

    def animtion_icon_menu(self):
        icon_menu = self.ids.toolbar.ids.left_actions.children[0]
        anim = Animation(opacity=0, d=0.2, t="out_quad")
        anim.bind(on_complete=self.animtion_icon_close)
        anim.start(icon_menu)

    def animtion_icon_close(self, instance_animation, instance_icon_menu):
        instance_icon_menu.icon = (self.close_icon if instance_icon_menu.icon
                                   == self._open_icon else self._open_icon)
        Animation(opacity=1, d=0.2).start(instance_icon_menu)

    def add_widget(self, widget, index=0, canvas=None):
        if widget.__class__ in (MDBackdropToolbar, _BackLayer, _FrontLayer):
            return super().add_widget(widget)
        else:
            if widget.__class__ is MDBackdropBackLayer:
                self.ids.back_layer.add_widget(widget)
            elif widget.__class__ is MDBackdropFrontLayer:
                self.ids.front_layer.add_widget(widget)