Initial implementation of console.
This commit is contained in:
parent
32076d4020
commit
8cb509d6f3
10
FlatCAM.py
10
FlatCAM.py
|
@ -3,11 +3,11 @@ from PyQt4 import QtGui
|
||||||
from FlatCAMApp import App
|
from FlatCAMApp import App
|
||||||
|
|
||||||
def debug_trace():
|
def debug_trace():
|
||||||
'''Set a tracepoint in the Python debugger that works with Qt'''
|
'''Set a tracepoint in the Python debugger that works with Qt'''
|
||||||
from PyQt4.QtCore import pyqtRemoveInputHook
|
from PyQt4.QtCore import pyqtRemoveInputHook
|
||||||
#from pdb import set_trace
|
#from pdb import set_trace
|
||||||
pyqtRemoveInputHook()
|
pyqtRemoveInputHook()
|
||||||
#set_trace()
|
#set_trace()
|
||||||
|
|
||||||
debug_trace()
|
debug_trace()
|
||||||
app = QtGui.QApplication(sys.argv)
|
app = QtGui.QApplication(sys.argv)
|
||||||
|
|
239
FlatCAMApp.py
239
FlatCAMApp.py
|
@ -8,6 +8,7 @@ import simplejson as json
|
||||||
import re
|
import re
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import os
|
import os
|
||||||
|
import Tkinter
|
||||||
|
|
||||||
from PyQt4 import QtCore
|
from PyQt4 import QtCore
|
||||||
|
|
||||||
|
@ -22,6 +23,8 @@ from FlatCAMGUI import *
|
||||||
from FlatCAMCommon import LoudDict
|
from FlatCAMCommon import LoudDict
|
||||||
from FlatCAMTool import *
|
from FlatCAMTool import *
|
||||||
|
|
||||||
|
from FlatCAMShell import FCShell
|
||||||
|
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
## App ##
|
## App ##
|
||||||
|
@ -251,7 +254,7 @@ class App(QtCore.QObject):
|
||||||
|
|
||||||
#### Check for updates ####
|
#### Check for updates ####
|
||||||
# Separate thread (Not worker)
|
# 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))
|
App.log.info("Checking for updates in backgroud (this is version %s)." % str(self.version))
|
||||||
|
|
||||||
self.worker2 = Worker(self, name="worker2")
|
self.worker2 = Worker(self, name="worker2")
|
||||||
|
@ -293,6 +296,7 @@ class App(QtCore.QObject):
|
||||||
self.ui.menuviewdisableall.triggered.connect(self.disable_plots)
|
self.ui.menuviewdisableall.triggered.connect(self.disable_plots)
|
||||||
self.ui.menuviewdisableother.triggered.connect(lambda: self.disable_plots(except_current=True))
|
self.ui.menuviewdisableother.triggered.connect(lambda: self.disable_plots(except_current=True))
|
||||||
self.ui.menuviewenable.triggered.connect(self.enable_all_plots)
|
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_about.triggered.connect(self.on_about)
|
||||||
self.ui.menuhelp_manual.triggered.connect(lambda: webbrowser.open(self.app_url))
|
self.ui.menuhelp_manual.triggered.connect(lambda: webbrowser.open(self.app_url))
|
||||||
# Toolbar
|
# Toolbar
|
||||||
|
@ -302,6 +306,7 @@ class App(QtCore.QObject):
|
||||||
self.ui.clear_plot_btn.triggered.connect(self.plotcanvas.clear)
|
self.ui.clear_plot_btn.triggered.connect(self.plotcanvas.clear)
|
||||||
self.ui.replot_btn.triggered.connect(self.on_toolbar_replot)
|
self.ui.replot_btn.triggered.connect(self.on_toolbar_replot)
|
||||||
self.ui.delete_btn.triggered.connect(self.on_delete)
|
self.ui.delete_btn.triggered.connect(self.on_delete)
|
||||||
|
self.ui.shell_btn.triggered.connect(lambda: self.shell.show())
|
||||||
# Object list
|
# Object list
|
||||||
self.collection.view.activated.connect(self.on_row_activated)
|
self.collection.view.activated.connect(self.on_row_activated)
|
||||||
# Options
|
# Options
|
||||||
|
@ -324,6 +329,20 @@ class App(QtCore.QObject):
|
||||||
self.measeurement_tool = Measurement(self)
|
self.measeurement_tool = Measurement(self)
|
||||||
self.measeurement_tool.install()
|
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.")
|
App.log.debug("END of constructor. Releasing control.")
|
||||||
|
|
||||||
def defaults_read_form(self):
|
def defaults_read_form(self):
|
||||||
|
@ -368,6 +387,110 @@ class App(QtCore.QObject):
|
||||||
# Send to worker
|
# Send to worker
|
||||||
self.worker_task.emit({'fcn': worker_task, 'params': [self]})
|
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):
|
def info(self, text):
|
||||||
self.ui.info_label.setText(QtCore.QString(text))
|
self.ui.info_label.setText(QtCore.QString(text))
|
||||||
|
|
||||||
|
@ -540,7 +663,7 @@ class App(QtCore.QObject):
|
||||||
|
|
||||||
title = QtGui.QLabel(
|
title = QtGui.QLabel(
|
||||||
"<font size=8><B>FlatCAM</B></font><BR>"
|
"<font size=8><B>FlatCAM</B></font><BR>"
|
||||||
"Version Alpha 6 (2014/09)<BR>"
|
"Version Alpha 7 (2014/10)<BR>"
|
||||||
"<BR>"
|
"<BR>"
|
||||||
"2D Post-processing for Manufacturing specialized in<BR>"
|
"2D Post-processing for Manufacturing specialized in<BR>"
|
||||||
"Printed Circuit Boards<BR>"
|
"Printed Circuit Boards<BR>"
|
||||||
|
@ -1379,6 +1502,114 @@ class App(QtCore.QObject):
|
||||||
def set_progress_bar(self, percentage, text=""):
|
def set_progress_bar(self, percentage, text=""):
|
||||||
self.ui.progress_bar.setValue(int(percentage))
|
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):
|
def setup_recent_items(self):
|
||||||
self.log.debug("setup_recent_items()")
|
self.log.debug("setup_recent_items()")
|
||||||
|
|
||||||
|
@ -1473,8 +1704,8 @@ class App(QtCore.QObject):
|
||||||
try:
|
try:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
except:
|
except:
|
||||||
App.log.error("Could nor parse information about latest version.")
|
App.log.error("Could not parse information about latest version.")
|
||||||
self.inform.emit("Could nor parse information about latest version.")
|
self.inform.emit("Could not parse information about latest version.")
|
||||||
f.close()
|
f.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,7 @@ class FlatCAMGUI(QtGui.QMainWindow):
|
||||||
|
|
||||||
### Tool ###
|
### Tool ###
|
||||||
self.menutool = self.menu.addMenu('&Tool')
|
self.menutool = self.menu.addMenu('&Tool')
|
||||||
|
self.menutoolshell = self.menutool.addAction(QtGui.QIcon('share/shell16.png'), '&Command Line')
|
||||||
|
|
||||||
### Help ###
|
### Help ###
|
||||||
self.menuhelp = self.menu.addMenu('&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.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.replot_btn = self.toolbar.addAction(QtGui.QIcon('share/replot32.png'), "&Replot")
|
||||||
self.delete_btn = self.toolbar.addAction(QtGui.QIcon('share/delete32.png'), "&Delete")
|
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 ###
|
### Splitter ###
|
||||||
|
@ -217,7 +219,7 @@ class FlatCAMGUI(QtGui.QMainWindow):
|
||||||
self.setWindowIcon(self.app_icon)
|
self.setWindowIcon(self.app_icon)
|
||||||
|
|
||||||
self.setGeometry(100, 100, 1024, 650)
|
self.setGeometry(100, 100, 1024, 650)
|
||||||
self.setWindowTitle('FlatCAM - Alpha 6')
|
self.setWindowTitle('FlatCAM - Alpha 7')
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -388,9 +388,23 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
|
||||||
|
|
||||||
def on_iso_button_click(self, *args):
|
def on_iso_button_click(self, *args):
|
||||||
self.read_form()
|
self.read_form()
|
||||||
dia = self.options["isotooldia"]
|
self.isolate()
|
||||||
passes = int(self.options["isopasses"])
|
|
||||||
overlap = self.options["isooverlap"] * dia
|
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):
|
for i in range(passes):
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -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"}]
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue