Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / conch / insults / window.py
1 # -*- test-case-name: twisted.conch.test.test_window -*-
2
3 """
4 Simple insults-based widget library
5
6 @author: Jp Calderone
7 """
8
9 import array
10
11 from twisted.conch.insults import insults, helper
12 from twisted.python import text as tptext
13
14 class YieldFocus(Exception):
15     """Input focus manipulation exception
16     """
17
18 class BoundedTerminalWrapper(object):
19     def __init__(self, terminal, width, height, xoff, yoff):
20         self.width = width
21         self.height = height
22         self.xoff = xoff
23         self.yoff = yoff
24         self.terminal = terminal
25         self.cursorForward = terminal.cursorForward
26         self.selectCharacterSet = terminal.selectCharacterSet
27         self.selectGraphicRendition = terminal.selectGraphicRendition
28         self.saveCursor = terminal.saveCursor
29         self.restoreCursor = terminal.restoreCursor
30
31     def cursorPosition(self, x, y):
32         return self.terminal.cursorPosition(
33             self.xoff + min(self.width, x),
34             self.yoff + min(self.height, y)
35             )
36
37     def cursorHome(self):
38         return self.terminal.cursorPosition(
39             self.xoff, self.yoff)
40
41     def write(self, bytes):
42         return self.terminal.write(bytes)
43
44 class Widget(object):
45     focused = False
46     parent = None
47     dirty = False
48     width = height = None
49
50     def repaint(self):
51         if not self.dirty:
52             self.dirty = True
53         if self.parent is not None and not self.parent.dirty:
54             self.parent.repaint()
55
56     def filthy(self):
57         self.dirty = True
58
59     def redraw(self, width, height, terminal):
60         self.filthy()
61         self.draw(width, height, terminal)
62
63     def draw(self, width, height, terminal):
64         if width != self.width or height != self.height or self.dirty:
65             self.width = width
66             self.height = height
67             self.dirty = False
68             self.render(width, height, terminal)
69
70     def render(self, width, height, terminal):
71         pass
72
73     def sizeHint(self):
74         return None
75
76     def keystrokeReceived(self, keyID, modifier):
77         if keyID == '\t':
78             self.tabReceived(modifier)
79         elif keyID == '\x7f':
80             self.backspaceReceived()
81         elif keyID in insults.FUNCTION_KEYS:
82             self.functionKeyReceived(keyID, modifier)
83         else:
84             self.characterReceived(keyID, modifier)
85
86     def tabReceived(self, modifier):
87         # XXX TODO - Handle shift+tab
88         raise YieldFocus()
89
90     def focusReceived(self):
91         """Called when focus is being given to this widget.
92
93         May raise YieldFocus is this widget does not want focus.
94         """
95         self.focused = True
96         self.repaint()
97
98     def focusLost(self):
99         self.focused = False
100         self.repaint()
101
102     def backspaceReceived(self):
103         pass
104
105     def functionKeyReceived(self, keyID, modifier):
106         func = getattr(self, 'func_' + keyID.name, None)
107         if func is not None:
108             func(modifier)
109
110     def characterReceived(self, keyID, modifier):
111         pass
112
113 class ContainerWidget(Widget):
114     """
115     @ivar focusedChild: The contained widget which currently has
116     focus, or None.
117     """
118     focusedChild = None
119     focused = False
120
121     def __init__(self):
122         Widget.__init__(self)
123         self.children = []
124
125     def addChild(self, child):
126         assert child.parent is None
127         child.parent = self
128         self.children.append(child)
129         if self.focusedChild is None and self.focused:
130             try:
131                 child.focusReceived()
132             except YieldFocus:
133                 pass
134             else:
135                 self.focusedChild = child
136         self.repaint()
137
138     def remChild(self, child):
139         assert child.parent is self
140         child.parent = None
141         self.children.remove(child)
142         self.repaint()
143
144     def filthy(self):
145         for ch in self.children:
146             ch.filthy()
147         Widget.filthy(self)
148
149     def render(self, width, height, terminal):
150         for ch in self.children:
151             ch.draw(width, height, terminal)
152
153     def changeFocus(self):
154         self.repaint()
155
156         if self.focusedChild is not None:
157             self.focusedChild.focusLost()
158             focusedChild = self.focusedChild
159             self.focusedChild = None
160             try:
161                 curFocus = self.children.index(focusedChild) + 1
162             except ValueError:
163                 raise YieldFocus()
164         else:
165             curFocus = 0
166         while curFocus < len(self.children):
167             try:
168                 self.children[curFocus].focusReceived()
169             except YieldFocus:
170                 curFocus += 1
171             else:
172                 self.focusedChild = self.children[curFocus]
173                 return
174         # None of our children wanted focus
175         raise YieldFocus()
176
177
178     def focusReceived(self):
179         self.changeFocus()
180         self.focused = True
181
182
183     def keystrokeReceived(self, keyID, modifier):
184         if self.focusedChild is not None:
185             try:
186                 self.focusedChild.keystrokeReceived(keyID, modifier)
187             except YieldFocus:
188                 self.changeFocus()
189                 self.repaint()
190         else:
191             Widget.keystrokeReceived(self, keyID, modifier)
192
193
194 class TopWindow(ContainerWidget):
195     """
196     A top-level container object which provides focus wrap-around and paint
197     scheduling.
198
199     @ivar painter: A no-argument callable which will be invoked when this
200     widget needs to be redrawn.
201
202     @ivar scheduler: A one-argument callable which will be invoked with a
203     no-argument callable and should arrange for it to invoked at some point in
204     the near future.  The no-argument callable will cause this widget and all
205     its children to be redrawn.  It is typically beneficial for the no-argument
206     callable to be invoked at the end of handling for whatever event is
207     currently active; for example, it might make sense to call it at the end of
208     L{twisted.conch.insults.insults.ITerminalProtocol.keystrokeReceived}.
209     Note, however, that since calls to this may also be made in response to no
210     apparent event, arrangements should be made for the function to be called
211     even if an event handler such as C{keystrokeReceived} is not on the call
212     stack (eg, using C{reactor.callLater} with a short timeout).
213     """
214     focused = True
215
216     def __init__(self, painter, scheduler):
217         ContainerWidget.__init__(self)
218         self.painter = painter
219         self.scheduler = scheduler
220
221     _paintCall = None
222     def repaint(self):
223         if self._paintCall is None:
224             self._paintCall = object()
225             self.scheduler(self._paint)
226         ContainerWidget.repaint(self)
227
228     def _paint(self):
229         self._paintCall = None
230         self.painter()
231
232     def changeFocus(self):
233         try:
234             ContainerWidget.changeFocus(self)
235         except YieldFocus:
236             try:
237                 ContainerWidget.changeFocus(self)
238             except YieldFocus:
239                 pass
240
241     def keystrokeReceived(self, keyID, modifier):
242         try:
243             ContainerWidget.keystrokeReceived(self, keyID, modifier)
244         except YieldFocus:
245             self.changeFocus()
246
247
248 class AbsoluteBox(ContainerWidget):
249     def moveChild(self, child, x, y):
250         for n in range(len(self.children)):
251             if self.children[n][0] is child:
252                 self.children[n] = (child, x, y)
253                 break
254         else:
255             raise ValueError("No such child", child)
256
257     def render(self, width, height, terminal):
258         for (ch, x, y) in self.children:
259             wrap = BoundedTerminalWrapper(terminal, width - x, height - y, x, y)
260             ch.draw(width, height, wrap)
261
262
263 class _Box(ContainerWidget):
264     TOP, CENTER, BOTTOM = range(3)
265
266     def __init__(self, gravity=CENTER):
267         ContainerWidget.__init__(self)
268         self.gravity = gravity
269
270     def sizeHint(self):
271         height = 0
272         width = 0
273         for ch in self.children:
274             hint = ch.sizeHint()
275             if hint is None:
276                 hint = (None, None)
277
278             if self.variableDimension == 0:
279                 if hint[0] is None:
280                     width = None
281                 elif width is not None:
282                     width += hint[0]
283                 if hint[1] is None:
284                     height = None
285                 elif height is not None:
286                     height = max(height, hint[1])
287             else:
288                 if hint[0] is None:
289                     width = None
290                 elif width is not None:
291                     width = max(width, hint[0])
292                 if hint[1] is None:
293                     height = None
294                 elif height is not None:
295                     height += hint[1]
296
297         return width, height
298
299
300     def render(self, width, height, terminal):
301         if not self.children:
302             return
303
304         greedy = 0
305         wants = []
306         for ch in self.children:
307             hint = ch.sizeHint()
308             if hint is None:
309                 hint = (None, None)
310             if hint[self.variableDimension] is None:
311                 greedy += 1
312             wants.append(hint[self.variableDimension])
313
314         length = (width, height)[self.variableDimension]
315         totalWant = sum([w for w in wants if w is not None])
316         if greedy:
317             leftForGreedy = int((length - totalWant) / greedy)
318
319         widthOffset = heightOffset = 0
320
321         for want, ch in zip(wants, self.children):
322             if want is None:
323                 want = leftForGreedy
324
325             subWidth, subHeight = width, height
326             if self.variableDimension == 0:
327                 subWidth = want
328             else:
329                 subHeight = want
330
331             wrap = BoundedTerminalWrapper(
332                 terminal,
333                 subWidth,
334                 subHeight,
335                 widthOffset,
336                 heightOffset,
337                 )
338             ch.draw(subWidth, subHeight, wrap)
339             if self.variableDimension == 0:
340                 widthOffset += want
341             else:
342                 heightOffset += want
343
344
345 class HBox(_Box):
346     variableDimension = 0
347
348 class VBox(_Box):
349     variableDimension = 1
350
351
352 class Packer(ContainerWidget):
353     def render(self, width, height, terminal):
354         if not self.children:
355             return
356
357         root = int(len(self.children) ** 0.5 + 0.5)
358         boxes = [VBox() for n in range(root)]
359         for n, ch in enumerate(self.children):
360             boxes[n % len(boxes)].addChild(ch)
361         h = HBox()
362         map(h.addChild, boxes)
363         h.render(width, height, terminal)
364
365
366 class Canvas(Widget):
367     focused = False
368
369     contents = None
370
371     def __init__(self):
372         Widget.__init__(self)
373         self.resize(1, 1)
374
375     def resize(self, width, height):
376         contents = array.array('c', ' ' * width * height)
377         if self.contents is not None:
378             for x in range(min(width, self._width)):
379                 for y in range(min(height, self._height)):
380                     contents[width * y + x] = self[x, y]
381         self.contents = contents
382         self._width = width
383         self._height = height
384         if self.x >= width:
385             self.x = width - 1
386         if self.y >= height:
387             self.y = height - 1
388
389     def __getitem__(self, (x, y)):
390         return self.contents[(self._width * y) + x]
391
392     def __setitem__(self, (x, y), value):
393         self.contents[(self._width * y) + x] = value
394
395     def clear(self):
396         self.contents = array.array('c', ' ' * len(self.contents))
397
398     def render(self, width, height, terminal):
399         if not width or not height:
400             return
401
402         if width != self._width or height != self._height:
403             self.resize(width, height)
404         for i in range(height):
405             terminal.cursorPosition(0, i)
406             terminal.write(''.join(self.contents[self._width * i:self._width * i + self._width])[:width])
407
408
409 def horizontalLine(terminal, y, left, right):
410     terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
411     terminal.cursorPosition(left, y)
412     terminal.write(chr(0161) * (right - left))
413     terminal.selectCharacterSet(insults.CS_US, insults.G0)
414
415 def verticalLine(terminal, x, top, bottom):
416     terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
417     for n in xrange(top, bottom):
418         terminal.cursorPosition(x, n)
419         terminal.write(chr(0170))
420     terminal.selectCharacterSet(insults.CS_US, insults.G0)
421
422
423 def rectangle(terminal, (top, left), (width, height)):
424     terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
425
426     terminal.cursorPosition(top, left)
427     terminal.write(chr(0154))
428     terminal.write(chr(0161) * (width - 2))
429     terminal.write(chr(0153))
430     for n in range(height - 2):
431         terminal.cursorPosition(left, top + n + 1)
432         terminal.write(chr(0170))
433         terminal.cursorForward(width - 2)
434         terminal.write(chr(0170))
435     terminal.cursorPosition(0, top + height - 1)
436     terminal.write(chr(0155))
437     terminal.write(chr(0161) * (width - 2))
438     terminal.write(chr(0152))
439
440     terminal.selectCharacterSet(insults.CS_US, insults.G0)
441
442 class Border(Widget):
443     def __init__(self, containee):
444         Widget.__init__(self)
445         self.containee = containee
446         self.containee.parent = self
447
448     def focusReceived(self):
449         return self.containee.focusReceived()
450
451     def focusLost(self):
452         return self.containee.focusLost()
453
454     def keystrokeReceived(self, keyID, modifier):
455         return self.containee.keystrokeReceived(keyID, modifier)
456
457     def sizeHint(self):
458         hint = self.containee.sizeHint()
459         if hint is None:
460             hint = (None, None)
461         if hint[0] is None:
462             x = None
463         else:
464             x = hint[0] + 2
465         if hint[1] is None:
466             y = None
467         else:
468             y = hint[1] + 2
469         return x, y
470
471     def filthy(self):
472         self.containee.filthy()
473         Widget.filthy(self)
474
475     def render(self, width, height, terminal):
476         if self.containee.focused:
477             terminal.write('\x1b[31m')
478         rectangle(terminal, (0, 0), (width, height))
479         terminal.write('\x1b[0m')
480         wrap = BoundedTerminalWrapper(terminal, width - 2, height - 2, 1, 1)
481         self.containee.draw(width - 2, height - 2, wrap)
482
483
484 class Button(Widget):
485     def __init__(self, label, onPress):
486         Widget.__init__(self)
487         self.label = label
488         self.onPress = onPress
489
490     def sizeHint(self):
491         return len(self.label), 1
492
493     def characterReceived(self, keyID, modifier):
494         if keyID == '\r':
495             self.onPress()
496
497     def render(self, width, height, terminal):
498         terminal.cursorPosition(0, 0)
499         if self.focused:
500             terminal.write('\x1b[1m' + self.label + '\x1b[0m')
501         else:
502             terminal.write(self.label)
503
504 class TextInput(Widget):
505     def __init__(self, maxwidth, onSubmit):
506         Widget.__init__(self)
507         self.onSubmit = onSubmit
508         self.maxwidth = maxwidth
509         self.buffer = ''
510         self.cursor = 0
511
512     def setText(self, text):
513         self.buffer = text[:self.maxwidth]
514         self.cursor = len(self.buffer)
515         self.repaint()
516
517     def func_LEFT_ARROW(self, modifier):
518         if self.cursor > 0:
519             self.cursor -= 1
520             self.repaint()
521
522     def func_RIGHT_ARROW(self, modifier):
523         if self.cursor < len(self.buffer):
524             self.cursor += 1
525             self.repaint()
526
527     def backspaceReceived(self):
528         if self.cursor > 0:
529             self.buffer = self.buffer[:self.cursor - 1] + self.buffer[self.cursor:]
530             self.cursor -= 1
531             self.repaint()
532
533     def characterReceived(self, keyID, modifier):
534         if keyID == '\r':
535             self.onSubmit(self.buffer)
536         else:
537             if len(self.buffer) < self.maxwidth:
538                 self.buffer = self.buffer[:self.cursor] + keyID + self.buffer[self.cursor:]
539                 self.cursor += 1
540                 self.repaint()
541
542     def sizeHint(self):
543         return self.maxwidth + 1, 1
544
545     def render(self, width, height, terminal):
546         currentText = self._renderText()
547         terminal.cursorPosition(0, 0)
548         if self.focused:
549             terminal.write(currentText[:self.cursor])
550             cursor(terminal, currentText[self.cursor:self.cursor+1] or ' ')
551             terminal.write(currentText[self.cursor+1:])
552             terminal.write(' ' * (self.maxwidth - len(currentText) + 1))
553         else:
554             more = self.maxwidth - len(currentText)
555             terminal.write(currentText + '_' * more)
556
557     def _renderText(self):
558         return self.buffer
559
560 class PasswordInput(TextInput):
561     def _renderText(self):
562         return '*' * len(self.buffer)
563
564 class TextOutput(Widget):
565     text = ''
566
567     def __init__(self, size=None):
568         Widget.__init__(self)
569         self.size = size
570
571     def sizeHint(self):
572         return self.size
573
574     def render(self, width, height, terminal):
575         terminal.cursorPosition(0, 0)
576         text = self.text[:width]
577         terminal.write(text + ' ' * (width - len(text)))
578
579     def setText(self, text):
580         self.text = text
581         self.repaint()
582
583     def focusReceived(self):
584         raise YieldFocus()
585
586 class TextOutputArea(TextOutput):
587     WRAP, TRUNCATE = range(2)
588
589     def __init__(self, size=None, longLines=WRAP):
590         TextOutput.__init__(self, size)
591         self.longLines = longLines
592
593     def render(self, width, height, terminal):
594         n = 0
595         inputLines = self.text.splitlines()
596         outputLines = []
597         while inputLines:
598             if self.longLines == self.WRAP:
599                 wrappedLines = tptext.greedyWrap(inputLines.pop(0), width)
600                 outputLines.extend(wrappedLines or [''])
601             else:
602                 outputLines.append(inputLines.pop(0)[:width])
603             if len(outputLines) >= height:
604                 break
605         for n, L in enumerate(outputLines[:height]):
606             terminal.cursorPosition(0, n)
607             terminal.write(L)
608
609 class Viewport(Widget):
610     _xOffset = 0
611     _yOffset = 0
612
613     def xOffset():
614         def get(self):
615             return self._xOffset
616         def set(self, value):
617             if self._xOffset != value:
618                 self._xOffset = value
619                 self.repaint()
620         return get, set
621     xOffset = property(*xOffset())
622
623     def yOffset():
624         def get(self):
625             return self._yOffset
626         def set(self, value):
627             if self._yOffset != value:
628                 self._yOffset = value
629                 self.repaint()
630         return get, set
631     yOffset = property(*yOffset())
632
633     _width = 160
634     _height = 24
635
636     def __init__(self, containee):
637         Widget.__init__(self)
638         self.containee = containee
639         self.containee.parent = self
640
641         self._buf = helper.TerminalBuffer()
642         self._buf.width = self._width
643         self._buf.height = self._height
644         self._buf.connectionMade()
645
646     def filthy(self):
647         self.containee.filthy()
648         Widget.filthy(self)
649
650     def render(self, width, height, terminal):
651         self.containee.draw(self._width, self._height, self._buf)
652
653         # XXX /Lame/
654         for y, line in enumerate(self._buf.lines[self._yOffset:self._yOffset + height]):
655             terminal.cursorPosition(0, y)
656             n = 0
657             for n, (ch, attr) in enumerate(line[self._xOffset:self._xOffset + width]):
658                 if ch is self._buf.void:
659                     ch = ' '
660                 terminal.write(ch)
661             if n < width:
662                 terminal.write(' ' * (width - n - 1))
663
664
665 class _Scrollbar(Widget):
666     def __init__(self, onScroll):
667         Widget.__init__(self)
668         self.onScroll = onScroll
669         self.percent = 0.0
670
671     def smaller(self):
672         self.percent = min(1.0, max(0.0, self.onScroll(-1)))
673         self.repaint()
674
675     def bigger(self):
676         self.percent = min(1.0, max(0.0, self.onScroll(+1)))
677         self.repaint()
678
679
680 class HorizontalScrollbar(_Scrollbar):
681     def sizeHint(self):
682         return (None, 1)
683
684     def func_LEFT_ARROW(self, modifier):
685         self.smaller()
686
687     def func_RIGHT_ARROW(self, modifier):
688         self.bigger()
689
690     _left = u'\N{BLACK LEFT-POINTING TRIANGLE}'
691     _right = u'\N{BLACK RIGHT-POINTING TRIANGLE}'
692     _bar = u'\N{LIGHT SHADE}'
693     _slider = u'\N{DARK SHADE}'
694     def render(self, width, height, terminal):
695         terminal.cursorPosition(0, 0)
696         n = width - 3
697         before = int(n * self.percent)
698         after = n - before
699         me = self._left + (self._bar * before) + self._slider + (self._bar * after) + self._right
700         terminal.write(me.encode('utf-8'))
701
702
703 class VerticalScrollbar(_Scrollbar):
704     def sizeHint(self):
705         return (1, None)
706
707     def func_UP_ARROW(self, modifier):
708         self.smaller()
709
710     def func_DOWN_ARROW(self, modifier):
711         self.bigger()
712
713     _up = u'\N{BLACK UP-POINTING TRIANGLE}'
714     _down = u'\N{BLACK DOWN-POINTING TRIANGLE}'
715     _bar = u'\N{LIGHT SHADE}'
716     _slider = u'\N{DARK SHADE}'
717     def render(self, width, height, terminal):
718         terminal.cursorPosition(0, 0)
719         knob = int(self.percent * (height - 2))
720         terminal.write(self._up.encode('utf-8'))
721         for i in xrange(1, height - 1):
722             terminal.cursorPosition(0, i)
723             if i != (knob + 1):
724                 terminal.write(self._bar.encode('utf-8'))
725             else:
726                 terminal.write(self._slider.encode('utf-8'))
727         terminal.cursorPosition(0, height - 1)
728         terminal.write(self._down.encode('utf-8'))
729
730
731 class ScrolledArea(Widget):
732     """
733     A L{ScrolledArea} contains another widget wrapped in a viewport and
734     vertical and horizontal scrollbars for moving the viewport around.
735     """
736     def __init__(self, containee):
737         Widget.__init__(self)
738         self._viewport = Viewport(containee)
739         self._horiz = HorizontalScrollbar(self._horizScroll)
740         self._vert = VerticalScrollbar(self._vertScroll)
741
742         for w in self._viewport, self._horiz, self._vert:
743             w.parent = self
744
745     def _horizScroll(self, n):
746         self._viewport.xOffset += n
747         self._viewport.xOffset = max(0, self._viewport.xOffset)
748         return self._viewport.xOffset / 25.0
749
750     def _vertScroll(self, n):
751         self._viewport.yOffset += n
752         self._viewport.yOffset = max(0, self._viewport.yOffset)
753         return self._viewport.yOffset / 25.0
754
755     def func_UP_ARROW(self, modifier):
756         self._vert.smaller()
757
758     def func_DOWN_ARROW(self, modifier):
759         self._vert.bigger()
760
761     def func_LEFT_ARROW(self, modifier):
762         self._horiz.smaller()
763
764     def func_RIGHT_ARROW(self, modifier):
765         self._horiz.bigger()
766
767     def filthy(self):
768         self._viewport.filthy()
769         self._horiz.filthy()
770         self._vert.filthy()
771         Widget.filthy(self)
772
773     def render(self, width, height, terminal):
774         wrapper = BoundedTerminalWrapper(terminal, width - 2, height - 2, 1, 1)
775         self._viewport.draw(width - 2, height - 2, wrapper)
776         if self.focused:
777             terminal.write('\x1b[31m')
778         horizontalLine(terminal, 0, 1, width - 1)
779         verticalLine(terminal, 0, 1, height - 1)
780         self._vert.draw(1, height - 1, BoundedTerminalWrapper(terminal, 1, height - 1, width - 1, 0))
781         self._horiz.draw(width, 1, BoundedTerminalWrapper(terminal, width, 1, 0, height - 1))
782         terminal.write('\x1b[0m')
783
784 def cursor(terminal, ch):
785     terminal.saveCursor()
786     terminal.selectGraphicRendition(str(insults.REVERSE_VIDEO))
787     terminal.write(ch)
788     terminal.restoreCursor()
789     terminal.cursorForward()
790
791 class Selection(Widget):
792     # Index into the sequence
793     focusedIndex = 0
794
795     # Offset into the displayed subset of the sequence
796     renderOffset = 0
797
798     def __init__(self, sequence, onSelect, minVisible=None):
799         Widget.__init__(self)
800         self.sequence = sequence
801         self.onSelect = onSelect
802         self.minVisible = minVisible
803         if minVisible is not None:
804             self._width = max(map(len, self.sequence))
805
806     def sizeHint(self):
807         if self.minVisible is not None:
808             return self._width, self.minVisible
809
810     def func_UP_ARROW(self, modifier):
811         if self.focusedIndex > 0:
812             self.focusedIndex -= 1
813             if self.renderOffset > 0:
814                 self.renderOffset -= 1
815             self.repaint()
816
817     def func_PGUP(self, modifier):
818         if self.renderOffset != 0:
819             self.focusedIndex -= self.renderOffset
820             self.renderOffset = 0
821         else:
822             self.focusedIndex = max(0, self.focusedIndex - self.height)
823         self.repaint()
824
825     def func_DOWN_ARROW(self, modifier):
826         if self.focusedIndex < len(self.sequence) - 1:
827             self.focusedIndex += 1
828             if self.renderOffset < self.height - 1:
829                 self.renderOffset += 1
830             self.repaint()
831
832
833     def func_PGDN(self, modifier):
834         if self.renderOffset != self.height - 1:
835             change = self.height - self.renderOffset - 1
836             if change + self.focusedIndex >= len(self.sequence):
837                 change = len(self.sequence) - self.focusedIndex - 1
838             self.focusedIndex += change
839             self.renderOffset = self.height - 1
840         else:
841             self.focusedIndex = min(len(self.sequence) - 1, self.focusedIndex + self.height)
842         self.repaint()
843
844     def characterReceived(self, keyID, modifier):
845         if keyID == '\r':
846             self.onSelect(self.sequence[self.focusedIndex])
847
848     def render(self, width, height, terminal):
849         self.height = height
850         start = self.focusedIndex - self.renderOffset
851         if start > len(self.sequence) - height:
852             start = max(0, len(self.sequence) - height)
853
854         elements = self.sequence[start:start+height]
855
856         for n, ele in enumerate(elements):
857             terminal.cursorPosition(0, n)
858             if n == self.renderOffset:
859                 terminal.saveCursor()
860                 if self.focused:
861                     modes = str(insults.REVERSE_VIDEO), str(insults.BOLD)
862                 else:
863                     modes = str(insults.REVERSE_VIDEO),
864                 terminal.selectGraphicRendition(*modes)
865             text = ele[:width]
866             terminal.write(text + (' ' * (width - len(text))))
867             if n == self.renderOffset:
868                 terminal.restoreCursor()