diff --git a/CHANGELOG.md b/CHANGELOG.md index bdb27840..627cb90c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ CHANGELOG for FlatCAM Evo beta ================================================= +27.03.2022 + +- trying to make loading a project an easier task for the application + 24.03.2022 - refactoring names for some classes diff --git a/appMain.py b/appMain.py index 1d217f08..d7b78b06 100644 --- a/appMain.py +++ b/appMain.py @@ -226,21 +226,16 @@ class App(QtCore.QObject): file_opened = QtCore.pyqtSignal(str, str) # File type and filename file_saved = QtCore.pyqtSignal(str, str) - # close app signal close_app_signal = pyqtSignal() - # will perform the cleanup operation after a Graceful Exit # usefull for the NCC Tool and Paint Tool where some progressive plotting might leave # graphic residues behind cleanup = pyqtSignal() - # emitted when the new_project is created in a threaded way new_project_signal = pyqtSignal() - # Percentage of progress progress = QtCore.pyqtSignal(int) - # Emitted when a new object has been added or deleted from/to the collection object_status_changed = QtCore.pyqtSignal(object, str, str) @@ -248,31 +243,28 @@ class App(QtCore.QObject): # Emmited when shell command is finished(one command only) shell_command_finished = QtCore.pyqtSignal(object) - # Emitted when multiprocess pool has been recreated pool_recreated = QtCore.pyqtSignal(object) - # Emitted when an unhandled exception happens # in the worker task. thread_exception = QtCore.pyqtSignal(object) - # used to signal that there are arguments for the app args_at_startup = QtCore.pyqtSignal(list) - # a reusable signal to replot a list of objects # should be disconnected after use so it can be reused replot_signal = pyqtSignal(list) - # signal emitted when jumping jump_signal = pyqtSignal(tuple) - # signal emitted when jumping locate_signal = pyqtSignal(tuple, str) proj_selection_changed = pyqtSignal(object, object) - - # used by the FlatCAMScript object to process a script + # used by the AppScript object to process a script run_script = pyqtSignal(str) + # used when loading a project and parsing the project file + restore_project = pyqtSignal(object, str, bool, bool, bool, bool) + # used when loading a project and restoring objects + restore_project_objects_sig = pyqtSignal(object, str, bool, bool) def __init__(self, qapp, user_defaults=True): """ @@ -1411,6 +1403,9 @@ class App(QtCore.QObject): # signals for displaying messages in the Tcl Shell are now connected in the ToolShell class + # loading an project + self.restore_project.connect(self.f_handlers.restore_project_handler) + self.restore_project_objects_sig.connect(self.f_handlers.restore_project_objects) # signal to be called when the app is quiting self.app_quit.connect(self.quit_application, type=Qt.ConnectionType.QueuedConnection) self.message.connect( @@ -1494,7 +1489,6 @@ class App(QtCore.QObject): # ########################################################################################################### # ####################################### FILE ASSOCIATIONS SIGNALS ######################################### # ########################################################################################################### - self.ui.util_pref_form.fa_excellon_group.restore_btn.clicked.connect( lambda: self.restore_extensions(ext_type='excellon')) self.ui.util_pref_form.fa_gcode_group.restore_btn.clicked.connect( @@ -2119,12 +2113,13 @@ class App(QtCore.QObject): self.ui.menu_plugins_shell.triggered.connect(self.ui.toggle_shell_ui) # third install all of them + t0 = time.time() try: self.install_tools(init_tcl=init_tcl) except AttributeError: pass - self.log.debug("Tools are initialized.") + self.log.debug("%s: %s" % ("Tools are initialized in", str(time.time() - t0))) # def parse_system_fonts(self): # self.worker_task.emit({'fcn': self.f_parse.get_fonts_by_types, @@ -10245,7 +10240,7 @@ class MenuFileHandlers(QtCore.QObject): self.log.debug("on_file_new_project()") - t0 = time.time() + t_start_proj = time.time() # close any editor that might be open if self.app.call_source != 'app': @@ -10281,7 +10276,7 @@ class MenuFileHandlers(QtCore.QObject): # delete any selection shape on canvas self.app.delete_selection_shape() - # delete all FlatCAM objects + # delete all App objects if keep_scripts is True: for prj_obj in self.app.collection.get_list(): if prj_obj.kind != 'script': @@ -10289,6 +10284,9 @@ class MenuFileHandlers(QtCore.QObject): else: self.app.collection.delete_all() + self.log.debug('%s: %s %s.' % + ("Deleted all the application objects", str(time.time() - t_start_proj), _("seconds"))) + # add in Selected tab an initial text that describe the flow of work in FlatCAm self.app.setup_default_properties_tab() @@ -10305,6 +10303,7 @@ class MenuFileHandlers(QtCore.QObject): if use_thread is True: self.app.new_project_signal.emit() else: + t0 = time.time() # Clear pool self.app.clear_pool() @@ -10313,6 +10312,8 @@ class MenuFileHandlers(QtCore.QObject): self.app.init_tools(init_tcl=True) else: self.app.init_tools(init_tcl=False) + self.log.debug( + '%s: %s %s.' % ("Initiated the MP pool and plugins in: ", str(time.time() - t0), _("seconds"))) # tcl needs to be reinitialized, otherwise old shell variables etc remains # self.app.shell.init_tcl() @@ -10336,7 +10337,7 @@ class MenuFileHandlers(QtCore.QObject): # take the focus of the Notebook on Project Tab. self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) - self.log.debug('%s: %s %s.' % (_("Project created in"), str(time.time() - t0), _("seconds"))) + self.log.debug('%s: %s %s.' % (_("Project created in"), str(time.time() - t_start_proj), _("seconds"))) self.app.ui.set_ui_title(name=_("New Project - Not saved")) self.inform.emit('[success] %s...' % _("New Project created")) @@ -10348,6 +10349,7 @@ class MenuFileHandlers(QtCore.QObject): :return: :rtype: """ + t0 = time.time() # Clear pool self.log.debug("New Project: cleaning multiprocessing pool.") @@ -10356,6 +10358,7 @@ class MenuFileHandlers(QtCore.QObject): # Init FlatCAMTools self.log.debug("New Project: initializing the Tools and Tcl Shell.") self.app.init_tools(init_tcl=True) + self.log.debug('%s: %s %s.' % ("Initiated the MP pool and plugins in: ", str(time.time() - t0), _("seconds"))) def on_filenewscript(self, silent=False): """ @@ -11854,7 +11857,7 @@ class MenuFileHandlers(QtCore.QObject): self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open config file"), filename)) return - def open_project(self, filename, run_from_arg=None, plot=True, cli=None, from_tcl=False): + def open_project(self, filename, run_from_arg=False, plot=True, cli=False, from_tcl=False): """ Loads a project from the specified file. @@ -11872,8 +11875,11 @@ class MenuFileHandlers(QtCore.QObject): :param from_tcl: True if run from Tcl Sehll :return: None """ - self.log.debug("Opening project: " + filename) - if not os.path.exists(filename): + + project_filename = filename + + self.log.debug("Opening project: " + project_filename) + if not os.path.exists(project_filename): self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available.")) return @@ -11893,78 +11899,85 @@ class MenuFileHandlers(QtCore.QObject): alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft, color=QtGui.QColor("lightgray")) - # Open and parse an uncompressed Project file - try: - f = open(filename, 'r') - except IOError: - if from_tcl: - name = filename.split('/')[-1].split('\\')[-1] - filename = self.options['global_tcl_path'] + '/' + name + def parse_worker(prj_filename): + with self.app.proc_container.new('%s' % _("Parsing...")): + # Open and parse an uncompressed Project file try: - f = open(filename, 'r') + f = open(prj_filename, 'r') except IOError: - self.log.error("Failed to open project file: %s" % filename) - self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open project file"), filename)) - return - else: - self.log.error("Failed to open project file: %s" % filename) - self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open project file"), filename)) - return - - try: - d = json.load(f, object_hook=dict2obj) - except Exception as e: - self.log.debug( - "Failed to parse project file, trying to see if it loads as an LZMA archive: %s because %s" % - (filename, str(e))) - f.close() - - # Open and parse a compressed Project file - try: - with lzma.open(filename) as f: - file_content = f.read().decode('utf-8') - d = json.loads(file_content, object_hook=dict2obj) - except Exception as e: - self.log.error("Failed to open project file: %s with error: %s" % (filename, str(e))) - self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open project file"), filename)) - return - - # Check for older projects - found_older_project = False - for obj in d['objs']: - if 'cnc_tools' in obj or 'exc_cnc_tools' in obj or 'apertures' in obj: - self.app.log.error( - 'MenuFileHandlers.open_project() --> %s %s. %s' % - ("Failed to open the CNCJob file:", str(obj['options']['name']), - "Maybe it is an old project.")) - found_older_project = True - - if found_older_project: - if not run_from_arg or not cli or from_tcl is False: - msgbox = FCMessageBox(parent=self.app.ui) - title = _("Legacy Project") - txt = _("The project was made with an older app version.\n" - "It may not load correctly.\n\n" - "Do you want to continue?") - msgbox.setWindowTitle(title) # taskbar still shows it - msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/flatcam_icon128.png')) - msgbox.setText('%s' % title) - msgbox.setInformativeText(txt) - msgbox.setIcon(QtWidgets.QMessageBox.Icon.Question) - - bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.ButtonRole.AcceptRole) - bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.ButtonRole.RejectRole) - - msgbox.setDefaultButton(bt_ok) - msgbox.exec() - response = msgbox.clickedButton() - - if response == bt_cancel: + if from_tcl: + name = prj_filename.split('/')[-1].split('\\')[-1] + prj_filename = os.path.join(self.options['global_tcl_path'], name) + try: + f = open(prj_filename, 'r') + except IOError: + self.log.error("Failed to open project file: %s" % prj_filename) + self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open project file"), prj_filename)) + return + else: + self.log.error("Failed to open project file: %s" % prj_filename) + self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open project file"), prj_filename)) return - else: - self.app.log.error("Legacy Project. Loading not supported.") - return + try: + d = json.load(f, object_hook=dict2obj) + except Exception as e: + self.log.debug( + "Failed to parse project file, trying to see if it loads as an LZMA archive: %s because %s" % + (prj_filename, str(e))) + f.close() + + # Open and parse a compressed Project file + try: + with lzma.open(prj_filename) as f: + file_content = f.read().decode('utf-8') + d = json.loads(file_content, object_hook=dict2obj) + except Exception as e: + self.log.error("Failed to open project file: %s with error: %s" % (prj_filename, str(e))) + self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open project file"), prj_filename)) + return + + # Check for older projects + found_older_project = False + for obj in d['objs']: + if 'cnc_tools' in obj or 'exc_cnc_tools' in obj or 'apertures' in obj: + self.app.log.error( + 'MenuFileHandlers.open_project() --> %s %s. %s' % + ("Failed to open the CNCJob file:", str(obj['options']['name']), + "Maybe it is an old project.")) + found_older_project = True + + if found_older_project: + if not run_from_arg or not cli or from_tcl is False: + msgbox = FCMessageBox(parent=self.app.ui) + title = _("Legacy Project") + txt = _("The project was made with an older app version.\n" + "It may not load correctly.\n\n" + "Do you want to continue?") + msgbox.setWindowTitle(title) # taskbar still shows it + msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/flatcam_icon128.png')) + msgbox.setText('%s' % title) + msgbox.setInformativeText(txt) + msgbox.setIcon(QtWidgets.QMessageBox.Icon.Question) + + bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.ButtonRole.AcceptRole) + bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.ButtonRole.RejectRole) + + msgbox.setDefaultButton(bt_ok) + msgbox.exec() + response = msgbox.clickedButton() + + if response == bt_cancel: + return + else: + self.app.log.error("Legacy Project. Loading not supported.") + return + + self.app.restore_project.emit(d, prj_filename, run_from_arg, from_tcl, cli, plot) + + self.app.worker_task.emit({'fcn': parse_worker, 'params': [project_filename]}) + + def restore_project_handler(self, proj_dict, filename, run_from_arg, from_tcl, cli, plot): # Clear the current project # # NOT THREAD SAFE # ## if run_from_arg is True: @@ -11996,13 +12009,13 @@ class MenuFileHandlers(QtCore.QObject): # self.app.defaults.update(self.app.options) # self.app.preferencesUiManager.save_defaults() # Project options - self.app.options.update(d['options']) + self.app.options.update(proj_dict['options']) if response == bt_no: pass else: # Load by default new options when not using GUI # Project options - self.app.options.update(d['options']) + self.app.options.update(proj_dict['options']) self.app.project_filename = filename @@ -12011,23 +12024,25 @@ class MenuFileHandlers(QtCore.QObject): if cli is None: self.app.set_screen_units(self.app.options["units"]) - # Re create objects - self.log.debug(" **************** Started PROEJCT loading... **************** ") + self.app.restore_project_objects_sig.emit(proj_dict, filename, cli, plot) - for obj in d['objs']: - try: - msg = "Recreating from opened project an %s object: %s" % \ - (obj['kind'].capitalize(), obj['obj_options']['name']) - except KeyError: - # allowance for older projects - msg = "Recreating from opened project an %s object: %s" % \ - (obj['kind'].capitalize(), obj['options']['name']) - self.app.log.debug(msg) + def restore_project_objects(self, proj_dict, filename, cli, plot): - def obj_init(new_obj, app_inst): + def worker_task(): + with self.app.proc_container.new('%s' % _("Loading...")): + # Re create objects + self.log.debug(" **************** Started PROEJCT loading... **************** ") + for obj in proj_dict['objs']: + try: + msg = "Recreating from opened project an %s object: %s" % \ + (obj['kind'].capitalize(), obj['obj_options']['name']) + except KeyError: + # allowance for older projects + msg = "Recreating from opened project an %s object: %s" % \ + (obj['kind'].capitalize(), obj['options']['name']) + self.app.log.debug(msg) - def worker_task(): - with app_inst.proc_container.new('%s...' % _("Opening")): + def obj_init(new_obj, app_inst): try: new_obj.from_dict(obj) except Exception as erro: @@ -12100,43 +12115,42 @@ class MenuFileHandlers(QtCore.QObject): # CNCJob.set_ui() new_obj.is_loaded_from_project = True - worker_task() - # app_inst.worker_task.emit({'fcn': worker_task, 'params': []}) + # for some reason, setting ui_title does not work when this method is called from Tcl Shell + # it's because the TclCommand is run in another thread (it inherits TclCommandSignaled) + try: + if cli is None: + self.app.ui.set_ui_title(name="{} {}: {}".format( + _("Loading Project ... restoring"), obj['kind'].upper(), obj['obj_options']['name'])) - # for some reason, setting ui_title does not work when this method is called from Tcl Shell - # it's because the TclCommand is run in another thread (it inherits TclCommandSignaled) - try: + ret = self.app.app_obj.new_object(obj['kind'], obj['obj_options']['name'], obj_init, plot=plot) + except KeyError: + # allowance for older projects + if cli is None: + self.app.ui.set_ui_title(name="{} {}: {}".format( + _("Loading Project ... restoring"), obj['kind'].upper(), obj['options']['name'])) + try: + ret = self.app.app_obj.new_object(obj['kind'], obj['options']['name'], obj_init, plot=plot) + except Exception: + continue + if ret == 'fail': + continue + + self.inform.emit('[success] %s: %s' % (_("Project loaded from"), filename)) + + self.app.should_we_save = False + self.app.file_opened.emit("project", filename) + + # restore autosaving after a project was loaded + self.app.block_autosave = False + + # for some reason, setting ui_title does not work when this method is called from Tcl Shell + # it's because the TclCommand is run in another thread (it inherit TclCommandSignaled) if cli is None: - self.app.ui.set_ui_title(name="{} {}: {}".format( - _("Loading Project ... restoring"), obj['kind'].upper(), obj['obj_options']['name'])) + self.app.ui.set_ui_title(name=self.app.project_filename) - ret = self.app.app_obj.new_object(obj['kind'], obj['obj_options']['name'], obj_init, plot=plot) - except KeyError: - # allowance for older projects - if cli is None: - self.app.ui.set_ui_title(name="{} {}: {}".format( - _("Loading Project ... restoring"), obj['kind'].upper(), obj['options']['name'])) - try: - ret = self.app.app_obj.new_object(obj['kind'], obj['options']['name'], obj_init, plot=plot) - except Exception: - continue - if ret == 'fail': - continue + self.log.debug(" **************** Finished PROJECT loading... **************** ") - self.inform.emit('[success] %s: %s' % (_("Project loaded from"), filename)) - - self.app.should_we_save = False - self.app.file_opened.emit("project", filename) - - # restore autosaving after a project was loaded - self.app.block_autosave = False - - # for some reason, setting ui_title does not work when this method is called from Tcl Shell - # it's because the TclCommand is run in another thread (it inherit TclCommandSignaled) - if cli is None: - self.app.ui.set_ui_title(name=self.app.project_filename) - - self.log.debug(" **************** Finished PROJECT loading... **************** ") + self.app.worker_task.emit({'fcn': worker_task, 'params': []}) def save_project(self, filename, quit_action=False, silent=False, from_tcl=False): """