Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions Lib/curses/textpad.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import curses
import curses.ascii
import unicodedata

def rectangle(win, uly, ulx, lry, lrx):
"""Draw a rectangle with corners at the provided upper-left
Expand Down Expand Up @@ -49,6 +50,8 @@ def __init__(self, win, insert_mode=False):
self._update_max_yx()
self.stripspaces = 1
self.lastcmd = None
# Whether the last command typed a character into the lower-right cell.
self._corner_typed = False
win.keypad(1)

def _update_max_yx(self):
Expand Down Expand Up @@ -88,6 +91,8 @@ def _insert_printable_char(self, ch):
self._update_max_yx()
(y, x) = self.win.getyx()
backyx = None
corner_typed = self._corner_typed
self._corner_typed = False
while True:
if self.insert_mode:
# The displaced cell, as a complexchar so addch() can rewrite it
Expand All @@ -99,17 +104,39 @@ def _insert_printable_char(self, ch):
# scrollable window).
if isinstance(ch, int):
self.win.insch(self._decode(ch), ch & curses.A_ATTRIBUTES)
elif (isinstance(ch, str)
and unicodedata.category(ch) in ('Mn', 'Me')):
if not corner_typed and x > 0:
# The cursor was moved here: attach the combining
# character to the preceding one, as elsewhere.
self.win.addch(ch)
break
# Attach it to the character typed into this cell.
cell = self.win.in_wch()
try:
cell = curses.complexchar(str(cell) + ch,
cell.attr, cell.pair)
except TypeError:
# The cell is full; drop it, as addch() does.
pass
else:
self.win.insch(cell)
else:
self.win.insch(ch)
if backyx is None:
# insch() leaves the cursor on the typed character.
self._corner_typed = True
break
# Pass the character as text: an integer is a byte, which addch()
# takes as a code point on a wide build.
if isinstance(ch, int):
self.win.addch(self._decode(ch), ch & curses.A_ATTRIBUTES)
else:
self.win.addch(ch)
# In insert mode keep shifting cells right until a blank one.
if not self.insert_mode or not str(oldch).isprintable():
# In insert mode keep shifting cells right until a blank one. A
# combining character joins the previous cell without taking one.
if (not self.insert_mode or not str(oldch).isprintable()
or self.win.getyx() == (y, x)):
break
ch = oldch
(y, x) = self.win.getyx()
Expand All @@ -135,6 +162,7 @@ def do_command(self, ch):
elif self._printable_key(ch):
self._insert_printable_char(ch)
return 1
self._corner_typed = False
if ch == curses.ascii.SOH: # ^a
self.win.move(y, 0)
elif ch in (curses.ascii.STX,curses.KEY_LEFT,
Expand Down
40 changes: 40 additions & 0 deletions Lib/test/test_curses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2723,6 +2723,46 @@ def test_textbox_combining(self):
box.do_command(ch)
self.assertEqual(box.gather(), text + ' ')

@requires_wide_build
def test_textbox_combining_fill_last_cell(self):
# A combining mark typed into the lower-right cell joins its base.
for text in [
'abe\u0301', # COMBINING ACUTE ACCENT
'ab\u0e01\u0e31', # THAI CHARACTER MAI HAN-AKAT, combining() == 0
'abx\u20dd', # COMBINING ENCLOSING CIRCLE, combining() == 0
]:
with self.subTest(text=text):
if not self._encodable(text):
self.skipTest('the locale cannot encode %r' % text)
box, win = self._make_textbox(1, 3, stripspaces=False)
for ch in text:
box.do_command(ch)
self.assertEqual(box.gather(), text)

@requires_wide_build
def test_textbox_combining_typed_over(self):
# A combining mark joins the character typed just before it.
text = 'e\u0301'
if not self._encodable(text):
self.skipTest('the locale cannot encode %r' % text)
for insert_mode, x, expected in [
(False, 0, 'e\u0301bc'),
(False, 1, 'ae\u0301c'),
(False, 2, 'abe\u0301'),
(True, 0, 'e\u0301ab'),
(True, 1, 'ae\u0301b'),
(True, 2, 'abe\u0301'),
]:
with self.subTest(insert_mode=insert_mode, x=x):
box, win = self._make_textbox(1, 3, stripspaces=False)
for ch in 'abc':
box.do_command(ch)
box.insert_mode = insert_mode
win.move(0, x)
for ch in text:
box.do_command(ch)
self.assertEqual(box.gather(), expected)

@requires_wide_build
def test_textbox_double_width(self):
# A double-width (East Asian) character occupies two cells. gather()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix entering a combining character in the lower-right cell of
:class:`curses.textpad.Textbox`:
it no longer takes a cell of its own,
pushing its base character off the window.
In insert mode, a combining character no longer shifts the following text.
Loading