~landscape/zope3/newer-from-ztk

« back to all changes in this revision

Viewing changes to src/twisted/conch/insults/window.py

  • Committer: Thomas Hervé
  • Date: 2009-07-08 13:52:04 UTC
  • Revision ID: thomas@canonical.com-20090708135204-df5eesrthifpylf8
Remove twisted copy

Show diffs side-by-side

added added

removed removed

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