Files
NomadNet/nomadnet/vendor/additional_urwid_widgets/widgets/integer_picker.py
2021-05-04 15:10:02 +02:00

277 lines
13 KiB
Python

#! /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