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.

Files changed (75) hide show
  1. urwid/__init__.py +333 -0
  2. urwid/canvas.py +1413 -0
  3. urwid/command_map.py +137 -0
  4. urwid/container.py +59 -0
  5. urwid/decoration.py +65 -0
  6. urwid/display/__init__.py +97 -0
  7. urwid/display/_posix_raw_display.py +413 -0
  8. urwid/display/_raw_display_base.py +914 -0
  9. urwid/display/_web.css +12 -0
  10. urwid/display/_web.js +462 -0
  11. urwid/display/_win32.py +171 -0
  12. urwid/display/_win32_raw_display.py +269 -0
  13. urwid/display/common.py +1219 -0
  14. urwid/display/curses.py +690 -0
  15. urwid/display/escape.py +624 -0
  16. urwid/display/html_fragment.py +251 -0
  17. urwid/display/lcd.py +518 -0
  18. urwid/display/raw.py +37 -0
  19. urwid/display/web.py +636 -0
  20. urwid/event_loop/__init__.py +55 -0
  21. urwid/event_loop/abstract_loop.py +175 -0
  22. urwid/event_loop/asyncio_loop.py +231 -0
  23. urwid/event_loop/glib_loop.py +294 -0
  24. urwid/event_loop/main_loop.py +721 -0
  25. urwid/event_loop/select_loop.py +230 -0
  26. urwid/event_loop/tornado_loop.py +206 -0
  27. urwid/event_loop/trio_loop.py +302 -0
  28. urwid/event_loop/twisted_loop.py +269 -0
  29. urwid/event_loop/zmq_loop.py +275 -0
  30. urwid/font.py +695 -0
  31. urwid/graphics.py +96 -0
  32. urwid/highlight.css +19 -0
  33. urwid/listbox.py +1899 -0
  34. urwid/monitored_list.py +522 -0
  35. urwid/numedit.py +376 -0
  36. urwid/signals.py +330 -0
  37. urwid/split_repr.py +130 -0
  38. urwid/str_util.py +358 -0
  39. urwid/text_layout.py +632 -0
  40. urwid/treetools.py +515 -0
  41. urwid/util.py +557 -0
  42. urwid/version.py +16 -0
  43. urwid/vterm.py +1806 -0
  44. urwid/widget/__init__.py +181 -0
  45. urwid/widget/attr_map.py +161 -0
  46. urwid/widget/attr_wrap.py +140 -0
  47. urwid/widget/bar_graph.py +649 -0
  48. urwid/widget/big_text.py +77 -0
  49. urwid/widget/box_adapter.py +126 -0
  50. urwid/widget/columns.py +1145 -0
  51. urwid/widget/constants.py +574 -0
  52. urwid/widget/container.py +227 -0
  53. urwid/widget/divider.py +110 -0
  54. urwid/widget/edit.py +718 -0
  55. urwid/widget/filler.py +403 -0
  56. urwid/widget/frame.py +539 -0
  57. urwid/widget/grid_flow.py +539 -0
  58. urwid/widget/line_box.py +194 -0
  59. urwid/widget/overlay.py +829 -0
  60. urwid/widget/padding.py +597 -0
  61. urwid/widget/pile.py +971 -0
  62. urwid/widget/popup.py +170 -0
  63. urwid/widget/progress_bar.py +141 -0
  64. urwid/widget/scrollable.py +597 -0
  65. urwid/widget/solid_fill.py +44 -0
  66. urwid/widget/text.py +354 -0
  67. urwid/widget/widget.py +852 -0
  68. urwid/widget/widget_decoration.py +166 -0
  69. urwid/widget/wimp.py +792 -0
  70. urwid/wimp.py +23 -0
  71. urwid-2.6.0.post0.dist-info/COPYING +504 -0
  72. urwid-2.6.0.post0.dist-info/METADATA +332 -0
  73. urwid-2.6.0.post0.dist-info/RECORD +75 -0
  74. urwid-2.6.0.post0.dist-info/WHEEL +5 -0
  75. 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