diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 2b05ce72..a382905d 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -11,6 +11,7 @@ #include #include #include + #include "CreateIndexDialog.h" #include "AboutDialog.h" #include "EditTableDialog.h" @@ -21,6 +22,7 @@ #include "EditDialog.h" #include "FindDialog.h" #include "SQLiteSyntaxHighlighter.h" +#include "sqltextedit.h" MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), @@ -168,6 +170,8 @@ void MainWindow::fileNew() void MainWindow::populateStructure() { ui->dbTreeWidget->model()->removeRows(0, ui->dbTreeWidget->model()->rowCount()); + ui->sqlTextEdit->clearFieldCompleterModelMap(); + ui->sqlTextEdit->setDefaultCompleterModel(new QStandardItemModel()); if (!db.isOpen()){ return; } @@ -177,6 +181,43 @@ void MainWindow::populateStructure() sqliteHighlighterLogUser->setTableNames(tblnames); sqliteHighlighterLogApp->setTableNames(tblnames); + // setup models for sqltextedit autocomplete + QStandardItemModel* completerModel = new QStandardItemModel(); + completerModel->setRowCount(tblnames.count()); + completerModel->setColumnCount(1); + + objectMap tab = db.getBrowsableObjects(); + int row = 0; + for(objectMap::ConstIterator it=tab.begin(); it!=tab.end(); ++it, ++row) + { + QString sName = it.value().getname(); + QStandardItem* item = new QStandardItem(sName); + item->setIcon(QIcon(QString(":icons/%1").arg(it.value().gettype()))); + completerModel->setItem(row, 0, item); + + // If it is a table add the field Nodes + if((*it).gettype() == "table" || (*it).gettype() == "view") + { + QStandardItemModel* tablefieldmodel = new QStandardItemModel(); + tablefieldmodel->setRowCount((*it).fldmap.count()); + tablefieldmodel->setColumnCount(1); + + fieldMap::ConstIterator fit; + int fldrow = 0; + for ( fit = (*it).fldmap.begin(); fit != (*it).fldmap.end(); ++fit, ++fldrow ) { + QString fieldname = fit.value().getname(); + QStandardItem* fldItem = new QStandardItem(fieldname); + fldItem->setIcon(QIcon(":/icons/field")); + tablefieldmodel->setItem(fldrow, 0, fldItem); + } + ui->sqlTextEdit->addFieldCompleterModel(sName.toLower(), tablefieldmodel); + } + + } + ui->sqlTextEdit->setDefaultCompleterModel(completerModel); + // end setup models for sqltextedit autocomplete + + // fill the structure tab QMap typeToParentItem; QTreeWidgetItem* itemTables = new QTreeWidgetItem(ui->dbTreeWidget); itemTables->setIcon(0, QIcon(QString(":/icons/table"))); diff --git a/src/MainWindow.ui b/src/MainWindow.ui index 70aa21cd..9f637c6a 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -775,7 +775,7 @@ - + Monospace @@ -1393,6 +1393,13 @@ + + + SqlTextEdit + QTextEdit +
sqltextedit.h
+
+
dbTreeWidget comboBrowseTable diff --git a/src/sqltextedit.cpp b/src/sqltextedit.cpp new file mode 100644 index 00000000..a1c452f4 --- /dev/null +++ b/src/sqltextedit.cpp @@ -0,0 +1,230 @@ +#include "sqltextedit.h" + +#include +#include +#include +#include +//#include + +SqlTextEdit::SqlTextEdit(QWidget* parent) : + QTextEdit(parent), m_Completer(0), m_defaultCompleterModel(0) +{ + // basic auto completer for sqliteedit + m_Completer = new QCompleter(this); + m_Completer->setCaseSensitivity(Qt::CaseInsensitive); + m_Completer->setCompletionMode(QCompleter::PopupCompletion); + m_Completer->setWrapAround(false); + m_Completer->setWidget(this); + + QObject::connect(m_Completer, SIGNAL(activated(QString)), + this, SLOT(insertCompletion(QString))); +} + +SqlTextEdit::~SqlTextEdit() +{ + clearFieldCompleterModelMap(); + delete m_defaultCompleterModel; +} + +void SqlTextEdit::setCompleter(QCompleter *completer) +{ + if (m_Completer) + QObject::disconnect(m_Completer, 0, this, 0); + + m_Completer = completer; + + if (!m_Completer) + return; + + m_Completer->setWidget(this); + m_Completer->setCompletionMode(QCompleter::PopupCompletion); + m_Completer->setCaseSensitivity(Qt::CaseInsensitive); + QObject::connect(m_Completer, SIGNAL(activated(QString)), + this, SLOT(insertCompletion(QString))); +} + +QCompleter* SqlTextEdit::completer() const +{ + return m_Completer; +} + +void SqlTextEdit::setDefaultCompleterModel(QAbstractItemModel *model) +{ + delete m_defaultCompleterModel; + m_defaultCompleterModel = model; + m_Completer->setModel(m_defaultCompleterModel); +} + +void SqlTextEdit::clearFieldCompleterModelMap() +{ + QAbstractItemModel* model; + foreach (model, m_fieldCompleterMap) + { + delete model; + } + m_fieldCompleterMap.clear(); +} + +QAbstractItemModel* SqlTextEdit::addFieldCompleterModel(const QString &tablename, QAbstractItemModel* model) +{ + m_fieldCompleterMap[tablename] = model; + return model; +} + +void SqlTextEdit::insertCompletion(const QString& completion) +{ + if (m_Completer->widget() != this) + return; + QTextCursor tc = textCursor(); + int extra = completion.length() - m_Completer->completionPrefix().length(); + tc.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor); + + // slight workaround for a field completion without any completionPrefix + // eg. "tablename.;" if you would select a field completion and hit enter + // without this workaround the text would be inserted after the ';' + // because endofword moves to the end of the line + if(tc.selectedText() == ".") + tc.movePosition(QTextCursor::Right); + else + tc.movePosition(QTextCursor::EndOfWord); + + tc.insertText(completion.right(extra)); + setTextCursor(tc); +} + +namespace { +bool isSqliteIdentifierChar(QChar c) { + return c.isLetterOrNumber() || c == '.' || c == '_'; +} +} + +/** + * @brief SqlTextEdit::identifierUnderCursor + * @return The partial or full sqlite identifier (table(.field)?)? under the cursor + * or a empty string. + */ +QString SqlTextEdit::identifierUnderCursor() const +{ + QTextCursor tc = textCursor(); + const int abspos = tc.position() - 1; + tc.movePosition(QTextCursor::StartOfLine); + const int linestartpos = tc.position(); + const int linepos = abspos - linestartpos; + tc.select(QTextCursor::LineUnderCursor); + QString line = tc.selectedText(); + int start = 0, end; + + // look where the identifier starts + for( int i = linepos; i >= 0 && i < line.length() && start == 0; --i) + { + if( !(isSqliteIdentifierChar(line.at(i)))) + start = i + 1; + } + + end = line.length(); + // see where the word ends + for( int i = start; i < line.length() && i >= 0 && end == line.length(); ++i) + { + if( !(isSqliteIdentifierChar(line.at(i)))) + end = i; + } + + // extract the identifier table.field + QString identifier = line.mid(start, end - start); + // check if it has a dot in it + int dotpos = identifier.indexOf('.'); + + // this is a little hack so editing a table name won't show fields + // fields are only shown if type the word at the end + if( dotpos > -1 && linepos + 1 != end ) + return identifier.left(dotpos); + else + return identifier; +} + +void SqlTextEdit::focusInEvent(QFocusEvent *e) +{ + if (m_Completer) + m_Completer->setWidget(this); + QTextEdit::focusInEvent(e); +} + +void SqlTextEdit::keyPressEvent(QKeyEvent *e) +{ + if (m_Completer && m_Completer->popup()->isVisible()) { + // The following keys are forwarded by the completer to the widget + switch (e->key()) { + case Qt::Key_Enter: + case Qt::Key_Return: + case Qt::Key_Escape: + case Qt::Key_Tab: + case Qt::Key_Backtab: + e->ignore(); + return; // let the completer do default behavior + default: + break; + } + } + + bool isShortcut = ((e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_Space); // CTRL+SPACE + if (!m_Completer || !isShortcut) // do not process the shortcut when we have a completer + QTextEdit::keyPressEvent(e); + const bool ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier); + const bool cursorKey = e->key() == Qt::Key_Left || + e->key() == Qt::Key_Up || + e->key() == Qt::Key_Right || + e->key() == Qt::Key_Down; + if (!m_Completer || (ctrlOrShift && e->text().isEmpty()) || cursorKey) + return; + + QString identifier = identifierUnderCursor(); + QString table = identifier; + QString field; + int dotpos = 0; + if((dotpos = identifier.indexOf('.')) > 0) + { + table = identifier.left(dotpos); + field = identifier.mid(dotpos + 1); + } +// qDebug() << identifier << ":" << table << ":" << field; + if( dotpos > 0 ) + { + // swap model to field completion + FieldCompleterModelMap::ConstIterator it = m_fieldCompleterMap.find(table.toLower()); + if( it != m_fieldCompleterMap.end() ) + { + if( *it != m_Completer->model() ) + m_Completer->setModel(*it); + if (field != m_Completer->completionPrefix()) { + m_Completer->setCompletionPrefix(field); + m_Completer->popup()->setCurrentIndex(m_Completer->completionModel()->index(0, 0)); + } + QRect cr = cursorRect(); + cr.setWidth(m_Completer->popup()->sizeHintForColumn(0) + + m_Completer->popup()->verticalScrollBar()->sizeHint().width()); + m_Completer->complete(cr); + } + return; + } + + // table completion mode + if( m_Completer->model() != m_defaultCompleterModel ) + m_Completer->setModel(m_defaultCompleterModel); + static QString eow("~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-="); // end of word + bool hasModifier = (e->modifiers() != Qt::NoModifier) && !ctrlOrShift; + + if (!isShortcut && (hasModifier || e->text().isEmpty()|| identifier.length() < 3 + || eow.contains(e->text().right(1)))) { + m_Completer->popup()->hide(); + return; + } + + if (identifier != m_Completer->completionPrefix()) { + m_Completer->setCompletionPrefix(identifier); + m_Completer->popup()->setCurrentIndex(m_Completer->completionModel()->index(0, 0)); + } + QRect cr = cursorRect(); + cr.setWidth(m_Completer->popup()->sizeHintForColumn(0) + + m_Completer->popup()->verticalScrollBar()->sizeHint().width()); + m_Completer->complete(cr); // popup it up! +} diff --git a/src/sqltextedit.h b/src/sqltextedit.h new file mode 100644 index 00000000..0a02a1ee --- /dev/null +++ b/src/sqltextedit.h @@ -0,0 +1,47 @@ +#ifndef SQLTEXTEDIT_H +#define SQLTEXTEDIT_H + +#include + +class QCompleter; +class QAbstractItemModel; + +/** + * @brief The SqlTextEdit class + * With basic table and fieldname auto completion. + * This class is based on the Qt custom completion example. + */ +class SqlTextEdit : public QTextEdit +{ + Q_OBJECT +public: + explicit SqlTextEdit(QWidget *parent = 0); + virtual ~SqlTextEdit(); + + void setCompleter(QCompleter* completer); + QCompleter* completer() const; + void setDefaultCompleterModel(QAbstractItemModel* model); + + // map that associates table -> field model + typedef QMap FieldCompleterModelMap; + + void clearFieldCompleterModelMap(); + QAbstractItemModel* addFieldCompleterModel(const QString& tablename, QAbstractItemModel *model); + +protected: + void keyPressEvent(QKeyEvent *e); + void focusInEvent(QFocusEvent *e); + +private: + QString identifierUnderCursor() const; + +private slots: + void insertCompletion(const QString& completion); + +private: + QCompleter* m_Completer; + QAbstractItemModel* m_defaultCompleterModel; + FieldCompleterModelMap m_fieldCompleterMap; +}; + +#endif // SQLTEXTEDIT_H diff --git a/src/src.pro b/src/src.pro index 79ee3a9b..186ca471 100644 --- a/src/src.pro +++ b/src/src.pro @@ -23,7 +23,8 @@ HEADERS += \ FindDialog.h \ EditDialog.h \ ExportCsvDialog.h \ - ImportCsvDialog.h + ImportCsvDialog.h \ + sqltextedit.h SOURCES += \ sqlitedb.cpp \ @@ -39,7 +40,8 @@ SOURCES += \ FindDialog.cpp \ EditDialog.cpp \ ExportCsvDialog.cpp \ - ImportCsvDialog.cpp + ImportCsvDialog.cpp \ + sqltextedit.cpp QMAKE_CXXFLAGS += -DAPP_VERSION=\\\"`cd $$PWD;git log -n1 --format=%h_git`\\\"