Initial implementation of console.

This commit is contained in:
Juan Pablo Caram 2014-09-13 17:29:07 -04:00
parent 32076d4020
commit 8cb509d6f3
7 changed files with 526 additions and 14 deletions

View File

@ -3,11 +3,11 @@ from PyQt4 import QtGui
from FlatCAMApp import App
def debug_trace():
'''Set a tracepoint in the Python debugger that works with Qt'''
from PyQt4.QtCore import pyqtRemoveInputHook
#from pdb import set_trace
pyqtRemoveInputHook()
#set_trace()
'''Set a tracepoint in the Python debugger that works with Qt'''
from PyQt4.QtCore import pyqtRemoveInputHook
#from pdb import set_trace
pyqtRemoveInputHook()
#set_trace()
debug_trace()
app = QtGui.QApplication(sys.argv)

View File

@ -8,6 +8,7 @@ import simplejson as json
import re
import webbrowser
import os
import Tkinter
from PyQt4 import QtCore
@ -22,6 +23,8 @@ from FlatCAMGUI import *
from FlatCAMCommon import LoudDict
from FlatCAMTool import *
from FlatCAMShell import FCShell
########################################
## App ##
@ -251,7 +254,7 @@ class App(QtCore.QObject):
#### Check for updates ####
# Separate thread (Not worker)
self.version = 6
self.version = 7
App.log.info("Checking for updates in backgroud (this is version %s)." % str(self.version))
self.worker2 = Worker(self, name="worker2")
@ -293,6 +296,7 @@ class App(QtCore.QObject):
self.ui.menuviewdisableall.triggered.connect(self.disable_plots)
self.ui.menuviewdisableother.triggered.connect(lambda: self.disable_plots(except_current=True))
self.ui.menuviewenable.triggered.connect(self.enable_all_plots)
self.ui.menutoolshell.triggered.connect(lambda: self.shell.show())
self.ui.menuhelp_about.triggered.connect(self.on_about)
self.ui.menuhelp_manual.triggered.connect(lambda: webbrowser.open(self.app_url))
# Toolbar
@ -302,6 +306,7 @@ class App(QtCore.QObject):
self.ui.clear_plot_btn.triggered.connect(self.plotcanvas.clear)
self.ui.replot_btn.triggered.connect(self.on_toolbar_replot)
self.ui.delete_btn.triggered.connect(self.on_delete)
self.ui.shell_btn.triggered.connect(lambda: self.shell.show())
# Object list
self.collection.view.activated.connect(self.on_row_activated)
# Options
@ -324,6 +329,20 @@ class App(QtCore.QObject):
self.measeurement_tool = Measurement(self)
self.measeurement_tool.install()
#############
### Shell ###
#############
# TODO: Move this to its own class
self.shell = FCShell(self)
self.shell.setWindowIcon(self.ui.app_icon)
self.shell.setWindowTitle("FlatCAM Shell")
self.shell.show()
self.shell.resize(550, 300)
self.shell.append_output("FlatCAM Alpha 7\n(c) 2014 Juan Pablo Caram\n\n")
self.shell.append_output("Type help to get started.\n\n")
self.tcl = Tkinter.Tcl()
self.setup_shell()
App.log.debug("END of constructor. Releasing control.")
def defaults_read_form(self):
@ -368,6 +387,110 @@ class App(QtCore.QObject):
# Send to worker
self.worker_task.emit({'fcn': worker_task, 'params': [self]})
def execCommand(self, text):
"""
Hadles input from the shell.
:param text: Input command
:return: None
"""
text = str(text)
try:
result = self.tcl.eval(str(text))
self.shell.append_output(result + '\n')
except Tkinter.TclError, e:
self.shell.append_error('ERROR: ' + str(e) + '\n')
raise
return
def shhelp(p=None):
if not p:
return "Available commands:\n" + '\n'.join([' ' + cmd for cmd in commands])
if p not in commands:
return "Unknown command: %s" % p
return commands[p]["help"]
commands = {
"open_gerber": {
"fcn": self.open_gerber,
"params": 1,
"converters": [lambda x: x],
"retfcn": lambda x: None,
"help": "Opens a Gerber file.\n> open_gerber <filename>\n filename: Path to file to open."
},
"open_excellon": {
"fcn": self.open_excellon,
"params": 1,
"converters": [lambda x: x],
"retfcn": lambda x: None,
"help": "Opens an Excellon file.\n> open_excellon <filename>\n filename: Path to file to open."
},
"open_gcode": {
"fcn": self.open_gcode,
"params": 1,
"converters": [lambda x: x],
"retfcn": lambda x: None,
"help": "Opens an G-Code file.\n> open_gcode <filename>\n filename: Path to file to open."
},
"open_project": {
"fcn": self.open_project,
"params": 1,
"converters": [lambda x: x],
"retfcn": lambda x: None,
"help": "Opens a FlatCAM project.\n> open_project <filename>\n filename: Path to file to open."
},
"save_project": {
"fcn": self.save_project,
"params": 1,
"converters": [lambda x: x],
"retfcn": lambda x: None,
"help": "Saves the FlatCAM project to file.\n> save_project <filename>\n filename: Path to file to save."
},
"help": {
"fcn": shhelp,
"params": [0, 1],
"converters": [lambda x: x],
"retfcn": lambda x: x,
"help": "Shows list of commands."
}
}
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:
self.shell.append_error(''.join(traceback.format_exc()))
#self.shell.append_error("?\n")
def info(self, text):
self.ui.info_label.setText(QtCore.QString(text))
@ -540,7 +663,7 @@ class App(QtCore.QObject):
title = QtGui.QLabel(
"<font size=8><B>FlatCAM</B></font><BR>"
"Version Alpha 6 (2014/09)<BR>"
"Version Alpha 7 (2014/10)<BR>"
"<BR>"
"2D Post-processing for Manufacturing specialized in<BR>"
"Printed Circuit Boards<BR>"
@ -1379,6 +1502,114 @@ class App(QtCore.QObject):
def set_progress_bar(self, percentage, text=""):
self.ui.progress_bar.setValue(int(percentage))
def setup_shell(self):
self.log.debug("setup_shell()")
def shelp(p=None):
if not p:
return "Available commands:\n" + '\n'.join([' ' + cmd for cmd in commands]) + \
"\n\nType help <command_name> for usage.\n Example: help open_gerber"
if p not in commands:
return "Unknown command: %s" % p
return commands[p]["help"]
def options(name):
ops = self.collection.get_by_name(str(name)).options
return '\n'.join(["%s: %s" % (o, ops[o]) for o in ops])
def isolate(name, dia=None, passes=None, overlap=None):
dia = float(dia) if dia is not None else None
passes = int(passes) if passes is not None else None
overlap = float(overlap) if overlap is not None else None
self.collection.get_by_name(str(name)).isolate(dia, passes, overlap)
commands = {
'help': {
'fcn': shelp,
'help': "Shows list of commands."
},
'open_gerber': {
'fcn': self.open_gerber,
'help': "Opens a Gerber file.\n> open_gerber <filename>\n filename: Path to file to open."
},
'open_excellon': {
'fcn': self.open_excellon,
'help': "Opens an Excellon file.\n> open_excellon <filename>\n filename: Path to file to open."
},
'open_gcode': {
'fcn': self.open_gcode,
'help': "Opens an G-Code file.\n> open_gcode <filename>\n filename: Path to file to open."
},
'open_project': {
'fcn': self.open_project,
"help": "Opens a FlatCAM project.\n> open_project <filename>\n filename: Path to file to open."
},
'save_project': {
'fcn': self.save_project,
'help': "Saves the FlatCAM project to file.\n> save_project <filename>\n filename: Path to file to save."
},
'set_active': {
'fcn': self.collection.set_active,
'help': "Sets a FlatCAM object as active.\n > set_active <name>\n name: Name of the object."
},
'get_names': {
'fcn': lambda: '\n'.join(self.collection.get_names()),
'help': "Lists the names of objects in the project.\n > get_names"
},
'new': {
'fcn': self.on_file_new,
'help': "Starts a new project. Clears objects from memory.\n > new"
},
'options': {
'fcn': options,
'help': "Shows the settings for an object.\n > options <name>\n name: Object name."
},
'isolate': {
'fcn': isolate,
'help': "Creates isolation routing geometry for the given Gerber.\n" +
"> isolate <name> [dia [passes [overlap]]]\n" +
" name: Name if the object\n"
" dia: Tool diameter\n passes: # of tool width\n" +
" overlap: Fraction of tool diameter to overlap passes"
},
'scale': {
'fcn': lambda name, factor: self.collection.get_by_name(str(name)).scale(float(factor)),
'help': "Resizes the object by a factor.\n" +
"> scale <name> <factor>\n" +
" name: Name of the object\n factor: Fraction by which to scale"
},
'offset': {
'fcn': lambda name, x, y: self.collection.get_by_name(str(name)).offset([float(x), float(y)]),
'help': "Changes the position of the object.\n" +
"> offset <name> <x> <y>\n" +
" name: Name of the object\n" +
" x: X-axis distance\n" +
" y: Y-axis distance"
},
'plot': {
'fcn': self.plot_all,
'help': 'Updates the plot on the user interface'
}
}
for cmd in commands:
self.tcl.createcommand(cmd, commands[cmd]['fcn'])
self.tcl.eval('''
rename puts original_puts
proc puts {args} {
if {[llength $args] == 1} {
return "[lindex $args 0]"
} else {
eval original_puts $args
}
}
''')
def setup_recent_items(self):
self.log.debug("setup_recent_items()")
@ -1473,8 +1704,8 @@ class App(QtCore.QObject):
try:
data = json.load(f)
except:
App.log.error("Could nor parse information about latest version.")
self.inform.emit("Could nor parse information about latest version.")
App.log.error("Could not parse information about latest version.")
self.inform.emit("Could not parse information about latest version.")
f.close()
return

View File

@ -88,6 +88,7 @@ class FlatCAMGUI(QtGui.QMainWindow):
### Tool ###
self.menutool = self.menu.addMenu('&Tool')
self.menutoolshell = self.menutool.addAction(QtGui.QIcon('share/shell16.png'), '&Command Line')
### Help ###
self.menuhelp = self.menu.addMenu('&Help')
@ -106,6 +107,7 @@ class FlatCAMGUI(QtGui.QMainWindow):
self.clear_plot_btn = self.toolbar.addAction(QtGui.QIcon('share/clear_plot32.png'), "&Clear Plot")
self.replot_btn = self.toolbar.addAction(QtGui.QIcon('share/replot32.png'), "&Replot")
self.delete_btn = self.toolbar.addAction(QtGui.QIcon('share/delete32.png'), "&Delete")
self.shell_btn = self.toolbar.addAction(QtGui.QIcon('share/shell32.png'), "&Command Line")
################
### Splitter ###
@ -217,7 +219,7 @@ class FlatCAMGUI(QtGui.QMainWindow):
self.setWindowIcon(self.app_icon)
self.setGeometry(100, 100, 1024, 650)
self.setWindowTitle('FlatCAM - Alpha 6')
self.setWindowTitle('FlatCAM - Alpha 7')
self.show()

View File

@ -388,9 +388,23 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
def on_iso_button_click(self, *args):
self.read_form()
dia = self.options["isotooldia"]
passes = int(self.options["isopasses"])
overlap = self.options["isooverlap"] * dia
self.isolate()
def isolate(self, dia=None, passes=None, overlap=None):
"""
Creates an isolation routing geometry object in the project.
:param dia: Tool diameter
:param passes: Number of tool widths to cut
:param overlap: Overlap between passes in fraction of tool diameter
:return: None
"""
if dia is None:
dia = self.options["isotooldia"]
if passes is None:
passes = int(self.options["isopasses"])
if overlap is None:
overlap = self.options["isooverlap"] * dia
for i in range(passes):

27
FlatCAMShell.py Normal file
View File

@ -0,0 +1,27 @@
import sys
from PyQt4.QtGui import QApplication
import termwidget
class FCShell(termwidget.TermWidget):
def __init__(self, sysShell, *args):
termwidget.TermWidget.__init__(self, *args)
self._sysShell = sysShell
def is_command_complete(self, text):
def skipQuotes(text):
quote = text[0]
text = text[1:]
endIndex = str(text).index(quote)
return text[endIndex:]
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._sysShell.execCommand(text)

View File

@ -1 +1 @@
[{"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/not_loaded.gtl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/diag_1TOP.art"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/22TOP.art"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Top.gbr"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/holes.drl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/CBS-F_Cu.gtl"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/BLDC2003Through.drl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/CBS-B_Cu.gbl"}, {"kind": "project", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/gerber_project.fcam"}, {"kind": "project", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/drill_project.fcam"}]
[{"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/maitest.gtl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\maitest.gtl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/CBS-F_Cu.gtl"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/CBS.drl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Gerbers/AVR_Transistor_Tester_copper_top.GTL"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/LockController_v1.0_pcb-RoundHoles.TXT/LockController_v1.0_pcb-RoundHoles.TXT"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Gerbers/AVR_Transistor_Tester.DRL"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/FlatCam_Drilling_Test/FlatCam_Drilling_Test.drl"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Excellon_Planck/X-Y CONTROLLER - Drill Data - Through Hole.drl"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Scrivania/girasettori_v3.drd"}]

238
termwidget.py Normal file
View File

@ -0,0 +1,238 @@
"""
Terminal emulator widget.
Shows intput and output text. Allows to enter commands. Supports history.
"""
import cgi
from PyQt4.QtCore import pyqtSignal
from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \
QSizePolicy, QTextCursor, QTextEdit, \
QVBoxLayout, QWidget
class _ExpandableTextEdit(QTextEdit):
"""
Class implements edit line, which expands themselves automatically
"""
historyNext = pyqtSignal()
historyPrev = pyqtSignal()
def __init__(self, termWidget, *args):
QTextEdit.__init__(self, *args)
self.setStyleSheet("font: 9pt \"Courier\";")
self._fittedHeight = 1
self.textChanged.connect(self._fit_to_document)
self._fit_to_document()
self._termWidget = termWidget
def sizeHint(self):
"""
QWidget sizeHint impelemtation
"""
hint = QTextEdit.sizeHint(self)
hint.setHeight(self._fittedHeight)
return hint
def _fit_to_document(self):
"""
Update widget height to fit all text
"""
documentSize = self.document().size().toSize()
self._fittedHeight = documentSize.height() + (self.height() - self.viewport().height())
self.setMaximumHeight(self._fittedHeight)
self.updateGeometry();
def keyPressEvent(self, event):
"""
Catch keyboard events. Process Enter, Up, Down
"""
if event.matches(QKeySequence.InsertParagraphSeparator):
text = self.toPlainText()
if self._termWidget.is_command_complete(text):
self._termWidget.exec_current_command()
return
elif event.matches(QKeySequence.MoveToNextLine):
text = self.toPlainText()
cursorPos = self.textCursor().position()
textBeforeEnd = text[cursorPos:]
# if len(textBeforeEnd.splitlines()) <= 1:
if len(textBeforeEnd.split('\n')) <= 1:
self.historyNext.emit()
return
elif event.matches(QKeySequence.MoveToPreviousLine):
text = self.toPlainText()
cursorPos = self.textCursor().position()
textBeforeStart = text[:cursorPos]
# lineCount = len(textBeforeStart.splitlines())
lineCount = len(textBeforeStart.split('\n'))
if len(textBeforeStart) > 0 and \
(textBeforeStart[-1] == '\n' or textBeforeStart[-1] == '\r'):
lineCount += 1
if lineCount <= 1:
self.historyPrev.emit()
return
elif event.matches(QKeySequence.MoveToNextPage) or \
event.matches(QKeySequence.MoveToPreviousPage):
return self._termWidget.browser().keyPressEvent(event)
QTextEdit.keyPressEvent(self, event)
class TermWidget(QWidget):
"""
Widget wich represents terminal. It only displays text and allows to enter text.
All highlevel 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, *args):
QWidget.__init__(self, *args)
self._browser = QTextEdit(self)
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.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
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')
text = cgi.escape(text)
text = text.replace('\n', '<br/>')
if style != 'out':
def_bg = self._browser.palette().color(QPalette.Base)
h, s, v, a = def_bg.getHsvF()
if style == 'in':
if v > 0.5: # white background
v = v - (v / 8) # make darker
else:
v = v + ((1 - v) / 4) # make ligher
else: # err
if v < 0.5:
v = v + ((1 - v) / 4) # make ligher
if h == -1: # make red
h = 0
s = .4
else:
h = h + ((1 - h) * 0.5) # make more red
bg = QColor.fromHsvF(h, s, v).name()
text = '<span style="background-color: %s; font-weight: bold;">%s</span>' % (str(bg), text)
else:
text = '<span>%s</span>' % text # without span <br/> is ignored!!!
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 positon
and stops moving. As quick fix of this problem, now we always scroll down when add new text.
To fix it correctly, srcoll to the bottom, if before intput has been resized,
scrollbar was in the bottom, and remove next lien
"""
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
Reimplement in the child classes to actually execute command
"""
text = self._edit.toPlainText()
self._append_to_browser('in', '> ' + text + '\n')
if len(self._history) < 2 or\
self._history[-2] != text: # don't insert duplicating items
self._history.insert(-1, text)
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):
"""
Reimplement in the child classes
"""
pass
def add_line_break_to_input(self):
self._edit.textCursor().insertText('\n')
def append_output(self, text):
"""Appent text to output widget
"""
self._append_to_browser('out', text)
def append_error(self, text):
"""Appent 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. 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)