flatcam/flatcamTools/ToolShell.py

374 lines
13 KiB
Python
Raw Normal View History

# ##########################################################
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-01-03 19:25:08 +00:00
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import QVBoxLayout, QWidget
from flatcamGUI.GUIElements import _BrowserTextEdit, _ExpandableTextEdit
2019-01-03 19:25:08 +00:00
import html
import sys
2019-03-07 16:04:11 +00:00
import tkinter as tk
import gettext
import FlatCAMTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
2019-01-03 19:25:08 +00:00
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
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
"""
def __init__(self, version, app, *args):
2019-01-03 19:25:08 +00:00
QWidget.__init__(self, *args)
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
def open_processing(self, detail=None):
2019-01-03 19:25:08 +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:
self._edit.setPlainText(_("...processing..."))
2019-01-03 19:25:08 +00:00
else:
self._edit.setPlainText('%s [%s]' % (_("...processing..."), detail))
2019-01-03 19:25:08 +00:00
self._edit.setDisabled(True)
self._edit.setFocus()
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
"""
assert style in ('in', 'out', 'err', 'warning', 'success', 'selected', 'raw')
2019-01-03 19:25:08 +00:00
if style != 'raw':
text = html.escape(text)
text = text.replace('\n', '<br/>')
else:
text = text.replace('\n', '<br>')
text = text.replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;')
2019-01-03 19:25:08 +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':
text = '<span style="font-weight: bold; color: red;">%s</span>'\
'<span style="font-weight: bold;">%s</span>'\
%(mtype, body)
elif style == '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 == '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 == 'selected':
text = ''
elif style == 'raw':
text = text
2019-01-03 19:25:08 +00:00
else:
# 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()
# scrollattheend = old_value == scrollbar.maximum()
2019-01-03 19:25:08 +00:00
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
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.
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
Re-implement in the child classes to actually execute command
2019-01-03 19:25:08 +00:00
"""
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('\\', '\\\\')
2019-01-03 19:25:08 +00:00
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
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):
"""
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):
"""
Append text to output widget
2019-01-03 19:25:08 +00:00
"""
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)
2019-01-03 19:25:08 +00:00
def append_error(self, text):
"""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-01-03 19:25:08 +00:00
class FCShell(TermWidget):
def __init__(self, sysShell, version, *args):
"""
: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
"""
TermWidget.__init__(self, version, *args, app=sysShell)
2019-01-03 19:25:08 +00:00
self._sysShell = sysShell
def is_command_complete(self, text):
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:]
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):
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")