window.py 27 KB

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