mirror of
https://github.com/sqlitebrowser/sqlitebrowser.git
synced 2026-01-20 02:50:46 -06:00
add basic autocompletion for tables and fields to the sql text input
this does only work on full table names, NOT on aliases for a full auto completion to work we need a sqlite parser
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
#include <QStandardItemModel>
|
||||
#include <QDragEnterEvent>
|
||||
#include <QScrollBar>
|
||||
|
||||
#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<QString, QTreeWidgetItem*> typeToParentItem;
|
||||
QTreeWidgetItem* itemTables = new QTreeWidgetItem(ui->dbTreeWidget);
|
||||
itemTables->setIcon(0, QIcon(QString(":/icons/table")));
|
||||
|
||||
@@ -775,7 +775,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="sqlTextEdit">
|
||||
<widget class="SqlTextEdit" name="sqlTextEdit">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Monospace</family>
|
||||
@@ -1393,6 +1393,13 @@
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>SqlTextEdit</class>
|
||||
<extends>QTextEdit</extends>
|
||||
<header>sqltextedit.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>dbTreeWidget</tabstop>
|
||||
<tabstop>comboBrowseTable</tabstop>
|
||||
|
||||
230
src/sqltextedit.cpp
Normal file
230
src/sqltextedit.cpp
Normal file
@@ -0,0 +1,230 @@
|
||||
#include "sqltextedit.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QAbstractItemView>
|
||||
#include <QCompleter>
|
||||
#include <QScrollBar>
|
||||
//#include <QDebug>
|
||||
|
||||
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!
|
||||
}
|
||||
47
src/sqltextedit.h
Normal file
47
src/sqltextedit.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#ifndef SQLTEXTEDIT_H
|
||||
#define SQLTEXTEDIT_H
|
||||
|
||||
#include <QTextEdit>
|
||||
|
||||
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<QString,QAbstractItemModel*> 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
|
||||
@@ -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`\\\"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user