first commit
This commit is contained in:
commit
417e54da96
5696 changed files with 900003 additions and 0 deletions
|
@ -0,0 +1 @@
|
|||
from .timepicker import MDTimePicker # NOQA F401
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,340 @@
|
|||
<TimeInputLabel@MDLabel>:
|
||||
theme_text_color: "Custom"
|
||||
font_size: dp(10)
|
||||
halign: "left"
|
||||
valign: "bottom"
|
||||
adaptive_size: True
|
||||
|
||||
|
||||
<AmPmSelectorLabel>
|
||||
halign: "center"
|
||||
valign: "center"
|
||||
theme_text_color: "Custom"
|
||||
|
||||
|
||||
<AmPmSelector>
|
||||
size_hint: None, None
|
||||
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: root.border_color
|
||||
RoundedRectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
radius: [root.border_radius, ]
|
||||
|
||||
#AM
|
||||
Color:
|
||||
rgba: root._am_bg_color
|
||||
RoundedRectangle:
|
||||
pos:
|
||||
[ \
|
||||
self.pos[0] + root.border_width, \
|
||||
self.pos[1] + self.height/2 + self.border_width * 0.5 \
|
||||
] if self.orientation == "vertical" else \
|
||||
[ \
|
||||
self.pos[0] + root.border_width, \
|
||||
self.pos[1] + root.border_width \
|
||||
]
|
||||
size:
|
||||
[ \
|
||||
self.size[0] - root.border_width * 2, \
|
||||
self.size[1] / 2 - self.border_width * 1.5 \
|
||||
] if self.orientation == "vertical" else \
|
||||
[ \
|
||||
self.size[0] / 2 - root.border_width * 1.5, \
|
||||
self.size[1] - root.border_width * 2 \
|
||||
]
|
||||
radius:
|
||||
[root.border_radius, root.border_radius, 0, 0] \
|
||||
if self.orientation == "vertical" else \
|
||||
[root.border_radius, 0, 0, root.border_radius]
|
||||
|
||||
#PM
|
||||
Color:
|
||||
rgba: root._pm_bg_color
|
||||
RoundedRectangle:
|
||||
pos:
|
||||
[ \
|
||||
self.pos[0] + root.border_width, \
|
||||
self.pos[1] + self.border_width \
|
||||
] if self.orientation == "vertical" else \
|
||||
[ \
|
||||
self.pos[0] + root.size[0] / 2 + root.border_width / 2, \
|
||||
self.pos[1] + root.border_width \
|
||||
]
|
||||
size:
|
||||
[ \
|
||||
self.size[0] - root.border_width * 2, \
|
||||
self.size[1] / 2 - self.border_width * 1.5 \
|
||||
] if self.orientation == "vertical" else \
|
||||
[ \
|
||||
self.size[0] / 2 - root.border_width * 1.5, \
|
||||
self.size[1] - root.border_width * 2 \
|
||||
]
|
||||
radius:
|
||||
[0, 0, root.border_radius, root.border_radius] \
|
||||
if self.orientation == "vertical" else \
|
||||
[0 ,root.border_radius, root.border_radius, 0]
|
||||
|
||||
# AM
|
||||
AmPmSelectorLabel:
|
||||
text: "AM"
|
||||
on_release: root.selected = "am"
|
||||
text_color: root.text_color
|
||||
|
||||
AmPmSelectorLabel:
|
||||
text: "PM"
|
||||
on_release: root.selected = "pm"
|
||||
text_color: root.text_color
|
||||
|
||||
|
||||
<TimeInputTextField>
|
||||
size_hint: None, 1
|
||||
width: dp(96)
|
||||
mode: "fill"
|
||||
active_line: False
|
||||
font_size: dp(56)
|
||||
radius: [dp(10), ]
|
||||
fill_color_normal:
|
||||
root.parent.parent.parent.accent_color \
|
||||
if root.parent.parent.parent.accent_color else \
|
||||
( \
|
||||
[*root.parent.bg_color_active[:3], 0.5] \
|
||||
if root.parent.state in ["hour", "minute"] else \
|
||||
[*root.bg_color[:3], 0.5] \
|
||||
)
|
||||
fill_color_focus:
|
||||
(1, 1, 1, 0.5) \
|
||||
if root.parent.parent.parent.primary_color else \
|
||||
self.theme_cls.bg_dark
|
||||
text_color_focus:
|
||||
root.parent.parent.parent.accent_color \
|
||||
if root.parent.parent.parent.accent_color else \
|
||||
self.theme_cls.primary_color
|
||||
|
||||
|
||||
<TimeInput>
|
||||
size_hint: None, None
|
||||
_hour: hour
|
||||
_minute: minute
|
||||
|
||||
TimeInputTextField:
|
||||
id: hour
|
||||
num_type: "hour"
|
||||
pos: 0, 0
|
||||
text_color: root.text_color
|
||||
disabled: root.disabled
|
||||
on_text: root.dispatch("on_time_input")
|
||||
radius: root.hour_radius
|
||||
on_select:
|
||||
root.dispatch("on_hour_select")
|
||||
root.state = "hour"
|
||||
|
||||
MDLabel:
|
||||
text: ":"
|
||||
size_hint: None, None
|
||||
size: dp(24), dp(80)
|
||||
halign: "center"
|
||||
valign: "center"
|
||||
font_size: dp(50)
|
||||
pos: dp(96), 0
|
||||
theme_text_color: "Custom"
|
||||
text_color: root.text_color
|
||||
|
||||
TimeInputTextField:
|
||||
id: minute
|
||||
num_type: "minute"
|
||||
pos: dp(120), 0
|
||||
text_color: root.text_color
|
||||
disabled: root.disabled
|
||||
on_text: root.dispatch("on_time_input")
|
||||
radius: root.minute_radius
|
||||
on_select:
|
||||
root.dispatch("on_minute_select")
|
||||
root.state = "minute"
|
||||
|
||||
|
||||
<CircularSelector>
|
||||
circular_padding: dp(28)
|
||||
size_hint: None, None
|
||||
size: [dp(256), dp(256)]
|
||||
row_spacing: dp(40)
|
||||
|
||||
canvas.before:
|
||||
PushMatrix
|
||||
Scale:
|
||||
origin: self.scale_origin
|
||||
x: root.scale
|
||||
y: root.scale
|
||||
Color:
|
||||
rgba: root.bg_color
|
||||
Ellipse:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
PushMatrix
|
||||
Scale:
|
||||
origin: self.center
|
||||
x: root.content_scale
|
||||
y: root.content_scale
|
||||
Color:
|
||||
rgb: root.selector_color
|
||||
a: 0 if self.selector_pos == [0, 0] else 1
|
||||
Ellipse:
|
||||
size: self.selector_size, self.selector_size
|
||||
pos:
|
||||
[self.selector_pos[0] - self.selector_size / 2, \
|
||||
self.selector_pos[1] - self.selector_size / 2]
|
||||
Ellipse:
|
||||
size: dp(10), dp(10)
|
||||
pos: [self.center[0] - dp(5), self.center[1] - dp(5)]
|
||||
Line:
|
||||
points: [self.center, self.selector_pos]
|
||||
width: dp(1)
|
||||
canvas.after:
|
||||
PopMatrix
|
||||
PopMatrix
|
||||
|
||||
|
||||
<SelectorLabel>
|
||||
halign: "center"
|
||||
valign: "center"
|
||||
adaptive_size: True
|
||||
theme_text_color: "Custom"
|
||||
|
||||
|
||||
<MDTimePicker>
|
||||
auto_dismiss: True
|
||||
size_hint: None, None
|
||||
_time_input: _time_input
|
||||
_selector: _selector
|
||||
_am_pm_selector: _am_pm_selector
|
||||
_minute_label: _minute_label
|
||||
_hour_label: _hour_label
|
||||
|
||||
MDRelativeLayout:
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba:
|
||||
root.primary_color \
|
||||
if root.primary_color \
|
||||
else root.theme_cls.bg_normal
|
||||
|
||||
RoundedRectangle:
|
||||
size: self.size
|
||||
radius: root.radius
|
||||
|
||||
MDLabel:
|
||||
id: label_title
|
||||
font_style: "Body2"
|
||||
bold: True
|
||||
theme_text_color: "Custom"
|
||||
size_hint_x: None
|
||||
width: root.width
|
||||
adaptive_height: True
|
||||
text: root.title
|
||||
font_name: root.font_name
|
||||
pos: (dp(24), root.height - self.height - dp(18))
|
||||
text_color:
|
||||
root.text_toolbar_color if root.text_toolbar_color \
|
||||
else root.theme_cls.text_color
|
||||
|
||||
TimeInput:
|
||||
id: _time_input
|
||||
bg_color:
|
||||
root.accent_color if root.accent_color else \
|
||||
root.theme_cls.primary_light
|
||||
bg_color_active:
|
||||
root.selector_color if root.selector_color \
|
||||
else root.theme_cls.primary_color
|
||||
text_color:
|
||||
root.input_field_text_color if root.input_field_text_color else \
|
||||
root.theme_cls.text_color
|
||||
on_time_input: root._get_time_input(*self.get_time())
|
||||
on_hour_select: _selector.switch_mode("hour")
|
||||
on_minute_select: _selector.switch_mode("minute")
|
||||
minute_radius: root.minute_radius
|
||||
hour_radius: root.hour_radius
|
||||
|
||||
TimeInputLabel:
|
||||
id: _hour_label
|
||||
text: "Hour"
|
||||
opacity: 0
|
||||
text_color:
|
||||
root.text_toolbar_color if root.text_toolbar_color else \
|
||||
root.theme_cls.secondary_text_color
|
||||
|
||||
TimeInputLabel:
|
||||
id: _minute_label
|
||||
text: "Minute"
|
||||
opacity: 0
|
||||
text_color:
|
||||
root.text_toolbar_color if root.text_toolbar_color else \
|
||||
root.theme_cls.secondary_text_color
|
||||
|
||||
AmPmSelector:
|
||||
id: _am_pm_selector
|
||||
owner: root
|
||||
border_color:
|
||||
root.accent_color if root.accent_color else \
|
||||
root.theme_cls.primary_color
|
||||
border_radius: root.am_pm_radius
|
||||
bg_color:
|
||||
root.primary_color if root.primary_color else \
|
||||
root.theme_cls.bg_normal
|
||||
border_width: root.am_pm_border_width
|
||||
bg_color_active:
|
||||
root.selector_color if root.selector_color else \
|
||||
root.theme_cls.primary_light
|
||||
text_color:
|
||||
root.input_field_text_color if root.input_field_text_color else \
|
||||
root.theme_cls.text_color
|
||||
on_selected: root._get_am_pm(self.selected)
|
||||
|
||||
CircularSelector:
|
||||
id: _selector
|
||||
text_color:
|
||||
root.text_color if root.text_color else \
|
||||
root.theme_cls.text_color
|
||||
bg_color:
|
||||
root.accent_color if root.accent_color else \
|
||||
root.theme_cls.primary_light
|
||||
selector_color:
|
||||
root.primary_color if root.primary_color else \
|
||||
root.theme_cls.primary_color
|
||||
font_name: root.font_name
|
||||
on_selector_change: root._get_dial_time(_selector)
|
||||
|
||||
MDIconButton:
|
||||
id: input_clock_switch
|
||||
icon: "keyboard"
|
||||
pos: dp(12), dp(8)
|
||||
theme_icon_color: "Custom"
|
||||
icon_size: "24dp"
|
||||
on_release: root._switch_input()
|
||||
icon_color:
|
||||
root.text_toolbar_color if root.text_toolbar_color else \
|
||||
root.theme_cls.secondary_text_color
|
||||
|
||||
MDFlatButton:
|
||||
id: cancel_button
|
||||
text: "CANCEL"
|
||||
on_release: root.dispatch("on_cancel", None)
|
||||
theme_text_color: "Custom"
|
||||
pos: root.width - self.width - ok_button.width - dp(10), dp(10)
|
||||
font_name: root.font_name
|
||||
text_color:
|
||||
root.theme_cls.primary_color \
|
||||
if not root.text_button_color else root.text_button_color
|
||||
|
||||
MDFlatButton:
|
||||
id: ok_button
|
||||
width: dp(32)
|
||||
pos: root.width - self.width, dp(10)
|
||||
text: "OK"
|
||||
theme_text_color: "Custom"
|
||||
font_name: root.font_name
|
||||
text_color:
|
||||
root.theme_cls.primary_color \
|
||||
if not root.text_button_color else root.text_button_color
|
||||
on_release: root.dispatch("on_save", root._get_data())
|
|
@ -0,0 +1,832 @@
|
|||
"""
|
||||
Components/TimePicker
|
||||
=====================
|
||||
|
||||
.. seealso::
|
||||
|
||||
`Material Design spec, Time picker <https://material.io/components/time-pickers>`_
|
||||
|
||||
.. rubric:: Includes time picker.
|
||||
|
||||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/picker-previous.png
|
||||
:align: center
|
||||
|
||||
.. warning:: The widget is under testing. Therefore, we would be grateful if
|
||||
you would let us know about the bugs found.
|
||||
|
||||
.. rubric:: Usage
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. tab:: Declarative KV style
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kivy.lang import Builder
|
||||
|
||||
from kivymd.app import MDApp
|
||||
from kivymd.uix.pickers import MDTimePicker
|
||||
|
||||
KV = '''
|
||||
MDFloatLayout:
|
||||
|
||||
MDRaisedButton:
|
||||
text: "Open time picker"
|
||||
pos_hint: {'center_x': .5, 'center_y': .5}
|
||||
on_release: app.show_time_picker()
|
||||
'''
|
||||
|
||||
|
||||
class Test(MDApp):
|
||||
def build(self):
|
||||
self.theme_cls.theme_style = "Dark"
|
||||
self.theme_cls.primary_palette = "Orange"
|
||||
return Builder.load_string(KV)
|
||||
|
||||
def show_time_picker(self):
|
||||
'''Open time picker dialog.'''
|
||||
|
||||
time_dialog = MDTimePicker()
|
||||
time_dialog.open()
|
||||
|
||||
|
||||
Test().run()
|
||||
|
||||
.. tab:: Declarative python style
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kivymd.app import MDApp
|
||||
from kivymd.uix.button import MDRaisedButton
|
||||
from kivymd.uix.pickers import MDTimePicker
|
||||
from kivymd.uix.screen import MDScreen
|
||||
|
||||
|
||||
class Test(MDApp):
|
||||
def build(self):
|
||||
self.theme_cls.theme_style = "Dark"
|
||||
self.theme_cls.primary_palette = "Orange"
|
||||
return (
|
||||
MDScreen(
|
||||
MDRaisedButton(
|
||||
text="Open time picker",
|
||||
pos_hint={'center_x': .5, 'center_y': .5},
|
||||
on_release=self.show_time_picker,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def show_time_picker(self, *args):
|
||||
'''Open time picker dialog.'''
|
||||
|
||||
MDTimePicker().open()
|
||||
|
||||
|
||||
Test().run()
|
||||
|
||||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDTimePicker.png
|
||||
:align: center
|
||||
|
||||
Binding method returning set time
|
||||
---------------------------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def show_time_picker(self):
|
||||
time_dialog = MDTimePicker()
|
||||
time_dialog.bind(time=self.get_time)
|
||||
time_dialog.open()
|
||||
|
||||
def get_time(self, instance, time):
|
||||
'''
|
||||
The method returns the set time.
|
||||
|
||||
:type instance: <kivymd.uix.picker.MDTimePicker object>
|
||||
:type time: <class 'datetime.time'>
|
||||
'''
|
||||
|
||||
return time
|
||||
|
||||
Open time dialog with the specified time
|
||||
----------------------------------------
|
||||
|
||||
Use the :attr:`~MDTimePicker.set_time` method of the
|
||||
:class:`~MDTimePicker.` class.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def show_time_picker(self):
|
||||
from datetime import datetime
|
||||
|
||||
# Must be a datetime object
|
||||
previous_time = datetime.strptime("03:20:00", '%H:%M:%S').time()
|
||||
time_dialog = MDTimePicker()
|
||||
time_dialog.set_time(previous_time)
|
||||
time_dialog.open()
|
||||
|
||||
.. note:: For customization of the :class:`~MDTimePicker` class, see the
|
||||
documentation in the :class:`~kivymd.uix.pickers.datepicker.datepicker.BaseDialogPicker` class.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
MDTimePicker(
|
||||
primary_color="brown",
|
||||
accent_color="red",
|
||||
text_button_color="white",
|
||||
).open()
|
||||
|
||||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/time-picker-customization.png
|
||||
:align: center
|
||||
"""
|
||||
|
||||
__all__ = ("MDTimePicker",)
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from typing import List, Union
|
||||
|
||||
from kivy.animation import Animation
|
||||
from kivy.clock import Clock
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.lang import Builder
|
||||
from kivy.metrics import dp
|
||||
from kivy.properties import (
|
||||
BooleanProperty,
|
||||
ColorProperty,
|
||||
ListProperty,
|
||||
NumericProperty,
|
||||
ObjectProperty,
|
||||
OptionProperty,
|
||||
StringProperty,
|
||||
VariableListProperty,
|
||||
)
|
||||
from kivy.uix.behaviors import ButtonBehavior
|
||||
from kivy.vector import Vector
|
||||
|
||||
from kivymd import uix_path
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.circularlayout import MDCircularLayout
|
||||
from kivymd.uix.label import MDLabel
|
||||
from kivymd.uix.pickers.datepicker import BaseDialogPicker
|
||||
from kivymd.uix.relativelayout import MDRelativeLayout
|
||||
from kivymd.uix.textfield import MDTextField
|
||||
|
||||
with open(
|
||||
os.path.join(uix_path, "pickers", "timepicker", "timepicker.kv"),
|
||||
encoding="utf-8",
|
||||
) as kv_file:
|
||||
Builder.load_string(kv_file.read())
|
||||
|
||||
|
||||
class AmPmSelectorLabel(ButtonBehavior, MDLabel):
|
||||
pass
|
||||
|
||||
|
||||
class AmPmSelector(MDBoxLayout):
|
||||
border_radius = NumericProperty()
|
||||
border_color = ColorProperty()
|
||||
bg_color = ColorProperty()
|
||||
bg_color_active = ColorProperty()
|
||||
border_width = NumericProperty()
|
||||
am = ObjectProperty()
|
||||
am = ObjectProperty()
|
||||
owner = ObjectProperty()
|
||||
text_color = ColorProperty()
|
||||
selected = StringProperty()
|
||||
|
||||
_am_bg_color = ColorProperty()
|
||||
_pm_bg_color = ColorProperty()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.bind(selected=self._upadte_color)
|
||||
Clock.schedule_once(self._upadte_color)
|
||||
|
||||
def _upadte_color(self, *args):
|
||||
bg_color = (
|
||||
self.owner.accent_color
|
||||
if self.owner.accent_color
|
||||
else self.bg_color_active
|
||||
)
|
||||
if self.selected == "am":
|
||||
self._am_bg_color = bg_color
|
||||
self._pm_bg_color = (
|
||||
self.owner.primary_color
|
||||
if self.owner.accent_color
|
||||
else self.bg_color
|
||||
)
|
||||
elif self.selected == "pm":
|
||||
self._am_bg_color = (
|
||||
self.owner.primary_color
|
||||
if self.owner.accent_color
|
||||
else self.bg_color
|
||||
)
|
||||
self._pm_bg_color = bg_color
|
||||
|
||||
|
||||
class TimeInputTextField(MDTextField):
|
||||
num_type = OptionProperty("hour", options=["hour", "minute"])
|
||||
hour_regx = "^[0-9]$|^0[1-9]$|^1[0-2]$"
|
||||
minute_regx = "^[0-9]$|^0[0-9]$|^[1-5][0-9]$"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
Clock.schedule_once(self.set_text)
|
||||
self.register_event_type("on_select")
|
||||
self.bind(text_color_focus=self.setter("hint_text_color_normal"))
|
||||
|
||||
def validate_time(self, text) -> Union[None, re.Match]:
|
||||
reg = self.hour_regx if self.num_type == "hour" else self.minute_regx
|
||||
return re.match(reg, text)
|
||||
|
||||
def insert_text(self, text, from_undo=False):
|
||||
strip_text = self.text.strip()
|
||||
current_string = "".join([strip_text, text])
|
||||
if not self.validate_time(current_string):
|
||||
text = ""
|
||||
return super().insert_text(text, from_undo=from_undo)
|
||||
|
||||
def set_text(self, *args) -> None:
|
||||
"""
|
||||
Texts should be center aligned. Now we are setting the padding of text
|
||||
to somehow make them aligned.
|
||||
"""
|
||||
|
||||
def set_text(*args):
|
||||
if not self.text:
|
||||
self.text = " "
|
||||
|
||||
self._refresh_text(self.text)
|
||||
max_size = max(self._lines_rects, key=lambda r: r.size[0]).size
|
||||
dx = (self.width - max_size[0]) / 2.0
|
||||
dy = (self.height - max_size[1]) / 2.0
|
||||
self.padding = [dx, dy, dx, dy]
|
||||
|
||||
if len(self.text) > 1:
|
||||
self.text = self.text.replace(" ", "")
|
||||
|
||||
Clock.schedule_once(set_text)
|
||||
|
||||
def on_focus(self, *args) -> None:
|
||||
super().on_focus(*args)
|
||||
if self.text.strip():
|
||||
if (
|
||||
not self.focus
|
||||
and int(self.text) == 0
|
||||
and self.num_type == "hour"
|
||||
):
|
||||
self.text = "12"
|
||||
else:
|
||||
self.text = " 12" if self.num_type == "hour" else " 00"
|
||||
|
||||
def on_select(self, *args) -> None:
|
||||
pass
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.collide_point(*touch.pos):
|
||||
self.dispatch("on_select")
|
||||
super().on_touch_down(touch)
|
||||
|
||||
|
||||
class TimeInput(MDRelativeLayout):
|
||||
"""Implements two text fields for displaying and entering a time value."""
|
||||
|
||||
bg_color = ColorProperty()
|
||||
bg_color_active = ColorProperty()
|
||||
text_color = ColorProperty()
|
||||
disabled = BooleanProperty(True)
|
||||
minute_radius = ListProperty([0, 0, 0, 0])
|
||||
hour_radius = ListProperty([0, 0, 0, 0])
|
||||
state = StringProperty("hour")
|
||||
|
||||
_hour = ObjectProperty()
|
||||
_minute = ObjectProperty()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.register_event_type("on_time_input")
|
||||
self.register_event_type("on_hour_select")
|
||||
self.register_event_type("on_minute_select")
|
||||
|
||||
def set_time(self, time_list) -> None:
|
||||
hour, minute = time_list
|
||||
self._hour.text = hour
|
||||
self._minute.text = minute
|
||||
|
||||
def get_time(self) -> List[str]:
|
||||
hour = self._hour.text.strip()
|
||||
minute = self._minute.text.strip()
|
||||
return [hour, minute]
|
||||
|
||||
def on_time_input(self, *args) -> None:
|
||||
pass
|
||||
|
||||
def on_minute_select(self, *args) -> None:
|
||||
pass
|
||||
|
||||
def on_hour_select(self, *args) -> None:
|
||||
pass
|
||||
|
||||
def _update_padding(self, *args):
|
||||
self._hour.set_text()
|
||||
self._minute.set_text()
|
||||
|
||||
|
||||
class SelectorLabel(MDLabel):
|
||||
pass
|
||||
|
||||
|
||||
class CircularSelector(MDCircularLayout, EventDispatcher):
|
||||
"""Implements clock face display."""
|
||||
|
||||
mode = OptionProperty("hour", options=["hour", "minute"]) # and military
|
||||
text_color = ColorProperty()
|
||||
selected_hour = StringProperty("12")
|
||||
selected_minute = StringProperty("0")
|
||||
selector_size = NumericProperty("48dp")
|
||||
selector_pos = ListProperty([0, 0])
|
||||
selector_color = ColorProperty()
|
||||
bg_color = ColorProperty()
|
||||
font_name = StringProperty()
|
||||
scale = NumericProperty(1)
|
||||
content_scale = NumericProperty(1)
|
||||
t = StringProperty("out_quad")
|
||||
d = NumericProperty(0.2)
|
||||
scale_origin = ListProperty([100, 100])
|
||||
|
||||
_centers_pos = ListProperty()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.bind(
|
||||
mode=self._update_labels,
|
||||
selected_hour=self.update_time,
|
||||
selected_minute=self.update_time,
|
||||
)
|
||||
Clock.schedule_once(lambda x: self._update_labels(animate=False))
|
||||
self.register_event_type("on_selector_change")
|
||||
|
||||
def do_layout(self, *largs, **kwargs):
|
||||
self.update_time()
|
||||
return super().do_layout(*largs, **kwargs)
|
||||
|
||||
def set_selector(self, selected) -> bool:
|
||||
"""Sets the selector's position towards the given text."""
|
||||
|
||||
widget = None
|
||||
for wid in self.children:
|
||||
wid.text_color = self.text_color
|
||||
if wid.text == selected:
|
||||
widget = wid
|
||||
if not widget:
|
||||
return False
|
||||
self.selector_pos = widget.center
|
||||
widget.text_color = [1, 1, 1, 1]
|
||||
self.dispatch("on_selector_change")
|
||||
return True
|
||||
|
||||
def set_time(self, selected) -> None:
|
||||
if self.mode == "hour":
|
||||
self.selected_hour = selected
|
||||
elif self.mode == "minute":
|
||||
self.selected_minute = selected
|
||||
|
||||
def update_time(self, *args) -> None:
|
||||
if self.mode == "hour":
|
||||
self.set_selector(self.selected_hour)
|
||||
elif self.mode == "minute":
|
||||
self.set_selector(self.selected_minute)
|
||||
|
||||
def get_selected(self) -> str:
|
||||
return self.selected
|
||||
|
||||
def switch_mode(self, mode) -> None:
|
||||
if mode != self.mode:
|
||||
self.mode = mode
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.collide_point(*touch.pos):
|
||||
touch.grab(self)
|
||||
closest_wid = self._get_closest_widget(touch.pos)
|
||||
self.set_time(closest_wid.text)
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if touch.grab_current == self:
|
||||
closest_wid = self._get_closest_widget(touch.pos)
|
||||
self.set_time(closest_wid.text)
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.grab_current is self:
|
||||
touch.ungrab(self)
|
||||
return True
|
||||
|
||||
def on_selector_change(self, *args):
|
||||
pass
|
||||
|
||||
def _update_labels(self, animate=True, *args):
|
||||
"""
|
||||
This method builds the selector based on current mode which currently
|
||||
can be hour or minute.
|
||||
"""
|
||||
|
||||
if self.mode == "hour":
|
||||
param = (1, 12)
|
||||
self.degree_spacing = 30
|
||||
self.start_from = 60
|
||||
elif self.mode == "minute":
|
||||
param = (0, 59, 5)
|
||||
self.degree_spacing = 6
|
||||
self.start_from = 90
|
||||
elif self.mode == "military":
|
||||
param = (1, 24)
|
||||
self.degree_spacing = 30
|
||||
self.start_from = 90
|
||||
if animate:
|
||||
anim = Animation(content_scale=0, t=self.t, d=self.d)
|
||||
anim.bind(on_complete=lambda *args: self._add_items(*param))
|
||||
anim.start(self)
|
||||
else:
|
||||
self._add_items(*param)
|
||||
|
||||
def _add_items(self, start, end, step=1):
|
||||
"""
|
||||
Adds all number in range `[start, end + 1]` to the circular layout with
|
||||
the specified step. Step means that all widgets will be added to layout
|
||||
but sets the opacity for skipped widgets to `0` because we are using
|
||||
the label's text as a reference to the selected number so we have to
|
||||
add these to layout.
|
||||
"""
|
||||
|
||||
self.clear_widgets()
|
||||
i = 0
|
||||
for x in range(start, end + 1):
|
||||
label = SelectorLabel(
|
||||
text=f"{x}",
|
||||
)
|
||||
if i % step != 0:
|
||||
label.opacity = 0
|
||||
self.bind(
|
||||
text_color=label.setter("text_color"),
|
||||
font_name=label.setter("font_name"),
|
||||
)
|
||||
self.add_widget(label)
|
||||
i += 1
|
||||
Clock.schedule_once(self.update_time)
|
||||
Clock.schedule_once(self._get_centers, 0.1)
|
||||
anim = Animation(content_scale=1, t=self.t, d=self.d)
|
||||
anim.start(self)
|
||||
|
||||
def _get_centers(self, *args):
|
||||
"""
|
||||
Returns a list of all center. we use this for positioning the selector
|
||||
indicator.
|
||||
"""
|
||||
|
||||
self._centers_pos = []
|
||||
for child in self.children:
|
||||
self._centers_pos.append(child.center)
|
||||
|
||||
def _get_closest_widget(self, pos):
|
||||
"""
|
||||
Returns the nearest widget to the given position. we use this to create
|
||||
the magnetic effect.
|
||||
"""
|
||||
|
||||
distance = [Vector(pos).distance(point) for point in self._centers_pos]
|
||||
if not distance:
|
||||
return False
|
||||
index = distance.index(min(distance))
|
||||
return self.children[index]
|
||||
|
||||
|
||||
class MDTimePicker(BaseDialogPicker):
|
||||
hour = StringProperty("12")
|
||||
"""
|
||||
Current hour.
|
||||
|
||||
:attr:`hour` is an :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `'12'`.
|
||||
"""
|
||||
|
||||
minute = StringProperty("0")
|
||||
"""
|
||||
Current minute.
|
||||
|
||||
:attr:`minute` is an :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `0`.
|
||||
"""
|
||||
|
||||
minute_radius = VariableListProperty(dp(5), length=4)
|
||||
"""
|
||||
Radius of the minute input field.
|
||||
|
||||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/time-picker-minute-radius.png
|
||||
:align: center
|
||||
|
||||
:attr:`minute_radius` is an :class:`~kivy.properties.ListProperty`
|
||||
and defaults to `[dp(5), dp(5), dp(5), dp(5)]`.
|
||||
"""
|
||||
|
||||
hour_radius = VariableListProperty(dp(5), length=4)
|
||||
"""
|
||||
Radius of the hour input field.
|
||||
|
||||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/time-picker-hour-radius.png
|
||||
:align: center
|
||||
|
||||
:attr:`hour_radius` is an :class:`~kivy.properties.ListProperty`
|
||||
and defaults to `[dp(5), dp(5), dp(5), dp(5)]`.
|
||||
"""
|
||||
|
||||
am_pm_radius = NumericProperty("5dp")
|
||||
"""
|
||||
Radius of the AM/PM selector.
|
||||
|
||||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/time-picker-am-pm-radius.png
|
||||
:align: center
|
||||
|
||||
:attr:`am_pm_radius` is an :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `dp(5)`.
|
||||
"""
|
||||
|
||||
am_pm_border_width = NumericProperty("1dp")
|
||||
"""
|
||||
Width of the AM/PM selector's borders.
|
||||
|
||||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/time-picker-am-pm-border-width.png
|
||||
:align: center
|
||||
|
||||
:attr:`am_pm_border_width` is an :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `dp(1)`.
|
||||
"""
|
||||
|
||||
am_pm = OptionProperty("am", options=["am", "pm"])
|
||||
"""
|
||||
Current AM/PM mode.
|
||||
|
||||
:attr:`am_pm` is an :class:`~kivy.properties.OptionProperty`
|
||||
and defaults to `'am'`.
|
||||
"""
|
||||
|
||||
animation_duration = NumericProperty(0.3)
|
||||
"""
|
||||
Duration of the animations.
|
||||
|
||||
:attr:`animation_duration` is an :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `0.2`.
|
||||
"""
|
||||
|
||||
animation_transition = StringProperty("out_quad")
|
||||
"""
|
||||
Transition type of the animations.
|
||||
|
||||
:attr:`animation_transition` is an :class:`~kivy.properties.StringProperty`
|
||||
and defaults to `'out_quad'`.
|
||||
"""
|
||||
|
||||
time = ObjectProperty(allownone=True)
|
||||
"""
|
||||
Returns the current time object.
|
||||
|
||||
:attr:`time` is an :class:`~kivy.properties.ObjectProperty`
|
||||
and defaults to `None`.
|
||||
"""
|
||||
|
||||
_state = StringProperty()
|
||||
_selector = ObjectProperty()
|
||||
_time_input = ObjectProperty()
|
||||
_am_pm_selector = ObjectProperty()
|
||||
_hour_label = ObjectProperty()
|
||||
_minute_label = ObjectProperty()
|
||||
_anim_playing = BooleanProperty(False)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.bind(
|
||||
hour=self._set_current_time,
|
||||
minute=self._set_current_time,
|
||||
am_pm=self._set_current_time,
|
||||
)
|
||||
self.theme_cls.bind(device_orientation=self._check_orienation)
|
||||
if self.title == "SELECT DATE":
|
||||
self.title = "SELECT TIME"
|
||||
self.set_time(datetime.time(hour=12, minute=0)) # default time
|
||||
self._check_orienation()
|
||||
|
||||
def set_time(self, time_obj) -> None:
|
||||
"""Manually set time dialog with the specified time."""
|
||||
|
||||
hour = time_obj.hour
|
||||
minute = time_obj.minute
|
||||
if hour > 12:
|
||||
hour -= 12
|
||||
mode = "pm"
|
||||
else:
|
||||
mode = "am"
|
||||
hour = str(hour)
|
||||
minute = str(minute)
|
||||
self._set_time_input(hour, minute)
|
||||
self._set_dial_time(hour, minute)
|
||||
self._set_am_pm(mode)
|
||||
|
||||
def get_state(self) -> str:
|
||||
"""
|
||||
Returns the current state of TimePicker.
|
||||
Can be one of `portrait`, `landscape` or `input`.
|
||||
"""
|
||||
|
||||
return self._state
|
||||
|
||||
def _get_dial_time(self, instance):
|
||||
mode = instance.mode
|
||||
if mode == "hour":
|
||||
self.hour = instance.selected_hour
|
||||
elif mode == "minute":
|
||||
self.minute = instance.selected_minute
|
||||
else:
|
||||
raise Exception("invalid mode for MDTimePicker: " % mode)
|
||||
self._set_time_input(self.hour, self.minute)
|
||||
|
||||
def _set_dial_time(self, hour, minute):
|
||||
self._selector.selected_minute = minute
|
||||
self._selector.selected_hour = hour
|
||||
|
||||
def _get_time_input(self, hour, minute):
|
||||
if hour:
|
||||
self.hour = f"{int(hour):01d}"
|
||||
if minute:
|
||||
self.minute = f"{int(minute):01d}"
|
||||
self._set_dial_time(self.hour, self.minute)
|
||||
|
||||
def _set_time_input(self, hour, minute):
|
||||
hour = f"{int(hour):02d}"
|
||||
minute = f"{int(minute):02d}"
|
||||
if self._state != "input":
|
||||
self._time_input.set_time([hour, minute])
|
||||
|
||||
def _get_am_pm(self, selected):
|
||||
self.am_pm = selected
|
||||
|
||||
def _set_am_pm(self, selected: str) -> None:
|
||||
"""Used by set_time() to manually set the mode to "am" or "pm"."""
|
||||
self.am_pm = selected
|
||||
self._am_pm_selector.mode = self.am_pm
|
||||
self._am_pm_selector.selected = self.am_pm
|
||||
|
||||
def _get_data(self):
|
||||
try:
|
||||
if time.strftime("%p"):
|
||||
result = datetime.datetime.strptime(
|
||||
f"{int(self.hour):02d}:{int(self.minute):02d} {self.am_pm}",
|
||||
"%I:%M %p",
|
||||
).time()
|
||||
else:
|
||||
result = datetime.datetime.strptime(
|
||||
f"{int(self.hour):02d}:{int(self.minute):02d}",
|
||||
"%I:%M",
|
||||
).time()
|
||||
return result
|
||||
except ValueError:
|
||||
return None # hour is zero
|
||||
|
||||
def _check_orienation(self, *args, do_anim=False):
|
||||
orientation = self.theme_cls.device_orientation
|
||||
if self._state != "input" and orientation != self._state:
|
||||
self._update_pos_size(orientation, anim=do_anim)
|
||||
|
||||
def _update_pos_size(self, orientation, anim=False):
|
||||
d = self.animation_duration
|
||||
# time input
|
||||
time_input_pos = (
|
||||
[dp(24), dp(368)]
|
||||
if orientation == "portrait"
|
||||
else (
|
||||
[dp(24), dp(178)]
|
||||
if orientation == "landscape"
|
||||
else [dp(24), dp(96)]
|
||||
)
|
||||
)
|
||||
if anim:
|
||||
_time_input = Animation(
|
||||
pos=time_input_pos,
|
||||
d=d,
|
||||
t=self.animation_transition, # 80 - 8,
|
||||
)
|
||||
_time_input.start(self._time_input)
|
||||
else:
|
||||
self._time_input.pos = time_input_pos
|
||||
|
||||
self._time_input.disabled = False if orientation == "input" else True
|
||||
self._time_input.size = (
|
||||
[dp(216), dp(62)] if orientation == "input" else [dp(216), dp(72)]
|
||||
)
|
||||
Clock.schedule_once(self._time_input._update_padding)
|
||||
|
||||
# Circular selector.
|
||||
if orientation == "input":
|
||||
if self.theme_cls.device_orientation == "portrait":
|
||||
selector_pos = [dp(34), dp(-256)]
|
||||
self._selector.scale_origin = [dp(162), dp(200)]
|
||||
else:
|
||||
selector_pos = [dp(324), dp(-19)]
|
||||
self._selector.scale_origin = [dp(292), dp(109)]
|
||||
elif orientation == "portrait":
|
||||
self._selector.pos = selector_pos = [dp(36), dp(76)]
|
||||
else:
|
||||
self._selector.pos = selector_pos = [dp(304), dp(76)]
|
||||
|
||||
Animation(
|
||||
pos=selector_pos,
|
||||
scale=0 if orientation == "input" else 1,
|
||||
opacity=0 if orientation == "input" else 1,
|
||||
d=d,
|
||||
t=self.animation_transition,
|
||||
).start(self._selector)
|
||||
|
||||
# AM/PM selector.
|
||||
am_pm_pos = (
|
||||
[dp(252), dp(368)]
|
||||
if orientation == "portrait"
|
||||
else (
|
||||
[dp(24), dp(126)]
|
||||
if orientation == "landscape"
|
||||
else [dp(252), dp(96)]
|
||||
)
|
||||
)
|
||||
am_pm_size = (
|
||||
[dp(52), dp(80)]
|
||||
if orientation == "portrait"
|
||||
else (
|
||||
[dp(216), dp(40)]
|
||||
if orientation == "landscape"
|
||||
else [dp(48), dp(70)]
|
||||
)
|
||||
)
|
||||
if anim:
|
||||
Animation(
|
||||
pos=am_pm_pos,
|
||||
size=am_pm_size,
|
||||
d=d,
|
||||
t=self.animation_transition,
|
||||
).start(self._am_pm_selector)
|
||||
else:
|
||||
self._am_pm_selector.pos = am_pm_pos
|
||||
self._am_pm_selector.size = am_pm_size
|
||||
|
||||
self._am_pm_selector.orientation = (
|
||||
"horizontal" if orientation == "landscape" else "vertical"
|
||||
)
|
||||
|
||||
# MDTimePicker.
|
||||
time_picker_size = (
|
||||
[dp(328), dp(500)]
|
||||
if orientation == "portrait"
|
||||
else (
|
||||
[dp(584), dp(368)]
|
||||
if orientation == "landscape"
|
||||
else [dp(324), dp(218)]
|
||||
)
|
||||
)
|
||||
if anim:
|
||||
Animation(
|
||||
size=time_picker_size,
|
||||
d=d,
|
||||
t=self.animation_transition,
|
||||
).start(self)
|
||||
else:
|
||||
self.size = time_picker_size
|
||||
|
||||
# Minute label.
|
||||
Animation(
|
||||
pos=[dp(144), dp(76)],
|
||||
opacity=1 if orientation == "input" else 0,
|
||||
d=d,
|
||||
t=self.animation_transition,
|
||||
).start(self._minute_label)
|
||||
|
||||
# Hour label.
|
||||
Animation(
|
||||
pos=[dp(24), dp(76)],
|
||||
opacity=1 if orientation == "input" else 0,
|
||||
d=d,
|
||||
t=self.animation_transition,
|
||||
).start(self._hour_label)
|
||||
|
||||
self._state = orientation
|
||||
self.ids.input_clock_switch.icon = (
|
||||
"clock-time-four-outline" if orientation == "input" else "keyboard"
|
||||
)
|
||||
|
||||
def _set_current_time(self, *args):
|
||||
self.time = self._get_data()
|
||||
|
||||
def _switch_input(self):
|
||||
self._update_pos_size(
|
||||
self.theme_cls.device_orientation
|
||||
if self._state == "input"
|
||||
else "input",
|
||||
anim=True,
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue