123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- # -*- 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')]}}
- 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': ''}
- 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',))
- def destroy(self):
- try:
- self.after_cancel(self._determine_downarrow_name_after_id)
- except ValueError:
- # nothing to cancel
- pass
- ttk.Entry.destroy(self)
- 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)
- 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)
- 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))
- 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)
- 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
- 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()
- def get_date(self):
- """Return the content of the DateEntry as a datetime.date instance."""
- self._validate_date()
- return self.parse_date(self.get())
|