diff --git a/mcstas-comps/contrib/SpinWave_BCO.comp b/mcstas-comps/contrib/SpinWave_BCO.comp index 8cf889611d..d688642e4f 100644 --- a/mcstas-comps/contrib/SpinWave_BCO.comp +++ b/mcstas-comps/contrib/SpinWave_BCO.comp @@ -67,6 +67,7 @@ * V_my_s: [m^-1] Attenuation factor due to incoherent scattering * V_my_a_v: [m^-1] Attenuation factor due to absorbtion * +* %E ******************************************************************************/ DEFINE COMPONENT SpinWave_BCO diff --git a/mcstas-comps/misc/Cone.comp b/mcstas-comps/misc/Cone.comp index 177cc724d3..4c56a5f586 100644 --- a/mcstas-comps/misc/Cone.comp +++ b/mcstas-comps/misc/Cone.comp @@ -13,6 +13,7 @@ * Origin: ILL * Modified by: E. Farhi, based on Incoherent * +* %D * An inactive geometrical shape (cone), for drawing purposes only. * Derived from the Shape component * diff --git a/mcstas-comps/union/Union_NCrystal_material.comp b/mcstas-comps/union/Union_NCrystal_material.comp index a1ae8f6f6c..093b605666 100644 --- a/mcstas-comps/union/Union_NCrystal_material.comp +++ b/mcstas-comps/union/Union_NCrystal_material.comp @@ -54,6 +54,7 @@ * interact_fraction: [1] Fraction of scattering events assigned to this process 0-1 (default -1 lets Union assign it). * include_SLD: [0/1] Include the scattering length density calculated from cfg in the material refraction information. * init: [string] Deprecated and unused. Accepted so that instruments written for McStas/McXtrace 3.8.7 and earlier, which name the Union_init component here, still compile. +* %E *******************************************************************************/ /******************************************************************************/ diff --git a/mcxtrace-comps/misc/Cone.comp b/mcxtrace-comps/misc/Cone.comp index f7b73011d0..d316788f5f 100644 --- a/mcxtrace-comps/misc/Cone.comp +++ b/mcxtrace-comps/misc/Cone.comp @@ -13,6 +13,7 @@ * Origin: ILL * Modified by: E. Farhi, based on Incoherent * +* %D * An inactive geometrical shape (cone), for drawing purposes only. * Derived from the Shape component * diff --git a/tools/Python/mccodelib/pqtgfrontend.py b/tools/Python/mccodelib/pqtgfrontend.py index 18f2d70195..6718679cf7 100644 --- a/tools/Python/mccodelib/pqtgfrontend.py +++ b/tools/Python/mccodelib/pqtgfrontend.py @@ -373,7 +373,8 @@ def click_handler(event): posScene = event.scenePos() plotIdx = self.get_plot_index(posScene) posItem = event.pos() - except AttributeError: + except (AttributeError, RuntimeError): + # RuntimeError: the clicked item was deleted by a replot before this click was delivered return if 0 <= plotIdx < len(node_list): diff --git a/tools/Python/mccodelib/utils.py b/tools/Python/mccodelib/utils.py index 83395495d8..5e68c78722 100644 --- a/tools/Python/mccodelib/utils.py +++ b/tools/Python/mccodelib/utils.py @@ -1037,7 +1037,12 @@ def dumpfile_pqtg(scene, filenamebase='mcplot', format='png'): exporter.export(outputfile) elif format=='svg': exporter = pg.exporters.SVGExporter(scene) - exporter.export(outputfile) + try: + exporter.export(outputfile) + except ValueError as e: + # pyqtgraph 0.14.0 cannot parse the SVG path syntax written by Qt 6.11 + print('SVG export failed in pyqtgraph (%s), use PNG export instead' % e) + return # This is where PDF export would be handled via conversion from SVG # elif format=='pdf': # exporter = pg.exporters.SVGExporter(scene) diff --git a/tools/Python/mcdisplay/matplotlib/mcdisplay.py b/tools/Python/mcdisplay/matplotlib/mcdisplay.py index 87a2699caa..156791a4e2 100755 --- a/tools/Python/mcdisplay/matplotlib/mcdisplay.py +++ b/tools/Python/mcdisplay/matplotlib/mcdisplay.py @@ -76,10 +76,10 @@ def dumpfile(frmat, filename = None): # Check for existance of earlier exports if os.path.isfile(saveto): index=1 - saveto = f"{basename}_{index}{frmat}" + saveto = f"{basename}_{index}{ext}" while os.path.isfile(saveto): index += 1 - saveto = f"{basename}_{index}{frmat}" + saveto = f"{basename}_{index}{ext}" savefig(saveto) print("Saved " + saveto) diff --git a/tools/Python/mcgui/mcgui.py b/tools/Python/mcgui/mcgui.py index bf34c0a41b..e154a15eea 100755 --- a/tools/Python/mcgui/mcgui.py +++ b/tools/Python/mcgui/mcgui.py @@ -226,6 +226,10 @@ def canPlot(self): __thread_exc_signal = QtCore.Signal(str) def compile(self, mpi=True): + # replacing a running QThread aborts the application + if getattr(self, 'compilethread', None) is not None and self.compilethread.isRunning(): + self.__emitter.status('Compile already in progress') + return # using Qt in-built cross-thread signaling self.__thread_exc_signal.connect(handleExceptionMsg) @@ -616,7 +620,7 @@ def initDynamicView(self): def handleRunOrInterruptSim(self): if self.state.isSimRunning(): self.state.interrupt() - else: + elif self.state.canRun(): # ensure auto-save self.view.ew.save() @@ -639,6 +643,7 @@ def msg(msg, em=self.emitter): em.message(msg) handler = ThreadInfoHandler(lambda msg, em=self.emitter: em.message(msg)) + instr = self.state.getInstrumentFile() somethread = McRunQThread() somethread.cmd = mccode_config.configuration["MCRUN"] + ' ' + os.path.basename(self.state.getInstrumentFile()) + " --info" somethread.cwd = os.path.dirname(self.state.getInstrumentFile()) @@ -652,6 +657,11 @@ def msg(msg, em=self.emitter): time.sleep(0.2) QtWidgets.QApplication.processEvents() + # The GUI stays live during --info; the user may have closed or switched instrument + if self.state.getInstrumentFile() != instr: + self.emitter.status("Instrument changed while getting parameters, run cancelled") + return + params = [] for l in handler.stdout_lst: if 'Param:' in l: @@ -891,11 +901,12 @@ def displayNotSavedWhitespaceError(self, can_throw_func, raise_err=False): def handleSaveAs(self): oldinstr = self.state.getInstrumentFile() - if oldinstr != '': - newinstr = self.view.showSaveAsDialog(oldinstr) - if self.displayNotSavedWhitespaceError(lambda: self.state.checkInstrFileCandidate(newinstr))==False: - return - + if oldinstr == '': + return + newinstr = self.view.showSaveAsDialog(oldinstr) + if self.displayNotSavedWhitespaceError(lambda: self.state.checkInstrFileCandidate(newinstr))==False: + return + if newinstr != '': self.state.unloadInstrument() text = get_file_contents(oldinstr) @@ -930,29 +941,39 @@ def handleNewFromTemplate(self, instr_templ=''): wd = pathlib.PurePath(self.state.getWorkDir()) if wd.stem==instrbase.stem: self.state.setWorkDir("..") - new_instr_req = self.view.showNewInstrFromTemplateDialog(os.path.join(self.state.getWorkDir(), os.path.basename(os.path.dirname(str(instr_templ))))) - if self.displayNotSavedWhitespaceError(lambda: self.state.checkInstrFileCandidate(new_instr_req))==False: + # Templates are copied including their containing folder, into a user-selected parent folder + srcdir = pathlib.Path(str(instr_templ)).parent + parent = self.view.showNewInstrFromTemplateDialog(self.state.getWorkDir()) + if not parent: + return + parent = pathlib.Path(parent) + # User may have selected the template-named folder itself + trgdir = parent if parent.name == srcdir.name else parent / srcdir.name + new_instr = str(trgdir / pathlib.Path(str(instr_templ)).name) + if self.displayNotSavedWhitespaceError(lambda: self.state.checkInstrFileCandidate(new_instr))==False: return - - if new_instr_req != '': - if self.view.closeCodeEditorWindow(): - text = get_file_contents(instr_templ) - self.state.unloadInstrument() - # Templates are now copied including their containing folder - srcdir = os.path.dirname(str(instr_templ)) - trgdir = pathlib.Path(new_instr_req) - # Case 1: destination folder already exists, "merge": - if pathlib.Path.exists(trgdir): - files=os.listdir(srcdir) - for fname in files: - shutil.copy2(os.path.join(srcdir,fname), trgdir) - else: - # Case 2: create a new folder: - shutil.copytree(srcdir,trgdir) - new_instr = os.path.join(trgdir, os.path.basename(str(instr_templ))) - self.state.loadInstrument(new_instr) - self.emitter.status("Instrument created: " + os.path.basename(str(new_instr))) + choice = 'copy' + if trgdir.exists(): + n = 2 + while (trgdir.parent / (srcdir.name + '_' + str(n))).exists(): + n += 1 + newdir = trgdir.parent / (srcdir.name + '_' + str(n)) + choice = self.view.askTemplateDirExists(trgdir, newdir) + if choice is None: + return + if choice == 'new': + trgdir = newdir + new_instr = str(trgdir / pathlib.Path(str(instr_templ)).name) + + if self.view.closeCodeEditorWindow(): + self.state.unloadInstrument() + if choice == 'replace': + shutil.copytree(srcdir, trgdir, dirs_exist_ok=True) + elif choice != 'open': + shutil.copytree(srcdir, trgdir) + self.state.loadInstrument(new_instr) + self.emitter.status("Instrument created: " + os.path.basename(new_instr)) def handleOpenInstrument(self): instr = self.view.showOpenInstrumentDlg(self.state.getWorkDir()) @@ -1002,7 +1023,7 @@ def handleEnvironment(self): def handleDefault(self): reply = QtWidgets.QMessageBox.question(self.view.mw, 'Define system default?', - 'Do you want to make the current ' + mccode_config.configuration["MCCODE"] + ' the system default?'), + 'Do you want to make the current ' + mccode_config.configuration["MCCODE"] + ' the system default?') if reply == QtWidgets.QMessageBox.Yes: subprocess.Popen(mccode_config.configuration["MCCODE"] +'-postinst set_mccode_default', shell=True) @@ -1090,6 +1111,8 @@ def main(): mccode_config.check_env_vars() mcguiApp = QtWidgets.QApplication(sys.argv) + # Window/taskbar icon, and the Dock icon on macOS. ponytail: 115px logo, ship a 256px+ PNG if it looks soft + mcguiApp.setWindowIcon(QtGui.QIcon(os.path.join(os.path.dirname(os.path.abspath(__file__)), mccode_config.configuration["MCCODE"] + '-py.png'))) mcguiApp.ctr = McGuiAppController() font = QFont() font.setFixedPitch(True) diff --git a/tools/Python/mcgui/viewclasses.py b/tools/Python/mcgui/viewclasses.py index 9ee2c956e0..2ba342465a 100644 --- a/tools/Python/mcgui/viewclasses.py +++ b/tools/Python/mcgui/viewclasses.py @@ -191,9 +191,19 @@ def showNewInstrDialog(self, lookdir): dlg.setNameFilter(mccode_config.configuration["MCCODE"]+" instruments (*.instr)"); return dlg.getSaveFileName(parent=None, caption='Create Instrument file...')[0] - def showNewInstrFromTemplateDialog(self, instr): - dlg = QtWidgets.QFileDialog() - return dlg.getSaveFileName(parent=None, caption='Create Instrument file from Template...', directory=instr)[0] + def showNewInstrFromTemplateDialog(self, workdir): + return QtWidgets.QFileDialog.getExistingDirectory(self.mw, 'Copy template into folder...', workdir) + + def askTemplateDirExists(self, trgdir, newdir): + box = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Question, 'Folder exists', + str(trgdir) + ' already exists.', parent=self.mw) + open_btn = box.addButton('Open existing', QtWidgets.QMessageBox.AcceptRole) + new_btn = box.addButton('New copy (' + newdir.name + ')', QtWidgets.QMessageBox.AcceptRole) + repl_btn = box.addButton('Replace with template', QtWidgets.QMessageBox.DestructiveRole) + box.addButton(QtWidgets.QMessageBox.Cancel) + box.setDefaultButton(new_btn) + box.exec() + return {open_btn: 'open', new_btn: 'new', repl_btn: 'replace'}.get(box.clickedButton()) def showSaveAsDialog(self, instr): dlg = QtWidgets.QFileDialog() @@ -458,7 +468,11 @@ def closeEvent(self, event): def __handleComponentClicked(self, comp_parser): dlg = McInsertComponentDialog() - dlg.initComponentData(comp_parser) + try: + dlg.initComponentData(comp_parser) + except Exception as e: + QtWidgets.QMessageBox.critical(self, 'Could not read component', str(comp_parser.file) + ':\n' + str(e)) + return if dlg.exec(): comp_type, inst_name, params, atrel = dlg.getValues() else: @@ -662,6 +676,17 @@ def __init__(self, parent=None): self._set_inspect_visible(False) self.ui.cbxSimTrace.currentIndexChanged.connect(lambda i: self._set_inspect_visible(i)) + def accept(self): + try: + float(self.ui.edtNeutronCnt.text()) + if self.ui.edtRandomSeed.text(): + int(self.ui.edtRandomSeed.text()) + except ValueError: + QtWidgets.QMessageBox.warning(self, 'Invalid value', + 'The ray count must be a number (e.g. 1e6) and the random seed a whole number.') + return + super(McStartSimDialog, self).accept() + def _setupAdvancedCollapse(self): ''' replace the static "Advanced" group box with a roll-up section: a QToolButton with arrow toggles gbxAdvanced visibility ''' @@ -847,7 +872,7 @@ def getValues(self): inspect = None idx = self.ui.cbxInspect.currentIndex() if idx > 0: - inspect = self._last_inspect_compnames[idx] + inspect = self._last_inspect_compnames[idx-1] mcdisplay = None idx = self.ui.cbxMcdisplays.currentIndex() @@ -1298,9 +1323,12 @@ def __pullValuesTo_mccode_config(self): mccode_config.compilation[str(self.ui.edtMpicc.conf_var)] = str(self.ui.edtMpicc.text()) mccode_config.compilation[str(self.ui.edtMPIrun.conf_var)] = str(self.ui.edtMPIrun.text()) mccode_config.compilation[str(self.ui.edtNumNodes.conf_var)] = str(self.ui.edtNumNodes.text()) - mccode_config.configuration[str(self.ui.edtNumCols.conf_var)] = str(self.ui.edtNumCols.text()) + # GUICOLS and GUIFONTSIZE are parsed with int(): keep the old value unless a positive integer is given + if self.ui.edtNumCols.text().isdigit() and int(self.ui.edtNumCols.text()) > 0: + mccode_config.configuration[str(self.ui.edtNumCols.conf_var)] = str(self.ui.edtNumCols.text()) mccode_config.configuration[str(self.ui.edtNDbufsiz.conf_var)] = str(self.ui.edtNDbufsiz.text()) - mccode_config.configuration[str(self.ui.edtFontSize.conf_var)] = str(self.ui.edtFontSize.text()) + if self.ui.edtFontSize.text().isdigit() and int(self.ui.edtFontSize.text()) > 0: + mccode_config.configuration[str(self.ui.edtFontSize.conf_var)] = str(self.ui.edtFontSize.text()) mccode_config.configuration[str(self.ui.editor.conf_var)] = str(self.ui.editor.text()) # Export selected variables to the system / mcrun target_mccode=mccode_config.configuration["MCCODE"].upper() diff --git a/tools/Python/mcplot/matplotlib/plotfuncs.py b/tools/Python/mcplot/matplotlib/plotfuncs.py index 575b5c6891..fe6887d03d 100644 --- a/tools/Python/mcplot/matplotlib/plotfuncs.py +++ b/tools/Python/mcplot/matplotlib/plotfuncs.py @@ -492,10 +492,10 @@ def dumpfile(frmat, filename = None): # Check for existance of earlier exports if os.path.isfile(saveto): index=1 - saveto = f"{basename}_{index}{frmat}" + saveto = f"{basename}_{index}{ext}" while os.path.isfile(saveto): index += 1 - saveto = f"{basename}_{index}{frmat}" + saveto = f"{basename}_{index}{ext}" savefig(saveto) print("Saved " + saveto) @@ -503,7 +503,8 @@ def dumpfile(frmat, filename = None): def click(event, subplts, click_cbs, ctrl_cbs, back_cb, dc_cb): subplt = event.inaxes - if not subplt: + # also ignore axes that are not panels, e.g. a 2D plot's colorbar + if not subplt or subplt not in subplts: return False else: # Don't treat a click that landed on the panel's own legend as a