Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhanced date entry #392

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 115 additions & 41 deletions src/ttkbootstrap/widgets.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import tkinter as tk
from tkinter import ttk
from tkinter import font
from tkinter.ttk import Button, Checkbutton, Combobox
from tkinter.ttk import Widget, Button, Checkbutton, Combobox
from tkinter.ttk import Entry, Frame, Label
from tkinter.ttk import Labelframe, LabelFrame, Menubutton
from tkinter.ttk import Notebook, OptionMenu, PanedWindow
from tkinter.ttk import Panedwindow, Progressbar, Radiobutton
from tkinter.ttk import Scale, Scrollbar, Separator
from tkinter.ttk import Sizegrip, Spinbox, Treeview
from typing import Optional, Callable, Any

from ttkbootstrap.constants import *

# date entry imports
Expand Down Expand Up @@ -94,13 +96,15 @@ class DateEntry(ttk.Frame):
"""

def __init__(
self,
master=None,
dateformat=r"%x",
firstweekday=6,
startdate=None,
bootstyle="",
**kwargs,
self,
master: Optional[Widget] = None,
dateformat: str = r'%x',
firstweekday: int = 6,
startdate: Optional[datetime] = None,
bootstyle: str = '',
change_date_title: str = 'Choose new date',
update_date_callback: Optional[Callable] = None,
**kwargs,
):
"""
Parameters:
Expand All @@ -110,7 +114,9 @@ def __init__(

dateformat (str, optional):
The format string used to render the text in the entry
widget. For more information on acceptable formats, see https://strftime.org/
widget. For more information on acceptable formats

@see https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior

firstweekday (int, optional):
Specifies the first day of the week. 0=Monday, 1=Tuesday,
Expand All @@ -126,15 +132,25 @@ def __init__(
options include -> primary, secondary, success, info,
warning, danger, dark, light.

change_date_title (str, optional):
Title for PopUp window

update_date_callback (callable, optional):
Callback function, that will be triggered if date has changed successfully

**kwargs (Dict[str, Any], optional):
Other keyword arguments passed to the frame containing the
entry and date button.
"""
self._dateformat = dateformat
self.__dateformat = dateformat # User should NOT be able to change this, therefore double underscores
self._firstweekday = firstweekday

self._startdate = startdate or datetime.today()
initial_date = startdate or datetime.today()
self._startdate = self.__clean_datetime__(initial_date)
self._bootstyle = bootstyle
self.__enabled = True # User should NOT be able to change this, therefore double underscores
self._change_date_title = change_date_title
self._update_callback = update_date_callback if update_date_callback else lambda: True
super().__init__(master, **kwargs)

# add visual components
Expand All @@ -153,17 +169,18 @@ def __init__(
self.button.pack(side=tk.LEFT)

# starting value
self.entry.insert(tk.END, self._startdate.strftime(self._dateformat))
self.entry.insert(tk.END, self._startdate.strftime(self.__dateformat))

def __getitem__(self, key: str):
return self.configure(cnf=key)

def __setitem__(self, key: str, value):
self.configure(cnf=None, **{key: value})

def _configure_set(self, **kwargs):
"""Override configure method to allow for setting custom
DateEntry parameters"""
def _configure_set(self, **kwargs) -> Any:
"""
Override configure method to allow for setting custom DateEntry parameters
"""

if "state" in kwargs:
state = kwargs.pop("state")
Expand All @@ -175,7 +192,7 @@ def _configure_set(self, **kwargs):
else:
kwargs[state] = state
if "dateformat" in kwargs:
self._dateformat = kwargs.pop("dateformat")
self.__dateformat = kwargs.pop("dateformat")
if "firstweekday" in kwargs:
self._firstweekday = kwargs.pop("firstweekday")
if "startdate" in kwargs:
Expand All @@ -190,14 +207,14 @@ def _configure_set(self, **kwargs):

super(ttk.Frame, self).configure(**kwargs)

def _configure_get(self, cnf):
def _configure_get(self, cnf) -> Any:
"""Override the configure get method"""
if cnf == "state":
entrystate = self.entry.cget("state")
buttonstate = self.button.cget("state")
return {"Entry": entrystate, "Button": buttonstate}
if cnf == "dateformat":
return self._dateformat
return self.__dateformat
if cnf == "firstweekday":
return self._firstweekday
if cnf == "startdate":
Expand All @@ -207,7 +224,7 @@ def _configure_get(self, cnf):
else:
return super(ttk.Frame, self).configure(cnf=cnf)

def configure(self, cnf=None, **kwargs):
def configure(self, cnf=None, **kwargs) -> Any:
"""Configure the options for this widget.

Parameters:
Expand All @@ -223,31 +240,88 @@ def configure(self, cnf=None, **kwargs):
else:
return self._configure_set(**kwargs)

def _on_date_ask(self):
"""Callback for pushing the date button"""
_val = self.entry.get() or datetime.today().strftime(self._dateformat)
try:
self._startdate = datetime.strptime(_val, self._dateformat)
except Exception as e:
print("Date entry text does not match", self._dateformat)
self._startdate = datetime.today()
self.entry.delete(first=0, last=tk.END)
self.entry.insert(
tk.END, self._startdate.strftime(self._dateformat)
)
@property
def enabled(self) -> bool:
"""
If ``True`` this date picker is enabled and user can pick a new date, if ``False`` user can't use this picker

old_date = datetime.strptime(_val, self._dateformat)
:return: ``True`` if usable, ``False`` otherwise
"""
return self.__enabled

# get the new date and insert into the entry
new_date = Querybox.get_date(
parent=self.entry,
startdate=old_date,
firstweekday=self._firstweekday,
bootstyle=self._bootstyle,
)
self.entry.delete(first=0, last=tk.END)
self.entry.insert(tk.END, new_date.strftime(self._dateformat))
@property
def date_format(self) -> str:
"""
Returns date format string, that is used to convert from strings to datetime objects respectively vice versa

:return: Date format as string
"""
return self.__dateformat

def get_date(self) -> datetime:
"""
Returns currently selected date as datetime object
"""
return self.configure(cnf='startdate')

@staticmethod
def __clean_datetime__(new_date: datetime) -> datetime:
"""This is a date picker, therefore erase all unnecessary elements: hours, minutes, seconds, ..."""
return datetime(new_date.year, new_date.month, new_date.day, tzinfo=new_date.tzinfo)

def set_date(self, new_date: datetime) -> None:
"""
Sets given datetime object as currently selected date.

(NOTE: Hours, minutes, seconds, milliseconds, microseconds will be ignored)

:param new_date: New date that will become the currently selected one
"""
pure_date = self.__clean_datetime__(new_date)
if self.__enabled:
self.configure(startdate=pure_date)
self.entry.delete(first=0, last=END)
self.entry.insert(END, new_date.strftime(self.__dateformat))
else:
self.enable()
self.configure(startdate=pure_date)
self.entry.delete(first=0, last=END)
self.entry.insert(END, new_date.strftime(self.__dateformat))
self.disable()

def disable(self) -> None:
""" Disables this date picker """
self.__enabled = False
self.entry.state(['disabled'])
self.button.state(['disabled'])

def enable(self) -> None:
""" Enables this date picker """
self.__enabled = True
self.entry.state(['!disabled'])
self.button.state(['!disabled'])

def _on_date_ask(self) -> None:
"""
Callback for pushing the date button

:raise ValueError If entered string does NOT match with currently used date format
"""
current_date = self.entry.get() or datetime.today().strftime(self.__dateformat)
try:
self._startdate = datetime.strptime(current_date, self.__dateformat)
except ValueError:
# Rollback to current date & reraise exception
self.entry.delete(first=0, last=END)
self.entry.insert(END, self._startdate.strftime(self.__dateformat))
raise

old_date = datetime.strptime(current_date, self.__dateformat)
new_date = Querybox.get_date(self.entry, self._change_date_title, self._firstweekday, old_date, self._bootstyle)
self.entry.delete(first=0, last=END)
self.entry.insert(END, new_date.strftime(self.__dateformat))
self.entry.focus_force()
self._update_callback()


class Floodgauge(Progressbar):
Expand Down