diff options
Diffstat (limited to 'SQLiteStudio3/guiSQLiteStudio/windows')
31 files changed, 8945 insertions, 0 deletions
diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/bugreporthistorywindow.cpp b/SQLiteStudio3/guiSQLiteStudio/windows/bugreporthistorywindow.cpp new file mode 100644 index 0000000..c92f6f4 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/bugreporthistorywindow.cpp @@ -0,0 +1,155 @@ +#include "bugreporthistorywindow.h" +#include "ui_bugreporthistorywindow.h" +#include "common/unused.h" +#include "services/config.h" +#include <QDebug> +#include <QLabel> + +CFG_KEYS_DEFINE(BugReportHistoryWindow) + +BugReportHistoryWindow::BugReportHistoryWindow(QWidget *parent) : + MdiChild(parent), + ui(new Ui::BugReportHistoryWindow) +{ + init(); +} + +BugReportHistoryWindow::~BugReportHistoryWindow() +{ + delete ui; +} + +bool BugReportHistoryWindow::restoreSessionNextTime() +{ + return false; +} + +QVariant BugReportHistoryWindow::saveSession() +{ + return QVariant(); +} + +bool BugReportHistoryWindow::restoreSession(const QVariant& sessionValue) +{ + UNUSED(sessionValue); + return false; +} + +Icon* BugReportHistoryWindow::getIconNameForMdiWindow() +{ + return ICONS.BUG_LIST; +} + +QString BugReportHistoryWindow::getTitleForMdiWindow() +{ + return tr("Reports history"); +} + +void BugReportHistoryWindow::createActions() +{ + createAction(CLEAR_HISTORY, ICONS.CLEAR_HISTORY, tr("Clear reports history"), this, SLOT(clearHistory()), ui->toolBar); + createAction(DELETE_SELECTED, ICONS.DELETE_ROW, tr("Delete selected entry"), this, SLOT(deleteSelected()), ui->toolBar); +} + +void BugReportHistoryWindow::setupDefShortcuts() +{ + setShortcutContext({ + DELETE_SELECTED + }, + Qt::WidgetWithChildrenShortcut); + + BIND_SHORTCUTS(BugReportHistoryWindow, Action); +} + +QToolBar* BugReportHistoryWindow::getToolBar(int toolbar) const +{ + UNUSED(toolbar); + return ui->toolBar; +} + +void BugReportHistoryWindow::init() +{ + ui->setupUi(this); + initActions(); + + reload(); + connect(ui->reportsList->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(updateState())); + connect(CFG, SIGNAL(reportsHistoryRefreshNeeded()), this, SLOT(reload())); + + updateState(); +} + +void BugReportHistoryWindow::updateState() +{ + actionMap[DELETE_SELECTED]->setEnabled(ui->reportsList->selectedItems().size() > 0); +} + +void BugReportHistoryWindow::reload() +{ + static_qstring(urlTpl, "<a href=\"%1\">%2</a>"); + QString invalidUrlTpl = tr("Invalid response from server."); + + QList<Config::ReportHistoryEntryPtr> entries = CFG->getReportHistory(); + ui->reportsList->clear(); + ui->reportsList->setRowCount(entries.size()); + + QTableWidgetItem* item = nullptr; + QLabel* urlLabel = nullptr; + int row = 0; + for (const Config::ReportHistoryEntryPtr& entry : entries) + { + item = new QTableWidgetItem((entry->isFeatureRequest ? ICONS.FEATURE_REQUEST : ICONS.BUG), entry->title); + item->setData(ENTRY_ID, entry->id); + ui->reportsList->setItem(row, 0, item); + + item = new QTableWidgetItem(QDateTime::fromTime_t(entry->timestamp).toString("yyyy-MM-dd HH:mm:ss")); + ui->reportsList->setItem(row, 1, item); + + if (entry->url.startsWith("http://")) + urlLabel = new QLabel(urlTpl.arg(entry->url, entry->url)); + else + urlLabel = new QLabel(invalidUrlTpl); + + urlLabel->setOpenExternalLinks(true); + ui->reportsList->setCellWidget(row, 2, urlLabel); + + row++; + } + + ui->reportsList->setHorizontalHeaderLabels({tr("Title"), tr("Reported at"), tr("URL")}); + ui->reportsList->resizeColumnsToContents(); +} + +void BugReportHistoryWindow::clearHistory() +{ + CFG->clearReportHistory(); +} + +void BugReportHistoryWindow::deleteSelected() +{ + QList<QTableWidgetItem*> items = ui->reportsList->selectedItems(); + if (items.size() == 0) + { + qDebug() << "Called BugReportHistoryWindow::deleteSelected(), but there's no row selected."; + return; + } + + int id = items.first()->data(ENTRY_ID).toInt(); + if (id == 0) + { + qDebug() << "Called BugReportHistoryWindow::deleteSelected(), but there's no ID in selected row."; + return; + } + + CFG->deleteReport(id); +} + +bool BugReportHistoryWindow::isUncommited() const +{ + return false; +} + +QString BugReportHistoryWindow::getQuitUncommitedConfirmMessage() const +{ + return QString(); +} diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/bugreporthistorywindow.h b/SQLiteStudio3/guiSQLiteStudio/windows/bugreporthistorywindow.h new file mode 100644 index 0000000..e582a48 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/bugreporthistorywindow.h @@ -0,0 +1,65 @@ +#ifndef BUGREPORTHISTORYWINDOW_H +#define BUGREPORTHISTORYWINDOW_H + +#include "mdichild.h" +#include <QWidget> + +namespace Ui { + class BugReportHistoryWindow; +} + +CFG_KEY_LIST(BugReportHistoryWindow, QObject::tr("Reports history window"), + CFG_KEY_ENTRY(DELETE_SELECTED, Qt::Key_Delete, QObject::tr("Delete selected entry")) +) + +class GUI_API_EXPORT BugReportHistoryWindow : public MdiChild +{ + Q_OBJECT + Q_ENUMS(Action) + + public: + enum Action + { + DELETE_SELECTED, + CLEAR_HISTORY + }; + + enum ToolBar + { + TOOLBAR + }; + + explicit BugReportHistoryWindow(QWidget *parent = 0); + ~BugReportHistoryWindow(); + + bool restoreSessionNextTime(); + bool isUncommited() const; + QString getQuitUncommitedConfirmMessage() const; + + protected: + QVariant saveSession(); + bool restoreSession(const QVariant &sessionValue); + Icon* getIconNameForMdiWindow(); + QString getTitleForMdiWindow(); + void createActions(); + void setupDefShortcuts(); + QToolBar* getToolBar(int toolbar) const; + + private: + enum UserRole + { + ENTRY_ID = Qt::UserRole + 1 + }; + + void init(); + + Ui::BugReportHistoryWindow *ui = nullptr; + + private slots: + void updateState(); + void reload(); + void clearHistory(); + void deleteSelected(); +}; + +#endif // BUGREPORTHISTORYWINDOW_H diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/bugreporthistorywindow.ui b/SQLiteStudio3/guiSQLiteStudio/windows/bugreporthistorywindow.ui new file mode 100644 index 0000000..3218822 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/bugreporthistorywindow.ui @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BugReportHistoryWindow</class> + <widget class="QWidget" name="BugReportHistoryWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QToolBar" name="toolBar"/> + </item> + <item> + <widget class="QTableWidget" name="reportsList"> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="verticalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <column> + <property name="text"> + <string>Title</string> + </property> + </column> + <column> + <property name="text"> + <string>Reported at</string> + </property> + </column> + <column> + <property name="text"> + <string>URL</string> + </property> + </column> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/collationseditor.cpp b/SQLiteStudio3/guiSQLiteStudio/windows/collationseditor.cpp new file mode 100644 index 0000000..1d0594d --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/collationseditor.cpp @@ -0,0 +1,389 @@ +#include "collationseditor.h" +#include "ui_collationseditor.h" +#include "common/unused.h" +#include "selectabledbmodel.h" +#include "dbtree/dbtree.h" +#include "dbtree/dbtreemodel.h" +#include "collationseditormodel.h" +#include "common/utils.h" +#include "uiutils.h" +#include "services/pluginmanager.h" +#include "syntaxhighlighterplugin.h" +#include "plugins/scriptingplugin.h" +#include "uiconfig.h" +#include <QDesktopServices> +#include <QSyntaxHighlighter> + +CollationsEditor::CollationsEditor(QWidget *parent) : + MdiChild(parent), + ui(new Ui::CollationsEditor) +{ + init(); +} + +CollationsEditor::~CollationsEditor() +{ + delete ui; +} + +bool CollationsEditor::restoreSessionNextTime() +{ + return false; +} + +QVariant CollationsEditor::saveSession() +{ + return QVariant(); +} + +bool CollationsEditor::restoreSession(const QVariant& sessionValue) +{ + UNUSED(sessionValue); + return true; +} + +Icon* CollationsEditor::getIconNameForMdiWindow() +{ + return ICONS.CONSTRAINT_COLLATION; +} + +QString CollationsEditor::getTitleForMdiWindow() +{ + return tr("Collations editor"); +} + +void CollationsEditor::createActions() +{ + createAction(COMMIT, ICONS.COMMIT, tr("Commit all collation changes"), this, SLOT(commit()), ui->toolbar); + createAction(ROLLBACK, ICONS.ROLLBACK, tr("Rollback all collation changes"), this, SLOT(rollback()), ui->toolbar); + ui->toolbar->addSeparator(); + createAction(ADD, ICONS.NEW_COLLATION, tr("Create new collation"), this, SLOT(newCollation()), ui->toolbar); + createAction(DELETE, ICONS.DELETE_COLLATION, tr("Delete selected collation"), this, SLOT(deleteCollation()), ui->toolbar); + ui->toolbar->addSeparator(); + createAction(HELP, ICONS.HELP, tr("Editing collations manual"), this, SLOT(help()), ui->toolbar); +} + +void CollationsEditor::setupDefShortcuts() +{ + +} + +QToolBar* CollationsEditor::getToolBar(int toolbar) const +{ + UNUSED(toolbar); + return ui->toolbar; +} + +void CollationsEditor::init() +{ + ui->setupUi(this); + initActions(); + + setFont(CFG_UI.Fonts.SqlEditor.get()); + + model = new CollationsEditorModel(this); + collationFilterModel = new QSortFilterProxyModel(this); + collationFilterModel->setSourceModel(model); + ui->collationList->setModel(collationFilterModel); + + dbListModel = new SelectableDbModel(this); + dbListModel->setDisabledVersion(2); + dbListModel->setSourceModel(DBTREE->getModel()); + ui->databaseList->setModel(dbListModel); + ui->databaseList->expandAll(); + + model->setData(COLLATIONS->getAllCollations()); + + connect(ui->collationList->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(collationSelected(QItemSelection,QItemSelection))); + connect(ui->collationList->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(updateState())); + connect(ui->codeEdit, SIGNAL(textChanged()), this, SLOT(updateModified())); + connect(ui->nameEdit, SIGNAL(textChanged(QString)), this, SLOT(updateModified())); + connect(ui->allDatabasesRadio, SIGNAL(clicked()), this, SLOT(updateModified())); + connect(ui->selectedDatabasesRadio, SIGNAL(clicked()), this, SLOT(updateModified())); + connect(ui->langCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(updateModified())); + + connect(dbListModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(updateModified())); + connect(CFG_UI.Fonts.SqlEditor, SIGNAL(changed(QVariant)), this, SLOT(changeFont(QVariant))); + + // Language plugins + foreach (ScriptingPlugin* plugin, PLUGINS->getLoadedPlugins<ScriptingPlugin>()) + ui->langCombo->addItem(plugin->getLanguage()); + + // Syntax highlighting plugins + foreach (SyntaxHighlighterPlugin* plugin, PLUGINS->getLoadedPlugins<SyntaxHighlighterPlugin>()) + highlighterPlugins[plugin->getLanguageName()] = plugin; + + updateState(); +} + +int CollationsEditor::getCurrentCollationRow() const +{ + QModelIndexList idxList = ui->collationList->selectionModel()->selectedIndexes(); + if (idxList.size() == 0) + return -1; + + return idxList.first().row(); +} + +void CollationsEditor::collationDeselected(int row) +{ + model->setName(row, ui->nameEdit->text()); + model->setLang(row, ui->langCombo->currentText()); + model->setAllDatabases(row, ui->allDatabasesRadio->isChecked()); + model->setCode(row, ui->codeEdit->toPlainText()); + model->setModified(row, currentModified); + + if (ui->selectedDatabasesRadio->isChecked()) + model->setDatabases(row, getCurrentDatabases()); + + model->validateNames(); +} + +void CollationsEditor::collationSelected(int row) +{ + updatesForSelection = true; + ui->nameEdit->setText(model->getName(row)); + ui->codeEdit->setPlainText(model->getCode(row)); + ui->langCombo->setCurrentText(model->getLang(row)); + + // Databases + dbListModel->setDatabases(model->getDatabases(row)); + ui->databaseList->expandAll(); + + if (model->getAllDatabases(row)) + ui->allDatabasesRadio->setChecked(true); + else + ui->selectedDatabasesRadio->setChecked(true); + + updatesForSelection = false; + currentModified = false; + + updateCurrentCollationState(); +} + +void CollationsEditor::clearEdits() +{ + ui->nameEdit->setText(QString::null); + ui->codeEdit->setPlainText(QString::null); + ui->langCombo->setCurrentText(QString::null); + ui->allDatabasesRadio->setChecked(true); + ui->langCombo->setCurrentIndex(-1); +} + +void CollationsEditor::selectCollation(int row) +{ + if (!model->isValidRowIndex(row)) + return; + + ui->collationList->selectionModel()->setCurrentIndex(model->index(row), QItemSelectionModel::Clear|QItemSelectionModel::SelectCurrent); +} + +QStringList CollationsEditor::getCurrentDatabases() const +{ + return dbListModel->getDatabases(); +} + +void CollationsEditor::setFont(const QFont& font) +{ + ui->codeEdit->setFont(font); +} + +void CollationsEditor::help() +{ + static const QString url = QStringLiteral("http://wiki.sqlitestudio.pl/index.php/User_Manual#Custom_collations"); + QDesktopServices::openUrl(QUrl(url, QUrl::StrictMode)); +} + +void CollationsEditor::commit() +{ + int row = getCurrentCollationRow(); + if (model->isValidRowIndex(row)) + collationDeselected(row); + + QList<CollationManager::CollationPtr> collations = model->getCollations(); + + COLLATIONS->setCollations(collations); + model->clearModified(); + currentModified = false; + + if (model->isValidRowIndex(row)) + selectCollation(row); + + updateState(); +} + +void CollationsEditor::rollback() +{ + int selectedBefore = getCurrentCollationRow(); + + model->setData(COLLATIONS->getAllCollations()); + currentModified = false; + clearEdits(); + + if (model->isValidRowIndex(selectedBefore)) + selectCollation(selectedBefore); + + updateState(); +} + +void CollationsEditor::newCollation() +{ + if (ui->langCombo->currentIndex() == -1 && ui->langCombo->count() > 0) + ui->langCombo->setCurrentIndex(0); + + CollationManager::CollationPtr coll = CollationManager::CollationPtr::create(); + coll->name = generateUniqueName("collation", model->getCollationNames()); + + if (ui->langCombo->currentIndex() > -1) + coll->lang = ui->langCombo->currentText(); + + model->addCollation(coll); + + selectCollation(model->rowCount() - 1); +} + +void CollationsEditor::deleteCollation() +{ + int row = getCurrentCollationRow(); + model->deleteCollation(row); + clearEdits(); + + row = getCurrentCollationRow(); + if (model->isValidRowIndex(row)) + collationSelected(row); + + updateState(); +} + +void CollationsEditor::updateState() +{ + bool modified = model->isModified() || currentModified; + bool valid = model->isValid(); + + actionMap[COMMIT]->setEnabled(modified && valid); + actionMap[ROLLBACK]->setEnabled(modified); + actionMap[DELETE]->setEnabled(ui->collationList->selectionModel()->selectedIndexes().size() > 0); +} + +void CollationsEditor::updateCurrentCollationState() +{ + int row = getCurrentCollationRow(); + bool validRow = model->isValidRowIndex(row); + ui->rightWidget->setEnabled(validRow); + if (!validRow) + { + setValidState(ui->langCombo, true); + setValidState(ui->nameEdit, true); + setValidState(ui->codeEdit, 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 collation.")); + + bool langOk = ui->langCombo->currentIndex() >= 0; + ui->codeGroup->setEnabled(langOk); + ui->databasesGroup->setEnabled(langOk); + ui->nameEdit->setEnabled(langOk); + ui->nameLabel->setEnabled(langOk); + ui->databaseList->setEnabled(ui->selectedDatabasesRadio->isChecked()); + setValidState(ui->langCombo, langOk, tr("Pick the implementation language.")); + + bool codeOk = !ui->codeEdit->toPlainText().trimmed().isEmpty(); + setValidState(ui->codeEdit, codeOk, tr("Enter a non-empty implementation code.")); + + // Syntax highlighter + QString lang = ui->langCombo->currentText(); + if (lang != currentHighlighterLang) + { + QSyntaxHighlighter* highlighter = nullptr; + if (currentHighlighter) + { + // 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 = currentHighlighter; + currentHighlighter = nullptr; + delete highlighter; + } + + if (langOk && highlighterPlugins.contains(lang)) + { + currentHighlighter = highlighterPlugins[lang]->createSyntaxHighlighter(ui->codeEdit); + } + + currentHighlighterLang = lang; + } + model->setValid(row, langOk && codeOk && nameOk); + updateState(); +} + +void CollationsEditor::collationSelected(const QItemSelection& selected, const QItemSelection& deselected) +{ + int deselCnt = deselected.indexes().size(); + int selCnt = selected.indexes().size(); + + if (deselCnt > 0) + collationDeselected(deselected.indexes().first().row()); + + if (selCnt > 0) + collationSelected(selected.indexes().first().row()); + + if (deselCnt > 0 && selCnt == 0) + { + currentModified = false; + clearEdits(); + } +} + +void CollationsEditor::updateModified() +{ + if (updatesForSelection) + return; + + int row = getCurrentCollationRow(); + if (model->isValidRowIndex(row)) + { + bool nameDiff = model->getName(row) != ui->nameEdit->text(); + bool codeDiff = model->getCode(row) != ui->codeEdit->toPlainText(); + bool langDiff = model->getLang(row) != ui->langCombo->currentText(); + bool allDatabasesDiff = model->getAllDatabases(row) != ui->allDatabasesRadio->isChecked(); + bool dbDiff = getCurrentDatabases().toSet() != model->getDatabases(row).toSet(); // QSet to ignore order + + currentModified = (nameDiff || codeDiff || langDiff || allDatabasesDiff || dbDiff); + } + + updateCurrentCollationState(); +} + +void CollationsEditor::applyFilter(const QString& value) +{ + // + // See FunctionsEditor::applyFilter() for details why we remember current selection and restore it at the end. + // + + int row = getCurrentCollationRow(); + ui->collationList->selectionModel()->clearSelection(); + + collationFilterModel->setFilterFixedString(value); + + selectCollation(row); +} + +void CollationsEditor::changeFont(const QVariant& font) +{ + setFont(font.value<QFont>()); +} + + +bool CollationsEditor::isUncommited() const +{ + return model->isModified(); +} + +QString CollationsEditor::getQuitUncommitedConfirmMessage() const +{ + return tr("Collations editor window has uncommited modifications."); +} diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/collationseditor.h b/SQLiteStudio3/guiSQLiteStudio/windows/collationseditor.h new file mode 100644 index 0000000..62cb281 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/collationseditor.h @@ -0,0 +1,89 @@ +#ifndef COLLATIONSEDITOR_H +#define COLLATIONSEDITOR_H + +#include "mdichild.h" +#include "common/extactioncontainer.h" +#include <QItemSelection> +#include <QModelIndex> +#include <QWidget> + +namespace Ui { + class CollationsEditor; +} + +class SyntaxHighlighterPlugin; +class SelectableDbModel; +class CollationsEditorModel; +class QSortFilterProxyModel; +class QSyntaxHighlighter; + +class GUI_API_EXPORT CollationsEditor : public MdiChild +{ + Q_OBJECT + + public: + enum Action + { + COMMIT, + ROLLBACK, + ADD, + DELETE, + HELP + }; + + enum ToolBar + { + TOOLBAR + }; + + explicit CollationsEditor(QWidget *parent = 0); + ~CollationsEditor(); + + bool restoreSessionNextTime(); + bool isUncommited() const; + QString getQuitUncommitedConfirmMessage() const; + + protected: + QVariant saveSession(); + bool restoreSession(const QVariant &sessionValue); + Icon* getIconNameForMdiWindow(); + QString getTitleForMdiWindow(); + void createActions(); + void setupDefShortcuts(); + QToolBar* getToolBar(int toolbar) const; + + private: + void init(); + int getCurrentCollationRow() const; + void collationDeselected(int row); + void collationSelected(int row); + void clearEdits(); + void selectCollation(int row); + QStringList getCurrentDatabases() const; + void setFont(const QFont& font); + + Ui::CollationsEditor *ui = nullptr; + CollationsEditorModel* model = nullptr; + QSortFilterProxyModel* collationFilterModel = nullptr; + SelectableDbModel* dbListModel = nullptr; + QHash<QString,SyntaxHighlighterPlugin*> highlighterPlugins; + QSyntaxHighlighter* currentHighlighter = nullptr; + QString currentHighlighterLang; + bool currentModified = false; + bool updatesForSelection = false; + + private slots: + void help(); + void commit(); + void rollback(); + void newCollation(); + void deleteCollation(); + void updateState(); + void updateCurrentCollationState(); + void collationSelected(const QItemSelection& selected, const QItemSelection& deselected); + void updateModified(); + void applyFilter(const QString& value); + void changeFont(const QVariant& font); +}; + +#endif // COLLATIONSEDITOR_H diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/collationseditor.ui b/SQLiteStudio3/guiSQLiteStudio/windows/collationseditor.ui new file mode 100644 index 0000000..635ae59 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/collationseditor.ui @@ -0,0 +1,210 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CollationsEditor</class> + <widget class="QWidget" name="CollationsEditor"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>765</width> + <height>529</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QToolBar" name="toolbar"/> + </item> + <item> + <widget class="QWidget" name="mainWidgt" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QWidget" name="leftWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLineEdit" name="lineEdit"> + <property name="placeholderText"> + <string>Filter collations</string> + </property> + </widget> + </item> + <item> + <widget class="QListView" name="collationList"/> + </item> + </layout> + </widget> + <widget class="QWidget" name="rightWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>4</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item row="1" column="0"> + <widget class="QLineEdit" name="nameEdit"/> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string>Collation name:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="langCombo"/> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="langLabel"> + <property name="text"> + <string>Implementation language:</string> + </property> + </widget> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QWidget" name="rightBottomWidget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QSplitter" name="splitter_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <widget class="QGroupBox" name="databasesGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Databases</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QRadioButton" name="allDatabasesRadio"> + <property name="text"> + <string>Register in all databases</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="selectedDatabasesRadio"> + <property name="text"> + <string>Register in following databases:</string> + </property> + </widget> + </item> + <item> + <widget class="QTreeView" name="databaseList"> + <attribute name="headerVisible"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + <widget class="QGroupBox" name="codeGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>2</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Implementation code:</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QPlainTextEdit" name="codeEdit"/> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/collationseditormodel.cpp b/SQLiteStudio3/guiSQLiteStudio/windows/collationseditormodel.cpp new file mode 100644 index 0000000..05ca4e1 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/collationseditormodel.cpp @@ -0,0 +1,287 @@ +#include "collationseditormodel.h" +#include "common/unused.h" +#include "common/strhash.h" +#include "services/pluginmanager.h" +#include "plugins/scriptingplugin.h" +#include "icon.h" + +#define SETTER(X, Y) \ + if (!isValidRowIndex(row) || X == Y) \ + return; \ + \ + X = Y; \ + emitDataChanged(row); + +#define GETTER(X, Y) \ + if (!isValidRowIndex(row)) \ + return Y; \ + \ + return X; + +CollationsEditorModel::CollationsEditorModel(QObject *parent) : + QAbstractListModel(parent) +{ + init(); +} + +void CollationsEditorModel::clearModified() +{ + beginResetModel(); + foreach (Collation* coll, collationList) + coll->modified = false; + + listModified = false; + originalCollationList = collationList; + + endResetModel(); +} + +bool CollationsEditorModel::isModified() const +{ + if (collationList != originalCollationList) + return true; + + foreach (Collation* coll, collationList) + { + if (coll->modified) + return true; + } + return false; +} + +bool CollationsEditorModel::isModified(int row) const +{ + GETTER(collationList[row]->modified, false); +} + +void CollationsEditorModel::setModified(int row, bool modified) +{ + SETTER(collationList[row]->modified, modified); +} + +void CollationsEditorModel::setName(int row, const QString& name) +{ + SETTER(collationList[row]->data->name, name); +} + +QString CollationsEditorModel::getName(int row) const +{ + GETTER(collationList[row]->data->name, QString()); +} + +void CollationsEditorModel::setLang(int row, const QString& lang) +{ + SETTER(collationList[row]->data->lang, lang); +} + +QString CollationsEditorModel::getLang(int row) const +{ + GETTER(collationList[row]->data->lang, QString()); +} + +void CollationsEditorModel::setAllDatabases(int row, bool allDatabases) +{ + SETTER(collationList[row]->data->allDatabases, allDatabases); +} + +bool CollationsEditorModel::getAllDatabases(int row) const +{ + GETTER(collationList[row]->data->allDatabases, true); +} + +void CollationsEditorModel::setCode(int row, const QString& code) +{ + SETTER(collationList[row]->data->code, code); +} + +QString CollationsEditorModel::getCode(int row) const +{ + GETTER(collationList[row]->data->code, QString()); +} + +void CollationsEditorModel::setDatabases(int row, const QStringList& databases) +{ + SETTER(collationList[row]->data->databases, databases); +} + +QStringList CollationsEditorModel::getDatabases(int row) +{ + GETTER(collationList[row]->data->databases, QStringList()); +} + +bool CollationsEditorModel::isValid(int row) const +{ + GETTER(collationList[row]->valid, false); +} + +void CollationsEditorModel::setValid(int row, bool valid) +{ + SETTER(collationList[row]->valid, valid); +} + +bool CollationsEditorModel::isValid() const +{ + foreach (Collation* coll, collationList) + { + if (!coll->valid) + return false; + } + return true; +} + +void CollationsEditorModel::setData(const QList<CollationManager::CollationPtr>& collations) +{ + beginResetModel(); + + Collation* collationPtr = nullptr; + foreach (collationPtr, collationList) + delete collationPtr; + + collationList.clear(); + + foreach (const CollationManager::CollationPtr& coll, collations) + collationList << new Collation(coll); + + listModified = false; + originalCollationList = collationList; + + endResetModel(); +} + +void CollationsEditorModel::addCollation(const CollationManager::CollationPtr& collation) +{ + int row = collationList.size(); + + beginInsertRows(QModelIndex(), row, row); + + collationList << new Collation(collation); + listModified = true; + + endInsertRows(); +} + +void CollationsEditorModel::deleteCollation(int row) +{ + if (!isValidRowIndex(row)) + return; + + beginRemoveRows(QModelIndex(), row, row); + + delete collationList[row]; + collationList.removeAt(row); + + listModified = true; + + endRemoveRows(); +} + +QList<CollationManager::CollationPtr> CollationsEditorModel::getCollations() const +{ + QList<CollationManager::CollationPtr> results; + + foreach (Collation* coll, collationList) + results << coll->data; + + return results; +} + +QStringList CollationsEditorModel::getCollationNames() const +{ + QStringList names; + foreach (Collation* coll, collationList) + names << coll->data->name; + + return names; +} + +void CollationsEditorModel::validateNames() +{ + StrHash<QList<int>> counter; + + int row = 0; + foreach (Collation* coll, collationList) + { + coll->valid &= true; + counter[coll->data->name] << row++; + } + + QHashIterator<QString,QList<int>> cntIt = counter.iterator(); + while (cntIt.hasNext()) + { + cntIt.next(); + if (cntIt.value().size() > 1) + { + foreach (int cntRow, cntIt.value()) + setValid(cntRow, false); + } + } + + QModelIndex idx; + for (int i = 0; i < collationList.size(); i++) + { + idx = index(i); + emit dataChanged(idx, idx); + } +} + +bool CollationsEditorModel::isAllowedName(int rowToSkip, const QString& nameToValidate) +{ + QStringList names = getCollationNames(); + names.removeAt(rowToSkip); + return !names.contains(nameToValidate, Qt::CaseInsensitive); +} + +bool CollationsEditorModel::isValidRowIndex(int row) const +{ + return (row >= 0 && row < collationList.size()); +} + +int CollationsEditorModel::rowCount(const QModelIndex& parent) const +{ + UNUSED(parent); + return collationList.size(); +} + +QVariant CollationsEditorModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || !isValidRowIndex(index.row())) + return QVariant(); + + if (role == Qt::DisplayRole) + return collationList[index.row()]->data->name; + + if (role == Qt::DecorationRole && langToIcon.contains(collationList[index.row()]->data->lang)) + { + QIcon icon = langToIcon[collationList[index.row()]->data->lang]; + if (!collationList[index.row()]->valid) + icon = Icon::merge(icon, Icon::ERROR); + + return icon; + } + + return QVariant(); + +} + +void CollationsEditorModel::init() +{ + foreach (ScriptingPlugin* plugin, PLUGINS->getLoadedPlugins<ScriptingPlugin>()) + langToIcon[plugin->getLanguage()] = QIcon(plugin->getIconPath()); +} + +void CollationsEditorModel::emitDataChanged(int row) +{ + QModelIndex idx = index(row); + emit dataChanged(idx, idx); +} + +CollationsEditorModel::Collation::Collation() +{ + data = CollationManager::CollationPtr::create(); +} + +CollationsEditorModel::Collation::Collation(const CollationManager::CollationPtr& other) +{ + data = CollationManager::CollationPtr::create(*other); + originalName = data->name; +} diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/collationseditormodel.h b/SQLiteStudio3/guiSQLiteStudio/windows/collationseditormodel.h new file mode 100644 index 0000000..0c17c5b --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/collationseditormodel.h @@ -0,0 +1,77 @@ +#ifndef COLLATIONSEDITORMODEL_H +#define COLLATIONSEDITORMODEL_H + +#include "services/collationmanager.h" +#include "guiSQLiteStudio_global.h" +#include <QIcon> +#include <QHash> +#include <QAbstractListModel> + +class GUI_API_EXPORT CollationsEditorModel : public QAbstractListModel +{ + Q_OBJECT + public: + using QAbstractItemModel::setData; + + explicit CollationsEditorModel(QObject *parent = 0); + + void clearModified(); + bool isModified() const; + bool isModified(int row) const; + void setModified(int row, bool modified); + void setName(int row, const QString& name); + QString getName(int row) const; + void setLang(int row, const QString& lang); + QString getLang(int row) const; + void setAllDatabases(int row, bool allDatabases); + bool getAllDatabases(int row) const; + void setCode(int row, const QString& code); + QString getCode(int row) const; + void setDatabases(int row, const QStringList& databases); + QStringList getDatabases(int row); + bool isValid(int row) const; + void setValid(int row, bool valid); + bool isValid() const; + void setData(const QList<CollationManager::CollationPtr>& collations); + void addCollation(const CollationManager::CollationPtr& collation); + void deleteCollation(int row); + QList<CollationManager::CollationPtr> getCollations() const; + QStringList getCollationNames() const; + void validateNames(); + bool isAllowedName(int rowToSkip, const QString& nameToValidate); + bool isValidRowIndex(int row) const; + + int rowCount(const QModelIndex& parent = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role) const; + + private: + struct Collation + { + Collation(); + Collation(const CollationManager::CollationPtr& other); + + CollationManager::CollationPtr data; + bool modified = false; + bool valid = true; + QString originalName; + }; + + void init(); + void emitDataChanged(int row); + + QList<Collation*> collationList; + + /** + * @brief List of collation pointers before modifications. + * + * This list is kept to check for modifications in the overall list of collations. + * Pointers on this list may be already deleted, so don't use them! + * It's only used to compare list of pointers to collationList, so it can tell you + * if the list was modified in regards of adding or deleting collations. + */ + QList<Collation*> originalCollationList; + QHash<QString,QIcon> langToIcon; + bool listModified = false; +}; + +#endif // COLLATIONSEDITORMODEL_H diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/constrainttabmodel.cpp b/SQLiteStudio3/guiSQLiteStudio/windows/constrainttabmodel.cpp new file mode 100644 index 0000000..2d8897b --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/constrainttabmodel.cpp @@ -0,0 +1,395 @@ +#include "constrainttabmodel.h" +#include "common/unused.h" +#include "iconmanager.h" +#include "common/utils_sql.h" +#include <QDebug> + +ConstraintTabModel::ConstraintTabModel(QObject *parent) : + QAbstractTableModel(parent) +{ +} + +int ConstraintTabModel::rowCount(const QModelIndex& parent) const +{ + UNUSED(parent); + if (createTable.isNull()) + return 0; + + int cnt = 0; + foreach (SqliteCreateTable::Column* col, createTable->columns) + cnt += col->constraints.size(); + + cnt += createTable->constraints.size(); + return cnt; +} + +int ConstraintTabModel::columnCount(const QModelIndex& parent) const +{ + UNUSED(parent); + return 4; +} + +QVariant ConstraintTabModel::data(const QModelIndex& index, int role) const +{ + if (createTable.isNull()) + return QVariant(); + + int constrIdx = index.row(); + int currIdx = -1; + foreach (SqliteCreateTable::Column* column, createTable->columns) + { + foreach (SqliteCreateTable::Column::Constraint* constr, column->constraints) + { + currIdx++; + + if (currIdx == constrIdx) + return data(constr, index.column(), role); + } + } + + foreach (SqliteCreateTable::Constraint* constr, createTable->constraints) + { + currIdx++; + + if (currIdx == constrIdx) + return data(constr, index.column(), role); + } + + return QVariant(); +} + +QVariant ConstraintTabModel::data(SqliteCreateTable::Constraint* constr, int column, int role) const +{ + switch (getColumn(column)) + { + case ConstraintTabModel::Columns::SCOPE: + { + if (role == Qt::DisplayRole) + return tr("Table", "table constraints"); + + break; + } + case Columns::TYPE: + { + if (role == Qt::DisplayRole) + return getTypeLabel(constr->type); + + if (role == Qt::DecorationRole) + return getTypeIcon(constr->type); + + break; + } + case Columns::NAME: + { + if (role == Qt::DisplayRole) + return stripObjName(constr->name, createTable->dialect); + + break; + } + case Columns::DETAILS: + { + if (role == Qt::DisplayRole) + return getDetails(constr); + + break; + } + } + return QVariant(); +} + +QVariant ConstraintTabModel::data(SqliteCreateTable::Column::Constraint* constr, int column, int role) const +{ + switch (getColumn(column)) + { + case ConstraintTabModel::Columns::SCOPE: + { + if (role == Qt::DisplayRole) + { + QString colName = dynamic_cast<SqliteCreateTable::Column*>(constr->parentStatement())->name; + return tr("Column (%1)", "table constraints").arg(colName); + } + + break; + } + case Columns::TYPE: + { + if (role == Qt::DisplayRole) + return getTypeLabel(constr->type); + + if (role == Qt::DecorationRole) + return getTypeIcon(constr->type); + + break; + } + case Columns::NAME: + { + if (role == Qt::DisplayRole) + return stripObjName(constr->name, createTable->dialect); + + break; + } + case Columns::DETAILS: + { + if (role == Qt::DisplayRole) + return getDetails(constr); + + break; + } + } + return QVariant(); +} + +QVariant ConstraintTabModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QAbstractTableModel::headerData(section, orientation, role); + + if (orientation == Qt::Vertical) + return section + 1; + + switch (getColumn(section)) + { + case ConstraintTabModel::Columns::SCOPE: + return tr("Scope", "table constraints"); + case Columns::TYPE: + return tr("Type", "table constraints"); + case Columns::DETAILS: + return tr("Details", "table constraints"); + case Columns::NAME: + return tr("Name", "table constraints"); + } + return QVariant(); +} + +void ConstraintTabModel::setCreateTable(const QPointer<SqliteCreateTable>& value) +{ + beginResetModel(); + createTable = value; + endResetModel(); +} + +ConstraintTabModel::Columns ConstraintTabModel::getColumn(int idx) const +{ + return static_cast<Columns>(idx); +} + +QString ConstraintTabModel::getTypeLabel(SqliteCreateTable::Constraint::Type type) const +{ + switch (type) + { + case SqliteCreateTable::Constraint::PRIMARY_KEY: + return "PRIMARY KEY"; + case SqliteCreateTable::Constraint::UNIQUE: + return "UNIQUE"; + case SqliteCreateTable::Constraint::CHECK: + return "CHECK"; + case SqliteCreateTable::Constraint::FOREIGN_KEY: + return "FOREIGN KEY"; + case SqliteCreateTable::Constraint::NAME_ONLY: + return QString::null; + } + return QString::null; +} + +QString ConstraintTabModel::getTypeLabel(SqliteCreateTable::Column::Constraint::Type type) const +{ + switch (type) + { + case SqliteCreateTable::Column::Constraint::PRIMARY_KEY: + return "PRIMARY KEY"; + case SqliteCreateTable::Column::Constraint::NOT_NULL: + return "NOT NULL"; + case SqliteCreateTable::Column::Constraint::UNIQUE: + return "UNIQUE"; + case SqliteCreateTable::Column::Constraint::CHECK: + return "CHECK"; + case SqliteCreateTable::Column::Constraint::DEFAULT: + return "DEFAULT"; + case SqliteCreateTable::Column::Constraint::COLLATE: + return "COLLATE"; + case SqliteCreateTable::Column::Constraint::FOREIGN_KEY: + return "FOREIGN KEY"; + case SqliteCreateTable::Column::Constraint::NULL_: + case SqliteCreateTable::Column::Constraint::NAME_ONLY: + case SqliteCreateTable::Column::Constraint::DEFERRABLE_ONLY: + break; + } + return QString::null; +} + +QIcon ConstraintTabModel::getTypeIcon(SqliteCreateTable::Constraint::Type type) const +{ + switch (type) + { + case SqliteCreateTable::Constraint::PRIMARY_KEY: + return ICONS.CONSTRAINT_PRIMARY_KEY; + case SqliteCreateTable::Constraint::UNIQUE: + return ICONS.CONSTRAINT_UNIQUE; + case SqliteCreateTable::Constraint::CHECK: + return ICONS.CONSTRAINT_CHECK; + case SqliteCreateTable::Constraint::FOREIGN_KEY: + return ICONS.CONSTRAINT_FOREIGN_KEY; + case SqliteCreateTable::Constraint::NAME_ONLY: + return QIcon(); + } + return QIcon(); +} + +QIcon ConstraintTabModel::getTypeIcon(SqliteCreateTable::Column::Constraint::Type type) const +{ + switch (type) + { + case SqliteCreateTable::Column::Constraint::PRIMARY_KEY: + return ICONS.CONSTRAINT_PRIMARY_KEY; + case SqliteCreateTable::Column::Constraint::NOT_NULL: + return ICONS.CONSTRAINT_NOT_NULL; + case SqliteCreateTable::Column::Constraint::UNIQUE: + return ICONS.CONSTRAINT_UNIQUE; + case SqliteCreateTable::Column::Constraint::CHECK: + return ICONS.CONSTRAINT_CHECK; + case SqliteCreateTable::Column::Constraint::DEFAULT: + return ICONS.CONSTRAINT_DEFAULT; + case SqliteCreateTable::Column::Constraint::COLLATE: + return ICONS.CONSTRAINT_COLLATION; + case SqliteCreateTable::Column::Constraint::FOREIGN_KEY: + return ICONS.CONSTRAINT_FOREIGN_KEY; + case SqliteCreateTable::Column::Constraint::NULL_: + case SqliteCreateTable::Column::Constraint::NAME_ONLY: + case SqliteCreateTable::Column::Constraint::DEFERRABLE_ONLY: + break; + } + return QIcon(); +} + +QString ConstraintTabModel::getDetails(SqliteCreateTable::Constraint* constr) const +{ + switch (constr->type) + { + case SqliteCreateTable::Constraint::PRIMARY_KEY: + return getPkDetails(constr); + case SqliteCreateTable::Constraint::UNIQUE: + return getUniqueDetails(constr); + case SqliteCreateTable::Constraint::CHECK: + return getCheckDetails(constr); + case SqliteCreateTable::Constraint::FOREIGN_KEY: + return getFkDetails(constr); + case SqliteCreateTable::Constraint::NAME_ONLY: + return QString::null; + } + return QString::null; +} + +QString ConstraintTabModel::getDetails(SqliteCreateTable::Column::Constraint* constr) const +{ + switch (constr->type) + { + case SqliteCreateTable::Column::Constraint::PRIMARY_KEY: + return getPkDetails(constr); + case SqliteCreateTable::Column::Constraint::NOT_NULL: + return getNotNullDetails(constr); + case SqliteCreateTable::Column::Constraint::UNIQUE: + return getUniqueDetails(constr); + case SqliteCreateTable::Column::Constraint::CHECK: + return getCheckDetails(constr); + case SqliteCreateTable::Column::Constraint::DEFAULT: + return getDefaultDetails(constr); + case SqliteCreateTable::Column::Constraint::COLLATE: + return getCollateDetails(constr); + case SqliteCreateTable::Column::Constraint::FOREIGN_KEY: + return getFkDetails(constr); + case SqliteCreateTable::Column::Constraint::NULL_: + case SqliteCreateTable::Column::Constraint::NAME_ONLY: + case SqliteCreateTable::Column::Constraint::DEFERRABLE_ONLY: + break; + } + return QString::null; +} + +QString ConstraintTabModel::getPkDetails(SqliteCreateTable::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "KEY", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString ConstraintTabModel::getUniqueDetails(SqliteCreateTable::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "UNIQUE", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString ConstraintTabModel::getCheckDetails(SqliteCreateTable::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "CHECK", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString ConstraintTabModel::getFkDetails(SqliteCreateTable::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "KEY", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString ConstraintTabModel::getPkDetails(SqliteCreateTable::Column::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "KEY", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString ConstraintTabModel::getUniqueDetails(SqliteCreateTable::Column::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "UNIQUE", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString ConstraintTabModel::getCheckDetails(SqliteCreateTable::Column::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "CHECK", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString ConstraintTabModel::getFkDetails(SqliteCreateTable::Column::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "REFERENCES", Qt::CaseInsensitive); + return getConstrDetails(constr, idx); +} + +QString ConstraintTabModel::getNotNullDetails(SqliteCreateTable::Column::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "NULL", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString ConstraintTabModel::getCollateDetails(SqliteCreateTable::Column::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "COLLATE", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString ConstraintTabModel::getDefaultDetails(SqliteCreateTable::Column::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "DEFAULT", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString ConstraintTabModel::getConstrDetails(SqliteCreateTable::Constraint* constr, int tokenOffset) const +{ + return getConstrDetails(constr->tokens, tokenOffset); +} + +QString ConstraintTabModel::getConstrDetails(SqliteCreateTable::Column::Constraint* constr, int tokenOffset) const +{ + return getConstrDetails(constr->tokens, tokenOffset); +} + +QString ConstraintTabModel::getConstrDetails(const TokenList& constrTokens, int tokenOffset) const +{ + TokenList tokens = constrTokens.mid(tokenOffset); + tokens.trimLeft(); + return tokens.detokenize(); +} + +void ConstraintTabModel::updateModel() +{ + beginResetModel(); + endResetModel(); +} diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/constrainttabmodel.h b/SQLiteStudio3/guiSQLiteStudio/windows/constrainttabmodel.h new file mode 100644 index 0000000..f93415b --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/constrainttabmodel.h @@ -0,0 +1,70 @@ +#ifndef CONSTRAINTTABMODEL_H +#define CONSTRAINTTABMODEL_H + +#include "parser/ast/sqlitecreatetable.h" +#include "guiSQLiteStudio_global.h" +#include <QAbstractTableModel> +#include <QPointer> + +class GUI_API_EXPORT ConstraintTabModel : public QAbstractTableModel +{ + Q_OBJECT + public: + explicit ConstraintTabModel(QObject *parent = 0); + + int rowCount(const QModelIndex& parent = QModelIndex()) const; + int columnCount(const QModelIndex& parent = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role) const; + QVariant data(SqliteCreateTable::Constraint* constr, int column, int role) const; + QVariant data(SqliteCreateTable::Column::Constraint* constr, int column, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + + void setCreateTable(const QPointer<SqliteCreateTable>& value); + + private: + enum class Columns + { + SCOPE, + TYPE, + NAME, + DETAILS + }; + + Columns getColumn(int idx) const; + + QString getTypeLabel(SqliteCreateTable::Constraint::Type type) const; + QString getTypeLabel(SqliteCreateTable::Column::Constraint::Type type) const; + + QIcon getTypeIcon(SqliteCreateTable::Constraint::Type type) const; + QIcon getTypeIcon(SqliteCreateTable::Column::Constraint::Type type) const; + + QString getDetails(SqliteCreateTable::Constraint* constr) const; + QString getDetails(SqliteCreateTable::Column::Constraint* constr) const; + + QString getPkDetails(SqliteCreateTable::Constraint* constr) const; + QString getUniqueDetails(SqliteCreateTable::Constraint* constr) const; + QString getCheckDetails(SqliteCreateTable::Constraint* constr) const; + QString getFkDetails(SqliteCreateTable::Constraint* constr) const; + + QString getPkDetails(SqliteCreateTable::Column::Constraint* constr) const; + QString getUniqueDetails(SqliteCreateTable::Column::Constraint* constr) const; + QString getCheckDetails(SqliteCreateTable::Column::Constraint* constr) const; + QString getFkDetails(SqliteCreateTable::Column::Constraint* constr) const; + QString getNotNullDetails(SqliteCreateTable::Column::Constraint* constr) const; + QString getCollateDetails(SqliteCreateTable::Column::Constraint* constr) const; + QString getDefaultDetails(SqliteCreateTable::Column::Constraint* constr) const; + + QString getConstrDetails(SqliteCreateTable::Constraint* constr, int tokenOffset) const; + QString getConstrDetails(SqliteCreateTable::Column::Constraint* constr, int tokenOffset) const; + QString getConstrDetails(const TokenList& constrTokens, int tokenOffset) const; + + QPointer<SqliteCreateTable> createTable; + + signals: + + public slots: + void updateModel(); + +}; + +#endif // CONSTRAINTTABMODEL_H diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/ddlhistorywindow.cpp b/SQLiteStudio3/guiSQLiteStudio/windows/ddlhistorywindow.cpp new file mode 100644 index 0000000..3aeccfb --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/ddlhistorywindow.cpp @@ -0,0 +1,150 @@ +#include "ddlhistorywindow.h" +#include "ui_ddlhistorywindow.h" +#include "services/config.h" +#include "common/userinputfilter.h" +#include "common/extlineedit.h" +#include "dblistmodel.h" +#include "ddlhistorymodel.h" +#include "common/unused.h" +#include "iconmanager.h" +#include <QDate> +#include <QLineEdit> +#include <QStringListModel> + +DdlHistoryWindow::DdlHistoryWindow(QWidget *parent) : + MdiChild(parent), + ui(new Ui::DdlHistoryWindow) +{ + init(); +} + +DdlHistoryWindow::~DdlHistoryWindow() +{ + delete ui; +} + +void DdlHistoryWindow::changeEvent(QEvent *e) +{ + QWidget::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + ui->retranslateUi(this); + break; + default: + break; + } +} + +void DdlHistoryWindow::init() +{ + ui->setupUi(this); + + dataModel = CFG->getDdlHistoryModel(); + + dbListModel = new QStringListModel(this); + QStringList dbList = dataModel->getDbNames(); + dbList.prepend(""); + dbListModel->setStringList(dbList); + ui->comboBox->setModel(dbListModel); + ui->comboBox->setCurrentIndex(-1); + connect(ui->comboBox, SIGNAL(currentTextChanged(QString)), this, SLOT(applyFilter(QString))); + connect(dataModel, SIGNAL(refreshed()), this, SLOT(refreshDbList())); + + ui->tableView->setModel(dataModel); + ui->tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + ui->tableView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + ui->tableView->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + ui->tableView->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents); + + connect(ui->tableView->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), + this, SLOT(activated(QModelIndex,QModelIndex))); +} + +void DdlHistoryWindow::activated(const QModelIndex& current, const QModelIndex& previous) +{ + UNUSED(previous); + + int row = current.row(); + QString dbName = dataModel->data(dataModel->index(row, 0)).toString(); + QString dbFile = dataModel->data(dataModel->index(row, 1)).toString(); + QString dateString = dataModel->data(dataModel->index(row, 2)).toString(); + QDate date = QDate::fromString(dateString, "yyyy-MM-dd"); + + static const QString templ = tr("-- Queries executed on database %1 (%2)\n" + "-- Date and time of execution: %3\n" + "%4"); + + QStringList contentEntries; + QList<Config::DdlHistoryEntryPtr> entries = CFG->getDdlHistoryFor(dbName, dbFile, date); + foreach (Config::DdlHistoryEntryPtr entry, entries) + { + contentEntries << templ.arg(entry->dbName).arg(entry->dbFile) + .arg(entry->timestamp.toString("yyyy-MM-dd HH:mm:ss")) + .arg(entry->queries); + } + + ui->ddlEdit->setPlainText(contentEntries.join("\n\n")); +} + +void DdlHistoryWindow::applyFilter(const QString& filterValue) +{ + dataModel->setDbNameForFilter(filterValue); +} + +void DdlHistoryWindow::refreshDbList() +{ + QStringList dbList = dataModel->getDbNames(); + dbList.prepend(""); + dbListModel->setStringList(dbList); +} + +bool DdlHistoryWindow::restoreSessionNextTime() +{ + return false; +} + +QVariant DdlHistoryWindow::saveSession() +{ + return QVariant(); +} + +bool DdlHistoryWindow::restoreSession(const QVariant& sessionValue) +{ + UNUSED(sessionValue); + return true; +} + +Icon* DdlHistoryWindow::getIconNameForMdiWindow() +{ + return ICONS.DDL_HISTORY; +} + +QString DdlHistoryWindow::getTitleForMdiWindow() +{ + return tr("DDL history"); +} + +void DdlHistoryWindow::createActions() +{ +} + +void DdlHistoryWindow::setupDefShortcuts() +{ +} + +QToolBar* DdlHistoryWindow::getToolBar(int toolbar) const +{ + UNUSED(toolbar); + return nullptr; +} + + +bool DdlHistoryWindow::isUncommited() const +{ + return false; +} + +QString DdlHistoryWindow::getQuitUncommitedConfirmMessage() const +{ + return QString(); +} diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/ddlhistorywindow.h b/SQLiteStudio3/guiSQLiteStudio/windows/ddlhistorywindow.h new file mode 100644 index 0000000..16a07ce --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/ddlhistorywindow.h @@ -0,0 +1,54 @@ +#ifndef DDLHISTORYWINDOW_H +#define DDLHISTORYWINDOW_H + +#include "mdichild.h" + +namespace Ui { + class DdlHistoryWindow; +} + +class QStringListModel; +class UserInputFilter; +class DdlHistoryModel; + +class GUI_API_EXPORT DdlHistoryWindow : public MdiChild +{ + Q_OBJECT + + public: + enum ToolBar + { + }; + + explicit DdlHistoryWindow(QWidget *parent = 0); + ~DdlHistoryWindow(); + + bool restoreSessionNextTime(); + bool isUncommited() const; + QString getQuitUncommitedConfirmMessage() const; + + protected: + void changeEvent(QEvent *e); + QVariant saveSession(); + bool restoreSession(const QVariant& sessionValue); + Icon* getIconNameForMdiWindow(); + QString getTitleForMdiWindow(); + void createActions(); + void setupDefShortcuts(); + QToolBar* getToolBar(int toolbar) const; + + private: + void init(); + + Ui::DdlHistoryWindow *ui = nullptr; + QStringListModel* dbListModel = nullptr; + DdlHistoryModel* dataModel = nullptr; + UserInputFilter* filter = nullptr; + + private slots: + void activated(const QModelIndex& current, const QModelIndex& previous); + void applyFilter(const QString& filterValue); + void refreshDbList(); +}; + +#endif // DDLHISTORYWINDOW_H diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/ddlhistorywindow.ui b/SQLiteStudio3/guiSQLiteStudio/windows/ddlhistorywindow.ui new file mode 100644 index 0000000..6591198 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/ddlhistorywindow.ui @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DdlHistoryWindow</class> + <widget class="QWidget" name="DdlHistoryWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>749</width> + <height>599</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QWidget" name="widget" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Filter by database:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="comboBox"> + <property name="minimumSize"> + <size> + <width>200</width> + <height>0</height> + </size> + </property> + <property name="editable"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <widget class="QTableView" name="tableView"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="verticalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <property name="horizontalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + </widget> + <widget class="SqlView" name="ddlEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>2</verstretch> + </sizepolicy> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>SqlView</class> + <extends>QPlainTextEdit</extends> + <header>sqlview.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/editorwindow.cpp b/SQLiteStudio3/guiSQLiteStudio/windows/editorwindow.cpp new file mode 100644 index 0000000..7856a5e --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/editorwindow.cpp @@ -0,0 +1,652 @@ +#include "editorwindow.h" +#include "ui_editorwindow.h" +#include "uiutils.h" +#include "datagrid/sqlquerymodel.h" +#include "iconmanager.h" +#include "dblistmodel.h" +#include "services/notifymanager.h" +#include "dbtree/dbtree.h" +#include "datagrid/sqlqueryitem.h" +#include "datagrid/sqlqueryview.h" +#include "mainwindow.h" +#include "mdiarea.h" +#include "common/unused.h" +#include "common/extaction.h" +#include "uiconfig.h" +#include "services/config.h" +#include "parser/lexer.h" +#include "common/utils_sql.h" +#include "parser/parser.h" +#include <QComboBox> +#include <QDebug> +#include <QStringListModel> +#include <QActionGroup> +#include <QMessageBox> +#include <dbobjectdialogs.h> +#include <dialogs/exportdialog.h> + +CFG_KEYS_DEFINE(EditorWindow) +EditorWindow::ResultsDisplayMode EditorWindow::resultsDisplayMode; +QHash<EditorWindow::Action,QAction*> EditorWindow::staticActions; +QHash<EditorWindow::ActionGroup,QActionGroup*> EditorWindow::staticActionGroups; + +EditorWindow::EditorWindow(QWidget *parent) : + MdiChild(parent), + ui(new Ui::EditorWindow) +{ + ui->setupUi(this); + init(); +} + +EditorWindow::EditorWindow(const EditorWindow& editor) : + MdiChild(editor.parentWidget()), + ui(new Ui::EditorWindow) +{ + ui->setupUi(this); + init(); + ui->sqlEdit->setAutoCompletion(false); + ui->sqlEdit->setPlainText(editor.ui->sqlEdit->toPlainText()); + ui->sqlEdit->setAutoCompletion(true); +} + +EditorWindow::~EditorWindow() +{ + delete ui; +} + +void EditorWindow::staticInit() +{ + qRegisterMetaType<EditorWindow>("EditorWindow"); + resultsDisplayMode = ResultsDisplayMode::BELOW_QUERY; + loadTabsMode(); + createStaticActions(); +} + +void EditorWindow::insertAction(ExtActionPrototype* action, EditorWindow::ToolBar toolbar) +{ + return ExtActionContainer::insertAction<EditorWindow>(action, toolbar); +} + +void EditorWindow::insertActionBefore(ExtActionPrototype* action, EditorWindow::Action beforeAction, EditorWindow::ToolBar toolbar) +{ + return ExtActionContainer::insertActionBefore<EditorWindow>(action, beforeAction, toolbar); +} + +void EditorWindow::insertActionAfter(ExtActionPrototype* action, EditorWindow::Action afterAction, EditorWindow::ToolBar toolbar) +{ + return ExtActionContainer::insertActionAfter<EditorWindow>(action, afterAction, toolbar); +} + +void EditorWindow::removeAction(ExtActionPrototype* action, EditorWindow::ToolBar toolbar) +{ + ExtActionContainer::removeAction<EditorWindow>(action, toolbar); +} + +void EditorWindow::init() +{ + setFocusProxy(ui->sqlEdit); + updateResultsDisplayMode(); + + resultsModel = new SqlQueryModel(this); + ui->dataView->init(resultsModel); + + createDbCombo(); + initActions(); + updateShortcutTips(); + + Db* currentDb = getCurrentDb(); + resultsModel->setDb(currentDb); + ui->sqlEdit->setDb(currentDb); + + connect(resultsModel, SIGNAL(executionSuccessful()), this, SLOT(executionSuccessful())); + connect(resultsModel, SIGNAL(executionFailed(QString)), this, SLOT(executionFailed(QString))); + connect(resultsModel, SIGNAL(totalRowsAndPagesAvailable()), this, SLOT(totalRowsAndPagesAvailable())); + + // SQL history list + ui->historyList->setModel(CFG->getSqlHistoryModel()); + ui->historyList->resizeColumnToContents(1); + connect(ui->historyList->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), + this, SLOT(historyEntrySelected(QModelIndex,QModelIndex))); + connect(ui->historyList, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(historyEntryActivated(QModelIndex))); + + updateState(); +} + +void EditorWindow::loadTabsMode() +{ + QString tabsString = CFG_UI.General.SqlEditorTabs.get(); + if (tabsString == "SEPARATE_TAB") + resultsDisplayMode = ResultsDisplayMode::SEPARATE_TAB; + else if (tabsString == "BELOW_QUERY") + resultsDisplayMode = ResultsDisplayMode::BELOW_QUERY; +} + +void EditorWindow::createStaticActions() +{ + staticActions[RESULTS_IN_TAB] = new ExtAction(ICONS.RESULTS_IN_TAB, tr("Results in the separate tab"), MainWindow::getInstance()); + staticActions[RESULTS_BELOW] = new ExtAction(ICONS.RESULTS_BELOW, tr("Results below the query"), MainWindow::getInstance()); + + staticActionGroups[ActionGroup::RESULTS_POSITIONING] = new QActionGroup(MainWindow::getInstance()); + staticActionGroups[ActionGroup::RESULTS_POSITIONING]->addAction(staticActions[RESULTS_IN_TAB]); + staticActionGroups[ActionGroup::RESULTS_POSITIONING]->addAction(staticActions[RESULTS_BELOW]); + + connect(staticActions[RESULTS_BELOW], &QAction::triggered, [=]() + { + resultsDisplayMode = ResultsDisplayMode::BELOW_QUERY; + CFG_UI.General.SqlEditorTabs.set("BELOW_QUERY"); + }); + connect(staticActions[RESULTS_IN_TAB], &QAction::triggered, [=]() + { + resultsDisplayMode = ResultsDisplayMode::SEPARATE_TAB; + CFG_UI.General.SqlEditorTabs.set("SEPARATE_TAB"); + }); + + staticActions[RESULTS_BELOW]->setCheckable(true); + staticActions[RESULTS_IN_TAB]->setCheckable(true); + if (resultsDisplayMode == ResultsDisplayMode::BELOW_QUERY) + staticActions[RESULTS_BELOW]->setChecked(true); + else + staticActions[RESULTS_IN_TAB]->setChecked(true); +} + +Icon* EditorWindow::getIconNameForMdiWindow() +{ + return ICONS.OPEN_SQL_EDITOR; +} + +QString EditorWindow::getTitleForMdiWindow() +{ + QStringList existingNames = MainWindow::getInstance()->getMdiArea()->getWindowTitles(); + QString title = tr("SQL editor %1").arg(sqlEditorNum++); + while (existingNames.contains(title)) + title = tr("SQL editor %1").arg(sqlEditorNum++); + + return title; +} + +QSize EditorWindow::sizeHint() const +{ + return QSize(500, 400); +} + +QAction* EditorWindow::getAction(EditorWindow::Action action) +{ + switch (action) + { + case RESULTS_BELOW: + case RESULTS_IN_TAB: + { + if (!staticActions.contains(action)) + return nullptr; + + return staticActions.value(action); + } + default: + break; + } + + return ExtActionContainer::getAction(action); +} + +QString EditorWindow::getQueryToExecute(bool doSelectCurrentQuery) +{ + QString sql; + if (ui->sqlEdit->textCursor().hasSelection()) + { + sql = ui->sqlEdit->textCursor().selectedText(); + fixTextCursorSelectedText(sql); + } + else if (CFG_UI.General.ExecuteCurrentQueryOnly.get()) + { + ui->sqlEdit->saveSelection(); + selectCurrentQuery(true); + sql = ui->sqlEdit->textCursor().selectedText(); + fixTextCursorSelectedText(sql); + if (!doSelectCurrentQuery) + ui->sqlEdit->restoreSelection(); + } + else + { + sql = ui->sqlEdit->toPlainText(); + } + return sql; +} + +bool EditorWindow::setCurrentDb(Db *db) +{ + if (dbCombo->findText(db->getName()) == -1) + return false; + + dbCombo->setCurrentText(db->getName()); + return true; +} + +void EditorWindow::setContents(const QString &sql) +{ + ui->sqlEdit->setPlainText(sql); +} + +QString EditorWindow::getContents() const +{ + return ui->sqlEdit->toPlainText(); +} + +void EditorWindow::execute() +{ + execQuery(); +} + +QToolBar* EditorWindow::getToolBar(int toolbar) const +{ + UNUSED(toolbar); + return ui->toolBar; +} + +SqlEditor* EditorWindow::getEditor() const +{ + return ui->sqlEdit; +} + +QVariant EditorWindow::saveSession() +{ + QHash<QString,QVariant> sessionValue; + sessionValue["query"] = ui->sqlEdit->toPlainText(); + sessionValue["curPos"] = ui->sqlEdit->textCursor().position(); + + Db* db = getCurrentDb(); + if (db) + sessionValue["db"] = db->getName(); + + return sessionValue; +} + +bool EditorWindow::restoreSession(const QVariant& sessionValue) +{ + QHash<QString, QVariant> value = sessionValue.toHash(); + if (value.size() == 0) + return true; + + if (value.contains("query")) + { + ui->sqlEdit->setAutoCompletion(false); + ui->sqlEdit->setPlainText(value["query"].toString()); + ui->sqlEdit->setAutoCompletion(true); + } + + if (value.contains("curPos")) + { + QTextCursor cursor = ui->sqlEdit->textCursor(); + cursor.setPosition(value["curPos"].toInt()); + ui->sqlEdit->setTextCursor(cursor); + } + + if (value.contains("db")) + { + dbCombo->setCurrentText(value["db"].toString()); + if (dbCombo->currentText().isEmpty() && dbCombo->count() > 0) + dbCombo->setCurrentIndex(0); + } + return true; +} + +void EditorWindow::changeEvent(QEvent *e) +{ + QWidget::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + ui->retranslateUi(this); + break; + default: + break; + } +} + +Db* EditorWindow::getCurrentDb() +{ + return dbComboModel->getDb(dbCombo->currentIndex()); +} + +void EditorWindow::updateResultsDisplayMode() +{ + switch (resultsDisplayMode) + { + case EditorWindow::ResultsDisplayMode::SEPARATE_TAB: + { + // Remove old view + ui->resultsContainer->hide(); + ui->resultsContainer->layout()->removeWidget(ui->resultsFrame); + + // Add new view + ui->tabWidget->insertTab(1, ui->results, tr("Results")); + ui->resultsFrame->setParent(ui->results); + ui->results->layout()->addWidget(ui->resultsFrame); + break; + } + case EditorWindow::ResultsDisplayMode::BELOW_QUERY: + { + int currIdx = ui->tabWidget->currentIndex(); + + // Remove old view + ui->tabWidget->removeTab(1); + ui->results->layout()->removeWidget(ui->resultsFrame); + + // Add new view + ui->resultsContainer->show(); + ui->resultsFrame->setParent(ui->resultsContainer); + ui->resultsContainer->layout()->addWidget(ui->resultsFrame); + + // If results tab was selected before, switch to first tab + if (currIdx == 1) + { + ui->tabWidget->setCurrentIndex(0); + ui->dataView->setCurrentIndex(0); + ui->dataView->getGridView()->setFocus(); + } + break; + } + } +} + +void EditorWindow::createActions() +{ + // SQL editor toolbar + createAction(EXEC_QUERY, ICONS.EXEC_QUERY, tr("Execute query"), this, SLOT(execQuery()), ui->toolBar, ui->sqlEdit); + createAction(EXPLAIN_QUERY, ICONS.EXPLAIN_QUERY, tr("Explain query"), this, SLOT(explainQuery()), ui->toolBar, ui->sqlEdit); + ui->toolBar->addSeparator(); + ui->toolBar->addAction(ui->sqlEdit->getAction(SqlEditor::FORMAT_SQL)); + createAction(CLEAR_HISTORY, ICONS.CLEAR_HISTORY, tr("Clear execution history", "sql editor"), this, SLOT(clearHistory()), ui->toolBar); + ui->toolBar->addSeparator(); + createAction(EXPORT_RESULTS, ICONS.TABLE_EXPORT, tr("Export results", "sql editor"), this, SLOT(exportResults()), ui->toolBar); + ui->toolBar->addSeparator(); + createAction(CREATE_VIEW_FROM_QUERY, ICONS.VIEW_ADD, tr("Create view from query", "sql editor"), this, SLOT(createViewFromQuery()), ui->toolBar); + ui->toolBar->addSeparator(); + ui->toolBar->addAction(ui->sqlEdit->getAction(SqlEditor::SAVE_SQL_FILE)); + ui->toolBar->addAction(ui->sqlEdit->getAction(SqlEditor::OPEN_SQL_FILE)); + ui->toolBar->addSeparator(); + actionMap[CURRENT_DB] = ui->toolBar->addWidget(dbCombo); + ui->toolBar->addSeparator(); + ui->toolBar->addAction(staticActions[RESULTS_IN_TAB]); + ui->toolBar->addAction(staticActions[RESULTS_BELOW]); + createAction(PREV_DB, tr("Previous database"), this, SLOT(prevDb()), this); + createAction(NEXT_DB, tr("Next database"), this, SLOT(nextDb()), this); + + // Other actions + createAction(SHOW_NEXT_TAB, tr("Show next tab", "sql editor"), this, SLOT(showNextTab()), this); + createAction(SHOW_PREV_TAB, tr("Show previous tab", "sql editor"), this, SLOT(showPrevTab()), this); + createAction(FOCUS_RESULTS_BELOW, tr("Focus results below", "sql editor"), this, SLOT(focusResultsBelow()), this); + createAction(FOCUS_EDITOR_ABOVE, tr("Focus SQL editor above", "sql editor"), this, SLOT(focusEditorAbove()), this); + + // Static action triggers + connect(staticActions[RESULTS_IN_TAB], SIGNAL(triggered()), this, SLOT(updateResultsDisplayMode())); + connect(staticActions[RESULTS_BELOW], SIGNAL(triggered()), this, SLOT(updateResultsDisplayMode())); +} + +void EditorWindow::createDbCombo() +{ + dbCombo = new QComboBox(this); + dbComboModel = new DbListModel(this); + dbComboModel->setCombo(dbCombo); + dbCombo->setModel(dbComboModel); + dbCombo->setEditable(false); + dbCombo->setFixedWidth(100); + connect(dbCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(dbChanged())); +} + +void EditorWindow::setupDefShortcuts() +{ + // Widget context + setShortcutContext({EXEC_QUERY, EXEC_QUERY, SHOW_NEXT_TAB, SHOW_PREV_TAB, FOCUS_RESULTS_BELOW, + FOCUS_EDITOR_ABOVE}, Qt::WidgetWithChildrenShortcut); + + BIND_SHORTCUTS(EditorWindow, Action); +} + +void EditorWindow::selectCurrentQuery(bool fallBackToPreviousIfNecessary) +{ + Dialect dialect = Dialect::Sqlite3; + Db* db = getCurrentDb(); + if (db && db->isValid()) + dialect = db->getDialect(); + + QTextCursor cursor = ui->sqlEdit->textCursor(); + int pos = cursor.position(); + int queryStartPos; + QString contents = ui->sqlEdit->toPlainText(); + QString query = getQueryWithPosition(contents, pos, dialect, &queryStartPos); + TokenList tokens = Lexer::tokenize(query, dialect); + tokens.trim(); + tokens.trimRight(Token::OPERATOR, ";"); + + if (tokens.size() == 0 && fallBackToPreviousIfNecessary) + { + // Fallback + pos = contents.lastIndexOf(";", pos - 1); + if (pos > -1) + { + query = getQueryWithPosition(contents, pos, dialect, &queryStartPos); + tokens = Lexer::tokenize(query, dialect); + tokens.trim(); + tokens.trimRight(Token::OPERATOR, ";"); + } + } + + if (tokens.size() == 0) + { + qWarning() << "No tokens to select in EditorWindow::selectCurrentQuery()."; + return; + } + + cursor.clearSelection(); + cursor.setPosition(tokens.first()->start + queryStartPos); + cursor.setPosition(tokens.last()->end + 1 + queryStartPos, QTextCursor::KeepAnchor); + ui->sqlEdit->setTextCursor(cursor); +} + +void EditorWindow::updateShortcutTips() +{ + if (actionMap.contains(PREV_DB) && actionMap.contains(NEXT_DB)) + { + QString prevDbKey = actionMap[PREV_DB]->shortcut().toString(QKeySequence::NativeText); + QString nextDbKey = actionMap[NEXT_DB]->shortcut().toString(QKeySequence::NativeText); + dbCombo->setToolTip(tr("Active database (%1/%2)").arg(prevDbKey).arg(nextDbKey)); + } +} + +void EditorWindow::execQuery(bool explain) +{ + QString sql = getQueryToExecute(true); + resultsModel->setDb(getCurrentDb()); + resultsModel->setExplainMode(explain); + resultsModel->setQuery(sql); + ui->dataView->refreshData(); + updateState(); + + if (resultsDisplayMode == ResultsDisplayMode::SEPARATE_TAB) + { + ui->tabWidget->setCurrentIndex(1); + ui->dataView->setCurrentIndex(0); + ui->dataView->getGridView()->setFocus(); + } +} + +void EditorWindow::explainQuery() +{ + execQuery(true); +} + +void EditorWindow::dbChanged() +{ + Db* currentDb = getCurrentDb(); + ui->sqlEdit->setDb(currentDb); +} + +void EditorWindow::executionSuccessful() +{ + double secs = ((double)resultsModel->getExecutionTime()) / 1000; + QString time = QString::number(secs, 'f', 3); + notifyInfo(tr("Query finished in %2 second(s).").arg(time)); + + lastQueryHistoryId = CFG->addSqlHistory(resultsModel->getQuery(), resultsModel->getDb()->getName(), resultsModel->getExecutionTime(), 0); + + // If we added first history entry - resize dates column. + if (ui->historyList->model()->rowCount() == 1) + ui->historyList->resizeColumnToContents(1); + + Db* currentDb = getCurrentDb(); + if (currentDb && resultsModel->wasSchemaModified()) + DBTREE->refreshSchema(currentDb); + + lastSuccessfulQuery = resultsModel->getQuery(); + + updateState(); +} + +void EditorWindow::executionFailed(const QString &errorText) +{ + notifyError(errorText); + updateState(); +} + +void EditorWindow::totalRowsAndPagesAvailable() +{ + qint64 rowsReturned = resultsModel->getTotalRowsReturned(); + qint64 rowsAffected = resultsModel->getTotalRowsAffected(); + qint64 rows; + if (rowsReturned > 0) + rows = rowsReturned; + else + rows = rowsAffected; + + CFG->updateSqlHistory(lastQueryHistoryId, resultsModel->getQuery(), resultsModel->getDb()->getName(), resultsModel->getExecutionTime(), rows); +} + +void EditorWindow::prevDb() +{ + int idx = dbCombo->currentIndex() - 1; + if (idx < 0) + return; + + dbCombo->setCurrentIndex(idx); +} + +void EditorWindow::nextDb() +{ + int idx = dbCombo->currentIndex() + 1; + if (idx >= dbCombo->count()) + return; + + dbCombo->setCurrentIndex(idx); +} + +void EditorWindow::showNextTab() +{ + int tabIdx = ui->tabWidget->currentIndex(); + tabIdx++; + ui->tabWidget->setCurrentIndex(tabIdx); +} + +void EditorWindow::showPrevTab() +{ + int tabIdx = ui->tabWidget->currentIndex(); + tabIdx--; + ui->tabWidget->setCurrentIndex(tabIdx); +} + +void EditorWindow::focusResultsBelow() +{ + if (resultsDisplayMode != ResultsDisplayMode::BELOW_QUERY) + return; + + ui->dataView->setCurrentIndex(0); + ui->dataView->getGridView()->setFocus(); +} + +void EditorWindow::focusEditorAbove() +{ + if (resultsDisplayMode != ResultsDisplayMode::BELOW_QUERY) + return; + + ui->sqlEdit->setFocus(); +} + +void EditorWindow::historyEntrySelected(const QModelIndex& current, const QModelIndex& previous) +{ + UNUSED(previous); + QString sql = ui->historyList->model()->index(current.row(), 4).data().toString(); + ui->historyContents->setPlainText(sql); +} + +void EditorWindow::historyEntryActivated(const QModelIndex& current) +{ + QString sql = ui->historyList->model()->index(current.row(), 4).data().toString(); + ui->sqlEdit->setPlainText(sql); + ui->tabWidget->setCurrentIndex(0); +} + +void EditorWindow::clearHistory() +{ + QMessageBox::StandardButton res = QMessageBox::question(this, tr("Clear execution history"), tr("Are you sure you want to erase the entire SQL execution history? " + "This cannot be undone.")); + if (res != QMessageBox::Yes) + return; + + CFG->clearSqlHistory(); +} + +void EditorWindow::exportResults() +{ + if (!ExportManager::isAnyPluginAvailable()) + { + notifyError(tr("Cannot export, because no export plugin is loaded.")); + return; + } + + QString query = lastSuccessfulQuery.isEmpty() ? getQueryToExecute() : lastSuccessfulQuery; + QStringList queries = splitQueries(query, getCurrentDb()->getDialect(), false); + if (queries.size() == 0) + { + qWarning() << "No queries after split in EditorWindow::exportResults()"; + return; + } + + ExportDialog dialog(this); + dialog.setQueryMode(getCurrentDb(), queries.last().trimmed()); + dialog.exec(); +} + +void EditorWindow::createViewFromQuery() +{ + if (!getCurrentDb()) + { + notifyError(tr("No database selected in the SQL editor. Cannot create a view for unknown database.")); + return; + } + + QString sql = getQueryToExecute(true); + DbObjectDialogs dialogs(getCurrentDb()); + dialogs.addView(sql); +} + +void EditorWindow::updateState() +{ + bool executionInProgress = resultsModel->isExecutionInProgress(); + actionMap[CURRENT_DB]->setEnabled(!executionInProgress); + actionMap[EXEC_QUERY]->setEnabled(!executionInProgress); + actionMap[EXPLAIN_QUERY]->setEnabled(!executionInProgress); +} + +int qHash(EditorWindow::ActionGroup actionGroup) +{ + return static_cast<int>(actionGroup); +} + + +bool EditorWindow::isUncommited() const +{ + return ui->dataView->isUncommited(); +} + +QString EditorWindow::getQuitUncommitedConfirmMessage() const +{ + return tr("Editor window \"%1\" has uncommited data.").arg(getMdiWindow()->windowTitle()); +} diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/editorwindow.h b/SQLiteStudio3/guiSQLiteStudio/windows/editorwindow.h new file mode 100644 index 0000000..0052a74 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/editorwindow.h @@ -0,0 +1,155 @@ +#ifndef EDITOR_H +#define EDITOR_H + +#include "db/db.h" +#include "mdichild.h" +#include "common/extactioncontainer.h" +#include "guiSQLiteStudio_global.h" +#include <QWidget> + +namespace Ui { + class EditorWindow; +} + +class SqlQueryModel; +class QComboBox; +class QActionGroup; +class DbListModel; +class QLabel; +class QLineEdit; +class ExtLineEdit; +class IntValidator; +class FormView; +class SqlQueryItem; +class SqlEditor; + +CFG_KEY_LIST(EditorWindow, QObject::tr("SQL editor window"), + CFG_KEY_ENTRY(EXEC_QUERY, Qt::Key_F9, QObject::tr("Execute query")) + CFG_KEY_ENTRY(EXPLAIN_QUERY, Qt::Key_F8, QObject::tr("Execute \"EXPLAIN\" query")) + CFG_KEY_ENTRY(PREV_DB, Qt::CTRL + Qt::Key_Up, QObject::tr("Switch current working database to previous on the list")) + CFG_KEY_ENTRY(NEXT_DB, Qt::CTRL + Qt::Key_Down, QObject::tr("Switch current working database to next on the list")) + CFG_KEY_ENTRY(SHOW_NEXT_TAB, Qt::ALT + Qt::Key_Right, QObject::tr("Go to next editor tab")) + CFG_KEY_ENTRY(SHOW_PREV_TAB, Qt::ALT + Qt::Key_Left, QObject::tr("Go to previous editor tab")) + CFG_KEY_ENTRY(FOCUS_RESULTS_BELOW, Qt::ALT + Qt::Key_PageDown, QObject::tr("Move keyboard input focus to the results view below")) + CFG_KEY_ENTRY(FOCUS_EDITOR_ABOVE, Qt::ALT + Qt::Key_PageUp, QObject::tr("Move keyboard input focus to the SQL editor above")) +) + +class GUI_API_EXPORT EditorWindow : public MdiChild +{ + Q_OBJECT + Q_ENUMS(Action) + + public: + enum class ResultsDisplayMode + { + SEPARATE_TAB = 0, + BELOW_QUERY = 1 + }; + + enum Action + { + EXEC_QUERY, + EXPLAIN_QUERY, + RESULTS_IN_TAB, + RESULTS_BELOW, + CURRENT_DB, + NEXT_DB, + PREV_DB, + SHOW_NEXT_TAB, + SHOW_PREV_TAB, + FOCUS_RESULTS_BELOW, + FOCUS_EDITOR_ABOVE, + CLEAR_HISTORY, + EXPORT_RESULTS, + CREATE_VIEW_FROM_QUERY + }; + + enum ToolBar + { + TOOLBAR_MAIN + }; + + enum class ActionGroup + { + RESULTS_POSITIONING + }; + + explicit EditorWindow(QWidget *parent = 0); + EditorWindow(const EditorWindow& editor); + ~EditorWindow(); + + static void staticInit(); + static void insertAction(ExtActionPrototype* action, ToolBar toolbar = TOOLBAR_MAIN); + static void insertActionBefore(ExtActionPrototype* action, Action beforeAction, ToolBar toolbar = TOOLBAR_MAIN); + static void insertActionAfter(ExtActionPrototype* action, Action afterAction, ToolBar toolbar = TOOLBAR_MAIN); + static void removeAction(ExtActionPrototype* action, ToolBar toolbar = TOOLBAR_MAIN); + + QSize sizeHint() const; + QAction* getAction(Action action); + QString getQueryToExecute(bool doSelectCurrentQuery = false); + bool setCurrentDb(Db* db); + void setContents(const QString& sql); + QString getContents() const; + void execute(); + QToolBar* getToolBar(int toolbar) const; + SqlEditor* getEditor() const; + bool isUncommited() const; + QString getQuitUncommitedConfirmMessage() const; + + protected: + void changeEvent(QEvent *e); + QVariant saveSession(); + bool restoreSession(const QVariant& sessionValue); + Icon* getIconNameForMdiWindow(); + QString getTitleForMdiWindow(); + Db* getCurrentDb(); + + private: + static void createStaticActions(); + static void loadTabsMode(); + + void init(); + void createActions(); + void createDbCombo(); + void setupDefShortcuts(); + void selectCurrentQuery(bool fallBackToPreviousIfNecessary = false); + void updateShortcutTips(); + + static ResultsDisplayMode resultsDisplayMode; + static QHash<Action,QAction*> staticActions; + static QHash<ActionGroup,QActionGroup*> staticActionGroups; + + Ui::EditorWindow *ui = nullptr; + SqlQueryModel* resultsModel = nullptr; + QHash<ActionGroup,QActionGroup*> actionGroups; + QComboBox* dbCombo = nullptr; + DbListModel* dbComboModel = nullptr; + int sqlEditorNum = 1; + qint64 lastQueryHistoryId = 0; + QString lastSuccessfulQuery; + + private slots: + void execQuery(bool explain = false); + void explainQuery(); + void dbChanged(); + void executionSuccessful(); + void executionFailed(const QString& errorText); + void totalRowsAndPagesAvailable(); + void updateResultsDisplayMode(); + void prevDb(); + void nextDb(); + void showNextTab(); + void showPrevTab(); + void focusResultsBelow(); + void focusEditorAbove(); + void historyEntrySelected(const QModelIndex& current, const QModelIndex& previous); + void historyEntryActivated(const QModelIndex& current); + void clearHistory(); + void exportResults(); + void createViewFromQuery(); + void updateState(); +}; + +GUI_API_EXPORT int qHash(EditorWindow::ActionGroup action); + +#endif // EDITOR_H diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/editorwindow.ui b/SQLiteStudio3/guiSQLiteStudio/windows/editorwindow.ui new file mode 100644 index 0000000..b0d598b --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/editorwindow.ui @@ -0,0 +1,137 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>EditorWindow</class> + <widget class="QWidget" name="EditorWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>502</width> + <height>325</height> + </rect> + </property> + <property name="windowTitle"> + <string>SQL editor</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="margin"> + <number>0</number> + </property> + <item> + <widget class="QToolBar" name="toolBar"/> + </item> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="query"> + <attribute name="title"> + <string>Query</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QSplitter" name="splitter_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <widget class="SqlEditor" name="sqlEdit"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + </widget> + <widget class="QWidget" name="resultsContainer" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <property name="margin"> + <number>0</number> + </property> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="results"> + <attribute name="title"> + <string notr="true">Results</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QWidget" name="resultsFrame" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <property name="margin"> + <number>0</number> + </property> + <item> + <widget class="DataView" name="dataView"> + <property name="tabPosition"> + <enum>QTabWidget::South</enum> + </property> + <property name="currentIndex"> + <number>-1</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="history"> + <attribute name="title"> + <string>History</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <widget class="QTableView" name="historyList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + </widget> + <widget class="SqlView" name="historyContents"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>DataView</class> + <extends>QTabWidget</extends> + <header>dataview.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>SqlEditor</class> + <extends>QPlainTextEdit</extends> + <header>sqleditor.h</header> + </customwidget> + <customwidget> + <class>SqlView</class> + <extends>QPlainTextEdit</extends> + <header>sqlview.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> 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."); +} diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/functionseditor.h b/SQLiteStudio3/guiSQLiteStudio/windows/functionseditor.h new file mode 100644 index 0000000..0fa496e --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/functionseditor.h @@ -0,0 +1,109 @@ +#ifndef FUNCTIONSEDITOR_H +#define FUNCTIONSEDITOR_H + +#include "mdichild.h" +#include "common/extactioncontainer.h" +#include "services/config.h" +#include "services/functionmanager.h" +#include <QItemSelection> +#include <QSortFilterProxyModel> + +namespace Ui { +class FunctionsEditor; +} + +class FunctionsEditorModel; +class ScriptingPlugin; +class SyntaxHighlighterPlugin; +class DbTreeItem; +class QTreeWidgetItem; +class QSyntaxHighlighter; +class SelectableDbModel; + +class GUI_API_EXPORT FunctionsEditor : public MdiChild +{ + Q_OBJECT + + public: + enum Action + { + COMMIT, + ROLLBACK, + ADD, + DELETE, + ARG_ADD, + ARG_EDIT, + ARG_DEL, + ARG_MOVE_UP, + ARG_MOVE_DOWN, + HELP + }; + + enum ToolBar + { + TOOLBAR + }; + + explicit FunctionsEditor(QWidget *parent = 0); + ~FunctionsEditor(); + + bool restoreSessionNextTime(); + bool isUncommited() const; + QString getQuitUncommitedConfirmMessage() const; + + protected: + QVariant saveSession(); + bool restoreSession(const QVariant &sessionValue); + Icon* getIconNameForMdiWindow(); + QString getTitleForMdiWindow(); + void createActions(); + void setupDefShortcuts(); + QToolBar* getToolBar(int toolbar) const; + + private: + void init(); + int getCurrentFunctionRow() const; + void functionDeselected(int row); + void functionSelected(int row); + void clearEdits(); + void selectFunction(int row); + void setFont(const QFont& font); + QModelIndex getSelectedArg() const; + QStringList getCurrentArgList() const; + QStringList getCurrentDatabases() const; + FunctionManager::ScriptFunction::Type getCurrentFunctionType() const; + + Ui::FunctionsEditor *ui = nullptr; + FunctionsEditorModel* model = nullptr; + QSortFilterProxyModel* functionFilterModel = nullptr; + bool currentModified = false; + QHash<QString,ScriptingPlugin*> scriptingPlugins; + QHash<QString,SyntaxHighlighterPlugin*> highlighterPlugins; + SelectableDbModel* dbListModel = nullptr; + QString currentHighlighterLang; + QSyntaxHighlighter* currentMainHighlighter = nullptr; + QSyntaxHighlighter* currentFinalHighlighter = nullptr; + QSyntaxHighlighter* currentInitHighlighter = nullptr; + bool updatesForSelection = false; + + private slots: + void commit(); + void rollback(); + void newFunction(); + void deleteFunction(); + void updateModified(); + void updateState(); + void updateCurrentFunctionState(); + void functionSelected(const QItemSelection& selected, const QItemSelection& deselected); + void addFunctionArg(); + void editFunctionArg(); + void delFunctionArg(); + void moveFunctionArgUp(); + void moveFunctionArgDown(); + void updateArgsState(); + void applyFilter(const QString& value); + void help(); + void changeFont(const QVariant& font); +}; + +#endif // FUNCTIONSEDITOR_H diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/functionseditor.ui b/SQLiteStudio3/guiSQLiteStudio/windows/functionseditor.ui new file mode 100644 index 0000000..d5d5015 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/functionseditor.ui @@ -0,0 +1,346 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FunctionsEditor</class> + <widget class="QWidget" name="FunctionsEditor"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>816</width> + <height>621</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QToolBar" name="toolBar"/> + </item> + <item> + <widget class="QWidget" name="mainWidget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QWidget" name="leftWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLineEdit" name="functionFilterEdit"> + <property name="minimumSize"> + <size> + <width>150</width> + <height>0</height> + </size> + </property> + <property name="placeholderText"> + <string>Filter funtions</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QListView" name="list"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="verticalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="rightWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>4</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout_8"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QSplitter" name="splitter_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <widget class="QWidget" name="topWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string>Function name:</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QComboBox" name="typeCombo"/> + </item> + <item row="0" column="3"> + <widget class="QLabel" name="langLabel"> + <property name="text"> + <string>Implementation language:</string> + </property> + </widget> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QLineEdit" name="nameEdit"/> + </item> + <item row="0" column="2"> + <widget class="QLabel" name="typeLabel"> + <property name="text"> + <string>Type:</string> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="QComboBox" name="langCombo"/> + </item> + <item row="4" column="0"> + <widget class="QGroupBox" name="argsGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>4</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Input arguments</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QCheckBox" name="undefArgsCheck"> + <property name="text"> + <string>Undefined</string> + </property> + </widget> + </item> + <item row="2" column="0" rowspan="5" colspan="3"> + <widget class="QListWidget" name="argsList"> + <property name="dragEnabled"> + <bool>true</bool> + </property> + <property name="dragDropMode"> + <enum>QAbstractItemView::InternalMove</enum> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QToolBar" name="argsToolBar"/> + </item> + <item row="0" column="1"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item row="4" column="1" colspan="3"> + <widget class="QGroupBox" name="databasesGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Databases</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QRadioButton" name="allDatabasesRadio"> + <property name="text"> + <string>Register in all databases</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="selDatabasesRadio"> + <property name="text"> + <string>Register in following databases:</string> + </property> + </widget> + </item> + <item> + <widget class="QTreeView" name="databasesList"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <attribute name="headerVisible"> + <bool>false</bool> + </attribute> + <attribute name="headerDefaultSectionSize"> + <number>0</number> + </attribute> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QGroupBox" name="initCodeGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>2</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Initialization code:</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QPlainTextEdit" name="initCodeEdit"/> + </item> + </layout> + </widget> + <widget class="QGroupBox" name="mainCodeGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>2</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Function implementation code:</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QPlainTextEdit" name="mainCodeEdit"/> + </item> + </layout> + </widget> + <widget class="QGroupBox" name="finalCodeGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>2</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Final step implementation code:</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_7"> + <item> + <widget class="QPlainTextEdit" name="finalCodeEdit"/> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>functionFilterEdit</tabstop> + <tabstop>list</tabstop> + <tabstop>nameEdit</tabstop> + <tabstop>typeCombo</tabstop> + <tabstop>langCombo</tabstop> + <tabstop>undefArgsCheck</tabstop> + <tabstop>argsList</tabstop> + <tabstop>allDatabasesRadio</tabstop> + <tabstop>selDatabasesRadio</tabstop> + <tabstop>databasesList</tabstop> + <tabstop>initCodeEdit</tabstop> + <tabstop>mainCodeEdit</tabstop> + <tabstop>finalCodeEdit</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/functionseditormodel.cpp b/SQLiteStudio3/guiSQLiteStudio/windows/functionseditormodel.cpp new file mode 100644 index 0000000..cf7efdf --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/functionseditormodel.cpp @@ -0,0 +1,348 @@ +#include "functionseditormodel.h" +#include "common/strhash.h" +#include "common/unused.h" +#include "services/pluginmanager.h" +#include "plugins/scriptingplugin.h" +#include "icon.h" +#include <QDebug> + +#define SETTER(X, Y) \ + if (!isValidRowIndex(row) || X == Y) \ + return; \ + \ + X = Y; \ + emitDataChanged(row); + +#define GETTER(X, Y) \ + if (!isValidRowIndex(row)) \ + return Y; \ + \ + return X; + +FunctionsEditorModel::FunctionsEditorModel(QObject *parent) : + QAbstractListModel(parent) +{ + init(); +} + +void FunctionsEditorModel::clearModified() +{ + beginResetModel(); + foreach (Function* func, functionList) + func->modified = false; + + listModified = false; + originalFunctionList = functionList; + + endResetModel(); +} + +bool FunctionsEditorModel::isModified() const +{ + if (functionList != originalFunctionList) + return true; + + foreach (Function* func, functionList) + { + if (func->modified) + return true; + } + return false; +} + +bool FunctionsEditorModel::isModified(int row) const +{ + GETTER(functionList[row]->modified, false); +} + +void FunctionsEditorModel::setModified(int row, bool modified) +{ + SETTER(functionList[row]->modified, modified); +} + +bool FunctionsEditorModel::isValid() const +{ + foreach (Function* func, functionList) + { + if (!func->valid) + return false; + } + return true; +} + +bool FunctionsEditorModel::isValid(int row) const +{ + GETTER(functionList[row]->valid, false); +} + +void FunctionsEditorModel::setValid(int row, bool valid) +{ + SETTER(functionList[row]->valid, valid); +} + +void FunctionsEditorModel::setCode(int row, const QString& code) +{ + SETTER(functionList[row]->data.code, code); +} + +QString FunctionsEditorModel::getCode(int row) const +{ + GETTER(functionList[row]->data.code, QString::null); +} + +void FunctionsEditorModel::setFinalCode(int row, const QString& code) +{ + SETTER(functionList[row]->data.finalCode, code); +} + +QString FunctionsEditorModel::getFinalCode(int row) const +{ + GETTER(functionList[row]->data.finalCode, QString::null); +} + +void FunctionsEditorModel::setInitCode(int row, const QString& code) +{ + SETTER(functionList[row]->data.initCode, code); +} + +QString FunctionsEditorModel::getInitCode(int row) const +{ + GETTER(functionList[row]->data.initCode, QString::null); +} + +void FunctionsEditorModel::setName(int row, const QString& newName) +{ + SETTER(functionList[row]->data.name, newName); +} + +QString FunctionsEditorModel::getName(int row) const +{ + GETTER(functionList[row]->data.name, QString::null); +} + +void FunctionsEditorModel::setLang(int row, const QString& lang) +{ + SETTER(functionList[row]->data.lang, lang); +} + +QString FunctionsEditorModel::getLang(int row) const +{ + GETTER(functionList[row]->data.lang, QString::null); +} + +bool FunctionsEditorModel::getUndefinedArgs(int row) const +{ + GETTER(functionList[row]->data.undefinedArgs, true); +} + +void FunctionsEditorModel::setUndefinedArgs(int row, bool value) +{ + SETTER(functionList[row]->data.undefinedArgs, value); +} + +bool FunctionsEditorModel::getAllDatabases(int row) const +{ + GETTER(functionList[row]->data.allDatabases, true); +} + +void FunctionsEditorModel::setAllDatabases(int row, bool value) +{ + SETTER(functionList[row]->data.allDatabases, value); +} + +FunctionManager::ScriptFunction::Type FunctionsEditorModel::getType(int row) const +{ + GETTER(functionList[row]->data.type, FunctionManager::ScriptFunction::SCALAR); +} + +void FunctionsEditorModel::setType(int row, FunctionManager::ScriptFunction::Type type) +{ + SETTER(functionList[row]->data.type, type); +} + +bool FunctionsEditorModel::isAggregate(int row) const +{ + GETTER(functionList[row]->data.type == FunctionManager::ScriptFunction::AGGREGATE, false); +} + +bool FunctionsEditorModel::isScalar(int row) const +{ + GETTER(functionList[row]->data.type == FunctionManager::ScriptFunction::SCALAR, false); +} + +QStringList FunctionsEditorModel::getArguments(int row) const +{ + GETTER(functionList[row]->data.arguments, QStringList()); +} + +void FunctionsEditorModel::setArguments(int row, const QStringList& value) +{ + SETTER(functionList[row]->data.arguments, value); +} + +QStringList FunctionsEditorModel::getDatabases(int row) const +{ + GETTER(functionList[row]->data.databases, QStringList()); +} + +void FunctionsEditorModel::setDatabases(int row, const QStringList& value) +{ + SETTER(functionList[row]->data.databases, value); +} + +void FunctionsEditorModel::setData(const QList<FunctionManager::ScriptFunction*>& functions) +{ + beginResetModel(); + + for (Function* functionPtr : functionList) + delete functionPtr; + + functionList.clear(); + + foreach (FunctionManager::ScriptFunction* func, functions) + functionList << new Function(func); + + listModified = false; + originalFunctionList = functionList; + + endResetModel(); +} + +void FunctionsEditorModel::addFunction(FunctionManager::ScriptFunction* function) +{ + int row = functionList.size(); + + beginInsertRows(QModelIndex(), row, row); + + functionList << new Function(function); + listModified = true; + + endInsertRows(); +} + +void FunctionsEditorModel::deleteFunction(int row) +{ + if (!isValidRowIndex(row)) + return; + + beginRemoveRows(QModelIndex(), row, row); + + delete functionList[row]; + functionList.removeAt(row); + + listModified = true; + + endRemoveRows(); +} + +QList<FunctionManager::ScriptFunction*> FunctionsEditorModel::generateFunctions() const +{ + QList<FunctionManager::ScriptFunction*> results; + + foreach (Function* func, functionList) + results << new FunctionManager::ScriptFunction(func->data); + + return results; +} + +QStringList FunctionsEditorModel::getFunctionNames() const +{ + QStringList names; + foreach (Function* func, functionList) + names << func->data.name; + + return names; +} + +void FunctionsEditorModel::validateNames() +{ + StrHash<QList<int>> counter; + + int row = 0; + foreach (Function* func, functionList) + { + func->valid &= true; + counter[func->data.name] << row++; + } + + QHashIterator<QString,QList<int>> cntIt = counter.iterator(); + while (cntIt.hasNext()) + { + cntIt.next(); + if (cntIt.value().size() > 1) + { + foreach (int cntRow, cntIt.value()) + setValid(cntRow, false); + } + } + + QModelIndex idx; + for (int i = 0; i < functionList.size(); i++) + { + idx = index(i); + emit dataChanged(idx, idx); + } +} + +bool FunctionsEditorModel::isAllowedName(int rowToSkip, const QString& nameToValidate) +{ + QStringList names = getFunctionNames(); + names.removeAt(rowToSkip); + return !names.contains(nameToValidate, Qt::CaseInsensitive); +} + +int FunctionsEditorModel::rowCount(const QModelIndex& parent) const +{ + UNUSED(parent); + return functionList.size(); +} + +QVariant FunctionsEditorModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || !isValidRowIndex(index.row())) + return QVariant(); + + if (role == Qt::DisplayRole) + { + Function* fn = functionList[index.row()]; + return fn->data.toString(); + } + + if (role == Qt::DecorationRole && langToIcon.contains(functionList[index.row()]->data.lang)) + { + QIcon icon = langToIcon[functionList[index.row()]->data.lang]; + if (!functionList[index.row()]->valid) + icon = Icon::merge(icon, Icon::ERROR); + + return icon; + } + + return QVariant(); +} + +void FunctionsEditorModel::init() +{ + foreach (ScriptingPlugin* plugin, PLUGINS->getLoadedPlugins<ScriptingPlugin>()) + langToIcon[plugin->getLanguage()] = QIcon(plugin->getIconPath()); +} + +bool FunctionsEditorModel::isValidRowIndex(int row) const +{ + return (row >= 0 && row < functionList.size()); +} + +void FunctionsEditorModel::emitDataChanged(int row) +{ + QModelIndex idx = index(row); + emit dataChanged(idx, idx); +} + +FunctionsEditorModel::Function::Function() +{ +} + +FunctionsEditorModel::Function::Function(FunctionManager::ScriptFunction* other) +{ + data = FunctionManager::ScriptFunction(*other); + originalName = data.name; +} diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/functionseditormodel.h b/SQLiteStudio3/guiSQLiteStudio/windows/functionseditormodel.h new file mode 100644 index 0000000..79f073f --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/functionseditormodel.h @@ -0,0 +1,98 @@ +#ifndef FUNCTIONSEDITORMODEL_H +#define FUNCTIONSEDITORMODEL_H + +#include "services/config.h" +#include "services/functionmanager.h" +#include "guiSQLiteStudio_global.h" +#include <QIcon> +#include <QAbstractListModel> + +class GUI_API_EXPORT FunctionsEditorModel : public QAbstractListModel +{ + Q_OBJECT + + public: + using QAbstractItemModel::setData; + + enum Role + { + CODE = 1000, + MODIFIED = 1001, + VALID = 1002, + TYPE = 1003 + }; + + explicit FunctionsEditorModel(QObject *parent = 0); + + void clearModified(); + bool isModified() const; + bool isModified(int row) const; + void setModified(int row, bool modified); + bool isValid() const; + bool isValid(int row) const; + void setValid(int row, bool valid); + void setCode(int row, const QString& code); + QString getCode(int row) const; + void setFinalCode(int row, const QString& code); + QString getFinalCode(int row) const; + void setInitCode(int row, const QString& code); + QString getInitCode(int row) const; + void setName(int row, const QString& newName); + QString getName(int row) const; + void setLang(int row, const QString& lang); + QString getLang(int row) const; + QStringList getDatabases(int row) const; + void setDatabases(int row, const QStringList& value); + QStringList getArguments(int row) const; + void setArguments(int row, const QStringList& value); + FunctionManager::ScriptFunction::Type getType(int row) const; + void setType(int row, FunctionManager::ScriptFunction::Type type); + bool isAggregate(int row) const; + bool isScalar(int row) const; + bool getUndefinedArgs(int row) const; + void setUndefinedArgs(int row, bool value); + bool getAllDatabases(int row) const; + void setAllDatabases(int row, bool value); + void setData(const QList<FunctionManager::ScriptFunction*>& functions); + void addFunction(FunctionManager::ScriptFunction* function); + void deleteFunction(int row); + QList<FunctionManager::ScriptFunction*> generateFunctions() const; + QStringList getFunctionNames() const; + void validateNames(); + bool isAllowedName(int rowToSkip, const QString& nameToValidate); + bool isValidRowIndex(int row) const; + + int rowCount(const QModelIndex& parent = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role) const; + + private: + struct Function + { + Function(); + Function(FunctionManager::ScriptFunction* other); + + FunctionManager::ScriptFunction data; + bool modified = false; + bool valid = true; + QString originalName; + }; + + void init(); + void emitDataChanged(int row); + + QList<Function*> functionList; + + /** + * @brief List of function pointers before modifications. + * + * This list is kept to check for modifications in the overall list of functions. + * Pointers on this list may be already deleted, so don't use them! + * It's only used to compare list of pointers to functionList, so it can tell you + * if the list was modified in regards of adding or deleting functions. + */ + QList<Function*> originalFunctionList; + QHash<QString,QIcon> langToIcon; + bool listModified = false; +}; + +#endif // FUNCTIONSEDITORMODEL_H diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/tableconstraintsmodel.cpp b/SQLiteStudio3/guiSQLiteStudio/windows/tableconstraintsmodel.cpp new file mode 100644 index 0000000..850d8a7 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/tableconstraintsmodel.cpp @@ -0,0 +1,482 @@ +#include "tableconstraintsmodel.h" +#include "iconmanager.h" +#include "common/utils_sql.h" +#include "common/unused.h" +#include <QDebug> +#include <QMimeData> + +TableConstraintsModel::TableConstraintsModel(QObject *parent) : + QAbstractTableModel(parent) +{ +} + +int TableConstraintsModel::rowCount(const QModelIndex& parent) const +{ + UNUSED(parent); + if (createTable.isNull()) + return 0; + + return createTable->constraints.size(); +} + +int TableConstraintsModel::columnCount(const QModelIndex& parent) const +{ + UNUSED(parent); + return 3; +} + +QVariant TableConstraintsModel::data(const QModelIndex& index, int role) const +{ + if (createTable.isNull()) + return QVariant(); + + SqliteCreateTable::Constraint* constr = createTable->constraints[index.row()]; + switch (getColumn(index.column())) + { + case Columns::TYPE: + { + if (role == Qt::DisplayRole) + return getTypeLabel(constr->type); + + if (role == Qt::DecorationRole) + return getTypeIcon(constr->type); + + break; + } + case Columns::NAME: + { + if (role == Qt::DisplayRole) + return stripObjName(constr->name, createTable->dialect); + + break; + } + case Columns::DETAILS: + { + if (role == Qt::DisplayRole) + return getDetails(constr); + + break; + } + } + return QVariant(); +} + +QVariant TableConstraintsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QAbstractTableModel::headerData(section, orientation, role); + + if (orientation == Qt::Vertical) + return section + 1; + + switch (getColumn(section)) + { + case TableConstraintsModel::Columns::TYPE: + return tr("Type", "table constraints"); + case TableConstraintsModel::Columns::DETAILS: + return tr("Details", "table constraints"); + case TableConstraintsModel::Columns::NAME: + return tr("Name", "table constraints"); + } + return QVariant(); +} + +Qt::DropActions TableConstraintsModel::supportedDropActions() const +{ + return Qt::MoveAction; +} + +Qt::DropActions TableConstraintsModel::supportedDragActions() const +{ + return Qt::CopyAction|Qt::MoveAction; +} + +QStringList TableConstraintsModel::mimeTypes() const +{ + return {mimeType}; +} + +QMimeData* TableConstraintsModel::mimeData(const QModelIndexList& indexes) const +{ + if (indexes.size() < 1) + return nullptr; + + QModelIndex idx = indexes.first(); + + QMimeData *data = new QMimeData(); + + QByteArray output; + QDataStream stream(&output, QIODevice::WriteOnly); + + stream << idx.row(); + data->setData(mimeType, output); + + return data; +} + +bool TableConstraintsModel::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const +{ + UNUSED(action); + UNUSED(row); + UNUSED(column); + UNUSED(parent); + + if (!data) + return false; + + if (!data->hasFormat(mimeType)) + return false; + + return true; +} + +bool TableConstraintsModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) +{ + UNUSED(column); + + if (action == Qt::IgnoreAction) + return true; + + if (!data) + return false; + + if (!data->hasFormat(mimeType)) + return false; + + if (action != Qt::MoveAction) + return false; + + if (row < 0) + { + if (!parent.isValid() && !createTable.isNull()) + row = createTable->constraints.size(); + else + row = parent.row(); + } + + if (row < 0) + return false; + + QByteArray byteData = data->data(mimeType); + QDataStream stream(&byteData, QIODevice::ReadOnly); + int oldRow; + stream >> oldRow; + + moveConstraintColumnTo(oldRow, row); + return true; +} + +Qt::ItemFlags TableConstraintsModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags defFlags = QAbstractItemModel::flags(index); + if (!index.isValid()) + return defFlags|Qt::ItemIsDropEnabled; + + return defFlags|Qt::ItemIsDragEnabled|Qt::ItemIsDropEnabled; +} + +bool TableConstraintsModel::isModified() const +{ + return modified; +} + +void TableConstraintsModel::setCreateTable(SqliteCreateTable* value) +{ + beginResetModel(); + createTable = value; + endResetModel(); + modified = false; + emit modifiyStateChanged(); +} + +SqliteCreateTable::Constraint* TableConstraintsModel::getConstraint(int constrIdx) const +{ + if (createTable.isNull()) + return nullptr; + + return createTable->constraints[constrIdx]; +} + +void TableConstraintsModel::replaceConstraint(int constrIdx, SqliteCreateTable::Constraint* constr) +{ + if (createTable.isNull()) + return; + + delete createTable->constraints[constrIdx]; + createTable->constraints[constrIdx] = constr; + constr->setParent(createTable); + modified = true; + + emit modifiyStateChanged(); + emit dataChanged(createIndex(constrIdx, 0), createIndex(constrIdx, columnCount()-1)); +} + +void TableConstraintsModel::constraintModified(int constrIdx) +{ + modified = true; + + emit modifiyStateChanged(); + emit dataChanged(createIndex(constrIdx, 0), createIndex(constrIdx, columnCount()-1)); +} + +void TableConstraintsModel::insertConstraint(int constrIdx, SqliteCreateTable::Constraint* constr) +{ + if (createTable.isNull()) + return; + + beginInsertRows(QModelIndex(), constrIdx, constrIdx); + createTable->constraints.insert(constrIdx, constr); + constr->setParent(createTable); + endInsertRows(); + + modified = true; + emit modifiyStateChanged(); +} + +void TableConstraintsModel::appendConstraint(SqliteCreateTable::Constraint* constr) +{ + if (createTable.isNull()) + return; + + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + createTable->constraints.append(constr); + constr->setParent(createTable); + endInsertRows(); + + modified = true; + emit modifiyStateChanged(); +} + +void TableConstraintsModel::delConstraint(int constrIdx) +{ + if (createTable.isNull()) + return; + + beginRemoveRows(QModelIndex(), constrIdx, constrIdx); + delete createTable->constraints[constrIdx]; + createTable->constraints.removeAt(constrIdx); + endRemoveRows(); + + modified = true; + emit modifiyStateChanged(); +} + +void TableConstraintsModel::moveConstraintUp(int constrIdx) +{ + moveConstraintColumnTo(constrIdx, constrIdx-1); +} + +void TableConstraintsModel::moveConstraintDown(int constrIdx) +{ + moveConstraintColumnTo(constrIdx, constrIdx+1); +} + +void TableConstraintsModel::moveConstraintColumnTo(int constrIdx, int newIdx) +{ + if (createTable.isNull()) + return; + + if (newIdx == constrIdx) + return; + + if (newIdx == constrIdx+1) + { + // See TableStructureModel::moveColumnTo() for details above code below. + int tmpIdx = newIdx; + newIdx = constrIdx; + constrIdx = tmpIdx; + } + + beginMoveRows(QModelIndex(), constrIdx, constrIdx, QModelIndex(), newIdx); + if (newIdx >= createTable->constraints.size()) + { + SqliteCreateTable::Constraint* constr = createTable->constraints.takeAt(constrIdx); + createTable->constraints.append(constr); + } + else + createTable->constraints.move(constrIdx, newIdx); + + endMoveRows(); + + modified = true; + emit modifiyStateChanged(); + emit constraintOrderChanged(); +} + +TableConstraintsModel::Columns TableConstraintsModel::getColumn(int idx) const +{ + return static_cast<Columns>(idx); +} + +QString TableConstraintsModel::getTypeLabel(SqliteCreateTable::Constraint::Type type) const +{ + switch (type) + { + case SqliteCreateTable::Constraint::PRIMARY_KEY: + return "PRIMARY KEY"; + case SqliteCreateTable::Constraint::UNIQUE: + return "UNIQUE"; + case SqliteCreateTable::Constraint::CHECK: + return "CHECK"; + case SqliteCreateTable::Constraint::FOREIGN_KEY: + return "FOREIGN KEY"; + case SqliteCreateTable::Constraint::NAME_ONLY: + return QString::null; + } + return QString::null; +} + +QIcon TableConstraintsModel::getTypeIcon(SqliteCreateTable::Constraint::Type type) const +{ + switch (type) + { + case SqliteCreateTable::Constraint::PRIMARY_KEY: + return ICONS.CONSTRAINT_PRIMARY_KEY; + case SqliteCreateTable::Constraint::UNIQUE: + return ICONS.CONSTRAINT_UNIQUE; + case SqliteCreateTable::Constraint::CHECK: + return ICONS.CONSTRAINT_CHECK; + case SqliteCreateTable::Constraint::FOREIGN_KEY: + return ICONS.CONSTRAINT_FOREIGN_KEY; + case SqliteCreateTable::Constraint::NAME_ONLY: + return QIcon(); + } + return QIcon(); +} + +QString TableConstraintsModel::getDetails(SqliteCreateTable::Constraint* constr) const +{ + switch (constr->type) + { + case SqliteCreateTable::Constraint::PRIMARY_KEY: + return getPkDetails(constr); + case SqliteCreateTable::Constraint::UNIQUE: + return getUniqueDetails(constr); + case SqliteCreateTable::Constraint::CHECK: + return getCheckDetails(constr); + case SqliteCreateTable::Constraint::FOREIGN_KEY: + return getFkDetails(constr); + case SqliteCreateTable::Constraint::NAME_ONLY: + return QString::null; + } + return QString::null; +} + +QString TableConstraintsModel::getPkDetails(SqliteCreateTable::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "KEY", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString TableConstraintsModel::getUniqueDetails(SqliteCreateTable::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "UNIQUE", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString TableConstraintsModel::getCheckDetails(SqliteCreateTable::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "CHECK", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString TableConstraintsModel::getFkDetails(SqliteCreateTable::Constraint* constr) const +{ + int idx = constr->tokens.indexOf(Token::KEYWORD, "KEY", Qt::CaseInsensitive); + return getConstrDetails(constr, idx + 1); +} + +QString TableConstraintsModel::getConstrDetails(SqliteCreateTable::Constraint* constr, int tokenOffset) const +{ + TokenList tokens = constr->tokens.mid(tokenOffset); + tokens.trimLeft(); + return tokens.detokenize(); +} + +void TableConstraintsModel::columnRenamed(SqliteCreateTable::Constraint* constr, const QString& oldColumn, const QString& newColumn) +{ + foreach (SqliteIndexedColumn* idxCol, constr->indexedColumns) + { + if (idxCol->name.compare(oldColumn, Qt::CaseInsensitive) == 0) + { + idxCol->name = newColumn; + modified = true; + } + } + + emit modifiyStateChanged(); +} + +bool TableConstraintsModel::handleColumnDeleted(SqliteCreateTable::Constraint* constr, const QString& column) +{ + switch (constr->type) + { + case SqliteCreateTable::Constraint::PRIMARY_KEY: + case SqliteCreateTable::Constraint::UNIQUE: + { + QMutableListIterator<SqliteIndexedColumn*> it(constr->indexedColumns); + SqliteIndexedColumn* idxCol = nullptr; + while (it.hasNext()) + { + idxCol = it.next(); + if (idxCol->name.compare(column, Qt::CaseInsensitive) == 0) + { + it.remove(); + delete idxCol; + modified = true; + } + } + + emit modifiyStateChanged(); + return constr->indexedColumns.count() > 0; + } + case SqliteCreateTable::Constraint::CHECK: + case SqliteCreateTable::Constraint::FOREIGN_KEY: + case SqliteCreateTable::Constraint::NAME_ONLY: + break; + } + return true; +} + +void TableConstraintsModel::columnModified(const QString& oldColumn, SqliteCreateTable::Column* newColumn) +{ + if (newColumn->name == oldColumn) + return; + + int idx = 0; + foreach (SqliteCreateTable::Constraint* constr, createTable->constraints) + { + if (constr->doesAffectColumn(oldColumn)) + { + columnRenamed(constr, oldColumn, newColumn->name); + constr->rebuildTokens(); + emit dataChanged(createIndex(idx, 0), createIndex(idx, columnCount()-1)); + } + + idx++; + } +} + +void TableConstraintsModel::columnDeleted(const QString& column) +{ + QList<int> toDelete; + int idx = 0; + foreach (SqliteCreateTable::Constraint* constr, createTable->constraints) + { + if (constr->doesAffectColumn(column)) + { + if (handleColumnDeleted(constr, column)) + { + constr->rebuildTokens(); + emit dataChanged(createIndex(idx, 0), createIndex(idx, columnCount()-1)); + } + else + toDelete << idx; + } + + idx++; + } + + foreach (int idx, toDelete) + delConstraint(idx); +} diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/tableconstraintsmodel.h b/SQLiteStudio3/guiSQLiteStudio/windows/tableconstraintsmodel.h new file mode 100644 index 0000000..e3ee9e3 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/tableconstraintsmodel.h @@ -0,0 +1,72 @@ +#ifndef TABLECONSTRAINTSMODEL_H +#define TABLECONSTRAINTSMODEL_H + +#include "parser/ast/sqlitecreatetable.h" +#include "guiSQLiteStudio_global.h" +#include <QAbstractTableModel> +#include <QPointer> + +class GUI_API_EXPORT TableConstraintsModel : public QAbstractTableModel +{ + Q_OBJECT + public: + explicit TableConstraintsModel(QObject *parent = 0); + + int rowCount(const QModelIndex& parent = QModelIndex()) const; + int columnCount(const QModelIndex& parent = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + Qt::DropActions supportedDropActions() const; + Qt::DropActions supportedDragActions() const; + QStringList mimeTypes() const; + QMimeData* mimeData(const QModelIndexList& indexes) const; + bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const; + bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); + Qt::ItemFlags flags(const QModelIndex& index) const; + bool isModified() const; + void setCreateTable(SqliteCreateTable* value); + SqliteCreateTable::Constraint* getConstraint(int constrIdx) const; + void replaceConstraint(int constrIdx, SqliteCreateTable::Constraint* constr); + void constraintModified(int constrIdx); + void insertConstraint(int constrIdx, SqliteCreateTable::Constraint* constr); + void appendConstraint(SqliteCreateTable::Constraint* constr); + void delConstraint(int constrIdx); + void moveConstraintUp(int constrIdx); + void moveConstraintDown(int constrIdx); + void moveConstraintColumnTo(int constrIdx, int newIdx); + + private: + enum class Columns + { + TYPE, + NAME, + DETAILS + }; + + Columns getColumn(int idx) const; + QString getTypeLabel(SqliteCreateTable::Constraint::Type type) const; + QIcon getTypeIcon(SqliteCreateTable::Constraint::Type type) const; + QString getDetails(SqliteCreateTable::Constraint* constr) const; + QString getPkDetails(SqliteCreateTable::Constraint* constr) const; + QString getUniqueDetails(SqliteCreateTable::Constraint* constr) const; + QString getCheckDetails(SqliteCreateTable::Constraint* constr) const; + QString getFkDetails(SqliteCreateTable::Constraint* constr) const; + QString getConstrDetails(SqliteCreateTable::Constraint* constr, int tokenOffset) const; + void columnRenamed(SqliteCreateTable::Constraint* constr, const QString& oldColumn, const QString& newColumn); + bool handleColumnDeleted(SqliteCreateTable::Constraint* constr, const QString& column); + + static const constexpr char* mimeType = "application/x-sqlitestudio-tablestructureconstraintmodel-row-index"; + + QPointer<SqliteCreateTable> createTable; + bool modified = false; + + public slots: + void columnModified(const QString& oldColumn, SqliteCreateTable::Column* newColumn); + void columnDeleted(const QString& column); + + signals: + void modifiyStateChanged(); + void constraintOrderChanged(); +}; + +#endif // TABLECONSTRAINTSMODEL_H diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/tablestructuremodel.cpp b/SQLiteStudio3/guiSQLiteStudio/windows/tablestructuremodel.cpp new file mode 100644 index 0000000..80c4567 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/tablestructuremodel.cpp @@ -0,0 +1,604 @@ +#include "tablestructuremodel.h" +#include "iconmanager.h" +#include "common/unused.h" +#include "uiconfig.h" +#include <QFont> +#include <QDebug> +#include <QMimeData> + +TableStructureModel::TableStructureModel(QObject *parent) : + QAbstractTableModel(parent) +{ +} + +int TableStructureModel::rowCount(const QModelIndex& parent) const +{ + UNUSED(parent); + if (createTable.isNull()) + return 0; + + return createTable->columns.size(); +} + +int TableStructureModel::columnCount(const QModelIndex& parent) const +{ + UNUSED(parent); + if (createTable.isNull()) + return 0; + + switch (createTable->dialect) + { + case Dialect::Sqlite3: + return 9; + case Dialect::Sqlite2: + return 7; + } + return 0; +} + +QVariant TableStructureModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (createTable.isNull()) + return QVariant(); + + if (!isValidColumnIdx(index.column())) + return QVariant(); + + int row = index.row(); + if (createTable->columns.size() <= row) + return QVariant(); + + switch (getHeaderColumn(index.column())) + { + case TableStructureModel::Columns::NAME: + { + if (role != Qt::DisplayRole) + break; + + return getColumnName(row); + } + case TableStructureModel::Columns::TYPE: + { + if (role != Qt::DisplayRole) + break; + + return getColumnType(row); + } + case TableStructureModel::Columns::PK: + { + if (role != Qt::DecorationRole) + break; + + return getColumnPk(row); + } + case TableStructureModel::Columns::FK: + { + if (role != Qt::DecorationRole) + break; + + return getColumnFk(row); + } + case TableStructureModel::Columns::UNIQUE: + { + if (role != Qt::DecorationRole) + break; + + return getColumnUnique(row); + } + case TableStructureModel::Columns::CHECK: + { + if (role != Qt::DecorationRole) + break; + + return getColumnCheck(row); + } + case TableStructureModel::Columns::NOTNULL: + { + if (role != Qt::DecorationRole) + break; + + return getColumnNotNull(row); + } + case TableStructureModel::Columns::COLLATE: + { + if (role != Qt::DecorationRole) + break; + + return getColumnCollate(row); + } + case TableStructureModel::Columns::DEFAULT: + { + if (role == Qt::FontRole) + return getColumnDefaultFont(row); + + if (role == Qt::ForegroundRole) + return getColumnDefaultColor(row); + + if (role == Qt::DisplayRole) + return getColumnDefaultValue(row); + + break; + } + } + return QVariant(); +} + +QVariant TableStructureModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QAbstractTableModel::headerData(section, orientation, role); + + if (orientation == Qt::Vertical) + return section + 1; + + // Now it's horizontal orientation with DisplayRole + return columnLabel(section); +} + +TableStructureModel::Columns TableStructureModel::getHeaderColumn(int colIdx) const +{ + if (!createTable.isNull() && createTable->dialect == Dialect::Sqlite2) + { + if (colIdx >= 3) + colIdx++; // skip FK + + if (colIdx >= 7) + colIdx++; // skip COLLATE + } + return static_cast<Columns>(colIdx); +} + +bool TableStructureModel::isValidColumnIdx(int colIdx) const +{ + if (!createTable.isNull() && createTable->dialect == Dialect::Sqlite2) + return colIdx >= 0 && colIdx < 7; + + return colIdx >= 0 && colIdx < 9; +} + +SqliteCreateTable::Column* TableStructureModel::getColumn(int colIdx) const +{ + if (createTable.isNull()) + return nullptr; + + return createTable->columns[colIdx]; +} + +void TableStructureModel::replaceColumn(int colIdx, SqliteCreateTable::Column* column) +{ + if (createTable.isNull()) + return; + + SqliteCreateTable::Column* oldColumn = createTable->columns[colIdx]; + QString oldColumnName = oldColumn->name; + + delete oldColumn; + createTable->columns[colIdx] = column; + column->setParent(createTable); + modified = true; + + emit modifiyStateChanged(); + emit dataChanged(createIndex(colIdx, 0), createIndex(colIdx, columnCount()-1)); + emit columnModified(oldColumnName, column); +} + +void TableStructureModel::insertColumn(int colIdx, SqliteCreateTable::Column* column) +{ + if (createTable.isNull()) + return; + + beginInsertRows(QModelIndex(), colIdx, colIdx); + createTable->columns.insert(colIdx, column); + column->setParent(createTable); + endInsertRows(); + + modified = true; + emit modifiyStateChanged(); +} + +void TableStructureModel::appendColumn(SqliteCreateTable::Column* column) +{ + if (createTable.isNull()) + return; + + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + createTable->columns.append(column); + column->setParent(createTable); + endInsertRows(); + + modified = true; + emit modifiyStateChanged(); +} + +void TableStructureModel::delColumn(int colIdx) +{ + if (createTable.isNull()) + return; + + QString name = createTable->columns[colIdx]->name; + + beginRemoveRows(QModelIndex(), colIdx, colIdx); + delete createTable->columns[colIdx]; + createTable->columns.removeAt(colIdx); + endRemoveRows(); + + modified = true; + emit modifiyStateChanged(); + emit columnDeleted(name); +} + +void TableStructureModel::moveColumnUp(int colIdx) +{ + moveColumnTo(colIdx, colIdx-1); +} + +void TableStructureModel::moveColumnDown(int colIdx) +{ + moveColumnTo(colIdx, colIdx+1); +} + +void TableStructureModel::moveColumnTo(int colIdx, int newIdx) +{ + if (createTable.isNull()) + return; + + if (newIdx == colIdx) + return; + + int totalCols = createTable->columns.size(); + if (colIdx + 1 == totalCols && newIdx == totalCols) // Moving last column out of range? Nothing to do. + return; + + if (newIdx == colIdx+1) + { + // From Qt docs: "you must ensure that the destinationChild is not within the range of sourceFirst and sourceLast + 1". + // So in this case - which is easy to handle - we will invert operation. We will move target index one level up, + // instead of moving source index down. + int tmpIdx = newIdx; + newIdx = colIdx; + colIdx = tmpIdx; + } + + beginMoveRows(QModelIndex(), colIdx, colIdx, QModelIndex(), newIdx); + if (newIdx >= totalCols) + { + SqliteCreateTable::Column* col = createTable->columns.takeAt(colIdx); + createTable->columns.append(col); + } + else + createTable->columns.move(colIdx, newIdx); + + endMoveRows(); + + modified = true; + emit modifiyStateChanged(); + emit columnsOrderChanged(); +} + +QModelIndex TableStructureModel::findColumn(const QString& columnName, Qt::CaseSensitivity cs) const +{ + int row = 0; + foreach (SqliteCreateTable::Column* col, createTable->columns) + { + if (col->name.compare(columnName, cs) == 0) + return createIndex(row, 0); + + row++; + } + return QModelIndex(); +} + +QString TableStructureModel::columnLabel(int column) const +{ + switch (getHeaderColumn(column)) + { + case Columns::NAME: + return tr("Name", "table structure columns"); + case Columns::TYPE: + return tr("Data type", "table structure columns"); + case Columns::PK: + return "P"; + case Columns::FK: + return "F"; + case Columns::UNIQUE: + return "U"; + case Columns::CHECK: + return "H"; + case Columns::NOTNULL: + return "N"; + case Columns::COLLATE: + return "C"; + case Columns::DEFAULT: + return tr("Default value", "table structure columns"); + } + return QString::null; +} + +QVariant TableStructureModel::getColumnName(int row) const +{ + return getColumn(row)->name; +} + +QVariant TableStructureModel::getColumnType(int row) const +{ + SqliteColumnType* type = getColumn(row)->type; + return type ? type->detokenize() : ""; +} + +QVariant TableStructureModel::getColumnPk(int row) const +{ + if (isColumnPk(getColumn(row))) + return ICONS.CONSTRAINT_PRIMARY_KEY; + + return QVariant(); +} + +QVariant TableStructureModel::getColumnFk(int row) const +{ + if (isColumnFk(getColumn(row))) + return ICONS.CONSTRAINT_FOREIGN_KEY; + + return QVariant(); +} + +QVariant TableStructureModel::getColumnUnique(int row) const +{ + if (isColumnUnique(getColumn(row))) + return ICONS.CONSTRAINT_UNIQUE; + + return QVariant(); +} + +QVariant TableStructureModel::getColumnCheck(int row) const +{ + if (isColumnCheck(getColumn(row))) + return ICONS.CONSTRAINT_CHECK; + + return QVariant(); +} + +QVariant TableStructureModel::getColumnNotNull(int row) const +{ + if (isColumnNotNull(getColumn(row))) + return ICONS.CONSTRAINT_NOT_NULL; + + return QVariant(); +} + +QVariant TableStructureModel::getColumnCollate(int row) const +{ + if (isColumnCollate(getColumn(row))) + return ICONS.CONSTRAINT_COLLATION; + + return QVariant(); +} + +QVariant TableStructureModel::getColumnDefaultValue(int row) const +{ + QVariant value = getColumnDefault(row); + if (value.isNull()) + return "NULL"; + + return value; +} + +QVariant TableStructureModel::getColumnDefaultFont(int row) const +{ + QVariant value = getColumnDefault(row); + if (value.isNull()) + { + QFont font; + font.setItalic(true); + return font; + } + return QVariant(); +} + +QVariant TableStructureModel::getColumnDefaultColor(int row) const +{ + QVariant value = getColumnDefault(row); + if (value.isNull()) + return QColor(CFG_UI.Colors.DataNullFg); + + return QVariant(); +} + +QVariant TableStructureModel::getColumnDefault(int row) const +{ + SqliteCreateTable::Column::Constraint* constr = getColumn(row)->getConstraint(SqliteCreateTable::Column::Constraint::DEFAULT); + if (!constr) + return QVariant(); + + if (!constr->id.isNull()) + return constr->id; + else if (!constr->literalValue.isNull()) + return constr->literalValue; + else if (!constr->ctime.isNull()) + return constr->ctime; + else if (constr->expr) + return constr->expr->detokenize(); + else + return QVariant(); +} + +bool TableStructureModel::isColumnPk(SqliteCreateTable::Column* column) const +{ + if (column->hasConstraint(SqliteCreateTable::Column::Constraint::PRIMARY_KEY)) + return true; + + QList<SqliteCreateTable::Constraint*> constraints = createTable->getConstraints(SqliteCreateTable::Constraint::PRIMARY_KEY); + foreach (SqliteCreateTable::Constraint* constr, constraints) + if (constr->doesAffectColumn(column->name)) + return true; + + return false; +} + +bool TableStructureModel::isColumnFk(SqliteCreateTable::Column* column) const +{ + if (column->hasConstraint(SqliteCreateTable::Column::Constraint::FOREIGN_KEY)) + return true; + + QList<SqliteCreateTable::Constraint*> constraints = createTable->getConstraints(SqliteCreateTable::Constraint::FOREIGN_KEY); + foreach (SqliteCreateTable::Constraint* constr, constraints) + if (constr->doesAffectColumn(column->name)) + return true; + + return false; +} + +bool TableStructureModel::isColumnUnique(SqliteCreateTable::Column* column) const +{ + if (column->hasConstraint(SqliteCreateTable::Column::Constraint::UNIQUE)) + return true; + + QList<SqliteCreateTable::Constraint*> constraints = createTable->getConstraints(SqliteCreateTable::Constraint::UNIQUE); + foreach (SqliteCreateTable::Constraint* constr, constraints) + if (constr->doesAffectColumn(column->name)) + return true; + + return false; +} + +bool TableStructureModel::isColumnCheck(SqliteCreateTable::Column* column) const +{ + if (column->hasConstraint(SqliteCreateTable::Column::Constraint::CHECK)) + return true; + + QList<SqliteCreateTable::Constraint*> constraints = createTable->getConstraints(SqliteCreateTable::Constraint::CHECK); + foreach (SqliteCreateTable::Constraint* constr, constraints) + if (constr->expr->getContextColumns(false).contains(column->name, Qt::CaseInsensitive)) + return true; + + return false; +} + +bool TableStructureModel::isColumnNotNull(SqliteCreateTable::Column* column) const +{ + if (column->hasConstraint(SqliteCreateTable::Column::Constraint::NOT_NULL)) + return true; + + return false; +} + +bool TableStructureModel::isColumnCollate(SqliteCreateTable::Column* column) const +{ + if (column->hasConstraint(SqliteCreateTable::Column::Constraint::COLLATE)) + return true; + + return false; +} + +void TableStructureModel::setCreateTable(SqliteCreateTable* value) +{ + beginResetModel(); + createTable = value; + endResetModel(); + + modified = false; + emit modifiyStateChanged(); +} + +bool TableStructureModel::isModified() const +{ + return modified; +} + +Qt::DropActions TableStructureModel::supportedDropActions() const +{ + return Qt::MoveAction; +} + +Qt::DropActions TableStructureModel::supportedDragActions() const +{ + return Qt::CopyAction|Qt::MoveAction; +} + + +QStringList TableStructureModel::mimeTypes() const +{ + return {mimeType}; +} + +QMimeData* TableStructureModel::mimeData(const QModelIndexList& indexes) const +{ + if (indexes.size() < 1) + return nullptr; + + QModelIndex idx = indexes.first(); + + QMimeData *data = new QMimeData(); + + QByteArray output; + QDataStream stream(&output, QIODevice::WriteOnly); + + stream << idx.row(); + data->setData(mimeType, output); + + return data; +} + + +bool TableStructureModel::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const +{ + UNUSED(action); + UNUSED(row); + UNUSED(column); + UNUSED(parent); + + if (!data) + return false; + + if (!data->hasFormat(mimeType)) + return false; + + return true; +} + +bool TableStructureModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) +{ + UNUSED(column); + + if (action == Qt::IgnoreAction) + return true; + + if (!data) + return false; + + if (!data->hasFormat(mimeType)) + return false; + + if (action != Qt::MoveAction) + return false; + + if (row < 0) + { + if (!parent.isValid() && !createTable.isNull()) + row = createTable->columns.size(); + else + row = parent.row(); + } + + if (row < 0) + return false; + + QByteArray byteData = data->data(mimeType); + QDataStream stream(&byteData, QIODevice::ReadOnly); + int oldRow; + stream >> oldRow; + + moveColumnTo(oldRow, row); + return true; +} + +Qt::ItemFlags TableStructureModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags defFlags = QAbstractItemModel::flags(index); + if (!index.isValid()) + return defFlags|Qt::ItemIsDropEnabled; + + return defFlags|Qt::ItemIsDragEnabled|Qt::ItemIsDropEnabled; +} diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/tablestructuremodel.h b/SQLiteStudio3/guiSQLiteStudio/windows/tablestructuremodel.h new file mode 100644 index 0000000..e1cfa4e --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/tablestructuremodel.h @@ -0,0 +1,89 @@ +#ifndef TABLESTRUCTUREMODEL_H +#define TABLESTRUCTUREMODEL_H + +#include "parser/ast/sqlitecreatetable.h" +#include "guiSQLiteStudio_global.h" +#include <QAbstractTableModel> +#include <QPointer> + +class GUI_API_EXPORT TableStructureModel : public QAbstractTableModel +{ + Q_OBJECT + + public: + explicit TableStructureModel(QObject *parent = 0); + + int rowCount(const QModelIndex& parent = QModelIndex()) const; + int columnCount(const QModelIndex& parent = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + Qt::DropActions supportedDropActions() const; + Qt::DropActions supportedDragActions() const; + QStringList mimeTypes() const; + QMimeData* mimeData(const QModelIndexList& indexes) const; + bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const; + bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); + Qt::ItemFlags flags(const QModelIndex& index) const; + bool isModified() const; + void setCreateTable(SqliteCreateTable* value); + SqliteCreateTable::Column* getColumn(int colIdx) const; + void replaceColumn(int colIdx, SqliteCreateTable::Column* column); + void insertColumn(int colIdx, SqliteCreateTable::Column* column); + void appendColumn(SqliteCreateTable::Column* column); + void delColumn(int colIdx); + void moveColumnUp(int colIdx); + void moveColumnDown(int colIdx); + void moveColumnTo(int colIdx, int newIdx); + QModelIndex findColumn(const QString& columnName, Qt::CaseSensitivity cs = Qt::CaseSensitive) const; + + private: + enum class Columns + { + NAME, + TYPE, + PK, + FK, + UNIQUE, + CHECK, + NOTNULL, + COLLATE, + DEFAULT + }; + + Columns getHeaderColumn(int colIdx) const; + bool isValidColumnIdx(int colIdx) const; + bool doesColumnHasConstraint(SqliteCreateTable::Column* column, SqliteCreateTable::Column::Constraint::Type type); + QString columnLabel(int column) const; + QString columnLabelForSqlite2(int column) const; + QVariant getColumnName(int row) const; + QVariant getColumnType(int row) const; + QVariant getColumnPk(int row) const; + QVariant getColumnFk(int row) const; + QVariant getColumnUnique(int row) const; + QVariant getColumnCheck(int row) const; + QVariant getColumnNotNull(int row) const; + QVariant getColumnCollate(int row) const; + QVariant getColumnDefaultValue(int row) const; + QVariant getColumnDefaultFont(int row) const; + QVariant getColumnDefaultColor(int row) const; + QVariant getColumnDefault(int row) const; + bool isColumnPk(SqliteCreateTable::Column* column) const; + bool isColumnFk(SqliteCreateTable::Column* column) const; + bool isColumnUnique(SqliteCreateTable::Column* column) const; + bool isColumnCheck(SqliteCreateTable::Column* column) const; + bool isColumnNotNull(SqliteCreateTable::Column* column) const; + bool isColumnCollate(SqliteCreateTable::Column* column) const; + + static const constexpr char* mimeType = "application/x-sqlitestudio-tablestructuremodel-row-index"; + + QPointer<SqliteCreateTable> createTable; + bool modified = false; + + signals: + void columnModified(const QString& oldColumn, SqliteCreateTable::Column* newColumn); + void columnDeleted(const QString& column); + void modifiyStateChanged(); + void columnsOrderChanged(); +}; + +#endif // TABLESTRUCTUREMODEL_H diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/tablewindow.cpp b/SQLiteStudio3/guiSQLiteStudio/windows/tablewindow.cpp new file mode 100644 index 0000000..56accd0 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/tablewindow.cpp @@ -0,0 +1,1508 @@ +#include "tablewindow.h" +#include "ui_tablewindow.h" +#include "services/dbmanager.h" +#include "services/notifymanager.h" +#include "sqlitestudio.h" +#include "common/unused.h" +#include "schemaresolver.h" +#include "iconmanager.h" +#include "common/intvalidator.h" +#include "common/extlineedit.h" +#include "datagrid/sqltablemodel.h" +#include "common/extaction.h" +#include "mainwindow.h" +#include "tablestructuremodel.h" +#include "tableconstraintsmodel.h" +#include "dialogs/columndialog.h" +#include "dialogs/constraintdialog.h" +#include "mdiarea.h" +#include "sqlitesyntaxhighlighter.h" +#include "dialogs/newconstraintdialog.h" +#include "db/chainexecutor.h" +#include "common/widgetcover.h" +#include "mdiwindow.h" +#include "dbtree/dbtree.h" +#include "constrainttabmodel.h" +#include "parser/ast/sqlitecreateindex.h" +#include "parser/ast/sqlitecreatetrigger.h" +#include "dialogs/messagelistdialog.h" +#include "services/codeformatter.h" +#include "uiconfig.h" +#include "dialogs/ddlpreviewdialog.h" +#include "services/config.h" +#include "services/importmanager.h" +#include "dbobjectdialogs.h" +#include "dialogs/exportdialog.h" +#include <QMenu> +#include <QToolButton> +#include <QLabel> +#include <QDebug> +#include <QMessageBox> +#include <tablemodifier.h> +#include <QProgressBar> +#include <QPushButton> +#include <QDebug> +#include <QStyleFactory> +#include <dialogs/importdialog.h> +#include <dialogs/populatedialog.h> + +// TODO extend QTableView for columns and constraints, so they show full-row-width drop indicator, +// instead of single column drop indicator. + +CFG_KEYS_DEFINE(TableWindow) + +TableWindow::TableWindow(QWidget* parent) : + MdiChild(parent), + ui(new Ui::TableWindow) +{ + init(); + applyInitialTab(); +} + +TableWindow::TableWindow(Db* db, QWidget* parent) : + MdiChild(parent), + db(db), + ui(new Ui::TableWindow) +{ + newTable(); + init(); + initDbAndTable(); + applyInitialTab(); +} + +TableWindow::TableWindow(const TableWindow& win) : + MdiChild(win.parentWidget()), + db(win.db), + database(win.database), + table(win.table), + ui(new Ui::TableWindow) +{ + init(); + initDbAndTable(); + applyInitialTab(); +} + +TableWindow::TableWindow(QWidget *parent, Db* db, const QString& database, const QString& table) : + MdiChild(parent), + db(db), + database(database), + table(table), + ui(new Ui::TableWindow) +{ + init(); + initDbAndTable(); + applyInitialTab(); +} + +TableWindow::~TableWindow() +{ + delete ui; + + if (tableModifier) + { + delete tableModifier; + tableModifier = nullptr; + } +} + +void TableWindow::staticInit() +{ + qRegisterMetaType<TableWindow>("TableWindow"); +} + +void TableWindow::insertAction(ExtActionPrototype* action, TableWindow::ToolBar toolbar) +{ + return ExtActionContainer::insertAction<TableWindow>(action, toolbar); +} + +void TableWindow::insertActionBefore(ExtActionPrototype* action, TableWindow::Action beforeAction, TableWindow::ToolBar toolbar) +{ + return ExtActionContainer::insertActionBefore<TableWindow>(action, beforeAction, toolbar); +} + +void TableWindow::insertActionAfter(ExtActionPrototype* action, TableWindow::Action afterAction, TableWindow::ToolBar toolbar) +{ + return ExtActionContainer::insertActionAfter<TableWindow>(action, afterAction, toolbar); +} + +void TableWindow::removeAction(ExtActionPrototype* action, TableWindow::ToolBar toolbar) +{ + ExtActionContainer::removeAction<TableWindow>(action, toolbar); +} + +void TableWindow::newTable() +{ + existingTable = false; + table = ""; +} + +void TableWindow::init() +{ + ui->setupUi(this); + ui->structureSplitter->setStretchFactor(0, 2); + +#ifdef Q_OS_MACX + QStyle *fusion = QStyleFactory::create("Fusion"); + ui->structureToolBar->setStyle(fusion); + ui->structureTab->layout()->setSpacing(0); + ui->tableConstraintsToolbar->setStyle(fusion); + ui->constraintsWidget->layout()->setSpacing(0); + ui->indexToolBar->setStyle(fusion); + ui->indexesTab->layout()->setSpacing(0); + ui->triggerToolBar->setStyle(fusion); + ui->triggersTab->layout()->setSpacing(0); +#endif + + dataModel = new SqlTableModel(this); + ui->dataView->init(dataModel); + + initActions(); + + connect(dataModel, SIGNAL(executionSuccessful()), this, SLOT(executionSuccessful())); + connect(dataModel, SIGNAL(executionFailed(QString)), this, SLOT(executionFailed(QString))); + connect(ui->tabWidget, SIGNAL(currentChanged(int)), this, SLOT(tabChanged(int))); + connect(this, SIGNAL(modifyStatusChanged()), this, SLOT(updateStructureCommitState())); + connect(ui->tableNameEdit, SIGNAL(textChanged(QString)), this, SIGNAL(modifyStatusChanged())); + connect(ui->tableNameEdit, SIGNAL(textChanged(QString)), this, SLOT(nameChanged())); + connect(ui->indexList, SIGNAL(itemSelectionChanged()), this, SLOT(updateIndexesState())); + connect(ui->triggerList, SIGNAL(itemSelectionChanged()), this, SLOT(updateTriggersState())); + + structureExecutor = new ChainExecutor(this); + connect(structureExecutor, SIGNAL(success()), this, SLOT(changesSuccessfullyCommited())); + connect(structureExecutor, SIGNAL(failure(int,QString)), this, SLOT(changesFailedToCommit(int,QString))); + + setupCoverWidget(); + updateAfterInit(); +} + +void TableWindow::createActions() +{ + createAction(EXPORT, ICONS.TABLE_EXPORT, tr("Export table", "table window"), this, SLOT(exportTable()), this); + createAction(IMPORT, ICONS.TABLE_IMPORT, tr("Import data to table", "table window"), this, SLOT(importTable()), this); + createAction(POPULATE, ICONS.TABLE_POPULATE, tr("Populate table", "table window"), this, SLOT(populateTable()), this); + + createStructureActions(); + createDataGridActions(); + createDataFormActions(); + createIndexActions(); + createTriggerActions(); + + createAction(NEXT_TAB, "next tab", this, SLOT(nextTab()), this); + createAction(PREV_TAB, "prev tab", this, SLOT(prevTab()), this); +} + +void TableWindow::createStructureActions() +{ + createAction(REFRESH_STRUCTURE, ICONS.RELOAD, tr("Refresh structure", "table window"), this, SLOT(refreshStructure()), ui->structureToolBar); + ui->structureToolBar->addSeparator(); + createAction(COMMIT_STRUCTURE, ICONS.COMMIT, tr("Commit structure changes", "table window"), this, SLOT(commitStructure()), ui->structureToolBar); + createAction(ROLLBACK_STRUCTURE, ICONS.ROLLBACK, tr("Rollback structure changes", "table window"), this, SLOT(rollbackStructure()), ui->structureToolBar); + createAction(ADD_COLUMN, ICONS.TABLE_COLUMN_ADD, tr("Add column", "table window"), this, SLOT(addColumn()), ui->structureToolBar, ui->structureView); + createAction(EDIT_COLUMN, ICONS.TABLE_COLUMN_EDIT, tr("Edit column", "table window"), this, SLOT(editColumn()), ui->structureToolBar, ui->structureView); + createAction(DEL_COLUMN, ICONS.TABLE_COLUMN_DELETE, tr("Delete column", "table window"), this, SLOT(delColumn()), ui->structureToolBar, ui->structureView); + createAction(MOVE_COLUMN_UP, ICONS.MOVE_UP, tr("Move column up", "table window"), this, SLOT(moveColumnUp()), ui->structureToolBar, ui->structureView); + createAction(MOVE_COLUMN_DOWN, ICONS.MOVE_DOWN, tr("Move column down", "table window"), this, SLOT(moveColumnDown()), ui->structureToolBar, ui->structureView); + ui->structureToolBar->addSeparator(); + ui->structureToolBar->addAction(actionMap[IMPORT]); + ui->structureToolBar->addAction(actionMap[EXPORT]); + ui->structureToolBar->addAction(actionMap[POPULATE]); + ui->structureToolBar->addSeparator(); + createAction(CREATE_SIMILAR, ICONS.TABLE_CREATE_SIMILAR, tr("Create similar table", "table window"), this, SLOT(createSimilarTable()), ui->structureToolBar); + + // Table constraints + createAction(ADD_TABLE_CONSTRAINT, ICONS.TABLE_CONSTRAINT_ADD, tr("Add table constraint", "table window"), this, SLOT(addConstraint()), ui->tableConstraintsToolbar, ui->tableConstraintsView); + createAction(EDIT_TABLE_CONSTRAINT, ICONS.TABLE_CONSTRAINT_EDIT, tr("Edit table constraint", "table window"), this, SLOT(editConstraint()), ui->tableConstraintsToolbar, ui->tableConstraintsView); + createAction(DEL_TABLE_CONSTRAINT, ICONS.TABLE_COLUMN_DELETE, tr("Delete table constraint", "table window"), this, SLOT(delConstraint()), ui->tableConstraintsToolbar, ui->tableConstraintsView); + createAction(MOVE_CONSTRAINT_UP, ICONS.MOVE_UP, tr("Move table constraint up", "table window"), this, SLOT(moveConstraintUp()), ui->tableConstraintsToolbar, ui->tableConstraintsView); + createAction(MOVE_CONSTRAINT_DOWN, ICONS.MOVE_DOWN, tr("Move table constraint down", "table window"), this, SLOT(moveConstraintDown()), ui->tableConstraintsToolbar, ui->tableConstraintsView); + ui->tableConstraintsToolbar->addSeparator(); + createAction(ADD_TABLE_PK, ICONS.CONSTRAINT_PRIMARY_KEY_ADD, tr("Add table primary key", "table window"), this, SLOT(addPk()), ui->tableConstraintsToolbar, ui->tableConstraintsView); + createAction(ADD_TABLE_FK, ICONS.CONSTRAINT_FOREIGN_KEY_ADD, tr("Add table foreign key", "table window"), this, SLOT(addFk()), ui->tableConstraintsToolbar, ui->tableConstraintsView); + createAction(ADD_TABLE_UNIQUE, ICONS.CONSTRAINT_UNIQUE_ADD, tr("Add table unique constraint", "table window"), this, SLOT(addUnique()), ui->tableConstraintsToolbar, ui->tableConstraintsView); + createAction(ADD_TABLE_CHECK, ICONS.CONSTRAINT_CHECK_ADD, tr("Add table check constraint", "table window"), this, SLOT(addCheck()), ui->tableConstraintsToolbar, ui->tableConstraintsView); +} + +void TableWindow::createDataGridActions() +{ + QAction* before = ui->dataView->getAction(DataView::FILTER_VALUE); + ui->dataView->getToolBar(DataView::TOOLBAR_GRID)->insertAction(before, actionMap[IMPORT]); + ui->dataView->getToolBar(DataView::TOOLBAR_GRID)->insertAction(before, actionMap[EXPORT]); + ui->dataView->getToolBar(DataView::TOOLBAR_GRID)->insertAction(before, actionMap[POPULATE]); + ui->dataView->getToolBar(DataView::TOOLBAR_GRID)->insertSeparator(before); +} + +void TableWindow::createDataFormActions() +{ +} + +void TableWindow::createIndexActions() +{ + createAction(REFRESH_INDEXES, ICONS.RELOAD, tr("Refresh index list", "table window"), this, SLOT(updateIndexes()), ui->indexToolBar, ui->indexList); + ui->indexToolBar->addSeparator(); + createAction(ADD_INDEX, ICONS.INDEX_ADD, tr("Create index", "table window"), this, SLOT(addIndex()), ui->indexToolBar, ui->indexList); + createAction(EDIT_INDEX, ICONS.INDEX_EDIT, tr("Edit index", "table window"), this, SLOT(editIndex()), ui->indexToolBar, ui->indexList); + createAction(DEL_INDEX, ICONS.INDEX_DEL, tr("Delete index", "table window"), this, SLOT(delIndex()), ui->indexToolBar, ui->indexList); + connect(ui->indexList, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(editIndex())); +} + +void TableWindow::createTriggerActions() +{ + createAction(REFRESH_TRIGGERS, ICONS.RELOAD, tr("Refresh trigger list", "table window"), this, SLOT(updateTriggers()), ui->triggerToolBar, ui->triggerList); + ui->triggerToolBar->addSeparator(); + createAction(ADD_TRIGGER, ICONS.TRIGGER_ADD, tr("Create trigger", "table window"), this, SLOT(addTrigger()), ui->triggerToolBar, ui->triggerList); + createAction(EDIT_TRIGGER, ICONS.TRIGGER_EDIT, tr("Edit trigger", "table window"), this, SLOT(editTrigger()), ui->triggerToolBar, ui->triggerList); + createAction(DEL_TRIGGER, ICONS.TRIGGER_DEL, tr("Delete trigger", "table window"), this, SLOT(delTrigger()), ui->triggerToolBar, ui->triggerList); + connect(ui->triggerList, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(editTrigger())); +} + +void TableWindow::editColumn(const QModelIndex& idx) +{ + if (!idx.isValid()) + { + qWarning() << "Called TableWindow::editColumn() with invalid index."; + return; + } + + SqliteCreateTable::Column* column = structureModel->getColumn(idx.row()); + ColumnDialog columnDialog(db, this); + columnDialog.setColumn(column); + if (columnDialog.exec() != QDialog::Accepted) + return; + + SqliteCreateTable::Column* modifiedColumn = columnDialog.getModifiedColumn(); + structureModel->replaceColumn(idx.row(), modifiedColumn); + ui->structureView->resizeColumnToContents(0); +} + +void TableWindow::delColumn(const QModelIndex& idx) +{ + if (!idx.isValid()) + { + qWarning() << "Called TableWindow::delColumn() with invalid index."; + return; + } + + SqliteCreateTable::Column* column = structureModel->getColumn(idx.row()); + + QString msg = tr("Are you sure you want to delete column '%1'?", "table window").arg(column->name); + int btn = QMessageBox::question(this, tr("Delete column", "table window"), msg); + if (btn != QMessageBox::Yes) + return; + + structureModel->delColumn(idx.row()); + ui->structureView->resizeColumnToContents(0); +} + +void TableWindow::executeStructureChanges() +{ + QStringList sqls; + + createTable->rebuildTokens(); + if (!existingTable) + { + sqls << createTable->detokenize(); + } + else + { + if (tableModifier) + delete tableModifier; + + tableModifier = new TableModifier(db, database, table); + tableModifier->alterTable(createTable); + + if (tableModifier->hasMessages()) + { + MessageListDialog dialog(tr("Following problems will take place while modifying the table.\n" + "Would you like to proceed?", "table window")); + dialog.setWindowTitle(tr("Table modification", "table window")); + foreach (const QString& error, tableModifier->getErrors()) + dialog.addError(error); + + foreach (const QString& warn, tableModifier->getWarnings()) + dialog.addWarning(warn); + + if (dialog.exec() != QDialog::Accepted) + return; + } + + sqls = tableModifier->generateSqls(); + } + + if (!CFG_UI.General.DontShowDdlPreview.get()) + { + DdlPreviewDialog dialog(db, this); + dialog.setDdl(sqls); + if (dialog.exec() != QDialog::Accepted) + return; + } + + modifyingThisTable = true; + structureExecutor->setDb(db); + structureExecutor->setQueries(sqls); + widgetCover->show(); + structureExecutor->exec(); +} + +void TableWindow::updateAfterInit() +{ + updateStructureCommitState(); + updateStructureToolbarState(); + updateTableConstraintsToolbarState(); + updateNewTableState(); + updateIndexesState(); + updateTriggersState(); +} + +QModelIndex TableWindow::structureCurrentIndex() const +{ + return ui->structureView->selectionModel()->currentIndex(); +} + +void TableWindow::updateStructureToolbarState() +{ + QItemSelectionModel *selModel = ui->structureView->selectionModel(); + bool validIdx = false; + bool isFirst = false; + bool isLast = false; + if (selModel) + { + QModelIndex currIdx = selModel->currentIndex(); + if (currIdx.isValid()) + { + validIdx = true; + if (currIdx.row() == 0) + isFirst = true; + + if (currIdx.row() == (structureModel->rowCount() - 1)) + isLast = true; + } + } + + actionMap[EDIT_COLUMN]->setEnabled(validIdx); + actionMap[DEL_COLUMN]->setEnabled(validIdx); + actionMap[MOVE_COLUMN_UP]->setEnabled(validIdx && !isFirst); + actionMap[MOVE_COLUMN_DOWN]->setEnabled(validIdx && !isLast); +} + +void TableWindow::updateStructureCommitState() +{ + bool modified = isModified(); + actionMap[COMMIT_STRUCTURE]->setEnabled(modified); + actionMap[ROLLBACK_STRUCTURE]->setEnabled(modified && existingTable); +} + +void TableWindow::updateTableConstraintsToolbarState() +{ + QItemSelectionModel *selModel = ui->tableConstraintsView->selectionModel(); + bool anyColumn = structureModel && structureModel->rowCount() > 0; + bool validIdx = false; + bool isFirst = false; + bool isLast = false; + if (selModel) + { + QModelIndex currIdx = selModel->currentIndex(); + if (currIdx.isValid()) + { + validIdx = true; + if (currIdx.row() == 0) + isFirst = true; + + if (currIdx.row() == (structureConstraintsModel->rowCount() - 1)) + isLast = true; + } + } + + actionMap[EDIT_TABLE_CONSTRAINT]->setEnabled(anyColumn && validIdx); + actionMap[DEL_TABLE_CONSTRAINT]->setEnabled(anyColumn && validIdx); + actionMap[MOVE_CONSTRAINT_UP]->setEnabled(anyColumn && validIdx && !isFirst); + actionMap[MOVE_CONSTRAINT_DOWN]->setEnabled(anyColumn && validIdx && !isLast); +} + +void TableWindow::setupDefShortcuts() +{ + // Widget context + setShortcutContext({ + REFRESH_STRUCTURE, + REFRESH_INDEXES, + REFRESH_TRIGGERS, + ADD_COLUMN, + EDIT_COLUMN, + DEL_COLUMN, + ADD_TABLE_CONSTRAINT, + EDIT_TABLE_CONSTRAINT, + DEL_TABLE_CONSTRAINT, + ADD_INDEX, + EDIT_INDEX, + DEL_INDEX, + ADD_TRIGGER, + EDIT_TRIGGER, + DEL_TRIGGER, + }, + Qt::WidgetWithChildrenShortcut); + + BIND_SHORTCUTS(TableWindow, Action); +} + +void TableWindow::executionSuccessful() +{ + modifyingThisTable = false; + dataLoaded = true; +} + +void TableWindow::executionFailed(const QString& errorText) +{ + modifyingThisTable = false; + notifyError(tr("Could not load data for table %1. Error details: %2").arg(table).arg(errorText)); +} + +void TableWindow::initDbAndTable() +{ + if (db->getVersion() == 2) + { + ui->withoutRowIdCheck->setVisible(false); + } + + if (existingTable) + { + dataModel->setDb(db); + dataModel->setDatabaseAndTable(database, table); + } + + ui->tableNameEdit->setText(table); // TODO no attached/temp db name support here + + if (structureModel) + { + delete structureModel; + structureModel = nullptr; + } + + if (structureConstraintsModel) + { + delete structureConstraintsModel; + structureConstraintsModel = nullptr; + } + + if (constraintTabModel) + { + delete constraintTabModel; + constraintTabModel = nullptr; + } + + structureModel = new TableStructureModel(this); + structureConstraintsModel = new TableConstraintsModel(this); + constraintTabModel = new ConstraintTabModel(this); + + // Columns model signals + connect(structureModel, SIGNAL(columnModified(QString,SqliteCreateTable::Column*)), + structureConstraintsModel, SLOT(columnModified(QString,SqliteCreateTable::Column*))); + connect(structureModel, SIGNAL(columnDeleted(QString)), + structureConstraintsModel, SLOT(columnDeleted(QString))); + connect(structureModel, SIGNAL(columnsOrderChanged()), this, SLOT(updateStructureToolbarState())); + + connect(structureModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(updateDdlTab())); + connect(structureModel, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), this, SLOT(updateDdlTab())); + connect(structureModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(updateDdlTab())); + connect(structureModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(updateDdlTab())); + connect(structureModel, SIGNAL(modifiyStateChanged()), this, SIGNAL(modifyStatusChanged())); + + ui->structureView->setModel(structureModel); + ui->structureView->verticalHeader()->setDefaultSectionSize(ui->structureView->fontMetrics().height() + 8); + + // Constraints model signals + connect(structureConstraintsModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(updateDdlTab())); + connect(structureConstraintsModel, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), this, SLOT(updateDdlTab())); + connect(structureConstraintsModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(updateDdlTab())); + connect(structureConstraintsModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(updateDdlTab())); + + connect(structureConstraintsModel, SIGNAL(modifiyStateChanged()), this, SIGNAL(modifyStatusChanged())); + connect(structureConstraintsModel, SIGNAL(constraintOrderChanged()), this, SLOT(updateTableConstraintsToolbarState())); + + ui->tableConstraintsView->setModel(structureConstraintsModel); + ui->tableConstraintsView->verticalHeader()->setDefaultSectionSize(ui->tableConstraintsView->fontMetrics().height() + 8); + + // Constraint tab model signals + connect(structureModel, SIGNAL(rowsInserted(QModelIndex,int,int)), constraintTabModel, SLOT(updateModel())); + connect(structureModel, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), constraintTabModel, SLOT(updateModel())); + connect(structureModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), constraintTabModel, SLOT(updateModel())); + connect(structureModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), constraintTabModel, SLOT(updateModel())); + connect(structureConstraintsModel, SIGNAL(rowsInserted(QModelIndex,int,int)), constraintTabModel, SLOT(updateModel())); + connect(structureConstraintsModel, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), constraintTabModel, SLOT(updateModel())); + connect(structureConstraintsModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), constraintTabModel, SLOT(updateModel())); + connect(structureConstraintsModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), constraintTabModel, SLOT(updateModel())); + + ui->constraintsView->setModel(constraintTabModel); + + connect(ui->withoutRowIdCheck, SIGNAL(clicked()), this, SLOT(withOutRowIdChanged())); + + ui->ddlEdit->setSqliteVersion(db->getVersion()); + parseDdl(); + updateIndexes(); + updateTriggers(); + + // (Re)connect to DB signals + connect(db, SIGNAL(dbObjectDeleted(QString,QString,DbObjectType)), this, SLOT(checkIfTableDeleted(QString,QString,DbObjectType))); + + // Selection model is recreated when setModel() is called on the view + connect(ui->structureView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), + this, SLOT(updateStructureToolbarState())); + connect(ui->tableConstraintsView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), + this, SLOT(updateTableConstraintsToolbarState())); +} + +void TableWindow::setupCoverWidget() +{ + widgetCover = new WidgetCover(this); + widgetCover->initWithInterruptContainer(); + widgetCover->hide(); + connect(widgetCover, SIGNAL(cancelClicked()), structureExecutor, SLOT(interrupt())); +} + +void TableWindow::parseDdl() +{ + if (existingTable) + { + SchemaResolver resolver(db); + SqliteQueryPtr parsedObject = resolver.getParsedObject(database, table, SchemaResolver::TABLE); + if (!parsedObject.dynamicCast<SqliteCreateTable>()) + { + notifyError(tr("Could not process the %1 table correctly. Unable to open a table window.").arg(table)); + invalid = true; + return; + } + + createTable = parsedObject.dynamicCast<SqliteCreateTable>(); + } + else + { + createTable = SqliteCreateTablePtr::create(); + createTable->table = table; + createTable->dialect = db->getDialect(); + } + originalCreateTable = SqliteCreateTablePtr::create(*createTable); + structureModel->setCreateTable(createTable.data()); + structureConstraintsModel->setCreateTable(createTable.data()); + constraintTabModel->setCreateTable(createTable.data()); + ui->withoutRowIdCheck->setChecked(!createTable->withOutRowId.isNull()); + ui->tableConstraintsView->resizeColumnsToContents(); + ui->structureView->resizeColumnsToContents(); + ui->constraintsView->resizeColumnsToContents(); + + updateStructureToolbarState(); + updateTableConstraintsToolbarState(); + updateDdlTab(); +} + +void TableWindow::changeEvent(QEvent *e) +{ + QWidget::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + ui->retranslateUi(this); + break; + default: + break; + } +} + +QVariant TableWindow::saveSession() +{ + if (!db || DBLIST->isTemporary(db)) + return QVariant(); + + QHash<QString,QVariant> sessionValue; + sessionValue["table"] = table; + sessionValue["db"] = db->getName(); + return sessionValue; +} + +bool TableWindow::restoreSession(const QVariant& sessionValue) +{ + QHash<QString, QVariant> value = sessionValue.toHash(); + if (value.size() == 0) + { + notifyWarn("Could not restore window, because no database or table was stored in session for this window."); + return false; + } + + if (!value.contains("db") || !value.contains("table")) + { + notifyWarn("Could not restore window, because no database or table was stored in session for this window."); + return false; + } + + db = DBLIST->getByName(value["db"].toString()); + if (!db || !db->isValid() || (!db->isOpen() && !db->open())) + { + notifyWarn(tr("Could not restore window, because database %1 could not be resolved.").arg(value["db"].toString())); + return false; + } + + table = value["table"].toString(); + database = value["database"].toString(); + SchemaResolver resolver(db); + if (!resolver.getTables(database).contains(table, Qt::CaseInsensitive)) + { + notifyWarn(tr("Could not restore window, because the table %1 doesn't exist in the database %2.").arg(table).arg(db->getName())); + return false; + } + + initDbAndTable(); + applyInitialTab(); + return true; +} + +Icon* TableWindow::getIconNameForMdiWindow() +{ + return ICONS.TABLE; +} + +QString TableWindow::getTitleForMdiWindow() +{ + QString dbSuffix = (!db ? "" : (" (" + db->getName() + ")")); + if (existingTable) + return table + dbSuffix; + + QStringList existingNames = MainWindow::getInstance()->getMdiArea()->getWindowTitles(); + if (existingNames.contains(windowTitle())) + return windowTitle(); + + // Generate new name + QString title = tr("New table %1").arg(newTableWindowNum++); + while (existingNames.contains(title)) + title = tr("New table %1").arg(newTableWindowNum++); + + title += dbSuffix; + return title; +} + +Db* TableWindow::getDb() const +{ + return db; +} + +QString TableWindow::getTable() const +{ + return table; +} + +void TableWindow::dbClosedFinalCleanup() +{ + db = nullptr; + dataModel->setDb(nullptr); + structureExecutor->setDb(nullptr); +} + +void TableWindow::checkIfTableDeleted(const QString& database, const QString& object, DbObjectType type) +{ + UNUSED(database); + + // TODO uncomment below when dbnames are supported +// if (this->database != database) +// return; + + switch (type) + { + case DbObjectType::TABLE: + break; + case DbObjectType::INDEX: + checkIfIndexDeleted(object); + return; + case DbObjectType::TRIGGER: + checkIfTriggerDeleted(object); + return; + case DbObjectType::VIEW: + return; + } + + if (modifyingThisTable) + return; + + if (object.compare(table, Qt::CaseInsensitive) == 0) + { + dbClosedFinalCleanup(); + getMdiWindow()->close(); + } +} + +void TableWindow::checkIfIndexDeleted(const QString& object) +{ + for (int i = 0, total = ui->indexList->rowCount(); i < total; ++i) + { + if (ui->indexList->item(i, 0)->text().compare(object, Qt::CaseInsensitive) == 0) + { + ui->indexList->removeRow(i); + return; + } + } +} + +void TableWindow::checkIfTriggerDeleted(const QString& object) +{ + for (int i = 0, total = ui->triggerList->rowCount(); i < total; ++i) + { + if (ui->triggerList->item(i, 0)->text().compare(object, Qt::CaseInsensitive) == 0) + { + ui->triggerList->removeRow(i); + return; + } + } +} + +void TableWindow::refreshStructure() +{ + parseDdl(); + updateIndexes(); + updateTriggers(); +} + +void TableWindow::commitStructure(bool skipWarning) +{ + if (!isModified()) + { + qWarning() << "Called TableWindow::commitStructure(), but isModified() returned false."; + updateStructureCommitState(); + return; + } + + if (!validate(skipWarning)) + return; + + executeStructureChanges(); +} + +void TableWindow::changesSuccessfullyCommited() +{ + QStringList sqls = structureExecutor->getQueries(); + CFG->addDdlHistory(sqls.join("\n"), db->getName(), db->getPath()); + + widgetCover->hide(); + + originalCreateTable = createTable; + structureModel->setCreateTable(createTable.data()); + structureConstraintsModel->setCreateTable(createTable.data()); + dataLoaded = false; + + QString oldTable = table; + database = createTable->database; + table = createTable->table; + existingTable = true; + initDbAndTable(); + updateStructureCommitState(); + updateNewTableState(); + updateWindowTitle(); + + DBTREE->refreshSchema(db); + + if (tableModifier) + { + QList<QStringList> modifiedObjects = { + tableModifier->getModifiedTables(), + tableModifier->getModifiedIndexes(), + tableModifier->getModifiedTriggers(), + tableModifier->getModifiedViews() + }; + NotifyManager* notifyManager = NotifyManager::getInstance(); + foreach (const QStringList& objList, modifiedObjects) + { + foreach (const QString& obj, objList) + { + if (obj.compare(oldTable, Qt::CaseInsensitive) == 0) + continue; + + notifyManager->modified(db, database, obj); + } + } + } +} + +void TableWindow::changesFailedToCommit(int errorCode, const QString& errorText) +{ + qDebug() << "TableWindow::changesFailedToCommit:" << errorCode << errorText; + + widgetCover->hide(); + notifyError(tr("Could not commit table structure. Error message: %1", "table window").arg(errorText)); +} + +void TableWindow::rollbackStructure() +{ + createTable = SqliteCreateTablePtr::create(*originalCreateTable.data()); + structureModel->setCreateTable(createTable.data()); + structureConstraintsModel->setCreateTable(createTable.data()); + constraintTabModel->setCreateTable(createTable.data()); + ui->tableNameEdit->setText(createTable->table); + + updateStructureCommitState(); + updateStructureToolbarState(); + updateTableConstraintsToolbarState(); +} + +void TableWindow::addColumn() +{ + SqliteCreateTable::Column column; + column.setParent(createTable.data()); + + ColumnDialog columnDialog(db, this); + columnDialog.setColumn(&column); + if (columnDialog.exec() != QDialog::Accepted) + return; + + SqliteCreateTable::Column* modifiedColumn = columnDialog.getModifiedColumn(); + structureModel->appendColumn(modifiedColumn); + ui->structureView->resizeColumnToContents(0); + + ui->structureView->setCurrentIndex(structureModel->index(structureModel->rowCount()-1, 0)); +} + +void TableWindow::editColumn() +{ + editColumn(structureCurrentIndex()); +} + +void TableWindow::delColumn() +{ + QModelIndex idx = structureCurrentIndex(); + delColumn(idx); +} + +void TableWindow::moveColumnUp() +{ + QModelIndex idx = structureCurrentIndex(); + if (!idx.isValid()) + { + qWarning() << "Called TableWindow::moveColumnUp() with invalid index."; + return; + } + + structureModel->moveColumnUp(idx.row()); +} + +void TableWindow::moveColumnDown() +{ + QModelIndex idx = structureCurrentIndex(); + if (!idx.isValid()) + { + qWarning() << "Called TableWindow::moveColumnDown() with invalid index."; + return; + } + + structureModel->moveColumnDown(idx.row()); +} + + +void TableWindow::addConstraint(ConstraintDialog::Constraint mode) +{ + NewConstraintDialog dialog(mode, createTable.data(), db, this); + if (dialog.exec() != QDialog::Accepted) + return; + + SqliteStatement* constrStmt = dialog.getConstraint(); + SqliteCreateTable::Constraint* tableConstr = dynamic_cast<SqliteCreateTable::Constraint*>(constrStmt); + if (!tableConstr) + { + qCritical() << "Constraint returned from ConstraintDialog was not of table type, while we're trying to add table constraint."; + return; + } + + structureConstraintsModel->appendConstraint(tableConstr); + ui->tableConstraintsView->resizeColumnToContents(0); + ui->tableConstraintsView->resizeColumnToContents(1); +} + +bool TableWindow::validate(bool skipWarning) +{ + if (!existingTable && !skipWarning && ui->tableNameEdit->text().isEmpty()) + { + int res = QMessageBox::warning(this, tr("Empty name"), tr("A blank name for the table is allowed in SQLite, but it is not recommended.\n" + "Are you sure you want to create a table with blank name?"), QMessageBox::Yes, QMessageBox::No); + + if (res != QMessageBox::Yes) + return false; + } + + if (structureModel->rowCount() == 0) + { + notifyError(tr("Cannot create a table without at least one column.")); + return false; + } + + if (ui->withoutRowIdCheck->isChecked()) + { + bool hasPk = false; + bool isPkAutoIncr = false; + + if (createTable->getConstraints(SqliteCreateTable::Constraint::PRIMARY_KEY).size() > 0) + hasPk = true; + + SqliteCreateTable::Column::Constraint* colConstraint = nullptr; + foreach (SqliteCreateTable::Column* column, createTable->columns) + { + colConstraint = column->getConstraint(SqliteCreateTable::Column::Constraint::PRIMARY_KEY); + if (colConstraint) + { + hasPk = true; + if (colConstraint->autoincrKw) + isPkAutoIncr = true; + } + } + + if (!hasPk) + { + notifyError(tr("Cannot create table without ROWID, if it has no PRIMARY KEY defined." + " Either uncheck the WITHOUT ROWID, or define a PRIMARY KEY.")); + return false; + } + + if (isPkAutoIncr) + { + notifyError(tr("Cannot use AUTOINCREMENT for PRIMARY KEY when WITHOUT ROWID clause is used." + " Either uncheck the WITHOUT ROWID, or the AUTOINCREMENT in a PRIMARY KEY.")); + return false; + } + } + + return true; +} + +bool TableWindow::isModified() const +{ + return (structureModel && structureModel->isModified()) || + (structureConstraintsModel && structureConstraintsModel->isModified()) || + (originalCreateTable && + (originalCreateTable->table != ui->tableNameEdit->text() || + originalCreateTable->withOutRowId != createTable->withOutRowId) + ) || + !existingTable; +} + +TokenList TableWindow::indexColumnTokens(SqliteCreateIndexPtr index) +{ + if (index->indexedColumns.size() == 0) + return TokenList(); + + SqliteIndexedColumn* firstCol = index->indexedColumns.first(); + SqliteIndexedColumn* lastCol = index->indexedColumns.last(); + if (firstCol->tokens.size() == 0) + return TokenList(); + + if (lastCol->tokens.size() == 0) + return TokenList(); + + int firstIdx = index->tokens.indexOf(firstCol->tokens.first()); + int lastIdx = index->tokens.indexOf(lastCol->tokens.last()); + + return index->tokens.mid(firstIdx, lastIdx-firstIdx+1); +} + +QString TableWindow::getCurrentIndex() const +{ + int row = ui->indexList->currentRow(); + QTableWidgetItem* item = ui->indexList->item(row, 0); + if (!item) + return QString::null; + + return item->text(); +} + +QString TableWindow::getCurrentTrigger() const +{ + int row = ui->triggerList->currentRow(); + QTableWidgetItem* item = ui->triggerList->item(row, 0); + if (!item) + return QString::null; + + return item->text(); +} + +void TableWindow::applyInitialTab() +{ + if (existingTable && !table.isNull() && CFG_UI.General.OpenTablesOnData.get()) + ui->tabWidget->setCurrentIndex(1); + else + ui->tabWidget->setCurrentIndex(0); +} + +void TableWindow::updateDdlTab() +{ + CodeFormatter* formatter = SQLITESTUDIO->getCodeFormatter(); + createTable->rebuildTokens(); + ui->ddlEdit->setPlainText(formatter->format("sql", createTable->detokenize(), db)); +} + +void TableWindow::updateNewTableState() +{ + for (int i = 1; i < 5; i++) + ui->tabWidget->setTabEnabled(i, existingTable); + + actionMap[EXPORT]->setEnabled(existingTable); + actionMap[IMPORT]->setEnabled(existingTable); + actionMap[POPULATE]->setEnabled(existingTable); + actionMap[CREATE_SIMILAR]->setEnabled(existingTable); + actionMap[REFRESH_STRUCTURE]->setEnabled(existingTable); +} + +void TableWindow::addConstraint() +{ + addConstraint(ConstraintDialog::UNKNOWN); +} + +void TableWindow::editConstraint() +{ + QModelIndex idx = ui->tableConstraintsView->currentIndex(); + editConstraint(idx); +} + +void TableWindow::delConstraint() +{ + QModelIndex idx = ui->tableConstraintsView->currentIndex(); + delConstraint(idx); +} + +void TableWindow::editConstraint(const QModelIndex& idx) +{ + if (!idx.isValid()) + return; + + SqliteCreateTable::Constraint* constr = structureConstraintsModel->getConstraint(idx.row()); + ConstraintDialog dialog(ConstraintDialog::EDIT, constr, createTable.data(), db, this); + if (dialog.exec() != QDialog::Accepted) + return; + + structureConstraintsModel->constraintModified(idx.row()); + ui->tableConstraintsView->resizeColumnToContents(0); + ui->tableConstraintsView->resizeColumnToContents(1); +} + +void TableWindow::delConstraint(const QModelIndex& idx) +{ + if (!idx.isValid()) + return; + + SqliteCreateTable::Constraint* constr = structureConstraintsModel->getConstraint(idx.row()); + + QString arg = constr->name.isNull() ? constr->typeString() : constr->name; + QString msg = tr("Are you sure you want to delete table constraint '%1'?", "table window").arg(arg); + int btn = QMessageBox::question(this, tr("Delete constraint", "table window"), msg); + if (btn != QMessageBox::Yes) + return; + + structureConstraintsModel->delConstraint(idx.row()); + ui->structureView->resizeColumnToContents(0); +} + +void TableWindow::moveConstraintUp() +{ + QModelIndex idx = ui->tableConstraintsView->currentIndex(); + if (!idx.isValid()) + return; + + structureConstraintsModel->moveConstraintUp(idx.row()); + updateTableConstraintsToolbarState(); + updateStructureCommitState(); +} + +void TableWindow::moveConstraintDown() +{ + QModelIndex idx = ui->tableConstraintsView->currentIndex(); + if (!idx.isValid()) + return; + + structureConstraintsModel->moveConstraintDown(idx.row()); + updateTableConstraintsToolbarState(); + updateStructureCommitState(); +} + +void TableWindow::addPk() +{ + addConstraint(ConstraintDialog::PK); +} + +void TableWindow::addFk() +{ + addConstraint(ConstraintDialog::FK); +} + +void TableWindow::addUnique() +{ + addConstraint(ConstraintDialog::UNIQUE); +} + +void TableWindow::addCheck() +{ + addConstraint(ConstraintDialog::CHECK); +} + +void TableWindow::exportTable() +{ + if (!ExportManager::isAnyPluginAvailable()) + { + notifyError(tr("Cannot export, because no export plugin is loaded.")); + return; + } + + ExportDialog dialog(this); + dialog.setTableMode(db, table); + dialog.exec(); +} + +void TableWindow::importTable() +{ + if (!ImportManager::isAnyPluginAvailable()) + { + notifyError(tr("Cannot import, because no import plugin is loaded.")); + return; + } + + ImportDialog dialog(this); + dialog.setDbAndTable(db, table); + if (dialog.exec() == QDialog::Accepted && dataLoaded) + ui->dataView->refreshData(); +} + +void TableWindow::populateTable() +{ + PopulateDialog dialog(this); + dialog.setDbAndTable(db, table); + if (dialog.exec() == QDialog::Accepted && dataLoaded) + ui->dataView->refreshData(); +} + +void TableWindow::createSimilarTable() +{ + DbObjectDialogs dialog(db); + dialog.addTableSimilarTo(QString(), table); +} + +void TableWindow::tabChanged(int newTab) +{ + switch (newTab) + { + case 1: + { + if (isModified()) + { + int res = QMessageBox::question(this, tr("Uncommited changes"), + tr("There are uncommited structure modifications. You cannot browse or edit data until you have " + "table structure settled.\n" + "Do you want to commit the structure, or do you want to go back to the structure tab?"), + tr("Go back to structure tab"), tr("Commit modifications and browse data.")); + + ui->tabWidget->setCurrentIndex(0); + if (res == 1) + commitStructure(true); + + break; + } + + if (!dataLoaded) + ui->dataView->refreshData(); + + break; + } + } +} + +void TableWindow::on_structureView_doubleClicked(const QModelIndex &index) +{ + editColumn(index); +} + +void TableWindow::on_tableConstraintsView_doubleClicked(const QModelIndex &index) +{ + editConstraint(index); +} + +void TableWindow::nameChanged() +{ + if (!createTable) + return; + + createTable->table = ui->tableNameEdit->text(); + updateDdlTab(); +} + +void TableWindow::withOutRowIdChanged() +{ + if (!createTable) + return; + + createTable->withOutRowId = ui->withoutRowIdCheck->isChecked() ? QStringLiteral("ROWID") : QString::null; + updateDdlTab(); + emit modifyStatusChanged(); +} + +void TableWindow::addIndex() +{ + DbObjectDialogs dialogs(db, this); + dialogs.addIndex(table); + updateIndexes(); +} + +void TableWindow::editIndex() +{ + QString index = getCurrentIndex(); + if (index.isNull()) + return; + + DbObjectDialogs dialogs(db, this); + dialogs.editIndex(index); + updateIndexes(); +} + +void TableWindow::delIndex() +{ + QString index = getCurrentIndex(); + if (index.isNull()) + return; + + DbObjectDialogs dialogs(db, this); + dialogs.dropObject(index); + updateIndexes(); +} + +void TableWindow::addTrigger() +{ + DbObjectDialogs dialogs(db, this); + dialogs.addTriggerOnTable(table); + updateTriggers(); +} + +void TableWindow::editTrigger() +{ + QString trigger = getCurrentTrigger(); + if (trigger.isNull()) + return; + + DbObjectDialogs dialogs(db, this); + dialogs.editTrigger(trigger); + updateTriggers(); +} + +void TableWindow::delTrigger() +{ + QString trigger = getCurrentTrigger(); + if (trigger.isNull()) + return; + + DbObjectDialogs dialogs(db, this); + dialogs.dropObject(trigger); + updateTriggers(); +} + +void TableWindow::updateIndexesState() +{ + bool editDel = ui->indexList->currentItem() != nullptr; + actionMap[REFRESH_INDEXES]->setEnabled(existingTable); + actionMap[ADD_INDEX]->setEnabled(existingTable); + actionMap[EDIT_INDEX]->setEnabled(editDel); + actionMap[DEL_INDEX]->setEnabled(editDel); +} + +void TableWindow::updateTriggersState() +{ + bool editDel = ui->triggerList->currentItem() != nullptr; + actionMap[REFRESH_TRIGGERS]->setEnabled(existingTable); + actionMap[ADD_TRIGGER]->setEnabled(existingTable); + actionMap[EDIT_TRIGGER]->setEnabled(editDel); + actionMap[DEL_TRIGGER]->setEnabled(editDel); +} + +void TableWindow::nextTab() +{ + int idx = ui->tabWidget->currentIndex(); + idx++; + ui->tabWidget->setCurrentIndex(idx); +} + +void TableWindow::prevTab() +{ + int idx = ui->tabWidget->currentIndex(); + idx--; + ui->tabWidget->setCurrentIndex(idx); +} + +void TableWindow::updateIndexes() +{ + ui->indexList->clear(); + + if (!db || !db->isValid()) + return; + + SchemaResolver resolver(db); + resolver.setIgnoreSystemObjects(true); + QList<SqliteCreateIndexPtr> indexes = resolver.getParsedIndexesForTable(database, table); + + ui->indexList->setColumnCount(4); + ui->indexList->setRowCount(indexes.size()); + ui->indexList->setHorizontalHeaderLabels({ + tr("Name", "table window indexes"), + tr("Unique", "table window indexes"), + tr("Columns", "table window indexes"), + tr("Partial index condition", "table window indexes"), + }); + + Dialect dialect= db->getDialect(); + if (dialect == Dialect::Sqlite2) + ui->indexList->setColumnCount(3); + + ui->indexList->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); + + QTableWidgetItem* item = nullptr; + int row = 0; + foreach (SqliteCreateIndexPtr index, indexes) + { + item = new QTableWidgetItem(index->index); + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + ui->indexList->setItem(row, 0, item); + + // TODO a delegate to make the checkbox in the center, or use setCellWidget() + item = new QTableWidgetItem(); + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + item->setCheckState(index->uniqueKw ? Qt::Checked : Qt::Unchecked); + ui->indexList->setItem(row, 1, item); + + item = new QTableWidgetItem(indexColumnTokens(index).detokenize()); + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + ui->indexList->setItem(row, 2, item); + + if (dialect == Dialect::Sqlite3) + { + item = new QTableWidgetItem(index->where ? index->where->detokenize() : ""); + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + ui->indexList->setItem(row, 3, item); + } + + row++; + } + + ui->indexList->resizeColumnsToContents(); + updateIndexesState(); +} + +void TableWindow::updateTriggers() +{ + if (!db || !db->isValid()) + return; + + SchemaResolver resolver(db); + QList<SqliteCreateTriggerPtr> triggers = resolver.getParsedTriggersForTable(database, table); + + ui->triggerList->setColumnCount(4); + ui->triggerList->setRowCount(triggers.size()); + ui->triggerList->horizontalHeader()->setMaximumSectionSize(200); + ui->triggerList->setHorizontalHeaderLabels({ + tr("Name", "table window triggers"), + tr("Event", "table window triggers"), + tr("Condition", "table window triggers"), + tr("Details", "table window triggers") + }); + + QTableWidgetItem* item = nullptr; + QString timeAndEvent; + int row = 0; + foreach (SqliteCreateTriggerPtr trig, triggers) + { + item = new QTableWidgetItem(trig->trigger); + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + ui->triggerList->setItem(row, 0, item); + + timeAndEvent = trig->tokensMap["trigger_time"].detokenize() + trig->tokensMap["trigger_event"].detokenize(); + item = new QTableWidgetItem(timeAndEvent); + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + ui->triggerList->setItem(row, 1, item); + + item = new QTableWidgetItem(trig->precondition ? trig->precondition->detokenize().trimmed() : ""); + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + ui->triggerList->setItem(row, 2, item); + + item = new QTableWidgetItem(trig->tokensMap["trigger_cmd_list"].detokenize().trimmed()); + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + ui->triggerList->setItem(row, 3, item); + + row++; + } + + ui->triggerList->resizeColumnsToContents(); + updateTriggersState(); +} + +void TableWindow::editColumn(const QString& columnName) +{ + QModelIndex colIdx = structureModel->findColumn(columnName); + if (!colIdx.isValid()) + return; + + editColumn(colIdx); +} + +void TableWindow::delColumn(const QString& columnName) +{ + QModelIndex colIdx = structureModel->findColumn(columnName); + if (!colIdx.isValid()) + return; + + delColumn(colIdx); +} + +bool TableWindow::restoreSessionNextTime() +{ + return existingTable && db && !DBLIST->isTemporary(db); +} + +QToolBar* TableWindow::getToolBar(int toolbar) const +{ + switch (static_cast<ToolBar>(toolbar)) + { + case TOOLBAR_STRUCTURE: + return ui->structureToolBar; + case TOOLBAR_INDEXES: + return ui->indexToolBar; + case TOOLBAR_TRIGGERS: + return ui->triggerToolBar; + } + return nullptr; +} + +bool TableWindow::handleInitialFocus() +{ + if (!existingTable) + { + ui->tableNameEdit->setFocus(); + return true; + } + return false; +} + +bool TableWindow::isUncommited() const +{ + return ui->dataView->isUncommited() || isModified(); +} + +QString TableWindow::getQuitUncommitedConfirmMessage() const +{ + QString title = getMdiWindow()->windowTitle(); + if (ui->dataView->isUncommited() && isModified()) + return tr("Table window \"%1\" has uncommited structure modifications and data.").arg(title); + else if (ui->dataView->isUncommited()) + return tr("Table window \"%1\" has uncommited data.").arg(title); + else if (isModified()) + return tr("Table window \"%1\" has uncommited structure modifications.").arg(title); + else + { + qCritical() << "Unhandled message case in TableWindow::getQuitUncommitedConfirmMessage()."; + return QString(); + } +} + +void TableWindow::useCurrentTableAsBaseForNew() +{ + newTable(); + ui->tableNameEdit->clear(); + updateWindowTitle(); + ui->tableNameEdit->setFocus(); + updateAfterInit(); +} + +Db* TableWindow::getAssociatedDb() const +{ + return db; +} diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/tablewindow.h b/SQLiteStudio3/guiSQLiteStudio/windows/tablewindow.h new file mode 100644 index 0000000..b0ee1e3 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/tablewindow.h @@ -0,0 +1,241 @@ +#ifndef TABLEWINDOW_H +#define TABLEWINDOW_H + +#include "db/db.h" +#include "mdichild.h" +#include "common/extactioncontainer.h" +#include "parser/ast/sqlitecreatetable.h" +#include "parser/ast/sqlitecreateindex.h" +#include "dialogs/constraintdialog.h" +#include "db/chainexecutor.h" +#include "guiSQLiteStudio_global.h" +#include <QPointer> + +class TableModifier; +class SqlTableModel; +class ExtLineEdit; +class IntValidator; +class QLabel; +class TableStructureModel; +class TableConstraintsModel; +class QProgressBar; +class WidgetCover; +class SqliteSyntaxHighlighter; +class ConstraintTabModel; + +namespace Ui { + class TableWindow; +} + +CFG_KEY_LIST(TableWindow, QObject::tr("Table window"), + CFG_KEY_ENTRY(REFRESH_STRUCTURE, Qt::Key_F5, QObject::tr("Refresh table structure")) + CFG_KEY_ENTRY(ADD_COLUMN, Qt::Key_Insert, QObject::tr("Add new column")) + CFG_KEY_ENTRY(EDIT_COLUMN, Qt::Key_Return, QObject::tr("Edit selected column")) + CFG_KEY_ENTRY(DEL_COLUMN, Qt::Key_Delete, QObject::tr("Delete selected column")) + CFG_KEY_ENTRY(EXPORT, Qt::CTRL + Qt::Key_E, QObject::tr("Export table data")) + CFG_KEY_ENTRY(IMPORT, Qt::CTRL + Qt::Key_I, QObject::tr("Import data to the table")) + CFG_KEY_ENTRY(ADD_TABLE_CONSTRAINT, Qt::Key_Insert, QObject::tr("Add new table constraint")) + CFG_KEY_ENTRY(EDIT_TABLE_CONSTRAINT, Qt::Key_Return, QObject::tr("Edit selected table constraint")) + CFG_KEY_ENTRY(DEL_TABLE_CONSTRAINT, Qt::Key_Delete, QObject::tr("Delete selected table constraint")) + CFG_KEY_ENTRY(REFRESH_INDEXES, Qt::Key_F5, QObject::tr("Refresh table index list")) + CFG_KEY_ENTRY(ADD_INDEX, Qt::Key_Insert, QObject::tr("Add new index")) + CFG_KEY_ENTRY(EDIT_INDEX, Qt::Key_Return, QObject::tr("Edit selected index")) + CFG_KEY_ENTRY(DEL_INDEX, Qt::Key_Delete, QObject::tr("Delete selected index")) + CFG_KEY_ENTRY(REFRESH_TRIGGERS, Qt::Key_F5, QObject::tr("Refresh table trigger list")) + CFG_KEY_ENTRY(ADD_TRIGGER, Qt::Key_Insert, QObject::tr("Add new trigger")) + CFG_KEY_ENTRY(EDIT_TRIGGER, Qt::Key_Return, QObject::tr("Edit selected trigger")) + CFG_KEY_ENTRY(DEL_TRIGGER, Qt::Key_Delete, QObject::tr("Delete selected trigger")) + CFG_KEY_ENTRY(NEXT_TAB, Qt::ALT + Qt::Key_Right, QObject::tr("Go to next tab")) + CFG_KEY_ENTRY(PREV_TAB, Qt::ALT + Qt::Key_Left, QObject::tr("Go to previous tab")) +) + +class GUI_API_EXPORT TableWindow : public MdiChild +{ + Q_OBJECT + Q_ENUMS(Action) + + public: + enum Action + { + // Structure tab + REFRESH_STRUCTURE, + COMMIT_STRUCTURE, + ROLLBACK_STRUCTURE, + ADD_COLUMN, + EDIT_COLUMN, + DEL_COLUMN, + MOVE_COLUMN_UP, + MOVE_COLUMN_DOWN, + ADD_TABLE_CONSTRAINT, + EDIT_TABLE_CONSTRAINT, + DEL_TABLE_CONSTRAINT, + ADD_TABLE_PK, + ADD_TABLE_FK, + ADD_TABLE_UNIQUE, + ADD_TABLE_CHECK, + MOVE_CONSTRAINT_UP, + MOVE_CONSTRAINT_DOWN, + EXPORT, + IMPORT, + POPULATE, + CREATE_SIMILAR, + // Indexes tab + REFRESH_INDEXES, + ADD_INDEX, + EDIT_INDEX, + DEL_INDEX, + // Triggers tab + REFRESH_TRIGGERS, + ADD_TRIGGER, + EDIT_TRIGGER, + DEL_TRIGGER, + // All tabs + NEXT_TAB, + PREV_TAB + }; + + enum ToolBar + { + TOOLBAR_STRUCTURE, + TOOLBAR_INDEXES, + TOOLBAR_TRIGGERS + }; + + explicit TableWindow(QWidget *parent = 0); + TableWindow(Db* db, QWidget *parent = 0); + TableWindow(const TableWindow& win); + TableWindow(QWidget *parent, Db* db, const QString& database, const QString& table); + ~TableWindow(); + + static void staticInit(); + static void insertAction(ExtActionPrototype* action, ToolBar toolbar = TOOLBAR_STRUCTURE); + static void insertActionBefore(ExtActionPrototype* action, Action beforeAction, ToolBar toolbar = TOOLBAR_STRUCTURE); + static void insertActionAfter(ExtActionPrototype* action, Action afterAction, ToolBar toolbar = TOOLBAR_STRUCTURE); + static void removeAction(ExtActionPrototype* action, ToolBar toolbar = TOOLBAR_STRUCTURE); + + QString getTable() const; + Db* getDb() const; + bool handleInitialFocus(); + bool isUncommited() const; + QString getQuitUncommitedConfirmMessage() const; + void useCurrentTableAsBaseForNew(); + Db* getAssociatedDb() const; + + protected: + void changeEvent(QEvent *e); + QVariant saveSession(); + bool restoreSession(const QVariant& sessionValue); + Icon* getIconNameForMdiWindow(); + QString getTitleForMdiWindow(); + void createActions(); + void setupDefShortcuts(); + bool restoreSessionNextTime(); + QToolBar* getToolBar(int toolbar) const; + + private: + void init(); + void newTable(); + void parseDdl(); + void initDbAndTable(); + void setupCoverWidget(); + void createStructureActions(); + void createDataGridActions(); + void createDataFormActions(); + void createIndexActions(); + void createTriggerActions(); + void editColumn(const QModelIndex& idx); + void delColumn(const QModelIndex& idx); + void editConstraint(const QModelIndex& idx); + void delConstraint(const QModelIndex& idx); + void executeStructureChanges(); + void updateAfterInit(); + QModelIndex structureCurrentIndex() const; + void addConstraint(ConstraintDialog::Constraint mode); + bool validate(bool skipWarning = false); + bool isModified() const; + TokenList indexColumnTokens(SqliteCreateIndexPtr index); + QString getCurrentIndex() const; + QString getCurrentTrigger() const; + void applyInitialTab(); + + int newTableWindowNum = 1; + + Db* db = nullptr; + QString database; + QString table; + Ui::TableWindow *ui = nullptr; + SqlTableModel* dataModel = nullptr; + bool dataLoaded = false; + bool existingTable = true; + SqliteCreateTablePtr createTable; + SqliteCreateTablePtr originalCreateTable; + TableStructureModel* structureModel = nullptr; + TableConstraintsModel* structureConstraintsModel = nullptr; + ConstraintTabModel* constraintTabModel = nullptr; + WidgetCover* widgetCover = nullptr; + ChainExecutor* structureExecutor = nullptr; + TableModifier* tableModifier = nullptr; + bool modifyingThisTable = false; + + private slots: + void executionSuccessful(); + void executionFailed(const QString& errorText); + void dbClosedFinalCleanup(); + void checkIfTableDeleted(const QString& database, const QString& object, DbObjectType type); + void checkIfIndexDeleted(const QString& object); + void checkIfTriggerDeleted(const QString& object); + void refreshStructure(); + void commitStructure(bool skipWarning = false); + void changesSuccessfullyCommited(); + void changesFailedToCommit(int errorCode, const QString& errorText); + void rollbackStructure(); + void editColumn(); + void delColumn(); + void moveColumnUp(); + void moveColumnDown(); + void addConstraint(); + void editConstraint(); + void delConstraint(); + void moveConstraintUp(); + void moveConstraintDown(); + void addPk(); + void addFk(); + void addUnique(); + void addCheck(); + void exportTable(); + void importTable(); + void populateTable(); + void createSimilarTable(); + void tabChanged(int newTab); + void updateStructureToolbarState(); + void updateStructureCommitState(); + void updateTableConstraintsToolbarState(); + void updateDdlTab(); + void updateNewTableState(); + void on_structureView_doubleClicked(const QModelIndex &index); + void on_tableConstraintsView_doubleClicked(const QModelIndex &index); + void nameChanged(); + void withOutRowIdChanged(); + void addIndex(); + void editIndex(); + void delIndex(); + void addTrigger(); + void editTrigger(); + void delTrigger(); + void updateIndexesState(); + void updateTriggersState(); + void nextTab(); + void prevTab(); + + public slots: + void updateIndexes(); + void updateTriggers(); + void addColumn(); + void editColumn(const QString& columnName); + void delColumn(const QString& columnName); + + signals: + void modifyStatusChanged(); +}; + +#endif // TABLEWINDOW_H diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/tablewindow.ui b/SQLiteStudio3/guiSQLiteStudio/windows/tablewindow.ui new file mode 100644 index 0000000..8c46443 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/tablewindow.ui @@ -0,0 +1,307 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>TableWindow</class> + <widget class="QWidget" name="TableWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>609</width> + <height>415</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="structureTab"> + <attribute name="title"> + <string>Structure</string> + </attribute> + <layout class="QVBoxLayout" name="structureTabLayout"> + <item> + <widget class="QToolBar" name="structureToolBar"/> + </item> + <item> + <widget class="QWidget" name="structureTopBar" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="tableNameLabel"> + <property name="text"> + <string>Table name:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="tableNameEdit"> + <property name="maximumSize"> + <size> + <width>200</width> + <height>16777215</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="withoutRowIdCheck"> + <property name="text"> + <string>WITHOUT ROWID</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QSplitter" name="structureSplitter"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="childrenCollapsible"> + <bool>false</bool> + </property> + <widget class="QTableView" name="structureView"> + <property name="dragEnabled"> + <bool>true</bool> + </property> + <property name="dragDropOverwriteMode"> + <bool>false</bool> + </property> + <property name="dragDropMode"> + <enum>QAbstractItemView::InternalMove</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="horizontalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + </widget> + <widget class="QWidget" name="constraintsWidget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QToolBar" name="tableConstraintsToolbar"/> + </item> + <item> + <widget class="QTableView" name="tableConstraintsView"> + <property name="dragEnabled"> + <bool>true</bool> + </property> + <property name="dragDropOverwriteMode"> + <bool>false</bool> + </property> + <property name="dragDropMode"> + <enum>QAbstractItemView::InternalMove</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="horizontalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="dataTab"> + <attribute name="title"> + <string>Data</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="DataView" name="dataView"> + <property name="tabPosition"> + <enum>QTabWidget::South</enum> + </property> + <property name="currentIndex"> + <number>-1</number> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="constraintsTab"> + <attribute name="title"> + <string>Constraints</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QTableView" name="constraintsView"> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="indexesTab"> + <attribute name="title"> + <string>Indexes</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QToolBar" name="indexToolBar"/> + </item> + <item> + <widget class="QTableWidget" name="indexList"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="horizontalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="triggersTab"> + <attribute name="title"> + <string>Triggers</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QToolBar" name="triggerToolBar"/> + </item> + <item> + <widget class="QTableWidget" name="triggerList"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="horizontalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="ddlTab"> + <attribute name="title"> + <string>DDL</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="SqlView" name="ddlEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>SqlView</class> + <extends>QPlainTextEdit</extends> + <header>sqlview.h</header> + </customwidget> + <customwidget> + <class>DataView</class> + <extends>QTabWidget</extends> + <header>dataview.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/viewwindow.cpp b/SQLiteStudio3/guiSQLiteStudio/windows/viewwindow.cpp new file mode 100644 index 0000000..d54a359 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/viewwindow.cpp @@ -0,0 +1,760 @@ +#include "viewwindow.h" +#include "ui_viewwindow.h" +#include "common/unused.h" +#include "schemaresolver.h" +#include "services/notifymanager.h" +#include "services/dbmanager.h" +#include "mainwindow.h" +#include "mdiarea.h" +#include "sqlitesyntaxhighlighter.h" +#include "datagrid/sqlquerymodel.h" +#include "common/utils_sql.h" +#include "viewmodifier.h" +#include "common/widgetcover.h" +#include "db/chainexecutor.h" +#include "dbtree/dbtree.h" +#include "parser/ast/sqlitecreatetrigger.h" +#include "dialogs/messagelistdialog.h" +#include "dbobjectdialogs.h" +#include "dialogs/ddlpreviewdialog.h" +#include "uiconfig.h" +#include "services/config.h" +#include <QPushButton> +#include <QProgressBar> +#include <QDebug> +#include <QMessageBox> + +CFG_KEYS_DEFINE(ViewWindow) + +ViewWindow::ViewWindow(QWidget *parent) : + MdiChild(parent), + ui(new Ui::ViewWindow) +{ + init(); + applyInitialTab(); +} + +ViewWindow::ViewWindow(Db* db, QWidget* parent) : + MdiChild(parent), + db(db), + ui(new Ui::ViewWindow) +{ + newView(); + init(); + applyInitialTab(); +} + +ViewWindow::ViewWindow(const ViewWindow& win) : + MdiChild(win.parentWidget()), + db(win.db), + database(win.database), + view(win.view), + ui(new Ui::ViewWindow) +{ + init(); + initView(); + applyInitialTab(); +} + +ViewWindow::ViewWindow(QWidget* parent, Db* db, const QString& database, const QString& view) : + MdiChild(parent), + db(db), + database(database), + view(view), + ui(new Ui::ViewWindow) +{ + init(); + initView(); + applyInitialTab(); +} + +ViewWindow::~ViewWindow() +{ + delete ui; +} + +void ViewWindow::changeEvent(QEvent *e) +{ + QWidget::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + ui->retranslateUi(this); + break; + default: + break; + } +} + +QVariant ViewWindow::saveSession() +{ + QHash<QString,QVariant> sessionValue; + sessionValue["view"] = view; + sessionValue["db"] = db->getName(); + return sessionValue; +} + +bool ViewWindow::restoreSession(const QVariant& sessionValue) +{ + QHash<QString, QVariant> value = sessionValue.toHash(); + if (value.size() == 0) + { + notifyWarn("Could not restore window, because no database or view was stored in session for this window."); + return false; + } + + if (!value.contains("db") || !value.contains("view")) + { + notifyWarn("Could not restore window, because no database or view was stored in session for this window."); + return false; + } + + db = DBLIST->getByName(value["db"].toString()); + if (!db) + { + notifyWarn(tr("Could not restore window, because database %1 could not be resolved.").arg(value["db"].toString())); + return false; + } + + if (!db->isOpen() && !db->open()) + { + notifyWarn(tr("Could not restore window, because database %1 could not be open.").arg(value["db"].toString())); + return false; + } + + view = value["view"].toString(); + database = value["database"].toString(); + SchemaResolver resolver(db); + if (!resolver.getViews(database).contains(view, Qt::CaseInsensitive)) + { + notifyWarn(tr("Could not restore window, because the view %1 doesn't exist in the database %2.").arg(view).arg(db->getName())); + return false; + } + + initView(); + applyInitialTab(); + return true; +} + +Icon* ViewWindow::getIconNameForMdiWindow() +{ + return ICONS.VIEW; +} + +QString ViewWindow::getTitleForMdiWindow() +{ + QString dbSuffix = (!db ? "" : (" (" + db->getName() + ")")); + if (existingView) + return view + dbSuffix; + + QStringList existingNames = MDIAREA->getWindowTitles(); + if (existingNames.contains(windowTitle())) + return windowTitle(); + + // Generate new name + QString title = tr("New view %1").arg(newViewWindowNum++); + while (existingNames.contains(title)) + title = tr("New view %1").arg(newViewWindowNum++); + + title += dbSuffix; + return title; +} + +void ViewWindow::createActions() +{ + createQueryTabActions(); + createTriggersTabActions(); + + createAction(NEXT_TAB, "next tab", this, SLOT(nextTab()), this); + createAction(PREV_TAB, "prev tab", this, SLOT(prevTab()), this); +} + +void ViewWindow::setupDefShortcuts() +{ + // Widget context + setShortcutContext({ + REFRESH_TRIGGERS, + ADD_TRIGGER, + EDIT_TRIGGER, + DEL_TRIGGER, + }, + Qt::WidgetWithChildrenShortcut); + + BIND_SHORTCUTS(ViewWindow, Action); +} + +bool ViewWindow::restoreSessionNextTime() +{ + return existingView; +} + +QToolBar* ViewWindow::getToolBar(int toolbar) const +{ + switch (static_cast<ToolBar>(toolbar)) + { + case TOOLBAR_QUERY: + return ui->queryToolbar; + case TOOLBAR_TRIGGERS: + return ui->triggersToolbar; + } + return nullptr; +} + +void ViewWindow::init() +{ + ui->setupUi(this); + + dataModel = new SqlQueryModel(this); + ui->dataView->init(dataModel); + + ui->queryEdit->setVirtualSqlExpression("CREATE VIEW name AS %1"); + ui->queryEdit->setDb(db); + + connect(dataModel, SIGNAL(executionSuccessful()), this, SLOT(executionSuccessful())); + connect(dataModel, SIGNAL(executionFailed(QString)), this, SLOT(executionFailed(QString))); + connect(ui->tabWidget, SIGNAL(currentChanged(int)), this, SLOT(tabChanged(int))); + connect(ui->nameEdit, SIGNAL(textChanged(QString)), this, SLOT(updateQueryToolbarStatus())); + connect(ui->queryEdit, SIGNAL(textChanged()), this, SLOT(updateQueryToolbarStatus())); + connect(ui->queryEdit, SIGNAL(errorsChecked(bool)), this, SLOT(updateQueryToolbarStatus())); + connect(ui->triggersList, SIGNAL(itemSelectionChanged()), this, SLOT(updateTriggersState())); + + structureExecutor = new ChainExecutor(this); + connect(structureExecutor, SIGNAL(success()), this, SLOT(changesSuccessfullyCommited())); + connect(structureExecutor, SIGNAL(failure(int,QString)), this, SLOT(changesFailedToCommit(int,QString))); + + setupCoverWidget(); + + initActions(); + + refreshTriggers(); + updateQueryToolbarStatus(); + updateTriggersState(); +} + +void ViewWindow::newView() +{ + existingView = false; + view = ""; +} + +void ViewWindow::initView() +{ + ui->nameEdit->setText(view); + + parseDdl(); + + if (!createView) + return; // error occured while parsing ddl, window will be closed + + if (existingView) + { + dataModel->setDb(db); + dataModel->setQuery(originalCreateView->select->detokenize()); + } + + ui->queryEdit->setDb(db); + ui->queryEdit->setPlainText(createView->select->detokenize()); + updateDdlTab(); + + ui->ddlEdit->setSqliteVersion(db->getVersion()); + + refreshTriggers(); + + connect(db, SIGNAL(dbObjectDeleted(QString,QString,DbObjectType)), this, SLOT(checkIfViewDeleted(QString,QString,DbObjectType))); +} + +void ViewWindow::setupCoverWidget() +{ + widgetCover = new WidgetCover(this); + widgetCover->hide(); + connect(widgetCover, SIGNAL(cancelClicked()), structureExecutor, SLOT(interrupt())); +} + +void ViewWindow::createQueryTabActions() +{ + createAction(REFRESH_QUERY, ICONS.RELOAD, tr("Refresh the view", "view window"), this, SLOT(refreshView()), ui->queryToolbar); + ui->queryToolbar->addSeparator(); + createAction(COMMIT_QUERY, ICONS.COMMIT, tr("Commit the view changes", "view window"), this, SLOT(commitView()), ui->queryToolbar); + createAction(ROLLBACK_QUERY, ICONS.ROLLBACK, tr("Rollback the view changes", "view window"), this, SLOT(rollbackView()), ui->queryToolbar); + ui->queryToolbar->addSeparator(); + ui->queryToolbar->addAction(ui->queryEdit->getAction(SqlEditor::FORMAT_SQL)); +} + +void ViewWindow::createTriggersTabActions() +{ + createAction(REFRESH_TRIGGERS, ICONS.RELOAD, tr("Refresh trigger list", "view window"), this, SLOT(refreshTriggers()), ui->triggersToolbar, ui->triggersList); + ui->triggersToolbar->addSeparator(); + createAction(ADD_TRIGGER, ICONS.TRIGGER_ADD, tr("Create new triger", "view window"), this, SLOT(addTrigger()), ui->triggersToolbar, ui->triggersList); + createAction(EDIT_TRIGGER, ICONS.TRIGGER_EDIT, tr("Edit selected triger", "view window"), this, SLOT(editTrigger()), ui->triggersToolbar, ui->triggersList); + createAction(DEL_TRIGGER, ICONS.TRIGGER_DEL, tr("Delete selected triger", "view window"), this, SLOT(deleteTrigger()), ui->triggersToolbar, ui->triggersList); +} +QString ViewWindow::getView() const +{ + return view; +} + +void ViewWindow::setSelect(const QString &selectSql) +{ + ui->queryEdit->setPlainText(selectSql); +} + +bool ViewWindow::isUncommited() const +{ + return ui->dataView->isUncommited() || isModified(); +} + +QString ViewWindow::getQuitUncommitedConfirmMessage() const +{ + QString title = getMdiWindow()->windowTitle(); + if (ui->dataView->isUncommited() && isModified()) + return tr("View window \"%1\" has uncommited structure modifications and data.").arg(title); + else if (ui->dataView->isUncommited()) + return tr("View window \"%1\" has uncommited data.").arg(title); + else if (isModified()) + return tr("View window \"%1\" has uncommited structure modifications.").arg(title); + else + { + qCritical() << "Unhandled message case in ViewWindow::getQuitUncommitedConfirmMessage()."; + return QString(); + } +} + +Db* ViewWindow::getAssociatedDb() const +{ + return db; +} + +void ViewWindow::staticInit() +{ + qRegisterMetaType<ViewWindow>("ViewWindow"); +} + +void ViewWindow::insertAction(ExtActionPrototype* action, ViewWindow::ToolBar toolbar) +{ + return ExtActionContainer::insertAction<ViewWindow>(action, toolbar); +} + +void ViewWindow::insertActionBefore(ExtActionPrototype* action, ViewWindow::Action beforeAction, ViewWindow::ToolBar toolbar) +{ + return ExtActionContainer::insertActionBefore<ViewWindow>(action, beforeAction, toolbar); +} + +void ViewWindow::insertActionAfter(ExtActionPrototype* action, ViewWindow::Action afterAction, ViewWindow::ToolBar toolbar) +{ + return ExtActionContainer::insertActionAfter<ViewWindow>(action, afterAction, toolbar); +} + +void ViewWindow::removeAction(ExtActionPrototype* action, ViewWindow::ToolBar toolbar) +{ + ExtActionContainer::removeAction<ViewWindow>(action, toolbar); +} + +QString ViewWindow::getDatabase() const +{ + return database; +} + +Db* ViewWindow::getDb() const +{ + return db; +} + +void ViewWindow::refreshView() +{ + initView(); + updateTriggersState(); +} + +void ViewWindow::commitView(bool skipWarnings) +{ + if (!isModified()) + { + qWarning() << "Called ViewWindow::commitView(), but isModified() returned false."; + updateQueryToolbarStatus(); + return; + } + + if (!validate(skipWarnings)) + return; + + executeStructureChanges(); +} + +void ViewWindow::rollbackView() +{ + createView = SqliteCreateViewPtr::create(*originalCreateView.data()); + ui->nameEdit->setText(createView->view); + ui->queryEdit->setPlainText(createView->select->detokenize()); + + updateQueryToolbarStatus(); + updateDdlTab(); +} + +QString ViewWindow::getCurrentTrigger() const +{ + int row = ui->triggersList->currentRow(); + QTableWidgetItem* item = ui->triggersList->item(row, 0); + if (!item) + return QString::null; + + return item->text(); +} + +void ViewWindow::applyInitialTab() +{ + if (existingView && !view.isNull() && CFG_UI.General.OpenViewsOnData.get()) + ui->tabWidget->setCurrentIndex(1); + else + ui->tabWidget->setCurrentIndex(0); +} + +void ViewWindow::addTrigger() +{ + DbObjectDialogs dialogs(db, this); + dialogs.addTriggerOnView(view); + refreshTriggers(); +} + +void ViewWindow::editTrigger() +{ + QString trigger = getCurrentTrigger(); + if (trigger.isNull()) + return; + + DbObjectDialogs dialogs(db, this); + dialogs.editTrigger(trigger); + refreshTriggers(); +} + +void ViewWindow::deleteTrigger() +{ + QString trigger = getCurrentTrigger(); + if (trigger.isNull()) + return; + + DbObjectDialogs dialogs(db, this); + dialogs.dropObject(trigger); + refreshTriggers(); +} + +void ViewWindow::executionSuccessful() +{ + modifyingThisView = false; + dataLoaded = true; +} + +void ViewWindow::executionFailed(const QString& errorMessage) +{ + modifyingThisView = false; + notifyError(tr("Could not load data for view %1. Error details: %2").arg(view).arg(errorMessage)); +} + +void ViewWindow::tabChanged(int tabIdx) +{ + switch (tabIdx) + { + case 1: + { + if (isModified()) + { + int res = QMessageBox::question(this, tr("Uncommited changes"), + tr("There are uncommited structure modifications. You cannot browse or edit data until you have " + "the view structure settled.\n" + "Do you want to commit the structure, or do you want to go back to the structure tab?"), + tr("Go back to structure tab"), tr("Commit modifications and browse data.")); + + ui->tabWidget->setCurrentIndex(0); + if (res == 1) + commitView(true); + + break; + } + + if (!dataLoaded) + ui->dataView->refreshData(); + + break; + } + case 3: + { + updateDdlTab(); + break; + } + } +} + +void ViewWindow::updateQueryToolbarStatus() +{ + bool modified = isModified(); + bool queryOk = ui->queryEdit->isSyntaxChecked() && !ui->queryEdit->haveErrors(); + actionMap[COMMIT_QUERY]->setEnabled(modified && queryOk); + actionMap[ROLLBACK_QUERY]->setEnabled(modified && existingView); + actionMap[REFRESH_QUERY]->setEnabled(existingView); +} + +void ViewWindow::changesSuccessfullyCommited() +{ + QStringList sqls = structureExecutor->getQueries(); + CFG->addDdlHistory(sqls.join("\n"), db->getName(), db->getPath()); + + widgetCover->hide(); + + originalCreateView = createView; + dataLoaded = false; + + //QString oldView = view; // uncomment when implementing notify manager call + database = createView->database; + view = createView->view; + existingView = true; + initView(); + updateQueryToolbarStatus(); + updateWindowTitle(); + + DBTREE->refreshSchema(db); +} + +void ViewWindow::changesFailedToCommit(int errorCode, const QString& errorText) +{ + qDebug() << "ViewWindow::changesFailedToCommit:" << errorCode << errorText; + + widgetCover->hide(); + + NotifyManager::getInstance()->error(tr("Could not commit view changes. Error message: %1", "view window").arg(errorText)); +} + +void ViewWindow::updateTriggersState() +{ + bool editDel = ui->triggersList->currentItem() != nullptr; + actionMap[REFRESH_TRIGGERS]->setEnabled(existingView); + actionMap[ADD_TRIGGER]->setEnabled(existingView); + actionMap[EDIT_TRIGGER]->setEnabled(editDel); + actionMap[DEL_TRIGGER]->setEnabled(editDel); +} + +void ViewWindow::nextTab() +{ + int idx = ui->tabWidget->currentIndex(); + idx++; + ui->tabWidget->setCurrentIndex(idx); +} + +void ViewWindow::prevTab() +{ + int idx = ui->tabWidget->currentIndex(); + idx--; + ui->tabWidget->setCurrentIndex(idx); +} + +void ViewWindow::dbClosedFinalCleanup() +{ + dataModel->setDb(nullptr); + ui->queryEdit->setDb(nullptr); + structureExecutor->setDb(nullptr); +} + +void ViewWindow::checkIfViewDeleted(const QString& database, const QString& object, DbObjectType type) +{ + UNUSED(database); + + if (type == DbObjectType::TRIGGER) + { + for (int i = 0, total = ui->triggersList->rowCount(); i < total; ++i) + { + if (ui->triggersList->item(i, 0)->text().compare(object, Qt::CaseInsensitive) == 0) + { + ui->triggersList->removeRow(i); + return; + } + } + } + + if (type != DbObjectType::VIEW) + return; + + if (modifyingThisView) + return; + + // TODO uncomment below when dbnames are supported +// if (this->database != database) +// return; + + if (object.compare(view, Qt::CaseInsensitive) == 0) + { + dbClosedFinalCleanup(); + getMdiWindow()->close(); + } +} + +void ViewWindow::refreshTriggers() +{ + if (!db || !db->isValid()) + return; + + SchemaResolver resolver(db); + QList<SqliteCreateTriggerPtr> triggers = resolver.getParsedTriggersForView(database, view); + + ui->triggersList->setColumnCount(4); + ui->triggersList->setRowCount(triggers.size()); + ui->triggersList->horizontalHeader()->setMaximumSectionSize(200); + ui->triggersList->setHorizontalHeaderLabels({ + tr("Name", "view window triggers"), + tr("Instead of", "view window triggers"), + tr("Condition", "view window triggers"), + tr("Details", "table window triggers") + }); + + QTableWidgetItem* item = nullptr; + QString event; + int row = 0; + foreach (SqliteCreateTriggerPtr trig, triggers) + { + item = new QTableWidgetItem(trig->trigger); + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + ui->triggersList->setItem(row, 0, item); + + event = trig->tokensMap["trigger_event"].detokenize(); + item = new QTableWidgetItem(event); + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + ui->triggersList->setItem(row, 1, item); + + item = new QTableWidgetItem(trig->precondition ? trig->precondition->detokenize().trimmed() : ""); + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + ui->triggersList->setItem(row, 2, item); + + item = new QTableWidgetItem(trig->tokensMap["trigger_cmd_list"].detokenize().trimmed()); + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + ui->triggersList->setItem(row, 3, item); + + row++; + } + + ui->triggersList->resizeColumnsToContents(); + updateTriggersState(); +} + +void ViewWindow::parseDdl() +{ + if (existingView) + { + SchemaResolver resolver(db); + SqliteQueryPtr parsedObject = resolver.getParsedObject(database, view, SchemaResolver::VIEW); + if (!parsedObject.dynamicCast<SqliteCreateView>()) + { + notifyError(tr("Could not process the %1 view correctly. Unable to open a view window.").arg(view)); + invalid = true; + return; + } + + createView = parsedObject.dynamicCast<SqliteCreateView>(); + } + else + { + createView = SqliteCreateViewPtr::create(); + createView->view = view; + createView->dialect = db->getDialect(); + } + originalCreateView = SqliteCreateViewPtr::create(*createView); + originalQuery = originalCreateView->select->detokenize(); +} + +void ViewWindow::updateDdlTab() +{ + QString ddl = "CREATE VIEW %1 AS %2"; + ui->ddlEdit->setPlainText(ddl.arg(wrapObjIfNeeded(ui->nameEdit->text(), db->getDialect())).arg(ui->queryEdit->toPlainText())); +} + +bool ViewWindow::isModified() const +{ + return (originalCreateView && originalCreateView->view != ui->nameEdit->text()) || + ui->queryEdit->toPlainText() != originalQuery || + !existingView; +} + +bool ViewWindow::validate(bool skipWarnings) +{ + if (!existingView && !skipWarnings && ui->nameEdit->text().isEmpty()) + { + int res = QMessageBox::warning(this, tr("Empty name"), tr("A blank name for the view is allowed in SQLite, but it is not recommended.\n" + "Are you sure you want to create a view with blank name?"), QMessageBox::Yes, QMessageBox::No); + + if (res != QMessageBox::Yes) + return false; + } + + // Rebuilding createView statement and validating it on the fly. + QString ddl = "CREATE VIEW %1 AS %2"; + QString viewName = wrapObjIfNeeded(ui->nameEdit->text(), db->getDialect()); + QString select = ui->queryEdit->toPlainText(); + + Parser parser(db->getDialect()); + if (!parser.parse(ddl.arg(viewName).arg(select)) || parser.getQueries().size() < 1) + { + notifyError(tr("The SELECT statement could not be parsed. Please correct the query and retry.\nDetails: %1").arg(parser.getErrorString())); + return false; + } + + SqliteQueryPtr query = parser.getQueries().first(); + SqliteCreateViewPtr viewStmt = query.dynamicCast<SqliteCreateView>(); + if (!viewStmt) + { + notifyError(tr("The view could not be modified due to internal SQLiteStudio error. Please report this!")); + qCritical() << "Could not parse new view, because parsed object is of different type. The type is" + << sqliteQueryTypeToString(query->queryType) << "for following query:" << ddl; + return false; + } + + createView = viewStmt; + return true; +} + +void ViewWindow::executeStructureChanges() +{ + QStringList sqls; + QList<bool> sqlMandatoryFlags; + + createView->rebuildTokens(); + if (!existingView) + { + sqls << createView->detokenize(); + } + else + { + if (viewModifier) + delete viewModifier; + + viewModifier = new ViewModifier(db, database, view); + viewModifier->alterView(createView); + + if (viewModifier->hasMessages()) + { + MessageListDialog dialog(tr("Following problems will take place while modifying the view.\n" + "Would you like to proceed?", "view window")); + dialog.setWindowTitle(tr("View modification", "view window")); + foreach (const QString& error, viewModifier->getErrors()) + dialog.addError(error); + + foreach (const QString& warn, viewModifier->getWarnings()) + dialog.addWarning(warn); + + if (dialog.exec() != QDialog::Accepted) + return; + } + + sqls = viewModifier->generateSqls(); + sqlMandatoryFlags = viewModifier->getMandatoryFlags(); + } + + if (!CFG_UI.General.DontShowDdlPreview.get()) + { + DdlPreviewDialog dialog(db, this); + dialog.setDdl(sqls); + if (dialog.exec() != QDialog::Accepted) + return; + } + + modifyingThisView = true; + structureExecutor->setDb(db); + structureExecutor->setQueries(sqls); + structureExecutor->setMandatoryQueries(sqlMandatoryFlags); + structureExecutor->exec(); + widgetCover->show(); +} diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/viewwindow.h b/SQLiteStudio3/guiSQLiteStudio/windows/viewwindow.h new file mode 100644 index 0000000..6b50135 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/viewwindow.h @@ -0,0 +1,146 @@ +#ifndef VIEWWINDOW_H +#define VIEWWINDOW_H + +#include "mdichild.h" +#include "common/extactioncontainer.h" +#include "db/db.h" +#include "parser/ast/sqlitecreateview.h" +#include "guiSQLiteStudio_global.h" +#include <QWidget> + +namespace Ui { + class ViewWindow; +} + +class SqliteSyntaxHighlighter; +class SqlQueryModel; +class WidgetCover; +class QPushButton; +class QProgressBar; +class ChainExecutor; +class ViewModifier; + +CFG_KEY_LIST(ViewWindow, QObject::tr("A view window"), + CFG_KEY_ENTRY(REFRESH_TRIGGERS, Qt::Key_F5, QObject::tr("Refresh view trigger list")) + CFG_KEY_ENTRY(ADD_TRIGGER, Qt::Key_Insert, QObject::tr("Add new trigger")) + CFG_KEY_ENTRY(EDIT_TRIGGER, Qt::Key_Return, QObject::tr("Edit selected trigger")) + CFG_KEY_ENTRY(DEL_TRIGGER, Qt::Key_Delete, QObject::tr("Delete selected trigger")) + CFG_KEY_ENTRY(NEXT_TAB, Qt::ALT + Qt::Key_Right, QObject::tr("Go to next tab")) + CFG_KEY_ENTRY(PREV_TAB, Qt::ALT + Qt::Key_Left, QObject::tr("Go to previous tab")) +) + +class GUI_API_EXPORT ViewWindow : public MdiChild +{ + Q_OBJECT + Q_ENUMS(Action) + + public: + enum Action + { + // Structure tab + REFRESH_QUERY, + COMMIT_QUERY, + ROLLBACK_QUERY, + // Triggers tab + REFRESH_TRIGGERS, + ADD_TRIGGER, + EDIT_TRIGGER, + DEL_TRIGGER, + // All tabs + NEXT_TAB, + PREV_TAB + }; + + enum ToolBar + { + TOOLBAR_QUERY, + TOOLBAR_TRIGGERS + }; + + explicit ViewWindow(QWidget *parent = 0); + ViewWindow(Db* db, QWidget *parent = 0); + ViewWindow(const ViewWindow& win); + ViewWindow(QWidget *parent, Db* db, const QString& database, const QString& view); + ~ViewWindow(); + + Db* getDb() const; + QString getDatabase() const; + QString getView() const; + void setSelect(const QString& selectSql); + bool isUncommited() const; + QString getQuitUncommitedConfirmMessage() const; + Db* getAssociatedDb() const; + + static void staticInit(); + static void insertAction(ExtActionPrototype* action, ToolBar toolbar = TOOLBAR_QUERY); + static void insertActionBefore(ExtActionPrototype* action, Action beforeAction, ToolBar toolbar = TOOLBAR_QUERY); + static void insertActionAfter(ExtActionPrototype* action, Action afterAction, ToolBar toolbar = TOOLBAR_QUERY); + static void removeAction(ExtActionPrototype* action, ToolBar toolbar = TOOLBAR_QUERY); + + protected: + void changeEvent(QEvent *e); + QVariant saveSession(); + bool restoreSession(const QVariant& sessionValue); + Icon* getIconNameForMdiWindow(); + QString getTitleForMdiWindow(); + void createActions(); + void setupDefShortcuts(); + bool restoreSessionNextTime(); + QToolBar* getToolBar(int toolbar) const; + + private: + void init(); + void newView(); + void initView(); + void setupCoverWidget(); + void createQueryTabActions(); + void createTriggersTabActions(); + void parseDdl(); + void updateDdlTab(); + bool isModified() const; + bool validate(bool skipWarnings = false); + void executeStructureChanges(); + QString getCurrentTrigger() const; + void applyInitialTab(); + + Db* db = nullptr; + QString database; + QString view; + bool existingView = true; + bool dataLoaded = false; + int newViewWindowNum = 1; + bool modified = false; + SqliteCreateViewPtr originalCreateView; + SqliteCreateViewPtr createView; + SqlQueryModel* dataModel = nullptr; + QString originalQuery; + WidgetCover* widgetCover = nullptr; + ChainExecutor* structureExecutor = nullptr; + ViewModifier* viewModifier = nullptr; + Ui::ViewWindow *ui = nullptr; + bool modifyingThisView = false; + + private slots: + void refreshView(); + void commitView(bool skipWarnings = false); + void rollbackView(); + void addTrigger(); + void editTrigger(); + void deleteTrigger(); + void executionSuccessful(); + void executionFailed(const QString& errorMessage); + void tabChanged(int tabIdx); + void updateQueryToolbarStatus(); + void changesSuccessfullyCommited(); + void changesFailedToCommit(int errorCode, const QString& errorText); + void updateTriggersState(); + void nextTab(); + void prevTab(); + void dbClosedFinalCleanup(); + void checkIfViewDeleted(const QString& database, const QString& object, DbObjectType type); + + public slots: + void refreshTriggers(); +}; + +#endif // VIEWWINDOW_H diff --git a/SQLiteStudio3/guiSQLiteStudio/windows/viewwindow.ui b/SQLiteStudio3/guiSQLiteStudio/windows/viewwindow.ui new file mode 100644 index 0000000..734a265 --- /dev/null +++ b/SQLiteStudio3/guiSQLiteStudio/windows/viewwindow.ui @@ -0,0 +1,137 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ViewWindow</class> + <widget class="QWidget" name="ViewWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>621</width> + <height>468</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="margin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="queryTab"> + <attribute name="title"> + <string>Query</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QToolBar" name="queryToolbar"/> + </item> + <item> + <widget class="QWidget" name="nameWidget" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="margin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string>View name:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="nameEdit"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="SqlEditor" name="queryEdit"/> + </item> + </layout> + </widget> + <widget class="QWidget" name="dataTab"> + <attribute name="title"> + <string>Data</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="DataView" name="dataView"/> + </item> + </layout> + </widget> + <widget class="QWidget" name="triggersTab"> + <attribute name="title"> + <string>Triggers</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QToolBar" name="triggersToolbar"/> + </item> + <item> + <widget class="QTableWidget" name="triggersList"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="horizontalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="ddl"> + <attribute name="title"> + <string>DDL</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="SqlView" name="ddlEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>DataView</class> + <extends>QTabWidget</extends> + <header>dataview.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>SqlEditor</class> + <extends>QPlainTextEdit</extends> + <header>sqleditor.h</header> + </customwidget> + <customwidget> + <class>SqlView</class> + <extends>QPlainTextEdit</extends> + <header>sqlview.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> |
