diff --git a/CHANGELOG.md b/CHANGELOG.md
index d4ba6b4a..8bb22aa7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ CHANGELOG for FlatCAM Evo beta
=================================================
+21.05.2022
+
+- more code refactored in the appMain.py
+
20.05.2022
- small fix for a bug that interfere with running the 2D graphic mode
diff --git a/appCommon/RegisterFileKeywords.py b/appCommon/RegisterFileKeywords.py
index d09da33a..1e2396cc 100644
--- a/appCommon/RegisterFileKeywords.py
+++ b/appCommon/RegisterFileKeywords.py
@@ -4,12 +4,14 @@ from PyQt6 import QtCore
from dataclasses import dataclass
import ctypes
-import winreg
from copy import deepcopy
import os
import sys
import typing
+if sys.platform == 'win32':
+ import winreg
+
if typing.TYPE_CHECKING:
import appMain
diff --git a/appHandlers/AppIO.py b/appHandlers/AppIO.py
new file mode 100644
index 00000000..2f05a00a
--- /dev/null
+++ b/appHandlers/AppIO.py
@@ -0,0 +1,2915 @@
+
+from appEditors.AppExcEditor import AppExcEditor
+from appEditors.AppGeoEditor import AppGeoEditor
+from appEditors.AppGerberEditor import AppGerberEditor
+
+from appGUI.GUIElements import FCFileSaveDialog, FCMessageBox
+from camlib import to_dict, dict2obj, ET, ParseError
+from appParsers.ParseHPGL2 import HPGL2
+
+from appObjects.ObjectCollection import *
+
+from reportlab.graphics import renderPDF
+from reportlab.pdfgen import canvas
+from reportlab.lib.units import inch, mm
+from reportlab.lib.pagesizes import landscape, portrait
+from svglib.svglib import svg2rlg
+from xml.dom.minidom import parseString as parse_xml_string
+
+import time
+import sys
+import os
+from copy import deepcopy
+import re
+
+import numpy as np
+from numpy import Inf
+
+from datetime import datetime
+import simplejson as json
+
+from appCommon.Common import LoudDict
+
+from vispy.gloo.util import _screenshot
+from vispy.io import write_png
+
+import traceback
+import lzma
+from io import StringIO
+
+# App Translation
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+import typing
+
+if typing.TYPE_CHECKING:
+ import appMain
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+ _ = gettext.gettext
+
+
+class AppIO(QtCore.QObject):
+ def __init__(self, app):
+ """
+ A class that holds all the menu -> file handlers
+ """
+ super().__init__()
+
+ self.app = app
+ self.log = self.app.log
+ self.inform = self.app.inform
+ self.splash = self.app.splash
+ self.worker_task = self.app.worker_task
+ self.options = self.app.options
+ self.app_units = self.app.app_units
+ self.pagesize = {}
+
+ self.app.new_project_signal.connect(self.on_new_project_house_keeping)
+
+ def on_fileopengerber(self, name=None):
+ """
+ File menu callback for opening a Gerber.
+
+ :param name:
+ :return: None
+ """
+
+ self.log.debug("on_fileopengerber()")
+
+ _filter_ = "Gerber Files (*.gbr *.ger *.gtl *.gbl *.gts *.gbs *.gtp *.gbp *.gto *.gbo *.gm1 *.gml *.gm3 " \
+ "*.gko *.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim *.mil *.grb " \
+ "*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb *.pho *.gdo *.art *.gbd *.outline);;" \
+ "Protel Files (*.gtl *.gbl *.gts *.gbs *.gto *.gbo *.gtp *.gbp *.gml *.gm1 *.gm3 *.gko " \
+ "*.outline);;" \
+ "Eagle Files (*.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim " \
+ "*.mil);;" \
+ "OrCAD Files (*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb);;" \
+ "Allegro Files (*.art);;" \
+ "Mentor Files (*.pho *.gdo);;" \
+ "All Files (*.*)"
+
+ if name is None:
+ try:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"),
+ directory=self.app.get_last_folder(),
+ filter=_filter_,
+ initialFilter=self.app.last_op_gerber_filter)
+ except TypeError:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"), filter=_filter_)
+
+ filenames = [str(filename) for filename in filenames]
+ self.app.last_op_gerber_filter = _f
+ else:
+ filenames = [name]
+ self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
+ "Canvas initialization finished in"),
+ '%.2f' % self.app.used_time,
+ _("Opening Gerber file.")),
+ alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
+ color=QtGui.QColor("lightgray"))
+
+ if len(filenames) == 0:
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ else:
+ for filename in filenames:
+ if filename != '':
+ self.worker_task.emit({'fcn': self.open_gerber, 'params': [filename]})
+
+ def on_fileopenexcellon(self, name=None):
+ """
+ File menu callback for opening an Excellon file.
+
+ :param name:
+ :return: None
+ """
+
+ self.log.debug("on_fileopenexcellon()")
+
+ _filter_ = "Excellon Files (*.drl *.txt *.xln *.drd *.tap *.exc *.ncd);;" \
+ "All Files (*.*)"
+ if name is None:
+ try:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"),
+ directory=self.app.get_last_folder(),
+ filter=_filter_,
+ initialFilter=self.app.last_op_excellon_filter)
+ except TypeError:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"), filter=_filter_)
+ filenames = [str(filename) for filename in filenames]
+ self.app.last_op_excellon_filter = _f
+ else:
+ filenames = [str(name)]
+ self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
+ "Canvas initialization finished in"),
+ '%.2f' % self.app.used_time,
+ _("Opening Excellon file.")),
+ alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
+ color=QtGui.QColor("lightgray"))
+
+ if len(filenames) == 0:
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ else:
+ for filename in filenames:
+ if filename != '':
+ self.worker_task.emit({'fcn': self.open_excellon, 'params': [filename]})
+
+ def on_fileopengcode(self, name=None):
+ """
+
+ File menu call back for opening gcode.
+
+ :param name:
+ :return:
+ """
+
+ self.log.debug("on_fileopengcode()")
+
+ # https://bobcadsupport.com/helpdesk/index.php?/Knowledgebase/Article/View/13/5/known-g-code-file-extensions
+ _filter_ = "G-Code Files (*.txt *.nc *.ncc *.tap *.gcode *.cnc *.ecs *.fnc *.dnc *.ncg *.gc *.fan *.fgc" \
+ " *.din *.xpi *.hnc *.h *.i *.ncp *.min *.gcd *.rol *.knc *.mpr *.ply *.out *.eia *.sbp *.mpf);;" \
+ "All Files (*.*)"
+
+ if name is None:
+ try:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"),
+ directory=self.app.get_last_folder(),
+ filter=_filter_,
+ initialFilter=self.app.last_op_gcode_filter)
+ except TypeError:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"), filter=_filter_)
+
+ filenames = [str(filename) for filename in filenames]
+ self.app.last_op_gcode_filter = _f
+ else:
+ filenames = [name]
+ self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
+ "Canvas initialization finished in"),
+ '%.2f' % self.app.used_time,
+ _("Opening G-Code file.")),
+ alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
+ color=QtGui.QColor("lightgray"))
+
+ if len(filenames) == 0:
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ else:
+ for filename in filenames:
+ if filename != '':
+ self.worker_task.emit({'fcn': self.open_gcode, 'params': [filename, None, True]})
+
+ def on_file_openproject(self):
+ """
+ File menu callback for opening a project.
+
+ :return: None
+ """
+
+ self.log.debug("on_file_openproject()")
+
+ _filter_ = "FlatCAM Project (*.FlatPrj);;All Files (*.*)"
+ try:
+ filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Project"),
+ directory=self.app.get_last_folder(), filter=_filter_)
+ except TypeError:
+ filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Project"), filter=_filter_)
+
+ filename = str(filename)
+
+ if filename == "":
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ else:
+ # self.worker_task.emit({'fcn': self.open_project,
+ # 'params': [filename]})
+ # The above was failing because open_project() is not
+ # thread safe. The new_project()
+ self.open_project(filename)
+
+ def on_fileopenhpgl2(self, name=None):
+ """
+ File menu callback for opening a HPGL2.
+
+ :param name:
+ :return: None
+ """
+ self.log.debug("on_fileopenhpgl2()")
+
+ _filter_ = "HPGL2 Files (*.plt);;" \
+ "All Files (*.*)"
+
+ if name is None:
+ try:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open HPGL2"),
+ directory=self.app.get_last_folder(),
+ filter=_filter_)
+ except TypeError:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open HPGL2"), filter=_filter_)
+
+ filenames = [str(filename) for filename in filenames]
+ else:
+ filenames = [name]
+ self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
+ "Canvas initialization finished in"),
+ '%.2f' % self.app.used_time,
+ _("Opening HPGL2 file.")),
+ alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
+ color=QtGui.QColor("lightgray"))
+
+ if len(filenames) == 0:
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ else:
+ for filename in filenames:
+ if filename != '':
+ self.worker_task.emit({'fcn': self.open_hpgl2, 'params': [filename]})
+
+ def on_file_openconfig(self):
+ """
+ File menu callback for opening a config file.
+
+ :return: None
+ """
+
+ self.log.debug("on_file_openconfig()")
+
+ _filter_ = "FlatCAM Config (*.FlatConfig);;FlatCAM Config (*.json);;All Files (*.*)"
+ try:
+ filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Configuration File"),
+ directory=self.app.data_path, filter=_filter_)
+ except TypeError:
+ filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Configuration File"),
+ filter=_filter_)
+
+ if filename == "":
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ else:
+ self.open_config_file(filename)
+
+ def on_file_exportsvg(self):
+ """
+ Callback for menu item File->Export SVG.
+
+ :return: None
+ """
+ self.log.debug("on_file_exportsvg()")
+
+ obj = self.app.collection.get_active()
+ if obj is None:
+ self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
+ return
+
+ # Check for more compatible types and add as required
+ if (not isinstance(obj, GeometryObject)
+ and not isinstance(obj, GerberObject)
+ and not isinstance(obj, CNCJobObject)
+ and not isinstance(obj, ExcellonObject)):
+ msg = _("Only Geometry, Gerber and CNCJob objects can be used.")
+ msgbox = FCMessageBox(parent=self.app.ui)
+ msgbox.setWindowTitle(msg) # taskbar still shows it
+ msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
+
+ msgbox.setInformativeText(msg)
+ msgbox.setIconPixmap(QtGui.QPixmap(self.app.resource_location + '/waning.png'))
+
+ bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.ButtonRole.AcceptRole)
+ msgbox.setDefaultButton(bt_ok)
+ msgbox.exec()
+ return
+
+ name = obj.obj_options["name"]
+
+ _filter = "SVG File (*.svg);;All Files (*.*)"
+ try:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Export SVG"),
+ directory=self.app.get_last_save_folder() + '/' + str(name) + '_svg',
+ ext_filter=_filter)
+ except TypeError:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Export SVG"),
+ ext_filter=_filter)
+
+ filename = str(filename)
+
+ if filename == "":
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ return
+ else:
+ self.export_svg(name, filename)
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("SVG", filename)
+ self.app.file_saved.emit("SVG", filename)
+
+ def on_file_exportpng(self):
+
+ self.log.debug("on_file_exportpng()")
+
+ date = str(datetime.today()).rpartition('.')[0]
+ date = ''.join(c for c in date if c not in ':-')
+ date = date.replace(' ', '_')
+
+ data = None
+ if self.app.use_3d_engine:
+ image = _screenshot(alpha=False)
+ 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'))
+ return
+
+ filter_ = "PNG File (*.png);;All Files (*.*)"
+ try:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Export PNG Image"),
+ directory=self.app.get_last_save_folder() + '/png_' + date,
+ ext_filter=filter_)
+ except TypeError:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Export PNG Image"),
+ ext_filter=filter_)
+
+ filename = str(filename)
+
+ if filename == "":
+ self.inform.emit(_("Cancelled."))
+ return
+ else:
+ if self.app.use_3d_engine:
+ write_png(filename, data)
+ else:
+ self.app.plotcanvas.figure.savefig(filename)
+
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("png", filename)
+ self.app.file_saved.emit("png", filename)
+
+ def on_file_savegerber(self):
+ """
+ Callback for menu item in Project context menu.
+
+ :return: None
+ """
+ self.log.debug("on_file_savegerber()")
+
+ obj = self.app.collection.get_active()
+ if obj is None:
+ self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
+ return
+
+ # Check for more compatible types and add as required
+ if not isinstance(obj, GerberObject):
+ self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Gerber objects can be saved as Gerber files..."))
+ return
+
+ name = self.app.collection.get_active().obj_options["name"]
+
+ _filter = "Gerber File (*.GBR);;Gerber File (*.GRB);;All Files (*.*)"
+ try:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption="Save Gerber source file",
+ directory=self.app.get_last_save_folder() + '/' + name,
+ ext_filter=_filter)
+ except TypeError:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Save Gerber source file"),
+ ext_filter=_filter)
+
+ filename = str(filename)
+
+ if filename == "":
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ return
+ else:
+ self.save_source_file(name, filename)
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("Gerber", filename)
+ self.app.file_saved.emit("Gerber", filename)
+
+ def on_file_savescript(self):
+ """
+ Callback for menu item in Project context menu.
+
+ :return: None
+ """
+ self.log.debug("on_file_savescript()")
+
+ obj = self.app.collection.get_active()
+ if obj is None:
+ self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
+ return
+
+ # Check for more compatible types and add as required
+ if not isinstance(obj, ScriptObject):
+ self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Script objects can be saved as TCL Script files..."))
+ return
+
+ name = self.app.collection.get_active().obj_options["name"]
+
+ _filter = "FlatCAM Scripts (*.FlatScript);;All Files (*.*)"
+ try:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption="Save Script source file",
+ directory=self.app.get_last_save_folder() + '/' + name,
+ ext_filter=_filter)
+ except TypeError:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Save Script source file"),
+ ext_filter=_filter)
+
+ filename = str(filename)
+
+ if filename == "":
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ return
+ else:
+ self.save_source_file(name, filename)
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("Script", filename)
+ self.app.file_saved.emit("Script", filename)
+
+ def on_file_savedocument(self):
+ """
+ Callback for menu item in Project context menu.
+
+ :return: None
+ """
+ self.log.debug("on_file_savedocument()")
+
+ obj = self.app.collection.get_active()
+ if obj is None:
+ self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
+ return
+
+ # Check for more compatible types and add as required
+ if not isinstance(obj, ScriptObject):
+ self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Document objects can be saved as Document files..."))
+ return
+
+ name = self.app.collection.get_active().obj_options["name"]
+
+ _filter = "FlatCAM Documents (*.FlatDoc);;All Files (*.*)"
+ try:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption="Save Document source file",
+ directory=self.app.get_last_save_folder() + '/' + name,
+ ext_filter=_filter)
+ except TypeError:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Save Document source file"),
+ ext_filter=_filter)
+
+ filename = str(filename)
+
+ if filename == "":
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ return
+ else:
+ self.save_source_file(name, filename)
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("Document", filename)
+ self.app.file_saved.emit("Document", filename)
+
+ def on_file_saveexcellon(self):
+ """
+ Callback for menu item in project context menu.
+
+ :return: None
+ """
+ self.log.debug("on_file_saveexcellon()")
+
+ obj = self.app.collection.get_active()
+ if obj is None:
+ self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
+ return
+
+ # Check for more compatible types and add as required
+ if not isinstance(obj, ExcellonObject):
+ self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Excellon objects can be saved as Excellon files..."))
+ return
+
+ name = self.app.collection.get_active().obj_options["name"]
+
+ _filter = "Excellon File (*.DRL);;Excellon File (*.TXT);;All Files (*.*)"
+ try:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Save Excellon source file"),
+ directory=self.app.get_last_save_folder() + '/' + name,
+ ext_filter=_filter)
+ except TypeError:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Save Excellon source file"), ext_filter=_filter)
+
+ filename = str(filename)
+
+ if filename == "":
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ return
+ else:
+ self.save_source_file(name, filename)
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("Excellon", filename)
+ self.app.file_saved.emit("Excellon", filename)
+
+ def on_file_exportexcellon(self):
+ """
+ Callback for menu item File->Export->Excellon.
+
+ :return: None
+ """
+ self.log.debug("on_file_exportexcellon()")
+
+ obj = self.app.collection.get_active()
+ if obj is None:
+ self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
+ return
+
+ # Check for more compatible types and add as required
+ if not isinstance(obj, ExcellonObject):
+ self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Excellon objects can be saved as Excellon files..."))
+ return
+
+ name = self.app.collection.get_active().obj_options["name"]
+
+ _filter = self.options["excellon_save_filters"]
+ try:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Export Excellon"),
+ directory=self.app.get_last_save_folder() + '/' + name,
+ ext_filter=_filter)
+ except TypeError:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Export Excellon"),
+ ext_filter=_filter)
+
+ filename = str(filename)
+
+ if filename == "":
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ return
+ else:
+ used_extension = filename.rpartition('.')[2]
+ obj.update_filters(last_ext=used_extension, filter_string='excellon_save_filters')
+
+ self.export_excellon(name, filename)
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("Excellon", filename)
+ self.app.file_saved.emit("Excellon", filename)
+
+ def on_file_exportgerber(self):
+ """
+ Callback for menu item File->Export->Gerber.
+
+ :return: None
+ """
+ self.log.debug("on_file_exportgerber()")
+
+ obj = self.app.collection.get_active()
+ if obj is None:
+ self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
+ return
+
+ # Check for more compatible types and add as required
+ if not isinstance(obj, GerberObject):
+ self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Gerber objects can be saved as Gerber files..."))
+ return
+
+ name = self.app.collection.get_active().obj_options["name"]
+
+ _filter_ = self.options['gerber_save_filters']
+ try:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Export Gerber"),
+ directory=self.app.get_last_save_folder() + '/' + name,
+ ext_filter=_filter_)
+ except TypeError:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Export Gerber"),
+ ext_filter=_filter_)
+
+ filename = str(filename)
+
+ if filename == "":
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ return
+ else:
+ used_extension = filename.rpartition('.')[2]
+ obj.update_filters(last_ext=used_extension, filter_string='gerber_save_filters')
+
+ self.export_gerber(name, filename)
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("Gerber", filename)
+ self.app.file_saved.emit("Gerber", filename)
+
+ def on_file_exportdxf(self):
+ """
+ Callback for menu item File->Export DXF.
+
+ :return: None
+ """
+ self.log.debug("on_file_exportdxf()")
+
+ obj = self.app.collection.get_active()
+ if obj is None:
+ self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
+ return
+
+ # Check for more compatible types and add as required
+ if obj.kind != 'geometry':
+ msg = _("Only Geometry objects can be used.")
+ msgbox = FCMessageBox(parent=self.app.ui)
+ msgbox.setWindowTitle(msg) # taskbar still shows it
+ msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
+
+ msgbox.setInformativeText(msg)
+ msgbox.setIconPixmap(QtGui.QPixmap(self.app.resource_location + '/waning.png'))
+
+ msgbox.setIcon(QtWidgets.QMessageBox.Icon.Warning)
+
+ msgbox.setInformativeText(msg)
+ bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.ButtonRole.AcceptRole)
+ msgbox.setDefaultButton(bt_ok)
+ msgbox.exec()
+ return
+
+ name = self.app.collection.get_active().obj_options["name"]
+
+ _filter_ = "DXF File .dxf (*.DXF);;All Files (*.*)"
+ try:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Export DXF"),
+ directory=self.app.get_last_save_folder() + '/' + name,
+ ext_filter=_filter_)
+ except TypeError:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Export DXF"),
+ ext_filter=_filter_)
+
+ filename = str(filename)
+
+ if filename == "":
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ return
+ else:
+ self.export_dxf(name, filename)
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("DXF", filename)
+ self.app.file_saved.emit("DXF", filename)
+
+ def on_file_importsvg(self, type_of_obj):
+ """
+ Callback for menu item File->Import SVG.
+ :param type_of_obj: to import the SVG as Geometry or as Gerber
+ :type type_of_obj: str
+ :return: None
+ """
+ self.log.debug("on_file_importsvg()")
+
+ _filter_ = "SVG File .svg (*.svg);;All Files (*.*)"
+ try:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import SVG"),
+ directory=self.app.get_last_folder(),
+ filter=_filter_)
+ except TypeError:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import SVG"),
+ filter=_filter_)
+
+ if type_of_obj != "geometry" and type_of_obj != "gerber":
+ type_of_obj = "geometry"
+
+ filenames = [str(filename) for filename in filenames]
+
+ if len(filenames) == 0:
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ else:
+ for filename in filenames:
+ if filename != '':
+ self.worker_task.emit({'fcn': self.import_svg, 'params': [filename, type_of_obj]})
+
+ def on_file_importdxf(self, type_of_obj):
+ """
+ Callback for menu item File->Import DXF.
+ :param type_of_obj: to import the DXF as Geometry or as Gerber
+ :type type_of_obj: str
+ :return: None
+ """
+ self.log.debug("on_file_importdxf()")
+
+ _filter_ = "DXF File .dxf (*.DXF);;All Files (*.*)"
+ try:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import DXF"),
+ directory=self.app.get_last_folder(),
+ filter=_filter_)
+ except TypeError:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import DXF"),
+ filter=_filter_)
+
+ if type_of_obj != "geometry" and type_of_obj != "gerber":
+ type_of_obj = "geometry"
+
+ filenames = [str(filename) for filename in filenames]
+
+ if len(filenames) == 0:
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ else:
+ for filename in filenames:
+ if filename != '':
+ self.worker_task.emit({'fcn': self.import_dxf, 'params': [filename, type_of_obj]})
+
+ def on_file_new_click(self):
+ """
+ Callback for menu item File -> New.
+ Executed on clicking the Menu -> File -> New Project
+
+ :return:
+ """
+ self.log.debug("on_file_new_click()")
+
+ if self.app.collection.get_list() and self.app.should_we_save:
+ msgbox = FCMessageBox(parent=self.app.ui)
+ title = _("Save changes")
+ txt = _("There are files/objects opened.\n"
+ "Creating a New project will delete them.\n"
+ "Do you want to Save the project?")
+ msgbox.setWindowTitle(title) # taskbar still shows it
+ msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
+ msgbox.setText('%s' % title)
+ msgbox.setInformativeText(txt)
+ msgbox.setIconPixmap(QtGui.QPixmap(self.app.resource_location + '/save_as.png'))
+
+ bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.ButtonRole.YesRole)
+ bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.ButtonRole.NoRole)
+ bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.ButtonRole.RejectRole)
+
+ msgbox.setDefaultButton(bt_yes)
+ msgbox.exec()
+ response = msgbox.clickedButton()
+
+ if response == bt_yes:
+ self.on_file_saveprojectas(use_thread=True)
+ elif response == bt_cancel:
+ return
+ elif response == bt_no:
+ self.on_file_new_project(use_thread=True, silenced=True)
+ else:
+ self.on_file_new_project(use_thread=True, silenced=True)
+
+ def on_file_new_project(self, cli=None, reset_tcl=True, use_thread=None, silenced=None, keep_scripts=True):
+ """
+ Returns the application to its startup state. This method is thread-safe.
+
+ :param cli: Boolean. If True this method was run from command line
+ :param reset_tcl: Boolean. If False, on new project creation the Tcl instance is not recreated, therefore it
+ will remember all the previous variables. If True then the Tcl is re-instantiated.
+ :param use_thread: Bool. If True some part of the initialization are done threaded
+ :param silenced: Bool or None. If True then the app will not ask to save the current parameters.
+ :param keep_scripts: Bool. If True the Script objects are not deleted when creating a new project
+ :return: None
+ """
+
+ self.log.debug("on_file_new_project()")
+
+ t_start_proj = time.time()
+
+ # close any editor that might be open
+ if self.app.call_source != 'app':
+ self.app.editor2object(cleanup=True)
+ # ## EDITOR section
+ self.app.geo_editor = AppGeoEditor(self.app)
+ self.app.exc_editor = AppExcEditor(self.app)
+ self.app.grb_editor = AppGerberEditor(self.app)
+
+ for obj in self.app.collection.get_list():
+ # delete shapes left drawn from mark shape_collections, if any
+ if isinstance(obj, GerberObject):
+ try:
+ obj.mark_shapes_storage.clear()
+ obj.mark_shapes.clear(update=True)
+ obj.mark_shapes.enabled = False
+ except AttributeError:
+ pass
+
+ # also delete annotation shapes, if any
+ elif isinstance(obj, CNCJobObject):
+ try:
+ obj.text_col.enabled = False
+ del obj.text_col
+ obj.annotation.clear(update=True)
+ del obj.annotation
+ except AttributeError:
+ pass
+
+ # delete the exclusion areas
+ self.app.exc_areas.clear_shapes()
+
+ # delete any selection shape on canvas
+ self.app.delete_selection_shape()
+
+ # delete all App objects
+ if keep_scripts is True:
+ for prj_obj in self.app.collection.get_list():
+ if prj_obj.kind != 'script':
+ self.app.collection.delete_by_name(prj_obj.obj_options['name'], select_project=False)
+ 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()
+
+ # Clear project filename
+ self.app.project_filename = None
+
+ default_file = self.app.defaults_path()
+ # Load the application options
+ self.options.load(filename=default_file, inform=self.inform)
+
+ # Re-fresh project options
+ self.app.on_defaults2options()
+
+ if use_thread is True:
+ self.app.new_project_signal.emit()
+ else:
+ t0 = time.time()
+ # Clear pool
+ self.app.clear_pool()
+
+ # Init FlatCAMTools
+ if reset_tcl is True:
+ 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()
+
+ # Try to close all tabs in the PlotArea but only if the appGUI is active (CLI is None)
+ if cli is None:
+ # we need to go in reverse because once we remove a tab then the index changes
+ # meaning that removing the first tab (idx = 0) then the tab at former idx = 1 will assume idx = 0
+ # and so on. Therefore the deletion should be done in reverse
+ wdg_count = self.app.ui.plot_tab_area.tabBar.count() - 1
+ for index in range(wdg_count, -1, -1):
+ try:
+ self.app.ui.plot_tab_area.closeTab(index)
+ except Exception as e:
+ self.log.error("App.on_file_new_project() --> %s" % str(e))
+
+ # # And then add again the Plot Area
+ self.app.ui.plot_tab_area.insertTab(0, self.app.ui.plot_tab, _("Plot Area"))
+ self.app.ui.plot_tab_area.protectTab(0)
+
+ # 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() - t_start_proj), _("seconds")))
+ self.app.ui.set_ui_title(name=_("New Project - Not saved"))
+
+ self.inform.emit('[success] %s...' % _("New Project created"))
+
+ def on_new_project_house_keeping(self):
+ """
+ Do dome of the new project initialization in a threaded way
+
+ :return:
+ :rtype:
+ """
+ t0 = time.time()
+
+ # Clear pool
+ self.log.debug("New Project: cleaning multiprocessing pool.")
+ self.app.clear_pool()
+
+ # 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):
+ """
+ Will create a new script file and open it in the Code Editor
+
+ :param silent: if True will not display status messages
+ :return: None
+ """
+ self.log.debug("on_filenewscript()")
+
+ if silent is False:
+ self.inform.emit('[success] %s' % _("New TCL script file created in Code Editor."))
+
+ # hide coordinates toolbars in the infobar while in DB
+ self.app.ui.coords_toolbar.hide()
+ self.app.ui.delta_coords_toolbar.hide()
+
+ self.app.app_obj.new_script_object()
+
+ def on_fileopenscript(self, name=None, silent=False):
+ """
+ Will open a Tcl script file into the Code Editor
+
+ :param silent: if True will not display status messages
+ :param name: name of a Tcl script file to open
+ :return: None
+ """
+
+ self.log.debug("on_fileopenscript()")
+
+ _filter_ = "TCL script .FlatScript (*.FlatScript);;TCL script .tcl (*.TCL);;TCL script .txt (*.TXT);;" \
+ "All Files (*.*)"
+
+ if name:
+ filenames = [name]
+ else:
+ try:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(
+ caption=_("Open TCL script"), directory=self.app.get_last_folder(), filter=_filter_)
+ except TypeError:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open TCL script"), filter=_filter_)
+
+ if len(filenames) == 0:
+ if silent is False:
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ else:
+ for filename in filenames:
+ if filename != '':
+ self.worker_task.emit({'fcn': self.open_script, 'params': [filename]})
+
+ def on_fileopenscript_example(self, name=None, silent=False):
+ """
+ Will open a Tcl script file into the Code Editor
+
+ :param silent: if True will not display status messages
+ :param name: name of a Tcl script file to open
+ :return:
+ """
+
+ self.log.debug("on_fileopenscript_example()")
+
+ _filter_ = "TCL script .FlatScript (*.FlatScript);;TCL script .tcl (*.TCL);;TCL script .txt (*.TXT);;" \
+ "All Files (*.*)"
+
+ # test if the app was frozen and choose the path for the configuration file
+ if getattr(sys, "frozen", False) is True:
+ example_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + '\\assets\\examples'
+ else:
+ example_path = os.path.dirname(os.path.realpath(__file__)) + '\\assets\\examples'
+
+ if name:
+ filenames = [name]
+ else:
+ try:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(
+ caption=_("Open TCL script"), directory=example_path, filter=_filter_)
+ except TypeError:
+ filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open TCL script"), filter=_filter_)
+
+ if len(filenames) == 0:
+ if silent is False:
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ else:
+ for filename in filenames:
+ if filename != '':
+ self.worker_task.emit({'fcn': self.open_script, 'params': [filename]})
+
+ def on_filerunscript(self, name=None, silent=False):
+ """
+ File menu callback for loading and running a TCL script.
+
+ :param silent: if True will not display status messages
+ :param name: name of a Tcl script file to be run by FlatCAM
+ :return: None
+ """
+
+ self.log.debug("on_file_runscript()")
+
+ if name:
+ filename = name
+ if self.app.cmd_line_headless != 1:
+ self.splash.showMessage('%s: %ssec\n%s' %
+ (_("Canvas initialization started.\n"
+ "Canvas initialization finished in"), '%.2f' % self.app.used_time,
+ _("Executing ScriptObject file.")
+ ),
+ alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
+ color=QtGui.QColor("lightgray"))
+ else:
+ _filter_ = "TCL script .FlatScript (*.FlatScript);;TCL script .tcl (*.TCL);;TCL script .txt (*.TXT);;" \
+ "All Files (*.*)"
+ try:
+ filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Run TCL script"),
+ directory=self.app.get_last_folder(),
+ filter=_filter_)
+ except TypeError:
+ filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Run TCL script"), filter=_filter_)
+
+ # The Qt methods above will return a QString which can cause problems later.
+ # So far json.dump() will fail to serialize it.
+ filename = str(filename)
+
+ if filename == "":
+ if silent is False:
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ else:
+ if self.app.cmd_line_headless != 1:
+ if self.app.ui.shell_dock.isHidden():
+ self.app.ui.shell_dock.show()
+
+ try:
+ with open(filename, "r") as tcl_script:
+ cmd_line_shellfile_content = tcl_script.read()
+ if self.app.cmd_line_headless != 1:
+ self.app.shell.exec_command(cmd_line_shellfile_content)
+ else:
+ self.app.shell.exec_command(cmd_line_shellfile_content, no_echo=True)
+
+ if silent is False:
+ self.inform.emit('[success] %s' % _("TCL script file opened in Code Editor and executed."))
+ except Exception as e:
+ self.app.error("App.on_filerunscript() -> %s" % str(e))
+ sys.exit(2)
+
+ def on_file_saveproject(self, silent=False):
+ """
+ Callback for menu item File->Save Project. Saves the project to
+ ``self.project_filename`` or calls ``self.on_file_saveprojectas()``
+ if set to None. The project is saved by calling ``self.save_project()``.
+
+ :param silent: if True will not display status messages
+ :return: None
+ """
+ self.log.debug("on_file_saveproject()")
+
+ if self.app.project_filename is None:
+ self.on_file_saveprojectas()
+ else:
+ self.worker_task.emit({'fcn': self.save_project, 'params': [self.app.project_filename, silent]})
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("project", self.app.project_filename)
+ self.app.file_saved.emit("project", self.app.project_filename)
+
+ self.app.ui.set_ui_title(name=self.app.project_filename)
+
+ self.app.should_we_save = False
+
+ def on_file_saveprojectas(self, make_copy=False, use_thread=True, quit_action=False):
+ """
+ Callback for menu item File->Save Project As... Opens a file
+ chooser and saves the project to the given file via
+ ``self.save_project()``.
+
+ :param make_copy if to be create a copy of the project; boolean
+ :param use_thread: if to be run in a separate thread; boolean
+ :param quit_action: if to be followed by quiting the application; boolean
+ :return: None
+ """
+ self.log.debug("on_file_saveprojectas()")
+
+ date = str(datetime.today()).rpartition('.')[0]
+ date = ''.join(c for c in date if c not in ':-')
+ date = date.replace(' ', '_')
+
+ filter_ = "FlatCAM Project .FlatPrj (*.FlatPrj);; All Files (*.*)"
+ try:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Save Project As ..."),
+ directory='{l_save}/{proj}_{date}'.format(l_save=str(self.app.get_last_save_folder()), date=date,
+ proj=_("Project")),
+ ext_filter=filter_
+ )
+ except TypeError:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Save Project As ..."),
+ ext_filter=filter_)
+
+ filename = str(filename)
+
+ if filename == '':
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ return
+
+ if use_thread is True:
+ self.worker_task.emit({'fcn': self.save_project, 'params': [filename, quit_action]})
+ else:
+ self.save_project(filename, quit_action)
+
+ # self.save_project(filename)
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("project", filename)
+ self.app.file_saved.emit("project", filename)
+
+ if not make_copy:
+ self.app.project_filename = filename
+
+ self.app.ui.set_ui_title(name=self.app.project_filename)
+ self.app.should_we_save = False
+
+ def on_file_save_objects_pdf(self, use_thread=True):
+ self.log.debug("on_file_save_objects_pdf()")
+
+ date = str(datetime.today()).rpartition('.')[0]
+ date = ''.join(c for c in date if c not in ':-')
+ date = date.replace(' ', '_')
+
+ try:
+ obj_selection = self.app.collection.get_selected()
+ if len(obj_selection) == 1:
+ obj_name = str(obj_selection[0].obj_options['name'])
+ else:
+ obj_name = _("General_print")
+ except AttributeError as att_err:
+ self.log.debug("App.on_file_save_object_pdf() --> %s" % str(att_err))
+ self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
+ return
+
+ if not obj_selection:
+ self.inform.emit(
+ '[WARNING_NOTCL] %s %s' % (_("No object is selected."), _("Print everything in the workspace.")))
+ obj_selection = self.app.collection.get_list()
+
+ filter_ = "PDF File .pdf (*.PDF);; All Files (*.*)"
+ try:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Save Object as PDF ..."),
+ directory='{l_save}/{obj_name}_{date}'.format(l_save=str(self.app.get_last_save_folder()),
+ obj_name=obj_name,
+ date=date),
+ ext_filter=filter_
+ )
+ except TypeError:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Save Object as PDF ..."),
+ ext_filter=filter_)
+
+ filename = str(filename)
+
+ if filename == '':
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ return
+
+ if use_thread is True:
+ self.app.proc_container.new(_("Printing PDF ..."))
+ self.worker_task.emit({'fcn': self.save_pdf, 'params': [filename, obj_selection]})
+ else:
+ self.save_pdf(filename, obj_selection)
+
+ # self.save_project(filename)
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("pdf", filename)
+ self.app.file_saved.emit("pdf", filename)
+
+ def save_pdf(self, file_name, obj_selection):
+ self.log.debug("save_pdf()")
+
+ p_size = self.options['global_workspaceT']
+ orientation = self.options['global_workspace_orientation']
+ color = 'black'
+ transparency_level = 1.0
+
+ self.pagesize.update(
+ {
+ 'Bounds': None,
+ 'A0': (841 * mm, 1189 * mm),
+ 'A1': (594 * mm, 841 * mm),
+ 'A2': (420 * mm, 594 * mm),
+ 'A3': (297 * mm, 420 * mm),
+ 'A4': (210 * mm, 297 * mm),
+ 'A5': (148 * mm, 210 * mm),
+ 'A6': (105 * mm, 148 * mm),
+ 'A7': (74 * mm, 105 * mm),
+ 'A8': (52 * mm, 74 * mm),
+ 'A9': (37 * mm, 52 * mm),
+ 'A10': (26 * mm, 37 * mm),
+
+ 'B0': (1000 * mm, 1414 * mm),
+ 'B1': (707 * mm, 1000 * mm),
+ 'B2': (500 * mm, 707 * mm),
+ 'B3': (353 * mm, 500 * mm),
+ 'B4': (250 * mm, 353 * mm),
+ 'B5': (176 * mm, 250 * mm),
+ 'B6': (125 * mm, 176 * mm),
+ 'B7': (88 * mm, 125 * mm),
+ 'B8': (62 * mm, 88 * mm),
+ 'B9': (44 * mm, 62 * mm),
+ 'B10': (31 * mm, 44 * mm),
+
+ 'C0': (917 * mm, 1297 * mm),
+ 'C1': (648 * mm, 917 * mm),
+ 'C2': (458 * mm, 648 * mm),
+ 'C3': (324 * mm, 458 * mm),
+ 'C4': (229 * mm, 324 * mm),
+ 'C5': (162 * mm, 229 * mm),
+ 'C6': (114 * mm, 162 * mm),
+ 'C7': (81 * mm, 114 * mm),
+ 'C8': (57 * mm, 81 * mm),
+ 'C9': (40 * mm, 57 * mm),
+ 'C10': (28 * mm, 40 * mm),
+
+ # American paper sizes
+ 'LETTER': (8.5 * inch, 11 * inch),
+ 'LEGAL': (8.5 * inch, 14 * inch),
+ 'ELEVENSEVENTEEN': (11 * inch, 17 * inch),
+
+ # From https://en.wikipedia.org/wiki/Paper_size
+ 'JUNIOR_LEGAL': (5 * inch, 8 * inch),
+ 'HALF_LETTER': (5.5 * inch, 8 * inch),
+ 'GOV_LETTER': (8 * inch, 10.5 * inch),
+ 'GOV_LEGAL': (8.5 * inch, 13 * inch),
+ 'LEDGER': (17 * inch, 11 * inch),
+ }
+ )
+
+ # make sure that the Excellon objeacts are drawn on top of everything
+ excellon_objs = [obj for obj in obj_selection if obj.kind == 'excellon']
+ cncjob_objs = [obj for obj in obj_selection if obj.kind == 'cncjob']
+ # reverse the object order such that the first selected is on top
+ rest_objs = [obj for obj in obj_selection if obj.kind != 'excellon' and obj.kind != 'cncjob'][::-1]
+ obj_selection = rest_objs + cncjob_objs + excellon_objs
+
+ # generate the SVG files from the application objects
+ exported_svg = []
+ for obj in obj_selection:
+ svg_obj = obj.export_svg(scale_stroke_factor=0.0)
+
+ if obj.kind.lower() == 'gerber' or obj.kind.lower() == 'excellon':
+ color = obj.fill_color[:-2]
+ transparency_level = obj.fill_color[-2:]
+ elif obj.kind.lower() == 'geometry':
+ color = self.options["global_draw_color"]
+
+ # Change the attributes of the exported SVG
+ # We don't need stroke-width
+ # We set opacity to maximum
+ # We set the colour to WHITE
+
+ try:
+ root = ET.fromstring(svg_obj)
+ except Exception as e:
+ self.log.debug("AppIO.save_pdf() -> Missing root node -> %s" % str(e))
+ self.app.inform.emit("[ERROR_NOTCL] %s" % _("Failed."))
+ return
+
+ for child in root:
+ child.set('fill', str(color))
+ child.set('opacity', str(transparency_level))
+ child.set('stroke', str(color))
+
+ exported_svg.append(ET.tostring(root))
+
+ xmin = Inf
+ ymin = Inf
+ xmax = -Inf
+ ymax = -Inf
+
+ for obj in obj_selection:
+ try:
+ gxmin, gymin, gxmax, gymax = obj.bounds()
+ xmin = min([xmin, gxmin])
+ ymin = min([ymin, gymin])
+ xmax = max([xmax, gxmax])
+ ymax = max([ymax, gymax])
+ except Exception as e:
+ self.log.error("Tried to get bounds of empty geometry in App.save_pdf(). %s" % str(e))
+
+ # Determine bounding area for svg export
+ bounds = [xmin, ymin, xmax, ymax]
+ size = bounds[2] - bounds[0], bounds[3] - bounds[1]
+
+ # This contain the measure units
+ uom = obj_selection[0].units.lower()
+
+ # Define a boundary around SVG of about 1.0mm (~39mils)
+ if uom in "mm":
+ boundary = 1.0
+ else:
+ boundary = 0.0393701
+
+ # Convert everything to strings for use in the xml doc
+ svgwidth = str(size[0] + (2 * boundary))
+ svgheight = str(size[1] + (2 * boundary))
+ minx = str(bounds[0] - boundary)
+ miny = str(bounds[1] + boundary + size[1])
+
+ # Add a SVG Header and footer to the svg output from shapely
+ # The transform flips the Y Axis so that everything renders
+ # properly within svg apps such as inkscape
+ svg_header = ''
+
+ svg_elem = str(svg_header)
+ for svg_item in exported_svg:
+ svg_elem += str(svg_item)
+ svg_elem += str(svg_footer)
+
+ # Parse the xml through a xml parser just to add line feeds
+ # and to make it look more pretty for the output
+ doc = parse_xml_string(svg_elem)
+ doc_final = doc.toprettyxml()
+
+ try:
+ if self.app_units.upper() == 'IN':
+ unit = inch
+ else:
+ unit = mm
+
+ doc_final = StringIO(doc_final)
+ drawing = svg2rlg(doc_final)
+
+ if p_size == 'Bounds':
+ renderPDF.drawToFile(drawing, file_name)
+ else:
+ if orientation == 'p':
+ page_size = portrait(self.pagesize[p_size])
+ else:
+ page_size = landscape(self.pagesize[p_size])
+
+ my_canvas = canvas.Canvas(file_name, pagesize=page_size)
+ my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
+ renderPDF.draw(drawing, my_canvas, 0, 0)
+ my_canvas.save()
+ except Exception as e:
+ self.log.error("AppIO.save_pdf() --> PDF output --> %s" % str(e))
+ return 'fail'
+
+ self.inform.emit('[success] %s: %s' % (_("PDF file saved to"), file_name))
+
+ def export_svg(self, obj_name, filename, scale_stroke_factor=0.00):
+ """
+ Exports a Geometry Object to an SVG file.
+
+ :param obj_name: the name of the FlatCAM object to be saved as SVG
+ :param filename: Path to the SVG file to save to.
+ :param scale_stroke_factor: factor by which to change/scale the thickness of the features
+ :return:
+ """
+ if filename is None:
+ filename = self.app.options["global_last_save_folder"] if \
+ self.app.options["global_last_save_folder"] is not None else self.app.options["global_last_folder"]
+
+ self.log.debug("export_svg()")
+
+ try:
+ obj = self.app.collection.get_by_name(str(obj_name))
+ except Exception:
+ return 'fail'
+
+ with self.app.proc_container.new(_("Exporting ...")):
+ exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor)
+
+ # Determine bounding area for svg export
+ bounds = obj.bounds()
+ size = obj.size()
+
+ # Convert everything to strings for use in the xml doc
+ svgwidth = str(size[0])
+ svgheight = str(size[1])
+ minx = str(bounds[0])
+ miny = str(bounds[1] - size[1])
+ uom = obj.units.lower()
+
+ # Add a SVG Header and footer to the svg output from shapely
+ # The transform flips the Y Axis so that everything renders
+ # properly within svg apps such as inkscape
+ svg_header = ''
+ svg_elem = svg_header + exported_svg + svg_footer
+
+ # Parse the xml through a xml parser just to add line feeds
+ # and to make it look more pretty for the output
+ svgcode = parse_xml_string(svg_elem)
+ svgcode = svgcode.toprettyxml()
+
+ try:
+ with open(filename, 'w') as fp:
+ fp.write(svgcode)
+ except PermissionError:
+ self.inform.emit('[WARNING] %s' %
+ _("Permission denied, saving not possible.\n"
+ "Most likely another app is holding the file open and not accessible."))
+ return 'fail'
+
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("SVG", filename)
+ self.app.file_saved.emit("SVG", filename)
+ self.inform.emit('[success] %s: %s' % (_("SVG file exported to"), filename))
+
+ def on_import_preferences(self):
+ """
+ Loads the application default settings from a saved file into
+ ``self.options`` dictionary.
+
+ :return: None
+ """
+
+ self.log.debug("App.on_import_preferences()")
+
+ # Show file chooser
+ filter_ = "Config File (*.FlatConfig);;All Files (*.*)"
+ try:
+ filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Preferences"),
+ directory=self.app.data_path,
+ filter=filter_)
+ except TypeError:
+ filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Preferences"),
+ filter=filter_)
+ filename = str(filename)
+ if filename == "":
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ return
+
+ # Load in the options from the chosen file
+ self.options.load(filename=filename, inform=self.inform)
+
+ self.app.preferencesUiManager.on_preferences_edited()
+ self.inform.emit('[success] %s: %s' % (_("Imported Defaults from"), filename))
+
+ def on_export_preferences(self):
+ """
+ Save the options dictionary to a file.
+
+ :return: None
+ """
+ self.log.debug("on_export_preferences()")
+
+ # defaults_file_content = None
+
+ # Show file chooser
+ date = str(datetime.today()).rpartition('.')[0]
+ date = ''.join(c for c in date if c not in ':-')
+ date = date.replace(' ', '_')
+ filter__ = "Config File .FlatConfig (*.FlatConfig);;All Files (*.*)"
+ try:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Export FlatCAM Preferences"),
+ directory=os.path.join(self.app.data_path, 'preferences_%s' % date),
+ ext_filter=filter__
+ )
+ except TypeError:
+ filename, _f = FCFileSaveDialog.get_saved_filename(
+ caption=_("Export FlatCAM Preferences"), ext_filter=filter__)
+ filename = str(filename)
+ if filename == "":
+ self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+ return 'fail'
+
+ # Update options
+ self.app.preferencesUiManager.defaults_read_form()
+ self.options.propagate_defaults()
+
+ # Save update options
+ try:
+ self.options.write(filename=filename)
+ except Exception:
+ self.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed to write defaults to file."), str(filename)))
+ return
+
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("preferences", filename)
+ self.app.file_saved.emit("preferences", filename)
+ self.inform.emit('[success] %s: %s' % (_("Exported preferences to"), filename))
+
+ def export_excellon(self, obj_name, filename, local_use=None, use_thread=True):
+ """
+ Exports a Excellon Object to an Excellon file.
+
+ :param obj_name: the name of the FlatCAM object to be saved as Excellon
+ :param filename: Path to the Excellon file to save to.
+ :param local_use:
+ :param use_thread: if to be run in a separate thread
+ :return:
+ """
+
+ if filename is None:
+ if self.app.options["global_last_save_folder"]:
+ filename = self.app.options["global_last_save_folder"] + '/' + 'exported_excellon'
+ else:
+ filename = self.app.options["global_last_folder"] + '/' + 'exported_excellon'
+
+ self.log.debug("export_excellon()")
+
+ format_exc = ';FILE_FORMAT=%d:%d\n' % (self.options["excellon_exp_integer"],
+ self.options["excellon_exp_decimals"]
+ )
+
+ if local_use is None:
+ try:
+ obj = self.app.collection.get_by_name(str(obj_name))
+ except Exception:
+ return "Could not retrieve object: %s" % obj_name
+ else:
+ obj = local_use
+
+ if not isinstance(obj, ExcellonObject):
+ self.inform.emit('[ERROR_NOTCL] %s' %
+ _("Failed. Only Excellon objects can be saved as Excellon files..."))
+ return
+
+ # updated units
+ eunits = self.options["excellon_exp_units"]
+ ewhole = self.options["excellon_exp_integer"]
+ efract = self.options["excellon_exp_decimals"]
+ ezeros = self.options["excellon_exp_zeros"]
+ eformat = self.options["excellon_exp_format"]
+ slot_type = self.options["excellon_exp_slot_type"]
+
+ fc_units = self.app_units.upper()
+ if fc_units == 'MM':
+ factor = 1 if eunits == 'METRIC' else 0.03937
+ else:
+ factor = 25.4 if eunits == 'METRIC' else 1
+
+ def make_excellon():
+ try:
+ time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
+
+ header = 'M48\n'
+ header += ';EXCELLON GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
+ (str(self.app.version), str(self.app.version_date))
+
+ header += ';Filename: %s' % str(obj_name) + '\n'
+ header += ';Created on : %s' % time_str + '\n'
+
+ if eformat == 'dec':
+ has_slots, excellon_code = obj.export_excellon(ewhole, efract, factor=factor, slot_type=slot_type)
+ header += eunits + '\n'
+
+ for tool in obj.tools:
+ if eunits == 'METRIC':
+ header += "T{tool}F00S00C{:.{dec}f}\n".format(float(obj.tools[tool]['tooldia']) * factor,
+ tool=str(tool),
+ dec=2)
+ else:
+ header += "T{tool}F00S00C{:.{dec}f}\n".format(float(obj.tools[tool]['tooldia']) * factor,
+ tool=str(tool),
+ dec=4)
+ else:
+ if ezeros == 'LZ':
+ has_slots, excellon_code = obj.export_excellon(ewhole, efract,
+ form='ndec', e_zeros='LZ', factor=factor,
+ slot_type=slot_type)
+ header += '%s,%s\n' % (eunits, 'LZ')
+ header += format_exc
+
+ for tool in obj.tools:
+ if eunits == 'METRIC':
+ header += "T{tool}F00S00C{:.{dec}f}\n".format(
+ float(obj.tools[tool]['tooldia']) * factor,
+ tool=str(tool),
+ dec=2)
+ else:
+ header += "T{tool}F00S00C{:.{dec}f}\n".format(
+ float(obj.tools[tool]['tooldia']) * factor,
+ tool=str(tool),
+ dec=4)
+ else:
+ has_slots, excellon_code = obj.export_excellon(ewhole, efract,
+ form='ndec', e_zeros='TZ', factor=factor,
+ slot_type=slot_type)
+ header += '%s,%s\n' % (eunits, 'TZ')
+ header += format_exc
+
+ for tool in obj.tools:
+ if eunits == 'METRIC':
+ header += "T{tool}F00S00C{:.{dec}f}\n".format(
+ float(obj.tools[tool]['tooldia']) * factor,
+ tool=str(tool),
+ dec=2)
+ else:
+ header += "T{tool}F00S00C{:.{dec}f}\n".format(
+ float(obj.tools[tool]['tooldia']) * factor,
+ tool=str(tool),
+ dec=4)
+ header += '%\n'
+ footer = 'M30\n'
+
+ exported_excellon = header
+ exported_excellon += excellon_code
+ exported_excellon += footer
+
+ if local_use is None:
+ try:
+ with open(filename, 'w') as fp:
+ fp.write(exported_excellon)
+ except PermissionError:
+ self.inform.emit('[WARNING] %s' %
+ _("Permission denied, saving not possible.\n"
+ "Most likely another app is holding the file open and not accessible."))
+ return 'fail'
+
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("Excellon", filename)
+ self.app.file_saved.emit("Excellon", filename)
+ self.inform.emit('[success] %s: %s' % (_("Excellon file exported to"), filename))
+ else:
+ return exported_excellon
+ except Exception as e:
+ self.log.error("App.export_excellon.make_excellon() --> %s" % str(e))
+ return 'fail'
+
+ if use_thread is True:
+
+ with self.app.proc_container.new(_("Exporting ...")):
+
+ def job_thread_exc(app_obj):
+ ret = make_excellon()
+ if ret == 'fail':
+ app_obj.inform.emit('[ERROR_NOTCL] %s' % _('Could not export.'))
+ return
+
+ self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]})
+ else:
+ eret = make_excellon()
+ if eret == 'fail':
+ self.inform.emit('[ERROR_NOTCL] %s' % _('Could not export.'))
+ return 'fail'
+ if local_use is not None:
+ return eret
+
+ def export_gerber(self, obj_name, filename, local_use=None, use_thread=True):
+ """
+ Exports a Gerber Object to an Gerber file.
+
+ :param obj_name: the name of the FlatCAM object to be saved as Gerber
+ :param filename: Path to the Gerber file to save to.
+ :param local_use: if the Gerber code is to be saved to a file (None) or used within FlatCAM.
+ When not None, the value will be the actual Gerber object for which to create
+ the Gerber code
+ :param use_thread: if to be run in a separate thread
+ :return:
+ """
+ if filename is None:
+ filename = self.app.options["global_last_save_folder"] if \
+ self.app.options["global_last_save_folder"] is not None else self.app.options["global_last_folder"]
+
+ self.log.debug("export_gerber()")
+
+ if local_use is None:
+ try:
+ obj = self.app.collection.get_by_name(str(obj_name))
+ except Exception:
+ return 'fail'
+ else:
+ obj = local_use
+
+ # updated units
+ gunits = self.options["gerber_exp_units"]
+ gwhole = self.options["gerber_exp_integer"]
+ gfract = self.options["gerber_exp_decimals"]
+ gzeros = self.options["gerber_exp_zeros"]
+
+ fc_units = self.app_units.upper()
+ if fc_units == 'MM':
+ factor = 1 if gunits == 'MM' else 0.03937
+ else:
+ factor = 25.4 if gunits == 'MM' else 1
+
+ def make_gerber():
+ try:
+ time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
+
+ header = 'G04*\n'
+ header += 'G04 RS-274X GERBER GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' % \
+ (str(self.app.version), str(self.app.version_date))
+
+ header += 'G04 Filename: %s*' % str(obj_name) + '\n'
+ header += 'G04 Created on : %s*' % time_str + '\n'
+ header += '%%FS%sAX%s%sY%s%s*%%\n' % (gzeros, gwhole, gfract, gwhole, gfract)
+ header += "%MO{units}*%\n".format(units=gunits)
+
+ for apid in obj.tools:
+ if obj.tools[apid]['type'] == 'C':
+ header += "%ADD{apid}{type},{size}*%\n".format(
+ apid=str(apid),
+ type='C',
+ size=(factor * obj.tools[apid]['size'])
+ )
+ elif obj.tools[apid]['type'] == 'R':
+ header += "%ADD{apid}{type},{width}X{height}*%\n".format(
+ apid=str(apid),
+ type='R',
+ width=(factor * obj.tools[apid]['width']),
+ height=(factor * obj.tools[apid]['height'])
+ )
+ elif obj.tools[apid]['type'] == 'O':
+ header += "%ADD{apid}{type},{width}X{height}*%\n".format(
+ apid=str(apid),
+ type='O',
+ width=(factor * obj.tools[apid]['width']),
+ height=(factor * obj.tools[apid]['height'])
+ )
+
+ header += '\n'
+
+ # obsolete units but some software may need it
+ if gunits == 'IN':
+ header += 'G70*\n'
+ else:
+ header += 'G71*\n'
+
+ # Absolute Mode
+ header += 'G90*\n'
+
+ header += 'G01*\n'
+ # positive polarity
+ header += '%LPD*%\n'
+
+ footer = 'M02*\n'
+
+ gerber_code = obj.export_gerber(gwhole, gfract, g_zeros=gzeros, factor=factor)
+
+ exported_gerber = header
+ exported_gerber += gerber_code
+ exported_gerber += footer
+
+ if local_use is None:
+ try:
+ with open(filename, 'w') as fp:
+ fp.write(exported_gerber)
+ except PermissionError:
+ self.inform.emit('[WARNING] %s' %
+ _("Permission denied, saving not possible.\n"
+ "Most likely another app is holding the file open and not accessible."))
+ return 'fail'
+
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("Gerber", filename)
+ self.app.file_saved.emit("Gerber", filename)
+ self.inform.emit('[success] %s: %s' % (_("Gerber file exported to"), filename))
+ else:
+ return exported_gerber
+ except Exception as e:
+ self.log.error("App.export_gerber.make_gerber() --> %s" % str(e))
+ return 'fail'
+
+ if use_thread is True:
+ with self.app.proc_container.new(_("Exporting ...")):
+
+ def job_thread_grb(app_obj):
+ ret = make_gerber()
+ if ret == 'fail':
+ app_obj.inform.emit('[ERROR_NOTCL] %s' % _('Could not export.'))
+ return 'fail'
+
+ self.worker_task.emit({'fcn': job_thread_grb, 'params': [self]})
+ else:
+ gret = make_gerber()
+ if gret == 'fail':
+ self.inform.emit('[ERROR_NOTCL] %s' % _('Could not export.'))
+ return 'fail'
+ if local_use is not None:
+ return gret
+
+ def export_dxf(self, obj_name, filename, local_use=None, use_thread=True):
+ """
+ Exports a Geometry Object to an DXF file.
+
+ :param obj_name: the name of the FlatCAM object to be saved as DXF
+ :param filename: Path to the DXF file to save to.
+ :param local_use: if the Gerber code is to be saved to a file (None) or used within FlatCAM.
+ When not None, the value will be the actual Geometry object for which to create
+ the Geometry/DXF code
+ :param use_thread: if to be run in a separate thread
+ :return:
+ """
+ if filename is None:
+ filename = self.app.options["global_last_save_folder"] if \
+ self.app.options["global_last_save_folder"] is not None else self.app.options["global_last_folder"]
+
+ self.log.debug("export_dxf()")
+
+ if local_use is None:
+ try:
+ obj = self.app.collection.get_by_name(str(obj_name))
+ except Exception:
+ return 'fail'
+ else:
+ obj = local_use
+
+ def make_dxf():
+ try:
+ dxf_code = obj.export_dxf()
+ if local_use is None:
+ try:
+ dxf_code.saveas(filename)
+ except PermissionError:
+ self.inform.emit('[WARNING] %s' %
+ _("Permission denied, saving not possible.\n"
+ "Most likely another app is holding the file open and not accessible."))
+ return 'fail'
+
+ if self.options["global_open_style"] is False:
+ self.app.file_opened.emit("DXF", filename)
+ self.app.file_saved.emit("DXF", filename)
+ self.inform.emit('[success] %s: %s' % (_("DXF file exported to"), filename))
+ else:
+ return dxf_code
+ except Exception as e:
+ self.log.error("App.export_dxf.make_dxf() --> %s" % str(e))
+ return 'fail'
+
+ if use_thread is True:
+
+ with self.app.proc_container.new(_("Exporting ...")):
+
+ def job_thread_exc(app_obj):
+ ret_dxf_val = make_dxf()
+ if ret_dxf_val == 'fail':
+ app_obj.inform.emit('[WARNING_NOTCL] %s' % _('Could not export.'))
+ return
+
+ self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]})
+ else:
+ ret = make_dxf()
+ if ret == 'fail':
+ self.inform.emit('[WARNING_NOTCL] %s' % _('Could not export.'))
+ return
+ if local_use is not None:
+ return ret
+
+ def import_svg(self, filename, geo_type='geometry', outname=None, plot=True):
+ """
+ Adds a new Geometry Object to the projects and populates
+ it with shapes extracted from the SVG file.
+
+ :param plot: If True then the resulting object will be plotted on canvas
+ :param filename: Path to the SVG file.
+ :param geo_type: Type of FlatCAM object that will be created from SVG
+ :param outname: The name given to the resulting FlatCAM object
+ :return:
+ """
+ self.log.debug("App.import_svg()")
+ if not os.path.exists(filename):
+ self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
+ return
+
+ obj_type = ""
+ if geo_type is None or geo_type == "geometry":
+ obj_type = "geometry"
+ elif geo_type == "gerber":
+ obj_type = "gerber"
+ else:
+ self.inform.emit('[ERROR_NOTCL] %s' %
+ _("Not supported type is picked as parameter. Only Geometry and Gerber are supported"))
+ return
+
+ units = self.app_units.upper()
+
+ def obj_init(geo_obj, app_obj):
+ res = geo_obj.import_svg(filename, obj_type, units=units)
+ if res == 'fail':
+ return 'fail'
+
+ geo_obj.multigeo = True
+
+ with open(filename) as f:
+ file_content = f.read()
+ geo_obj.source_file = file_content
+
+ # appGUI feedback
+ app_obj.inform.emit('[success] %s: %s' % (_("Opened"), filename))
+
+ with self.app.proc_container.new('%s ...' % _("Importing")):
+
+ # Object name
+ name = outname or filename.split('/')[-1].split('\\')[-1]
+
+ ret = self.app.app_obj.new_object(obj_type, name, obj_init, autoselected=False, plot=plot)
+
+ if ret == 'fail':
+ self.inform.emit('[ERROR_NOTCL]%s' % _('Import failed.'))
+ return 'fail'
+
+ # Register recent file
+ self.app.file_opened.emit("svg", filename)
+
+ def import_dxf(self, filename, geo_type='geometry', outname=None, plot=True):
+ """
+ Adds a new Geometry Object to the projects and populates
+ it with shapes extracted from the DXF file.
+
+ :param filename: Path to the DXF file.
+ :param geo_type: Type of FlatCAM object that will be created from DXF
+ :param outname: Name for the imported Geometry
+ :param plot: If True then the resulting object will be plotted on canvas
+ :return:
+ """
+ self.log.debug(" ********* Importing DXF as: %s ********* " % geo_type.capitalize())
+ if not os.path.exists(filename):
+ self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
+ return
+
+ obj_type = ""
+ if geo_type is None or geo_type == "geometry":
+ obj_type = "geometry"
+ elif geo_type == "gerber":
+ obj_type = geo_type
+ else:
+ self.inform.emit('[ERROR_NOTCL] %s' %
+ _("Not supported type is picked as parameter. Only Geometry and Gerber are supported"))
+ return
+
+ units = self.app_units.upper()
+
+ def obj_init(geo_obj, app_obj):
+ if obj_type == "geometry":
+ geo_obj.import_dxf_as_geo(filename, units=units)
+ elif obj_type == "gerber":
+ geo_obj.import_dxf_as_gerber(filename, units=units)
+ else:
+ return "fail"
+
+ with open(filename) as f:
+ file_content = f.read()
+ geo_obj.source_file = file_content
+
+ # appGUI feedback
+ app_obj.inform.emit('[success] %s: %s' % (_("Opened"), filename))
+
+ with self.app.proc_container.new('%s ...' % _("Importing")):
+
+ # Object name
+ name = outname or filename.split('/')[-1].split('\\')[-1]
+
+ ret = self.app.app_obj.new_object(obj_type, name, obj_init, autoselected=False, plot=plot)
+
+ if ret == 'fail':
+ self.inform.emit('[ERROR_NOTCL]%s' % _('Import failed.'))
+ return 'fail'
+
+ # Register recent file
+ self.app.file_opened.emit("dxf", filename)
+
+ def import_pdf(self, filename):
+ self.app.pdf_tool.periodic_check(1000)
+ self.worker_task.emit({'fcn': self.app.pdf_tool.open_pdf, 'params': [filename]})
+
+ def open_gerber(self, filename, outname=None, plot=True, from_tcl=False):
+ """
+ Opens a Gerber file, parses it and creates a new object for
+ it in the program. Thread-safe.
+
+ :param outname: Name of the resulting object. None causes the
+ name to be that of the file. Str.
+ :param filename: Gerber file filename
+ :type filename: str
+ :param plot: boolean, to plot or not the resulting object
+ :param from_tcl: True if run from Tcl Shell
+ :return: None
+ """
+
+ # How the object should be initialized
+ def obj_init(gerber_obj, app_obj):
+
+ assert isinstance(gerber_obj, GerberObject), \
+ "Expected to initialize a GerberObject but got %s" % type(gerber_obj)
+
+ # Opening the file happens here
+ try:
+ parse_ret_val = gerber_obj.parse_file(filename)
+ except IOError:
+ app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open file"), filename))
+ return "fail"
+ except ParseError as parse_err:
+ app_obj.inform.emit('[ERROR_NOTCL] %s: %s. %s' % (_("Failed to parse file"), filename, str(parse_err)))
+ app_obj.log.error(str(parse_err))
+ return "fail"
+ except Exception as e:
+ app_obj.log.error("App.open_gerber() --> %s" % str(e))
+ msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
+ msg += traceback.format_exc()
+ app_obj.inform.emit(msg)
+ return "fail"
+
+ if gerber_obj.is_empty():
+ app_obj.inform.emit('[ERROR_NOTCL] %s' %
+ _("Object is not Gerber file or empty. Aborting object creation."))
+ return "fail"
+
+ if parse_ret_val:
+ return parse_ret_val
+
+ self.log.debug("open_gerber()")
+ if not os.path.exists(filename):
+ self.inform.emit('[ERROR_NOTCL] %s. %s' % (filename, _("File no longer available.")))
+ return
+
+ with self.app.proc_container.new('%s...' % _("Opening")):
+ # Object name
+ name = outname or filename.split('/')[-1].split('\\')[-1]
+
+ # # ## Object creation # ##
+ ret_val = self.app.app_obj.new_object("gerber", name, obj_init, autoselected=False, plot=plot)
+ if ret_val == 'fail':
+ if from_tcl:
+ filename = self.options['global_tcl_path'] + '/' + name
+ ret_val = self.app.app_obj.new_object("gerber", name, obj_init, autoselected=False, plot=plot)
+ if ret_val == 'fail':
+ self.inform.emit('[ERROR_NOTCL]%s' % _('Open Gerber failed. Probable not a Gerber file.'))
+ return 'fail'
+
+ # Register recent file
+ self.app.file_opened.emit("gerber", filename)
+
+ # appGUI feedback
+ self.app.inform.emit('[success] %s: %s' % (_("Opened"), filename))
+
+ def open_excellon(self, filename, outname=None, plot=True, from_tcl=False):
+ """
+ Opens an Excellon file, parses it and creates a new object for
+ it in the program. Thread-safe.
+
+ :param outname: Name of the resulting object. None causes the name to be that of the file.
+ :param filename: Excellon file filename
+ :type filename: str
+ :param plot: boolean, to plot or not the resulting object
+ :param from_tcl: True if run from Tcl Shell
+ :return: None
+ """
+
+ self.log.debug("open_excellon()")
+
+ if not os.path.exists(filename):
+ self.inform.emit('[ERROR_NOTCL] %s. %s' % (filename, _("File no longer available.")))
+ return
+
+ # How the object should be initialized
+ def obj_init(excellon_obj, app_obj):
+ # populate excellon_obj.tools dict
+ try:
+ ret = excellon_obj.parse_file(filename=filename)
+ if ret == "fail":
+ app_obj.log.debug("Excellon parsing failed.")
+ self.inform.emit('[ERROR_NOTCL] %s' % _("This is not Excellon file."))
+ return "fail"
+ except IOError:
+ app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Cannot open file"), filename))
+ app_obj.log.debug("Could not open Excellon object.")
+ return "fail"
+ except Exception:
+ msg = '[ERROR_NOTCL] %s' % _("An internal error has occurred. See shell.\n")
+ msg += traceback.format_exc()
+ app_obj.inform.emit(msg)
+ return "fail"
+
+ # populate excellon_obj.solid_geometry list
+ ret = excellon_obj.create_geometry()
+ if ret == 'fail':
+ app_obj.log.debug("Could not create geometry for Excellon object.")
+ return "fail"
+
+ for tool in excellon_obj.tools:
+ if excellon_obj.tools[tool]['solid_geometry']:
+ return
+ app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("No geometry found in file"), filename))
+ return "fail"
+
+ with self.app.proc_container.new('%s...' % _("Opening")):
+ # Object name
+ name = outname or filename.split('/')[-1].split('\\')[-1]
+ ret_val = self.app.app_obj.new_object("excellon", name, obj_init, autoselected=False, plot=plot)
+ if ret_val == 'fail':
+ if from_tcl:
+ filename = self.options['global_tcl_path'] + '/' + name
+ ret_val = self.app.app_obj.new_object("excellon", name, obj_init, autoselected=False, plot=plot)
+ if ret_val == 'fail':
+ self.inform.emit('[ERROR_NOTCL] %s' %
+ _('Open Excellon file failed. Probable not an Excellon file.'))
+ return
+
+ # Register recent file
+ self.app.file_opened.emit("excellon", filename)
+
+ # appGUI feedback
+ self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
+
+ def open_gcode(self, filename, outname=None, force_parsing=None, plot=True, from_tcl=False):
+ """
+ Opens a G-gcode file, parses it and creates a new object for
+ it in the program. Thread-safe.
+
+ :param filename: G-code file filename
+ :param outname: Name of the resulting object. None causes the name to be that of the file.
+ :param force_parsing:
+ :param plot: If True plot the object on canvas
+ :param from_tcl: True if run from Tcl Shell
+ :return: None
+ """
+ self.log.debug("open_gcode()")
+
+ if not os.path.exists(filename):
+ self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
+ return
+
+ # How the object should be initialized
+ def obj_init(job_obj, app_obj_: "appMain.App"):
+ """
+ :param job_obj: the resulting object
+ :type app_obj_: App
+ """
+
+ app_obj_.inform.emit('%s...' % _("Reading GCode file")) # noqa
+ try:
+ f = open(filename)
+ gcode = f.read()
+ f.close()
+ except IOError:
+ app_obj_.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open"), filename)) # noqa
+ return "fail"
+
+ # try to find from what kind of object this GCode was created
+ gcode_origin = 'Geometry'
+ match = re.search(r'^.*Type:\s*.*(\bGeometry\b|\bExcellon\b)', gcode, re.MULTILINE)
+ if match:
+ gcode_origin = match.group(1)
+ job_obj.obj_options['type'] = gcode_origin
+ # add at least one default tool
+ if 'excellon' in gcode_origin.lower():
+ job_obj.tools = {1: {'data': {'tools_drill_ppname_e': 'default'}}}
+ if 'geometry' in gcode_origin.lower():
+ job_obj.tools = {1: {'data': {'tools_mill_ppname_g': 'default'}}}
+
+ # try to find from what kind of object this GCode was created
+ match = re.search(r'^.*Preprocessor:\s*.*\bGeometry\b|\bExcellon\b:\s(\b.*\b)', gcode, re.MULTILINE)
+ detected_preprocessor = 'default'
+ if match:
+ detected_preprocessor = match.group(1)
+ # determine if there is any tool data
+ match = re.findall(r'^.*Tool:\s*(\d*)\s*->\s*Dia:\s*(\d*\.?\d*)', gcode, re.MULTILINE)
+ if match:
+ job_obj.tools = {}
+ for m in match:
+ if 'excellon' in gcode_origin.lower():
+ job_obj.tools[int(m[0])] = {
+ 'tooldia': float(m[1]),
+ 'nr_drills': 0,
+ 'nr_slots': 0,
+ 'offset_z': 0,
+ 'data': {'tools_drill_ppname_e': detected_preprocessor}
+ }
+ # if 'geometry' in gcode_origin.lower():
+ # job_obj.tools[int(m[0])] = {
+ # 'tooldia': float(m[1]),
+ # 'data': {
+ # 'tools_mill_ppname_g': detected_preprocessor,
+ # 'tools_mill_offset_value': 0.0,
+ # 'tools_mill_job_type': _('Roughing'),
+ # 'tools_mill_tool_shape': "C1"
+ #
+ # }
+ # }
+ job_obj.used_tools = list(job_obj.tools.keys())
+ # determine if there is any Cut Z data
+ match = re.findall(r'^.*Tool:\s*(\d*)\s*->\s*Z_Cut:\s*([\-|+]?\d*\.?\d*)', gcode, re.MULTILINE)
+ if match:
+ for m in match:
+ if 'excellon' in gcode_origin.lower():
+ if int(m[0]) in job_obj.tools:
+ job_obj.tools[int(m[0])]['offset_z'] = 0.0
+ job_obj.tools[int(m[0])]['data']['tools_drill_cutz'] = float(m[1])
+ # if 'geometry' in gcode_origin.lower():
+ # if int(m[0]) in job_obj.tools:
+ # job_obj.tools[int(m[0])]['data']['tools_mill_cutz'] = float(m[1])
+
+ job_obj.gcode = gcode
+
+ gcode_ret = job_obj.gcode_parse(force_parsing=force_parsing)
+ if gcode_ret == "fail":
+ self.inform.emit('[ERROR_NOTCL] %s' % _("This is not GCODE"))
+ return "fail"
+
+ for k in job_obj.tools:
+ job_obj.tools[k]['gcode'] = gcode
+ job_obj.tools[k]['gcode_parsed'] = []
+
+ for k in job_obj.tools:
+ print(k, job_obj.tools[k])
+ job_obj.create_geometry()
+
+ with self.app.proc_container.new('%s...' % _("Opening")):
+
+ # Object name
+ name = outname or filename.split('/')[-1].split('\\')[-1]
+
+ # New object creation and file processing
+ ret_val = self.app.app_obj.new_object("cncjob", name, obj_init, autoselected=False, plot=plot)
+ if ret_val == 'fail':
+ if from_tcl:
+ filename = self.options['global_tcl_path'] + '/' + name
+ ret_val = self.app.app_obj.new_object("cncjob", name, obj_init, autoselected=False, plot=plot)
+ if ret_val == 'fail':
+ self.inform.emit('[ERROR_NOTCL] %s' %
+ _("Failed to create CNCJob Object. Probable not a GCode file. "
+ "Try to load it from File menu.\n "
+ "Attempting to create a FlatCAM CNCJob Object from "
+ "G-Code file failed during processing"))
+ return "fail"
+
+ # Register recent file
+ self.app.file_opened.emit("cncjob", filename)
+
+ # appGUI feedback
+ self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
+
+ def open_hpgl2(self, filename, outname=None):
+ """
+ Opens a HPGL2 file, parses it and creates a new object for
+ it in the program. Thread-safe.
+
+ :param outname: Name of the resulting object. None causes the name to be that of the file.
+ :param filename: HPGL2 file filename
+ :return: None
+ """
+ filename = filename
+
+ # How the object should be initialized
+ def obj_init(geo_obj, app_obj):
+
+ assert isinstance(geo_obj, GeometryObject), \
+ "Expected to initialize a GeometryObject but got %s" % type(geo_obj)
+
+ # Opening the file happens here
+ obj = HPGL2(self.app)
+ try:
+ HPGL2.parse_file(obj, filename)
+ except IOError:
+ app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open file"), filename))
+ return "fail"
+ except ParseError as parse_err:
+ app_obj.inform.emit('[ERROR_NOTCL] %s: %s. %s' % (_("Failed to parse file"), filename, str(parse_err)))
+ app_obj.log.error(str(parse_err))
+ return "fail"
+ except Exception as e:
+ app_obj.log.error("App.open_hpgl2() --> %s" % str(e))
+ msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
+ msg += traceback.format_exc()
+ app_obj.inform.emit(msg)
+ return "fail"
+
+ geo_obj.multigeo = True
+ geo_obj.solid_geometry = deepcopy(obj.solid_geometry)
+ geo_obj.tools = deepcopy(obj.tools)
+ geo_obj.source_file = deepcopy(obj.source_file)
+
+ del obj
+
+ if not geo_obj.solid_geometry:
+ app_obj.inform.emit('[ERROR_NOTCL] %s' %
+ _("Object is not HPGL2 file or empty. Aborting object creation."))
+ return "fail"
+
+ self.log.debug("open_hpgl2()")
+
+ with self.app.proc_container.new('%s...' % _("Opening")):
+ # Object name
+ name = outname or filename.split('/')[-1].split('\\')[-1]
+
+ # # ## Object creation # ##
+ ret = self.app.app_obj.new_object("geometry", name, obj_init, autoselected=False)
+ if ret == 'fail':
+ self.inform.emit('[ERROR_NOTCL]%s' % _('Failed. Probable not a HPGL2 file.'))
+ return 'fail'
+
+ # Register recent file
+ self.app.file_opened.emit("geometry", filename)
+
+ # appGUI feedback
+ self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
+
+ def open_script(self, filename, outname=None, silent=False):
+ """
+ Opens a Script file, parses it and creates a new object for
+ it in the program. Thread-safe.
+
+ :param outname: Name of the resulting object. None causes the name to be that of the file.
+ :param filename: Script file filename
+ :param silent: If True there will be no messages printed to StatusBar
+ :return: None
+ """
+
+ def obj_init(script_obj, app_obj):
+
+ assert isinstance(script_obj, ScriptObject), \
+ "Expected to initialize a ScriptObject but got %s" % type(script_obj)
+
+ if silent is False:
+ app_obj.inform.emit('[success] %s' % _("TCL script file opened in Code Editor."))
+
+ try:
+ script_obj.parse_file(filename)
+ except IOError:
+ app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open file"), filename))
+ return "fail"
+ except ParseError as parse_err:
+ app_obj.inform.emit('[ERROR_NOTCL] %s: %s. %s' % (_("Failed to parse file"), filename, str(parse_err)))
+ app_obj.log.error(str(parse_err))
+ return "fail"
+ except Exception as e:
+ app_obj.log.error("App.open_script() -> %s" % str(e))
+ msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
+ msg += traceback.format_exc()
+ app_obj.inform.emit(msg)
+ return "fail"
+
+ self.log.debug("open_script()")
+ if not os.path.exists(filename):
+ self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
+ return
+
+ with self.app.proc_container.new('%s...' % _("Opening")):
+
+ # Object name
+ script_name = outname or filename.split('/')[-1].split('\\')[-1]
+
+ # Object creation
+ ret_val = self.app.app_obj.new_object("script", script_name, obj_init, autoselected=False, plot=False)
+ if ret_val == 'fail':
+ filename = self.options['global_tcl_path'] + '/' + script_name
+ ret_val = self.app.app_obj.new_object("script", script_name, obj_init, autoselected=False, plot=False)
+ if ret_val == 'fail':
+ self.inform.emit('[ERROR_NOTCL]%s' % _('Failed to open TCL Script.'))
+ return 'fail'
+
+ # Register recent file
+ self.app.file_opened.emit("script", filename)
+
+ # appGUI feedback
+ self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
+
+ def open_config_file(self, filename, run_from_arg=None):
+ """
+ Loads a config file from the specified file.
+
+ :param filename: Name of the file from which to load.
+ :param run_from_arg: if True the FlatConfig file will be open as an command line argument
+ :return: None
+ """
+ self.log.debug("Opening config file: " + filename)
+
+ if run_from_arg:
+ self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
+ "Canvas initialization finished in"),
+ '%.2f' % self.app.used_time,
+ _("Opening FlatCAM Config file.")),
+ alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
+ color=QtGui.QColor("lightgray"))
+ # # add the tab if it was closed
+ # self.ui.plot_tab_area.addTab(self.ui.text_editor_tab, _("Code Editor"))
+ # # first clear previous text in text editor (if any)
+ # self.ui.text_editor_tab.code_editor.clear()
+ #
+ # # Switch plot_area to CNCJob tab
+ # self.ui.plot_tab_area.setCurrentWidget(self.ui.text_editor_tab)
+
+ # close the Code editor if already open
+ if self.app.toggle_codeeditor:
+ self.app.on_toggle_code_editor()
+
+ self.app.on_toggle_code_editor()
+
+ try:
+ if filename:
+ f = QtCore.QFile(filename)
+ if f.open(QtCore.QIODevice.OpenModeFlag.ReadOnly):
+ stream = QtCore.QTextStream(f)
+ code_edited = stream.readAll()
+ self.app.text_editor_tab.load_text(code_edited, clear_text=True, move_to_start=True)
+ f.close()
+ except IOError:
+ self.log.error("Failed to open config file: %s" % filename)
+ self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed."), filename))
+ return
+
+ def open_project(self, filename, run_from_arg=False, plot=True, cli=False, from_tcl=False):
+ """
+ Loads a project from the specified file.
+
+ 1) Loads and parses file
+ 2) Registers the file as recently opened.
+ 3) Calls on_file_new_project()
+ 4) Updates options
+ 5) Calls app_obj.new_object() with the object's from_dict() as init method.
+ 6) Calls plot_all() if plot=True
+
+ :param filename: Name of the file from which to load.
+ :param run_from_arg: True if run for arguments
+ :param plot: If True plot all objects in the project
+ :param cli: Run from command line
+ :param from_tcl: True if run from Tcl Sehll
+ :return: None
+ """
+
+ 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
+
+ # block autosaving while a project is loaded
+ self.app.block_autosave = True
+
+ # 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=_("Loading Project ... Please Wait ..."))
+
+ if run_from_arg:
+ self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
+ "Canvas initialization finished in"),
+ '%.2f' % self.app.used_time,
+ _("Opening FlatCAM Project file.")),
+ alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
+ color=QtGui.QColor("lightgray"))
+
+ def parse_worker(prj_filename):
+ with self.app.proc_container.new('%s' % _("Parsing...")):
+ # Open and parse an uncompressed Project file
+ try:
+ f = open(prj_filename, 'r')
+ except IOError:
+ 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
+
+ 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(
+ 'AppIO.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 + '/app128.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:
+ pass
+ elif cli is True:
+ self.app.delete_selection_shape()
+ else:
+ self.on_file_new_project()
+
+ if not run_from_arg or not cli or from_tcl is False:
+ msgbox = FCMessageBox(parent=self.app.ui)
+ title = _("Import Settings")
+ txt = _("Do you want to import the loaded project settings?")
+ msgbox.setWindowTitle(title) # taskbar still shows it
+ msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
+ msgbox.setText('%s' % title)
+ msgbox.setInformativeText(txt)
+ msgbox.setIconPixmap(QtGui.QPixmap(self.app.resource_location + '/import.png'))
+
+ bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.ButtonRole.YesRole)
+ bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.ButtonRole.NoRole)
+ # bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.ButtonRole.RejectRole)
+
+ msgbox.setDefaultButton(bt_yes)
+ msgbox.exec()
+ response = msgbox.clickedButton()
+
+ if response == bt_yes:
+ # self.app.defaults.update(self.app.options)
+ # self.app.preferencesUiManager.save_defaults()
+ # Project 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(proj_dict['options'])
+
+ self.app.project_filename = filename
+
+ # 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)
+ if cli is None:
+ self.app.set_screen_units(self.app.options["units"])
+
+ self.app.restore_project_objects_sig.emit(proj_dict, filename, cli, plot)
+
+ def restore_project_objects(self, proj_dict, filename, cli, plot):
+
+ 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 obj_init(new_obj, app_inst):
+ try:
+ new_obj.from_dict(obj)
+ except Exception as erro:
+ app_inst.log.error('AppIO.open_project() --> ' + str(erro))
+ return 'fail'
+
+ # make the 'obj_options' dict a LoudDict
+ try:
+ new_obj_options = LoudDict()
+ new_obj_options.update(new_obj.obj_options)
+ new_obj.obj_options = new_obj_options
+ except AttributeError:
+ new_obj_options = LoudDict()
+ new_obj_options.update(new_obj.options)
+ new_obj.obj_options = new_obj_options
+ except Exception as erro:
+ app_inst.log.error('AppIO.open_project() make a LoudDict--> ' + str(erro))
+ return 'fail'
+
+ # #############################################################################################
+ # for older projects loading try to convert the 'apertures' or 'cnc_tools' or 'exc_cnc_tools'
+ # attributes, if found, to 'tools'
+ # #############################################################################################
+ # for older loaded projects
+ if 'apertures' in obj:
+ new_obj.tools = obj['apertures']
+ if 'cnc_tools' in obj and obj['cnc_tools']:
+ new_obj.tools = obj['cnc_tools']
+ # new_obj.used_tools = [int(k) for k in new_obj.tools.keys()]
+ # first_key = list(obj['cnc_tools'].keys())[0]
+ # used_preprocessor = obj['cnc_tools'][first_key]['data']['ppname_g']
+ # new_obj.gc_start = new_obj.doformat(self.app.preprocessors[used_preprocessor].start_code)
+ if 'exc_cnc_tools' in obj and obj['exc_cnc_tools']:
+ new_obj.tools = obj['exc_cnc_tools']
+ # add the used_tools (all of them will be used)
+ new_obj.used_tools = [float(k) for k in new_obj.tools.keys()]
+ # add a missing key, 'tooldia' used for plotting CNCJob objects
+ for td in new_obj.tools:
+ new_obj.tools[td]['tooldia'] = float(td)
+ # #############################################################################################
+ # #############################################################################################
+
+ # try to make the keys in the tools dictionary to be integers
+ # JSON serialization makes them strings
+ # not all FlatCAM objects have the 'tools' dictionary attribute
+ try:
+ new_obj.tools = {
+ int(tool): tool_dict for tool, tool_dict in list(new_obj.tools.items())
+ }
+ except ValueError:
+ # for older loaded projects
+ new_obj.tools = {
+ float(tool): tool_dict for tool, tool_dict in list(new_obj.tools.items())
+ }
+ except Exception as erro:
+ app_inst.log.error('AppIO.open_project() keys to int--> ' + str(erro))
+ return 'fail'
+
+ # #############################################################################################
+ # for older loaded projects
+ # ony older CNCJob objects hold those
+ if 'cnc_tools' in obj:
+ new_obj.obj_options['type'] = 'Geometry'
+ if 'exc_cnc_tools' in obj:
+ new_obj.obj_options['type'] = 'Excellon'
+ # #############################################################################################
+
+ if new_obj.kind == 'cncjob':
+ # some attributes are serialized so we need t otake this into consideration in
+ # CNCJob.set_ui()
+ new_obj.is_loaded_from_project = True
+
+ # 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']))
+
+ 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=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):
+ """
+ Saves the current project to the specified file.
+
+ :param filename: Name of the file in which to save.
+ :type filename: str
+ :param quit_action: if the project saving will be followed by an app quit; boolean
+ :param silent: if True will not display status messages
+ :param from_tcl True is run from Tcl Shell
+ :return: None
+ """
+ self.log.debug("save_project() -> Saving Project")
+ self.app.save_in_progress = True
+
+ if from_tcl:
+ self.log.debug("AppIO.save_project() -> Project saved from TCL command.")
+
+ with self.app.proc_container.new(_("Saving Project ...")):
+ # Capture the latest changes
+ # Current object
+ try:
+ current_object = self.app.collection.get_active()
+ if current_object:
+ current_object.read_form()
+ except Exception as e:
+ self.log.error("save_project() --> There was no active object. Skipping read_form. %s" % str(e))
+
+ app_options = {k: v for k, v in self.app.options.items()}
+ d = {
+ "objs": [obj.to_dict() for obj in self.app.collection.get_list()],
+ "options": app_options,
+ "version": self.app.version
+ }
+
+ if self.options["global_save_compressed"] is True:
+ try:
+ project_as_json = json.dumps(d, default=to_dict, indent=2, sort_keys=True).encode('utf-8')
+ except Exception as e:
+ self.log.error(
+ "Failed to serialize file before compression: %s because: %s" % (str(filename), str(e)))
+ self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
+ return
+
+ try:
+ # with lzma.open(filename, "w", preset=int(self.options['global_compression_level'])) as f:
+ # # # Write
+ # f.write(project_as_json)
+
+ compressor_obj = lzma.LZMACompressor(preset=int(self.options['global_compression_level']))
+ out1 = compressor_obj.compress(project_as_json)
+ out2 = compressor_obj.flush()
+ project_zipped = b"".join([out1, out2])
+ except Exception as errrr:
+ self.log.error("Failed to save compressed file: %s because: %s" % (str(filename), str(errrr)))
+ self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
+ return
+
+ if project_zipped != b'':
+ with open(filename, "wb") as f_to_write:
+ f_to_write.write(project_zipped)
+
+ self.inform.emit('[success] %s: %s' % (_("Project saved to"), str(filename)))
+ else:
+ self.log.error("Failed to save file: %s. Empty binary file.", str(filename))
+ self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
+ return
+ else:
+ # Open file
+ try:
+ f = open(filename, 'w')
+ except IOError:
+ self.log.error("Failed to open file for saving: %s", str(filename))
+ self.inform.emit('[ERROR_NOTCL] %s' % _("The object is used by another application."))
+ return
+
+ # Write
+ try:
+ json.dump(d, f, default=to_dict, indent=2, sort_keys=True)
+ except Exception as e:
+ self.log.error(
+ "Failed to serialize file: %s because: %s" % (str(filename), str(e)))
+ self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
+ return
+ f.close()
+
+ # verification of the saved project
+ # Open and parse
+ try:
+ saved_f = open(filename, 'r')
+ except IOError:
+ if silent is False:
+ self.inform.emit('[ERROR_NOTCL] %s: %s %s' %
+ (_("Failed to verify project file"), str(filename), _("Retry to save it.")))
+ return
+
+ try:
+ saved_d = json.load(saved_f, object_hook=dict2obj)
+ if not saved_d:
+ self.inform.emit('[ERROR_NOTCL] %s: %s %s' %
+ (_("Failed to parse saved project file"),
+ str(filename),
+ _("Retry to save it."))) # noqa
+ f.close()
+ return
+ except Exception:
+ if silent is False:
+ self.inform.emit('[ERROR_NOTCL] %s: %s %s' %
+ (_("Failed to parse saved project file"),
+ str(filename),
+ _("Retry to save it."))) # noqa
+ f.close()
+ return
+
+ saved_f.close()
+
+ if silent is False:
+ if 'version' in saved_d:
+ self.inform.emit('[success] %s: %s' % (_("Project saved to"), str(filename)))
+ else:
+ self.inform.emit('[ERROR_NOTCL] %s: %s %s' %
+ (_("Failed to parse saved project file"),
+ str(filename),
+ _("Retry to save it."))) # noqa
+
+ tb_settings = QSettings("Open Source", "FlatCAM")
+ lock_state = self.app.ui.lock_action.isChecked()
+ tb_settings.setValue('toolbar_lock', lock_state)
+
+ # This will write the setting to the platform specific storage.
+ del tb_settings
+
+ # if quit:
+ # t = threading.Thread(target=lambda: self.check_project_file_size(1, filename=filename))
+ # t.start()
+ self.app.start_delayed_quit(delay=500, filename=filename, should_quit=quit_action)
+
+ def save_source_file(self, obj_name, filename):
+ """
+ Exports a FlatCAM Object to an Gerber/Excellon file.
+
+ :param obj_name: the name of the FlatCAM object for which to save it's embedded source file
+ :param filename: Path to the Gerber file to save to.
+ :return:
+ """
+
+ if filename is None:
+ filename = self.app.options["global_last_save_folder"] if \
+ self.app.options["global_last_save_folder"] is not None else self.app.options["global_last_folder"]
+
+ self.log.debug("save_source_file()")
+
+ obj = self.app.collection.get_by_name(obj_name)
+
+ file_string = StringIO(obj.source_file)
+ time_string = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
+
+ if file_string.getvalue() == '':
+ msg = _("Save cancelled because source file is empty. Try to export the file.")
+ self.inform.emit('[ERROR_NOTCL] %s' % msg) # noqa
+ return 'fail'
+
+ try:
+ with open(filename, 'w') as file:
+ file.writelines('G04*\n')
+ file.writelines('G04 %s (RE)GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' %
+ (obj.kind.upper(), str(self.app.version), str(self.app.version_date)))
+ file.writelines('G04 Filename: %s*\n' % str(obj_name))
+ file.writelines('G04 Created on : %s*\n' % time_string)
+
+ for line in file_string:
+ file.writelines(line)
+ except PermissionError:
+ self.inform.emit('[WARNING] %s' %
+ _("Permission denied, saving not possible.\n"
+ "Most likely another app is holding the file open and not accessible.")) # noqa
+ return 'fail'
+
+ def on_file_savedefaults(self):
+ """
+ Callback for menu item File->Save Defaults. Saves application default options
+ ``self.options`` to current_defaults.FlatConfig.
+
+ :return: None
+ """
+ self.app.defaults.update(self.app.options)
+ self.app.preferencesUiManager.save_defaults()
diff --git a/appHandlers/__init__.py b/appHandlers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/appMain.py b/appMain.py
index f4aecb28..f76cc75d 100644
--- a/appMain.py
+++ b/appMain.py
@@ -15,24 +15,15 @@ import getopt
import random
import simplejson as json
import shutil
-import lzma
from datetime import datetime
-import ctypes
import traceback
from shapely.geometry import Point, MultiPolygon, MultiLineString
from shapely.ops import unary_union
from io import StringIO
-from reportlab.graphics import renderPDF
-from reportlab.pdfgen import canvas
-from reportlab.lib.units import inch, mm
-from reportlab.lib.pagesizes import landscape, portrait
-from svglib.svglib import svg2rlg
-
import gc
-from xml.dom.minidom import parseString as parse_xml_string
from multiprocessing.connection import Listener, Client
from multiprocessing import Pool
@@ -51,18 +42,16 @@ import qdarktheme.themes.light.stylesheet as qlightsheet
# Various
from appGUI.themes import dark_style_sheet, light_style_sheet
-from appCommon.Common import LoudDict
from appCommon.Common import color_variant
from appCommon.Common import ExclusionAreas
from appCommon.Common import AppLogging
from appCommon.RegisterFileKeywords import RegisterFK, Extensions, KeyWords
+from appHandlers.AppIO import AppIO
+
from Bookmark import BookmarkManager
from appDatabase import ToolsDB2
-from vispy.gloo.util import _screenshot
-from vispy.io import write_png
-
# App defaults (preferences)
from defaults import AppDefaults
from defaults import AppOptions
@@ -77,7 +66,7 @@ from appObjects.AppObject import AppObject
# App Parsing files
from appParsers.ParseExcellon import Excellon
from appParsers.ParseGerber import Gerber
-from camlib import to_dict, dict2obj, ET, ParseError, Geometry, CNCjob
+from camlib import to_dict, Geometry, CNCjob
# App appGUI
from appGUI.PlotCanvas import *
@@ -96,7 +85,6 @@ from appEditors.AppExcEditor import AppExcEditor
from appEditors.AppGerberEditor import AppGerberEditor
from appEditors.AppTextEditor import AppTextEditor
from appEditors.appGCodeEditor import AppGCodeEditor
-from appParsers.ParseHPGL2 import HPGL2
# App Workers
from appProcess import *
@@ -112,9 +100,6 @@ import builtins
import darkdetect
-if sys.platform == 'win32':
- import winreg
-
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
@@ -443,7 +428,7 @@ class App(QtCore.QObject):
self.new_launch = ArgsThread()
self.new_launch.open_signal[list].connect(self.on_startup_args)
self.new_launch.moveToThread(self.listen_th)
- self.new_launch.start.emit()
+ self.new_launch.start.emit() # noqa
# ############################################################################################################
# ########################################## OS-specific #####################################################
@@ -842,8 +827,13 @@ class App(QtCore.QObject):
# ###########################################################################################################
# ##################################### UPDATE PREFERENCES GUI FORMS ########################################
# ###########################################################################################################
- self.preferencesUiManager = PreferencesUIManager(data_path=self.data_path, ui=self.ui, inform=self.inform,
- options=self.options, defaults=self.defaults)
+ self.preferencesUiManager = PreferencesUIManager(
+ data_path=self.data_path,
+ ui=self.ui,
+ inform=self.inform,
+ options=self.options,
+ defaults=self.defaults
+ )
self.preferencesUiManager.defaults_write_form()
@@ -1107,7 +1097,7 @@ class App(QtCore.QObject):
# ###################################### INSTANTIATE CLASSES THAT HOLD THE MENU HANDLERS ####################
# ###########################################################################################################
# ###########################################################################################################
- self.f_handlers = MenuFileHandlers(app=self)
+ self.f_handlers = AppIO(app=self)
# this is calculated in the class above (somehow?)
self.options["root_folder_path"] = self.app_home
@@ -1167,8 +1157,8 @@ 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)
+ self.restore_project.connect(self.f_handlers.restore_project_handler) # noqa
+ self.restore_project_objects_sig.connect(self.f_handlers.restore_project_objects) # noqa
# signal to be called when the app is quiting
self.app_quit.connect(self.quit_application, type=Qt.ConnectionType.QueuedConnection)
self.message.connect(
@@ -1269,10 +1259,10 @@ class App(QtCore.QObject):
self.ui.notebook.tab_closed_signal.connect(self.on_notebook_closed)
# signal to close the application
- self.close_app_signal.connect(self.kill_app)
+ self.close_app_signal.connect(self.kill_app) # noqa
# signal to process the body of a script
- self.run_script.connect(self.script_processing)
+ self.run_script.connect(self.script_processing) # noqa
# ################################# FINISHED CONNECTING SIGNALS #############################################
# ###########################################################################################################
# ###########################################################################################################
@@ -4817,7 +4807,7 @@ class App(QtCore.QObject):
# first disconnect it as it may have been used by something else
try:
- self.replot_signal.disconnect()
+ self.replot_signal.disconnect() # noqa
except TypeError:
pass
self.replot_signal[list].connect(origin_replot)
@@ -5153,7 +5143,7 @@ class App(QtCore.QObject):
else:
location = custom_location
- self.jump_signal.emit(location)
+ self.jump_signal.emit(location) # noqa
if fit_center:
self.plotcanvas.fit_center(loc=location)
@@ -5269,7 +5259,7 @@ class App(QtCore.QObject):
cy = loc_b[1] + abs((loc_b[3] - loc_b[1]) / 2)
location = (cx, cy)
- self.locate_signal.emit(location, location_point)
+ self.locate_signal.emit(location, location_point) # noqa
if fit_center:
self.plotcanvas.fit_center(loc=location)
@@ -5946,7 +5936,7 @@ class App(QtCore.QObject):
msg = "%s %s" % (_("Aborting."), _("The current task will be gracefully closed as soon as possible..."))
self.inform.emit(msg)
self.abort_flag = True
- self.cleanup.emit()
+ self.cleanup.emit() # noqa
def app_is_idle(self):
if self.abort_flag:
@@ -6339,7 +6329,7 @@ class App(QtCore.QObject):
# this signal is used by the Plugins to change the selection on App objects combo boxes when the
# selection happen in Project Tab (collection view)
# when the plugin is closed then it's not needed
- self.proj_selection_changed.disconnect()
+ self.proj_selection_changed.disconnect() # noqa
except (TypeError, AttributeError):
pass
@@ -8903,8 +8893,8 @@ class ArgsThread(QtCore.QObject):
self.listener = None
self.thread_exit = False
- self.start.connect(self.run)
- self.stop.connect(self.close_listener)
+ self.start.connect(self.run) # noqa
+ self.stop.connect(self.close_listener) # noqa
def my_loop(self, address):
try:
@@ -8942,7 +8932,7 @@ class ArgsThread(QtCore.QObject):
msg = conn.recv()
if msg == 'close':
break
- self.open_signal.emit(msg)
+ self.open_signal.emit(msg) # noqa
conn.close()
# the decorator is a must; without it this technique will not work unless the start signal is connected
@@ -8967,2869 +8957,4 @@ class ArgsThread(QtCore.QObject):
except Exception:
pass
-
-class MenuFileHandlers(QtCore.QObject):
- def __init__(self, app):
- """
- A class that holds all the menu -> file handlers
- """
- super().__init__()
-
- self.app = app
- self.log = self.app.log
- self.inform = self.app.inform
- self.splash = self.app.splash
- self.worker_task = self.app.worker_task
- self.options = self.app.options
- self.app_units = self.app.app_units
- self.pagesize = {}
-
- self.app.new_project_signal.connect(self.on_new_project_house_keeping)
-
- def on_fileopengerber(self, name=None):
- """
- File menu callback for opening a Gerber.
-
- :param name:
- :return: None
- """
-
- self.log.debug("on_fileopengerber()")
-
- _filter_ = "Gerber Files (*.gbr *.ger *.gtl *.gbl *.gts *.gbs *.gtp *.gbp *.gto *.gbo *.gm1 *.gml *.gm3 " \
- "*.gko *.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim *.mil *.grb " \
- "*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb *.pho *.gdo *.art *.gbd *.outline);;" \
- "Protel Files (*.gtl *.gbl *.gts *.gbs *.gto *.gbo *.gtp *.gbp *.gml *.gm1 *.gm3 *.gko " \
- "*.outline);;" \
- "Eagle Files (*.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim " \
- "*.mil);;" \
- "OrCAD Files (*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb);;" \
- "Allegro Files (*.art);;" \
- "Mentor Files (*.pho *.gdo);;" \
- "All Files (*.*)"
-
- if name is None:
- try:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"),
- directory=self.app.get_last_folder(),
- filter=_filter_,
- initialFilter=self.app.last_op_gerber_filter)
- except TypeError:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"), filter=_filter_)
-
- filenames = [str(filename) for filename in filenames]
- self.app.last_op_gerber_filter = _f
- else:
- filenames = [name]
- self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
- "Canvas initialization finished in"),
- '%.2f' % self.app.used_time,
- _("Opening Gerber file.")),
- alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
- color=QtGui.QColor("lightgray"))
-
- if len(filenames) == 0:
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- else:
- for filename in filenames:
- if filename != '':
- self.worker_task.emit({'fcn': self.open_gerber, 'params': [filename]})
-
- def on_fileopenexcellon(self, name=None):
- """
- File menu callback for opening an Excellon file.
-
- :param name:
- :return: None
- """
-
- self.log.debug("on_fileopenexcellon()")
-
- _filter_ = "Excellon Files (*.drl *.txt *.xln *.drd *.tap *.exc *.ncd);;" \
- "All Files (*.*)"
- if name is None:
- try:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"),
- directory=self.app.get_last_folder(),
- filter=_filter_,
- initialFilter=self.app.last_op_excellon_filter)
- except TypeError:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"), filter=_filter_)
- filenames = [str(filename) for filename in filenames]
- self.app.last_op_excellon_filter = _f
- else:
- filenames = [str(name)]
- self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
- "Canvas initialization finished in"),
- '%.2f' % self.app.used_time,
- _("Opening Excellon file.")),
- alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
- color=QtGui.QColor("lightgray"))
-
- if len(filenames) == 0:
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- else:
- for filename in filenames:
- if filename != '':
- self.worker_task.emit({'fcn': self.open_excellon, 'params': [filename]})
-
- def on_fileopengcode(self, name=None):
- """
-
- File menu call back for opening gcode.
-
- :param name:
- :return:
- """
-
- self.log.debug("on_fileopengcode()")
-
- # https://bobcadsupport.com/helpdesk/index.php?/Knowledgebase/Article/View/13/5/known-g-code-file-extensions
- _filter_ = "G-Code Files (*.txt *.nc *.ncc *.tap *.gcode *.cnc *.ecs *.fnc *.dnc *.ncg *.gc *.fan *.fgc" \
- " *.din *.xpi *.hnc *.h *.i *.ncp *.min *.gcd *.rol *.knc *.mpr *.ply *.out *.eia *.sbp *.mpf);;" \
- "All Files (*.*)"
-
- if name is None:
- try:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"),
- directory=self.app.get_last_folder(),
- filter=_filter_,
- initialFilter=self.app.last_op_gcode_filter)
- except TypeError:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"), filter=_filter_)
-
- filenames = [str(filename) for filename in filenames]
- self.app.last_op_gcode_filter = _f
- else:
- filenames = [name]
- self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
- "Canvas initialization finished in"),
- '%.2f' % self.app.used_time,
- _("Opening G-Code file.")),
- alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
- color=QtGui.QColor("lightgray"))
-
- if len(filenames) == 0:
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- else:
- for filename in filenames:
- if filename != '':
- self.worker_task.emit({'fcn': self.open_gcode, 'params': [filename, None, True]})
-
- def on_file_openproject(self):
- """
- File menu callback for opening a project.
-
- :return: None
- """
-
- self.log.debug("on_file_openproject()")
-
- _filter_ = "FlatCAM Project (*.FlatPrj);;All Files (*.*)"
- try:
- filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Project"),
- directory=self.app.get_last_folder(), filter=_filter_)
- except TypeError:
- filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Project"), filter=_filter_)
-
- filename = str(filename)
-
- if filename == "":
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- else:
- # self.worker_task.emit({'fcn': self.open_project,
- # 'params': [filename]})
- # The above was failing because open_project() is not
- # thread safe. The new_project()
- self.open_project(filename)
-
- def on_fileopenhpgl2(self, name=None):
- """
- File menu callback for opening a HPGL2.
-
- :param name:
- :return: None
- """
- self.log.debug("on_fileopenhpgl2()")
-
- _filter_ = "HPGL2 Files (*.plt);;" \
- "All Files (*.*)"
-
- if name is None:
- try:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open HPGL2"),
- directory=self.app.get_last_folder(),
- filter=_filter_)
- except TypeError:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open HPGL2"), filter=_filter_)
-
- filenames = [str(filename) for filename in filenames]
- else:
- filenames = [name]
- self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
- "Canvas initialization finished in"),
- '%.2f' % self.app.used_time,
- _("Opening HPGL2 file.")),
- alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
- color=QtGui.QColor("lightgray"))
-
- if len(filenames) == 0:
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- else:
- for filename in filenames:
- if filename != '':
- self.worker_task.emit({'fcn': self.open_hpgl2, 'params': [filename]})
-
- def on_file_openconfig(self):
- """
- File menu callback for opening a config file.
-
- :return: None
- """
-
- self.log.debug("on_file_openconfig()")
-
- _filter_ = "FlatCAM Config (*.FlatConfig);;FlatCAM Config (*.json);;All Files (*.*)"
- try:
- filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Configuration File"),
- directory=self.app.data_path, filter=_filter_)
- except TypeError:
- filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Configuration File"),
- filter=_filter_)
-
- if filename == "":
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- else:
- self.open_config_file(filename)
-
- def on_file_exportsvg(self):
- """
- Callback for menu item File->Export SVG.
-
- :return: None
- """
- self.log.debug("on_file_exportsvg()")
-
- obj = self.app.collection.get_active()
- if obj is None:
- self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
- return
-
- # Check for more compatible types and add as required
- if (not isinstance(obj, GeometryObject)
- and not isinstance(obj, GerberObject)
- and not isinstance(obj, CNCJobObject)
- and not isinstance(obj, ExcellonObject)):
- msg = _("Only Geometry, Gerber and CNCJob objects can be used.")
- msgbox = FCMessageBox(parent=self.app.ui)
- msgbox.setWindowTitle(msg) # taskbar still shows it
- msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
-
- msgbox.setInformativeText(msg)
- msgbox.setIconPixmap(QtGui.QPixmap(self.app.resource_location + '/waning.png'))
-
- bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.ButtonRole.AcceptRole)
- msgbox.setDefaultButton(bt_ok)
- msgbox.exec()
- return
-
- name = obj.obj_options["name"]
-
- _filter = "SVG File (*.svg);;All Files (*.*)"
- try:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Export SVG"),
- directory=self.app.get_last_save_folder() + '/' + str(name) + '_svg',
- ext_filter=_filter)
- except TypeError:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Export SVG"),
- ext_filter=_filter)
-
- filename = str(filename)
-
- if filename == "":
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- return
- else:
- self.export_svg(name, filename)
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("SVG", filename)
- self.app.file_saved.emit("SVG", filename)
-
- def on_file_exportpng(self):
-
- self.log.debug("on_file_exportpng()")
-
- date = str(datetime.today()).rpartition('.')[0]
- date = ''.join(c for c in date if c not in ':-')
- date = date.replace(' ', '_')
-
- data = None
- if self.app.use_3d_engine:
- image = _screenshot(alpha=False)
- 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'))
- return
-
- filter_ = "PNG File (*.png);;All Files (*.*)"
- try:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Export PNG Image"),
- directory=self.app.get_last_save_folder() + '/png_' + date,
- ext_filter=filter_)
- except TypeError:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Export PNG Image"),
- ext_filter=filter_)
-
- filename = str(filename)
-
- if filename == "":
- self.inform.emit(_("Cancelled."))
- return
- else:
- if self.app.use_3d_engine:
- write_png(filename, data)
- else:
- self.app.plotcanvas.figure.savefig(filename)
-
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("png", filename)
- self.app.file_saved.emit("png", filename)
-
- def on_file_savegerber(self):
- """
- Callback for menu item in Project context menu.
-
- :return: None
- """
- self.log.debug("on_file_savegerber()")
-
- obj = self.app.collection.get_active()
- if obj is None:
- self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
- return
-
- # Check for more compatible types and add as required
- if not isinstance(obj, GerberObject):
- self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Gerber objects can be saved as Gerber files..."))
- return
-
- name = self.app.collection.get_active().obj_options["name"]
-
- _filter = "Gerber File (*.GBR);;Gerber File (*.GRB);;All Files (*.*)"
- try:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption="Save Gerber source file",
- directory=self.app.get_last_save_folder() + '/' + name,
- ext_filter=_filter)
- except TypeError:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Save Gerber source file"),
- ext_filter=_filter)
-
- filename = str(filename)
-
- if filename == "":
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- return
- else:
- self.save_source_file(name, filename)
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("Gerber", filename)
- self.app.file_saved.emit("Gerber", filename)
-
- def on_file_savescript(self):
- """
- Callback for menu item in Project context menu.
-
- :return: None
- """
- self.log.debug("on_file_savescript()")
-
- obj = self.app.collection.get_active()
- if obj is None:
- self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
- return
-
- # Check for more compatible types and add as required
- if not isinstance(obj, ScriptObject):
- self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Script objects can be saved as TCL Script files..."))
- return
-
- name = self.app.collection.get_active().obj_options["name"]
-
- _filter = "FlatCAM Scripts (*.FlatScript);;All Files (*.*)"
- try:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption="Save Script source file",
- directory=self.app.get_last_save_folder() + '/' + name,
- ext_filter=_filter)
- except TypeError:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Save Script source file"),
- ext_filter=_filter)
-
- filename = str(filename)
-
- if filename == "":
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- return
- else:
- self.save_source_file(name, filename)
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("Script", filename)
- self.app.file_saved.emit("Script", filename)
-
- def on_file_savedocument(self):
- """
- Callback for menu item in Project context menu.
-
- :return: None
- """
- self.log.debug("on_file_savedocument()")
-
- obj = self.app.collection.get_active()
- if obj is None:
- self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
- return
-
- # Check for more compatible types and add as required
- if not isinstance(obj, ScriptObject):
- self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Document objects can be saved as Document files..."))
- return
-
- name = self.app.collection.get_active().obj_options["name"]
-
- _filter = "FlatCAM Documents (*.FlatDoc);;All Files (*.*)"
- try:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption="Save Document source file",
- directory=self.app.get_last_save_folder() + '/' + name,
- ext_filter=_filter)
- except TypeError:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Save Document source file"),
- ext_filter=_filter)
-
- filename = str(filename)
-
- if filename == "":
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- return
- else:
- self.save_source_file(name, filename)
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("Document", filename)
- self.app.file_saved.emit("Document", filename)
-
- def on_file_saveexcellon(self):
- """
- Callback for menu item in project context menu.
-
- :return: None
- """
- self.log.debug("on_file_saveexcellon()")
-
- obj = self.app.collection.get_active()
- if obj is None:
- self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
- return
-
- # Check for more compatible types and add as required
- if not isinstance(obj, ExcellonObject):
- self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Excellon objects can be saved as Excellon files..."))
- return
-
- name = self.app.collection.get_active().obj_options["name"]
-
- _filter = "Excellon File (*.DRL);;Excellon File (*.TXT);;All Files (*.*)"
- try:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Save Excellon source file"),
- directory=self.app.get_last_save_folder() + '/' + name,
- ext_filter=_filter)
- except TypeError:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Save Excellon source file"), ext_filter=_filter)
-
- filename = str(filename)
-
- if filename == "":
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- return
- else:
- self.save_source_file(name, filename)
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("Excellon", filename)
- self.app.file_saved.emit("Excellon", filename)
-
- def on_file_exportexcellon(self):
- """
- Callback for menu item File->Export->Excellon.
-
- :return: None
- """
- self.log.debug("on_file_exportexcellon()")
-
- obj = self.app.collection.get_active()
- if obj is None:
- self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
- return
-
- # Check for more compatible types and add as required
- if not isinstance(obj, ExcellonObject):
- self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Excellon objects can be saved as Excellon files..."))
- return
-
- name = self.app.collection.get_active().obj_options["name"]
-
- _filter = self.options["excellon_save_filters"]
- try:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Export Excellon"),
- directory=self.app.get_last_save_folder() + '/' + name,
- ext_filter=_filter)
- except TypeError:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Export Excellon"),
- ext_filter=_filter)
-
- filename = str(filename)
-
- if filename == "":
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- return
- else:
- used_extension = filename.rpartition('.')[2]
- obj.update_filters(last_ext=used_extension, filter_string='excellon_save_filters')
-
- self.export_excellon(name, filename)
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("Excellon", filename)
- self.app.file_saved.emit("Excellon", filename)
-
- def on_file_exportgerber(self):
- """
- Callback for menu item File->Export->Gerber.
-
- :return: None
- """
- self.log.debug("on_file_exportgerber()")
-
- obj = self.app.collection.get_active()
- if obj is None:
- self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
- return
-
- # Check for more compatible types and add as required
- if not isinstance(obj, GerberObject):
- self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Gerber objects can be saved as Gerber files..."))
- return
-
- name = self.app.collection.get_active().obj_options["name"]
-
- _filter_ = self.options['gerber_save_filters']
- try:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Export Gerber"),
- directory=self.app.get_last_save_folder() + '/' + name,
- ext_filter=_filter_)
- except TypeError:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Export Gerber"),
- ext_filter=_filter_)
-
- filename = str(filename)
-
- if filename == "":
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- return
- else:
- used_extension = filename.rpartition('.')[2]
- obj.update_filters(last_ext=used_extension, filter_string='gerber_save_filters')
-
- self.export_gerber(name, filename)
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("Gerber", filename)
- self.app.file_saved.emit("Gerber", filename)
-
- def on_file_exportdxf(self):
- """
- Callback for menu item File->Export DXF.
-
- :return: None
- """
- self.log.debug("on_file_exportdxf()")
-
- obj = self.app.collection.get_active()
- if obj is None:
- self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
- return
-
- # Check for more compatible types and add as required
- if obj.kind != 'geometry':
- msg = _("Only Geometry objects can be used.")
- msgbox = FCMessageBox(parent=self.app.ui)
- msgbox.setWindowTitle(msg) # taskbar still shows it
- msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
-
- msgbox.setInformativeText(msg)
- msgbox.setIconPixmap(QtGui.QPixmap(self.app.resource_location + '/waning.png'))
-
- msgbox.setIcon(QtWidgets.QMessageBox.Icon.Warning)
-
- msgbox.setInformativeText(msg)
- bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.ButtonRole.AcceptRole)
- msgbox.setDefaultButton(bt_ok)
- msgbox.exec()
- return
-
- name = self.app.collection.get_active().obj_options["name"]
-
- _filter_ = "DXF File .dxf (*.DXF);;All Files (*.*)"
- try:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Export DXF"),
- directory=self.app.get_last_save_folder() + '/' + name,
- ext_filter=_filter_)
- except TypeError:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Export DXF"),
- ext_filter=_filter_)
-
- filename = str(filename)
-
- if filename == "":
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- return
- else:
- self.export_dxf(name, filename)
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("DXF", filename)
- self.app.file_saved.emit("DXF", filename)
-
- def on_file_importsvg(self, type_of_obj):
- """
- Callback for menu item File->Import SVG.
- :param type_of_obj: to import the SVG as Geometry or as Gerber
- :type type_of_obj: str
- :return: None
- """
- self.log.debug("on_file_importsvg()")
-
- _filter_ = "SVG File .svg (*.svg);;All Files (*.*)"
- try:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import SVG"),
- directory=self.app.get_last_folder(),
- filter=_filter_)
- except TypeError:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import SVG"),
- filter=_filter_)
-
- if type_of_obj != "geometry" and type_of_obj != "gerber":
- type_of_obj = "geometry"
-
- filenames = [str(filename) for filename in filenames]
-
- if len(filenames) == 0:
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- else:
- for filename in filenames:
- if filename != '':
- self.worker_task.emit({'fcn': self.import_svg, 'params': [filename, type_of_obj]})
-
- def on_file_importdxf(self, type_of_obj):
- """
- Callback for menu item File->Import DXF.
- :param type_of_obj: to import the DXF as Geometry or as Gerber
- :type type_of_obj: str
- :return: None
- """
- self.log.debug("on_file_importdxf()")
-
- _filter_ = "DXF File .dxf (*.DXF);;All Files (*.*)"
- try:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import DXF"),
- directory=self.app.get_last_folder(),
- filter=_filter_)
- except TypeError:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import DXF"),
- filter=_filter_)
-
- if type_of_obj != "geometry" and type_of_obj != "gerber":
- type_of_obj = "geometry"
-
- filenames = [str(filename) for filename in filenames]
-
- if len(filenames) == 0:
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- else:
- for filename in filenames:
- if filename != '':
- self.worker_task.emit({'fcn': self.import_dxf, 'params': [filename, type_of_obj]})
-
- def on_file_new_click(self):
- """
- Callback for menu item File -> New.
- Executed on clicking the Menu -> File -> New Project
-
- :return:
- """
- self.log.debug("on_file_new_click()")
-
- if self.app.collection.get_list() and self.app.should_we_save:
- msgbox = FCMessageBox(parent=self.app.ui)
- title = _("Save changes")
- txt = _("There are files/objects opened.\n"
- "Creating a New project will delete them.\n"
- "Do you want to Save the project?")
- msgbox.setWindowTitle(title) # taskbar still shows it
- msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
- msgbox.setText('%s' % title)
- msgbox.setInformativeText(txt)
- msgbox.setIconPixmap(QtGui.QPixmap(self.app.resource_location + '/save_as.png'))
-
- bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.ButtonRole.YesRole)
- bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.ButtonRole.NoRole)
- bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.ButtonRole.RejectRole)
-
- msgbox.setDefaultButton(bt_yes)
- msgbox.exec()
- response = msgbox.clickedButton()
-
- if response == bt_yes:
- self.on_file_saveprojectas(use_thread=True)
- elif response == bt_cancel:
- return
- elif response == bt_no:
- self.on_file_new_project(use_thread=True, silenced=True)
- else:
- self.on_file_new_project(use_thread=True, silenced=True)
-
- def on_file_new_project(self, cli=None, reset_tcl=True, use_thread=None, silenced=None, keep_scripts=True):
- """
- Returns the application to its startup state. This method is thread-safe.
-
- :param cli: Boolean. If True this method was run from command line
- :param reset_tcl: Boolean. If False, on new project creation the Tcl instance is not recreated, therefore it
- will remember all the previous variables. If True then the Tcl is re-instantiated.
- :param use_thread: Bool. If True some part of the initialization are done threaded
- :param silenced: Bool or None. If True then the app will not ask to save the current parameters.
- :param keep_scripts: Bool. If True the Script objects are not deleted when creating a new project
- :return: None
- """
-
- self.log.debug("on_file_new_project()")
-
- t_start_proj = time.time()
-
- # close any editor that might be open
- if self.app.call_source != 'app':
- self.app.editor2object(cleanup=True)
- # ## EDITOR section
- self.app.geo_editor = AppGeoEditor(self.app)
- self.app.exc_editor = AppExcEditor(self.app)
- self.app.grb_editor = AppGerberEditor(self.app)
-
- for obj in self.app.collection.get_list():
- # delete shapes left drawn from mark shape_collections, if any
- if isinstance(obj, GerberObject):
- try:
- obj.mark_shapes_storage.clear()
- obj.mark_shapes.clear(update=True)
- obj.mark_shapes.enabled = False
- except AttributeError:
- pass
-
- # also delete annotation shapes, if any
- elif isinstance(obj, CNCJobObject):
- try:
- obj.text_col.enabled = False
- del obj.text_col
- obj.annotation.clear(update=True)
- del obj.annotation
- except AttributeError:
- pass
-
- # delete the exclusion areas
- self.app.exc_areas.clear_shapes()
-
- # delete any selection shape on canvas
- self.app.delete_selection_shape()
-
- # delete all App objects
- if keep_scripts is True:
- for prj_obj in self.app.collection.get_list():
- if prj_obj.kind != 'script':
- self.app.collection.delete_by_name(prj_obj.obj_options['name'], select_project=False)
- 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()
-
- # Clear project filename
- self.app.project_filename = None
-
- default_file = self.app.defaults_path()
- # Load the application options
- self.options.load(filename=default_file, inform=self.inform)
-
- # Re-fresh project options
- self.app.on_defaults2options()
-
- if use_thread is True:
- self.app.new_project_signal.emit()
- else:
- t0 = time.time()
- # Clear pool
- self.app.clear_pool()
-
- # Init FlatCAMTools
- if reset_tcl is True:
- 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()
-
- # Try to close all tabs in the PlotArea but only if the appGUI is active (CLI is None)
- if cli is None:
- # we need to go in reverse because once we remove a tab then the index changes
- # meaning that removing the first tab (idx = 0) then the tab at former idx = 1 will assume idx = 0
- # and so on. Therefore the deletion should be done in reverse
- wdg_count = self.app.ui.plot_tab_area.tabBar.count() - 1
- for index in range(wdg_count, -1, -1):
- try:
- self.app.ui.plot_tab_area.closeTab(index)
- except Exception as e:
- self.log.error("App.on_file_new_project() --> %s" % str(e))
-
- # # And then add again the Plot Area
- self.app.ui.plot_tab_area.insertTab(0, self.app.ui.plot_tab, _("Plot Area"))
- self.app.ui.plot_tab_area.protectTab(0)
-
- # 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() - t_start_proj), _("seconds")))
- self.app.ui.set_ui_title(name=_("New Project - Not saved"))
-
- self.inform.emit('[success] %s...' % _("New Project created"))
-
- def on_new_project_house_keeping(self):
- """
- Do dome of the new project initialization in a threaded way
-
- :return:
- :rtype:
- """
- t0 = time.time()
-
- # Clear pool
- self.log.debug("New Project: cleaning multiprocessing pool.")
- self.app.clear_pool()
-
- # 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):
- """
- Will create a new script file and open it in the Code Editor
-
- :param silent: if True will not display status messages
- :return: None
- """
- self.log.debug("on_filenewscript()")
-
- if silent is False:
- self.inform.emit('[success] %s' % _("New TCL script file created in Code Editor."))
-
- # hide coordinates toolbars in the infobar while in DB
- self.app.ui.coords_toolbar.hide()
- self.app.ui.delta_coords_toolbar.hide()
-
- self.app.app_obj.new_script_object()
-
- def on_fileopenscript(self, name=None, silent=False):
- """
- Will open a Tcl script file into the Code Editor
-
- :param silent: if True will not display status messages
- :param name: name of a Tcl script file to open
- :return: None
- """
-
- self.log.debug("on_fileopenscript()")
-
- _filter_ = "TCL script .FlatScript (*.FlatScript);;TCL script .tcl (*.TCL);;TCL script .txt (*.TXT);;" \
- "All Files (*.*)"
-
- if name:
- filenames = [name]
- else:
- try:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(
- caption=_("Open TCL script"), directory=self.app.get_last_folder(), filter=_filter_)
- except TypeError:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open TCL script"), filter=_filter_)
-
- if len(filenames) == 0:
- if silent is False:
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- else:
- for filename in filenames:
- if filename != '':
- self.worker_task.emit({'fcn': self.open_script, 'params': [filename]})
-
- def on_fileopenscript_example(self, name=None, silent=False):
- """
- Will open a Tcl script file into the Code Editor
-
- :param silent: if True will not display status messages
- :param name: name of a Tcl script file to open
- :return:
- """
-
- self.log.debug("on_fileopenscript_example()")
-
- _filter_ = "TCL script .FlatScript (*.FlatScript);;TCL script .tcl (*.TCL);;TCL script .txt (*.TXT);;" \
- "All Files (*.*)"
-
- # test if the app was frozen and choose the path for the configuration file
- if getattr(sys, "frozen", False) is True:
- example_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + '\\assets\\examples'
- else:
- example_path = os.path.dirname(os.path.realpath(__file__)) + '\\assets\\examples'
-
- if name:
- filenames = [name]
- else:
- try:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(
- caption=_("Open TCL script"), directory=example_path, filter=_filter_)
- except TypeError:
- filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open TCL script"), filter=_filter_)
-
- if len(filenames) == 0:
- if silent is False:
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- else:
- for filename in filenames:
- if filename != '':
- self.worker_task.emit({'fcn': self.open_script, 'params': [filename]})
-
- def on_filerunscript(self, name=None, silent=False):
- """
- File menu callback for loading and running a TCL script.
-
- :param silent: if True will not display status messages
- :param name: name of a Tcl script file to be run by FlatCAM
- :return: None
- """
-
- self.log.debug("on_file_runscript()")
-
- if name:
- filename = name
- if self.app.cmd_line_headless != 1:
- self.splash.showMessage('%s: %ssec\n%s' %
- (_("Canvas initialization started.\n"
- "Canvas initialization finished in"), '%.2f' % self.app.used_time,
- _("Executing ScriptObject file.")
- ),
- alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
- color=QtGui.QColor("lightgray"))
- else:
- _filter_ = "TCL script .FlatScript (*.FlatScript);;TCL script .tcl (*.TCL);;TCL script .txt (*.TXT);;" \
- "All Files (*.*)"
- try:
- filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Run TCL script"),
- directory=self.app.get_last_folder(),
- filter=_filter_)
- except TypeError:
- filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Run TCL script"), filter=_filter_)
-
- # The Qt methods above will return a QString which can cause problems later.
- # So far json.dump() will fail to serialize it.
- filename = str(filename)
-
- if filename == "":
- if silent is False:
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- else:
- if self.app.cmd_line_headless != 1:
- if self.app.ui.shell_dock.isHidden():
- self.app.ui.shell_dock.show()
-
- try:
- with open(filename, "r") as tcl_script:
- cmd_line_shellfile_content = tcl_script.read()
- if self.app.cmd_line_headless != 1:
- self.app.shell.exec_command(cmd_line_shellfile_content)
- else:
- self.app.shell.exec_command(cmd_line_shellfile_content, no_echo=True)
-
- if silent is False:
- self.inform.emit('[success] %s' % _("TCL script file opened in Code Editor and executed."))
- except Exception as e:
- self.app.error("App.on_filerunscript() -> %s" % str(e))
- sys.exit(2)
-
- def on_file_saveproject(self, silent=False):
- """
- Callback for menu item File->Save Project. Saves the project to
- ``self.project_filename`` or calls ``self.on_file_saveprojectas()``
- if set to None. The project is saved by calling ``self.save_project()``.
-
- :param silent: if True will not display status messages
- :return: None
- """
- self.log.debug("on_file_saveproject()")
-
- if self.app.project_filename is None:
- self.on_file_saveprojectas()
- else:
- self.worker_task.emit({'fcn': self.save_project, 'params': [self.app.project_filename, silent]})
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("project", self.app.project_filename)
- self.app.file_saved.emit("project", self.app.project_filename)
-
- self.app.ui.set_ui_title(name=self.app.project_filename)
-
- self.app.should_we_save = False
-
- def on_file_saveprojectas(self, make_copy=False, use_thread=True, quit_action=False):
- """
- Callback for menu item File->Save Project As... Opens a file
- chooser and saves the project to the given file via
- ``self.save_project()``.
-
- :param make_copy if to be create a copy of the project; boolean
- :param use_thread: if to be run in a separate thread; boolean
- :param quit_action: if to be followed by quiting the application; boolean
- :return: None
- """
- self.log.debug("on_file_saveprojectas()")
-
- date = str(datetime.today()).rpartition('.')[0]
- date = ''.join(c for c in date if c not in ':-')
- date = date.replace(' ', '_')
-
- filter_ = "FlatCAM Project .FlatPrj (*.FlatPrj);; All Files (*.*)"
- try:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Save Project As ..."),
- directory='{l_save}/{proj}_{date}'.format(l_save=str(self.app.get_last_save_folder()), date=date,
- proj=_("Project")),
- ext_filter=filter_
- )
- except TypeError:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Save Project As ..."),
- ext_filter=filter_)
-
- filename = str(filename)
-
- if filename == '':
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- return
-
- if use_thread is True:
- self.worker_task.emit({'fcn': self.save_project, 'params': [filename, quit_action]})
- else:
- self.save_project(filename, quit_action)
-
- # self.save_project(filename)
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("project", filename)
- self.app.file_saved.emit("project", filename)
-
- if not make_copy:
- self.app.project_filename = filename
-
- self.app.ui.set_ui_title(name=self.app.project_filename)
- self.app.should_we_save = False
-
- def on_file_save_objects_pdf(self, use_thread=True):
- self.log.debug("on_file_save_objects_pdf()")
-
- date = str(datetime.today()).rpartition('.')[0]
- date = ''.join(c for c in date if c not in ':-')
- date = date.replace(' ', '_')
-
- try:
- obj_selection = self.app.collection.get_selected()
- if len(obj_selection) == 1:
- obj_name = str(obj_selection[0].obj_options['name'])
- else:
- obj_name = _("General_print")
- except AttributeError as att_err:
- self.log.debug("App.on_file_save_object_pdf() --> %s" % str(att_err))
- self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
- return
-
- if not obj_selection:
- self.inform.emit(
- '[WARNING_NOTCL] %s %s' % (_("No object is selected."), _("Print everything in the workspace.")))
- obj_selection = self.app.collection.get_list()
-
- filter_ = "PDF File .pdf (*.PDF);; All Files (*.*)"
- try:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Save Object as PDF ..."),
- directory='{l_save}/{obj_name}_{date}'.format(l_save=str(self.app.get_last_save_folder()),
- obj_name=obj_name,
- date=date),
- ext_filter=filter_
- )
- except TypeError:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Save Object as PDF ..."),
- ext_filter=filter_)
-
- filename = str(filename)
-
- if filename == '':
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- return
-
- if use_thread is True:
- self.app.proc_container.new(_("Printing PDF ..."))
- self.worker_task.emit({'fcn': self.save_pdf, 'params': [filename, obj_selection]})
- else:
- self.save_pdf(filename, obj_selection)
-
- # self.save_project(filename)
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("pdf", filename)
- self.app.file_saved.emit("pdf", filename)
-
- def save_pdf(self, file_name, obj_selection):
- self.log.debug("save_pdf()")
-
- p_size = self.options['global_workspaceT']
- orientation = self.options['global_workspace_orientation']
- color = 'black'
- transparency_level = 1.0
-
- self.pagesize.update(
- {
- 'Bounds': None,
- 'A0': (841 * mm, 1189 * mm),
- 'A1': (594 * mm, 841 * mm),
- 'A2': (420 * mm, 594 * mm),
- 'A3': (297 * mm, 420 * mm),
- 'A4': (210 * mm, 297 * mm),
- 'A5': (148 * mm, 210 * mm),
- 'A6': (105 * mm, 148 * mm),
- 'A7': (74 * mm, 105 * mm),
- 'A8': (52 * mm, 74 * mm),
- 'A9': (37 * mm, 52 * mm),
- 'A10': (26 * mm, 37 * mm),
-
- 'B0': (1000 * mm, 1414 * mm),
- 'B1': (707 * mm, 1000 * mm),
- 'B2': (500 * mm, 707 * mm),
- 'B3': (353 * mm, 500 * mm),
- 'B4': (250 * mm, 353 * mm),
- 'B5': (176 * mm, 250 * mm),
- 'B6': (125 * mm, 176 * mm),
- 'B7': (88 * mm, 125 * mm),
- 'B8': (62 * mm, 88 * mm),
- 'B9': (44 * mm, 62 * mm),
- 'B10': (31 * mm, 44 * mm),
-
- 'C0': (917 * mm, 1297 * mm),
- 'C1': (648 * mm, 917 * mm),
- 'C2': (458 * mm, 648 * mm),
- 'C3': (324 * mm, 458 * mm),
- 'C4': (229 * mm, 324 * mm),
- 'C5': (162 * mm, 229 * mm),
- 'C6': (114 * mm, 162 * mm),
- 'C7': (81 * mm, 114 * mm),
- 'C8': (57 * mm, 81 * mm),
- 'C9': (40 * mm, 57 * mm),
- 'C10': (28 * mm, 40 * mm),
-
- # American paper sizes
- 'LETTER': (8.5 * inch, 11 * inch),
- 'LEGAL': (8.5 * inch, 14 * inch),
- 'ELEVENSEVENTEEN': (11 * inch, 17 * inch),
-
- # From https://en.wikipedia.org/wiki/Paper_size
- 'JUNIOR_LEGAL': (5 * inch, 8 * inch),
- 'HALF_LETTER': (5.5 * inch, 8 * inch),
- 'GOV_LETTER': (8 * inch, 10.5 * inch),
- 'GOV_LEGAL': (8.5 * inch, 13 * inch),
- 'LEDGER': (17 * inch, 11 * inch),
- }
- )
-
- # make sure that the Excellon objeacts are drawn on top of everything
- excellon_objs = [obj for obj in obj_selection if obj.kind == 'excellon']
- cncjob_objs = [obj for obj in obj_selection if obj.kind == 'cncjob']
- # reverse the object order such that the first selected is on top
- rest_objs = [obj for obj in obj_selection if obj.kind != 'excellon' and obj.kind != 'cncjob'][::-1]
- obj_selection = rest_objs + cncjob_objs + excellon_objs
-
- # generate the SVG files from the application objects
- exported_svg = []
- for obj in obj_selection:
- svg_obj = obj.export_svg(scale_stroke_factor=0.0)
-
- if obj.kind.lower() == 'gerber' or obj.kind.lower() == 'excellon':
- color = obj.fill_color[:-2]
- transparency_level = obj.fill_color[-2:]
- elif obj.kind.lower() == 'geometry':
- color = self.options["global_draw_color"]
-
- # Change the attributes of the exported SVG
- # We don't need stroke-width
- # We set opacity to maximum
- # We set the colour to WHITE
-
- try:
- root = ET.fromstring(svg_obj)
- except Exception as e:
- self.log.debug("MenuFileHandlers.save_pdf() -> Missing root node -> %s" % str(e))
- self.app.inform.emit("[ERROR_NOTCL] %s" % _("Failed."))
- return
-
- for child in root:
- child.set('fill', str(color))
- child.set('opacity', str(transparency_level))
- child.set('stroke', str(color))
-
- exported_svg.append(ET.tostring(root))
-
- xmin = Inf
- ymin = Inf
- xmax = -Inf
- ymax = -Inf
-
- for obj in obj_selection:
- try:
- gxmin, gymin, gxmax, gymax = obj.bounds()
- xmin = min([xmin, gxmin])
- ymin = min([ymin, gymin])
- xmax = max([xmax, gxmax])
- ymax = max([ymax, gymax])
- except Exception as e:
- self.log.error("Tried to get bounds of empty geometry in App.save_pdf(). %s" % str(e))
-
- # Determine bounding area for svg export
- bounds = [xmin, ymin, xmax, ymax]
- size = bounds[2] - bounds[0], bounds[3] - bounds[1]
-
- # This contain the measure units
- uom = obj_selection[0].units.lower()
-
- # Define a boundary around SVG of about 1.0mm (~39mils)
- if uom in "mm":
- boundary = 1.0
- else:
- boundary = 0.0393701
-
- # Convert everything to strings for use in the xml doc
- svgwidth = str(size[0] + (2 * boundary))
- svgheight = str(size[1] + (2 * boundary))
- minx = str(bounds[0] - boundary)
- miny = str(bounds[1] + boundary + size[1])
-
- # Add a SVG Header and footer to the svg output from shapely
- # The transform flips the Y Axis so that everything renders
- # properly within svg apps such as inkscape
- svg_header = ''
-
- svg_elem = str(svg_header)
- for svg_item in exported_svg:
- svg_elem += str(svg_item)
- svg_elem += str(svg_footer)
-
- # Parse the xml through a xml parser just to add line feeds
- # and to make it look more pretty for the output
- doc = parse_xml_string(svg_elem)
- doc_final = doc.toprettyxml()
-
- try:
- if self.app_units.upper() == 'IN':
- unit = inch
- else:
- unit = mm
-
- doc_final = StringIO(doc_final)
- drawing = svg2rlg(doc_final)
-
- if p_size == 'Bounds':
- renderPDF.drawToFile(drawing, file_name)
- else:
- if orientation == 'p':
- page_size = portrait(self.pagesize[p_size])
- else:
- page_size = landscape(self.pagesize[p_size])
-
- my_canvas = canvas.Canvas(file_name, pagesize=page_size)
- my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
- renderPDF.draw(drawing, my_canvas, 0, 0)
- my_canvas.save()
- except Exception as e:
- self.log.error("MenuFileHandlers.save_pdf() --> PDF output --> %s" % str(e))
- return 'fail'
-
- self.inform.emit('[success] %s: %s' % (_("PDF file saved to"), file_name))
-
- def export_svg(self, obj_name, filename, scale_stroke_factor=0.00):
- """
- Exports a Geometry Object to an SVG file.
-
- :param obj_name: the name of the FlatCAM object to be saved as SVG
- :param filename: Path to the SVG file to save to.
- :param scale_stroke_factor: factor by which to change/scale the thickness of the features
- :return:
- """
- if filename is None:
- filename = self.app.options["global_last_save_folder"] if \
- self.app.options["global_last_save_folder"] is not None else self.app.options["global_last_folder"]
-
- self.log.debug("export_svg()")
-
- try:
- obj = self.app.collection.get_by_name(str(obj_name))
- except Exception:
- return 'fail'
-
- with self.app.proc_container.new(_("Exporting ...")):
- exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor)
-
- # Determine bounding area for svg export
- bounds = obj.bounds()
- size = obj.size()
-
- # Convert everything to strings for use in the xml doc
- svgwidth = str(size[0])
- svgheight = str(size[1])
- minx = str(bounds[0])
- miny = str(bounds[1] - size[1])
- uom = obj.units.lower()
-
- # Add a SVG Header and footer to the svg output from shapely
- # The transform flips the Y Axis so that everything renders
- # properly within svg apps such as inkscape
- svg_header = ''
- svg_elem = svg_header + exported_svg + svg_footer
-
- # Parse the xml through a xml parser just to add line feeds
- # and to make it look more pretty for the output
- svgcode = parse_xml_string(svg_elem)
- svgcode = svgcode.toprettyxml()
-
- try:
- with open(filename, 'w') as fp:
- fp.write(svgcode)
- except PermissionError:
- self.inform.emit('[WARNING] %s' %
- _("Permission denied, saving not possible.\n"
- "Most likely another app is holding the file open and not accessible."))
- return 'fail'
-
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("SVG", filename)
- self.app.file_saved.emit("SVG", filename)
- self.inform.emit('[success] %s: %s' % (_("SVG file exported to"), filename))
-
- def on_import_preferences(self):
- """
- Loads the application default settings from a saved file into
- ``self.options`` dictionary.
-
- :return: None
- """
-
- self.log.debug("App.on_import_preferences()")
-
- # Show file chooser
- filter_ = "Config File (*.FlatConfig);;All Files (*.*)"
- try:
- filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Preferences"),
- directory=self.app.data_path,
- filter=filter_)
- except TypeError:
- filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Preferences"),
- filter=filter_)
- filename = str(filename)
- if filename == "":
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- return
-
- # Load in the options from the chosen file
- self.options.load(filename=filename, inform=self.inform)
-
- self.app.preferencesUiManager.on_preferences_edited()
- self.inform.emit('[success] %s: %s' % (_("Imported Defaults from"), filename))
-
- def on_export_preferences(self):
- """
- Save the options dictionary to a file.
-
- :return: None
- """
- self.log.debug("on_export_preferences()")
-
- # defaults_file_content = None
-
- # Show file chooser
- date = str(datetime.today()).rpartition('.')[0]
- date = ''.join(c for c in date if c not in ':-')
- date = date.replace(' ', '_')
- filter__ = "Config File .FlatConfig (*.FlatConfig);;All Files (*.*)"
- try:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Export FlatCAM Preferences"),
- directory=os.path.join(self.app.data_path, 'preferences_%s' % date),
- ext_filter=filter__
- )
- except TypeError:
- filename, _f = FCFileSaveDialog.get_saved_filename(
- caption=_("Export FlatCAM Preferences"), ext_filter=filter__)
- filename = str(filename)
- if filename == "":
- self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
- return 'fail'
-
- # Update options
- self.app.preferencesUiManager.defaults_read_form()
- self.options.propagate_defaults()
-
- # Save update options
- try:
- self.options.write(filename=filename)
- except Exception:
- self.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed to write defaults to file."), str(filename)))
- return
-
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("preferences", filename)
- self.app.file_saved.emit("preferences", filename)
- self.inform.emit('[success] %s: %s' % (_("Exported preferences to"), filename))
-
- def export_excellon(self, obj_name, filename, local_use=None, use_thread=True):
- """
- Exports a Excellon Object to an Excellon file.
-
- :param obj_name: the name of the FlatCAM object to be saved as Excellon
- :param filename: Path to the Excellon file to save to.
- :param local_use:
- :param use_thread: if to be run in a separate thread
- :return:
- """
-
- if filename is None:
- if self.app.options["global_last_save_folder"]:
- filename = self.app.options["global_last_save_folder"] + '/' + 'exported_excellon'
- else:
- filename = self.app.options["global_last_folder"] + '/' + 'exported_excellon'
-
- self.log.debug("export_excellon()")
-
- format_exc = ';FILE_FORMAT=%d:%d\n' % (self.options["excellon_exp_integer"],
- self.options["excellon_exp_decimals"]
- )
-
- if local_use is None:
- try:
- obj = self.app.collection.get_by_name(str(obj_name))
- except Exception:
- return "Could not retrieve object: %s" % obj_name
- else:
- obj = local_use
-
- if not isinstance(obj, ExcellonObject):
- self.inform.emit('[ERROR_NOTCL] %s' %
- _("Failed. Only Excellon objects can be saved as Excellon files..."))
- return
-
- # updated units
- eunits = self.options["excellon_exp_units"]
- ewhole = self.options["excellon_exp_integer"]
- efract = self.options["excellon_exp_decimals"]
- ezeros = self.options["excellon_exp_zeros"]
- eformat = self.options["excellon_exp_format"]
- slot_type = self.options["excellon_exp_slot_type"]
-
- fc_units = self.app_units.upper()
- if fc_units == 'MM':
- factor = 1 if eunits == 'METRIC' else 0.03937
- else:
- factor = 25.4 if eunits == 'METRIC' else 1
-
- def make_excellon():
- try:
- time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
-
- header = 'M48\n'
- header += ';EXCELLON GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
- (str(self.app.version), str(self.app.version_date))
-
- header += ';Filename: %s' % str(obj_name) + '\n'
- header += ';Created on : %s' % time_str + '\n'
-
- if eformat == 'dec':
- has_slots, excellon_code = obj.export_excellon(ewhole, efract, factor=factor, slot_type=slot_type)
- header += eunits + '\n'
-
- for tool in obj.tools:
- if eunits == 'METRIC':
- header += "T{tool}F00S00C{:.{dec}f}\n".format(float(obj.tools[tool]['tooldia']) * factor,
- tool=str(tool),
- dec=2)
- else:
- header += "T{tool}F00S00C{:.{dec}f}\n".format(float(obj.tools[tool]['tooldia']) * factor,
- tool=str(tool),
- dec=4)
- else:
- if ezeros == 'LZ':
- has_slots, excellon_code = obj.export_excellon(ewhole, efract,
- form='ndec', e_zeros='LZ', factor=factor,
- slot_type=slot_type)
- header += '%s,%s\n' % (eunits, 'LZ')
- header += format_exc
-
- for tool in obj.tools:
- if eunits == 'METRIC':
- header += "T{tool}F00S00C{:.{dec}f}\n".format(
- float(obj.tools[tool]['tooldia']) * factor,
- tool=str(tool),
- dec=2)
- else:
- header += "T{tool}F00S00C{:.{dec}f}\n".format(
- float(obj.tools[tool]['tooldia']) * factor,
- tool=str(tool),
- dec=4)
- else:
- has_slots, excellon_code = obj.export_excellon(ewhole, efract,
- form='ndec', e_zeros='TZ', factor=factor,
- slot_type=slot_type)
- header += '%s,%s\n' % (eunits, 'TZ')
- header += format_exc
-
- for tool in obj.tools:
- if eunits == 'METRIC':
- header += "T{tool}F00S00C{:.{dec}f}\n".format(
- float(obj.tools[tool]['tooldia']) * factor,
- tool=str(tool),
- dec=2)
- else:
- header += "T{tool}F00S00C{:.{dec}f}\n".format(
- float(obj.tools[tool]['tooldia']) * factor,
- tool=str(tool),
- dec=4)
- header += '%\n'
- footer = 'M30\n'
-
- exported_excellon = header
- exported_excellon += excellon_code
- exported_excellon += footer
-
- if local_use is None:
- try:
- with open(filename, 'w') as fp:
- fp.write(exported_excellon)
- except PermissionError:
- self.inform.emit('[WARNING] %s' %
- _("Permission denied, saving not possible.\n"
- "Most likely another app is holding the file open and not accessible."))
- return 'fail'
-
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("Excellon", filename)
- self.app.file_saved.emit("Excellon", filename)
- self.inform.emit('[success] %s: %s' % (_("Excellon file exported to"), filename))
- else:
- return exported_excellon
- except Exception as e:
- self.log.error("App.export_excellon.make_excellon() --> %s" % str(e))
- return 'fail'
-
- if use_thread is True:
-
- with self.app.proc_container.new(_("Exporting ...")):
-
- def job_thread_exc(app_obj):
- ret = make_excellon()
- if ret == 'fail':
- app_obj.inform.emit('[ERROR_NOTCL] %s' % _('Could not export.'))
- return
-
- self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]})
- else:
- eret = make_excellon()
- if eret == 'fail':
- self.inform.emit('[ERROR_NOTCL] %s' % _('Could not export.'))
- return 'fail'
- if local_use is not None:
- return eret
-
- def export_gerber(self, obj_name, filename, local_use=None, use_thread=True):
- """
- Exports a Gerber Object to an Gerber file.
-
- :param obj_name: the name of the FlatCAM object to be saved as Gerber
- :param filename: Path to the Gerber file to save to.
- :param local_use: if the Gerber code is to be saved to a file (None) or used within FlatCAM.
- When not None, the value will be the actual Gerber object for which to create
- the Gerber code
- :param use_thread: if to be run in a separate thread
- :return:
- """
- if filename is None:
- filename = self.app.options["global_last_save_folder"] if \
- self.app.options["global_last_save_folder"] is not None else self.app.options["global_last_folder"]
-
- self.log.debug("export_gerber()")
-
- if local_use is None:
- try:
- obj = self.app.collection.get_by_name(str(obj_name))
- except Exception:
- return 'fail'
- else:
- obj = local_use
-
- # updated units
- gunits = self.options["gerber_exp_units"]
- gwhole = self.options["gerber_exp_integer"]
- gfract = self.options["gerber_exp_decimals"]
- gzeros = self.options["gerber_exp_zeros"]
-
- fc_units = self.app_units.upper()
- if fc_units == 'MM':
- factor = 1 if gunits == 'MM' else 0.03937
- else:
- factor = 25.4 if gunits == 'MM' else 1
-
- def make_gerber():
- try:
- time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
-
- header = 'G04*\n'
- header += 'G04 RS-274X GERBER GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' % \
- (str(self.app.version), str(self.app.version_date))
-
- header += 'G04 Filename: %s*' % str(obj_name) + '\n'
- header += 'G04 Created on : %s*' % time_str + '\n'
- header += '%%FS%sAX%s%sY%s%s*%%\n' % (gzeros, gwhole, gfract, gwhole, gfract)
- header += "%MO{units}*%\n".format(units=gunits)
-
- for apid in obj.tools:
- if obj.tools[apid]['type'] == 'C':
- header += "%ADD{apid}{type},{size}*%\n".format(
- apid=str(apid),
- type='C',
- size=(factor * obj.tools[apid]['size'])
- )
- elif obj.tools[apid]['type'] == 'R':
- header += "%ADD{apid}{type},{width}X{height}*%\n".format(
- apid=str(apid),
- type='R',
- width=(factor * obj.tools[apid]['width']),
- height=(factor * obj.tools[apid]['height'])
- )
- elif obj.tools[apid]['type'] == 'O':
- header += "%ADD{apid}{type},{width}X{height}*%\n".format(
- apid=str(apid),
- type='O',
- width=(factor * obj.tools[apid]['width']),
- height=(factor * obj.tools[apid]['height'])
- )
-
- header += '\n'
-
- # obsolete units but some software may need it
- if gunits == 'IN':
- header += 'G70*\n'
- else:
- header += 'G71*\n'
-
- # Absolute Mode
- header += 'G90*\n'
-
- header += 'G01*\n'
- # positive polarity
- header += '%LPD*%\n'
-
- footer = 'M02*\n'
-
- gerber_code = obj.export_gerber(gwhole, gfract, g_zeros=gzeros, factor=factor)
-
- exported_gerber = header
- exported_gerber += gerber_code
- exported_gerber += footer
-
- if local_use is None:
- try:
- with open(filename, 'w') as fp:
- fp.write(exported_gerber)
- except PermissionError:
- self.inform.emit('[WARNING] %s' %
- _("Permission denied, saving not possible.\n"
- "Most likely another app is holding the file open and not accessible."))
- return 'fail'
-
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("Gerber", filename)
- self.app.file_saved.emit("Gerber", filename)
- self.inform.emit('[success] %s: %s' % (_("Gerber file exported to"), filename))
- else:
- return exported_gerber
- except Exception as e:
- self.log.error("App.export_gerber.make_gerber() --> %s" % str(e))
- return 'fail'
-
- if use_thread is True:
- with self.app.proc_container.new(_("Exporting ...")):
-
- def job_thread_grb(app_obj):
- ret = make_gerber()
- if ret == 'fail':
- app_obj.inform.emit('[ERROR_NOTCL] %s' % _('Could not export.'))
- return 'fail'
-
- self.worker_task.emit({'fcn': job_thread_grb, 'params': [self]})
- else:
- gret = make_gerber()
- if gret == 'fail':
- self.inform.emit('[ERROR_NOTCL] %s' % _('Could not export.'))
- return 'fail'
- if local_use is not None:
- return gret
-
- def export_dxf(self, obj_name, filename, local_use=None, use_thread=True):
- """
- Exports a Geometry Object to an DXF file.
-
- :param obj_name: the name of the FlatCAM object to be saved as DXF
- :param filename: Path to the DXF file to save to.
- :param local_use: if the Gerber code is to be saved to a file (None) or used within FlatCAM.
- When not None, the value will be the actual Geometry object for which to create
- the Geometry/DXF code
- :param use_thread: if to be run in a separate thread
- :return:
- """
- if filename is None:
- filename = self.app.options["global_last_save_folder"] if \
- self.app.options["global_last_save_folder"] is not None else self.app.options["global_last_folder"]
-
- self.log.debug("export_dxf()")
-
- if local_use is None:
- try:
- obj = self.app.collection.get_by_name(str(obj_name))
- except Exception:
- return 'fail'
- else:
- obj = local_use
-
- def make_dxf():
- try:
- dxf_code = obj.export_dxf()
- if local_use is None:
- try:
- dxf_code.saveas(filename)
- except PermissionError:
- self.inform.emit('[WARNING] %s' %
- _("Permission denied, saving not possible.\n"
- "Most likely another app is holding the file open and not accessible."))
- return 'fail'
-
- if self.options["global_open_style"] is False:
- self.app.file_opened.emit("DXF", filename)
- self.app.file_saved.emit("DXF", filename)
- self.inform.emit('[success] %s: %s' % (_("DXF file exported to"), filename))
- else:
- return dxf_code
- except Exception as e:
- self.log.error("App.export_dxf.make_dxf() --> %s" % str(e))
- return 'fail'
-
- if use_thread is True:
-
- with self.app.proc_container.new(_("Exporting ...")):
-
- def job_thread_exc(app_obj):
- ret_dxf_val = make_dxf()
- if ret_dxf_val == 'fail':
- app_obj.inform.emit('[WARNING_NOTCL] %s' % _('Could not export.'))
- return
-
- self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]})
- else:
- ret = make_dxf()
- if ret == 'fail':
- self.inform.emit('[WARNING_NOTCL] %s' % _('Could not export.'))
- return
- if local_use is not None:
- return ret
-
- def import_svg(self, filename, geo_type='geometry', outname=None, plot=True):
- """
- Adds a new Geometry Object to the projects and populates
- it with shapes extracted from the SVG file.
-
- :param plot: If True then the resulting object will be plotted on canvas
- :param filename: Path to the SVG file.
- :param geo_type: Type of FlatCAM object that will be created from SVG
- :param outname: The name given to the resulting FlatCAM object
- :return:
- """
- self.log.debug("App.import_svg()")
- if not os.path.exists(filename):
- self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
- return
-
- obj_type = ""
- if geo_type is None or geo_type == "geometry":
- obj_type = "geometry"
- elif geo_type == "gerber":
- obj_type = "gerber"
- else:
- self.inform.emit('[ERROR_NOTCL] %s' %
- _("Not supported type is picked as parameter. Only Geometry and Gerber are supported"))
- return
-
- units = self.app_units.upper()
-
- def obj_init(geo_obj, app_obj):
- res = geo_obj.import_svg(filename, obj_type, units=units)
- if res == 'fail':
- return 'fail'
-
- geo_obj.multigeo = True
-
- with open(filename) as f:
- file_content = f.read()
- geo_obj.source_file = file_content
-
- # appGUI feedback
- app_obj.inform.emit('[success] %s: %s' % (_("Opened"), filename))
-
- with self.app.proc_container.new('%s ...' % _("Importing")):
-
- # Object name
- name = outname or filename.split('/')[-1].split('\\')[-1]
-
- ret = self.app.app_obj.new_object(obj_type, name, obj_init, autoselected=False, plot=plot)
-
- if ret == 'fail':
- self.inform.emit('[ERROR_NOTCL]%s' % _('Import failed.'))
- return 'fail'
-
- # Register recent file
- self.app.file_opened.emit("svg", filename)
-
- def import_dxf(self, filename, geo_type='geometry', outname=None, plot=True):
- """
- Adds a new Geometry Object to the projects and populates
- it with shapes extracted from the DXF file.
-
- :param filename: Path to the DXF file.
- :param geo_type: Type of FlatCAM object that will be created from DXF
- :param outname: Name for the imported Geometry
- :param plot: If True then the resulting object will be plotted on canvas
- :return:
- """
- self.log.debug(" ********* Importing DXF as: %s ********* " % geo_type.capitalize())
- if not os.path.exists(filename):
- self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
- return
-
- obj_type = ""
- if geo_type is None or geo_type == "geometry":
- obj_type = "geometry"
- elif geo_type == "gerber":
- obj_type = geo_type
- else:
- self.inform.emit('[ERROR_NOTCL] %s' %
- _("Not supported type is picked as parameter. Only Geometry and Gerber are supported"))
- return
-
- units = self.app_units.upper()
-
- def obj_init(geo_obj, app_obj):
- if obj_type == "geometry":
- geo_obj.import_dxf_as_geo(filename, units=units)
- elif obj_type == "gerber":
- geo_obj.import_dxf_as_gerber(filename, units=units)
- else:
- return "fail"
-
- with open(filename) as f:
- file_content = f.read()
- geo_obj.source_file = file_content
-
- # appGUI feedback
- app_obj.inform.emit('[success] %s: %s' % (_("Opened"), filename))
-
- with self.app.proc_container.new('%s ...' % _("Importing")):
-
- # Object name
- name = outname or filename.split('/')[-1].split('\\')[-1]
-
- ret = self.app.app_obj.new_object(obj_type, name, obj_init, autoselected=False, plot=plot)
-
- if ret == 'fail':
- self.inform.emit('[ERROR_NOTCL]%s' % _('Import failed.'))
- return 'fail'
-
- # Register recent file
- self.app.file_opened.emit("dxf", filename)
-
- def import_pdf(self, filename):
- self.app.pdf_tool.periodic_check(1000)
- self.worker_task.emit({'fcn': self.app.pdf_tool.open_pdf, 'params': [filename]})
-
- def open_gerber(self, filename, outname=None, plot=True, from_tcl=False):
- """
- Opens a Gerber file, parses it and creates a new object for
- it in the program. Thread-safe.
-
- :param outname: Name of the resulting object. None causes the
- name to be that of the file. Str.
- :param filename: Gerber file filename
- :type filename: str
- :param plot: boolean, to plot or not the resulting object
- :param from_tcl: True if run from Tcl Shell
- :return: None
- """
-
- # How the object should be initialized
- def obj_init(gerber_obj, app_obj):
-
- assert isinstance(gerber_obj, GerberObject), \
- "Expected to initialize a GerberObject but got %s" % type(gerber_obj)
-
- # Opening the file happens here
- try:
- parse_ret_val = gerber_obj.parse_file(filename)
- except IOError:
- app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open file"), filename))
- return "fail"
- except ParseError as parse_err:
- app_obj.inform.emit('[ERROR_NOTCL] %s: %s. %s' % (_("Failed to parse file"), filename, str(parse_err)))
- app_obj.log.error(str(parse_err))
- return "fail"
- except Exception as e:
- app_obj.log.error("App.open_gerber() --> %s" % str(e))
- msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
- msg += traceback.format_exc()
- app_obj.inform.emit(msg)
- return "fail"
-
- if gerber_obj.is_empty():
- app_obj.inform.emit('[ERROR_NOTCL] %s' %
- _("Object is not Gerber file or empty. Aborting object creation."))
- return "fail"
-
- if parse_ret_val:
- return parse_ret_val
-
- self.log.debug("open_gerber()")
- if not os.path.exists(filename):
- self.inform.emit('[ERROR_NOTCL] %s. %s' % (filename, _("File no longer available.")))
- return
-
- with self.app.proc_container.new('%s...' % _("Opening")):
- # Object name
- name = outname or filename.split('/')[-1].split('\\')[-1]
-
- # # ## Object creation # ##
- ret_val = self.app.app_obj.new_object("gerber", name, obj_init, autoselected=False, plot=plot)
- if ret_val == 'fail':
- if from_tcl:
- filename = self.options['global_tcl_path'] + '/' + name
- ret_val = self.app.app_obj.new_object("gerber", name, obj_init, autoselected=False, plot=plot)
- if ret_val == 'fail':
- self.inform.emit('[ERROR_NOTCL]%s' % _('Open Gerber failed. Probable not a Gerber file.'))
- return 'fail'
-
- # Register recent file
- self.app.file_opened.emit("gerber", filename)
-
- # appGUI feedback
- self.app.inform.emit('[success] %s: %s' % (_("Opened"), filename))
-
- def open_excellon(self, filename, outname=None, plot=True, from_tcl=False):
- """
- Opens an Excellon file, parses it and creates a new object for
- it in the program. Thread-safe.
-
- :param outname: Name of the resulting object. None causes the name to be that of the file.
- :param filename: Excellon file filename
- :type filename: str
- :param plot: boolean, to plot or not the resulting object
- :param from_tcl: True if run from Tcl Shell
- :return: None
- """
-
- self.log.debug("open_excellon()")
-
- if not os.path.exists(filename):
- self.inform.emit('[ERROR_NOTCL] %s. %s' % (filename, _("File no longer available.")))
- return
-
- # How the object should be initialized
- def obj_init(excellon_obj, app_obj):
- # populate excellon_obj.tools dict
- try:
- ret = excellon_obj.parse_file(filename=filename)
- if ret == "fail":
- app_obj.log.debug("Excellon parsing failed.")
- self.inform.emit('[ERROR_NOTCL] %s' % _("This is not Excellon file."))
- return "fail"
- except IOError:
- app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Cannot open file"), filename))
- app_obj.log.debug("Could not open Excellon object.")
- return "fail"
- except Exception:
- msg = '[ERROR_NOTCL] %s' % _("An internal error has occurred. See shell.\n")
- msg += traceback.format_exc()
- app_obj.inform.emit(msg)
- return "fail"
-
- # populate excellon_obj.solid_geometry list
- ret = excellon_obj.create_geometry()
- if ret == 'fail':
- app_obj.log.debug("Could not create geometry for Excellon object.")
- return "fail"
-
- for tool in excellon_obj.tools:
- if excellon_obj.tools[tool]['solid_geometry']:
- return
- app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("No geometry found in file"), filename))
- return "fail"
-
- with self.app.proc_container.new('%s...' % _("Opening")):
- # Object name
- name = outname or filename.split('/')[-1].split('\\')[-1]
- ret_val = self.app.app_obj.new_object("excellon", name, obj_init, autoselected=False, plot=plot)
- if ret_val == 'fail':
- if from_tcl:
- filename = self.options['global_tcl_path'] + '/' + name
- ret_val = self.app.app_obj.new_object("excellon", name, obj_init, autoselected=False, plot=plot)
- if ret_val == 'fail':
- self.inform.emit('[ERROR_NOTCL] %s' %
- _('Open Excellon file failed. Probable not an Excellon file.'))
- return
-
- # Register recent file
- self.app.file_opened.emit("excellon", filename)
-
- # appGUI feedback
- self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
-
- def open_gcode(self, filename, outname=None, force_parsing=None, plot=True, from_tcl=False):
- """
- Opens a G-gcode file, parses it and creates a new object for
- it in the program. Thread-safe.
-
- :param filename: G-code file filename
- :param outname: Name of the resulting object. None causes the name to be that of the file.
- :param force_parsing:
- :param plot: If True plot the object on canvas
- :param from_tcl: True if run from Tcl Shell
- :return: None
- """
- self.log.debug("open_gcode()")
-
- if not os.path.exists(filename):
- self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
- return
-
- # How the object should be initialized
- def obj_init(job_obj, app_obj_):
- """
- :param job_obj: the resulting object
- :type app_obj_: App
- """
- assert isinstance(app_obj_, App), \
- "Initializer expected App, got %s" % type(app_obj_)
-
- app_obj_.inform.emit('%s...' % _("Reading GCode file"))
- try:
- f = open(filename)
- gcode = f.read()
- f.close()
- except IOError:
- app_obj_.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open"), filename))
- return "fail"
-
- # try to find from what kind of object this GCode was created
- gcode_origin = 'Geometry'
- match = re.search(r'^.*Type:\s*.*(\bGeometry\b|\bExcellon\b)', gcode, re.MULTILINE)
- if match:
- gcode_origin = match.group(1)
- job_obj.obj_options['type'] = gcode_origin
- # add at least one default tool
- if 'excellon' in gcode_origin.lower():
- job_obj.tools = {1: {'data': {'tools_drill_ppname_e': 'default'}}}
- if 'geometry' in gcode_origin.lower():
- job_obj.tools = {1: {'data': {'tools_mill_ppname_g': 'default'}}}
-
- # try to find from what kind of object this GCode was created
- match = re.search(r'^.*Preprocessor:\s*.*\bGeometry\b|\bExcellon\b:\s(\b.*\b)', gcode, re.MULTILINE)
- detected_preprocessor = 'default'
- if match:
- detected_preprocessor = match.group(1)
- # determine if there is any tool data
- match = re.findall(r'^.*Tool:\s*([0-9]*)\s*->\s*Dia:\s*(\d*\.?\d*)', gcode, re.MULTILINE)
- if match:
- job_obj.tools = {}
- for m in match:
- if 'excellon' in gcode_origin.lower():
- job_obj.tools[int(m[0])] = {
- 'tooldia': float(m[1]),
- 'nr_drills': 0,
- 'nr_slots': 0,
- 'offset_z': 0,
- 'data': {'tools_drill_ppname_e': detected_preprocessor}
- }
- # if 'geometry' in gcode_origin.lower():
- # job_obj.tools[int(m[0])] = {
- # 'tooldia': float(m[1]),
- # 'data': {
- # 'tools_mill_ppname_g': detected_preprocessor,
- # 'tools_mill_offset_value': 0.0,
- # 'tools_mill_job_type': _('Roughing'),
- # 'tools_mill_tool_shape': "C1"
- #
- # }
- # }
- job_obj.used_tools = list(job_obj.tools.keys())
- # determine if there is any Cut Z data
- match = re.findall(r'^.*Tool:\s*([0-9]*)\s*->\s*Z_Cut:\s*([\-|+]?\d*\.?\d*)', gcode, re.MULTILINE)
- if match:
- for m in match:
- if 'excellon' in gcode_origin.lower():
- if int(m[0]) in job_obj.tools:
- job_obj.tools[int(m[0])]['offset_z'] = 0.0
- job_obj.tools[int(m[0])]['data']['tools_drill_cutz'] = float(m[1])
- # if 'geometry' in gcode_origin.lower():
- # if int(m[0]) in job_obj.tools:
- # job_obj.tools[int(m[0])]['data']['tools_mill_cutz'] = float(m[1])
-
- job_obj.gcode = gcode
-
- gcode_ret = job_obj.gcode_parse(force_parsing=force_parsing)
- if gcode_ret == "fail":
- self.inform.emit('[ERROR_NOTCL] %s' % _("This is not GCODE"))
- return "fail"
-
- for k in job_obj.tools:
- job_obj.tools[k]['gcode'] = gcode
- job_obj.tools[k]['gcode_parsed'] = []
-
- for k in job_obj.tools:
- print(k, job_obj.tools[k])
- job_obj.create_geometry()
-
- with self.app.proc_container.new('%s...' % _("Opening")):
-
- # Object name
- name = outname or filename.split('/')[-1].split('\\')[-1]
-
- # New object creation and file processing
- ret_val = self.app.app_obj.new_object("cncjob", name, obj_init, autoselected=False, plot=plot)
- if ret_val == 'fail':
- if from_tcl:
- filename = self.options['global_tcl_path'] + '/' + name
- ret_val = self.app.app_obj.new_object("cncjob", name, obj_init, autoselected=False, plot=plot)
- if ret_val == 'fail':
- self.inform.emit('[ERROR_NOTCL] %s' %
- _("Failed to create CNCJob Object. Probable not a GCode file. "
- "Try to load it from File menu.\n "
- "Attempting to create a FlatCAM CNCJob Object from "
- "G-Code file failed during processing"))
- return "fail"
-
- # Register recent file
- self.app.file_opened.emit("cncjob", filename)
-
- # appGUI feedback
- self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
-
- def open_hpgl2(self, filename, outname=None):
- """
- Opens a HPGL2 file, parses it and creates a new object for
- it in the program. Thread-safe.
-
- :param outname: Name of the resulting object. None causes the name to be that of the file.
- :param filename: HPGL2 file filename
- :return: None
- """
- filename = filename
-
- # How the object should be initialized
- def obj_init(geo_obj, app_obj):
-
- assert isinstance(geo_obj, GeometryObject), \
- "Expected to initialize a GeometryObject but got %s" % type(geo_obj)
-
- # Opening the file happens here
- obj = HPGL2(self.app)
- try:
- HPGL2.parse_file(obj, filename)
- except IOError:
- app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open file"), filename))
- return "fail"
- except ParseError as parse_err:
- app_obj.inform.emit('[ERROR_NOTCL] %s: %s. %s' % (_("Failed to parse file"), filename, str(parse_err)))
- app_obj.log.error(str(parse_err))
- return "fail"
- except Exception as e:
- app_obj.log.error("App.open_hpgl2() --> %s" % str(e))
- msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
- msg += traceback.format_exc()
- app_obj.inform.emit(msg)
- return "fail"
-
- geo_obj.multigeo = True
- geo_obj.solid_geometry = deepcopy(obj.solid_geometry)
- geo_obj.tools = deepcopy(obj.tools)
- geo_obj.source_file = deepcopy(obj.source_file)
-
- del obj
-
- if not geo_obj.solid_geometry:
- app_obj.inform.emit('[ERROR_NOTCL] %s' %
- _("Object is not HPGL2 file or empty. Aborting object creation."))
- return "fail"
-
- self.log.debug("open_hpgl2()")
-
- with self.app.proc_container.new('%s...' % _("Opening")):
- # Object name
- name = outname or filename.split('/')[-1].split('\\')[-1]
-
- # # ## Object creation # ##
- ret = self.app.app_obj.new_object("geometry", name, obj_init, autoselected=False)
- if ret == 'fail':
- self.inform.emit('[ERROR_NOTCL]%s' % _('Failed. Probable not a HPGL2 file.'))
- return 'fail'
-
- # Register recent file
- self.app.file_opened.emit("geometry", filename)
-
- # appGUI feedback
- self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
-
- def open_script(self, filename, outname=None, silent=False):
- """
- Opens a Script file, parses it and creates a new object for
- it in the program. Thread-safe.
-
- :param outname: Name of the resulting object. None causes the name to be that of the file.
- :param filename: Script file filename
- :param silent: If True there will be no messages printed to StatusBar
- :return: None
- """
-
- def obj_init(script_obj, app_obj):
-
- assert isinstance(script_obj, ScriptObject), \
- "Expected to initialize a ScriptObject but got %s" % type(script_obj)
-
- if silent is False:
- app_obj.inform.emit('[success] %s' % _("TCL script file opened in Code Editor."))
-
- try:
- script_obj.parse_file(filename)
- except IOError:
- app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open file"), filename))
- return "fail"
- except ParseError as parse_err:
- app_obj.inform.emit('[ERROR_NOTCL] %s: %s. %s' % (_("Failed to parse file"), filename, str(parse_err)))
- app_obj.log.error(str(parse_err))
- return "fail"
- except Exception as e:
- app_obj.log.error("App.open_script() -> %s" % str(e))
- msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
- msg += traceback.format_exc()
- app_obj.inform.emit(msg)
- return "fail"
-
- self.log.debug("open_script()")
- if not os.path.exists(filename):
- self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
- return
-
- with self.app.proc_container.new('%s...' % _("Opening")):
-
- # Object name
- script_name = outname or filename.split('/')[-1].split('\\')[-1]
-
- # Object creation
- ret_val = self.app.app_obj.new_object("script", script_name, obj_init, autoselected=False, plot=False)
- if ret_val == 'fail':
- filename = self.options['global_tcl_path'] + '/' + script_name
- ret_val = self.app.app_obj.new_object("script", script_name, obj_init, autoselected=False, plot=False)
- if ret_val == 'fail':
- self.inform.emit('[ERROR_NOTCL]%s' % _('Failed to open TCL Script.'))
- return 'fail'
-
- # Register recent file
- self.app.file_opened.emit("script", filename)
-
- # appGUI feedback
- self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
-
- def open_config_file(self, filename, run_from_arg=None):
- """
- Loads a config file from the specified file.
-
- :param filename: Name of the file from which to load.
- :param run_from_arg: if True the FlatConfig file will be open as an command line argument
- :return: None
- """
- self.log.debug("Opening config file: " + filename)
-
- if run_from_arg:
- self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
- "Canvas initialization finished in"),
- '%.2f' % self.app.used_time,
- _("Opening FlatCAM Config file.")),
- alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
- color=QtGui.QColor("lightgray"))
- # # add the tab if it was closed
- # self.ui.plot_tab_area.addTab(self.ui.text_editor_tab, _("Code Editor"))
- # # first clear previous text in text editor (if any)
- # self.ui.text_editor_tab.code_editor.clear()
- #
- # # Switch plot_area to CNCJob tab
- # self.ui.plot_tab_area.setCurrentWidget(self.ui.text_editor_tab)
-
- # close the Code editor if already open
- if self.app.toggle_codeeditor:
- self.app.on_toggle_code_editor()
-
- self.app.on_toggle_code_editor()
-
- try:
- if filename:
- f = QtCore.QFile(filename)
- if f.open(QtCore.QIODevice.OpenModeFlag.ReadOnly):
- stream = QtCore.QTextStream(f)
- code_edited = stream.readAll()
- self.app.text_editor_tab.load_text(code_edited, clear_text=True, move_to_start=True)
- f.close()
- except IOError:
- self.log.error("Failed to open config file: %s" % filename)
- self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open config file"), filename))
- return
-
- def open_project(self, filename, run_from_arg=False, plot=True, cli=False, from_tcl=False):
- """
- Loads a project from the specified file.
-
- 1) Loads and parses file
- 2) Registers the file as recently opened.
- 3) Calls on_file_new_project()
- 4) Updates options
- 5) Calls app_obj.new_object() with the object's from_dict() as init method.
- 6) Calls plot_all() if plot=True
-
- :param filename: Name of the file from which to load.
- :param run_from_arg: True if run for arguments
- :param plot: If True plot all objects in the project
- :param cli: Run from command line
- :param from_tcl: True if run from Tcl Sehll
- :return: None
- """
-
- 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
-
- # block autosaving while a project is loaded
- self.app.block_autosave = True
-
- # 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=_("Loading Project ... Please Wait ..."))
-
- if run_from_arg:
- self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
- "Canvas initialization finished in"),
- '%.2f' % self.app.used_time,
- _("Opening FlatCAM Project file.")),
- alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
- color=QtGui.QColor("lightgray"))
-
- def parse_worker(prj_filename):
- with self.app.proc_container.new('%s' % _("Parsing...")):
- # Open and parse an uncompressed Project file
- try:
- f = open(prj_filename, 'r')
- except IOError:
- 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
-
- 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 + '/app128.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:
- pass
- elif cli is True:
- self.app.delete_selection_shape()
- else:
- self.on_file_new_project()
-
- if not run_from_arg or not cli or from_tcl is False:
- msgbox = FCMessageBox(parent=self.app.ui)
- title = _("Import Settings")
- txt = _("Do you want to import the loaded project settings?")
- msgbox.setWindowTitle(title) # taskbar still shows it
- msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
- msgbox.setText('%s' % title)
- msgbox.setInformativeText(txt)
- msgbox.setIconPixmap(QtGui.QPixmap(self.app.resource_location + '/import.png'))
-
- bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.ButtonRole.YesRole)
- bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.ButtonRole.NoRole)
- # bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.ButtonRole.RejectRole)
-
- msgbox.setDefaultButton(bt_yes)
- msgbox.exec()
- response = msgbox.clickedButton()
-
- if response == bt_yes:
- # self.app.defaults.update(self.app.options)
- # self.app.preferencesUiManager.save_defaults()
- # Project 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(proj_dict['options'])
-
- self.app.project_filename = filename
-
- # 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)
- if cli is None:
- self.app.set_screen_units(self.app.options["units"])
-
- self.app.restore_project_objects_sig.emit(proj_dict, filename, cli, plot)
-
- def restore_project_objects(self, proj_dict, filename, cli, plot):
-
- 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 obj_init(new_obj, app_inst):
- try:
- new_obj.from_dict(obj)
- except Exception as erro:
- app_inst.log.error('MenuFileHandlers.open_project() --> ' + str(erro))
- return 'fail'
-
- # make the 'obj_options' dict a LoudDict
- try:
- new_obj_options = LoudDict()
- new_obj_options.update(new_obj.obj_options)
- new_obj.obj_options = new_obj_options
- except AttributeError:
- new_obj_options = LoudDict()
- new_obj_options.update(new_obj.options)
- new_obj.obj_options = new_obj_options
- except Exception as erro:
- app_inst.log.error('MenuFileHandlers.open_project() make a LoudDict--> ' + str(erro))
- return 'fail'
-
- # #############################################################################################
- # for older projects loading try to convert the 'apertures' or 'cnc_tools' or 'exc_cnc_tools'
- # attributes, if found, to 'tools'
- # #############################################################################################
- # for older loaded projects
- if 'apertures' in obj:
- new_obj.tools = obj['apertures']
- if 'cnc_tools' in obj and obj['cnc_tools']:
- new_obj.tools = obj['cnc_tools']
- # new_obj.used_tools = [int(k) for k in new_obj.tools.keys()]
- # first_key = list(obj['cnc_tools'].keys())[0]
- # used_preprocessor = obj['cnc_tools'][first_key]['data']['ppname_g']
- # new_obj.gc_start = new_obj.doformat(self.app.preprocessors[used_preprocessor].start_code)
- if 'exc_cnc_tools' in obj and obj['exc_cnc_tools']:
- new_obj.tools = obj['exc_cnc_tools']
- # add the used_tools (all of them will be used)
- new_obj.used_tools = [float(k) for k in new_obj.tools.keys()]
- # add a missing key, 'tooldia' used for plotting CNCJob objects
- for td in new_obj.tools:
- new_obj.tools[td]['tooldia'] = float(td)
- # #############################################################################################
- # #############################################################################################
-
- # try to make the keys in the tools dictionary to be integers
- # JSON serialization makes them strings
- # not all FlatCAM objects have the 'tools' dictionary attribute
- try:
- new_obj.tools = {
- int(tool): tool_dict for tool, tool_dict in list(new_obj.tools.items())
- }
- except ValueError:
- # for older loaded projects
- new_obj.tools = {
- float(tool): tool_dict for tool, tool_dict in list(new_obj.tools.items())
- }
- except Exception as erro:
- app_inst.log.error('MenuFileHandlers.open_project() keys to int--> ' + str(erro))
- return 'fail'
-
- # #############################################################################################
- # for older loaded projects
- # ony older CNCJob objects hold those
- if 'cnc_tools' in obj:
- new_obj.obj_options['type'] = 'Geometry'
- if 'exc_cnc_tools' in obj:
- new_obj.obj_options['type'] = 'Excellon'
- # #############################################################################################
-
- if new_obj.kind == 'cncjob':
- # some attributes are serialized so we need t otake this into consideration in
- # CNCJob.set_ui()
- new_obj.is_loaded_from_project = True
-
- # 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']))
-
- 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=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):
- """
- Saves the current project to the specified file.
-
- :param filename: Name of the file in which to save.
- :type filename: str
- :param quit_action: if the project saving will be followed by an app quit; boolean
- :param silent: if True will not display status messages
- :param from_tcl True is run from Tcl Shell
- :return: None
- """
- self.log.debug("save_project() -> Saving Project")
- self.app.save_in_progress = True
-
- if from_tcl:
- self.log.debug("MenuFileHandlers.save_project() -> Project saved from TCL command.")
-
- with self.app.proc_container.new(_("Saving Project ...")):
- # Capture the latest changes
- # Current object
- try:
- current_object = self.app.collection.get_active()
- if current_object:
- current_object.read_form()
- except Exception as e:
- self.log.error("save_project() --> There was no active object. Skipping read_form. %s" % str(e))
-
- app_options = {k: v for k, v in self.app.options.items()}
- d = {
- "objs": [obj.to_dict() for obj in self.app.collection.get_list()],
- "options": app_options,
- "version": self.app.version
- }
-
- if self.options["global_save_compressed"] is True:
- try:
- project_as_json = json.dumps(d, default=to_dict, indent=2, sort_keys=True).encode('utf-8')
- except Exception as e:
- self.log.error(
- "Failed to serialize file before compression: %s because: %s" % (str(filename), str(e)))
- self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
- return
-
- try:
- # with lzma.open(filename, "w", preset=int(self.options['global_compression_level'])) as f:
- # # # Write
- # f.write(project_as_json)
-
- compressor_obj = lzma.LZMACompressor(preset=int(self.options['global_compression_level']))
- out1 = compressor_obj.compress(project_as_json)
- out2 = compressor_obj.flush()
- project_zipped = b"".join([out1, out2])
- except Exception as errrr:
- self.log.error("Failed to save compressed file: %s because: %s" % (str(filename), str(errrr)))
- self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
- return
-
- if project_zipped != b'':
- with open(filename, "wb") as f_to_write:
- f_to_write.write(project_zipped)
-
- self.inform.emit('[success] %s: %s' % (_("Project saved to"), str(filename)))
- else:
- self.log.error("Failed to save file: %s. Empty binary file.", str(filename))
- self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
- return
- else:
- # Open file
- try:
- f = open(filename, 'w')
- except IOError:
- self.log.error("Failed to open file for saving: %s", str(filename))
- self.inform.emit('[ERROR_NOTCL] %s' % _("The object is used by another application."))
- return
-
- # Write
- try:
- json.dump(d, f, default=to_dict, indent=2, sort_keys=True)
- except Exception as e:
- self.log.error(
- "Failed to serialize file: %s because: %s" % (str(filename), str(e)))
- self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
- return
- f.close()
-
- # verification of the saved project
- # Open and parse
- try:
- saved_f = open(filename, 'r')
- except IOError:
- if silent is False:
- self.inform.emit('[ERROR_NOTCL] %s: %s %s' %
- (_("Failed to verify project file"), str(filename), _("Retry to save it.")))
- return
-
- try:
- saved_d = json.load(saved_f, object_hook=dict2obj)
- if not saved_d:
- self.inform.emit('[ERROR_NOTCL] %s: %s %s' %
- (_("Failed to parse saved project file"),
- str(filename),
- _("Retry to save it."))) # noqa
- f.close()
- return
- except Exception:
- if silent is False:
- self.inform.emit('[ERROR_NOTCL] %s: %s %s' %
- (_("Failed to parse saved project file"),
- str(filename),
- _("Retry to save it."))) # noqa
- f.close()
- return
-
- saved_f.close()
-
- if silent is False:
- if 'version' in saved_d:
- self.inform.emit('[success] %s: %s' % (_("Project saved to"), str(filename)))
- else:
- self.inform.emit('[ERROR_NOTCL] %s: %s %s' %
- (_("Failed to parse saved project file"),
- str(filename),
- _("Retry to save it."))) # noqa
-
- tb_settings = QSettings("Open Source", "FlatCAM")
- lock_state = self.app.ui.lock_action.isChecked()
- tb_settings.setValue('toolbar_lock', lock_state)
-
- # This will write the setting to the platform specific storage.
- del tb_settings
-
- # if quit:
- # t = threading.Thread(target=lambda: self.check_project_file_size(1, filename=filename))
- # t.start()
- self.app.start_delayed_quit(delay=500, filename=filename, should_quit=quit_action)
-
- def save_source_file(self, obj_name, filename):
- """
- Exports a FlatCAM Object to an Gerber/Excellon file.
-
- :param obj_name: the name of the FlatCAM object for which to save it's embedded source file
- :param filename: Path to the Gerber file to save to.
- :return:
- """
-
- if filename is None:
- filename = self.app.options["global_last_save_folder"] if \
- self.app.options["global_last_save_folder"] is not None else self.app.options["global_last_folder"]
-
- self.log.debug("save_source_file()")
-
- obj = self.app.collection.get_by_name(obj_name)
-
- file_string = StringIO(obj.source_file)
- time_string = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
-
- if file_string.getvalue() == '':
- msg = _("Save cancelled because source file is empty. Try to export the file.")
- self.inform.emit('[ERROR_NOTCL] %s' % msg) # noqa
- return 'fail'
-
- try:
- with open(filename, 'w') as file:
- file.writelines('G04*\n')
- file.writelines('G04 %s (RE)GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' %
- (obj.kind.upper(), str(self.app.version), str(self.app.version_date)))
- file.writelines('G04 Filename: %s*\n' % str(obj_name))
- file.writelines('G04 Created on : %s*\n' % time_string)
-
- for line in file_string:
- file.writelines(line)
- except PermissionError:
- self.inform.emit('[WARNING] %s' %
- _("Permission denied, saving not possible.\n"
- "Most likely another app is holding the file open and not accessible.")) # noqa
- return 'fail'
-
- def on_file_savedefaults(self):
- """
- Callback for menu item File->Save Defaults. Saves application default options
- ``self.options`` to current_defaults.FlatConfig.
-
- :return: None
- """
- self.app.defaults.update(self.app.options)
- self.app.preferencesUiManager.save_defaults()
-
# end of file