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