ToolShell.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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. from PyQt5.QtCore import Qt
  9. from PyQt5.QtGui import QTextCursor
  10. from PyQt5.QtWidgets import QVBoxLayout, QWidget
  11. from flatcamGUI.GUIElements import _BrowserTextEdit, _ExpandableTextEdit
  12. import html
  13. import sys
  14. import tkinter as tk
  15. import gettext
  16. import FlatCAMTranslation as fcTranslate
  17. import builtins
  18. fcTranslate.apply_language('strings')
  19. if '_' not in builtins.__dict__:
  20. _ = gettext.gettext
  21. class TermWidget(QWidget):
  22. """
  23. Widget which represents terminal. It only displays text and allows to enter text.
  24. All high level logic should be implemented by client classes
  25. User pressed Enter. Client class should decide, if command must be executed or user may continue edit it
  26. """
  27. def __init__(self, version, app, *args):
  28. QWidget.__init__(self, *args)
  29. self._browser = _BrowserTextEdit(version=version, app=app)
  30. self._browser.setStyleSheet("font: 9pt \"Courier\";")
  31. self._browser.setReadOnly(True)
  32. self._browser.document().setDefaultStyleSheet(
  33. self._browser.document().defaultStyleSheet() +
  34. "span {white-space:pre;}")
  35. self._edit = _ExpandableTextEdit(self, self)
  36. self._edit.historyNext.connect(self._on_history_next)
  37. self._edit.historyPrev.connect(self._on_history_prev)
  38. self._edit.setFocus()
  39. self.setFocusProxy(self._edit)
  40. layout = QVBoxLayout(self)
  41. layout.setSpacing(0)
  42. layout.setContentsMargins(0, 0, 0, 0)
  43. layout.addWidget(self._browser)
  44. layout.addWidget(self._edit)
  45. self._history = [''] # current empty line
  46. self._historyIndex = 0
  47. def open_processing(self, detail=None):
  48. """
  49. Open processing and disable using shell commands again until all commands are finished
  50. :param detail: text detail about what is currently called from TCL to python
  51. :return: None
  52. """
  53. self._edit.setTextColor(Qt.white)
  54. self._edit.setTextBackgroundColor(Qt.darkGreen)
  55. if detail is None:
  56. self._edit.setPlainText(_("...processing..."))
  57. else:
  58. self._edit.setPlainText('%s [%s]' % (_("...processing..."), detail))
  59. self._edit.setDisabled(True)
  60. self._edit.setFocus()
  61. def close_processing(self):
  62. """
  63. Close processing and enable using shell commands again
  64. :return:
  65. """
  66. self._edit.setTextColor(Qt.black)
  67. self._edit.setTextBackgroundColor(Qt.white)
  68. self._edit.setPlainText('')
  69. self._edit.setDisabled(False)
  70. self._edit.setFocus()
  71. def _append_to_browser(self, style, text):
  72. """
  73. Convert text to HTML for inserting it to browser
  74. """
  75. assert style in ('in', 'out', 'err', 'warning', 'success', 'selected', 'raw')
  76. if style != 'raw':
  77. text = html.escape(text)
  78. text = text.replace('\n', '<br/>')
  79. else:
  80. text = text.replace('\n', '<br>')
  81. text = text.replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;')
  82. if style == 'in':
  83. text = '<span style="font-weight: bold;">%s</span>' % text
  84. elif style == 'err':
  85. text = '<span style="font-weight: bold; color: red;">%s</span>' % text
  86. elif style == 'warning':
  87. text = '<span style="font-weight: bold; color: #f4b642;">%s</span>' % text
  88. elif style == 'success':
  89. text = '<span style="font-weight: bold; color: #15b300;">%s</span>' % text
  90. elif style == 'selected':
  91. text = ''
  92. elif style == 'raw':
  93. text = text
  94. else:
  95. text = '<span>%s</span>' % text # without span <br/> is ignored!!!
  96. scrollbar = self._browser.verticalScrollBar()
  97. old_value = scrollbar.value()
  98. # scrollattheend = old_value == scrollbar.maximum()
  99. self._browser.moveCursor(QTextCursor.End)
  100. self._browser.insertHtml(text)
  101. """TODO When user enters second line to the input, and input is resized, scrollbar changes its position
  102. and stops moving. As quick fix of this problem, now we always scroll down when add new text.
  103. To fix it correctly, scroll to the bottom, if before input has been resized,
  104. scrollbar was in the bottom, and remove next line
  105. """
  106. scrollattheend = True
  107. if scrollattheend:
  108. scrollbar.setValue(scrollbar.maximum())
  109. else:
  110. scrollbar.setValue(old_value)
  111. def exec_current_command(self):
  112. """
  113. Save current command in the history. Append it to the log. Clear edit line
  114. Re-implement in the child classes to actually execute command
  115. """
  116. text = str(self._edit.toPlainText())
  117. # in Windows replace all backslash symbols '\' with '\\' slash because Windows paths are made with backslash
  118. # and in Python single slash is the escape symbol
  119. if sys.platform == 'win32':
  120. text = text.replace('\\', '\\\\')
  121. self._append_to_browser('in', '> ' + text + '\n')
  122. if len(self._history) < 2 or self._history[-2] != text: # don't insert duplicating items
  123. try:
  124. if text[-1] == '\n':
  125. self._history.insert(-1, text[:-1])
  126. else:
  127. self._history.insert(-1, text)
  128. except IndexError:
  129. return
  130. self._historyIndex = len(self._history) - 1
  131. self._history[-1] = ''
  132. self._edit.clear()
  133. if not text[-1] == '\n':
  134. text += '\n'
  135. self.child_exec_command(text)
  136. def child_exec_command(self, text):
  137. """
  138. Re-implement in the child classes
  139. """
  140. pass
  141. def add_line_break_to_input(self):
  142. self._edit.textCursor().insertText('\n')
  143. def append_output(self, text):
  144. """
  145. Append text to output widget
  146. """
  147. self._append_to_browser('out', text)
  148. def append_raw(self, text):
  149. """
  150. Append text to output widget as it is
  151. """
  152. self._append_to_browser('raw', text)
  153. def append_success(self, text):
  154. """Append text to output widget
  155. """
  156. self._append_to_browser('success', text)
  157. def append_selected(self, text):
  158. """Append text to output widget
  159. """
  160. self._append_to_browser('selected', text)
  161. def append_warning(self, text):
  162. """Append text to output widget
  163. """
  164. self._append_to_browser('warning', text)
  165. def append_error(self, text):
  166. """Append error text to output widget. Text is drawn with red background
  167. """
  168. self._append_to_browser('err', text)
  169. def is_command_complete(self, text):
  170. """
  171. Executed by _ExpandableTextEdit. Reimplement this function in the child classes.
  172. """
  173. return True
  174. def browser(self):
  175. return self._browser
  176. def _on_history_next(self):
  177. """
  178. Down pressed, show next item from the history
  179. """
  180. if (self._historyIndex + 1) < len(self._history):
  181. self._historyIndex += 1
  182. self._edit.setPlainText(self._history[self._historyIndex])
  183. self._edit.moveCursor(QTextCursor.End)
  184. def _on_history_prev(self):
  185. """
  186. Up pressed, show previous item from the history
  187. """
  188. if self._historyIndex > 0:
  189. if self._historyIndex == (len(self._history) - 1):
  190. self._history[-1] = self._edit.toPlainText()
  191. self._historyIndex -= 1
  192. self._edit.setPlainText(self._history[self._historyIndex])
  193. self._edit.moveCursor(QTextCursor.End)
  194. class FCShell(TermWidget):
  195. def __init__(self, sysShell, version, *args):
  196. """
  197. :param sysShell: When instantiated the sysShell will be actually the FlatCAMApp.App() class
  198. :param version: FlatCAM version string
  199. :param args: Parameters passed to the TermWidget parent class
  200. """
  201. TermWidget.__init__(self, version, *args, app=sysShell)
  202. self._sysShell = sysShell
  203. def is_command_complete(self, text):
  204. def skipQuotes(txt):
  205. quote = txt[0]
  206. text_val = txt[1:]
  207. endIndex = str(text_val).index(quote)
  208. return text[endIndex:]
  209. while text:
  210. if text[0] in ('"', "'"):
  211. try:
  212. text = skipQuotes(text)
  213. except ValueError:
  214. return False
  215. text = text[1:]
  216. return True
  217. def child_exec_command(self, text):
  218. self.exec_command(text)
  219. def exec_command(self, text, no_echo=False):
  220. """
  221. Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
  222. Also handles execution in separated threads
  223. :param text: FlatCAM TclCommand with parameters
  224. :param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
  225. will create crashes of the _Expandable_Edit widget
  226. :return: output if there was any
  227. """
  228. self._sysShell.report_usage('exec_command')
  229. return self.exec_command_test(text, False, no_echo=no_echo)
  230. def exec_command_test(self, text, reraise=True, no_echo=False):
  231. """
  232. Same as exec_command(...) with additional control over exceptions.
  233. Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
  234. :param text: Input command
  235. :param reraise: Re-raise TclError exceptions in Python (mostly for unittests).
  236. :param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
  237. will create crashes of the _Expandable_Edit widget
  238. :return: Output from the command
  239. """
  240. tcl_command_string = str(text)
  241. try:
  242. if no_echo is False:
  243. self.open_processing() # Disables input box.
  244. result = self._sysShell.tcl.eval(str(tcl_command_string))
  245. if result != 'None' and no_echo is False:
  246. self.append_output(result + '\n')
  247. except tk.TclError as e:
  248. # This will display more precise answer if something in TCL shell fails
  249. result = self._sysShell.tcl.eval("set errorInfo")
  250. self._sysShell.log.error("Exec command Exception: %s" % (result + '\n'))
  251. if no_echo is False:
  252. self.append_error('ERROR: ' + result + '\n')
  253. # Show error in console and just return or in test raise exception
  254. if reraise:
  255. raise e
  256. finally:
  257. if no_echo is False:
  258. self.close_processing()
  259. pass
  260. return result
  261. # """
  262. # Code below is unsused. Saved for later.
  263. # """
  264. # parts = re.findall(r'([\w\\:\.]+|".*?")+', text)
  265. # parts = [p.replace('\n', '').replace('"', '') for p in parts]
  266. # self.log.debug(parts)
  267. # try:
  268. # if parts[0] not in commands:
  269. # self.shell.append_error("Unknown command\n")
  270. # return
  271. #
  272. # #import inspect
  273. # #inspect.getargspec(someMethod)
  274. # if (type(commands[parts[0]]["params"]) is not list and len(parts)-1 != commands[parts[0]]["params"]) or \
  275. # (type(commands[parts[0]]["params"]) is list and len(parts)-1 not in commands[parts[0]]["params"]):
  276. # self.shell.append_error(
  277. # "Command %s takes %d arguments. %d given.\n" %
  278. # (parts[0], commands[parts[0]]["params"], len(parts)-1)
  279. # )
  280. # return
  281. #
  282. # cmdfcn = commands[parts[0]]["fcn"]
  283. # cmdconv = commands[parts[0]]["converters"]
  284. # if len(parts) - 1 > 0:
  285. # retval = cmdfcn(*[cmdconv[i](parts[i + 1]) for i in range(len(parts)-1)])
  286. # else:
  287. # retval = cmdfcn()
  288. # retfcn = commands[parts[0]]["retfcn"]
  289. # if retval and retfcn(retval):
  290. # self.shell.append_output(retfcn(retval) + "\n")
  291. #
  292. # except Exception as e:
  293. # #self.shell.append_error(''.join(traceback.format_exc()))
  294. # #self.shell.append_error("?\n")
  295. # self.shell.append_error(str(e) + "\n")