File indexing completed on 2025-01-05 03:35:43
0001 /* 0002 File : SearchReplaceWidget.cpp 0003 Project : LabPlot 0004 Description : Search&Replace widget for the spreadsheet 0005 -------------------------------------------------------------------- 0006 SPDX-FileCopyrightText: 2023 Alexander Semke <alexander.semke@web.de> 0007 0008 SPDX-License-Identifier: GPL-2.0-or-later 0009 */ 0010 0011 #include "SearchReplaceWidget.h" 0012 #include "backend/core/Settings.h" 0013 #include "backend/spreadsheet/Spreadsheet.h" 0014 #include "backend/spreadsheet/SpreadsheetModel.h" 0015 #include "commonfrontend/spreadsheet/SpreadsheetView.h" 0016 #include "kdefrontend/GuiTools.h" 0017 0018 #include <KConfigGroup> 0019 #include <KMessageWidget> 0020 0021 #include <QLineEdit> 0022 #include <QMenu> 0023 #include <QRadioButton> 0024 #include <QStack> 0025 0026 // TODO: use the current default datetime format or always enforce the same format/behavior? 0027 static const QString defaultDateTimeFormat = QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz"); 0028 0029 SearchReplaceWidget::SearchReplaceWidget(Spreadsheet* spreadsheet, QWidget* parent) 0030 : QWidget(parent) 0031 , m_spreadsheet(spreadsheet) { 0032 m_view = static_cast<SpreadsheetView*>(spreadsheet->view()); 0033 0034 auto* layout = new QVBoxLayout(this); 0035 this->setLayout(layout); 0036 QSizePolicy sizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 0037 this->setSizePolicy(sizePolicy); 0038 } 0039 0040 SearchReplaceWidget::~SearchReplaceWidget() { 0041 // save the current settings, 0042 // save everything except of the patterns, they will be set when the widget is opened again 0043 KConfigGroup conf = Settings::group(QLatin1String("SearchReplaceWidget")); 0044 0045 if (m_searchWidget) { 0046 conf.writeEntry("SimpleMatchCase", uiSearch.tbMatchCase->isChecked()); 0047 0048 // history for the text value 0049 QStringList items; 0050 for (int i = 0; i < uiSearch.cbFind->count(); ++i) 0051 items << uiSearch.cbFind->itemText(i); 0052 0053 if (!items.empty()) 0054 conf.writeEntry("SimpleValueHistory", items); 0055 } 0056 0057 if (m_searchReplaceWidget) { 0058 conf.writeEntry("DataType", uiSearchReplace.cbDataType->currentData().toInt()); 0059 conf.writeEntry("Order", uiSearchReplace.cbOrder->currentData().toInt()); 0060 conf.writeEntry("MatchCase", uiSearchReplace.tbMatchCase->isChecked()); 0061 conf.writeEntry("SelectionOnly", uiSearchReplace.tbSelectionOnly->isChecked()); 0062 conf.writeEntry("Operator", uiSearchReplace.cbOperator->currentData().toInt()); 0063 conf.writeEntry("OperatorText", uiSearchReplace.cbOperatorText->currentData().toInt()); 0064 conf.writeEntry("OperatorDateTime", uiSearchReplace.cbOperatorDateTime->currentData().toInt()); 0065 0066 // history for the first numerical value 0067 QStringList items; 0068 for (int i = 0; i < uiSearchReplace.cbValue1->count(); ++i) 0069 items << uiSearchReplace.cbValue1->itemText(i); 0070 0071 if (!items.empty()) { 0072 conf.writeEntry("Value1History", items); 0073 items.clear(); 0074 } 0075 0076 // history for the second numerical value 0077 for (int i = 0; i < uiSearchReplace.cbValue2->count(); ++i) 0078 items << uiSearchReplace.cbValue2->itemText(i); 0079 0080 if (!items.empty()) { 0081 conf.writeEntry("Value2History", items); 0082 items.clear(); 0083 } 0084 0085 // history for the text value 0086 for (int i = 0; i < uiSearchReplace.cbValueText->count(); ++i) 0087 items << uiSearchReplace.cbValueText->itemText(i); 0088 0089 if (!items.empty()) { 0090 conf.writeEntry("ValueTextHistory", items); 0091 items.clear(); 0092 } 0093 0094 // history for replace numeric values 0095 for (int i = 0; i < uiSearchReplace.cbReplace->count(); ++i) 0096 items << uiSearchReplace.cbReplace->itemText(i); 0097 0098 if (!items.empty()) { 0099 conf.writeEntry("ReplaceHistory", items); 0100 items.clear(); 0101 } 0102 0103 // history for replace numeric values 0104 for (int i = 0; i < uiSearchReplace.cbReplace->count(); ++i) 0105 items << uiSearchReplace.cbReplace->itemText(i); 0106 0107 if (!items.empty()) 0108 conf.writeEntry("ReplaceTextHistory", items); 0109 0110 // history for replace datetime value 0111 conf.writeEntry("ReplaceDateTimeHistory", uiSearchReplace.dteReplace->dateTime()); 0112 } 0113 } 0114 0115 void SearchReplaceWidget::setReplaceEnabled(bool enabled) { 0116 m_replaceEnabled = !enabled; 0117 switchFindReplace(); 0118 } 0119 0120 void SearchReplaceWidget::setInitialPattern(AbstractColumn::ColumnMode mode, const QString& pattern) { 0121 m_initialColumnMode = mode; 0122 0123 if (m_searchWidget) 0124 uiSearch.cbFind->setCurrentText(pattern); 0125 else if (m_searchReplaceWidget) { 0126 switch (m_initialColumnMode) { 0127 case AbstractColumn::ColumnMode::Text: 0128 uiSearchReplace.cbDataType->setCurrentIndex(uiSearchReplace.cbDataType->findData((int)DataType::Text)); 0129 uiSearchReplace.cbValueText->setCurrentText(pattern); 0130 break; 0131 case AbstractColumn::ColumnMode::Double: 0132 case AbstractColumn::ColumnMode::Integer: 0133 case AbstractColumn::ColumnMode::BigInt: 0134 uiSearchReplace.cbDataType->setCurrentIndex(uiSearchReplace.cbDataType->findData((int)DataType::Numeric)); 0135 ; 0136 bool ok; 0137 QLocale().toDouble(pattern, &ok); 0138 if (ok) 0139 uiSearchReplace.cbValue1->setCurrentText(pattern); 0140 else 0141 uiSearchReplace.cbValue1->setCurrentText(QString()); 0142 break; 0143 case AbstractColumn::ColumnMode::DateTime: 0144 case AbstractColumn::ColumnMode::Day: 0145 case AbstractColumn::ColumnMode::Month: 0146 uiSearchReplace.cbDataType->setCurrentIndex(uiSearchReplace.cbDataType->findData((int)DataType::DateTime)); 0147 auto value = QDateTime::fromString(pattern, defaultDateTimeFormat); 0148 if (value.isValid()) 0149 uiSearchReplace.dteValue1->setDateTime(value); 0150 else 0151 uiSearchReplace.dteValue1->setDateTime(QDateTime::currentDateTime()); 0152 break; 0153 } 0154 } 0155 } 0156 0157 void SearchReplaceWidget::setFocus() { 0158 if (m_replaceEnabled) 0159 uiSearchReplace.cbValueText->setFocus(); 0160 else 0161 uiSearch.cbFind->setFocus(); 0162 } 0163 0164 void SearchReplaceWidget::initSearchWidget() { 0165 m_searchWidget = new QWidget(this); 0166 uiSearch.setupUi(m_searchWidget); 0167 uiSearch.cbFind->lineEdit()->setClearButtonEnabled(true); 0168 static_cast<QVBoxLayout*>(layout())->insertWidget(0, m_searchWidget); 0169 0170 // restore saved settings if available 0171 KConfigGroup conf = Settings::group(QLatin1String("SearchReplaceWidget")); 0172 uiSearch.cbFind->addItems(conf.readEntry(QLatin1String("SimpleValueHistory"), QStringList())); 0173 uiSearch.cbFind->setCurrentText(QString()); // will be set to the initial search pattern later 0174 uiSearch.tbMatchCase->setChecked(conf.readEntry(QLatin1String("SimpleMatchCase"), false)); 0175 0176 // connections 0177 connect(uiSearch.cbFind->lineEdit(), &QLineEdit::returnPressed, this, [=]() { 0178 findNextSimple(true); 0179 addCurrentTextToHistory(uiSearch.cbFind); 0180 }); 0181 connect(uiSearch.cbFind->lineEdit(), &QLineEdit::textChanged, this, [=]() { 0182 m_patternFound = false; 0183 findNextSimple(false); 0184 }); 0185 0186 connect(uiSearch.tbFindNext, &QToolButton::clicked, this, [=]() { 0187 findNextSimple(true); 0188 addCurrentTextToHistory(uiSearch.cbFind); 0189 }); 0190 connect(uiSearch.tbFindPrev, &QToolButton::clicked, this, [=]() { 0191 findPreviousSimple(true); 0192 addCurrentTextToHistory(uiSearch.cbFind); 0193 }); 0194 0195 connect(uiSearch.tbMatchCase, &QToolButton::toggled, this, [=]() { 0196 findNextSimple(false); 0197 }); 0198 connect(uiSearch.tbSwitchFindReplace, &QToolButton::clicked, this, &SearchReplaceWidget::switchFindReplace); 0199 connect(uiSearch.bCancel, &QPushButton::clicked, this, &SearchReplaceWidget::cancel); 0200 } 0201 0202 void SearchReplaceWidget::initSearchReplaceWidget() { 0203 // init UI 0204 m_searchReplaceWidget = new QWidget(this); 0205 uiSearchReplace.setupUi(m_searchReplaceWidget); 0206 static_cast<QVBoxLayout*>(layout())->insertWidget(1, m_searchReplaceWidget); 0207 0208 uiSearchReplace.cbDataType->addItem(i18n("Text"), int(DataType::Text)); 0209 uiSearchReplace.cbDataType->addItem(i18n("Numeric"), int(DataType::Numeric)); 0210 uiSearchReplace.cbDataType->addItem(i18n("Date & Time"), int(DataType::DateTime)); 0211 0212 uiSearchReplace.cbOperatorText->addItem(i18n("Equal To"), int(OperatorText::EqualTo)); 0213 uiSearchReplace.cbOperatorText->addItem(i18n("Not Equal To"), int(OperatorText::NotEqualTo)); 0214 uiSearchReplace.cbOperatorText->addItem(i18n("Starts With"), int(OperatorText::StartsWith)); 0215 uiSearchReplace.cbOperatorText->addItem(i18n("Ends With"), int(OperatorText::EndsWith)); 0216 uiSearchReplace.cbOperatorText->addItem(i18n("Contains"), int(OperatorText::Contain)); 0217 uiSearchReplace.cbOperatorText->addItem(i18n("Does Not Contain"), int(OperatorText::NotContain)); 0218 uiSearchReplace.cbOperatorText->insertSeparator(6); 0219 uiSearchReplace.cbOperatorText->addItem(i18n("Regular Expression"), int(OperatorText::RegEx)); 0220 0221 uiSearchReplace.cbOperator->addItem(i18n("Equal to"), int(Operator::EqualTo)); 0222 uiSearchReplace.cbOperator->addItem(i18n("Not Equal to"), int(Operator::NotEqualTo)); 0223 uiSearchReplace.cbOperator->addItem(i18n("Between (Incl. End Points)"), int(Operator::BetweenIncl)); 0224 uiSearchReplace.cbOperator->addItem(i18n("Between (Excl. End Points)"), int(Operator::BetweenExcl)); 0225 uiSearchReplace.cbOperator->addItem(i18n("Greater than"), int(Operator::GreaterThan)); 0226 uiSearchReplace.cbOperator->addItem(i18n("Greater than or Equal to"), int(Operator::GreaterThanEqualTo)); 0227 uiSearchReplace.cbOperator->addItem(i18n("Less than"), int(Operator::LessThan)); 0228 uiSearchReplace.cbOperator->addItem(i18n("Less than or Equal to"), int(Operator::LessThanEqualTo)); 0229 0230 uiSearchReplace.cbOperatorDateTime->addItem(i18n("Equal to"), int(Operator::EqualTo)); 0231 uiSearchReplace.cbOperatorDateTime->addItem(i18n("Not Equal to"), int(Operator::NotEqualTo)); 0232 uiSearchReplace.cbOperatorDateTime->addItem(i18n("Between (Incl. End Points)"), int(Operator::BetweenIncl)); 0233 uiSearchReplace.cbOperatorDateTime->addItem(i18n("Between (Excl. End Points)"), int(Operator::BetweenExcl)); 0234 uiSearchReplace.cbOperatorDateTime->addItem(i18n("Greater than"), int(Operator::GreaterThan)); 0235 uiSearchReplace.cbOperatorDateTime->addItem(i18n("Greater than or Equal to"), int(Operator::GreaterThanEqualTo)); 0236 uiSearchReplace.cbOperatorDateTime->addItem(i18n("Less than"), int(Operator::LessThan)); 0237 uiSearchReplace.cbOperatorDateTime->addItem(i18n("Less than or Equal to"), int(Operator::LessThanEqualTo)); 0238 0239 uiSearchReplace.cbValueText->lineEdit()->setClearButtonEnabled(true); 0240 uiSearchReplace.cbValue1->lineEdit()->setClearButtonEnabled(true); 0241 uiSearchReplace.cbValue2->lineEdit()->setClearButtonEnabled(true); 0242 0243 uiSearchReplace.cbValue1->lineEdit()->setValidator(new QDoubleValidator(uiSearchReplace.cbValue1->lineEdit())); 0244 uiSearchReplace.cbValue2->lineEdit()->setValidator(new QDoubleValidator(uiSearchReplace.cbValue2->lineEdit())); 0245 0246 uiSearchReplace.cbOrder->addItem(i18n("Column Major"), int(Order::ColumnMajor)); 0247 uiSearchReplace.cbOrder->addItem(i18n("Row Major"), int(Order::RowMajor)); 0248 0249 // set meaninungful non-empty initial value for DateTime so the user doesn't need to type 0250 // everything from scratch when switching to DateTime type 0251 auto now = QDateTime::currentDateTime(); 0252 uiSearchReplace.dteValue1->setDateTime(now); 0253 uiSearchReplace.dteValue2->setDateTime(now); 0254 0255 uiSearchReplace.dteValue1->setDisplayFormat(defaultDateTimeFormat); 0256 uiSearchReplace.dteValue2->setDisplayFormat(defaultDateTimeFormat); 0257 uiSearchReplace.dteReplace->setDisplayFormat(defaultDateTimeFormat); 0258 0259 // restore saved settings if available 0260 KConfigGroup conf = Settings::group(QLatin1String("SearchReplaceWidget")); 0261 uiSearchReplace.cbDataType->setCurrentIndex(uiSearchReplace.cbOperator->findData(conf.readEntry("DataType", 0))); 0262 uiSearchReplace.cbOrder->setCurrentIndex(uiSearchReplace.cbOperator->findData(conf.readEntry("Order", 0))); 0263 uiSearchReplace.tbMatchCase->setChecked(conf.readEntry("MatchCase", false)); 0264 uiSearchReplace.tbSelectionOnly->setChecked(conf.readEntry("SelectionOnly", false)); 0265 uiSearchReplace.cbOperator->setCurrentIndex(uiSearchReplace.cbOperator->findData(conf.readEntry("Operator", 0))); 0266 uiSearchReplace.cbOperatorText->setCurrentIndex(uiSearchReplace.cbOperatorText->findData(conf.readEntry("OperatorText", 0))); 0267 uiSearchReplace.cbOperatorDateTime->setCurrentIndex(uiSearchReplace.cbOperatorDateTime->findData(conf.readEntry("OperatorDateTime", 0))); 0268 0269 dataTypeChanged(uiSearchReplace.cbDataType->currentIndex()); 0270 operatorChanged(uiSearchReplace.cbOperator->currentIndex()); 0271 operatorDateTimeChanged(uiSearchReplace.cbOperatorDateTime->currentIndex()); 0272 0273 // history 0274 uiSearchReplace.cbValue1->addItems(conf.readEntry("Value1History", QStringList())); 0275 uiSearchReplace.cbValue1->setCurrentText(QString()); // will be set to the initial search pattern later 0276 uiSearchReplace.cbValue2->addItems(conf.readEntry("Value2History", QStringList())); 0277 uiSearchReplace.cbValue2->setCurrentText(QString()); 0278 uiSearchReplace.cbValueText->addItems(conf.readEntry("ValueTextHistory", QStringList())); 0279 uiSearchReplace.cbValueText->setCurrentText(QString()); 0280 uiSearchReplace.cbReplace->addItems(conf.readEntry("ReplaceHistory", QStringList())); 0281 uiSearchReplace.cbReplace->setCurrentText(QString()); 0282 uiSearchReplace.cbReplaceText->addItems(conf.readEntry("ReplaceTextHistory", QStringList())); 0283 uiSearchReplace.cbReplaceText->setCurrentText(QString()); 0284 uiSearchReplace.dteReplace->setDateTime(conf.readEntry("ReplaceDateTimeHistory", QDateTime::currentDateTime())); 0285 0286 // connections 0287 connect(uiSearchReplace.cbDataType, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SearchReplaceWidget::dataTypeChanged); 0288 0289 connect(uiSearchReplace.cbOperator, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SearchReplaceWidget::operatorChanged); 0290 connect(uiSearchReplace.cbOperatorText, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [=]() { 0291 findNext(true); 0292 }); 0293 connect(uiSearchReplace.cbOperatorDateTime, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SearchReplaceWidget::operatorDateTimeChanged); 0294 0295 connect(uiSearchReplace.cbValue1->lineEdit(), &QLineEdit::returnPressed, this, [=]() { 0296 findNext(true); 0297 }); 0298 connect(uiSearchReplace.cbValue2->lineEdit(), &QLineEdit::returnPressed, this, [=]() { 0299 findNext(true); 0300 }); 0301 connect(uiSearchReplace.cbValueText->lineEdit(), &QLineEdit::returnPressed, this, [=]() { 0302 findNext(true); 0303 }); 0304 connect(uiSearchReplace.dteValue1, &QDateTimeEdit::dateTimeChanged, this, [=]() { 0305 m_patternFound = false; 0306 findNext(false); 0307 }); 0308 connect(uiSearchReplace.dteValue2, &QDateTimeEdit::dateTimeChanged, this, [=]() { 0309 m_patternFound = false; 0310 findNext(false); 0311 }); 0312 connect(uiSearchReplace.tbFindNext, &QToolButton::clicked, this, [=]() { 0313 findNext(true); 0314 }); 0315 connect(uiSearchReplace.tbFindPrev, &QToolButton::clicked, this, [=]() { 0316 findPrevious(true); 0317 }); 0318 connect(uiSearchReplace.bFindAll, &QPushButton::clicked, this, &SearchReplaceWidget::findAll); 0319 0320 connect(uiSearchReplace.cbReplace->lineEdit(), &QLineEdit::returnPressed, this, &SearchReplaceWidget::replaceNext); 0321 connect(uiSearchReplace.bReplaceNext, &QPushButton::clicked, this, &SearchReplaceWidget::replaceNext); 0322 connect(uiSearchReplace.bReplaceAll, &QPushButton::clicked, this, &SearchReplaceWidget::replaceAll); 0323 connect(uiSearchReplace.tbMatchCase, &QToolButton::toggled, this, [=]() { 0324 findNext(false); 0325 }); 0326 0327 connect(uiSearchReplace.tbSwitchFindReplace, &QToolButton::clicked, this, &SearchReplaceWidget::switchFindReplace); 0328 connect(uiSearchReplace.bCancel, &QPushButton::clicked, this, &SearchReplaceWidget::cancel); 0329 0330 // custom context menus for LineEdit in ComboBox 0331 uiSearchReplace.cbValueText->setContextMenuPolicy(Qt::CustomContextMenu); 0332 connect(uiSearchReplace.cbValueText, 0333 &QComboBox::customContextMenuRequested, 0334 this, 0335 QOverload<const QPoint&>::of(&SearchReplaceWidget::findContextMenuRequest)); 0336 0337 uiSearchReplace.cbReplaceText->setContextMenuPolicy(Qt::CustomContextMenu); 0338 connect(uiSearchReplace.cbReplaceText, 0339 &QComboBox::customContextMenuRequested, 0340 this, 0341 QOverload<const QPoint&>::of(&SearchReplaceWidget::replaceContextMenuRequest)); 0342 } 0343 0344 void SearchReplaceWidget::addCurrentTextToHistory(QComboBox* comboBox) const { 0345 const QString& text = comboBox->currentText(); 0346 if (text.isEmpty()) 0347 return; 0348 0349 const int index = comboBox->findText(text); 0350 0351 if (index > 0) 0352 comboBox->removeItem(index); 0353 0354 if (index != 0) { 0355 comboBox->insertItem(0, text); 0356 comboBox->setCurrentIndex(0); 0357 } 0358 } 0359 0360 void SearchReplaceWidget::setDataType(DataType dataType) { 0361 uiSearchReplace.cbDataType->setCurrentIndex(uiSearchReplace.cbDataType->findData((int)dataType)); 0362 } 0363 0364 void SearchReplaceWidget::setOrder(Order order) { 0365 uiSearchReplace.cbOrder->setCurrentIndex(uiSearchReplace.cbOrder->findData((int)order)); 0366 } 0367 0368 void SearchReplaceWidget::setTextOperator(OperatorText op) { 0369 uiSearchReplace.cbOperatorText->setCurrentIndex(uiSearchReplace.cbOperatorText->findData((int)op)); 0370 } 0371 0372 void SearchReplaceWidget::setReplaceText(const QString& text) { 0373 switch (m_initialColumnMode) { 0374 case AbstractColumn::ColumnMode::Text: 0375 uiSearchReplace.cbReplaceText->setCurrentText(text); 0376 break; 0377 case AbstractColumn::ColumnMode::Double: 0378 case AbstractColumn::ColumnMode::Integer: 0379 case AbstractColumn::ColumnMode::BigInt: 0380 uiSearchReplace.cbReplace->setCurrentText(text); 0381 break; 0382 case AbstractColumn::ColumnMode::DateTime: 0383 case AbstractColumn::ColumnMode::Day: 0384 case AbstractColumn::ColumnMode::Month: 0385 uiSearchReplace.dteReplace->setDateTime(QDateTime::fromString(text, defaultDateTimeFormat)); 0386 break; 0387 } 0388 0389 uiSearchReplace.cbReplace->setCurrentText(text); 0390 } 0391 0392 // ********************************************************** 0393 // ************************* SLOTs ************************** 0394 // ********************************************************** 0395 void SearchReplaceWidget::cancel() { 0396 m_spreadsheet->model()->setSearchText(QString()); // clear the global search text that was potentialy set during "find all" 0397 showMessage(QString()); 0398 close(); 0399 } 0400 0401 void SearchReplaceWidget::findContextMenuRequest(const QPoint& pos) { 0402 showExtendedContextMenu(false /* replace */, pos); 0403 } 0404 0405 void SearchReplaceWidget::replaceContextMenuRequest(const QPoint& pos) { 0406 showExtendedContextMenu(true /* replace */, pos); 0407 } 0408 0409 void SearchReplaceWidget::dataTypeChanged(int index) { 0410 const auto type = static_cast<DataType>(index); 0411 switch (type) { 0412 case DataType::Text: { 0413 // show text 0414 uiSearchReplace.frameText->show(); 0415 uiSearchReplace.lReplaceText->show(); 0416 uiSearchReplace.cbReplaceText->show(); 0417 uiSearchReplace.cbReplaceText->lineEdit()->setValidator(nullptr); 0418 uiSearchReplace.tbMatchCase->show(); 0419 0420 // hide numeric 0421 uiSearchReplace.frameNumeric->hide(); 0422 uiSearchReplace.lReplace->hide(); 0423 uiSearchReplace.cbReplace->hide(); 0424 0425 // hide datetime 0426 uiSearchReplace.frameDateTime->hide(); 0427 uiSearchReplace.lReplaceDateTime->hide(); 0428 uiSearchReplace.dteReplace->hide(); 0429 0430 break; 0431 } 0432 case DataType::Numeric: { 0433 // show numeric 0434 uiSearchReplace.frameNumeric->show(); 0435 uiSearchReplace.lReplace->show(); 0436 uiSearchReplace.cbReplace->show(); 0437 0438 // hide text 0439 uiSearchReplace.frameText->hide(); 0440 uiSearchReplace.lReplaceText->hide(); 0441 uiSearchReplace.cbReplaceText->hide(); 0442 uiSearchReplace.tbMatchCase->hide(); 0443 0444 // hide datetime 0445 uiSearchReplace.frameDateTime->hide(); 0446 uiSearchReplace.lReplaceDateTime->hide(); 0447 uiSearchReplace.dteReplace->hide(); 0448 0449 // clear the replace pattern if it doesn't represent a numeric value 0450 const auto& pattern = uiSearchReplace.cbReplace->currentText(); 0451 bool ok; 0452 QLocale().toDouble(pattern, &ok); 0453 if (!ok) 0454 uiSearchReplace.cbReplace->setCurrentText(QString()); 0455 uiSearchReplace.cbReplace->lineEdit()->setValidator(new QDoubleValidator(uiSearchReplace.cbReplace->lineEdit())); 0456 break; 0457 } 0458 case DataType::DateTime: { 0459 // show datetime 0460 uiSearchReplace.frameDateTime->show(); 0461 uiSearchReplace.lReplaceDateTime->show(); 0462 uiSearchReplace.dteReplace->show(); 0463 0464 // hide text 0465 uiSearchReplace.frameText->hide(); 0466 uiSearchReplace.lReplaceText->hide(); 0467 uiSearchReplace.cbReplaceText->hide(); 0468 uiSearchReplace.tbMatchCase->hide(); 0469 0470 // hide numeric 0471 uiSearchReplace.frameNumeric->hide(); 0472 uiSearchReplace.lReplace->hide(); 0473 uiSearchReplace.cbReplace->hide(); 0474 0475 // clear the replace pattern if it doesn't represent a datetime value 0476 const auto& pattern = uiSearchReplace.cbReplace->currentText(); 0477 QDateTime value = QDateTime::fromString(pattern, defaultDateTimeFormat); 0478 if (!value.isValid()) 0479 uiSearchReplace.cbReplace->setCurrentText(QString()); 0480 0481 break; 0482 } 0483 } 0484 } 0485 0486 void SearchReplaceWidget::operatorChanged(int /* index */) const { 0487 const auto op = static_cast<Operator>(uiSearchReplace.cbOperator->currentData().toInt()); 0488 bool visible = (op == Operator::BetweenIncl) || (op == Operator::BetweenExcl); 0489 0490 uiSearchReplace.lMin->setVisible(visible); 0491 uiSearchReplace.lMax->setVisible(visible); 0492 uiSearchReplace.lAnd->setVisible(visible); 0493 uiSearchReplace.cbValue2->setVisible(visible); 0494 } 0495 0496 void SearchReplaceWidget::operatorDateTimeChanged(int /* index */) const { 0497 const auto op = static_cast<Operator>(uiSearchReplace.cbOperatorDateTime->currentData().toInt()); 0498 bool visible = (op == Operator::BetweenIncl) || (op == Operator::BetweenExcl); 0499 0500 uiSearchReplace.lMinDateTime->setVisible(visible); 0501 uiSearchReplace.lMaxDateTime->setVisible(visible); 0502 uiSearchReplace.lAndDateTime->setVisible(visible); 0503 uiSearchReplace.dteValue2->setVisible(visible); 0504 } 0505 0506 // settings 0507 void SearchReplaceWidget::switchFindReplace() { 0508 m_replaceEnabled = !m_replaceEnabled; 0509 if (m_replaceEnabled) { // show the find&replace widget 0510 if (!m_searchReplaceWidget) 0511 initSearchReplaceWidget(); 0512 0513 m_searchReplaceWidget->show(); 0514 0515 // make the first column in the different QFrames having the same width 0516 // TODO: doesn't work on first open 0517 uiSearchReplace.cbDataType->setMinimumWidth(uiSearchReplace.cbOperator->width()); 0518 uiSearchReplace.cbOrder->setMinimumWidth(uiSearchReplace.cbOperator->width()); 0519 uiSearchReplace.cbOperatorText->setMinimumWidth(uiSearchReplace.cbOperator->width()); 0520 uiSearchReplace.cbOperatorDateTime->setMinimumWidth(uiSearchReplace.cbOperator->width()); 0521 0522 if (m_searchWidget) { 0523 // switching from simple to advanced search, 0524 // take over the current search pattern it it's valid for the current data type 0525 const auto type = static_cast<DataType>(uiSearchReplace.cbDataType->currentIndex()); 0526 const auto& pattern = uiSearch.cbFind->currentText(); 0527 switch (type) { 0528 case DataType::Text: { 0529 uiSearchReplace.cbValueText->setCurrentText(pattern); 0530 break; 0531 } 0532 case DataType::Numeric: { 0533 bool ok; 0534 QLocale().toDouble(pattern, &ok); 0535 if (ok) 0536 uiSearchReplace.cbValue1->setCurrentText(pattern); 0537 else 0538 uiSearchReplace.cbValue1->setCurrentText(QString()); 0539 break; 0540 } 0541 case DataType::DateTime: { 0542 QDateTime value = QDateTime::fromString(pattern, defaultDateTimeFormat); 0543 if (value.isValid()) 0544 uiSearchReplace.dteValue1->setDateTime(value); 0545 else 0546 uiSearchReplace.dteValue1->setDateTime(QDateTime::currentDateTime()); 0547 break; 0548 } 0549 } 0550 0551 m_searchWidget->hide(); 0552 } 0553 } else { // show the find widget 0554 if (!m_searchWidget) 0555 initSearchWidget(); 0556 0557 m_searchWidget->show(); 0558 0559 if (m_searchReplaceWidget) { 0560 // switching from advanced to simple search, show the current search pattern 0561 const auto type = static_cast<DataType>(uiSearchReplace.cbDataType->currentIndex()); 0562 switch (type) { 0563 case DataType::Text: { 0564 uiSearch.cbFind->setCurrentText(uiSearchReplace.cbValueText->currentText()); 0565 break; 0566 } 0567 case DataType::Numeric: { 0568 uiSearch.cbFind->setCurrentText(uiSearchReplace.cbValue1->currentText()); 0569 break; 0570 } 0571 case DataType::DateTime: { 0572 uiSearch.cbFind->setCurrentText(uiSearchReplace.dteValue1->text()); 0573 break; 0574 } 0575 } 0576 0577 m_searchReplaceWidget->hide(); 0578 } 0579 } 0580 0581 showMessage(QString()); 0582 } 0583 0584 // ********************************************************** 0585 // ************** simple find functions ******************* 0586 // ********************************************************** 0587 /*! 0588 * search the next cell in the column-major order that matches 0589 * to the specified pattern. The search is done ignoring the data type 0590 * and iterpreting everything as text. Used in the "simple search"-mode. 0591 */ 0592 bool SearchReplaceWidget::findNextSimple(bool proceed) { 0593 const QString& pattern = uiSearch.cbFind->currentText(); 0594 if (pattern.isEmpty()) { 0595 GuiTools::highlight(uiSearch.cbFind->lineEdit(), false); 0596 return true; 0597 } 0598 0599 const auto cs = uiSearch.tbMatchCase->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive; 0600 0601 // spreadsheet size and the start cell 0602 const int colCount = m_spreadsheet->columnCount(); 0603 const int rowCount = m_spreadsheet->rowCount(); 0604 int curRow = m_view->firstSelectedRow(); 0605 int curCol = m_view->firstSelectedColumn(); 0606 0607 if (proceed) { 0608 if (curRow != rowCount - 1) 0609 ++curRow; // not the last row yet, navigate to the next row 0610 else { 0611 // last row 0612 if (curCol != colCount - 1) { 0613 // not the last column yet, navigate to the first row in the next column 0614 ++curCol; 0615 curRow = 0; 0616 } else { 0617 // last row in the last column, cannot navigate any further 0618 GuiTools::highlight(uiSearch.cbFind->lineEdit(), !m_patternFound); 0619 return false; 0620 } 0621 } 0622 } 0623 0624 // all settings are determined -> search the next cell matching the specified pattern(s) 0625 const auto& columns = m_spreadsheet->children<Column>(); 0626 bool startCol = true; 0627 bool startRow = true; 0628 0629 // search in the column-major order ignoring the data type 0630 // and iterpreting everything as text 0631 for (int col = 0; col < colCount; ++col) { 0632 if (startCol && col < curCol) 0633 continue; 0634 0635 auto* column = columns.at(col)->asStringColumn(); 0636 0637 for (int row = 0; row < rowCount; ++row) { 0638 if (startRow && row < curRow) 0639 continue; 0640 0641 if (column->textAt(row).contains(pattern, cs)) { 0642 m_patternFound = true; 0643 m_view->goToCell(row, col); 0644 GuiTools::highlight(uiSearch.cbFind->lineEdit(), false); 0645 return true; 0646 } 0647 0648 startRow = false; 0649 } 0650 0651 startCol = false; 0652 } 0653 0654 GuiTools::highlight(uiSearch.cbFind->lineEdit(), !m_patternFound); 0655 showMessage(QString()); 0656 return false; 0657 } 0658 0659 /*! 0660 * search the previous cell in the column-major order that matches 0661 * to the specified pattern. The search is done ignoring the data type 0662 * and iterpreting everything as text. Used in the "simple search"-mode. 0663 */ 0664 bool SearchReplaceWidget::findPreviousSimple(bool proceed) { 0665 const QString& pattern = uiSearch.cbFind->currentText(); 0666 if (pattern.isEmpty()) { 0667 GuiTools::highlight(uiSearch.cbFind->lineEdit(), false); 0668 showMessage(QString()); 0669 return true; 0670 } 0671 0672 const auto cs = uiSearch.tbMatchCase->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive; 0673 0674 // spreadsheet size and the start cell 0675 const int colCount = m_spreadsheet->columnCount(); 0676 const int rowCount = m_spreadsheet->rowCount(); 0677 int curRow = m_view->firstSelectedRow(); 0678 int curCol = m_view->firstSelectedColumn(); 0679 0680 if (proceed) { 0681 if (curRow > 0) 0682 --curRow; // not the first row yet, navigate to the previous cell 0683 else { 0684 // first row 0685 if (curCol > 0) { 0686 // not the first column yet, navigate to the last row in the previous column 0687 --curCol; 0688 curRow = rowCount - 1; 0689 } else { 0690 // first row in the first column, cannot navigate any further 0691 GuiTools::highlight(uiSearch.cbFind->lineEdit(), !m_patternFound); 0692 return false; 0693 } 0694 } 0695 } 0696 0697 // all settings are determined -> search the next cell matching the specified pattern(s) 0698 const auto& columns = m_spreadsheet->children<Column>(); 0699 bool startCol = true; 0700 bool startRow = true; 0701 0702 for (int col = colCount; col >= 0; --col) { 0703 if (startCol && col > curCol) 0704 continue; 0705 0706 auto* column = columns.at(col)->asStringColumn(); 0707 0708 for (int row = rowCount; row >= 0; --row) { 0709 if (startRow && row > curRow) 0710 continue; 0711 0712 if (column->textAt(row).contains(pattern, cs)) { 0713 m_patternFound = true; 0714 m_view->goToCell(row, col); 0715 GuiTools::highlight(uiSearch.cbFind->lineEdit(), false); 0716 return true; 0717 } 0718 0719 startRow = false; 0720 } 0721 0722 startCol = false; 0723 } 0724 0725 GuiTools::highlight(uiSearch.cbFind->lineEdit(), !m_patternFound); 0726 showMessage(QString()); 0727 return false; 0728 } 0729 0730 // ********************************************************** 0731 // **** advanced and data type specific find functions **** 0732 // ********************************************************** 0733 bool SearchReplaceWidget::findNext(bool proceed, bool findAndReplace) { 0734 // search pattern(s) 0735 const auto type = static_cast<DataType>(uiSearchReplace.cbDataType->currentIndex()); 0736 QString pattern1; 0737 QString pattern2; 0738 QString replaceValue; 0739 switch (type) { 0740 case DataType::Text: 0741 pattern1 = uiSearchReplace.cbValueText->currentText(); 0742 addCurrentTextToHistory(uiSearchReplace.cbValueText); 0743 if (findAndReplace) { 0744 replaceValue = uiSearchReplace.cbReplaceText->currentText(); 0745 addCurrentTextToHistory(uiSearchReplace.cbReplaceText); 0746 } 0747 break; 0748 case DataType::Numeric: 0749 pattern1 = uiSearchReplace.cbValue1->currentText(); 0750 pattern2 = uiSearchReplace.cbValue2->currentText(); 0751 addCurrentTextToHistory(uiSearchReplace.cbValue1); 0752 addCurrentTextToHistory(uiSearchReplace.cbValue2); 0753 if (findAndReplace) { 0754 replaceValue = uiSearchReplace.cbReplace->currentText(); 0755 addCurrentTextToHistory(uiSearchReplace.cbReplace); 0756 } 0757 break; 0758 case DataType::DateTime: 0759 pattern1 = uiSearchReplace.dteValue1->text(); 0760 pattern2 = uiSearchReplace.dteValue2->text(); 0761 if (findAndReplace) 0762 replaceValue = uiSearchReplace.dteReplace->text(); 0763 break; 0764 } 0765 0766 if (pattern1.isEmpty()) { 0767 highlight(type, false); 0768 return true; 0769 } 0770 0771 if (findAndReplace && replaceValue.isEmpty()) 0772 return false; 0773 0774 // settings 0775 const auto opText = static_cast<OperatorText>(uiSearchReplace.cbOperatorText->currentData().toInt()); 0776 const auto opNumeric = static_cast<Operator>(uiSearchReplace.cbOperator->currentData().toInt()); 0777 const auto opDateTime = static_cast<Operator>(uiSearchReplace.cbOperatorDateTime->currentData().toInt()); 0778 const auto cs = uiSearchReplace.tbMatchCase->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive; 0779 const bool columnMajor = (uiSearchReplace.cbOrder->currentIndex() == 0); 0780 0781 // spreadsheet size and the start cell 0782 const int colCount = m_spreadsheet->columnCount(); 0783 const int rowCount = m_spreadsheet->rowCount(); 0784 int curRow = m_view->firstSelectedRow(); 0785 int curCol = m_view->firstSelectedColumn(); 0786 0787 if (columnMajor && proceed) { 0788 if (curRow != rowCount - 1) 0789 ++curRow; // not the last row yet, navigate to the next row 0790 else { 0791 // last row 0792 if (curCol != colCount - 1) { 0793 // not the last column yet, navigate to the first row in the next column 0794 ++curCol; 0795 curRow = 0; 0796 } else { 0797 // last row in the last column, cannot navigate any further 0798 highlight(type, !m_patternFound); 0799 return false; 0800 } 0801 } 0802 } 0803 0804 if (!columnMajor && proceed) { 0805 if (curCol != colCount - 1) 0806 ++curCol; // not the last column yet, navigate to the next column 0807 else { 0808 // last column 0809 if (curRow != rowCount - 1) { 0810 // not the last row yet, navigate to the next row in the first column 0811 ++curRow; 0812 curCol = 0; 0813 } else { 0814 // last row in the last column, cannot navigate any further 0815 highlight(type, !m_patternFound); 0816 return false; 0817 } 0818 } 0819 } 0820 0821 // all settings are determined -> search the next cell matching the specified pattern(s) 0822 const auto& columns = m_spreadsheet->children<Column>(); 0823 bool startCol = true; 0824 bool startRow = true; 0825 bool match = false; 0826 0827 if (columnMajor) { 0828 for (int col = 0; col < colCount; ++col) { 0829 if (startCol && col < curCol) 0830 continue; 0831 0832 auto* column = columns.at(col); 0833 if (!checkColumnType(column, type)) { 0834 startRow = false; 0835 continue; 0836 } 0837 0838 for (int row = 0; row < rowCount; ++row) { 0839 if (startRow && row < curRow) 0840 continue; 0841 0842 match = checkColumnRow(column, type, row, opText, opNumeric, opDateTime, pattern1, pattern2, cs); 0843 if (match) { 0844 m_patternFound = true; 0845 m_view->goToCell(row, col); 0846 if (findAndReplace) 0847 setValue(column, type, row, replaceValue); 0848 highlight(type, false); 0849 return true; 0850 } 0851 0852 startRow = false; 0853 } 0854 0855 startCol = false; 0856 } 0857 } else { // row-major 0858 for (int row = 0; row < rowCount; ++row) { 0859 if (startRow && row < curRow) 0860 continue; 0861 0862 for (int col = 0; col < colCount; ++col) { 0863 if (startCol && col < curCol) 0864 continue; 0865 0866 auto* column = columns.at(col); 0867 if (!checkColumnType(column, type)) { 0868 startCol = false; 0869 continue; 0870 } 0871 0872 match = checkColumnRow(column, type, row, opText, opNumeric, opDateTime, pattern1, pattern2, cs); 0873 if (match) { 0874 m_patternFound = true; 0875 m_view->goToCell(row, col); 0876 if (findAndReplace) 0877 setValue(column, type, row, replaceValue); 0878 highlight(type, false); 0879 return true; 0880 } 0881 0882 startCol = false; 0883 } 0884 0885 startRow = false; 0886 } 0887 } 0888 0889 highlight(type, !m_patternFound); 0890 return false; 0891 } 0892 0893 bool SearchReplaceWidget::findPrevious(bool proceed) { 0894 // search pattern(s) 0895 const auto type = static_cast<DataType>(uiSearchReplace.cbDataType->currentIndex()); 0896 QString pattern1; 0897 QString pattern2; 0898 switch (type) { 0899 case DataType::Text: 0900 pattern1 = uiSearchReplace.cbValueText->currentText(); 0901 addCurrentTextToHistory(uiSearchReplace.cbValueText); 0902 break; 0903 case DataType::Numeric: 0904 pattern1 = uiSearchReplace.cbValue1->currentText(); 0905 pattern2 = uiSearchReplace.cbValue2->currentText(); 0906 addCurrentTextToHistory(uiSearchReplace.cbValue1); 0907 addCurrentTextToHistory(uiSearchReplace.cbValue2); 0908 break; 0909 case DataType::DateTime: 0910 pattern1 = uiSearchReplace.dteValue1->text(); 0911 pattern2 = uiSearchReplace.dteValue2->text(); 0912 break; 0913 } 0914 0915 if (pattern1.isEmpty()) { 0916 highlight(type, false); 0917 return true; 0918 } 0919 0920 // settings 0921 const auto opText = static_cast<OperatorText>(uiSearchReplace.cbOperatorText->currentData().toInt()); 0922 const auto opNumeric = static_cast<Operator>(uiSearchReplace.cbOperator->currentData().toInt()); 0923 const auto opDateTime = static_cast<Operator>(uiSearchReplace.cbOperatorDateTime->currentData().toInt()); 0924 const auto cs = uiSearchReplace.tbMatchCase->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive; 0925 const bool columnMajor = (uiSearchReplace.cbOrder->currentIndex() == 0); 0926 0927 // spreadsheet size and the start cell 0928 const int colCount = m_spreadsheet->columnCount(); 0929 const int rowCount = m_spreadsheet->rowCount(); 0930 int curRow = m_view->firstSelectedRow(); 0931 int curCol = m_view->firstSelectedColumn(); 0932 0933 if (columnMajor && proceed) { 0934 if (curRow > 0) 0935 --curRow; // not the first row yet, navigate to the previous cell 0936 else { 0937 // first row 0938 if (curCol > 0) { 0939 // not the first column yet, navigate to the last row in the previous column 0940 --curCol; 0941 curRow = rowCount - 1; 0942 } else { 0943 // first row in the first column, cannot navigate any further 0944 highlight(type, !m_patternFound); 0945 return false; 0946 } 0947 } 0948 } 0949 0950 if (!columnMajor && proceed) { 0951 if (curCol > 0) 0952 --curCol; // not the first column yet, navigate to the previous column 0953 else { 0954 // first column 0955 if (curRow > 0) { 0956 // not the first row yet, navigate to the previous row in the last column 0957 --curRow; 0958 curCol = colCount - 1; 0959 } else { 0960 // first row in the first column, cannot navigate any further 0961 highlight(type, !m_patternFound); 0962 return false; 0963 } 0964 } 0965 } 0966 0967 // all settings are determined -> search the next cell matching the specified pattern(s) 0968 const auto& columns = m_spreadsheet->children<Column>(); 0969 bool startCol = true; 0970 bool startRow = true; 0971 bool match = false; 0972 0973 if (columnMajor) { 0974 for (int col = colCount - 1; col >= 0; --col) { 0975 if (startCol && col > curCol) 0976 continue; 0977 0978 auto* column = columns.at(col); 0979 if (!checkColumnType(column, type)) { 0980 startCol = false; 0981 continue; 0982 } 0983 0984 for (int row = rowCount - 1; row >= 0; --row) { 0985 if (startRow && row > curRow) 0986 continue; 0987 0988 match = checkColumnRow(column, type, row, opText, opNumeric, opDateTime, pattern1, pattern2, cs); 0989 if (match) { 0990 m_patternFound = true; 0991 m_view->goToCell(row, col); 0992 highlight(type, false); 0993 return true; 0994 } 0995 0996 startRow = false; 0997 } 0998 0999 startCol = false; 1000 } 1001 } else { // row-major 1002 for (int row = rowCount - 1; row >= 0; --row) { 1003 if (startRow && row > curRow) 1004 continue; 1005 1006 for (int col = colCount - 1; col >= 0; --col) { 1007 if (startCol && col > curCol) 1008 continue; 1009 1010 auto* column = columns.at(col); 1011 if (!checkColumnType(column, type)) { 1012 startCol = false; 1013 continue; 1014 } 1015 1016 match = checkColumnRow(column, type, row, opText, opNumeric, opDateTime, pattern1, pattern2, cs); 1017 if (match) { 1018 m_patternFound = true; 1019 m_view->goToCell(row, col); 1020 highlight(type, false); 1021 return true; 1022 } 1023 1024 startCol = false; 1025 } 1026 1027 startRow = false; 1028 } 1029 } 1030 1031 highlight(type, !m_patternFound); 1032 return false; 1033 } 1034 1035 void SearchReplaceWidget::findAll() { 1036 const auto type = static_cast<DataType>(uiSearchReplace.cbDataType->currentIndex()); 1037 QString pattern1; 1038 QString pattern2; 1039 switch (type) { 1040 case DataType::Text: 1041 pattern1 = uiSearchReplace.cbValueText->currentText(); 1042 break; 1043 case DataType::Numeric: 1044 pattern1 = uiSearchReplace.cbValue1->currentText(); 1045 pattern2 = uiSearchReplace.cbValue2->currentText(); 1046 break; 1047 case DataType::DateTime: 1048 pattern1 = uiSearchReplace.dteValue1->text(); 1049 pattern1 = uiSearchReplace.dteValue2->text(); 1050 break; 1051 } 1052 1053 if (pattern1.isEmpty()) { 1054 highlight(type, false); 1055 return; 1056 } 1057 1058 // clear the previous selection 1059 m_view->clearSelection(); 1060 1061 // settings 1062 const auto opText = static_cast<OperatorText>(uiSearchReplace.cbOperatorText->currentData().toInt()); 1063 const auto opNumeric = static_cast<Operator>(uiSearchReplace.cbOperator->currentData().toInt()); 1064 const auto opDateTime = static_cast<Operator>(uiSearchReplace.cbOperatorDateTime->currentData().toInt()); 1065 const auto cs = uiSearchReplace.tbMatchCase->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive; 1066 const int colCount = m_spreadsheet->columnCount(); 1067 const int rowCount = m_spreadsheet->rowCount(); 1068 1069 // all settings are determined -> select all cells matching the specified pattern(s) 1070 const auto& columns = m_spreadsheet->children<Column>(); 1071 bool match = false; 1072 int matchCount = 0; 1073 1074 for (int col = 0; col < colCount; ++col) { 1075 auto* column = columns.at(col); 1076 if (!checkColumnType(column, type)) 1077 continue; 1078 1079 for (int row = 0; row < rowCount; ++row) { 1080 match = checkColumnRow(column, type, row, opText, opNumeric, opDateTime, pattern1, pattern2, cs); 1081 if (match) { 1082 m_view->selectCell(row, col); 1083 ++matchCount; 1084 } 1085 } 1086 } 1087 1088 if (matchCount > 0) 1089 showMessage(i18np("%1 match found", "%1 matches found", matchCount)); 1090 else 1091 showMessage(QString()); 1092 } 1093 1094 void SearchReplaceWidget::replaceNext() { 1095 // check the current cell first (no proceed) whether the value matches and needs to be replaced. 1096 // if it doesn't match, proceed to the next cell. 1097 const bool found = findNext(false /* proceed */, true /* find and replace */); 1098 if (!found) 1099 findNext(true /* proceed */, true /* find and replace */); 1100 } 1101 1102 void SearchReplaceWidget::replaceAll() { 1103 const auto type = static_cast<DataType>(uiSearchReplace.cbDataType->currentIndex()); 1104 QString pattern1; 1105 QString pattern2; 1106 QString replaceValue; 1107 switch (type) { 1108 case DataType::Text: 1109 pattern1 = uiSearchReplace.cbValueText->currentText(); 1110 addCurrentTextToHistory(uiSearchReplace.cbReplaceText); 1111 replaceValue = uiSearchReplace.cbReplaceText->currentText(); 1112 break; 1113 case DataType::Numeric: 1114 pattern1 = uiSearchReplace.cbValue1->currentText(); 1115 pattern2 = uiSearchReplace.cbValue2->currentText(); 1116 addCurrentTextToHistory(uiSearchReplace.cbReplace); 1117 replaceValue = uiSearchReplace.cbReplace->currentText(); 1118 break; 1119 case DataType::DateTime: 1120 pattern1 = uiSearchReplace.dteValue1->text(); 1121 pattern2 = uiSearchReplace.dteValue2->text(); 1122 replaceValue = uiSearchReplace.dteReplace->text(); 1123 break; 1124 } 1125 1126 if (pattern1.isEmpty()) { 1127 highlight(type, false); 1128 return; 1129 } 1130 1131 if (replaceValue.isEmpty()) 1132 return; 1133 1134 // clear the previous selection 1135 m_view->clearSelection(); 1136 1137 // settings 1138 const auto opText = static_cast<OperatorText>(uiSearchReplace.cbOperatorText->currentData().toInt()); 1139 const auto opNumeric = static_cast<Operator>(uiSearchReplace.cbOperator->currentData().toInt()); 1140 const auto opDateTime = static_cast<Operator>(uiSearchReplace.cbOperatorDateTime->currentData().toInt()); 1141 const auto cs = uiSearchReplace.tbMatchCase->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive; 1142 const int colCount = m_spreadsheet->columnCount(); 1143 const int rowCount = m_spreadsheet->rowCount(); 1144 1145 // all settings are determined -> select all cells matching the specified pattern(s) 1146 const auto& columns = m_spreadsheet->children<Column>(); 1147 bool match = false; 1148 int matchCount = 0; 1149 m_spreadsheet->beginMacro(i18n("%1: replace values", m_spreadsheet->name())); 1150 1151 for (int col = 0; col < colCount; ++col) { 1152 auto* column = columns.at(col); 1153 if (!checkColumnType(column, type)) 1154 continue; 1155 1156 for (int row = 0; row < rowCount; ++row) { 1157 match = checkColumnRow(column, type, row, opText, opNumeric, opDateTime, pattern1, pattern2, cs); 1158 if (match) { 1159 setValue(column, type, row, replaceValue); 1160 ++matchCount; 1161 } 1162 } 1163 } 1164 1165 m_spreadsheet->endMacro(); 1166 1167 if (matchCount > 0) 1168 showMessage(i18np("%1 replacement made", "%1 replacements made", matchCount)); 1169 else 1170 showMessage(QString()); 1171 } 1172 1173 // ********************************************************** 1174 // ************ find/replace helper functions ************** 1175 // ********************************************************** 1176 bool SearchReplaceWidget::checkColumnType(Column* column, DataType type) { 1177 bool valid = false; 1178 1179 switch (type) { 1180 case DataType::Text: 1181 valid = (column->columnMode() == AbstractColumn::ColumnMode::Text); 1182 break; 1183 case DataType::Numeric: 1184 valid = column->isNumeric(); 1185 break; 1186 case DataType::DateTime: 1187 valid = (column->columnMode() == AbstractColumn::ColumnMode::DateTime); 1188 break; 1189 } 1190 1191 return valid; 1192 } 1193 1194 bool SearchReplaceWidget::checkColumnRow(Column* column, 1195 DataType type, 1196 int row, 1197 OperatorText opText, 1198 Operator opNumeric, 1199 Operator opDateTime, 1200 const QString& pattern1, 1201 const QString pattern2, 1202 Qt::CaseSensitivity cs) { 1203 bool match = false; 1204 switch (type) { 1205 case DataType::Text: 1206 match = checkCellText(column->textAt(row), pattern1, opText, cs); 1207 break; 1208 case DataType::Numeric: 1209 match = checkCellNumeric(column->valueAt(row), pattern1, pattern2, opNumeric); 1210 break; 1211 case DataType::DateTime: 1212 match = checkCellDateTime(column->dateTimeAt(row), uiSearchReplace.dteValue1->dateTime(), uiSearchReplace.dteValue2->dateTime(), opDateTime); 1213 break; 1214 } 1215 1216 return match; 1217 } 1218 1219 bool SearchReplaceWidget::checkCellText(const QString& cellText, const QString& pattern, OperatorText op, Qt::CaseSensitivity cs) { 1220 bool match = false; 1221 1222 switch (op) { 1223 case OperatorText::EqualTo: { 1224 match = (cellText.compare(pattern, cs) == 0); 1225 break; 1226 } 1227 case OperatorText::NotEqualTo: { 1228 match = (cellText.compare(pattern, cs) != 0); 1229 break; 1230 } 1231 case OperatorText::StartsWith: { 1232 match = cellText.startsWith(pattern, cs); 1233 break; 1234 } 1235 case OperatorText::EndsWith: { 1236 match = cellText.endsWith(pattern, cs); 1237 break; 1238 } 1239 case OperatorText::Contain: { 1240 match = (cellText.indexOf(pattern, cs) != -1); 1241 break; 1242 } 1243 case OperatorText::NotContain: { 1244 match = (cellText.indexOf(pattern, cs) == -1); 1245 break; 1246 } 1247 case OperatorText::RegEx: { 1248 QRegularExpression re(pattern); 1249 if (cs == Qt::CaseInsensitive) 1250 re.setPatternOptions(QRegularExpression::CaseInsensitiveOption); 1251 match = re.match(cellText).hasMatch(); 1252 break; 1253 } 1254 } 1255 1256 return match; 1257 } 1258 1259 bool SearchReplaceWidget::checkCellNumeric(double cellValue, const QString& pattern1, const QString& pattern2, Operator op) { 1260 if (pattern1.isEmpty()) 1261 return false; 1262 1263 if ((op == Operator::BetweenIncl || op == Operator::BetweenExcl) && pattern2.isEmpty()) 1264 return false; 1265 1266 bool ok; 1267 const auto numberLocale = QLocale(); 1268 1269 const double patternValue1 = numberLocale.toDouble(pattern1, &ok); 1270 if (!ok) 1271 return false; 1272 1273 double patternValue2 = 0.; 1274 if (op == Operator::BetweenIncl || op == Operator::BetweenExcl) { 1275 patternValue2 = numberLocale.toDouble(pattern2, &ok); 1276 if (!ok) 1277 return false; 1278 } 1279 1280 bool match = false; 1281 1282 switch (op) { 1283 case Operator::EqualTo: { 1284 match = (cellValue == patternValue1); 1285 break; 1286 } 1287 case Operator::NotEqualTo: { 1288 match = (cellValue != patternValue1); 1289 break; 1290 } 1291 case Operator::BetweenIncl: { 1292 match = (cellValue >= patternValue1 && cellValue <= patternValue2); 1293 break; 1294 } 1295 case Operator::BetweenExcl: { 1296 match = (cellValue > patternValue1 && cellValue < patternValue2); 1297 break; 1298 } 1299 case Operator::GreaterThan: { 1300 match = (cellValue > patternValue1); 1301 break; 1302 } 1303 case Operator::GreaterThanEqualTo: { 1304 match = (cellValue >= patternValue1); 1305 break; 1306 } 1307 case Operator::LessThan: { 1308 match = (cellValue < patternValue1); 1309 break; 1310 } 1311 case Operator::LessThanEqualTo: { 1312 match = (cellValue <= patternValue1); 1313 break; 1314 } 1315 } 1316 1317 return match; 1318 } 1319 1320 bool SearchReplaceWidget::checkCellDateTime(const QDateTime& cellValueDateTime, 1321 const QDateTime& patternDateTimeValue1, 1322 const QDateTime& patternDateTimeValue2, 1323 Operator op) { 1324 if (!patternDateTimeValue1.isValid()) 1325 return false; 1326 1327 if ((op == Operator::BetweenIncl || op == Operator::BetweenExcl) && !patternDateTimeValue2.isValid()) 1328 return false; 1329 1330 const double cellValue = cellValueDateTime.toMSecsSinceEpoch(); 1331 const double patternValue1 = patternDateTimeValue1.toMSecsSinceEpoch(); 1332 const double patternValue2 = patternDateTimeValue2.toMSecsSinceEpoch(); 1333 bool match = false; 1334 1335 switch (op) { 1336 case Operator::EqualTo: { 1337 match = (cellValue == patternValue1); 1338 break; 1339 } 1340 case Operator::NotEqualTo: { 1341 match = (cellValue != patternValue1); 1342 break; 1343 } 1344 case Operator::BetweenIncl: { 1345 match = (cellValue >= patternValue1 && cellValue <= patternValue2); 1346 break; 1347 } 1348 case Operator::BetweenExcl: { 1349 match = (cellValue > patternValue1 && cellValue < patternValue2); 1350 break; 1351 } 1352 case Operator::GreaterThan: { 1353 match = (cellValue > patternValue1); 1354 break; 1355 } 1356 case Operator::GreaterThanEqualTo: { 1357 match = (cellValue >= patternValue1); 1358 break; 1359 } 1360 case Operator::LessThan: { 1361 match = (cellValue < patternValue1); 1362 break; 1363 } 1364 case Operator::LessThanEqualTo: { 1365 match = (cellValue <= patternValue1); 1366 break; 1367 } 1368 } 1369 1370 return match; 1371 } 1372 1373 void SearchReplaceWidget::setValue(Column* column, DataType type, int row, const QString& replaceValue) { 1374 switch (type) { 1375 case DataType::Text: 1376 column->setTextAt(row, replaceValue); 1377 break; 1378 case DataType::Numeric: { 1379 bool ok; 1380 const auto mode = column->columnMode(); 1381 if (mode == AbstractColumn::ColumnMode::Double) { 1382 const double value = QLocale().toDouble(replaceValue, &ok); 1383 if (ok) 1384 column->setValueAt(row, value); 1385 } else if (mode == AbstractColumn::ColumnMode::Integer) { 1386 const int value = QLocale().toInt(replaceValue, &ok); 1387 if (ok) 1388 column->setIntegerAt(row, value); 1389 } else if (mode == AbstractColumn::ColumnMode::BigInt) { 1390 const qint64 value = QLocale().toLongLong(replaceValue, &ok); 1391 if (ok) 1392 column->setBigIntAt(row, value); 1393 } 1394 break; 1395 } 1396 case DataType::DateTime: 1397 const auto& value = uiSearchReplace.dteReplace->dateTime(); 1398 if (value.isValid()) 1399 column->setDateTimeAt(row, value); 1400 break; 1401 } 1402 } 1403 1404 void SearchReplaceWidget::highlight(DataType type, bool invalid) { 1405 switch (type) { 1406 case DataType::Text: 1407 GuiTools::highlight(uiSearchReplace.cbValueText, invalid); 1408 break; 1409 case DataType::Numeric: 1410 GuiTools::highlight(uiSearchReplace.cbValue1, invalid); 1411 GuiTools::highlight(uiSearchReplace.cbValue2, invalid); 1412 break; 1413 case DataType::DateTime: 1414 GuiTools::highlight(uiSearchReplace.dteValue1, invalid); 1415 GuiTools::highlight(uiSearchReplace.dteValue2, invalid); 1416 break; 1417 } 1418 1419 showMessage(QString()); 1420 } 1421 1422 void SearchReplaceWidget::showMessage(const QString& message) { 1423 if (message.isEmpty()) { 1424 if (m_messageWidget && m_messageWidget->isVisible()) 1425 m_messageWidget->close(); 1426 } else { 1427 if (!m_messageWidget) { 1428 m_messageWidget = new KMessageWidget(this); 1429 // m_messageWidget->setCloseButtonVisible(false); 1430 m_messageWidget->setMessageType(KMessageWidget::Information); 1431 auto* vBoxLayout = static_cast<QVBoxLayout*>(layout()); 1432 vBoxLayout->insertWidget(0, m_messageWidget); 1433 } 1434 m_messageWidget->setText(message); 1435 // m_messageWidget->move(300, 500); 1436 m_messageWidget->animatedShow(); 1437 } 1438 } 1439 1440 // ********************************************************** 1441 // **** context menu related helper classes and functions *** 1442 // ********************************************************** 1443 // the code below is taken from src/ktexteditor/katesearchbar.cpp from the ktexteditor repository. 1444 // idially, the same i18n is done for the strings below as in ktexteditor. 1445 1446 class AddMenuManager { 1447 private: 1448 QVector<QString> m_insertBefore; 1449 QVector<QString> m_insertAfter; 1450 QSet<QAction*> m_actionPointers; 1451 uint m_indexWalker{0}; 1452 QMenu* m_menu{nullptr}; 1453 1454 public: 1455 AddMenuManager(QMenu* parent, int expectedItemCount) 1456 : m_insertBefore(QVector<QString>(expectedItemCount)) 1457 , m_insertAfter(QVector<QString>(expectedItemCount)) { 1458 Q_ASSERT(parent != nullptr); 1459 1460 m_menu = parent->addMenu(i18n("Add...")); 1461 if (!m_menu) 1462 return; 1463 1464 m_menu->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); 1465 } 1466 1467 void enableMenu(bool enabled) { 1468 if (m_menu == nullptr) 1469 return; 1470 1471 m_menu->setEnabled(enabled); 1472 } 1473 1474 void addEntry(const QString& before, 1475 const QString& after, 1476 const QString& description, 1477 const QString& realBefore = QString(), 1478 const QString& realAfter = QString()) { 1479 if (!m_menu) 1480 return; 1481 1482 auto* const action = m_menu->addAction(before + after + QLatin1Char('\t') + description); 1483 m_insertBefore[m_indexWalker] = QString(realBefore.isEmpty() ? before : realBefore); 1484 m_insertAfter[m_indexWalker] = QString(realAfter.isEmpty() ? after : realAfter); 1485 action->setData(QVariant(m_indexWalker++)); 1486 m_actionPointers.insert(action); 1487 } 1488 1489 void addSeparator() { 1490 if (!m_menu) 1491 return; 1492 1493 m_menu->addSeparator(); 1494 } 1495 1496 void handle(QAction* action, QLineEdit* lineEdit) { 1497 if (!m_actionPointers.contains(action)) 1498 return; 1499 1500 const int cursorPos = lineEdit->cursorPosition(); 1501 const int index = action->data().toUInt(); 1502 const QString& before = m_insertBefore[index]; 1503 const QString& after = m_insertAfter[index]; 1504 lineEdit->insert(before + after); 1505 lineEdit->setCursorPosition(cursorPos + before.count()); 1506 lineEdit->setFocus(); 1507 } 1508 }; 1509 1510 struct ParInfo { 1511 int openIndex; 1512 bool capturing; 1513 int captureNumber; // 1..9 1514 }; 1515 1516 void SearchReplaceWidget::showExtendedContextMenu(bool replace, const QPoint& pos) { 1517 // create the original menu 1518 QLineEdit* lineEdit; 1519 if (replace) 1520 lineEdit = uiSearchReplace.cbReplaceText->lineEdit(); 1521 else 1522 lineEdit = uiSearchReplace.cbValueText->lineEdit(); 1523 1524 auto* const contextMenu = lineEdit->createStandardContextMenu(); 1525 if (!contextMenu) 1526 return; 1527 1528 // the extension should only be available for regex 1529 const auto opText = static_cast<OperatorText>(uiSearchReplace.cbOperatorText->currentData().toInt()); 1530 if (opText != OperatorText::RegEx) { 1531 contextMenu->exec(lineEdit->mapToGlobal(pos)); 1532 return; 1533 } 1534 1535 bool extendMenu = true; 1536 bool regexMode = true; 1537 1538 AddMenuManager addMenuManager(contextMenu, 37); 1539 if (!extendMenu) 1540 addMenuManager.enableMenu(extendMenu); 1541 else { 1542 // Build menu 1543 if (!replace) { 1544 if (regexMode) { 1545 addMenuManager.addEntry(QStringLiteral("^"), QString(), i18n("Beginning of line")); 1546 addMenuManager.addEntry(QStringLiteral("$"), QString(), i18n("End of line")); 1547 addMenuManager.addSeparator(); 1548 addMenuManager.addEntry(QStringLiteral("."), QString(), i18n("Match any character excluding new line (by default)")); 1549 addMenuManager.addEntry(QStringLiteral("+"), QString(), i18n("One or more occurrences")); 1550 addMenuManager.addEntry(QStringLiteral("*"), QString(), i18n("Zero or more occurrences")); 1551 addMenuManager.addEntry(QStringLiteral("?"), QString(), i18n("Zero or one occurrences")); 1552 addMenuManager.addEntry(QStringLiteral("{a"), 1553 QStringLiteral(",b}"), 1554 i18n("<a> through <b> occurrences"), 1555 QStringLiteral("{"), 1556 QStringLiteral(",}")); 1557 1558 addMenuManager.addSeparator(); 1559 addMenuManager.addSeparator(); 1560 addMenuManager.addEntry(QStringLiteral("("), QStringLiteral(")"), i18n("Group, capturing")); 1561 addMenuManager.addEntry(QStringLiteral("|"), QString(), i18n("Or")); 1562 addMenuManager.addEntry(QStringLiteral("["), QStringLiteral("]"), i18n("Set of characters")); 1563 addMenuManager.addEntry(QStringLiteral("[^"), QStringLiteral("]"), i18n("Negative set of characters")); 1564 addMenuManager.addSeparator(); 1565 } 1566 } else { 1567 addMenuManager.addEntry(QStringLiteral("\\0"), QString(), i18n("Whole match reference")); 1568 addMenuManager.addSeparator(); 1569 if (regexMode) { 1570 const QString pattern = uiSearchReplace.cbReplace->currentText(); 1571 const QVector<QString> capturePatterns = this->capturePatterns(pattern); 1572 1573 const int captureCount = capturePatterns.count(); 1574 for (int i = 1; i <= 9; i++) { 1575 const QString number = QString::number(i); 1576 const QString& captureDetails = 1577 (i <= captureCount) ? QLatin1String(" = (") + QStringView(capturePatterns[i - 1]).left(30) + QLatin1Char(')') : QString(); 1578 addMenuManager.addEntry(QLatin1String("\\") + number, QString(), i18n("Reference") + QLatin1Char(' ') + number + captureDetails); 1579 } 1580 1581 addMenuManager.addSeparator(); 1582 } 1583 } 1584 1585 addMenuManager.addEntry(QStringLiteral("\\n"), QString(), i18n("Line break")); 1586 addMenuManager.addEntry(QStringLiteral("\\t"), QString(), i18n("Tab")); 1587 1588 if (!replace && regexMode) { 1589 addMenuManager.addEntry(QStringLiteral("\\b"), QString(), i18n("Word boundary")); 1590 addMenuManager.addEntry(QStringLiteral("\\B"), QString(), i18n("Not word boundary")); 1591 addMenuManager.addEntry(QStringLiteral("\\d"), QString(), i18n("Digit")); 1592 addMenuManager.addEntry(QStringLiteral("\\D"), QString(), i18n("Non-digit")); 1593 addMenuManager.addEntry(QStringLiteral("\\s"), QString(), i18n("Whitespace (excluding line breaks)")); 1594 addMenuManager.addEntry(QStringLiteral("\\S"), QString(), i18n("Non-whitespace")); 1595 addMenuManager.addEntry(QStringLiteral("\\w"), QString(), i18n("Word character (alphanumerics plus '_')")); 1596 addMenuManager.addEntry(QStringLiteral("\\W"), QString(), i18n("Non-word character")); 1597 } 1598 1599 addMenuManager.addEntry(QStringLiteral("\\0???"), QString(), i18n("Octal character 000 to 377 (2^8-1)"), QStringLiteral("\\0")); 1600 addMenuManager.addEntry(QStringLiteral("\\x{????}"), QString(), i18n("Hex character 0000 to FFFF (2^16-1)"), QStringLiteral("\\x{....}")); 1601 addMenuManager.addEntry(QStringLiteral("\\\\"), QString(), i18n("Backslash")); 1602 1603 if (!replace && regexMode) { 1604 addMenuManager.addSeparator(); 1605 addMenuManager.addEntry(QStringLiteral("(?:E"), QStringLiteral(")"), i18n("Group, non-capturing"), QStringLiteral("(?:")); 1606 addMenuManager.addEntry(QStringLiteral("(?=E"), QStringLiteral(")"), i18n("Positive Lookahead"), QStringLiteral("(?=")); 1607 addMenuManager.addEntry(QStringLiteral("(?!E"), QStringLiteral(")"), i18n("Negative lookahead"), QStringLiteral("(?!")); 1608 // variable length positive/negative lookbehind is an experimental feature in Perl 5.30 1609 // see: https://perldoc.perl.org/perlre.html 1610 // currently QRegularExpression only supports fixed-length positive/negative lookbehind (2020-03-01) 1611 addMenuManager.addEntry(QStringLiteral("(?<=E"), QStringLiteral(")"), i18n("Fixed-length positive lookbehind"), QStringLiteral("(?<=")); 1612 addMenuManager.addEntry(QStringLiteral("(?<!E"), QStringLiteral(")"), i18n("Fixed-length negative lookbehind"), QStringLiteral("(?<!")); 1613 } 1614 1615 // TODO: support case conversion line in Kate later? 1616 /* 1617 if (replace) { 1618 addMenuManager.addSeparator(); 1619 addMenuManager.addEntry(QStringLiteral("\\L"), QString(), i18n("Begin lowercase conversion")); 1620 addMenuManager.addEntry(QStringLiteral("\\U"), QString(), i18n("Begin uppercase conversion")); 1621 addMenuManager.addEntry(QStringLiteral("\\E"), QString(), i18n("End case conversion")); 1622 addMenuManager.addEntry(QStringLiteral("\\l"), QString(), i18n("Lowercase first character conversion")); 1623 addMenuManager.addEntry(QStringLiteral("\\u"), QString(), i18n("Uppercase first character conversion")); 1624 addMenuManager.addEntry(QStringLiteral("\\#[#..]"), QString(), i18n("Replacement counter (for Replace All)"), QStringLiteral("\\#")); 1625 } 1626 */ 1627 } 1628 1629 // Show menu 1630 auto* const result = contextMenu->exec(lineEdit->mapToGlobal(pos)); 1631 if (result) 1632 addMenuManager.handle(result, lineEdit); 1633 } 1634 1635 QVector<QString> SearchReplaceWidget::capturePatterns(const QString& pattern) const { 1636 QVector<QString> capturePatterns; 1637 capturePatterns.reserve(9); 1638 QStack<ParInfo> parInfos; 1639 1640 const int inputLen = pattern.length(); 1641 int input = 0; // walker index 1642 bool insideClass = false; 1643 int captureCount = 0; 1644 1645 while (input < inputLen) { 1646 if (insideClass) { 1647 // Wait for closing, unescaped ']' 1648 if (pattern[input].unicode() == L']') 1649 insideClass = false; 1650 1651 input++; 1652 } else { 1653 switch (pattern[input].unicode()) { 1654 case L'\\': 1655 // Skip this and any next character 1656 input += 2; 1657 break; 1658 1659 case L'(': 1660 ParInfo curInfo; 1661 curInfo.openIndex = input; 1662 curInfo.capturing = (input + 1 >= inputLen) || (pattern[input + 1].unicode() != '?'); 1663 if (curInfo.capturing) { 1664 captureCount++; 1665 } 1666 curInfo.captureNumber = captureCount; 1667 parInfos.push(curInfo); 1668 1669 input++; 1670 break; 1671 1672 case L')': 1673 if (!parInfos.empty()) { 1674 ParInfo& top = parInfos.top(); 1675 if (top.capturing && (top.captureNumber <= 9)) { 1676 const int start = top.openIndex + 1; 1677 const int len = input - start; 1678 if (capturePatterns.size() < top.captureNumber) { 1679 capturePatterns.resize(top.captureNumber); 1680 } 1681 capturePatterns[top.captureNumber - 1] = pattern.mid(start, len); 1682 } 1683 parInfos.pop(); 1684 } 1685 1686 input++; 1687 break; 1688 1689 case L'[': 1690 input++; 1691 insideClass = true; 1692 break; 1693 1694 default: 1695 input++; 1696 break; 1697 } 1698 } 1699 } 1700 1701 return capturePatterns; 1702 }