1 /***************************************************************************
2  *   Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
16  ***************************************************************************/
17 
18 #include "valuelist.h"
19 
20 #include <typeinfo>
21 
22 #include <QTreeView>
23 #include <QHeaderView>
24 #include <QGridLayout>
25 #include <QStringListModel>
26 #include <QScrollBar>
27 #include <QTimer>
28 #include <QSortFilterProxyModel>
29 #include <QAction>
30 
31 #include <KLineEdit>
32 #include <KComboBox>
33 #include <KConfigGroup>
34 #include <KLocalizedString>
35 #include <KToggleAction>
36 #include <KSharedConfig>
37 
38 #include "bibtexfields.h"
39 #include "entry.h"
40 #include "fileview.h"
41 #include "valuelistmodel.h"
42 #include "models/filemodel.h"
43 
44 class ValueList::ValueListPrivate
45 {
46 private:
47     ValueList *p;
48     ValueListDelegate *delegate;
49 
50 public:
51     KSharedConfigPtr config;
52     const QString configGroupName;
53     const QString configKeyFieldName, configKeyShowCountColumn, configKeySortByCountAction, configKeyHeaderState;
54 
55     FileView *fileView;
56     QTreeView *treeviewFieldValues;
57     ValueListModel *model;
58     QSortFilterProxyModel *sortingModel;
59     KComboBox *comboboxFieldNames;
60     KLineEdit *lineeditFilter;
61     const int countWidth;
62     QAction *assignSelectionAction;
63     QAction *removeSelectionAction;
64     KToggleAction *showCountColumnAction;
65     KToggleAction *sortByCountAction;
66 
ValueListPrivate(ValueList * parent)67     ValueListPrivate(ValueList *parent)
68             : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("Value List Docklet")),
69           configKeyFieldName(QStringLiteral("FieldName")), configKeyShowCountColumn(QStringLiteral("ShowCountColumn")),
70           configKeySortByCountAction(QStringLiteral("SortByCountAction")), configKeyHeaderState(QStringLiteral("HeaderState")),
71           fileView(nullptr), model(nullptr), sortingModel(nullptr), countWidth(8 + parent->fontMetrics().width(i18n("Count"))) {
72         setupGUI();
73         initialize();
74     }
75 
setupGUI()76     void setupGUI() {
77         QBoxLayout *layout = new QVBoxLayout(p);
78         layout->setMargin(0);
79 
80         comboboxFieldNames = new KComboBox(true, p);
81         layout->addWidget(comboboxFieldNames);
82 
83         lineeditFilter = new KLineEdit(p);
84         layout->addWidget(lineeditFilter);
85         lineeditFilter->setClearButtonEnabled(true);
86         lineeditFilter->setPlaceholderText(i18n("Filter value list"));
87 
88         treeviewFieldValues = new QTreeView(p);
89         layout->addWidget(treeviewFieldValues);
90         treeviewFieldValues->setEditTriggers(QAbstractItemView::EditKeyPressed);
91         treeviewFieldValues->setSortingEnabled(true);
92         treeviewFieldValues->sortByColumn(0, Qt::AscendingOrder);
93         delegate = new ValueListDelegate(treeviewFieldValues);
94         treeviewFieldValues->setItemDelegate(delegate);
95         treeviewFieldValues->setRootIsDecorated(false);
96         treeviewFieldValues->setSelectionMode(QTreeView::ExtendedSelection);
97         treeviewFieldValues->setAlternatingRowColors(true);
98         treeviewFieldValues->header()->setSectionResizeMode(QHeaderView::Fixed);
99 
100         treeviewFieldValues->setContextMenuPolicy(Qt::ActionsContextMenu);
101         /// create context menu item to start renaming
102         QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Replace all occurrences"), p);
103         connect(action, &QAction::triggered, p, &ValueList::startItemRenaming);
104         treeviewFieldValues->addAction(action);
105         /// create context menu item to delete value
106         action = new QAction(QIcon::fromTheme(QStringLiteral("edit-table-delete-row")), i18n("Delete all occurrences"), p);
107         connect(action, &QAction::triggered, p, &ValueList::deleteAllOccurrences);
108         treeviewFieldValues->addAction(action);
109         /// create context menu item to search for multiple selections
110         action = new QAction(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Search for selected values"), p);
111         connect(action, &QAction::triggered, p, &ValueList::searchSelection);
112         treeviewFieldValues->addAction(action);
113         /// create context menu item to assign value to selected bibliography elements
114         assignSelectionAction = new QAction(QIcon::fromTheme(QStringLiteral("emblem-new")), i18n("Add value to selected entries"), p);
115         connect(assignSelectionAction, &QAction::triggered, p, &ValueList::assignSelection);
116         treeviewFieldValues->addAction(assignSelectionAction);
117         /// create context menu item to remove value from selected bibliography elements
118         removeSelectionAction = new QAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Remove value from selected entries"), p);
119         connect(removeSelectionAction, &QAction::triggered, p, &ValueList::removeSelection);
120         treeviewFieldValues->addAction(removeSelectionAction);
121 
122         p->setEnabled(false);
123 
124         connect(comboboxFieldNames, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), p, &ValueList::fieldNamesChanged);
125         connect(comboboxFieldNames, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), lineeditFilter, &KLineEdit::clear);
126         connect(treeviewFieldValues, &QTreeView::activated, p, &ValueList::listItemActivated);
127         connect(delegate, &ValueListDelegate::closeEditor, treeviewFieldValues, &QTreeView::reset);
128 
129         /// add context menu to header
130         treeviewFieldValues->header()->setContextMenuPolicy(Qt::ActionsContextMenu);
131         showCountColumnAction = new KToggleAction(i18n("Show Count Column"), treeviewFieldValues);
132         connect(showCountColumnAction, &QAction::triggered, p, &ValueList::showCountColumnToggled);
133         treeviewFieldValues->header()->addAction(showCountColumnAction);
134 
135         sortByCountAction = new KToggleAction(i18n("Sort by Count"), treeviewFieldValues);
136         connect(sortByCountAction, &QAction::triggered, p, &ValueList::sortByCountToggled);
137         treeviewFieldValues->header()->addAction(sortByCountAction);
138     }
139 
setComboboxFieldNamesCurrentItem(const QString & text)140     void setComboboxFieldNamesCurrentItem(const QString &text) {
141         int index = comboboxFieldNames->findData(text, Qt::UserRole, Qt::MatchExactly);
142         if (index < 0) index = comboboxFieldNames->findData(text, Qt::UserRole, Qt::MatchStartsWith);
143         if (index < 0) index = comboboxFieldNames->findData(text, Qt::UserRole, Qt::MatchContains);
144         if (index >= 0) comboboxFieldNames->setCurrentIndex(index);
145     }
146 
initialize()147     void initialize() {
148         lineeditFilter->clear();
149         comboboxFieldNames->clear();
150         for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) {
151             if (!fd.upperCamelCaseAlt.isEmpty()) continue; /// keep only "single" fields and not combined ones like "Author or Editor"
152             if (fd.upperCamelCase.startsWith('^')) continue; /// skip "type" and "id"
153             comboboxFieldNames->addItem(fd.label, fd.upperCamelCase);
154         }
155         /// Sort the combo box locale-aware. Thus we need a SortFilterProxyModel
156         QSortFilterProxyModel *proxy = new QSortFilterProxyModel(comboboxFieldNames);
157         proxy->setSortLocaleAware(true);
158         proxy->setSourceModel(comboboxFieldNames->model());
159         comboboxFieldNames->model()->setParent(proxy);
160         comboboxFieldNames->setModel(proxy);
161         comboboxFieldNames->model()->sort(0);
162 
163         KConfigGroup configGroup(config, configGroupName);
164         QString fieldName = configGroup.readEntry(configKeyFieldName, QString(Entry::ftAuthor));
165         setComboboxFieldNamesCurrentItem(fieldName);
166         if (allowsMultipleValues(fieldName))
167             assignSelectionAction->setText(i18n("Add value to selected entries"));
168         else
169             assignSelectionAction->setText(i18n("Replace value of selected entries"));
170         showCountColumnAction->setChecked(configGroup.readEntry(configKeyShowCountColumn, true));
171         sortByCountAction->setChecked(configGroup.readEntry(configKeySortByCountAction, false));
172         sortByCountAction->setEnabled(!showCountColumnAction->isChecked());
173         QByteArray headerState = configGroup.readEntry(configKeyHeaderState, QByteArray());
174         treeviewFieldValues->header()->restoreState(headerState);
175 
176         connect(treeviewFieldValues->header(), &QHeaderView::sortIndicatorChanged, p, &ValueList::columnsChanged);
177     }
178 
update()179     void update() {
180         QString text = comboboxFieldNames->itemData(comboboxFieldNames->currentIndex()).toString();
181         if (text.isEmpty()) text = comboboxFieldNames->currentText();
182 
183         delegate->setFieldName(text);
184         model = fileView == nullptr ? nullptr : fileView->valueListModel(text);
185         QAbstractItemModel *usedModel = model;
186         if (usedModel != nullptr) {
187             model->setShowCountColumn(showCountColumnAction->isChecked());
188             model->setSortBy(sortByCountAction->isChecked() ? ValueListModel::SortByCount : ValueListModel::SortByText);
189 
190             if (sortingModel != nullptr) delete sortingModel;
191             sortingModel = new QSortFilterProxyModel(p);
192             sortingModel->setSourceModel(model);
193             if (treeviewFieldValues->header()->isSortIndicatorShown())
194                 sortingModel->sort(treeviewFieldValues->header()->sortIndicatorSection(), treeviewFieldValues->header()->sortIndicatorOrder());
195             else
196                 sortingModel->sort(1, Qt::DescendingOrder);
197             sortingModel->setSortRole(ValueListModel::SortRole);
198             sortingModel->setFilterKeyColumn(0);
199             sortingModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
200             sortingModel->setFilterRole(ValueListModel::SearchTextRole);
201             connect(lineeditFilter, &KLineEdit::textEdited, sortingModel, &QSortFilterProxyModel::setFilterFixedString);
202             sortingModel->setSortLocaleAware(true);
203             usedModel = sortingModel;
204         }
205         treeviewFieldValues->setModel(usedModel);
206 
207         KConfigGroup configGroup(config, configGroupName);
208         configGroup.writeEntry(configKeyFieldName, text);
209         config->sync();
210     }
211 
allowsMultipleValues(const QString & field) const212     bool allowsMultipleValues(const QString &field) const {
213         return (field.compare(Entry::ftAuthor, Qt::CaseInsensitive) == 0
214                 || field.compare(Entry::ftEditor, Qt::CaseInsensitive) == 0
215                 || field.compare(Entry::ftUrl, Qt::CaseInsensitive) == 0
216                 || field.compare(Entry::ftFile, Qt::CaseInsensitive) == 0
217                 || field.compare(Entry::ftLocalFile, Qt::CaseInsensitive) == 0
218                 || field.compare(Entry::ftDOI, Qt::CaseInsensitive) == 0
219                 || field.compare(Entry::ftKeywords, Qt::CaseInsensitive) == 0);
220     }
221 };
222 
ValueList(QWidget * parent)223 ValueList::ValueList(QWidget *parent)
224         : QWidget(parent), d(new ValueListPrivate(this))
225 {
226     QTimer::singleShot(500, this, &ValueList::delayedResize);
227 }
228 
~ValueList()229 ValueList::~ValueList()
230 {
231     delete d;
232 }
233 
setFileView(FileView * fileView)234 void ValueList::setFileView(FileView *fileView)
235 {
236     if (d->fileView != nullptr)
237         disconnect(d->fileView, &FileView::selectedElementsChanged, this, &ValueList::editorSelectionChanged);
238     d->fileView = fileView;
239     if (d->fileView != nullptr) {
240         connect(d->fileView, &FileView::selectedElementsChanged, this, &ValueList::editorSelectionChanged);
241         connect(d->fileView, &FileView::destroyed, this, &ValueList::editorDestroyed);
242     }
243     editorSelectionChanged();
244     update();
245     resizeEvent(nullptr);
246 }
247 
update()248 void ValueList::update()
249 {
250     d->update();
251     setEnabled(d->fileView != nullptr);
252 }
253 
resizeEvent(QResizeEvent *)254 void ValueList::resizeEvent(QResizeEvent *)
255 {
256     int widgetWidth = d->treeviewFieldValues->size().width() - d->treeviewFieldValues->verticalScrollBar()->size().width() - 8;
257     d->treeviewFieldValues->setColumnWidth(0, widgetWidth - d->countWidth);
258     d->treeviewFieldValues->setColumnWidth(1, d->countWidth);
259 }
260 
listItemActivated(const QModelIndex & index)261 void ValueList::listItemActivated(const QModelIndex &index)
262 {
263     setEnabled(false);
264     QString itemText = d->sortingModel->mapToSource(index).data(ValueListModel::SearchTextRole).toString();
265     QVariant fieldVar = d->comboboxFieldNames->itemData(d->comboboxFieldNames->currentIndex());
266     QString fieldText = fieldVar.toString();
267     if (fieldText.isEmpty()) fieldText = d->comboboxFieldNames->currentText();
268 
269     SortFilterFileModel::FilterQuery fq;
270     fq.terms << itemText;
271     fq.combination = SortFilterFileModel::EveryTerm;
272     fq.field = fieldText;
273     fq.searchPDFfiles = false;
274 
275     d->fileView->setFilterBarFilter(fq);
276     setEnabled(true);
277 }
278 
searchSelection()279 void ValueList::searchSelection()
280 {
281     QVariant fieldVar = d->comboboxFieldNames->itemData(d->comboboxFieldNames->currentIndex());
282     QString fieldText = fieldVar.toString();
283     if (fieldText.isEmpty()) fieldText = d->comboboxFieldNames->currentText();
284 
285     SortFilterFileModel::FilterQuery fq;
286     fq.combination = SortFilterFileModel::EveryTerm;
287     fq.field = fieldText;
288     const auto selectedIndexes = d->treeviewFieldValues->selectionModel()->selectedIndexes();
289     for (const QModelIndex &index : selectedIndexes) {
290         if (index.column() == 0) {
291             QString itemText = index.data(ValueListModel::SearchTextRole).toString();
292             fq.terms << itemText;
293         }
294     }
295     fq.searchPDFfiles = false;
296 
297     if (!fq.terms.isEmpty())
298         d->fileView->setFilterBarFilter(fq);
299 }
300 
assignSelection()301 void ValueList::assignSelection() {
302     QString field = d->comboboxFieldNames->itemData(d->comboboxFieldNames->currentIndex()).toString();
303     if (field.isEmpty()) field = d->comboboxFieldNames->currentText();
304     if (field.isEmpty()) return; ///< empty field is invalid; quit
305 
306     const Value toBeAssignedValue = d->sortingModel->mapToSource(d->treeviewFieldValues->currentIndex()).data(Qt::EditRole).value<Value>();
307     if (toBeAssignedValue.isEmpty()) return; ///< empty value is invalid; quit
308     const QString toBeAssignedValueText = PlainTextValue::text(toBeAssignedValue);
309 
310     /// Keep track if any modifications were made to the bibliography file
311     bool madeModification = false;
312 
313     /// Go through all selected elements in current editor
314     const QList<QSharedPointer<Element> > &selection = d->fileView->selectedElements();
315     for (const auto &element : selection) {
316         /// Only entries (not macros or comments) are of interest
317         QSharedPointer<Entry> entry = element.dynamicCast<Entry>();
318         if (!entry.isNull()) {
319             /// Fields are separated into two categories:
320             /// 1. Where more values can be appended, like authors or URLs
321             /// 2. Where values should be replaced, like title, year, or journal
322             if (d->allowsMultipleValues(field)) {
323                 /// Fields for which multiple values are valid
324                 bool valueItemAlreadyContained = false; ///< add only if to-be-assigned value is not yet contained
325                 Value entrysValueForField = entry->value(field);
326                 for (const auto &containedValueItem : const_cast<const Value &>(entrysValueForField)) {
327                     valueItemAlreadyContained |= PlainTextValue::text(containedValueItem) == toBeAssignedValueText;
328                     if (valueItemAlreadyContained) break;
329                 }
330 
331                 if (!valueItemAlreadyContained) {
332                     /// Add each ValueItem from the to-be-assigned value to the entry's value for this field
333                     entrysValueForField.reserve(toBeAssignedValue.size());
334                     for (const auto &newValueItem : toBeAssignedValue) {
335                         entrysValueForField.append(newValueItem);
336                     }
337                     /// "Write back" value to field in entry
338                     entry->remove(field);
339                     entry->insert(field, entrysValueForField);
340                     /// Keep track that bibliography file has been modified
341                     madeModification = true;
342                 }
343             } else {
344                 /// Fields for which only value is valid, thus the old value will be replaced
345                 entry->remove(field);
346                 entry->insert(field, toBeAssignedValue);
347                 /// Keep track that bibliography file has been modified
348                 madeModification = true;
349             }
350 
351         }
352     }
353 
354     if (madeModification) {
355         /// Notify main editor about change it its data
356         d->fileView->externalModification();
357     }
358 }
359 
removeSelection()360 void ValueList::removeSelection() {
361     QString field = d->comboboxFieldNames->itemData(d->comboboxFieldNames->currentIndex()).toString();
362     if (field.isEmpty()) field = d->comboboxFieldNames->currentText();
363     if (field.isEmpty()) return; ///< empty field is invalid; quit
364 
365     const Value toBeRemovedValue = d->sortingModel->mapToSource(d->treeviewFieldValues->currentIndex()).data(Qt::EditRole).value<Value>();
366     if (toBeRemovedValue.isEmpty()) return; ///< empty value is invalid; quit
367     const QString toBeRemovedValueText = PlainTextValue::text(toBeRemovedValue);
368 
369     /// Keep track if any modifications were made to the bibliography file
370     bool madeModification = false;
371 
372     /// Go through all selected elements in current editor
373     const QList<QSharedPointer<Element> > &selection = d->fileView->selectedElements();
374     for (const auto &element : selection) {
375         /// Only entries (not macros or comments) are of interest
376         QSharedPointer<Entry> entry = element.dynamicCast<Entry>();
377         if (!entry.isNull()) {
378             Value entrysValueForField = entry->value(field);
379             bool valueModified = false;
380             for (int i = 0; i < entrysValueForField.count(); ++i) {
381                 const QString valueItemText = PlainTextValue::text(entrysValueForField[i]);
382                 if (valueItemText == toBeRemovedValueText) {
383                     valueModified = true;
384                     entrysValueForField.remove(i);
385                     break;
386                 }
387             }
388             if (valueModified) {
389                 entry->remove(field);
390                 entry->insert(field, entrysValueForField);
391                 madeModification = true;
392             }
393         }
394     }
395 
396     if (madeModification) {
397         update();
398         /// Notify main editor about change it its data
399         d->fileView->externalModification();
400     }
401 }
402 
startItemRenaming()403 void ValueList::startItemRenaming()
404 {
405     /// Get current index from sorted model
406     QModelIndex sortedIndex = d->treeviewFieldValues->currentIndex();
407     /// Make the tree view start and editing delegate on the index
408     d->treeviewFieldValues->edit(sortedIndex);
409 }
410 
deleteAllOccurrences()411 void ValueList::deleteAllOccurrences()
412 {
413     /// Get current index from sorted model
414     QModelIndex sortedIndex = d->treeviewFieldValues->currentIndex();
415     /// Get "real" index from original model, but resort to sibling in first column
416     QModelIndex realIndex = d->sortingModel->mapToSource(sortedIndex);
417     realIndex = realIndex.sibling(realIndex.row(), 0);
418 
419     /// Remove current index from data model
420     d->model->removeValue(realIndex);
421     /// Notify main editor about change it its data
422     d->fileView->externalModification();
423 }
424 
showCountColumnToggled()425 void ValueList::showCountColumnToggled()
426 {
427     if (d->model != nullptr)
428         d->model->setShowCountColumn(d->showCountColumnAction->isChecked());
429     if (d->showCountColumnAction->isChecked())
430         resizeEvent(nullptr);
431 
432     d->sortByCountAction->setEnabled(!d->showCountColumnAction->isChecked());
433 
434     KConfigGroup configGroup(d->config, d->configGroupName);
435     configGroup.writeEntry(d->configKeyShowCountColumn, d->showCountColumnAction->isChecked());
436     d->config->sync();
437 }
438 
sortByCountToggled()439 void ValueList::sortByCountToggled()
440 {
441     if (d->model != nullptr)
442         d->model->setSortBy(d->sortByCountAction->isChecked() ? ValueListModel::SortByCount : ValueListModel::SortByText);
443 
444     KConfigGroup configGroup(d->config, d->configGroupName);
445     configGroup.writeEntry(d->configKeySortByCountAction, d->sortByCountAction->isChecked());
446     d->config->sync();
447 }
448 
delayedResize()449 void ValueList::delayedResize()
450 {
451     resizeEvent(nullptr);
452 }
453 
columnsChanged()454 void ValueList::columnsChanged()
455 {
456     QByteArray headerState = d->treeviewFieldValues->header()->saveState();
457     KConfigGroup configGroup(d->config, d->configGroupName);
458     configGroup.writeEntry(d->configKeyHeaderState, headerState);
459     d->config->sync();
460 
461     resizeEvent(nullptr);
462 }
463 
editorSelectionChanged()464 void ValueList::editorSelectionChanged() {
465     const bool selectedElements = d->fileView == nullptr ? false : d->fileView->selectedElements().count() > 0;
466     d->assignSelectionAction->setEnabled(selectedElements);
467     d->removeSelectionAction->setEnabled(selectedElements);
468 }
469 
editorDestroyed()470 void ValueList::editorDestroyed() {
471     /// Reset internal variable to NULL to avoid
472     /// accessing invalid pointer/data later
473     d->fileView = nullptr;
474     editorSelectionChanged();
475 }
476 
fieldNamesChanged(int i)477 void ValueList::fieldNamesChanged(int i) {
478     const QString field = d->comboboxFieldNames->itemData(i).toString();
479     if (d->allowsMultipleValues(field))
480         d->assignSelectionAction->setText(i18n("Add value to selected entries"));
481     else
482         d->assignSelectionAction->setText(i18n("Replace value of selected entries"));
483     update();
484 }
485