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

Revision 31872, 25.8 KB (checked in by exarkun, 12 months ago)

Merge scrolledarea-init-4197

Author: ali, exarkun
Reviewer: glyph, ivank
Fixes: #4197

Change the twisted.conch.insults.window.ScrolledArea initializer to not
pass the contained widget to the parent __init__, since the parent does
not expect it and on some versions of Python this results in a TypeError.

Line 
1# -*- test-case-name: twisted.conch.test.test_window -*-
2
3"""
4Simple insults-based widget library
5
6@author: Jp Calderone
7"""
8
9import array
10
11from twisted.conch.insults import insults, helper
12from twisted.python import text as tptext
13
14class YieldFocus(Exception):
15    """Input focus manipulation exception
16    """
17
18class 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
44class 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
113class 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
194class 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
248class 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
263class _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
345class HBox(_Box):
346    variableDimension = 0
347
348class VBox(_Box):
349    variableDimension = 1
350
351
352class 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
366class 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
409def 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
415def 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
423def 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
442class 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
484class 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
504class 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
560class PasswordInput(TextInput):
561    def _renderText(self):
562        return '*' * len(self.buffer)
563
564class 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
586class 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
609class 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
665class _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
680class 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
703class 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
731class 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
784def cursor(terminal, ch):
785    terminal.saveCursor()
786    terminal.selectGraphicRendition(str(insults.REVERSE_VIDEO))
787    terminal.write(ch)
788    terminal.restoreCursor()
789    terminal.cursorForward()
790
791class 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()
Note: See TracBrowser for help on using the browser.