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