Sideband/sbapp/kivymd/uix/pickers/datepicker/datepicker.py

1445 lines
49 KiB
Python
Raw Normal View History

2022-07-07 14:16:10 -06:00
"""
Components/DatePicker
=====================
.. seealso::
`Material Design spec, Date picker <https://material.io/components/date-pickers>`_
.. rubric:: Includes date picker.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/picker-previous.png
:align: center
.. rubric:: Usage
2022-07-07 14:16:10 -06:00
2022-10-08 09:17:59 -06:00
.. tabs::
2022-07-07 14:16:10 -06:00
2022-10-08 09:17:59 -06:00
.. tab:: Declarative KV style
2022-07-07 14:16:10 -06:00
2022-10-08 09:17:59 -06:00
.. code-block:: python
2022-07-07 14:16:10 -06:00
2022-10-08 09:17:59 -06:00
from kivy.lang import Builder
2022-10-02 09:16:59 -06:00
2022-10-08 09:17:59 -06:00
from kivymd.app import MDApp
from kivymd.uix.pickers import MDDatePicker
2022-10-02 09:16:59 -06:00
2022-10-08 09:17:59 -06:00
KV = '''
MDFloatLayout:
2022-10-02 09:16:59 -06:00
2022-10-08 09:17:59 -06:00
MDRaisedButton:
text: "Open date picker"
pos_hint: {'center_x': .5, 'center_y': .5}
on_release: app.show_date_picker()
'''
2022-10-02 09:16:59 -06:00
2022-10-08 09:17:59 -06:00
class Test(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Orange"
return Builder.load_string(KV)
2022-10-02 09:16:59 -06:00
2022-10-08 09:17:59 -06:00
def on_save(self, instance, value, date_range):
'''
Events called when the "OK" dialog box button is clicked.
2022-10-02 09:16:59 -06:00
2022-10-08 09:17:59 -06:00
:type instance: <kivymd.uix.picker.MDDatePicker object>;
:param value: selected date;
:type value: <class 'datetime.date'>;
:param date_range: list of 'datetime.date' objects in the selected range;
:type date_range: <class 'list'>;
'''
2022-10-02 09:16:59 -06:00
2022-10-08 09:17:59 -06:00
print(instance, value, date_range)
2022-10-02 09:16:59 -06:00
2022-10-08 09:17:59 -06:00
def on_cancel(self, instance, value):
'''Events called when the "CANCEL" dialog box button is clicked.'''
2022-10-02 09:16:59 -06:00
2022-10-08 09:17:59 -06:00
def show_date_picker(self):
date_dialog = MDDatePicker()
date_dialog.bind(on_save=self.on_save, on_cancel=self.on_cancel)
date_dialog.open()
2022-07-07 14:16:10 -06:00
2022-10-08 09:17:59 -06:00
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 MDDatePicker
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 data picker",
pos_hint={'center_x': .5, 'center_y': .5},
on_release=self.show_date_picker,
)
)
)
def on_save(self, instance, value, date_range):
'''
Events called when the "OK" dialog box button is clicked.
:type instance: <kivymd.uix.picker.MDDatePicker object>;
:param value: selected date;
:type value: <class 'datetime.date'>;
:param date_range: list of 'datetime.date' objects in the selected range;
:type date_range: <class 'list'>;
'''
print(instance, value, date_range)
def on_cancel(self, instance, value):
'''Events called when the "CANCEL" dialog box button is clicked.'''
def show_date_picker(self, *args):
date_dialog = MDDatePicker()
date_dialog.bind(on_save=self.on_save, on_cancel=self.on_cancel)
date_dialog.open()
2022-07-07 14:16:10 -06:00
2022-10-02 09:16:59 -06:00
2022-10-08 09:17:59 -06:00
Test().run()
2022-10-02 09:16:59 -06:00
2022-10-08 09:17:59 -06:00
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDDatePicker.png
2022-07-07 14:16:10 -06:00
:align: center
Open date dialog with the specified date
----------------------------------------
.. code-block:: python
def show_date_picker(self):
date_dialog = MDDatePicker(year=1983, month=4, day=12)
date_dialog.open()
2022-10-08 09:17:59 -06:00
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/specified-date.png
2022-07-07 14:16:10 -06:00
:align: center
Interval date
-------------
You can set the time interval from and to the set date. All days of the week
that are not included in this range will have the status `disabled`.
.. code-block:: python
def show_date_picker(self):
date_dialog = MDDatePicker(
2022-10-08 09:17:59 -06:00
min_date=datetime.date.today(),
max_date=datetime.date(
datetime.date.today().year,
datetime.date.today().month,
datetime.date.today().day + 2,
),
2022-07-07 14:16:10 -06:00
)
date_dialog.open()
2022-10-08 09:17:59 -06:00
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/range-date.png
2022-07-07 14:16:10 -06:00
:align: center
The range of available dates can be changed in the picker dialog:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/change-range-date.gif
:align: center
Select year
-----------
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/select-year-date.gif
:align: center
.. warning:: The list of years when opening is not automatically set
to the current year.
You can set the range of years using the :attr:`~kivymd.uix.picker.MDDatePicker.min_year` and
:attr:`~kivymd.uix.picker.MDDatePicker.max_year` attributes:
.. code-block:: python
def show_date_picker(self):
2022-10-08 09:17:59 -06:00
date_dialog = MDDatePicker(min_year=2022, max_year=2030)
2022-07-07 14:16:10 -06:00
date_dialog.open()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/min-max-year-date.png
:align: center
Set and select a date range
---------------------------
.. code-block:: python
def show_date_picker(self):
date_dialog = MDDatePicker(mode="range")
date_dialog.open()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/set-select-range-date.gif
:align: center
"""
2022-10-08 09:17:59 -06:00
from __future__ import annotations
2022-07-07 14:16:10 -06:00
__all__ = ("MDDatePicker", "BaseDialogPicker", "DatePickerInputField")
import calendar
import datetime
2022-10-08 09:17:59 -06:00
import math
2022-07-07 14:16:10 -06:00
import os
import time
from datetime import date
2022-10-08 09:17:59 -06:00
from itertools import zip_longest
2022-07-07 14:16:10 -06:00
from typing import Union
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
BooleanProperty,
ColorProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.behaviors import ButtonBehavior, FocusBehavior
from kivy.uix.recyclegridlayout import RecycleGridLayout
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivymd import uix_path
from kivymd.theming import ThemableBehavior, ThemeManager
from kivymd.toast import toast
from kivymd.uix.behaviors import (
CircularRippleBehavior,
2022-10-08 09:17:59 -06:00
CommonElevationBehavior,
2022-07-07 14:16:10 -06:00
SpecificBackgroundColorBehavior,
)
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDIconButton
from kivymd.uix.dialog import BaseDialog
from kivymd.uix.label import MDLabel
from kivymd.uix.textfield import MDTextField
from kivymd.uix.tooltip import MDTooltip
with open(
os.path.join(uix_path, "pickers", "datepicker", "datepicker.kv"),
encoding="utf-8",
) as kv_file:
Builder.load_string(kv_file.read())
class BaseDialogPicker(
BaseDialog,
2022-10-08 09:17:59 -06:00
CommonElevationBehavior,
2022-07-07 14:16:10 -06:00
SpecificBackgroundColorBehavior,
):
"""
Base class for :class:`~kivymd.uix.picker.MDDatePicker` and
:class:`~kivymd.uix.picker.MDTimePicker` classes.
2023-07-09 18:49:58 -06:00
For more information, see in the
:class:`~kivymd.uix.dialog.BaseDialog` and
:class:`~kivymd.uix.behaviors.CommonElevationBehavior` and
:class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior`
classes documentation.
2022-07-07 14:16:10 -06:00
: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)
"""
2022-10-08 09:17:59 -06:00
Background color of toolbar in (r, g, b, a) or string format.
2022-07-07 14:16:10 -06:00
.. code-block:: python
2022-10-08 09:17:59 -06:00
MDDatePicker(primary_color="brown")
2022-07-07 14:16:10 -06:00
.. 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)
"""
2022-10-08 09:17:59 -06:00
Background color of calendar/clock face in (r, g, b, a) or string format.
2022-07-07 14:16:10 -06:00
.. code-block:: python
MDDatePicker(
2022-10-08 09:17:59 -06:00
primary_color="brown",
accent_color="darkred",
2022-07-07 14:16:10 -06:00
)
.. 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)
"""
2022-10-08 09:17:59 -06:00
Background color of the selected day of the month or hour in (r, g, b, a)
or string format.
2022-07-07 14:16:10 -06:00
.. code-block:: python
MDDatePicker(
2022-10-08 09:17:59 -06:00
primary_color="brown",
accent_color="darkred",
selector_color="red",
2022-07-07 14:16:10 -06:00
)
.. 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)
"""
2022-10-08 09:17:59 -06:00
Color of labels for text on a toolbar in (r, g, b, a) or string format.
2022-07-07 14:16:10 -06:00
.. code-block:: python
MDDatePicker(
2022-10-08 09:17:59 -06:00
primary_color="brown",
accent_color="darkred",
selector_color="red",
text_toolbar_color="lightgrey",
2022-07-07 14:16:10 -06:00
)
.. 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)
"""
2022-10-08 09:17:59 -06:00
Color of text labels in calendar/clock face in (r, g, b, a) or string format.
2022-07-07 14:16:10 -06:00
.. code-block:: python
MDDatePicker(
2022-10-08 09:17:59 -06:00
primary_color="brown",
accent_color="darkred",
selector_color="red",
text_toolbar_color="lightgrey",
text_color="orange",
2022-07-07 14:16:10 -06:00
)
.. 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)
"""
2022-10-08 09:17:59 -06:00
Color of the text of the current day of the month/hour in (r, g, b, a)
or string format.
2022-07-07 14:16:10 -06:00
.. code-block:: python
MDDatePicker(
2022-10-08 09:17:59 -06:00
primary_color="brown",
accent_color="darkred",
selector_color="red",
text_toolbar_color="lightgrey",
text_color="orange",
text_current_color="white",
2022-07-07 14:16:10 -06:00
)
.. 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(
2022-10-08 09:17:59 -06:00
primary_color="brown",
accent_color="darkred",
selector_color="red",
text_toolbar_color="lightgrey",
text_color="orange",
text_current_color="white",
text_button_color="lightgrey",
2022-07-07 14:16:10 -06:00
)
.. 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`.
"""
2022-10-08 09:17:59 -06:00
input_field_background_color_normal = ColorProperty(None)
2022-07-07 14:16:10 -06:00
"""
2022-10-08 09:17:59 -06:00
Background color normal of input fields in (r, g, b, a) or string format.
.. versionadded:: 1.1.0
2022-07-07 14:16:10 -06:00
.. code-block:: python
MDDatePicker(
2022-10-08 09:17:59 -06:00
primary_color="brown",
accent_color="darkred",
selector_color="red",
text_toolbar_color="lightgrey",
text_color="orange",
text_current_color="white",
text_button_color="lightgrey",
input_field_background_color_normal="coral",
2022-07-07 14:16:10 -06:00
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/input-field-background-color-date.png
:align: center
2022-10-08 09:17:59 -06:00
:attr:`input_field_background_color_normal` is an :class:`~kivy.properties.ColorProperty`
2022-07-07 14:16:10 -06:00
and defaults to `None`.
"""
2022-10-08 09:17:59 -06:00
input_field_background_color_focus = ColorProperty(None)
"""
Background color normal of input fields in (r, g, b, a) or string format.
.. versionadded:: 1.1.0
.. code-block:: python
MDDatePicker(
primary_color="brown",
accent_color="darkred",
selector_color="red",
text_toolbar_color="lightgrey",
text_color="orange",
text_current_color="white",
text_button_color="lightgrey",
input_field_background_color_normal="coral",
input_field_background_color_focus="red",
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/input-field-background-color-focus-date.png
:align: center
:attr:`input_field_background_color_focus` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
input_field_background_color = ColorProperty(None)
"""
.. deprecated:: 1.1.0
Use :attr:`input_field_background_color_normal` instead.
"""
2022-07-07 14:16:10 -06:00
input_field_text_color = ColorProperty(None)
"""
2022-10-08 09:17:59 -06:00
.. deprecated:: 1.1.0
Use :attr:`input_field_text_color_normal` instead.
"""
2022-10-02 09:16:59 -06:00
2022-10-08 09:17:59 -06:00
input_field_text_color_normal = ColorProperty(None)
"""
Text color normal of input fields in (r, g, b, a) or string format.
.. versionadded:: 1.1.0
2022-10-02 09:16:59 -06:00
.. code-block:: python
MDDatePicker(
2022-10-08 09:17:59 -06:00
primary_color="brown",
accent_color="darkred",
selector_color="red",
text_toolbar_color="lightgrey",
text_color="orange",
text_current_color="white",
text_button_color="lightgrey",
input_field_background_color_normal="brown",
input_field_background_color_focus="red",
input_field_text_color_normal="white",
2022-10-02 09:16:59 -06:00
)
2022-10-08 09:17:59 -06:00
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/input-field-text-color-normal-date.png
2022-10-02 09:16:59 -06:00
:align: center
2022-10-08 09:17:59 -06:00
:attr:`input_field_text_color_normal` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
input_field_text_color_focus = ColorProperty(None)
"""
Text color focus of input fields in (r, g, b, a) or string format.
.. versionadded:: 1.1.0
.. code-block:: python
MDDatePicker(
primary_color="brown",
accent_color="darkred",
selector_color="red",
text_toolbar_color="lightgrey",
text_color="orange",
text_current_color="white",
text_button_color="lightgrey",
input_field_background_color_normal="brown",
input_field_background_color_focus="red",
input_field_text_color_normal="white",
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/input-field-text-color-normal-date.png
:align: center
:attr:`input_field_text_color_focus` is an :class:`~kivy.properties.ColorProperty`
2022-07-07 14:16:10 -06:00
and defaults to `None`.
"""
font_name = StringProperty("Roboto")
"""
Font name for dialog window text.
.. code-block:: python
MDDatePicker(
2022-10-08 09:17:59 -06:00
primary_color="brown",
accent_color="darkred",
selector_color="red",
text_toolbar_color="lightgrey",
text_color="orange",
text_current_color="white",
text_button_color="lightgrey",
input_field_background_color_normal="brown",
input_field_background_color_focus="red",
input_field_text_color_normal="white",
input_field_text_color_focus="lightgrey",
font_name="nasalization.ttf",
2022-07-07 14:16:10 -06:00
)
.. 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")
2022-10-08 09:17:59 -06:00
def on_input_field_background_color(
self, instance, value: str | list | tuple
) -> None:
"""For supported of current API."""
self.input_field_background_color_normal = value
def on_input_field_text_color(
self, instance, value: str | list | tuple
) -> None:
"""For supported of current API."""
self.input_field_text_color_normal = value
2022-07-07 14:16:10 -06:00
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 DatePickerBaseTooltip(MDTooltip):
"""Implements tooltips for members of the :class:`~MDDatePicker` class."""
owner = ObjectProperty() # MDDatePicker object
hint_text = StringProperty()
class DatePickerIconTooltipButton(MDIconButton, DatePickerBaseTooltip):
pass
class DatePickerWeekdayLabel(MDLabel, DatePickerBaseTooltip):
pass
class DatePickerTypeDateError(Exception):
pass
class DatePickerInputField(MDTextField):
2023-07-09 18:49:58 -06:00
"""
Implements date input in dd/mm/yyyy format.
For more information, see in the
:class:`~kivymd.uix.textfield.MDTextField` class documentation.
"""
2022-07-07 14:16:10 -06:00
helper_text_mode = StringProperty("on_error")
owner = ObjectProperty() # MDDatePicker object
2023-07-09 18:49:58 -06:00
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.bind(text=self._on_text_check_errors)
def _on_text_check_errors(self, widget, text):
if text == "":
self.error = False
return
try:
datetime.datetime.strptime(text, "%d/%m/%Y")
self.error = False
except ValueError:
self.error = True
2022-07-07 14:16:10 -06:00
def set_error(self):
"""Sets a text field to an error state."""
self.error = True
def input_filter(self, value: str, boolean: bool) -> Union[str, None]:
"""Filters the input according to the specified mode."""
if self.is_numeric(value):
return value
def is_numeric(self, value: str) -> bool:
"""
Returns true if the value of the `value` argument can be converted
to an integer, or if the value of the `value` argument is '/'.
"""
try:
if value == "/":
return True
int(value)
return True
except ValueError:
return False
def get_list_date(self) -> list:
"""
Returns a list as `[dd, mm, yyyy]` from a text fied for entering a date.
"""
return [d for d in self.text.split("/") if d]
class DatePickerInputFieldContainer(MDBoxLayout):
owner = ObjectProperty() # MDDatePicker object
class SelectYearList(FocusBehavior, LayoutSelectionBehavior, RecycleGridLayout):
"""A class that implements a list for choosing a year."""
class DatePickerDaySelectableItem(
ThemableBehavior, CircularRippleBehavior, ButtonBehavior, AnchorLayout
):
"""A class that implements a list for choosing a day."""
text = StringProperty()
owner = ObjectProperty()
is_today = BooleanProperty(False)
is_selected = BooleanProperty(False)
2023-07-09 18:49:58 -06:00
is_in_range = BooleanProperty(False)
is_range_start = BooleanProperty(False)
is_range_end = BooleanProperty(False)
is_month_end = BooleanProperty(False)
is_week_end = BooleanProperty(False)
2022-07-07 14:16:10 -06:00
def on_release(self):
2023-07-09 18:49:58 -06:00
self.owner.set_selected_widget(self)
2022-07-07 14:16:10 -06:00
2022-10-08 09:17:59 -06:00
def on_touch_down(self, touch):
# If year_layout is active don't dispatch on_touch_down events,
# so date items don't consume touch.
if not self.owner.ids._year_layout.disabled:
return
super().on_touch_down(touch)
2022-07-07 14:16:10 -06:00
class DatePickerYearSelectableItem(RecycleDataViewBehavior, MDLabel):
"""Implements an item for a pick list of the year."""
index = None
2023-07-09 18:49:58 -06:00
selected = BooleanProperty(False)
2022-07-07 14:16:10 -06:00
owner = ObjectProperty()
def refresh_view_attrs(self, rv, index, data):
self.index = index
return super().refresh_view_attrs(rv, index, data)
def on_touch_down(self, touch):
if super().on_touch_down(touch):
return True
2022-10-08 09:17:59 -06:00
if self.collide_point(*touch.pos):
2022-07-07 14:16:10 -06:00
self.owner.year = int(self.text)
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, table_data, index, is_selected):
2023-07-09 18:49:58 -06:00
self.selected = is_selected
2022-07-07 14:16:10 -06:00
# TODO: Add the feature to embed the `MDDatePicker` class in other layouts
# and not use it as a modal dialog.
# Add a date input mask. Currently, the date is entered in the format
# 'dd/mm/yy'. In some countries, the date is formatted as 'mm/dd/yy'.
class MDDatePicker(BaseDialogPicker):
text_weekday_color = ColorProperty(None)
"""
2022-10-08 09:17:59 -06:00
Text color of weekday names in (r, g, b, a) or string format.
2022-07-07 14:16:10 -06:00
.. 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`.
"""
2023-07-09 18:49:58 -06:00
min_date = ObjectProperty(allownone=True)
2022-07-07 14:16:10 -06:00
"""
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`.
"""
2023-07-09 18:49:58 -06:00
max_date = ObjectProperty(allownone=True)
2022-07-07 14:16:10 -06:00
"""
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
2023-07-09 18:49:58 -06:00
_fields_container = None
2022-07-07 14:16:10 -06:00
_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
2023-07-09 18:49:58 -06:00
_date_label_text = StringProperty()
2022-07-07 14:16:10 -06:00
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
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.generate_list_widgets_days()
self.update_calendar(self.sel_year, self.sel_month)
def on_device_orientation(
self, instance_theme_manager: ThemeManager, orientation_value: str
) -> None:
"""Called when the device's screen orientation changes."""
2023-07-09 18:49:58 -06:00
# Separators of the label text depend on the orientation.
self._update_date_label_text()
2022-07-07 14:16:10 -06:00
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.
"""
2023-07-09 18:49:58 -06:00
if self._input_date_dialog_open and not self._try_apply_input():
2022-07-07 14:16:10 -06:00
return
self.dispatch(
"on_save",
date(self.sel_year, self.sel_month, self.sel_day),
2023-07-09 18:49:58 -06:00
self.get_date_range(),
2022-07-07 14:16:10 -06:00
)
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)
2022-10-08 09:17:59 -06:00
Animation(_scale_calendar_layout=1, d=0.15).start(self)
2022-07-07 14:16:10 -06:00
2022-10-08 09:17:59 -06:00
# Move selection to the same day and month of the selected year.
self.sel_year = self.year
last_day = calendar.monthrange(self.year, self.sel_month)[1]
self.sel_day = min(self.sel_day, last_day)
2022-07-07 14:16:10 -06:00
self.update_calendar(self.year, self.month)
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
Animation(opacity=0, d=0.15).start(self.ids.chevron_left)
Animation(opacity=0, d=0.15).start(self.ids.chevron_right)
2022-10-08 09:17:59 -06:00
Animation(_scale_calendar_layout=0, d=0.15).start(self)
2022-07-07 14:16:10 -06:00
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()
2022-10-08 09:17:59 -06:00
if self.min_year <= self.year < self.max_year:
index = self.year - self.min_year
self.ids._year_layout.children[0].select_node(index)
else:
self.ids._year_layout.children[0].clear_selection()
2022-07-07 14:16:10 -06:00
def transformation_to_dialog_input_date(self) -> None:
self.ids.triangle.disabled = True
if self._select_year_dialog_open:
self.transformation_from_dialog_select_year()
self._input_date_dialog_open = True
self.ids.edit_icon.icon = "calendar"
self.ids.label_title.text = self.title_input
2023-07-09 18:49:58 -06:00
self._fields_container = DatePickerInputFieldContainer(owner=self)
if self.mode == "picker":
selected_date = date(self.sel_year, self.sel_month, self.sel_day)
selected_dates = [selected_date]
else:
selected_dates = [self.min_date, self.max_date]
for selected_date in selected_dates:
field = self.get_field(selected_date)
field.bind(text=self._on_date_field_text_changes)
self._fields_container.add_widget(field)
self.ids.container.add_widget(self._fields_container)
2022-07-07 14:16:10 -06:00
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)
2023-07-09 18:49:58 -06:00
Animation(opacity=1, d=0.15).start(self._fields_container)
# The label text separator in landscape orientation depends on the
# open dialog.
self._update_date_label_text()
2022-07-07 14:16:10 -06:00
def transformation_from_dialog_input_date(
self, interval: Union[int, float]
) -> None:
2023-07-09 18:49:58 -06:00
if not self._try_apply_input():
return
2022-07-07 14:16:10 -06:00
self._input_date_dialog_open = False
self.ids.triangle.disabled = False
2023-07-09 18:49:58 -06:00
self.ids.edit_icon.icon = "pencil"
self.ids.label_title.text = self.title
self.ids.container.remove_widget(self._fields_container)
self._fields_container = None
2022-07-07 14:16:10 -06:00
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)
2023-07-09 18:49:58 -06:00
# The label text separator in landscape orientation depends on the
# open dialog.
self._update_date_label_text()
2022-07-07 14:16:10 -06:00
2023-07-09 18:49:58 -06:00
def _get_dates_from_fields(self):
"""
Return a list of dates entered by the user in the input fields.
2022-07-07 14:16:10 -06:00
2023-07-09 18:49:58 -06:00
If there is an error in the field or the field is empty, None will be
in its place in the list. The length of the list will be 0 if the input
dialog is closed, otherwise 1 in picker mode or 2 in range mode.
"""
if not self._fields_container:
return []
dates = []
# Widgets are arranged in the reverse order of their addition.
for field in reversed(self._fields_container.children):
try:
date = datetime.datetime.strptime(field.text, "%d/%m/%Y").date()
except ValueError:
date = None
dates.append(date)
return dates
def _try_apply_input(self) -> bool:
"""
Apply the dates entered by the user, update the calendar and return
True. If there are errors in the fields, do nothing and return False.
"""
dates = self._get_dates_from_fields()
if not dates:
return True
# Widgets are arranged in the reverse order of their addition.
fields = reversed(self._fields_container.children)
if any(d is None and f.text for f, d in zip(fields, dates)):
return False
if self.mode == "picker":
selected_date = date(self.sel_year, self.sel_month, self.sel_day)
selected_date = dates[0] or selected_date
self.sel_year = selected_date.year
self.sel_month = selected_date.month
self.sel_day = selected_date.day
self.update_calendar(self.sel_year, self.sel_month)
elif self.mode == "range":
date1, date2 = dates[0] or self.min_date, dates[1] or self.max_date
ends = list(filter(bool, [date1, date2]))
if ends:
self.min_date = min(ends)
self.max_date = max(ends)
self.update_calendar(self.year, self.month)
return True
def _on_date_field_text_changes(self, *args):
self._update_date_label_text()
2022-07-07 14:16:10 -06:00
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:
2023-07-09 18:49:58 -06:00
# This method is no longer used, use update_calendar instead.
2022-07-07 14:16:10 -06:00
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.
"""
2023-07-09 18:49:58 -06:00
# This method no longer used, use update_calendar instead.
year = int(list_date[2]) if len(list_date) > 2 else self.sel_year
month = int(list_date[1]) if len(list_date) > 1 else self.sel_month
day = int(list_date[0]) if len(list_date) > 0 else self.sel_day
day = min(day, calendar.monthrange(year, month)[1])
self.sel_year, self.sel_month, self.sel_day = year, month, day
self.update_calendar(year, month)
2022-07-07 14:16:10 -06:00
def update_calendar(self, year, month) -> None:
2022-10-08 09:17:59 -06:00
self.year, self.month = year, month
if self.mode == "picker":
selected_date = date(self.sel_year, self.sel_month, self.sel_day)
selected_dates = {selected_date}
2022-07-07 14:16:10 -06:00
else:
2023-07-09 18:49:58 -06:00
selected_dates = {self.min_date, self.max_date}
# The label text depends on the selected date or date range.
self._update_date_label_text()
month_end = date(year, month, calendar.monthrange(year, month)[1])
2022-10-08 09:17:59 -06:00
dates = self.calendar.itermonthdates(year, month)
for widget, widget_date in zip_longest(self._calendar_list, dates):
# Only widgets whose dates are in the displayed month are visible.
visible = (
widget_date is not None
and widget_date.month == month
and widget_date.year == year
)
widget.text = str(widget_date.day) if visible else ""
widget.is_today = visible and widget_date == self.today
widget.is_selected = visible and widget_date in selected_dates
# I don't understand why, but this line is important. Without this
# line, some widgets that we are trying to disable remain enabled.
widget.disabled = False
2023-07-09 18:49:58 -06:00
widget.disabled = not visible
widget.is_in_range = (
visible
and self.min_date is not None
and self.max_date is not None
and self.min_date <= widget_date <= self.max_date
)
widget.is_range_start = (
visible
and self.min_date is not None
and widget_date == self.min_date
2022-10-08 09:17:59 -06:00
)
2023-07-09 18:49:58 -06:00
widget.is_range_end = (
visible
and self.max_date is not None
and widget_date == self.max_date
)
widget.is_month_end = widget_date == month_end
2022-07-07 14:16:10 -06:00
2023-07-09 18:49:58 -06:00
def get_field(self, date=None) -> MDTextField:
2022-07-07 14:16:10 -06:00
"""Creates and returns a text field object used to enter dates."""
if issubclass(self.input_field_cls, MDTextField):
2022-10-08 09:17:59 -06:00
text_color_focus = (
self.input_field_text_color_focus
if self.input_field_text_color_focus
else self.theme_cls.primary_color
)
text_color_normal = (
self.input_field_text_color_normal
if self.input_field_text_color_normal
else self.theme_cls.disabled_hint_text_color
)
fill_color_focus = (
self.input_field_background_color_focus
if self.input_field_background_color_focus
else self.theme_cls.bg_dark
)
fill_color_normal = (
self.input_field_background_color_normal
if self.input_field_background_color_normal
else self.theme_cls.bg_darkest
)
2022-07-07 14:16:10 -06:00
field = self.input_field_cls(
owner=self,
2023-07-09 18:49:58 -06:00
text=date.strftime("%d/%m/%Y") if date else "",
2022-07-07 14:16:10 -06:00
helper_text=self.helper_text,
2022-10-08 09:17:59 -06:00
fill_color_normal=fill_color_normal,
fill_color_focus=fill_color_focus,
hint_text_color_normal=text_color_normal,
hint_text_color_focus=text_color_focus,
text_color_normal=text_color_normal,
text_color_focus=text_color_focus,
line_color_focus=text_color_focus,
line_color_normal=text_color_normal,
2022-07-07 14:16:10 -06:00
)
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:
2023-07-09 18:49:58 -06:00
if not self.min_date or not self.max_date:
return []
2022-07-07 14:16:10 -06:00
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.
"""
2023-07-09 18:49:58 -06:00
# In portrait orientation, the label is stretched in width, so we
# should not insert line breaks. When the input dialog is open, the
# label moves to the right and also stretches in width.
horizontal = orientation == "portrait" or self._input_date_dialog_open
def date_repr(date):
return date.strftime("%b").capitalize() + " " + str(date.day)
2022-07-07 14:16:10 -06:00
2023-07-09 18:49:58 -06:00
input_dates = self._get_dates_from_fields()
2022-07-07 14:16:10 -06:00
if self.mode == "picker":
2023-07-09 18:49:58 -06:00
selected_date = date(self.sel_year, self.sel_month, self.sel_day)
if input_dates:
selected_date = input_dates[0] or selected_date
weekday_repr = selected_date.strftime("%a").capitalize()
separator = ", " if horizontal else ",\n"
return weekday_repr + separator + date_repr(selected_date)
2022-07-07 14:16:10 -06:00
elif self.mode == "range":
2023-07-09 18:49:58 -06:00
start, end = self.min_date, self.max_date
if input_dates:
start, end = input_dates[0] or start, input_dates[1] or end
ends = [end for end in (start, end) if end]
if len(ends) == 0:
start_repr, end_repr = "Start", "End"
else:
start, end = min(ends), max(ends)
start_repr, end_repr = date_repr(start), date_repr(end)
separator = "" if horizontal else ",\n"
return start_repr + separator + end_repr
def _update_date_label_text(self):
self._date_label_text = self.set_text_full_date(
self.sel_year,
self.sel_month,
self.sel_day,
self.theme_cls.device_orientation,
)
2022-07-07 14:16:10 -06:00
def set_selected_widget(self, widget) -> None:
2023-07-09 18:49:58 -06:00
if self._select_year_dialog_open or self._input_date_dialog_open:
return
try:
widget_date = date(self.year, self.month, int(widget.text))
except ValueError:
return
if self.mode == "picker":
self.sel_year = widget_date.year
self.sel_month = widget_date.month
self.sel_day = widget_date.day
self.update_calendar(self.sel_year, self.sel_month)
elif self.mode == "range":
ends = [end for end in (self.min_date, self.max_date) if end]
if widget_date in ends:
ends = [end for end in ends if end != widget_date]
elif len(ends) < 2:
ends.append(widget_date)
else:
start, end = min(ends), max(ends)
if abs(widget_date - start).days < abs(widget_date - end).days:
start = widget_date
else:
end = widget_date
ends = [start, end]
if len(ends) == 0:
self.min_date, self.max_date = None, None
else:
self.min_date, self.max_date = min(ends), max(ends)
self.update_calendar(self.year, self.month)
2022-07-07 14:16:10 -06:00
def set_month_day(self, day) -> None:
2022-10-08 09:17:59 -06:00
# This method is no longer used. The code bellow repeats the behavior
# that was previously required of it for backward compatibility
# reasons.
self.sel_day = day
self.update_calendar(self.sel_year, self.sel_month)
2022-07-07 14:16:10 -06:00
def set_position_to_current_year(self) -> None:
2022-10-08 09:17:59 -06:00
year_layout = self.ids._year_layout
# When this method is called for the first time, RecycleView has not
# yet added widgets to the year list, so we use the default height.
widget_height = year_layout.children[0].default_size[1]
cols_amount = year_layout.children[0].cols
rows_amount = math.ceil((self.max_year - self.min_year) / cols_amount)
row_index = (self.year - self.min_year) // cols_amount
# To find the middle of the current year widget, we add the height of
# the rows under this widget with half the widget height.
widget_center_y = (rows_amount - row_index - 1 + 0.5) * widget_height
viewport_height = year_layout.height
year_list_height = rows_amount * widget_height
# If there are too few years in the list to fill the entire viewport,
# RecycleView displays additional empty space outside the list.
# We have to move the viewport up so that this space is displayed
# under the years list. Also, this guard condition protects against
# the division by zero error below.
if viewport_height >= year_list_height:
year_layout.scroll_y = 1
return
viewport_bottom = widget_center_y - 0.5 * viewport_height
# We set scroll_y property to the ratio of the actual lifting height
# of the viewport to the maximum possible, and clamp this ratio in the
# range from 0 to 1 so that the viewport still is in a valid position
# if it is impossible to show the widget in the middle.
scroll_y = viewport_bottom / (year_list_height - viewport_height)
year_layout.scroll_y = min(1, max(0, scroll_y))
2022-07-07 14:16:10 -06:00
def generate_list_widgets_years(self) -> None:
2022-10-08 09:17:59 -06:00
self.ids._year_layout.data = []
2022-07-07 14:16:10 -06:00
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,
"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)
2023-07-09 18:49:58 -06:00
for i in range(6 * 7): # 6 weeks, 7 days a week
2022-07-07 14:16:10 -06:00
day_selectable_item = DatePickerDaySelectableItem(
2023-07-09 18:49:58 -06:00
is_week_end=i % 7 == 6,
2022-07-07 14:16:10 -06:00
owner=self,
)
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.
"""
2023-07-09 18:49:58 -06:00
2022-10-08 09:17:59 -06:00
month_delta = 1 if operation == "next" else -1
year = self.year + (self.month - 1 + month_delta) // 12
month = (self.month - 1 + month_delta) % 12 + 1
2023-07-09 18:49:58 -06:00
2022-10-08 09:17:59 -06:00
if year <= 0:
year, month = 1, 1
2022-07-07 14:16:10 -06:00
self.update_calendar(year, month)