From 4eac6ccef40419cfd159d171485576ea246e46d3 Mon Sep 17 00:00:00 2001 From: david mueller Date: Fri, 21 Aug 2026 10:44:39 +0200 Subject: [PATCH 1/3] Gmoccapy: Value validation in tool and offset table - value validation is done by the widgets the values are entered for - the calculator returns the values that were entered and does not do validation - offsetvalue editing is handled in the widget regardless whether directly with the keyboard or the calculator - On save the table is checked for duplicate tool numbers --- lib/python/gladevcp/calculatorwidget.py | 17 +++--- lib/python/gladevcp/offsetpage_widget.py | 35 +++++++++--- lib/python/gladevcp/tooledit_widget.py | 47 +++++++++------ src/emc/usr_intf/gmoccapy/dialogs.py | 5 +- src/emc/usr_intf/gmoccapy/gmoccapy.py | 73 +++++++++--------------- 5 files changed, 92 insertions(+), 85 deletions(-) diff --git a/lib/python/gladevcp/calculatorwidget.py b/lib/python/gladevcp/calculatorwidget.py index cb6dbb8511f..68cb7a2d982 100644 --- a/lib/python/gladevcp/calculatorwidget.py +++ b/lib/python/gladevcp/calculatorwidget.py @@ -164,13 +164,15 @@ def set_value( self, value ): def get_value( self ): self.compute() try: - value = self.entry.get_text() - if self.use_localization: - return locale.atof( value ) - else: - return float(value) + value = float(self.entry.get_text()) except: return None + if self.use_localization: + try: + value = locale.atof( value ) + except: + return None + return value def get_preset_value( self ): return self.preset_value @@ -198,10 +200,7 @@ def compute( self ): else: qualified = qualified + i try : - if self.integer_only: - b = str( int( eval( qualified ) ) ) - else: - b = str( eval( qualified ) ) + b = str( eval( qualified ) ) except: b = "Error" self.eval_string = '' diff --git a/lib/python/gladevcp/offsetpage_widget.py b/lib/python/gladevcp/offsetpage_widget.py index 7e1a3fb1df8..3d6c6aa5054 100755 --- a/lib/python/gladevcp/offsetpage_widget.py +++ b/lib/python/gladevcp/offsetpage_widget.py @@ -159,6 +159,16 @@ def __init__(self, filename = None, *a, **kw): # check linuxcnc status every half second GLib.timeout_add(500, self.periodic_check) + def warning_dialog(self, message): + dialog = Gtk.MessageDialog(parent=self.wTree.get_object("window1"), + destroy_with_parent = True, + message_type=Gtk.MessageType.ERROR, + text=message) + dialog.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT) + dialog.show() + dialog.run() + dialog.destroy() + # Reload the offsets into display def reload_offsets(self): g28, g30, g92, g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3 = self.read_file() @@ -353,13 +363,16 @@ def set_editing(self, widget): self.queue_draw() # When the column is edited this does the work - def col_editted(self, widget, filtered_path, new_text, col): + def col_editted(self, widget, path, new_text, col): + self.validate_input(path, new_text, col) + + + def validate_input(self, path, new_text, col): model, treeiter = self.view2.get_selection().get_selected() path = self.modelfilter.get_path(treeiter) (store_path,) = self.modelfilter.convert_path_to_child_path(path) row = store_path axisnum = col - 1 - # print "EDITED:", new_text, col, int(filtered_path), row, "axis num:", axisnum def system_to_p(system): convert = { "G54":1, "G55":2, "G56":3, "G57":4, "G58":5, "G59":6, "G59.1":7, "G59.2":8, "G59.3":9} @@ -374,23 +387,27 @@ def system_to_p(system): tmpl = lambda s: self.mm_text_template % s else: tmpl = lambda s: self.imperial_text_template % s - + msg = None # allow 'Comment' column text to be arbitrarily changed if col == 11: self.store[row][15] = new_text return # for all other columns we expect a float value else: - try: - if self.use_localization: + if self.use_localization: + try: # using locale settings can lead to issues but we make it optional for backwards compatibility new_float = float(locale.atof(new_text)) - else: + except Exception as e: + print("offsetpage_widget, Error trying to convert to localized float: ", e) + else: + try: # this is the preferred way, allowing dot or comma as decimal symbol new_float = float(new_text.replace(',', '.')) - except Exception as error: - print('new_text: ', new_text, error) - print(_("offsetpage widget error: unrecognized float input")) + except: + msg = (_(f"\nMust be a decimal number")) + if msg is not None: + self.warning_dialog(msg) return # ignore entries to the Rot column in non-wcs rows diff --git a/lib/python/gladevcp/tooledit_widget.py b/lib/python/gladevcp/tooledit_widget.py index 8f21e9db42c..3b798577efd 100644 --- a/lib/python/gladevcp/tooledit_widget.py +++ b/lib/python/gladevcp/tooledit_widget.py @@ -262,8 +262,7 @@ def set_filename(self,filename): self.toolfile = filename self.reload(None) - def warning_dialog(self, line_number): - message = f"Error in tool table line {line_number} in column orientation.\nValid range is 0 ~ 9." + def warning_dialog(self, message): dialog = Gtk.MessageDialog(parent=self.wTree.get_object("window1"), destroy_with_parent = True, message_type=Gtk.MessageType.ERROR, @@ -327,7 +326,8 @@ def reload(self,widget): value = int(float(word.lstrip(i))) array[offset] = value if value not in range(10): - self.warning_dialog(line_number) + msg = f"Error in tool table line {line_number} in column orientation.\nValid range is 0 ~ 9." + self.warning_dialog(msg) break except: print(_("Tooledit widget float error")) @@ -350,12 +350,19 @@ def save(self,widget): # pre check before saving the file # if not done before, the file will be saved only until the erroneous line and the rest will be lost line_number = 0 + tool_nbrs = [] for row in liststore: values = [ value for value in row ] line_number += 1 - if values[15] > 9: - self.warning_dialog(line_number) + msg = None + if values[1] in tool_nbrs: + msg = f"\nError in tool table:\nDuplicate tool number '{values[1]}'" + elif values[15] not in range(10): + msg = f"\nError in tool table line {line_number} in column orientation.\nValid range is 0 ~ 9." + if msg is not None: + self.warning_dialog(msg) return + tool_nbrs.append(values[1]) if(locale.getlocale(locale.LC_NUMERIC)[0] is None): raise ExceptionMessage("\n\n"+_("Something wrong with the locale settings. Will not save the tool table.")) @@ -509,32 +516,40 @@ def validate_input(self, path, new_text, col): elif filter == 'tool': (store_path,) = self.tool_filter.convert_path_to_child_path(path) path = store_path - + msg = None + # validate positive integer for tool and pocket number if col in(1,2): - try: - self.model[path][col] = int(new_text) + try : + value = float(new_text) + if value != int(value) or value < 0: + raise TypeError + else: + self.model[path][col] = int(value) except: - pass + msg = (_(f"\nMust be a positive whole number")) # validate input for float columns elif col in range(3,15): try: self.model[path][col] = f"{float(new_text.replace(',', '.')):10.4f}" except: - pass + msg = (_(f"\nMust be a decimal number")) # validate input for orientation: check if int and valid range elif col == 15: - try: - value = int(new_text) - if value in range(10): - self.model[path][col] = value + try : + value = float(new_text) + if value != int(value) or value not in range(10): + raise TypeError + else: + self.model[path][col] = int(value) except: - pass + msg = (_(f"\nMust be one of (0,1,2,3,4,5,6,7,8,9)")) elif col == 16: try: self.model[path][col] = (new_text) except: pass - #print path,new_text, col + if msg is not None: + self.warning_dialog(msg) if filter in('wear','tool'): self.save(None) diff --git a/src/emc/usr_intf/gmoccapy/dialogs.py b/src/emc/usr_intf/gmoccapy/dialogs.py index 08a16ee7249..842df6b2a08 100644 --- a/src/emc/usr_intf/gmoccapy/dialogs.py +++ b/src/emc/usr_intf/gmoccapy/dialogs.py @@ -99,10 +99,7 @@ def entry_dialog(self, caller, data = None, header = _("Enter value") , label = dialog.destroy() if response == Gtk.ResponseType.ACCEPT: if value != None: - if integer: - return int(value) - else: - return float(value) + return value else: return "ERROR" return "CANCEL" diff --git a/src/emc/usr_intf/gmoccapy/gmoccapy.py b/src/emc/usr_intf/gmoccapy/gmoccapy.py index 1e6852dcc90..38bbe573104 100644 --- a/src/emc/usr_intf/gmoccapy/gmoccapy.py +++ b/src/emc/usr_intf/gmoccapy/gmoccapy.py @@ -2106,21 +2106,21 @@ def on_tool_col_edit_started(self, widget, filtered_path, new_text, col): header=_("Enter value"), label=_("Tool") + f" {model[treeiter][1]}, {captations[col]}:", integer=col in [1,2,15]) - if value == "ERROR": - LOG.debug("conversion error") - self.dialogs.warning_dialog(self, _("Conversion error !"), - ("Please enter only numerical values\nValues have not been applied")) - elif value == "CANCEL": + if value == "CANCEL": pass else: + if isinstance(value, float): + cell_text = f"{value:11.4f}" + else: + cell_text = value path = model.get_path(treeiter) row = path.get_indices()[0] # Clicking on a cell emits 'editing-started' which leads to the evaluation of the text in edit mode. # To use the return value of the calculator, it must be pretended that there is no editable (=no edit mode). self.widgets.tooledit1.editable = None - self.widgets.tooledit1.validate_input(row, f"{value:11.4f}", col) + self.widgets.tooledit1.validate_input(row, cell_text, col) self.widgets.tooledit1.edited = True - # this is needed to get offsetview out of editing mode + # this is needed to get out of editing mode GLib.timeout_add(50, toolview.set_cursor, toolpage.model.get_path(treeiter), @@ -2504,51 +2504,29 @@ def on_offset_col_edit_started(self, widget, filtered_path, new_text, col): (store_path,) = offsetpage.modelfilter.convert_path_to_child_path(path) row = store_path if self.widgets.offsetpage1.btn_edit_offsets.get_active(): - offset = self.dialogs.entry_dialog(self, + value = self.dialogs.entry_dialog(self, data=offsetpage.store[row][col], header=_("Enter value for offset"), label=f"{offsetpage.store[row][0]} {AXISLIST[col]}-" + _("offset:"), integer=False) - if offset == "ERROR": - LOG.debug("conversion error") - self.dialogs.warning_dialog(self, _("Conversion error !"), - ("Please enter only numerical values\nValues have not been applied")) - elif offset == "CANCEL": - pass + if value == "CANCEL": + pass + else: + if isinstance(value, float): + cell_text = f"{value:11.4f}" else: - axisnum = col - 1 - try: - if self.stat.task_mode != linuxcnc.MODE_MDI: - self.command.mode(linuxcnc.MODE_MDI) - self.command.wait_complete() - if row == 0: - self.command.mdi("G43.1 %s %10.4f" % (AXISLIST[col], offset)) - elif row == 1: - self.command.mdi("#%s = %10.4f" % (str(5161 + axisnum), offset)) - elif row == 2: - self.command.mdi("#%s = %10.4f" % (str(5181 + axisnum), offset)) - elif row == 3: - self.command.mdi("G92 %s %10.4f" % (AXISLIST[col], offset)) - else: - pnum = row-3 - if not pnum == None: - if col == 10: - self.command.mdi("G10 L2 P%d R %10.4f" % (pnum, offset)) - else: - self.command.mdi("G10 L2 P%d %s %10.4f" % (pnum, AXISLIST[col], offset)) - self.command.mode(linuxcnc.MODE_MANUAL) - self.command.wait_complete() - self.command.mode(linuxcnc.MODE_MDI) - self.command.wait_complete() - except: - print(_("offsetpage widget error: MDI call error")) - offsetpage.reload_offsets() - # this is needed to get offsetview out of editing mode - GLib.timeout_add(50, - offsetview.set_cursor, - path, - offsetview.get_columns()[0], - True) + cell_text = value + path = model.get_path(treeiter) + row = path.get_indices()[0] + self.widgets.offsetpage1.validate_input(row, cell_text, col) + self.widgets.offsetpage1.edited = True + + GLib.timeout_add(50, + offsetview.set_cursor, + model.get_path(treeiter), + offsetview.get_columns()[0], + True) + # Icon file selection stuff def _init_IconFileSelection(self): @@ -6619,3 +6597,4 @@ def _terminate(signum, frame): # start the event loop Gtk.main() + From 50dcc121da316b7d871f9ea94db1d1ee5636d4e1 Mon Sep 17 00:00:00 2001 From: david mueller Date: Wed, 26 Aug 2026 10:03:54 +0200 Subject: [PATCH 2/3] Gmoccapy tooledit_widget: new tool gets lowest available toolnr The tool number for a new tool is preset to the lowest available number found in the table instead of '0' --- lib/python/gladevcp/tooledit_widget.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/python/gladevcp/tooledit_widget.py b/lib/python/gladevcp/tooledit_widget.py index 3b798577efd..f4ab2f748e2 100644 --- a/lib/python/gladevcp/tooledit_widget.py +++ b/lib/python/gladevcp/tooledit_widget.py @@ -251,10 +251,21 @@ def match_tool(model, path, iter, pathlist): except: print(_("tooledit_widget error: cannot select tool number"),toolnumber) - def add(self,widget,data=[1,0,0,'0','0','0','0','0','0','0','0','0','0','0','0',0,"comment"]): + def add(self, widget, data=None): + liststore = self.model + if data is None: # an empty line is being added + data = [1,0,0,'0','0','0','0','0','0','0','0','0','0','0','0',0,"comment"] + tool_nbrs = [0] + for row in liststore: + values = [ value for value in row ] + tool_nbrs.append(values[1]) + missing = sorted(set(range(1, max(tool_nbrs))) - set(tool_nbrs)) + if len(missing) > 0: + data[1] = min(missing) + else: + data[1] = max(tool_nbrs)+1 self.model.append(data) self.num_of_col +=1 - liststore = self.model self.wTree.get_object("treeview1").scroll_to_cell(len(liststore)-1) # this is for adding a filename path after the tooleditor is already loaded. From 3399b5b0de50cbd6d5679b5526abe173dbef8827 Mon Sep 17 00:00:00 2001 From: david mueller Date: Wed, 26 Aug 2026 11:18:14 +0200 Subject: [PATCH 3/3] Gmoccapy: Check tool file on load and show user messages on startup Lines containing whitespace only are skipped rather than creating a new line with all '0' A message is shown on startup if - duplicate tool numbers have been found in the tool table - orientation is not in range 0...9 - a line is malformed, these are saved to a separate file so they are not silently deleted on save --- lib/python/gladevcp/tooledit_widget.py | 51 ++++++++++++++++++++++---- src/emc/usr_intf/gmoccapy/gmoccapy.py | 6 +++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/lib/python/gladevcp/tooledit_widget.py b/lib/python/gladevcp/tooledit_widget.py index f4ab2f748e2..73aa6562211 100644 --- a/lib/python/gladevcp/tooledit_widget.py +++ b/lib/python/gladevcp/tooledit_widget.py @@ -69,7 +69,7 @@ def __init__(self,toolfile=None, *a, **kw): self.editable = None self.edit_path = None self.edit_column = None - + self.tooltable_error_msg = None # connect the signals from Glade dic = { "on_delete_clicked" : self.delete, @@ -296,7 +296,15 @@ def reload(self,widget): logfile = open(self.toolfile, "r").readlines() self.toolinfo = [] line_number = 0 + tool_nbrs = [] + rejected_lines = [] + error_msg = [] for rawline in logfile: + if rawline.isspace(): + continue + elif rawline[0] != ';' and rawline[0] != 'T': + rejected_lines.append(rawline) + continue # strip the comments from line and add directly to array # if index = -1 the delimiter ; is missing - clear comments index = rawline.find(";") @@ -324,34 +332,63 @@ def reload(self,widget): if word.startswith(';'): break if word.startswith(i): if offset == 1: + try: + current_tool = int(word.lstrip(i)) + tool_nbrs.append(current_tool) + except: + rejected_lines.append(rawline) + continue if int(word.lstrip(i)) == self.toolinfo_num: toolinfo_flag = True if offset in(1,2): try: array[offset]= int(word.lstrip(i)) except: - print(_("Tooledit widget int error")) + rejected_lines.append(rawline) + continue elif offset == 15: try: # Accept also float for 'orientation' for backward compatibility value = int(float(word.lstrip(i))) array[offset] = value if value not in range(10): - msg = f"Error in tool table line {line_number} in column orientation.\nValid range is 0 ~ 9." - self.warning_dialog(msg) - break + msg = _(f"Tool {current_tool} Orientation: '{value}'\nValid range is 0 ~ 9.") + error_msg.append(msg) except: - print(_("Tooledit widget float error")) + rejected_lines.append(rawline) + continue else: try: array[offset]= f"{float(word.lstrip(i)):10.4f}" except: - print(_("Tooledit widget float error")) + rejected_lines.append(rawline) + continue break if toolinfo_flag: self.toolinfo = array # add array line to liststore self.add(None,array) + # check for duplicate toolnumbers + s = [] + dup_tool = [] + for n in tool_nbrs: + if n in s: + dup_tool.append(n) + else: + s.append(n) + # compose user message about duplicate tool nummbers + if len(dup_tool) > 0: + msg = _("Duplicate tool number(s): ") + str(dup_tool)[1:-1] +"" + error_msg.append(msg) + # write malformed lines to a file and compose user message + if len(rejected_lines) > 0: + file_name = "tooltable_rejected.txt" + self.rejected_lines = rejected_lines + with open(file_name, "w", encoding="utf-8") as f: + f.writelines(rejected_lines) + msg = (_(f"{len(rejected_lines)} malformed line(s) have been rejected and copied to '{file_name}'")) + error_msg.append(msg) + self.tooltable_error_msg = error_msg def save(self,widget): if self.toolfile == None: return diff --git a/src/emc/usr_intf/gmoccapy/gmoccapy.py b/src/emc/usr_intf/gmoccapy/gmoccapy.py index 38bbe573104..0e7bfbb4b17 100644 --- a/src/emc/usr_intf/gmoccapy/gmoccapy.py +++ b/src/emc/usr_intf/gmoccapy/gmoccapy.py @@ -517,6 +517,7 @@ def __init__(self, argv): self.progress = 0 self._startup_message() + self._tooltable_message() # This allows sourcing an user defined file rcfile = "~/.gmoccapyrc" @@ -573,6 +574,11 @@ def _startup_message(self): self.notification.add_message(message, INFO_ICON, show_checkbox=True) self.num = len(messages) + def _tooltable_message(self): + if self.widgets.tooledit1.tooltable_error_msg is not None: + title = _("Error in tool table\n") + for msg in self.widgets.tooledit1.tooltable_error_msg: + self.notification.add_message(title + msg, ALERT_ICON, show_checkbox=False) def _get_ini_data(self): self.get_ini_info = getiniinfo.GetIniInfo()