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
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()
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
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()
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)
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()
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()
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]) """
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
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
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
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
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
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)
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
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.
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)
class FloatButton(AnchorLayout): callback = ObjectProperty() md_bg_color = ColorProperty([1, 1, 1, 1]) icon = StringProperty()
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]
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]
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 )
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`
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.
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)
class ColourProperties(MasterColour): # , MasterTextColour, MasterTrimColour): background_colour = ColorProperty() foreground_colour = ColorProperty() trim_colour = ColorProperty() text_colour = ColorProperty()
class HomeMenuCard(MDCard): icon = StringProperty() bg_color = ColorProperty() icon_color = ColorProperty(None, allownone=True) text = StringProperty()
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
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)
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])
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)