diff --git a/src/sas/qtgui/Utilities/PluginDefinition.py b/src/sas/qtgui/Utilities/PluginDefinition.py index fb52ff3432..dada924d69 100644 --- a/src/sas/qtgui/Utilities/PluginDefinition.py +++ b/src/sas/qtgui/Utilities/PluginDefinition.py @@ -5,12 +5,25 @@ from sas.qtgui.Utilities.UI.PluginDefinitionUI import Ui_PluginDefinition from sas.qtgui.Utilities import GuiUtils -# txtName -# txtDescription -# chkOverwrite -# tblParams -# tblParamsPD -# txtFunction + +def remove_empty_table_rows(tbl: QtWidgets.QTableWidget): + """A helper function to remove empty rows in a PySide Table, if there are more than two empty rows at the end. + This function ensures there is always an empty row in the table. + + :param tbl: A QTableWidget on i rows and j columns. + """ + for i in range(0, tbl.rowCount()): + for j in range(0, tbl.columnCount()): + cell_contents = tbl.item(i, j) + if cell_contents is None or cell_contents.text() == "": + # cell_contents may be None: Ensure this check remains, otherwise the .text() call may throw an error + pass + else: + break + else: + tbl.removeRow(i) + tbl.insertRow(tbl.rowCount()) + class PluginDefinition(QtWidgets.QDialog, Ui_PluginDefinition): """ @@ -63,6 +76,7 @@ def addWidgets(self): return y """ + self.model['text'] = text self.txtFunction.insertPlainText(text) self.txtFunction.setFont(GuiUtils.getMonospaceFont()) @@ -131,15 +145,14 @@ def onParamsChanged(self, row, column): self.parameter_dict[row] = (param, value) self.model['parameters'] = self.parameter_dict - # Check if the update was Value for last row. If so, add a new row - if column == 1 and row == self.tblParams.rowCount()-1: - # Add a row - self.tblParams.insertRow(self.tblParams.rowCount()) + # Check if there are empty rows. + remove_empty_table_rows(self.tblParams) + self.modelModified.emit() def onParamsPDChanged(self, row, column): """ - Respond to changes in non-polydisperse parameter table + Respond to changes in polydisperse parameter table """ param = value = None if self.tblParamsPD.item(row, 0): @@ -151,10 +164,9 @@ def onParamsPDChanged(self, row, column): self.pd_parameter_dict[row] = (param, value) self.model['pd_parameters'] = self.pd_parameter_dict - # Check if the update was Value for last row. If so, add a new row - if column == 1 and row == self.tblParamsPD.rowCount()-1: - # Add a row - self.tblParamsPD.insertRow(self.tblParamsPD.rowCount()) + # Check if there are empty rows. + remove_empty_table_rows(self.tblParamsPD) + self.modelModified.emit() diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/TabbedModelEditor.py index c1cc9b74d2..613047259c 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/TabbedModelEditor.py @@ -192,18 +192,19 @@ def loadFile(self, filename): # Name the tab with .py filename self.tabWidget.setTabText(0, display_name) + # In case previous model was incorrect, change the frame colours back + self.editor_widget.txtEditor.setStyleSheet("") + self.editor_widget.txtEditor.setToolTip("") + # Check the validity of loaded model if the model is python if self.is_python: - error_line = self.checkModel(plugin_text) + error_line = self.checkModel(self.filename) if error_line > 0: # select bad line cursor = QtGui.QTextCursor(self.editor_widget.txtEditor.document().findBlockByLineNumber(error_line-1)) self.editor_widget.txtEditor.setTextCursor(cursor) - return - - # In case previous model was incorrect, change the frame colours back - self.editor_widget.txtEditor.setStyleSheet("") - self.editor_widget.txtEditor.setToolTip("") + # Do not return because we still want to load C file if it exists + QtWidgets.QMessageBox.warning(self, "Model check failed", "The loaded model contains errors. Please correct all errors before using model.") # See if there is filename.c present c_path = self.filename.parent / self.filename.name.replace(".py", ".c") @@ -305,6 +306,10 @@ def updateFromPlugin(self): # get required filename filename = model['filename'] + if filename == "": + QtWidgets.QMessageBox.critical(self, "Plugin Error", "Please specify a filename.") + return + # check if file exists plugin_location = models.find_plugins_dir() full_path = os.path.join(plugin_location, filename) @@ -324,8 +329,9 @@ def updateFromPlugin(self): # disable "Apply" self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) - # Run the model test in sasmodels - if not self.isModelCorrect(full_path): + # Run the model test in sasmodels and check model syntax. Returns error line if checks fail. + error_line = self.checkModel(full_path) + if error_line > 0: return self.editor_widget.setEnabled(True) @@ -347,18 +353,21 @@ def updateFromPlugin(self): self.parent.communicate.statusBarUpdateSignal.emit(msg) logging.info(msg) - def checkModel(self, model_str): + def checkModel(self, full_path): """ - Run the ast check - and return True if the model is good. - False otherwise. + Run ast and model checks + Attempt to return the line number of the error if any + :param full_path: full path to the model file """ # successfulCheck = True error_line = 0 try: + with open(full_path, 'r', encoding="utf-8") as plugin: + model_str = plugin.read() ast.parse(model_str) + GuiUtils.checkModel(full_path) - except SyntaxError as ex: + except Exception as ex: msg = "Error building model: " + str(ex) logging.error(msg) # print four last lines of the stack trace @@ -371,57 +380,41 @@ def checkModel(self, model_str): # Set the status bar message # GuiUtils.Communicate.statusBarUpdateSignal.emit("Model check failed") self.parent.communicate.statusBarUpdateSignal.emit("Model check failed") - # Put a thick, red border around the mini-editor - self.tabWidget.currentWidget().txtEditor.setStyleSheet("border: 5px solid red") - # last_lines = traceback.format_exc().split('\n')[-4:] - traceback_to_show = '\n'.join(last_lines) - self.tabWidget.currentWidget().txtEditor.setToolTip(traceback_to_show) + + # Put a thick, red border around the editor. + from sas.qtgui.Utilities.CodeEditor import QCodeEditor + + # Find all QTextBrowser and QCodeEditor children + text_browsers = self.tabWidget.currentWidget().findChildren(QtWidgets.QTextBrowser) + code_editors = self.tabWidget.currentWidget().findChildren(QCodeEditor) + + # Combine the lists and apply the stylesheet + for child in text_browsers + code_editors: + child.setStyleSheet("border: 5px solid red") + # last_lines = traceback.format_exc().split('\n')[-4:] + traceback_to_show = '\n'.join(last_lines) + child.setToolTip(traceback_to_show) + # attempt to find the failing command line number, usually the last line with # `File ... line` syntax - for line in reversed(all_lines): - if 'File' in line and 'line' in line: + reversed_error_text = list(reversed(all_lines)) + for line in reversed_error_text: + if ('File' in line and 'line' in line): + # If model check fails (not syntax) then 'line' and 'File' will be in adjacent lines error_line = re.split('line ', line)[1] try: error_line = int(error_line) break except ValueError: - error_line = 0 - return error_line - - def isModelCorrect(self, full_path): - """ - Run the sasmodels method for model check - and return True if the model is good. - False otherwise. - """ - successfulCheck = True - try: - model_results = GuiUtils.checkModel(full_path) - logging.info(model_results) - # We can't guarantee the type of the exception coming from - # Sasmodels, so need the overreaching general Exception - except Exception as ex: - msg = "Error building model: "+ str(ex) - logging.error(msg) - #print three last lines of the stack trace - # this will point out the exact line failing - last_lines = traceback.format_exc().split('\n')[-4:] - traceback_to_show = '\n'.join(last_lines) - logging.error(traceback_to_show) - - # Set the status bar message - self.parent.communicate.statusBarUpdateSignal.emit("Model check failed") + # Sometimes the line number is followed by more text + try: + error_line = error_line.split(',')[0] + error_line = int(error_line) + break + except ValueError: + error_line = 0 - # Remove the file so it is not being loaded on refresh - os.remove(full_path) - # Put a thick, red border around the mini-editor - self.plugin_widget.txtFunction.setStyleSheet("border: 5px solid red") - # Use the last line of the traceback for the tooltip - last_lines = traceback.format_exc().split('\n')[-2:] - traceback_to_show = '\n'.join(last_lines) - self.plugin_widget.txtFunction.setToolTip(traceback_to_show) - successfulCheck = False - return successfulCheck + return error_line def updateFromEditor(self): """ @@ -432,13 +425,32 @@ def updateFromEditor(self): if not w.is_python: base, _ = os.path.splitext(filename) filename = base + '.c' - # make sure we have the file handle ready - assert(filename != "") + assert filename != "" + # Retrieve model string model_str = self.getModel()['text'] + # Save the file + self.writeFile(filename, model_str) + + # Get model filepath + plugin_location = models.find_plugins_dir() + full_path = os.path.join(plugin_location, filename) + if not w.is_python and self.is_python: + pass + elif os.path.splitext(full_path)[1] != ".py": + full_path += ".py" + + # Check model as long as there is a .py file in one of the tabs if w.is_python and self.is_python: - error_line = self.checkModel(model_str) + check_model = True + elif not w.is_python and self.is_python: + # Set full_path to the .py file so that we can run a model check on it (the .py model should link to the .c model) + full_path = self.filename.with_suffix(".py") + check_model = True + + if check_model: + error_line = self.checkModel(full_path) if error_line > 0: # select bad line cursor = QtGui.QTextCursor(w.txtEditor.document().findBlockByLineNumber(error_line-1)) @@ -446,10 +458,14 @@ def updateFromEditor(self): return # change the frame colours back - w.txtEditor.setStyleSheet("") - w.txtEditor.setToolTip("") - # Save the file - self.writeFile(filename, model_str) + try: + self.c_editor_widget.txtEditor.setStyleSheet("") + self.c_editor_widget.txtEditor.setToolTip("") + except AttributeError: + pass + self.editor_widget.txtEditor.setStyleSheet("") + self.editor_widget.txtEditor.setToolTip("") + # Update the tab title self.setTabEdited(False) diff --git a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui index 00b365b52b..0fa294b07e 100755 --- a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui +++ b/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui @@ -133,10 +133,13 @@ value <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:6.6pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:7.8pt;"><br /></p></body></html> +hr { height: 1px; border-width: 0; } +li.unchecked::marker { content: "\2610"; } +li.checked::marker { content: "\2612"; } +</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Consolas'; font-size:11pt;"><br /></p></body></html>