termwidget.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. """
  2. Terminal emulator widget.
  3. Shows intput and output text. Allows to enter commands. Supports history.
  4. """
  5. import cgi
  6. from PyQt4 import QtCore
  7. from PyQt4.QtCore import pyqtSignal
  8. from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \
  9. QSizePolicy, QTextCursor, QTextEdit, \
  10. QVBoxLayout, QWidget
  11. class _ExpandableTextEdit(QTextEdit):
  12. """
  13. Class implements edit line, which expands themselves automatically
  14. """
  15. historyNext = pyqtSignal()
  16. historyPrev = pyqtSignal()
  17. def __init__(self, termWidget, *args):
  18. QTextEdit.__init__(self, *args)
  19. self.setStyleSheet("font: 9pt \"Courier\";")
  20. self._fittedHeight = 1
  21. self.textChanged.connect(self._fit_to_document)
  22. self._fit_to_document()
  23. self._termWidget = termWidget
  24. def sizeHint(self):
  25. """
  26. QWidget sizeHint impelemtation
  27. """
  28. hint = QTextEdit.sizeHint(self)
  29. hint.setHeight(self._fittedHeight)
  30. return hint
  31. def _fit_to_document(self):
  32. """
  33. Update widget height to fit all text
  34. """
  35. documentSize = self.document().size().toSize()
  36. self._fittedHeight = documentSize.height() + (self.height() - self.viewport().height())
  37. self.setMaximumHeight(self._fittedHeight)
  38. self.updateGeometry();
  39. def keyPressEvent(self, event):
  40. """
  41. Catch keyboard events. Process Enter, Up, Down
  42. """
  43. if event.matches(QKeySequence.InsertParagraphSeparator):
  44. text = self.toPlainText()
  45. if self._termWidget.is_command_complete(text):
  46. self._termWidget.exec_current_command()
  47. return
  48. elif event.matches(QKeySequence.MoveToNextLine):
  49. text = self.toPlainText()
  50. cursorPos = self.textCursor().position()
  51. textBeforeEnd = text[cursorPos:]
  52. # if len(textBeforeEnd.splitlines()) <= 1:
  53. if len(textBeforeEnd.split('\n')) <= 1:
  54. self.historyNext.emit()
  55. return
  56. elif event.matches(QKeySequence.MoveToPreviousLine):
  57. text = self.toPlainText()
  58. cursorPos = self.textCursor().position()
  59. textBeforeStart = text[:cursorPos]
  60. # lineCount = len(textBeforeStart.splitlines())
  61. lineCount = len(textBeforeStart.split('\n'))
  62. if len(textBeforeStart) > 0 and \
  63. (textBeforeStart[-1] == '\n' or textBeforeStart[-1] == '\r'):
  64. lineCount += 1
  65. if lineCount <= 1:
  66. self.historyPrev.emit()
  67. return
  68. elif event.matches(QKeySequence.MoveToNextPage) or \
  69. event.matches(QKeySequence.MoveToPreviousPage):
  70. return self._termWidget.browser().keyPressEvent(event)
  71. QTextEdit.keyPressEvent(self, event)
  72. class TermWidget(QWidget):
  73. """
  74. Widget wich represents terminal. It only displays text and allows to enter text.
  75. All highlevel logic should be implemented by client classes
  76. User pressed Enter. Client class should decide, if command must be executed or user may continue edit it
  77. """
  78. def __init__(self, *args):
  79. QWidget.__init__(self, *args)
  80. self._browser = QTextEdit(self)
  81. self._browser.setStyleSheet("font: 9pt \"Courier\";")
  82. self._browser.setReadOnly(True)
  83. self._browser.document().setDefaultStyleSheet(self._browser.document().defaultStyleSheet() +
  84. "span {white-space:pre;}")
  85. self._edit = _ExpandableTextEdit(self, self)
  86. self._edit.historyNext.connect(self._on_history_next)
  87. self._edit.historyPrev.connect(self._on_history_prev)
  88. self.setFocusProxy(self._edit)
  89. layout = QVBoxLayout(self)
  90. layout.setSpacing(0)
  91. layout.setContentsMargins(0, 0, 0, 0)
  92. layout.addWidget(self._browser)
  93. layout.addWidget(self._edit)
  94. self._history = [''] # current empty line
  95. self._historyIndex = 0
  96. self._edit.setFocus()
  97. def open_proccessing(self, detail=None):
  98. """
  99. Open processing and disable using shell commands again until all commands are finished
  100. :return:
  101. """
  102. self._edit.setTextColor(QtCore.Qt.white)
  103. self._edit.setTextBackgroundColor(QtCore.Qt.darkGreen)
  104. if detail is None:
  105. self._edit.setPlainText("...proccessing...")
  106. else:
  107. self._edit.setPlainText("...proccessing... [%s]" % detail)
  108. self._edit.setDisabled(True)
  109. def close_proccessing(self):
  110. """
  111. Close processing and enable using shell commands again
  112. :return:
  113. """
  114. self._edit.setTextColor(QtCore.Qt.black)
  115. self._edit.setTextBackgroundColor(QtCore.Qt.white)
  116. self._edit.setPlainText('')
  117. self._edit.setDisabled(False)
  118. def _append_to_browser(self, style, text):
  119. """
  120. Convert text to HTML for inserting it to browser
  121. """
  122. assert style in ('in', 'out', 'err')
  123. text = cgi.escape(text)
  124. text = text.replace('\n', '<br/>')
  125. if style != 'out':
  126. def_bg = self._browser.palette().color(QPalette.Base)
  127. h, s, v, a = def_bg.getHsvF()
  128. if style == 'in':
  129. if v > 0.5: # white background
  130. v = v - (v / 8) # make darker
  131. else:
  132. v = v + ((1 - v) / 4) # make ligher
  133. else: # err
  134. if v < 0.5:
  135. v = v + ((1 - v) / 4) # make ligher
  136. if h == -1: # make red
  137. h = 0
  138. s = .4
  139. else:
  140. h = h + ((1 - h) * 0.5) # make more red
  141. bg = QColor.fromHsvF(h, s, v).name()
  142. text = '<span style="background-color: %s; font-weight: bold;">%s</span>' % (str(bg), text)
  143. else:
  144. text = '<span>%s</span>' % text # without span <br/> is ignored!!!
  145. scrollbar = self._browser.verticalScrollBar()
  146. old_value = scrollbar.value()
  147. scrollattheend = old_value == scrollbar.maximum()
  148. self._browser.moveCursor(QTextCursor.End)
  149. self._browser.insertHtml(text)
  150. """TODO When user enters second line to the input, and input is resized, scrollbar changes its positon
  151. and stops moving. As quick fix of this problem, now we always scroll down when add new text.
  152. To fix it correctly, srcoll to the bottom, if before intput has been resized,
  153. scrollbar was in the bottom, and remove next lien
  154. """
  155. scrollattheend = True
  156. if scrollattheend:
  157. scrollbar.setValue(scrollbar.maximum())
  158. else:
  159. scrollbar.setValue(old_value)
  160. def exec_current_command(self):
  161. """
  162. Save current command in the history. Append it to the log. Clear edit line
  163. Reimplement in the child classes to actually execute command
  164. """
  165. text = str(self._edit.toPlainText())
  166. self._append_to_browser('in', '> ' + text + '\n')
  167. if len(self._history) < 2 or\
  168. self._history[-2] != text: # don't insert duplicating items
  169. if text[-1] == '\n':
  170. self._history.insert(-1, text[:-1])
  171. else:
  172. self._history.insert(-1, text)
  173. self._historyIndex = len(self._history) - 1
  174. self._history[-1] = ''
  175. self._edit.clear()
  176. if not text[-1] == '\n':
  177. text += '\n'
  178. self.child_exec_command(text)
  179. def child_exec_command(self, text):
  180. """
  181. Reimplement in the child classes
  182. """
  183. pass
  184. def add_line_break_to_input(self):
  185. self._edit.textCursor().insertText('\n')
  186. def append_output(self, text):
  187. """Appent text to output widget
  188. """
  189. self._append_to_browser('out', text)
  190. def append_error(self, text):
  191. """Appent error text to output widget. Text is drawn with red background
  192. """
  193. self._append_to_browser('err', text)
  194. def is_command_complete(self, text):
  195. """
  196. Executed by _ExpandableTextEdit. Reimplement this function in the child classes.
  197. """
  198. return True
  199. def browser(self):
  200. return self._browser
  201. def _on_history_next(self):
  202. """
  203. Down pressed, show next item from the history
  204. """
  205. if (self._historyIndex + 1) < len(self._history):
  206. self._historyIndex += 1
  207. self._edit.setPlainText(self._history[self._historyIndex])
  208. self._edit.moveCursor(QTextCursor.End)
  209. def _on_history_prev(self):
  210. """
  211. Up pressed, show previous item from the history
  212. """
  213. if self._historyIndex > 0:
  214. if self._historyIndex == (len(self._history) - 1):
  215. self._history[-1] = self._edit.toPlainText()
  216. self._historyIndex -= 1
  217. self._edit.setPlainText(self._history[self._historyIndex])
  218. self._edit.moveCursor(QTextCursor.End)