Skip to content
Snippets Groups Projects
UserFunctionDialog.cpp 15.6 KiB
Newer Older
#include "MantidQtMantidWidgets/UserFunctionDialog.h"
#include "MantidQtMantidWidgets/RenameParDialog.h"
#include "MantidAPI/Expression.h"
#include "MantidKernel/ConfigService.h"

#include <QComboBox>
#include <QStringListModel>
#include <QDialogButtonBox>
#include <QMessageBox>
#include <QKeyEvent>
#include <QFile>
#include <QTextStream>

#include <algorithm>

using namespace MantidQt::MantidWidgets;

UserFunctionDialog::UserFunctionDialog(QWidget *parent,const QString& formula)
:QDialog(parent)
{
  m_uiForm.setupUi(this);
  connect(m_uiForm.lstCategory,SIGNAL(currentTextChanged(const QString&)),this,SLOT(selectCategory(const QString&)));
  connect(m_uiForm.lstFunction,SIGNAL(currentTextChanged(const QString&)),this,SLOT(selectFunction(const QString&)));
  connect(m_uiForm.btnSave,SIGNAL(clicked()),this,SLOT(saveFunction()));
  connect(m_uiForm.btnRemove,SIGNAL(clicked()),this,SLOT(removeCurrentFunction()));
  connect(m_uiForm.btnAdd,SIGNAL(clicked()),this,SLOT(addExpression()));
  connect(m_uiForm.btnUse,SIGNAL(clicked()),this,SLOT(accept()));
  connect(m_uiForm.btnCancel,SIGNAL(clicked()),this,SLOT(reject()));
  connect(m_uiForm.teUserFunction,SIGNAL(textChanged()),this,SLOT(updateFunction()));
  m_uiForm.teUserFunction->installEventFilter(this);

  loadFunctions();
  updateCategories();
  if ( !formula.isEmpty() )
  {
    QRect rect = m_uiForm.teUserFunction->cursorRect();
    QTextCursor cursor = m_uiForm.teUserFunction->cursorForPosition(rect.topLeft());
    cursor.insertText(formula);
    //updateFunction();
  }
}

/**
 * Write saved functions in the destructor
 */
UserFunctionDialog::~UserFunctionDialog()
{
  QFile funFile(QString::fromStdString (Mantid::Kernel::ConfigService::Instance().getBaseDir()) + "Mantid.user.functions");
  if (funFile.open(QIODevice::WriteOnly | QIODevice::Text))
  {
    QMap<QString,QString>::const_iterator it = m_funs.begin();
    for(; it != m_funs.end(); ++it)
    {
      QTextStream out(&funFile);
      QStringList cn = it.key().split('.');
      if (cn[0] != "Base" && cn[0] != "Built-in")
      {
        out << it.key() << "=" << it.value() <<'\n';
      }
    }
  }
}

/**
 * Load saved functions form Mantid(.user).properties file.
 * Property: userfunctions.CategoryName.FunctionName = Expression-in-Mu::Parser-format
 */
void UserFunctionDialog::loadFunctions()
{
  // define the built-in functions
  setFunction ("Base","abs","abs(x)","Absolute value of x");
  setFunction ("Base","sin","sin(x)","Sine of x");
  setFunction ("Base","cos","cos(x)","Cosine of x");
  setFunction ("Base","tan","tan(x)","Tangent of x");
  setFunction ("Base","asin","asin(x)","Arc-sine of x");
  setFunction ("Base","acos","acos(x)","Arc-cosine of x");
  setFunction ("Base","atan","atan(x)","Arc-tangent of x");
  setFunction ("Base","sinh","sinh(x)","Sine hyperbolic of x");
  setFunction ("Base","cosh","cosh(x)","Cosine hyperbolic of x");
  setFunction ("Base","tanh","tanh(x)","Tangent hyperbolic of x");
  setFunction ("Base","asinh","asinh(x)","Arc-sine hyperbolic of x");
  setFunction ("Base","acosh","acosh(x)","Arc-cosine hyperbolic of x");
  setFunction ("Base","atanh","atanh(x)","Arc-tangent hyperbolic of x");
  setFunction ("Base","log2","log2(x)","Logarithm to the base 2");
  setFunction ("Base","log10","log10(x)","Logarithm to the base 10");
  setFunction ("Base","log","log(x)","Logarithm to the base 10");
  setFunction ("Base","ln","ln(x)","Logarithm to the base e = 2.71828...");
  setFunction ("Base","exp","exp(x)","e to the power of x");
  setFunction ("Base","sqrt","sqrt(x)","Sqare root of x");
  setFunction ("Base","sign","sign(x)","Sign of x");
  setFunction ("Base","rint","rint(x)","Round to nearest integer");
  setFunction ("Built-in","Gauss","h*exp(-s*(x-c)^2)");
  setFunction ("Built-in","ExpDecay","h*exp(-x/t)");
  QFile funFile(QString::fromStdString (Mantid::Kernel::ConfigService::Instance().getBaseDir()) + "Mantid.user.functions");
  if (funFile.exists() && funFile.open(QIODevice::ReadOnly | QIODevice::Text))
  {
    QTextStream in(&funFile);
    while (!in.atEnd()) {
      QString line = in.readLine();
      QStringList key_value = line.split('=');
      if (key_value.size() != 2) continue;
      m_funs.insert (key_value[0].trimmed(),key_value[1].trimmed());
    }
  }
}

/**
 * Update the GUI element displaying categories.
 */
void UserFunctionDialog::updateCategories()
{
  m_uiForm.lstCategory->clear();
  QSet<QString> cats = names();
  foreach(QString cat ,cats)
  {
    m_uiForm.lstCategory->addItem(cat);
  }
}

/**
 * Make a category current
 * @param cat The category to select
 */
void UserFunctionDialog::selectCategory(const QString& cat)
{
  QSet<QString> funs = names(cat);
  m_uiForm.lstFunction->clear();
    QString value = getFunction(cat,fun);
    if ( !value.isEmpty() )
    {
      m_uiForm.lstFunction->addItem(fun);
    }
  }
  if (m_uiForm.lstFunction->count() > 0)
  {
    m_uiForm.lstFunction->sortItems();
    m_uiForm.lstFunction->setCurrentRow(0);
  }
  else
  {
    m_uiForm.teExpression->clear();
  }
  m_uiForm.btnRemove->setEnabled (!isBuiltin(cat));
}

/**
 * Make a function current
 * @param fun The function to select
 */
void UserFunctionDialog::selectFunction(const QString& fun)
{
  QString cat = m_uiForm.lstCategory->currentItem()->text();
  m_uiForm.teExpression->clear();

  QString value = getFunction(cat,fun);
  QString comment = getComment(cat,fun);
  if ( !comment.isEmpty() )
  {
    value += "\n\n" + comment;
  }

  m_uiForm.teExpression->setText(value);
}

/**
 * Add selected expression to the user function
 */
void UserFunctionDialog::addExpression()
{
  QString expr = m_uiForm.teExpression->toPlainText();
  int iBr = expr.indexOf('\n');
  if (iBr > 0)
  {
    expr.remove(iBr,expr.size());
  }

  checkParameters(expr);

  QRect rect = m_uiForm.teUserFunction->cursorRect();
  QTextCursor cursor = m_uiForm.teUserFunction->cursorForPosition(rect.topLeft());
  if (cursor.position() > 0)
  {
    expr.prepend('+');
  }
  cursor.insertText(expr);

}

/**
 * Check an expression for name clashes with user function
 * @param expr An expression prepared to be added to the user function.
 */
void UserFunctionDialog::checkParameters(QString& expr)
{
  if (expr.isEmpty()) return;
  QString fun = m_uiForm.teUserFunction->toPlainText();
  if (fun.isEmpty()) return;

  // collect parameter names in sets vars1 and vars2
  Mantid::API::Expression e1;
  Mantid::API::Expression e2;
  try
  {
  e1.parse(fun.toStdString());
  e2.parse(expr.toStdString());
  std::set<std::string> vars1 = e1.getVariables();
  std::set<std::string> vars2 = e2.getVariables();
  vars1.erase("x");
  vars2.erase("x");

  // combine all names frm the two sets
  std::vector<std::string> all(vars1.size()+vars2.size(),"");
  std::set_union(vars1.begin(),vars1.end(),vars2.begin(),vars2.end(),all.begin());
  std::vector<std::string>::iterator it = std::find(all.begin(),all.end(),"");
  if (it != all.end())
  {
    all.erase(it,all.end());
  }

  // compare variable names and collect common names
  std::vector<std::string> common(std::min<int>(vars1.size(), vars2.size()),"");
  std::set_intersection(vars1.begin(),vars1.end(),vars2.begin(),vars2.end(),common.begin());
  it = std::find(common.begin(),common.end(),"");
  if (it != common.end())
  {
    common.erase(it,common.end());
  }

  // ask the user to rename the common names
  if ( !common.empty() )
  {
    RenameParDialog dlg(all,common);
    if (dlg.exec() == QDialog::Accepted)
    {
      std::vector<std::string> vars_new;
      dlg.setOutput(vars_new);
      std::vector<std::string>::const_iterator v_old = common.begin();
      std::vector<std::string>::const_iterator v_new = vars_new.begin();
      for(; v_old != common.end(); ++v_old,++v_new)
        e2.renameAll(*v_old,*v_new);
        expr = QString::fromStdString(e2.str());
    }
  }

}

/**
 * Updates the parameter list
 */
void UserFunctionDialog::updateFunction()
{
  QString fun = m_uiForm.teUserFunction->toPlainText();
  Mantid::API::Expression e;
  try
  {
    e.parse(fun.toStdString());
  }
  catch(...)
  {// the formula could be being edited manually
    return;
  }
  std::set<std::string> vars = e.getVariables();
  vars.erase("x");
  QString params;
  for(std::set<std::string>::iterator it=vars.begin();it!=vars.end();++it)
  {
    if (it != vars.begin())
    {
      params += ",";
    }
    params += QString::fromStdString(*it);
  }
  m_uiForm.leParams->setText(params);

}

/**
 * Returns function names: If the input cat parameter is empty the returned set
 * contains funtion categories, otherwise it returns function names in category cat.
 * @param cat The category for which functions will be returned.
 * @return A set of funtion names.
 */
QSet<QString> UserFunctionDialog::names(const QString& cat)const
{
  QSet<QString> out;
  if (cat.isEmpty())
  {
    QMap<QString,QString>::const_iterator it = m_funs.begin();
    for(; it != m_funs.end(); ++it)
    {
      QStringList cn = it.key().split('.');
      out.insert(cn[0]);
    }
  }
  else
  {
    QMap<QString,QString>::const_iterator it = m_funs.begin();
    for(; it != m_funs.end(); ++it)
    {
      QStringList cn = it.key().split('.');
      if (cn[0] == cat)
      {
        out.insert(cn[1]);
      }
    }
  }
  return out;
}

/**
 * Save the constructed function for future use
 */
void UserFunctionDialog::saveFunction()
{
  QString cur_category = m_uiForm.lstCategory->currentItem()->text();
  if (cur_category == "Base" || cur_category == "Built-in")
  {
    cur_category = "";
  }
  
  InputFunctionNameDialog* dlg = new InputFunctionNameDialog(this,cur_category);
  if (dlg->exec() == QDialog::Accepted)
  {
    QString cat;
    QString fun;
    QString comment;
    dlg->getFunctionName(cat,fun,comment);
    if (fun.isEmpty())
    {
      QMessageBox::critical(this,"Mantid - Error","The function name is empty");
      return;
    }
    // check if the category already exists
    QList<QListWidgetItem*> items = m_uiForm.lstCategory->findItems(cat,Qt::MatchExactly);
    if ( !items.isEmpty() )
    {// check if a function with this name already exists
      const QSet<QString> functions = names(cat);
      QSet<QString>::const_iterator found = functions.find(fun);
      if (found != functions.end() &&
        QMessageBox::question(this,"Mantid","A function with name "+fun+" already exists in category "+cat+".\n"
          "Would you like to replace it?",QMessageBox::Yes|QMessageBox::No) == QMessageBox::No)
      {
        return;
      }
    }
    QString expr = m_uiForm.teUserFunction->toPlainText();
    setFunction(cat,fun,expr,comment);
    updateCategories();
  }//QDialog::Accepted
}

/**
 * Remove the current function
 */
void UserFunctionDialog::removeCurrentFunction()
{
  QString cat = m_uiForm.lstCategory->currentItem()->text();
  if (isBuiltin(cat))
  {
    return;
  }
  QString fun = m_uiForm.lstFunction->currentItem()->text();
  if (QMessageBox::question(this,"Mantid","Are you sure you want to remove function " + fun + "?"
    ,QMessageBox::Yes|QMessageBox::No) == QMessageBox::Yes)
  {
    QString fun_key = cat + "." + fun;
    QMap<QString,QString>::iterator it = m_funs.find(fun_key);
    if (it != m_funs.end())
    {
      m_funs.erase(it);
      it = m_funs.find(fun_key + ".comment");
      if (it != m_funs.end())
      {
        m_funs.erase(it);
      }
    }
  }
  selectCategory(cat);
}

QStringList UserFunctionDialog::categories()const
{
  QStringList out;
  for(int i = 0; i < m_uiForm.lstCategory->count(); ++i)
  {
    out << m_uiForm.lstCategory->item(i)->text();
  }
  return out;
}

bool UserFunctionDialog::eventFilter(QObject *obj, QEvent *ev)
{
  if (ev->type() == QEvent::KeyPress) {
    QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev);
    if ( keyEvent->key() == Qt::Key_Return) 
    {
      return true;
    }
  }

  // standard event processing
  return QObject::eventFilter(obj, ev);
}

/**
 * Get the expression for saved function in category cat with name fun. If any of the
 * arguments are empty string or function does not exist return empty string.
 * @param cat The category
 * @param fun The name of the function
 * @return An expression that can be used as mu::Parser formula
 */
QString UserFunctionDialog::getFunction(const QString& cat,const QString& fun)const
{
  if (cat.isEmpty() || fun.isEmpty()) return "";
  QMap<QString,QString>::const_iterator it = m_funs.find(cat + "." + fun);
  if (it != m_funs.end()) return it.value();
  return "";
}

/**
 * Get the comment for saved function in category cat with name fun. If any of the
 * arguments are empty string or function does not exist return empty string.
 * @param cat The category
 * @param fun The name of the function
 */
QString UserFunctionDialog::getComment(const QString& cat,const QString& fun)const
{
  if (cat.isEmpty() || fun.isEmpty()) return "";
  QMap<QString,QString>::const_iterator it = m_funs.find(cat + "." + fun + ".comment");
  if (it != m_funs.end()) return it.value();
  return "";
}

/**
 * Set an expression to a new function in category cat and with name fun. If any of the
 * arguments are empty string does nothing.
 * @param cat The category
 * @param fun The name of the function
 * @param expr The expression
 * @param comment The comment
 */
void UserFunctionDialog::setFunction(const QString& cat,const QString& fun,const QString& expr,const QString& comment)
{
  if (cat.isEmpty() || fun.isEmpty() || expr.isEmpty()) return;
  //if (cat == "Base" || cat == "Built-in") return;
  QString fun_key = cat + "." + fun;
  m_funs[fun_key] = expr;
  QString cmnt_key = fun_key + ".comment";
  if (!comment.isEmpty())
  {
    m_funs[cmnt_key] = comment;
  }
  else
  {
    QMap<QString,QString>::iterator it = m_funs.find(cmnt_key);
    if (it != m_funs.end())
    {
      m_funs.erase(it);
    }
  }
}

/**
 * Checks if a category is a buil-in one and cannot be changed
 */
bool UserFunctionDialog::isBuiltin(const QString& cat)const
{
  return cat == "Base" || cat == "Built-in";
}

/**
 * Constructor
 * @param category The initial suggestion for the category 
 */
InputFunctionNameDialog::InputFunctionNameDialog(QWidget *parent,const QString& category)
:QDialog(parent)
{
  QVBoxLayout* layout = new QVBoxLayout();
  layout->addWidget(new QLabel("Enter new or select a category"));
  QStringList cats = ((UserFunctionDialog*)parent)->categories();
  cats.removeOne("Base");
  cats.removeOne("Built-in");
  m_category = new QComboBox();
  m_category->addItems(cats);
  m_category->setEditable(true);
  int index = m_category->findText(category);
  if (index >= 0)
  {
    m_category->setCurrentIndex(index);
  }
  layout->addWidget(m_category);
  connect(m_category,SIGNAL(currentIndexChanged(const QString&)),parent,SLOT(selectCategory(const QString&)));
  layout->addWidget(new QLabel("Enter a name for the new function"));
  m_name = new QLineEdit();
  layout->addWidget(m_name);
  layout->addWidget(new QLabel("Enter a comment"));
  m_comment = new QTextEdit();
  layout->addWidget(m_comment);

  QDialogButtonBox* buttons = new QDialogButtonBox();
  buttons->addButton("OK",QDialogButtonBox::AcceptRole);
  buttons->addButton("Cancel",QDialogButtonBox::RejectRole);
  buttons->setCenterButtons(true);
  connect(buttons,SIGNAL(accepted()),this,SLOT(accept()));
  connect(buttons,SIGNAL(rejected()),this,SLOT(reject()));
  layout->addWidget(buttons);
  setLayout(layout);
}

/**
 * Return the entered category and function name and comment
 * @param category A string to recieve the category
 * @param name A string to recieve the function name
 */
void InputFunctionNameDialog::getFunctionName(QString& category,QString& name,QString& comment)
{
  category = m_category->currentText();
  name = m_name->text();
  comment = m_comment->toPlainText();