123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928 |
- # -*- test-case-name: twisted.conch.test.test_window -*-
-
- """
- Simple insults-based widget library
-
- @author: Jp Calderone
- """
-
- import array
-
- from twisted.conch.insults import helper, insults
- from twisted.python import text as tptext
-
-
- class YieldFocus(Exception):
- """
- Input focus manipulation exception
- """
-
-
- class BoundedTerminalWrapper:
- 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:
- 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)
- text = self.contents[
- self._width * i : self._width * i + self._width
- ].tobytes()
- 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(b"\161" * (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(b"\170")
- 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(b"\154")
- terminal.write(b"\161" * (width - 2))
- terminal.write(b"\153")
- for n in range(height - 2):
- terminal.cursorPosition(left, top + n + 1)
- terminal.write(b"\170")
- terminal.cursorForward(width - 2)
- terminal.write(b"\170")
- terminal.cursorPosition(0, top + height - 1)
- terminal.write(b"\155")
- terminal.write(b"\161" * (width - 2))
- terminal.write(b"\152")
-
- 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
-
- @property
- def xOffset(self):
- return self._xOffset
-
- @xOffset.setter
- def xOffset(self, value):
- if self._xOffset != value:
- self._xOffset = value
- self.repaint()
-
- @property
- def yOffset(self):
- return self._yOffset
-
- @yOffset.setter
- def yOffset(self, value):
- if self._yOffset != value:
- self._yOffset = value
- self.repaint()
-
- _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 = "\N{BLACK LEFT-POINTING TRIANGLE}"
- _right = "\N{BLACK RIGHT-POINTING TRIANGLE}"
- _bar = "\N{LIGHT SHADE}"
- _slider = "\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 = "\N{BLACK UP-POINTING TRIANGLE}"
- _down = "\N{BLACK DOWN-POINTING TRIANGLE}"
- _bar = "\N{LIGHT SHADE}"
- _slider = "\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()
|