From 9185b34922208527cbb94642633483d43ed775f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Inostroza?= Date: Sat, 16 Dec 2017 13:47:37 -0300 Subject: [PATCH 01/46] Synchronize PlotDock with 'Execute SQL' table. --- src/MainWindow.cpp | 46 +++++++++++++++++++++++++++++++++++++++------- src/MainWindow.h | 3 ++- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 43cb0ced..8664453f 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -694,6 +694,34 @@ void MainWindow::deleteRecord() } } +void MainWindow::selectCurrentTabTableLines(int firstLine, int lastLine) +{ + if(lastLine >= m_currentTabTableModel->totalRowCount()) + return; + + SqlExecutionArea *qw = (SqlExecutionArea*)ui->tabSqlAreas->currentWidget(); + ExtendedTableWidget *tw = qw->getTableResult(); + + if(firstLine >= m_currentTabTableModel->totalRowCount()) + return; + + QApplication::setOverrideCursor( Qt::WaitCursor ); + // Make sure this line has already been fetched + while(firstLine >= m_currentTabTableModel->rowCount() && m_currentTabTableModel->canFetchMore()) + m_currentTabTableModel->fetchMore(); + + // Select it + tw->clearSelection(); + tw->selectRow(firstLine); + tw->scrollTo(tw->currentIndex(), QAbstractItemView::PositionAtTop); + QApplication::restoreOverrideCursor(); + + QModelIndex topLeft = m_currentTabTableModel->index(firstLine, 0); + QModelIndex bottomRight = m_currentTabTableModel->index(lastLine, m_currentTabTableModel->columnCount()-1); + + tw->selectionModel()->select(QItemSelection(topLeft, bottomRight), QItemSelectionModel::Select | QItemSelectionModel::Rows); +} + void MainWindow::selectTableLine(int lineToSelect) { // Are there even that many lines? @@ -715,16 +743,20 @@ void MainWindow::selectTableLine(int lineToSelect) void MainWindow::selectTableLines(int firstLine, int count) { int lastLine = firstLine+count-1; - // Are there even that many lines? - if(lastLine >= m_browseTableModel->totalRowCount()) - return; + if(ui->mainTab->currentIndex() == 1) { + // Are there even that many lines? + if(lastLine >= m_browseTableModel->totalRowCount()) + return; - selectTableLine(firstLine); + selectTableLine(firstLine); - QModelIndex topLeft = ui->dataTable->model()->index(firstLine, 0); - QModelIndex bottomRight = ui->dataTable->model()->index(lastLine, ui->dataTable->model()->columnCount()-1); + QModelIndex topLeft = ui->dataTable->model()->index(firstLine, 0); + QModelIndex bottomRight = ui->dataTable->model()->index(lastLine, ui->dataTable->model()->columnCount()-1); - ui->dataTable->selectionModel()->select(QItemSelection(topLeft, bottomRight), QItemSelectionModel::Select | QItemSelectionModel::Rows); + ui->dataTable->selectionModel()->select(QItemSelection(topLeft, bottomRight), QItemSelectionModel::Select | QItemSelectionModel::Rows); + }else if(ui->mainTab->currentIndex() == 3) { + selectCurrentTabTableLines(firstLine, lastLine); + } } void MainWindow::navigatePrevious() diff --git a/src/MainWindow.h b/src/MainWindow.h index f3cf28ea..a41c9480 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -222,7 +222,8 @@ private slots: bool fileClose(); void addRecord(); void deleteRecord(); - void selectTableLine( int lineToSelect ); + void selectTableLine(int lineToSelect); + void selectCurrentTabTableLines(int firstLine, int lastLine); void selectTableLines(int firstLine, int count); void navigatePrevious(); void navigateNext(); From 25381a57d9e7168c5927687ca0bd7ac8ce1b3662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Inostroza?= Date: Mon, 18 Dec 2017 17:02:27 -0300 Subject: [PATCH 02/46] Use Tabs enum for tab identification. --- src/MainWindow.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 8664453f..8b79dd1e 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -743,7 +743,7 @@ void MainWindow::selectTableLine(int lineToSelect) void MainWindow::selectTableLines(int firstLine, int count) { int lastLine = firstLine+count-1; - if(ui->mainTab->currentIndex() == 1) { + if(ui->mainTab->currentIndex() == BrowseTab) { // Are there even that many lines? if(lastLine >= m_browseTableModel->totalRowCount()) return; @@ -754,7 +754,7 @@ void MainWindow::selectTableLines(int firstLine, int count) QModelIndex bottomRight = ui->dataTable->model()->index(lastLine, ui->dataTable->model()->columnCount()-1); ui->dataTable->selectionModel()->select(QItemSelection(topLeft, bottomRight), QItemSelectionModel::Select | QItemSelectionModel::Rows); - }else if(ui->mainTab->currentIndex() == 3) { + }else if(ui->mainTab->currentIndex() == ExecuteTab) { selectCurrentTabTableLines(firstLine, lastLine); } } From e7a810913f58d2aa6ba9c5acf2dde2f7e4539d57 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Fri, 22 Dec 2017 19:15:41 +0100 Subject: [PATCH 03/46] Don't copy binary data to text type clipboard Don't copy images or other binary data to the text MIME type of the system clipboard. They are still copied into the internal buffer and into the HTML MIME type though. We don't copy them into the text-only clipboard because that doesn't really work well. Neither the original binary data nor a "BLOB" string nor the BASE64 encoded data seem to be a safe bet, so we just omit these cells. --- src/ExtendedTableWidget.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ExtendedTableWidget.cpp b/src/ExtendedTableWidget.cpp index 8cdbb8cb..c5534dcc 100644 --- a/src/ExtendedTableWidget.cpp +++ b/src/ExtendedTableWidget.cpp @@ -187,8 +187,7 @@ void ExtendedTableWidget::copy(const bool withHeaders) while (i.hasNext()) { if (isColumnHidden(i.next().column())) i.remove(); - } - + } // Abort if there's nothing to copy if (indices.isEmpty()) @@ -312,13 +311,11 @@ void ExtendedTableWidget::copy(const bool withHeaders) QString imageBase64 = ba.toBase64(); htmlResult.append("\"Image\""); } else { QByteArray text; - if (m->isBinary(index)) - text = data.toByteArray().toBase64(); // TODO: Or should be just "BLOB"? - else + if (!m->isBinary(index)) text = data.toByteArray(); // Table cell data: text From a98cd8e8f93794545b483e8f47b9ffc8a81991ca Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Sun, 31 Dec 2017 12:47:44 +0100 Subject: [PATCH 04/46] Avoid accidental truncation of cell data For table cells with text larger than 32768 characters, the data gets truncated. If the user doesn't notice this somehow and hits the Escape key the truncated data is written back to the database resulting in a possible data loss. This problem is fixed by this commit in two ways: 1) The maximum length of the editor widgets is increased to the maximum value making this problem much more unlikely. 2) If the user is still hitting this value, i.e. the text is truncated anyway, the widget is put into read only mode and no data is written back to the database. See issue #1281. --- src/ExtendedTableWidget.cpp | 46 +++++++++++++++++++++++++++++++++++++ src/ExtendedTableWidget.h | 16 +++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/ExtendedTableWidget.cpp b/src/ExtendedTableWidget.cpp index c5534dcc..9587d496 100644 --- a/src/ExtendedTableWidget.cpp +++ b/src/ExtendedTableWidget.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include QList ExtendedTableWidget::m_buffer; QString ExtendedTableWidget::m_generatorStamp; @@ -89,6 +91,46 @@ QList parseClipboard(QString clipboard) } + +ExtendedTableWidgetEditorDelegate::ExtendedTableWidgetEditorDelegate(QObject* parent) + : QStyledItemDelegate(parent) +{ +} + +QWidget* ExtendedTableWidgetEditorDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& /*option*/, const QModelIndex& /*index*/) const +{ + // Just create a normal line editor but set the maximum length to the highest possible value instead of the default 32768. + QLineEdit* editor = new QLineEdit(parent); + editor->setMaxLength(std::numeric_limits::max()); + return editor; +} + +void ExtendedTableWidgetEditorDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const +{ + QLineEdit* lineedit = static_cast(editor); + + // Set the data for the line editor + QString data = index.data(Qt::EditRole).toString(); + lineedit->setText(data); + + // Put the editor in read only mode if the actual data is larger than the maximum length to avoid accidental truncation of the data + lineedit->setReadOnly(data.size() > lineedit->maxLength()); +} + +void ExtendedTableWidgetEditorDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const +{ + // Only apply the data back to the model if the editor is not in read only mode to avoid accidental truncation of the data + QLineEdit* lineedit = static_cast(editor); + if(!lineedit->isReadOnly()) + model->setData(index, lineedit->text()); +} + +void ExtendedTableWidgetEditorDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& /*index*/) const +{ + editor->setGeometry(option.rect); +} + + ExtendedTableWidget::ExtendedTableWidget(QWidget* parent) : QTableView(parent) { @@ -122,6 +164,10 @@ ExtendedTableWidget::ExtendedTableWidget(QWidget* parent) : m_contextMenu->addAction(pasteAction); setContextMenuPolicy(Qt::CustomContextMenu); + // Create and set up delegate + m_editorDelegate = new ExtendedTableWidgetEditorDelegate(this); + setItemDelegate(m_editorDelegate); + // This is only for displaying the shortcut in the context menu. // An entry in keyPressEvent is still needed. nullAction->setShortcut(QKeySequence(tr("Alt+Del"))); diff --git a/src/ExtendedTableWidget.h b/src/ExtendedTableWidget.h index ae86ac4f..24256c77 100644 --- a/src/ExtendedTableWidget.h +++ b/src/ExtendedTableWidget.h @@ -5,11 +5,26 @@ #include #include #include +#include class QMenu; class FilterTableHeader; namespace sqlb { class ObjectIdentifier; } +// We use this class to provide editor widgets for the ExtendedTableWidget. It's used for every cell in the table view. +class ExtendedTableWidgetEditorDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + explicit ExtendedTableWidgetEditorDelegate(QObject* parent = nullptr); + + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + void setEditorData(QWidget* editor, const QModelIndex& index) const override; + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; + void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const override; +}; + class ExtendedTableWidget : public QTableView { Q_OBJECT @@ -55,6 +70,7 @@ protected: FilterTableHeader* m_tableHeader; QMenu* m_contextMenu; + ExtendedTableWidgetEditorDelegate* m_editorDelegate; }; #endif From 7ce7f0c05b531e878892f640a4a0459a2a196a8c Mon Sep 17 00:00:00 2001 From: mgrojo Date: Sun, 31 Dec 2017 12:57:48 +0100 Subject: [PATCH 05/46] Use current encoding for the binary check and being able to reset encoding The current table encoding is used in the binary check, so text encoded in 8-bit encodings is properly recognised as text in the table widget. Make it possible to reset the encoding used in the way suggested to the user: "Leave the field empty for using the database encoding". Currently it was rejecting it with the message: "This encoding is either not valid or not supported." See issue #1279 --- src/MainWindow.cpp | 2 +- src/sqlitetablemodel.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 43cb0ced..d4df0d72 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -2542,7 +2542,7 @@ void MainWindow::browseDataSetTableEncoding(bool forAllTables) if(ok) { // Check if encoding is valid - if(!QTextCodec::codecForName(encoding.toUtf8())) + if(!encoding.isEmpty() && !QTextCodec::codecForName(encoding.toUtf8())) { QMessageBox::warning(this, qApp->applicationName(), tr("This encoding is either not valid or not supported.")); return; diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index dbef08eb..4f11ab1c 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -873,7 +873,7 @@ bool SqliteTableModel::isBinary(const QModelIndex& index) const { // We're using the same way to detect binary data here as in the EditDialog class. For performance reasons we're only looking at // the first couple of bytes though. - QByteArray data = m_data.at(index.row()).at(index.column()).left(512); + QByteArray data = decode(m_data.at(index.row()).at(index.column()).left(512)); return QString(data).toUtf8() != data; } From 5562119563deb726128ebecb5608f55a9859a215 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Sun, 31 Dec 2017 15:25:01 +0100 Subject: [PATCH 06/46] Move check for binary data into separate function There are two places in the code where we check for binary data in database cells. This commit takes the code and moves it into a separate function so it's easier to improve the situation in both function simultaneously. --- CMakeLists.txt | 2 ++ src/Data.cpp | 17 +++++++++++++++++ src/Data.h | 12 ++++++++++++ src/EditDialog.cpp | 5 +++-- src/sqlitetablemodel.cpp | 6 ++---- src/src.pro | 6 ++++-- 6 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 src/Data.cpp create mode 100644 src/Data.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b85e12a..488cfc92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,6 +86,7 @@ set(SQLB_HDR src/grammar/sqlite3TokenTypes.hpp src/grammar/Sqlite3Lexer.hpp src/grammar/Sqlite3Parser.hpp + src/Data.h ) set(SQLB_MOC_HDR @@ -163,6 +164,7 @@ set(SQLB_SRC src/RemotePushDialog.cpp src/FindReplaceDialog.cpp src/ExtendedScintilla.cpp + src/Data.cpp ) set(SQLB_FORMS diff --git a/src/Data.cpp b/src/Data.cpp new file mode 100644 index 00000000..117e02ce --- /dev/null +++ b/src/Data.cpp @@ -0,0 +1,17 @@ +#include "Data.h" + +#include + +bool isTextOnly(QByteArray data, const QString& encoding, bool quickTest) +{ + // Truncate to the first couple of bytes for quick testing + if(quickTest) + data = data.left(512); + + // Convert to Unicode if necessary + if(!encoding.isEmpty()) + data = QTextCodec::codecForName(encoding.toUtf8())->toUnicode(data).toUtf8(); + + // Perform check + return QString(data).toUtf8() == data; +} diff --git a/src/Data.h b/src/Data.h new file mode 100644 index 00000000..e6de8d88 --- /dev/null +++ b/src/Data.h @@ -0,0 +1,12 @@ +#ifndef DATA_H +#define DATA_H + +#include + +// This returns false if the data in the data parameter contains binary data. If it is text only, the function returns +// true. If the second parameter is specified, it will be used to convert the data from the given encoding to Unicode +// before doing the check. The third parameter can be used to only check the first couple of bytes which speeds up the +// text but makes it less reliable +bool isTextOnly(QByteArray data, const QString& encoding = QString(), bool quickTest = false); + +#endif diff --git a/src/EditDialog.cpp b/src/EditDialog.cpp index 8fd73e30..7328a06e 100644 --- a/src/EditDialog.cpp +++ b/src/EditDialog.cpp @@ -4,6 +4,7 @@ #include "Settings.h" #include "src/qhexedit.h" #include "FileDialog.h" +#include "Data.h" #include #include @@ -619,8 +620,8 @@ int EditDialog::checkDataType(const QByteArray& data) return Image; // Check if it's text only - if (QString(cellData).toUtf8() == cellData) { // Is there a better way to check this? - + if(isTextOnly(cellData)) + { QJsonDocument jsonDoc = QJsonDocument::fromJson(QString(cellData).toUtf8()); if (!jsonDoc.isNull()) return JSON; diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index 4f11ab1c..cdafe8bd 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -2,6 +2,7 @@ #include "sqlitedb.h" #include "sqlite.h" #include "Settings.h" +#include "Data.h" #include #include @@ -871,10 +872,7 @@ void SqliteTableModel::clearCache() bool SqliteTableModel::isBinary(const QModelIndex& index) const { - // We're using the same way to detect binary data here as in the EditDialog class. For performance reasons we're only looking at - // the first couple of bytes though. - QByteArray data = decode(m_data.at(index.row()).at(index.column()).left(512)); - return QString(data).toUtf8() != data; + return !isTextOnly(m_data.at(index.row()).at(index.column()), m_encoding, true); } QByteArray SqliteTableModel::encode(const QByteArray& str) const diff --git a/src/src.pro b/src/src.pro index 70c08e01..bc059f39 100644 --- a/src/src.pro +++ b/src/src.pro @@ -61,7 +61,8 @@ HEADERS += \ RemotePushDialog.h \ jsontextedit.h \ FindReplaceDialog.h \ - ExtendedScintilla.h + ExtendedScintilla.h \ + Data.h SOURCES += \ sqlitedb.cpp \ @@ -100,7 +101,8 @@ SOURCES += \ RemotePushDialog.cpp \ jsontextedit.cpp \ FindReplaceDialog.cpp \ - ExtendedScintilla.cpp + ExtendedScintilla.cpp \ + Data.cpp RESOURCES += icons/icons.qrc \ translations/flags/flags.qrc \ From 8f0312487f478109c67830eb13b363d07623b6d6 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Mon, 1 Jan 2018 15:17:22 +0100 Subject: [PATCH 07/46] Fix cmake build --- src/tests/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 52e30676..8b5672a0 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -11,6 +11,7 @@ set(TESTSQLOBJECTS_SRC ../grammar/Sqlite3Parser.cpp ../Settings.cpp testsqlobjects.cpp + ../Data.cpp ) set(TESTSQLOBJECTS_HDR @@ -18,6 +19,7 @@ set(TESTSQLOBJECTS_HDR ../grammar/Sqlite3Lexer.hpp ../grammar/Sqlite3Parser.hpp ../sqlitetypes.h + ../Data.h ) set(TESTSQLOBJECTS_MOC_HDR @@ -83,6 +85,7 @@ set(TESTREGEX_SRC ../grammar/Sqlite3Parser.cpp ../Settings.cpp TestRegex.cpp + ../Data.cpp ) set(TESTREGEX_HDR @@ -90,6 +93,7 @@ set(TESTREGEX_HDR ../grammar/Sqlite3Lexer.hpp ../grammar/Sqlite3Parser.hpp ../sqlitetypes.h + ../Data.h ) set(TESTREGEX_MOC_HDR From 27c657902e354b11fb5778cc09518a95d37d3698 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Mon, 1 Jan 2018 17:20:50 +0100 Subject: [PATCH 08/46] Improve handling of BOMs in table cells Detect some Unicode BOMs and always treat data starting with a BOM as text. We might need to fine-tune this later but it should be an improvement already. In the Edit Dialog remove the BOM from the text editor but keep it in the hex editor. Also add it back to the text when saving changes in text mode. This way the BOM is out of the way for text edits but is not lost either when editing a cell. --- src/Data.cpp | 36 ++++++++++++++++++++++++++++++++++++ src/Data.h | 8 ++++++++ src/EditDialog.cpp | 21 ++++++++++++++------- src/EditDialog.h | 1 + 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/Data.cpp b/src/Data.cpp index 117e02ce..33254219 100644 --- a/src/Data.cpp +++ b/src/Data.cpp @@ -4,6 +4,10 @@ bool isTextOnly(QByteArray data, const QString& encoding, bool quickTest) { + // If the data starts with a Unicode BOM, we always assume it is text + if(startsWithBom(data)) + return true; + // Truncate to the first couple of bytes for quick testing if(quickTest) data = data.left(512); @@ -15,3 +19,35 @@ bool isTextOnly(QByteArray data, const QString& encoding, bool quickTest) // Perform check return QString(data).toUtf8() == data; } + +bool startsWithBom(const QByteArray& data) +{ + // Note that these aren't all possible BOMs. But they are probably the most common ones. + + if(data.startsWith("\xEF\xBB\xBF") || + data.startsWith("\xFE\xFF") || data.startsWith("\xFF\xFE") || + data.startsWith("\x00\x00\xFE\xFF") || data.startsWith("\xFF\xFE\x00\x00")) + return true; + else + return false; +} + +QByteArray removeBom(QByteArray& data) +{ + if(data.startsWith("\xEF\xBB\xBF")) + { + QByteArray bom = data.left(3); + data.remove(0, 3); + return bom; + } else if(data.startsWith("\xFE\xFF") || data.startsWith("\xFF\xFE")) { + QByteArray bom = data.left(2); + data.remove(0, 2); + return bom; + } else if(data.startsWith("\x00\x00\xFE\xFF") || data.startsWith("\xFF\xFE\x00\x00")) { + QByteArray bom = data.left(4); + data.remove(0, 4); + return bom; + } else { + return QByteArray(); + } +} diff --git a/src/Data.h b/src/Data.h index e6de8d88..2cbf9ddc 100644 --- a/src/Data.h +++ b/src/Data.h @@ -9,4 +9,12 @@ // text but makes it less reliable bool isTextOnly(QByteArray data, const QString& encoding = QString(), bool quickTest = false); +// This function returns true if the data in the data parameter starts with a Unicode BOM. Otherwise it returns false. +bool startsWithBom(const QByteArray& data); + +// This function checks if the data in the data parameter starts with a Unicode BOM. If so, the BOM is removed from the +// byte array and passed back to the caller separately as the return value of the function. If the data does not start +// with a BOM an empty byte array is returned and the original data is not modified. +QByteArray removeBom(QByteArray& data); + #endif diff --git a/src/EditDialog.cpp b/src/EditDialog.cpp index 7328a06e..2a56b3ab 100644 --- a/src/EditDialog.cpp +++ b/src/EditDialog.cpp @@ -93,6 +93,9 @@ void EditDialog::loadData(const QByteArray& data) QImage img; QString textData; + // Clear previously removed BOM + removedBom.clear(); + // Determine the data type, saving that info in the class variable dataType = checkDataType(data); @@ -150,25 +153,28 @@ void EditDialog::loadData(const QByteArray& data) case Text: case JSON: - // Set enabled any of the text widgets ui->editorText->setEnabled(true); jsonEdit->setEnabled(true); switch (editMode) { case TextEditor: + { // The text widget buffer is now the main data source dataSource = TextBuffer; - // Load the text into the text editor - textData = QString::fromUtf8(data.constData(), data.size()); + // Load the text into the text editor, remove BOM first if there is one + QByteArray dataWithoutBom = data; + removedBom = removeBom(dataWithoutBom); + + textData = QString::fromUtf8(dataWithoutBom.constData(), dataWithoutBom.size()); ui->editorText->setPlainText(textData); // Select all of the text by default ui->editorText->selectAll(); break; - + } case JsonEditor: // The JSON widget buffer is now the main data source dataSource = JsonBuffer; @@ -373,6 +379,7 @@ void EditDialog::setNull() hexEdit->setData(QByteArray()); jsonEdit->clear(); dataType = Null; + removedBom.clear(); // Check if in text editor mode int editMode = ui->editorStack->currentIndex(); @@ -425,10 +432,10 @@ void EditDialog::accept() } else { // It's not NULL, so proceed with normal text string checking QString oldData = currentIndex.data(Qt::EditRole).toString(); - QString newData = ui->editorText->toPlainText(); + QString newData = removedBom + ui->editorText->toPlainText(); if (oldData != newData) // The data is different, so commit it back to the database - emit recordTextUpdated(currentIndex, newData.toUtf8(), false); + emit recordTextUpdated(currentIndex, removedBom + newData.toUtf8(), false); } break; case JsonBuffer: @@ -509,7 +516,7 @@ void EditDialog::editModeChanged(int newMode) case HexEditor: // Switching to the hex editor // Convert the text widget buffer for the hex widget - hexEdit->setData(ui->editorText->toPlainText().toUtf8()); + hexEdit->setData(removedBom + ui->editorText->toPlainText().toUtf8()); // The hex widget buffer is now the main data source dataSource = HexBuffer; diff --git a/src/EditDialog.h b/src/EditDialog.h index 56981abb..490f3eae 100644 --- a/src/EditDialog.h +++ b/src/EditDialog.h @@ -57,6 +57,7 @@ private: bool textNullSet; bool isReadOnly; bool mustIndentAndCompact; + QByteArray removedBom; enum DataSources { TextBuffer, From 106e57eedcaa2379bbf136977ff4093a5e63ba6a Mon Sep 17 00:00:00 2001 From: Manuel Date: Mon, 1 Jan 2018 20:07:52 +0100 Subject: [PATCH 09/46] Allow users to drag and drop fields from DB Schema dock to editor (#1250) * Allow users to drag and drop fields from DB Schema dock to editor This is the first attempt to provide the functionality described in #119. Drag in the dock is enabled (it was only enabled in the Database Structure tab). Then the fields are enabled for dragging and finally the MIME data exported for the drag and drop is tailored for exporting only the escaped field identifier. This may interfere to other uses of drag&drop in the Database Structure. * Allow the user to drag individual items from the DB Schema dock By allowing extended selections of individual items in the dock version, we let users select what they want to drag and drop. When dragging a list of items from the Name column, a list of escaped identifiers with commas is exported, making easier the composition of SELECT and other statements. In this way, every item name can be dropped, without loosing the ability of exporting the SQL statement for the item. It will just depend on which column is selected. For the DB Structure tab, the original behaviour of single row selection is maintained. In that case, only the SQL column is exported, as before. See issue #119. --- src/DbStructureModel.cpp | 63 +++++++++++++++++++++++++--------------- src/MainWindow.ui | 23 +++++++++++++++ 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/DbStructureModel.cpp b/src/DbStructureModel.cpp index 9bb1d2f4..d44e421d 100644 --- a/src/DbStructureModel.cpp +++ b/src/DbStructureModel.cpp @@ -66,9 +66,9 @@ Qt::ItemFlags DbStructureModel::flags(const QModelIndex &index) const // All items are enabled and selectable Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled; - // Only enable dragging for entire table objects + // Only enable dragging for entire table objects and for fields (composition in SQL text editor) QString type = data(index.sibling(index.row(), ColumnObjectType), Qt::DisplayRole).toString(); - if(type == "table" || type == "view" || type == "index" || type == "trigger") + if(type == "table" || type == "field" || type == "view" || type == "index" || type == "trigger") flags |= Qt::ItemIsDragEnabled; return flags; @@ -190,32 +190,43 @@ QStringList DbStructureModel::mimeTypes() const QMimeData* DbStructureModel::mimeData(const QModelIndexList& indices) const { + // We store the SQL data and the names data separately + QByteArray sqlData, namesData; + // Loop through selected indices - QByteArray d; for(const QModelIndex& index : indices) { - // Only export data for valid indices and only for the SQL column, i.e. only once per row - if(index.isValid() && index.column() == ColumnSQL) - { - // Add the SQL code used to create the object - d = d.append(data(index, Qt::DisplayRole).toString() + ";\n"); + // Only export data for valid indices and only once per row (SQL column or Name column). + // For names, export an escaped identifier of the item for statement composition in SQL editor. + // Commas are included for a list of identifiers. + if(index.isValid()) { + QString objectType = data(index.sibling(index.row(), ColumnObjectType), Qt::DisplayRole).toString(); - // If it is a table also add the content - if(data(index.sibling(index.row(), ColumnObjectType), Qt::DisplayRole).toString() == "table") + if(index.column() == ColumnName) + namesData.append(sqlb::escapeIdentifier(data(index, Qt::DisplayRole).toString()) + ", "); + + if(objectType != "field" && index.column() == ColumnSQL) { - SqliteTableModel tableModel(m_db); - sqlb::ObjectIdentifier objid(data(index.sibling(index.row(), ColumnSchema), Qt::DisplayRole).toString(), - data(index.sibling(index.row(), ColumnName), Qt::DisplayRole).toString()); - tableModel.setTable(objid); - tableModel.waitForFetchingFinished(); - for(int i=0; i < tableModel.rowCount(); ++i) + // Add the SQL code used to create the object + sqlData.append(data(index, Qt::DisplayRole).toString() + ";\n"); + + // If it is a table also add the content + if(objectType == "table") { - QString insertStatement = "INSERT INTO " + objid.toString() + " VALUES("; - for(int j=1; j < tableModel.columnCount(); ++j) - insertStatement += QString("'%1',").arg(tableModel.data(tableModel.index(i, j)).toString()); - insertStatement.chop(1); - insertStatement += ");\n"; - d = d.append(insertStatement); + SqliteTableModel tableModel(m_db); + sqlb::ObjectIdentifier objid(data(index.sibling(index.row(), ColumnSchema), Qt::DisplayRole).toString(), + data(index.sibling(index.row(), ColumnName), Qt::DisplayRole).toString()); + tableModel.setTable(objid); + tableModel.waitForFetchingFinished(); + for(int i=0; i < tableModel.rowCount(); ++i) + { + QString insertStatement = "INSERT INTO " + objid.toString() + " VALUES("; + for(int j=1; j < tableModel.columnCount(); ++j) + insertStatement += QString("'%1',").arg(tableModel.data(tableModel.index(i, j)).toString()); + insertStatement.chop(1); + insertStatement += ");\n"; + sqlData.append(insertStatement); + } } } } @@ -224,7 +235,13 @@ QMimeData* DbStructureModel::mimeData(const QModelIndexList& indices) const // Create the MIME data object QMimeData* mime = new QMimeData(); mime->setProperty("db_file", m_db.currentFile()); // Also save the file name to avoid dropping an object on the same database as it comes from - mime->setData("text/plain", d); + // When we have both SQL and Names data (probable row selection mode) we give precedence to the SQL data + if (sqlData.length() == 0 && namesData.length() > 0) { + // Remove last ", " + namesData.chop(2); + mime->setData("text/plain", namesData); + } else + mime->setData("text/plain", sqlData); return mime; } diff --git a/src/MainWindow.ui b/src/MainWindow.ui index af6b40c2..3ff316a5 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -63,6 +63,11 @@ Qt::CustomContextMenu + + This is the structure of the opened database. +You can drag SQL sentences from an object row and drop them into other applications or into another instance of 'DB Browser for SQLite'. + + true @@ -1119,9 +1124,27 @@ + + This is the structure of the opened database. +You can drag multiple object names from the Name column and drop them into the SQL editor . +You can drag SQL sentences from the Schema column and drop them into the SQL editor or into other applications. + + + + true + + + QAbstractItemView::DragDrop + true + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectItems + QAbstractItemView::ScrollPerPixel From 117af5aeeb146860f080584c82876f71ac227ff1 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Mon, 1 Jan 2018 20:11:26 +0100 Subject: [PATCH 10/46] Put full cell data into INSERT statements when dragging tables In the Structure tab you can drag & drop entire tables. This copies the CREATE statement of the table along with the INSERT statements for the table data. However, for the table data we would use the data as shown in the table view cells, i.e. 'NULL' for NULL values or truncated data for very long strings. This is at least partly improved by this commit. It doesn't treat NULL or BLOB values 100% correctly but fixes the truncation problem and at least makes the issues in the other cases a bit more obvious. --- src/DbStructureModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DbStructureModel.cpp b/src/DbStructureModel.cpp index d44e421d..73fdf1cc 100644 --- a/src/DbStructureModel.cpp +++ b/src/DbStructureModel.cpp @@ -222,7 +222,7 @@ QMimeData* DbStructureModel::mimeData(const QModelIndexList& indices) const { QString insertStatement = "INSERT INTO " + objid.toString() + " VALUES("; for(int j=1; j < tableModel.columnCount(); ++j) - insertStatement += QString("'%1',").arg(tableModel.data(tableModel.index(i, j)).toString()); + insertStatement += QString("'%1',").arg(tableModel.data(tableModel.index(i, j), Qt::EditRole).toString()); insertStatement.chop(1); insertStatement += ");\n"; sqlData.append(insertStatement); From c9c848e99555f816aa183e7dad426e3151e0f51d Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Tue, 2 Jan 2018 18:15:38 +0100 Subject: [PATCH 11/46] Fix wrong BOM detection This fixes a bug introduced in 27c657902e354b11fb5778cc09518a95d37d3698. In that commit we are checking if a string starts with a Unicode BOM and remove the BOM if necessary. Apparently though Qt's startsWith() function doesn't work as exected here, so it's changed to a manual comparison in this commit which seems to work fine. The first person who can explain that behaviour to me gets a free beer if we meet in person. See issue #1279. See issue #1282. --- src/Data.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Data.cpp b/src/Data.cpp index 33254219..9cd6f556 100644 --- a/src/Data.cpp +++ b/src/Data.cpp @@ -34,16 +34,16 @@ bool startsWithBom(const QByteArray& data) QByteArray removeBom(QByteArray& data) { - if(data.startsWith("\xEF\xBB\xBF")) + if(data.left(3) == QByteArray("\xEF\xBB\xBF")) { QByteArray bom = data.left(3); data.remove(0, 3); return bom; - } else if(data.startsWith("\xFE\xFF") || data.startsWith("\xFF\xFE")) { + } else if(data.left(2) == QByteArray("\xFE\xFF") || data.left(2) == QByteArray("\xFF\xFE")) { QByteArray bom = data.left(2); data.remove(0, 2); return bom; - } else if(data.startsWith("\x00\x00\xFE\xFF") || data.startsWith("\xFF\xFE\x00\x00")) { + } else if(data.left(4) == QByteArray("\x00\x00\xFE\xFF") || data.left(4) == QByteArray("\xFF\xFE\x00\x00")) { QByteArray bom = data.left(4); data.remove(0, 4); return bom; From feda408161ae98fee69bfd46a91c607bbc40921d Mon Sep 17 00:00:00 2001 From: mgrojo Date: Tue, 2 Jan 2018 21:13:52 +0100 Subject: [PATCH 12/46] Fix wrong BOM detection in startsWithBom and fix BOM checks containing 0 The same fix for BOM detection in startsWithBom as already applied in c9c848e99555f816aa183e7dad426e3151e0f51d for removeBom, otherwise binary data is considered text. Fixed also check for "\x00\x00\xFE\xFF" and "\xFF\xFE\x00\x00", that are the problematic BOMs since they contain the null character. --- src/Data.cpp | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Data.cpp b/src/Data.cpp index 9cd6f556..9436711f 100644 --- a/src/Data.cpp +++ b/src/Data.cpp @@ -1,6 +1,15 @@ #include "Data.h" #include +#include + +// Note that these aren't all possible BOMs. But they are probably the most common ones. +// The size is needed at least for the ones with character zero in them. +static const QByteArray bom3("\xEF\xBB\xBF", 3); +static const QByteArray bom2a("\xFE\xFF", 2); +static const QByteArray bom2b("\xFF\xFE", 2); +static const QByteArray bom4a("\x00\x00\xFE\xFF", 4); +static const QByteArray bom4b("\xFF\xFE\x00\x00", 4); bool isTextOnly(QByteArray data, const QString& encoding, bool quickTest) { @@ -22,28 +31,26 @@ bool isTextOnly(QByteArray data, const QString& encoding, bool quickTest) bool startsWithBom(const QByteArray& data) { - // Note that these aren't all possible BOMs. But they are probably the most common ones. - - if(data.startsWith("\xEF\xBB\xBF") || - data.startsWith("\xFE\xFF") || data.startsWith("\xFF\xFE") || - data.startsWith("\x00\x00\xFE\xFF") || data.startsWith("\xFF\xFE\x00\x00")) - return true; + if(data.startsWith(bom3) || + data.startsWith(bom2a) || data.startsWith(bom2b) || + data.startsWith(bom4a) || data.startsWith(bom4b)) + return true; else return false; } QByteArray removeBom(QByteArray& data) { - if(data.left(3) == QByteArray("\xEF\xBB\xBF")) + if(data.startsWith(bom3)) { QByteArray bom = data.left(3); data.remove(0, 3); return bom; - } else if(data.left(2) == QByteArray("\xFE\xFF") || data.left(2) == QByteArray("\xFF\xFE")) { + } else if(data.startsWith(bom2a) || data.startsWith(bom2b)) { QByteArray bom = data.left(2); data.remove(0, 2); return bom; - } else if(data.left(4) == QByteArray("\x00\x00\xFE\xFF") || data.left(4) == QByteArray("\xFF\xFE\x00\x00")) { + } else if(data.startsWith(bom4a) || data.startsWith(bom4b)) { QByteArray bom = data.left(4); data.remove(0, 4); return bom; From 1be61dbb4f36530839b619862a022724f1c82cf4 Mon Sep 17 00:00:00 2001 From: mgrojo Date: Thu, 4 Jan 2018 21:05:24 +0100 Subject: [PATCH 13/46] Removed unused QDebug include sentences Last one was accidentally left by commit feda408161ae98fee69bfd46a91c607bbc40921d --- src/Data.cpp | 1 - src/sqlitedb.cpp | 1 - src/sqlitetablemodel.cpp | 1 - 3 files changed, 3 deletions(-) diff --git a/src/Data.cpp b/src/Data.cpp index 9436711f..e3a5e491 100644 --- a/src/Data.cpp +++ b/src/Data.cpp @@ -1,7 +1,6 @@ #include "Data.h" #include -#include // Note that these aren't all possible BOMs. But they are probably the most common ones. // The size is needed at least for the ones with character zero in them. diff --git a/src/sqlitedb.cpp b/src/sqlitedb.cpp index e30c56c8..c54dc573 100644 --- a/src/sqlitedb.cpp +++ b/src/sqlitedb.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index cdafe8bd..a762bfaf 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -4,7 +4,6 @@ #include "Settings.h" #include "Data.h" -#include #include #include #include From 6f5e507556711b9e67ed6aebe9f3301b77abafc4 Mon Sep 17 00:00:00 2001 From: mgrojo Date: Thu, 4 Jan 2018 21:48:19 +0100 Subject: [PATCH 14/46] Right click on vertical header to delete the selected record(s) The text is got from the "Delete record" button, so the text is updated to "Delete records" when appropriate. See feature request #1283 --- src/MainWindow.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index d4df0d72..a10d9992 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -2452,6 +2452,13 @@ void MainWindow::showRecordPopupMenu(const QPoint& pos) duplicateRecord(row); }); + QAction* deleteRecordAction = new QAction(ui->buttonDeleteRecord->text(), &popupRecordMenu); + popupRecordMenu.addAction(deleteRecordAction); + + connect(deleteRecordAction, &QAction::triggered, [&]() { + deleteRecord(); + }); + popupRecordMenu.exec(ui->dataTable->verticalHeader()->mapToGlobal(pos)); } From 12ad5e5b684378486bd6c7991bd4cb2a647c8f21 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Fri, 5 Jan 2018 00:41:27 +0000 Subject: [PATCH 15/46] Add SQLCipher to the Ubuntu build instructions --- BUILDING.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index dccf83ad..9df79344 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -54,12 +54,13 @@ The same process works for building the code in any platform supported by Qt ### Ubuntu Linux ```bash -$ sudo apt install build-essential git cmake libsqlite3-dev qt5-default qttools5-dev-tools +$ sudo apt install build-essential git cmake libsqlite3-dev qt5-default qttools5-dev-tools \ + libsqlcipher-dev $ git clone https://github.com/sqlitebrowser/sqlitebrowser $ cd sqlitebrowser $ mkdir build $ cd build -$ cmake -Wno-dev .. +$ cmake -Dsqlcipher=1 -Wno-dev .. $ make $ sudo make install ``` From 14da8dcf984b9c8d28e506dcb645a1adf270ac58 Mon Sep 17 00:00:00 2001 From: Oscar Cowdery Lack Date: Sat, 6 Jan 2018 01:56:37 +1100 Subject: [PATCH 16/46] Remove hardcoded styles from hyperlinks (#1286) These hardcoded styles are unnecessary, as Qt will automatically add styling to hyperlinks that matches the system theme. --- src/AboutDialog.ui | 2 +- src/FindReplaceDialog.ui | 2 +- src/MainWindow.ui | 34 +++++++++++++++++----------------- src/SqlExecutionArea.ui | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/AboutDialog.ui b/src/AboutDialog.ui index adeacc95..e2844807 100644 --- a/src/AboutDialog.ui +++ b/src/AboutDialog.ui @@ -99,7 +99,7 @@ - <html><head/><body><p>DB Browser for SQLite is an open source, freeware visual tool used to create, design and edit SQLite database files.</p><p>It is bi-licensed under the Mozilla Public License Version 2, as well as the GNU General Public License Version 3 or later. You can modify or redistribute it under the conditions of these licenses.</p><p>See <a href="http://www.gnu.org/licenses/gpl.html"><span style=" text-decoration: underline; color:#0000ff;">http://www.gnu.org/licenses/gpl.html</span></a> and <a href="https://www.mozilla.org/MPL/2.0/index.txt"><span style=" text-decoration: underline; color:#0000ff;">https://www.mozilla.org/MPL/2.0/index.txt</span></a> for details.</p><p>For more information on this program please visit our website at: <a href="http://sqlitebrowser.org"><span style=" text-decoration: underline; color:#0000ff;">http://sqlitebrowser.org</span></a></p><p><span style=" font-size:small;">This software uses the GPL/LGPL Qt Toolkit from </span><a href="http://qt-project.org/"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">http://qt-project.org/</span></a><span style=" font-size:small;"><br/>See </span><a href="http://qt-project.org/doc/qt-5/licensing.html"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">http://qt-project.org/doc/qt-5/licensing.html</span></a><span style=" font-size:small;"> for licensing terms and information.</span></p><p><span style=" font-size:small;">It also uses the Silk icon set by Mark James licensed under a Creative Commons Attribution 2.5 and 3.0 license.<br/>See </span><a href="http://www.famfamfam.com/lab/icons/silk/"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">http://www.famfamfam.com/lab/icons/silk/</span></a><span style=" font-size:small;"> for details.</span></p></body></html> + <html><head/><body><p>DB Browser for SQLite is an open source, freeware visual tool used to create, design and edit SQLite database files.</p><p>It is bi-licensed under the Mozilla Public License Version 2, as well as the GNU General Public License Version 3 or later. You can modify or redistribute it under the conditions of these licenses.</p><p>See <a href="http://www.gnu.org/licenses/gpl.html">http://www.gnu.org/licenses/gpl.html</a> and <a href="https://www.mozilla.org/MPL/2.0/index.txt">https://www.mozilla.org/MPL/2.0/index.txt</a> for details.</p><p>For more information on this program please visit our website at: <a href="http://sqlitebrowser.org">http://sqlitebrowser.org</a></p><p><span style=" font-size:small;">This software uses the GPL/LGPL Qt Toolkit from </span><a href="http://qt-project.org/"><span style=" font-size:small;">http://qt-project.org/</span></a><span style=" font-size:small;"><br/>See </span><a href="http://qt-project.org/doc/qt-5/licensing.html"><span style=" font-size:small;">http://qt-project.org/doc/qt-5/licensing.html</span></a><span style=" font-size:small;"> for licensing terms and information.</span></p><p><span style=" font-size:small;">It also uses the Silk icon set by Mark James licensed under a Creative Commons Attribution 2.5 and 3.0 license.<br/>See </span><a href="http://www.famfamfam.com/lab/icons/silk/"><span style=" font-size:small;">http://www.famfamfam.com/lab/icons/silk/</span></a><span style=" font-size:small;"> for details.</span></p></body></html> Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop diff --git a/src/FindReplaceDialog.ui b/src/FindReplaceDialog.ui index 2569a657..689e6e6a 100644 --- a/src/FindReplaceDialog.ui +++ b/src/FindReplaceDialog.ui @@ -103,7 +103,7 @@ - <html><head/><body><p>When checked, the pattern to find is interpreted as a UNIX regular expression. See <a href="https://en.wikibooks.org/wiki/Regular_Expressions"><span style=" text-decoration: underline; color:#0000ff;">Regular Expression in Wikibooks</span></a>.</p></body></html> + <html><head/><body><p>When checked, the pattern to find is interpreted as a UNIX regular expression. See <a href="https://en.wikibooks.org/wiki/Regular_Expressions">Regular Expression in Wikibooks</a>.</p></body></html> Use regular e&xpressions diff --git a/src/MainWindow.ui b/src/MainWindow.ui index 3ff316a5..6211e390 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -401,7 +401,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_auto_vacuum"><span style=" text-decoration: underline; color:#0000ff;">Auto Vacuum</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_auto_vacuum">Auto Vacuum</a></p></body></html> true @@ -436,7 +436,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_automatic_index"><span style=" text-decoration: underline; color:#0000ff;">Automatic Index</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_automatic_index">Automatic Index</a></p></body></html> true @@ -456,7 +456,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_checkpoint_fullfsync"><span style=" text-decoration: underline; color:#0000ff;">Checkpoint Full FSYNC</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_checkpoint_fullfsync">Checkpoint Full FSYNC</a></p></body></html> true @@ -476,7 +476,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_foreign_keys"><span style=" text-decoration: underline; color:#0000ff;">Foreign Keys</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_foreign_keys">Foreign Keys</a></p></body></html> true @@ -496,7 +496,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_fullfsync"><span style=" text-decoration: underline; color:#0000ff;">Full FSYNC</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_fullfsync">Full FSYNC</a></p></body></html> true @@ -516,7 +516,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_ignore_check_constraints"><span style=" text-decoration: underline; color:#0000ff;">Ignore Check Constraints</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_ignore_check_constraints">Ignore Check Constraints</a></p></body></html> true @@ -536,7 +536,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_journal_mode"><span style=" text-decoration: underline; color:#0000ff;">Journal Mode</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_journal_mode">Journal Mode</a></p></body></html> true @@ -583,7 +583,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_journal_size_limit"><span style=" text-decoration: underline; color:#0000ff;">Journal Size Limit</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_journal_size_limit">Journal Size Limit</a></p></body></html> true @@ -606,7 +606,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_locking_mode"><span style=" text-decoration: underline; color:#0000ff;">Locking Mode</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_locking_mode">Locking Mode</a></p></body></html> true @@ -633,7 +633,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_max_page_count"><span style=" text-decoration: underline; color:#0000ff;">Max Page Count</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_max_page_count">Max Page Count</a></p></body></html> true @@ -653,7 +653,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_page_size"><span style=" text-decoration: underline; color:#0000ff;">Page Size</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_page_size">Page Size</a></p></body></html> true @@ -676,7 +676,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_recursive_triggers"><span style=" text-decoration: underline; color:#0000ff;">Recursive Triggers</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_recursive_triggers">Recursive Triggers</a></p></body></html> true @@ -696,7 +696,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_secure_delete"><span style=" text-decoration: underline; color:#0000ff;">Secure Delete</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_secure_delete">Secure Delete</a></p></body></html> true @@ -716,7 +716,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_synchronous"><span style=" text-decoration: underline; color:#0000ff;">Synchronous</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_synchronous">Synchronous</a></p></body></html> true @@ -748,7 +748,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_temp_store"><span style=" text-decoration: underline; color:#0000ff;">Temp Store</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_temp_store">Temp Store</a></p></body></html> true @@ -780,7 +780,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_user_version"><span style=" text-decoration: underline; color:#0000ff;">User Version</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_user_version">User Version</a></p></body></html> true @@ -800,7 +800,7 @@ You can drag SQL sentences from an object row and drop them into other applicati - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_wal_autocheckpoint"><span style=" text-decoration: underline; color:#0000ff;">WAL Auto Checkpoint</span></a></p></body></html> + <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_wal_autocheckpoint">WAL Auto Checkpoint</a></p></body></html> true diff --git a/src/SqlExecutionArea.ui b/src/SqlExecutionArea.ui index 32fb1864..57d5e753 100644 --- a/src/SqlExecutionArea.ui +++ b/src/SqlExecutionArea.ui @@ -143,7 +143,7 @@ Interpret search pattern as a regular expression - <html><head/><body><p>When checked, the pattern to find is interpreted as a UNIX regular expression. See <a href="https://en.wikibooks.org/wiki/Regular_Expressions"><span style=" text-decoration: underline; color:#0000ff;">Regular Expression in Wikibooks</span></a>.</p></body></html> + <html><head/><body><p>When checked, the pattern to find is interpreted as a UNIX regular expression. See <a href="https://en.wikibooks.org/wiki/Regular_Expressions">Regular Expression in Wikibooks</a>.</p></body></html> Regular Expression From 5f702db0e0142ea3e1f3ceef9fa0ff80517f51cb Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Fri, 5 Jan 2018 16:07:18 +0100 Subject: [PATCH 17/46] Don't set modified flag for database when attaching/detaching database Don't set the modified flag of the main database if we're attaching or detaching another database since these actions don't alter the original database. See issue #1249. --- src/MainWindow.cpp | 7 ++++++- src/MainWindow.h | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index a10d9992..6c9a79f8 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -976,6 +976,8 @@ MainWindow::StatementType MainWindow::getQueryType(const QString& query) const if(query.startsWith("UPDATE", Qt::CaseInsensitive)) return UpdateStatement; if(query.startsWith("DELETE", Qt::CaseInsensitive)) return DeleteStatement; if(query.startsWith("CREATE", Qt::CaseInsensitive)) return CreateStatement; + if(query.startsWith("ATTACH", Qt::CaseInsensitive)) return AttachStatement; + if(query.startsWith("DETACH", Qt::CaseInsensitive)) return DetachStatement; return OtherStatement; } @@ -1156,7 +1158,10 @@ void MainWindow::executeQuery() if(query_part_type == InsertStatement || query_part_type == UpdateStatement || query_part_type == DeleteStatement) stmtHasChangedDatabase = tr(", %1 rows affected").arg(sqlite3_changes(db._db)); - modified = true; + // Attach/Detach statements don't modify the original database + if(query_part_type != StatementType::AttachStatement && query_part_type != StatementType::DetachStatement) + modified = true; + statusMessage = tr("Query executed successfully: %1 (took %2ms%3)").arg(queryPart.trimmed()).arg(timer.elapsed()).arg(stmtHasChangedDatabase); ok = true; break; diff --git a/src/MainWindow.h b/src/MainWindow.h index f3cf28ea..b5f6faba 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -143,6 +143,8 @@ private: UpdateStatement, DeleteStatement, CreateStatement, + AttachStatement, + DetachStatement, OtherStatement, }; From 7d2931baa2521d0b03b3385bddd3e4e413c75123 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Fri, 5 Jan 2018 16:27:23 +0100 Subject: [PATCH 18/46] Add a --read-only command line option Add a new command line option -R / --read-only for opening a database in read-only mode. See issue #1265. --- src/Application.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Application.cpp b/src/Application.cpp index 5b07774e..1c590063 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -75,6 +75,7 @@ Application::Application(int& argc, char** argv) : QString fileToOpen; QString tableToBrowse; QStringList sqlToExecute; + bool readOnly = false; m_dontShowMainWindow = false; for(int i=1;ifileOpen(fileToOpen)) + if(m_mainWindow->fileOpen(fileToOpen, false, readOnly)) { // If database could be opened run the SQL scripts for(const QString& f : sqlToExecute) From eae0730e3eaa41e117ca21cdb78171d738bbbb4f Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Fri, 5 Jan 2018 16:45:50 +0100 Subject: [PATCH 19/46] Fix removing comments from SQL when there are quoted strings In the code for removing comments from SQL statements we have to make sure to only match the '--' characters when they are not inside a quoted string or identifier. This works fine and as expected for single quotes. However, for double quotes it doesn't. This is fixed by this commit. See issue #1270. --- src/sqlitetablemodel.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index a762bfaf..daca841d 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -678,16 +678,16 @@ void SqliteTableModel::removeCommentsFromQuery(QString& query) { // deal with end-of-line comments { /* The regular expression for removing end of line comments works like this: - * ^((?:(?:[^'-]|-(?!-))*|(?:'[^']*'))*)(--.*)$ - * ^ $ # anchor beginning and end of string so we use it all - * ( )( ) # two separate capture groups for code and comment - * --.* # comment starts with -- and consumes everything afterwards - * (?: | )* # code is none or many strings alternating with non-strings - * (?:'[^']*') # a string is a quote, followed by none or more non-quotes, followed by a quote - * (?:[^'-]|-(?!-))* # non-string is a sequence of characters which aren't quotes or hyphens, - * OR if they are hyphens then they can't be followed immediately by another hyphen + * ^((?:(?:[^'-]|-(?!-))*|(?:(?P['"])[^(?P=quote)]*(?P=quote)))*)(--.*)$ + * ^ $ # anchor beginning and end of string so we use it all + * ( )( ) # two separate capture groups for code and comment + * --.* # comment starts with -- and consumes everything afterwards + * (?: | )* # code is none or many strings alternating with non-strings + * (?:(?P['"])[^(?P=quote)]*(?P=quote)) # a string is a quote, followed by none or more non-quotes, followed by a quote + * (?:[^'-]|-(?!-))* # non-string is a sequence of characters which aren't quotes or hyphens, + * OR if they are hyphens then they can't be followed immediately by another hyphen */ - QRegExp rxSQL("^((?:(?:[^'-]|-(?!-))*|(?:'[^']*'))*)(--[^\\r\\n]*)([\\r\\n]*)(.*)$"); // set up regex to find end-of-line comment + QRegExp rxSQL("^((?:(?:[^'-]|-(?!-))*|(?:'[^']*'))(?:(?P['\"])[^(?P=quote)]*(?P=quote))*)(--[^\\r\\n]*)([\\r\\n]*)(.*)$"); // set up regex to find end-of-line comment QString result; while(query.size() != 0) From 29fa332f9749cc3753ecdd983c8dc5faffdaaf2f Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Fri, 5 Jan 2018 17:24:23 +0100 Subject: [PATCH 20/46] Fix bug introduced in eae0730e3eaa41e117ca21cdb78171d738bbbb4f --- src/sqlitetablemodel.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index daca841d..d2f81cfa 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -678,16 +678,16 @@ void SqliteTableModel::removeCommentsFromQuery(QString& query) { // deal with end-of-line comments { /* The regular expression for removing end of line comments works like this: - * ^((?:(?:[^'-]|-(?!-))*|(?:(?P['"])[^(?P=quote)]*(?P=quote)))*)(--.*)$ - * ^ $ # anchor beginning and end of string so we use it all - * ( )( ) # two separate capture groups for code and comment - * --.* # comment starts with -- and consumes everything afterwards - * (?: | )* # code is none or many strings alternating with non-strings - * (?:(?P['"])[^(?P=quote)]*(?P=quote)) # a string is a quote, followed by none or more non-quotes, followed by a quote - * (?:[^'-]|-(?!-))* # non-string is a sequence of characters which aren't quotes or hyphens, - * OR if they are hyphens then they can't be followed immediately by another hyphen + * ^((?:(?:[^'-]|-(?!-))*|(?:(?:P['"])[^(?:P=quote)]*(?:P=quote)))*)(--.*)$ + * ^ $ # anchor beginning and end of string so we use it all + * ( )( ) # two separate capture groups for code and comment + * --.* # comment starts with -- and consumes everything afterwards + * (?: | )* # code is none or many strings alternating with non-strings + * (?:(?:P['"])[^(?:P=quote)]*(?:P=quote)) # a string is a quote, followed by none or more non-quotes, followed by a quote + * (?:[^'-]|-(?!-))* # non-string is a sequence of characters which aren't quotes or hyphens, + * OR if they are hyphens then they can't be followed immediately by another hyphen */ - QRegExp rxSQL("^((?:(?:[^'-]|-(?!-))*|(?:'[^']*'))(?:(?P['\"])[^(?P=quote)]*(?P=quote))*)(--[^\\r\\n]*)([\\r\\n]*)(.*)$"); // set up regex to find end-of-line comment + QRegExp rxSQL("^((?:(?:[^'-]|-(?!-))*|(?:'[^']*'))(?:(?:P['\"])[^(?:P=quote)]*(?:P=quote))*)(--[^\\r\\n]*)([\\r\\n]*)(.*)$"); // set up regex to find end-of-line comment QString result; while(query.size() != 0) From 743e7985c2dc5b6c65ac0ff46abba33a443f26ba Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Fri, 5 Jan 2018 17:35:26 +0100 Subject: [PATCH 21/46] Add support for conflict clauses to the grammar parser Add support for parsing and generating CREATE statements with an ON CONFLICT clause for their primary key. --- src/sqlitetypes.cpp | 26 ++++++++++++++++++++++++++ src/sqlitetypes.h | 6 ++++++ 2 files changed, 32 insertions(+) diff --git a/src/sqlitetypes.cpp b/src/sqlitetypes.cpp index 8c1f7429..0e398bc0 100644 --- a/src/sqlitetypes.cpp +++ b/src/sqlitetypes.cpp @@ -92,6 +92,7 @@ public: private: void parsecolumn(Table* table, antlr::RefAST c); + QString parseConflictClause(antlr::RefAST c); private: antlr::RefAST m_root; @@ -219,6 +220,9 @@ QString PrimaryKeyConstraint::toSql(const FieldVector& applyOn) const result += QString("CONSTRAINT %1 ").arg(escapeIdentifier(m_name)); result += QString("PRIMARY KEY(%1)").arg(fieldVectorToFieldNames(applyOn).join(",")); + if(!m_conflictAction.isEmpty()) + result += " ON CONFLICT " + m_conflictAction; + return result; } @@ -794,6 +798,10 @@ TablePtr CreateTableWalker::table() } } while(tc != antlr::nullAST && tc->getType() != sqlite3TokenTypes::RPAREN); + // We're either done now or there is a conflict clause + tc = tc->getNextSibling(); // skip RPAREN + pk->setConflictAction(parseConflictClause(tc)); + tab->addConstraint(fields, ConstraintPtr(pk)); } break; @@ -1026,6 +1034,9 @@ void CreateTableWalker::parsecolumn(Table* table, antlr::RefAST c) table->setFullyParsed(false); con = con->getNextSibling(); //skip } + + primaryKey->setConflictAction(parseConflictClause(con)); + if(con != antlr::nullAST && con->getType() == sqlite3TokenTypes::AUTOINCREMENT) autoincrement = true; } @@ -1145,6 +1156,21 @@ void CreateTableWalker::parsecolumn(Table* table, antlr::RefAST c) } } +QString CreateTableWalker::parseConflictClause(antlr::RefAST c) +{ + QString conflictAction; + + if(c != antlr::nullAST && c->getType() == sqlite3TokenTypes::ON && c->getNextSibling()->getType() == sqlite3TokenTypes::CONFLICT) + { + c = c->getNextSibling(); // skip ON + c = c->getNextSibling(); // skip CONFLICT + conflictAction = identifier(c); + c = c->getNextSibling(); // skip action + } + + return conflictAction; +} + QString IndexedColumn::toString(const QString& indent, const QString& sep) const diff --git a/src/sqlitetypes.h b/src/sqlitetypes.h index b5160733..632727c3 100644 --- a/src/sqlitetypes.h +++ b/src/sqlitetypes.h @@ -265,9 +265,15 @@ class PrimaryKeyConstraint : public Constraint public: PrimaryKeyConstraint() {} + void setConflictAction(const QString& conflict) { m_conflictAction = conflict; } + const QString& conflictAction() const { return m_conflictAction; } + virtual QString toSql(const FieldVector& applyOn) const; virtual ConstraintTypes type() const { return PrimaryKeyConstraintType; } + +private: + QString m_conflictAction; }; class CheckConstraint : public Constraint From 652637232dc6516d1f1817b778f4551fc9965f48 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Fri, 5 Jan 2018 18:20:28 +0100 Subject: [PATCH 22/46] Add tests --- src/tests/TestRegex.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/tests/TestRegex.cpp b/src/tests/TestRegex.cpp index b643b11c..f6625b55 100644 --- a/src/tests/TestRegex.cpp +++ b/src/tests/TestRegex.cpp @@ -57,6 +57,19 @@ void TestRegex::sqlQueryComments_data() "SELECT '-- comment inside quotes'" << // cleanQuery "SELECT '-- comment inside quotes'"; + + /* TODO Fix issue #1270, then activate these + QTest::newRow("single_quote_comment") + << // dirtyQuery + "SELECT 'something--something' -- comment" + << // cleanQuery + "SELECT 'something--something'"; + + QTest::newRow("double_quote_comment") + << // dirtyQuery + "SELECT \"something--something\" -- comment" + << // cleanQuery + "SELECT \"something--something\"";*/ } void TestRegex::sqlQueryComments() From 35b8ac77717c9cf770fb6d7c4877dd8577b96c0e Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Fri, 5 Jan 2018 18:22:15 +0100 Subject: [PATCH 23/46] Revert "Fix removing comments from SQL when there are quoted strings" This reverts commit eae0730e3eaa41e117ca21cdb78171d738bbbb4f. --- src/sqlitetablemodel.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index d2f81cfa..e606e158 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -678,16 +678,16 @@ void SqliteTableModel::removeCommentsFromQuery(QString& query) { // deal with end-of-line comments { /* The regular expression for removing end of line comments works like this: - * ^((?:(?:[^'-]|-(?!-))*|(?:(?:P['"])[^(?:P=quote)]*(?:P=quote)))*)(--.*)$ - * ^ $ # anchor beginning and end of string so we use it all - * ( )( ) # two separate capture groups for code and comment - * --.* # comment starts with -- and consumes everything afterwards - * (?: | )* # code is none or many strings alternating with non-strings - * (?:(?:P['"])[^(?:P=quote)]*(?:P=quote)) # a string is a quote, followed by none or more non-quotes, followed by a quote - * (?:[^'-]|-(?!-))* # non-string is a sequence of characters which aren't quotes or hyphens, - * OR if they are hyphens then they can't be followed immediately by another hyphen + * ^((?:(?:[^'-]|-(?!-))*|(?:'[^']*'))*)(--.*)$ + * ^ $ # anchor beginning and end of string so we use it all + * ( )( ) # two separate capture groups for code and comment + * --.* # comment starts with -- and consumes everything afterwards + * (?: | )* # code is none or many strings alternating with non-strings + * (?:'[^']*') # a string is a quote, followed by none or more non-quotes, followed by a quote + * (?:[^'-]|-(?!-))* # non-string is a sequence of characters which aren't quotes or hyphens, */ - QRegExp rxSQL("^((?:(?:[^'-]|-(?!-))*|(?:'[^']*'))(?:(?:P['\"])[^(?:P=quote)]*(?:P=quote))*)(--[^\\r\\n]*)([\\r\\n]*)(.*)$"); // set up regex to find end-of-line comment + + QRegExp rxSQL("^((?:(?:[^'-]|-(?!-))*|(?:'[^']*'))*)(--[^\\r\\n]*)([\\r\\n]*)(.*)$"); // set up regex to find end-of-line comment QString result; while(query.size() != 0) From bbac655499d5f4d2334bb7068c47f120be2e9225 Mon Sep 17 00:00:00 2001 From: mgrojo Date: Fri, 5 Jan 2018 18:25:57 +0100 Subject: [PATCH 24/46] Select the pointed table row when right clicking on the vertical header This makes sense from the user point of view, since it remarks that the options from the menu applies to that row. This change will also fix the different behaviour of the "Delete record" and "Duplicate record" in the context menu. See issue #1283 --- src/MainWindow.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 6c9a79f8..8fd01c0b 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -2447,6 +2447,8 @@ void MainWindow::showRecordPopupMenu(const QPoint& pos) if (row == -1) return; + ui->dataTable->selectRow(row); + QMenu popupRecordMenu(this); QAction* action = new QAction("Duplicate record", &popupRecordMenu); // Set shortcut for documentation purposes (the actual functional shortcut is not set here) From cb98e29a7c21996d951edc5bd8c0809d556f7a75 Mon Sep 17 00:00:00 2001 From: mgrojo Date: Fri, 5 Jan 2018 20:51:36 +0100 Subject: [PATCH 25/46] Allow deleting and duplicating a selection of rows Before bbac655499d5f4d2334bb7068c47f120be2e9225 it was possible to delete a set of selected rows. This makes it possible again by only selecting the row if it is not already inside the selected rows. See issue #1283. Additionally and for coherence, the "Duplicate record" from the context menu is also made to apply to the list of selected rows. See issue #1090 --- src/MainWindow.cpp | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 8fd01c0b..d824ac20 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -2447,16 +2447,32 @@ void MainWindow::showRecordPopupMenu(const QPoint& pos) if (row == -1) return; - ui->dataTable->selectRow(row); + // Select the row if it is not already in the selection. + QModelIndexList rowList = ui->dataTable->selectionModel()->selectedRows(); + bool found = false; + for (QModelIndex index : rowList) { + if (row == index.row()) { + found = true; + break; + } + } + if (!found) + ui->dataTable->selectRow(row); + + rowList = ui->dataTable->selectionModel()->selectedRows(); + + QString duplicateText = rowList.count() > 1 ? tr("Duplicate records") : tr("Duplicate record"); QMenu popupRecordMenu(this); - QAction* action = new QAction("Duplicate record", &popupRecordMenu); + QAction* action = new QAction(duplicateText, &popupRecordMenu); // Set shortcut for documentation purposes (the actual functional shortcut is not set here) action->setShortcut(QKeySequence(tr("Ctrl+\""))); popupRecordMenu.addAction(action); connect(action, &QAction::triggered, [&]() { - duplicateRecord(row); + for (QModelIndex index : rowList) { + duplicateRecord(index.row()); + } }); QAction* deleteRecordAction = new QAction(ui->buttonDeleteRecord->text(), &popupRecordMenu); From b99edacc9febc324ee361b3e2fc70a6bc1cf43af Mon Sep 17 00:00:00 2001 From: mgrojo Date: Sat, 6 Jan 2018 21:53:15 +0100 Subject: [PATCH 26/46] Automatic completion of SQL keywords in upper case Added a new setting for completing the SQL keywords in upper case (default being true). Scintilla setAutoCompletionCaseSensitivity is set to false. Otherwise the completion is only done in lowercase when both case versions are added, or if only upper case version is added, writing lower case letters does not use the upper case version of the word in the completion list. This change doesn't have apparently any downside, since SQL is actually case insensitive. Consequently the list of keywords is only added in one of the two letter case versions, depending on the new setting value. The new preference check-box is only enabled when the auto-complete check-box is checked. See issues #1238 and #1287. --- src/ExtendedScintilla.cpp | 2 +- src/PreferencesDialog.cpp | 3 + src/PreferencesDialog.ui | 116 +++++++++++++++++++++++++------------- src/Settings.cpp | 4 ++ src/SqlUiLexer.cpp | 8 ++- 5 files changed, 90 insertions(+), 43 deletions(-) diff --git a/src/ExtendedScintilla.cpp b/src/ExtendedScintilla.cpp index 5e9e3aa1..266bddaf 100644 --- a/src/ExtendedScintilla.cpp +++ b/src/ExtendedScintilla.cpp @@ -111,7 +111,7 @@ void ExtendedScintilla::reloadSettings() if(Settings::getValue("editor", "auto_completion").toBool()) { setAutoCompletionThreshold(3); - setAutoCompletionCaseSensitivity(true); + setAutoCompletionCaseSensitivity(false); setAutoCompletionShowSingle(true); setAutoCompletionSource(QsciScintilla::AcsAPIs); } else { diff --git a/src/PreferencesDialog.cpp b/src/PreferencesDialog.cpp index a80edacf..003fcf03 100644 --- a/src/PreferencesDialog.cpp +++ b/src/PreferencesDialog.cpp @@ -166,6 +166,8 @@ void PreferencesDialog::loadSettings() ui->spinTabSize->setValue(Settings::getValue("editor", "tabsize").toInt()); ui->spinLogFontSize->setValue(Settings::getValue("log", "fontsize").toInt()); ui->checkAutoCompletion->setChecked(Settings::getValue("editor", "auto_completion").toBool()); + ui->checkCompleteUpper->setEnabled(Settings::getValue("editor", "auto_completion").toBool()); + ui->checkCompleteUpper->setChecked(Settings::getValue("editor", "upper_keywords").toBool()); ui->checkErrorIndicators->setChecked(Settings::getValue("editor", "error_indicators").toBool()); ui->checkHorizontalTiling->setChecked(Settings::getValue("editor", "horizontal_tiling").toBool()); @@ -216,6 +218,7 @@ void PreferencesDialog::saveSettings() Settings::setValue("editor", "tabsize", ui->spinTabSize->value()); Settings::setValue("log", "fontsize", ui->spinLogFontSize->value()); Settings::setValue("editor", "auto_completion", ui->checkAutoCompletion->isChecked()); + Settings::setValue("editor", "upper_keywords", ui->checkCompleteUpper->isChecked()); Settings::setValue("editor", "error_indicators", ui->checkErrorIndicators->isChecked()); Settings::setValue("editor", "horizontal_tiling", ui->checkHorizontalTiling->isChecked()); diff --git a/src/PreferencesDialog.ui b/src/PreferencesDialog.ui index bb2519f6..8f9f7736 100644 --- a/src/PreferencesDialog.ui +++ b/src/PreferencesDialog.ui @@ -960,46 +960,6 @@ - - - - Error indicators - - - checkErrorIndicators - - - - - - - When set, the SQL code lines that caused errors during the last execution are highlighted and the results frame indicates the error in the background - - - enabled - - - - - - - Hori&zontal tiling - - - checkHorizontalTiling - - - - - - - If enabled the SQL code editor and the result table view are shown side by side instead of one over the other. - - - enabled - - - @@ -1017,6 +977,66 @@ + + + + Keywords in &UPPER CASE + + + checkCompleteUpper + + + + + + + When set, the SQL keywords are completed in UPPER CASE letters. + + + enabled + + + + + + + Error indicators + + + checkErrorIndicators + + + + + + + When set, the SQL code lines that caused errors during the last execution are highlighted and the results frame indicates the error in the background + + + enabled + + + + + + + Hori&zontal tiling + + + checkHorizontalTiling + + + + + + + If enabled the SQL code editor and the result table view are shown side by side instead of one over the other. + + + enabled + + + @@ -1573,6 +1593,22 @@ + + checkAutoCompletion + toggled(bool) + checkCompleteUpper + setEnabled(bool) + + + 474 + 464 + + + 474 + 492 + + + saveSettings() diff --git a/src/Settings.cpp b/src/Settings.cpp index 6d6efbaa..dd8c4c88 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -260,6 +260,10 @@ QVariant Settings::getDefaultValue(const QString& group, const QString& name) if(group == "editor" && name == "auto_completion") return true; + // editor/upper_keywords? + if(group == "editor" && name == "upper_keywords") + return true; + // editor/error_indicators? if(group == "editor" && name == "error_indicators") return true; diff --git a/src/SqlUiLexer.cpp b/src/SqlUiLexer.cpp index 9e7fc6c9..c0884540 100644 --- a/src/SqlUiLexer.cpp +++ b/src/SqlUiLexer.cpp @@ -1,5 +1,6 @@ #include "SqlUiLexer.h" #include "Qsci/qsciapis.h" +#include "Settings.h" SqlUiLexer::SqlUiLexer(QObject* parent) : QsciLexerSQL(parent) @@ -47,10 +48,13 @@ void SqlUiLexer::setupAutoCompletion() << "WHERE" << "WITH" << "WITHOUT" // Data types << "INT" << "INTEGER" << "REAL" << "TEXT" << "BLOB" << "NUMERIC" << "CHAR"; + bool upperKeywords = Settings::getValue("editor", "upper_keywords").toBool(); for(const QString& keyword : keywordPatterns) { - autocompleteApi->add(keyword + "?" + QString::number(ApiCompleterIconIdKeyword)); - autocompleteApi->add(keyword.toLower() + "?" + QString::number(ApiCompleterIconIdKeyword)); + if (upperKeywords) + autocompleteApi->add(keyword + "?" + QString::number(ApiCompleterIconIdKeyword)); + else + autocompleteApi->add(keyword.toLower() + "?" + QString::number(ApiCompleterIconIdKeyword)); } // Functions From b08960f5043d7649a601ed19f7d382e0ae031ea1 Mon Sep 17 00:00:00 2001 From: mgrojo Date: Sun, 7 Jan 2018 14:54:29 +0100 Subject: [PATCH 27/46] Plot: Respect the "Order by" clause when plotting The tables/queries sorted by X are drawn using QCPGraph as before. Since QCPGraph does automatically sort by X, we change to QCPCurve that requires a third data vector to reflect the order. We get that from the current row order. In the case of curves, only None and Line is supported as line style. Since the order is now important for the plot, it is automatically updated whenever the user sorts by another column in the browsed table. This addresses issue #821 and indirectly fixes the problem of incorrect point->row selection link when the table is not sorted by X, reported in issue #838. --- src/MainWindow.cpp | 2 ++ src/PlotDock.cpp | 75 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index d824ac20..d77281b5 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1635,6 +1635,8 @@ void MainWindow::browseTableHeaderClicked(int logicalindex) // select the first item in the column so the header is bold // we might try to select the last selected item ui->dataTable->setCurrentIndex(ui->dataTable->currentIndex().sibling(0, logicalindex)); + + plotDock->updatePlot(m_browseTableModel, &browseTableSettings[currentlyBrowsedTableName()]); } void MainWindow::resizeEvent(QResizeEvent*) diff --git a/src/PlotDock.cpp b/src/PlotDock.cpp index 03e208cf..49959798 100644 --- a/src/PlotDock.cpp +++ b/src/PlotDock.cpp @@ -195,7 +195,7 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett QStringList yAxisLabels; // Clear graphs and axis labels - ui->plotWidget->clearGraphs(); + ui->plotWidget->clearPlottables(); ui->plotWidget->xAxis->setLabel(QString()); ui->plotWidget->yAxis->setLabel(QString()); @@ -231,17 +231,16 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett // leading 16 bit are column index uint itemdata = item->data(0, Qt::UserRole).toUInt(); int column = itemdata >> 16; - QCPGraph* graph = ui->plotWidget->addGraph(); - graph->setPen(QPen(item->backgroundColor(PlotColumnY))); - graph->setSelectable (QCP::stDataRange); + bool isSorted = true; // prepare the data vectors for qcustomplot // possible improvement might be a QVector subclass that directly // access the model data, to save memory, we are copying here - QVector xdata(model->rowCount()), ydata(model->rowCount()); + QVector xdata(model->rowCount()), ydata(model->rowCount()), tdata(model->rowCount()); for(int i = 0; i < model->rowCount(); ++i) { + tdata[i] = i; // convert x type axis if it's datetime if(xtype == QVariant::DateTime) { @@ -258,6 +257,9 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett xdata[i] = model->data(model->index(i, x)).toDouble(); } + if (i != 0) + isSorted &= (xdata[i-1] <= xdata[i]); + // Get the y value for this point. If the selected column is -1, i.e. the row number, just use the current row number from the loop // instead of retrieving some value from the model. QVariant pointdata; @@ -271,14 +273,38 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett else ydata[i] = pointdata.toDouble(); } - - // set some graph styles - graph->setData(xdata, ydata); - graph->setLineStyle((QCPGraph::LineStyle) ui->comboLineType->currentIndex()); // WARN: ssDot is removed int shapeIdx = ui->comboPointShape->currentIndex(); if (shapeIdx > 0) shapeIdx += 1; - graph->setScatterStyle(QCPScatterStyle((QCPScatterStyle::ScatterShape)shapeIdx, 5)); + QCPScatterStyle scatterStyle = QCPScatterStyle(static_cast(shapeIdx), 5); + + QCPAbstractPlottable* plottable; + // When it is already sorted by x, we draw a graph. + // When it is not sorted by x, we draw a curve, so the order selected by the user in the table or in the query is + // respected. In this case the line will have loops and only None and Line is supported as line style. + // TODO: how to make the user aware of this without disturbing. + if (isSorted) { + QCPGraph* graph = ui->plotWidget->addGraph(); + plottable = graph; + graph->setData(xdata, ydata, /*alreadySorted*/ true); + // set some graph styles not supported by the abstract plottable + graph->setLineStyle((QCPGraph::LineStyle) ui->comboLineType->currentIndex()); + graph->setScatterStyle(scatterStyle); + + } else { + QCPCurve* curve = new QCPCurve(ui->plotWidget->xAxis, ui->plotWidget->yAxis); + plottable = curve; + curve->setData(tdata, xdata, ydata, /*alreadySorted*/ true); + // set some curve styles not supported by the abstract plottable + if (ui->comboLineType->currentIndex() == QCPCurve::lsNone) + curve->setLineStyle(QCPCurve::lsNone); + else + curve->setLineStyle(QCPCurve::lsLine); + curve->setScatterStyle(scatterStyle); + } + + plottable->setPen(QPen(item->backgroundColor(PlotColumnY))); + plottable->setSelectable (QCP::stDataRange); // gather Y label column names if(column == RowNumId) @@ -497,14 +523,28 @@ void PlotDock::on_comboLineType_currentIndexChanged(int index) { Q_ASSERT(index >= QCPGraph::lsNone && index <= QCPGraph::lsImpulse); + + bool hasCurves = (ui->plotWidget->plottableCount() > ui->plotWidget->graphCount()); QCPGraph::LineStyle lineStyle = (QCPGraph::LineStyle) index; + if (lineStyle > QCPGraph::lsLine && hasCurves) { + QMessageBox::warning(this, qApp->applicationName(), + tr("There are curves in this plot and the selected line style can only be applied to graphs sorted by X. " + "Either sort the table or query by X to remove curves or select one of the styles supported by curves: " + "None or Line.")); + return; + } for (int i = 0, ie = ui->plotWidget->graphCount(); i < ie; ++i) { QCPGraph * graph = ui->plotWidget->graph(i); if (graph) graph->setLineStyle(lineStyle); } - ui->plotWidget->replot(); + // We have changed the style only for graphs, but not for curves. + // If there are any in the plot, we have to update it completely in order to apply the new style + if (hasCurves) + updatePlot(m_currentPlotModel, m_currentTableSettings, false); + else + ui->plotWidget->replot(); // Save settings for this table if(m_currentTableSettings) @@ -525,6 +565,8 @@ void PlotDock::on_comboPointShape_currentIndexChanged(int index) if (index > 0) index += 1; Q_ASSERT(index >= QCPScatterStyle::ssNone && index < QCPScatterStyle::ssPixmap); + + bool hasCurves = (ui->plotWidget->plottableCount() > ui->plotWidget->graphCount()); QCPScatterStyle::ScatterShape shape = (QCPScatterStyle::ScatterShape) index; for (int i = 0, ie = ui->plotWidget->graphCount(); i < ie; ++i) { @@ -532,7 +574,12 @@ void PlotDock::on_comboPointShape_currentIndexChanged(int index) if (graph) graph->setScatterStyle(QCPScatterStyle(shape, 5)); } - ui->plotWidget->replot(); + // We have changed the style only for graphs, but not for curves. + // If there are any in the plot, we have to update it completely in order to apply the new style + if (hasCurves) + updatePlot(m_currentPlotModel, m_currentTableSettings, false); + else + ui->plotWidget->replot(); // Save settings for this table if(m_currentTableSettings) @@ -601,9 +648,9 @@ void PlotDock::fetchAllData() void PlotDock::selectionChanged() { - for (QCPGraph* graph : ui->plotWidget->selectedGraphs()) { + for (QCPAbstractPlottable* plottable : ui->plotWidget->selectedPlottables()) { - for (QCPDataRange dataRange : graph->selection().dataRanges()) { + for (QCPDataRange dataRange : plottable->selection().dataRanges()) { int index = dataRange.begin(); if (dataRange.length() != 0) { From ce2b33ab6d838aaed21a575f622af1c588569ae4 Mon Sep 17 00:00:00 2001 From: mgrojo Date: Mon, 8 Jan 2018 23:30:55 +0100 Subject: [PATCH 28/46] Use the correct function for checking the count of plotted items Now that the plot may contain graphs and curves, the plottableCount() function must be used instead of graphCount() for checking whether the plot has any plotted items. This had the effect reported in issue #821 of breaking the enable check for the fetch-all button. --- src/PlotDock.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PlotDock.cpp b/src/PlotDock.cpp index 49959798..1d760e36 100644 --- a/src/PlotDock.cpp +++ b/src/PlotDock.cpp @@ -326,7 +326,7 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett ui->plotWidget->replot(); // Warn user if not all data has been fetched and hint about the button for loading all the data - if (ui->plotWidget->graphCount() > 0 && model->canFetchMore()) { + if (ui->plotWidget->plottableCount() > 0 && model->canFetchMore()) { ui->buttonLoadAllData->setEnabled(true); ui->buttonLoadAllData->setStyleSheet("QToolButton {color: white; background-color: rgb(255, 102, 102)}"); ui->buttonLoadAllData->setToolTip(tr("Load all data and redraw plot.\n" From b2fbf450121a395fc83c8a331c5742f446adfa6e Mon Sep 17 00:00:00 2001 From: mgrojo Date: Thu, 11 Jan 2018 21:07:06 +0100 Subject: [PATCH 29/46] Plot: detect dates with time, pure dates and times data types Three different date/time data types are detected and appropriately displayed in the axis: - DateTime: "yyyy-MM-dd\nhh:mm:ss" - Date: "yyyy-MM-dd" - Time: "hh:mm:ss" --- src/PlotDock.cpp | 53 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/src/PlotDock.cpp b/src/PlotDock.cpp index 1d760e36..80393d13 100644 --- a/src/PlotDock.cpp +++ b/src/PlotDock.cpp @@ -209,17 +209,33 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett int xtype = xitemdata & (uint)0xFF; // check if we have a x axis with datetime data - if(xtype == QVariant::DateTime) - { + switch (xtype) { + case QVariant::Date: { QSharedPointer ticker(new QCPAxisTickerDateTime); ticker->setDateTimeFormat("yyyy-MM-dd"); ui->plotWidget->xAxis->setTicker(ticker); - } else { + break; + } + case QVariant::DateTime: { + QSharedPointer ticker(new QCPAxisTickerDateTime); + ticker->setDateTimeFormat("yyyy-MM-dd\nhh:mm:ss"); + ui->plotWidget->xAxis->setTicker(ticker); + break; + } + case QVariant::Time: { + QSharedPointer ticker(new QCPAxisTickerDateTime); + ticker->setDateTimeFormat("hh:mm:ss"); + ticker->setDateTimeSpec(Qt::UTC); + ui->plotWidget->xAxis->setTicker(ticker); + break; + } + default: { QSharedPointer ticker(new QCPAxisTickerFixed); ticker->setTickStepStrategy(QCPAxisTicker::tssReadability); ticker->setScaleStrategy(QCPAxisTickerFixed::ssMultiples); ui->plotWidget->xAxis->setTicker(ticker); } + } // add graph for each selected y axis for(int i = 0; i < ui->treePlotColumns->topLevelItemCount(); ++i) @@ -242,12 +258,21 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett { tdata[i] = i; // convert x type axis if it's datetime - if(xtype == QVariant::DateTime) - { + switch (xtype) { + case QVariant::DateTime: + case QVariant::Date: { QString s = model->data(model->index(i, x)).toString(); QDateTime d = QDateTime::fromString(s, Qt::ISODate); xdata[i] = d.toMSecsSinceEpoch() / 1000.0; - } else { + break; + } + case QVariant::Time: { + QString s = model->data(model->index(i, x)).toString(); + QTime t = QTime::fromString(s); + xdata[i] = t.msecsSinceStartOfDay() / 1000.0; + break; + } + default: { // Get the x value for this point. If the selected column is -1, i.e. the row number, just use the current row number from the loop // instead of retrieving some value from the model. if(x == RowNumId) @@ -256,6 +281,7 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett else xdata[i] = model->data(model->index(i, x)).toDouble(); } + } if (i != 0) isSorted &= (xdata[i-1] <= xdata[i]); @@ -605,9 +631,18 @@ QVariant::Type PlotDock::guessDataType(SqliteTableModel* model, int column) type = QVariant::Double; } else { QString s = model->data(model->index(i, column)).toString(); - QDate d = QDate::fromString(s, Qt::ISODate); - if(d.isValid()) - type = QVariant::DateTime; + QDateTime dt = QDateTime::fromString(s, Qt::ISODate); + QTime t = QTime::fromString(s); + if (dt.isValid()) + // Since the way to discriminate dates with times and pure dates is that the time part is 0, we must take into account + // that some DateTimes could have "00:00:00" as time part and still the entire column has time information, so a single + // final Date should not set the type to Date if it has already been guessed as DateTime. + if (type != QVariant::DateTime && dt.time().msecsSinceStartOfDay() == 0) + type = QVariant::Date; + else + type = QVariant::DateTime; + else if (t.isValid()) + type = QVariant::Time; else type = QVariant::String; } From 512b6941140417377751b719de20e4a46ce03a9e Mon Sep 17 00:00:00 2001 From: Manuel Date: Sat, 13 Jan 2018 16:27:19 +0100 Subject: [PATCH 30/46] Fix crash when Tab is pressed at the last cell of a view's grid (#1289) The crash is avoided if the table model is not editable, since insertRows checks that. A table model for a DB view should be not editable, so this fix avoids the crash and possibly other possible misbehaviours. --- src/sqlitetablemodel.cpp | 2 +- src/sqlitetablemodel.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index e606e158..f28854b6 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -938,7 +938,7 @@ void SqliteTableModel::setPseudoPk(const QString& pseudoPk) bool SqliteTableModel::isEditable() const { - return !m_sTable.isEmpty(); + return !m_sTable.isEmpty() && m_db.getObjectByName(m_sTable)->type() == sqlb::Object::Types::Table; } void SqliteTableModel::waitForFetchingFinished() diff --git a/src/sqlitetablemodel.h b/src/sqlitetablemodel.h index d61ba0fd..4170013c 100644 --- a/src/sqlitetablemodel.h +++ b/src/sqlitetablemodel.h @@ -64,7 +64,7 @@ public: // This returns true if the model is set up for editing. The model is able to operate in more or less two different modes, table browsing // and query browsing. We only support editing data for the table browsing mode and not for the query mode. This function returns true if - // the model is currently editable, i.e. it's running in table mode. + // the model is currently editable, i.e. it's running in table mode and it isn't a view. bool isEditable() const; // Helper function for removing all comments from a SQL query From 58ab9cc1b7763434cb02fce577e7d7c610d1b05d Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Sat, 13 Jan 2018 16:29:17 +0100 Subject: [PATCH 31/46] Check for pseudo-PKs when determining whether the model is editable --- src/sqlitedb.cpp | 2 ++ src/sqlitetablemodel.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sqlitedb.cpp b/src/sqlitedb.cpp index c54dc573..30d6d887 100644 --- a/src/sqlitedb.cpp +++ b/src/sqlitedb.cpp @@ -948,6 +948,8 @@ QString DBBrowserDB::addRecord(const sqlb::ObjectIdentifier& tablename) if (!isOpen()) return QString(); sqlb::TablePtr table = getObjectByName(tablename).dynamicCast(); + if(!table) + return QString(); // For tables without rowid we have to set the primary key by ourselves. We do so by querying for the largest value in the PK column // and adding one to it. diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index f28854b6..d413ef26 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -938,7 +938,7 @@ void SqliteTableModel::setPseudoPk(const QString& pseudoPk) bool SqliteTableModel::isEditable() const { - return !m_sTable.isEmpty() && m_db.getObjectByName(m_sTable)->type() == sqlb::Object::Types::Table; + return !m_sTable.isEmpty() && (m_db.getObjectByName(m_sTable)->type() == sqlb::Object::Types::Table || !m_pseudoPk.isEmpty()); } void SqliteTableModel::waitForFetchingFinished() From a993c19853f4addd8eaffbee0509db388f8cfd49 Mon Sep 17 00:00:00 2001 From: Giuseppe Zizza Date: Sat, 13 Jan 2018 16:43:24 +0100 Subject: [PATCH 32/46] File Extension management interface (#659) [NEW] Add new interface and functionality to manage database file extension. (Implements feature request #659) --- CMakeLists.txt | 3 + src/FileDialog.cpp | 5 ++ src/FileDialog.h | 5 +- src/FileExtensionManager.cpp | 113 ++++++++++++++++++++++++++ src/FileExtensionManager.h | 30 +++++++ src/FileExtensionManager.ui | 153 +++++++++++++++++++++++++++++++++++ src/PreferencesDialog.cpp | 16 +++- src/PreferencesDialog.h | 4 + src/PreferencesDialog.ui | 14 ++++ src/Settings.cpp | 3 + src/src.pro | 5 +- 11 files changed, 345 insertions(+), 6 deletions(-) create mode 100644 src/FileExtensionManager.cpp create mode 100644 src/FileExtensionManager.h create mode 100644 src/FileExtensionManager.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index 488cfc92..62f4ed8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,6 +123,7 @@ set(SQLB_MOC_HDR src/RemotePushDialog.h src/FindReplaceDialog.h src/ExtendedScintilla.h + src/FileExtensionManager.h ) set(SQLB_SRC @@ -164,6 +165,7 @@ set(SQLB_SRC src/RemotePushDialog.cpp src/FindReplaceDialog.cpp src/ExtendedScintilla.cpp + src/FileExtensionManager.cpp src/Data.cpp ) @@ -185,6 +187,7 @@ set(SQLB_FORMS src/RemoteDock.ui src/RemotePushDialog.ui src/FindReplaceDialog.ui + src/FileExtensionManager.ui ) set(SQLB_RESOURCES diff --git a/src/FileDialog.cpp b/src/FileDialog.cpp index e7d4b188..a8cb3d91 100644 --- a/src/FileDialog.cpp +++ b/src/FileDialog.cpp @@ -70,3 +70,8 @@ void FileDialog::setFileDialogPath(const QString& new_path) break; // Do nothing } } + +QString FileDialog::getSqlDatabaseFileFilter() +{ + return Settings::getValue("General", "DBFileExtensions").toString() + ";;" + QObject::tr("All files (*)"); //Always add "All files (*)" to the available filters +} diff --git a/src/FileDialog.h b/src/FileDialog.h index 7b19bcaf..d23a8dd1 100644 --- a/src/FileDialog.h +++ b/src/FileDialog.h @@ -20,10 +20,7 @@ public: static QString getExistingDirectory(QWidget* parent = nullptr, const QString& caption = QString(), Options options = 0); - static QString getSqlDatabaseFileFilter() - { - return QObject::tr("SQLite database files (*.db *.sqlite *.sqlite3 *.db3);;All files (*)"); - } + static QString getSqlDatabaseFileFilter(); private: static QString getFileDialogPath(); diff --git a/src/FileExtensionManager.cpp b/src/FileExtensionManager.cpp new file mode 100644 index 00000000..3cb91fa9 --- /dev/null +++ b/src/FileExtensionManager.cpp @@ -0,0 +1,113 @@ +#include "FileExtensionManager.h" +#include "ui_FileExtensionManager.h" + +FileExtensionManager::FileExtensionManager(QStringList init, QWidget *parent) : + QDialog(parent), + ui(new Ui::FileExtensionManager) +{ + ui->setupUi(this); + + int i = 0; + foreach(QString itemString, init) + { + QString description = itemString.left(itemString.indexOf('(')).trimmed(); + QString extension = itemString; + extension = extension.remove (0, itemString.indexOf('(')+1).remove(')').simplified().trimmed(); + if ( extension.compare("*") != 0 ) //We exclude "All files" from the table + { + QTableWidgetItem *newItemDescription = new QTableWidgetItem(description); + QTableWidgetItem *newItemExtension = new QTableWidgetItem(extension); + ui->tableExtensions->insertRow(i); + ui->tableExtensions->setItem(i, 0, newItemDescription); + ui->tableExtensions->setItem(i, 1, newItemExtension); + i++; + } + } + + connect(ui->buttonAdd, SIGNAL(clicked(bool)), this, SLOT(addItem())); + connect(ui->buttonRemove, SIGNAL(clicked(bool)), this, SLOT(removeItem())); + + connect(ui->buttonDown, SIGNAL(clicked(bool)), this, SLOT(downItem())); + connect(ui->buttonUp, SIGNAL(clicked(bool)), this, SLOT(upItem())); +} + +FileExtensionManager::~FileExtensionManager() +{ + delete ui; +} + +void FileExtensionManager::addItem() +{ + int i = ui->tableExtensions->rowCount(); + ui->tableExtensions->insertRow(i); + QTableWidgetItem *newItemDescription = new QTableWidgetItem(tr("Description")); + QTableWidgetItem *newItemExtension = new QTableWidgetItem(tr("*.extension")); + ui->tableExtensions->setItem(i, 0, newItemDescription); + ui->tableExtensions->setItem(i, 1, newItemExtension); +} + +void FileExtensionManager::removeItem() +{ + QList selectedRows; + foreach (QTableWidgetItem *item, ui->tableExtensions->selectedItems()) + { + if (selectedRows.contains(item->row()) == false) + { + selectedRows.append(item->row()); + } + } + + qSort(selectedRows); + + for (int i = selectedRows.size()-1; i >= 0; --i) + { + ui->tableExtensions->removeRow(selectedRows[i]); + } +} + +void FileExtensionManager::upItem() +{ + if (ui->tableExtensions->selectedItems().isEmpty()) return; + + int selectedRow = ui->tableExtensions->selectedItems().first()->row(); + if(selectedRow == 0) + return; + + QTableWidgetItem *t1, *t2; + t1 = ui->tableExtensions->takeItem(selectedRow, 0); + t2 = ui->tableExtensions->takeItem(selectedRow, 1); + ui->tableExtensions->removeRow(selectedRow); + ui->tableExtensions->insertRow(selectedRow-1); + ui->tableExtensions->setItem(selectedRow-1, 0, t1); + ui->tableExtensions->setItem(selectedRow-1, 1, t2); + ui->tableExtensions->selectRow(selectedRow-1); +} + +void FileExtensionManager::downItem() +{ + if (ui->tableExtensions->selectedItems().isEmpty()) return; + + int selectedRow = ui->tableExtensions->selectedItems().first()->row(); + if(selectedRow == ui->tableExtensions->rowCount() - 1) + return; + + QTableWidgetItem *t1, *t2; + t1 = ui->tableExtensions->takeItem(selectedRow, 0); + t2 = ui->tableExtensions->takeItem(selectedRow, 1); + ui->tableExtensions->removeRow(selectedRow); + ui->tableExtensions->insertRow(selectedRow+1); + ui->tableExtensions->setItem(selectedRow+1, 0, t1); + ui->tableExtensions->setItem(selectedRow+1, 1, t2); + ui->tableExtensions->selectRow(selectedRow+1); +} + +QStringList FileExtensionManager::getDBFileExtensions() +{ + QStringList result; + for (int i = 0; i < ui->tableExtensions->rowCount(); ++i) + { + result.append(QString("%1 (%2)").arg(ui->tableExtensions->item(i, 0)->text()).arg(ui->tableExtensions->item(i, 1)->text())); + } + return result; +} + diff --git a/src/FileExtensionManager.h b/src/FileExtensionManager.h new file mode 100644 index 00000000..3b6e9597 --- /dev/null +++ b/src/FileExtensionManager.h @@ -0,0 +1,30 @@ +#ifndef FILEEXTENSIONMANAGER_H +#define FILEEXTENSIONMANAGER_H + +#include + +namespace Ui { +class FileExtensionManager; +} + +class FileExtensionManager : public QDialog +{ + Q_OBJECT + +public: + explicit FileExtensionManager(QStringList init, QWidget *parent = nullptr); + ~FileExtensionManager(); + + QStringList getDBFileExtensions(); + +private: + Ui::FileExtensionManager *ui; + +public slots: + void addItem(); + void removeItem(); + void upItem(); + void downItem(); +}; + +#endif // FILEEXTENSIONMANAGER_H diff --git a/src/FileExtensionManager.ui b/src/FileExtensionManager.ui new file mode 100644 index 00000000..681e8a25 --- /dev/null +++ b/src/FileExtensionManager.ui @@ -0,0 +1,153 @@ + + + FileExtensionManager + + + + 0 + 0 + 578 + 463 + + + + Dialog + + + + + + + + &Up + + + + :/icons/up:/icons/up + + + + + + + &Down + + + + :/icons/down:/icons/down + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Add + + + + :/icons/field_add:/icons/field_add + + + + + + + &Remove + + + + :/icons/field_delete:/icons/field_delete + + + + + + + + + QAbstractScrollArea::AdjustToContents + + + true + + + 100 + + + 100 + + + + Description + + + + + Extensions + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + FileExtensionManager + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + FileExtensionManager + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/PreferencesDialog.cpp b/src/PreferencesDialog.cpp index 003fcf03..41b9c59d 100644 --- a/src/PreferencesDialog.cpp +++ b/src/PreferencesDialog.cpp @@ -5,6 +5,7 @@ #include "Application.h" #include "MainWindow.h" #include "RemoteDatabase.h" +#include "FileExtensionManager.h" #include #include @@ -14,7 +15,8 @@ PreferencesDialog::PreferencesDialog(QWidget* parent) : QDialog(parent), - ui(new Ui::PreferencesDialog) + ui(new Ui::PreferencesDialog), + m_dbFileExtensions(FileDialog::getSqlDatabaseFileFilter().split(";;")) { ui->setupUi(this); ui->treeSyntaxHighlighting->setColumnHidden(0, true); @@ -280,6 +282,8 @@ void PreferencesDialog::saveSettings() Settings::setValue("General", "language", newLanguage); Settings::setValue("General", "toolbarStyle", ui->toolbarStyleComboBox->currentIndex()); + Settings::setValue("General", "DBFileExtensions", m_dbFileExtensions.join(";;") ); + accept(); } @@ -564,3 +568,13 @@ void PreferencesDialog::updatePreviewFont() ui->txtBlob->setFont(textFont); } } + +void PreferencesDialog::on_buttonManageFileExtension_clicked() +{ + FileExtensionManager *manager = new FileExtensionManager(m_dbFileExtensions, this); + + if(manager->exec() == QDialog::Accepted) + { + m_dbFileExtensions = manager->getDBFileExtensions(); + } +} diff --git a/src/PreferencesDialog.h b/src/PreferencesDialog.h index 94b32eb2..eba8cbc3 100644 --- a/src/PreferencesDialog.h +++ b/src/PreferencesDialog.h @@ -36,9 +36,13 @@ private slots: void chooseRemoteCloneDirectory(); void updatePreviewFont(); + void on_buttonManageFileExtension_clicked(); + private: Ui::PreferencesDialog *ui; + QStringList m_dbFileExtensions; + void fillLanguageBox(); void loadColorSetting(QFrame *frame, const QString &name); void setColorSetting(QFrame *frame, const QColor &color); diff --git a/src/PreferencesDialog.ui b/src/PreferencesDialog.ui index 8f9f7736..1029df6f 100644 --- a/src/PreferencesDialog.ui +++ b/src/PreferencesDialog.ui @@ -216,6 +216,20 @@ + + + + DB file extensions + + + + + + + Manage + + + diff --git a/src/Settings.cpp b/src/Settings.cpp index dd8c4c88..796ba3ac 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -152,6 +152,9 @@ QVariant Settings::getDefaultValue(const QString& group, const QString& name) if(group == "General" && name == "toolbarStyle") return static_cast(Qt::ToolButtonTextBesideIcon); + if(group == "General" && name == "DBFileExtensions") + return QObject::tr("SQLite database files (*.db *.sqlite *.sqlite3 *.db3)"); + // checkversion group? if(group == "checkversion") { diff --git a/src/src.pro b/src/src.pro index bc059f39..44f6e33b 100644 --- a/src/src.pro +++ b/src/src.pro @@ -62,6 +62,7 @@ HEADERS += \ jsontextedit.h \ FindReplaceDialog.h \ ExtendedScintilla.h \ + FileExtensionManager.h \ Data.h SOURCES += \ @@ -102,6 +103,7 @@ SOURCES += \ jsontextedit.cpp \ FindReplaceDialog.cpp \ ExtendedScintilla.cpp \ + FileExtensionManager.cpp \ Data.cpp RESOURCES += icons/icons.qrc \ @@ -126,7 +128,8 @@ FORMS += \ PlotDock.ui \ RemoteDock.ui \ RemotePushDialog.ui \ - FindReplaceDialog.ui + FindReplaceDialog.ui \ + FileExtensionManager.ui TRANSLATIONS += \ translations/sqlb_ar_SA.ts \ From efb32857d6336116867d1214ba756ae2f744742e Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Sat, 13 Jan 2018 21:04:07 +0000 Subject: [PATCH 33/46] Remove AppImage generation, as it's causing 100% build failure --- .travis.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8baa5a3c..d7443360 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,20 +51,6 @@ script: - cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr -DENABLE_TESTING=ON -Dsqlcipher=1 .. - make - ctest -V - - # AppImage generation - - sudo apt-get -y install checkinstall - - sudo checkinstall --pkgname=app --pkgversion="1" --pkgrelease="1" --backup=no --fstrans=no --default --deldoc - - mkdir appdir ; cd appdir - - dpkg -x ../app_1-1_amd64.deb . ; find . - - cp ./usr/share/applications/sqlitebrowser.desktop . - - cp ./usr/share/icons/hicolor/256x256/apps/sqlitebrowser.png . - - cd .. - - wget -c "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage" - - chmod a+x linuxdeployqt*.AppImage - - unset QTDIR; unset QT_PLUGIN_PATH ; unset LD_LIBRARY_PATH - - ./linuxdeployqt*.AppImage ./appdir/usr/bin/sqlitebrowser -bundle-non-qt-libs - - ./linuxdeployqt*.AppImage ./appdir/usr/bin/sqlitebrowser -appimage - - curl --upload-file ./DB*.AppImage https://transfer.sh/sqlitebrowser-git.$(git rev-parse --short HEAD)-x86_64.AppImage notifications: email: @@ -75,4 +61,3 @@ notifications: - mgrojo@gmail.com on_success: never on_failure: always - From 39a54605004951f100619dd9d0096accff9dcd61 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Mon, 15 Jan 2018 22:38:10 +0100 Subject: [PATCH 34/46] Fix crashes in Edit Index dialog when database has no tables Fix some crashes in the Edit Index dialog which happen if you try to add a new index when there are no tables in the database yet. See issue #1293. --- src/EditIndexDialog.cpp | 9 ++++++++- src/sqlitetypes.h | 8 ++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/EditIndexDialog.cpp b/src/EditIndexDialog.cpp index b3ea640d..3b7b0e98 100644 --- a/src/EditIndexDialog.cpp +++ b/src/EditIndexDialog.cpp @@ -112,7 +112,10 @@ void EditIndexDialog::tableChanged(const QString& new_table, bool initialLoad) void EditIndexDialog::updateColumnLists() { // Fill the table column list - sqlb::FieldInfoList tableFields = pdb.getObjectByName(sqlb::ObjectIdentifier(ui->comboTableName->currentData())).dynamicCast()->fieldInformation(); + sqlb::TablePtr table = pdb.getObjectByName(sqlb::ObjectIdentifier(ui->comboTableName->currentData())).dynamicCast(); + if(!table) + return; + sqlb::FieldInfoList tableFields = table->fieldInformation(); ui->tableTableColumns->setRowCount(tableFields.size()); int tableRows = 0; for(int i=0;ieditIndexName->text().isEmpty()) valid = false; + // Check if a table is selected (this is especially important in the case where there are no tables in the database yet). + if(ui->comboTableName->currentText().isNull()) + valid = false; + // Check if index has any columns if(index.columns().size() == 0) valid = false; diff --git a/src/sqlitetypes.h b/src/sqlitetypes.h index 632727c3..2e86b28f 100644 --- a/src/sqlitetypes.h +++ b/src/sqlitetypes.h @@ -40,8 +40,12 @@ public: explicit ObjectIdentifier(QVariant variant) { QStringList str = variant.toStringList(); - m_schema = str.first(); - m_name = str.last(); + if(str.size()) + { + m_schema = str.first(); + if(str.size() >= 2) + m_name = str.last(); + } } bool operator==(const ObjectIdentifier& rhs) const From 012ad9217a9bb71264bb6c5ad3a1da847610effc Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Mon, 15 Jan 2018 23:10:23 +0100 Subject: [PATCH 35/46] Fix drag & drop of tables onto the structure view When dragging and dropping a table from one instance of the application to the other, the tree structure representing the database was broken. We would show the 'Browsables' and 'All' nodes at the top level instead of the child nodes of the 'All' node. This happened because after dropping a table, we would reload the database structure and rebuild the tree structure but didn't notify the tree view in the main window about the update. This is fixed by this commit, so the main window's widgets are always notified about the new tree structure. See issue #1288. --- src/DbStructureModel.cpp | 3 ++- src/DbStructureModel.h | 8 ++++++-- src/MainWindow.cpp | 13 +++++++------ src/MainWindow.h | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/DbStructureModel.cpp b/src/DbStructureModel.cpp index 73fdf1cc..e9623548 100644 --- a/src/DbStructureModel.cpp +++ b/src/DbStructureModel.cpp @@ -138,6 +138,7 @@ void DbStructureModel::reloadData() if(!m_db.isOpen()) { endResetModel(); + emit structureUpdated(); return; } @@ -179,6 +180,7 @@ void DbStructureModel::reloadData() // Refresh the view endResetModel(); + emit structureUpdated(); } QStringList DbStructureModel::mimeTypes() const @@ -263,7 +265,6 @@ bool DbStructureModel::dropMimeData(const QMimeData* data, Qt::DropAction action if(m_db.executeMultiSQL(d, true, true)) { m_db.updateSchema(); - reloadData(); return true; } else { QMessageBox::warning(nullptr, QApplication::applicationName(), m_db.lastError()); diff --git a/src/DbStructureModel.h b/src/DbStructureModel.h index 83d3446c..43b0d5cd 100644 --- a/src/DbStructureModel.h +++ b/src/DbStructureModel.h @@ -15,8 +15,6 @@ public: explicit DbStructureModel(DBBrowserDB& db, QObject* parent = nullptr); ~DbStructureModel(); - void reloadData(); - QVariant data(const QModelIndex& index, int role) const; Qt::ItemFlags flags(const QModelIndex& index) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; @@ -38,6 +36,12 @@ public: ColumnSchema, }; +public slots: + void reloadData(); + +signals: + void structureUpdated(); + private: DBBrowserDB& m_db; QTreeWidgetItem* rootItem; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 8f22c924..fdc0dcd8 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -92,7 +92,6 @@ void MainWindow::init() // Connect SQL logging and database state setting to main window connect(&db, SIGNAL(dbChanged(bool)), this, SLOT(dbState(bool))); connect(&db, SIGNAL(sqlExecuted(QString, int)), this, SLOT(logSql(QString,int))); - connect(&db, SIGNAL(structureUpdated()), this, SLOT(populateStructure())); connect(&db, &DBBrowserDB::requestCollation, this, &MainWindow::requestCollation); // Set the validator for the goto line edit @@ -107,6 +106,11 @@ void MainWindow::init() // Set up DB structure tab dbStructureModel = new DbStructureModel(db, this); + connect(&db, &DBBrowserDB::structureUpdated, [this]() { + QString old_table = ui->comboBrowseTable->currentText(); + dbStructureModel->reloadData(); + populateStructure(old_table); + }); ui->dbTreeWidget->setModel(dbStructureModel); ui->dbTreeWidget->setColumnWidth(DbStructureModel::ColumnName, 300); ui->dbTreeWidget->setColumnHidden(DbStructureModel::ColumnObjectType, true); @@ -396,12 +400,9 @@ void MainWindow::fileNew() } } -void MainWindow::populateStructure() +void MainWindow::populateStructure(const QString& old_table) { - QString old_table = ui->comboBrowseTable->currentText(); - // Refresh the structure tab - dbStructureModel->reloadData(); ui->dbTreeWidget->setRootIndex(dbStructureModel->index(1, 0)); // Show the 'All' part of the db structure ui->dbTreeWidget->expandToDepth(0); ui->treeSchemaDock->setRootIndex(dbStructureModel->index(1, 0)); // Show the 'All' part of the db structure @@ -1970,7 +1971,7 @@ void MainWindow::reloadSettings() loadExtensionsFromSettings(); // Refresh view - populateStructure(); + dbStructureModel->reloadData(); populateTable(); // Hide or show the remote dock as needed diff --git a/src/MainWindow.h b/src/MainWindow.h index dbbdc75b..242617af 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -213,7 +213,7 @@ public slots: void refresh(); void jumpToRow(const sqlb::ObjectIdentifier& table, QString column, const QByteArray& value); void switchToBrowseDataTab(QString tableToBrowse = QString()); - void populateStructure(); + void populateStructure(const QString& old_table = QString()); private slots: void createTreeContextMenu(const QPoint & qPoint); From 404f48c5a9da7d6fc9c5e983bc0411374aff53b7 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Mon, 15 Jan 2018 23:51:30 +0100 Subject: [PATCH 36/46] Fix possible 'database locked' error when detaching a database Revert the savepoint in the Execute SQL whenever possible. We need to do this after each statement because there are some rare cases where the next statement might be affected by what is only a temporary and unnecessary savepoint. For example in this case: ATTACH 'xxx' AS 'db2' SELECT * FROM db2.xy; -- Savepoint created here DETACH db2; -- Savepoint makes this statement fail See issue #1249. --- src/MainWindow.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index fdc0dcd8..95150fd0 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1216,6 +1216,17 @@ void MainWindow::executeQuery() execution_start_index = execution_end_index; + // Revert to save point now if it wasn't needed. We need to do this here because there are some rare cases where the next statement might + // be affected by what is only a temporary and unnecessary savepoint. For example in this case: + // ATTACH 'xxx' AS 'db2' + // SELECT * FROM db2.xy; -- Savepoint created here + // DETACH db2; -- Savepoint makes this statement fail + if(!modified && !wasdirty && savepoint_created) + { + db.revertToSavepoint(); // better rollback, if the logic is not enough we can tune it. + savepoint_created = false; + } + // Process events to keep the UI responsive qApp->processEvents(); } @@ -1225,9 +1236,6 @@ void MainWindow::executeQuery() connect(sqlWidget->getTableResult(), &ExtendedTableWidget::activated, this, &MainWindow::dataTableSelectionChanged); connect(sqlWidget->getTableResult(), SIGNAL(doubleClicked(QModelIndex)), this, SLOT(doubleClickTable(QModelIndex))); - if(!modified && !wasdirty && savepoint_created) - db.revertToSavepoint(); // better rollback, if the logic is not enough we can tune it. - // If the DB structure was changed by some command in this SQL script, update our schema representations if(structure_updated) db.updateSchema(); From 33801d528520ae0776976e9e2884a29135d4af93 Mon Sep 17 00:00:00 2001 From: mgrojo Date: Tue, 16 Jan 2018 00:37:28 +0100 Subject: [PATCH 37/46] Synchronize PlotDock with 'Execute SQL' table. Continuation of PR #1271 Make use of signals to connect the selection in plot to the associated table widget. Every time that the plot is updated from the Main Window the table widget associated to the table or query is connected to the plot and the previous widget is disconnected. This allows the selection of the correct table widget. Line selection methods moved to the Extended Table Widget to be used as slots for this connection. The destroyed signal is also connected for resetting the plot. This fixes a crash that already existed before this PR, when closing a SQL tab while the plot is still associated to the table results model. --- src/ExtendedTableWidget.cpp | 37 +++++++++++++++++ src/ExtendedTableWidget.h | 2 + src/MainWindow.cpp | 81 ++++++++----------------------------- src/MainWindow.h | 6 +-- src/PlotDock.cpp | 5 +++ src/PlotDock.h | 1 + 6 files changed, 65 insertions(+), 67 deletions(-) diff --git a/src/ExtendedTableWidget.cpp b/src/ExtendedTableWidget.cpp index 9587d496..1ed5663b 100644 --- a/src/ExtendedTableWidget.cpp +++ b/src/ExtendedTableWidget.cpp @@ -657,3 +657,40 @@ void ExtendedTableWidget::dropEvent(QDropEvent* event) model()->dropMimeData(event->mimeData(), Qt::CopyAction, index.row(), index.column(), QModelIndex()); event->acceptProposedAction(); } + +void ExtendedTableWidget::selectTableLine(int lineToSelect) +{ + SqliteTableModel* m = qobject_cast(model()); + + // Are there even that many lines? + if(lineToSelect >= m->totalRowCount()) + return; + + QApplication::setOverrideCursor( Qt::WaitCursor ); + // Make sure this line has already been fetched + while(lineToSelect >= m->rowCount() && m->canFetchMore()) + m->fetchMore(); + + // Select it + clearSelection(); + selectRow(lineToSelect); + scrollTo(currentIndex(), QAbstractItemView::PositionAtTop); + QApplication::restoreOverrideCursor(); +} + +void ExtendedTableWidget::selectTableLines(int firstLine, int count) +{ + SqliteTableModel* m = qobject_cast(model()); + + int lastLine = firstLine+count-1; + // Are there even that many lines? + if(lastLine >= m->totalRowCount()) + return; + + selectTableLine(firstLine); + + QModelIndex topLeft = m->index(firstLine, 0); + QModelIndex bottomRight = m->index(lastLine, m->columnCount()-1); + + selectionModel()->select(QItemSelection(topLeft, bottomRight), QItemSelectionModel::Select | QItemSelectionModel::Rows); +} diff --git a/src/ExtendedTableWidget.h b/src/ExtendedTableWidget.h index 24256c77..fe4fd751 100644 --- a/src/ExtendedTableWidget.h +++ b/src/ExtendedTableWidget.h @@ -40,6 +40,8 @@ public: public slots: void reloadSettings(); + void selectTableLine(int lineToSelect); + void selectTableLines(int firstLine, int count); signals: void foreignKeyClicked(const sqlb::ObjectIdentifier& table, const QString& column, const QByteArray& value); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 95150fd0..cb7aea68 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -102,7 +102,7 @@ void MainWindow::init() connect(m_browseTableModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(dataTableSelectionChanged(QModelIndex))); // Select in table the rows correspoding to the selected points in plot - connect(plotDock, SIGNAL(pointsSelected(int,int)), this, SLOT(selectTableLines(int,int))); + connect(plotDock, SIGNAL(pointsSelected(int,int)), ui->dataTable, SLOT(selectTableLines(int,int))); // Set up DB structure tab dbStructureModel = new DbStructureModel(db, this); @@ -531,7 +531,7 @@ void MainWindow::populateTable() m_browseTableModel->setEncoding(defaultBrowseTableEncoding); // Plot - plotDock->updatePlot(m_browseTableModel, &browseTableSettings[tablename]); + attachPlot(ui->dataTable, m_browseTableModel, &browseTableSettings[tablename]); // The filters can be left empty as they are } else { @@ -588,7 +588,7 @@ void MainWindow::populateTable() m_browseTableModel->setEncoding(storedData.encoding); // Plot - plotDock->updatePlot(m_browseTableModel, &browseTableSettings[tablename], true, false); + attachPlot(ui->dataTable, m_browseTableModel, &browseTableSettings[tablename], false); } // Show/hide menu options depending on whether this is a table or a view @@ -630,8 +630,8 @@ bool MainWindow::fileClose() // Reset the recordset label inside the Browse tab now setRecordsetLabel(); - // Reset the plot dock model - plotDock->updatePlot(nullptr); + // Reset the plot dock model and connection + attachPlot(nullptr, nullptr); activateFields(false); @@ -695,69 +695,22 @@ void MainWindow::deleteRecord() } } -void MainWindow::selectCurrentTabTableLines(int firstLine, int lastLine) +void MainWindow::attachPlot(ExtendedTableWidget* tableWidget, SqliteTableModel* model, BrowseDataTableSettings* settings, bool keepOrResetSelection) { - if(lastLine >= m_currentTabTableModel->totalRowCount()) - return; + plotDock->updatePlot(model, settings, true, keepOrResetSelection); + // Disconnect previous connection + disconnect(plotDock, SIGNAL(pointsSelected(int,int)), nullptr, nullptr); + if(tableWidget) { + // Connect plot selection to the current table results widget. + connect(plotDock, SIGNAL(pointsSelected(int,int)), tableWidget, SLOT(selectTableLines(int,int))); + connect(tableWidget, SIGNAL(destroyed()), plotDock, SLOT(resetPlot())); - SqlExecutionArea *qw = (SqlExecutionArea*)ui->tabSqlAreas->currentWidget(); - ExtendedTableWidget *tw = qw->getTableResult(); - - if(firstLine >= m_currentTabTableModel->totalRowCount()) - return; - - QApplication::setOverrideCursor( Qt::WaitCursor ); - // Make sure this line has already been fetched - while(firstLine >= m_currentTabTableModel->rowCount() && m_currentTabTableModel->canFetchMore()) - m_currentTabTableModel->fetchMore(); - - // Select it - tw->clearSelection(); - tw->selectRow(firstLine); - tw->scrollTo(tw->currentIndex(), QAbstractItemView::PositionAtTop); - QApplication::restoreOverrideCursor(); - - QModelIndex topLeft = m_currentTabTableModel->index(firstLine, 0); - QModelIndex bottomRight = m_currentTabTableModel->index(lastLine, m_currentTabTableModel->columnCount()-1); - - tw->selectionModel()->select(QItemSelection(topLeft, bottomRight), QItemSelectionModel::Select | QItemSelectionModel::Rows); + } } void MainWindow::selectTableLine(int lineToSelect) { - // Are there even that many lines? - if(lineToSelect >= m_browseTableModel->totalRowCount()) - return; - - QApplication::setOverrideCursor( Qt::WaitCursor ); - // Make sure this line has already been fetched - while(lineToSelect >= m_browseTableModel->rowCount() && m_browseTableModel->canFetchMore()) - m_browseTableModel->fetchMore(); - - // Select it - ui->dataTable->clearSelection(); - ui->dataTable->selectRow(lineToSelect); - ui->dataTable->scrollTo(ui->dataTable->currentIndex(), QAbstractItemView::PositionAtTop); - QApplication::restoreOverrideCursor(); -} - -void MainWindow::selectTableLines(int firstLine, int count) -{ - int lastLine = firstLine+count-1; - if(ui->mainTab->currentIndex() == BrowseTab) { - // Are there even that many lines? - if(lastLine >= m_browseTableModel->totalRowCount()) - return; - - selectTableLine(firstLine); - - QModelIndex topLeft = ui->dataTable->model()->index(firstLine, 0); - QModelIndex bottomRight = ui->dataTable->model()->index(lastLine, ui->dataTable->model()->columnCount()-1); - - ui->dataTable->selectionModel()->select(QItemSelection(topLeft, bottomRight), QItemSelectionModel::Select | QItemSelectionModel::Rows); - }else if(ui->mainTab->currentIndex() == ExecuteTab) { - selectCurrentTabTableLines(firstLine, lastLine); - } + ui->dataTable->selectTableLine(lineToSelect); } void MainWindow::navigatePrevious() @@ -1231,7 +1184,7 @@ void MainWindow::executeQuery() qApp->processEvents(); } sqlWidget->finishExecution(statusMessage, ok); - plotDock->updatePlot(sqlWidget->getModel()); + attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel()); connect(sqlWidget->getTableResult(), &ExtendedTableWidget::activated, this, &MainWindow::dataTableSelectionChanged); connect(sqlWidget->getTableResult(), SIGNAL(doubleClicked(QModelIndex)), this, SLOT(doubleClickTable(QModelIndex))); @@ -1677,7 +1630,7 @@ void MainWindow::browseTableHeaderClicked(int logicalindex) // we might try to select the last selected item ui->dataTable->setCurrentIndex(ui->dataTable->currentIndex().sibling(0, logicalindex)); - plotDock->updatePlot(m_browseTableModel, &browseTableSettings[currentlyBrowsedTableName()]); + attachPlot(ui->dataTable, m_browseTableModel, &browseTableSettings[currentlyBrowsedTableName()]); } void MainWindow::resizeEvent(QResizeEvent*) diff --git a/src/MainWindow.h b/src/MainWindow.h index 242617af..be6e567b 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -18,6 +18,7 @@ class DbStructureModel; class RemoteDock; class RemoteDatabase; class FindReplaceDialog; +class ExtendedTableWidget; namespace Ui { class MainWindow; @@ -194,6 +195,8 @@ private: void loadExtensionsFromSettings(); void saveAsView(QString query); void duplicateRecord(int currentRow); + void selectTableLine(int lineToSelect); + void attachPlot(ExtendedTableWidget* tableWidget, SqliteTableModel* model, BrowseDataTableSettings* settings = nullptr, bool keepOrResetSelection = true); sqlb::ObjectIdentifier currentlyBrowsedTableName() const; @@ -224,9 +227,6 @@ private slots: bool fileClose(); void addRecord(); void deleteRecord(); - void selectTableLine(int lineToSelect); - void selectCurrentTabTableLines(int firstLine, int lastLine); - void selectTableLines(int firstLine, int count); void navigatePrevious(); void navigateNext(); void navigateBegin(); diff --git a/src/PlotDock.cpp b/src/PlotDock.cpp index 80393d13..d1e2daf2 100644 --- a/src/PlotDock.cpp +++ b/src/PlotDock.cpp @@ -365,6 +365,11 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett } } +void PlotDock::resetPlot() +{ + updatePlot(nullptr); +} + void PlotDock::on_treePlotColumns_itemChanged(QTreeWidgetItem* changeitem, int column) { // disable change updates, or we get unwanted redrawing and weird behavior diff --git a/src/PlotDock.h b/src/PlotDock.h index 05b1b490..5b296012 100644 --- a/src/PlotDock.h +++ b/src/PlotDock.h @@ -66,6 +66,7 @@ public: public slots: void updatePlot(SqliteTableModel* model, BrowseDataTableSettings* settings = nullptr, bool update = true, bool keepOrResetSelection = true); void fetchAllData(); + void resetPlot(); signals: void pointsSelected(int firstIndex, int count); From 31f1eafdd8616aa762dabf2018c53e75f0f17ae3 Mon Sep 17 00:00:00 2001 From: Iulian Onofrei <6d0847b9@opayq.com> Date: Wed, 17 Jan 2018 18:11:40 +0200 Subject: [PATCH 38/46] Fix macOS name in GitHub issue template --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index bf7c1413..27c0111c 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -26,7 +26,7 @@ below with an "x", then click the "Submit new issue" button at the bottom - [ ] Windows: ( _version:_ ___ ) - [ ] Linux: ( _distro:_ ___ ) -- [ ] Mac OS: ( _version:_ ___ ) +- [ ] macOS: ( _version:_ ___ ) - [ ] Other: ___ #### I'm using DB4S version: From d1261146b394cd02bbd02f487cac1bb31bcbda6c Mon Sep 17 00:00:00 2001 From: mgrojo Date: Thu, 18 Jan 2018 20:32:07 +0100 Subject: [PATCH 39/46] Preserve format in Copy Create Statement The original Create statement formatting is preserved when the menu option "Copy Create statement" is selected, regardless of the setting that removes the line breaks in the schema view. The Edit Role is requested to the model for getting the original text data. See issue #1300 --- src/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index cb7aea68..5d9c0483 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -2391,7 +2391,7 @@ void MainWindow::copyCurrentCreateStatement() return; // Get the CREATE statement from the Schema column - QString stmt = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), 3)).toString(); + QString stmt = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), 3), Qt::EditRole).toString(); // Copy the statement to the global application clipboard QApplication::clipboard()->setText(stmt); From 6a8c2e7a57c6301fbbef8824d88cbe2c30d09e7d Mon Sep 17 00:00:00 2001 From: mgrojo Date: Thu, 18 Jan 2018 20:35:08 +0100 Subject: [PATCH 40/46] Revert "Preserve format in Copy Create Statement" This reverts commit d1261146b394cd02bbd02f487cac1bb31bcbda6c. --- src/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 5d9c0483..cb7aea68 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -2391,7 +2391,7 @@ void MainWindow::copyCurrentCreateStatement() return; // Get the CREATE statement from the Schema column - QString stmt = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), 3), Qt::EditRole).toString(); + QString stmt = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), 3)).toString(); // Copy the statement to the global application clipboard QApplication::clipboard()->setText(stmt); From be66535c20bb38274963b2203189fb518eb404c6 Mon Sep 17 00:00:00 2001 From: mgrojo Date: Thu, 18 Jan 2018 20:54:11 +0100 Subject: [PATCH 41/46] Revert "Merge branch 'master' of https://github.com/sqlitebrowser/sqlitebrowser" This reverts commit e9385e6f8b15db7a9e2550db792c5d2357c47453, reversing changes made to d1261146b394cd02bbd02f487cac1bb31bcbda6c. --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 27c0111c..bf7c1413 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -26,7 +26,7 @@ below with an "x", then click the "Submit new issue" button at the bottom - [ ] Windows: ( _version:_ ___ ) - [ ] Linux: ( _distro:_ ___ ) -- [ ] macOS: ( _version:_ ___ ) +- [ ] Mac OS: ( _version:_ ___ ) - [ ] Other: ___ #### I'm using DB4S version: From 7d1ddbd7177338aae94fe288f3154af71c180203 Mon Sep 17 00:00:00 2001 From: mgrojo Date: Thu, 18 Jan 2018 20:59:02 +0100 Subject: [PATCH 42/46] Preserve format in Copy Create Statement The original Create statement formatting is preserved when the menu option "Copy Create statement" is selected, regardless of the setting that removes the line breaks in the schema view. The Edit Role is requested to the model for getting the original text data. See issue #1300 --- src/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index cb7aea68..5d9c0483 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -2391,7 +2391,7 @@ void MainWindow::copyCurrentCreateStatement() return; // Get the CREATE statement from the Schema column - QString stmt = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), 3)).toString(); + QString stmt = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), 3), Qt::EditRole).toString(); // Copy the statement to the global application clipboard QApplication::clipboard()->setText(stmt); From 4b3780e22aa26e632b8b385c4b134c7374e273fc Mon Sep 17 00:00:00 2001 From: mgrojo Date: Thu, 18 Jan 2018 22:55:59 +0100 Subject: [PATCH 43/46] Auto-completion for column names in the SQL editor Isolated column names are added to the list of possible auto-completions, so they can be completed without having to enter first the table followed by dot. "Table.field" completion is still supported for completing only for fields inside that context. See issue #1242 --- src/SqlUiLexer.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/SqlUiLexer.cpp b/src/SqlUiLexer.cpp index c0884540..5a9b6e41 100644 --- a/src/SqlUiLexer.cpp +++ b/src/SqlUiLexer.cpp @@ -138,10 +138,14 @@ void SqlUiLexer::setTableNames(const TablesAndColumnsMap& tables) setupAutoCompletion(); for(auto it=tables.constBegin();it!=tables.constEnd();++it) { - for(const QString& field : it.value()) + for(const QString& field : it.value()) { + // Completion for table.field autocompleteApi->add(it.key() + "?" + QString::number(SqlUiLexer::ApiCompleterIconIdTable) + "." + field + "?" + QString::number(SqlUiLexer::ApiCompleterIconIdColumn)); + // Completion for isolated field + autocompleteApi->add(field + "?" + QString::number(SqlUiLexer::ApiCompleterIconIdColumn)); + } // Store the table name list in order to highlight them in a different colour listTables.append(it.key()); } From 8c85ff9157bfb154772eae46ec30787f2fff2c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ChangJoo=20Park=28=EB=B0=95=EC=B0=BD=EC=A3=BC=29?= Date: Mon, 22 Jan 2018 21:40:40 +0900 Subject: [PATCH 44/46] fix typo. database in Korean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit database is *데이터베이스* --- src/translations/sqlb_ko_KR.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/translations/sqlb_ko_KR.ts b/src/translations/sqlb_ko_KR.ts index d984b169..e6350789 100644 --- a/src/translations/sqlb_ko_KR.ts +++ b/src/translations/sqlb_ko_KR.ts @@ -1877,7 +1877,7 @@ Do you want to insert it anyway? Edit Database &Cell - 데티어베이스 셀 수정하기(&C) + 데이터베이스 셀 수정하기(&C) From 1110b87d95108f3f01430f7e05aec4c9e7ebc628 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Fri, 26 Jan 2018 14:44:13 +0100 Subject: [PATCH 45/46] Fix structure view after changing settings This is fixing a follow-up issue of 012ad9217a9bb71264bb6c5ad3a1da847610effc. See issue #1288. --- src/MainWindow.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 5d9c0483..fcf46479 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1933,6 +1933,7 @@ void MainWindow::reloadSettings() // Refresh view dbStructureModel->reloadData(); + populateStructure(); populateTable(); // Hide or show the remote dock as needed From 74f19d7bc86bd125fa3f6a27671a355dde391a57 Mon Sep 17 00:00:00 2001 From: phydroxide <31145228+phydroxide@users.noreply.github.com> Date: Fri, 26 Jan 2018 06:29:38 -0800 Subject: [PATCH 46/46] Update BUILDING.md (#1307) * Update BUILDING.md Package list for Yum incomplete * Update BUILDING.md My first git clone or contribution ever so bear with me --- BUILDING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/BUILDING.md b/BUILDING.md index 9df79344..0fb6283a 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -87,6 +87,11 @@ $ sudo make install This should complete without errors, and `sqlitebrowser` should now be launch-able from the command line. +**Note 3** +On CentOS if cmake complains about missing Qt5 Libraries or sqllite: +``` sudo yum install ant-antlr antlr-C++ cmake gcc-c++ git qt-dev qwt-qt5-devel sqllite-devel qt qt5widgets qt5-qtbase-devel qt5-linguist sqlite-devel``` + + ### MacOS X The application can be compiled to a single executable binary file, similar to