blob: 24a964a1c7b8cde13c979539bd10853df3910740 [file] [log] [blame]
Andrew M. Kuchlinge0d00902000-07-11 10:38:24 +00001"""Simple textbox editing widget with Emacs-like keybindings."""
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +00002
Martin v. Löwis9d51fc82002-02-23 22:31:53 +00003import curses, ascii
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +00004
5def rectangle(win, uly, ulx, lry, lrx):
Andrew M. Kuchlinga13ea552004-10-19 19:21:20 +00006 """Draw a rectangle with corners at the provided upper-left
7 and lower-right coordinates.
8 """
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +00009 win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1)
10 win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
11 win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
12 win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1)
13 win.addch(uly, ulx, curses.ACS_ULCORNER)
14 win.addch(uly, lrx, curses.ACS_URCORNER)
15 win.addch(lry, lrx, curses.ACS_LRCORNER)
16 win.addch(lry, ulx, curses.ACS_LLCORNER)
17
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000018class Textbox:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000019 """Editing widget using the interior of a window object.
20 Supports the following Emacs-like key bindings:
21
22 Ctrl-A Go to left edge of window.
23 Ctrl-B Cursor left, wrapping to previous line if appropriate.
24 Ctrl-D Delete character under cursor.
Eric S. Raymond5af256d2000-08-04 07:33:18 +000025 Ctrl-E Go to right edge (stripspaces off) or end of line (stripspaces on).
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000026 Ctrl-F Cursor right, wrapping to next line when appropriate.
27 Ctrl-G Terminate, returning the window contents.
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000028 Ctrl-H Delete character backward.
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000029 Ctrl-J Terminate if the window is 1 line, otherwise insert newline.
30 Ctrl-K If line is blank, delete it, otherwise clear to end of line.
Eric S. Raymond5af256d2000-08-04 07:33:18 +000031 Ctrl-L Refresh screen.
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000032 Ctrl-N Cursor down; move down one line.
33 Ctrl-O Insert a blank line at cursor location.
34 Ctrl-P Cursor up; move up one line.
35
36 Move operations do nothing if the cursor is at an edge where the movement
37 is not possible. The following synonyms are supported where possible:
38
39 KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000040 KEY_BACKSPACE = Ctrl-h
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000041 """
42 def __init__(self, win):
43 self.win = win
44 (self.maxy, self.maxx) = win.getmaxyx()
45 self.maxy = self.maxy - 1
46 self.maxx = self.maxx - 1
47 self.stripspaces = 1
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000048 self.lastcmd = None
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000049 win.keypad(1)
50
Eric S. Raymond5af256d2000-08-04 07:33:18 +000051 def _end_of_line(self, y):
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000052 "Go to the location of the first blank on the given line."
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000053 last = self.maxx
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000054 while 1:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000055 if ascii.ascii(self.win.inch(y, last)) != ascii.SP:
Andrew M. Kuchling76276172005-06-02 00:10:04 +000056 last = min(self.maxx, last+1)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000057 break
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000058 elif last == 0:
59 break
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000060 last = last - 1
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000061 return last
62
63 def do_command(self, ch):
64 "Process a single editing command."
65 (y, x) = self.win.getyx()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000066 self.lastcmd = ch
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000067 if ascii.isprint(ch):
68 if y < self.maxy or x < self.maxx:
69 # The try-catch ignores the error we trigger from some curses
70 # versions by trying to write into the lowest-rightmost spot
Andrew M. Kuchlingfee31262001-08-13 13:47:23 +000071 # in the window.
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000072 try:
73 self.win.addch(ch)
Andrew M. Kuchlingfee31262001-08-13 13:47:23 +000074 except curses.error:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000075 pass
Guido van Rossumbffa52f2002-09-29 00:25:51 +000076 elif ch == ascii.SOH: # ^a
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000077 self.win.move(y, 0)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000078 elif ch in (ascii.STX,curses.KEY_LEFT, ascii.BS,curses.KEY_BACKSPACE):
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000079 if x > 0:
80 self.win.move(y, x-1)
81 elif y == 0:
82 pass
83 elif self.stripspaces:
Eric S. Raymond5af256d2000-08-04 07:33:18 +000084 self.win.move(y-1, self._end_of_line(y-1))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000085 else:
86 self.win.move(y-1, self.maxx)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000087 if ch in (ascii.BS, curses.KEY_BACKSPACE):
88 self.win.delch()
Guido van Rossumbffa52f2002-09-29 00:25:51 +000089 elif ch == ascii.EOT: # ^d
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000090 self.win.delch()
Guido van Rossumbffa52f2002-09-29 00:25:51 +000091 elif ch == ascii.ENQ: # ^e
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000092 if self.stripspaces:
Eric S. Raymond5af256d2000-08-04 07:33:18 +000093 self.win.move(y, self._end_of_line(y))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000094 else:
95 self.win.move(y, self.maxx)
Guido van Rossumbffa52f2002-09-29 00:25:51 +000096 elif ch in (ascii.ACK, curses.KEY_RIGHT): # ^f
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000097 if x < self.maxx:
98 self.win.move(y, x+1)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000099 elif y == self.maxy:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000100 pass
101 else:
102 self.win.move(y+1, 0)
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000103 elif ch == ascii.BEL: # ^g
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000104 return 0
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000105 elif ch == ascii.NL: # ^j
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000106 if self.maxy == 0:
107 return 0
108 elif y < self.maxy:
109 self.win.move(y+1, 0)
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000110 elif ch == ascii.VT: # ^k
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000111 if x == 0 and self._end_of_line(y) == 0:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000112 self.win.deleteln()
113 else:
Andrew M. Kuchlingccab0012004-10-19 19:29:40 +0000114 # first undo the effect of self._end_of_line
115 self.win.move(y, x)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000116 self.win.clrtoeol()
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000117 elif ch == ascii.FF: # ^l
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000118 self.win.refresh()
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000119 elif ch in (ascii.SO, curses.KEY_DOWN): # ^n
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000120 if y < self.maxy:
121 self.win.move(y+1, x)
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000122 if x > self._end_of_line(y+1):
123 self.win.move(y+1, self._end_of_line(y+1))
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000124 elif ch == ascii.SI: # ^o
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000125 self.win.insertln()
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000126 elif ch in (ascii.DLE, curses.KEY_UP): # ^p
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000127 if y > 0:
128 self.win.move(y-1, x)
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000129 if x > self._end_of_line(y-1):
130 self.win.move(y-1, self._end_of_line(y-1))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000131 return 1
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000132
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000133 def gather(self):
134 "Collect and return the contents of the window."
135 result = ""
136 for y in range(self.maxy+1):
137 self.win.move(y, 0)
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000138 stop = self._end_of_line(y)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000139 if stop == 0 and self.stripspaces:
140 continue
141 for x in range(self.maxx+1):
142 if self.stripspaces and x == stop:
143 break
144 result = result + chr(ascii.ascii(self.win.inch(y, x)))
145 if self.maxy > 0:
146 result = result + "\n"
147 return result
148
149 def edit(self, validate=None):
150 "Edit in the widget window and collect the results."
151 while 1:
152 ch = self.win.getch()
153 if validate:
154 ch = validate(ch)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000155 if not ch:
156 continue
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000157 if not self.do_command(ch):
158 break
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000159 self.win.refresh()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000160 return self.gather()
161
162if __name__ == '__main__':
163 def test_editbox(stdscr):
Andrew M. Kuchlinga13ea552004-10-19 19:21:20 +0000164 ncols, nlines = 9, 4
165 uly, ulx = 15, 20
Andrew M. Kuchling8520b942004-10-19 19:36:09 +0000166 stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
Andrew M. Kuchlinga13ea552004-10-19 19:21:20 +0000167 win = curses.newwin(nlines, ncols, uly, ulx)
168 rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000169 stdscr.refresh()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000170 return Textbox(win).edit()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000171
172 str = curses.wrapper(test_editbox)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000173 print('Contents of text box:', repr(str))