Source code for tkcalendar.dateentry

# -*- coding: utf-8 -*-
"""
tkcalendar - Calendar and DateEntry widgets for Tkinter
Copyright 2017-2019 Juliette Monsel <j_4321@protonmail.com>
with contributions from:
  - Neal Probert (https://github.com/nprobert)
  - arahorn28 (https://github.com/arahorn28)

tkcalendar is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

tkcalendar is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.


DateEntry widget
"""


from sys import platform
try:
    import tkinter as tk
    from tkinter import ttk
except ImportError:
    import Tkinter as tk
    import ttk

from tkcalendar.calendar_ import Calendar

# temporary fix for issue #61 and https://bugs.python.org/issue38661
MAPS = {'winnative': {'focusfill': [('readonly', 'focus', 'SystemHighlight')],
                      'foreground': [('disabled', 'SystemGrayText'),
                                     ('readonly', 'focus', 'SystemHighlightText')],
                      'selectforeground': [('!focus', 'SystemWindowText')],
                      'fieldbackground': [('readonly', 'SystemButtonFace'),
                                          ('disabled', 'SystemButtonFace')],
                      'selectbackground': [('!focus', 'SystemWindow')]},
        'clam': {'foreground': [('readonly', 'focus', '#ffffff')],
                 'fieldbackground': [('readonly', 'focus', '#4a6984'), ('readonly', '#dcdad5')],
                 'background': [('active', '#eeebe7'), ('pressed', '#eeebe7')],
                 'arrowcolor': [('disabled', '#999999')]},
        'alt': {'fieldbackground': [('readonly', '#d9d9d9'),
                                    ('disabled', '#d9d9d9')],
                'arrowcolor': [('disabled', '#a3a3a3')]},
        'default': {'fieldbackground': [('readonly', '#d9d9d9'), ('disabled', '#d9d9d9')],
                    'arrowcolor': [('disabled', '#a3a3a3')]},
        'classic': {'fieldbackground': [('readonly', '#d9d9d9'), ('disabled', '#d9d9d9')]},
        'vista': {'focusfill': [('readonly', 'focus', 'SystemHighlight')],
                  'foreground': [('disabled', 'SystemGrayText'),
                                 ('readonly', 'focus', 'SystemHighlightText')],
                  'selectforeground': [('!focus', 'SystemWindowText')],
                  'selectbackground': [('!focus', 'SystemWindow')]},
        'xpnative': {'focusfill': [('readonly', 'focus', 'SystemHighlight')],
                     'foreground': [('disabled', 'SystemGrayText'),
                                    ('readonly', 'focus', 'SystemHighlightText')],
                     'selectforeground': [('!focus', 'SystemWindowText')],
                     'selectbackground': [('!focus', 'SystemWindow')]}}


[docs]class DateEntry(ttk.Entry): """Date selection entry with drop-down calendar.""" entry_kw = {'exportselection': 1, 'invalidcommand': '', 'justify': 'left', 'show': '', 'cursor': 'xterm', 'style': '', 'state': 'normal', 'takefocus': 'ttk::takefocus', 'textvariable': '', 'validate': 'none', 'validatecommand': '', 'width': 12, 'xscrollcommand': ''}
[docs] def __init__(self, master=None, **kw): """ Create an entry with a drop-down calendar to select a date. When the entry looses focus, if the user input is not a valid date, the entry content is reset to the last valid date. Keyword Options --------------- usual ttk.Entry options and Calendar options. The Calendar option 'cursor' has been renamed 'calendar_cursor' to avoid name clashes with the corresponding ttk.Entry option. Virtual event ------------- A ``<<DateEntrySelected>>`` event is generated each time the user selects a date. """ # sort keywords between entry options and calendar options kw['selectmode'] = 'day' entry_kw = {} style = kw.pop('style', 'DateEntry') for key in self.entry_kw: entry_kw[key] = kw.pop(key, self.entry_kw[key]) entry_kw['font'] = kw.get('font', None) self._cursor = entry_kw['cursor'] # entry cursor kw['cursor'] = kw.pop('calendar_cursor', None) ttk.Entry.__init__(self, master, **entry_kw) self._determine_downarrow_name_after_id = '' # drop-down calendar self._top_cal = tk.Toplevel(self) self._top_cal.withdraw() if platform == "linux": self._top_cal.attributes('-type', 'DROPDOWN_MENU') self._top_cal.overrideredirect(True) self._calendar = Calendar(self._top_cal, **kw) self._calendar.pack() # locale date parsing / formatting self.format_date = self._calendar.format_date self.parse_date = self._calendar.parse_date # style self._theme_name = '' # to detect theme changes self.style = ttk.Style(self) self._setup_style() self.configure(style=style) # add validation to Entry so that only dates in the locale's format # are accepted validatecmd = self.register(self._validate_date) self.configure(validate='focusout', validatecommand=validatecmd) # initially selected date self._date = self._calendar.selection_get() if self._date is None: today = self._calendar.date.today() year = kw.get('year', today.year) month = kw.get('month', today.month) day = kw.get('day', today.day) try: self._date = self._calendar.date(year, month, day) except ValueError: self._date = today self._set_text(self.format_date(self._date)) # --- bindings # reconfigure style if theme changed self.bind('<<ThemeChanged>>', lambda e: self.after(10, self._on_theme_change)) # determine new downarrow button bbox self.bind('<Configure>', self._determine_downarrow_name) self.bind('<Map>', self._determine_downarrow_name) # handle appearence to make the entry behave like a Combobox but with # a drop-down calendar instead of a drop-down list self.bind('<Leave>', lambda e: self.state(['!active'])) self.bind('<Motion>', self._on_motion) self.bind('<ButtonPress-1>', self._on_b1_press) # update entry content when date is selected in the Calendar self._calendar.bind('<<CalendarSelected>>', self._select) # hide calendar if it looses focus self._calendar.bind('<FocusOut>', self._on_focus_out_cal)
def __getitem__(self, key): """Return the resource value for a KEY given as string.""" return self.cget(key) def __setitem__(self, key, value): self.configure(**{key: value}) def _setup_style(self, event=None): """Style configuration to make the DateEntry look like a Combobbox.""" self.style.layout('DateEntry', self.style.layout('TCombobox')) self.update_idletasks() conf = self.style.configure('TCombobox') if conf: self.style.configure('DateEntry', **conf) maps = self.style.map('TCombobox') if maps: try: self.style.map('DateEntry', **maps) except tk.TclError: # temporary fix for issue #61 and https://bugs.python.org/issue38661 maps = MAPS.get(self.style.theme_use(), MAPS['default']) self.style.map('DateEntry', **maps) try: self.after_cancel(self._determine_downarrow_name_after_id) except ValueError: # nothing to cancel pass self._determine_downarrow_name_after_id = self.after(10, self._determine_downarrow_name) def _determine_downarrow_name(self, event=None): """Determine downarrow button name.""" try: self.after_cancel(self._determine_downarrow_name_after_id) except ValueError: # nothing to cancel pass if self.winfo_ismapped(): self.update_idletasks() y = self.winfo_height() // 2 x = self.winfo_width() - 10 name = self.identify(x, y) if name: self._downarrow_name = name else: self._determine_downarrow_name_after_id = self.after(10, self._determine_downarrow_name) def _on_motion(self, event): """Set widget state depending on mouse position to mimic Combobox behavior.""" x, y = event.x, event.y if 'disabled' not in self.state(): if self.identify(x, y) == self._downarrow_name: self.state(['active']) ttk.Entry.configure(self, cursor='arrow') else: self.state(['!active']) ttk.Entry.configure(self, cursor=self._cursor) def _on_theme_change(self): theme = self.style.theme_use() if self._theme_name != theme: # the theme has changed, update the DateEntry style to look like a combobox self._theme_name = theme self._setup_style() def _on_b1_press(self, event): """Trigger self.drop_down on downarrow button press and set widget state to ['pressed', 'active'].""" x, y = event.x, event.y if (('disabled' not in self.state()) and self.identify(x, y) == self._downarrow_name): self.state(['pressed']) self.drop_down() def _on_focus_out_cal(self, event): """Withdraw drop-down calendar when it looses focus.""" if self.focus_get() is not None: if self.focus_get() == self: x, y = event.x, event.y if (type(x) != int or type(y) != int or self.identify(x, y) != self._downarrow_name): self._top_cal.withdraw() self.state(['!pressed']) else: self._top_cal.withdraw() self.state(['!pressed']) elif self.grab_current(): # 'active' won't be in state because of the grab x, y = self._top_cal.winfo_pointerxy() xc = self._top_cal.winfo_rootx() yc = self._top_cal.winfo_rooty() w = self._top_cal.winfo_width() h = self._top_cal.winfo_height() if xc <= x <= xc + w and yc <= y <= yc + h: # re-focus calendar so that <FocusOut> will be triggered next time self._calendar.focus_force() else: self._top_cal.withdraw() self.state(['!pressed']) else: if 'active' in self.state(): # re-focus calendar so that <FocusOut> will be triggered next time self._calendar.focus_force() else: self._top_cal.withdraw() self.state(['!pressed']) def _validate_date(self): """Date entry validation: only dates in locale '%x' format are accepted.""" try: date = self.parse_date(self.get()) self._date = self._calendar.check_date_range(date) if self._date != date: self._set_text(self.format_date(self._date)) return False else: return True except (ValueError, IndexError): self._set_text(self.format_date(self._date)) return False def _select(self, event=None): """Display the selected date in the entry and hide the calendar.""" date = self._calendar.selection_get() if date is not None: self._set_text(self.format_date(date)) self._date = date self.event_generate('<<DateEntrySelected>>') self._top_cal.withdraw() if 'readonly' not in self.state(): self.focus_set() def _set_text(self, txt): """Insert text in the entry.""" if 'readonly' in self.state(): readonly = True self.state(('!readonly',)) else: readonly = False self.delete(0, 'end') self.insert(0, txt) if readonly: self.state(('readonly',))
[docs] def destroy(self): try: self.after_cancel(self._determine_downarrow_name_after_id) except ValueError: # nothing to cancel pass ttk.Entry.destroy(self)
[docs] def drop_down(self): """Display or withdraw the drop-down calendar depending on its current state.""" if self._calendar.winfo_ismapped(): self._top_cal.withdraw() else: self._validate_date() date = self.parse_date(self.get()) x = self.winfo_rootx() y = self.winfo_rooty() + self.winfo_height() if self.winfo_toplevel().attributes('-topmost'): self._top_cal.attributes('-topmost', True) else: self._top_cal.attributes('-topmost', False) self._top_cal.geometry('+%i+%i' % (x, y)) self._top_cal.deiconify() self._calendar.focus_set() self._calendar.selection_set(date)
[docs] def state(self, *args): """ Modify or inquire widget state. Widget state is returned if statespec is None, otherwise it is set according to the statespec flags and then a new state spec is returned indicating which flags were changed. statespec is expected to be a sequence. """ if args: # change cursor depending on state to mimic Combobox behavior states = args[0] if 'disabled' in states or 'readonly' in states: self.configure(cursor='arrow') elif '!disabled' in states or '!readonly' in states: self.configure(cursor='xterm') return ttk.Entry.state(self, *args)
[docs] def keys(self): """Return a list of all resource names of this widget.""" keys = list(self.entry_kw) keys.extend(self._calendar.keys()) keys.append('calendar_cursor') return list(set(keys))
[docs] def cget(self, key): """Return the resource value for a KEY given as string.""" if key in self.entry_kw: return ttk.Entry.cget(self, key) elif key == 'calendar_cursor': return self._calendar.cget('cursor') else: return self._calendar.cget(key)
[docs] def configure(self, cnf={}, **kw): """ Configure resources of a widget. The values for resources are specified as keyword arguments. To get an overview about the allowed keyword arguments call the method :meth:`~DateEntry.keys`. """ if not isinstance(cnf, dict): raise TypeError("Expected a dictionary or keyword arguments.") kwargs = cnf.copy() kwargs.update(kw) entry_kw = {} keys = list(kwargs.keys()) for key in keys: if key in self.entry_kw: entry_kw[key] = kwargs.pop(key) font = kwargs.get('font', None) if font is not None: entry_kw['font'] = font self._cursor = str(entry_kw.get('cursor', self._cursor)) if entry_kw.get('state') == 'readonly' and self._cursor == 'xterm' and 'cursor' not in entry_kw: entry_kw['cursor'] = 'arrow' self._cursor = 'arrow' ttk.Entry.configure(self, entry_kw) kwargs['cursor'] = kwargs.pop('calendar_cursor', None) self._calendar.configure(kwargs) if 'date_pattern' in kwargs or 'locale' in kwargs: self._set_text(self.format_date(self._date))
config = configure
[docs] def set_date(self, date): """ Set the value of the DateEntry to date. date can be a datetime.date, a datetime.datetime or a string in locale '%x' format. """ try: txt = self.format_date(date) except AssertionError: txt = str(date) try: self.parse_date(txt) except Exception: raise ValueError("%r is not a valid date." % date) self._set_text(txt) self._validate_date()
[docs] def get_date(self): """Return the content of the DateEntry as a datetime.date instance.""" self._validate_date() return self.parse_date(self.get())