Skip to content

Commit 043a583

Browse files
committed
Add list proxy
1 parent 2f908d3 commit 043a583

File tree

7 files changed

+107
-8
lines changed

7 files changed

+107
-8
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ LATEST
1010
- Added mouse scroll wheel support.
1111
- Added reset() to Renderers.
1212
- Added type hints to source code.
13+
- Textbox value now supports list methods - e.g. `TextBox.value.append("foo")` will change the value.
1314
- Fixed double-width alignment issues on DropdownList.
1415
- Improved TextBox line wrapping to break on word boundaries.
1516
- Fixed logic for highlighting selected widget controls without focus.

asciimatics/widgets/filebrowser.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,9 @@ def _populate_list(self, value: str):
145145
name = f"|-- {my_file} -> {real_path}"
146146

147147
# Normalize names for MacOS and then add to the list.
148-
tree.append(([
149-
unicodedata.normalize("NFC", name),
150-
readable_mem(details.st_size),
151-
readable_timestamp(details.st_mtime)
152-
],
148+
tree.append(([unicodedata.normalize("NFC", name),
149+
readable_mem(details.st_size),
150+
readable_timestamp(details.st_mtime)],
153151
full_path))
154152

155153
tree_view.extend(sorted(tree_dirs))

asciimatics/widgets/textbox.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""This module implements a multi line editing text box"""
22
from __future__ import annotations
33
from copy import copy
4+
from functools import partial
45
from logging import getLogger
56
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union
7+
from wrapt import ObjectProxy
68
from asciimatics.event import KeyboardEvent, MouseEvent, Event
79
from asciimatics.screen import Screen
810
from asciimatics.strings import ColouredText
@@ -15,6 +17,37 @@
1517
logger = getLogger(__name__)
1618

1719

20+
class _ListWrapper(ObjectProxy): # pylint: disable=abstract-method
21+
"""
22+
A proxy for list objects.
23+
24+
This class can be returned by the `value` property, allowing the caller to modify it via standard
25+
list modifications. For example, `textbox.value.append("foo")` will now update the value of the textbox.
26+
"""
27+
28+
def __init__(self, lst: List, parent: TextBox):
29+
super().__init__(lst)
30+
self._self_parent = parent
31+
32+
def append(self, stuff):
33+
self._self_parent.proxy_update(partial(self.__wrapped__.append, stuff))
34+
35+
def extend(self, stuff):
36+
self._self_parent.proxy_update(partial(self.__wrapped__.extend, stuff))
37+
38+
def insert(self, key, value):
39+
self._self_parent.proxy_update(partial(self.__wrapped__.insert, key, value))
40+
41+
def __setitem__(self, key, value):
42+
self._self_parent.proxy_update(partial(self.__wrapped__.__setitem__, key, value))
43+
44+
def __delitem__(self, key):
45+
self._self_parent.proxy_update(partial(self.__wrapped__.__delitem__, key))
46+
47+
def clear(self):
48+
self._self_parent.proxy_update(self.__wrapped__.clear)
49+
50+
1851
class TextBox(Widget):
1952
"""
2053
A TextBox is a widget for multi-line text editing.
@@ -146,8 +179,10 @@ def reset(self):
146179
self._start_column = 0
147180
if self._auto_scroll or self._line > len(self._value) - 1:
148181
self._line = len(self._value) - 1
149-
150-
self._column = 0 if self._is_disabled else len(self._value[self._line])
182+
try:
183+
self._column = 0 if self._is_disabled else len(self._value[self._line])
184+
except IndexError:
185+
self._column = 0
151186
self._reflowed_text_cache = None
152187

153188
def _change_line(self, delta: int):
@@ -347,10 +382,13 @@ def auto_scroll(self, new_value):
347382
def value(self):
348383
"""
349384
The current value for this TextBox.
385+
386+
NOTE: this now uses a proxy to allow insertion and other modifications.
350387
"""
351388
if self._value is None:
352389
self._value = [""]
353-
return "\n".join([str(x) for x in self._value]) if self._as_string else self._value
390+
return "\n".join([str(x)
391+
for x in self._value]) if self._as_string else _ListWrapper(self._value, self)
354392

355393
@value.setter
356394
def value(self, new_value):
@@ -360,8 +398,23 @@ def value(self, new_value):
360398
new_value = [""]
361399
elif self._as_string:
362400
new_value = new_value.split("\n")
401+
elif isinstance(new_value, _ListWrapper):
402+
new_value = new_value.__wrapped__
363403
self._value = new_value
404+
self._handle_changes(old_value)
405+
406+
def proxy_update(self, make_update):
407+
"""
408+
Handle an update from the proxy value property object.
409+
410+
This is an internal method that processes potential updates to the value of the TextBox. It
411+
should never be called directly by an application.
412+
"""
413+
old_value = list(self._value)
414+
make_update()
415+
self._handle_changes(old_value)
364416

417+
def _handle_changes(self, old_value):
365418
# TODO: Sort out speed of this code
366419
if self._parser:
367420
new_value = []

mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
packages = asciimatics
33
check_untyped_defs = True
44

5+
[mypy-wrapt.*]
6+
ignore_missing_imports = True
7+
58
[mypy-wcwidth.*]
69
ignore_missing_imports = True
710

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ dependencies = [
5555
'pyfiglet >= 0.7.2',
5656
'Pillow >= 2.7.0',
5757
'wcwidth',
58+
'wrapt',
5859
"pywin32 >= 1.0; platform_system=='Windows'",
5960
]
6061
requires-python = ">= 3.8"

requirements/base.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
wcwidth
2+
wrapt
23
pyfiglet >= 0.7.2
34
setuptools_scm

tests/test_widgets.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3764,6 +3764,48 @@ def test_textbox_autoscroll(self):
37643764
"| |\n" +
37653765
"+--------------------------------------+\n")
37663766

3767+
def test_textbox_proxy(self):
3768+
"""
3769+
Check the list property supports normal list operators.
3770+
"""
3771+
def _on_change():
3772+
self._new_value = [x for x in self._test_widget.value]
3773+
3774+
# Now set up the Frame ready for testing
3775+
self._new_value = None
3776+
screen = MagicMock(spec=Screen, colours=8, unicode_aware=False)
3777+
scene = Scene([], duration=-1)
3778+
canvas = Canvas(screen, 10, 40, 0, 0)
3779+
form = Frame(canvas, canvas.height, canvas.width)
3780+
layout = Layout([100])
3781+
form.add_layout(layout)
3782+
self._test_widget = textbox = TextBox(4, label="TB", on_change=_on_change)
3783+
textbox.value = ["Foo"]
3784+
layout.add_widget(textbox)
3785+
form.fix()
3786+
scene.add_effect(form)
3787+
scene.reset()
3788+
3789+
# Check returned list proxies updates to the form.
3790+
textbox.value.append("Bar")
3791+
self.assertEqual(textbox.value, ["Foo", "Bar"])
3792+
self.assertEqual(textbox.value, self._new_value)
3793+
textbox.value.insert(1, "World")
3794+
self.assertEqual(textbox.value, ["Foo", "World", "Bar"])
3795+
self.assertEqual(textbox.value, self._new_value)
3796+
textbox.value[0] = "Hello"
3797+
self.assertEqual(textbox.value, ["Hello", "World", "Bar"])
3798+
self.assertEqual(textbox.value, self._new_value)
3799+
del textbox.value[2]
3800+
self.assertEqual(textbox.value, ["Hello", "World"])
3801+
self.assertEqual(textbox.value, self._new_value)
3802+
textbox.value.extend(["Foo", "Bar"])
3803+
self.assertEqual(textbox.value, ["Hello", "World", "Foo", "Bar"])
3804+
self.assertEqual(textbox.value, self._new_value)
3805+
textbox.value.clear()
3806+
self.assertEqual(textbox.value, [])
3807+
self.assertEqual(textbox.value, self._new_value)
3808+
37673809
def test_layout_gutters(self):
37683810
"""
37693811
Validate that the gutters parameter works for Layouts

0 commit comments

Comments
 (0)