Initialization of NomadNet project
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from ..assisting_modules.modifier_key import MODIFIER_KEY # pylint: disable=unused-import
|
||||
from ..assisting_modules.useful_functions import recursively_replace
|
||||
from .indicative_listbox import IndicativeListBox
|
||||
from .integer_picker import IntegerPicker
|
||||
from .selectable_row import SelectableRow
|
||||
|
||||
import calendar
|
||||
import datetime
|
||||
import enum
|
||||
import urwid
|
||||
|
||||
|
||||
class DatePicker(urwid.WidgetWrap):
|
||||
"""Serves as a selector for dates."""
|
||||
|
||||
_TYPE_ERR_MSG = "type {} was expected for {}, but found: {}."
|
||||
_VALUE_ERR_MSG = "unrecognized value: {}."
|
||||
|
||||
# These values are interpreted during the creation of the list items for the day picker.
|
||||
class DAY_FORMAT(enum.Enum):
|
||||
DAY_OF_MONTH = 1
|
||||
DAY_OF_MONTH_TWO_DIGIT = 2
|
||||
WEEKDAY = 3
|
||||
|
||||
# These values are interpreted during the initialization and define the arrangement of the pickers.
|
||||
class PICKER(enum.Enum):
|
||||
YEAR = 1
|
||||
MONTH = 2
|
||||
DAY = 3
|
||||
|
||||
# Specifies which dates are selectable.
|
||||
class RANGE(enum.Enum):
|
||||
ALL = 1
|
||||
ONLY_PAST = 2
|
||||
ONLY_FUTURE = 3
|
||||
|
||||
def __init__(self, initial_date=datetime.date.today(), *, date_range=RANGE.ALL, month_names=calendar.month_name, day_names=calendar.day_abbr,
|
||||
day_format=(DAY_FORMAT.WEEKDAY, DAY_FORMAT.DAY_OF_MONTH), columns=(PICKER.DAY, PICKER.MONTH, PICKER.YEAR),
|
||||
modifier_key=MODIFIER_KEY.CTRL, return_unused_navigation_input=False, year_jump_len=50, space_between=2,
|
||||
min_width_each_picker=9, year_align="center", month_align="center", day_align="center", topBar_align="center",
|
||||
topBar_endCovered_prop=("▲", None, None), topBar_endExposed_prop=("───", None, None), bottomBar_align="center",
|
||||
bottomBar_endCovered_prop=("▼", None, None), bottomBar_endExposed_prop=("───", None, None), highlight_prop=(None, None)):
|
||||
assert (type(date_range) == self.__class__.RANGE), self.__class__._TYPE_ERR_MSG.format("<enum 'DatePicker.RANGE'>",
|
||||
"'date_range'",
|
||||
type(date_range))
|
||||
|
||||
for df in day_format:
|
||||
assert (type(df) == self.__class__.DAY_FORMAT), self.__class__._TYPE_ERR_MSG.format("<enum 'DatePicker.DAY_FORMAT'>",
|
||||
"all elements of 'day_format'",
|
||||
type(df))
|
||||
|
||||
# Relevant for 'RANGE.ONLY_PAST' and 'RANGE.ONLY_FUTURE' to limit the respective choices.
|
||||
self._initial_year = initial_date.year
|
||||
self._initial_month = initial_date.month
|
||||
self._initial_day = initial_date.day
|
||||
|
||||
# The date pool can be limited, so that only past or future dates are selectable. The initial date is included in the
|
||||
# pool.
|
||||
self._date_range = date_range
|
||||
|
||||
# The presentation of months and weekdays can be changed by passing alternative values (e.g. abbreviations or numerical
|
||||
# representations).
|
||||
self._month_names = month_names
|
||||
self._day_names = day_names
|
||||
|
||||
# Since there are different needs regarding the appearance of the day picker, an iterable can be passed, which allows a
|
||||
# customization of the presentation.
|
||||
self._day_format = day_format
|
||||
|
||||
# Specifies the text alignment of the individual pickers. The year alignment is passed directly to the year picker.
|
||||
self._month_align = month_align
|
||||
self._day_align = day_align
|
||||
|
||||
# The default style of a list entry. Since only one list entry will be visible at a time and there is also off focus
|
||||
# highlighting, the normal value can be 'None' (it is never shown).
|
||||
self._item_attr = (None, highlight_prop[0])
|
||||
|
||||
# A full list of months. (From 'January' to 'December'.)
|
||||
self._month_list = self._generate_months()
|
||||
|
||||
# Set the respective values depending on the date range.
|
||||
min_year = datetime.MINYEAR
|
||||
max_year = datetime.MAXYEAR
|
||||
|
||||
month_position = self._initial_month - 1
|
||||
day_position = self._initial_day - 1
|
||||
|
||||
if date_range == self.__class__.RANGE.ALL:
|
||||
initial_month_list = self._month_list
|
||||
|
||||
elif date_range == self.__class__.RANGE.ONLY_PAST:
|
||||
max_year = self._initial_year
|
||||
|
||||
# The months of the very last year may be shorten.
|
||||
self._shortened_month_list = self._generate_months(end=self._initial_month)
|
||||
initial_month_list = self._shortened_month_list
|
||||
|
||||
elif date_range == self.__class__.RANGE.ONLY_FUTURE:
|
||||
min_year = self._initial_year
|
||||
|
||||
# The months of the very first year may be shorten.
|
||||
self._shortened_month_list = self._generate_months(start=self._initial_month)
|
||||
initial_month_list = self._shortened_month_list
|
||||
|
||||
# The list may not start at 1 but some other day of month, therefore use the first list item.
|
||||
month_position = 0
|
||||
day_position = 0
|
||||
|
||||
else:
|
||||
raise ValueError(self.__class__._VALUE_ERR_MSG.format(date_range))
|
||||
|
||||
# Create pickers.
|
||||
self._year_picker = IntegerPicker(self._initial_year,
|
||||
min_v=min_year,
|
||||
max_v=max_year,
|
||||
jump_len=year_jump_len,
|
||||
on_selection_change=self._year_has_changed,
|
||||
modifier_key=modifier_key,
|
||||
return_unused_navigation_input=return_unused_navigation_input,
|
||||
topBar_align=topBar_align,
|
||||
topBar_endCovered_prop=topBar_endCovered_prop,
|
||||
topBar_endExposed_prop=topBar_endExposed_prop,
|
||||
bottomBar_align=bottomBar_align,
|
||||
bottomBar_endCovered_prop=bottomBar_endCovered_prop,
|
||||
bottomBar_endExposed_prop=bottomBar_endExposed_prop,
|
||||
display_syntax="{:04}",
|
||||
display_align=year_align,
|
||||
display_prop=highlight_prop)
|
||||
|
||||
self._month_picker = IndicativeListBox(initial_month_list,
|
||||
position=month_position,
|
||||
on_selection_change=self._month_has_changed,
|
||||
modifier_key=modifier_key,
|
||||
return_unused_navigation_input=return_unused_navigation_input,
|
||||
topBar_align=topBar_align,
|
||||
topBar_endCovered_prop=topBar_endCovered_prop,
|
||||
topBar_endExposed_prop=topBar_endExposed_prop,
|
||||
bottomBar_align=bottomBar_align,
|
||||
bottomBar_endCovered_prop=bottomBar_endCovered_prop,
|
||||
bottomBar_endExposed_prop=bottomBar_endExposed_prop,
|
||||
highlight_offFocus=highlight_prop[1])
|
||||
|
||||
self._day_picker = IndicativeListBox(self._generate_days(self._initial_year, self._initial_month),
|
||||
position=day_position,
|
||||
modifier_key=modifier_key,
|
||||
return_unused_navigation_input=return_unused_navigation_input,
|
||||
topBar_align=topBar_align,
|
||||
topBar_endCovered_prop=topBar_endCovered_prop,
|
||||
topBar_endExposed_prop=topBar_endExposed_prop,
|
||||
bottomBar_align=bottomBar_align,
|
||||
bottomBar_endCovered_prop=bottomBar_endCovered_prop,
|
||||
bottomBar_endExposed_prop=bottomBar_endExposed_prop,
|
||||
highlight_offFocus=highlight_prop[1])
|
||||
|
||||
# To mimic a selection widget, 'IndicativeListbox' is wrapped in a 'urwid.BoxAdapter'. Since two rows are used for the bars,
|
||||
# size 3 makes exactly one list item visible.
|
||||
boxed_month_picker = urwid.BoxAdapter(self._month_picker, 3)
|
||||
boxed_day_picker = urwid.BoxAdapter(self._day_picker, 3)
|
||||
|
||||
# Replace the 'DatePicker.PICKER' elements of the parameter 'columns' with the corresponding pickers.
|
||||
replacements = {self.__class__.PICKER.YEAR : self._year_picker,
|
||||
self.__class__.PICKER.MONTH : boxed_month_picker,
|
||||
self.__class__.PICKER.DAY : boxed_day_picker}
|
||||
|
||||
columns = recursively_replace(columns, replacements)
|
||||
|
||||
# wrap 'urwid.Columns'
|
||||
super().__init__(urwid.Columns(columns,
|
||||
min_width=min_width_each_picker,
|
||||
dividechars=space_between))
|
||||
|
||||
def __repr__(self):
|
||||
return "{}(date='{}', date_range='{}', initial_date='{}-{:02}-{:02}', selected_date='{}')".format(self.__class__.__name__,
|
||||
self.get_date(),
|
||||
self._date_range,
|
||||
self._initial_year,
|
||||
self._initial_month,
|
||||
self._initial_day,
|
||||
self.get_date())
|
||||
|
||||
# The returned widget is used for all list entries.
|
||||
def _generate_item(self, cols, *, align="center"):
|
||||
return urwid.AttrMap(SelectableRow(cols, align=align),
|
||||
self._item_attr[0],
|
||||
self._item_attr[1])
|
||||
|
||||
def _generate_months(self, start=1, end=12):
|
||||
months = []
|
||||
|
||||
for month in range(start, end+1):
|
||||
item = self._generate_item([self._month_names[month]], align=self._month_align)
|
||||
|
||||
# Add a new instance variable which holds the numerical value. This makes it easier to get the displayed value.
|
||||
item._numerical_value = month
|
||||
|
||||
months.append(item)
|
||||
|
||||
return months
|
||||
|
||||
def _generate_days(self, year, month):
|
||||
start = 1
|
||||
weekday, end = calendar.monthrange(year, month) # end is included in the range
|
||||
|
||||
# If the date range is 'ONLY_PAST', the last month does not end as usual but on the specified day.
|
||||
if (self._date_range == self.__class__.RANGE.ONLY_PAST) and (year == self._initial_year) and (month == self._initial_month):
|
||||
end = self._initial_day
|
||||
|
||||
# If the date range is 'ONLY_FUTURE', the first month does not start as usual but on the specified day.
|
||||
elif (self._date_range == self.__class__.RANGE.ONLY_FUTURE) and (year == self._initial_year) and (month == self._initial_month):
|
||||
start = self._initial_day
|
||||
weekday = calendar.weekday(year, month, start)
|
||||
|
||||
days = []
|
||||
|
||||
for day in range(start, end+1):
|
||||
cols = []
|
||||
|
||||
# The 'DatePicker.DAY_FORMAT' elements of the iterable are translated into columns of the day picker. This allows the
|
||||
# presentation to be customized.
|
||||
for df in self._day_format:
|
||||
if df == self.__class__.DAY_FORMAT.DAY_OF_MONTH:
|
||||
cols.append(str(day))
|
||||
|
||||
elif df == self.__class__.DAY_FORMAT.DAY_OF_MONTH_TWO_DIGIT:
|
||||
cols.append(str(day).zfill(2))
|
||||
|
||||
elif df == self.__class__.DAY_FORMAT.WEEKDAY:
|
||||
cols.append(self._day_names[weekday])
|
||||
|
||||
else:
|
||||
raise ValueError(self.__class__._VALUE_ERR_MSG.format(df))
|
||||
|
||||
item = self._generate_item(cols, align=self._day_align)
|
||||
|
||||
# Add a new instance variable which holds the numerical value. This makes it easier to get the displayed value.
|
||||
item._numerical_value = day
|
||||
|
||||
# Keeps track of the weekday.
|
||||
weekday = (weekday + 1) if (weekday < 6) else 0
|
||||
|
||||
days.append(item)
|
||||
|
||||
return days
|
||||
|
||||
def _year_has_changed(self, previous_year, current_year):
|
||||
month_position_before_change = self._month_picker.get_selected_position()
|
||||
|
||||
# Since there are no years in 'RANGE.ALL' that do not have the full month range, the body never needs to be changed after
|
||||
# initialization.
|
||||
if self._date_range != self.__class__.RANGE.ALL:
|
||||
# 'None' stands for trying to keep the old value.
|
||||
provisional_position = None
|
||||
|
||||
# If the previous year was the specified year, the shortened month range must be replaced by the complete one. If this
|
||||
# shortened month range does not begin at 'January', then the difference must be taken into account.
|
||||
if previous_year == self._initial_year:
|
||||
if self._date_range == self.__class__.RANGE.ONLY_FUTURE:
|
||||
provisional_position = self._month_picker.get_selected_item()._numerical_value - 1
|
||||
|
||||
self._month_picker.set_body(self._month_list,
|
||||
alternative_position=provisional_position)
|
||||
|
||||
# If the specified year is selected, the full month range must be replaced with the shortened one.
|
||||
elif current_year == self._initial_year:
|
||||
if self._date_range == self.__class__.RANGE.ONLY_FUTURE:
|
||||
provisional_position = month_position_before_change - (self._initial_month - 1)
|
||||
|
||||
self._month_picker.set_body(self._shortened_month_list,
|
||||
alternative_position=provisional_position)
|
||||
|
||||
# Since the month has changed, the corresponding method is called.
|
||||
self._month_has_changed(month_position_before_change,
|
||||
self._month_picker.get_selected_position(),
|
||||
previous_year=previous_year)
|
||||
|
||||
def _month_has_changed(self, previous_position, current_position, *, previous_year=None):
|
||||
# 'None' stands for trying to keep the old value.
|
||||
provisional_position = None
|
||||
|
||||
current_year = self._year_picker.get_value()
|
||||
|
||||
# Out of range values are changed by 'IndicativeListBox' to the nearest valid values.
|
||||
|
||||
# If the date range is 'ONLY_FUTURE', it may be that a month does not start on the first day. In this case, the value must
|
||||
# be changed to reflect this difference.
|
||||
if self._date_range == self.__class__.RANGE.ONLY_FUTURE:
|
||||
# If the current or previous year is the specified year and the month was the specified month, the value has an offset
|
||||
# of the specified day. Therefore the deposited numerical value is used. ('-1' because it's an index.)
|
||||
if ((current_year == self._initial_year) or (previous_year == self._initial_year)) and (previous_position == 0):
|
||||
provisional_position = self._day_picker.get_selected_item()._numerical_value - 1
|
||||
|
||||
# If the current year is the specified year and the current month is the specified month, the month begins not with
|
||||
# the first day, but with the specified day.
|
||||
elif (current_year == self._initial_year) and (current_position == 0):
|
||||
provisional_position = self._day_picker.get_selected_position() - (self._initial_day - 1)
|
||||
|
||||
self._day_picker.set_body(self._generate_days(current_year,
|
||||
self._month_picker.get_selected_item()._numerical_value),
|
||||
alternative_position=provisional_position)
|
||||
|
||||
def get_date(self):
|
||||
return datetime.date(self._year_picker.get_value(),
|
||||
self._month_picker.get_selected_item()._numerical_value,
|
||||
self._day_picker.get_selected_item()._numerical_value)
|
||||
|
||||
def set_date(self, date):
|
||||
# If the date range is limited, test for the new limit.
|
||||
if self._date_range != self.__class__.RANGE.ALL:
|
||||
limit = datetime.date(self._initial_year, self._initial_month, self._initial_day)
|
||||
|
||||
if (self._date_range == self.__class__.RANGE.ONLY_PAST) and (date > limit):
|
||||
raise ValueError("The passed date is outside the upper bound of the date range.")
|
||||
|
||||
elif (self._date_range == self.__class__.RANGE.ONLY_FUTURE) and (date < limit):
|
||||
raise ValueError("The passed date is outside the lower bound of the date range.")
|
||||
|
||||
year = date.year
|
||||
month = date.month
|
||||
day = date.day
|
||||
|
||||
# Set the new values, if needed.
|
||||
if year != self._year_picker.get_value():
|
||||
self._year_picker.set_value(year)
|
||||
|
||||
if month != self._month_picker.get_selected_item()._numerical_value:
|
||||
month_position = month - 1 # '-1' because it's an index.
|
||||
|
||||
if (self._date_range == self.__class__.RANGE.ONLY_FUTURE) and (year == self._initial_year):
|
||||
# If the value should be negative, the behavior of 'IndicativeListBox' shows effect and position 0 is selected.
|
||||
month_position = month_position - (self._initial_month - 1)
|
||||
|
||||
self._month_picker.select_item(month_position)
|
||||
|
||||
if day != self._day_picker.get_selected_item()._numerical_value:
|
||||
day_position = day - 1 # '-1' because it's an index.
|
||||
|
||||
if (self._date_range == self.__class__.RANGE.ONLY_FUTURE) and (year == self._initial_year) and (month == self._initial_month):
|
||||
day_position = day_position - (self._initial_day - 1)
|
||||
|
||||
self._day_picker.select_item(day_position)
|
||||
|
||||
Reference in New Issue
Block a user