tesseramento/datepicker.py

623 lines
20 KiB
Python

# Geraldo 20190506
# File originale nel repository di ActiveState su GitBub
# https://github.com/ActiveState/code/tree/master/recipes/Python/580725_Tkinter_Datepicker_like_jQuery_UI
# Log modifiche rispetto all'originale:
# -------------------------------------
# 20190506
# - Trim trailing space
# - Code style yapf->PEP8
# ----------------------------------------------------------------------------------------------------------
# Author: Miguel Martinez Lopez
#
# Version: 1.0.7
#
# Uncomment the next line to see my email
# print("Author's email: %s"%"61706c69636163696f6e616d656469646140676d61696c2e636f6d".decode("hex")) # noqa
"""
These are the default bindings:
Click button 1 on entry: Show calendar
Click button 1 outsite calendar and entry: Hide calendar
Escape: Hide calendar
CTRL + PAGE UP: Move to the previous month.
CTRL + PAGE DOWN: Move to the next month.
CTRL + SHIFT + PAGE UP: Move to the previous year.
CTRL + SHIFT + PAGE DOWN: Move to the next year.
CTRL + LEFT: Move to the previous day.
CTRL + RIGHT: Move to the next day.
CTRL + UP: Move to the previous week.
CTRL + DOWN: Move to the next week.
CTRL + END: Close the datepicker and erase the date.
CTRL + HOME: Move to the current month.
CTRL + SPACE: Show date on calendar
CTRL + Return: Set current selection to entry
"""
import calendar
import datetime
try:
import Tkinter
import tkFont
import ttk
from Tkconstants import CENTER, LEFT, N, E, W, S
from Tkinter import StringVar
except ImportError: # py3k
import tkinter as Tkinter
import tkinter.font as tkFont
import tkinter.ttk as ttk
from tkinter.constants import CENTER, LEFT, N, E, W, S
from tkinter import StringVar
def get_calendar(locale, fwday):
# instantiate proper calendar class
if locale is None:
return calendar.TextCalendar(fwday)
else:
return calendar.LocaleTextCalendar(fwday, locale)
class Calendar(ttk.Frame):
datetime = calendar.datetime.datetime
timedelta = calendar.datetime.timedelta
def __init__(self,
master=None,
year=None,
month=None,
firstweekday=calendar.MONDAY,
locale=None,
activebackground='#b1dcfb',
activeforeground='black',
selectbackground='#003eff',
selectforeground='white',
command=None,
borderwidth=1,
relief="solid",
on_click_month_button=None):
"""
WIDGET OPTIONS
locale, firstweekday, year, month, selectbackground,
selectforeground, activebackground, activeforeground,
command, borderwidth, relief, on_click_month_button
"""
if year is None:
year = self.datetime.now().year
if month is None:
month = self.datetime.now().month
self._selected_date = None
self._sel_bg = selectbackground
self._sel_fg = selectforeground
self._act_bg = activebackground
self._act_fg = activeforeground
self.on_click_month_button = on_click_month_button
self._selection_is_visible = False
self._command = command
ttk.Frame.__init__(
self, master, borderwidth=borderwidth, relief=relief)
self.bind("<FocusIn>",
lambda event: self.event_generate('<<DatePickerFocusIn>>'))
self.bind("<FocusOut>",
lambda event: self.event_generate('<<DatePickerFocusOut>>'))
self._cal = get_calendar(locale, firstweekday)
# custom ttk styles
style = ttk.Style()
style.layout('L.TButton', ([('Button.focus', {
'children': [('Button.leftarrow', None)]
})]))
style.layout('R.TButton', ([('Button.focus', {
'children': [('Button.rightarrow', None)]
})]))
self._font = tkFont.Font()
self._header_var = StringVar()
# header frame and its widgets
hframe = ttk.Frame(self)
lbtn = ttk.Button(
hframe, style='L.TButton', command=self._on_press_left_button)
lbtn.pack(side=LEFT)
self._header = ttk.Label(
hframe, width=15, anchor=CENTER, textvariable=self._header_var)
self._header.pack(side=LEFT, padx=12)
rbtn = ttk.Button(
hframe, style='R.TButton', command=self._on_press_right_button)
rbtn.pack(side=LEFT)
hframe.grid(columnspan=7, pady=4)
self._day_labels = {}
days_of_the_week = self._cal.formatweekheader(3).split()
for i, day_of_the_week in enumerate(days_of_the_week):
Tkinter.Label(
self, text=day_of_the_week, background='grey90').grid(
row=1, column=i, sticky=N + E + W + S)
for i in range(6):
for j in range(7):
self._day_labels[i, j] = label = Tkinter.Label(
self, background="white")
label.grid(row=i + 2, column=j, sticky=N + E + W + S)
label.bind("<Enter>",
lambda event: event.widget.configure(
background=self._act_bg,
foreground=self._act_fg))
label.bind(
"<Leave>",
lambda event: event.widget.configure(background="white"))
label.bind("<1>", self._pressed)
# adjust its columns width
font = tkFont.Font()
maxwidth = max(font.measure(text) for text in days_of_the_week)
for i in range(7):
self.grid_columnconfigure(i, minsize=maxwidth, weight=1)
self._year = None
self._month = None
# insert dates in the currently empty calendar
self._build_calendar(year, month)
def _build_calendar(self, year, month):
if not (self._year == year and self._month == month):
self._year = year
self._month = month
# update header text (Month, YEAR)
header = self._cal.formatmonthname(year, month, 0)
self._header_var.set(header.title())
# update calendar shown dates
cal = self._cal.monthdayscalendar(year, month)
for i in range(len(cal)):
week = cal[i]
fmt_week = [('%02d' % day) if day else '' for day in week]
for j, day_number in enumerate(fmt_week):
self._day_labels[i, j]["text"] = day_number
if len(cal) < 6:
for j in range(7):
self._day_labels[5, j]["text"] = ""
if (self._selected_date is not None
and self._selected_date.year == self._year
and self._selected_date.month == self._month):
self._show_selection()
def _find_label_coordinates(self, date):
first_weekday_of_the_month = (date.weekday() - date.day) % 7
return divmod(
(first_weekday_of_the_month - self._cal.firstweekday) % 7 +
date.day, 7)
def _show_selection(self):
"""Show a new selection."""
i, j = self._find_label_coordinates(self._selected_date)
label = self._day_labels[i, j]
label.configure(background=self._sel_bg, foreground=self._sel_fg)
label.unbind("<Enter>")
label.unbind("<Leave>")
self._selection_is_visible = True
def _clear_selection(self):
"""Show a new selection."""
i, j = self._find_label_coordinates(self._selected_date)
label = self._day_labels[i, j]
label.configure(background="white", foreground="black")
label.bind("<Enter>",
lambda event: event.widget.configure(
background=self._act_bg, foreground=self._act_fg))
label.bind(
"<Leave>",
lambda event: event.widget.configure(background="white"))
self._selection_is_visible = False
# Callback
def _pressed(self, evt):
"""Clicked somewhere in the calendar."""
text = evt.widget["text"]
if text == "":
return
day_number = int(text)
new_selected_date = datetime.datetime(self._year, self._month,
day_number)
if self._selected_date != new_selected_date:
if self._selected_date is not None:
self._clear_selection()
self._selected_date = new_selected_date
self._show_selection()
if self._command:
self._command(self._selected_date)
def _on_press_left_button(self):
self.prev_month()
if self.on_click_month_button is not None:
self.on_click_month_button()
def _on_press_right_button(self):
self.next_month()
if self.on_click_month_button is not None:
self.on_click_month_button()
def select_prev_day(self):
"""Updated calendar to show the previous day."""
if self._selected_date is None:
self._selected_date = datetime.datetime(self._year, self._month, 1)
else:
self._clear_selection()
self._selected_date = self._selected_date - self.timedelta(days=1)
self._build_calendar(
self._selected_date.year,
self._selected_date.month) # reconstruct calendar
def select_next_day(self):
"""Update calendar to show the next day."""
if self._selected_date is None:
self._selected_date = datetime.datetime(self._year, self._month, 1)
else:
self._clear_selection()
self._selected_date = self._selected_date + self.timedelta(days=1)
self._build_calendar(
self._selected_date.year,
self._selected_date.month) # reconstruct calendar
def select_prev_week_day(self):
"""Updated calendar to show the previous week."""
if self._selected_date is None:
self._selected_date = datetime.datetime(self._year, self._month, 1)
else:
self._clear_selection()
self._selected_date = self._selected_date - self.timedelta(days=7)
self._build_calendar(
self._selected_date.year,
self._selected_date.month) # reconstruct calendar
def select_next_week_day(self):
"""Update calendar to show the next week."""
if self._selected_date is None:
self._selected_date = datetime.datetime(self._year, self._month, 1)
else:
self._clear_selection()
self._selected_date = self._selected_date + self.timedelta(days=7)
self._build_calendar(
self._selected_date.year,
self._selected_date.month) # reconstruct calendar
def select_current_date(self):
"""Update calendar to current date."""
if self._selection_is_visible:
self._clear_selection()
self._selected_date = datetime.datetime.now()
self._build_calendar(self._selected_date.year,
self._selected_date.month)
def prev_month(self):
"""Updated calendar to show the previous week."""
if self._selection_is_visible:
self._clear_selection()
date = self.datetime(self._year, self._month,
1) - self.timedelta(days=1)
self._build_calendar(date.year, date.month) # reconstuct calendar
def next_month(self):
"""Update calendar to show the next month."""
if self._selection_is_visible:
self._clear_selection()
date = self.datetime(self._year, self._month, 1) + self.timedelta(
days=calendar.monthrange(self._year, self._month)[1] + 1)
self._build_calendar(date.year, date.month) # reconstuct calendar
def prev_year(self):
"""Updated calendar to show the previous year."""
if self._selection_is_visible:
self._clear_selection()
self._build_calendar(self._year - 1,
self._month) # reconstruct calendar
def next_year(self):
"""Update calendar to show the next year."""
if self._selection_is_visible:
self._clear_selection()
self._build_calendar(self._year + 1,
self._month) # reconstruct calendar
def get_selection(self):
"""Return a datetime representing the current selected date."""
return self._selected_date
selection = get_selection
def set_selection(self, date):
"""Set the selected date."""
if self._selected_date is not None and self._selected_date != date:
self._clear_selection()
self._selected_date = date
self._build_calendar(date.year, date.month) # reconstruct calendar
# see this URL for date format information:
# https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
class Datepicker(ttk.Entry):
def __init__(self,
master,
entrywidth=None,
entrystyle=None,
datevar=None,
dateformat="%Y-%m-%d",
onselect=None,
firstweekday=calendar.MONDAY,
locale=None,
activebackground='#b1dcfb',
activeforeground='black',
selectbackground='#003eff',
selectforeground='white',
borderwidth=1,
relief="solid"):
if datevar is not None:
self.date_var = datevar
else:
self.date_var = Tkinter.StringVar()
entry_config = {}
if entrywidth is not None:
entry_config["width"] = entrywidth
if entrystyle is not None:
entry_config["style"] = entrystyle
ttk.Entry.__init__(
self, master, textvariable=self.date_var, **entry_config)
self.date_format = dateformat
self._is_calendar_visible = False
self._on_select_date_command = onselect
self.calendar_frame = Calendar(
self.winfo_toplevel(),
firstweekday=firstweekday,
locale=locale,
activebackground=activebackground,
activeforeground=activeforeground,
selectbackground=selectbackground,
selectforeground=selectforeground,
command=self._on_selected_date,
on_click_month_button=lambda: self.focus())
self.bind_all("<1>", self._on_click, "+")
self.bind("<FocusOut>", lambda event: self._on_entry_focus_out())
self.bind("<Escape>", lambda event: self.hide_calendar())
self.calendar_frame.bind("<<DatePickerFocusOut>>",
lambda event: self._on_calendar_focus_out())
# CTRL + PAGE UP: Move to the previous month.
self.bind("<Control-Prior>",
lambda event: self.calendar_frame.prev_month())
# CTRL + PAGE DOWN: Move to the next month.
self.bind("<Control-Next>",
lambda event: self.calendar_frame.next_month())
# CTRL + SHIFT + PAGE UP: Move to the previous year.
self.bind("<Control-Shift-Prior>",
lambda event: self.calendar_frame.prev_year())
# CTRL + SHIFT + PAGE DOWN: Move to the next year.
self.bind("<Control-Shift-Next>",
lambda event: self.calendar_frame.next_year())
# CTRL + LEFT: Move to the previous day.
self.bind("<Control-Left>",
lambda event: self.calendar_frame.select_prev_day())
# CTRL + RIGHT: Move to the next day.
self.bind("<Control-Right>",
lambda event: self.calendar_frame.select_next_day())
# CTRL + UP: Move to the previous week.
self.bind("<Control-Up>",
lambda event: self.calendar_frame.select_prev_week_day())
# CTRL + DOWN: Move to the next week.
self.bind("<Control-Down>",
lambda event: self.calendar_frame.select_next_week_day())
# CTRL + END: Close the datepicker and erase the date.
self.bind("<Control-End>", lambda event: self.erase())
# CTRL + HOME: Move to the current month.
self.bind("<Control-Home>",
lambda event: self.calendar_frame.select_current_date())
# CTRL + SPACE: Show date on calendar
self.bind("<Control-space>",
lambda event: self.show_date_on_calendar())
# CTRL + Return: Set to entry current selection
self.bind("<Control-Return>",
lambda event: self.set_date_from_calendar())
def set_date_from_calendar(self):
if self.is_calendar_visible:
selected_date = self.calendar_frame.selection()
if selected_date is not None:
self.date_var.set(selected_date.strftime(self.date_format))
if self._on_select_date_command is not None:
self._on_select_date_command(selected_date)
self.hide_calendar()
@property
def current_text(self):
return self.date_var.get()
@current_text.setter
def current_text(self, text):
return self.date_var.set(text)
@property
def current_date(self):
try:
date = datetime.datetime.strptime(self.date_var.get(),
self.date_format)
return date
except ValueError:
return None
@current_date.setter
def current_date(self, date):
self.date_var.set(date.strftime(self.date_format))
@property
def is_valid_date(self):
if self.current_date is None:
return False
else:
return True
def show_date_on_calendar(self):
date = self.current_date
if date is not None:
self.calendar_frame.set_selection(date)
self.show_calendar()
def show_calendar(self):
if not self._is_calendar_visible:
self.calendar_frame.place(in_=self, relx=0, rely=1)
self.calendar_frame.lift()
self._is_calendar_visible = True
def hide_calendar(self):
if self._is_calendar_visible:
self.calendar_frame.place_forget()
self._is_calendar_visible = False
def erase(self):
self.hide_calendar()
self.date_var.set("")
@property
def is_calendar_visible(self):
return self._is_calendar_visible
def _on_entry_focus_out(self):
if not str(self.focus_get()).startswith(str(self.calendar_frame)):
self.hide_calendar()
def _on_calendar_focus_out(self):
if self.focus_get() != self:
self.hide_calendar()
def _on_selected_date(self, date):
self.date_var.set(date.strftime(self.date_format))
self.hide_calendar()
if self._on_select_date_command is not None:
self._on_select_date_command(date)
def _on_click(self, event):
str_widget = str(event.widget)
if str_widget == str(self):
if not self._is_calendar_visible:
self.show_date_on_calendar()
else:
if not str_widget.startswith(str(
self.calendar_frame)) and self._is_calendar_visible:
self.hide_calendar()
if __name__ == "__main__":
import sys
try:
from Tkinter import Tk, Frame, Label
except ImportError:
from tkinter import Tk, Frame, Label
root = Tk()
root.geometry("500x600")
main = Frame(root, pady=15, padx=15)
main.pack(expand=True, fill="both")
Label(main, justify="left", text=__doc__).pack(anchor="w", pady=(0, 15))
Datepicker(main).pack(anchor="w")
if 'win' not in sys.platform:
style = ttk.Style()
style.theme_use('clam')
root.mainloop()