diff --git a/src/TableBrowser.cpp b/src/TableBrowser.cpp index 90aa519a..fdbcc571 100644 --- a/src/TableBrowser.cpp +++ b/src/TableBrowser.cpp @@ -256,8 +256,21 @@ TableBrowser::TableBrowser(QWidget* parent) : connect(ui->actionFind, &QAction::triggered, [this](bool checked) { if(checked) { + ui->widgetReplace->hide(); ui->frameFind->show(); ui->editFindExpression->setFocus(); + ui->actionReplace->setChecked(false); + } else { + ui->buttonFindClose->click(); + } + }); + connect(ui->actionReplace, &QAction::triggered, [this](bool checked) { + if(checked) + { + ui->widgetReplace->show(); + ui->frameFind->show(); + ui->editFindExpression->setFocus(); + ui->actionFind->setChecked(false); } else { ui->buttonFindClose->click(); } @@ -274,6 +287,7 @@ TableBrowser::TableBrowser(QWidget* parent) : ui->dataTable->setFocus(); ui->frameFind->hide(); ui->actionFind->setChecked(false); + ui->actionReplace->setChecked(false); }); connect(ui->buttonFindPrevious, &QToolButton::clicked, this, [this](){ find(ui->editFindExpression->text(), false); @@ -281,6 +295,12 @@ TableBrowser::TableBrowser(QWidget* parent) : connect(ui->buttonFindNext, &QToolButton::clicked, this, [this](){ find(ui->editFindExpression->text(), true); }); + connect(ui->buttonReplaceNext, &QToolButton::clicked, this, [this](){ + find(ui->editFindExpression->text(), true, true, ReplaceMode::ReplaceNext); + }); + connect(ui->buttonReplaceAll, &QToolButton::clicked, this, [this](){ + find(ui->editFindExpression->text(), true, true, ReplaceMode::ReplaceAll); + }); } TableBrowser::~TableBrowser() @@ -1386,8 +1406,36 @@ void TableBrowser::jumpToRow(const sqlb::ObjectIdentifier& table, std::string co updateTable(); } -void TableBrowser::find(const QString& expr, bool forward, bool include_first) +static QString replaceInValue(QString value, const QString& find, const QString& replace, Qt::MatchFlags flags) { + // Helper function which replaces a string in another string by a third string. It uses regular expressions if told so. + if(flags.testFlag(Qt::MatchRegExp)) + { + QRegularExpression reg_exp(find, (flags.testFlag(Qt::MatchCaseSensitive) ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption)); + if(!flags.testFlag(Qt::MatchContains)) + { +#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 + } + + return value.replace(reg_exp, replace); + } else { + return value.replace(find, replace, flags.testFlag(Qt::MatchCaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive); + } +} + +void TableBrowser::find(const QString& expr, bool forward, bool include_first, ReplaceMode replace) +{ + // Reset the colour of the line edit, assuming there is no error. + ui->editFindExpression->setStyleSheet(""); + + // You are not allowed to search for an ampty string + if(expr.isEmpty()) + return; + // 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()) @@ -1422,16 +1470,64 @@ void TableBrowser::find(const QString& expr, bool forward, bool include_first) column_list.push_back(i); } - // Perform the actual search using the model class - const auto match = m_model->nextMatch(start, column_list, expr, flags, !forward, include_first); + // Are we only searching for text or are we supposed to replace text? + switch(replace) + { + case ReplaceMode::NoReplace: { + // Perform the actual search using the model class + const auto match = m_model->nextMatch(start, column_list, expr, flags, !forward, include_first); - // Select the next match if we found one - if(match.isValid()) - ui->dataTable->setCurrentIndex(match); + // 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.isEmpty()) - ui->editFindExpression->setStyleSheet(""); - else - ui->editFindExpression->setStyleSheet("QLineEdit {color: white; background-color: rgb(255, 102, 102)}"); + // Make the expression control red if no results were found + if(!match.isValid()) + ui->editFindExpression->setStyleSheet("QLineEdit {color: white; background-color: rgb(255, 102, 102)}"); + } break; + case ReplaceMode::ReplaceNext: { + // Find the next match + const auto match = m_model->nextMatch(start, column_list, expr, flags, !forward, include_first); + + // If there was a match, perform the replacement on the cell and select it + if(match.isValid()) + { + m_model->setData(match, replaceInValue(match.data(Qt::EditRole).toString(), expr, ui->editReplaceExpression->text(), flags)); + ui->dataTable->setCurrentIndex(match); + } + + // Make the expression control red if no results were found + if(!match.isValid()) + ui->editFindExpression->setStyleSheet("QLineEdit {color: white; background-color: rgb(255, 102, 102)}"); + } break; + case ReplaceMode::ReplaceAll: { + // Find all matches + std::set all_matches; + while(true) + { + // Find the next match + const auto match = m_model->nextMatch(start, column_list, expr, flags, !forward, include_first); + + // If there was a match, perform the replacement and continue from that position. If there was no match, stop looking for other matches. + // Additionally, keep track of all the matches so far in order to avoid running over them again indefinitely, e.g. when replacing "1" by "10". + if(match.isValid() && all_matches.find(match) == all_matches.end()) + { + all_matches.insert(match); + m_model->setData(match, replaceInValue(match.data(Qt::EditRole).toString(), expr, ui->editReplaceExpression->text(), flags)); + + // Start searching from the last match onwards in order to not search through the same cells over and over again. + start = match; + include_first = false; + } else { + break; + } + } + + // Make the expression control red if no results were found + if(!all_matches.empty()) + QMessageBox::information(this, qApp->applicationName(), tr("%1 replacement(s) made.").arg(all_matches.size())); + else + ui->editFindExpression->setStyleSheet("QLineEdit {color: white; background-color: rgb(255, 102, 102)}"); + } break; + } } diff --git a/src/TableBrowser.h b/src/TableBrowser.h index d3ede0a6..4252edc4 100644 --- a/src/TableBrowser.h +++ b/src/TableBrowser.h @@ -158,7 +158,13 @@ private slots: void setDefaultTableEncoding(); private: - void find(const QString& expr, bool forward, bool include_first = false); + enum class ReplaceMode + { + NoReplace, + ReplaceNext, + ReplaceAll, + }; + void find(const QString& expr, bool forward, bool include_first = false, ReplaceMode replace = ReplaceMode::NoReplace); private: Ui::TableBrowser* ui; diff --git a/src/TableBrowser.ui b/src/TableBrowser.ui index ba3d1517..2a3ea3b4 100644 --- a/src/TableBrowser.ui +++ b/src/TableBrowser.ui @@ -6,7 +6,7 @@ 0 0 - 651 + 695 400 @@ -86,6 +86,7 @@ + @@ -200,7 +201,7 @@ 16777215 - 31 + 62 @@ -209,7 +210,10 @@ QFrame::Raised - + + + 1 + 1 @@ -222,120 +226,208 @@ 1 - - 3 - - - - - Find next match [Enter, F3] - - - Find next match with wrapping - - - - :/icons/down:/icons/down - - - F3 - + + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::DefaultContextMenu + + + Text pattern to find considering the checks in this frame + + + Find in table + + + true + + + + + + + Find previous match [Shift+F3] + + + Find previous match with mapping + + + + :/icons/up:/icons/up + + + Shift+F3 + + + + + + + Find next match [Enter, F3] + + + Find next match with wrapping + + + + :/icons/down:/icons/down + + + F3 + + + + + + + The found pattern must match in letter case + + + Case Sensitive + + + + + + + The found pattern must be a whole word + + + Whole Cell + + + + + + + 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 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close Find Bar + + + Close Find Bar + + + + :/icons/close:/icons/close + + + true + + + + - - - - 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 - + + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::DefaultContextMenu + + + Text to replace with + + + Replace with + + + true + + + + + + + Replace next match + + + Replace + + + + + + + Replace all matches + + + Replace all + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + @@ -950,6 +1042,24 @@ Ctrl+Space + + + true + + + + :/icons/text_replace:/icons/text_replace + + + Replace + + + Replace text in cells + + + Ctrl+H + + @@ -969,15 +1079,20 @@ comboBrowseTable - dataTable editGlobalFilter + fontComboBox + fontSizeBox + dataTable editFindExpression + editReplaceExpression buttonFindPrevious buttonFindNext checkFindCaseSensitive checkFindWholeCell checkFindRegEx buttonFindClose + buttonReplaceNext + buttonReplaceAll buttonBegin buttonPrevious buttonNext @@ -1012,8 +1127,8 @@ navigatePrevious() - 54 - 358 + 55 + 395 399 @@ -1028,8 +1143,8 @@ navigateNext() - 139 - 358 + 140 + 395 399 @@ -1044,8 +1159,8 @@ navigateGoto() - 403 - 360 + 452 + 397 399 @@ -1060,8 +1175,8 @@ navigateGoto() - 550 - 360 + 648 + 397 399 @@ -1076,8 +1191,8 @@ navigateEnd() - 169 - 358 + 170 + 395 499 @@ -1092,8 +1207,8 @@ navigateBegin() - 24 - 358 + 25 + 395 499