mirror of
https://github.com/markqvist/NomadNet.git
synced 2024-12-23 22:29:28 -05:00
Included additional-urwid-widgets by AFoeee
This commit is contained in:
parent
ac81f0768a
commit
f5d263ee31
21
nomadnet/vendor/additional_urwid_widgets/LICENSE
vendored
Normal file
21
nomadnet/vendor/additional_urwid_widgets/LICENSE
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 AFoeee
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
10
nomadnet/vendor/additional_urwid_widgets/__init__.py
vendored
Normal file
10
nomadnet/vendor/additional_urwid_widgets/__init__.py
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from .assisting_modules.modifier_key import MODIFIER_KEY
|
||||
from .widgets.date_picker import DatePicker
|
||||
from .widgets.indicative_listbox import IndicativeListBox
|
||||
from .widgets.integer_picker import IntegerPicker
|
||||
from .widgets.message_dialog import MessageDialog
|
||||
from .widgets.selectable_row import SelectableRow
|
2
nomadnet/vendor/additional_urwid_widgets/assisting_modules/__init__.py
vendored
Normal file
2
nomadnet/vendor/additional_urwid_widgets/assisting_modules/__init__.py
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
26
nomadnet/vendor/additional_urwid_widgets/assisting_modules/modifier_key.py
vendored
Normal file
26
nomadnet/vendor/additional_urwid_widgets/assisting_modules/modifier_key.py
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class MODIFIER_KEY(enum.Enum):
|
||||
"""Represents modifier keys such as 'ctrl', 'shift' and so on.
|
||||
Not every combination of modifier and input is useful."""
|
||||
|
||||
NONE = ""
|
||||
SHIFT = "shift"
|
||||
ALT = "meta"
|
||||
CTRL = "ctrl"
|
||||
SHIFT_ALT = "shift meta"
|
||||
SHIFT_CTRL = "shift ctrl"
|
||||
ALT_CTRL = "meta ctrl"
|
||||
SHIFT_ALT_CTRL = "shift meta ctrl"
|
||||
|
||||
def append_to(self, text, separator=" "):
|
||||
return (text + separator + self.value) if (self != self.__class__.NONE) else text
|
||||
|
||||
def prepend_to(self, text, separator=" "):
|
||||
return (self.value + separator + text) if (self != self.__class__.NONE) else text
|
||||
|
47
nomadnet/vendor/additional_urwid_widgets/assisting_modules/useful_functions.py
vendored
Normal file
47
nomadnet/vendor/additional_urwid_widgets/assisting_modules/useful_functions.py
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""A non-thematic collection of useful functions."""
|
||||
|
||||
|
||||
def recursively_replace(original, replacements, include_original_keys=False):
|
||||
"""Clones an iterable and recursively replaces specific values."""
|
||||
|
||||
# If this function would be called recursively, the parameters 'replacements' and 'include_original_keys' would have to be
|
||||
# passed each time. Therefore, a helper function with a reduced parameter list is used for the recursion, which nevertheless
|
||||
# can access the said parameters.
|
||||
|
||||
def _recursion_helper(obj):
|
||||
#Determine if the object should be replaced. If it is not hashable, the search will throw a TypeError.
|
||||
try:
|
||||
if obj in replacements:
|
||||
return replacements[obj]
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# An iterable is recursively processed depending on its class.
|
||||
if hasattr(obj, "__iter__") and not isinstance(obj, (str, bytes, bytearray)):
|
||||
if isinstance(obj, dict):
|
||||
contents = {}
|
||||
|
||||
for key, val in obj.items():
|
||||
new_key = _recursion_helper(key) if include_original_keys else key
|
||||
new_val = _recursion_helper(val)
|
||||
|
||||
contents[new_key] = new_val
|
||||
|
||||
else:
|
||||
contents = []
|
||||
|
||||
for element in obj:
|
||||
new_element = _recursion_helper(element)
|
||||
|
||||
contents.append(new_element)
|
||||
|
||||
# Use the same class as the original.
|
||||
return obj.__class__(contents)
|
||||
|
||||
# If it is not replaced and it is not an iterable, return it.
|
||||
return obj
|
||||
|
||||
return _recursion_helper(original)
|
2
nomadnet/vendor/additional_urwid_widgets/widgets/__init__.py
vendored
Normal file
2
nomadnet/vendor/additional_urwid_widgets/widgets/__init__.py
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
345
nomadnet/vendor/additional_urwid_widgets/widgets/date_picker.py
vendored
Normal file
345
nomadnet/vendor/additional_urwid_widgets/widgets/date_picker.py
vendored
Normal file
@ -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)
|
||||
|
433
nomadnet/vendor/additional_urwid_widgets/widgets/indicative_listbox.py
vendored
Normal file
433
nomadnet/vendor/additional_urwid_widgets/widgets/indicative_listbox.py
vendored
Normal file
@ -0,0 +1,433 @@
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from ..assisting_modules.modifier_key import MODIFIER_KEY # pylint: disable=unused-import
|
||||
|
||||
import enum
|
||||
import random
|
||||
import urwid
|
||||
|
||||
|
||||
class IndicativeListBox(urwid.WidgetWrap):
|
||||
"""Adds two bars to a 'urwid.ListBox', that make it obvious that due to limited space only a part of the list items is displayed."""
|
||||
|
||||
_TYPE_ERR_MSG = "type {} was expected for {}, but found: {}."
|
||||
_VALUE_ERR_MSG = "unrecognized value: {}."
|
||||
|
||||
# These values are translated by 'get_nearest_valid_position()' into the corresponding int values.
|
||||
class POSITION(enum.Enum):
|
||||
LAST = 1
|
||||
MIDDLE = 2
|
||||
RANDOM = 3
|
||||
|
||||
def __init__(self, body, *, position=0, on_selection_change=None, initialization_is_selection_change=False,
|
||||
modifier_key=MODIFIER_KEY.NONE, return_unused_navigation_input=True, 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_offFocus=None):
|
||||
# If not already done, wrap each item of the body in an 'urwid.AttrMap'. This is necessary to enable off focus highlighting.
|
||||
body[:] = [urwid.AttrMap(item, None) if not isinstance(item, urwid.AttrMap) else item
|
||||
for item in body]
|
||||
|
||||
# The body of the 'urwid.Frame' is a 'urwid.ListBox'.
|
||||
self._listbox = urwid.ListBox(body)
|
||||
|
||||
# Select the specified list position, or the nearest valid one.
|
||||
nearest_valid_position = self._get_nearest_valid_position(position)
|
||||
|
||||
if nearest_valid_position is not None:
|
||||
self._listbox.set_focus(nearest_valid_position)
|
||||
|
||||
# The bars are just 'urwid.Text' widgets.
|
||||
self._top_bar = urwid.AttrMap(urwid.Text("", align=topBar_align),
|
||||
None)
|
||||
|
||||
self._bottom_bar = urwid.AttrMap(urwid.Text("", align=bottomBar_align),
|
||||
None)
|
||||
|
||||
# Wrap 'urwid.Frame'.
|
||||
super().__init__(urwid.Frame(self._listbox,
|
||||
header=self._top_bar,
|
||||
footer=self._bottom_bar,
|
||||
focus_part="body"))
|
||||
|
||||
# During the initialization of 'urwid.AttrMap', the value can be passed as non-dict. After initializing, its value can be
|
||||
# manipulated by passing a dict. The dicts I create below will be used later to change the appearance of the bars.
|
||||
self._topBar_endCovered_markup = topBar_endCovered_prop[0]
|
||||
self._topBar_endCovered_focus = {None:topBar_endCovered_prop[1]}
|
||||
self._topBar_endCovered_offFocus = {None:topBar_endCovered_prop[2]}
|
||||
|
||||
self._topBar_endExposed_markup = topBar_endExposed_prop[0]
|
||||
self._topBar_endExposed_focus = {None:topBar_endExposed_prop[1]}
|
||||
self._topBar_endExposed_offFocus = {None:topBar_endExposed_prop[2]}
|
||||
|
||||
self._bottomBar_endCovered_markup = bottomBar_endCovered_prop[0]
|
||||
self._bottomBar_endCovered_focus = {None:bottomBar_endCovered_prop[1]}
|
||||
self._bottomBar_endCovered_offFocus = {None:bottomBar_endCovered_prop[2]}
|
||||
|
||||
self._bottomBar_endExposed_markup = bottomBar_endExposed_prop[0]
|
||||
self._bottomBar_endExposed_focus = {None:bottomBar_endExposed_prop[1]}
|
||||
self._bottomBar_endExposed_offFocus = {None:bottomBar_endExposed_prop[2]}
|
||||
|
||||
# This is used to highlight the selected item when the widget does not have the focus.
|
||||
self._highlight_offFocus = {None:highlight_offFocus}
|
||||
self._last_focus_state = None
|
||||
self._original_item_attr_map = None
|
||||
|
||||
# A hook which is triggered when the selection changes.
|
||||
self.on_selection_change = on_selection_change
|
||||
|
||||
# Initialization can be seen as a special case of selection change. This is interesting in combination with the hook.
|
||||
self._initialization_is_selection_change = initialization_is_selection_change
|
||||
|
||||
# 'MODIFIER_KEY' changes the behavior of the list box, so that it responds only to modified input. ('up' => 'ctrl up')
|
||||
self._modifier_key = modifier_key
|
||||
|
||||
# If the list item at the top is selected and you navigate further upwards, the input is normally not swallowed by the
|
||||
# list box, but passed on so that other widgets can interpret it. This may result in transferring the focus.
|
||||
self._return_unused_navigation_input = return_unused_navigation_input
|
||||
|
||||
# Is 'on_selection_change' triggered during the initialization?
|
||||
if initialization_is_selection_change and (on_selection_change is not None):
|
||||
on_selection_change(None,
|
||||
self.get_selected_position())
|
||||
|
||||
def __repr__(self):
|
||||
return "{}(body='{}', position='{}')".format(self.__class__.__name__,
|
||||
self.get_body(),
|
||||
self.get_selected_position())
|
||||
|
||||
def render(self, size, focus=False):
|
||||
just_maxcol = (size[0],)
|
||||
|
||||
# The size also includes the two bars, so subtract these.
|
||||
modified_size = (size[0], # cols
|
||||
size[1] - self._top_bar.rows(just_maxcol) - self._bottom_bar.rows(just_maxcol)) # rows
|
||||
|
||||
# Evaluates which ends are visible and calculates how many list entries are hidden above/below. This is a modified form
|
||||
# of 'urwid.ListBox.ends_visible()'.
|
||||
middle, top, bottom = self._listbox.calculate_visible(modified_size, focus=focus)
|
||||
|
||||
if middle is None: # empty list box
|
||||
top_is_visible = True
|
||||
bottom_is_visible = True
|
||||
|
||||
else:
|
||||
top_is_visible = False
|
||||
bottom_is_visible = False
|
||||
|
||||
trim_top, above = top
|
||||
trim_bottom, below = bottom
|
||||
|
||||
if trim_top == 0:
|
||||
pos = above[-1][1] if (len(above) != 0) else middle[2]
|
||||
|
||||
if self._listbox._body.get_prev(pos) == (None,None):
|
||||
top_is_visible = True
|
||||
else:
|
||||
covered_above = pos
|
||||
else:
|
||||
covered_above = self.rearmost_position()
|
||||
|
||||
if trim_bottom == 0:
|
||||
row_offset, _, pos, rows, _ = middle
|
||||
|
||||
row_offset += rows # Include the selected position.
|
||||
|
||||
for _, pos, rows in below: # 'pos' is overridden
|
||||
row_offset += rows
|
||||
|
||||
if (row_offset < modified_size[1]) or (self._listbox._body.get_next(pos) == (None, None)):
|
||||
bottom_is_visible = True
|
||||
else:
|
||||
covered_below = self.rearmost_position() - pos
|
||||
else:
|
||||
covered_below = self.rearmost_position()
|
||||
|
||||
# Changes the appearance of the bar at the top depending on whether the first list item is visible and the widget has
|
||||
# the focus.
|
||||
if top_is_visible:
|
||||
self._top_bar.original_widget.set_text(self._topBar_endExposed_markup)
|
||||
self._top_bar.set_attr_map(self._topBar_endExposed_focus
|
||||
if focus else self._topBar_endExposed_offFocus)
|
||||
else:
|
||||
self._top_bar.original_widget.set_text(self._topBar_endCovered_markup.format(covered_above))
|
||||
self._top_bar.set_attr_map(self._topBar_endCovered_focus
|
||||
if focus else self._topBar_endCovered_offFocus)
|
||||
|
||||
# Changes the appearance of the bar at the bottom depending on whether the last list item is visible and the widget
|
||||
# has the focus.
|
||||
if bottom_is_visible:
|
||||
self._bottom_bar.original_widget.set_text(self._bottomBar_endExposed_markup)
|
||||
self._bottom_bar.set_attr_map(self._bottomBar_endExposed_focus
|
||||
if focus else self._bottomBar_endExposed_offFocus)
|
||||
else:
|
||||
self._bottom_bar.original_widget.set_text(self._bottomBar_endCovered_markup.format(covered_below))
|
||||
self._bottom_bar.set_attr_map(self._bottomBar_endCovered_focus
|
||||
if focus else self._bottomBar_endCovered_offFocus)
|
||||
|
||||
# The highlighting in urwid is bound to the focus. This means that the selected item is only distinguishable as long as
|
||||
# the widget has the focus. To compensate this, the color scheme of the selected item is otherwiese temporarily changed.
|
||||
if focus and not self._last_focus_state and (self._original_item_attr_map is not None):
|
||||
# Resets the appearance of the selected item to its original value.
|
||||
self._listbox.focus.set_attr_map(self._original_item_attr_map)
|
||||
|
||||
elif (self._highlight_offFocus is not None) \
|
||||
and not focus \
|
||||
and (self._last_focus_state or (self._last_focus_state is None)) \
|
||||
and not self.body_is_empty():
|
||||
# Store the 'attr_map' of the selected item and then change it to accomplish off focus highlighting.
|
||||
self._original_item_attr_map = self._listbox.focus.get_attr_map()
|
||||
self._listbox.focus.set_attr_map(self._highlight_offFocus)
|
||||
|
||||
# Store the last focus to do/undo the off focus highlighting only if the focus has really changed and not if the
|
||||
# widget is re-rendered because the terminal size has changed or similar.
|
||||
self._last_focus_state = focus
|
||||
|
||||
return super().render(size, focus=focus)
|
||||
|
||||
def keypress(self, size, key):
|
||||
just_maxcol = (size[0],)
|
||||
|
||||
# The size also includes the two bars, so subtract these.
|
||||
modified_size = (size[0], # cols
|
||||
size[1] - self._top_bar.rows(just_maxcol) - self._bottom_bar.rows(just_maxcol)) # rows
|
||||
|
||||
# Store the focus position before passing the keystroke to the contained list box. That way, it can be compared with the
|
||||
# position after the input is processed. If the list box body is empty, store None.
|
||||
focus_position_before_input = self.get_selected_position()
|
||||
|
||||
# A keystroke is changed to a modified one ('up' => 'ctrl up'). This prevents the widget from responding when the arrows
|
||||
# keys are used to navigate between widgets. That way it can be used in a 'urwid.Pile' or similar.
|
||||
if key == self._modifier_key.prepend_to("up"):
|
||||
key = self._pass_key_to_contained_listbox(modified_size, "up")
|
||||
|
||||
elif key == self._modifier_key.prepend_to("down"):
|
||||
key = self._pass_key_to_contained_listbox(modified_size, "down")
|
||||
|
||||
elif key == self._modifier_key.prepend_to("page up"):
|
||||
key = self._pass_key_to_contained_listbox(modified_size, "page up")
|
||||
|
||||
elif key == self._modifier_key.prepend_to("page down"):
|
||||
key = self._pass_key_to_contained_listbox(modified_size, "page down")
|
||||
|
||||
elif key == self._modifier_key.prepend_to("home"):
|
||||
# Check if the first list item is already selected.
|
||||
if (focus_position_before_input is not None) and (focus_position_before_input != 0):
|
||||
self.select_first_item()
|
||||
key = None
|
||||
elif not self._return_unused_navigation_input:
|
||||
key = None
|
||||
|
||||
elif key == self._modifier_key.prepend_to("end"):
|
||||
# Check if the last list item is already selected.
|
||||
if (focus_position_before_input is not None) and (focus_position_before_input != self.rearmost_position()):
|
||||
self.select_last_item()
|
||||
key = None
|
||||
elif not self._return_unused_navigation_input:
|
||||
key = None
|
||||
|
||||
elif key not in ("up", "down", "page up", "page down", "home", "end"):
|
||||
key = self._listbox.keypress(modified_size, key)
|
||||
|
||||
focus_position_after_input = self.get_selected_position()
|
||||
|
||||
# If the focus position has changed, execute the hook (if existing).
|
||||
if (focus_position_before_input != focus_position_after_input) and (self.on_selection_change is not None):
|
||||
self.on_selection_change(focus_position_before_input,
|
||||
focus_position_after_input)
|
||||
|
||||
return key
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
just_maxcol = (size[0],)
|
||||
|
||||
topBar_rows = self._top_bar.rows(just_maxcol)
|
||||
bottomBar_rows = self._bottom_bar.rows(just_maxcol)
|
||||
|
||||
# The size also includes the two bars, so subtract these.
|
||||
modified_size = (size[0], # cols
|
||||
size[1] - topBar_rows - bottomBar_rows) # rows
|
||||
|
||||
was_handeled = False
|
||||
|
||||
# An event is changed to a modified one ('mouse press' => 'ctrl mouse press'). This prevents the widget from responding
|
||||
# when mouse buttons are also used to navigate between widgets.
|
||||
if event == self._modifier_key.prepend_to("mouse press"):
|
||||
# Store the focus position before passing the input to the contained list box. That way, it can be compared with the
|
||||
# position after the input is processed. If the list box body is empty, store None.
|
||||
focus_position_before_input = self.get_selected_position()
|
||||
|
||||
# left mouse button, if not top bar or bottom bar.
|
||||
if (button == 1.0) and (topBar_rows <= row < (size[1] - bottomBar_rows)):
|
||||
# Because 'row' includes the top bar, the offset must be substracted before passing it to the contained list box.
|
||||
result = self._listbox.mouse_event(modified_size, event, button, col, (row - topBar_rows), focus)
|
||||
was_handeled = result if self._return_unused_navigation_input else True
|
||||
|
||||
# mousewheel up
|
||||
elif button == 4.0:
|
||||
was_handeled = self._pass_key_to_contained_listbox(modified_size, "page up")
|
||||
|
||||
# mousewheel down
|
||||
elif button == 5.0:
|
||||
was_handeled = self._pass_key_to_contained_listbox(modified_size, "page down")
|
||||
|
||||
focus_position_after_input = self.get_selected_position()
|
||||
|
||||
# If the focus position has changed, execute the hook (if existing).
|
||||
if (focus_position_before_input != focus_position_after_input) and (self.on_selection_change is not None):
|
||||
self.on_selection_change(focus_position_before_input,
|
||||
focus_position_after_input)
|
||||
|
||||
return was_handeled
|
||||
|
||||
# Pass the keystroke to the original widget. If it is not used, evaluate the corresponding variable to decide if it gets
|
||||
# swallowed or not.
|
||||
def _pass_key_to_contained_listbox(self, size, key):
|
||||
result = self._listbox.keypress(size, key)
|
||||
return result if self._return_unused_navigation_input else None
|
||||
|
||||
def get_body(self):
|
||||
return self._listbox.body
|
||||
|
||||
def body_len(self):
|
||||
return len(self.get_body())
|
||||
|
||||
def rearmost_position(self):
|
||||
return len(self.get_body()) - 1 # last valid index
|
||||
|
||||
def body_is_empty(self):
|
||||
return self.body_len() == 0
|
||||
|
||||
def position_is_valid(self, position):
|
||||
return (position < self.body_len()) and (position >= 0)
|
||||
|
||||
# If the passed position is valid, it is returned. Otherwise, the nearest valid position is returned. This ensures that
|
||||
# positions which are out of range do not result in an error.
|
||||
def _get_nearest_valid_position(self, position):
|
||||
if self.body_is_empty():
|
||||
return None
|
||||
|
||||
pos_type = type(position)
|
||||
|
||||
if pos_type == int:
|
||||
if self.position_is_valid(position):
|
||||
return position
|
||||
|
||||
elif position < 0:
|
||||
return 0
|
||||
|
||||
else:
|
||||
return self.rearmost_position()
|
||||
|
||||
elif pos_type == self.__class__.POSITION:
|
||||
if position == self.__class__.POSITION.LAST:
|
||||
return self.rearmost_position()
|
||||
|
||||
elif position == self.__class__.POSITION.MIDDLE:
|
||||
return self.body_len() // 2
|
||||
|
||||
elif position == self.__class__.POSITION.RANDOM:
|
||||
return random.randint(0, self.rearmost_position())
|
||||
|
||||
else:
|
||||
raise ValueError(self.__class__._VALUE_ERR_MSG.format(position))
|
||||
|
||||
else:
|
||||
raise TypeError(self.__class__._TYPE_ERR_MSG.format("<class 'int'> or <enum 'IndicativeListBox.POSITION'>",
|
||||
"'position'",
|
||||
pos_type))
|
||||
|
||||
def get_item(self, position):
|
||||
if self.position_is_valid(position):
|
||||
body = self.get_body()
|
||||
return body[position]
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_first_item(self):
|
||||
return self.get_item(0)
|
||||
|
||||
def get_last_item(self):
|
||||
return self.get_item(self.rearmost_position())
|
||||
|
||||
def get_selected_item(self):
|
||||
return self._listbox.focus
|
||||
|
||||
def get_selected_position(self):
|
||||
return self._listbox.focus_position if not self.body_is_empty() else None
|
||||
|
||||
def first_item_is_selected(self):
|
||||
return self.get_selected_position() == 0
|
||||
|
||||
def last_item_is_selected(self):
|
||||
return self.get_selected_position() == self.rearmost_position()
|
||||
|
||||
def _reset_highlighting(self):
|
||||
# Resets the appearance of the selected item to its original value, if off focus highlighting is active.
|
||||
if not self._last_focus_state and (self._original_item_attr_map is not None):
|
||||
self._listbox.focus.set_attr_map(self._original_item_attr_map)
|
||||
|
||||
# The next time the widget is rendered, the highlighting is redone.
|
||||
self._original_item_attr_map = None
|
||||
self._last_focus_state = None
|
||||
|
||||
def set_body(self, body, *, alternative_position=None):
|
||||
focus_position_before_change = self.get_selected_position()
|
||||
|
||||
self._reset_highlighting()
|
||||
|
||||
# Wrap each item in an 'urwid.AttrMap', if not already done.
|
||||
self._listbox.body[:] = [urwid.AttrMap(item, None) if not isinstance(item, urwid.AttrMap) else item
|
||||
for item in body]
|
||||
|
||||
# Normally it is tried to hold the focus position. If this is not desired, a position can be passed.
|
||||
if alternative_position is not None:
|
||||
nearest_valid_position = self._get_nearest_valid_position(alternative_position)
|
||||
|
||||
if nearest_valid_position is not None:
|
||||
# Because the widget has been re-rendered, the off focus highlighted item must be restored to its original state.
|
||||
self._reset_highlighting()
|
||||
|
||||
self._listbox.set_focus(nearest_valid_position)
|
||||
|
||||
# If an initialization is considered a selection change, execute the hook (if existing).
|
||||
if self._initialization_is_selection_change and (self.on_selection_change is not None):
|
||||
self.on_selection_change(focus_position_before_change,
|
||||
self.get_selected_position())
|
||||
|
||||
def select_item(self, position):
|
||||
focus_position_before_change = self.get_selected_position()
|
||||
|
||||
nearest_valid_position = self._get_nearest_valid_position(position)
|
||||
|
||||
# Focus the new position, if possible and not already focused.
|
||||
if (nearest_valid_position is not None) and (nearest_valid_position != focus_position_before_change):
|
||||
self._reset_highlighting()
|
||||
|
||||
self._listbox.set_focus(nearest_valid_position)
|
||||
|
||||
# Execute the hook (if existing).
|
||||
if (self.on_selection_change is not None):
|
||||
self.on_selection_change(focus_position_before_change,
|
||||
nearest_valid_position)
|
||||
|
||||
def select_first_item(self):
|
||||
self.select_item(0)
|
||||
|
||||
def select_last_item(self):
|
||||
self.select_item(self.rearmost_position())
|
||||
|
||||
def delete_position(self, position):
|
||||
# The saved properties get reseted, just in case that the appearance of the items differs.
|
||||
self._reset_highlighting()
|
||||
|
||||
body = self.get_body()
|
||||
del body[position]
|
||||
|
||||
def delete_selected_position(self):
|
||||
pos = self.get_selected_position()
|
||||
|
||||
# If the list body is not empty, delete the selected item.
|
||||
if pos is not None:
|
||||
self.delete_position(pos)
|
277
nomadnet/vendor/additional_urwid_widgets/widgets/integer_picker.py
vendored
Normal file
277
nomadnet/vendor/additional_urwid_widgets/widgets/integer_picker.py
vendored
Normal file
@ -0,0 +1,277 @@
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from ..assisting_modules.modifier_key import MODIFIER_KEY # pylint: disable=unused-import
|
||||
from .selectable_row import SelectableRow
|
||||
|
||||
import sys # pylint: disable=unused-import
|
||||
import urwid
|
||||
|
||||
|
||||
class IntegerPicker(urwid.WidgetWrap):
|
||||
"""Serves as a selector for integer numbers."""
|
||||
|
||||
def __init__(self, value, *, min_v=(-sys.maxsize - 1), max_v=sys.maxsize, step_len=1, jump_len=100, on_selection_change=None,
|
||||
initialization_is_selection_change=False, modifier_key=MODIFIER_KEY.NONE, ascending=True,
|
||||
return_unused_navigation_input=True, 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), display_syntax="{}", display_align="center", display_prop=(None, None)):
|
||||
assert (min_v <= max_v), "'min_v' must be less than or equal to 'max_v'."
|
||||
|
||||
assert (min_v <= value <= max_v), "'min_v <= value <= max_v' must be True."
|
||||
|
||||
self._value = value
|
||||
|
||||
self._minimum = min_v
|
||||
self._maximum = max_v
|
||||
|
||||
# Specifies how far to move in the respective direction when the keys 'up/down' are pressed.
|
||||
self._step_len = step_len
|
||||
|
||||
# Specifies how far to jump in the respective direction when the keys 'page up/down' or the mouse events 'wheel up/down'
|
||||
# are passed.
|
||||
self._jump_len = jump_len
|
||||
|
||||
# A hook which is triggered when the value changes.
|
||||
self.on_selection_change = on_selection_change
|
||||
|
||||
# 'MODIFIER_KEY' changes the behavior, so that the widget responds only to modified input. ('up' => 'ctrl up')
|
||||
self._modifier_key = modifier_key
|
||||
|
||||
# Specifies whether moving upwards represents a decrease or an increase of the value.
|
||||
self._ascending = ascending
|
||||
|
||||
# If the minimum has been reached and an attempt is made to select an even smaller value, the input is normally not
|
||||
# swallowed by the widget, but passed on so that other widgets can interpret it. This may result in transferring the focus.
|
||||
self._return_unused_navigation_input = return_unused_navigation_input
|
||||
|
||||
# The bars are just 'urwid.Text' widgets.
|
||||
self._top_bar = urwid.AttrMap(urwid.Text("", topBar_align),
|
||||
None)
|
||||
|
||||
self._bottom_bar = urwid.AttrMap(urwid.Text("", bottomBar_align),
|
||||
None)
|
||||
|
||||
# During the initialization of 'urwid.AttrMap', the value can be passed as non-dict. After initializing, its value can be
|
||||
# manipulated by passing a dict. The dicts I create below will be used later to change the appearance of the widgets.
|
||||
self._topBar_endCovered_markup = topBar_endCovered_prop[0]
|
||||
self._topBar_endCovered_focus = {None:topBar_endCovered_prop[1]}
|
||||
self._topBar_endCovered_offFocus = {None:topBar_endCovered_prop[2]}
|
||||
|
||||
self._topBar_endExposed_markup = topBar_endExposed_prop[0]
|
||||
self._topBar_endExposed_focus = {None:topBar_endExposed_prop[1]}
|
||||
self._topBar_endExposed_offFocus = {None:topBar_endExposed_prop[2]}
|
||||
|
||||
self._bottomBar_endCovered_markup = bottomBar_endCovered_prop[0]
|
||||
self._bottomBar_endCovered_focus = {None:bottomBar_endCovered_prop[1]}
|
||||
self._bottomBar_endCovered_offFocus = {None:bottomBar_endCovered_prop[2]}
|
||||
|
||||
self._bottomBar_endExposed_markup = bottomBar_endExposed_prop[0]
|
||||
self._bottomBar_endExposed_focus = {None:bottomBar_endExposed_prop[1]}
|
||||
self._bottomBar_endExposed_offFocus = {None:bottomBar_endExposed_prop[2]}
|
||||
|
||||
# Format the number before displaying it. That way it is easier to read.
|
||||
self._display_syntax = display_syntax
|
||||
|
||||
# The current value is displayed via this widget.
|
||||
self._display = SelectableRow([display_syntax.format(value)],
|
||||
align=display_align)
|
||||
|
||||
display_attr = urwid.AttrMap(self._display,
|
||||
display_prop[1],
|
||||
display_prop[0])
|
||||
|
||||
# wrap 'urwid.Pile'
|
||||
super().__init__(urwid.Pile([self._top_bar,
|
||||
display_attr,
|
||||
self._bottom_bar]))
|
||||
|
||||
# Is 'on_selection_change' triggered during the initialization?
|
||||
if initialization_is_selection_change and (on_selection_change is not None):
|
||||
on_selection_change(None, value)
|
||||
|
||||
def __repr__(self):
|
||||
return "{}(value='{}', min_v='{}', max_v='{}', ascending='{}')".format(self.__class__.__name__,
|
||||
self._value,
|
||||
self._minimum,
|
||||
self._maximum,
|
||||
self._ascending)
|
||||
|
||||
def render(self, size, focus=False):
|
||||
# Changes the appearance of the bar at the top depending on whether the upper limit is reached.
|
||||
if self._value == (self._minimum if self._ascending else self._maximum):
|
||||
self._top_bar.original_widget.set_text(self._topBar_endExposed_markup)
|
||||
self._top_bar.set_attr_map(self._topBar_endExposed_focus
|
||||
if focus else self._topBar_endExposed_offFocus)
|
||||
else:
|
||||
self._top_bar.original_widget.set_text(self._topBar_endCovered_markup)
|
||||
self._top_bar.set_attr_map(self._topBar_endCovered_focus
|
||||
if focus else self._topBar_endCovered_offFocus)
|
||||
|
||||
# Changes the appearance of the bar at the bottom depending on whether the lower limit is reached.
|
||||
if self._value == (self._maximum if self._ascending else self._minimum):
|
||||
self._bottom_bar.original_widget.set_text(self._bottomBar_endExposed_markup)
|
||||
self._bottom_bar.set_attr_map(self._bottomBar_endExposed_focus
|
||||
if focus else self._bottomBar_endExposed_offFocus)
|
||||
else:
|
||||
self._bottom_bar.original_widget.set_text(self._bottomBar_endCovered_markup)
|
||||
self._bottom_bar.set_attr_map(self._bottomBar_endCovered_focus
|
||||
if focus else self._bottomBar_endCovered_offFocus)
|
||||
|
||||
return super().render(size, focus=focus)
|
||||
|
||||
def keypress(self, size, key):
|
||||
# A keystroke is changed to a modified one ('up' => 'ctrl up'). This prevents the widget from responding when the arrows
|
||||
# keys are used to navigate between widgets. That way it can be used in a 'urwid.Pile' or similar.
|
||||
if key == self._modifier_key.prepend_to("up"):
|
||||
successful = self._change_value(-self._step_len)
|
||||
|
||||
elif key == self._modifier_key.prepend_to("down"):
|
||||
successful = self._change_value(self._step_len)
|
||||
|
||||
elif key == self._modifier_key.prepend_to("page up"):
|
||||
successful = self._change_value(-self._jump_len)
|
||||
|
||||
elif key == self._modifier_key.prepend_to("page down"):
|
||||
successful = self._change_value(self._jump_len)
|
||||
|
||||
elif key == self._modifier_key.prepend_to("home"):
|
||||
successful = self._change_value(float("-inf"))
|
||||
|
||||
elif key == self._modifier_key.prepend_to("end"):
|
||||
successful = self._change_value(float("inf"))
|
||||
|
||||
else:
|
||||
successful = False
|
||||
|
||||
return key if not successful else None
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
if focus:
|
||||
# An event is changed to a modified one ('mouse press' => 'ctrl mouse press'). This prevents the original widget from
|
||||
# responding when mouse buttons are also used to navigate between widgets.
|
||||
if event == self._modifier_key.prepend_to("mouse press"):
|
||||
# mousewheel up
|
||||
if button == 4.0:
|
||||
result = self._change_value(-self._jump_len)
|
||||
return result if self._return_unused_navigation_input else True
|
||||
|
||||
# mousewheel down
|
||||
elif button == 5.0:
|
||||
result = self._change_value(self._jump_len)
|
||||
return result if self._return_unused_navigation_input else True
|
||||
|
||||
return False
|
||||
|
||||
# This method tries to change the value depending on the desired arrangement and returns True if this change was successful.
|
||||
def _change_value(self, summand):
|
||||
value_before_input = self._value
|
||||
|
||||
if self._ascending:
|
||||
new_value = self._value + summand
|
||||
|
||||
if summand < 0:
|
||||
# If the corresponding limit has already been reached, then determine whether the unused input should be
|
||||
# returned or swallowed.
|
||||
if self._value == self._minimum:
|
||||
return not self._return_unused_navigation_input
|
||||
|
||||
# If the new value stays within the permitted range, use it.
|
||||
elif new_value > self._minimum:
|
||||
self._value = new_value
|
||||
|
||||
# The permitted range would be exceeded, so the limit is set instead.
|
||||
else:
|
||||
self._value = self._minimum
|
||||
|
||||
elif summand > 0:
|
||||
if self._value == self._maximum:
|
||||
return not self._return_unused_navigation_input
|
||||
|
||||
elif new_value < self._maximum:
|
||||
self._value = new_value
|
||||
|
||||
else:
|
||||
self._value = self._maximum
|
||||
else:
|
||||
new_value = self._value - summand
|
||||
|
||||
if summand < 0:
|
||||
if self._value == self._maximum:
|
||||
return not self._return_unused_navigation_input
|
||||
|
||||
elif new_value < self._maximum:
|
||||
self._value = new_value
|
||||
|
||||
else:
|
||||
self._value = self._maximum
|
||||
|
||||
elif summand > 0:
|
||||
if self._value == self._minimum:
|
||||
return not self._return_unused_navigation_input
|
||||
|
||||
elif new_value > self._minimum:
|
||||
self._value = new_value
|
||||
|
||||
else:
|
||||
self._value = self._minimum
|
||||
|
||||
# Update the displayed value.
|
||||
self._display.set_contents([self._display_syntax.format(self._value)])
|
||||
|
||||
# If the value has changed, execute the hook (if existing).
|
||||
if (value_before_input != self._value) and (self.on_selection_change is not None):
|
||||
self.on_selection_change(value_before_input,
|
||||
self._value)
|
||||
|
||||
return True
|
||||
|
||||
def get_value(self):
|
||||
return self._value
|
||||
|
||||
def set_value(self, value):
|
||||
if not (self._minimum <= value <= self._maximum):
|
||||
raise ValueError("'minimum <= value <= maximum' must be True.")
|
||||
|
||||
if value != self._value:
|
||||
value_before_change = self._value
|
||||
self._value = value
|
||||
|
||||
# Update the displayed value.
|
||||
self._display.set_contents([self._display_syntax.format(self._value)])
|
||||
|
||||
# Execute the hook (if existing).
|
||||
if (self.on_selection_change is not None):
|
||||
self.on_selection_change(value_before_change, self._value)
|
||||
|
||||
def set_to_minimum(self):
|
||||
self.set_value(self._minimum)
|
||||
|
||||
def set_to_maximum(self):
|
||||
self.set_value(self._maximum)
|
||||
|
||||
def set_minimum(self, new_min):
|
||||
if new_min > self._maximum:
|
||||
raise ValueError("'new_min' must be less than or equal to the maximum value.")
|
||||
|
||||
self._minimum = new_min
|
||||
|
||||
if self._value < new_min:
|
||||
self.set_to_minimum()
|
||||
|
||||
def set_maximum(self, new_max):
|
||||
if new_max < self._minimum:
|
||||
raise ValueError("'new_max' must be greater than or equal to the minimum value.")
|
||||
|
||||
self._maximum = new_max
|
||||
|
||||
if self._value > new_max:
|
||||
self.set_to_maximum()
|
||||
|
||||
def minimum_is_selected(self):
|
||||
return self._value == self._minimum
|
||||
|
||||
def maximum_is_selected(self):
|
||||
return self._value == self._maximum
|
||||
|
40
nomadnet/vendor/additional_urwid_widgets/widgets/message_dialog.py
vendored
Normal file
40
nomadnet/vendor/additional_urwid_widgets/widgets/message_dialog.py
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import urwid
|
||||
|
||||
|
||||
class MessageDialog(urwid.WidgetWrap):
|
||||
"""Wraps 'urwid.Overlay' to show a message and expects a reaction from the user."""
|
||||
|
||||
def __init__(self, contents, btns, overlay_size, *, contents_align="left", space_between_btns=2, title="", title_align="center",
|
||||
background=urwid.SolidFill("#"), overlay_align=("center", "middle"), overlay_min_size=(None, None), left=0, right=0,
|
||||
top=0, bottom=0):
|
||||
# Message part
|
||||
texts = [urwid.Text(content, align=contents_align)
|
||||
for content in contents]
|
||||
|
||||
# Lower part
|
||||
lower_part = [urwid.Divider(" "),
|
||||
urwid.Columns(btns, dividechars=space_between_btns)]
|
||||
|
||||
# frame
|
||||
line_box = urwid.LineBox(urwid.Pile(texts + lower_part),
|
||||
title=title,
|
||||
title_align=title_align)
|
||||
|
||||
# Wrap 'urwid.Overlay'
|
||||
super().__init__(urwid.Overlay(urwid.Filler(line_box),
|
||||
background,
|
||||
overlay_align[0],
|
||||
overlay_size[0],
|
||||
overlay_align[1],
|
||||
overlay_size[1],
|
||||
min_width=overlay_min_size[0],
|
||||
min_height=overlay_min_size[1],
|
||||
left=left,
|
||||
right=right,
|
||||
top=top,
|
||||
bottom=bottom))
|
||||
|
47
nomadnet/vendor/additional_urwid_widgets/widgets/selectable_row.py
vendored
Normal file
47
nomadnet/vendor/additional_urwid_widgets/widgets/selectable_row.py
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import urwid
|
||||
|
||||
|
||||
class SelectableRow(urwid.WidgetWrap):
|
||||
"""Wraps 'urwid.Columns' to make it selectable.
|
||||
This class has been slightly modified, but essentially corresponds to this class posted on stackoverflow.com:
|
||||
https://stackoverflow.com/questions/52106244/how-do-you-combine-multiple-tui-forms-to-write-more-complex-applications#answer-52174629"""
|
||||
|
||||
def __init__(self, contents, *, align="left", on_select=None, space_between=2):
|
||||
# A list-like object, where each element represents the value of a column.
|
||||
self.contents = contents
|
||||
|
||||
self._columns = urwid.Columns([urwid.Text(c, align=align) for c in contents],
|
||||
dividechars=space_between)
|
||||
|
||||
# Wrap 'urwid.Columns'.
|
||||
super().__init__(self._columns)
|
||||
|
||||
# A hook which defines the behavior that is executed when a specified key is pressed.
|
||||
self.on_select = on_select
|
||||
|
||||
def __repr__(self):
|
||||
return "{}(contents='{}')".format(self.__class__.__name__,
|
||||
self.contents)
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def keypress(self, size, key):
|
||||
if (key == "enter") and (self.on_select is not None):
|
||||
self.on_select(self)
|
||||
key = None
|
||||
|
||||
return key
|
||||
|
||||
def set_contents(self, contents):
|
||||
# Update the list record inplace...
|
||||
self.contents[:] = contents
|
||||
|
||||
# ... and update the displayed items.
|
||||
for t, (w, _) in zip(contents, self._columns.contents):
|
||||
w.set_text(t)
|
||||
|
Loading…
Reference in New Issue
Block a user