/* BEGIN software license
 *
 * MsXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright(C) 2009,...,2026 Filippo Rusconi
 *
 * http://www.msxpertsuite.org
 *
 * This file is part of the MsXpertSuite project.
 *
 * The MsXpertSuite project is the successor of the massXpert project. This
 * project now includes various independent modules:
 *
 * - MassXpert, model polymer chemistries and simulate mass spectrometric data;
 * - MineXpert, a powerful TIC chromatogram/mass spectrum viewer/miner;
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * END software license
 */


/////////////////////// Qt includes
#include <QMessageBox>
#include <QFileDialog>
#include <QDebug>
#include <QKeyEvent>
#include <QKeySequence>
#include <QSettings>


/////////////////////// libXpertMass includes
#include <MsXpS/libXpertMassCore/IsotopicDataUserConfigHandler.hpp>
#include <MsXpS/libXpertMassCore/IsotopicClusterGenerator.hpp>
#include <MsXpS/libXpertMassCore/IsotopicClusterShaper.hpp>
#include <MsXpS/libXpertMassCore/IsotopicData.hpp>
#include <MsXpS/libXpertMassCore/MassPeakShaper.hpp>
#include <MsXpS/libXpertMassCore/MassPeakShaperConfig.hpp>


/////////////////////// libXpertMassGui includes
#include <MsXpS/libXpertMassGui/ColorSelector.hpp>


/////////////////////// Local includes
#include "../nongui/globals.hpp"
#include "ui_CleavageDlg.h"
#include "CleavageDlg.hpp"
#include "CleaveOligomerTableViewModel.hpp"
#include "SequenceEditorWnd.hpp"


namespace MsXpS
{

namespace MassXpert
{


CleavageDlg::CleavageDlg(
  SequenceEditorWnd *editor_wnd_p,
  libXpertMassCore::PolymerQSPtr polymer_sp,
  const libXpertMassCore::PolChemDefCstSPtr pol_chem_def_csp,
  const QString &config_settings_file_path,
  const QString &application_name,
  const QString &description,
  QByteArray hash,
  const libXpertMassCore::CalcOptions &calc_options,
  const libXpertMassCore::Ionizer &ionizer)
  : AbstractSeqEdWndDependentDlg(editor_wnd_p,
                                 polymer_sp,
                                 pol_chem_def_csp,
                                 config_settings_file_path,
                                 "CleavageDlg",
                                 application_name,
                                 description),
    m_polymerHash(hash),
    m_calcOptions(calc_options),
    m_ionizer(ionizer),
    mp_cleavageConfig(new libXpertMassCore::CleavageConfig(this)),
    mp_ui(new Ui::CleavageDlg)
{
  if(polymer_sp == nullptr || pol_chem_def_csp == nullptr)
    qFatal() << "Programming error. Pointer cannot be nullptr.";

  if(!initialize())
    qFatal() << "Programming error. Failed to initialize dialog window.";

  // qDebug() << "Instantiating CleavageDlg with Ionizer:" <<
  // m_ionizer.toString();
}


CleavageDlg::~CleavageDlg()
{
  m_oligomers.clear();

  delete mpa_resultsString;

  delete mpa_oligomerTableViewModel;
  delete mpa_proxyModel;

  delete mpa_cleaver;

  writeSettings();
}

void
CleavageDlg::writeSettings()
{
  QSettings settings(m_configSettingsFilePath, QSettings::IniFormat);

  settings.beginGroup(m_wndTypeName);
  settings.setValue("geometry", saveGeometry());
  settings.setValue("oligomersSplitter", mp_ui->oligomersSplitter->saveState());
  settings.setValue("oligoDetailsSplitter",
                    mp_ui->oligoDetailsSplitter->saveState());

  settings.endGroup();
}


void
CleavageDlg::readSettings()
{
  QSettings settings(m_configSettingsFilePath, QSettings::IniFormat);

  settings.beginGroup(m_wndTypeName);

  restoreGeometry(settings.value("geometry").toByteArray());

  mp_ui->oligomersSplitter->restoreState(
    settings.value("oligomersSplitter").toByteArray());

  mp_ui->oligoDetailsSplitter->restoreState(
    settings.value("oligoDetailsSplitter").toByteArray());

  settings.endGroup();
}


bool
CleavageDlg::initialize()
{
  mp_ui->setupUi(this);

  setWindowIcon(qApp->windowIcon());

  // Update the window title because the window title element in m_ui might be
  // either erroneous or empty.
  setWindowTitle(
    QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription));

  mp_ui->delimiterLineEdit->setText("$");

  mp_ui->massSpectrumTitleLineEdit->setText(msp_polymer->getName());
  populateListOfAvailableCleavageAgents();
  populateSelectedOligomerData();

  // Set pointers to 0 so that after the setupTableView call below
  // they'll get their proper value. We'll then be able to free all
  // that stuff in the destructor.
  mpa_cleaver                = nullptr;
  mpa_proxyModel             = nullptr;
  mpa_oligomerTableViewModel = nullptr;

  setupTableView();

  // Set the default color of the mass spectra trace upon mass spectrum
  // synthesis
  QColor color("black");
  // Now prepare the color in the form of a QByteArray
  QDataStream stream(&m_colorByteArray, QIODevice::WriteOnly);
  stream << color;

  // The tolerance when filtering mono/avg masses...
  QStringList stringList;

  stringList << tr("AMU") << tr("PCT") << tr("PPM");

  mp_ui->toleranceComboBox->insertItems(0, stringList);

  mp_ui->toleranceComboBox->setToolTip(
    tr("AMU: atom mass unit \n"
       "PCT: percent \n"
       "PPM: part per million"));

  filterAct = new QAction(tr("Toggle Filtering"), this);
  filterAct->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_G));
  this->addAction(filterAct);
  connect(filterAct, SIGNAL(triggered()), this, SLOT(filterOptionsToggled()));

  mp_ui->filteringOptionsGroupBox->addAction(filterAct);
  // When the dialog box is created it is created with the groupbox
  // unchecked.
  mp_ui->filteringOptionsFrame->setVisible(false);

  // When the filtering group box will be opened, the focus will be
  // on the first widget of the groupbox:
  mp_focusWidget = mp_ui->filterPartialLineEdit;

  // The results-exporting menus. ////////////////////////////////

  QStringList comboBoxItemList;

  comboBoxItemList.insert(
    (int)ExportResultsActions::EXPORT_TO_CLIPBOARD_OVERWRITE,
    "Overwrite clipboard (Ctrl+O)");
  comboBoxItemList.insert((int)ExportResultsActions::EXPORT_TO_CLIPBOARD_APPEND,
                          "To clipboard");
  comboBoxItemList.insert((int)ExportResultsActions::EXPORT_TO_FILE, "To file");
  comboBoxItemList.insert((int)ExportResultsActions::SELECT_FILE,
                          "Select file");

  mp_ui->exportResultsComboBox->addItems(comboBoxItemList);

  connect(mp_ui->exportResultsComboBox,
          SIGNAL(activated(int)),
          this,
          SLOT(exportResults(int)));

  comboBoxItemList.clear();

  comboBoxItemList.insert((int)MassSpectrumSynthesisActions::LOAD_ISOTOPIC_DATA,
                          "Load isotopic data from file");

  comboBoxItemList.insert(
    (int)MassSpectrumSynthesisActions::CONFIGURE_MASS_PEAK_SHAPER,
    "Configure the mass peak shaper");

  comboBoxItemList.insert(
    (int)MassSpectrumSynthesisActions::SYNTHESIZE_MASS_SPECTRA,
    "Synthesize the mass spectra");

  comboBoxItemList.insert(
    (int)MassSpectrumSynthesisActions::COPY_MASS_SPECTRUM_TO_CLIPBOARD,
    "Mass spectrum not yet available");

  comboBoxItemList.insert(
    (int)MassSpectrumSynthesisActions::SAVE_MASS_SPECTRUM_TO_FILE,
    "Mass spectrum not yet available");

  mp_ui->massSpectrumSynthesisComboBox->addItems(comboBoxItemList);

  connect(mp_ui->massSpectrumSynthesisComboBox,
          SIGNAL(activated(int)),
          this,
          SLOT(massSpectrumSynthesisMenuActivated(int)));

  comboBoxItemList.clear();

  // The color button
  connect(mp_ui->colorSelectorPushButton,
          &QPushButton::clicked,
          this,
          &CleavageDlg::traceColorPushButtonClicked);

  comboBoxItemList << "%e$%i$%l$%s$%m$%p\\n";
  mp_ui->outputPatternComboBox->addItems(comboBoxItemList);

  connect(mp_ui->outputPatternHelpPushButton,
          &QPushButton::clicked,
          this,
          &CleavageDlg::showOutputPatternDefinitionHelp);

  mpa_resultsString = new QString();

  //////////////////////////////////// The results-exporting menus.

  readSettings();

  connect(mp_ui->cleavePushButton, SIGNAL(clicked()), this, SLOT(cleave()));

  connect(mp_ui->filterPartialLineEdit,
          SIGNAL(returnPressed()),
          this,
          SLOT(filterPartial()));

  connect(mp_ui->filterMonoMassLineEdit,
          SIGNAL(returnPressed()),
          this,
          SLOT(filterMonoMass()));

  connect(mp_ui->filterAvgMassLineEdit,
          SIGNAL(returnPressed()),
          this,
          SLOT(filterAvgMass()));

  connect(mp_ui->filterChargeLineEdit,
          SIGNAL(returnPressed()),
          this,
          SLOT(filterCharge()));

  connect(mp_ui->filteringOptionsGroupBox,
          SIGNAL(clicked(bool)),
          this,
          SLOT(filterOptions(bool)));

  show();

  return true;
}

void
CleavageDlg::setStackOligomers(bool stack_oligomers)
{
  mp_ui->stackOligomersCheckBox->setCheckState(stack_oligomers ? Qt::Checked
                                                               : Qt::Unchecked);
}


bool
CleavageDlg::populateSelectedOligomerData()
{
  // Deal with the actual (or not) selection in the
  libXpertMassCore::IndexRangeCollection index_range_collection;

  bool selection_is_real =
    mp_editorWnd->mpa_editorGraphicsView->selectionIndices(
      index_range_collection);

  // In  each one of the logical branches below we need to end up
  // with setting the m_cleaveIndexRange because that is needed
  // to update the GUI polymer selection data start/stop values.

  // If the checkbox asking for whole sequence cleavage is
  // checked, then we have to make the cleavage on the whole
  // sequence. Likewise, if there is not real sequence selection
  // do the cleavage  on the whole polymer sequence.
  if(!selection_is_real || mp_ui->wholeSequenceCheckBox->isChecked())
    { // The cleavage is to be performed on the whole polymer
      // sequence.

      index_range_collection.setIndexRange(0, msp_polymer->size() - 1);

      // qDebug() << "The whole sequence is to be cleaved, indices:"
      //          << index_range_collection.indicesAsText();

      m_cleaveIndexRange.m_start = 0;
      m_cleaveIndexRange.m_stop  = msp_polymer->size() - 1;

      m_calcOptions.setIndexRange(m_cleaveIndexRange);
    }
  else
    {
      if(selection_is_real)
        {
          // qDebug() << "Got true with the indices from seq ed window:"
          //          << index_range_collection.indicesAsText();

          if(index_range_collection.size() > 1)
            {
              QMessageBox::information(
                this,
                QString("%1 - %2")
                  .arg(m_applicationName)
                  .arg(m_windowDescription),
                "Cleavage simulations with multi-region\nselection are not"
                "supported. Using the most inclusive sequence index range.",
                QMessageBox::Ok);

              libXpertMassCore::IndexRange *index_range_p =
                index_range_collection.mostInclusiveLeftRightIndexRange();
              m_cleaveIndexRange.initialize(*index_range_p);
              delete index_range_p;

              // qDebug() << "There were more than one selections, set the most
              // "
              //             "inclusive one:"
              //          << m_cleaveIndexRange.indicesAsText();

              m_calcOptions.setIndexRange(m_cleaveIndexRange);
            }
          else
            {
              // The selection is real and there is only one
              // selected index range.
              m_cleaveIndexRange.initialize(
                index_range_collection.getRangeCstRefAt(0));
              m_calcOptions.setIndexRange(m_cleaveIndexRange);
            }
        }
    }

  mp_ui->oligomerStartLabel->setText(
    QString().setNum(m_cleaveIndexRange.m_start + 1));
  mp_ui->oligomerEndLabel->setText(
    QString().setNum(m_cleaveIndexRange.m_stop + 1));


  int partial_cross_links_count = 0;
  std::size_t in_count;
  std::size_t out_count;

  for(const libXpertMassCore::CrossLinkSPtr &cross_link_sp :
      msp_polymer->getCrossLinksCstRef())
    {
      libXpertMassCore::Enums::CrossLinkEncompassed ret =
        cross_link_sp->isEncompassedByIndexRangeCollection(
          index_range_collection, in_count, out_count);

      if(ret == libXpertMassCore::Enums::CrossLinkEncompassed::FULLY)
        {
          // 		qDebug() << __FILE__ << __LINE__
          // 			  << "CrossLink at iter:" << iter
          // 			  << "is fully encompassed";
        }
      else if(ret == libXpertMassCore::Enums::CrossLinkEncompassed::PARTIALLY)
        {
          // 		qDebug() << __FILE__ << __LINE__
          // 			  << "CrossLink at iter:" << iter
          // 			  << "is partially encompassed";

          ++partial_cross_links_count;
        }
      else
        {
          // 		qDebug() << __FILE__ << __LINE__
          // 			  << "CrossLink at iter:" << iter
          // 			  << "is not encompassed at all";
        }
    }

  if(partial_cross_links_count)
    {
      // Alert the user on the fact that the currently selected
      // region does not encompass all of the cross-linked material.

      QMessageBox::information(
        this,
        QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
        "There are incomplete cross-links",
        QMessageBox::Ok);
    }

  // qDebug() << __FILE__ << __LINE__
  // << "Updated index_range.";

  return true;
}


void
CleavageDlg::populateListOfAvailableCleavageAgents()
{
  PolChemDefRenderingCstRPtr pol_chem_def_rendering_crp =
    mp_editorWnd->getPolChemDefRenderingCstRPtr();

  if(pol_chem_def_rendering_crp == nullptr)
    qFatal() << "Programming error. Pointer cannot be nullptr.";

  const std::vector<MsXpS::libXpertMassCore::CleavageAgentSPtr>
    &cleavage_agents = pol_chem_def_rendering_crp->getPolChemDefCstSPtr()
                         ->getCleavageAgentsCstRef();

  for(const libXpertMassCore::CleavageAgentCstSPtr cleavage_agent_csp :
      cleavage_agents)
    mp_ui->cleavageAgentListWidget->addItem(cleavage_agent_csp->getName());

  return;
}

void
CleavageDlg::setupTableView()
{
  // Model stuff all thought for sorting.
  mpa_oligomerTableViewModel =
    new CleaveOligomerTableViewModel(&m_oligomers, this);

  mpa_proxyModel = new CleaveOligomerTableViewSortProxyModel(this);
  mpa_proxyModel->setSourceModel(mpa_oligomerTableViewModel);
  mpa_proxyModel->setFilterKeyColumn(-1);

  mp_ui->oligomerTableView->setModel(mpa_proxyModel);
  mp_ui->oligomerTableView->setParentDlg(this);
  mp_ui->oligomerTableView->setOligomerCollection(&m_oligomers);
  mpa_oligomerTableViewModel->setTableView(mp_ui->oligomerTableView);
}


SequenceEditorWnd *
CleavageDlg::editorWnd()
{
  return mp_editorWnd;
}

QByteArray
CleavageDlg::polymerHash() const
{
  return m_polymerHash;
}


void
CleavageDlg::cleave()
{
  // What's the cleavage agent ?
  QListWidgetItem *current_item_p =
    mp_ui->cleavageAgentListWidget->currentItem();

  QString cleavage_agent_name = current_item_p->text();

  libXpertMassCore::CleavageAgent *cleavage_agent_p =
    new libXpertMassCore::CleavageAgent(this);

  if(!cleavage_agent_p->initialize(
       *mcsp_polChemDef->getCleavageAgentCstSPtrByName(cleavage_agent_name)))
    qFatal()
      << "Programming error. The cleavage agent cannot be unknown to "
         "the polymer chemistry definition.";

  mp_cleavageConfig->setCleavageAgent(*cleavage_agent_p);

  // What's the asked level of partial cleavages?
  mp_cleavageConfig->setPartials(mp_ui->partialCleavageSpinBox->value());

  mp_cleavageConfig->setIonizeLevels(mp_ui->ionizeLevelStartSpinBox->value(),
                                     mp_ui->ionizeLevelEndSpinBox->value());

  // The list in which we'll store all the allocated oligomers.
  libXpertMassCore::OligomerCollection temp_oligomer_collection;

  delete mpa_cleaver;
  mpa_cleaver = nullptr;

  // Update the mass calculation engine's configuration data
  // directly from the sequence editor window.
  m_calcOptions.initialize(*mp_editorWnd->getCalcOptions());

  qDebug() << "CalcOptions initialized from sequence editor window:"
           << m_calcOptions.toString();

  // Update the selection data from the sequence editor window.
  // The m_cleaveIndexRange will be updated and also set to m_calcOptions.
  if(!populateSelectedOligomerData())
    return;

  qDebug().noquote() << "After populating the selected oligomer data:"
                     << m_calcOptions.toString()
                     << " and passing them as-is to the cleaver.";

  // m_ionizer is the copy of the Ionizer in the SequenceEditorWnd. It might
  // contain an ionization status (that is, charge() or currentCharge() returns
  // non-0) but we do not want this, we want to transmit an ionizer with the
  // ionization formula and the nominal charge set. The other data must be 0.

  libXpertMassCore::Ionizer ionizer(m_ionizer);
  ionizer.setLevel(0);
  ionizer.forceCurrentStateLevel(0);

  mpa_cleaver = new libXpertMassCore::Cleaver(
    msp_polymer, mcsp_polChemDef, *mp_cleavageConfig, m_calcOptions, ionizer);

  qDebug().noquote() << "After having allocated the cleaver:\n"
                     << m_calcOptions.toString();

  if(!mpa_cleaver->cleave())
    {
      delete mpa_cleaver;
      mpa_cleaver = nullptr;

      QMessageBox::critical(
        this,
        QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
        "Failed to perform cleavage.",
        QMessageBox::Ok);
      return;
    }

  std::size_t initial_oligomer_count =
    mpa_cleaver->getOligomerCollectionCstRef().size();

  qDebug() << "Returned from Cleaver cleave work with" << initial_oligomer_count
           << "oligomers generated.";

  // At this point we have a brand new set of oligomers in the Cleaver
  // object. We either stack
  // these on top of the previous ones, or we replace the previous ones with the
  // new ones.  Are we stacking new oligomers on top of the old
  // ones?

  if(!mp_ui->stackOligomersCheckBox->isChecked())
    {
      // We are going to remove *all* the previous oligomers.
      mpa_oligomerTableViewModel->removeOligomers(0, m_oligomers.size() - 1);
    }

  // At this point we can set up the data to the treeview model.

  // We are *transferring* the oligomers from
  // temp_oligomer_collection to the list of m_oligomers list
  // oligomers of which there is a pointer in the model : we are not duplicating
  // the oligomers. When the transfer from temp_oligomer_collection
  // to m_oligomers _via_ the model will have been done,
  // temp_oligomer_collection will be empty and m_oligomers will
  // hold them all, with the model having been made aware of that with the
  // beginInsertRows/endInsertRows statement pair.

  std::size_t added_oligomer_count = mpa_oligomerTableViewModel->addOligomers(
    mpa_cleaver->getOligomerCollectionCstRef());

  // qDebug() << __FILE__ << __LINE__
  //          << "addedOligomers:" << addedOligomers;

  if(added_oligomer_count != initial_oligomer_count)
    qFatal()
      << "Programming error. Failed to add all the Oligomer objects.";

  // Now that we have *transferred* (not copied) all the oligomers
  // in the model, we can clear the temp_oligomer_collection without
  // freeing the instances it currently contain...
  temp_oligomer_collection.clear();

  // Set focus to the treeView.
  mp_ui->oligomerTableView->setFocus();

  QString title;

  int oligomerCount = mpa_oligomerTableViewModel->rowCount();

  if(!oligomerCount)
    title = tr("Oligomers (empty list)");
  else if(oligomerCount == 1)
    title = tr("Oligomers (one item)");
  else
    title = tr("Oligomers (%1 items)").arg(oligomerCount);

  mp_ui->oligomerGroupBox->setTitle(title);

  // Update the cleavage details so that we know how the oligos were
  // computed.
  updateCleavageDetails(m_calcOptions);
}


void
CleavageDlg::updateCleavageDetails(
  const libXpertMassCore::CalcOptions &calcOptions)
{
  mp_ui->leftCapCheckBox->setChecked(static_cast<bool>(
    calcOptions.getCapType() & libXpertMassCore::Enums::CapType::LEFT));
  mp_ui->rightCapCheckBox->setChecked(static_cast<bool>(
    calcOptions.getCapType() & libXpertMassCore::Enums::CapType::RIGHT));

  mp_ui->modificationsCheckBox->setChecked(
    static_cast<bool>(calcOptions.getMonomerEntities() &
                      libXpertMassCore::Enums::ChemicalEntity::MODIF));

  mp_ui->crossLinksCheckBox->setChecked(
    static_cast<bool>(calcOptions.getMonomerEntities() &
                      libXpertMassCore::Enums::ChemicalEntity::CROSS_LINKER));

  mp_ui->leftModifCheckBox->setChecked(
    static_cast<bool>(calcOptions.getPolymerEntities() &
                      libXpertMassCore::Enums::ChemicalEntity::LEFT_END_MODIF));
  mp_ui->rightModifCheckBox->setChecked(static_cast<bool>(
    calcOptions.getPolymerEntities() &
    libXpertMassCore::Enums::ChemicalEntity::RIGHT_END_MODIF));
}


void
CleavageDlg::updateOligomerSequence(QString *text)
{
  Q_ASSERT(text);

  mp_ui->oligomerSequenceTextEdit->clear();
  mp_ui->oligomerSequenceTextEdit->append(*text);
}


void
CleavageDlg::setCumulatedProbabilities(double cumul_probs)
{
  mp_ui->cumulatedProbsDoubleSpinBox->setValue(cumul_probs);
}

void
CleavageDlg::setNormalizationIntensity(double intensity)
{
  mp_ui->normalizeIntensityDoubleSpinBox->setValue(intensity);
}

bool
CleavageDlg::calculateTolerance(double mass)
{
  // Get the tolerance that is in its lineEdit.

  QString text     = mp_ui->toleranceLineEdit->text();
  double tolerance = 0;
  bool ok          = false;

  if(!text.isEmpty())
    {
      // Convert the string to a double.

      ok        = false;
      tolerance = text.toDouble(&ok);

      if(!tolerance && !ok)
        return false;
    }
  else
    {
      m_tolerance = 0;
    }

  // What's the item currently selected in the comboBox?
  int index = mp_ui->toleranceComboBox->currentIndex();

  if(index == 0)
    {
      // MASS_TOLERANCE_AMU
      m_tolerance = tolerance;
    }
  else if(index == 1)
    {
      // MASS_TOLERANCE_PCT
      m_tolerance = (tolerance / 100) * mass;
    }
  else if(index == 2)
    {
      // MASS_TOLERANCE_PPM
      m_tolerance = (tolerance / 1000000) * mass;
    }
  else
    Q_ASSERT(0);

  return true;
}

QString
CleavageDlg::lastUsedCleavageAgentName() const
{
  if(mpa_cleaver != nullptr)
    return mpa_cleaver->getCleaveAgentName();
  else
    return QString("");
}

bool
CleavageDlg::configureCleavage(const QString cleavage_agent,
                               int partials,
                               int start_ionize_level,
                               int stop_ionize_level,
                               bool should_embed_sequence)
{
  QList<QListWidgetItem *> items = mp_ui->cleavageAgentListWidget->findItems(
    cleavage_agent, Qt::MatchFixedString | Qt::MatchCaseSensitive);

  if(!items.size() || items.size() > 1)
    {
      qInfo() << "Cannot configure cleavage with unknown cleavage agent or "
                 "more than one candidate.";
      return false;
    }

  mp_ui->cleavageAgentListWidget->setCurrentItem(items.at(0));

  if(partials < 0)
    {
      qInfo() << "Cannot configure cleavage with negative partial cleavages.";
      return false;
    }

  mp_ui->partialCleavageSpinBox->setValue(partials);

  if(start_ionize_level < 0 || stop_ionize_level < 0)
    {
      qInfo() << "Cannot configure cleavage with negative ionization levels.";
      return false;
    }

  mp_ui->ionizeLevelStartSpinBox->setValue(start_ionize_level);
  mp_ui->ionizeLevelEndSpinBox->setValue(stop_ionize_level);

  mp_ui->withSequenceCheckBox->setCheckState(
    should_embed_sequence ? Qt::Checked : Qt::Unchecked);

  return true;
}

void
CleavageDlg::filterOptions(bool checked)
{
  if(!checked)
    {
      mpa_proxyModel->setFilterKeyColumn(-1);

      mpa_proxyModel->applyNewFilter();

      mp_ui->filteringOptionsFrame->setVisible(false);
    }
  else
    {
      mp_ui->filteringOptionsFrame->setVisible(true);

      // In this case, set focus to the last focused widget in the
      // groupbox or the first widget in the groubox if this is the
      // first time the filtering is used.
      mp_focusWidget->setFocus();
    }
}


void
CleavageDlg::filterOptionsToggled()
{
  bool isChecked = mp_ui->filteringOptionsGroupBox->isChecked();

  mp_ui->filteringOptionsGroupBox->setChecked(!isChecked);
  filterOptions(!isChecked);
}


void
CleavageDlg::filterPartial()
{
  // First off, we have to get the partial that is in the lineEdit.

  QString text = mp_ui->filterPartialLineEdit->text();

  if(text.isEmpty())
    return;

  // Convert the string to a int.

  bool ok     = false;
  int partial = text.toInt(&ok);

  if(!partial && !ok)
    return;

  mpa_proxyModel->setPartialFilter(partial);
  mpa_proxyModel->setFilterKeyColumn(0);
  mpa_proxyModel->applyNewFilter();

  mp_focusWidget = mp_ui->filterPartialLineEdit;
}


void
CleavageDlg::filterMonoMass()
{
  // First off, we have to get the mass that is in the lineEdit.

  QString text = mp_ui->filterMonoMassLineEdit->text();

  if(text.isEmpty())
    return;

  // Convert the string to a double.

  bool ok     = false;
  double mass = text.toDouble(&ok);

  if(!mass && !ok)
    return;

  // At this point, depending on the item that is currently selected
  // in the comboBox, we'll have to actually compute the tolerance.

  if(!calculateTolerance(mass))
    return;

  mpa_proxyModel->setMonoFilter(mass);
  mpa_proxyModel->setTolerance(m_tolerance);

  mpa_proxyModel->setFilterKeyColumn(3);
  mpa_proxyModel->applyNewFilter();

  mp_focusWidget = mp_ui->filterMonoMassLineEdit;
}


void
CleavageDlg::filterAvgMass()
{
  // First off, we have to get the mass that is in the lineEdit.

  QString text = mp_ui->filterAvgMassLineEdit->text();

  if(text.isEmpty())
    return;

  // Convert the string to a double.

  bool ok     = false;
  double mass = text.toDouble(&ok);

  if(!mass && !ok)
    return;

  // At this point, depending on the item that is currently selected
  // in the comboBox, we'll have to actually compute the tolerance.

  if(!calculateTolerance(mass))
    return;

  mpa_proxyModel->setAvgFilter(mass);
  mpa_proxyModel->setTolerance(m_tolerance);

  mpa_proxyModel->setFilterKeyColumn(4);
  mpa_proxyModel->applyNewFilter();

  mp_focusWidget = mp_ui->filterAvgMassLineEdit;
}


void
CleavageDlg::filterCharge()
{
  // First off, we have to get the charge that is in the lineEdit.

  QString text = mp_ui->filterChargeLineEdit->text();

  if(text.isEmpty())
    return;

  // Convert the string to a int.

  bool ok    = false;
  int charge = text.toInt(&ok);

  if(!charge && !ok)
    return;

  mpa_proxyModel->setChargeFilter(charge);
  mpa_proxyModel->setFilterKeyColumn(5);
  mpa_proxyModel->applyNewFilter();

  mp_focusWidget = mp_ui->filterChargeLineEdit;
}

void
CleavageDlg::setTraceColor(QColor color)
{
  // We want to color the pushbutton.
  QPushButton *colored_push_button = mp_ui->colorSelectorPushButton;

  if(color.isValid())
    {
      QPalette palette = colored_push_button->palette();
      palette.setColor(QPalette::Button, color);
      colored_push_button->setAutoFillBackground(true);
      colored_push_button->setPalette(palette);
      colored_push_button->update();

      // Now prepare the color in the form of a QByteArray

      QDataStream stream(&m_colorByteArray, QIODevice::WriteOnly);

      stream << color;
    }
}

void
CleavageDlg::setTraceTitle(const QString &title)
{
  mp_ui->massSpectrumTitleLineEdit->setText(title);
}

int
CleavageDlg::selectAllTableViewRows()
{
  mpa_oligomerTableViewModel->tableView()->selectAll();

  return mpa_oligomerTableViewModel->tableView()
    ->selectionModel()
    ->selectedRows()
    .count();
}

void
CleavageDlg::traceColorPushButtonClicked()
{
  // Allow (true) the user to select a color that has been chosen already.
  QColor color = libXpertMassGui::ColorSelector::chooseColor(true);

  setTraceColor(color);
}

void
CleavageDlg::massSpectrumSynthesisMenuActivated(int index)
{
  if(index == (int)MassSpectrumSynthesisActions::LOAD_ISOTOPIC_DATA)
    {
      loadIsotopicDataFromFile();
      return;
    }
  if(index == (int)MassSpectrumSynthesisActions::CONFIGURE_MASS_PEAK_SHAPER)
    configureMassPeakShaper();
  else if(index == (int)MassSpectrumSynthesisActions::SYNTHESIZE_MASS_SPECTRA)
    synthesizeMassSpectra();
  else if(index ==
          (int)MassSpectrumSynthesisActions::COPY_MASS_SPECTRUM_TO_CLIPBOARD)
    {
      if(!m_syntheticMassSpectrum.size())
        {
          mp_ui->massSpectrumSynthesisComboBox->setItemText(
            (int)MassSpectrumSynthesisActions::COPY_MASS_SPECTRUM_TO_CLIPBOARD,
            "Mass spectrum not yet available");

          return;
        }

      QClipboard *clipboard = QApplication::clipboard();
      clipboard->setText(m_syntheticMassSpectrum.toString());
    }
  else if(index ==
          (int)MassSpectrumSynthesisActions::SAVE_MASS_SPECTRUM_TO_FILE)
    {
      if(!m_syntheticMassSpectrum.size())
        {
          mp_ui->massSpectrumSynthesisComboBox->setItemText(
            (int)MassSpectrumSynthesisActions::SAVE_MASS_SPECTRUM_TO_FILE,
            "Mass spectrum not yet available");

          return;
        }

      QString file_path = QFileDialog::getSaveFileName(
        this,
        tr("Select file to export the mass spectrum  to"),
        QDir::homePath(),
        tr("Data files(*.xy *.XY)"));

      QFile file(file_path);

      if(!file.open(QIODevice::WriteOnly))
        {
          QMessageBox::information(0,
                                   tr("MassXpert3 - Export mass spectrum"),
                                   tr("Failed to open file in write mode."),
                                   QMessageBox::Ok);
          return;
        }

      QTextStream stream(&file);
      stream.setEncoding(QStringConverter::Utf8);

      prepareResultsTxtString();

      stream << m_syntheticMassSpectrum.toString();

      file.close();
    }
  else
    qFatal("Programming error. Should never reach this point.");
}


libXpertMassGui::MassPeakShaperConfigDlg *
CleavageDlg::configureMassPeakShaper()
{
  if(mp_massPeakShaperConfigDlg == nullptr)
    {
      mp_massPeakShaperConfigDlg = new libXpertMassGui::MassPeakShaperConfigDlg(
        this, m_applicationName, "Mass peak shaper configuration");

      if(mp_massPeakShaperConfigDlg == nullptr)
        qFatal("Programming error. Failed to allocate the dialog window.");
    }

  // The signal below is only emitted when checking parameters worked ok.
  connect(mp_massPeakShaperConfigDlg,
          &libXpertMassGui::MassPeakShaperConfigDlg::
            updatedMassPeakShaperConfigSignal,
          [this](const libXpertMassCore::MassPeakShaperConfig &config) {
            m_massPeakShaperConfig.initialize(config);

            // qDebug().noquote() << "Our local copy of the config:"
            //<< m_massPeakShaperConfig.toString();
          });

  mp_massPeakShaperConfigDlg->activateWindow();
  mp_massPeakShaperConfigDlg->raise();
  mp_massPeakShaperConfigDlg->show();

  return mp_massPeakShaperConfigDlg;
}


bool
CleavageDlg::loadIsotopicDataFromFile()
{
  QString file_name = QFileDialog::getOpenFileName(
    this, tr("Load User IsoSpec table"), QDir::home().absolutePath());

  if(file_name.isEmpty())
    {
      QMessageBox::warning(
        this,
        QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
        tr("Failed to set the file name."),
        QMessageBox::Ok);

      return false;
    }

  libXpertMassCore::IsotopicDataUserConfigHandler handler;
  handler.loadData(file_name);

  msp_isotopicData = handler.getIsotopicData();

  if(msp_isotopicData == nullptr || !msp_isotopicData->size())
    {
      QMessageBox::warning(
        this,
        QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
        "Failed to load the isotopic data.",
        QMessageBox::Ok);

      return false;
    }

  return true;
}


void
CleavageDlg::synthesizeMassSpectra()
{
  // Clear the synthetic mass spectrum. Make sure the mass spectrum to clipboard
  // menu item is not "available".
  m_syntheticMassSpectrum.clear();
  mp_ui->massSpectrumSynthesisComboBox->setItemText(
    (int)MassSpectrumSynthesisActions::COPY_MASS_SPECTRUM_TO_CLIPBOARD,
    "Mass spectrum not yet available");

  mp_ui->massSpectrumSynthesisComboBox->setItemText(
    (int)MassSpectrumSynthesisActions::SAVE_MASS_SPECTRUM_TO_FILE,
    "Mass spectrum not yet available");

  setCursor(Qt::WaitCursor);

  libXpertMassCore::ErrorList error_list;

  if(!m_massPeakShaperConfig.resolve(error_list))
    {
      QMessageBox msgBox;
      QString msg =
        QString("Mass peak shaper configuration errors:\n%1")
          .arg(libXpertMassCore::Utils::joinErrorList(error_list, "\n"));
      msgBox.setText(msg);
      msgBox.exec();

      configureMassPeakShaper();
    }

  // All the selected oligomers need to be processed such that using both
  // their elemental composition and their charge, we create isotopic
  // clusters that we can then merge into a single mass spectrum.

  libXpertMassCore::OligomerCollection selected_oligomers;

  std::size_t count =
    mp_ui->oligomerTableView->selectedOligomers(selected_oligomers, -1);

  if(!count)
    {
      QMessageBox msgBox;
      QString msg("Please, select at least one oligomer in the table view.");
      msgBox.setText(msg);
      msgBox.exec();

      setCursor(Qt::ArrowCursor);
      return;
    }

  // qDebug() << "Selected oligomers:" << count;

  // At this point, we need to get a handle on the isotopic data. Note how we
  // want a non-const shared pointer!

  if(msp_isotopicData == nullptr)
    msp_isotopicData = std::make_shared<libXpertMassCore::IsotopicData>(
      *mp_editorWnd->getPolChemDefRenderingRPtr()
         ->getPolChemDefCstSPtr()
         ->getIsotopicDataCstSPtr());

  if(msp_isotopicData == nullptr)
    qFatal("Programming error. The isotopic data cannot be nullptr.");

  // Great, we now we have isotopes to do the calculations!

  // And now we can instantiate a cluster generator

  libXpertMassCore::IsotopicClusterGenerator isotopic_cluster_generator(
    msp_isotopicData);

  // Construct the formula/charge pairs needed for the isotopic cluster
  // calculations.
  // libXpertMassCore::FormulaChargePair stands for std::pair<QString, int>
  std::vector<libXpertMassCore::FormulaChargePair> formula_charge_pairs;

  for(libXpertMassCore::OligomerSPtr oligomer_sp :
      selected_oligomers.getOligomersCstRef())
    {
      int charge           = oligomer_sp->getIonizerCstRef().charge();
      QString formula_text = oligomer_sp->getFormulaCstRef().getActionFormula();

      // qDebug() << "oligomer's elemental composition:" << formula_text
      //<< "with charge:" << charge;

      // Instantiate on-the-fly the formula/charge pair that is fed to the
      // generator.

      formula_charge_pairs.push_back(
        libXpertMassCore::FormulaChargePair(formula_text, charge));
    }

  // At this point, we can copy all the formula/charge pairs to the cluster
  // generator.

  isotopic_cluster_generator.setFormulaChargePairs(formula_charge_pairs);

  isotopic_cluster_generator.setIsotopicDataType(
    libXpertMassCore::IsotopicDataType::LIBRARY_CONFIG);

  double cumulated_probabilities = mp_ui->cumulatedProbsDoubleSpinBox->value();
  isotopic_cluster_generator.setMaxSummedProbability(cumulated_probabilities);
  double normalization_intensity =
    mp_ui->normalizeIntensityDoubleSpinBox->value();

  // Not needed here because it will be taken into account at peak shaping
  // time
  // isotopic_cluster_generator.setNormalizationIntensity(normalization_intensity);
  // qDebug() << "Set the generator's normalization intensity:"
  //<< normalization_intensity;

  // And now perform the work. For each formula/charge pair, the generator will
  // create an isotopic cluster shaped trace associated to the corresponding
  // charge in a libXpertMassCore::IsotopicClusterChargePair. All these pairs
  // are stored in a vector.

  count = isotopic_cluster_generator.run();

  if(!count)
    {
      qDebug() << "Failed to create any isotopic cluster.";

      QMessageBox::information(
        this,
        QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
        "Failed to compute a single isotopic cluster.",
        QMessageBox::Ok);

      setCursor(Qt::ArrowCursor);
      return;
    }

  // qDebug() << "The number of clusters generated:" << count;

  std::vector<libXpertMassCore::IsotopicClusterChargePair>
    isotopic_cluster_charge_pairs =
      isotopic_cluster_generator.getIsotopicClusterChargePairs();

  // At this point we should use these pairs to create a shape for each. But
  // first reset to 1 the charge because that charge was already accounted for
  // at the generation of the cluster. We do not want to divide m/z again by
  // charge. If charge had been 1, that would be no problem, but let's say the
  // charge was 2, if we did maintain that charge to a value of 2, then we would
  // get a tetra-protonated species cluster.

  // Note how we ask a reference to the pair that is iterated into, otherwise we
  // would get a copy and we would lose the local charge modification.
  for(libXpertMassCore::IsotopicClusterChargePair &pair :
      isotopic_cluster_charge_pairs)
    pair.second = 1;

  // Now instantiate the isotopic cluster shaper and set the clusters' data in
  // it.

  libXpertMassCore::IsotopicClusterShaper isotopic_cluster_shaper(
    isotopic_cluster_charge_pairs, m_massPeakShaperConfig);

  isotopic_cluster_shaper.setNormalizeIntensity(normalization_intensity);

  // And now run the shaper.
  m_syntheticMassSpectrum = isotopic_cluster_shaper.run();

  if(!m_syntheticMassSpectrum.size())
    {
      qDebug() << "The synthetic mass spectrum has not data point.";
      setCursor(Qt::ArrowCursor);
      return;
    }

  mp_ui->massSpectrumSynthesisComboBox->setItemText(
    (int)MassSpectrumSynthesisActions::COPY_MASS_SPECTRUM_TO_CLIPBOARD,
    "Mass spectrum to clipboard");

  mp_ui->massSpectrumSynthesisComboBox->setItemText(
    (int)MassSpectrumSynthesisActions::SAVE_MASS_SPECTRUM_TO_FILE,
    "Mass spectrum to file");

  setCursor(Qt::ArrowCursor);

  QString trace_title = mp_ui->massSpectrumTitleLineEdit->text();

  if(trace_title.isEmpty())
    trace_title = mp_editorWnd->getSequenceName();

  if(trace_title.isEmpty())
    trace_title = mp_editorWnd->getPolymerSPtr()->getFilePath();

  emit displayMassSpectrumSignal(
    trace_title,
    m_colorByteArray,
    std::make_shared<const pappso::Trace>(m_syntheticMassSpectrum));
}


void
CleavageDlg::keyPressEvent(QKeyEvent *event)
{
  int modifiers = QGuiApplication::keyboardModifiers();

  if(event->key() == Qt::Key_P && modifiers & Qt::ControlModifier)
    {
      // The user wants to append the selected oligomers to the
      // clipboard.

      // qDebug() << __FILE__ << __LINE__
      // << "Ctrl-P";

      mp_ui->exportResultsComboBox->setCurrentIndex(
        (int)ExportResultsActions::EXPORT_TO_CLIPBOARD_APPEND);
      mp_ui->exportResultsComboBox->activated(
        (int)ExportResultsActions::EXPORT_TO_CLIPBOARD_APPEND);
      event->accept();
      return;
    }

  if(event->key() == Qt::Key_O && modifiers & Qt::ControlModifier)
    {
      // The user wants to overwrite the selected oligomers to the clipboard.

      // qDebug() << __FILE__ << __LINE__
      // << "Ctrl-O";

      mp_ui->exportResultsComboBox->setCurrentIndex(
        (int)ExportResultsActions::EXPORT_TO_CLIPBOARD_OVERWRITE);
      mp_ui->exportResultsComboBox->activated(
        (int)ExportResultsActions::EXPORT_TO_CLIPBOARD_OVERWRITE);

      event->accept();
      return;
    }

  if(event->key() == Qt::Key_F && modifiers & Qt::ControlModifier)
    {
      // The user wants to write the selected oligomers to file.

      // qDebug() << __FILE__ << __LINE__
      // << "Ctrl-F";

      mp_ui->exportResultsComboBox->setCurrentIndex(
        (int)ExportResultsActions::EXPORT_TO_FILE);
      mp_ui->exportResultsComboBox->activated(
        (int)ExportResultsActions::EXPORT_TO_FILE);
      event->accept();
      return;
    }

  if(event->key() == Qt::Key_S && modifiers & Qt::ControlModifier)
    {
      // The user wants to select the file in which to write the results.

      // qDebug() << __FILE__ << __LINE__
      // << "Ctrl-S";

      mp_ui->exportResultsComboBox->setCurrentIndex(
        (int)ExportResultsActions::SELECT_FILE);
      mp_ui->exportResultsComboBox->activated(
        (int)ExportResultsActions::SELECT_FILE);
      event->accept();
      return;
    }
}


// The results-exporting functions. ////////////////////////////////
// The results-exporting functions. ////////////////////////////////
// The results-exporting functions. ////////////////////////////////
void
CleavageDlg::showOutputPatternDefinitionHelp()
{
  QMessageBox msgBox;
  QString msg(
    "The following special variables are available to craft a formatting "
    "string\n\n");
  msg += "%f : filename\n";
  msg += "%i : sequence name (identity)\n";
  msg += "%n : oligomer name\n";
  msg += "%s : oligomer sequence\n";
  msg +=
    "%l : elided sequence, like \"LEFT...RIGH\" (first 4 left...4 last "
    "right)\n";
  msg += "%c : oligomer index_range\n";
  msg += "%o : 0 if oligomer is not modified, 1 if it is\n";
  msg += "%p : partial cleavage\n";
  msg += "%e : cleavage agent name\n";
  msg += "%m : mono m/z\n";
  msg += "%a : avg m/z\n";
  msg += "%z : charge\n";

  msg += "\nNote that any text can be interspersed in the format string.\n";
  msg +=
    "If a '\\' is required in the output, it must be escaped like this: "
    "'\\\\'\n";
  msg +=
    "If a '%' is required in the output, it must be escaped like this: "
    "'\\%'\n";
  msg +=
    "If a newline (carriage return) is required in the output, write '\\n'\n";

  msg += "\nExample:\n\n";
  msg += "%e$%n$%s%m$%p\\n\n";
  msg += "will yield the following string:\n";
  msg += "Trypsin$0#20#z=1$SEQENCE$12458.2354$0\n";

  msgBox.setText(msg);

  msgBox.exec();
}


void
CleavageDlg::exportResults(int index)
{
  // Remember that we had set up the combobox with the following strings:
  // << tr("To Clipboard (overwrite)")
  // << tr("To Clipboard (append)")
  // << tr("To File")
  // << tr("Select File");

  if(index == 0)
    {
      exportResultsToClipboard(false /*!append*/);
    }
  else if(index == 1)
    {
      exportResultsToClipboard(true /*append*/);
    }
  else if(index == 2)
    {
      exportResultsToFile();
    }
  else if(index == 3)
    {
      selectResultsFile();
    }
  else
    Q_ASSERT(0);
}


void
CleavageDlg::prepareResultsTxtString(bool append)
{
  // Set the delimiter to the char/string that is in the
  // corresponding text line edit widget.

  QString delimiter = mp_ui->delimiterLineEdit->text();
  // If delimiter is empty, the function that will prepare the
  // string will put the default character, that is '$'.

  mpa_resultsString->clear();

  // We only put the header info if the user does not want to have
  // the data formatted for XpertMiner, which only can get the data
  // in the format :
  //
  // mass <delim> charge <delim> name <delim> coords
  //
  // Note that name and coords are optional.
  bool forXpertMiner = false;
  bool noHeader      = false;

  forXpertMiner = mp_ui->forXpertMinerCheckBox->isChecked();
  noHeader      = mp_ui->noHeaderCheckBox->isChecked();

  if(!noHeader && !forXpertMiner && !append)
    {
      *mpa_resultsString += QString(
                              "# \n"
                              "# ---------------------------\n"
                              "# Polymer sequence cleavage: %1\n"
                              "# ---------------------------\n")
                              .arg(msp_polymer->getName());
    }

  // Then, we should ask whether the masses to be exported are
  // the mono or avg masses.

  libXpertMassCore::Enums::MassType mass_type;
  if(mp_ui->monoRadioButton->isChecked())
    mass_type = libXpertMassCore::Enums::MassType::MONO;
  else
    mass_type = libXpertMassCore::Enums::MassType::AVG;

  bool withSequence = mp_ui->withSequenceCheckBox->isChecked();

  QString pattern = mp_ui->outputPatternComboBox->currentText();
  if(pattern == "Output pattern")
    pattern.clear();

  QString *text =
    mp_ui->oligomerTableView->describeSelectedOligomersAsPlainText(
      pattern, delimiter, withSequence, forXpertMiner, mass_type);
  *mpa_resultsString += *text;

  delete text;
}


bool
CleavageDlg::exportResultsToClipboard(bool append)
{
  // Below, if append is true, the prepared string will not start with the
  // usual stanza.
  prepareResultsTxtString(append);

  QClipboard *clipboard = QApplication::clipboard();

  // Depending on the fact that we are appending or not, we need to first
  // get the text from the clipboard or not.
  if(append)
    {
      QString clipboardText = clipboard->text(QClipboard::Clipboard);
      clipboardText.append(*mpa_resultsString);
      clipboard->setText(clipboardText, QClipboard::Clipboard);
    }
  else
    {
      clipboard->setText(*mpa_resultsString, QClipboard::Clipboard);
    }

  return true;
}


bool
CleavageDlg::exportResultsToFile()
{
  if(m_resultsFilePath.isEmpty())
    {
      if(!selectResultsFile())
        return false;
    }

  QFile file(m_resultsFilePath);

  if(!file.open(QIODevice::WriteOnly | QIODevice::Append))
    {
      QMessageBox::information(this,
                               tr("MassXpert3 - Export Data"),
                               tr("Failed to open file in append mode."),
                               QMessageBox::Ok);
      return false;
    }

  QTextStream stream(&file);
  stream.setEncoding(QStringConverter::Utf8);

  prepareResultsTxtString();

  stream << *mpa_resultsString;

  file.close();

  return true;
}


bool
CleavageDlg::selectResultsFile()
{
  m_resultsFilePath =
    QFileDialog::getSaveFileName(this,
                                 tr("Select file to export data to"),
                                 QDir::homePath(),
                                 tr("Data files(*.dat *.DAT)"));

  if(m_resultsFilePath.isEmpty())
    return false;

  return true;
}


//////////////////////////////////// The results-exporting functions.
//////////////////////////////////// The results-exporting functions.
//////////////////////////////////// The results-exporting functions.

} // namespace MassXpert
} // namespace MsXpS
