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 }