""" Components/Text Field ===================== .. seealso:: `Material Design spec, Text fields `_ .. rubric:: Text fields let users enter and edit text. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields.png :align: center `KivyMD` provides the following field classes for use: - MDTextField_ - MDTextFieldRound_ - MDTextFieldRect_ .. Note:: :class:`~MDTextField` inherited from :class:`~kivy.uix.textinput.TextInput`. Therefore, most parameters and all events of the :class:`~kivy.uix.textinput.TextInput` class are also available in the :class:`~MDTextField` class. .. MDTextField: MDTextField ----------- :class:`~MDTextField` can be with helper text and without. Without helper text mode ------------------------ .. code-block:: kv MDTextField: hint_text: "No helper text" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-no-helper-mode.gif :align: center Helper text mode on ``on_focus`` event -------------------------------------- .. code-block:: kv MDTextField: hint_text: "Helper text on focus" helper_text: "This will disappear when you click off" helper_text_mode: "on_focus" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-mode-on-focus.gif :align: center Persistent helper text mode --------------------------- .. code-block:: kv MDTextField: hint_text: "Persistent helper text" helper_text: "Text is always here" helper_text_mode: "persistent" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-mode-persistent.gif :align: center Helper text mode `'on_error'` ---------------------------- To display an error in a text field when using the ``helper_text_mode: "on_error"`` parameter, set the `"error"` text field parameter to `True`: .. code-block:: python from kivy.lang import Builder from kivymd.app import MDApp KV = ''' BoxLayout: padding: "10dp" MDTextField: id: text_field_error hint_text: "Helper text on error (press 'Enter')" helper_text: "There will always be a mistake" helper_text_mode: "on_error" pos_hint: {"center_y": .5} ''' class Test(MDApp): def __init__(self, **kwargs): super().__init__(**kwargs) self.screen = Builder.load_string(KV) def build(self): self.screen.ids.text_field_error.bind( on_text_validate=self.set_error_message, on_focus=self.set_error_message, ) return self.screen def set_error_message(self, instance_textfield): self.screen.ids.text_field_error.error = True Test().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-mode-on-error.gif :align: center Helper text mode `'on_error'` (with required) -------------------------------------------- .. code-block:: kv MDTextField: hint_text: "required = True" required: True helper_text_mode: "on_error" helper_text: "Enter text" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-required.gif :align: center Text length control ------------------- .. code-block:: kv MDTextField: hint_text: "Max text length = 5" max_text_length: 5 .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-length.gif :align: center Multi line text --------------- .. code-block:: kv MDTextField: multiline: True hint_text: "Multi-line text" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-multi-line.gif :align: center Color mode ---------- .. code-block:: kv MDTextField: hint_text: "color_mode = 'accent'" color_mode: 'accent' Available options are `'primary'`, `'accent'` or `'custom`'. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-color-mode.gif :align: center .. code-block:: kv MDTextField: hint_text: "color_mode = 'custom'" color_mode: 'custom' helper_text_mode: "on_focus" helper_text: "Color is defined by 'line_color_focus' property" line_color_focus: 1, 0, 1, 1 .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-color-mode-custom.gif :align: center .. code-block:: kv MDTextField: hint_text: "Line color normal" line_color_normal: app.theme_cls.accent_color .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-line-color-normal.png :align: center Rectangle mode -------------- .. code-block:: kv MDTextField: hint_text: "Rectangle mode" mode: "rectangle" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-rectangle-mode.gif :align: center Fill mode --------- .. code-block:: kv MDTextField: hint_text: "Fill mode" mode: "fill" fill_color: 0, 0, 0, .4 .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-fill-mode.gif :align: center .. MDTextFieldRect: MDTextFieldRect --------------- .. Note:: :class:`~MDTextFieldRect` inherited from :class:`~kivy.uix.textinput.TextInput`. You can use all parameters and attributes of the :class:`~kivy.uix.textinput.TextInput` class in the :class:`~MDTextFieldRect` class. .. code-block:: kv MDTextFieldRect: size_hint: 1, None height: "30dp" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-rect.gif :align: center .. Warning:: While there is no way to change the color of the border. .. MDTextFieldRound: MDTextFieldRound ---------------- Without icon ------------ .. code-block:: kv MDTextFieldRound: hint_text: 'Empty field' .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-round.gif :align: center With left icon -------------- .. Warning:: The icons in the :class:`~MDTextFieldRound` are static. You cannot bind events to them. .. code-block:: kv MDTextFieldRound: icon_left: "email" hint_text: "Field with left icon" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-round-left-icon.png :align: center With left and right icons ------------------------- .. code-block:: kv MDTextFieldRound: icon_left: 'key-variant' icon_right: 'eye-off' hint_text: 'Field with left and right icons' .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-round-left-right-icon.png :align: center Control background color ------------------------ .. code-block:: kv MDTextFieldRound: icon_left: 'key-variant' normal_color: app.theme_cls.accent_color .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-round-normal-color.gif :align: center .. code-block:: kv MDTextFieldRound: icon_left: 'key-variant' normal_color: app.theme_cls.accent_color color_active: 1, 0, 0, 1 .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-round-active-color.gif :align: center Clickable icon for MDTextFieldRound ----------------------------------- .. code-block:: python from kivy.lang import Builder from kivy.properties import StringProperty from kivymd.app import MDApp from kivymd.uix.relativelayout import MDRelativeLayout KV = ''' : size_hint_y: None height: text_field.height MDTextFieldRound: id: text_field hint_text: root.hint_text text: root.text password: True color_active: app.theme_cls.primary_light icon_left: "key-variant" padding: self._lbl_icon_left.texture_size[1] + dp(10) if self.icon_left else dp(15), \ (self.height / 2) - (self.line_height / 2), \ self._lbl_icon_right.texture_size[1] + dp(20), \ 0 MDIconButton: icon: "eye-off" ripple_scale: .5 pos_hint: {"center_y": .5} pos: text_field.width - self.width + dp(8), 0 on_release: self.icon = "eye" if self.icon == "eye-off" else "eye-off" text_field.password = False if text_field.password is True else True MDScreen: ClickableTextFieldRound: size_hint_x: None width: "300dp" hint_text: "Password" pos_hint: {"center_x": .5, "center_y": .5} ''' class ClickableTextFieldRound(MDRelativeLayout): text = StringProperty() hint_text = StringProperty() # Here specify the required parameters for MDTextFieldRound: # [...] class Test(MDApp): def build(self): return Builder.load_string(KV) Test().run() With right icon --------------- .. Note:: The icon on the right is available for use in all text fields. .. code-block:: kv MDTextField: hint_text: "Name" mode: "fill" fill_color: 0, 0, 0, .4 icon_right: "arrow-down-drop-circle-outline" icon_right_color: app.theme_cls.primary_color .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-fill-mode-icon.png :align: center .. code-block:: kv MDTextField: hint_text: "Name" icon_right: "arrow-down-drop-circle-outline" icon_right_color: app.theme_cls.primary_color .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-right-icon.png :align: center .. code-block:: kv MDTextField: hint_text: "Name" mode: "rectangle" icon_right: "arrow-down-drop-circle-outline" icon_right_color: app.theme_cls.primary_color .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-rectangle-right-icon.png :align: center .. seealso:: See more information in the :class:`~MDTextFieldRect` class. """ __all__ = ("MDTextField", "MDTextFieldRect", "MDTextFieldRound") import sys from kivy.animation import Animation from kivy.lang import Builder from kivy.metrics import dp, sp from kivy.properties import ( BooleanProperty, ListProperty, NumericProperty, ObjectProperty, OptionProperty, StringProperty, ) from kivy.uix.label import Label from kivy.uix.textinput import TextInput from kivymd.font_definitions import theme_font_styles from kivymd.material_resources import DEVICE_TYPE from kivymd.theming import ThemableBehavior from kivymd.uix.label import MDIcon Builder.load_string( """ #:import images_path kivymd.images_path canvas.before: Clear # Disabled line. Color: rgba: self.line_color_normal if root.mode == "line" else (0, 0, 0, 0) Line: points: self.x, self.y + dp(16), self.x + self.width, self.y + dp(16) width: 1 dash_length: dp(3) dash_offset: 2 if self.disabled else 0 # Active line. Color: rgba: self._current_line_color if root.mode in ("line", "fill") and root.active_line else (0, 0, 0, 0) Rectangle: size: self._line_width, dp(2) pos: self.center_x - (self._line_width / 2), self.y + (dp(16) if root.mode != "fill" else 0) # Helper text. Color: rgba: self._current_error_color Rectangle: texture: self._msg_lbl.texture size: self._msg_lbl.texture_size[0] - (dp(3) if root.mode in ("fill", "rectangle") else 0), \ self._msg_lbl.texture_size[1] - (dp(3) if root.mode in ("fill", "rectangle") else 0) pos: self.x + (dp(8) if root.mode == "fill" else 0), self.y + (dp(3) if root.mode in ("fill", "rectangle") else 0) # Texture of right Icon. Color: rgba: self.icon_right_color Rectangle: texture: self._lbl_icon_right.texture size: self._lbl_icon_right.texture_size if self.icon_right else (0, 0) pos: (self.width + self.x) - (self._lbl_icon_right.texture_size[1]) - dp(8), \ self.center[1] - self._lbl_icon_right.texture_size[1] / 2 + (dp(8) if root.mode != "fill" else 0) \ if root.mode != "rectangle" else \ self.center[1] - self._lbl_icon_right.texture_size[1] / 2 - dp(4) Color: rgba: self._current_right_lbl_color Rectangle: texture: self._right_msg_lbl.texture size: self._right_msg_lbl.texture_size pos: self.x + self.width - self._right_msg_lbl.texture_size[0] - dp(16), self.y Color: rgba: (self._current_line_color if self.focus and not \ self._cursor_blink else (0, 0, 0, 0)) Rectangle: pos: (int(x) for x in self.cursor_pos) size: 1, -self.line_height # Hint text. Color: rgba: self._current_hint_text_color if not self.current_hint_text_color else self.current_hint_text_color Rectangle: texture: self._hint_lbl.texture size: self._hint_lbl.texture_size pos: self.x + (dp(8) if root.mode == "fill" else 0), self.y + self.height - self._hint_y Color: rgba: self.disabled_foreground_color if self.disabled else\ (self.hint_text_color if not self.text and not\ self.focus else self.foreground_color) # "rectangle" mode Color: rgba: self._current_line_color Line: width: dp(1) if root.mode == "rectangle" else dp(0.00001) points: ( self.x + root._line_blank_space_right_point, self.top - self._hint_lbl.texture_size[1] // 2, self.right + dp(12), self.top - self._hint_lbl.texture_size[1] // 2, self.right + dp(12), self.y, self.x - dp(12), self.y, self.x - dp(12), self.top - self._hint_lbl.texture_size[1] // 2, self.x + root._line_blank_space_left_point, self.top - self._hint_lbl.texture_size[1] // 2 ) # "fill" mode. canvas.after: Color: rgba: root.fill_color if root.mode == "fill" else (0, 0, 0, 0) RoundedRectangle: pos: self.x, self.y size: self.width, self.height + dp(8) radius: (10, 10, 0, 0, 0) font_name: "Roboto" if not root.font_name else root.font_name foreground_color: app.theme_cls.text_color font_size: "16sp" bold: False padding: 0 if root.mode != "fill" else "8dp", \ "16dp" if root.mode != "fill" else "24dp", \ 0 if root.mode != "fill" and not root.icon_right else ("14dp" if not root.icon_right else self._lbl_icon_right.texture_size[1] + dp(20)), \ "16dp" if root.mode == "fill" else "10dp" multiline: False size_hint_y: None height: self.minimum_height + (dp(8) if root.mode != "fill" else 0) size_hint_x: None width: self.texture_size[0] shorten: True shorten_from: "right" on_focus: root.anim_rect((root.x, root.y, root.right, root.y, root.right, \ root.top, root.x, root.top, root.x, root.y), 1) if root.focus \ else root.anim_rect((root.x - dp(60), root.y - dp(60), \ root.right + dp(60), root.y - dp(60), root.right + dp(60), root.top + dp(60), \ root.x - dp(60), root.top + dp(60), \ root.x - dp(60), root.y - dp(60)), 0) canvas.after: Color: rgba: root._primary_color Line: width: dp(1.5) points: ( self.x - dp(60), self.y - dp(60), self.right + dp(60), self.y - dp(60), self.right + dp(60), self.top + dp(60), self.x - dp(60), self.top + dp(60), self.x - dp(60), self.y - dp(60) ) : multiline: False size_hint: 1, None height: self.line_height + dp(10) background_active: f'{images_path}transparent.png' background_normal: f'{images_path}transparent.png' padding: self._lbl_icon_left.texture_size[1] + dp(10) if self.icon_left else dp(15), \ (self.height / 2) - (self.line_height / 2), \ self._lbl_icon_right.texture_size[1] + dp(20) if self.icon_right else dp(15), \ 0 canvas.before: Color: rgba: self.normal_color if not self.focus else self._color_active Ellipse: angle_start: 180 angle_end: 360 pos: self.pos[0] - self.size[1] / 2, self.pos[1] size: self.size[1], self.size[1] Ellipse: angle_start: 360 angle_end: 540 pos: self.size[0] + self.pos[0] - self.size[1]/2.0, self.pos[1] size: self.size[1], self.size[1] Rectangle: pos: self.pos size: self.size Color: rgba: self.line_color Line: points: self.pos[0] , self.pos[1], self.pos[0] + self.size[0], self.pos[1] Line: points: self.pos[0], self.pos[1] + self.size[1], self.pos[0] + self.size[0], self.pos[1] + self.size[1] Line: ellipse: self.pos[0] - self.size[1] / 2, self.pos[1], self.size[1], self.size[1], 180, 360 Line: ellipse: self.size[0] + self.pos[0] - self.size[1] / 2.0, self.pos[1], self.size[1], self.size[1], 360, 540 # Texture of left Icon. Color: rgba: self.icon_left_color Rectangle: texture: self._lbl_icon_left.texture size: self._lbl_icon_left.texture_size if self.icon_left \ else (0, 0) pos: self.x, \ self.center[1] - self._lbl_icon_right.texture_size[1] / 2 # Texture of right Icon. Color: rgba: self.icon_right_color Rectangle: texture: self._lbl_icon_right.texture size: self._lbl_icon_right.texture_size if self.icon_right \ else (0, 0) pos: (self.width + self.x) - (self._lbl_icon_right.texture_size[1]), \ self.center[1] - self._lbl_icon_right.texture_size[1] / 2 Color: rgba: root.theme_cls.disabled_hint_text_color if not self.focus \ else root.foreground_color """ ) class MDTextFieldRect(ThemableBehavior, TextInput): _primary_color = ListProperty((0, 0, 0, 0)) def __init__(self, **kwargs): super().__init__(**kwargs) self._update_primary_color() self.theme_cls.bind(primary_color=self._update_primary_color) def anim_rect(self, points, alpha): instance_line = self.canvas.children[-1].children[-1] instance_color = self.canvas.children[-1].children[0] if alpha == 1: d_line = 0.3 d_color = 0.4 else: d_line = 0.05 d_color = 0.05 Animation(points=points, d=d_line, t="out_cubic").start(instance_line) Animation(a=alpha, d=d_color).start(instance_color) def _update_primary_color(self, *args): self._primary_color = self.theme_cls.primary_color self._primary_color[3] = 0 class TextfieldLabel(ThemableBehavior, Label): font_style = OptionProperty("Body1", options=theme_font_styles) # field = ObjectProperty() def __init__(self, **kwargs): super().__init__(**kwargs) self.font_size = sp(self.theme_cls.font_styles[self.font_style][1]) 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 = ListProperty() """ Line color normal in ``rgba`` format. :attr:`line_color_normal` is an :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ line_color_focus = ListProperty() """ Line color focus in ``rgba`` format. :attr:`line_color_focus` is an :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ error_color = ListProperty() """ Error color in ``rgba`` format for ``required = True``. :attr:`error_color` is an :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ fill_color = ListProperty((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.ListProperty` 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 = ListProperty() """ ``hint_text`` text color. :attr:`current_hint_text_color` is an :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ icon_right = StringProperty() """Right icon. :attr:`icon_right` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ icon_right_color = ListProperty((0, 0, 0, 1)) """Color of right icon in ``rgba`` format. :attr:`icon_right_color` is an :class:`~kivy.properties.ListProperty` and defaults to `(0, 0, 0, 1)`. """ _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 = ListProperty((0, 0, 0, 0)) _current_error_color = ListProperty((0, 0, 0, 0)) _current_hint_text_color = ListProperty((0, 0, 0, 0)) _current_right_lbl_color = ListProperty((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._set_hint, _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 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: self._line_blank_space_right_point = ( self._get_line_blank_space_right_point() ) _fill_color = self.fill_color _fill_color[3] = self.fill_color[3] - 0.1 if not self._get_has_error(): Animation( _line_blank_space_right_point=self._line_blank_space_right_point, _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", ).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, 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, t="out_quad").start(self) def on_text(self, instance, 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 _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_line_blank_space_right_point(self): # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_line_blank_space_right_point.png return ( self._hint_lbl.texture_size[0] - self._hint_lbl.texture_size[0] / 100 * dp(18) if DEVICE_TYPE == "desktop" else dp(10) ) 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_hint(self, instance, text): self._hint_lbl.text = text 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 MDTextFieldRound(ThemableBehavior, TextInput): icon_left = StringProperty() """Left icon. :attr:`icon_left` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ icon_left_color = ListProperty((0, 0, 0, 1)) """Color of left icon in ``rgba`` format. :attr:`icon_left_color` is an :class:`~kivy.properties.ListProperty` 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 = ListProperty((0, 0, 0, 1)) """Color of right icon. :attr:`icon_right_color` is an :class:`~kivy.properties.ListProperty` and defaults to `(0, 0, 0, 1)`. """ line_color = ListProperty() """Field line color. :attr:`line_color` is an :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ normal_color = ListProperty() """Field color if `focus` is `False`. :attr:`normal_color` is an :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ color_active = ListProperty() """Field color if `focus` is `True`. :attr:`color_active` is an :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ _color_active = ListProperty() def __init__(self, **kwargs): super().__init__(**kwargs) self._lbl_icon_left = MDIcon(theme_text_color="Custom") self._lbl_icon_right = MDIcon(theme_text_color="Custom") self.cursor_color = self.theme_cls.primary_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.theme_cls.text_color self.icon_right_color = 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 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_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