Automated update
This commit is contained in:
@@ -0,0 +1,724 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import string
|
||||
import typing
|
||||
|
||||
from urwid import text_layout
|
||||
from urwid.canvas import CompositeCanvas
|
||||
from urwid.command_map import Command
|
||||
from urwid.split_repr import remove_defaults
|
||||
from urwid.str_util import is_wide_char, move_next_char, move_prev_char
|
||||
from urwid.util import decompose_tagmarkup
|
||||
|
||||
from .constants import Align, Sizing, WrapMode
|
||||
from .text import Text, TextError
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Hashable
|
||||
|
||||
from typing_extensions import Literal
|
||||
|
||||
from urwid.canvas import TextCanvas
|
||||
|
||||
|
||||
class EditError(TextError):
|
||||
pass
|
||||
|
||||
|
||||
class Edit(Text):
|
||||
"""
|
||||
Text editing widget implements cursor movement, text insertion and
|
||||
deletion. A caption may prefix the editing area. Uses text class
|
||||
for text layout.
|
||||
|
||||
Users of this class may listen for ``"change"`` or ``"postchange"``
|
||||
events. See :func:``connect_signal``.
|
||||
|
||||
* ``"change"`` is sent just before the value of edit_text changes.
|
||||
It receives the new text as an argument. Note that ``"change"`` cannot
|
||||
change the text in question as edit_text changes the text afterwards.
|
||||
* ``"postchange"`` is sent after the value of edit_text changes.
|
||||
It receives the old value of the text as an argument and thus is
|
||||
appropriate for changing the text. It is possible for a ``"postchange"``
|
||||
event handler to get into a loop of changing the text and then being
|
||||
called when the event is re-emitted. It is up to the event
|
||||
handler to guard against this case (for instance, by not changing the
|
||||
text if it is signaled for text that it has already changed once).
|
||||
"""
|
||||
|
||||
_sizing = frozenset([Sizing.FLOW])
|
||||
_selectable = True
|
||||
ignore_focus = False
|
||||
# (this variable is picked up by the MetaSignals metaclass)
|
||||
signals: typing.ClassVar[list[str]] = ["change", "postchange"]
|
||||
|
||||
def valid_char(self, ch: str) -> bool:
|
||||
"""
|
||||
Filter for text that may be entered into this widget by the user
|
||||
|
||||
:param ch: character to be inserted
|
||||
:type ch: str
|
||||
|
||||
This implementation returns True for all printable characters.
|
||||
"""
|
||||
return is_wide_char(ch, 0) or (len(ch) == 1 and ord(ch) >= 32)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
caption: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]] = "",
|
||||
edit_text: str = "",
|
||||
multiline: bool = False,
|
||||
align: Literal["left", "center", "right"] | Align = Align.LEFT,
|
||||
wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,
|
||||
allow_tab: bool = False,
|
||||
edit_pos: int | None = None,
|
||||
layout: text_layout.TextLayout = None,
|
||||
mask: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
:param caption: markup for caption preceding edit_text, see
|
||||
:class:`Text` for description of text markup.
|
||||
:type caption: text markup
|
||||
:param edit_text: initial text for editing, type (bytes or unicode)
|
||||
must match the text in the caption
|
||||
:type edit_text: bytes or unicode
|
||||
:param multiline: True: 'enter' inserts newline False: return it
|
||||
:type multiline: bool
|
||||
:param align: typically 'left', 'center' or 'right'
|
||||
:type align: text alignment mode
|
||||
:param wrap: typically 'space', 'any' or 'clip'
|
||||
:type wrap: text wrapping mode
|
||||
:param allow_tab: True: 'tab' inserts 1-8 spaces False: return it
|
||||
:type allow_tab: bool
|
||||
:param edit_pos: initial position for cursor, None:end of edit_text
|
||||
:type edit_pos: int
|
||||
:param layout: defaults to a shared :class:`StandardTextLayout` instance
|
||||
:type layout: text layout instance
|
||||
:param mask: hide text entered with this character, None:disable mask
|
||||
:type mask: bytes or unicode
|
||||
|
||||
>>> Edit()
|
||||
<Edit selectable flow widget '' edit_pos=0>
|
||||
>>> Edit(u"Y/n? ", u"yes")
|
||||
<Edit selectable flow widget 'yes' caption='Y/n? ' edit_pos=3>
|
||||
>>> Edit(u"Name ", u"Smith", edit_pos=1)
|
||||
<Edit selectable flow widget 'Smith' caption='Name ' edit_pos=1>
|
||||
>>> Edit(u"", u"3.14", align='right')
|
||||
<Edit selectable flow widget '3.14' align='right' edit_pos=4>
|
||||
"""
|
||||
|
||||
super().__init__("", align, wrap, layout)
|
||||
self.multiline = multiline
|
||||
self.allow_tab = allow_tab
|
||||
self._edit_pos = 0
|
||||
self._caption, self._attrib = decompose_tagmarkup(caption)
|
||||
self._edit_text = ""
|
||||
self.highlight: tuple[int, int] | None = None
|
||||
self.set_edit_text(edit_text)
|
||||
if edit_pos is None:
|
||||
edit_pos = len(edit_text)
|
||||
self.set_edit_pos(edit_pos)
|
||||
self.set_mask(mask)
|
||||
self._shift_view_to_cursor = False
|
||||
|
||||
def _repr_words(self) -> list[str]:
|
||||
return (
|
||||
super()._repr_words()[:-1]
|
||||
+ [repr(self._edit_text)]
|
||||
+ [f"caption={self._caption!r}"] * bool(self._caption)
|
||||
+ ["multiline"] * (self.multiline is True)
|
||||
)
|
||||
|
||||
def _repr_attrs(self) -> dict[str, typing.Any]:
|
||||
attrs = {**super()._repr_attrs(), "edit_pos": self._edit_pos}
|
||||
return remove_defaults(attrs, Edit.__init__)
|
||||
|
||||
def get_text(self) -> tuple[str | bytes, list[tuple[Hashable, int]]]:
|
||||
"""
|
||||
Returns ``(text, display attributes)``. See :meth:`Text.get_text`
|
||||
for details.
|
||||
|
||||
Text returned includes the caption and edit_text, possibly masked.
|
||||
|
||||
>>> Edit(u"What? ","oh, nothing.").get_text()
|
||||
('What? oh, nothing.', [])
|
||||
>>> Edit(('bright',u"user@host:~$ "),"ls").get_text()
|
||||
('user@host:~$ ls', [('bright', 13)])
|
||||
>>> Edit(u"password:", u"seekrit", mask=u"*").get_text()
|
||||
('password:*******', [])
|
||||
"""
|
||||
|
||||
if self._mask is None:
|
||||
return self._caption + self._edit_text, self._attrib
|
||||
|
||||
return self._caption + (self._mask * len(self._edit_text)), self._attrib
|
||||
|
||||
def set_text(self, markup: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]) -> None:
|
||||
"""
|
||||
Not supported by Edit widget.
|
||||
|
||||
>>> Edit().set_text("test")
|
||||
Traceback (most recent call last):
|
||||
EditError: set_text() not supported. Use set_caption() or set_edit_text() instead.
|
||||
"""
|
||||
# FIXME: this smells. reimplement Edit as a WidgetWrap subclass to
|
||||
# clean this up
|
||||
|
||||
# hack to let Text.__init__() work
|
||||
if not hasattr(self, "_text") and markup == "": # noqa: PLC1901,RUF100
|
||||
self._text = None
|
||||
return
|
||||
|
||||
raise EditError("set_text() not supported. Use set_caption() or set_edit_text() instead.")
|
||||
|
||||
def get_pref_col(self, size: tuple[int]) -> int:
|
||||
"""
|
||||
Return the preferred column for the cursor, or the
|
||||
current cursor x value. May also return ``'left'`` or ``'right'``
|
||||
to indicate the leftmost or rightmost column available.
|
||||
|
||||
This method is used internally and by other widgets when
|
||||
moving the cursor up or down between widgets so that the
|
||||
column selected is one that the user would expect.
|
||||
|
||||
>>> size = (10,)
|
||||
>>> Edit().get_pref_col(size)
|
||||
0
|
||||
>>> e = Edit(u"", u"word")
|
||||
>>> e.get_pref_col(size)
|
||||
4
|
||||
>>> e.keypress(size, 'left')
|
||||
>>> e.get_pref_col(size)
|
||||
3
|
||||
>>> e.keypress(size, 'end')
|
||||
>>> e.get_pref_col(size)
|
||||
<Align.RIGHT: 'right'>
|
||||
>>> e = Edit(u"", u"2\\nwords")
|
||||
>>> e.keypress(size, 'left')
|
||||
>>> e.keypress(size, 'up')
|
||||
>>> e.get_pref_col(size)
|
||||
4
|
||||
>>> e.keypress(size, 'left')
|
||||
>>> e.get_pref_col(size)
|
||||
0
|
||||
"""
|
||||
(maxcol,) = size
|
||||
pref_col, then_maxcol = self.pref_col_maxcol
|
||||
if then_maxcol != maxcol:
|
||||
return self.get_cursor_coords((maxcol,))[0]
|
||||
|
||||
return pref_col
|
||||
|
||||
def set_caption(self, caption: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]) -> None:
|
||||
"""
|
||||
Set the caption markup for this widget.
|
||||
|
||||
:param caption: markup for caption preceding edit_text, see
|
||||
:meth:`Text.__init__` for description of text markup.
|
||||
|
||||
>>> e = Edit("")
|
||||
>>> e.set_caption("cap1")
|
||||
>>> print(e.caption)
|
||||
cap1
|
||||
>>> e.set_caption(('bold', "cap2"))
|
||||
>>> print(e.caption)
|
||||
cap2
|
||||
>>> e.attrib
|
||||
[('bold', 4)]
|
||||
>>> e.caption = "cap3" # not supported because caption stores text but set_caption() takes markup
|
||||
Traceback (most recent call last):
|
||||
AttributeError: can't set attribute
|
||||
"""
|
||||
self._caption, self._attrib = decompose_tagmarkup(caption)
|
||||
self._invalidate()
|
||||
|
||||
@property
|
||||
def caption(self) -> str:
|
||||
"""
|
||||
Read-only property returning the caption for this widget.
|
||||
"""
|
||||
return self._caption
|
||||
|
||||
def set_edit_pos(self, pos: int) -> None:
|
||||
"""
|
||||
Set the cursor position with a self.edit_text offset.
|
||||
Clips pos to [0, len(edit_text)].
|
||||
|
||||
:param pos: cursor position
|
||||
:type pos: int
|
||||
|
||||
>>> e = Edit(u"", u"word")
|
||||
>>> e.edit_pos
|
||||
4
|
||||
>>> e.set_edit_pos(2)
|
||||
>>> e.edit_pos
|
||||
2
|
||||
>>> e.edit_pos = -1 # Urwid 0.9.9 or later
|
||||
>>> e.edit_pos
|
||||
0
|
||||
>>> e.edit_pos = 20
|
||||
>>> e.edit_pos
|
||||
4
|
||||
"""
|
||||
pos = min(max(pos, 0), len(self._edit_text))
|
||||
self.highlight = None
|
||||
self.pref_col_maxcol = None, None
|
||||
self._edit_pos = pos
|
||||
self._invalidate()
|
||||
|
||||
edit_pos = property(
|
||||
lambda self: self._edit_pos,
|
||||
set_edit_pos,
|
||||
doc="""
|
||||
Property controlling the edit position for this widget.
|
||||
""",
|
||||
)
|
||||
|
||||
def set_mask(self, mask: str | None) -> None:
|
||||
"""
|
||||
Set the character for masking text away.
|
||||
|
||||
:param mask: hide text entered with this character, None:disable mask
|
||||
:type mask: bytes or unicode
|
||||
"""
|
||||
|
||||
self._mask = mask
|
||||
self._invalidate()
|
||||
|
||||
def set_edit_text(self, text: str) -> None:
|
||||
"""
|
||||
Set the edit text for this widget.
|
||||
|
||||
:param text: text for editing, type (bytes or unicode)
|
||||
must match the text in the caption
|
||||
:type text: bytes or unicode
|
||||
|
||||
>>> e = Edit()
|
||||
>>> e.set_edit_text(u"yes")
|
||||
>>> print(e.edit_text)
|
||||
yes
|
||||
>>> e
|
||||
<Edit selectable flow widget 'yes' edit_pos=0>
|
||||
>>> e.edit_text = u"no" # Urwid 0.9.9 or later
|
||||
>>> print(e.edit_text)
|
||||
no
|
||||
"""
|
||||
text = self._normalize_to_caption(text)
|
||||
self.highlight = None
|
||||
self._emit("change", text)
|
||||
old_text = self._edit_text
|
||||
self._edit_text = text
|
||||
self.edit_pos = min(self.edit_pos, len(text))
|
||||
|
||||
self._emit("postchange", old_text)
|
||||
self._invalidate()
|
||||
|
||||
def get_edit_text(self) -> str:
|
||||
"""
|
||||
Return the edit text for this widget.
|
||||
|
||||
>>> e = Edit(u"What? ", u"oh, nothing.")
|
||||
>>> print(e.get_edit_text())
|
||||
oh, nothing.
|
||||
>>> print(e.edit_text)
|
||||
oh, nothing.
|
||||
"""
|
||||
return self._edit_text
|
||||
|
||||
edit_text = property(
|
||||
get_edit_text,
|
||||
set_edit_text,
|
||||
doc="""
|
||||
Property controlling the edit text for this widget.
|
||||
""",
|
||||
)
|
||||
|
||||
def insert_text(self, text: str) -> None:
|
||||
"""
|
||||
Insert text at the cursor position and update cursor.
|
||||
This method is used by the keypress() method when inserting
|
||||
one or more characters into edit_text.
|
||||
|
||||
:param text: text for inserting, type (bytes or unicode)
|
||||
must match the text in the caption
|
||||
:type text: bytes or unicode
|
||||
|
||||
>>> e = Edit(u"", u"42")
|
||||
>>> e.insert_text(u".5")
|
||||
>>> e
|
||||
<Edit selectable flow widget '42.5' edit_pos=4>
|
||||
>>> e.set_edit_pos(2)
|
||||
>>> e.insert_text(u"a")
|
||||
>>> print(e.edit_text)
|
||||
42a.5
|
||||
"""
|
||||
text = self._normalize_to_caption(text)
|
||||
result_text, result_pos = self.insert_text_result(text)
|
||||
self.set_edit_text(result_text)
|
||||
self.set_edit_pos(result_pos)
|
||||
self.highlight = None
|
||||
|
||||
def _normalize_to_caption(self, text: str | bytes) -> str | bytes:
|
||||
"""Return text converted to the same type as self.caption (bytes or unicode)"""
|
||||
tu = isinstance(text, str)
|
||||
cu = isinstance(self._caption, str)
|
||||
if tu == cu:
|
||||
return text
|
||||
if tu:
|
||||
return text.encode("ascii") # follow python2's implicit conversion
|
||||
return text.decode("ascii")
|
||||
|
||||
def insert_text_result(self, text: str) -> tuple[str | bytes, int]:
|
||||
"""
|
||||
Return result of insert_text(text) without actually performing the
|
||||
insertion. Handy for pre-validation.
|
||||
|
||||
:param text: text for inserting, type (bytes or unicode)
|
||||
must match the text in the caption
|
||||
:type text: bytes or unicode
|
||||
"""
|
||||
|
||||
# if there's highlighted text, it'll get replaced by the new text
|
||||
text = self._normalize_to_caption(text)
|
||||
if self.highlight:
|
||||
start, stop = self.highlight # pylint: disable=unpacking-non-sequence # already checked
|
||||
btext, etext = self.edit_text[:start], self.edit_text[stop:]
|
||||
result_text = btext + etext
|
||||
result_pos = start
|
||||
else:
|
||||
result_text = self.edit_text
|
||||
result_pos = self.edit_pos
|
||||
|
||||
try:
|
||||
result_text = result_text[:result_pos] + text + result_text[result_pos:]
|
||||
except (IndexError, TypeError) as exc:
|
||||
raise ValueError(repr((self.edit_text, result_text, text))).with_traceback(exc.__traceback__) from exc
|
||||
|
||||
result_pos += len(text)
|
||||
return (result_text, result_pos)
|
||||
|
||||
def keypress(
|
||||
self,
|
||||
size: tuple[int], # type: ignore[override]
|
||||
key: str,
|
||||
) -> str | None:
|
||||
"""
|
||||
Handle editing keystrokes, return others.
|
||||
|
||||
>>> e, size = Edit(), (20,)
|
||||
>>> e.keypress(size, 'x')
|
||||
>>> e.keypress(size, 'left')
|
||||
>>> e.keypress(size, '1')
|
||||
>>> print(e.edit_text)
|
||||
1x
|
||||
>>> e.keypress(size, 'backspace')
|
||||
>>> e.keypress(size, 'end')
|
||||
>>> e.keypress(size, '2')
|
||||
>>> print(e.edit_text)
|
||||
x2
|
||||
>>> e.keypress(size, 'shift f1')
|
||||
'shift f1'
|
||||
"""
|
||||
pos = self.edit_pos
|
||||
if self.valid_char(key):
|
||||
if isinstance(key, str) and not isinstance(self._caption, str):
|
||||
key = key.encode("utf-8")
|
||||
self.insert_text(key)
|
||||
return None
|
||||
|
||||
if key == "tab" and self.allow_tab:
|
||||
key = " " * (8 - (self.edit_pos % 8))
|
||||
self.insert_text(key)
|
||||
return None
|
||||
|
||||
if key == "enter" and self.multiline:
|
||||
key = "\n"
|
||||
self.insert_text(key)
|
||||
return None
|
||||
|
||||
if self._command_map[key] == Command.LEFT:
|
||||
if pos == 0:
|
||||
return key
|
||||
pos = move_prev_char(self.edit_text, 0, pos)
|
||||
self.set_edit_pos(pos)
|
||||
return None
|
||||
|
||||
if self._command_map[key] == Command.RIGHT:
|
||||
if pos >= len(self.edit_text):
|
||||
return key
|
||||
pos = move_next_char(self.edit_text, pos, len(self.edit_text))
|
||||
self.set_edit_pos(pos)
|
||||
return None
|
||||
|
||||
if self._command_map[key] in {Command.UP, Command.DOWN}:
|
||||
self.highlight = None
|
||||
|
||||
_x, y = self.get_cursor_coords(size)
|
||||
pref_col = self.get_pref_col(size)
|
||||
if pref_col is None:
|
||||
raise ValueError(pref_col)
|
||||
|
||||
# if pref_col is None:
|
||||
# pref_col = x
|
||||
|
||||
if self._command_map[key] == Command.UP:
|
||||
y -= 1
|
||||
else:
|
||||
y += 1
|
||||
|
||||
if not self.move_cursor_to_coords(size, pref_col, y):
|
||||
return key
|
||||
return None
|
||||
|
||||
if key == "backspace":
|
||||
self.pref_col_maxcol = None, None
|
||||
if not self._delete_highlighted():
|
||||
if pos == 0:
|
||||
return key
|
||||
pos = move_prev_char(self.edit_text, 0, pos)
|
||||
self.set_edit_text(self.edit_text[:pos] + self.edit_text[self.edit_pos :])
|
||||
self.set_edit_pos(pos)
|
||||
return None
|
||||
return None
|
||||
|
||||
if key == "delete":
|
||||
self.pref_col_maxcol = None, None
|
||||
if not self._delete_highlighted():
|
||||
if pos >= len(self.edit_text):
|
||||
return key
|
||||
pos = move_next_char(self.edit_text, pos, len(self.edit_text))
|
||||
self.set_edit_text(self.edit_text[: self.edit_pos] + self.edit_text[pos:])
|
||||
return None
|
||||
return None
|
||||
|
||||
if self._command_map[key] in {Command.MAX_LEFT, Command.MAX_RIGHT}:
|
||||
self.highlight = None
|
||||
self.pref_col_maxcol = None, None
|
||||
|
||||
_x, y = self.get_cursor_coords(size)
|
||||
|
||||
if self._command_map[key] == Command.MAX_LEFT:
|
||||
self.move_cursor_to_coords(size, Align.LEFT, y)
|
||||
else:
|
||||
self.move_cursor_to_coords(size, Align.RIGHT, y)
|
||||
return None
|
||||
|
||||
# key wasn't handled
|
||||
return key
|
||||
|
||||
def move_cursor_to_coords(
|
||||
self,
|
||||
size: tuple[int],
|
||||
x: int | Literal[Align.LEFT, Align.RIGHT],
|
||||
y: int,
|
||||
) -> bool:
|
||||
"""
|
||||
Set the cursor position with (x,y) coordinates.
|
||||
Returns True if move succeeded, False otherwise.
|
||||
|
||||
>>> size = (10,)
|
||||
>>> e = Edit("","edit\\ntext")
|
||||
>>> e.move_cursor_to_coords(size, 5, 0)
|
||||
True
|
||||
>>> e.edit_pos
|
||||
4
|
||||
>>> e.move_cursor_to_coords(size, 5, 3)
|
||||
False
|
||||
>>> e.move_cursor_to_coords(size, 0, 1)
|
||||
True
|
||||
>>> e.edit_pos
|
||||
5
|
||||
"""
|
||||
(maxcol,) = size
|
||||
trans = self.get_line_translation(maxcol)
|
||||
_top_x, top_y = self.position_coords(maxcol, 0)
|
||||
if y < top_y or y >= len(trans):
|
||||
return False
|
||||
|
||||
pos = text_layout.calc_pos(self.get_text()[0], trans, x, y)
|
||||
e_pos = min(max(pos - len(self.caption), 0), len(self.edit_text))
|
||||
self.edit_pos = e_pos
|
||||
self.pref_col_maxcol = x, maxcol
|
||||
self._invalidate()
|
||||
return True
|
||||
|
||||
def mouse_event(
|
||||
self,
|
||||
size: tuple[int], # type: ignore[override]
|
||||
event: str,
|
||||
button: int,
|
||||
col: int,
|
||||
row: int,
|
||||
focus: bool,
|
||||
) -> bool | None:
|
||||
"""
|
||||
Move the cursor to the location clicked for button 1.
|
||||
|
||||
>>> size = (20,)
|
||||
>>> e = Edit("","words here")
|
||||
>>> e.mouse_event(size, 'mouse press', 1, 2, 0, True)
|
||||
True
|
||||
>>> e.edit_pos
|
||||
2
|
||||
"""
|
||||
if button == 1:
|
||||
return self.move_cursor_to_coords(size, col, row)
|
||||
return False
|
||||
|
||||
def _delete_highlighted(self) -> bool:
|
||||
"""
|
||||
Delete all highlighted text and update cursor position, if any
|
||||
text is highlighted.
|
||||
"""
|
||||
if not self.highlight:
|
||||
return False
|
||||
start, stop = self.highlight # pylint: disable=unpacking-non-sequence # already checked
|
||||
btext, etext = self.edit_text[:start], self.edit_text[stop:]
|
||||
self.set_edit_text(btext + etext)
|
||||
self.edit_pos = start
|
||||
self.highlight = None
|
||||
return True
|
||||
|
||||
def render(
|
||||
self,
|
||||
size: tuple[int], # type: ignore[override]
|
||||
focus: bool = False,
|
||||
) -> TextCanvas | CompositeCanvas:
|
||||
"""
|
||||
Render edit widget and return canvas. Include cursor when in
|
||||
focus.
|
||||
|
||||
>>> edit = Edit("? ","yes")
|
||||
>>> c = edit.render((10,), focus=True)
|
||||
>>> c.text
|
||||
[b'? yes ']
|
||||
>>> c.cursor
|
||||
(5, 0)
|
||||
"""
|
||||
self._shift_view_to_cursor = bool(focus) # noqa: FURB123,RUF100
|
||||
|
||||
canv: TextCanvas | CompositeCanvas = super().render(size, focus)
|
||||
if focus:
|
||||
canv = CompositeCanvas(canv)
|
||||
canv.cursor = self.get_cursor_coords(size)
|
||||
|
||||
# .. will need to FIXME if I want highlight to work again
|
||||
# if self.highlight:
|
||||
# hstart, hstop = self.highlight_coords()
|
||||
# d.coords['highlight'] = [ hstart, hstop ]
|
||||
return canv
|
||||
|
||||
def get_line_translation(
|
||||
self,
|
||||
maxcol: int,
|
||||
ta: tuple[str | bytes, list[tuple[Hashable, int]]] | None = None,
|
||||
) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]:
|
||||
trans = super().get_line_translation(maxcol, ta)
|
||||
if not self._shift_view_to_cursor:
|
||||
return trans
|
||||
|
||||
text, _ignore = self.get_text()
|
||||
x, y = text_layout.calc_coords(text, trans, self.edit_pos + len(self.caption))
|
||||
if x < 0:
|
||||
return [
|
||||
*trans[:y],
|
||||
*[text_layout.shift_line(trans[y], -x)],
|
||||
*trans[y + 1 :],
|
||||
]
|
||||
|
||||
if x >= maxcol:
|
||||
return [
|
||||
*trans[:y],
|
||||
*[text_layout.shift_line(trans[y], -(x - maxcol + 1))],
|
||||
*trans[y + 1 :],
|
||||
]
|
||||
|
||||
return trans
|
||||
|
||||
def get_cursor_coords(self, size: tuple[int]) -> tuple[int, int]:
|
||||
"""
|
||||
Return the (*x*, *y*) coordinates of cursor within widget.
|
||||
|
||||
>>> Edit("? ","yes").get_cursor_coords((10,))
|
||||
(5, 0)
|
||||
"""
|
||||
(maxcol,) = size
|
||||
|
||||
self._shift_view_to_cursor = True
|
||||
return self.position_coords(maxcol, self.edit_pos)
|
||||
|
||||
def position_coords(self, maxcol: int, pos: int) -> tuple[int, int]:
|
||||
"""
|
||||
Return (*x*, *y*) coordinates for an offset into self.edit_text.
|
||||
"""
|
||||
|
||||
p = pos + len(self.caption)
|
||||
trans = self.get_line_translation(maxcol)
|
||||
x, y = text_layout.calc_coords(self.get_text()[0], trans, p)
|
||||
return x, y
|
||||
|
||||
|
||||
class IntEdit(Edit):
|
||||
"""Edit widget for integer values"""
|
||||
|
||||
def valid_char(self, ch: str) -> bool:
|
||||
"""
|
||||
Return true for decimal digits.
|
||||
"""
|
||||
return len(ch) == 1 and ch in string.digits
|
||||
|
||||
def __init__(self, caption="", default: int | str | None = None) -> None:
|
||||
"""
|
||||
caption -- caption markup
|
||||
default -- default edit value
|
||||
|
||||
>>> IntEdit(u"", 42)
|
||||
<IntEdit selectable flow widget '42' edit_pos=2>
|
||||
"""
|
||||
if default is not None:
|
||||
val = str(default)
|
||||
else:
|
||||
val = ""
|
||||
super().__init__(caption, val)
|
||||
|
||||
def keypress(
|
||||
self,
|
||||
size: tuple[int], # type: ignore[override]
|
||||
key: str,
|
||||
) -> str | None:
|
||||
"""
|
||||
Handle editing keystrokes. Remove leading zeros.
|
||||
|
||||
>>> e, size = IntEdit(u"", 5002), (10,)
|
||||
>>> e.keypress(size, 'home')
|
||||
>>> e.keypress(size, 'delete')
|
||||
>>> print(e.edit_text)
|
||||
002
|
||||
>>> e.keypress(size, 'end')
|
||||
>>> print(e.edit_text)
|
||||
2
|
||||
"""
|
||||
unhandled = super().keypress(size, key)
|
||||
|
||||
if not unhandled:
|
||||
# trim leading zeros
|
||||
while self.edit_pos > 0 and self.edit_text[:1] == "0":
|
||||
self.set_edit_pos(self.edit_pos - 1)
|
||||
self.set_edit_text(self.edit_text[1:])
|
||||
|
||||
return unhandled
|
||||
|
||||
def value(self) -> int:
|
||||
"""
|
||||
Return the numeric value of self.edit_text.
|
||||
|
||||
>>> e, size = IntEdit(), (10,)
|
||||
>>> e.keypress(size, '5')
|
||||
>>> e.keypress(size, '1')
|
||||
>>> e.value() == 51
|
||||
True
|
||||
"""
|
||||
if self.edit_text:
|
||||
return int(self.edit_text)
|
||||
|
||||
return 0
|
||||
Reference in New Issue
Block a user