File indexing completed on 2025-01-12 04:35:49

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2004-2019 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 "valuelist.h"
0021 
0022 #include <typeinfo>
0023 
0024 #include <QTreeView>
0025 #include <QHeaderView>
0026 #include <QGridLayout>
0027 #include <QStringListModel>
0028 #include <QScrollBar>
0029 #include <QLineEdit>
0030 #include <QComboBox>
0031 #include <QTimer>
0032 #include <QSortFilterProxyModel>
0033 #include <QAction>
0034 
0035 #include <KConfigGroup>
0036 #include <KLocalizedString>
0037 #include <KToggleAction>
0038 #include <KSharedConfig>
0039 
0040 #include <BibTeXFields>
0041 #include <Entry>
0042 #include <file/FileView>
0043 #include <ValueListModel>
0044 #include <models/FileModel>
0045 
0046 class ValueList::Private
0047 {
0048 private:
0049     ValueList *p;
0050     ValueListDelegate *delegate;
0051 
0052 public:
0053     KSharedConfigPtr config;
0054     const QString configGroupName;
0055     const QString configKeyFieldName, configKeyShowCountColumn, configKeySortByCountAction, configKeyHeaderState;
0056 
0057     FileView *fileView;
0058     QTreeView *treeviewFieldValues;
0059     ValueListModel *model;
0060     QSortFilterProxyModel *sortingModel;
0061     QComboBox *comboboxFieldNames;
0062     QLineEdit *lineeditFilter;
0063     const int countWidth;
0064     KToggleAction *showCountColumnAction;
0065     KToggleAction *sortByCountAction;
0066 
0067     Private(ValueList *parent)
0068             : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("Value List Docklet")),
0069           configKeyFieldName(QStringLiteral("FieldName")), configKeyShowCountColumn(QStringLiteral("ShowCountColumn")),
0070           configKeySortByCountAction(QStringLiteral("SortByCountAction")), configKeyHeaderState(QStringLiteral("HeaderState")),
0071           fileView(nullptr), model(nullptr), sortingModel(nullptr),
0072 #if QT_VERSION >= 0x050b00
0073           countWidth(8 + parent->fontMetrics().horizontalAdvance(i18n("Count")))
0074 #else // QT_VERSION >= 0x050b00
0075           countWidth(8 + parent->fontMetrics().width(i18n("Count")))
0076 #endif // QT_VERSION >= 0x050b00
0077     {
0078         setupGUI();
0079         initialize();
0080     }
0081 
0082     void setupGUI() {
0083         QBoxLayout *layout = new QVBoxLayout(p);
0084         layout->setContentsMargins(0, 0, 0, 0);
0085         layout->setSpacing(0);
0086 
0087         auto nestedLayout = new QVBoxLayout();
0088         nestedLayout->setContentsMargins(
0089             p->style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
0090             p->style()->pixelMetric(QStyle::PM_LayoutTopMargin),
0091             p->style()->pixelMetric(QStyle::PM_LayoutBottomMargin),
0092             p->style()->pixelMetric(QStyle::PM_LayoutRightMargin));
0093         nestedLayout->setSpacing(p->style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing));
0094         layout->addLayout(nestedLayout);
0095 
0096         comboboxFieldNames = new QComboBox(p);
0097         comboboxFieldNames->setEditable(true);
0098         nestedLayout->addWidget(comboboxFieldNames);
0099 
0100         lineeditFilter = new QLineEdit(p);
0101         nestedLayout->addWidget(lineeditFilter);
0102         lineeditFilter->setClearButtonEnabled(true);
0103         lineeditFilter->setPlaceholderText(i18n("Filter value list"));
0104 
0105         treeviewFieldValues = new QTreeView(p);
0106 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0107         treeviewFieldValues->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge}));
0108 #endif
0109         layout->addWidget(treeviewFieldValues);
0110         treeviewFieldValues->setEditTriggers(QAbstractItemView::EditKeyPressed);
0111         treeviewFieldValues->setSortingEnabled(true);
0112         treeviewFieldValues->sortByColumn(0, Qt::AscendingOrder);
0113         delegate = new ValueListDelegate(treeviewFieldValues);
0114         treeviewFieldValues->setItemDelegate(delegate);
0115         treeviewFieldValues->setRootIsDecorated(false);
0116         treeviewFieldValues->setSelectionMode(QTreeView::SingleSelection);
0117         treeviewFieldValues->setAlternatingRowColors(true);
0118         treeviewFieldValues->header()->setSectionResizeMode(QHeaderView::Fixed);
0119 
0120         treeviewFieldValues->setContextMenuPolicy(Qt::ActionsContextMenu);
0121         // Create context menu item to start renaming the current value in all bibliography entries
0122         QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Edit to replace all occurrences"), p);
0123         connect(action, &QAction::triggered, p, &ValueList::listItemStartRenaming);
0124         treeviewFieldValues->addAction(action);
0125         // Create context menu item to remove current value from all bibliography entries
0126         action = new QAction(QIcon::fromTheme(QStringLiteral("edit-table-delete-row")), i18n("Remove all occurrences"), p);
0127         connect(action, &QAction::triggered, p, &ValueList::deleteAllOccurrences);
0128         treeviewFieldValues->addAction(action);
0129 
0130         p->setEnabled(false);
0131 
0132         connect(comboboxFieldNames, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), p, &ValueList::update);
0133         connect(comboboxFieldNames, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), lineeditFilter, &QLineEdit::clear);
0134         connect(treeviewFieldValues, &QTreeView::activated, p, &ValueList::listItemActivated);
0135         connect(delegate, &ValueListDelegate::closeEditor, treeviewFieldValues, &QTreeView::reset);
0136 
0137         /// add context menu to header
0138         treeviewFieldValues->header()->setContextMenuPolicy(Qt::ActionsContextMenu);
0139         showCountColumnAction = new KToggleAction(i18n("Show Count Column"), treeviewFieldValues);
0140         connect(showCountColumnAction, &QAction::triggered, p, &ValueList::showCountColumnToggled);
0141         treeviewFieldValues->header()->addAction(showCountColumnAction);
0142 
0143         sortByCountAction = new KToggleAction(i18n("Sort by Count"), treeviewFieldValues);
0144         connect(sortByCountAction, &QAction::triggered, p, &ValueList::sortByCountToggled);
0145         treeviewFieldValues->header()->addAction(sortByCountAction);
0146     }
0147 
0148     void setComboboxFieldNamesCurrentItem(const QString &text) {
0149         int index = comboboxFieldNames->findData(text, Qt::UserRole, Qt::MatchExactly);
0150         if (index < 0) index = comboboxFieldNames->findData(text, Qt::UserRole, Qt::MatchStartsWith);
0151         if (index < 0) index = comboboxFieldNames->findData(text, Qt::UserRole, Qt::MatchContains);
0152         if (index >= 0) comboboxFieldNames->setCurrentIndex(index);
0153     }
0154 
0155     void initialize() {
0156         lineeditFilter->clear();
0157         comboboxFieldNames->clear();
0158         for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) {
0159             if (!fd.upperCamelCaseAlt.isEmpty()) continue; /// keep only "single" fields and not combined ones like "Author or Editor"
0160             if (fd.upperCamelCase.startsWith(QLatin1Char('^'))) continue; /// skip "type" and "id" (those are marked with '^')
0161             comboboxFieldNames->addItem(fd.label, fd.upperCamelCase);
0162         }
0163         // Sort the combo box locale-aware. Thus we need a SortFilterProxyModel
0164         QSortFilterProxyModel *proxy = new QSortFilterProxyModel(comboboxFieldNames);
0165         proxy->setSortLocaleAware(true);
0166         proxy->setSourceModel(comboboxFieldNames->model());
0167         comboboxFieldNames->model()->setParent(proxy);
0168         comboboxFieldNames->setModel(proxy);
0169         comboboxFieldNames->model()->sort(0);
0170 
0171         KConfigGroup configGroup(config, configGroupName);
0172         const QString fieldName = configGroup.readEntry(configKeyFieldName, QString(Entry::ftAuthor));
0173         setComboboxFieldNamesCurrentItem(fieldName);
0174         showCountColumnAction->setChecked(configGroup.readEntry(configKeyShowCountColumn, true));
0175         sortByCountAction->setChecked(configGroup.readEntry(configKeySortByCountAction, false));
0176         sortByCountAction->setEnabled(!showCountColumnAction->isChecked());
0177         QByteArray headerState = configGroup.readEntry(configKeyHeaderState, QByteArray());
0178         treeviewFieldValues->header()->restoreState(headerState);
0179 
0180         connect(treeviewFieldValues->header(), &QHeaderView::sortIndicatorChanged, p, &ValueList::columnsChanged);
0181     }
0182 
0183     void update() {
0184         const QString fieldName = currentField();
0185 
0186         delegate->setFieldName(fieldName);
0187         model = fileView == nullptr ? nullptr : fileView->valueListModel(fieldName);
0188         QAbstractItemModel *usedModel = model;
0189         if (usedModel != nullptr) {
0190             model->setShowCountColumn(showCountColumnAction->isChecked());
0191             model->setSortBy(sortByCountAction->isChecked() ? ValueListModel::SortBy::Count : ValueListModel::SortBy::Text);
0192 
0193             if (sortingModel != nullptr) delete sortingModel;
0194             sortingModel = new QSortFilterProxyModel(p);
0195             sortingModel->setSourceModel(model);
0196             if (treeviewFieldValues->header()->isSortIndicatorShown())
0197                 sortingModel->sort(treeviewFieldValues->header()->sortIndicatorSection(), treeviewFieldValues->header()->sortIndicatorOrder());
0198             else
0199                 sortingModel->sort(1, Qt::DescendingOrder);
0200             sortingModel->setSortRole(ValueListModel::SortRole);
0201             sortingModel->setFilterKeyColumn(0);
0202             sortingModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0203             sortingModel->setFilterRole(ValueListModel::SearchTextRole);
0204             connect(lineeditFilter, &QLineEdit::textEdited, sortingModel, &QSortFilterProxyModel::setFilterFixedString);
0205             sortingModel->setSortLocaleAware(true);
0206             usedModel = sortingModel;
0207         }
0208         treeviewFieldValues->setModel(usedModel);
0209 
0210         KConfigGroup configGroup(config, configGroupName);
0211         configGroup.writeEntry(configKeyFieldName, fieldName);
0212         config->sync();
0213     }
0214 
0215     QString currentField() const {
0216         QString field = comboboxFieldNames->itemData(comboboxFieldNames->currentIndex()).toString();
0217         if (field.isEmpty()) field = comboboxFieldNames->currentText();
0218         return field;
0219     }
0220 };
0221 
0222 ValueList::ValueList(QWidget *parent)
0223         : QWidget(parent), d(new Private(this))
0224 {
0225     QTimer::singleShot(500, this, [this]() {
0226         resizeEvent(nullptr);
0227     });
0228 }
0229 
0230 ValueList::~ValueList()
0231 {
0232     delete d;
0233 }
0234 
0235 void ValueList::setFileView(FileView *fileView)
0236 {
0237     if (d->fileView != nullptr)
0238         disconnect(d->fileView, &FileView::destroyed, this, &ValueList::editorDestroyed);
0239     d->fileView = fileView;
0240     if (d->fileView != nullptr)
0241         connect(d->fileView, &FileView::destroyed, this, &ValueList::editorDestroyed);
0242     update();
0243     resizeEvent(nullptr);
0244 }
0245 
0246 void ValueList::update()
0247 {
0248     d->update();
0249     setEnabled(d->fileView != nullptr);
0250 }
0251 
0252 void ValueList::resizeEvent(QResizeEvent *)
0253 {
0254     int widgetWidth = d->treeviewFieldValues->size().width() - d->treeviewFieldValues->verticalScrollBar()->size().width() - 8;
0255     d->treeviewFieldValues->setColumnWidth(0, widgetWidth - d->countWidth);
0256     d->treeviewFieldValues->setColumnWidth(1, d->countWidth);
0257 }
0258 
0259 void ValueList::listItemActivated(const QModelIndex &index)
0260 {
0261     const QString itemText = d->sortingModel->mapToSource(index).data(ValueListModel::SearchTextRole).toString();
0262     if (itemText.isEmpty())
0263         return;
0264 
0265     SortFilterFileModel::FilterQuery fq;
0266     fq.terms = QStringList{itemText};
0267     fq.combination = SortFilterFileModel::FilterCombination::EveryTerm;
0268     fq.field = d->currentField();
0269     if (fq.field.isEmpty())
0270         return;
0271     fq.searchPDFfiles = false;
0272 
0273     setEnabled(false); // FIXME necesary?
0274     d->fileView->setFilterBarFilter(fq);
0275     setEnabled(true); // FIXME necesary?
0276 }
0277 
0278 void ValueList::listItemStartRenaming()
0279 {
0280     // Get current index from sorted model
0281     const QModelIndex sortedIndex = d->treeviewFieldValues->currentIndex();
0282     // Make the tree view start and editing delegate on the index
0283     d->treeviewFieldValues->edit(sortedIndex);
0284 }
0285 
0286 void ValueList::deleteAllOccurrences()
0287 {
0288     /// Get current index from sorted model
0289     QModelIndex sortedIndex = d->treeviewFieldValues->currentIndex();
0290     /// Get "real" index from original model, but resort to sibling in first column
0291     QModelIndex realIndex = d->sortingModel->mapToSource(sortedIndex);
0292     realIndex = realIndex.sibling(realIndex.row(), 0);
0293 
0294     /// Remove current index from data model
0295     d->model->removeValue(realIndex);
0296     /// Notify main editor about change it its data
0297     d->fileView->externalModification();
0298 }
0299 
0300 void ValueList::showCountColumnToggled()
0301 {
0302     if (d->model != nullptr)
0303         d->model->setShowCountColumn(d->showCountColumnAction->isChecked());
0304     if (d->showCountColumnAction->isChecked())
0305         resizeEvent(nullptr);
0306 
0307     d->sortByCountAction->setEnabled(!d->showCountColumnAction->isChecked());
0308 
0309     KConfigGroup configGroup(d->config, d->configGroupName);
0310     configGroup.writeEntry(d->configKeyShowCountColumn, d->showCountColumnAction->isChecked());
0311     d->config->sync();
0312 }
0313 
0314 void ValueList::sortByCountToggled()
0315 {
0316     if (d->model != nullptr)
0317         d->model->setSortBy(d->sortByCountAction->isChecked() ? ValueListModel::SortBy::Count : ValueListModel::SortBy::Text);
0318 
0319     KConfigGroup configGroup(d->config, d->configGroupName);
0320     configGroup.writeEntry(d->configKeySortByCountAction, d->sortByCountAction->isChecked());
0321     d->config->sync();
0322 }
0323 
0324 void ValueList::columnsChanged()
0325 {
0326     QByteArray headerState = d->treeviewFieldValues->header()->saveState();
0327     KConfigGroup configGroup(d->config, d->configGroupName);
0328     configGroup.writeEntry(d->configKeyHeaderState, headerState);
0329     d->config->sync();
0330 
0331     resizeEvent(nullptr);
0332 }
0333 
0334 void ValueList::editorDestroyed() {
0335     /// Reset internal variable to NULL to avoid
0336     /// accessing invalid pointer/data later
0337     d->fileView = nullptr;
0338 }