File indexing completed on 2025-03-16 04:40:57
0001 /*************************************************************************** 0002 * SPDX-License-Identifier: GPL-2.0-or-later 0003 * * 0004 * SPDX-FileCopyrightText: 2004-2020 Thomas Fischer <fischer@unix-ag.uni-kl.de> 0005 * * 0006 * This program is free software; you can redistribute it and/or modify * 0007 * it under the terms of the GNU General Public License as published by * 0008 * the Free Software Foundation; either version 2 of the License, or * 0009 * (at your option) any later version. * 0010 * * 0011 * This program is distributed in the hope that it will be useful, * 0012 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0014 * GNU General Public License for more details. * 0015 * * 0016 * You should have received a copy of the GNU General Public License * 0017 * along with this program; if not, see <https://www.gnu.org/licenses/>. * 0018 ***************************************************************************/ 0019 0020 #include "filterbar.h" 0021 0022 #include <algorithm> 0023 0024 #include <QLayout> 0025 #include <QLabel> 0026 #include <QTimer> 0027 #include <QIcon> 0028 #include <QPushButton> 0029 #include <QLineEdit> 0030 #include <QComboBox> 0031 0032 #include <KLocalizedString> 0033 #include <KConfigGroup> 0034 #include <KSharedConfig> 0035 0036 #include <BibTeXFields> 0037 0038 static bool sortStringsLocaleAware(const QString &s1, const QString &s2) { 0039 return QString::localeAwareCompare(s1, s2) < 0; 0040 } 0041 0042 class FilterBar::FilterBarPrivate 0043 { 0044 private: 0045 FilterBar *p; 0046 0047 public: 0048 KSharedConfigPtr config; 0049 const QString configGroupName; 0050 0051 QComboBox *comboBoxFilterText; 0052 const int maxNumStoredFilterTexts; 0053 QComboBox *comboBoxCombination; 0054 QComboBox *comboBoxField; 0055 QPushButton *buttonSearchPDFfiles; 0056 QPushButton *buttonClearAll; 0057 QTimer *delayedTimer; 0058 0059 FilterBarPrivate(FilterBar *parent) 0060 : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), 0061 configGroupName(QStringLiteral("Filter Bar")), maxNumStoredFilterTexts(12), delayedTimer(new QTimer(parent)) { 0062 delayedTimer->setSingleShot(true); 0063 setupGUI(); 0064 connect(delayedTimer, &QTimer::timeout, p, &FilterBar::publishFilter); 0065 } 0066 0067 void setupGUI() { 0068 QBoxLayout *layout = new QHBoxLayout(p); 0069 0070 QLabel *label = new QLabel(i18n("Filter:"), p); 0071 layout->addWidget(label, 0); 0072 0073 comboBoxFilterText = new QComboBox(p); 0074 label->setBuddy(comboBoxFilterText); 0075 layout->addWidget(comboBoxFilterText, 5); 0076 comboBoxFilterText->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); 0077 comboBoxFilterText->setEditable(true); 0078 QFontMetrics metrics(comboBoxFilterText->font()); 0079 #if QT_VERSION >= 0x050b00 0080 comboBoxFilterText->setMinimumWidth(metrics.horizontalAdvance(QStringLiteral("AIWaiw")) * 7); 0081 #else // QT_VERSION >= 0x050b00 0082 comboBoxFilterText->setMinimumWidth(metrics.width(QStringLiteral("AIWaiw")) * 7); 0083 #endif // QT_VERSION >= 0x050b00 0084 QLineEdit *lineEdit = static_cast<QLineEdit *>(comboBoxFilterText->lineEdit()); 0085 lineEdit->setClearButtonEnabled(true); 0086 lineEdit->setPlaceholderText(i18n("Filter bibliographic entries")); 0087 0088 comboBoxCombination = new QComboBox(p); 0089 layout->addWidget(comboBoxCombination, 1); 0090 comboBoxCombination->addItem(i18n("any word")); /// AnyWord=0 0091 comboBoxCombination->addItem(i18n("every word")); /// EveryWord=1 0092 comboBoxCombination->addItem(i18n("exact phrase")); /// ExactPhrase=2 0093 comboBoxCombination->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); 0094 0095 comboBoxField = new QComboBox(p); 0096 layout->addWidget(comboBoxField, 1); 0097 comboBoxField->addItem(i18n("any field"), QVariant()); 0098 comboBoxField->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); 0099 0100 /// Use a hash map to get an alphabetically sorted list 0101 QHash<QString, QString> fielddescs; 0102 for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) 0103 if (fd.upperCamelCaseAlt.isEmpty()) 0104 fielddescs.insert(fd.label, fd.upperCamelCase); 0105 /// Sort locale-aware 0106 QList<QString> keys = fielddescs.keys(); 0107 std::sort(keys.begin(), keys.end(), sortStringsLocaleAware); 0108 for (const QString &key : const_cast<const QList<QString> &>(keys)) { 0109 const QString &value = fielddescs[key]; 0110 comboBoxField->addItem(key, value); 0111 } 0112 0113 buttonSearchPDFfiles = new QPushButton(p); 0114 buttonSearchPDFfiles->setIcon(QIcon::fromTheme(QStringLiteral("application-pdf"))); 0115 buttonSearchPDFfiles->setToolTip(i18n("Include PDF files in full-text search")); 0116 buttonSearchPDFfiles->setCheckable(true); 0117 layout->addWidget(buttonSearchPDFfiles, 0); 0118 0119 buttonClearAll = new QPushButton(p); 0120 buttonClearAll->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl"))); 0121 buttonClearAll->setToolTip(i18n("Reset filter criteria")); 0122 layout->addWidget(buttonClearAll, 0); 0123 0124 /// restore history on filter texts 0125 /// see addCompletionString for more detailed explanation 0126 KConfigGroup configGroup(config, configGroupName); 0127 QStringList completionListDate = configGroup.readEntry(QStringLiteral("PreviousSearches"), QStringList()); 0128 for (QStringList::Iterator it = completionListDate.begin(); it != completionListDate.end(); ++it) 0129 comboBoxFilterText->addItem((*it).mid(12)); 0130 comboBoxFilterText->lineEdit()->setText(QString()); 0131 comboBoxCombination->setCurrentIndex(configGroup.readEntry("CurrentCombination", 0)); 0132 comboBoxField->setCurrentIndex(configGroup.readEntry("CurrentField", 0)); 0133 0134 #define connectStartingDelayedTimer(instance,signal) connect((instance),(signal),p,[this](){delayedTimer->start(500);}) 0135 connectStartingDelayedTimer(comboBoxFilterText->lineEdit(), &QLineEdit::textChanged); 0136 connect(comboBoxFilterText->lineEdit(), &QLineEdit::returnPressed, p, &FilterBar::userPressedEnter); 0137 connect(comboBoxCombination, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, &FilterBar::comboboxStatusChanged); 0138 connect(comboBoxField, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, &FilterBar::comboboxStatusChanged); 0139 connect(buttonSearchPDFfiles, &QPushButton::toggled, p, &FilterBar::comboboxStatusChanged); 0140 connectStartingDelayedTimer(comboBoxCombination, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged)); 0141 connectStartingDelayedTimer(comboBoxField, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged)); 0142 connectStartingDelayedTimer(buttonSearchPDFfiles, &QPushButton::toggled); 0143 connect(buttonClearAll, &QPushButton::clicked, p, &FilterBar::resetState); 0144 } 0145 0146 SortFilterFileModel::FilterQuery filter() { 0147 SortFilterFileModel::FilterQuery result; 0148 result.combination = comboBoxCombination->currentIndex() == 0 ? SortFilterFileModel::FilterCombination::AnyTerm : SortFilterFileModel::FilterCombination::EveryTerm; 0149 result.terms.clear(); 0150 if (comboBoxCombination->currentIndex() == 2) /// exact phrase 0151 result.terms << comboBoxFilterText->lineEdit()->text(); 0152 else { /// any or every word 0153 static const QRegularExpression sequenceOfSpacesRegExp(QStringLiteral("\\s+")); 0154 #if QT_VERSION >= 0x050e00 0155 result.terms = comboBoxFilterText->lineEdit()->text().split(sequenceOfSpacesRegExp, Qt::SkipEmptyParts); 0156 #else // QT_VERSION < 0x050e00 0157 result.terms = comboBoxFilterText->lineEdit()->text().split(sequenceOfSpacesRegExp, QString::SkipEmptyParts); 0158 #endif // QT_VERSION >= 0x050e00 0159 } 0160 result.field = comboBoxField->currentIndex() == 0 ? QString() : comboBoxField->itemData(comboBoxField->currentIndex(), Qt::UserRole).toString(); 0161 result.searchPDFfiles = buttonSearchPDFfiles->isChecked(); 0162 0163 return result; 0164 } 0165 0166 void setFilter(const SortFilterFileModel::FilterQuery &fq) { 0167 /// Avoid triggering loops of activation 0168 QSignalBlocker comboBoxCombinationSignalBlocker(comboBoxCombination); 0169 /// Set check state for action for either "any word", 0170 /// "every word", or "exact phrase", respectively 0171 const int combinationIndex = fq.combination == SortFilterFileModel::FilterCombination::AnyTerm ? 0 : (fq.terms.count() < 2 ? 2 : 1); 0172 comboBoxCombination->setCurrentIndex(combinationIndex); 0173 0174 /// Avoid triggering loops of activation 0175 QSignalBlocker comboBoxFieldSignalBlocker(comboBoxField); 0176 /// Find and check action that corresponds to field name ("author", ...) 0177 const QString lower = fq.field.toLower(); 0178 for (int idx = comboBoxField->count() - 1; idx >= 0; --idx) { 0179 if (comboBoxField->itemData(idx, Qt::UserRole).toString().toLower() == lower) { 0180 comboBoxField->setCurrentIndex(idx); 0181 break; 0182 } 0183 } 0184 0185 /// Avoid triggering loops of activation 0186 QSignalBlocker buttonSearchPDFfilesSignalBlocker(buttonSearchPDFfiles); 0187 /// Set flag if associated PDF files have to be searched 0188 buttonSearchPDFfiles->setChecked(fq.searchPDFfiles); 0189 0190 /// Avoid triggering loops of activation 0191 QSignalBlocker comboBoxFilterTextSignalBlocker(comboBoxFilterText); 0192 /// Set filter text widget's content 0193 comboBoxFilterText->lineEdit()->setText(fq.terms.join(QStringLiteral(" "))); 0194 } 0195 0196 bool modelContainsText(QAbstractItemModel *model, const QString &text) { 0197 for (int row = 0; row < model->rowCount(); ++row) 0198 if (model->index(row, 0, QModelIndex()).data().toString().contains(text)) 0199 return true; 0200 return false; 0201 } 0202 0203 void addCompletionString(const QString &text) { 0204 KConfigGroup configGroup(config, configGroupName); 0205 0206 /// Previous searches are stored as a string list, where each individual 0207 /// string starts with 12 characters for the date and time when this 0208 /// search was used. Starting from the 13th character (12th, if you 0209 /// start counting from 0) the user's input is stored. 0210 /// This approach has several advantages: It does not require a more 0211 /// complex data structure, can easily read and written using 0212 /// KConfigGroup's functions, and can be sorted lexicographically/ 0213 /// chronologically using QStringList's sort. 0214 /// Disadvantage is that string fragments have to be managed manually. 0215 QStringList completionListDate = configGroup.readEntry(QStringLiteral("PreviousSearches"), QStringList()); 0216 for (QStringList::Iterator it = completionListDate.begin(); it != completionListDate.end();) 0217 if ((*it).mid(12) == text) 0218 it = completionListDate.erase(it); 0219 else 0220 ++it; 0221 completionListDate << (QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMddhhmm")) + text); 0222 0223 /// after sorting, discard all but the maxNumStoredFilterTexts most 0224 /// recent user-entered filter texts 0225 completionListDate.sort(); 0226 while (completionListDate.count() > maxNumStoredFilterTexts) 0227 completionListDate.removeFirst(); 0228 0229 configGroup.writeEntry(QStringLiteral("PreviousSearches"), completionListDate); 0230 config->sync(); 0231 0232 /// add user-entered filter text to combobox's drop-down list 0233 if (!text.isEmpty() && !modelContainsText(comboBoxFilterText->model(), text)) 0234 comboBoxFilterText->addItem(text); 0235 } 0236 0237 void storeComboBoxStatus() { 0238 KConfigGroup configGroup(config, configGroupName); 0239 configGroup.writeEntry(QStringLiteral("CurrentCombination"), comboBoxCombination->currentIndex()); 0240 configGroup.writeEntry(QStringLiteral("CurrentField"), comboBoxField->currentIndex()); 0241 configGroup.writeEntry(QStringLiteral("SearchPDFFiles"), buttonSearchPDFfiles->isChecked()); 0242 config->sync(); 0243 } 0244 0245 void restoreState() { 0246 KConfigGroup configGroup(config, configGroupName); 0247 comboBoxCombination->setCurrentIndex(configGroup.readEntry(QStringLiteral("CurrentCombination"), 0)); 0248 comboBoxField->setCurrentIndex(configGroup.readEntry(QStringLiteral("CurrentField"), 0)); 0249 buttonSearchPDFfiles->setChecked(configGroup.readEntry(QStringLiteral("SearchPDFFiles"), false)); 0250 } 0251 0252 void resetState() { 0253 comboBoxFilterText->lineEdit()->clear(); 0254 comboBoxCombination->setCurrentIndex(0); 0255 comboBoxField->setCurrentIndex(0); 0256 buttonSearchPDFfiles->setChecked(false); 0257 } 0258 }; 0259 0260 FilterBar::FilterBar(QWidget *parent) 0261 : QWidget(parent), d(new FilterBarPrivate(this)) 0262 { 0263 d->restoreState(); 0264 0265 setFocusProxy(d->comboBoxFilterText); 0266 0267 QTimer::singleShot(250, this, &FilterBar::buttonHeight); 0268 } 0269 0270 FilterBar::~FilterBar() 0271 { 0272 delete d; 0273 } 0274 0275 void FilterBar::setFilter(const SortFilterFileModel::FilterQuery &fq) 0276 { 0277 d->setFilter(fq); 0278 Q_EMIT filterChanged(fq); 0279 } 0280 0281 SortFilterFileModel::FilterQuery FilterBar::filter() 0282 { 0283 return d->filter(); 0284 } 0285 0286 void FilterBar::setPlaceholderText(const QString &msg) { 0287 QLineEdit *lineEdit = static_cast<QLineEdit *>(d->comboBoxFilterText->lineEdit()); 0288 lineEdit->setPlaceholderText(msg); 0289 } 0290 0291 void FilterBar::comboboxStatusChanged() 0292 { 0293 d->buttonSearchPDFfiles->setEnabled(d->comboBoxField->currentIndex() == 0); 0294 d->storeComboBoxStatus(); 0295 } 0296 0297 void FilterBar::resetState() 0298 { 0299 d->resetState(); 0300 Q_EMIT filterChanged(d->filter()); 0301 } 0302 0303 void FilterBar::userPressedEnter() 0304 { 0305 /// only store text in auto-completion if user pressed enter 0306 d->addCompletionString(d->comboBoxFilterText->lineEdit()->text()); 0307 0308 publishFilter(); 0309 } 0310 0311 void FilterBar::publishFilter() 0312 { 0313 Q_EMIT filterChanged(d->filter()); 0314 } 0315 0316 void FilterBar::buttonHeight() 0317 { 0318 QSizePolicy sp = d->buttonSearchPDFfiles->sizePolicy(); 0319 d->buttonSearchPDFfiles->setSizePolicy(sp.horizontalPolicy(), QSizePolicy::MinimumExpanding); 0320 d->buttonClearAll->setSizePolicy(sp.horizontalPolicy(), QSizePolicy::MinimumExpanding); 0321 }