"""Modal editor to create and persist CrystFEL .cell files from an INI workspace."""
import os
from PyQt6.QtWidgets import (
QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout, QMessageBox, QFormLayout, QComboBox
)
from coseda.crystfel_cell import (
CRYSTFEL_LATTICE_TYPES,
crystfel_unique_axis_allowed,
normalize_crystfel_lattice_type,
)
[docs]
class CellfileWindow(QWidget):
"""Dialog for entering lattice parameters and writing a CrystFEL .cell file."""
def __init__(self, ini_path):
"""Load defaults from the provided INI and build the form UI."""
super().__init__()
self.ini_path = ini_path
self.project_dir = os.path.dirname(self.ini_path)
self.ini_config = {}
try:
with open(self.ini_path, "r") as f:
for line in f:
if line.startswith("[Paths]"):
break
for line in f:
if line.strip().startswith("["):
break
if "=" in line:
key, val = line.strip().split("=", 1)
self.ini_config[key.strip()] = val.strip()
if "outputfolder" in self.ini_config:
self.project_dir = os.path.join(self.project_dir, self.ini_config["outputfolder"])
except Exception:
pass
self.setWindowTitle("Generate .cell File")
self.fields = {
"lattice_type": QComboBox(),
"centering": QComboBox(),
"unique_axis": QComboBox(),
"a": QLineEdit(),
"b": QLineEdit(),
"c": QLineEdit(),
"al": QLineEdit(),
"be": QLineEdit(),
"ga": QLineEdit()
}
self.fields["lattice_type"].addItems(CRYSTFEL_LATTICE_TYPES)
self.fields["centering"].addItems(["P", "A", "B", "C", "I", "R", "F"])
self.fields["unique_axis"].addItems(["a", "b", "c"])
self.fields["unique_axis"].setCurrentIndex(-1)
def on_lattice_type_changed(text):
lattice_type = normalize_crystfel_lattice_type(text, self.fields["centering"].currentText())
enabled = crystfel_unique_axis_allowed(lattice_type)
self.fields["unique_axis"].setEnabled(enabled)
if enabled and lattice_type in ("tetragonal", "hexagonal") and self.fields["unique_axis"].currentIndex() == -1:
self.fields["unique_axis"].setCurrentText("c")
elif not enabled:
self.fields["unique_axis"].setCurrentIndex(-1)
self.fields["lattice_type"].currentTextChanged.connect(on_lattice_type_changed)
self.fields["centering"].currentTextChanged.connect(
lambda: on_lattice_type_changed(self.fields["lattice_type"].currentText())
)
on_lattice_type_changed(self.fields["lattice_type"].currentText())
form_layout = QFormLayout()
for label, widget in self.fields.items():
form_layout.addRow(label, widget)
save_button = QPushButton("Save .cell File")
save_button.clicked.connect(self.save_cell_file)
layout = QVBoxLayout()
layout.addLayout(form_layout)
layout.addWidget(save_button)
self.setLayout(layout)
[docs]
def save_cell_file(self):
"""Validate inputs, write the .cell file, and update the INI Paths.cellfile entry."""
params = {
key: field.currentText().strip() if isinstance(field, QComboBox) else field.text().strip()
for key, field in self.fields.items()
}
params["lattice_type"] = normalize_crystfel_lattice_type(params["lattice_type"], params["centering"])
if not crystfel_unique_axis_allowed(params["lattice_type"]):
params["unique_axis"] = ""
missing = [key for key, value in params.items() if not value]
if not crystfel_unique_axis_allowed(params["lattice_type"]):
missing = [key for key in missing if key != "unique_axis"]
if missing:
QMessageBox.warning(self, "Missing Fields", f"Please fill in all fields: {', '.join(missing)}")
return
content = [
"CrystFEL unit cell file version 1.0",
"",
f"lattice_type = {params['lattice_type']}",
f"centering = {params['centering']}",
"",
f"a = {params['a']} A",
f"b = {params['b']} A",
f"c = {params['c']} A",
f"al = {params['al']} deg",
f"be = {params['be']} deg",
f"ga = {params['ga']} deg"
]
if params["unique_axis"]:
content.insert(4, f"unique_axis = {params['unique_axis']}")
filepath = os.path.join(self.project_dir, "cellfile.cell")
try:
with open(filepath, "w") as f:
f.write("\n".join(content))
QMessageBox.information(self, "Saved", f"Cell file saved to:\n{filepath}")
# Update INI file with cellfile entry in [Paths]
try:
from configparser import ConfigParser
parser = ConfigParser()
parser.read(self.ini_path)
if not parser.has_section("Paths"):
parser.add_section("Paths")
parser.set("Paths", "cellfile", os.path.basename(filepath))
with open(self.ini_path, "w") as f:
parser.write(f)
except Exception as e:
QMessageBox.warning(self, "Warning", f"Could not update INI file:\n{str(e)}")
self.close()
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to save file:\n{str(e)}")