window.py 27 KB

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