diff options
Diffstat (limited to 'SQLiteStudio3/guiSQLiteStudio/windows/functionseditor.cpp')
| -rw-r--r-- | SQLiteStudio3/guiSQLiteStudio/windows/functionseditor.cpp | 632 |
1 files changed, 632 insertions, 0 deletions
diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/functionseditor.cpp b/SQLiteStudio3/guiSQLiteStudio/windows/functionseditor.cpp new file mode 100644 index 0000000..29163e3 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/functionseditor.cpp @@ -0,0 +1,632 @@ +#include "functionseditor.h" +#include "ui_functionseditor.h" +#include "common/unused.h" +#include "common/utils.h" +#include "uiutils.h" +#include "functionseditormodel.h" +#include "services/pluginmanager.h" +#include "dbtree/dbtree.h" +#include "dbtree/dbtreemodel.h" +#include "dbtree/dbtreeitem.h" +#include "iconmanager.h" +#include "syntaxhighlighterplugin.h" +#include "sqlitesyntaxhighlighter.h" +#include "plugins/scriptingplugin.h" +#include "common/userinputfilter.h" +#include "selectabledbmodel.h" +#include "uiconfig.h" +#include <QDebug> +#include <QDesktopServices> +#include <QStyleFactory> + +// TODO handle plugin loading/unloading to update editor state + +FunctionsEditor::FunctionsEditor(QWidget *parent) : + MdiChild(parent), + ui(new Ui::FunctionsEditor) +{ + init(); +} + +FunctionsEditor::~FunctionsEditor() +{ + delete ui; +} + +bool FunctionsEditor::restoreSessionNextTime() +{ + return false; +} + +bool FunctionsEditor::restoreSession(const QVariant &sessionValue) +{ + UNUSED(sessionValue); + return true; +} + +Icon* FunctionsEditor::getIconNameForMdiWindow() +{ + return ICONS.FUNCTION; +} + +QString FunctionsEditor::getTitleForMdiWindow() +{ + return tr("SQL function editor"); +} + +void FunctionsEditor::createActions() +{ + createAction(COMMIT, ICONS.COMMIT, tr("Commit all function changes"), this, SLOT(commit()), ui->toolBar); + createAction(ROLLBACK, ICONS.ROLLBACK, tr("Rollback all function changes"), this, SLOT(rollback()), ui->toolBar); + ui->toolBar->addSeparator(); + createAction(ADD, ICONS.NEW_FUNCTION, tr("Create new function"), this, SLOT(newFunction()), ui->toolBar); + createAction(DELETE, ICONS.DELETE_FUNCTION, tr("Delete selected function"), this, SLOT(deleteFunction()), ui->toolBar); + ui->toolBar->addSeparator(); + createAction(HELP, ICONS.HELP, tr("Custom SQL functions manual"), this, SLOT(help()), ui->toolBar); + + // Args toolbar + createAction(ARG_ADD, ICONS.INSERT_FN_ARG, tr("Add function argument"), this, SLOT(addFunctionArg()), ui->argsToolBar); + createAction(ARG_EDIT, ICONS.RENAME_FN_ARG, tr("Rename function argument"), this, SLOT(editFunctionArg()), ui->argsToolBar); + createAction(ARG_DEL, ICONS.DELETE_FN_ARG, tr("Delete function argument"), this, SLOT(delFunctionArg()), ui->argsToolBar); + ui->argsToolBar->addSeparator(); + createAction(ARG_MOVE_UP, ICONS.MOVE_UP, tr("Move function argument up"), this, SLOT(moveFunctionArgUp()), ui->argsToolBar); + createAction(ARG_MOVE_DOWN, ICONS.MOVE_DOWN, tr("Move function argument down"), this, SLOT(moveFunctionArgDown()), ui->argsToolBar); + +#ifdef Q_OS_MACX + QStyle *fusion = QStyleFactory::create("Fusion"); + ui->toolBar->setStyle(fusion); + ui->argsToolBar->setStyle(fusion); +#endif +} + +void FunctionsEditor::setupDefShortcuts() +{ +} + +QToolBar* FunctionsEditor::getToolBar(int toolbar) const +{ + UNUSED(toolbar); + return ui->toolBar; +} + +void FunctionsEditor::init() +{ + ui->setupUi(this); + clearEdits(); + ui->initCodeGroup->setVisible(false); + ui->finalCodeGroup->setVisible(false); + + setFont(CFG_UI.Fonts.SqlEditor.get()); + + model = new FunctionsEditorModel(this); + functionFilterModel = new QSortFilterProxyModel(this); + functionFilterModel->setSourceModel(model); + ui->list->setModel(functionFilterModel); + + dbListModel = new SelectableDbModel(this); + dbListModel->setSourceModel(DBTREE->getModel()); + ui->databasesList->setModel(dbListModel); + ui->databasesList->expandAll(); + + ui->typeCombo->addItem(tr("Scalar"), FunctionManager::ScriptFunction::SCALAR); + ui->typeCombo->addItem(tr("Aggregate"), FunctionManager::ScriptFunction::AGGREGATE); + + new UserInputFilter(ui->functionFilterEdit, this, SLOT(applyFilter(QString))); + functionFilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + + initActions(); + + connect(ui->list->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(functionSelected(QItemSelection,QItemSelection))); + connect(ui->list->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(updateState())); + connect(ui->initCodeEdit, SIGNAL(textChanged()), this, SLOT(updateModified())); + connect(ui->mainCodeEdit, SIGNAL(textChanged()), this, SLOT(updateModified())); + connect(ui->finalCodeEdit, SIGNAL(textChanged()), this, SLOT(updateModified())); + connect(ui->nameEdit, SIGNAL(textChanged(QString)), this, SLOT(updateModified())); + connect(ui->undefArgsCheck, SIGNAL(clicked()), this, SLOT(updateModified())); + connect(ui->allDatabasesRadio, SIGNAL(clicked()), this, SLOT(updateModified())); + connect(ui->selDatabasesRadio, SIGNAL(clicked()), this, SLOT(updateModified())); + connect(ui->langCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(updateModified())); + connect(ui->typeCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(updateModified())); + + connect(ui->argsList->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(updateArgsState())); + connect(ui->argsList->model(), SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), this, SLOT(updateModified())); + connect(ui->argsList->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(updateModified())); + connect(ui->argsList->model(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(updateModified())); + connect(ui->argsList->model(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(updateModified())); + + connect(dbListModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(updateModified())); + connect(CFG_UI.Fonts.SqlEditor, SIGNAL(changed(QVariant)), this, SLOT(changeFont(QVariant))); + + model->setData(FUNCTIONS->getAllScriptFunctions()); + + // Language plugins + foreach (ScriptingPlugin* plugin, PLUGINS->getLoadedPlugins<ScriptingPlugin>()) + scriptingPlugins[plugin->getLanguage()] = plugin; + + ui->langCombo->addItems(scriptingPlugins.keys()); + + // Syntax highlighting plugins + foreach (SyntaxHighlighterPlugin* plugin, PLUGINS->getLoadedPlugins<SyntaxHighlighterPlugin>()) + highlighterPlugins[plugin->getLanguageName()] = plugin; + + updateState(); +} + +int FunctionsEditor::getCurrentFunctionRow() const +{ + QModelIndexList idxList = ui->list->selectionModel()->selectedIndexes(); + if (idxList.size() == 0) + return -1; + + return idxList.first().row(); +} + +void FunctionsEditor::functionDeselected(int row) +{ + model->setName(row, ui->nameEdit->text()); + model->setLang(row, ui->langCombo->currentText()); + model->setType(row, getCurrentFunctionType()); + model->setUndefinedArgs(row, ui->undefArgsCheck->isChecked()); + model->setAllDatabases(row, ui->allDatabasesRadio->isChecked()); + model->setCode(row, ui->mainCodeEdit->toPlainText()); + model->setModified(row, currentModified); + + if (model->isAggregate(row)) + { + model->setInitCode(row, ui->initCodeEdit->toPlainText()); + model->setFinalCode(row, ui->finalCodeEdit->toPlainText()); + } + else + { + model->setInitCode(row, QString::null); + model->setFinalCode(row, QString::null); + } + + if (!ui->undefArgsCheck->isChecked()) + model->setArguments(row, getCurrentArgList()); + + if (ui->selDatabasesRadio->isChecked()) + model->setDatabases(row, getCurrentDatabases()); + + model->validateNames(); +} + +void FunctionsEditor::functionSelected(int row) +{ + updatesForSelection = true; + ui->nameEdit->setText(model->getName(row)); + ui->initCodeEdit->setPlainText(model->getInitCode(row)); + ui->mainCodeEdit->setPlainText(model->getCode(row)); + ui->finalCodeEdit->setPlainText(model->getFinalCode(row)); + ui->undefArgsCheck->setChecked(model->getUndefinedArgs(row)); + ui->langCombo->setCurrentText(model->getLang(row)); + + // Arguments + ui->argsList->clear(); + QListWidgetItem* item = nullptr; + foreach (const QString& arg, model->getArguments(row)) + { + item = new QListWidgetItem(arg); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->argsList->addItem(item); + } + + // Databases + dbListModel->setDatabases(model->getDatabases(row)); + ui->databasesList->expandAll(); + + if (model->getAllDatabases(row)) + ui->allDatabasesRadio->setChecked(true); + else + ui->selDatabasesRadio->setChecked(true); + + // Type + FunctionManager::ScriptFunction::Type type = model->getType(row); + for (int i = 0; i < ui->typeCombo->count(); i++) + { + if (ui->typeCombo->itemData(i).toInt() == type) + { + ui->typeCombo->setCurrentIndex(i); + break; + } + } + + updatesForSelection = false; + currentModified = false; + + updateCurrentFunctionState(); +} + +void FunctionsEditor::clearEdits() +{ + ui->nameEdit->setText(QString::null); + ui->mainCodeEdit->setPlainText(QString::null); + ui->langCombo->setCurrentText(QString::null); + ui->undefArgsCheck->setChecked(true); + ui->argsList->clear(); + ui->allDatabasesRadio->setChecked(true); + ui->typeCombo->setCurrentIndex(0); + ui->langCombo->setCurrentIndex(-1); +} + +void FunctionsEditor::selectFunction(int row) +{ + if (!model->isValidRowIndex(row)) + return; + + ui->list->selectionModel()->setCurrentIndex(model->index(row), QItemSelectionModel::Clear|QItemSelectionModel::SelectCurrent); +} + +void FunctionsEditor::setFont(const QFont& font) +{ + ui->initCodeEdit->setFont(font); + ui->mainCodeEdit->setFont(font); + ui->finalCodeEdit->setFont(font); +} + +QModelIndex FunctionsEditor::getSelectedArg() const +{ + QModelIndexList indexes = ui->argsList->selectionModel()->selectedIndexes(); + if (indexes.size() == 0 || !indexes.first().isValid()) + return QModelIndex(); + + return indexes.first(); + +} + +QStringList FunctionsEditor::getCurrentArgList() const +{ + QStringList currArgList; + for (int row = 0; row < ui->argsList->model()->rowCount(); row++) + currArgList << ui->argsList->item(row)->text(); + + return currArgList; +} + +QStringList FunctionsEditor::getCurrentDatabases() const +{ + return dbListModel->getDatabases(); +} + +FunctionManager::ScriptFunction::Type FunctionsEditor::getCurrentFunctionType() const +{ + int intValue = ui->typeCombo->itemData(ui->typeCombo->currentIndex()).toInt(); + return static_cast<FunctionManager::ScriptFunction::Type>(intValue); +} + +void FunctionsEditor::commit() +{ + int row = getCurrentFunctionRow(); + if (model->isValidRowIndex(row)) + functionDeselected(row); + + QList<FunctionManager::ScriptFunction*> functions = model->generateFunctions(); + + FUNCTIONS->setScriptFunctions(functions); + model->clearModified(); + currentModified = false; + + if (model->isValidRowIndex(row)) + selectFunction(row); + + updateState(); +} + +void FunctionsEditor::rollback() +{ + int selectedBefore = getCurrentFunctionRow(); + + model->setData(FUNCTIONS->getAllScriptFunctions()); + currentModified = false; + clearEdits(); + + if (model->isValidRowIndex(selectedBefore)) + selectFunction(selectedBefore); + + updateState(); +} + +void FunctionsEditor::newFunction() +{ + if (ui->langCombo->currentIndex() == -1 && ui->langCombo->count() > 0) + ui->langCombo->setCurrentIndex(0); + + FunctionManager::ScriptFunction* func = new FunctionManager::ScriptFunction(); + func->name = generateUniqueName("function", model->getFunctionNames()); + + if (ui->langCombo->currentIndex() > -1) + func->lang = ui->langCombo->currentText(); + + model->addFunction(func); + + selectFunction(model->rowCount() - 1); +} + +void FunctionsEditor::deleteFunction() +{ + int row = getCurrentFunctionRow(); + model->deleteFunction(row); + clearEdits(); + + row = getCurrentFunctionRow(); + if (model->isValidRowIndex(row)) + functionSelected(row); + + updateState(); +} + +void FunctionsEditor::updateModified() +{ + if (updatesForSelection) + return; + + int row = getCurrentFunctionRow(); + if (model->isValidRowIndex(row)) + { + bool nameDiff = model->getName(row) != ui->nameEdit->text(); + bool codeDiff = model->getCode(row) != ui->mainCodeEdit->toPlainText(); + bool initCodeDiff = model->getInitCode(row) != ui->initCodeEdit->toPlainText(); + bool finalCodeDiff = model->getFinalCode(row) != ui->finalCodeEdit->toPlainText(); + bool langDiff = model->getLang(row) != ui->langCombo->currentText(); + bool undefArgsDiff = model->getUndefinedArgs(row) != ui->undefArgsCheck->isChecked(); + bool allDatabasesDiff = model->getAllDatabases(row) != ui->allDatabasesRadio->isChecked(); + bool argDiff = getCurrentArgList() != model->getArguments(row); + bool dbDiff = getCurrentDatabases().toSet() != model->getDatabases(row).toSet(); // QSet to ignore order + bool typeDiff = model->getType(row) != getCurrentFunctionType(); + + currentModified = (nameDiff || codeDiff || typeDiff || langDiff || undefArgsDiff || allDatabasesDiff || argDiff || dbDiff || + initCodeDiff || finalCodeDiff); + } + + updateCurrentFunctionState(); +} + +void FunctionsEditor::updateState() +{ + bool modified = model->isModified() || currentModified; + bool valid = model->isValid(); + + actionMap[COMMIT]->setEnabled(modified && valid); + actionMap[ROLLBACK]->setEnabled(modified); + actionMap[DELETE]->setEnabled(ui->list->selectionModel()->selectedIndexes().size() > 0); +} + +void FunctionsEditor::updateCurrentFunctionState() +{ + int row = getCurrentFunctionRow(); + bool validRow = model->isValidRowIndex(row); + ui->rightWidget->setEnabled(validRow); + if (!validRow) + { + setValidState(ui->langCombo, true); + setValidState(ui->nameEdit, true); + setValidState(ui->mainCodeEdit, true); + setValidState(ui->finalCodeEdit, true); + return; + } + + QString name = ui->nameEdit->text(); + bool nameOk = model->isAllowedName(row, name) && !name.trimmed().isEmpty(); + setValidState(ui->nameEdit, nameOk, tr("Enter a non-empty, unique name of the function.")); + + bool langOk = ui->langCombo->currentIndex() >= 0; + ui->initCodeGroup->setEnabled(langOk); + ui->mainCodeGroup->setEnabled(langOk); + ui->finalCodeGroup->setEnabled(langOk); + ui->argsGroup->setEnabled(langOk); + ui->databasesGroup->setEnabled(langOk); + ui->nameEdit->setEnabled(langOk); + ui->nameLabel->setEnabled(langOk); + ui->typeCombo->setEnabled(langOk); + ui->typeLabel->setEnabled(langOk); + setValidState(ui->langCombo, langOk, tr("Pick the implementation language.")); + + bool aggregate = getCurrentFunctionType() == FunctionManager::ScriptFunction::AGGREGATE; + ui->initCodeGroup->setVisible(aggregate); + ui->mainCodeGroup->setTitle(aggregate ? tr("Per step code:") : tr("Function implementation code:")); + ui->finalCodeGroup->setVisible(aggregate); + + ui->databasesList->setEnabled(ui->selDatabasesRadio->isChecked()); + + bool codeOk = !ui->mainCodeEdit->toPlainText().trimmed().isEmpty(); + setValidState(ui->mainCodeEdit, codeOk, tr("Enter a non-empty implementation code.")); + + bool finalCodeOk = true; + if (aggregate) + finalCodeOk = !ui->finalCodeEdit->toPlainText().trimmed().isEmpty(); + + setValidState(ui->finalCodeEdit, finalCodeOk); + + // Syntax highlighter + QString lang = ui->langCombo->currentText(); + if (lang != currentHighlighterLang) + { + QSyntaxHighlighter* highlighter = nullptr; + if (currentMainHighlighter) + { + // A pointers swap with local var - this is necessary, cause deleting highlighter + // triggers textChanged on QPlainTextEdit, which then calls this method, + // so it becomes an infinite recursion with deleting the same pointer. + // We set the pointer to null first, then delete it. That way it's safe. + highlighter = currentMainHighlighter; + currentMainHighlighter = nullptr; + delete highlighter; + } + + if (currentFinalHighlighter) + { + highlighter = currentFinalHighlighter; + currentFinalHighlighter = nullptr; + delete highlighter; + } + + if (currentInitHighlighter) + { + highlighter = currentInitHighlighter; + currentInitHighlighter = nullptr; + delete highlighter; + } + + if (langOk && highlighterPlugins.contains(lang)) + { + currentInitHighlighter = highlighterPlugins[lang]->createSyntaxHighlighter(ui->initCodeEdit); + currentMainHighlighter = highlighterPlugins[lang]->createSyntaxHighlighter(ui->mainCodeEdit); + currentFinalHighlighter = highlighterPlugins[lang]->createSyntaxHighlighter(ui->finalCodeEdit); + } + + currentHighlighterLang = lang; + } + + updateArgsState(); + model->setValid(row, langOk && codeOk && finalCodeOk && nameOk); + updateState(); +} + +void FunctionsEditor::functionSelected(const QItemSelection& selected, const QItemSelection& deselected) +{ + int deselCnt = deselected.indexes().size(); + int selCnt = selected.indexes().size(); + + if (deselCnt > 0) + functionDeselected(deselected.indexes().first().row()); + + if (selCnt > 0) + functionSelected(selected.indexes().first().row()); + + if (deselCnt > 0 && selCnt == 0) + { + currentModified = false; + clearEdits(); + } +} + +void FunctionsEditor::addFunctionArg() +{ + QListWidgetItem* item = new QListWidgetItem(tr("argument", "new function argument name in function editor window")); + item->setFlags(item->flags () | Qt::ItemIsEditable); + ui->argsList->addItem(item); + + QModelIndex idx = ui->argsList->model()->index(ui->argsList->model()->rowCount() - 1, 0); + ui->argsList->selectionModel()->setCurrentIndex(idx, QItemSelectionModel::Clear|QItemSelectionModel::SelectCurrent); + + ui->argsList->editItem(item); +} + +void FunctionsEditor::editFunctionArg() +{ + QModelIndex selected = getSelectedArg(); + if (!selected.isValid()) + return; + + int row = selected.row(); + QListWidgetItem* item = ui->argsList->item(row); + ui->argsList->editItem(item); +} + +void FunctionsEditor::delFunctionArg() +{ + QModelIndex selected = getSelectedArg(); + if (!selected.isValid()) + return; + + int row = selected.row(); + delete ui->argsList->takeItem(row); +} + +void FunctionsEditor::moveFunctionArgUp() +{ + QModelIndex selected = getSelectedArg(); + if (!selected.isValid()) + return; + + int row = selected.row(); + if (row <= 0) + return; + + ui->argsList->insertItem(row - 1, ui->argsList->takeItem(row)); + + QModelIndex idx = ui->argsList->model()->index(row - 1, 0); + ui->argsList->selectionModel()->setCurrentIndex(idx, QItemSelectionModel::Clear|QItemSelectionModel::SelectCurrent); +} + +void FunctionsEditor::moveFunctionArgDown() +{ + QModelIndex selected = getSelectedArg(); + if (!selected.isValid()) + return; + + int row = selected.row(); + if (row >= ui->argsList->model()->rowCount() - 1) + return; + + ui->argsList->insertItem(row + 1, ui->argsList->takeItem(row)); + + QModelIndex idx = ui->argsList->model()->index(row + 1, 0); + ui->argsList->selectionModel()->setCurrentIndex(idx, QItemSelectionModel::Clear|QItemSelectionModel::SelectCurrent); +} + +void FunctionsEditor::updateArgsState() +{ + bool argsEnabled = !ui->undefArgsCheck->isChecked(); + QModelIndexList indexes = ui->argsList->selectionModel()->selectedIndexes(); + bool argSelected = indexes.size() > 0; + + bool canMoveUp = false; + bool canMoveDown = false; + if (argSelected) + { + canMoveUp = indexes.first().row() > 0; + canMoveDown = (indexes.first().row() + 1) < ui->argsList->count(); + } + + actionMap[ARG_ADD]->setEnabled(argsEnabled); + actionMap[ARG_EDIT]->setEnabled(argsEnabled && argSelected); + actionMap[ARG_DEL]->setEnabled(argsEnabled && argSelected); + actionMap[ARG_MOVE_UP]->setEnabled(argsEnabled && canMoveUp); + actionMap[ARG_MOVE_DOWN]->setEnabled(argsEnabled && canMoveDown); + ui->argsList->setEnabled(argsEnabled); +} + +void FunctionsEditor::applyFilter(const QString& value) +{ + // Remembering old selection, clearing it and restoring afterwards is a workaround for a problem, + // which causees application to crash, when the item was selected, but after applying filter string, + // item was about to disappear. + // This must have something to do with the underlying model (FunctionsEditorModel) implementation, + // but for now I don't really know what is that. + // I have tested simple Qt application with the same routine, but the underlying model was QStandardItemModel + // and everything worked fine. + int row = getCurrentFunctionRow(); + ui->list->selectionModel()->clearSelection(); + + functionFilterModel->setFilterFixedString(value); + + selectFunction(row); +} + +void FunctionsEditor::help() +{ + static const QString url = QStringLiteral("http://wiki.sqlitestudio.pl/index.php/User_Manual#Custom_SQL_functions"); + QDesktopServices::openUrl(QUrl(url, QUrl::StrictMode)); +} + +void FunctionsEditor::changeFont(const QVariant& font) +{ + setFont(font.value<QFont>()); +} + +QVariant FunctionsEditor::saveSession() +{ + return QVariant(); +} + + +bool FunctionsEditor::isUncommited() const +{ + return model->isModified(); +} + +QString FunctionsEditor::getQuitUncommitedConfirmMessage() const +{ + return tr("Functions editor window has uncommited modifications."); +} |
