urwid 2.6.0.post0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of urwid might be problematic. Click here for more details.
- urwid/__init__.py +333 -0
- urwid/canvas.py +1413 -0
- urwid/command_map.py +137 -0
- urwid/container.py +59 -0
- urwid/decoration.py +65 -0
- urwid/display/__init__.py +97 -0
- urwid/display/_posix_raw_display.py +413 -0
- urwid/display/_raw_display_base.py +914 -0
- urwid/display/_web.css +12 -0
- urwid/display/_web.js +462 -0
- urwid/display/_win32.py +171 -0
- urwid/display/_win32_raw_display.py +269 -0
- urwid/display/common.py +1219 -0
- urwid/display/curses.py +690 -0
- urwid/display/escape.py +624 -0
- urwid/display/html_fragment.py +251 -0
- urwid/display/lcd.py +518 -0
- urwid/display/raw.py +37 -0
- urwid/display/web.py +636 -0
- urwid/event_loop/__init__.py +55 -0
- urwid/event_loop/abstract_loop.py +175 -0
- urwid/event_loop/asyncio_loop.py +231 -0
- urwid/event_loop/glib_loop.py +294 -0
- urwid/event_loop/main_loop.py +721 -0
- urwid/event_loop/select_loop.py +230 -0
- urwid/event_loop/tornado_loop.py +206 -0
- urwid/event_loop/trio_loop.py +302 -0
- urwid/event_loop/twisted_loop.py +269 -0
- urwid/event_loop/zmq_loop.py +275 -0
- urwid/font.py +695 -0
- urwid/graphics.py +96 -0
- urwid/highlight.css +19 -0
- urwid/listbox.py +1899 -0
- urwid/monitored_list.py +522 -0
- urwid/numedit.py +376 -0
- urwid/signals.py +330 -0
- urwid/split_repr.py +130 -0
- urwid/str_util.py +358 -0
- urwid/text_layout.py +632 -0
- urwid/treetools.py +515 -0
- urwid/util.py +557 -0
- urwid/version.py +16 -0
- urwid/vterm.py +1806 -0
- urwid/widget/__init__.py +181 -0
- urwid/widget/attr_map.py +161 -0
- urwid/widget/attr_wrap.py +140 -0
- urwid/widget/bar_graph.py +649 -0
- urwid/widget/big_text.py +77 -0
- urwid/widget/box_adapter.py +126 -0
- urwid/widget/columns.py +1145 -0
- urwid/widget/constants.py +574 -0
- urwid/widget/container.py +227 -0
- urwid/widget/divider.py +110 -0
- urwid/widget/edit.py +718 -0
- urwid/widget/filler.py +403 -0
- urwid/widget/frame.py +539 -0
- urwid/widget/grid_flow.py +539 -0
- urwid/widget/line_box.py +194 -0
- urwid/widget/overlay.py +829 -0
- urwid/widget/padding.py +597 -0
- urwid/widget/pile.py +971 -0
- urwid/widget/popup.py +170 -0
- urwid/widget/progress_bar.py +141 -0
- urwid/widget/scrollable.py +597 -0
- urwid/widget/solid_fill.py +44 -0
- urwid/widget/text.py +354 -0
- urwid/widget/widget.py +852 -0
- urwid/widget/widget_decoration.py +166 -0
- urwid/widget/wimp.py +792 -0
- urwid/wimp.py +23 -0
- urwid-2.6.0.post0.dist-info/COPYING +504 -0
- urwid-2.6.0.post0.dist-info/METADATA +332 -0
- urwid-2.6.0.post0.dist-info/RECORD +75 -0
- urwid-2.6.0.post0.dist-info/WHEEL +5 -0
- urwid-2.6.0.post0.dist-info/top_level.txt +1 -0
urwid/widget/edit.py
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import string
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from urwid import text_layout
|
|
7
|
+
from urwid.canvas import CompositeCanvas
|
|
8
|
+
from urwid.command_map import Command
|
|
9
|
+
from urwid.split_repr import remove_defaults
|
|
10
|
+
from urwid.str_util import is_wide_char, move_next_char, move_prev_char
|
|
11
|
+
from urwid.util import decompose_tagmarkup
|
|
12
|
+
|
|
13
|
+
from .constants import Align, Sizing, WrapMode
|
|
14
|
+
from .text import Text, TextError
|
|
15
|
+
|
|
16
|
+
if typing.TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Hashable
|
|
18
|
+
|
|
19
|
+
from typing_extensions import Literal
|
|
20
|
+
|
|
21
|
+
from urwid.canvas import TextCanvas
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EditError(TextError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Edit(Text):
|
|
29
|
+
"""
|
|
30
|
+
Text editing widget implements cursor movement, text insertion and
|
|
31
|
+
deletion. A caption may prefix the editing area. Uses text class
|
|
32
|
+
for text layout.
|
|
33
|
+
|
|
34
|
+
Users of this class may listen for ``"change"`` or ``"postchange"``
|
|
35
|
+
events. See :func:``connect_signal``.
|
|
36
|
+
|
|
37
|
+
* ``"change"`` is sent just before the value of edit_text changes.
|
|
38
|
+
It receives the new text as an argument. Note that ``"change"`` cannot
|
|
39
|
+
change the text in question as edit_text changes the text afterwards.
|
|
40
|
+
* ``"postchange"`` is sent after the value of edit_text changes.
|
|
41
|
+
It receives the old value of the text as an argument and thus is
|
|
42
|
+
appropriate for changing the text. It is possible for a ``"postchange"``
|
|
43
|
+
event handler to get into a loop of changing the text and then being
|
|
44
|
+
called when the event is re-emitted. It is up to the event
|
|
45
|
+
handler to guard against this case (for instance, by not changing the
|
|
46
|
+
text if it is signaled for text that it has already changed once).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
_sizing = frozenset([Sizing.FLOW])
|
|
50
|
+
_selectable = True
|
|
51
|
+
ignore_focus = False
|
|
52
|
+
# (this variable is picked up by the MetaSignals metaclass)
|
|
53
|
+
signals: typing.ClassVar[list[str]] = ["change", "postchange"]
|
|
54
|
+
|
|
55
|
+
def valid_char(self, ch: str) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
Filter for text that may be entered into this widget by the user
|
|
58
|
+
|
|
59
|
+
:param ch: character to be inserted
|
|
60
|
+
:type ch: str
|
|
61
|
+
|
|
62
|
+
This implementation returns True for all printable characters.
|
|
63
|
+
"""
|
|
64
|
+
return is_wide_char(ch, 0) or (len(ch) == 1 and ord(ch) >= 32)
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
caption: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]] = "",
|
|
69
|
+
edit_text: str = "",
|
|
70
|
+
multiline: bool = False,
|
|
71
|
+
align: Literal["left", "center", "right"] | Align = Align.LEFT,
|
|
72
|
+
wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,
|
|
73
|
+
allow_tab: bool = False,
|
|
74
|
+
edit_pos: int | None = None,
|
|
75
|
+
layout: text_layout.TextLayout = None,
|
|
76
|
+
mask: str | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""
|
|
79
|
+
:param caption: markup for caption preceding edit_text, see
|
|
80
|
+
:class:`Text` for description of text markup.
|
|
81
|
+
:type caption: text markup
|
|
82
|
+
:param edit_text: initial text for editing, type (bytes or unicode)
|
|
83
|
+
must match the text in the caption
|
|
84
|
+
:type edit_text: bytes or unicode
|
|
85
|
+
:param multiline: True: 'enter' inserts newline False: return it
|
|
86
|
+
:type multiline: bool
|
|
87
|
+
:param align: typically 'left', 'center' or 'right'
|
|
88
|
+
:type align: text alignment mode
|
|
89
|
+
:param wrap: typically 'space', 'any' or 'clip'
|
|
90
|
+
:type wrap: text wrapping mode
|
|
91
|
+
:param allow_tab: True: 'tab' inserts 1-8 spaces False: return it
|
|
92
|
+
:type allow_tab: bool
|
|
93
|
+
:param edit_pos: initial position for cursor, None:end of edit_text
|
|
94
|
+
:type edit_pos: int
|
|
95
|
+
:param layout: defaults to a shared :class:`StandardTextLayout` instance
|
|
96
|
+
:type layout: text layout instance
|
|
97
|
+
:param mask: hide text entered with this character, None:disable mask
|
|
98
|
+
:type mask: bytes or unicode
|
|
99
|
+
|
|
100
|
+
>>> Edit()
|
|
101
|
+
<Edit selectable flow widget '' edit_pos=0>
|
|
102
|
+
>>> Edit(u"Y/n? ", u"yes")
|
|
103
|
+
<Edit selectable flow widget 'yes' caption='Y/n? ' edit_pos=3>
|
|
104
|
+
>>> Edit(u"Name ", u"Smith", edit_pos=1)
|
|
105
|
+
<Edit selectable flow widget 'Smith' caption='Name ' edit_pos=1>
|
|
106
|
+
>>> Edit(u"", u"3.14", align='right')
|
|
107
|
+
<Edit selectable flow widget '3.14' align='right' edit_pos=4>
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
super().__init__("", align, wrap, layout)
|
|
111
|
+
self.multiline = multiline
|
|
112
|
+
self.allow_tab = allow_tab
|
|
113
|
+
self._edit_pos = 0
|
|
114
|
+
self._caption, self._attrib = decompose_tagmarkup(caption)
|
|
115
|
+
self._edit_text = ""
|
|
116
|
+
self.highlight: tuple[int, int] | None = None
|
|
117
|
+
self.set_edit_text(edit_text)
|
|
118
|
+
if edit_pos is None:
|
|
119
|
+
edit_pos = len(edit_text)
|
|
120
|
+
self.set_edit_pos(edit_pos)
|
|
121
|
+
self.set_mask(mask)
|
|
122
|
+
self._shift_view_to_cursor = False
|
|
123
|
+
|
|
124
|
+
def _repr_words(self) -> list[str]:
|
|
125
|
+
return (
|
|
126
|
+
super()._repr_words()[:-1]
|
|
127
|
+
+ [repr(self._edit_text)]
|
|
128
|
+
+ [f"caption={self._caption!r}"] * bool(self._caption)
|
|
129
|
+
+ ["multiline"] * (self.multiline is True)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _repr_attrs(self) -> dict[str, typing.Any]:
|
|
133
|
+
attrs = {**super()._repr_attrs(), "edit_pos": self._edit_pos}
|
|
134
|
+
return remove_defaults(attrs, Edit.__init__)
|
|
135
|
+
|
|
136
|
+
def get_text(self) -> tuple[str | bytes, list[tuple[Hashable, int]]]:
|
|
137
|
+
"""
|
|
138
|
+
Returns ``(text, display attributes)``. See :meth:`Text.get_text`
|
|
139
|
+
for details.
|
|
140
|
+
|
|
141
|
+
Text returned includes the caption and edit_text, possibly masked.
|
|
142
|
+
|
|
143
|
+
>>> Edit(u"What? ","oh, nothing.").get_text()
|
|
144
|
+
('What? oh, nothing.', [])
|
|
145
|
+
>>> Edit(('bright',u"user@host:~$ "),"ls").get_text()
|
|
146
|
+
('user@host:~$ ls', [('bright', 13)])
|
|
147
|
+
>>> Edit(u"password:", u"seekrit", mask=u"*").get_text()
|
|
148
|
+
('password:*******', [])
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
if self._mask is None:
|
|
152
|
+
return self._caption + self._edit_text, self._attrib
|
|
153
|
+
|
|
154
|
+
return self._caption + (self._mask * len(self._edit_text)), self._attrib
|
|
155
|
+
|
|
156
|
+
def set_text(self, markup: tuple[str, list[tuple[Hashable, int]]]) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Not supported by Edit widget.
|
|
159
|
+
|
|
160
|
+
>>> Edit().set_text("test")
|
|
161
|
+
Traceback (most recent call last):
|
|
162
|
+
EditError: set_text() not supported. Use set_caption() or set_edit_text() instead.
|
|
163
|
+
"""
|
|
164
|
+
# FIXME: this smells. reimplement Edit as a WidgetWrap subclass to
|
|
165
|
+
# clean this up
|
|
166
|
+
|
|
167
|
+
# hack to let Text.__init__() work
|
|
168
|
+
if not hasattr(self, "_text") and markup == "": # noqa: PLC1901,RUF100
|
|
169
|
+
self._text = None
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
raise EditError("set_text() not supported. Use set_caption() or set_edit_text() instead.")
|
|
173
|
+
|
|
174
|
+
def get_pref_col(self, size: tuple[int]) -> int:
|
|
175
|
+
"""
|
|
176
|
+
Return the preferred column for the cursor, or the
|
|
177
|
+
current cursor x value. May also return ``'left'`` or ``'right'``
|
|
178
|
+
to indicate the leftmost or rightmost column available.
|
|
179
|
+
|
|
180
|
+
This method is used internally and by other widgets when
|
|
181
|
+
moving the cursor up or down between widgets so that the
|
|
182
|
+
column selected is one that the user would expect.
|
|
183
|
+
|
|
184
|
+
>>> size = (10,)
|
|
185
|
+
>>> Edit().get_pref_col(size)
|
|
186
|
+
0
|
|
187
|
+
>>> e = Edit(u"", u"word")
|
|
188
|
+
>>> e.get_pref_col(size)
|
|
189
|
+
4
|
|
190
|
+
>>> e.keypress(size, 'left')
|
|
191
|
+
>>> e.get_pref_col(size)
|
|
192
|
+
3
|
|
193
|
+
>>> e.keypress(size, 'end')
|
|
194
|
+
>>> e.get_pref_col(size)
|
|
195
|
+
<Align.RIGHT: 'right'>
|
|
196
|
+
>>> e = Edit(u"", u"2\\nwords")
|
|
197
|
+
>>> e.keypress(size, 'left')
|
|
198
|
+
>>> e.keypress(size, 'up')
|
|
199
|
+
>>> e.get_pref_col(size)
|
|
200
|
+
4
|
|
201
|
+
>>> e.keypress(size, 'left')
|
|
202
|
+
>>> e.get_pref_col(size)
|
|
203
|
+
0
|
|
204
|
+
"""
|
|
205
|
+
(maxcol,) = size
|
|
206
|
+
pref_col, then_maxcol = self.pref_col_maxcol
|
|
207
|
+
if then_maxcol != maxcol:
|
|
208
|
+
return self.get_cursor_coords((maxcol,))[0]
|
|
209
|
+
|
|
210
|
+
return pref_col
|
|
211
|
+
|
|
212
|
+
def set_caption(self, caption: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]) -> None:
|
|
213
|
+
"""
|
|
214
|
+
Set the caption markup for this widget.
|
|
215
|
+
|
|
216
|
+
:param caption: markup for caption preceding edit_text, see
|
|
217
|
+
:meth:`Text.__init__` for description of text markup.
|
|
218
|
+
|
|
219
|
+
>>> e = Edit("")
|
|
220
|
+
>>> e.set_caption("cap1")
|
|
221
|
+
>>> print(e.caption)
|
|
222
|
+
cap1
|
|
223
|
+
>>> e.set_caption(('bold', "cap2"))
|
|
224
|
+
>>> print(e.caption)
|
|
225
|
+
cap2
|
|
226
|
+
>>> e.attrib
|
|
227
|
+
[('bold', 4)]
|
|
228
|
+
>>> e.caption = "cap3" # not supported because caption stores text but set_caption() takes markup
|
|
229
|
+
Traceback (most recent call last):
|
|
230
|
+
AttributeError: can't set attribute
|
|
231
|
+
"""
|
|
232
|
+
self._caption, self._attrib = decompose_tagmarkup(caption)
|
|
233
|
+
self._invalidate()
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def caption(self) -> str:
|
|
237
|
+
"""
|
|
238
|
+
Read-only property returning the caption for this widget.
|
|
239
|
+
"""
|
|
240
|
+
return self._caption
|
|
241
|
+
|
|
242
|
+
def set_edit_pos(self, pos: int) -> None:
|
|
243
|
+
"""
|
|
244
|
+
Set the cursor position with a self.edit_text offset.
|
|
245
|
+
Clips pos to [0, len(edit_text)].
|
|
246
|
+
|
|
247
|
+
:param pos: cursor position
|
|
248
|
+
:type pos: int
|
|
249
|
+
|
|
250
|
+
>>> e = Edit(u"", u"word")
|
|
251
|
+
>>> e.edit_pos
|
|
252
|
+
4
|
|
253
|
+
>>> e.set_edit_pos(2)
|
|
254
|
+
>>> e.edit_pos
|
|
255
|
+
2
|
|
256
|
+
>>> e.edit_pos = -1 # Urwid 0.9.9 or later
|
|
257
|
+
>>> e.edit_pos
|
|
258
|
+
0
|
|
259
|
+
>>> e.edit_pos = 20
|
|
260
|
+
>>> e.edit_pos
|
|
261
|
+
4
|
|
262
|
+
"""
|
|
263
|
+
pos = min(max(pos, 0), len(self._edit_text))
|
|
264
|
+
self.highlight = None
|
|
265
|
+
self.pref_col_maxcol = None, None
|
|
266
|
+
self._edit_pos = pos
|
|
267
|
+
self._invalidate()
|
|
268
|
+
|
|
269
|
+
edit_pos = property(
|
|
270
|
+
lambda self: self._edit_pos,
|
|
271
|
+
set_edit_pos,
|
|
272
|
+
doc="""
|
|
273
|
+
Property controlling the edit position for this widget.
|
|
274
|
+
""",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def set_mask(self, mask: str | None) -> None:
|
|
278
|
+
"""
|
|
279
|
+
Set the character for masking text away.
|
|
280
|
+
|
|
281
|
+
:param mask: hide text entered with this character, None:disable mask
|
|
282
|
+
:type mask: bytes or unicode
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
self._mask = mask
|
|
286
|
+
self._invalidate()
|
|
287
|
+
|
|
288
|
+
def set_edit_text(self, text: str) -> None:
|
|
289
|
+
"""
|
|
290
|
+
Set the edit text for this widget.
|
|
291
|
+
|
|
292
|
+
:param text: text for editing, type (bytes or unicode)
|
|
293
|
+
must match the text in the caption
|
|
294
|
+
:type text: bytes or unicode
|
|
295
|
+
|
|
296
|
+
>>> e = Edit()
|
|
297
|
+
>>> e.set_edit_text(u"yes")
|
|
298
|
+
>>> print(e.edit_text)
|
|
299
|
+
yes
|
|
300
|
+
>>> e
|
|
301
|
+
<Edit selectable flow widget 'yes' edit_pos=0>
|
|
302
|
+
>>> e.edit_text = u"no" # Urwid 0.9.9 or later
|
|
303
|
+
>>> print(e.edit_text)
|
|
304
|
+
no
|
|
305
|
+
"""
|
|
306
|
+
text = self._normalize_to_caption(text)
|
|
307
|
+
self.highlight = None
|
|
308
|
+
self._emit("change", text)
|
|
309
|
+
old_text = self._edit_text
|
|
310
|
+
self._edit_text = text
|
|
311
|
+
if self.edit_pos > len(text):
|
|
312
|
+
self.edit_pos = len(text)
|
|
313
|
+
self._emit("postchange", old_text)
|
|
314
|
+
self._invalidate()
|
|
315
|
+
|
|
316
|
+
def get_edit_text(self) -> str:
|
|
317
|
+
"""
|
|
318
|
+
Return the edit text for this widget.
|
|
319
|
+
|
|
320
|
+
>>> e = Edit(u"What? ", u"oh, nothing.")
|
|
321
|
+
>>> print(e.get_edit_text())
|
|
322
|
+
oh, nothing.
|
|
323
|
+
>>> print(e.edit_text)
|
|
324
|
+
oh, nothing.
|
|
325
|
+
"""
|
|
326
|
+
return self._edit_text
|
|
327
|
+
|
|
328
|
+
edit_text = property(
|
|
329
|
+
get_edit_text,
|
|
330
|
+
set_edit_text,
|
|
331
|
+
doc="""
|
|
332
|
+
Property controlling the edit text for this widget.
|
|
333
|
+
""",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def insert_text(self, text: str) -> None:
|
|
337
|
+
"""
|
|
338
|
+
Insert text at the cursor position and update cursor.
|
|
339
|
+
This method is used by the keypress() method when inserting
|
|
340
|
+
one or more characters into edit_text.
|
|
341
|
+
|
|
342
|
+
:param text: text for inserting, type (bytes or unicode)
|
|
343
|
+
must match the text in the caption
|
|
344
|
+
:type text: bytes or unicode
|
|
345
|
+
|
|
346
|
+
>>> e = Edit(u"", u"42")
|
|
347
|
+
>>> e.insert_text(u".5")
|
|
348
|
+
>>> e
|
|
349
|
+
<Edit selectable flow widget '42.5' edit_pos=4>
|
|
350
|
+
>>> e.set_edit_pos(2)
|
|
351
|
+
>>> e.insert_text(u"a")
|
|
352
|
+
>>> print(e.edit_text)
|
|
353
|
+
42a.5
|
|
354
|
+
"""
|
|
355
|
+
text = self._normalize_to_caption(text)
|
|
356
|
+
result_text, result_pos = self.insert_text_result(text)
|
|
357
|
+
self.set_edit_text(result_text)
|
|
358
|
+
self.set_edit_pos(result_pos)
|
|
359
|
+
self.highlight = None
|
|
360
|
+
|
|
361
|
+
def _normalize_to_caption(self, text: str | bytes) -> str | bytes:
|
|
362
|
+
"""
|
|
363
|
+
Return text converted to the same type as self.caption
|
|
364
|
+
(bytes or unicode)
|
|
365
|
+
"""
|
|
366
|
+
tu = isinstance(text, str)
|
|
367
|
+
cu = isinstance(self._caption, str)
|
|
368
|
+
if tu == cu:
|
|
369
|
+
return text
|
|
370
|
+
if tu:
|
|
371
|
+
return text.encode("ascii") # follow python2's implicit conversion
|
|
372
|
+
return text.decode("ascii")
|
|
373
|
+
|
|
374
|
+
def insert_text_result(self, text: str) -> tuple[str | bytes, int]:
|
|
375
|
+
"""
|
|
376
|
+
Return result of insert_text(text) without actually performing the
|
|
377
|
+
insertion. Handy for pre-validation.
|
|
378
|
+
|
|
379
|
+
:param text: text for inserting, type (bytes or unicode)
|
|
380
|
+
must match the text in the caption
|
|
381
|
+
:type text: bytes or unicode
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
# if there's highlighted text, it'll get replaced by the new text
|
|
385
|
+
text = self._normalize_to_caption(text)
|
|
386
|
+
if self.highlight:
|
|
387
|
+
start, stop = self.highlight # pylint: disable=unpacking-non-sequence # already checked
|
|
388
|
+
btext, etext = self.edit_text[:start], self.edit_text[stop:]
|
|
389
|
+
result_text = btext + etext
|
|
390
|
+
result_pos = start
|
|
391
|
+
else:
|
|
392
|
+
result_text = self.edit_text
|
|
393
|
+
result_pos = self.edit_pos
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
result_text = result_text[:result_pos] + text + result_text[result_pos:]
|
|
397
|
+
except (IndexError, TypeError) as exc:
|
|
398
|
+
raise ValueError(repr((self.edit_text, result_text, text))).with_traceback(exc.__traceback__) from exc
|
|
399
|
+
|
|
400
|
+
result_pos += len(text)
|
|
401
|
+
return (result_text, result_pos)
|
|
402
|
+
|
|
403
|
+
def keypress(self, size: tuple[int], key: str) -> str | None:
|
|
404
|
+
"""
|
|
405
|
+
Handle editing keystrokes, return others.
|
|
406
|
+
|
|
407
|
+
>>> e, size = Edit(), (20,)
|
|
408
|
+
>>> e.keypress(size, 'x')
|
|
409
|
+
>>> e.keypress(size, 'left')
|
|
410
|
+
>>> e.keypress(size, '1')
|
|
411
|
+
>>> print(e.edit_text)
|
|
412
|
+
1x
|
|
413
|
+
>>> e.keypress(size, 'backspace')
|
|
414
|
+
>>> e.keypress(size, 'end')
|
|
415
|
+
>>> e.keypress(size, '2')
|
|
416
|
+
>>> print(e.edit_text)
|
|
417
|
+
x2
|
|
418
|
+
>>> e.keypress(size, 'shift f1')
|
|
419
|
+
'shift f1'
|
|
420
|
+
"""
|
|
421
|
+
pos = self.edit_pos
|
|
422
|
+
if self.valid_char(key):
|
|
423
|
+
if isinstance(key, str) and not isinstance(self._caption, str):
|
|
424
|
+
# screen is sending us unicode input, must be using utf-8
|
|
425
|
+
# encoding because that's all we support, so convert it
|
|
426
|
+
# to bytes to match our caption's type
|
|
427
|
+
key = key.encode("utf-8")
|
|
428
|
+
self.insert_text(key)
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
if key == "tab" and self.allow_tab:
|
|
432
|
+
key = " " * (8 - (self.edit_pos % 8))
|
|
433
|
+
self.insert_text(key)
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
if key == "enter" and self.multiline:
|
|
437
|
+
key = "\n"
|
|
438
|
+
self.insert_text(key)
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
if self._command_map[key] == Command.LEFT:
|
|
442
|
+
if pos == 0:
|
|
443
|
+
return key
|
|
444
|
+
pos = move_prev_char(self.edit_text, 0, pos)
|
|
445
|
+
self.set_edit_pos(pos)
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
if self._command_map[key] == Command.RIGHT:
|
|
449
|
+
if pos >= len(self.edit_text):
|
|
450
|
+
return key
|
|
451
|
+
pos = move_next_char(self.edit_text, pos, len(self.edit_text))
|
|
452
|
+
self.set_edit_pos(pos)
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
if self._command_map[key] in {Command.UP, Command.DOWN}:
|
|
456
|
+
self.highlight = None
|
|
457
|
+
|
|
458
|
+
_x, y = self.get_cursor_coords(size)
|
|
459
|
+
pref_col = self.get_pref_col(size)
|
|
460
|
+
if pref_col is None:
|
|
461
|
+
raise ValueError(pref_col)
|
|
462
|
+
|
|
463
|
+
# if pref_col is None:
|
|
464
|
+
# pref_col = x
|
|
465
|
+
|
|
466
|
+
if self._command_map[key] == Command.UP:
|
|
467
|
+
y -= 1
|
|
468
|
+
else:
|
|
469
|
+
y += 1
|
|
470
|
+
|
|
471
|
+
if not self.move_cursor_to_coords(size, pref_col, y):
|
|
472
|
+
return key
|
|
473
|
+
return None
|
|
474
|
+
|
|
475
|
+
if key == "backspace":
|
|
476
|
+
self.pref_col_maxcol = None, None
|
|
477
|
+
if not self._delete_highlighted():
|
|
478
|
+
if pos == 0:
|
|
479
|
+
return key
|
|
480
|
+
pos = move_prev_char(self.edit_text, 0, pos)
|
|
481
|
+
self.set_edit_text(self.edit_text[:pos] + self.edit_text[self.edit_pos :])
|
|
482
|
+
self.set_edit_pos(pos)
|
|
483
|
+
return None
|
|
484
|
+
return None
|
|
485
|
+
|
|
486
|
+
if key == "delete":
|
|
487
|
+
self.pref_col_maxcol = None, None
|
|
488
|
+
if not self._delete_highlighted():
|
|
489
|
+
if pos >= len(self.edit_text):
|
|
490
|
+
return key
|
|
491
|
+
pos = move_next_char(self.edit_text, pos, len(self.edit_text))
|
|
492
|
+
self.set_edit_text(self.edit_text[: self.edit_pos] + self.edit_text[pos:])
|
|
493
|
+
return None
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
if self._command_map[key] in {Command.MAX_LEFT, Command.MAX_RIGHT}:
|
|
497
|
+
self.highlight = None
|
|
498
|
+
self.pref_col_maxcol = None, None
|
|
499
|
+
|
|
500
|
+
_x, y = self.get_cursor_coords(size)
|
|
501
|
+
|
|
502
|
+
if self._command_map[key] == Command.MAX_LEFT:
|
|
503
|
+
self.move_cursor_to_coords(size, Align.LEFT, y)
|
|
504
|
+
else:
|
|
505
|
+
self.move_cursor_to_coords(size, Align.RIGHT, y)
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
# key wasn't handled
|
|
509
|
+
return key
|
|
510
|
+
|
|
511
|
+
def move_cursor_to_coords(
|
|
512
|
+
self,
|
|
513
|
+
size: tuple[int],
|
|
514
|
+
x: int | Literal[Align.LEFT, Align.RIGHT],
|
|
515
|
+
y: int,
|
|
516
|
+
) -> bool:
|
|
517
|
+
"""
|
|
518
|
+
Set the cursor position with (x,y) coordinates.
|
|
519
|
+
Returns True if move succeeded, False otherwise.
|
|
520
|
+
|
|
521
|
+
>>> size = (10,)
|
|
522
|
+
>>> e = Edit("","edit\\ntext")
|
|
523
|
+
>>> e.move_cursor_to_coords(size, 5, 0)
|
|
524
|
+
True
|
|
525
|
+
>>> e.edit_pos
|
|
526
|
+
4
|
|
527
|
+
>>> e.move_cursor_to_coords(size, 5, 3)
|
|
528
|
+
False
|
|
529
|
+
>>> e.move_cursor_to_coords(size, 0, 1)
|
|
530
|
+
True
|
|
531
|
+
>>> e.edit_pos
|
|
532
|
+
5
|
|
533
|
+
"""
|
|
534
|
+
(maxcol,) = size
|
|
535
|
+
trans = self.get_line_translation(maxcol)
|
|
536
|
+
_top_x, top_y = self.position_coords(maxcol, 0)
|
|
537
|
+
if y < top_y or y >= len(trans):
|
|
538
|
+
return False
|
|
539
|
+
|
|
540
|
+
pos = text_layout.calc_pos(self.get_text()[0], trans, x, y)
|
|
541
|
+
e_pos = min(max(pos - len(self.caption), 0), len(self.edit_text))
|
|
542
|
+
self.edit_pos = e_pos
|
|
543
|
+
self.pref_col_maxcol = x, maxcol
|
|
544
|
+
self._invalidate()
|
|
545
|
+
return True
|
|
546
|
+
|
|
547
|
+
def mouse_event(
|
|
548
|
+
self,
|
|
549
|
+
size: tuple[int],
|
|
550
|
+
event: str,
|
|
551
|
+
button: int,
|
|
552
|
+
col: int,
|
|
553
|
+
row: int,
|
|
554
|
+
focus: bool,
|
|
555
|
+
) -> bool | None:
|
|
556
|
+
"""
|
|
557
|
+
Move the cursor to the location clicked for button 1.
|
|
558
|
+
|
|
559
|
+
>>> size = (20,)
|
|
560
|
+
>>> e = Edit("","words here")
|
|
561
|
+
>>> e.mouse_event(size, 'mouse press', 1, 2, 0, True)
|
|
562
|
+
True
|
|
563
|
+
>>> e.edit_pos
|
|
564
|
+
2
|
|
565
|
+
"""
|
|
566
|
+
if button == 1:
|
|
567
|
+
return self.move_cursor_to_coords(size, col, row)
|
|
568
|
+
return False
|
|
569
|
+
|
|
570
|
+
def _delete_highlighted(self) -> bool:
|
|
571
|
+
"""
|
|
572
|
+
Delete all highlighted text and update cursor position, if any
|
|
573
|
+
text is highlighted.
|
|
574
|
+
"""
|
|
575
|
+
if not self.highlight:
|
|
576
|
+
return False
|
|
577
|
+
start, stop = self.highlight # pylint: disable=unpacking-non-sequence # already checked
|
|
578
|
+
btext, etext = self.edit_text[:start], self.edit_text[stop:]
|
|
579
|
+
self.set_edit_text(btext + etext)
|
|
580
|
+
self.edit_pos = start
|
|
581
|
+
self.highlight = None
|
|
582
|
+
return True
|
|
583
|
+
|
|
584
|
+
def render(self, size: tuple[int], focus: bool = False) -> TextCanvas | CompositeCanvas:
|
|
585
|
+
"""
|
|
586
|
+
Render edit widget and return canvas. Include cursor when in
|
|
587
|
+
focus.
|
|
588
|
+
|
|
589
|
+
>>> edit = Edit("? ","yes")
|
|
590
|
+
>>> c = edit.render((10,), focus=True)
|
|
591
|
+
>>> c.text
|
|
592
|
+
[b'? yes ']
|
|
593
|
+
>>> c.cursor
|
|
594
|
+
(5, 0)
|
|
595
|
+
"""
|
|
596
|
+
self._shift_view_to_cursor = bool(focus) # noqa: FURB123,RUF100
|
|
597
|
+
|
|
598
|
+
canv: TextCanvas | CompositeCanvas = super().render(size, focus)
|
|
599
|
+
if focus:
|
|
600
|
+
canv = CompositeCanvas(canv)
|
|
601
|
+
canv.cursor = self.get_cursor_coords(size)
|
|
602
|
+
|
|
603
|
+
# .. will need to FIXME if I want highlight to work again
|
|
604
|
+
# if self.highlight:
|
|
605
|
+
# hstart, hstop = self.highlight_coords()
|
|
606
|
+
# d.coords['highlight'] = [ hstart, hstop ]
|
|
607
|
+
return canv
|
|
608
|
+
|
|
609
|
+
def get_line_translation(
|
|
610
|
+
self,
|
|
611
|
+
maxcol: int,
|
|
612
|
+
ta: tuple[str | bytes, list[tuple[Hashable, int]]] | None = None,
|
|
613
|
+
) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]:
|
|
614
|
+
trans = super().get_line_translation(maxcol, ta)
|
|
615
|
+
if not self._shift_view_to_cursor:
|
|
616
|
+
return trans
|
|
617
|
+
|
|
618
|
+
text, _ignore = self.get_text()
|
|
619
|
+
x, y = text_layout.calc_coords(text, trans, self.edit_pos + len(self.caption))
|
|
620
|
+
if x < 0:
|
|
621
|
+
return [
|
|
622
|
+
*trans[:y],
|
|
623
|
+
*[text_layout.shift_line(trans[y], -x)],
|
|
624
|
+
*trans[y + 1 :],
|
|
625
|
+
]
|
|
626
|
+
|
|
627
|
+
if x >= maxcol:
|
|
628
|
+
return [
|
|
629
|
+
*trans[:y],
|
|
630
|
+
*[text_layout.shift_line(trans[y], -(x - maxcol + 1))],
|
|
631
|
+
*trans[y + 1 :],
|
|
632
|
+
]
|
|
633
|
+
|
|
634
|
+
return trans
|
|
635
|
+
|
|
636
|
+
def get_cursor_coords(self, size: tuple[int]) -> tuple[int, int]:
|
|
637
|
+
"""
|
|
638
|
+
Return the (*x*, *y*) coordinates of cursor within widget.
|
|
639
|
+
|
|
640
|
+
>>> Edit("? ","yes").get_cursor_coords((10,))
|
|
641
|
+
(5, 0)
|
|
642
|
+
"""
|
|
643
|
+
(maxcol,) = size
|
|
644
|
+
|
|
645
|
+
self._shift_view_to_cursor = True
|
|
646
|
+
return self.position_coords(maxcol, self.edit_pos)
|
|
647
|
+
|
|
648
|
+
def position_coords(self, maxcol: int, pos: int) -> tuple[int, int]:
|
|
649
|
+
"""
|
|
650
|
+
Return (*x*, *y*) coordinates for an offset into self.edit_text.
|
|
651
|
+
"""
|
|
652
|
+
|
|
653
|
+
p = pos + len(self.caption)
|
|
654
|
+
trans = self.get_line_translation(maxcol)
|
|
655
|
+
x, y = text_layout.calc_coords(self.get_text()[0], trans, p)
|
|
656
|
+
return x, y
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
class IntEdit(Edit):
|
|
660
|
+
"""Edit widget for integer values"""
|
|
661
|
+
|
|
662
|
+
def valid_char(self, ch: str) -> bool:
|
|
663
|
+
"""
|
|
664
|
+
Return true for decimal digits.
|
|
665
|
+
"""
|
|
666
|
+
return len(ch) == 1 and ch in string.digits
|
|
667
|
+
|
|
668
|
+
def __init__(self, caption="", default: int | str | None = None) -> None:
|
|
669
|
+
"""
|
|
670
|
+
caption -- caption markup
|
|
671
|
+
default -- default edit value
|
|
672
|
+
|
|
673
|
+
>>> IntEdit(u"", 42)
|
|
674
|
+
<IntEdit selectable flow widget '42' edit_pos=2>
|
|
675
|
+
"""
|
|
676
|
+
if default is not None:
|
|
677
|
+
val = str(default)
|
|
678
|
+
else:
|
|
679
|
+
val = ""
|
|
680
|
+
super().__init__(caption, val)
|
|
681
|
+
|
|
682
|
+
def keypress(self, size: tuple[int], key: str) -> str | None:
|
|
683
|
+
"""
|
|
684
|
+
Handle editing keystrokes. Remove leading zeros.
|
|
685
|
+
|
|
686
|
+
>>> e, size = IntEdit(u"", 5002), (10,)
|
|
687
|
+
>>> e.keypress(size, 'home')
|
|
688
|
+
>>> e.keypress(size, 'delete')
|
|
689
|
+
>>> print(e.edit_text)
|
|
690
|
+
002
|
|
691
|
+
>>> e.keypress(size, 'end')
|
|
692
|
+
>>> print(e.edit_text)
|
|
693
|
+
2
|
|
694
|
+
"""
|
|
695
|
+
unhandled = super().keypress(size, key)
|
|
696
|
+
|
|
697
|
+
if not unhandled:
|
|
698
|
+
# trim leading zeros
|
|
699
|
+
while self.edit_pos > 0 and self.edit_text[:1] == "0":
|
|
700
|
+
self.set_edit_pos(self.edit_pos - 1)
|
|
701
|
+
self.set_edit_text(self.edit_text[1:])
|
|
702
|
+
|
|
703
|
+
return unhandled
|
|
704
|
+
|
|
705
|
+
def value(self) -> int:
|
|
706
|
+
"""
|
|
707
|
+
Return the numeric value of self.edit_text.
|
|
708
|
+
|
|
709
|
+
>>> e, size = IntEdit(), (10,)
|
|
710
|
+
>>> e.keypress(size, '5')
|
|
711
|
+
>>> e.keypress(size, '1')
|
|
712
|
+
>>> e.value() == 51
|
|
713
|
+
True
|
|
714
|
+
"""
|
|
715
|
+
if self.edit_text:
|
|
716
|
+
return int(self.edit_text)
|
|
717
|
+
|
|
718
|
+
return 0
|