From 4081debd71b2a225e90d563774e3cf2e893d28c1 Mon Sep 17 00:00:00 2001 From: mgrojo Date: Sun, 22 Sep 2019 21:22:34 +0200 Subject: [PATCH 1/8] Attach Database didn't escape filepath of selected file See issue #2002 --- src/sql/ObjectIdentifier.cpp | 5 +++++ src/sql/ObjectIdentifier.h | 3 +++ src/sqlitedb.cpp | 8 ++++++-- src/sqlitedb.h | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/sql/ObjectIdentifier.cpp b/src/sql/ObjectIdentifier.cpp index 8910b432..d94aa1c7 100644 --- a/src/sql/ObjectIdentifier.cpp +++ b/src/sql/ObjectIdentifier.cpp @@ -53,6 +53,11 @@ std::string escapeIdentifier(const std::string& id) } } +std::string escapeString(const std::string& literal) +{ + return '\'' + duplicate_char(literal, '\'') + '\''; +} + bool ObjectIdentifier::fromSerialised(const std::string& serialised) { auto pos_comma = serialised.find(","); diff --git a/src/sql/ObjectIdentifier.h b/src/sql/ObjectIdentifier.h index 380da00f..8d2f5282 100644 --- a/src/sql/ObjectIdentifier.h +++ b/src/sql/ObjectIdentifier.h @@ -20,6 +20,9 @@ char getIdentifierQuoteChar(); // Add quotes to an identifier std::string escapeIdentifier(const std::string& id); +// Add SQL quotes to a string literal and escape any single quote character +std::string escapeString(const std::string& literal); + // Object identifier consisting of schema name and object name class ObjectIdentifier { diff --git a/src/sqlitedb.cpp b/src/sqlitedb.cpp index 0c47515e..cb118fdc 100644 --- a/src/sqlitedb.cpp +++ b/src/sqlitedb.cpp @@ -45,6 +45,10 @@ QString escapeIdentifier(const QString& id) { return QString::fromStdString(escapeIdentifier(id.toStdString())); } +QString escapeString(const QString& literal) +{ + return QString::fromStdString(escapeString(literal.toStdString())); +} } // collation callbacks @@ -324,7 +328,7 @@ bool DBBrowserDB::attach(const QString& filePath, QString attach_as) } } - if(!executeSQL(QString("ATTACH '%1' AS %2 %3").arg(filePath).arg(sqlb::escapeIdentifier(attach_as)).arg(key), false)) + if(!executeSQL(QString("ATTACH %1 AS %2 %3").arg(sqlb::escapeString(filePath)).arg(sqlb::escapeIdentifier(attach_as)).arg(key), false)) { QMessageBox::warning(nullptr, qApp->applicationName(), lastErrorMessage); return false; @@ -334,7 +338,7 @@ bool DBBrowserDB::attach(const QString& filePath, QString attach_as) delete cipherSettings; #else // Attach database - if(!executeSQL(QString("ATTACH '%1' AS %2").arg(filePath).arg(sqlb::escapeIdentifier(attach_as)), false)) + if(!executeSQL(QString("ATTACH %1 AS %2").arg(sqlb::escapeString(filePath)).arg(sqlb::escapeIdentifier(attach_as)), false)) { QMessageBox::warning(nullptr, qApp->applicationName(), lastErrorMessage); return false; diff --git a/src/sqlitedb.h b/src/sqlitedb.h index d0f23ad7..bdaa0db5 100644 --- a/src/sqlitedb.h +++ b/src/sqlitedb.h @@ -33,6 +33,7 @@ int collCompare(void* pArg, int sizeA, const void* sA, int sizeB, const void* sB namespace sqlb { QString escapeIdentifier(const QString& id); +QString escapeString(const QString& literal); } /// represents a single SQLite database. except when noted otherwise, From 2d4c5ce0ba82065c71b47af196391bf20c2ba460 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Sun, 22 Sep 2019 20:43:07 +0200 Subject: [PATCH 2/8] Support inline preview of image data in cells This adds a new option in the Preferences dialog to enable a preview of images in BLOB cells directly in the grid view of the Browse Data tab. See issue #2000. --- src/Data.cpp | 16 ++++++++ src/Data.h | 3 ++ src/EditDialog.cpp | 10 ++--- src/PreferencesDialog.cpp | 2 + src/PreferencesDialog.ui | 80 ++++++++++++++++++++++++++------------- src/Settings.cpp | 2 + src/sqlitetablemodel.cpp | 10 +++++ 7 files changed, 90 insertions(+), 33 deletions(-) diff --git a/src/Data.cpp b/src/Data.cpp index ffda72e4..ba41efdc 100644 --- a/src/Data.cpp +++ b/src/Data.cpp @@ -1,6 +1,9 @@ #include "Data.h" +#include +#include #include + #include // Note that these aren't all possible BOMs. But they are probably the most common ones. @@ -88,6 +91,19 @@ QByteArray removeBom(QByteArray& data) } } +QString isImageData(const QByteArray& data) +{ + // Check if it's an image. First do a quick test by calling canRead() which only checks the first couple of bytes or so. Only if + // that returned true, do a more sophisticated test of the data. This way we get both, good performance and proper data checking. + QBuffer imageBuffer(const_cast(&data)); + QImageReader readerBuffer(&imageBuffer); + QString imageFormat = readerBuffer.format(); + if(readerBuffer.canRead() && !readerBuffer.read().isNull()) + return imageFormat; + else + return QString(); +} + QStringList toStringList(const QList& list) { QStringList strings; for (const QByteArray &item : list) { diff --git a/src/Data.h b/src/Data.h index a725d196..037076d3 100644 --- a/src/Data.h +++ b/src/Data.h @@ -21,6 +21,9 @@ bool startsWithBom(const QByteArray& data); // with a BOM an empty byte array is returned and the original data is not modified. QByteArray removeBom(QByteArray& data); +// Check if a byte array contains an image. Returns the name of the image format for images or a null string for non-image data. +QString isImageData(const QByteArray& data); + QStringList toStringList(const QList& list); QByteArray encodeString(const QByteArray& str, const QString& encoding); diff --git a/src/EditDialog.cpp b/src/EditDialog.cpp index f09540d8..11b052d4 100644 --- a/src/EditDialog.cpp +++ b/src/EditDialog.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -856,12 +855,9 @@ int EditDialog::checkDataType(const QByteArray& bArrdata) return Null; } - // Check if it's an image. First do a quick test by calling canRead() which only checks the first couple of bytes or so. Only if - // that returned true, do a more sophisticated test of the data. This way we get both, good performance and proper data checking. - QBuffer imageBuffer(&cellData); - QImageReader readerBuffer(&imageBuffer); - QString imageFormat = readerBuffer.format(); - if(readerBuffer.canRead() && !readerBuffer.read().isNull()) + // Check if it's an image + QString imageFormat = isImageData(cellData); + if(!imageFormat.isNull()) return imageFormat == "svg" ? SVG : Image; // Check if it's text only diff --git a/src/PreferencesDialog.cpp b/src/PreferencesDialog.cpp index 1ce272d0..18000b1f 100644 --- a/src/PreferencesDialog.cpp +++ b/src/PreferencesDialog.cpp @@ -109,6 +109,7 @@ void PreferencesDialog::loadSettings() ui->spinSymbolLimit->setValue(Settings::getValue("databrowser", "symbol_limit").toInt()); ui->spinCompleteThreshold->setValue(Settings::getValue("databrowser", "complete_threshold").toInt()); + ui->checkShowImagesInline->setChecked(Settings::getValue("databrowser", "image_preview").toBool()); ui->txtNull->setText(Settings::getValue("databrowser", "null_text").toString()); ui->txtBlob->setText(Settings::getValue("databrowser", "blob_text").toString()); ui->editFilterEscape->setText(Settings::getValue("databrowser", "filter_escape").toString()); @@ -217,6 +218,7 @@ void PreferencesDialog::saveSettings() Settings::setValue("databrowser", "font", ui->comboDataBrowserFont->currentText()); Settings::setValue("databrowser", "fontsize", ui->spinDataBrowserFontSize->value()); + Settings::setValue("databrowser", "image_preview", ui->checkShowImagesInline->isChecked()); saveColorSetting(ui->fr_null_fg, "null_fg"); saveColorSetting(ui->fr_null_bg, "null_bg"); saveColorSetting(ui->fr_reg_fg, "reg_fg"); diff --git a/src/PreferencesDialog.ui b/src/PreferencesDialog.ui index 089c639c..443929fc 100644 --- a/src/PreferencesDialog.ui +++ b/src/PreferencesDialog.ui @@ -821,6 +821,23 @@ Can be set to 0 for disabling completion. + + + + Show images in cell + + + checkShowImagesInline + + + + + + + Enable this option to show a preview of BLOBs containing image data in the cells. This can affect the performance of the data browser, however. + + + @@ -1866,10 +1883,12 @@ Can be set to 0 for disabling completion. + tabWidget comboDefaultLocation locationEdit setLocationButton languageComboBox + appStyleCombo toolbarStyleComboMain toolbarStyleComboStructure toolbarStyleComboBrowse @@ -1877,6 +1896,7 @@ Can be set to 0 for disabling completion. toolbarStyleComboEditCell checkUseRemotes checkUpdates + buttonManageFileExtension encodingComboBox foreignKeysCheckBox checkHideSchemaLinebreaks @@ -1886,6 +1906,9 @@ Can be set to 0 for disabling completion. editDatabaseDefaultSqlText comboDataBrowserFont spinDataBrowserFontSize + spinSymbolLimit + spinCompleteThreshold + checkShowImagesInline fr_null_fg fr_null_bg txtNull @@ -1902,20 +1925,25 @@ Can be set to 0 for disabling completion. spinEditorFontSize spinLogFontSize spinTabSize + wrapComboBox + quoteComboBox checkAutoCompletion + checkCompleteUpper checkErrorIndicators checkHorizontalTiling listExtensions buttonAddExtension buttonRemoveExtension checkRegexDisabled - editRemoteCloneDirectory - buttonRemoteBrowseCloneDirectory - tableCaCerts + checkAllowLoadExtension + tabCertificates tableClientCerts buttonClientCertAdd buttonClientCertRemove - tabWidget + buttonProxy + editRemoteCloneDirectory + buttonRemoteBrowseCloneDirectory + tableCaCerts @@ -1992,12 +2020,12 @@ Can be set to 0 for disabling completion. setVisible(bool) - 365 - 207 + 344 + 230 - 108 - 280 + 119 + 273 @@ -2008,8 +2036,8 @@ Can be set to 0 for disabling completion. setVisible(bool) - 365 - 207 + 344 + 230 365 @@ -2024,8 +2052,8 @@ Can be set to 0 for disabling completion. activateRemoteTab(bool) - 161 - 172 + 231 + 393 382 @@ -2040,8 +2068,8 @@ Can be set to 0 for disabling completion. addClientCertificate() - 578 - 315 + 722 + 110 596 @@ -2056,8 +2084,8 @@ Can be set to 0 for disabling completion. removeClientCertificate() - 578 - 353 + 722 + 139 597 @@ -2072,8 +2100,8 @@ Can be set to 0 for disabling completion. chooseRemoteCloneDirectory() - 567 - 54 + 732 + 538 595 @@ -2088,12 +2116,12 @@ Can be set to 0 for disabling completion. setEnabled(bool) - 474 - 464 + 642 + 450 - 474 - 492 + 642 + 480 @@ -2104,8 +2132,8 @@ Can be set to 0 for disabling completion. on_buttonBox_clicked(QAbstractButton*) - 385 - 591 + 394 + 584 385 @@ -2120,8 +2148,8 @@ Can be set to 0 for disabling completion. configureProxy() - 385 - 591 + 225 + 506 385 diff --git a/src/Settings.cpp b/src/Settings.cpp index 8c262c95..67aa490c 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -208,6 +208,8 @@ QVariant Settings::getDefaultValue(const std::string& group, const std::string& return 5000; if(name == "complete_threshold") return 1000; + if(name == "image_preview") + return false; if(name == "indent_compact") return false; if(name == "auto_switch_mode") diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index 1cf04cdf..3cacebf9 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -390,6 +390,16 @@ QVariant SqliteTableModel::data(const QModelIndex &index, int role) const .arg(QKeySequence(Qt::CTRL).toString(QKeySequence::NativeText)); else return QString(); + } else if(role == Qt::DecorationRole) { + if(!row_available) + return QVariant(); + + if(Settings::getValue("databrowser", "image_preview").toBool() && !isImageData(cached_row->at(column)).isNull()) + { + QImage img; + img.loadFromData(cached_row->at(column)); + return QPixmap::fromImage(img); + } } return QVariant(); From 91e036dd744540bc8c024aa449387d7cc6594bea Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Sun, 22 Sep 2019 21:18:35 +0200 Subject: [PATCH 3/8] Add move field to top/bottom buttons to Edit Table dialog See issue #1988. --- src/EditTableDialog.cpp | 34 +++++++-- src/EditTableDialog.h | 12 ++- src/EditTableDialog.ui | 119 +++++++++++++++++++++++++----- src/icons/bullet_arrow_bottom.png | Bin 0 -> 229 bytes src/icons/bullet_arrow_top.png | Bin 0 -> 230 bytes src/icons/icons.qrc | 2 + 6 files changed, 143 insertions(+), 24 deletions(-) create mode 100644 src/icons/bullet_arrow_bottom.png create mode 100644 src/icons/bullet_arrow_top.png diff --git a/src/EditTableDialog.cpp b/src/EditTableDialog.cpp index d4ac98bf..9cc3d882 100644 --- a/src/EditTableDialog.cpp +++ b/src/EditTableDialog.cpp @@ -845,24 +845,46 @@ void EditTableDialog::fieldSelectionChanged() if(hasSelection) { ui->buttonMoveUp->setEnabled(ui->treeWidget->selectionModel()->currentIndex().row() != 0); + ui->buttonMoveTop->setEnabled(ui->buttonMoveUp->isEnabled()); ui->buttonMoveDown->setEnabled(ui->treeWidget->selectionModel()->currentIndex().row() != ui->treeWidget->topLevelItemCount() - 1); + ui->buttonMoveBottom->setEnabled(ui->buttonMoveDown->isEnabled()); } } void EditTableDialog::moveUp() { - moveCurrentField(false); + moveCurrentField(MoveUp); } void EditTableDialog::moveDown() { - moveCurrentField(true); + moveCurrentField(MoveDown); } -void EditTableDialog::moveCurrentField(bool down) +void EditTableDialog::moveTop() +{ + moveCurrentField(MoveTop); +} + +void EditTableDialog::moveBottom() +{ + moveCurrentField(MoveBottom); +} + +void EditTableDialog::moveCurrentField(MoveFieldDirection dir) { int currentRow = ui->treeWidget->currentIndex().row(); - int newRow = currentRow + (down ? 1 : -1); + int newRow; + if(dir == MoveUp) + newRow = currentRow - 1; + else if(dir == MoveDown) + newRow = currentRow + 1; + else if(dir == MoveTop) + newRow = 0; + else if(dir == MoveBottom) + newRow = ui->treeWidget->topLevelItemCount() - 1; + else + return; // Save the comboboxes first by making copies QComboBox* newCombo[2]; @@ -891,7 +913,9 @@ void EditTableDialog::moveCurrentField(bool down) ui->treeWidget->setCurrentIndex(ui->treeWidget->currentIndex().sibling(newRow, 0)); // Finally update the table SQL - std::swap(m_table.fields[static_cast(newRow)], m_table.fields[static_cast(currentRow)]); + sqlb::Field temp = m_table.fields[static_cast(currentRow)]; + m_table.fields.erase(m_table.fields.begin() + currentRow); + m_table.fields.insert(m_table.fields.begin() + newRow, temp); // Update the SQL preview updateSqlText(); diff --git a/src/EditTableDialog.h b/src/EditTableDialog.h index 3cc29d16..2e9fdbf2 100644 --- a/src/EditTableDialog.h +++ b/src/EditTableDialog.h @@ -50,10 +50,18 @@ private: kConstraintSql = 3 }; + enum MoveFieldDirection + { + MoveUp, + MoveDown, + MoveTop, + MoveBottom + }; + void updateColumnWidth(); void updateSqlText(); - void moveCurrentField(bool down); + void moveCurrentField(MoveFieldDirection dir); private slots: void populateFields(); @@ -71,6 +79,8 @@ private slots: void updateTypeAndCollation(); void moveUp(); void moveDown(); + void moveTop(); + void moveBottom(); void setWithoutRowid(bool without_rowid); void changeSchema(const QString& schema); void removeConstraint(); diff --git a/src/EditTableDialog.ui b/src/EditTableDialog.ui index 7abf5a21..a4cf5053 100644 --- a/src/EditTableDialog.ui +++ b/src/EditTableDialog.ui @@ -6,7 +6,7 @@ 0 0 - 650 + 652 600 @@ -111,7 +111,7 @@ - Add field + Add @@ -131,7 +131,7 @@ false - Remove field + Remove @@ -145,13 +145,36 @@ + + + + false + + + Move to top + + + + :/icons/arrow_top:/icons/arrow_top + + + Qt::ToolButtonTextBesideIcon + + + true + + + Qt::NoArrow + + + false - Move field up + Move up @@ -171,7 +194,7 @@ false - Move field down + Move down @@ -185,6 +208,26 @@ + + + + false + + + Move to bottom + + + + :/icons/arrow_bottom:/icons/arrow_bottom + + + Qt::ToolButtonTextBesideIcon + + + true + + + @@ -275,7 +318,7 @@ Check constraint - + Collation @@ -463,12 +506,18 @@ buttonMore comboSchema checkWithoutRowid + groupDefinition addFieldButton removeFieldButton + buttonMoveTop buttonMoveUp buttonMoveDown + buttonMoveBottom treeWidget sqlTextEdit + buttonAddConstraint + buttonRemoveConstraint + tableConstraints @@ -529,8 +578,8 @@ addField() - 94 - 253 + 78 + 255 79 @@ -545,8 +594,8 @@ removeField() - 222 - 253 + 167 + 255 249 @@ -613,8 +662,8 @@ 78 - 117 - 176 + 138 + 172 @@ -625,8 +674,8 @@ setWithoutRowid(bool) - 344 - 143 + 485 + 160 324 @@ -657,8 +706,8 @@ changeSchema(QString) - 186 - 155 + 327 + 139 647 @@ -673,8 +722,8 @@ removeConstraint() - 186 - 155 + 295 + 255 647 @@ -682,6 +731,38 @@ + + buttonMoveTop + clicked() + EditTableDialog + moveTop() + + + 207 + 240 + + + 202 + 190 + + + + + buttonMoveBottom + clicked() + EditTableDialog + moveBottom() + + + 530 + 246 + + + 400 + 186 + + + fieldSelectionChanged() @@ -695,5 +776,7 @@ setWithoutRowid(bool) changeSchema(QString) removeConstraint() + moveTop() + moveBottom() diff --git a/src/icons/bullet_arrow_bottom.png b/src/icons/bullet_arrow_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..1a28d8250035963143465ead45faaae79de6dcbd GIT binary patch literal 229 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^zbpD<_bdI{u9mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-$B_jGX#(Kw&{<9vg>5sw;c`@i>p z_kaC=>wowE^MCGt=k0FV8~^>E{g3+d|7HJ+|I4@QcRd)^xi$V8^Vx_G`+r6+RSGlL znKt8REw4uuU$^wNG<`ek&pG#xJ(Hh0`*ZlR^+%Epor$llpY=cF-^u@z|Ed2wufXdW aBfwxkOHd_T*GCNK1O`u6KbLh*2~7Ys?Or1Q literal 0 HcmV?d00001 diff --git a/src/icons/bullet_arrow_top.png b/src/icons/bullet_arrow_top.png new file mode 100644 index 0000000000000000000000000000000000000000..0ce86d2b2bc8eb047ca749fff00716b15c5bd9a8 GIT binary patch literal 230 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^zbpD<_bdI{u9mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-%s@N{tu(Kvs0!a}YF0}=J!QSUY7M7z;-RJ$7n8*6-IkwdUVS%nn(+HZ=IM>KhySVgJojLZytKsQlJks9 zg>q@PUpbdv;&|+P!{NQ{65)mBh3B(*yP58!PISKz-O48F^~6K0e}RJHfr*NaZ|Z(M akrr=d7xdAvd!`9=1B0ilpUXO@geCyB6I2-h literal 0 HcmV?d00001 diff --git a/src/icons/icons.qrc b/src/icons/icons.qrc index 86caabe3..ce7ceebf 100644 --- a/src/icons/icons.qrc +++ b/src/icons/icons.qrc @@ -84,5 +84,7 @@ clear_cond_formats.png edit_cond_formats.png clear_sorting.png + bullet_arrow_bottom.png + bullet_arrow_top.png From a69c62790f67dccf931544cc9813167387d7e964 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Sun, 22 Sep 2019 22:06:04 +0200 Subject: [PATCH 4/8] Fix foreign key editor not working correctly in Edit Table dialog When creating a new foreign key constraint in the Edit Table dialog, a list of tables to reference is displayed. This list is updated whenever the name of the edited table changes because this is the only table the name of which can change while the Edit Table dialog is opened. However, this meant that when the name of the table changes to some existing table name, the field list of that existing table is replaced by the field list of the current table. If the name of the current table is changed once again, it is deleted entirely from the list. This commit fixes this behaviour by separating the static part of the table and field list from the one table which can change. See issue #1991. --- src/EditTableDialog.cpp | 1 - src/ForeignKeyEditorDelegate.cpp | 26 +++++++++++++++----------- src/ForeignKeyEditorDelegate.h | 2 -- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/EditTableDialog.cpp b/src/EditTableDialog.cpp index 9cc3d882..6a3a2dea 100644 --- a/src/EditTableDialog.cpp +++ b/src/EditTableDialog.cpp @@ -374,7 +374,6 @@ void EditTableDialog::checkInput() if (normTableName != m_table.name()) { const std::string oldTableName = m_table.name(); m_table.setName(normTableName); - m_fkEditorDelegate->updateTablesList(oldTableName); // update fk's that refer to table itself recursively const auto& fields = m_table.fields; diff --git a/src/ForeignKeyEditorDelegate.cpp b/src/ForeignKeyEditorDelegate.cpp index 971dcd4a..b21c3f84 100644 --- a/src/ForeignKeyEditorDelegate.cpp +++ b/src/ForeignKeyEditorDelegate.cpp @@ -85,7 +85,8 @@ ForeignKeyEditorDelegate::ForeignKeyEditorDelegate(const DBBrowserDB& db, sqlb:: { for(const auto& jt : it.second) { - if(jt.second->type() == sqlb::Object::Types::Table) + // Don't insert the current table into the list. The name and fields of the current table are always taken from the m_table reference + if(jt.second->type() == sqlb::Object::Types::Table && jt.second->name() != m_table.name()) m_tablesIds.insert({jt.second->name(), std::dynamic_pointer_cast(jt.second)->fieldNames()}); } } @@ -105,14 +106,25 @@ QWidget* ForeignKeyEditorDelegate::createEditor(QWidget* parent, const QStyleOpt QComboBox* box = editor->idsComboBox; box->clear(); box->addItem(QString()); // for those heroes who don't like to specify key explicitly - for(const auto& n : m_tablesIds[tableName.toStdString()]) - box->addItem(QString::fromStdString(n)); + + // For recursive foreign keys get the field list from the m_table reference. For other foreign keys from the prepared field lists. + if(tableName.toStdString() == m_table.name()) + { + for(const auto& n : m_table.fieldNames()) + box->addItem(QString::fromStdString(n)); + } else { + for(const auto& n : m_tablesIds[tableName.toStdString()]) + box->addItem(QString::fromStdString(n)); + } + + box->setCurrentIndex(0); }); editor->tablesComboBox->clear(); for(const auto& i : m_tablesIds) editor->tablesComboBox->addItem(QString::fromStdString(i.first)); + editor->tablesComboBox->addItem(QString::fromStdString(m_table.name())); // For recursive foreign keys return editor; } @@ -175,12 +187,4 @@ void ForeignKeyEditorDelegate::updateEditorGeometry(QWidget* editor, const QStyl editor->setGeometry(option.rect); } -void ForeignKeyEditorDelegate::updateTablesList(const std::string& oldTableName) -{ - // this is used for recursive table constraints when - // table column references column within same table - m_tablesIds.erase(oldTableName); - m_tablesIds.insert({m_table.name(), m_table.fieldNames()}); -} - #include "ForeignKeyEditorDelegate.moc" diff --git a/src/ForeignKeyEditorDelegate.h b/src/ForeignKeyEditorDelegate.h index 8f6560a6..076706a9 100644 --- a/src/ForeignKeyEditorDelegate.h +++ b/src/ForeignKeyEditorDelegate.h @@ -25,8 +25,6 @@ public: void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - void updateTablesList(const std::string& oldTableName); - private: const DBBrowserDB& m_db; sqlb::Table& m_table; From 845875ae190288df8728a169bd0f03184aecbaa9 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Sun, 22 Sep 2019 22:36:04 +0200 Subject: [PATCH 5/8] Fix retrieving list of selected columns for Browse Data tab When retrieving the list of selected columns of a table in the Browse Data tab only take into account columns which are selected entirely, not columns which have at least a single selected field. Before selecting a row would implicity select all columns. This fixes issues with resizing and with hiding a column. See issue #1999. --- src/ExtendedTableWidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ExtendedTableWidget.cpp b/src/ExtendedTableWidget.cpp index bb6856d8..efe5055c 100644 --- a/src/ExtendedTableWidget.cpp +++ b/src/ExtendedTableWidget.cpp @@ -844,7 +844,7 @@ int ExtendedTableWidget::numVisibleRows() const std::unordered_set ExtendedTableWidget::selectedCols() const { std::unordered_set selectedCols; - for(const QModelIndex & idx : selectedIndexes()) + for(const auto& idx : selectionModel()->selectedColumns()) selectedCols.insert(idx.column()); return selectedCols; } From 091273869db212e63a4db51a78391bac738ddf7f Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Thu, 26 Sep 2019 15:30:37 +0200 Subject: [PATCH 6/8] tests: Simplify code --- src/tests/testsqlobjects.cpp | 48 ++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/tests/testsqlobjects.cpp b/src/tests/testsqlobjects.cpp index 9b7efa35..bdc847b4 100644 --- a/src/tests/testsqlobjects.cpp +++ b/src/tests/testsqlobjects.cpp @@ -182,7 +182,7 @@ void TestTable::parseSQL() "\tinfo VARCHAR(255) CHECK (info == 'x')\n" ");"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.name(), "hero"); QCOMPARE(tab.rowidColumns(), {"_rowid_"}); @@ -212,7 +212,7 @@ void TestTable::parseSQLdefaultexpr() "date datetime default CURRENT_TIMESTAMP," "zoi integer)"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.name(), "chtest"); QCOMPARE(tab.fields.at(0).name(), "id"); @@ -246,7 +246,7 @@ void TestTable::parseSQLMultiPk() "PRIMARY KEY(\"id1\",\"id2\")\n" ");"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.name(), "hero"); QCOMPARE(tab.fields.at(0).name(), "id1"); @@ -265,7 +265,7 @@ void TestTable::parseSQLForeignKey() { std::string sSQL = "CREATE TABLE grammar_test(id, test, FOREIGN KEY(test) REFERENCES other_table);"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.name(), "grammar_test"); QCOMPARE(tab.fields.at(0).name(), "id"); @@ -276,7 +276,7 @@ void TestTable::parseSQLSingleQuotes() { std::string sSQL = "CREATE TABLE 'test'('id','test');"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.name(), "test"); QCOMPARE(tab.fields.at(0).name(), "id"); @@ -287,7 +287,7 @@ void TestTable::parseSQLSquareBrackets() { std::string sSQL = "CREATE TABLE [test]([id],[test]);"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.name(), "test"); QCOMPARE(tab.fields.at(0).name(), "id"); @@ -299,7 +299,7 @@ void TestTable::parseSQLKeywordInIdentifier() { std::string sSQL = "CREATE TABLE deffered(key integer primary key, if text);"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.name(), "deffered"); QCOMPARE(tab.fields.at(0).name(), "key"); @@ -313,7 +313,7 @@ void TestTable::parseSQLSomeKeywordsInIdentifier() "`Area of Work` TEXT," "`Average Number of Volunteers` INTEGER);"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.name(), "Average Number of Volunteers by Area of Work"); QCOMPARE(tab.fields.at(0).name(), "Area of Work"); @@ -324,7 +324,7 @@ void TestTable::parseSQLWithoutRowid() { std::string sSQL = "CREATE TABLE test(a integer primary key, b integer) WITHOUT ROWID;"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.primaryKey()->columnList(), {"a"}); QCOMPARE(tab.rowidColumns(), {"a"}); @@ -337,7 +337,7 @@ void TestTable::parseNonASCIIChars() "PRIMARY KEY(`Fieldöäüß`)" ");"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.name(), "lösung"); QCOMPARE(tab.fields.at(0).name(), "Fieldöäüß"); @@ -350,7 +350,7 @@ void TestTable::parseNonASCIICharsEs() "PRIMARY KEY(\"Field áéíóúÁÉÍÓÚñÑçÇ\")" ");"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.name(), "Cigüeñas de Alcalá"); QCOMPARE(tab.fields.at(0).name(), "Field áéíóúÁÉÍÓÚñÑçÇ"); @@ -360,7 +360,7 @@ void TestTable::parseSQLEscapedQuotes() { std::string sSql = "CREATE TABLE double_quotes(a text default 'a''a');"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSql)))); + Table tab(*Table::parseSQL(sSql)); QCOMPARE(tab.name(), "double_quotes"); QCOMPARE(tab.fields.at(0).name(), "a"); @@ -371,7 +371,7 @@ void TestTable::parseSQLForeignKeys() { std::string sql = "CREATE TABLE foreign_key_test(a int, b int, foreign key (a) references x, foreign key (b) references w(y,z) on delete set null);"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sql)))); + Table tab(*Table::parseSQL(sql)); QCOMPARE(tab.name(), "foreign_key_test"); QCOMPARE(tab.fields.at(0).name(), "a"); @@ -386,7 +386,7 @@ void TestTable::parseSQLCheckConstraint() { std::string sql = "CREATE TABLE a (\"b\" text CHECK(\"b\"='A' or \"b\"='B'));"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sql)))); + Table tab(*Table::parseSQL(sql)); QCOMPARE(tab.name(), "a"); QCOMPARE(tab.fields.at(0).name(), "b"); @@ -398,7 +398,7 @@ void TestTable::parseDefaultValues() { std::string sql = "CREATE TABLE test(a int DEFAULT 0, b int DEFAULT -1, c text DEFAULT 'hello', d text DEFAULT '0');"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sql)))); + Table tab(*Table::parseSQL(sql)); QCOMPARE(tab.name(), "test"); QCOMPARE(tab.fields.at(0).name(), "a"); @@ -422,7 +422,7 @@ void TestTable::createTableWithIn() "value NVARCHAR(5) CHECK (value IN ('a', 'b', 'c'))" ");"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.name(), "not_working"); QCOMPARE(tab.fields.at(1).check(), "\"value\" IN ('a', 'b', 'c')"); @@ -439,7 +439,7 @@ void TestTable::createTableWithNotLikeConstraint() "value6 INTEGER CHECK(value6 NOT BETWEEN 1 AND 100)\n" ");"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sSQL)))); + Table tab(*Table::parseSQL(sSQL)); QCOMPARE(tab.name(), "hopefully_working"); QCOMPARE(tab.fields.at(0).check(), "\"value\" NOT LIKE 'prefix%'"); @@ -458,7 +458,7 @@ void TestTable::rowValues() "CHECK((a, b) = (1, 2))\n" ");"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sql)))); + Table tab(*Table::parseSQL(sql)); QCOMPARE(tab.name(), "test"); QCOMPARE(std::dynamic_pointer_cast(tab.constraint({}, sqlb::Constraint::CheckConstraintType))->expression(), "(\"a\", \"b\") = (1, 2)"); @@ -473,7 +473,7 @@ void TestTable::complexExpressions() "d INTEGER CHECK((((d > 0))))\n" ");"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sql)))); + Table tab(*Table::parseSQL(sql)); QCOMPARE(tab.name(), "test"); QCOMPARE(tab.fields.at(0).check(), "(\"a\" > 0)"); @@ -488,7 +488,7 @@ void TestTable::datetimeExpression() "entry INTEGER DEFAULT (DATETIME(CURRENT_TIMESTAMP, 'LOCALTIME'))\n" ");"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sql)))); + Table tab(*Table::parseSQL(sql)); QCOMPARE(tab.name(), "test"); QCOMPARE(tab.fields.at(0).name(), "entry"); @@ -502,7 +502,7 @@ void TestTable::extraParentheses() "xy INTEGER DEFAULT (1 + (5) - 4)\n" ");"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sql)))); + Table tab(*Table::parseSQL(sql)); QCOMPARE(tab.name(), "test"); QCOMPARE(tab.fields.at(0).name(), "xy"); @@ -516,7 +516,7 @@ void TestTable::moduloOperator() "xy INTEGER DEFAULT (7 % 2)\n" ");"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sql)))); + Table tab(*Table::parseSQL(sql)); QCOMPARE(tab.name(), "test"); QCOMPARE(tab.fields.at(0).name(), "xy"); @@ -533,7 +533,7 @@ void TestTable::complexExpression() "CHECK((a = 'S' AND b IS NOT NULL) OR (a IN ('A', 'P')))" ");"; - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sql)))); + Table tab(*Table::parseSQL(sql)); QCOMPARE(tab.name(), "test"); QCOMPARE(tab.fields.at(0).name(), "uuid"); @@ -549,7 +549,7 @@ void TestTable::parseTest() { QFETCH(std::string, sql); - Table tab(*(std::dynamic_pointer_cast(Table::parseSQL(sql)))); + Table tab(*Table::parseSQL(sql)); QVERIFY(tab.fullyParsed()); } From 671cc6d6c614dbf1f02cb41bd284c5e1816f360b Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Thu, 26 Sep 2019 15:51:38 +0200 Subject: [PATCH 7/8] Add a find tool bar to the Browse Data tab This adds a find tool bar to the Browse Data tab which allows the user to search for values in the current table view. It only looks in non-filtered rows and in non-hidden columns. It respected display formats and sort order, too. The idea is to provide an additional level of searching: the first one is done by using the filters and actually reduces the number of rows in the view; the second level is done by using the new find tool bar and allows you to look for values in the remaining rows (or all rows if there is no filter). See issue #1608. --- src/TableBrowser.cpp | 88 ++++++++++++++++- src/TableBrowser.h | 3 + src/TableBrowser.ui | 208 +++++++++++++++++++++++++++++++++++---- src/sqlitetablemodel.cpp | 106 ++++++++++++++++++++ src/sqlitetablemodel.h | 13 +++ 5 files changed, 400 insertions(+), 18 deletions(-) diff --git a/src/TableBrowser.cpp b/src/TableBrowser.cpp index 15a4e043..f716184b 100644 --- a/src/TableBrowser.cpp +++ b/src/TableBrowser.cpp @@ -127,6 +127,41 @@ TableBrowser::TableBrowser(QWidget* parent) : connect(ui->dataTable->verticalHeader(), &QHeaderView::customContextMenuRequested, this, &TableBrowser::showRecordPopupMenu); connect(ui->dataTable, &ExtendedTableWidget::openFileFromDropEvent, this, &TableBrowser::requestFileOpen); connect(ui->dataTable, &ExtendedTableWidget::selectedRowsToBeDeleted, this, &TableBrowser::deleteRecord); + + // Set up find frame + ui->frameFind->hide(); + + QShortcut* shortcutHideFindFrame = new QShortcut(QKeySequence("ESC"), ui->editFindExpression); + connect(shortcutHideFindFrame, &QShortcut::activated, ui->buttonFindClose, &QToolButton::click); + + connect(ui->actionFind, &QAction::triggered, [this](bool checked) { + if(checked) + { + ui->frameFind->show(); + ui->editFindExpression->setFocus(); + } else { + ui->buttonFindClose->click(); + } + }); + + connect(ui->editFindExpression, &QLineEdit::returnPressed, ui->buttonFindNext, &QToolButton::click); + connect(ui->editFindExpression, &QLineEdit::textChanged, this, [this]() { + // When the text has changed but neither Return nor F3 or similar nor any buttons were pressed, we want to include the current + // cell in the search as well. This makes sure the selected cell does not jump around every time the text is changed but only + // when the current cell does not match the search expression anymore. + find(ui->editFindExpression->text(), true, true); + }); + connect(ui->buttonFindClose, &QToolButton::clicked, this, [this](){ + ui->dataTable->setFocus(); + ui->frameFind->hide(); + ui->actionFind->setChecked(false); + }); + connect(ui->buttonFindPrevious, &QToolButton::clicked, this, [this](){ + find(ui->editFindExpression->text(), false); + }); + connect(ui->buttonFindNext, &QToolButton::clicked, this, [this](){ + find(ui->editFindExpression->text(), true); + }); } TableBrowser::~TableBrowser() @@ -219,6 +254,7 @@ void TableBrowser::setEnabled(bool enable) ui->actionRefresh->setEnabled(enable); ui->actionPrintTable->setEnabled(enable); ui->editGlobalFilter->setEnabled(enable); + ui->actionFind->setEnabled(enable); updateInsertDeleteRecordButton(); } @@ -270,7 +306,7 @@ void TableBrowser::updateTable() } statusMessage += tr(". Sum: %1; Average: %2; Min: %3; Max: %4").arg(sum).arg(sum/sel.count()).arg(min).arg(max); } - }; + } emit statusMessageRequested(statusMessage); }); } @@ -1162,3 +1198,53 @@ void TableBrowser::jumpToRow(const sqlb::ObjectIdentifier& table, QString column ui->dataTable->filterHeader()->setFilter(static_cast(column_index-obj->fields.begin()+1), QString("=") + value); updateTable(); } + +void TableBrowser::find(const QString& expr, bool forward, bool include_first) +{ + // Get the cell from which the search should be started. If there is a selected cell, use that. If there is no selected cell, start at the first cell. + QModelIndex start; + if(ui->dataTable->selectionModel()->hasSelection()) + start = ui->dataTable->selectionModel()->selectedIndexes().front(); + else + start = m_browseTableModel->index(0, 0); + + // Prepare the match flags with all the search settings + Qt::MatchFlags flags = Qt::MatchWrap; + + if(ui->checkFindCaseSensitive->isChecked()) + flags |= Qt::MatchCaseSensitive; + + if(ui->checkFindWholeCell->isChecked()) + flags |= Qt::MatchFixedString; + else + flags |= Qt::MatchContains; + + if(ui->checkFindRegEx->isChecked()) + flags |= Qt::MatchRegExp; + + // Prepare list of columns to search in. We only search in non-hidden rows + std::vector column_list; + sqlb::ObjectIdentifier tableName = currentlyBrowsedTableName(); + if(browseTableSettings[tableName].showRowid) + column_list.push_back(0); + for(int i=1;icolumnCount();i++) + { + if(browseTableSettings[tableName].hiddenColumns.contains(i) == false) + column_list.push_back(i); + else if(browseTableSettings[tableName].hiddenColumns[i] == false) + column_list.push_back(i); + } + + // Perform the actual search using the model class + const auto match = m_browseTableModel->nextMatch(start, column_list, expr, flags, !forward, include_first); + + // Select the next match if we found one + if(match.isValid()) + ui->dataTable->setCurrentIndex(match); + + // Make the expression control red if no results were found + if(match.isValid() || expr == "") + ui->editFindExpression->setStyleSheet(""); + else + ui->editFindExpression->setStyleSheet("QLineEdit {color: white; background-color: rgb(255, 102, 102)}"); +} diff --git a/src/TableBrowser.h b/src/TableBrowser.h index 3feafa99..91b15c7a 100644 --- a/src/TableBrowser.h +++ b/src/TableBrowser.h @@ -150,6 +150,9 @@ private slots: void browseDataSetTableEncoding(bool forAllTables = false); void browseDataSetDefaultTableEncoding(); +private: + void find(const QString& expr, bool forward, bool include_first = false); + private: Ui::TableBrowser* ui; QIntValidator* gotoValidator; diff --git a/src/TableBrowser.ui b/src/TableBrowser.ui index 868e7f5c..40ca842b 100644 --- a/src/TableBrowser.ui +++ b/src/TableBrowser.ui @@ -6,8 +6,8 @@ 0 0 - 552 - 362 + 617 + 400 @@ -74,6 +74,8 @@ + + @@ -125,6 +127,152 @@ + + + + + 16777215 + 31 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 1 + + + 1 + + + 1 + + + 1 + + + 3 + + + + + Find next match [Enter, F3] + + + Find next match with wrapping + + + + :/icons/down:/icons/down + + + F3 + + + + + + + Find previous match [Shift+F3] + + + Find previous match with mapping + + + + :/icons/up:/icons/up + + + Shift+F3 + + + + + + + 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">Regular Expression in Wikibooks</a>.</p></body></html> + + + Regular Expression + + + + + + + The found pattern must be a whole word + + + Whole Cell + + + + + + + Qt::DefaultContextMenu + + + Text pattern to find considering the checks in this frame + + + Find in table + + + true + + + + + + + Close Find Bar + + + Close Find Bar + + + + :/icons/close:/icons/close + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + The found pattern must match in letter case + + + Case Sensitive + + + + + + @@ -507,6 +655,24 @@ Qt::WidgetShortcut + + + true + + + + :/icons/find:/icons/find + + + Find in cells + + + Open the find tool bar which allows you to search for values in the table view below. + + + Ctrl+F + + @@ -527,6 +693,14 @@ comboBrowseTable dataTable + editGlobalFilter + editFindExpression + buttonFindPrevious + buttonFindNext + checkFindCaseSensitive + checkFindWholeCell + checkFindRegEx + buttonFindClose buttonBegin buttonPrevious buttonNext @@ -545,8 +719,8 @@ updateTable() - 118 - 141 + 159 + 31 399 @@ -561,8 +735,8 @@ navigatePrevious() - 86 - 539 + 54 + 358 399 @@ -577,8 +751,8 @@ navigateNext() - 183 - 539 + 139 + 358 399 @@ -593,8 +767,8 @@ navigateGoto() - 365 - 539 + 403 + 360 399 @@ -609,8 +783,8 @@ navigateGoto() - 506 - 538 + 550 + 360 399 @@ -625,8 +799,8 @@ navigateEnd() - 223 - 539 + 169 + 358 499 @@ -641,8 +815,8 @@ navigateBegin() - 50 - 539 + 24 + 358 499 @@ -886,7 +1060,7 @@ 326 - 347 + 291 diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index 3cacebf9..65ddf9eb 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include "RowLoader.h" @@ -1004,3 +1005,108 @@ void SqliteTableModel::waitUntilIdle () const { worker->waitUntilIdle(); } + +QModelIndex SqliteTableModel::nextMatch(const QModelIndex& start, const std::vector& column_list, const QString& value, Qt::MatchFlags flags, bool reverse, bool dont_skip_to_next_field) const +{ + // Extract flags + bool whole_cell = !(flags & Qt::MatchContains); + bool regex = flags & Qt::MatchRegExp; + Qt::CaseSensitivity case_sensitive = ((flags & Qt::MatchCaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive); + bool wrap = flags & Qt::MatchWrap; + int increment = (reverse ? -1 : 1); + + // Prepare the regular expression for regex mode + QRegularExpression reg_exp; + if(regex) + { + reg_exp = QRegularExpression(value, (case_sensitive ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption)); + if(!reg_exp.isValid()) + return QModelIndex(); + + if(whole_cell) + { +#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0) + reg_exp.setPattern("\\A(" + reg_exp.pattern() + ")\\Z"); +#else + reg_exp.setPattern(QRegularExpression::anchoredPattern(reg_exp.pattern())); +#endif + } + } + + // Wait until the row count is there + waitUntilIdle(); + + // Make sure the start position starts in a column from the list of columns to search in + QModelIndex pos = start; + if(std::find(column_list.begin(), column_list.end(), pos.column()) == column_list.end()) + { + // If for some weird reason the start index is not in the column list, we simply use the first column of the column list instead + pos = pos.sibling(pos.row(), reverse ? column_list.back() : column_list.front()); + } + + // Get the last cell to search in. If wrapping is enabled, we search until we hit the start cell again. If wrapping is not enabled, we start at the last + // cell of the table. + QModelIndex end = (wrap ? pos : index(rowCount(), column_list.back())); + + // Loop through all cells for the search + while(true) + { + // Go to the next cell and skip all columns in between which we do not care about. This is done as the first step in order + // to skip the start index when matching the first cell is disabled. + if(dont_skip_to_next_field == false) + { + while(true) + { + // Next cell position + int next_row = pos.row(); + int next_column = pos.column() + increment; + + // Have we reached the end of the row? Then go to the next one + if(next_column < 0 || next_column >= static_cast(m_headers.size())) + { + next_row += increment; + next_column = (reverse ? column_list.back() : column_list.front()); + } + + // Have we reached the last row? Then wrap around to the first one + if(wrap && (next_row < 0 || next_row >= rowCount())) + next_row = (reverse ? rowCount()-1 : 0); + + // Set next index for search + pos = pos.sibling(next_row, next_column); + + // Have we hit the last column? We have not found anything then + if(pos == end) + return QModelIndex(); + + // Is this a column which we are supposed to search in? If so, stop looking for the next cell and start comparing + if(std::find(column_list.begin(), column_list.end(), next_column) != column_list.end()) + break; + } + } + + // Make sure the next time we hit the above check, we actuall move on to the next cell and do not skip the loop again. + dont_skip_to_next_field = false; + + // Get row from cache. If it is not in the cache, load the next chunk from the database + const size_t row = static_cast(pos.row()); + if(!m_cache.count(row)) + { + triggerCacheLoad(static_cast(row)); + waitUntilIdle(); + } + const Row* row_data = &m_cache.at(row); + + // Get cell data + const size_t column = static_cast(pos.column()); + QString data = row_data->at(column); + + // Perform comparison + if(whole_cell && !regex && data.compare(value, case_sensitive) == 0) + return pos; + else if(!whole_cell && !regex && data.contains(value, case_sensitive)) + return pos; + else if(regex && reg_exp.match(data).hasMatch()) + return pos; + } +} diff --git a/src/sqlitetablemodel.h b/src/sqlitetablemodel.h index 7850014c..1ef41a80 100644 --- a/src/sqlitetablemodel.h +++ b/src/sqlitetablemodel.h @@ -117,6 +117,19 @@ public: void addCondFormat(int column, const CondFormat& condFormat); void setCondFormats(int column, const std::vector& condFormats); + // Search for the specified expression in the given cells. This intended as a replacement for QAbstractItemModel::match() even though + // it does not override it, which - because of the different parameters - is not possible. + // start contains the index to start with, column_list contains the ordered list of the columns to look in, value is the value to search for, + // flags allows to modify the search process (Qt::MatchContains, Qt::MatchRegExp, Qt::MatchCaseSensitive, and Qt::MatchWrap are understood), + // reverse can be set to true to progress through the cells in backwards direction, and dont_skip_to_next_field can be set to true if the current + // cell can be matched as well. + QModelIndex nextMatch(const QModelIndex& start, + const std::vector& column_list, + const QString& value, + Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchContains), + bool reverse = false, + bool dont_skip_to_next_field = false) const; + DBBrowserDB& db() { return m_db; } public slots: From b4c8ec970615c1d7351fb29b892dc396f8806b8c Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Thu, 26 Sep 2019 16:48:21 +0200 Subject: [PATCH 8/8] Fix editing of collation in new fiews in the Edit Table dialog Setting a collation in a newly added field in the Edit Table dialog did not work when the field name was edited just before. This is fixed now. See issue #2011. --- src/EditTableDialog.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/EditTableDialog.cpp b/src/EditTableDialog.cpp index 6a3a2dea..86a970ed 100644 --- a/src/EditTableDialog.cpp +++ b/src/EditTableDialog.cpp @@ -504,6 +504,7 @@ void EditTableDialog::fieldItemChanged(QTreeWidgetItem *item, int column) field.setName(item->text(column).toStdString()); m_table.renameKeyInAllConstraints(oldFieldName.toStdString(), item->text(column).toStdString()); qobject_cast(ui->treeWidget->itemWidget(item, kType))->setProperty("column", item->text(column)); + qobject_cast(ui->treeWidget->itemWidget(item, kCollation))->setProperty("column", item->text(column)); // Update the field name in the map of old column names to new column names if(!m_bNewTable)