root / trunk / twisted / conch / insults / window.py

Revision 24441, 25.6 kB (checked in by thijs, 1 year ago)

Merge maintainer-email-2438: Get rid of references to maintainer email addresses from code.

Author: thijs
Reviewer: exarkun
Fixes: #2438

Line 
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     def __init__(self, containee):
733         Widget.__init__(self, containee)
734         self._viewport = Viewport(containee)
735         self._horiz = HorizontalScrollbar(self._horizScroll)
736         self._vert = VerticalScrollbar(self._vertScroll)
737
738         for w in self._viewport, self._horiz, self._vert:
739             w.parent = self
740
741     def _horizScroll(self, n):
742         self._viewport.xOffset += n
743         self._viewport.xOffset = max(0, self._viewport.xOffset)
744         return self._viewport.xOffset / 25.0
745
746     def _vertScroll(self, n):
747         self._viewport.yOffset += n
748         self._viewport.yOffset = max(0, self._viewport.yOffset)
749         return self._viewport.yOffset / 25.0
750
751     def func_UP_ARROW(self, modifier):
752         self._vert.smaller()
753
754     def func_DOWN_ARROW(self, modifier):
755         self._vert.bigger()
756
757     def func_LEFT_ARROW(self, modifier):
758         self._horiz.smaller()
759
760     def func_RIGHT_ARROW(self, modifier):
761         self._horiz.bigger()
762
763     def filthy(self):
764         self._viewport.filthy()
765         self._horiz.filthy()
766         self._vert.filthy()
767         Widget.filthy(self)
768
769     def render(self, width, height, terminal):
770         wrapper = BoundedTerminalWrapper(terminal, width - 2, height - 2, 1, 1)
771         self._viewport.draw(width - 2, height - 2, wrapper)
772         if self.focused:
773             terminal.write('\x1b[31m')
774         horizontalLine(terminal, 0, 1, width - 1)
775         verticalLine(terminal, 0, 1, height - 1)
776         self._vert.draw(1, height - 1, BoundedTerminalWrapper(terminal, 1, height - 1, width - 1, 0))
777         self._horiz.draw(width, 1, BoundedTerminalWrapper(terminal, width, 1, 0, height - 1))
778         terminal.write('\x1b[0m')
779
780 def cursor(terminal, ch):
781     terminal.saveCursor()
782     terminal.selectGraphicRendition(str(insults.REVERSE_VIDEO))
783     terminal.write(ch)
784     terminal.restoreCursor()
785     terminal.cursorForward()
786
787 class Selection(Widget):
788     # Index into the sequence
789     focusedIndex = 0
790
791     # Offset into the displayed subset of the sequence
792     renderOffset = 0
793
794     def __init__(self, sequence, onSelect, minVisible=None):
795         Widget.__init__(self)
796         self.sequence = sequence
797         self.onSelect = onSelect
798         self.minVisible = minVisible
799         if minVisible is not None:
800             self._width = max(map(len, self.sequence))
801
802     def sizeHint(self):
803         if self.minVisible is not None:
804             return self._width, self.minVisible
805
806     def func_UP_ARROW(self, modifier):
807         if self.focusedIndex > 0:
808             self.focusedIndex -= 1
809             if self.renderOffset > 0:
810                 self.renderOffset -= 1
811             self.repaint()
812
813     def func_PGUP(self, modifier):
814         if self.renderOffset != 0:
815             self.focusedIndex -= self.renderOffset
816             self.renderOffset = 0
817         else:
818             self.focusedIndex = max(0, self.focusedIndex - self.height)
819         self.repaint()
820
821     def func_DOWN_ARROW(self, modifier):
822         if self.focusedIndex < len(self.sequence) - 1:
823             self.focusedIndex += 1
824             if self.renderOffset < self.height - 1:
825                 self.renderOffset += 1
826             self.repaint()
827
828
829     def func_PGDN(self, modifier):
830         if self.renderOffset != self.height - 1:
831             change = self.height - self.renderOffset - 1
832             if change + self.focusedIndex >= len(self.sequence):
833                 change = len(self.sequence) - self.focusedIndex - 1
834             self.focusedIndex += change
835             self.renderOffset = self.height - 1
836         else:
837             self.focusedIndex = min(len(self.sequence) - 1, self.focusedIndex + self.height)
838         self.repaint()
839
840     def characterReceived(self, keyID, modifier):
841         if keyID == '\r':
842             self.onSelect(self.sequence[self.focusedIndex])
843
844     def render(self, width, height, terminal):
845         self.height = height
846         start = self.focusedIndex - self.renderOffset
847         if start > len(self.sequence) - height:
848             start = max(0, len(self.sequence) - height)
849
850         elements = self.sequence[start:start+height]
851
852         for n, ele in enumerate(elements):
853             terminal.cursorPosition(0, n)
854             if n == self.renderOffset:
855                 terminal.saveCursor()
856                 if self.focused:
857                     modes = str(insults.REVERSE_VIDEO), str(insults.BOLD)
858                 else:
859                     modes = str(insults.REVERSE_VIDEO),
860                 terminal.selectGraphicRendition(*modes)
861             text = ele[:width]
862             terminal.write(text + (' ' * (width - len(text))))
863             if n == self.renderOffset:
864                 terminal.restoreCursor()
Note: See TracBrowser for help on using the browser.