- finished the moving of all Tcl Shell stuff out of the FlatCAAMApp class to flatcamTools.ToolShell class

- updated the requirements.txt file to request that the Shapely package needs to be at least version 1.7.0 as it is needed in the latest versions of FlatCAM beta
- some TOOD cleanups
- minor changes
This commit is contained in:
Marius Stanciu
2020-04-27 06:55:08 +03:00
committed by Marius
parent c7bd395368
commit 61020e3624
9 changed files with 268 additions and 386 deletions

View File

@@ -73,8 +73,6 @@ from vispy.io import write_png
from flatcamTools import *
import tclCommands
import gettext
import FlatCAMTranslation as fcTranslate
import builtins
@@ -1173,6 +1171,15 @@ class App(QtCore.QObject):
# set FlatCAM units in the Status bar
self.set_screen_units(self.defaults['units'])
# #############################################################################
# ################################ AUTOSAVE SETUP #############################
# #############################################################################
self.block_autosave = False
self.autosave_timer = QtCore.QTimer(self)
self.save_project_auto_update()
self.autosave_timer.timeout.connect(self.save_project_auto)
# #############################################################################
# ######################## UPDATE PREFERENCES GUI FORMS #######################
# #############################################################################
@@ -1960,15 +1967,6 @@ class App(QtCore.QObject):
self.worker_task.connect(self.workers.add_task)
self.log.debug("Finished creating Workers crew.")
# #############################################################################
# ################################ AUTOSAVE SETUP #############################
# #############################################################################
self.block_autosave = False
self.autosave_timer = QtCore.QTimer(self)
self.save_project_auto_update()
self.autosave_timer.timeout.connect(self.save_project_auto)
# #############################################################################
# ################################# Activity Monitor ##########################
# #############################################################################
@@ -2553,23 +2551,11 @@ class App(QtCore.QObject):
# ####################################################################################
# ####################### Shell SETUP ################################################
# ####################################################################################
# this will hold the TCL instance
self.tcl = None
# the actual variable will be redeclared in setup_tcl()
self.tcl_commands_storage = None
self.init_tcl()
self.shell = FCShell(self, version=self.version)
self.shell._edit.set_model_data(self.myKeywords)
self.shell.setWindowIcon(self.ui.app_icon)
self.shell.setWindowTitle("FlatCAM Shell")
self.shell.resize(*self.defaults["global_shell_shape"])
self.shell._append_to_browser('in', "FlatCAM %s - " % self.version)
self.shell.append_output('%s\n\n' % _("Type >help< to get started"))
self.shell = FCShell(app=self, version=self.version)
self.ui.shell_dock.setWidget(self.shell)
self.log.debug("TCL Shell has been initialized.")
# show TCL shell at start-up based on the Menu -? Edit -> Preferences setting.
if self.defaults["global_shell_at_startup"]:
@@ -4251,7 +4237,7 @@ class App(QtCore.QObject):
# Object creation/instantiation
obj = classdict[kind](name)
obj.units = self.options["units"] # TODO: The constructor should look at defaults.
obj.units = self.options["units"]
# IMPORTANT
# The key names in defaults and options dictionary's are not random:
@@ -6128,9 +6114,10 @@ class App(QtCore.QObject):
def on_fullscreen(self, disable=False):
self.report_usage("on_fullscreen()")
flags = self.ui.windowFlags()
if self.toggle_fscreen is False and disable is False:
# self.ui.showFullScreen()
self.ui.setWindowFlags(self.ui.windowFlags() | Qt.FramelessWindowHint)
self.ui.setWindowFlags(flags | Qt.FramelessWindowHint)
a = self.ui.geometry()
self.x_pos = a.x()
self.y_pos = a.y()
@@ -6158,7 +6145,7 @@ class App(QtCore.QObject):
self.ui.splitter_left.setVisible(False)
self.toggle_fscreen = True
elif self.toggle_fscreen is True or disable is True:
self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.FramelessWindowHint)
self.ui.setWindowFlags(flags & ~Qt.FramelessWindowHint)
self.ui.setGeometry(self.x_pos, self.y_pos, self.width, self.height)
self.ui.showNormal()
self.restore_toolbar_view()
@@ -8312,7 +8299,7 @@ class App(QtCore.QObject):
:return: None
"""
if self.is_legacy is False:
self.plotcanvas.update() # TODO: Need update canvas?
self.plotcanvas.update()
else:
self.plotcanvas.auto_adjust_axes()
@@ -8320,7 +8307,6 @@ class App(QtCore.QObject):
self.collection.update_view()
# self.inform.emit(_("Plots updated ..."))
# TODO: Rework toolbar 'clear', 'replot' functions
def on_toolbar_replot(self):
"""
Callback for toolbar button. Re-plots all objects.
@@ -8372,7 +8358,6 @@ class App(QtCore.QObject):
def on_collection_updated(self, obj, state, old_name):
"""
Create a menu from the object loaded in the collection.
TODO: should use the collection model to do this
:param obj: object that was changed (added, deleted, renamed)
:param state: what was done with the object. Can be: added, deleted, delete_all, renamed
@@ -8572,8 +8557,11 @@ class App(QtCore.QObject):
grid_toggle.triggered.connect(lambda: self.ui.grid_snap_btn.trigger())
def set_grid(self):
self.ui.grid_gap_x_entry.setText(self.sender().text())
self.ui.grid_gap_y_entry.setText(self.sender().text())
menu_action = self.sender()
assert isinstance(menu_action, QtWidgets.QAction), "Expected QAction got %s" % type(menu_action)
self.ui.grid_gap_x_entry.setText(menu_action.text())
self.ui.grid_gap_y_entry.setText(menu_action.text())
def on_grid_add(self):
# ## Current application units in lower Case
@@ -9324,7 +9312,8 @@ class App(QtCore.QObject):
"""
Returns the application to its startup state. This method is thread-safe.
:return: None
:param cli: Boolean. If True this method was run from command line
:return: None
"""
self.report_usage("on_file_new")
@@ -9440,7 +9429,7 @@ class App(QtCore.QObject):
self.report_usage("obj_move()")
self.move_tool.run(toggle=False)
def on_fileopengerber(self, signal: bool = None, name=None):
def on_fileopengerber(self, signal, name=None):
"""
File menu callback for opening a Gerber.
@@ -9487,7 +9476,7 @@ class App(QtCore.QObject):
if filename != '':
self.worker_task.emit({'fcn': self.open_gerber, 'params': [filename]})
def on_fileopenexcellon(self, signal: bool = None, name=None):
def on_fileopenexcellon(self, signal, name=None):
"""
File menu callback for opening an Excellon file.
@@ -9524,7 +9513,7 @@ class App(QtCore.QObject):
if filename != '':
self.worker_task.emit({'fcn': self.open_excellon, 'params': [filename]})
def on_fileopengcode(self, signal: bool = None, name=None):
def on_fileopengcode(self, signal, name=None):
"""
File menu call back for opening gcode.
@@ -9566,7 +9555,7 @@ class App(QtCore.QObject):
if filename != '':
self.worker_task.emit({'fcn': self.open_gcode, 'params': [filename, None, True]})
def on_file_openproject(self, signal: bool = None):
def on_file_openproject(self, signal):
"""
File menu callback for opening a project.
@@ -9597,12 +9586,13 @@ class App(QtCore.QObject):
# thread safe. The new_project()
self.open_project(filename)
def on_fileopenhpgl2(self, signal: bool = None, name=None):
def on_fileopenhpgl2(self, signal, name=None):
"""
File menu callback for opening a HPGL2.
:param signal: required because clicking the entry will generate a checked signal which needs a container
:return: None
:param signal: required because clicking the entry will generate a checked signal which needs a container
:param name:
:return: None
"""
self.report_usage("on_fileopenhpgl2")
@@ -9635,12 +9625,12 @@ class App(QtCore.QObject):
if filename != '':
self.worker_task.emit({'fcn': self.open_hpgl2, 'params': [filename]})
def on_file_openconfig(self, signal: bool = None):
def on_file_openconfig(self, signal):
"""
File menu callback for opening a config file.
:param signal: required because clicking the entry will generate a checked signal which needs a container
:return: None
:param signal: required because clicking the entry will generate a checked signal which needs a container
:return: None
"""
self.report_usage("on_file_openconfig")
@@ -9726,8 +9716,7 @@ class App(QtCore.QObject):
image = _screenshot()
data = np.asarray(image)
if not data.ndim == 3 and data.shape[-1] in (3, 4):
self.inform.emit('[[WARNING_NOTCL]] %s' %
_('Data must be a 3D array with last dimension 3 or 4'))
self.inform.emit('[[WARNING_NOTCL]] %s' % _('Data must be a 3D array with last dimension 3 or 4'))
return
filter_ = "PNG File (*.png);;All Files (*.*)"
@@ -9770,8 +9759,7 @@ class App(QtCore.QObject):
# Check for more compatible types and add as required
if not isinstance(obj, FlatCAMGerber):
self.inform.emit('[ERROR_NOTCL] %s' %
_("Failed. Only Gerber objects can be saved as Gerber files..."))
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Gerber objects can be saved as Gerber files..."))
return
name = self.collection.get_active().options["name"]
@@ -10392,7 +10380,6 @@ class App(QtCore.QObject):
# The Qt methods above will return a QString which can cause problems later.
# So far json.dump() will fail to serialize it.
# TODO: Improve the serialization methods and remove this fix.
filename = str(filename)
if filename == "":
@@ -10853,7 +10840,6 @@ class App(QtCore.QObject):
try:
obj = self.collection.get_by_name(str(obj_name))
except Exception:
# TODO: The return behavior has not been established... should raise exception?
return "Could not retrieve object: %s" % obj_name
else:
obj = local_use
@@ -11003,7 +10989,6 @@ class App(QtCore.QObject):
try:
obj = self.collection.get_by_name(str(obj_name))
except Exception:
# TODO: The return behavior has not been established... should raise exception?
return "Could not retrieve object: %s" % obj_name
else:
obj = local_use
@@ -11806,7 +11791,6 @@ class App(QtCore.QObject):
:return:
"""
# TODO: Move this to constructor
icons = {
"gerber": self.resource_location + "/flatcam_icon16.png",
"excellon": self.resource_location + "/drill16.png",
@@ -11990,32 +11974,38 @@ class App(QtCore.QObject):
tsize = fsize + int(fsize / 2)
# selected_text = (_('''
# <p><span style="font-size:{tsize}px"><strong>Selected Tab - Choose an Item from Project Tab</strong></span></p>
# <p><span style="font-size:{tsize}px"><strong>Selected Tab - Choose an Item from Project Tab</strong></span>
# </p>
#
# <p><span style="font-size:{fsize}px"><strong>Details</strong>:<br />
# The normal flow when working in FlatCAM is the following:</span></p>
#
# <ol>
# <li><span style="font-size:{fsize}px">Loat/Import a Gerber, Excellon, Gcode, DXF, Raster Image or SVG file into
# <li><span style="font-size:{fsize}px">Loat/Import a Gerber, Excellon, Gcode, DXF, Raster Image or SVG
# file into
# FlatCAM using either the menu&#39;s, toolbars, key shortcuts or
# even dragging and dropping the files on the GUI.<br />
# <br />
# You can also load a <strong>FlatCAM project</strong> by double clicking on the project file, drag &amp; drop of the
# You can also load a <strong>FlatCAM project</strong> by double clicking on the project file, drag &amp;
# drop of the
# file into the FLATCAM GUI or through the menu/toolbar links offered within the app.</span><br />
# &nbsp;</li>
# <li><span style="font-size:{fsize}px">Once an object is available in the Project Tab, by selecting it and then
# <li><span style="font-size:{fsize}px">Once an object is available in the Project Tab, by selecting it
# and then
# focusing on <strong>SELECTED TAB </strong>(more simpler is to double click the object name in the
# Project Tab), <strong>SELECTED TAB </strong>will be updated with the object properties according to
# it&#39;s kind: Gerber, Excellon, Geometry or CNCJob object.<br />
# <br />
# If the selection of the object is done on the canvas by single click instead, and the <strong>SELECTED TAB</strong>
# If the selection of the object is done on the canvas by single click instead, and the
# <strong>SELECTED TAB</strong>
# is in focus, again the object properties will be displayed into the Selected Tab. Alternatively,
# double clicking on the object on the canvas will bring the <strong>SELECTED TAB</strong> and populate
# it even if it was out of focus.<br />
# <br />
# You can change the parameters in this screen and the flow direction is like this:<br />
# <br />
# <strong>Gerber/Excellon Object</strong> -&gt; Change Param -&gt; Generate Geometry -&gt;<strong> Geometry Object
# <strong>Gerber/Excellon Object</strong> -&gt; Change Param -&gt; Generate Geometry -&gt;
# <strong> Geometry Object
# </strong>-&gt; Add tools (change param in Selected Tab) -&gt; Generate CNCJob -&gt;<strong> CNCJob Object
# </strong>-&gt; Verify GCode (through Edit CNC Code) and/or append/prepend to GCode (again, done in
# <strong>SELECTED TAB)&nbsp;</strong>-&gt; Save GCode</span></li>
@@ -12128,7 +12118,7 @@ class App(QtCore.QObject):
# no_stats dict; just so it won't break things on website
no_ststs_dict = {}
no_ststs_dict["global_ststs"] = {}
full_url = App.version_url + "?s=" + str(self.defaults['global_serial']) + "&v=" + str(self.version) + \
full_url = App.version_url + "?s=" + str(self.defaults['global_serial']) + "&v=" + str(self.version) +\
"&os=" + str(self.os) + "&" + urllib.parse.urlencode(no_ststs_dict["global_ststs"])
App.log.debug("Checking for updates @ %s" % full_url)
@@ -12441,7 +12431,7 @@ class App(QtCore.QObject):
new_color = self.defaults['gerber_plot_fill']
clicked_action = self.sender()
assert isinstance(clicked_action, QAction), "Expected a QAction, got %s" % isinstance(clicked_action, QAction)
assert isinstance(clicked_action, QAction), "Expected a QAction, got %s" % type(clicked_action)
act_name = clicked_action.text()
sel_obj_list = self.collection.get_selected()
@@ -12717,8 +12707,12 @@ class App(QtCore.QObject):
:return:
"""
log.debug("App.save_project_auto_update() --> updated the interval timeout.")
if self.autosave_timer.isActive():
self.autosave_timer.stop()
try:
if self.autosave_timer.isActive():
self.autosave_timer.stop()
except Exception:
pass
if self.defaults['global_autosave'] is True:
self.autosave_timer.setInterval(int(self.defaults['global_autosave_timeout']))
self.autosave_timer.start()
@@ -12749,211 +12743,6 @@ class App(QtCore.QObject):
self.options.update(self.defaults)
# self.options_write_form()
def init_tcl(self):
"""
Initialize the TCL Shell. A dock widget that holds the GUI interface to the FlatCAM command line.
:return: None
"""
if hasattr(self, 'tcl') and self.tcl is not None:
# self.tcl = None
# TODO we need to clean non default variables and procedures here
# 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()
self.log.debug("TCL Shell has been initialized.")
def setup_shell(self):
"""
Creates shell functions. Runs once at startup.
:return: None
"""
self.log.debug("setup_shell()")
# def shelp(p=None):
# pass
# --- Migrated to new architecture ---
# 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 h(*args):
# """
# Pre-processes arguments to detect '-keyword value' pairs into dictionary
# and standalone parameters into list.
# """
#
# kwa = {}
# a = []
# n = len(args)
# name = None
# for i in range(n):
# match = re.search(r'^-([a-zA-Z].*)', args[i])
# if match:
# assert name is None
# name = match.group(1)
# continue
#
# if name is None:
# a.append(args[i])
# else:
# kwa[name] = args[i]
# name = None
#
# return a, kwa
# @contextmanager
# def wait_signal(signal, timeout=10000):
# """
# Block loop until signal emitted, timeout (ms) elapses
# or unhandled exception happens in a thread.
#
# :param timeout: time after which the loop is exited
# :param signal: Signal to wait for.
# """
# loop = QtCore.QEventLoop()
#
# # Normal termination
# signal.connect(loop.quit)
#
# # Termination by exception in thread
# self.thread_exception.connect(loop.quit)
#
# status = {'timed_out': False}
#
# def report_quit():
# status['timed_out'] = True
# loop.quit()
#
# yield
#
# # Temporarily change how exceptions are managed.
# oeh = sys.excepthook
# ex = []
#
# def except_hook(type_, value, traceback_):
# ex.append(value)
# oeh(type_, value, traceback_)
#
# sys.excepthook = except_hook
#
# # Terminate on timeout
# if timeout is not None:
# QtCore.QTimer.singleShot(timeout, report_quit)
#
# # # ## Block ## ##
# loop.exec_()
#
# # Restore exception management
# sys.excepthook = oeh
# if ex:
# self.raise_tcl_error(str(ex[0]))
#
# if status['timed_out']:
# raise Exception('Timed out!')
#
# def make_docs():
# output = ''
# import collections
# od = collections.OrderedDict(sorted(self.tcl_commands_storage.items()))
# for cmd_, val in od.items():
# output += cmd_ + ' \n' + ''.join(['~'] * len(cmd_)) + '\n'
#
# t = val['help']
# usage_i = t.find('>')
# if usage_i < 0:
# expl = t
# output += expl + '\n\n'
# continue
#
# expl = t[:usage_i - 1]
# output += expl + '\n\n'
#
# end_usage_i = t[usage_i:].find('\n')
#
# if end_usage_i < 0:
# end_usage_i = len(t[usage_i:])
# output += ' ' + t[usage_i:] + '\n No parameters.\n'
# else:
# extras = t[usage_i + end_usage_i + 1:]
# parts = [s.strip() for s in extras.split('\n')]
#
# output += ' ' + t[usage_i:usage_i + end_usage_i] + '\n'
# for p in parts:
# output += ' ' + p + '\n\n'
#
# return output
'''
Howto 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 ocurre, always use raise_tcl_error, never return "sometext" 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 catched 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.
'''
self.tcl_commands_storage = {}
# commands = {
# 'help': {
# 'fcn': shelp,
# 'help': _("Shows list of commands."),
# 'description': ''
# },
# }
# Import/overwrite tcl commands as objects of TclCommand descendants
# This modifies the variable 'commands'.
tclCommands.register_all_commands(self, 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
}
}
''')
# TODO: This shouldn't be here.
class TclErrorException(Exception):
"""
this exception is defined here, to be able catch it if we successfully handle all errors from shell command
"""
pass
def toggle_shell(self):
"""
Toggle shell: if is visible close it, if it is closed then open it
@@ -13025,62 +12814,6 @@ class App(QtCore.QObject):
except AttributeError:
log.debug("shell_message() is called before Shell Class is instantiated. The message is: %s", str(msg))
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.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 ArgsThread(QtCore.QObject):
open_signal = pyqtSignal(list)