Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

window.py 27KB

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