diff --git a/TODO.md b/TODO.md
index 95c3878..8264622 100644
--- a/TODO.md
+++ b/TODO.md
@@ -43,7 +43,7 @@
### GUI:
- [ ] Better help in configuration. A foldable dedicated text browser widget to show info?
-- [ ] Implement UserTally options/definition in GUI
+- [X] Implement UserTally options/definition in GUI
- [X] Getting Started
- [X] About
@@ -99,8 +99,8 @@ Tally:
### User
- GUI:
- - [ ] Add a database of pre-defined materials with full definition: composition, density, Ed, etc
- - [ ] Pressing add material presents to the user a selection/search function to discover & select
+ - [X] Add a database of pre-defined materials with full definition: composition, density, Ed, etc
+ - [X] Pressing add material presents to the user a selection/search function to discover & select
- CLI:
- [X] Block Ctrl-C signal so that data is saved before the program is aborted
diff --git a/source/gui/CMakeLists.txt b/source/gui/CMakeLists.txt
index fb80061..f333eab 100644
--- a/source/gui/CMakeLists.txt
+++ b/source/gui/CMakeLists.txt
@@ -9,6 +9,8 @@ set(GUI_HEADERS
simulationoptionsview.h
periodictablewidget.h
materialsdefview.h
+ materialdatabasedialog.h
+ usertallyview.h
optionsmodel.h
floatlineedit.h
mydatawidgetmapper.h
@@ -31,6 +33,8 @@ set(GUI_SOURCES
simulationoptionsview.cpp
periodictablewidget.cpp
materialsdefview.cpp
+ materialdatabasedialog.cpp
+ usertallyview.cpp
optionsmodel.cpp
floatlineedit.cpp
mydatawidgetmapper.cpp
diff --git a/source/gui/assets/data/materials_database.json b/source/gui/assets/data/materials_database.json
new file mode 100644
index 0000000..301f749
--- /dev/null
+++ b/source/gui/assets/data/materials_database.json
@@ -0,0 +1,503 @@
+[
+ {
+ "id": "Fe",
+ "description": "Iron (BCC, standard SRIM values)",
+ "density": 7.874,
+ "composition": [
+ { "symbol": "Fe", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 4.28, "Er": 40.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Fe-low",
+ "description": "Iron (low-damage model)",
+ "density": 7.874,
+ "composition": [
+ { "symbol": "Fe", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 4.28, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "FeO",
+ "description": "Iron oxide",
+ "density": 5.745,
+ "composition": [
+ { "symbol": "Fe", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 4.28, "Er": 40.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 1.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Fe2O3",
+ "description": "Hematite",
+ "density": 5.24,
+ "composition": [
+ { "symbol": "Fe", "X": 2.0, "Ed": 40.0, "El": 3.0, "Es": 4.28, "Er": 40.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 3.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Fe3O4",
+ "description": "Magnetite",
+ "density": 5.17,
+ "composition": [
+ { "symbol": "Fe", "X": 3.0, "Ed": 40.0, "El": 3.0, "Es": 4.28, "Er": 40.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 4.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Si",
+ "description": "Silicon",
+ "density": 2.329,
+ "composition": [
+ { "symbol": "Si", "X": 1.0, "Ed": 15.0, "El": 2.0, "Es": 4.7, "Er": 15.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "SiO2",
+ "description": "Silicon dioxide (amorphous)",
+ "density": 2.2,
+ "composition": [
+ { "symbol": "Si", "X": 1.0, "Ed": 15.0, "El": 2.0, "Es": 4.7, "Er": 15.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "SiC",
+ "description": "Silicon carbide",
+ "density": 3.21,
+ "composition": [
+ { "symbol": "Si", "X": 1.0, "Ed": 20.0, "El": 3.0, "Es": 4.7, "Er": 20.0, "Rc": 0.946 },
+ { "symbol": "C", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 7.4, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "C",
+ "description": "Carbon (graphite)",
+ "density": 2.267,
+ "composition": [
+ { "symbol": "C", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 7.4, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "C-diamond",
+ "description": "Carbon (diamond)",
+ "density": 3.51,
+ "composition": [
+ { "symbol": "C", "X": 1.0, "Ed": 43.0, "El": 3.0, "Es": 7.4, "Er": 43.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Al",
+ "description": "Aluminum",
+ "density": 2.70,
+ "composition": [
+ { "symbol": "Al", "X": 1.0, "Ed": 25.0, "El": 3.36, "Es": 3.36, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Al2O3",
+ "description": "Alumina",
+ "density": 3.97,
+ "composition": [
+ { "symbol": "Al", "X": 2.0, "Ed": 25.0, "El": 3.36, "Es": 3.36, "Er": 25.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 3.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "AlN",
+ "description": "Aluminum nitride",
+ "density": 3.26,
+ "composition": [
+ { "symbol": "Al", "X": 1.0, "Ed": 25.0, "El": 3.36, "Es": 3.36, "Er": 25.0, "Rc": 0.946 },
+ { "symbol": "N", "X": 1.0, "Ed": 10.0, "El": 2.0, "Es": 2.0, "Er": 10.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Cu",
+ "description": "Copper",
+ "density": 8.96,
+ "composition": [
+ { "symbol": "Cu", "X": 1.0, "Ed": 30.0, "El": 3.49, "Es": 3.49, "Er": 30.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Ni",
+ "description": "Nickel",
+ "density": 8.90,
+ "composition": [
+ { "symbol": "Ni", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 4.5, "Er": 40.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Cr",
+ "description": "Chromium",
+ "density": 7.19,
+ "composition": [
+ { "symbol": "Cr", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 4.0, "Er": 40.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "NiCr",
+ "description": "Nickel-chromium alloy",
+ "density": 8.4,
+ "composition": [
+ { "symbol": "Ni", "X": 0.8, "Ed": 40.0, "El": 3.0, "Es": 4.5, "Er": 40.0, "Rc": 0.946 },
+ { "symbol": "Cr", "X": 0.2, "Ed": 40.0, "El": 3.0, "Es": 4.0, "Er": 40.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "SS304",
+ "description": "Stainless steel 304",
+ "density": 8.0,
+ "composition": [
+ { "symbol": "Fe", "X": 0.70, "Ed": 40.0, "El": 3.0, "Es": 4.28, "Er": 40.0, "Rc": 0.946 },
+ { "symbol": "Cr", "X": 0.19, "Ed": 40.0, "El": 3.0, "Es": 4.0, "Er": 40.0, "Rc": 0.946 },
+ { "symbol": "Ni", "X": 0.11, "Ed": 40.0, "El": 3.0, "Es": 4.5, "Er": 40.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "W",
+ "description": "Tungsten",
+ "density": 19.25,
+ "composition": [
+ { "symbol": "W", "X": 1.0, "Ed": 90.0, "El": 3.0, "Es": 8.79, "Er": 90.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "WC",
+ "description": "Tungsten carbide",
+ "density": 15.63,
+ "composition": [
+ { "symbol": "W", "X": 1.0, "Ed": 90.0, "El": 3.0, "Es": 8.79, "Er": 90.0, "Rc": 0.946 },
+ { "symbol": "C", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 7.4, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Au",
+ "description": "Gold",
+ "density": 19.32,
+ "composition": [
+ { "symbol": "Au", "X": 1.0, "Ed": 25.0, "El": 3.81, "Es": 3.81, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Ag",
+ "description": "Silver",
+ "density": 10.49,
+ "composition": [
+ { "symbol": "Ag", "X": 1.0, "Ed": 25.0, "El": 2.95, "Es": 2.95, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Pt",
+ "description": "Platinum",
+ "density": 21.45,
+ "composition": [
+ { "symbol": "Pt", "X": 1.0, "Ed": 30.0, "El": 3.5, "Es": 3.5, "Er": 30.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Pd",
+ "description": "Palladium",
+ "density": 12.02,
+ "composition": [
+ { "symbol": "Pd", "X": 1.0, "Ed": 30.0, "El": 3.0, "Es": 3.0, "Er": 30.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Pb",
+ "description": "Lead",
+ "density": 11.34,
+ "composition": [
+ { "symbol": "Pb", "X": 1.0, "Ed": 25.0, "El": 2.0, "Es": 2.0, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "PbWO4",
+ "description": "Lead tungstate",
+ "density": 8.28,
+ "composition": [
+ { "symbol": "Pb", "X": 1.0, "Ed": 25.0, "El": 2.0, "Es": 2.0, "Er": 25.0, "Rc": 0.946 },
+ { "symbol": "W", "X": 1.0, "Ed": 90.0, "El": 3.0, "Es": 8.79, "Er": 90.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 4.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Ti",
+ "description": "Titanium",
+ "density": 4.506,
+ "composition": [
+ { "symbol": "Ti", "X": 1.0, "Ed": 30.0, "El": 3.0, "Es": 4.85, "Er": 30.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "TiO2",
+ "description": "Titanium dioxide",
+ "density": 4.23,
+ "composition": [
+ { "symbol": "Ti", "X": 1.0, "Ed": 30.0, "El": 3.0, "Es": 4.85, "Er": 30.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Zr",
+ "description": "Zirconium",
+ "density": 6.52,
+ "composition": [
+ { "symbol": "Zr", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 6.25, "Er": 40.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "ZrO2",
+ "description": "Zirconia",
+ "density": 5.68,
+ "composition": [
+ { "symbol": "Zr", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 6.25, "Er": 40.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Hf",
+ "description": "Hafnium",
+ "density": 13.31,
+ "composition": [
+ { "symbol": "Hf", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 6.0, "Er": 40.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "HfO2",
+ "description": "Hafnia",
+ "density": 9.68,
+ "composition": [
+ { "symbol": "Hf", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 6.0, "Er": 40.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Y2O3",
+ "description": "Yttria",
+ "density": 5.01,
+ "composition": [
+ { "symbol": "Y", "X": 2.0, "Ed": 25.0, "El": 3.0, "Es": 3.0, "Er": 25.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 3.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "CeO2",
+ "description": "Ceria",
+ "density": 7.22,
+ "composition": [
+ { "symbol": "Ce", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 3.0, "Er": 25.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "UO2",
+ "description": "Uranium dioxide",
+ "density": 10.97,
+ "composition": [
+ { "symbol": "U", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 6.0, "Er": 40.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "GaAs",
+ "description": "Gallium arsenide",
+ "density": 5.32,
+ "composition": [
+ { "symbol": "Ga", "X": 1.0, "Ed": 10.0, "El": 3.0, "Es": 3.0, "Er": 10.0, "Rc": 0.946 },
+ { "symbol": "As", "X": 1.0, "Ed": 10.0, "El": 3.0, "Es": 3.0, "Er": 10.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "GaN",
+ "description": "Gallium nitride",
+ "density": 6.15,
+ "composition": [
+ { "symbol": "Ga", "X": 1.0, "Ed": 10.0, "El": 3.0, "Es": 3.0, "Er": 10.0, "Rc": 0.946 },
+ { "symbol": "N", "X": 1.0, "Ed": 10.0, "El": 2.0, "Es": 2.0, "Er": 10.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "InP",
+ "description": "Indium phosphide",
+ "density": 4.81,
+ "composition": [
+ { "symbol": "In", "X": 1.0, "Ed": 10.0, "El": 3.0, "Es": 3.0, "Er": 10.0, "Rc": 0.946 },
+ { "symbol": "P", "X": 1.0, "Ed": 10.0, "El": 3.0, "Es": 3.0, "Er": 10.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Ge",
+ "description": "Germanium",
+ "density": 5.32,
+ "composition": [
+ { "symbol": "Ge", "X": 1.0, "Ed": 15.0, "El": 3.0, "Es": 3.0, "Er": 15.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Li",
+ "description": "Lithium",
+ "density": 0.534,
+ "composition": [
+ { "symbol": "Li", "X": 1.0, "Ed": 10.0, "El": 2.0, "Es": 2.0, "Er": 10.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Be",
+ "description": "Beryllium",
+ "density": 1.85,
+ "composition": [
+ { "symbol": "Be", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 3.0, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "B",
+ "description": "Boron",
+ "density": 2.34,
+ "composition": [
+ { "symbol": "B", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 3.0, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "BN",
+ "description": "Boron nitride",
+ "density": 2.1,
+ "composition": [
+ { "symbol": "B", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 3.0, "Er": 25.0, "Rc": 0.946 },
+ { "symbol": "N", "X": 1.0, "Ed": 10.0, "El": 2.0, "Es": 2.0, "Er": 10.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Mg",
+ "description": "Magnesium",
+ "density": 1.74,
+ "composition": [
+ { "symbol": "Mg", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 2.9, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "MgO",
+ "description": "Magnesium oxide",
+ "density": 3.58,
+ "composition": [
+ { "symbol": "Mg", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 2.9, "Er": 25.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 1.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Ca",
+ "description": "Calcium",
+ "density": 1.55,
+ "composition": [
+ { "symbol": "Ca", "X": 1.0, "Ed": 20.0, "El": 2.0, "Es": 2.0, "Er": 20.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "CaO",
+ "description": "Calcium oxide",
+ "density": 3.34,
+ "composition": [
+ { "symbol": "Ca", "X": 1.0, "Ed": 20.0, "El": 2.0, "Es": 2.0, "Er": 20.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 1.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Co",
+ "description": "Cobalt",
+ "density": 8.90,
+ "composition": [
+ { "symbol": "Co", "X": 1.0, "Ed": 35.0, "El": 3.0, "Es": 3.0, "Er": 35.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Mn",
+ "description": "Manganese",
+ "density": 7.21,
+ "composition": [
+ { "symbol": "Mn", "X": 1.0, "Ed": 35.0, "El": 3.0, "Es": 3.0, "Er": 35.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "V",
+ "description": "Vanadium",
+ "density": 6.11,
+ "composition": [
+ { "symbol": "V", "X": 1.0, "Ed": 35.0, "El": 3.0, "Es": 3.0, "Er": 35.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Nb",
+ "description": "Niobium",
+ "density": 8.57,
+ "composition": [
+ { "symbol": "Nb", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 3.0, "Er": 40.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Mo",
+ "description": "Molybdenum",
+ "density": 10.28,
+ "composition": [
+ { "symbol": "Mo", "X": 1.0, "Ed": 60.0, "El": 3.0, "Es": 5.0, "Er": 60.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Ta",
+ "description": "Tantalum",
+ "density": 16.65,
+ "composition": [
+ { "symbol": "Ta", "X": 1.0, "Ed": 90.0, "El": 3.0, "Es": 8.0, "Er": 90.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Sn",
+ "description": "Tin",
+ "density": 7.31,
+ "composition": [
+ { "symbol": "Sn", "X": 1.0, "Ed": 25.0, "El": 2.0, "Es": 2.0, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Zn",
+ "description": "Zinc",
+ "density": 7.14,
+ "composition": [
+ { "symbol": "Zn", "X": 1.0, "Ed": 25.0, "El": 2.0, "Es": 2.0, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "CuZn",
+ "description": "Brass (Cu-Zn)",
+ "density": 8.5,
+ "composition": [
+ { "symbol": "Cu", "X": 0.7, "Ed": 30.0, "El": 3.49, "Es": 3.49, "Er": 30.0, "Rc": 0.946 },
+ { "symbol": "Zn", "X": 0.3, "Ed": 25.0, "El": 2.0, "Es": 2.0, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "CuSn",
+ "description": "Bronze (Cu-Sn)",
+ "density": 8.8,
+ "composition": [
+ { "symbol": "Cu", "X": 0.9, "Ed": 30.0, "El": 3.49, "Es": 3.49, "Er": 30.0, "Rc": 0.946 },
+ { "symbol": "Sn", "X": 0.1, "Ed": 25.0, "El": 2.0, "Es": 2.0, "Er": 25.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "H2O",
+ "description": "Water",
+ "density": 1.0,
+ "composition": [
+ { "symbol": "H", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 1.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ },
+ {
+ "id": "Ice",
+ "description": "Water ice",
+ "density": 0.92,
+ "composition": [
+ { "symbol": "H", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 },
+ { "symbol": "O", "X": 1.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }
+ ]
+ }
+]
diff --git a/source/gui/assets/data/tally_templates.json b/source/gui/assets/data/tally_templates.json
new file mode 100644
index 0000000..5e85e93
--- /dev/null
+++ b/source/gui/assets/data/tally_templates.json
@@ -0,0 +1,77 @@
+[
+ {
+ "id": "DepthProfile",
+ "description": "Ion implantation depth distribution along x-axis (100 bins, 0-100 nm)",
+ "event": "IonStop",
+ "bins": { "x": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0,
+ 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0,
+ 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0,
+ 30.0, 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0,
+ 40.0, 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0,
+ 50.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0,
+ 60.0, 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0,
+ 70.0, 71.0, 72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 79.0,
+ 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0,
+ 90.0, 91.0, 92.0, 93.0, 94.0, 95.0, 96.0, 97.0, 98.0, 99.0, 100.0] }
+ },
+ {
+ "id": "AngularDistribution",
+ "description": "Angular distribution of exiting ions (direction cosines nx, ny)",
+ "event": "IonExit",
+ "bins": {
+ "nx": [-1.0, -0.9, -0.8, -0.6, -0.4, -0.2, 0.0, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0],
+ "ny": [-1.0, -0.8, -0.6, -0.4, -0.2, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0]
+ }
+ },
+ {
+ "id": "EnergySpectrum",
+ "description": "Energy spectrum of stopped ions (logarithmic bins, 1 eV - 1 MeV)",
+ "event": "IonStop",
+ "bins": { "E": [1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0,
+ 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0, 100000.0,
+ 200000.0, 500000.0, 1000000.0] }
+ },
+ {
+ "id": "ImplantationMap",
+ "description": "2D implantation position map (x-depth vs y-lateral, 50x20 bins)",
+ "event": "IonStop",
+ "bins": {
+ "x": [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0,
+ 20.0, 22.0, 24.0, 26.0, 28.0, 30.0, 32.0, 34.0, 36.0, 38.0,
+ 40.0, 42.0, 44.0, 46.0, 48.0, 50.0, 52.0, 54.0, 56.0, 58.0,
+ 60.0, 62.0, 64.0, 66.0, 68.0, 70.0, 72.0, 74.0, 76.0, 78.0,
+ 80.0, 82.0, 84.0, 86.0, 88.0, 90.0, 92.0, 94.0, 96.0, 98.0, 100.0],
+ "y": [-50.0, -45.0, -40.0, -35.0, -30.0, -25.0, -20.0, -15.0, -10.0, -5.0,
+ 0.0, 5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0]
+ }
+ },
+ {
+ "id": "VacancyDepthProfile",
+ "description": "Vacancy depth distribution along x-axis (100 bins, 0-100 nm)",
+ "event": "Vacancy",
+ "bins": { "x": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0,
+ 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0,
+ 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0,
+ 30.0, 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0,
+ 40.0, 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0,
+ 50.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0,
+ 60.0, 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0,
+ 70.0, 71.0, 72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 79.0,
+ 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0,
+ 90.0, 91.0, 92.0, 93.0, 94.0, 95.0, 96.0, 97.0, 98.0, 99.0, 100.0] }
+ },
+ {
+ "id": "LateralSpread",
+ "description": "Cylindrical radius r vs depth x (lateral spread map, 20x50 bins)",
+ "event": "IonStop",
+ "bins": {
+ "r": [0.0, 2.5, 5.0, 7.5, 10.0, 12.5, 15.0, 17.5, 20.0, 22.5,
+ 25.0, 27.5, 30.0, 32.5, 35.0, 37.5, 40.0, 42.5, 45.0, 47.5, 50.0],
+ "x": [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0,
+ 20.0, 22.0, 24.0, 26.0, 28.0, 30.0, 32.0, 34.0, 36.0, 38.0,
+ 40.0, 42.0, 44.0, 46.0, 48.0, 50.0, 52.0, 54.0, 56.0, 58.0,
+ 60.0, 62.0, 64.0, 66.0, 68.0, 70.0, 72.0, 74.0, 76.0, 78.0,
+ 80.0, 82.0, 84.0, 86.0, 88.0, 90.0, 92.0, 94.0, 96.0, 98.0, 100.0]
+ }
+ }
+]
diff --git a/source/gui/assets/ionicons/copy-outline.svg b/source/gui/assets/ionicons/copy-outline.svg
new file mode 100644
index 0000000..ef3cb11
--- /dev/null
+++ b/source/gui/assets/ionicons/copy-outline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/source/gui/assets/lucide/database-search.svg b/source/gui/assets/lucide/database-search.svg
new file mode 100644
index 0000000..8ced514
--- /dev/null
+++ b/source/gui/assets/lucide/database-search.svg
@@ -0,0 +1,18 @@
+
diff --git a/source/gui/materialdatabasedialog.cpp b/source/gui/materialdatabasedialog.cpp
new file mode 100644
index 0000000..3b346df
--- /dev/null
+++ b/source/gui/materialdatabasedialog.cpp
@@ -0,0 +1,256 @@
+#include "materialdatabasedialog.h"
+
+#include "json_defs_p.h"
+#include "periodic_table.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+const char *kDbPath = ":/assets/data/materials_database.json";
+
+bool containsCaseInsensitive(const QString &haystack, const QString &needle)
+{
+ if (needle.trimmed().isEmpty())
+ return true;
+ return haystack.contains(needle, Qt::CaseInsensitive);
+}
+
+QString joinMaterialElements(const material::material_desc_t &mat)
+{
+ QStringList symbols;
+ for (const auto &a : mat.composition)
+ symbols << QString::fromStdString(a.element.symbol);
+ symbols.removeDuplicates();
+ return symbols.join(", ");
+}
+
+} // namespace
+
+MaterialDatabaseDialog::MaterialDatabaseDialog(QWidget *parent) : QDialog(parent)
+{
+ setWindowTitle("Material Database");
+ resize(900, 520);
+
+ QVBoxLayout *root = new QVBoxLayout(this);
+
+ QHBoxLayout *filters = new QHBoxLayout;
+ filters->addWidget(new QLabel("Name filter:"));
+ nameFilter_ = new QLineEdit;
+ nameFilter_->setPlaceholderText("iron");
+ filters->addWidget(nameFilter_, 1);
+ filters->addSpacing(10);
+ filters->addWidget(new QLabel("Element filter:"));
+ elementFilter_ = new QLineEdit;
+ elementFilter_->setPlaceholderText("Fe");
+ filters->addWidget(elementFilter_);
+ root->addLayout(filters);
+
+ QHBoxLayout *content = new QHBoxLayout;
+ listWidget_ = new QListWidget;
+ listWidget_->setMinimumWidth(320);
+ content->addWidget(listWidget_, 1);
+
+ QVBoxLayout *preview = new QVBoxLayout;
+ previewTitle_ = new QLabel("Preview");
+ densityLabel_ = new QLabel("Density:");
+ elementsLabel_ = new QLabel("Elements:");
+
+ preview->addWidget(previewTitle_);
+ preview->addWidget(densityLabel_);
+ preview->addWidget(elementsLabel_);
+
+ compositionTable_ = new QTableWidget;
+ compositionTable_->setColumnCount(7);
+ compositionTable_->setHorizontalHeaderLabels(
+ { "Symbol", "X", "Ed", "El", "Es", "Er", "Rc" });
+ compositionTable_->verticalHeader()->setVisible(false);
+ compositionTable_->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ compositionTable_->setSelectionMode(QAbstractItemView::NoSelection);
+ compositionTable_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
+ preview->addWidget(compositionTable_, 1);
+
+ content->addLayout(preview, 2);
+ root->addLayout(content, 1);
+
+ QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Cancel);
+ acceptButton_ = box->addButton("Add to simulation", QDialogButtonBox::AcceptRole);
+ acceptButton_->setEnabled(false);
+ connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ root->addWidget(box);
+
+ connect(nameFilter_, &QLineEdit::textChanged, this, &MaterialDatabaseDialog::applyFilters);
+ connect(elementFilter_, &QLineEdit::textChanged, this, &MaterialDatabaseDialog::applyFilters);
+ connect(listWidget_, &QListWidget::currentRowChanged, this,
+ &MaterialDatabaseDialog::onCurrentMaterialChanged);
+
+ if (!loadDatabase()) {
+ QMessageBox::warning(this, "Material database",
+ "Failed to load embedded materials database.");
+ }
+ rebuildList();
+}
+
+material::material_desc_t MaterialDatabaseDialog::selectedMaterial() const
+{
+ int row = listWidget_->currentRow();
+ if (row < 0 || row >= static_cast(visibleRows_.size()))
+ return material::material_desc_t();
+
+ return allEntries_[visibleRows_[row]].mat;
+}
+
+bool MaterialDatabaseDialog::loadDatabase()
+{
+ allEntries_.clear();
+
+ QFile f(kDbPath);
+ if (!f.open(QIODevice::ReadOnly | QIODevice::Text))
+ return false;
+
+ ojson j;
+ try {
+ j = ojson::parse(f.readAll().toStdString(), nullptr, true, true);
+ } catch (...) {
+ return false;
+ }
+
+ if (!j.is_array())
+ return false;
+
+ for (const auto &node : j) {
+ if (!node.is_object() || !node.contains("id") || !node.contains("composition"))
+ continue;
+
+ DbEntry e;
+ e.mat.id = node.value("id", std::string("Material"));
+ e.mat.density = node.value("density", 1.0f);
+ e.mat.color = node.value("color", std::string("#55aaff"));
+
+ const std::string description = node.value("description", std::string());
+ e.description = QString::fromStdString(description);
+
+ const auto &comp = node["composition"];
+ if (comp.is_array()) {
+ for (const auto &atomNode : comp) {
+ atom::parameters a;
+ std::string symbol = atomNode.value("symbol", std::string("H"));
+ a.element.symbol = symbol;
+ const auto &pt = periodic_table::at(symbol);
+ if (pt.is_valid()) {
+ a.element.atomic_number = pt.Z;
+ a.element.atomic_mass = static_cast(pt.mass);
+ }
+
+ a.X = atomNode.value("X", 1.0f);
+ a.Ed = atomNode.value("Ed", 40.0f);
+ a.El = atomNode.value("El", 3.0f);
+ a.Es = atomNode.value("Es", 10.0f);
+ a.Er = atomNode.value("Er", 40.0f);
+ a.Rc = atomNode.value("Rc", 0.946f);
+ e.mat.composition.push_back(a);
+ }
+ }
+
+ e.displayText = QString::fromStdString(e.mat.id);
+ if (!e.description.isEmpty())
+ e.displayText += " - " + e.description;
+
+ allEntries_.push_back(e);
+ }
+
+ return !allEntries_.empty();
+}
+
+void MaterialDatabaseDialog::rebuildList()
+{
+ listWidget_->clear();
+ visibleRows_.clear();
+
+ const QString nameNeedle = nameFilter_->text().trimmed();
+ const QString elementNeedle = elementFilter_->text().trimmed();
+
+ for (int i = 0; i < static_cast(allEntries_.size()); ++i) {
+ const auto &entry = allEntries_[i];
+
+ QString nameText = entry.displayText + " " + QString::fromStdString(entry.mat.id);
+ if (!containsCaseInsensitive(nameText, nameNeedle))
+ continue;
+
+ if (!elementNeedle.isEmpty()) {
+ bool elementMatch = false;
+ for (const auto &a : entry.mat.composition) {
+ const QString symbol = QString::fromStdString(a.element.symbol);
+ if (symbol.startsWith(elementNeedle, Qt::CaseInsensitive)) {
+ elementMatch = true;
+ break;
+ }
+ }
+ if (!elementMatch)
+ continue;
+ }
+
+ visibleRows_.push_back(i);
+ listWidget_->addItem(entry.displayText);
+ }
+
+ if (listWidget_->count() > 0)
+ listWidget_->setCurrentRow(0);
+ else
+ updatePreview(-1);
+}
+
+void MaterialDatabaseDialog::updatePreview(int row)
+{
+ compositionTable_->setRowCount(0);
+
+ if (row < 0 || row >= static_cast(visibleRows_.size())) {
+ previewTitle_->setText("Preview");
+ densityLabel_->setText("Density:");
+ elementsLabel_->setText("Elements:");
+ acceptButton_->setEnabled(false);
+ return;
+ }
+
+ const auto &entry = allEntries_[visibleRows_[row]];
+ const auto &mat = entry.mat;
+
+ previewTitle_->setText(QString("Preview: %1").arg(mat.id.c_str()));
+ densityLabel_->setText(QString("Density: %1 g/cm^3").arg(mat.density));
+ elementsLabel_->setText(QString("Elements: %1").arg(joinMaterialElements(mat)));
+
+ compositionTable_->setRowCount(static_cast(mat.composition.size()));
+ for (int i = 0; i < static_cast(mat.composition.size()); ++i) {
+ const auto &a = mat.composition[i];
+ compositionTable_->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(a.element.symbol)));
+ compositionTable_->setItem(i, 1, new QTableWidgetItem(QString::number(a.X)));
+ compositionTable_->setItem(i, 2, new QTableWidgetItem(QString::number(a.Ed)));
+ compositionTable_->setItem(i, 3, new QTableWidgetItem(QString::number(a.El)));
+ compositionTable_->setItem(i, 4, new QTableWidgetItem(QString::number(a.Es)));
+ compositionTable_->setItem(i, 5, new QTableWidgetItem(QString::number(a.Er)));
+ compositionTable_->setItem(i, 6, new QTableWidgetItem(QString::number(a.Rc)));
+ }
+
+ acceptButton_->setEnabled(true);
+}
+
+void MaterialDatabaseDialog::applyFilters()
+{
+ rebuildList();
+}
+
+void MaterialDatabaseDialog::onCurrentMaterialChanged(int row)
+{
+ updatePreview(row);
+}
diff --git a/source/gui/materialdatabasedialog.h b/source/gui/materialdatabasedialog.h
new file mode 100644
index 0000000..d6d4de5
--- /dev/null
+++ b/source/gui/materialdatabasedialog.h
@@ -0,0 +1,54 @@
+#ifndef MATERIALDATABASEDIALOG_H
+#define MATERIALDATABASEDIALOG_H
+
+#include
+#include
+
+#include
+
+#include "target.h"
+
+class QListWidget;
+class QLabel;
+class QLineEdit;
+class QTableWidget;
+class QPushButton;
+
+class MaterialDatabaseDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit MaterialDatabaseDialog(QWidget *parent = nullptr);
+
+ material::material_desc_t selectedMaterial() const;
+
+private slots:
+ void applyFilters();
+ void onCurrentMaterialChanged(int row);
+
+private:
+ struct DbEntry {
+ material::material_desc_t mat;
+ QString description;
+ QString displayText;
+ };
+
+ bool loadDatabase();
+ void rebuildList();
+ void updatePreview(int row);
+
+ QLineEdit *nameFilter_;
+ QLineEdit *elementFilter_;
+ QListWidget *listWidget_;
+ QLabel *previewTitle_;
+ QLabel *densityLabel_;
+ QLabel *elementsLabel_;
+ QTableWidget *compositionTable_;
+ QPushButton *acceptButton_;
+
+ std::vector allEntries_;
+ std::vector visibleRows_;
+};
+
+#endif // MATERIALDATABASEDIALOG_H
diff --git a/source/gui/materialsdefview.cpp b/source/gui/materialsdefview.cpp
index b223715..35ea04d 100644
--- a/source/gui/materialsdefview.cpp
+++ b/source/gui/materialsdefview.cpp
@@ -3,6 +3,7 @@
#include "periodic_table.h"
#include "periodictablewidget.h"
#include "floatlineedit.h"
+#include "materialdatabasedialog.h"
#include "optionsmodel.h"
#include
@@ -20,6 +21,7 @@
#include
#include
#include
+#include
void MaterialsDefView::setBtMatColor(const QColor &clr)
{
@@ -44,27 +46,35 @@ MaterialsDefView::MaterialsDefView(OptionsModel *m, QWidget *parent) : QWidget{
QFormLayout *flayout = new QFormLayout;
cbMaterialID = new MyComboBox;
- cbMaterialID->setPlaceholderText("Material id");
+ cbMaterialID->setPlaceholderText("Add material or search database...");
cbMaterialID->setMinimumContentsLength(15);
+ cbMaterialID->setEnabled(false);
+ cbMaterialID->setToolTip(
+ "Select material. If empty, use 'Add material from database' or 'Add new material'.");
connect(cbMaterialID, &MyComboBox::doubleClicked, this, &MaterialsDefView::editMaterialName);
- connect(cbMaterialID, &MyComboBox::currentTextChanged, this,
+ connect(cbMaterialID, QOverload::of(&QComboBox::currentIndexChanged), this,
&MaterialsDefView::updateSelectedMaterial);
+ btDbMaterial = new QToolButton;
+ btDbMaterial->setIcon(QIcon(":/assets/lucide/database-search.svg"));
+ btDbMaterial->setToolTip("Add material from database");
+
btAddMaterial = new QToolButton;
btAddMaterial->setIcon(QIcon(":/assets/ionicons/add-outline.svg"));
- btAddMaterial->setToolTip("Add Material");
+ btAddMaterial->setToolTip("Add new material");
btDelMaterial = new QToolButton;
btDelMaterial->setIcon(QIcon(":/assets/ionicons/remove-outline.svg"));
- btDelMaterial->setToolTip("Remove Material");
+ btDelMaterial->setToolTip("Remove material");
btDelMaterial->setEnabled(false);
btEdtMaterial = new QToolButton;
btEdtMaterial->setIcon(QIcon(":/assets/ionicons/create-outline.svg"));
- btEdtMaterial->setToolTip("Edit Material's Name");
+ btEdtMaterial->setToolTip("Rename material");
btEdtMaterial->setEnabled(false);
+ connect(btDbMaterial, &QToolButton::clicked, this, &MaterialsDefView::addFromDatabase);
connect(btAddMaterial, &QToolButton::clicked, this, &MaterialsDefView::addMaterial);
connect(btDelMaterial, &QToolButton::clicked, this, &MaterialsDefView::removeMaterial);
connect(btEdtMaterial, &QToolButton::clicked, this, &MaterialsDefView::editMaterialName);
@@ -73,6 +83,7 @@ MaterialsDefView::MaterialsDefView(OptionsModel *m, QWidget *parent) : QWidget{
sbDensity->setMinimum(0.001);
sbDensity->setMaximum(100.0);
sbDensity->setDecimals(4);
+ sbDensity->setEnabled(false);
connect(sbDensity, SIGNAL(valueChanged(double)), this, SLOT(setDensity(double)));
btMatColor = new QToolButton;
@@ -80,9 +91,11 @@ MaterialsDefView::MaterialsDefView(OptionsModel *m, QWidget *parent) : QWidget{
btMatColor->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
setBtMatColor(Qt::darkBlue);
btMatColor->setToolTip("Select display color for this material");
+ btMatColor->setEnabled(false);
connect(btMatColor, &QToolButton::pressed, this, &MaterialsDefView::selectColor);
hbox->addWidget(cbMaterialID);
+ hbox->addWidget(btDbMaterial);
hbox->addWidget(btAddMaterial);
hbox->addWidget(btDelMaterial);
hbox->addWidget(btEdtMaterial);
@@ -105,6 +118,26 @@ MaterialsDefView::MaterialsDefView(OptionsModel *m, QWidget *parent) : QWidget{
setLayout(vbox);
}
+void MaterialsDefView::addFromDatabase()
+{
+ MaterialDatabaseDialog dlg(this);
+ if (dlg.exec() != QDialog::Accepted)
+ return;
+
+ material::material_desc_t mat = dlg.selectedMaterial();
+ if (mat.id.empty())
+ return;
+
+ auto &materials = model_->options()->Target.materials;
+ materials.push_back(mat);
+ setWidgetData();
+ cbMaterialID->setCurrentIndex(cbMaterialID->count() - 1);
+ updateSelectedMaterial();
+ model_->notifyDataChanged(materialsIndex_);
+
+ emit materialsChanged();
+}
+
void MaterialsDefView::addMaterial()
{
bool ok;
@@ -117,10 +150,9 @@ void MaterialsDefView::addMaterial()
newMaterial.id = id.toStdString();
materials.push_back(newMaterial);
setWidgetData(); // widgets updated
- cbMaterialID->setCurrentText(id);
- // fake setData just to let model_ know that
- // underlying data changed
- model_->setData(materialsIndex_, QVariant());
+ cbMaterialID->setCurrentIndex(cbMaterialID->count() - 1);
+ updateSelectedMaterial();
+ model_->notifyDataChanged(materialsIndex_);
emit materialsChanged();
}
@@ -140,9 +172,7 @@ void MaterialsDefView::editMaterialName()
cbMaterialID->setItemText(i, id);
material::material_desc_t &m = model_->options()->Target.materials[i];
m.id = id.toStdString();
- // fake setData just to let model_ know that
- // underlying data changed
- model_->setData(materialsIndex_, QVariant());
+ model_->notifyDataChanged(materialsIndex_);
}
setValueData(); // update material name
}
@@ -160,10 +190,7 @@ void MaterialsDefView::removeMaterial()
auto &materials = model_->options()->Target.materials;
materials.erase(materials.begin() + i);
setWidgetData(); // widgets updated
- // fake setData just to let model_ know that
- // underlying data changed
- model_->setData(materialsIndex_, QVariant());
-
+ model_->notifyDataChanged(materialsIndex_);
emit materialsChanged();
}
}
@@ -172,7 +199,7 @@ void MaterialsDefView::setWidgetData()
{
auto &materials = model_->options()->Target.materials;
- int i = cbMaterialID->currentIndex();
+ int prevIdx = cbMaterialID->currentIndex();
cbMaterialID->blockSignals(true);
cbMaterialID->clear();
@@ -180,29 +207,39 @@ void MaterialsDefView::setWidgetData()
materialsView->setMaterialIdx();
if (materials.empty()) {
+ cbMaterialID->setPlaceholderText("No materials - use + or DB button");
+ cbMaterialID->setEnabled(false);
+ cbMaterialID->setCurrentIndex(-1);
+ sbDensity->setEnabled(false);
+ btMatColor->setEnabled(false);
+ btDelMaterial->setEnabled(false);
+ btEdtMaterial->setEnabled(false);
cbMaterialID->blockSignals(false);
return;
}
- // copy materials to combo box
+ // Copy materials to combo box and disambiguate duplicate IDs in display text.
int n = materials.size();
+ QHash idCount;
for (int k = 0; k < n; ++k) {
- cbMaterialID->addItem(QString::fromStdString(materials[k].id));
+ QString id = QString::fromStdString(materials[k].id).trimmed();
+ if (id.isEmpty())
+ id = QString("Material #%1").arg(k + 1);
+ int occ = ++idCount[id];
+ QString displayId = (occ > 1) ? QString("%1 (%2)").arg(id).arg(occ) : id;
+ cbMaterialID->addItem(displayId);
}
- // update selection if out of bounds
- if (n) {
- if (i < 0)
- i = 0;
- else if (i >= n)
- i = n - 1;
- } else
- i = -1;
-
- // set data to selected material
+ // Always select the first material if previously empty, else keep previous index if valid
+ int i = (prevIdx < 0 || prevIdx >= n) ? 0 : prevIdx;
cbMaterialID->setCurrentIndex(i);
+
+ cbMaterialID->setEnabled(true);
+ sbDensity->setEnabled(true);
+ btMatColor->setEnabled(true);
btDelMaterial->setEnabled(i >= 0);
btEdtMaterial->setEnabled(i >= 0);
+
sbDensity->blockSignals(true);
sbDensity->setValue(materials[i].density);
sbDensity->blockSignals(false);
@@ -215,15 +252,31 @@ void MaterialsDefView::setValueData() { }
void MaterialsDefView::updateSelectedMaterial()
{
+ auto &materials = model_->options()->Target.materials;
+ if (materials.empty()) {
+ sbDensity->clear();
+ sbDensity->setEnabled(false);
+ btMatColor->setEnabled(false);
+ materialsView->setMaterialIdx();
+ btDelMaterial->setEnabled(false);
+ btEdtMaterial->setEnabled(false);
+ return;
+ }
+
int i = cbMaterialID->currentIndex();
- if (i < 0) { // no selection
+ if (i < 0 || i >= materials.size()) { // no valid selection
+ cbMaterialID->setCurrentIndex(-1);
sbDensity->clear();
+ sbDensity->setEnabled(false);
+ btMatColor->setEnabled(false);
materialsView->setMaterialIdx();
} else {
- const material::material_desc_t &mat = model_->options()->Target.materials[i];
+ const material::material_desc_t &mat = materials[i];
sbDensity->blockSignals(true);
sbDensity->setValue(mat.density);
sbDensity->blockSignals(false);
+ sbDensity->setEnabled(true);
+ btMatColor->setEnabled(true);
setBtMatColor(QColor(mat.color.c_str()));
materialsView->setMaterialIdx(i);
}
@@ -245,9 +298,7 @@ void MaterialsDefView::setDensity(double v)
QString matid = cbMaterialID->currentText();
material::material_desc_t &mat = materials[i];
mat.density = v;
- // fake setData just to let model_ know that
- // underlying data changed
- model_->setData(materialsIndex_, QVariant());
+ model_->notifyDataChanged(materialsIndex_);
}
void MaterialsDefView::selectColor()
@@ -269,9 +320,7 @@ void MaterialsDefView::selectColor()
if (clr.isValid()) {
mat.color = clr.name(QColor::HexArgb).toStdString();
setBtMatColor(clr);
- // fake setData just to let model_ know that
- // underlying data changed
- model_->setData(materialsIndex_, QVariant());
+ model_->notifyDataChanged(materialsIndex_);
emit materialsChanged();
}
@@ -431,9 +480,7 @@ bool MaterialCompositionModel::setData(const QModelIndex &index, const QVariant
default:;
}
- // fake setData just to let model_ know that
- // underlying data changed
- model_->setData(materialsIndex_, QVariant());
+ model_->notifyDataChanged(materialsIndex_);
return true;
}
@@ -451,9 +498,7 @@ bool MaterialCompositionModel::insertRows(int position, int rows, const QModelIn
mat->composition.push_back(atom::parameters());
endInsertRows();
- // fake setData just to let model_ know that
- // underlying data changed
- model_->setData(materialsIndex_, QVariant());
+ model_->notifyDataChanged(materialsIndex_);
return true;
}
@@ -473,9 +518,7 @@ bool MaterialCompositionModel::removeRows(int position, int rows, const QModelIn
mat->composition.erase(mat->composition.begin() + position);
endRemoveRows();
- // fake setData just to let model_ know that
- // underlying data changed
- model_->setData(materialsIndex_, QVariant());
+ model_->notifyDataChanged(materialsIndex_);
return true;
}
diff --git a/source/gui/materialsdefview.h b/source/gui/materialsdefview.h
index 6e3c888..b5668f9 100644
--- a/source/gui/materialsdefview.h
+++ b/source/gui/materialsdefview.h
@@ -23,6 +23,7 @@ class MaterialsDefView : public QWidget
Q_OBJECT
MyComboBox *cbMaterialID;
+ QToolButton *btDbMaterial;
QToolButton *btAddMaterial;
QToolButton *btDelMaterial;
QToolButton *btEdtMaterial;
@@ -42,6 +43,7 @@ class MaterialsDefView : public QWidget
void materialsChanged();
public slots:
+ void addFromDatabase();
void addMaterial();
void removeMaterial();
void editMaterialName();
diff --git a/source/gui/opentrim.qrc b/source/gui/opentrim.qrc
index bd0ff0d..d056227 100644
--- a/source/gui/opentrim.qrc
+++ b/source/gui/opentrim.qrc
@@ -26,6 +26,9 @@
assets/ionicons/list-outline.png
assets/ionicons/bar-chart-outline.png
assets/lucide/chart-line.svg
+ assets/lucide/database-search.svg
+ assets/data/materials_database.json
+ assets/data/tally_templates.json
md/quick_start.md
md/images/intro.png
md/images/intro22.png
@@ -37,6 +40,7 @@
assets/ionicons/open-outline.svg
assets/3d/cubeFlat.obj
assets/ionicons/create-outline.svg
+ assets/ionicons/copy-outline.svg
examples/270keV_He_on_C.json
diff --git a/source/gui/optionsmodel.cpp b/source/gui/optionsmodel.cpp
index 9a5ef4a..746a032 100644
--- a/source/gui/optionsmodel.cpp
+++ b/source/gui/optionsmodel.cpp
@@ -67,9 +67,19 @@ QVariant OptionsItem::value() const
}
bool OptionsItem::setValue(const QVariant &v)
{
+ QString vnew = v.toString();
+ if (vnew.trimmed().isEmpty()) {
+ const QString k = key().trimmed();
+ const QString jp = QString::fromStdString(jpath_);
+ if (k == "materials" || k == "regions" || k == "UserTally"
+ || jp.endsWith("/materials") || jp.endsWith("/regions") || jp.endsWith("/UserTally")) {
+ vnew = "[]";
+ }
+ }
+
QVariant v0 = value();
- if (v0.toString() != v.toString()) {
- set_(v.toString());
+ if (v0.toString() != vnew) {
+ set_(vnew);
return true;
}
return false;
@@ -112,6 +122,21 @@ bool OptionsItem::get_(QString &qs) const
bool OptionsItem::set_(const QString &qs)
{
std::string s = qs.toStdString();
+ if (qs.trimmed().isEmpty()) {
+ const QString jp = QString::fromStdString(jpath_);
+ if (jp.endsWith("/materials") || jp.endsWith("/regions") || jp.endsWith("/UserTally")) {
+ s = "[]";
+ }
+
+ QString current;
+ if (s.empty() && get_(current)) {
+ const QString trimmed = current.trimmed();
+ if (trimmed.startsWith('['))
+ s = "[]";
+ else if (trimmed.startsWith('{'))
+ s = "{}";
+ }
+ }
std::ostringstream os;
bool ret = options_->set(jpath_, s, &os);
if (!ret) {
@@ -450,17 +475,25 @@ QVariant OptionsModel::data(const QModelIndex &index, int role) const
}
bool OptionsModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
- // int col = index.column();
if (Qt::EditRole == role) {
- // if (col == 1) {
+ if (!index.isValid() || index.column() != 1 || !value.isValid())
+ return false;
+
OptionsItem *item = static_cast(index.internalPointer());
+ // Base OptionsItem represents struct/array containers; these should never be edited directly.
+ if (typeid(*item) == typeid(OptionsItem))
+ return false;
+
if (item->setValue(value))
emit dataChanged(index, index, { Qt::EditRole });
return true;
- //}
}
return false;
}
+void OptionsModel::notifyDataChanged(const QModelIndex &index)
+{
+ emit dataChanged(index, index, { Qt::EditRole });
+}
QVariant OptionsModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role != Qt::DisplayRole)
@@ -552,10 +585,16 @@ int OptionsModel::columnCount(const QModelIndex &parent) const
Qt::ItemFlags OptionsModel::flags(const QModelIndex &index) const
{
- if (index.column() == 1)
- return Qt::ItemIsEditable | QAbstractItemModel::flags(index);
- else
- return QAbstractItemModel::flags(index);
+ Qt::ItemFlags baseFlags = QAbstractItemModel::flags(index);
+ if (!index.isValid() || index.column() != 1)
+ return baseFlags;
+
+ OptionsItem *item = static_cast(index.internalPointer());
+ // Only typed option nodes are editable. Container nodes (struct/array) are read-only.
+ if (item && typeid(*item) != typeid(OptionsItem))
+ return baseFlags | Qt::ItemIsEditable;
+
+ return baseFlags;
}
OptionsItem *OptionsModel::getItem(const QModelIndex &index) const
diff --git a/source/gui/optionsmodel.h b/source/gui/optionsmodel.h
index 41f4c86..a3dd628 100644
--- a/source/gui/optionsmodel.h
+++ b/source/gui/optionsmodel.h
@@ -206,6 +206,7 @@ class OptionsModel : public QAbstractItemModel
Qt::ItemFlags flags(const QModelIndex &index) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
+ void notifyDataChanged(const QModelIndex &index);
OptionsItem *getItem(const QModelIndex &index) const;
diff --git a/source/gui/regionsview.cpp b/source/gui/regionsview.cpp
index d9fae3c..a234a66 100644
--- a/source/gui/regionsview.cpp
+++ b/source/gui/regionsview.cpp
@@ -137,9 +137,7 @@ bool RegionsModel::setData(const QModelIndex &index, const QVariant &value, int
break;
}
- // fake setData just to let model_ know that
- // underlying data changed
- model_->setData(regionsIndex_, QVariant());
+ model_->notifyDataChanged(regionsIndex_);
emit dataChanged(index, index);
@@ -161,9 +159,7 @@ bool RegionsModel::insertRows(int position, int rows, const QModelIndex &parent)
opt->Target.regions.push_back(reg);
endInsertRows();
- // fake setData just to let model_ know that
- // underlying data changed
- model_->setData(regionsIndex_, QVariant());
+ model_->notifyDataChanged(regionsIndex_);
return true;
}
@@ -181,9 +177,7 @@ bool RegionsModel::removeRows(int position, int rows, const QModelIndex &parent)
regions.erase(it);
endRemoveRows();
- // fake setData just to let model_ know that
- // underlying data changed
- model_->setData(regionsIndex_, QVariant());
+ model_->notifyDataChanged(regionsIndex_);
return true;
}
@@ -215,9 +209,7 @@ bool RegionsModel::moveRow(int from, int to)
regions.insert(regions.begin() + to, reg);
endInsertRows();
- // fake setData just to let model_ know that
- // underlying data changed
- model_->setData(regionsIndex_, QVariant());
+ model_->notifyDataChanged(regionsIndex_);
return true;
}
diff --git a/source/gui/simulationoptionsview.cpp b/source/gui/simulationoptionsview.cpp
index 3e14ba9..4195730 100644
--- a/source/gui/simulationoptionsview.cpp
+++ b/source/gui/simulationoptionsview.cpp
@@ -3,6 +3,7 @@
#include "periodic_table.h"
#include "periodictablewidget.h"
#include "materialsdefview.h"
+#include "usertallyview.h"
#include "regionsview.h"
#include "optionsmodel.h"
#include "mydatawidgetmapper.h"
@@ -72,7 +73,8 @@ SimulationOptionsView::SimulationOptionsView(MainUI *iui, QWidget *parent)
else if (category == "Target")
widget = createTargetTab(idx);
else if (category == "UserTally") {
- // TODO !!
+ userTallyView_ = new UserTallyView(model);
+ widget = userTallyView_;
} else
widget = createTab(idx);
@@ -193,6 +195,8 @@ void SimulationOptionsView::revert()
mapper->model()->setOptions(opt);
mapper->revert();
materialsView->setWidgetData();
+ if (userTallyView_)
+ userTallyView_->setWidgetData();
regionsView->revert();
jsonView->setPlainText(QString::fromStdString(ionsui->driverObj()->json()));
@@ -443,6 +447,8 @@ void SimulationOptionsView::onDriverStatusChanged()
mapper->setEnabled(isreset);
btSelectIon->setEnabled(isreset);
materialsView->setEnabled(isreset);
+ if (userTallyView_)
+ userTallyView_->setEnabled(isreset);
regionsView->setEnabled(isreset);
if (isreset)
applyRules();
diff --git a/source/gui/simulationoptionsview.h b/source/gui/simulationoptionsview.h
index a470cb3..99e8062 100644
--- a/source/gui/simulationoptionsview.h
+++ b/source/gui/simulationoptionsview.h
@@ -13,6 +13,7 @@ class QLineEdit;
class MyDataWidgetMapper;
class MaterialsDefView;
+class UserTallyView;
class RegionsView;
class SimBoxView;
class OptionsModel;
@@ -74,6 +75,7 @@ public slots:
QLineEdit *simTitle;
MyDataWidgetMapper *mapper;
MaterialsDefView *materialsView;
+ UserTallyView *userTallyView_{ nullptr };
RegionsView *regionsView;
SimBoxView *simBoxView;
QDialogButtonBox *buttonBox;
diff --git a/source/gui/usertallyview.cpp b/source/gui/usertallyview.cpp
new file mode 100644
index 0000000..a8a99a5
--- /dev/null
+++ b/source/gui/usertallyview.cpp
@@ -0,0 +1,1126 @@
+#include "usertallyview.h"
+
+#include "json_defs_p.h"
+#include "optionsmodel.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+const char *kTemplatePath = ":/assets/data/tally_templates.json";
+
+struct VariableInfo {
+ const char *name;
+ const char *description;
+};
+
+const VariableInfo kVars[] = {
+ { "x", "Position x" }, { "y", "Position y" },
+ { "z", "Position z" }, { "r", "Radius" },
+ { "rho", "Cylindrical rho" }, { "cosTheta", "Direction cosine theta" },
+ { "nx", "Direction x" }, { "ny", "Direction y" },
+ { "nz", "Direction z" }, { "E", "Ion energy" },
+ { "Tdam", "Damage energy" }, { "V", "Vacancies" },
+ { "atom_id", "Atomic species" }, { "recoil_id", "Recoil generation" }
+};
+
+Event parseEvent(const QString &name)
+{
+ if (name == "IonExit")
+ return Event::IonExit;
+ if (name == "Vacancy")
+ return Event::Vacancy;
+ if (name == "Replacement")
+ return Event::Replacement;
+ if (name == "CascadeComplete")
+ return Event::CascadeComplete;
+ if (name == "BoundaryCrossing")
+ return Event::BoundaryCrossing;
+ return Event::IonStop;
+}
+
+QString eventToString(Event ev)
+{
+ return event_name(ev);
+}
+
+void setSpin3(QDoubleSpinBox *sb[3], const vector3 &v)
+{
+ sb[0]->setValue(v.x());
+ sb[1]->setValue(v.y());
+ sb[2]->setValue(v.z());
+}
+
+vector3 spin3Value(QDoubleSpinBox *sb[3])
+{
+ return vector3(sb[0]->value(), sb[1]->value(), sb[2]->value());
+}
+
+bool isLabFrame(const coord_sys &cs)
+{
+ const vector3 O0(0.f, 0.f, 0.f);
+ const vector3 Z0(0.f, 0.f, 1.f);
+ const vector3 XZ0(1.f, 0.f, 1.f);
+ return cs.origin == O0 && cs.zaxis == Z0 && cs.xzvector == XZ0;
+}
+
+std::vector binsToRows(const user_tally::bin_var_t &bins)
+{
+ std::vector rows;
+ auto pushIf = [&rows](const char *name, const std::vector &v) {
+ if (v.size() >= 2)
+ rows.push_back({ name, BinVariableModel::edgesToString(v) });
+ };
+
+ pushIf("x", bins.x);
+ pushIf("y", bins.y);
+ pushIf("z", bins.z);
+ pushIf("r", bins.r);
+ pushIf("rho", bins.rho);
+ pushIf("cosTheta", bins.cosTheta);
+ pushIf("nx", bins.nx);
+ pushIf("ny", bins.ny);
+ pushIf("nz", bins.nz);
+ pushIf("E", bins.E);
+ pushIf("Tdam", bins.Tdam);
+ pushIf("V", bins.V);
+ pushIf("atom_id", bins.atom_id);
+ pushIf("recoil_id", bins.recoil_id);
+
+ return rows;
+}
+
+void rowsToBins(const std::vector &rows, user_tally::bin_var_t &bins)
+{
+ bins = user_tally::bin_var_t();
+
+ for (const auto &r : rows) {
+ std::vector edges = BinVariableModel::parseEdges(r.edges);
+ if (edges.size() < 2)
+ continue;
+
+ if (r.variable == "x")
+ bins.x = edges;
+ else if (r.variable == "y")
+ bins.y = edges;
+ else if (r.variable == "z")
+ bins.z = edges;
+ else if (r.variable == "r")
+ bins.r = edges;
+ else if (r.variable == "rho")
+ bins.rho = edges;
+ else if (r.variable == "cosTheta")
+ bins.cosTheta = edges;
+ else if (r.variable == "nx")
+ bins.nx = edges;
+ else if (r.variable == "ny")
+ bins.ny = edges;
+ else if (r.variable == "nz")
+ bins.nz = edges;
+ else if (r.variable == "E")
+ bins.E = edges;
+ else if (r.variable == "Tdam")
+ bins.Tdam = edges;
+ else if (r.variable == "V")
+ bins.V = edges;
+ else if (r.variable == "atom_id")
+ bins.atom_id = edges;
+ else if (r.variable == "recoil_id")
+ bins.recoil_id = edges;
+ }
+}
+
+class VariableComboDelegate : public QStyledItemDelegate
+{
+public:
+ QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &) const override
+ {
+ QComboBox *cb = new QComboBox(parent);
+ const QStringList vars = BinVariableModel::variableNames();
+ for (const QString &v : vars)
+ cb->addItem(v + " - " + BinVariableModel::variableDescription(v), v);
+ return cb;
+ }
+
+ void setEditorData(QWidget *editor, const QModelIndex &index) const override
+ {
+ QComboBox *cb = static_cast(editor);
+ const QString var = index.model()->data(index, Qt::EditRole).toString();
+ int i = cb->findData(var);
+ if (i >= 0)
+ cb->setCurrentIndex(i);
+ }
+
+ void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override
+ {
+ QComboBox *cb = static_cast(editor);
+ model->setData(index, cb->currentData(), Qt::EditRole);
+ }
+};
+
+} // namespace
+
+BinVariableModel::BinVariableModel(QObject *parent) : QAbstractTableModel(parent) { }
+
+int BinVariableModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid())
+ return 0;
+ return static_cast(rows_.size());
+}
+
+int BinVariableModel::columnCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent)
+ return 5;
+}
+
+QVariant BinVariableModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() < 0 || index.row() >= rowCount())
+ return QVariant();
+
+ const Row &r = rows_[index.row()];
+
+ if (role == Qt::BackgroundRole && index.column() == 1) {
+ const std::vector edges = parseEdges(r.edges);
+ if (!isStrictlyMonotonic(edges))
+ return QColor(255, 220, 220);
+ }
+
+ if (role != Qt::DisplayRole && role != Qt::EditRole)
+ return QVariant();
+
+ switch (index.column()) {
+ case 0:
+ return r.variable;
+ case 1:
+ return r.edges;
+ case 2: {
+ const std::vector edges = parseEdges(r.edges);
+ if (edges.size() < 2)
+ return 0;
+ return static_cast(edges.size() - 1);
+ }
+ case 3:
+ return "~";
+ case 4:
+ return "x";
+ default:
+ break;
+ }
+
+ return QVariant();
+}
+
+QVariant BinVariableModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (role != Qt::DisplayRole || orientation != Qt::Horizontal)
+ return QVariant();
+
+ switch (section) {
+ case 0:
+ return "Variable";
+ case 1:
+ return "Bin edges";
+ case 2:
+ return "N bins";
+ case 3:
+ return "[~]";
+ case 4:
+ return "[x]";
+ default:
+ return QVariant();
+ }
+}
+
+Qt::ItemFlags BinVariableModel::flags(const QModelIndex &index) const
+{
+ if (!index.isValid())
+ return Qt::NoItemFlags;
+
+ Qt::ItemFlags f = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+ if (index.column() == 0 || index.column() == 1)
+ f |= Qt::ItemIsEditable;
+ return f;
+}
+
+bool BinVariableModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ if (!index.isValid() || role != Qt::EditRole)
+ return false;
+
+ Row &r = rows_[index.row()];
+ if (index.column() == 0)
+ r.variable = value.toString();
+ else if (index.column() == 1)
+ r.edges = value.toString();
+ else
+ return false;
+
+ emit dataChanged(index, index);
+ emit dataChanged(this->index(index.row(), 2), this->index(index.row(), 2));
+ emit rowsChanged();
+ return true;
+}
+
+bool BinVariableModel::insertRows(int row, int count, const QModelIndex &parent)
+{
+ if (count <= 0)
+ return false;
+
+ beginInsertRows(parent, row, row + count - 1);
+ for (int i = 0; i < count; ++i)
+ rows_.insert(rows_.begin() + row, { "x", "0 1" });
+ endInsertRows();
+ emit rowsChanged();
+ return true;
+}
+
+bool BinVariableModel::removeRows(int row, int count, const QModelIndex &parent)
+{
+ if (count <= 0 || row < 0 || row + count > rowCount())
+ return false;
+
+ beginRemoveRows(parent, row, row + count - 1);
+ rows_.erase(rows_.begin() + row, rows_.begin() + row + count);
+ endRemoveRows();
+ emit rowsChanged();
+ return true;
+}
+
+void BinVariableModel::setRows(const std::vector &rows)
+{
+ beginResetModel();
+ rows_ = rows;
+ endResetModel();
+ emit rowsChanged();
+}
+
+std::vector BinVariableModel::rows() const
+{
+ return rows_;
+}
+
+void BinVariableModel::clear()
+{
+ beginResetModel();
+ rows_.clear();
+ endResetModel();
+ emit rowsChanged();
+}
+
+QStringList BinVariableModel::variableNames()
+{
+ QStringList vars;
+ for (const auto &v : kVars)
+ vars << v.name;
+ return vars;
+}
+
+QString BinVariableModel::variableDescription(const QString &name)
+{
+ for (const auto &v : kVars) {
+ if (name == v.name)
+ return v.description;
+ }
+ return QString();
+}
+
+std::vector BinVariableModel::parseEdges(const QString &text)
+{
+ std::vector out;
+ const QStringList parts = text.split(QRegularExpression("[\\s,;]+"), Qt::SkipEmptyParts);
+ for (const QString &p : parts) {
+ bool ok = false;
+ float v = p.toFloat(&ok);
+ if (ok)
+ out.push_back(v);
+ }
+ return out;
+}
+
+QString BinVariableModel::edgesToString(const std::vector &edges)
+{
+ QStringList s;
+ for (float v : edges)
+ s << QString::number(v, 'g', 8);
+ return s.join(' ');
+}
+
+bool BinVariableModel::isStrictlyMonotonic(const std::vector &edges)
+{
+ if (edges.size() < 2)
+ return false;
+ for (size_t i = 1; i < edges.size(); ++i) {
+ if (!(edges[i] > edges[i - 1]))
+ return false;
+ }
+ return true;
+}
+
+LinspaceDialog::LinspaceDialog(QWidget *parent) : QDialog(parent)
+{
+ setWindowTitle("Generate bin edges");
+ QVBoxLayout *v = new QVBoxLayout(this);
+
+ QFormLayout *f = new QFormLayout;
+ min_ = new QDoubleSpinBox;
+ max_ = new QDoubleSpinBox;
+ bins_ = new QSpinBox;
+
+ min_->setRange(-1e9, 1e9);
+ max_->setRange(-1e9, 1e9);
+ min_->setDecimals(6);
+ max_->setDecimals(6);
+ bins_->setRange(1, 1000000);
+
+ min_->setValue(0.0);
+ max_->setValue(1.0);
+ bins_->setValue(10);
+
+ f->addRow("Min value:", min_);
+ f->addRow("Max value:", max_);
+ f->addRow("N bins:", bins_);
+ v->addLayout(f);
+
+ preview_ = new QLabel;
+ v->addWidget(preview_);
+
+ QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+ connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ v->addWidget(box);
+
+ connect(min_, QOverload::of(&QDoubleSpinBox::valueChanged), this,
+ &LinspaceDialog::updatePreview);
+ connect(max_, QOverload::of(&QDoubleSpinBox::valueChanged), this,
+ &LinspaceDialog::updatePreview);
+ connect(bins_, QOverload::of(&QSpinBox::valueChanged), this,
+ &LinspaceDialog::updatePreview);
+ updatePreview();
+}
+
+std::vector LinspaceDialog::edges() const
+{
+ const int n = bins_->value();
+ const double a = min_->value();
+ const double b = max_->value();
+ std::vector e;
+ e.reserve(static_cast(n) + 1);
+
+ if (n <= 0)
+ return e;
+
+ const double step = (b - a) / static_cast(n);
+ for (int i = 0; i <= n; ++i)
+ e.push_back(static_cast(a + step * i));
+
+ return e;
+}
+
+void LinspaceDialog::updatePreview()
+{
+ const std::vector e = edges();
+ if (e.size() < 2) {
+ preview_->setText("No edges generated");
+ return;
+ }
+
+ QString txt = QString("%1 edges, %2 bins\n").arg(e.size()).arg(e.size() - 1);
+ txt += QString("%1, %2, ... , %3, %4")
+ .arg(e.front(), 0, 'g', 6)
+ .arg(e[1], 0, 'g', 6)
+ .arg(e[e.size() - 2], 0, 'g', 6)
+ .arg(e.back(), 0, 'g', 6);
+ preview_->setText(txt);
+}
+
+TallyTemplateDialog::TallyTemplateDialog(QWidget *parent) : QDialog(parent)
+{
+ setWindowTitle("Tally Templates");
+ resize(760, 460);
+
+ QVBoxLayout *root = new QVBoxLayout(this);
+ QHBoxLayout *body = new QHBoxLayout;
+
+ list_ = new QListWidget;
+ preview_ = new QTextBrowser;
+ preview_->setReadOnly(true);
+
+ body->addWidget(list_, 1);
+ body->addWidget(preview_, 2);
+ root->addLayout(body, 1);
+
+ QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Cancel);
+ accept_ = box->addButton("Load into current tally", QDialogButtonBox::AcceptRole);
+ accept_->setEnabled(false);
+ connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ root->addWidget(box);
+
+ connect(list_, &QListWidget::currentRowChanged, this, &TallyTemplateDialog::onCurrentTemplateChanged);
+
+ const bool ok = loadTemplates();
+ if (ok && list_->count() > 0)
+ list_->setCurrentRow(0);
+ else {
+ preview_->setPlainText("No templates available. Check resource file tally_templates.json.");
+ accept_->setEnabled(false);
+ }
+}
+
+bool TallyTemplateDialog::loadTemplates()
+{
+ entries_.clear();
+
+ static bool cacheInitialized = false;
+ static ojson cache;
+
+ if (!cacheInitialized) {
+ QFile f(kTemplatePath);
+ if (!f.open(QIODevice::ReadOnly | QIODevice::Text))
+ return false;
+
+ try {
+ cache = ojson::parse(f.readAll().toStdString(), nullptr, true, true);
+ cacheInitialized = true;
+ } catch (...) {
+ return false;
+ }
+ }
+
+ const ojson &j = cache;
+
+ if (!j.is_array())
+ return false;
+
+ for (const auto &node : j) {
+ if (!node.is_object())
+ continue;
+
+ TemplateEntry e;
+ e.parameters.id = node.value("id", std::string("Template"));
+ e.parameters.description = node.value("description", std::string());
+ e.parameters.event = parseEvent(QString::fromStdString(node.value("event", std::string("IonStop"))));
+
+ if (node.contains("bins") && node["bins"].is_object()) {
+ for (auto it = node["bins"].begin(); it != node["bins"].end(); ++it) {
+ std::vector edges;
+ if (it.value().is_array())
+ edges = it.value().get>();
+
+ const std::string k = it.key();
+ if (k == "x")
+ e.parameters.bins.x = edges;
+ else if (k == "y")
+ e.parameters.bins.y = edges;
+ else if (k == "z")
+ e.parameters.bins.z = edges;
+ else if (k == "r")
+ e.parameters.bins.r = edges;
+ else if (k == "rho")
+ e.parameters.bins.rho = edges;
+ else if (k == "cosTheta")
+ e.parameters.bins.cosTheta = edges;
+ else if (k == "nx")
+ e.parameters.bins.nx = edges;
+ else if (k == "ny")
+ e.parameters.bins.ny = edges;
+ else if (k == "nz")
+ e.parameters.bins.nz = edges;
+ else if (k == "E")
+ e.parameters.bins.E = edges;
+ else if (k == "Tdam")
+ e.parameters.bins.Tdam = edges;
+ else if (k == "V")
+ e.parameters.bins.V = edges;
+ else if (k == "atom_id")
+ e.parameters.bins.atom_id = edges;
+ else if (k == "recoil_id")
+ e.parameters.bins.recoil_id = edges;
+ }
+ }
+
+ entries_.push_back(e);
+ list_->addItem(QString::fromStdString(e.parameters.id));
+ }
+
+ return !entries_.empty();
+}
+
+user_tally::parameters TallyTemplateDialog::selectedTemplate() const
+{
+ int i = list_->currentRow();
+ if (i < 0 || i >= static_cast(entries_.size()))
+ return user_tally::parameters();
+ return entries_[i].parameters;
+}
+
+void TallyTemplateDialog::refreshPreview(int row)
+{
+ if (row < 0 || row >= static_cast(entries_.size())) {
+ preview_->clear();
+ accept_->setEnabled(false);
+ return;
+ }
+
+ const auto &p = entries_[row].parameters;
+ std::vector rows = binsToRows(p.bins);
+
+ QString html;
+ html += QString("Event: %1
").arg(eventToString(p.event));
+ html += QString("Description: %1
")
+ .arg(QString::fromStdString(p.description).toHtmlEscaped());
+ html += "Bins:
";
+ for (const auto &r : rows) {
+ const std::vector edges = BinVariableModel::parseEdges(r.edges);
+ if (edges.size() >= 2) {
+ html += QString("%1 : %2 → %3 (%4 bins)
")
+ .arg(r.variable.toHtmlEscaped())
+ .arg(edges.front(), 0, 'g', 4)
+ .arg(edges.back(), 0, 'g', 4)
+ .arg(edges.size() - 1);
+ } else {
+ html += QString("%1 : %2
")
+ .arg(r.variable.toHtmlEscaped())
+ .arg(r.edges.toHtmlEscaped());
+ }
+ }
+
+ preview_->setHtml(html);
+ accept_->setEnabled(true);
+}
+
+void TallyTemplateDialog::onCurrentTemplateChanged(int row)
+{
+ refreshPreview(row);
+}
+
+UserTallyView::UserTallyView(OptionsModel *m, QWidget *parent)
+ : QWidget(parent),
+ model_(m),
+ binModel_(new BinVariableModel(this))
+{
+ tallyIndex_ = model_->index("UserTally");
+
+ QVBoxLayout *root = new QVBoxLayout(this);
+
+ QHBoxLayout *top = new QHBoxLayout;
+ top->addWidget(new QLabel("Tally:"));
+ cbTallyID_ = new QComboBox;
+ cbTallyID_->setMinimumContentsLength(16);
+ top->addWidget(cbTallyID_, 1);
+
+ btDbTally_ = new QToolButton;
+ btDbTally_->setIcon(QIcon(":/assets/lucide/database-search.svg"));
+ btDbTally_->setToolTip("Load tally template");
+ top->addWidget(btDbTally_);
+
+ btAddTally_ = new QToolButton;
+ btAddTally_->setIcon(QIcon(":/assets/ionicons/add-outline.svg"));
+ btAddTally_->setToolTip("Add new tally");
+ top->addWidget(btAddTally_);
+
+ btDupTally_ = new QToolButton;
+ btDupTally_->setIcon(QIcon(":/assets/ionicons/copy-outline.svg"));
+ btDupTally_->setToolTip("Duplicate tally");
+ top->addWidget(btDupTally_);
+
+ btDelTally_ = new QToolButton;
+ btDelTally_->setIcon(QIcon(":/assets/ionicons/remove-outline.svg"));
+ btDelTally_->setToolTip("Remove tally");
+ top->addWidget(btDelTally_);
+
+ btEdtTally_ = new QToolButton;
+ btEdtTally_->setIcon(QIcon(":/assets/ionicons/create-outline.svg"));
+ btEdtTally_->setToolTip("Rename tally");
+ top->addWidget(btEdtTally_);
+
+ root->addLayout(top);
+
+ summaryLabel_ = new QLabel("No bins defined - tally is inactive");
+ memoryLabel_ = new QLabel("Memory: 0 B per tally flush");
+ warningLabel_ = new QLabel;
+ warningLabel_->setStyleSheet("color: #b22222;");
+ root->addWidget(summaryLabel_);
+ root->addWidget(memoryLabel_);
+ root->addWidget(warningLabel_);
+
+ QFormLayout *form = new QFormLayout;
+ leDescription_ = new QLineEdit;
+ cbEvent_ = new QComboBox;
+ cbEvent_->addItems({ "IonStop", "IonExit", "Vacancy", "Replacement", "CascadeComplete", "BoundaryCrossing" });
+ form->addRow("Description:", leDescription_);
+ form->addRow("Event:", cbEvent_);
+ root->addLayout(form);
+
+ QGroupBox *csBox = new QGroupBox("Coordinate system");
+ QVBoxLayout *csV = new QVBoxLayout(csBox);
+ cbUseLabFrame_ = new QCheckBox("Use lab frame");
+ csV->addWidget(cbUseLabFrame_);
+
+ coordWidget_ = new QWidget;
+ QGridLayout *cg = new QGridLayout(coordWidget_);
+
+ for (int i = 0; i < 3; ++i) {
+ sbOrigin_[i] = new QDoubleSpinBox;
+ sbZaxis_[i] = new QDoubleSpinBox;
+ sbXZvec_[i] = new QDoubleSpinBox;
+
+ sbOrigin_[i]->setRange(-1e6, 1e6);
+ sbZaxis_[i]->setRange(-1e3, 1e3);
+ sbXZvec_[i]->setRange(-1e3, 1e3);
+
+ sbOrigin_[i]->setDecimals(6);
+ sbZaxis_[i]->setDecimals(6);
+ sbXZvec_[i]->setDecimals(6);
+ }
+
+ cg->addWidget(new QLabel("Origin:"), 0, 0);
+ cg->addWidget(sbOrigin_[0], 0, 1);
+ cg->addWidget(sbOrigin_[1], 0, 2);
+ cg->addWidget(sbOrigin_[2], 0, 3);
+
+ cg->addWidget(new QLabel("Z-axis:"), 1, 0);
+ cg->addWidget(sbZaxis_[0], 1, 1);
+ cg->addWidget(sbZaxis_[1], 1, 2);
+ cg->addWidget(sbZaxis_[2], 1, 3);
+
+ cg->addWidget(new QLabel("XZ-plane vec:"), 2, 0);
+ cg->addWidget(sbXZvec_[0], 2, 1);
+ cg->addWidget(sbXZvec_[1], 2, 2);
+ cg->addWidget(sbXZvec_[2], 2, 3);
+
+ csV->addWidget(coordWidget_);
+ root->addWidget(csBox);
+
+ QHBoxLayout *binsHdr = new QHBoxLayout;
+ binsHdr->addWidget(new QLabel("Bin variables"));
+ binsHdr->addStretch();
+ btAddBin_ = new QToolButton;
+ btAddBin_->setIcon(QIcon(":/assets/ionicons/add-outline.svg"));
+ btAddBin_->setToolTip("Add variable");
+ binsHdr->addWidget(btAddBin_);
+ root->addLayout(binsHdr);
+
+ binTableView_ = new QTableView;
+ binTableView_->setModel(binModel_);
+ binTableView_->setItemDelegateForColumn(0, new VariableComboDelegate);
+ binTableView_->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+ binTableView_->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
+ binTableView_->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
+ binTableView_->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
+ binTableView_->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents);
+ root->addWidget(binTableView_, 1);
+
+ connect(btAddTally_, &QToolButton::clicked, this, &UserTallyView::addTally);
+ connect(btDupTally_, &QToolButton::clicked, this, &UserTallyView::duplicateTally);
+ connect(btDelTally_, &QToolButton::clicked, this, &UserTallyView::removeTally);
+ connect(btEdtTally_, &QToolButton::clicked, this, &UserTallyView::renameTally);
+ connect(btDbTally_, &QToolButton::clicked, this, &UserTallyView::loadFromTemplate);
+ connect(btAddBin_, &QToolButton::clicked, this, &UserTallyView::addBinVariable);
+
+ connect(cbTallyID_, QOverload::of(&QComboBox::currentIndexChanged), this,
+ &UserTallyView::updateSelectedTally);
+ connect(leDescription_, &QLineEdit::editingFinished, this, &UserTallyView::setValueData);
+ connect(cbEvent_, QOverload::of(&QComboBox::currentIndexChanged), this,
+ &UserTallyView::setValueData);
+ connect(cbUseLabFrame_, &QCheckBox::stateChanged, this, &UserTallyView::onUseLabFrameChanged);
+ connect(binModel_, &BinVariableModel::rowsChanged, this, &UserTallyView::setValueData);
+ connect(binModel_, &BinVariableModel::rowsChanged, this, &UserTallyView::updateSummaryLabel);
+ connect(binTableView_, &QTableView::clicked, this, &UserTallyView::onBinTableClicked);
+
+ for (int i = 0; i < 3; ++i) {
+ connect(sbOrigin_[i], QOverload::of(&QDoubleSpinBox::valueChanged), this,
+ &UserTallyView::setValueData);
+ connect(sbZaxis_[i], QOverload::of(&QDoubleSpinBox::valueChanged), this,
+ &UserTallyView::setValueData);
+ connect(sbXZvec_[i], QOverload::of(&QDoubleSpinBox::valueChanged), this,
+ &UserTallyView::setValueData);
+ }
+
+ setWidgetData();
+}
+
+void UserTallyView::setWidgetData()
+{
+ cbTallyID_->blockSignals(true);
+ cbTallyID_->clear();
+
+ auto &ut = model_->options()->UserTally;
+ for (const auto &t : ut)
+ cbTallyID_->addItem(QString::fromStdString(t.id));
+
+ cbTallyID_->blockSignals(false);
+
+ if (!ut.empty()) {
+ cbTallyID_->setCurrentIndex(0);
+ updateSelectedTally();
+ } else {
+ leDescription_->clear();
+ cbEvent_->setCurrentIndex(0);
+ cbUseLabFrame_->setChecked(true);
+ onUseLabFrameChanged();
+ binModel_->clear();
+ warningLabel_->clear();
+ updateSummaryLabel();
+ setTallyEditorEnabled(false);
+ }
+
+ const bool has = !ut.empty();
+ cbTallyID_->setEnabled(has);
+ btDupTally_->setEnabled(has);
+ btDelTally_->setEnabled(has);
+ btEdtTally_->setEnabled(has);
+}
+
+void UserTallyView::setTallyEditorEnabled(bool enabled)
+{
+ leDescription_->setEnabled(enabled);
+ cbEvent_->setEnabled(enabled);
+ cbUseLabFrame_->setEnabled(enabled);
+ coordWidget_->setEnabled(enabled && !cbUseLabFrame_->isChecked());
+ btAddBin_->setEnabled(enabled);
+ binTableView_->setEnabled(enabled);
+}
+
+void UserTallyView::setValueData()
+{
+ if (syncingUi_)
+ return;
+
+ user_tally::parameters *t = currentTally();
+ if (!t)
+ return;
+
+ t->description = leDescription_->text().toStdString();
+ t->event = currentEvent();
+
+ if (cbUseLabFrame_->isChecked()) {
+ t->coordinate_system.reset();
+ } else {
+ t->coordinate_system.origin = spin3Value(sbOrigin_);
+ t->coordinate_system.zaxis = spin3Value(sbZaxis_);
+ t->coordinate_system.xzvector = spin3Value(sbXZvec_);
+ }
+
+ rowsToBins(binModel_->rows(), t->bins);
+
+ model_->notifyDataChanged(tallyIndex_);
+ updateSummaryLabel();
+}
+
+void UserTallyView::addTally()
+{
+ user_tally::parameters p;
+ p.id = QString("Tally%1").arg(model_->options()->UserTally.size() + 1).toStdString();
+ p.description = "User tally";
+ p.event = Event::IonStop;
+
+ model_->options()->UserTally.push_back(p);
+ model_->notifyDataChanged(tallyIndex_);
+
+ setWidgetData();
+ cbTallyID_->setCurrentIndex(cbTallyID_->count() - 1);
+}
+
+void UserTallyView::removeTally()
+{
+ int i = cbTallyID_->currentIndex();
+ auto &ut = model_->options()->UserTally;
+ if (i < 0 || i >= static_cast(ut.size()))
+ return;
+
+ QMessageBox::StandardButton ret = QMessageBox::warning(
+ this, "Remove tally",
+ QString("%1 is being removed.\nClick OK to proceed.").arg(cbTallyID_->currentText()),
+ QMessageBox::Ok | QMessageBox::Cancel);
+ if (ret != QMessageBox::Ok)
+ return;
+
+ ut.erase(ut.begin() + i);
+ model_->notifyDataChanged(tallyIndex_);
+ setWidgetData();
+}
+
+void UserTallyView::duplicateTally()
+{
+ int i = cbTallyID_->currentIndex();
+ auto &ut = model_->options()->UserTally;
+ if (i < 0 || i >= static_cast(ut.size()))
+ return;
+
+ user_tally::parameters copy = ut[i];
+ QString baseId = QString::fromStdString(copy.id);
+ if (baseId.isEmpty())
+ baseId = "Tally";
+
+ QString newId = baseId + "_copy";
+ int suffix = 2;
+ auto exists = [&ut](const QString &id) {
+ for (const auto &t : ut) {
+ if (QString::fromStdString(t.id) == id)
+ return true;
+ }
+ return false;
+ };
+ while (exists(newId)) {
+ newId = QString("%1_copy%2").arg(baseId).arg(suffix++);
+ }
+
+ copy.id = newId.toStdString();
+ ut.push_back(copy);
+ model_->notifyDataChanged(tallyIndex_);
+
+ setWidgetData();
+ cbTallyID_->setCurrentText(newId);
+}
+
+void UserTallyView::renameTally()
+{
+ int i = cbTallyID_->currentIndex();
+ auto &ut = model_->options()->UserTally;
+ if (i < 0 || i >= static_cast(ut.size()))
+ return;
+
+ bool ok = false;
+ QString s = QInputDialog::getText(this, "Rename tally", "New tally id", QLineEdit::Normal,
+ cbTallyID_->currentText(), &ok);
+ if (!ok || s.trimmed().isEmpty())
+ return;
+
+ ut[i].id = s.toStdString();
+ model_->notifyDataChanged(tallyIndex_);
+
+ setWidgetData();
+ cbTallyID_->setCurrentText(s);
+}
+
+void UserTallyView::addBinVariable()
+{
+ binModel_->insertRows(binModel_->rowCount(), 1);
+}
+
+void UserTallyView::loadFromTemplate()
+{
+ user_tally::parameters *t = currentTally();
+ if (!t) {
+ addTally();
+ t = currentTally();
+ if (!t)
+ return;
+ }
+
+ TallyTemplateDialog dlg(this);
+
+ if (dlg.exec() != QDialog::Accepted)
+ return;
+
+ user_tally::parameters p = dlg.selectedTemplate();
+ t->description = p.description;
+ t->event = p.event;
+ t->bins = p.bins;
+
+ updateSelectedTally();
+ model_->notifyDataChanged(tallyIndex_);
+}
+
+void UserTallyView::updateSelectedTally()
+{
+ const user_tally::parameters *t = currentTally();
+ if (!t) {
+ setTallyEditorEnabled(false);
+ return;
+ }
+
+ syncingUi_ = true;
+
+ leDescription_->blockSignals(true);
+ cbEvent_->blockSignals(true);
+
+ leDescription_->setText(QString::fromStdString(t->description));
+ setCurrentEvent(t->event);
+
+ setSpin3(sbOrigin_, t->coordinate_system.origin);
+ setSpin3(sbZaxis_, t->coordinate_system.zaxis);
+ setSpin3(sbXZvec_, t->coordinate_system.xzvector);
+
+ cbUseLabFrame_->setChecked(isLabFrame(t->coordinate_system));
+ onUseLabFrameChanged();
+
+ binModel_->setRows(binsToRows(t->bins));
+
+ leDescription_->blockSignals(false);
+ cbEvent_->blockSignals(false);
+ syncingUi_ = false;
+
+ setTallyEditorEnabled(true);
+
+ btDelTally_->setEnabled(true);
+ btEdtTally_->setEnabled(true);
+ btDupTally_->setEnabled(true);
+
+ updateSummaryLabel();
+}
+
+void UserTallyView::updateSummaryLabel()
+{
+ const std::vector rows = binModel_->rows();
+
+ QStringList names;
+ QStringList dims;
+ long long totalBins = 1;
+ bool hasBins = false;
+ bool hasInvalid = false;
+
+ for (const auto &r : rows) {
+ const std::vector edges = BinVariableModel::parseEdges(r.edges);
+ if (edges.size() < 2) {
+ hasInvalid = true;
+ continue;
+ }
+ if (!BinVariableModel::isStrictlyMonotonic(edges)) {
+ hasInvalid = true;
+ continue;
+ }
+
+ const int n = static_cast(edges.size() - 1);
+ names << r.variable;
+ dims << QString::number(n);
+ totalBins *= n;
+ hasBins = true;
+ }
+
+ if (!hasBins) {
+ summaryLabel_->setText("No bins defined - tally is inactive");
+ summaryLabel_->setStyleSheet("");
+ memoryLabel_->setText("Memory: 0 B per tally flush");
+ } else if (names.size() == 1) {
+ summaryLabel_->setText(
+ QString("1D tally: %1 (%2 bins)").arg(names.join(" ")).arg(totalBins));
+ } else {
+ summaryLabel_->setText(QString("%1D tally: %2 (%3 = %4 bins)")
+ .arg(names.size())
+ .arg(names.join(" x "))
+ .arg(dims.join(" x "))
+ .arg(totalBins));
+ }
+
+ if (hasBins) {
+ if (totalBins >= 200000)
+ summaryLabel_->setStyleSheet("color: #b22222; font-weight: bold;");
+ else if (totalBins >= 10000)
+ summaryLabel_->setStyleSheet("color: #e07000; font-weight: bold;");
+ else
+ summaryLabel_->setStyleSheet("");
+ }
+
+ const double bytes = static_cast(totalBins) * sizeof(double);
+ QString memStr;
+ if (bytes < 1024.0)
+ memStr = QString("%1 B").arg(bytes, 0, 'f', 0);
+ else if (bytes < 1024.0 * 1024.0)
+ memStr = QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 1);
+ else
+ memStr = QString("%1 MB").arg(bytes / (1024.0 * 1024.0), 0, 'f', 2);
+ memoryLabel_->setText(QString("Memory: ~%1 per tally flush").arg(memStr));
+
+ QStringList warnings;
+ if (hasInvalid)
+ warnings << "Bin edges must be strictly monotonic for all rows.";
+ if (totalBins > 500000)
+ warnings << "Large tally warning: total bins exceed 500000.";
+ warningLabel_->setText(warnings.join(" "));
+}
+
+void UserTallyView::applyLinspaceToCurrentRow()
+{
+ const QModelIndex idx = binTableView_->currentIndex();
+ if (!idx.isValid())
+ return;
+
+ LinspaceDialog dlg(this);
+ if (dlg.exec() != QDialog::Accepted)
+ return;
+
+ const std::vector edges = dlg.edges();
+ if (edges.size() < 2)
+ return;
+
+ binModel_->setData(binModel_->index(idx.row(), 1), BinVariableModel::edgesToString(edges));
+}
+
+void UserTallyView::onBinTableClicked(const QModelIndex &index)
+{
+ if (!index.isValid())
+ return;
+
+ if (index.column() == 3) {
+ binTableView_->setCurrentIndex(index);
+ applyLinspaceToCurrentRow();
+ } else if (index.column() == 4) {
+ binModel_->removeRows(index.row(), 1);
+ }
+}
+
+void UserTallyView::onUseLabFrameChanged(int)
+{
+ const bool useLab = cbUseLabFrame_->isChecked();
+ coordWidget_->setVisible(!useLab);
+ setValueData();
+}
+
+Event UserTallyView::currentEvent() const
+{
+ return parseEvent(cbEvent_->currentText());
+}
+
+void UserTallyView::setCurrentEvent(Event ev)
+{
+ int i = cbEvent_->findText(eventToString(ev));
+ if (i < 0)
+ i = 0;
+ cbEvent_->setCurrentIndex(i);
+}
+
+user_tally::parameters *UserTallyView::currentTally()
+{
+ int i = cbTallyID_->currentIndex();
+ auto &ut = model_->options()->UserTally;
+ if (i < 0 || i >= static_cast(ut.size()))
+ return nullptr;
+ return &ut[i];
+}
+
+const user_tally::parameters *UserTallyView::currentTally() const
+{
+ int i = cbTallyID_->currentIndex();
+ const auto &ut = model_->options()->UserTally;
+ if (i < 0 || i >= static_cast(ut.size()))
+ return nullptr;
+ return &ut[i];
+}
diff --git a/source/gui/usertallyview.h b/source/gui/usertallyview.h
new file mode 100644
index 0000000..6a7e297
--- /dev/null
+++ b/source/gui/usertallyview.h
@@ -0,0 +1,169 @@
+#ifndef USERTALLYVIEW_H
+#define USERTALLYVIEW_H
+
+#include
+#include
+#include
+#include
+
+#include "mcdriver.h"
+
+class QCheckBox;
+class QComboBox;
+class QDoubleSpinBox;
+class QLabel;
+class QLineEdit;
+class QPushButton;
+class QSpinBox;
+class QTableView;
+class QToolButton;
+class QModelIndex;
+class QStringList;
+class QListWidget;
+class QTextBrowser;
+
+class OptionsModel;
+
+class BinVariableModel : public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ struct Row {
+ QString variable;
+ QString edges;
+ };
+
+ explicit BinVariableModel(QObject *parent = nullptr);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const override;
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
+ bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
+ bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
+
+ void setRows(const std::vector &rows);
+ std::vector rows() const;
+ void clear();
+
+ static QStringList variableNames();
+ static QString variableDescription(const QString &name);
+ static std::vector parseEdges(const QString &text);
+ static QString edgesToString(const std::vector &edges);
+ static bool isStrictlyMonotonic(const std::vector &edges);
+
+signals:
+ void rowsChanged();
+
+private:
+ std::vector rows_;
+};
+
+class LinspaceDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit LinspaceDialog(QWidget *parent = nullptr);
+ std::vector edges() const;
+
+private slots:
+ void updatePreview();
+
+private:
+ QDoubleSpinBox *min_;
+ QDoubleSpinBox *max_;
+ QSpinBox *bins_;
+ QLabel *preview_;
+};
+
+class TallyTemplateDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit TallyTemplateDialog(QWidget *parent = nullptr);
+ user_tally::parameters selectedTemplate() const;
+
+private slots:
+ void onCurrentTemplateChanged(int row);
+
+private:
+ struct TemplateEntry {
+ user_tally::parameters parameters;
+ };
+
+ bool loadTemplates();
+ void refreshPreview(int row);
+
+ QListWidget *list_;
+ QTextBrowser *preview_;
+ QPushButton *accept_;
+ std::vector entries_;
+};
+
+class UserTallyView : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit UserTallyView(OptionsModel *m, QWidget *parent = nullptr);
+
+public slots:
+ void setWidgetData();
+ void setValueData();
+ void addTally();
+ void removeTally();
+ void renameTally();
+ void duplicateTally();
+ void addBinVariable();
+ void loadFromTemplate();
+ void updateSelectedTally();
+ void updateSummaryLabel();
+
+private slots:
+ void applyLinspaceToCurrentRow();
+ void onBinTableClicked(const QModelIndex &index);
+ void onUseLabFrameChanged(int state = 0);
+
+private:
+ Event currentEvent() const;
+ void setCurrentEvent(Event ev);
+ void setTallyEditorEnabled(bool enabled);
+ user_tally::parameters *currentTally();
+ const user_tally::parameters *currentTally() const;
+
+ QComboBox *cbTallyID_;
+ QToolButton *btDbTally_;
+ QToolButton *btAddTally_;
+ QToolButton *btDupTally_;
+ QToolButton *btDelTally_;
+ QToolButton *btEdtTally_;
+ QToolButton *btAddBin_;
+
+ QLabel *summaryLabel_;
+ QLabel *memoryLabel_;
+ QLabel *warningLabel_;
+
+ QLineEdit *leDescription_;
+ QComboBox *cbEvent_;
+
+ QCheckBox *cbUseLabFrame_;
+ QWidget *coordWidget_;
+ QDoubleSpinBox *sbOrigin_[3];
+ QDoubleSpinBox *sbZaxis_[3];
+ QDoubleSpinBox *sbXZvec_[3];
+
+ BinVariableModel *binModel_;
+ QTableView *binTableView_;
+ bool syncingUi_ = false;
+
+ OptionsModel *model_;
+ QPersistentModelIndex tallyIndex_;
+};
+
+#endif // USERTALLYVIEW_H
diff --git a/source/lib/parse_json.cpp b/source/lib/parse_json.cpp
index 853ecde..14c1aa8 100644
--- a/source/lib/parse_json.cpp
+++ b/source/lib/parse_json.cpp
@@ -428,7 +428,12 @@ void mcconfig::set_impl_(const std::string &path, const std::string &json_str)
{
ojson j(*this);
ojson::json_pointer ptr(path.c_str());
- ojson v = ojson::parse(json_str);
+ std::string normalized = json_str;
+ if (normalized.empty()) {
+ // Defensive guard: empty input is invalid JSON. Preserve existing value instead.
+ normalized = j.at(ptr).dump();
+ }
+ ojson v = ojson::parse(normalized);
j.at(ptr) = v;
*this = j;
validate(true);