diff --git a/CHANGELOG.md b/CHANGELOG.md index af379d7a..ab71a6b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ CHANGELOG for FlatCAM beta ================================================= +11.03.2022 + +- added a new feature: now in the context menu (and main menu -> Edit) there is a new command that allow to move a selection of objects at specified numeric coordinates (either absolute or relative to current position) + 10.03.2022 - fixed an issue where using the 'G' shortcut key in Editors will not toggle the grid snap diff --git a/appGUI/GUIElements.py b/appGUI/GUIElements.py index b98de4f1..58346d53 100644 --- a/appGUI/GUIElements.py +++ b/appGUI/GUIElements.py @@ -2458,6 +2458,47 @@ class FCComboBox2(FCComboBox): self.setCurrentIndex(0) +class DialogBoxChoice(QtWidgets.QDialog): + def __init__(self, title=None, icon=None, choice='bl'): + """ + + :param title: string with the window title + """ + super(DialogBoxChoice, self).__init__() + + self.ok = False + + self.setWindowIcon(icon) + self.setWindowTitle(str(title)) + + grid0 = FCGridLayout(parent=self, h_spacing=5, v_spacing=5) + + self.ref_radio = RadioSet([ + {"label": _("Bottom Left"), "value": "bl"}, + {"label": _("Top Left"), "value": "tl"}, + {"label": _("Bottom Right"), "value": "br"}, + {"label": _("Top Right"), "value": "tr"}, + {"label": _("Center"), "value": "c"} + ], orientation='vertical', compact=True) + self.ref_radio.set_value(choice) + grid0.addWidget(self.ref_radio, 0, 0) + + self.button_box = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel, + Qt.Orientation.Horizontal, parent=self) + grid0.addWidget(self.button_box, 1, 0) + + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + + if self.exec() == QtWidgets.QDialog.DialogCode.Accepted: + self.ok = True + self.location_point = self.ref_radio.get_value() + else: + self.ok = False + self.location_point = None + + class FCInputDialog(QtWidgets.QInputDialog): def __init__(self, parent=None, ok=False, val=None, title=None, text=None, min=None, max=None, decimals=None, init_val=None): diff --git a/appGUI/MainGUI.py b/appGUI/MainGUI.py index 0e7b2c39..4ada23ad 100644 --- a/appGUI/MainGUI.py +++ b/appGUI/MainGUI.py @@ -429,6 +429,9 @@ class MainGUI(QtWidgets.QMainWindow): # Separator self.menuedit.addSeparator() + self.menuedit_numeric_move = self.menuedit.addAction( + QtGui.QIcon(self.app.resource_location + '/move32_bis.png'), + '%s\t%s' % (_('Num Move'), '')) self.menueditorigin = self.menuedit.addAction( QtGui.QIcon(self.app.resource_location + '/origin16.png'), '%s\t%s' % (_('Set Origin'), _('O'))) @@ -1664,10 +1667,20 @@ class MainGUI(QtWidgets.QMainWindow): # ######################################################################## self.popMenu = FCMenu() - self.popmenu_disable = self.popMenu.addAction( + # View + self.cmenu_viewmenu = self.popMenu.addMenu( + QtGui.QIcon(self.app.resource_location + '/view64.png'), _("View")) + self.popmenu_disable = self.cmenu_viewmenu.addAction( QtGui.QIcon(self.app.resource_location + '/disable32.png'), _("Toggle Visibility")) - self.popmenu_panel_toggle = self.popMenu.addAction( + self.popmenu_panel_toggle = self.cmenu_viewmenu.addAction( QtGui.QIcon(self.app.resource_location + '/notebook16.png'), _("Toggle Panel")) + self.cmenu_viewmenu.addSeparator() + self.zoomfit = self.cmenu_viewmenu.addAction( + QtGui.QIcon(self.app.resource_location + '/zoom_fit32.png'), _("Zoom Fit")) + self.clearplot = self.cmenu_viewmenu.addAction( + QtGui.QIcon(self.app.resource_location + '/clear_plot32.png'), _("Clear Plot")) + self.replot = self.cmenu_viewmenu.addAction( + QtGui.QIcon(self.app.resource_location + '/replot32.png'), _("Replot")) self.popMenu.addSeparator() self.cmenu_newmenu = self.popMenu.addMenu( @@ -1687,16 +1700,6 @@ class MainGUI(QtWidgets.QMainWindow): self.cmenu_gridmenu = self.popMenu.addMenu( QtGui.QIcon(self.app.resource_location + '/grid32_menu.png'), _("Grids")) - # View - self.cmenu_viewmenu = self.popMenu.addMenu( - QtGui.QIcon(self.app.resource_location + '/view64.png'), _("View")) - self.zoomfit = self.cmenu_viewmenu.addAction( - QtGui.QIcon(self.app.resource_location + '/zoom_fit32.png'), _("Zoom Fit")) - self.clearplot = self.cmenu_viewmenu.addAction( - QtGui.QIcon(self.app.resource_location + '/clear_plot32.png'), _("Clear Plot")) - self.replot = self.cmenu_viewmenu.addAction( - QtGui.QIcon(self.app.resource_location + '/replot32.png'), _("Replot")) - self.popMenu.addSeparator() # Set colors @@ -1846,6 +1849,8 @@ class MainGUI(QtWidgets.QMainWindow): self.popmenu_save.setVisible(False) self.popMenu.addSeparator() + self.popmenu_numeric_move = self.popMenu.addAction( + QtGui.QIcon(self.app.resource_location + '/move32_bis.png'), _("Num Move")) self.popmenu_move2origin = self.popMenu.addAction( QtGui.QIcon(self.app.resource_location + '/origin2_32.png'), _("Move2Origin")) self.popmenu_move = self.popMenu.addAction( diff --git a/appPlugins/ToolMove.py b/appPlugins/ToolMove.py index a1fe4e03..09e76616 100644 --- a/appPlugins/ToolMove.py +++ b/appPlugins/ToolMove.py @@ -151,68 +151,7 @@ class ToolMove(AppTool): dx = pos[0] - self.point1[0] dy = pos[1] - self.point1[1] - # move only the objects selected and plotted and visible - obj_list = [obj for obj in self.app.collection.get_selected() - if obj.obj_options['plot'] and obj.visible is True] - - def job_move(app_obj): - with self.app.proc_container.new('%s...' % _("Moving")): - - if not obj_list: - app_obj.app.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed."), - _("No object is selected."))) - return "fail" - - try: - # remove any mark aperture shape that may be displayed - for sel_obj in obj_list: - # if the Gerber mark shapes are enabled they need to be disabled before move - if sel_obj.kind == 'gerber': - sel_obj.ui.aperture_table_visibility_cb.setChecked(False) - - try: - sel_obj.replotApertures.emit() - except Exception: - pass - - # offset solid_geometry - sel_obj.offset((dx, dy)) - - # Update the object bounding box options - a, b, c, d = sel_obj.bounds() - sel_obj.obj_options['xmin'] = a - sel_obj.obj_options['ymin'] = b - sel_obj.obj_options['xmax'] = c - sel_obj.obj_options['ymax'] = d - - try: - sel_obj.set_offset_values() - except AttributeError: - # not all objects have this method - pass - - # update the source_file with the new positions - for sel_obj in obj_list: - out_name = sel_obj.obj_options["name"] - if sel_obj.kind == 'gerber': - sel_obj.source_file = self.app.f_handlers.export_gerber( - obj_name=out_name, filename=None, local_use=sel_obj, use_thread=False) - elif sel_obj.kind == 'excellon': - sel_obj.source_file = self.app.f_handlers.export_excellon( - obj_name=out_name, filename=None, local_use=sel_obj, use_thread=False) - except Exception as err: - app_obj.log.error('[ERROR_NOTCL] %s --> %s' % ('ToolMove.on_left_click()', str(err))) - return "fail" - - # time to plot the moved objects - app_obj.replot_signal.emit(obj_list) - - # delete the selection bounding box - self.delete_shape() - self.app.inform.emit('[success] %s %s ...' % - (str(sel_obj.kind).capitalize(), _('object was moved'))) - - self.app.worker_task.emit({'fcn': job_move, 'params': [self]}) + self.move_handler(offset=(dx, dy)) self.clicked_move = 0 self.toggle() @@ -234,6 +173,85 @@ class ToolMove(AppTool): self.app.worker_task.emit({'fcn': worker_task, 'params': []}) + def move_handler(self, offset, objects=None): + """ + Actual move is done here. + + :param offset: How much to move objects on both directions + :type offset: tuple + :param objects: objects to move + :type objects: [list, None] + :return: + :rtype: + """ + + dx, dy = offset + + if not objects: + # move only the objects selected and plotted and visible + obj_list = [ + obj for obj in self.app.collection.get_selected() if obj.obj_options['plot'] and obj.visible is True + ] + else: + obj_list = objects + + def job_move(app_obj): + with self.app.proc_container.new('%s...' % _("Moving")): + + if not obj_list: + app_obj.app.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed."), _("No object is selected."))) + return "fail" + + try: + # remove any mark aperture shape that may be displayed + for sel_obj in obj_list: + # if the Gerber mark shapes are enabled they need to be disabled before move + if sel_obj.kind == 'gerber': + sel_obj.ui.aperture_table_visibility_cb.setChecked(False) + + try: + sel_obj.replotApertures.emit() + except Exception: + pass + + # offset solid_geometry + sel_obj.offset((dx, dy)) + + # Update the object bounding box options + a, b, c, d = sel_obj.bounds() + sel_obj.obj_options['xmin'] = a + sel_obj.obj_options['ymin'] = b + sel_obj.obj_options['xmax'] = c + sel_obj.obj_options['ymax'] = d + + try: + sel_obj.set_offset_values() + except AttributeError: + # not all objects have this method + pass + + # update the source_file with the new positions + for sel_obj in obj_list: + out_name = sel_obj.obj_options["name"] + if sel_obj.kind == 'gerber': + sel_obj.source_file = self.app.f_handlers.export_gerber( + obj_name=out_name, filename=None, local_use=sel_obj, use_thread=False) + elif sel_obj.kind == 'excellon': + sel_obj.source_file = self.app.f_handlers.export_excellon( + obj_name=out_name, filename=None, local_use=sel_obj, use_thread=False) + except Exception as err: + app_obj.log.error('[ERROR_NOTCL] %s --> %s' % ('ToolMove.move_handler()', str(err))) + return "fail" + + # time to plot the moved objects + app_obj.replot_signal.emit(obj_list) + + # delete the selection bounding box + self.delete_shape() + self.app.inform.emit('[success] %s' % _("Done.")) + + self.app.worker_task.emit({'fcn': job_move, 'params': [self]}) + def on_move(self, event): event_pos = event.pos if self.app.use_3d_engine else (event.xdata, event.ydata) try: diff --git a/app_Main.py b/app_Main.py index b1ce5fe5..b2dd382d 100644 --- a/app_Main.py +++ b/app_Main.py @@ -20,7 +20,7 @@ from datetime import datetime import ctypes import traceback -from shapely.geometry import Point, MultiPolygon +from shapely.geometry import Point, MultiPolygon, MultiLineString from shapely.ops import unary_union from io import StringIO @@ -83,7 +83,7 @@ from appGUI.PlotCanvasLegacy import * from appGUI.PlotCanvas3d import * from appGUI.MainGUI import * from appGUI.GUIElements import FCFileSaveDialog, message_dialog, FlatCAMSystemTray, FCInputDialogSlider, \ - FCGridLayout, FCLabel + FCGridLayout, FCLabel, DialogBoxChoice # App Pre-processors from appPreProcessor import load_preprocessors @@ -295,7 +295,7 @@ class App(QtCore.QObject): handler.setFormatter(formatter) self.log.addHandler(handler) - self.log.info("FlatCAM Starting...") + self.log.info("Starting the application...") self.qapp = qapp @@ -1891,13 +1891,13 @@ class App(QtCore.QObject): self.distance_tool = Distance(self) self.distance_tool.install(icon=QtGui.QIcon(self.resource_location + '/distance16.png'), pos=self.ui.menuedit, - before=self.ui.menueditorigin, + before=self.ui.menuedit_numeric_move, separator=False) self.distance_min_tool = ObjectDistance(self) self.distance_min_tool.install(icon=QtGui.QIcon(self.resource_location + '/distance_min16.png'), pos=self.ui.menuedit, - before=self.ui.menueditorigin, + before=self.ui.menuedit_numeric_move, separator=True) self.dblsidedtool = DblSidedTool(self) @@ -1941,7 +1941,7 @@ class App(QtCore.QObject): self.move_tool = ToolMove(self) self.move_tool.install(icon=QtGui.QIcon(self.resource_location + '/move16.png'), pos=self.ui.menuedit, - before=self.ui.menueditorigin, separator=True) + before=self.ui.menuedit_numeric_move, separator=True) self.cutout_tool = CutOut(self) self.cutout_tool.install(icon=QtGui.QIcon(self.resource_location + '/cut32.png'), pos=self.ui.menu_plugins, @@ -2193,6 +2193,8 @@ class App(QtCore.QObject): self.ui.menueditconvert_any2gerber.triggered.connect(lambda: self.convert_any2gerber()) self.ui.menueditconvert_any2excellon.triggered.connect(lambda: self.convert_any2excellon()) + self.ui.menuedit_numeric_move.triggered.connect(lambda: self.on_numeric_move()) + self.ui.menueditorigin.triggered.connect(self.on_set_origin) self.ui.menuedit_move2origin.triggered.connect(self.on_move2origin) self.ui.menuedit_center_in_origin.triggered.connect(self.on_custom_origin) @@ -2297,6 +2299,7 @@ class App(QtCore.QObject): self.ui.popmenu_delete.triggered.connect(self.on_delete) self.ui.popmenu_edit.triggered.connect(self.object2editor) self.ui.popmenu_save.triggered.connect(lambda: self.editor2object()) + self.ui.popmenu_numeric_move.triggered.connect(lambda: self.on_numeric_move()) self.ui.popmenu_move.triggered.connect(self.obj_move) self.ui.popmenu_move2origin.triggered.connect(self.on_move2origin) @@ -5798,46 +5801,6 @@ class App(QtCore.QObject): self.inform.emit('[WARNING_NOTCL] %s' % _("No object is selected.")) return 'fail' - class DialogBoxChoice(QtWidgets.QDialog): - def __init__(self, title=None, icon=None, choice='bl'): - """ - - :param title: string with the window title - """ - super(DialogBoxChoice, self).__init__() - - self.ok = False - - self.setWindowIcon(icon) - self.setWindowTitle(str(title)) - - grid0 = FCGridLayout(parent=self, h_spacing=5, v_spacing=5) - - self.ref_radio = RadioSet([ - {"label": _("Bottom Left"), "value": "bl"}, - {"label": _("Top Left"), "value": "tl"}, - {"label": _("Bottom Right"), "value": "br"}, - {"label": _("Top Right"), "value": "tr"}, - {"label": _("Center"), "value": "c"} - ], orientation='vertical', compact=True) - self.ref_radio.set_value(choice) - grid0.addWidget(self.ref_radio, 0, 0) - - self.button_box = QtWidgets.QDialogButtonBox( - QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel, - Qt.Orientation.Horizontal, parent=self) - grid0.addWidget(self.button_box, 1, 0) - - self.button_box.accepted.connect(self.accept) - self.button_box.rejected.connect(self.reject) - - if self.exec() == QtWidgets.QDialog.DialogCode.Accepted: - self.ok = True - self.location_point = self.ref_radio.get_value() - else: - self.ok = False - self.location_point = None - dia_box = DialogBoxChoice(title=_("Locate ..."), icon=QtGui.QIcon(self.resource_location + '/locate16.png'), choice=self.options['global_locate_pt']) @@ -5939,6 +5902,78 @@ class App(QtCore.QObject): self.inform.emit('[success] %s' % _("Done.")) return location + def on_numeric_move(self, val=None): + """ + Move to a specific location (absolute or relative against current position)' + + :param val: custom offset value, (x, y) + :type val: tuple + :return: None + :rtype: None + """ + + # move only the objects selected and plotted and visible + obj_list = [ + obj for obj in self.collection.get_selected() if obj.obj_options['plot'] and obj.visible is True + ] + + if not obj_list: + self.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed."), _("Nothing selected."))) + return + + def bounds_rec(obj): + try: + minx = Inf + miny = Inf + maxx = -Inf + maxy = -Inf + + work_geo = obj.geoms if isinstance(obj, (MultiPolygon, MultiLineString)) else obj + for k in work_geo: + minx_, miny_, maxx_, maxy_ = bounds_rec(k) + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + return minx, miny, maxx, maxy + except TypeError: + # it's a App object, return its bounds + if obj: + return obj.bounds() + + bounds = bounds_rec(obj_list) + + if not val: + dia_box_location = (0.0, 0.0) + + dia_box = DialogBoxRadio(title=_("Move to ..."), + label=_("Enter the coordinates in format X,Y:"), + icon=QtGui.QIcon(self.resource_location + '/move32_bis.png'), + initial_text=dia_box_location, + reference=self.options['global_move_ref']) + + if dia_box.ok is True: + try: + location = eval(dia_box.location) + + if not isinstance(location, tuple): + self.inform.emit(_("Wrong coordinates. Enter coordinates in format: X,Y")) + return + + if dia_box.reference == 'abs': + abs_x = location[0] - bounds[0] + abs_y = location[1] - bounds[1] + location = (abs_x, abs_y) + self.options['global_jump_ref'] = dia_box.reference + except Exception: + return + else: + return + else: + location = val + + self.move_tool.move_handler(offset=location, objects=obj_list) + def on_copy_command(self): """ Will copy a selection of objects, creating new objects. @@ -8685,14 +8720,14 @@ class App(QtCore.QObject): # ## Latest version? if self.version >= data["version"]: - self.log.debug("FlatCAM is up to date!") - self.inform.emit('[success] %s' % _("FlatCAM is up to date!")) + self.log.debug("THe application is up to date!") + self.inform.emit('[success] %s' % _("The application is up to date!")) return self.log.debug("Newer version available.") title = _("Newer Version Available") msg = '%s

>%s
%s' % ( - _("There is a newer version of FlatCAM available for download:"), + _("There is a newer version available for download:"), str(data["name"]), str(data["message"]) ) diff --git a/defaults.py b/defaults.py index f922a3ea..216dd62a 100644 --- a/defaults.py +++ b/defaults.py @@ -40,6 +40,8 @@ class AppDefaults: "global_jump_ref": 'abs', "global_locate_pt": 'bl', + "global_move_ref": 'abs', + "global_toolbar_view": 511, "global_background_timeout": 300000, # Default value is 5 minutes