1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027 |
- # -*- test-case-name: twisted.conch.test.test_window -*-
- """
- Simple insults-based widget library
- @author: Jp Calderone
- """
- import array
- from twisted.conch.insults import insults, helper
- from twisted.python import text as tptext
- from twisted.python.compat import (_PY3, _bytesChr as chr)
- class YieldFocus(Exception):
- """
- Input focus manipulation exception
- """
- class BoundedTerminalWrapper(object):
- def __init__(self, terminal, width, height, xoff, yoff):
- self.width = width
- self.height = height
- self.xoff = xoff
- self.yoff = yoff
- self.terminal = terminal
- self.cursorForward = terminal.cursorForward
- self.selectCharacterSet = terminal.selectCharacterSet
- self.selectGraphicRendition = terminal.selectGraphicRendition
- self.saveCursor = terminal.saveCursor
- self.restoreCursor = terminal.restoreCursor
- def cursorPosition(self, x, y):
- return self.terminal.cursorPosition(
- self.xoff + min(self.width, x),
- self.yoff + min(self.height, y)
- )
- def cursorHome(self):
- return self.terminal.cursorPosition(
- self.xoff, self.yoff)
- def write(self, data):
- return self.terminal.write(data)
- class Widget(object):
- focused = False
- parent = None
- dirty = False
- width = height = None
- def repaint(self):
- if not self.dirty:
- self.dirty = True
- if self.parent is not None and not self.parent.dirty:
- self.parent.repaint()
- def filthy(self):
- self.dirty = True
- def redraw(self, width, height, terminal):
- self.filthy()
- self.draw(width, height, terminal)
- def draw(self, width, height, terminal):
- if width != self.width or height != self.height or self.dirty:
- self.width = width
- self.height = height
- self.dirty = False
- self.render(width, height, terminal)
- def render(self, width, height, terminal):
- pass
- def sizeHint(self):
- return None
- def keystrokeReceived(self, keyID, modifier):
- if keyID == b'\t':
- self.tabReceived(modifier)
- elif keyID == b'\x7f':
- self.backspaceReceived()
- elif keyID in insults.FUNCTION_KEYS:
- self.functionKeyReceived(keyID, modifier)
- else:
- self.characterReceived(keyID, modifier)
- def tabReceived(self, modifier):
- # XXX TODO - Handle shift+tab
- raise YieldFocus()
- def focusReceived(self):
- """
- Called when focus is being given to this widget.
- May raise YieldFocus is this widget does not want focus.
- """
- self.focused = True
- self.repaint()
- def focusLost(self):
- self.focused = False
- self.repaint()
- def backspaceReceived(self):
- pass
- def functionKeyReceived(self, keyID, modifier):
- name = keyID
- if not isinstance(keyID, str):
- name = name.decode("utf-8")
- func = getattr(self, 'func_' + name, None)
- if func is not None:
- func(modifier)
- def characterReceived(self, keyID, modifier):
- pass
- class ContainerWidget(Widget):
- """
- @ivar focusedChild: The contained widget which currently has
- focus, or None.
- """
- focusedChild = None
- focused = False
- def __init__(self):
- Widget.__init__(self)
- self.children = []
- def addChild(self, child):
- assert child.parent is None
- child.parent = self
- self.children.append(child)
- if self.focusedChild is None and self.focused:
- try:
- child.focusReceived()
- except YieldFocus:
- pass
- else:
- self.focusedChild = child
- self.repaint()
- def remChild(self, child):
- assert child.parent is self
- child.parent = None
- self.children.remove(child)
- self.repaint()
- def filthy(self):
- for ch in self.children:
- ch.filthy()
- Widget.filthy(self)
- def render(self, width, height, terminal):
- for ch in self.children:
- ch.draw(width, height, terminal)
- def changeFocus(self):
- self.repaint()
- if self.focusedChild is not None:
- self.focusedChild.focusLost()
- focusedChild = self.focusedChild
- self.focusedChild = None
- try:
- curFocus = self.children.index(focusedChild) + 1
- except ValueError:
- raise YieldFocus()
- else:
- curFocus = 0
- while curFocus < len(self.children):
- try:
- self.children[curFocus].focusReceived()
- except YieldFocus:
- curFocus += 1
- else:
- self.focusedChild = self.children[curFocus]
- return
- # None of our children wanted focus
- raise YieldFocus()
- def focusReceived(self):
- self.changeFocus()
- self.focused = True
- def keystrokeReceived(self, keyID, modifier):
- if self.focusedChild is not None:
- try:
- self.focusedChild.keystrokeReceived(keyID, modifier)
- except YieldFocus:
- self.changeFocus()
- self.repaint()
- else:
- Widget.keystrokeReceived(self, keyID, modifier)
- class TopWindow(ContainerWidget):
- """
- A top-level container object which provides focus wrap-around and paint
- scheduling.
- @ivar painter: A no-argument callable which will be invoked when this
- widget needs to be redrawn.
- @ivar scheduler: A one-argument callable which will be invoked with a
- no-argument callable and should arrange for it to invoked at some point in
- the near future. The no-argument callable will cause this widget and all
- its children to be redrawn. It is typically beneficial for the no-argument
- callable to be invoked at the end of handling for whatever event is
- currently active; for example, it might make sense to call it at the end of
- L{twisted.conch.insults.insults.ITerminalProtocol.keystrokeReceived}.
- Note, however, that since calls to this may also be made in response to no
- apparent event, arrangements should be made for the function to be called
- even if an event handler such as C{keystrokeReceived} is not on the call
- stack (eg, using
- L{reactor.callLater<twisted.internet.interfaces.IReactorTime.callLater>}
- with a short timeout).
- """
- focused = True
- def __init__(self, painter, scheduler):
- ContainerWidget.__init__(self)
- self.painter = painter
- self.scheduler = scheduler
- _paintCall = None
- def repaint(self):
- if self._paintCall is None:
- self._paintCall = object()
- self.scheduler(self._paint)
- ContainerWidget.repaint(self)
- def _paint(self):
- self._paintCall = None
- self.painter()
- def changeFocus(self):
- try:
- ContainerWidget.changeFocus(self)
- except YieldFocus:
- try:
- ContainerWidget.changeFocus(self)
- except YieldFocus:
- pass
- def keystrokeReceived(self, keyID, modifier):
- try:
- ContainerWidget.keystrokeReceived(self, keyID, modifier)
- except YieldFocus:
- self.changeFocus()
- class AbsoluteBox(ContainerWidget):
- def moveChild(self, child, x, y):
- for n in range(len(self.children)):
- if self.children[n][0] is child:
- self.children[n] = (child, x, y)
- break
- else:
- raise ValueError("No such child", child)
- def render(self, width, height, terminal):
- for (ch, x, y) in self.children:
- wrap = BoundedTerminalWrapper(terminal, width - x, height - y, x, y)
- ch.draw(width, height, wrap)
- class _Box(ContainerWidget):
- TOP, CENTER, BOTTOM = range(3)
- def __init__(self, gravity=CENTER):
- ContainerWidget.__init__(self)
- self.gravity = gravity
- def sizeHint(self):
- height = 0
- width = 0
- for ch in self.children:
- hint = ch.sizeHint()
- if hint is None:
- hint = (None, None)
- if self.variableDimension == 0:
- if hint[0] is None:
- width = None
- elif width is not None:
- width += hint[0]
- if hint[1] is None:
- height = None
- elif height is not None:
- height = max(height, hint[1])
- else:
- if hint[0] is None:
- width = None
- elif width is not None:
- width = max(width, hint[0])
- if hint[1] is None:
- height = None
- elif height is not None:
- height += hint[1]
- return width, height
- def render(self, width, height, terminal):
- if not self.children:
- return
- greedy = 0
- wants = []
- for ch in self.children:
- hint = ch.sizeHint()
- if hint is None:
- hint = (None, None)
- if hint[self.variableDimension] is None:
- greedy += 1
- wants.append(hint[self.variableDimension])
- length = (width, height)[self.variableDimension]
- totalWant = sum([w for w in wants if w is not None])
- if greedy:
- leftForGreedy = int((length - totalWant) / greedy)
- widthOffset = heightOffset = 0
- for want, ch in zip(wants, self.children):
- if want is None:
- want = leftForGreedy
- subWidth, subHeight = width, height
- if self.variableDimension == 0:
- subWidth = want
- else:
- subHeight = want
- wrap = BoundedTerminalWrapper(
- terminal,
- subWidth,
- subHeight,
- widthOffset,
- heightOffset,
- )
- ch.draw(subWidth, subHeight, wrap)
- if self.variableDimension == 0:
- widthOffset += want
- else:
- heightOffset += want
- class HBox(_Box):
- variableDimension = 0
- class VBox(_Box):
- variableDimension = 1
- class Packer(ContainerWidget):
- def render(self, width, height, terminal):
- if not self.children:
- return
- root = int(len(self.children) ** 0.5 + 0.5)
- boxes = [VBox() for n in range(root)]
- for n, ch in enumerate(self.children):
- boxes[n % len(boxes)].addChild(ch)
- h = HBox()
- map(h.addChild, boxes)
- h.render(width, height, terminal)
- class Canvas(Widget):
- focused = False
- contents = None
- def __init__(self):
- Widget.__init__(self)
- self.resize(1, 1)
- def resize(self, width, height):
- contents = array.array('B', b' ' * width * height)
- if self.contents is not None:
- for x in range(min(width, self._width)):
- for y in range(min(height, self._height)):
- contents[width * y + x] = self[x, y]
- self.contents = contents
- self._width = width
- self._height = height
- if self.x >= width:
- self.x = width - 1
- if self.y >= height:
- self.y = height - 1
- def __getitem__(self, index):
- (x, y) = index
- return self.contents[(self._width * y) + x]
- def __setitem__(self, index, value):
- (x, y) = index
- self.contents[(self._width * y) + x] = value
- def clear(self):
- self.contents = array.array('B', b' ' * len(self.contents))
- def render(self, width, height, terminal):
- if not width or not height:
- return
- if width != self._width or height != self._height:
- self.resize(width, height)
- for i in range(height):
- terminal.cursorPosition(0, i)
- if _PY3:
- text = self.contents[self._width * i:
- self._width * i + self._width
- ].tobytes()
- else:
- text = self.contents[self._width * i:
- self._width * i + self._width
- ].tostring()
- text = text[:width]
- terminal.write(text)
- def horizontalLine(terminal, y, left, right):
- terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
- terminal.cursorPosition(left, y)
- terminal.write(chr(0o161) * (right - left))
- terminal.selectCharacterSet(insults.CS_US, insults.G0)
- def verticalLine(terminal, x, top, bottom):
- terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
- for n in range(top, bottom):
- terminal.cursorPosition(x, n)
- terminal.write(chr(0o170))
- terminal.selectCharacterSet(insults.CS_US, insults.G0)
- def rectangle(terminal, position, dimension):
- """
- Draw a rectangle
- @type position: L{tuple}
- @param position: A tuple of the (top, left) coordinates of the rectangle.
- @type dimension: L{tuple}
- @param dimension: A tuple of the (width, height) size of the rectangle.
- """
- (top, left) = position
- (width, height) = dimension
- terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
- terminal.cursorPosition(top, left)
- terminal.write(chr(0o154))
- terminal.write(chr(0o161) * (width - 2))
- terminal.write(chr(0o153))
- for n in range(height - 2):
- terminal.cursorPosition(left, top + n + 1)
- terminal.write(chr(0o170))
- terminal.cursorForward(width - 2)
- terminal.write(chr(0o170))
- terminal.cursorPosition(0, top + height - 1)
- terminal.write(chr(0o155))
- terminal.write(chr(0o161) * (width - 2))
- terminal.write(chr(0o152))
- terminal.selectCharacterSet(insults.CS_US, insults.G0)
- class Border(Widget):
- def __init__(self, containee):
- Widget.__init__(self)
- self.containee = containee
- self.containee.parent = self
- def focusReceived(self):
- return self.containee.focusReceived()
- def focusLost(self):
- return self.containee.focusLost()
- def keystrokeReceived(self, keyID, modifier):
- return self.containee.keystrokeReceived(keyID, modifier)
- def sizeHint(self):
- hint = self.containee.sizeHint()
- if hint is None:
- hint = (None, None)
- if hint[0] is None:
- x = None
- else:
- x = hint[0] + 2
- if hint[1] is None:
- y = None
- else:
- y = hint[1] + 2
- return x, y
- def filthy(self):
- self.containee.filthy()
- Widget.filthy(self)
- def render(self, width, height, terminal):
- if self.containee.focused:
- terminal.write(b'\x1b[31m')
- rectangle(terminal, (0, 0), (width, height))
- terminal.write(b'\x1b[0m')
- wrap = BoundedTerminalWrapper(terminal, width - 2, height - 2, 1, 1)
- self.containee.draw(width - 2, height - 2, wrap)
- class Button(Widget):
- def __init__(self, label, onPress):
- Widget.__init__(self)
- self.label = label
- self.onPress = onPress
- def sizeHint(self):
- return len(self.label), 1
- def characterReceived(self, keyID, modifier):
- if keyID == b'\r':
- self.onPress()
- def render(self, width, height, terminal):
- terminal.cursorPosition(0, 0)
- if self.focused:
- terminal.write(b'\x1b[1m' + self.label + b'\x1b[0m')
- else:
- terminal.write(self.label)
- class TextInput(Widget):
- def __init__(self, maxwidth, onSubmit):
- Widget.__init__(self)
- self.onSubmit = onSubmit
- self.maxwidth = maxwidth
- self.buffer = b''
- self.cursor = 0
- def setText(self, text):
- self.buffer = text[:self.maxwidth]
- self.cursor = len(self.buffer)
- self.repaint()
- def func_LEFT_ARROW(self, modifier):
- if self.cursor > 0:
- self.cursor -= 1
- self.repaint()
- def func_RIGHT_ARROW(self, modifier):
- if self.cursor < len(self.buffer):
- self.cursor += 1
- self.repaint()
- def backspaceReceived(self):
- if self.cursor > 0:
- self.buffer = self.buffer[:self.cursor - 1] + self.buffer[self.cursor:]
- self.cursor -= 1
- self.repaint()
- def characterReceived(self, keyID, modifier):
- if keyID == b'\r':
- self.onSubmit(self.buffer)
- else:
- if len(self.buffer) < self.maxwidth:
- self.buffer = self.buffer[:self.cursor] + keyID + self.buffer[self.cursor:]
- self.cursor += 1
- self.repaint()
- def sizeHint(self):
- return self.maxwidth + 1, 1
- def render(self, width, height, terminal):
- currentText = self._renderText()
- terminal.cursorPosition(0, 0)
- if self.focused:
- terminal.write(currentText[:self.cursor])
- cursor(terminal, currentText[self.cursor:self.cursor+1] or b' ')
- terminal.write(currentText[self.cursor+1:])
- terminal.write(b' ' * (self.maxwidth - len(currentText) + 1))
- else:
- more = self.maxwidth - len(currentText)
- terminal.write(currentText + b'_' * more)
- def _renderText(self):
- return self.buffer
- class PasswordInput(TextInput):
- def _renderText(self):
- return '*' * len(self.buffer)
- class TextOutput(Widget):
- text = b''
- def __init__(self, size=None):
- Widget.__init__(self)
- self.size = size
- def sizeHint(self):
- return self.size
- def render(self, width, height, terminal):
- terminal.cursorPosition(0, 0)
- text = self.text[:width]
- terminal.write(text + b' ' * (width - len(text)))
- def setText(self, text):
- self.text = text
- self.repaint()
- def focusReceived(self):
- raise YieldFocus()
- class TextOutputArea(TextOutput):
- WRAP, TRUNCATE = range(2)
- def __init__(self, size=None, longLines=WRAP):
- TextOutput.__init__(self, size)
- self.longLines = longLines
- def render(self, width, height, terminal):
- n = 0
- inputLines = self.text.splitlines()
- outputLines = []
- while inputLines:
- if self.longLines == self.WRAP:
- line = inputLines.pop(0)
- if not isinstance(line, str):
- line = line.decode("utf-8")
- wrappedLines = []
- for wrappedLine in tptext.greedyWrap(line, width):
- if not isinstance(wrappedLine, bytes):
- wrappedLine = wrappedLine.encode("utf-8")
- wrappedLines.append(wrappedLine)
- outputLines.extend(wrappedLines or [b''])
- else:
- outputLines.append(inputLines.pop(0)[:width])
- if len(outputLines) >= height:
- break
- for n, L in enumerate(outputLines[:height]):
- terminal.cursorPosition(0, n)
- terminal.write(L)
- class Viewport(Widget):
- _xOffset = 0
- _yOffset = 0
- def xOffset():
- def get(self):
- return self._xOffset
- def set(self, value):
- if self._xOffset != value:
- self._xOffset = value
- self.repaint()
- return get, set
- xOffset = property(*xOffset())
- def yOffset():
- def get(self):
- return self._yOffset
- def set(self, value):
- if self._yOffset != value:
- self._yOffset = value
- self.repaint()
- return get, set
- yOffset = property(*yOffset())
- _width = 160
- _height = 24
- def __init__(self, containee):
- Widget.__init__(self)
- self.containee = containee
- self.containee.parent = self
- self._buf = helper.TerminalBuffer()
- self._buf.width = self._width
- self._buf.height = self._height
- self._buf.connectionMade()
- def filthy(self):
- self.containee.filthy()
- Widget.filthy(self)
- def render(self, width, height, terminal):
- self.containee.draw(self._width, self._height, self._buf)
- # XXX /Lame/
- for y, line in enumerate(self._buf.lines[self._yOffset:self._yOffset + height]):
- terminal.cursorPosition(0, y)
- n = 0
- for n, (ch, attr) in enumerate(line[self._xOffset:self._xOffset + width]):
- if ch is self._buf.void:
- ch = b' '
- terminal.write(ch)
- if n < width:
- terminal.write(b' ' * (width - n - 1))
- class _Scrollbar(Widget):
- def __init__(self, onScroll):
- Widget.__init__(self)
- self.onScroll = onScroll
- self.percent = 0.0
- def smaller(self):
- self.percent = min(1.0, max(0.0, self.onScroll(-1)))
- self.repaint()
- def bigger(self):
- self.percent = min(1.0, max(0.0, self.onScroll(+1)))
- self.repaint()
- class HorizontalScrollbar(_Scrollbar):
- def sizeHint(self):
- return (None, 1)
- def func_LEFT_ARROW(self, modifier):
- self.smaller()
- def func_RIGHT_ARROW(self, modifier):
- self.bigger()
- _left = u'\N{BLACK LEFT-POINTING TRIANGLE}'
- _right = u'\N{BLACK RIGHT-POINTING TRIANGLE}'
- _bar = u'\N{LIGHT SHADE}'
- _slider = u'\N{DARK SHADE}'
- def render(self, width, height, terminal):
- terminal.cursorPosition(0, 0)
- n = width - 3
- before = int(n * self.percent)
- after = n - before
- me = self._left + (self._bar * before) + self._slider + (self._bar * after) + self._right
- terminal.write(me.encode('utf-8'))
- class VerticalScrollbar(_Scrollbar):
- def sizeHint(self):
- return (1, None)
- def func_UP_ARROW(self, modifier):
- self.smaller()
- def func_DOWN_ARROW(self, modifier):
- self.bigger()
- _up = u'\N{BLACK UP-POINTING TRIANGLE}'
- _down = u'\N{BLACK DOWN-POINTING TRIANGLE}'
- _bar = u'\N{LIGHT SHADE}'
- _slider = u'\N{DARK SHADE}'
- def render(self, width, height, terminal):
- terminal.cursorPosition(0, 0)
- knob = int(self.percent * (height - 2))
- terminal.write(self._up.encode('utf-8'))
- for i in range(1, height - 1):
- terminal.cursorPosition(0, i)
- if i != (knob + 1):
- terminal.write(self._bar.encode('utf-8'))
- else:
- terminal.write(self._slider.encode('utf-8'))
- terminal.cursorPosition(0, height - 1)
- terminal.write(self._down.encode('utf-8'))
- class ScrolledArea(Widget):
- """
- A L{ScrolledArea} contains another widget wrapped in a viewport and
- vertical and horizontal scrollbars for moving the viewport around.
- """
- def __init__(self, containee):
- Widget.__init__(self)
- self._viewport = Viewport(containee)
- self._horiz = HorizontalScrollbar(self._horizScroll)
- self._vert = VerticalScrollbar(self._vertScroll)
- for w in self._viewport, self._horiz, self._vert:
- w.parent = self
- def _horizScroll(self, n):
- self._viewport.xOffset += n
- self._viewport.xOffset = max(0, self._viewport.xOffset)
- return self._viewport.xOffset / 25.0
- def _vertScroll(self, n):
- self._viewport.yOffset += n
- self._viewport.yOffset = max(0, self._viewport.yOffset)
- return self._viewport.yOffset / 25.0
- def func_UP_ARROW(self, modifier):
- self._vert.smaller()
- def func_DOWN_ARROW(self, modifier):
- self._vert.bigger()
- def func_LEFT_ARROW(self, modifier):
- self._horiz.smaller()
- def func_RIGHT_ARROW(self, modifier):
- self._horiz.bigger()
- def filthy(self):
- self._viewport.filthy()
- self._horiz.filthy()
- self._vert.filthy()
- Widget.filthy(self)
- def render(self, width, height, terminal):
- wrapper = BoundedTerminalWrapper(terminal, width - 2, height - 2, 1, 1)
- self._viewport.draw(width - 2, height - 2, wrapper)
- if self.focused:
- terminal.write(b'\x1b[31m')
- horizontalLine(terminal, 0, 1, width - 1)
- verticalLine(terminal, 0, 1, height - 1)
- self._vert.draw(1, height - 1, BoundedTerminalWrapper(terminal, 1, height - 1, width - 1, 0))
- self._horiz.draw(width, 1, BoundedTerminalWrapper(terminal, width, 1, 0, height - 1))
- terminal.write(b'\x1b[0m')
- def cursor(terminal, ch):
- terminal.saveCursor()
- terminal.selectGraphicRendition(str(insults.REVERSE_VIDEO))
- terminal.write(ch)
- terminal.restoreCursor()
- terminal.cursorForward()
- class Selection(Widget):
- # Index into the sequence
- focusedIndex = 0
- # Offset into the displayed subset of the sequence
- renderOffset = 0
- def __init__(self, sequence, onSelect, minVisible=None):
- Widget.__init__(self)
- self.sequence = sequence
- self.onSelect = onSelect
- self.minVisible = minVisible
- if minVisible is not None:
- self._width = max(map(len, self.sequence))
- def sizeHint(self):
- if self.minVisible is not None:
- return self._width, self.minVisible
- def func_UP_ARROW(self, modifier):
- if self.focusedIndex > 0:
- self.focusedIndex -= 1
- if self.renderOffset > 0:
- self.renderOffset -= 1
- self.repaint()
- def func_PGUP(self, modifier):
- if self.renderOffset != 0:
- self.focusedIndex -= self.renderOffset
- self.renderOffset = 0
- else:
- self.focusedIndex = max(0, self.focusedIndex - self.height)
- self.repaint()
- def func_DOWN_ARROW(self, modifier):
- if self.focusedIndex < len(self.sequence) - 1:
- self.focusedIndex += 1
- if self.renderOffset < self.height - 1:
- self.renderOffset += 1
- self.repaint()
- def func_PGDN(self, modifier):
- if self.renderOffset != self.height - 1:
- change = self.height - self.renderOffset - 1
- if change + self.focusedIndex >= len(self.sequence):
- change = len(self.sequence) - self.focusedIndex - 1
- self.focusedIndex += change
- self.renderOffset = self.height - 1
- else:
- self.focusedIndex = min(len(self.sequence) - 1, self.focusedIndex + self.height)
- self.repaint()
- def characterReceived(self, keyID, modifier):
- if keyID == b'\r':
- self.onSelect(self.sequence[self.focusedIndex])
- def render(self, width, height, terminal):
- self.height = height
- start = self.focusedIndex - self.renderOffset
- if start > len(self.sequence) - height:
- start = max(0, len(self.sequence) - height)
- elements = self.sequence[start:start+height]
- for n, ele in enumerate(elements):
- terminal.cursorPosition(0, n)
- if n == self.renderOffset:
- terminal.saveCursor()
- if self.focused:
- modes = str(insults.REVERSE_VIDEO), str(insults.BOLD)
- else:
- modes = str(insults.REVERSE_VIDEO),
- terminal.selectGraphicRendition(*modes)
- text = ele[:width]
- terminal.write(text + (b' ' * (width - len(text))))
- if n == self.renderOffset:
- terminal.restoreCursor()
|