1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
|
"""This is the baseclass for list box types"""
from datetime import datetime, timedelta
from abc import ABCMeta, abstractmethod
from asciimatics.event import KeyboardEvent, MouseEvent
from asciimatics.screen import Screen
from asciimatics.widgets.widget import Widget
from asciimatics.widgets.scrollbar import _ScrollBar
class _BaseListBox(Widget, metaclass=ABCMeta):
"""
An Internal class to contain common function between list box types.
"""
__slots__ = ["_options", "_titles", "_line", "_start_line", "_required_height", "_on_change",
"_on_select", "_validator", "_search", "_last_search", "_scroll_bar", "_parser"]
def __init__(self, height, options, titles=None, label=None, name=None, parser=None,
on_change=None, on_select=None, validator=None):
"""
:param height: The required number of input lines for this widget.
:param options: The options for each row in the widget.
:param label: An optional label for the widget.
:param name: The name for the widget.
:param parser: Optional parser to colour text.
:param on_change: Optional function to call when selection changes.
:param on_select: Optional function to call when the user actually selects an entry from
this list - e.g. by double-clicking or pressing Enter.
:param validator: Optional function to validate selection for this widget.
"""
super().__init__(name)
self._titles = titles
self._label = label
self._parser = parser
self._options = self._parse_options(options)
self._line = 0
self._value = None
self._start_line = 0
self._required_height = height
self._on_change = on_change
self._on_select = on_select
self._validator = validator
self._search = ""
self._last_search = datetime.now()
self._scroll_bar = None
def reset(self):
pass
def process_event(self, event):
if isinstance(event, KeyboardEvent):
if len(self._options) > 0 and event.key_code == Screen.KEY_UP:
# Move up one line in text - use value to trigger on_select.
self._line = max(0, self._line - 1)
self.value = self._options[self._line][1]
elif len(self._options) > 0 and event.key_code == Screen.KEY_DOWN:
# Move down one line in text - use value to trigger on_select.
self._line = min(len(self._options) - 1, self._line + 1)
self.value = self._options[self._line][1]
elif len(self._options) > 0 and event.key_code == Screen.KEY_PAGE_UP:
# Move up one page.
self._line = max(0, self._line - self._h + (1 if self._titles else 0))
self.value = self._options[self._line][1]
elif len(self._options) > 0 and event.key_code == Screen.KEY_PAGE_DOWN:
# Move down one page.
self._line = min(
len(self._options) - 1, self._line + self._h - (1 if self._titles else 0))
self.value = self._options[self._line][1]
elif event.key_code in [Screen.ctrl("m"), Screen.ctrl("j")]:
# Fire select callback.
if self._on_select:
self._on_select()
elif event.key_code > 0:
# Treat any other normal press as a search
now = datetime.now()
if now - self._last_search >= timedelta(seconds=1):
self._search = ""
self._search += chr(event.key_code)
self._last_search = now
# If we find a new match for the search string, update the list selection
new_value = self._find_option(self._search)
if new_value is not None:
self.value = new_value
else:
return event
elif isinstance(event, MouseEvent):
# Mouse event - adjust for scroll bar as needed.
if event.buttons != 0:
# Check for normal widget.
if (len(self._options) > 0 and
self.is_mouse_over(event, include_label=False,
width_modifier=1 if self._scroll_bar else 0)):
# Figure out selected line
new_line = event.y - self._y + self._start_line
if self._titles:
new_line -= 1
new_line = min(new_line, len(self._options) - 1)
# Update selection and fire select callback if needed.
if new_line >= 0:
self._line = new_line
self.value = self._options[self._line][1]
if event.buttons & MouseEvent.DOUBLE_CLICK != 0 and self._on_select:
self._on_select()
return None
# Check for scroll bar interactions:
if self._scroll_bar:
if self._scroll_bar.process_event(event):
return None
# Ignore other mouse events.
return event
else:
# Ignore other events
return event
# If we got here, we processed the event - swallow it.
return None
def _add_or_remove_scrollbar(self, width, height, dy):
"""
Add or remove a scrollbar from this listbox based on height and available options.
:param width: Width of the Listbox
:param height: Height of the Listbox.
:param dy: Vertical offset from top of widget.
"""
if self._scroll_bar is None and len(self._options) > height:
self._scroll_bar = _ScrollBar(
self._frame.canvas, self._frame.palette, self._x + width - 1, self._y + dy,
height, self._get_pos, self._set_pos)
elif self._scroll_bar is not None and len(self._options) <= height:
self._scroll_bar = None
def _get_pos(self):
"""
Get current position for scroll bar.
"""
if self._h >= len(self._options):
return 0
return self._start_line / (len(self._options) - self._h)
def _set_pos(self, pos):
"""
Set current position for scroll bar.
"""
if self._h < len(self._options):
pos *= len(self._options) - self._h
pos = int(round(max(0, pos), 0))
self._start_line = pos
@abstractmethod
def _find_option(self, search_value):
"""
Internal function called by the BaseListBox to do a text search on user input.
:param search_value: The string value to search for in the list.
:return: The value of the matching option (or None if nothing matches).
"""
def required_height(self, offset, width):
return self._required_height
@property
def start_line(self):
"""
The line that will be drawn at the top of the visible section of this list.
"""
return self._start_line
@start_line.setter
def start_line(self, new_value):
if 0 <= new_value < len(self._options):
self._start_line = new_value
@property
def value(self):
"""
The current value for this list box.
"""
return self._value
@value.setter
def value(self, new_value):
# Only trigger change notification after we've changed selection
old_value = self._value
self._value = new_value
for i, [_, value] in enumerate(self._options):
if value == new_value:
self._line = i
break
else:
# No matching value - pick a default.
if len(self._options) > 0:
self._line = 0
self._value = self._options[self._line][1]
else:
self._line = -1
self._value = None
if self._validator:
self._is_valid = self._validator(self._value)
if old_value != self._value and self._on_change:
self._on_change()
# Fix up the start line now that we've explicitly set a new value.
self._start_line = max(0, self._line - self._h + 1, min(self._start_line, self._line))
def _parse_options(self, options):
"""
Parse a the options list for ColouredText.
:param options: the options list to parse
:returns: the options list parsed and converted to ColouredText as needed.
"""
if self._parser:
parsed_value = []
for option in options:
parsed_value.append((self._parse_option(option[0]), option[1]))
return parsed_value
return options
@abstractmethod
def _parse_option(self, option):
"""
Parse a single option for ColouredText.
:param option: the option to parse
:returns: the option parsed and converted to ColouredText.
"""
@property
@abstractmethod
def options(self):
"""
The list of options available for user selection.
"""
|