2019-10-13 15:13:39 +00:00
|
|
|
# ##########################################################
|
2019-01-03 19:25:08 +00:00
|
|
|
# FlatCAM: 2D Post-processing for Manufacturing #
|
|
|
|
# http://flatcam.org #
|
|
|
|
# Author: Juan Pablo Caram (c) #
|
|
|
|
# Date: 2/5/2014 #
|
|
|
|
# MIT Licence #
|
2019-10-13 15:13:39 +00:00
|
|
|
# ##########################################################
|
2019-01-03 19:25:08 +00:00
|
|
|
|
2019-10-15 23:28:18 +00:00
|
|
|
|
2019-02-07 20:37:51 +00:00
|
|
|
from PyQt5.QtCore import Qt
|
|
|
|
from PyQt5.QtGui import QTextCursor
|
|
|
|
from PyQt5.QtWidgets import QVBoxLayout, QWidget
|
2019-03-11 10:23:26 +00:00
|
|
|
from flatcamGUI.GUIElements import _BrowserTextEdit, _ExpandableTextEdit
|
2019-01-03 19:25:08 +00:00
|
|
|
import html
|
2019-09-16 20:27:02 +00:00
|
|
|
import sys
|
2019-03-07 16:04:11 +00:00
|
|
|
|
2020-04-22 20:00:54 +00:00
|
|
|
import tkinter as tk
|
|
|
|
|
2019-03-07 23:32:18 +00:00
|
|
|
import gettext
|
|
|
|
import FlatCAMTranslation as fcTranslate
|
2019-07-16 13:22:20 +00:00
|
|
|
import builtins
|
2019-03-10 15:12:58 +00:00
|
|
|
|
2019-03-13 23:09:06 +00:00
|
|
|
fcTranslate.apply_language('strings')
|
2019-03-10 15:12:58 +00:00
|
|
|
if '_' not in builtins.__dict__:
|
|
|
|
_ = gettext.gettext
|
2019-01-03 19:25:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TermWidget(QWidget):
|
|
|
|
"""
|
2019-09-13 10:52:55 +00:00
|
|
|
Widget which represents terminal. It only displays text and allows to enter text.
|
|
|
|
All high level logic should be implemented by client classes
|
2019-01-03 19:25:08 +00:00
|
|
|
|
|
|
|
User pressed Enter. Client class should decide, if command must be executed or user may continue edit it
|
|
|
|
"""
|
|
|
|
|
2020-04-22 23:07:55 +00:00
|
|
|
def __init__(self, version, app, *args):
|
2019-01-03 19:25:08 +00:00
|
|
|
QWidget.__init__(self, *args)
|
|
|
|
|
2020-04-22 23:07:55 +00:00
|
|
|
self._browser = _BrowserTextEdit(version=version, app=app)
|
2019-01-03 19:25:08 +00:00
|
|
|
self._browser.setStyleSheet("font: 9pt \"Courier\";")
|
|
|
|
self._browser.setReadOnly(True)
|
|
|
|
self._browser.document().setDefaultStyleSheet(
|
|
|
|
self._browser.document().defaultStyleSheet() +
|
|
|
|
"span {white-space:pre;}")
|
|
|
|
|
|
|
|
self._edit = _ExpandableTextEdit(self, self)
|
|
|
|
self._edit.historyNext.connect(self._on_history_next)
|
|
|
|
self._edit.historyPrev.connect(self._on_history_prev)
|
|
|
|
self._edit.setFocus()
|
|
|
|
self.setFocusProxy(self._edit)
|
|
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
layout.setSpacing(0)
|
|
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
layout.addWidget(self._browser)
|
|
|
|
layout.addWidget(self._edit)
|
|
|
|
|
|
|
|
self._history = [''] # current empty line
|
|
|
|
self._historyIndex = 0
|
|
|
|
|
2020-01-01 14:13:33 +00:00
|
|
|
def open_processing(self, detail=None):
|
2019-01-03 19:25:08 +00:00
|
|
|
"""
|
2019-09-13 10:52:55 +00:00
|
|
|
Open processing and disable using shell commands again until all commands are finished
|
2019-01-03 19:25:08 +00:00
|
|
|
|
|
|
|
:param detail: text detail about what is currently called from TCL to python
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
|
|
|
|
self._edit.setTextColor(Qt.white)
|
|
|
|
self._edit.setTextBackgroundColor(Qt.darkGreen)
|
|
|
|
if detail is None:
|
2020-01-01 14:13:33 +00:00
|
|
|
self._edit.setPlainText(_("...processing..."))
|
2019-01-03 19:25:08 +00:00
|
|
|
else:
|
2020-01-01 14:13:33 +00:00
|
|
|
self._edit.setPlainText('%s [%s]' % (_("...processing..."), detail))
|
2019-01-03 19:25:08 +00:00
|
|
|
|
|
|
|
self._edit.setDisabled(True)
|
|
|
|
self._edit.setFocus()
|
|
|
|
|
2020-01-01 14:13:33 +00:00
|
|
|
def close_processing(self):
|
2019-01-03 19:25:08 +00:00
|
|
|
"""
|
|
|
|
Close processing and enable using shell commands again
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
|
|
|
|
self._edit.setTextColor(Qt.black)
|
|
|
|
self._edit.setTextBackgroundColor(Qt.white)
|
|
|
|
self._edit.setPlainText('')
|
|
|
|
self._edit.setDisabled(False)
|
|
|
|
self._edit.setFocus()
|
|
|
|
|
|
|
|
def _append_to_browser(self, style, text):
|
|
|
|
"""
|
|
|
|
Convert text to HTML for inserting it to browser
|
|
|
|
"""
|
2020-04-22 23:07:55 +00:00
|
|
|
assert style in ('in', 'out', 'err', 'warning', 'success', 'selected', 'raw')
|
2019-01-03 19:25:08 +00:00
|
|
|
|
2020-04-22 23:07:55 +00:00
|
|
|
if style != 'raw':
|
|
|
|
text = html.escape(text)
|
|
|
|
text = text.replace('\n', '<br/>')
|
|
|
|
else:
|
|
|
|
text = text.replace('\n', '<br>')
|
|
|
|
text = text.replace('\t', ' ')
|
2019-01-03 19:25:08 +00:00
|
|
|
|
2020-04-24 03:59:49 +00:00
|
|
|
idx = text.find(']')
|
|
|
|
mtype = text[:idx+1].upper()
|
|
|
|
mtype = mtype.replace('_NOTCL', '')
|
|
|
|
body = text[idx+1:]
|
2019-01-03 19:25:08 +00:00
|
|
|
if style == 'in':
|
|
|
|
text = '<span style="font-weight: bold;">%s</span>' % text
|
|
|
|
elif style == 'err':
|
2020-04-24 03:59:49 +00:00
|
|
|
text = '<span style="font-weight: bold; color: red;">%s</span>'\
|
|
|
|
'<span style="font-weight: bold;">%s</span>'\
|
|
|
|
%(mtype, body)
|
2019-02-03 13:13:09 +00:00
|
|
|
elif style == 'warning':
|
2020-04-24 03:59:49 +00:00
|
|
|
# text = '<span style="font-weight: bold; color: #f4b642;">%s</span>' % text
|
|
|
|
text = '<span style="font-weight: bold; color: #f4b642;">%s</span>' \
|
|
|
|
'<span style="font-weight: bold;">%s</span>' \
|
|
|
|
% (mtype, body)
|
2019-02-03 13:13:09 +00:00
|
|
|
elif style == 'success':
|
2020-04-24 03:59:49 +00:00
|
|
|
# text = '<span style="font-weight: bold; color: #15b300;">%s</span>' % text
|
|
|
|
text = '<span style="font-weight: bold; color: #15b300;">%s</span>' \
|
|
|
|
'<span style="font-weight: bold;">%s</span>' \
|
|
|
|
% (mtype, body)
|
2019-02-07 20:37:51 +00:00
|
|
|
elif style == 'selected':
|
2019-02-09 17:44:01 +00:00
|
|
|
text = ''
|
2020-04-22 23:07:55 +00:00
|
|
|
elif style == 'raw':
|
|
|
|
text = text
|
2019-01-03 19:25:08 +00:00
|
|
|
else:
|
2020-04-24 03:59:49 +00:00
|
|
|
# without span <br/> is ignored!!!
|
|
|
|
text = '<span>%s</span>' % text
|
2019-01-03 19:25:08 +00:00
|
|
|
|
|
|
|
scrollbar = self._browser.verticalScrollBar()
|
|
|
|
old_value = scrollbar.value()
|
2020-04-24 01:51:55 +00:00
|
|
|
# scrollattheend = old_value == scrollbar.maximum()
|
2019-01-03 19:25:08 +00:00
|
|
|
|
|
|
|
self._browser.moveCursor(QTextCursor.End)
|
|
|
|
self._browser.insertHtml(text)
|
|
|
|
|
2019-09-13 10:52:55 +00:00
|
|
|
"""TODO When user enters second line to the input, and input is resized, scrollbar changes its position
|
2019-01-03 19:25:08 +00:00
|
|
|
and stops moving. As quick fix of this problem, now we always scroll down when add new text.
|
2019-09-13 10:52:55 +00:00
|
|
|
To fix it correctly, scroll to the bottom, if before input has been resized,
|
|
|
|
scrollbar was in the bottom, and remove next line
|
2019-01-03 19:25:08 +00:00
|
|
|
"""
|
|
|
|
scrollattheend = True
|
|
|
|
|
|
|
|
if scrollattheend:
|
|
|
|
scrollbar.setValue(scrollbar.maximum())
|
|
|
|
else:
|
|
|
|
scrollbar.setValue(old_value)
|
|
|
|
|
|
|
|
def exec_current_command(self):
|
|
|
|
"""
|
|
|
|
Save current command in the history. Append it to the log. Clear edit line
|
2019-09-13 10:52:55 +00:00
|
|
|
Re-implement in the child classes to actually execute command
|
2019-01-03 19:25:08 +00:00
|
|
|
"""
|
|
|
|
text = str(self._edit.toPlainText())
|
2019-09-16 20:27:02 +00:00
|
|
|
|
|
|
|
# in Windows replace all backslash symbols '\' with '\\' slash because Windows paths are made with backslash
|
|
|
|
# and in Python single slash is the escape symbol
|
|
|
|
if sys.platform == 'win32':
|
|
|
|
text = text.replace('\\', '\\\\')
|
|
|
|
|
2019-01-03 19:25:08 +00:00
|
|
|
self._append_to_browser('in', '> ' + text + '\n')
|
|
|
|
|
2019-09-13 10:52:55 +00:00
|
|
|
if len(self._history) < 2 or self._history[-2] != text: # don't insert duplicating items
|
2019-09-17 15:37:34 +00:00
|
|
|
try:
|
|
|
|
if text[-1] == '\n':
|
|
|
|
self._history.insert(-1, text[:-1])
|
|
|
|
else:
|
|
|
|
self._history.insert(-1, text)
|
|
|
|
except IndexError:
|
|
|
|
return
|
2019-01-03 19:25:08 +00:00
|
|
|
|
|
|
|
self._historyIndex = len(self._history) - 1
|
|
|
|
|
|
|
|
self._history[-1] = ''
|
|
|
|
self._edit.clear()
|
|
|
|
|
|
|
|
if not text[-1] == '\n':
|
|
|
|
text += '\n'
|
|
|
|
|
|
|
|
self.child_exec_command(text)
|
|
|
|
|
|
|
|
def child_exec_command(self, text):
|
|
|
|
"""
|
2019-09-13 10:52:55 +00:00
|
|
|
Re-implement in the child classes
|
2019-01-03 19:25:08 +00:00
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
def add_line_break_to_input(self):
|
|
|
|
self._edit.textCursor().insertText('\n')
|
|
|
|
|
|
|
|
def append_output(self, text):
|
2019-09-13 10:52:55 +00:00
|
|
|
"""
|
|
|
|
Append text to output widget
|
2019-01-03 19:25:08 +00:00
|
|
|
"""
|
|
|
|
self._append_to_browser('out', text)
|
|
|
|
|
2020-04-22 23:07:55 +00:00
|
|
|
def append_raw(self, text):
|
|
|
|
"""
|
|
|
|
Append text to output widget as it is
|
|
|
|
"""
|
|
|
|
self._append_to_browser('raw', text)
|
|
|
|
|
2019-02-03 13:13:09 +00:00
|
|
|
def append_success(self, text):
|
2020-04-22 23:07:55 +00:00
|
|
|
"""Append text to output widget
|
2019-02-03 13:13:09 +00:00
|
|
|
"""
|
|
|
|
self._append_to_browser('success', text)
|
|
|
|
|
2019-02-07 20:37:51 +00:00
|
|
|
def append_selected(self, text):
|
2020-04-22 23:07:55 +00:00
|
|
|
"""Append text to output widget
|
2019-02-07 20:37:51 +00:00
|
|
|
"""
|
|
|
|
self._append_to_browser('selected', text)
|
|
|
|
|
2019-02-03 13:13:09 +00:00
|
|
|
def append_warning(self, text):
|
2020-04-22 23:07:55 +00:00
|
|
|
"""Append text to output widget
|
2019-02-03 13:13:09 +00:00
|
|
|
"""
|
|
|
|
self._append_to_browser('warning', text)
|
|
|
|
|
2019-01-03 19:25:08 +00:00
|
|
|
def append_error(self, text):
|
2020-04-22 23:07:55 +00:00
|
|
|
"""Append error text to output widget. Text is drawn with red background
|
2019-01-03 19:25:08 +00:00
|
|
|
"""
|
|
|
|
self._append_to_browser('err', text)
|
|
|
|
|
|
|
|
def is_command_complete(self, text):
|
|
|
|
"""
|
|
|
|
Executed by _ExpandableTextEdit. Reimplement this function in the child classes.
|
|
|
|
"""
|
|
|
|
return True
|
|
|
|
|
|
|
|
def browser(self):
|
|
|
|
return self._browser
|
|
|
|
|
|
|
|
def _on_history_next(self):
|
|
|
|
"""
|
|
|
|
Down pressed, show next item from the history
|
|
|
|
"""
|
|
|
|
if (self._historyIndex + 1) < len(self._history):
|
|
|
|
self._historyIndex += 1
|
|
|
|
self._edit.setPlainText(self._history[self._historyIndex])
|
|
|
|
self._edit.moveCursor(QTextCursor.End)
|
|
|
|
|
|
|
|
def _on_history_prev(self):
|
|
|
|
"""
|
|
|
|
Up pressed, show previous item from the history
|
|
|
|
"""
|
|
|
|
if self._historyIndex > 0:
|
|
|
|
if self._historyIndex == (len(self._history) - 1):
|
|
|
|
self._history[-1] = self._edit.toPlainText()
|
|
|
|
self._historyIndex -= 1
|
|
|
|
self._edit.setPlainText(self._history[self._historyIndex])
|
|
|
|
self._edit.moveCursor(QTextCursor.End)
|
|
|
|
|
2019-02-07 20:37:51 +00:00
|
|
|
|
2019-01-03 19:25:08 +00:00
|
|
|
class FCShell(TermWidget):
|
2019-01-06 18:40:05 +00:00
|
|
|
def __init__(self, sysShell, version, *args):
|
2020-04-22 20:00:54 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
:param sysShell: When instantiated the sysShell will be actually the FlatCAMApp.App() class
|
|
|
|
:param version: FlatCAM version string
|
|
|
|
:param args: Parameters passed to the TermWidget parent class
|
|
|
|
"""
|
2020-04-22 23:07:55 +00:00
|
|
|
TermWidget.__init__(self, version, *args, app=sysShell)
|
2019-01-03 19:25:08 +00:00
|
|
|
self._sysShell = sysShell
|
|
|
|
|
|
|
|
def is_command_complete(self, text):
|
2020-04-24 01:51:55 +00:00
|
|
|
def skipQuotes(txt):
|
|
|
|
quote = txt[0]
|
|
|
|
text_val = txt[1:]
|
|
|
|
endIndex = str(text_val).index(quote)
|
2019-01-03 19:25:08 +00:00
|
|
|
return text[endIndex:]
|
2020-04-24 01:51:55 +00:00
|
|
|
|
2019-01-03 19:25:08 +00:00
|
|
|
while text:
|
|
|
|
if text[0] in ('"', "'"):
|
|
|
|
try:
|
|
|
|
text = skipQuotes(text)
|
|
|
|
except ValueError:
|
|
|
|
return False
|
|
|
|
text = text[1:]
|
|
|
|
return True
|
|
|
|
|
|
|
|
def child_exec_command(self, text):
|
2020-04-22 20:00:54 +00:00
|
|
|
self.exec_command(text)
|
|
|
|
|
|
|
|
def exec_command(self, text, no_echo=False):
|
|
|
|
"""
|
|
|
|
Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
|
|
|
|
Also handles execution in separated threads
|
|
|
|
|
|
|
|
:param text: FlatCAM TclCommand with parameters
|
|
|
|
:param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
|
|
|
|
will create crashes of the _Expandable_Edit widget
|
|
|
|
:return: output if there was any
|
|
|
|
"""
|
|
|
|
|
|
|
|
self._sysShell.report_usage('exec_command')
|
|
|
|
|
|
|
|
return self.exec_command_test(text, False, no_echo=no_echo)
|
|
|
|
|
|
|
|
def exec_command_test(self, text, reraise=True, no_echo=False):
|
|
|
|
"""
|
|
|
|
Same as exec_command(...) with additional control over exceptions.
|
|
|
|
Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
|
|
|
|
|
|
|
|
:param text: Input command
|
|
|
|
:param reraise: Re-raise TclError exceptions in Python (mostly for unittests).
|
|
|
|
:param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
|
|
|
|
will create crashes of the _Expandable_Edit widget
|
|
|
|
:return: Output from the command
|
|
|
|
"""
|
|
|
|
|
|
|
|
tcl_command_string = str(text)
|
|
|
|
|
|
|
|
try:
|
|
|
|
if no_echo is False:
|
|
|
|
self.open_processing() # Disables input box.
|
|
|
|
|
|
|
|
result = self._sysShell.tcl.eval(str(tcl_command_string))
|
|
|
|
if result != 'None' and no_echo is False:
|
|
|
|
self.append_output(result + '\n')
|
|
|
|
|
|
|
|
except tk.TclError as e:
|
|
|
|
|
|
|
|
# This will display more precise answer if something in TCL shell fails
|
|
|
|
result = self._sysShell.tcl.eval("set errorInfo")
|
|
|
|
self._sysShell.log.error("Exec command Exception: %s" % (result + '\n'))
|
|
|
|
if no_echo is False:
|
|
|
|
self.append_error('ERROR: ' + result + '\n')
|
|
|
|
# Show error in console and just return or in test raise exception
|
|
|
|
if reraise:
|
|
|
|
raise e
|
|
|
|
finally:
|
|
|
|
if no_echo is False:
|
|
|
|
self.close_processing()
|
|
|
|
pass
|
|
|
|
return result
|
|
|
|
|
|
|
|
# """
|
|
|
|
# Code below is unsused. Saved for later.
|
|
|
|
# """
|
|
|
|
|
|
|
|
# parts = re.findall(r'([\w\\:\.]+|".*?")+', text)
|
|
|
|
# parts = [p.replace('\n', '').replace('"', '') for p in parts]
|
|
|
|
# self.log.debug(parts)
|
|
|
|
# try:
|
|
|
|
# if parts[0] not in commands:
|
|
|
|
# self.shell.append_error("Unknown command\n")
|
|
|
|
# return
|
|
|
|
#
|
|
|
|
# #import inspect
|
|
|
|
# #inspect.getargspec(someMethod)
|
|
|
|
# if (type(commands[parts[0]]["params"]) is not list and len(parts)-1 != commands[parts[0]]["params"]) or \
|
|
|
|
# (type(commands[parts[0]]["params"]) is list and len(parts)-1 not in commands[parts[0]]["params"]):
|
|
|
|
# self.shell.append_error(
|
|
|
|
# "Command %s takes %d arguments. %d given.\n" %
|
|
|
|
# (parts[0], commands[parts[0]]["params"], len(parts)-1)
|
|
|
|
# )
|
|
|
|
# return
|
|
|
|
#
|
|
|
|
# cmdfcn = commands[parts[0]]["fcn"]
|
|
|
|
# cmdconv = commands[parts[0]]["converters"]
|
|
|
|
# if len(parts) - 1 > 0:
|
|
|
|
# retval = cmdfcn(*[cmdconv[i](parts[i + 1]) for i in range(len(parts)-1)])
|
|
|
|
# else:
|
|
|
|
# retval = cmdfcn()
|
|
|
|
# retfcn = commands[parts[0]]["retfcn"]
|
|
|
|
# if retval and retfcn(retval):
|
|
|
|
# self.shell.append_output(retfcn(retval) + "\n")
|
|
|
|
#
|
|
|
|
# except Exception as e:
|
|
|
|
# #self.shell.append_error(''.join(traceback.format_exc()))
|
|
|
|
# #self.shell.append_error("?\n")
|
|
|
|
# self.shell.append_error(str(e) + "\n")
|