ToolShell.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 2/5/2014 #
  6. # MIT Licence #
  7. ############################################################
  8. import html
  9. from PyQt5.QtCore import pyqtSignal
  10. from PyQt5.QtCore import Qt, QStringListModel
  11. from PyQt5.QtGui import QColor, QKeySequence, QPalette, QTextCursor
  12. from PyQt5.QtWidgets import QLineEdit, QSizePolicy, QTextEdit, QVBoxLayout, QWidget, QCompleter, QAction
  13. class _BrowserTextEdit(QTextEdit):
  14. def __init__(self, version):
  15. QTextEdit.__init__(self)
  16. self.menu = None
  17. self.version = version
  18. def contextMenuEvent(self, event):
  19. self.menu = self.createStandardContextMenu(event.pos())
  20. clear_action = QAction("Clear", self)
  21. clear_action.setShortcut(QKeySequence(Qt.Key_Delete)) # it's not working, the shortcut
  22. self.menu.addAction(clear_action)
  23. clear_action.triggered.connect(self.clear)
  24. self.menu.exec_(event.globalPos())
  25. def clear(self):
  26. QTextEdit.clear(self)
  27. text = "FlatCAM %s (c)2014-2019 Juan Pablo Caram (Type help to get started)\n\n" % self.version
  28. text = html.escape(text)
  29. text = text.replace('\n', '<br/>')
  30. self.moveCursor(QTextCursor.End)
  31. self.insertHtml(text)
  32. class _ExpandableTextEdit(QTextEdit):
  33. """
  34. Class implements edit line, which expands themselves automatically
  35. """
  36. historyNext = pyqtSignal()
  37. historyPrev = pyqtSignal()
  38. def __init__(self, termwidget, *args):
  39. QTextEdit.__init__(self, *args)
  40. self.setStyleSheet("font: 9pt \"Courier\";")
  41. self._fittedHeight = 1
  42. self.textChanged.connect(self._fit_to_document)
  43. self._fit_to_document()
  44. self._termWidget = termwidget
  45. self.completer = MyCompleter()
  46. self.model = QStringListModel()
  47. self.completer.setModel(self.model)
  48. self.set_model_data(keyword_list=[])
  49. self.completer.insertText.connect(self.insertCompletion)
  50. def set_model_data(self, keyword_list):
  51. self.model.setStringList(keyword_list)
  52. def insertCompletion(self, completion):
  53. tc = self.textCursor()
  54. extra = (len(completion) - len(self.completer.completionPrefix()))
  55. tc.movePosition(QTextCursor.Left)
  56. tc.movePosition(QTextCursor.EndOfWord)
  57. tc.insertText(completion[-extra:])
  58. self.setTextCursor(tc)
  59. self.completer.popup().hide()
  60. def focusInEvent(self, event):
  61. if self.completer:
  62. self.completer.setWidget(self)
  63. QTextEdit.focusInEvent(self, event)
  64. def keyPressEvent(self, event):
  65. """
  66. Catch keyboard events. Process Enter, Up, Down
  67. """
  68. if event.matches(QKeySequence.InsertParagraphSeparator):
  69. text = self.toPlainText()
  70. if self._termWidget.is_command_complete(text):
  71. self._termWidget.exec_current_command()
  72. return
  73. elif event.matches(QKeySequence.MoveToNextLine):
  74. text = self.toPlainText()
  75. cursor_pos = self.textCursor().position()
  76. textBeforeEnd = text[cursor_pos:]
  77. if len(textBeforeEnd.split('\n')) <= 1:
  78. self.historyNext.emit()
  79. return
  80. elif event.matches(QKeySequence.MoveToPreviousLine):
  81. text = self.toPlainText()
  82. cursor_pos = self.textCursor().position()
  83. text_before_start = text[:cursor_pos]
  84. # lineCount = len(textBeforeStart.splitlines())
  85. line_count = len(text_before_start.split('\n'))
  86. if len(text_before_start) > 0 and \
  87. (text_before_start[-1] == '\n' or text_before_start[-1] == '\r'):
  88. line_count += 1
  89. if line_count <= 1:
  90. self.historyPrev.emit()
  91. return
  92. elif event.matches(QKeySequence.MoveToNextPage) or \
  93. event.matches(QKeySequence.MoveToPreviousPage):
  94. return self._termWidget.browser().keyPressEvent(event)
  95. tc = self.textCursor()
  96. if event.key() == Qt.Key_Tab and self.completer.popup().isVisible():
  97. self.completer.insertText.emit(self.completer.getSelected())
  98. self.completer.setCompletionMode(QCompleter.PopupCompletion)
  99. return
  100. QTextEdit.keyPressEvent(self, event)
  101. tc.select(QTextCursor.WordUnderCursor)
  102. cr = self.cursorRect()
  103. if len(tc.selectedText()) > 0:
  104. self.completer.setCompletionPrefix(tc.selectedText())
  105. popup = self.completer.popup()
  106. popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
  107. cr.setWidth(self.completer.popup().sizeHintForColumn(0)
  108. + self.completer.popup().verticalScrollBar().sizeHint().width())
  109. self.completer.complete(cr)
  110. else:
  111. self.completer.popup().hide()
  112. def sizeHint(self):
  113. """
  114. QWidget sizeHint impelemtation
  115. """
  116. hint = QTextEdit.sizeHint(self)
  117. hint.setHeight(self._fittedHeight)
  118. return hint
  119. def _fit_to_document(self):
  120. """
  121. Update widget height to fit all text
  122. """
  123. documentsize = self.document().size().toSize()
  124. self._fittedHeight = documentsize.height() + (self.height() - self.viewport().height())
  125. self.setMaximumHeight(self._fittedHeight)
  126. self.updateGeometry()
  127. def insertFromMimeData(self, mime_data):
  128. # Paste only plain text.
  129. self.insertPlainText(mime_data.text())
  130. class MyCompleter(QCompleter):
  131. insertText = pyqtSignal(str)
  132. def __init__(self, parent=None):
  133. QCompleter.__init__(self)
  134. self.setCompletionMode(QCompleter.PopupCompletion)
  135. self.highlighted.connect(self.setHighlighted)
  136. def setHighlighted(self, text):
  137. self.lastSelected = text
  138. def getSelected(self):
  139. return self.lastSelected
  140. class TermWidget(QWidget):
  141. """
  142. Widget wich represents terminal. It only displays text and allows to enter text.
  143. All highlevel logic should be implemented by client classes
  144. User pressed Enter. Client class should decide, if command must be executed or user may continue edit it
  145. """
  146. def __init__(self, version, *args):
  147. QWidget.__init__(self, *args)
  148. self._browser = _BrowserTextEdit(version=version)
  149. self._browser.setStyleSheet("font: 9pt \"Courier\";")
  150. self._browser.setReadOnly(True)
  151. self._browser.document().setDefaultStyleSheet(
  152. self._browser.document().defaultStyleSheet() +
  153. "span {white-space:pre;}")
  154. self._edit = _ExpandableTextEdit(self, self)
  155. self._edit.historyNext.connect(self._on_history_next)
  156. self._edit.historyPrev.connect(self._on_history_prev)
  157. self._edit.setFocus()
  158. self.setFocusProxy(self._edit)
  159. layout = QVBoxLayout(self)
  160. layout.setSpacing(0)
  161. layout.setContentsMargins(0, 0, 0, 0)
  162. layout.addWidget(self._browser)
  163. layout.addWidget(self._edit)
  164. self._history = [''] # current empty line
  165. self._historyIndex = 0
  166. def open_proccessing(self, detail=None):
  167. """
  168. Open processing and disable using shell commands again until all commands are finished
  169. :param detail: text detail about what is currently called from TCL to python
  170. :return: None
  171. """
  172. self._edit.setTextColor(Qt.white)
  173. self._edit.setTextBackgroundColor(Qt.darkGreen)
  174. if detail is None:
  175. self._edit.setPlainText("...proccessing...")
  176. else:
  177. self._edit.setPlainText("...proccessing... [%s]" % detail)
  178. self._edit.setDisabled(True)
  179. self._edit.setFocus()
  180. def close_proccessing(self):
  181. """
  182. Close processing and enable using shell commands again
  183. :return:
  184. """
  185. self._edit.setTextColor(Qt.black)
  186. self._edit.setTextBackgroundColor(Qt.white)
  187. self._edit.setPlainText('')
  188. self._edit.setDisabled(False)
  189. self._edit.setFocus()
  190. def _append_to_browser(self, style, text):
  191. """
  192. Convert text to HTML for inserting it to browser
  193. """
  194. assert style in ('in', 'out', 'err', 'warning', 'success')
  195. text = html.escape(text)
  196. text = text.replace('\n', '<br/>')
  197. if style == 'in':
  198. text = '<span style="font-weight: bold;">%s</span>' % text
  199. elif style == 'err':
  200. text = '<span style="font-weight: bold; color: red;">%s</span>' % text
  201. elif style == 'warning':
  202. text = '<span style="font-weight: bold; color: rgb(244, 182, 66);">%s</span>' % text
  203. elif style == 'success':
  204. text = '<span style="font-weight: bold; color: rgb(8, 68, 0);">%s</span>' % text
  205. else:
  206. text = '<span>%s</span>' % text # without span <br/> is ignored!!!
  207. scrollbar = self._browser.verticalScrollBar()
  208. old_value = scrollbar.value()
  209. scrollattheend = old_value == scrollbar.maximum()
  210. self._browser.moveCursor(QTextCursor.End)
  211. self._browser.insertHtml(text)
  212. """TODO When user enters second line to the input, and input is resized, scrollbar changes its positon
  213. and stops moving. As quick fix of this problem, now we always scroll down when add new text.
  214. To fix it correctly, srcoll to the bottom, if before intput has been resized,
  215. scrollbar was in the bottom, and remove next lien
  216. """
  217. scrollattheend = True
  218. if scrollattheend:
  219. scrollbar.setValue(scrollbar.maximum())
  220. else:
  221. scrollbar.setValue(old_value)
  222. def exec_current_command(self):
  223. """
  224. Save current command in the history. Append it to the log. Clear edit line
  225. Reimplement in the child classes to actually execute command
  226. """
  227. text = str(self._edit.toPlainText())
  228. self._append_to_browser('in', '> ' + text + '\n')
  229. if len(self._history) < 2 or\
  230. self._history[-2] != text: # don't insert duplicating items
  231. if text[-1] == '\n':
  232. self._history.insert(-1, text[:-1])
  233. else:
  234. self._history.insert(-1, text)
  235. self._historyIndex = len(self._history) - 1
  236. self._history[-1] = ''
  237. self._edit.clear()
  238. if not text[-1] == '\n':
  239. text += '\n'
  240. self.child_exec_command(text)
  241. def child_exec_command(self, text):
  242. """
  243. Reimplement in the child classes
  244. """
  245. pass
  246. def add_line_break_to_input(self):
  247. self._edit.textCursor().insertText('\n')
  248. def append_output(self, text):
  249. """Appent text to output widget
  250. """
  251. self._append_to_browser('out', text)
  252. def append_success(self, text):
  253. """Appent text to output widget
  254. """
  255. self._append_to_browser('success', text)
  256. def append_warning(self, text):
  257. """Appent text to output widget
  258. """
  259. self._append_to_browser('warning', text)
  260. def append_error(self, text):
  261. """Appent error text to output widget. Text is drawn with red background
  262. """
  263. self._append_to_browser('err', text)
  264. def is_command_complete(self, text):
  265. """
  266. Executed by _ExpandableTextEdit. Reimplement this function in the child classes.
  267. """
  268. return True
  269. def browser(self):
  270. return self._browser
  271. def _on_history_next(self):
  272. """
  273. Down pressed, show next item from the history
  274. """
  275. if (self._historyIndex + 1) < len(self._history):
  276. self._historyIndex += 1
  277. self._edit.setPlainText(self._history[self._historyIndex])
  278. self._edit.moveCursor(QTextCursor.End)
  279. def _on_history_prev(self):
  280. """
  281. Up pressed, show previous item from the history
  282. """
  283. if self._historyIndex > 0:
  284. if self._historyIndex == (len(self._history) - 1):
  285. self._history[-1] = self._edit.toPlainText()
  286. self._historyIndex -= 1
  287. self._edit.setPlainText(self._history[self._historyIndex])
  288. self._edit.moveCursor(QTextCursor.End)
  289. class FCShell(TermWidget):
  290. def __init__(self, sysShell, version, *args):
  291. TermWidget.__init__(self, version, *args)
  292. self._sysShell = sysShell
  293. def is_command_complete(self, text):
  294. def skipQuotes(text):
  295. quote = text[0]
  296. text = text[1:]
  297. endIndex = str(text).index(quote)
  298. return text[endIndex:]
  299. while text:
  300. if text[0] in ('"', "'"):
  301. try:
  302. text = skipQuotes(text)
  303. except ValueError:
  304. return False
  305. text = text[1:]
  306. return True
  307. def child_exec_command(self, text):
  308. self._sysShell.exec_command(text)