- renamed classes to have shorter names and grouped
This commit is contained in:
519
AppTools/ToolShell.py
Normal file
519
AppTools/ToolShell.py
Normal file
@@ -0,0 +1,519 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# http://flatcam.org #
|
||||
# Author: Juan Pablo Caram (c) #
|
||||
# Date: 2/5/2014 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QTextCursor
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QWidget
|
||||
from AppGUI.GUIElements import _BrowserTextEdit, _ExpandableTextEdit
|
||||
import html
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import tkinter as tk
|
||||
import tclCommands
|
||||
|
||||
import gettext
|
||||
import AppTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
|
||||
class TermWidget(QWidget):
|
||||
"""
|
||||
Widget which represents terminal. It only displays text and allows to enter text.
|
||||
All high level logic should be implemented by client classes
|
||||
|
||||
User pressed Enter. Client class should decide, if command must be executed or user may continue edit it
|
||||
"""
|
||||
|
||||
def __init__(self, version, app, *args):
|
||||
QWidget.__init__(self, *args)
|
||||
|
||||
self._browser = _BrowserTextEdit(version=version, app=app)
|
||||
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
|
||||
|
||||
def open_processing(self, detail=None):
|
||||
"""
|
||||
Open processing and disable using shell commands again until all commands are finished
|
||||
|
||||
: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:
|
||||
self._edit.setPlainText(_("...processing..."))
|
||||
else:
|
||||
self._edit.setPlainText('%s [%s]' % (_("...processing..."), detail))
|
||||
|
||||
self._edit.setDisabled(True)
|
||||
self._edit.setFocus()
|
||||
|
||||
def close_processing(self):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
assert style in ('in', 'out', 'err', 'warning', 'success', 'selected', 'raw')
|
||||
|
||||
if style != 'raw':
|
||||
text = html.escape(text)
|
||||
text = text.replace('\n', '<br/>')
|
||||
else:
|
||||
text = text.replace('\n', '<br>')
|
||||
text = text.replace('\t', ' ')
|
||||
|
||||
idx = text.find(']')
|
||||
mtype = text[:idx+1].upper()
|
||||
mtype = mtype.replace('_NOTCL', '')
|
||||
body = text[idx+1:]
|
||||
if style.lower() == 'in':
|
||||
text = '<span style="font-weight: bold;">%s</span>' % text
|
||||
elif style.lower() == 'err':
|
||||
text = '<span style="font-weight: bold; color: red;">%s</span>'\
|
||||
'<span style="font-weight: bold;">%s</span>'\
|
||||
% (mtype, body)
|
||||
elif style.lower() == 'warning':
|
||||
# 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)
|
||||
elif style.lower() == 'success':
|
||||
# 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)
|
||||
elif style.lower() == 'selected':
|
||||
text = ''
|
||||
elif style.lower() == 'raw':
|
||||
text = text
|
||||
else:
|
||||
# without span <br/> is ignored!!!
|
||||
text = '<span>%s</span>' % text
|
||||
|
||||
scrollbar = self._browser.verticalScrollBar()
|
||||
old_value = scrollbar.value()
|
||||
# scrollattheend = old_value == scrollbar.maximum()
|
||||
|
||||
self._browser.moveCursor(QTextCursor.End)
|
||||
self._browser.insertHtml(text)
|
||||
|
||||
"""TODO When user enters second line to the input, and input is resized, scrollbar changes its position
|
||||
and stops moving. As quick fix of this problem, now we always scroll down when add new text.
|
||||
To fix it correctly, scroll to the bottom, if before input has been resized,
|
||||
scrollbar was in the bottom, and remove next line
|
||||
"""
|
||||
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
|
||||
Re-implement in the child classes to actually execute command
|
||||
"""
|
||||
text = str(self._edit.toPlainText())
|
||||
|
||||
# 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('\\', '\\\\')
|
||||
|
||||
self._append_to_browser('in', '> ' + text + '\n')
|
||||
|
||||
if len(self._history) < 2 or self._history[-2] != text: # don't insert duplicating items
|
||||
try:
|
||||
if text[-1] == '\n':
|
||||
self._history.insert(-1, text[:-1])
|
||||
else:
|
||||
self._history.insert(-1, text)
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
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):
|
||||
"""
|
||||
Re-implement in the child classes
|
||||
"""
|
||||
pass
|
||||
|
||||
def add_line_break_to_input(self):
|
||||
self._edit.textCursor().insertText('\n')
|
||||
|
||||
def append_output(self, text):
|
||||
"""
|
||||
Append text to output widget
|
||||
"""
|
||||
self._append_to_browser('out', text)
|
||||
|
||||
def append_raw(self, text):
|
||||
"""
|
||||
Append text to output widget as it is
|
||||
"""
|
||||
self._append_to_browser('raw', text)
|
||||
|
||||
def append_success(self, text):
|
||||
"""Append text to output widget
|
||||
"""
|
||||
self._append_to_browser('success', text)
|
||||
|
||||
def append_selected(self, text):
|
||||
"""Append text to output widget
|
||||
"""
|
||||
self._append_to_browser('selected', text)
|
||||
|
||||
def append_warning(self, text):
|
||||
"""Append text to output widget
|
||||
"""
|
||||
self._append_to_browser('warning', text)
|
||||
|
||||
def append_error(self, text):
|
||||
"""Append error text to output widget. Text is drawn with red background
|
||||
"""
|
||||
self._append_to_browser('err', text)
|
||||
|
||||
def is_command_complete(self, text):
|
||||
"""
|
||||
Executed by _ExpandableTextEdit. Re-implement 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)
|
||||
|
||||
|
||||
class FCShell(TermWidget):
|
||||
def __init__(self, app, version, *args):
|
||||
"""
|
||||
Initialize the TCL Shell. A dock widget that holds the AppGUI interface to the FlatCAM command line.
|
||||
|
||||
:param app: 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
|
||||
"""
|
||||
TermWidget.__init__(self, version, *args, app=app)
|
||||
self.app = app
|
||||
|
||||
self.tcl_commands_storage = {}
|
||||
|
||||
self.init_tcl()
|
||||
|
||||
self._edit.set_model_data(self.app.myKeywords)
|
||||
self.setWindowIcon(self.app.ui.app_icon)
|
||||
self.setWindowTitle("FlatCAM Shell")
|
||||
self.resize(*self.app.defaults["global_shell_shape"])
|
||||
self._append_to_browser('in', "FlatCAM %s - " % version)
|
||||
self.append_output('%s\n\n' % _("Type >help< to get started"))
|
||||
|
||||
def init_tcl(self):
|
||||
if hasattr(self, 'tcl') and self.tcl is not None:
|
||||
# self.tcl = None
|
||||
# new object cannot be used here as it will not remember values created for next passes,
|
||||
# because tcl was executed in old instance of TCL
|
||||
pass
|
||||
else:
|
||||
self.tcl = tk.Tcl()
|
||||
self.setup_shell()
|
||||
|
||||
def setup_shell(self):
|
||||
"""
|
||||
Creates shell functions. Runs once at startup.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
|
||||
'''
|
||||
How to implement TCL shell commands:
|
||||
|
||||
All parameters passed to command should be possible to set as None and test it afterwards.
|
||||
This is because we need to see error caused in tcl,
|
||||
if None value as default parameter is not allowed TCL will return empty error.
|
||||
Use:
|
||||
def mycommand(name=None,...):
|
||||
|
||||
Test it like this:
|
||||
if name is None:
|
||||
|
||||
self.raise_tcl_error('Argument name is missing.')
|
||||
|
||||
When error occurred, always use raise_tcl_error, never return "some text" on error,
|
||||
otherwise we will miss it and processing will silently continue.
|
||||
Method raise_tcl_error pass error into TCL interpreter, then raise python exception,
|
||||
which is caught in exec_command and displayed in TCL shell console with red background.
|
||||
Error in console is displayed with TCL trace.
|
||||
|
||||
This behavior works only within main thread,
|
||||
errors with promissed tasks can be catched and detected only with log.
|
||||
TODO: this problem have to be addressed somehow, maybe rewrite promissing to be blocking somehow for
|
||||
TCL shell.
|
||||
|
||||
Kamil's comment: I will rewrite existing TCL commands from time to time to follow this rules.
|
||||
|
||||
'''
|
||||
|
||||
# Import/overwrite tcl commands as objects of TclCommand descendants
|
||||
# This modifies the variable 'self.tcl_commands_storage'.
|
||||
tclCommands.register_all_commands(self.app, self.tcl_commands_storage)
|
||||
|
||||
# Add commands to the tcl interpreter
|
||||
for cmd in self.tcl_commands_storage:
|
||||
self.tcl.createcommand(cmd, self.tcl_commands_storage[cmd]['fcn'])
|
||||
|
||||
# Make the tcl puts function return instead of print to stdout
|
||||
self.tcl.eval('''
|
||||
rename puts original_puts
|
||||
proc puts {args} {
|
||||
if {[llength $args] == 1} {
|
||||
return "[lindex $args 0]"
|
||||
} else {
|
||||
eval original_puts $args
|
||||
}
|
||||
}
|
||||
''')
|
||||
|
||||
def is_command_complete(self, text):
|
||||
|
||||
def skipQuotes(txt):
|
||||
quote = txt[0]
|
||||
text_val = txt[1:]
|
||||
endIndex = str(text_val).index(quote)
|
||||
return text[endIndex:]
|
||||
|
||||
# I'm disabling this because I need to be able to load paths that have spaces by
|
||||
# enclosing them in quotes --- Marius Stanciu
|
||||
# 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):
|
||||
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.app.defaults.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.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.tcl.eval("set errorInfo")
|
||||
self.app.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
|
||||
|
||||
def raise_tcl_unknown_error(self, unknownException):
|
||||
"""
|
||||
Raise exception if is different type than TclErrorException
|
||||
this is here mainly to show unknown errors inside TCL shell console.
|
||||
|
||||
:param unknownException:
|
||||
:return:
|
||||
"""
|
||||
|
||||
if not isinstance(unknownException, self.TclErrorException):
|
||||
self.raise_tcl_error("Unknown error: %s" % str(unknownException))
|
||||
else:
|
||||
raise unknownException
|
||||
|
||||
def display_tcl_error(self, error, error_info=None):
|
||||
"""
|
||||
Escape bracket [ with '\' otherwise there is error
|
||||
"ERROR: missing close-bracket" instead of real error
|
||||
|
||||
:param error: it may be text or exception
|
||||
:param error_info: Some informations about the error
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if isinstance(error, Exception):
|
||||
exc_type, exc_value, exc_traceback = error_info
|
||||
if not isinstance(error, self.TclErrorException):
|
||||
show_trace = 1
|
||||
else:
|
||||
show_trace = int(self.app.defaults['global_verbose_error_level'])
|
||||
|
||||
if show_trace > 0:
|
||||
trc = traceback.format_list(traceback.extract_tb(exc_traceback))
|
||||
trc_formated = []
|
||||
for a in reversed(trc):
|
||||
trc_formated.append(a.replace(" ", " > ").replace("\n", ""))
|
||||
text = "%s\nPython traceback: %s\n%s" % (exc_value, exc_type, "\n".join(trc_formated))
|
||||
else:
|
||||
text = "%s" % error
|
||||
else:
|
||||
text = error
|
||||
|
||||
text = text.replace('[', '\\[').replace('"', '\\"')
|
||||
self.tcl.eval('return -code error "%s"' % text)
|
||||
|
||||
def raise_tcl_error(self, text):
|
||||
"""
|
||||
This method pass exception from python into TCL as error, so we get stacktrace and reason
|
||||
|
||||
:param text: text of error
|
||||
:return: raise exception
|
||||
"""
|
||||
|
||||
self.display_tcl_error(text)
|
||||
raise self.TclErrorException(text)
|
||||
|
||||
class TclErrorException(Exception):
|
||||
"""
|
||||
this exception is defined here, to be able catch it if we successfully handle all errors from shell command
|
||||
"""
|
||||
pass
|
||||
|
||||
# """
|
||||
# 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")
|
||||
Reference in New Issue
Block a user