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 - diff --git a/BUILDING.md b/BUILDING.md index dccf83ad..0fb6283a 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 ``` @@ -86,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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b919e89..b790b95f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,7 @@ set(SQLB_HDR src/grammar/sqlite3TokenTypes.hpp src/grammar/Sqlite3Lexer.hpp src/grammar/Sqlite3Parser.hpp + src/Data.h ) set(SQLB_MOC_HDR @@ -123,6 +124,7 @@ set(SQLB_MOC_HDR src/RemotePushDialog.h src/FindReplaceDialog.h src/ExtendedScintilla.h + src/FileExtensionManager.h ) set(SQLB_SRC @@ -164,6 +166,8 @@ set(SQLB_SRC src/RemotePushDialog.cpp src/FindReplaceDialog.cpp src/ExtendedScintilla.cpp + src/FileExtensionManager.cpp + src/Data.cpp ) set(SQLB_FORMS @@ -184,6 +188,7 @@ set(SQLB_FORMS src/RemoteDock.ui src/RemotePushDialog.ui src/FindReplaceDialog.ui + src/FileExtensionManager.ui ) set(SQLB_RESOURCES 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/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) diff --git a/src/Data.cpp b/src/Data.cpp new file mode 100644 index 00000000..e3a5e491 --- /dev/null +++ b/src/Data.cpp @@ -0,0 +1,59 @@ +#include "Data.h" + +#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) +{ + // 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); + + // Convert to Unicode if necessary + if(!encoding.isEmpty()) + data = QTextCodec::codecForName(encoding.toUtf8())->toUnicode(data).toUtf8(); + + // Perform check + return QString(data).toUtf8() == data; +} + +bool startsWithBom(const QByteArray& data) +{ + 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.startsWith(bom3)) + { + QByteArray bom = data.left(3); + data.remove(0, 3); + return bom; + } else if(data.startsWith(bom2a) || data.startsWith(bom2b)) { + QByteArray bom = data.left(2); + data.remove(0, 2); + return bom; + } else if(data.startsWith(bom4a) || data.startsWith(bom4b)) { + QByteArray bom = data.left(4); + data.remove(0, 4); + return bom; + } else { + return QByteArray(); + } +} diff --git a/src/Data.h b/src/Data.h new file mode 100644 index 00000000..2cbf9ddc --- /dev/null +++ b/src/Data.h @@ -0,0 +1,20 @@ +#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); + +// 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/DbStructureModel.cpp b/src/DbStructureModel.cpp index 9bb1d2f4..e9623548 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; @@ -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 @@ -190,32 +192,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), Qt::EditRole).toString()); + insertStatement.chop(1); + insertStatement += ");\n"; + sqlData.append(insertStatement); + } } } } @@ -224,7 +237,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; } @@ -246,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/EditDialog.cpp b/src/EditDialog.cpp index cad770fe..2def845d 100644 --- a/src/EditDialog.cpp +++ b/src/EditDialog.cpp @@ -5,6 +5,7 @@ #include "src/qhexedit.h" #include "docktextedit.h" #include "FileDialog.h" +#include "Data.h" #include #include @@ -94,6 +95,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); @@ -152,25 +156,19 @@ void EditDialog::loadData(const QByteArray& data) case Text: case JSON: - // Can be stored in any widget, except the ImageViewer switch (editMode) { case TextEditor: setDataInBuffer(data, TextBuffer); break; - case JsonEditor: case XmlEditor: - setDataInBuffer(data, SciBuffer); break; - case HexEditor: - setDataInBuffer(data, HexBuffer); break; - case ImageViewer: // The image viewer cannot hold data nor display text. @@ -359,6 +357,8 @@ void EditDialog::setNull() ui->editorImage->clear(); hexEdit->setData(QByteArray()); sciEdit->clear(); + dataType = Null; + removedBom.clear(); // The text editors don't know the difference between an empty string // and a NULL, so we need to record NULL outside of that @@ -404,10 +404,10 @@ void EditDialog::accept() case TextBuffer: { 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 SciBuffer: @@ -494,16 +494,19 @@ void EditDialog::setDataInBuffer(const QByteArray& data, DataSources source) // 3) Enable the widget. switch (dataSource) { case TextBuffer: + { + // Load the text into the text editor, remove BOM first if there is one + QByteArray dataWithoutBom = data; + removedBom = removeBom(dataWithoutBom); - textData = QString::fromUtf8(data.constData(), data.size()); - ui->editorText->setPlainText(QString::fromUtf8(data.constData(), data.size())); + textData = QString::fromUtf8(dataWithoutBom.constData(), dataWithoutBom.size()); + ui->editorText->setPlainText(textData); // Select all of the text by default (this is useful for simple text data that we usually edit as a whole) ui->editorText->selectAll(); ui->editorText->setEnabled(true); - break; - + } case SciBuffer: switch (sciEdit->language()) { case DockTextEdit::JSON: @@ -587,7 +590,7 @@ void EditDialog::editModeChanged(int newMode) case HexEditor: // Switching to the hex editor // Convert the text widget buffer for the hex widget // The hex widget buffer is now the main data source - setDataInBuffer(ui->editorText->toPlainText().toUtf8(), HexBuffer); + setDataInBuffer(removedBom + ui->editorText->toPlainText().toUtf8(), HexBuffer); break; case ImageViewer: @@ -699,8 +702,8 @@ int EditDialog::checkDataType(const QByteArray& data) return imageFormat == "svg" ? SVG : 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(cellData); if (!jsonDoc.isNull()) return JSON; diff --git a/src/EditDialog.h b/src/EditDialog.h index cf91c8e9..40d62794 100644 --- a/src/EditDialog.h +++ b/src/EditDialog.h @@ -55,6 +55,7 @@ private: int dataType; bool isReadOnly; bool mustIndentAndCompact; + QByteArray removedBom; enum DataSources { TextBuffer, 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/ExtendedTableWidget.cpp b/src/ExtendedTableWidget.cpp index 8cdbb8cb..1ed5663b 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"))); @@ -187,8 +233,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 +357,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 @@ -614,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 ae86ac4f..fe4fd751 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 @@ -25,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); @@ -55,6 +72,7 @@ protected: FilterTableHeader* m_tableHeader; QMenu* m_contextMenu; + ExtendedTableWidgetEditorDelegate* m_editorDelegate; }; #endif 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/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.cpp b/src/MainWindow.cpp index 43cb0ced..fcf46479 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 @@ -103,10 +102,15 @@ 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); + 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 @@ -530,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 { @@ -587,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 @@ -629,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); @@ -694,37 +695,22 @@ void MainWindow::deleteRecord() } } -void MainWindow::selectTableLine(int lineToSelect) +void MainWindow::attachPlot(ExtendedTableWidget* tableWidget, SqliteTableModel* model, BrowseDataTableSettings* settings, bool keepOrResetSelection) { - // Are there even that many lines? - if(lineToSelect >= m_browseTableModel->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())); - 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) +void MainWindow::selectTableLine(int lineToSelect) { - int lastLine = firstLine+count-1; - // 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); + ui->dataTable->selectTableLine(lineToSelect); } void MainWindow::navigatePrevious() @@ -976,6 +962,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 +1144,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; @@ -1178,18 +1169,26 @@ 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(); } 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))); - 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(); @@ -1630,6 +1629,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)); + + attachPlot(ui->dataTable, m_browseTableModel, &browseTableSettings[currentlyBrowsedTableName()]); } void MainWindow::resizeEvent(QResizeEvent*) @@ -1931,6 +1932,7 @@ void MainWindow::reloadSettings() loadExtensionsFromSettings(); // Refresh view + dbStructureModel->reloadData(); populateStructure(); populateTable(); @@ -2390,7 +2392,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); @@ -2442,14 +2444,39 @@ void MainWindow::showRecordPopupMenu(const QPoint& pos) if (row == -1) return; + // 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); + popupRecordMenu.addAction(deleteRecordAction); + + connect(deleteRecordAction, &QAction::triggered, [&]() { + deleteRecord(); }); popupRecordMenu.exec(ui->dataTable->verticalHeader()->mapToGlobal(pos)); @@ -2542,7 +2569,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/MainWindow.h b/src/MainWindow.h index f3cf28ea..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; @@ -143,6 +144,8 @@ private: UpdateStatement, DeleteStatement, CreateStatement, + AttachStatement, + DetachStatement, OtherStatement, }; @@ -192,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; @@ -211,7 +216,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); @@ -222,8 +227,6 @@ private slots: bool fileClose(); void addRecord(); void deleteRecord(); - void selectTableLine( int lineToSelect ); - void selectTableLines(int firstLine, int count); void navigatePrevious(); void navigateNext(); void navigateBegin(); diff --git a/src/MainWindow.ui b/src/MainWindow.ui index af6b40c2..6211e390 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 @@ -396,7 +401,7 @@ - <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 @@ -431,7 +436,7 @@ - <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 @@ -451,7 +456,7 @@ - <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 @@ -471,7 +476,7 @@ - <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 @@ -491,7 +496,7 @@ - <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 @@ -511,7 +516,7 @@ - <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 @@ -531,7 +536,7 @@ - <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 @@ -578,7 +583,7 @@ - <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 @@ -601,7 +606,7 @@ - <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 @@ -628,7 +633,7 @@ - <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 @@ -648,7 +653,7 @@ - <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 @@ -671,7 +676,7 @@ - <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 @@ -691,7 +696,7 @@ - <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 @@ -711,7 +716,7 @@ - <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 @@ -743,7 +748,7 @@ - <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 @@ -775,7 +780,7 @@ - <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 @@ -795,7 +800,7 @@ - <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 @@ -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 diff --git a/src/PlotDock.cpp b/src/PlotDock.cpp index 03e208cf..d1e2daf2 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()); @@ -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) @@ -231,24 +247,32 @@ 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) - { + 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) @@ -257,6 +281,10 @@ 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]); // 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. @@ -271,14 +299,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) @@ -300,7 +352,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" @@ -313,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 @@ -497,14 +554,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 +596,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 +605,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) @@ -558,9 +636,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; } @@ -601,9 +688,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) { 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); diff --git a/src/PreferencesDialog.cpp b/src/PreferencesDialog.cpp index a80edacf..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); @@ -166,6 +168,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 +220,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()); @@ -277,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(); } @@ -561,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 bb2519f6..1029df6f 100644 --- a/src/PreferencesDialog.ui +++ b/src/PreferencesDialog.ui @@ -216,6 +216,20 @@ + + + + DB file extensions + + + + + + + Manage + + + @@ -960,46 +974,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 +991,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 +1607,22 @@ + + checkAutoCompletion + toggled(bool) + checkCompleteUpper + setEnabled(bool) + + + 474 + 464 + + + 474 + 492 + + + saveSettings() diff --git a/src/Settings.cpp b/src/Settings.cpp index 0079a17a..6aaeb35d 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") { @@ -260,6 +263,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/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 diff --git a/src/SqlUiLexer.cpp b/src/SqlUiLexer.cpp index 9e7fc6c9..5a9b6e41 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 @@ -134,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()); } diff --git a/src/sqlitedb.cpp b/src/sqlitedb.cpp index e30c56c8..30d6d887 100644 --- a/src/sqlitedb.cpp +++ b/src/sqlitedb.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -949,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 dbef08eb..d413ef26 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -2,8 +2,8 @@ #include "sqlitedb.h" #include "sqlite.h" #include "Settings.h" +#include "Data.h" -#include #include #include #include @@ -685,8 +685,8 @@ void SqliteTableModel::removeCommentsFromQuery(QString& query) { * (?: | )* # 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 */ + QRegExp rxSQL("^((?:(?:[^'-]|-(?!-))*|(?:'[^']*'))*)(--[^\\r\\n]*)([\\r\\n]*)(.*)$"); // set up regex to find end-of-line comment QString result; @@ -871,10 +871,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 = 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 @@ -941,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 || !m_pseudoPk.isEmpty()); } 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 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..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 @@ -265,9 +269,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 diff --git a/src/src.pro b/src/src.pro index 0165b78a..b6b6f934 100644 --- a/src/src.pro +++ b/src/src.pro @@ -61,7 +61,9 @@ HEADERS += \ RemotePushDialog.h \ docktextedit.h \ FindReplaceDialog.h \ - ExtendedScintilla.h + ExtendedScintilla.h \ + FileExtensionManager.h \ + Data.h SOURCES += \ sqlitedb.cpp \ @@ -100,7 +102,9 @@ SOURCES += \ RemotePushDialog.cpp \ docktextedit.cpp \ FindReplaceDialog.cpp \ - ExtendedScintilla.cpp + ExtendedScintilla.cpp \ + FileExtensionManager.cpp \ + Data.cpp RESOURCES += icons/icons.qrc \ translations/flags/flags.qrc \ @@ -124,7 +128,8 @@ FORMS += \ PlotDock.ui \ RemoteDock.ui \ RemotePushDialog.ui \ - FindReplaceDialog.ui + FindReplaceDialog.ui \ + FileExtensionManager.ui TRANSLATIONS += \ translations/sqlb_ar_SA.ts \ 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 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() 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)