// ************************************************************************************************ // // BornAgain: simulate and fit reflection and scattering // //! @file GUI/View/Instrument/InstrumentLibraryEditor.cpp //! @brief Implements class InstrumentLibraryEditor. //! //! @homepage http://www.bornagainproject.org //! @license GNU General Public License v3 or higher (see COPYING) //! @copyright Forschungszentrum Jülich GmbH 2018 //! @authors Scientific Computing Group at MLZ (see CITATION, AUTHORS) // // ************************************************************************************************ #include "GUI/View/Instrument/InstrumentLibraryEditor.h" #include "Base/Util/Assert.h" #include "GUI/Model/Device/InstrumentItems.h" #include "GUI/Model/Device/InstrumentLibrary.h" #include "GUI/Support/Style/Style.h" #include "GUI/View/Instrument/DepthprobeInstrumentEditor.h" #include "GUI/View/Instrument/GISASInstrumentEditor.h" #include "GUI/View/Instrument/OffspecInstrumentEditor.h" #include "GUI/View/Instrument/SpecularInstrumentEditor.h" #include "GUI/View/Item/ItemViewOverlayButtons.h" #include "GUI/View/Tool/ItemDelegateForHTML.h" #include "GUI/View/Tool/mainwindow_constants.h" #include "GUI/View/Widget/ApplicationSettings.h" #include <QAction> #include <QFormLayout> #include <QInputDialog> #include <QPushButton> #include <QSettings> #include <QSplitter> #include <QTextEdit> #include <QVBoxLayout> InstrumentLibraryEditor::InstrumentLibraryEditor(QWidget* parent, InstrumentLibrary* instrumentLibrary) : QDialog(parent) , m_instrumentLibrary(instrumentLibrary) , m_treeModel(new TreeModel(this, instrumentLibrary->instrumentModel())) , m_chosenItem(nullptr) { // object name is needed to reload application settings if (this->objectName().isEmpty()) this->setObjectName("InstrumentLibraryEditor"); setGeometry(0, 0, 780, 429); auto* verticalLayout = new QVBoxLayout(this); setLayout(verticalLayout); auto* splitter = new QSplitter; verticalLayout->addWidget(splitter); splitter->setOrientation(Qt::Horizontal); m_treeView = new QTreeView; splitter->addWidget(m_treeView); m_scrollArea = new QScrollArea; splitter->addWidget(m_scrollArea); m_scrollArea->setWidgetResizable(true); auto* scrollAreaWidgetContents = new QWidget; scrollAreaWidgetContents->setGeometry(0, 0, 69, 380); m_scrollArea->setWidget(scrollAreaWidgetContents); m_buttonBox = new QDialogButtonBox; verticalLayout->addWidget(m_buttonBox); m_buttonBox->setOrientation(Qt::Horizontal); m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); setWindowIcon(QIcon(":/images/library.svg")); setWindowFlag(Qt::WindowContextHelpButtonHint, false); m_treeModel->enableEmptyHeadlines(false); m_treeView->setItemsExpandable(false); m_treeView->setRootIsDecorated(false); m_treeView->setHeaderHidden(true); m_treeView->setContextMenuPolicy(Qt::CustomContextMenu); m_treeView->setModel(m_treeModel); m_treeView->expandAll(); m_treeView->setVerticalScrollMode(QTreeView::ScrollPerPixel); m_treeView->setIndentation(0); m_treeView->setItemDelegate(new ItemDelegateForHTML(this)); m_treeView->setIconSize(QSize(128, 128)); connect(m_treeModel, &QAbstractItemModel::modelReset, [this] { m_treeView->expandAll(); }); connect(m_buttonBox, &QDialogButtonBox::accepted, this, &InstrumentLibraryEditor::accept); connect(m_buttonBox, &QDialogButtonBox::rejected, this, &InstrumentLibraryEditor::reject); // ensure a current item when widget is shown GUI::Style::setResizable(this); appSettings->loadWindowSizeAndPos(this); applySplitterPos(); // unfix m_treeView width only on manual resize connect(splitter, &QSplitter::splitterMoved, [this] { m_treeView->setMinimumWidth(0); m_treeView->setMaximumWidth(QWIDGETSIZE_MAX); }); } InstrumentLibraryEditor::~InstrumentLibraryEditor() { appSettings->saveWindowSizeAndPos(this); saveSplitterPos(); } void InstrumentLibraryEditor::applySplitterPos() { QSettings settings; if (settings.childGroups().contains(GUI::Style::S_INSTRUMENT_LIBRARY_EDITOR)) { settings.beginGroup(GUI::Style::S_INSTRUMENT_LIBRARY_EDITOR); m_treeView->setFixedWidth( settings.value(GUI::Style::S_INSTRUMENT_LIBRARY_EDITOR_TREE_WIDTH).toInt()); settings.endGroup(); } else m_treeView->setFixedWidth(GUI::Style::INSTRUMENT_LIBRARY_EDITOR_TREE_WIDTH); } void InstrumentLibraryEditor::saveSplitterPos() { QSettings settings; settings.beginGroup(GUI::Style::S_INSTRUMENT_LIBRARY_EDITOR); settings.setValue(GUI::Style::S_INSTRUMENT_LIBRARY_EDITOR_TREE_WIDTH, m_treeView->width()); settings.endGroup(); settings.sync(); } InstrumentItem* InstrumentLibraryEditor::execChoose() { setWindowTitle("Instrument Library - Choose instrument"); ItemViewOverlayButtons::install( m_treeView, [this](const QModelIndex& i, bool h) { return getOverlayActions(i, h); }); m_treeView->setItemDelegate(new ItemDelegateForHTML(this)); connect(m_treeView, &QTreeView::doubleClicked, this, &InstrumentLibraryEditor::onItemDoubleClickedForChoose); connect(m_treeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &InstrumentLibraryEditor::onCurrentChangedForChoose); onCurrentChangedForChoose(); // select the first instrument QModelIndex index = m_treeModel->indexForItem(m_instrumentLibrary->instrumentItems().first()); m_treeView->setCurrentIndex(index); if (exec() == QDialog::Accepted) return m_chosenItem; return nullptr; } void InstrumentLibraryEditor::execAdd(const InstrumentItem& instrumentToAdd) { const QString& newName = m_instrumentLibrary->suggestName(instrumentToAdd.instrumentName()); auto* addedInstrument = m_instrumentLibrary->addItemCopy(newName, instrumentToAdd); setWindowTitle("Instrument Library - Add instrument"); m_treeModel->setNewInstrument(addedInstrument); ItemViewOverlayButtons::install( m_treeView, [this](const QModelIndex& i, bool h) { return getOverlayActions(i, h); }); m_treeView->setItemDelegate(new ItemDelegateForHTML(this)); connect(m_treeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &InstrumentLibraryEditor::createWidgetsForCurrentInstrument); QModelIndex index = m_treeModel->indexForItem(addedInstrument); m_treeView->expandAll(); m_treeView->setCurrentIndex(index); m_treeView->scrollTo(index, QAbstractItemView::PositionAtTop); createWidgetsForCurrentInstrument(); exec(); } void InstrumentLibraryEditor::onItemDoubleClickedForChoose(const QModelIndex& index) { m_chosenItem = m_treeModel->itemForIndex(index); if (m_chosenItem != nullptr) accept(); } void InstrumentLibraryEditor::onCurrentChangedForChoose() { m_chosenItem = m_treeModel->itemForIndex(m_treeView->currentIndex()); m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(m_chosenItem != nullptr); createWidgetsForCurrentInstrument(); } QList<QAction*> InstrumentLibraryEditor::getOverlayActions(const QModelIndex& index, bool asHover) { if (m_treeModel->isHeadline(index)) return {}; // -- index belongs to item if (!asHover) return {}; auto* item = m_treeModel->itemForIndex(index); if (item == nullptr) return {}; auto* removeAction = new QAction(this); removeAction->setText("Remove"); removeAction->setIcon(QIcon(":/images/delete.svg")); removeAction->setIconText("Remove"); removeAction->setToolTip("Remove this instrument"); connect(removeAction, &QAction::triggered, [this, item] { m_treeModel->removeItem(item); }); return {removeAction}; } void InstrumentLibraryEditor::createWidgetsForCurrentInstrument() { auto* currentInstrument = m_treeModel->itemForIndex(m_treeView->currentIndex()); if (!currentInstrument) { m_scrollArea->setWidget(new QWidget(m_scrollArea)); // blank widget return; } auto* w = new QWidget(m_scrollArea); auto* layout = new QVBoxLayout(w); auto title = QString("Summary (%1 instrument)").arg(currentInstrument->instrumentType()); auto* g = new CollapsibleGroupBox(title, m_scrollArea, currentInstrument->expandInfo); g->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); auto* formLayout = new QFormLayout(g->body()); formLayout->setContentsMargins(17, 17, 17, 17); layout->addWidget(g); auto* nameEdit = new QLineEdit(g); formLayout->addRow("Name:", nameEdit); nameEdit->setText(currentInstrument->instrumentName()); connect(nameEdit, &QLineEdit::textEdited, this, &InstrumentLibraryEditor::onInstrumentNameEdited); auto* descriptionEdit = new QTextEdit(g); descriptionEdit->setMinimumWidth(300); descriptionEdit->setFixedHeight(60); // TODO replace by 2*line_height descriptionEdit->setAcceptRichText(false); descriptionEdit->setTabChangesFocus(true); descriptionEdit->setPlainText(currentInstrument->description()); formLayout->addRow("Description:", descriptionEdit); connect(descriptionEdit, &QTextEdit::textChanged, [this, descriptionEdit] { onInstrumentDescriptionEdited(descriptionEdit->toPlainText()); }); auto* ec = m_instrumentLibrary->editController(); if (auto* sp = dynamic_cast<SpecularInstrumentItem*>(currentInstrument)) { auto* editor = new SpecularInstrumentEditor(m_scrollArea, sp, ec); connect(editor, &SpecularInstrumentEditor::dataChanged, this, &InstrumentLibraryEditor::onInstrumentChangedByEditor); layout->addWidget(editor); } else if (auto* os = dynamic_cast<OffspecInstrumentItem*>(currentInstrument)) { auto* editor = new OffspecInstrumentEditor(m_scrollArea, os, ec); connect(editor, &OffspecInstrumentEditor::dataChanged, this, &InstrumentLibraryEditor::onInstrumentChangedByEditor); layout->addWidget(editor); } else if (auto* gisas = dynamic_cast<GISASInstrumentItem*>(currentInstrument)) { auto* editor = new GISASInstrumentEditor(m_scrollArea, gisas); connect(editor, &GISASInstrumentEditor::dataChanged, this, &InstrumentLibraryEditor::onInstrumentChangedByEditor); layout->addWidget(editor); } else if (auto* dp = dynamic_cast<DepthprobeInstrumentItem*>(currentInstrument)) { auto* editor = new DepthprobeInstrumentEditor(m_scrollArea, dp, ec); connect(editor, &DepthprobeInstrumentEditor::dataChanged, this, &InstrumentLibraryEditor::onInstrumentChangedByEditor); layout->addWidget(editor); } else ASSERT_NEVER; m_scrollArea->setWidget(w); } void InstrumentLibraryEditor::onInstrumentNameEdited(const QString& newName) { QModelIndex index = m_treeView->currentIndex(); m_treeModel->setData(index, newName, Qt::EditRole); } void InstrumentLibraryEditor::onInstrumentDescriptionEdited(const QString& t) { QModelIndex index = m_treeView->currentIndex(); m_treeModel->setData(index, t, Qt::ToolTipRole); } void InstrumentLibraryEditor::onInstrumentChangedByEditor() { // uses 'MultiInstrumentNotifier::instrumentChanged' signal to set // 'InstrumentLibrary:m_modified' flag to true ==> save library on close. auto* currentInstrument = m_treeModel->itemForIndex(m_treeView->currentIndex()); m_instrumentLibrary->editController()->notifyInstrumentChanged(currentInstrument); } /*********************************************************************************************/ TreeModel::TreeModel(QObject* parent, InstrumentModel* model) : InstrumentsTreeModel(parent, model) { } void TreeModel::setNewInstrument(InstrumentItem* addedInstrument) { m_newInstrument = addedInstrument; } QVariant TreeModel::data(const QModelIndex& index, int role) const { if (isHeadline(index)) return InstrumentsTreeModel::data(index, role); auto* const item = itemForIndex(index); if (role == Qt::DisplayRole) { auto descr = item->description(); if (!descr.isEmpty()) { descr.prepend("<br><br>"); // max 4 lines while (descr.count("\n") > 3) { descr.truncate(descr.lastIndexOf("\n")); descr += " [...]"; } descr.replace("\n", "<br>"); } return "<b>" + item->instrumentName() + "</b>" + descr; } if (role == Qt::DecorationRole && (item == m_newInstrument)) { if (role == Qt::DecorationRole) switch (instrumentType(item)) { case Gisas: return QIcon(":/images/gisas_instrument_new.svg"); case Offspec: return QIcon(":/images/offspec_instrument_new.svg"); case Specular: return QIcon(":/images/specular_instrument_new.svg"); case Depthprobe: return QIcon(":/images/depth_instrument_new.svg"); default: break; } } return InstrumentsTreeModel::data(index, role); }