~landscape/zope3/ztk-1.1.3

« back to all changes in this revision

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

  • Committer: Andreas Hasenack
  • Date: 2009-07-20 17:49:16 UTC
  • Revision ID: andreas@canonical.com-20090720174916-g2tn6qmietz2hn0u
Revert twisted removal, it breaks several dozen tests [trivial]

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()