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 "valuelistmodel.h"
19
20 #include <typeinfo>
21
22 #include <QApplication>
23 #include <QTextDocument>
24 #include <QAbstractTextDocumentLayout>
25 #include <QListView>
26 #include <QGridLayout>
27 #include <QStringListModel>
28 #include <QPainter>
29 #include <QFrame>
30 #include <QLayout>
31 #include <QHeaderView>
32
33 #include <KComboBox>
34 #include <KLocalizedString>
35 #include <KSharedConfig>
36 #include <KConfigGroup>
37 #include <KColorScheme>
38 #include <KLineEdit>
39
40 #include "fieldlineedit.h"
41 #include "bibtexfields.h"
42 #include "entry.h"
43 #include "preferences.h"
44 #include "models/filemodel.h"
45 #include "logging_gui.h"
46
createEditor(QWidget * parent,const QStyleOptionViewItem & sovi,const QModelIndex & index) const47 QWidget *ValueListDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &sovi, const QModelIndex &index) const
48 {
49 if (index.column() == 0) {
50 const FieldDescription &fd = BibTeXFields::instance().find(m_fieldName);
51 FieldLineEdit *fieldLineEdit = new FieldLineEdit(fd.preferredTypeFlag, fd.typeFlags, false, parent);
52 fieldLineEdit->setAutoFillBackground(true);
53 return fieldLineEdit;
54 } else
55 return QStyledItemDelegate::createEditor(parent, sovi, index);
56 }
57
setEditorData(QWidget * editor,const QModelIndex & index) const58 void ValueListDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
59 {
60 if (index.column() == 0) {
61 FieldLineEdit *fieldLineEdit = qobject_cast<FieldLineEdit *>(editor);
62 if (fieldLineEdit != nullptr)
63 fieldLineEdit->reset(index.model()->data(index, Qt::EditRole).value<Value>());
64 }
65 }
66
setModelData(QWidget * editor,QAbstractItemModel * model,const QModelIndex & index) const67 void ValueListDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
68 {
69 FieldLineEdit *fieldLineEdit = qobject_cast<FieldLineEdit *>(editor);
70 if (fieldLineEdit != nullptr) {
71 Value v;
72 fieldLineEdit->apply(v);
73 if (v.count() == 1) /// field should contain exactly one value item (no zero, not two or more)
74 model->setData(index, QVariant::fromValue(v));
75 }
76 }
77
sizeHint(const QStyleOptionViewItem & option,const QModelIndex & index) const78 QSize ValueListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
79 {
80 QSize size = QStyledItemDelegate::sizeHint(option, index);
81 size.setHeight(qMax(size.height(), option.fontMetrics.height() * 3 / 2)); // TODO calculate height better
82 return size;
83 }
84
commitAndCloseEditor()85 void ValueListDelegate::commitAndCloseEditor()
86 {
87 KLineEdit *editor = qobject_cast<KLineEdit *>(sender());
88 emit commitData(editor);
89 emit closeEditor(editor);
90 }
91
initStyleOption(QStyleOptionViewItem * option,const QModelIndex & index) const92 void ValueListDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
93 {
94 QStyledItemDelegate::initStyleOption(option, index);
95 if (option->decorationPosition != QStyleOptionViewItem::Top) {
96 /// remove text from style (do not draw text)
97 option->text.clear();
98 }
99
100 }
101
paint(QPainter * painter,const QStyleOptionViewItem & _option,const QModelIndex & index) const102 void ValueListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &_option, const QModelIndex &index) const
103 {
104 QStyleOptionViewItem option = _option;
105
106 /// code heavily inspired by kdepimlibs-4.6.3/akonadi/collectionstatisticsdelegate.cpp
107
108 /// save painter's state, restored before leaving this function
109 painter->save();
110
111 /// first, paint the basic, but without the text. We remove the text
112 /// in initStyleOption(), which gets called by QStyledItemDelegate::paint().
113 QStyledItemDelegate::paint(painter, option, index);
114
115 /// now, we retrieve the correct style option by calling intiStyleOption from
116 /// the superclass.
117 QStyledItemDelegate::initStyleOption(&option, index);
118 QString field = option.text;
119
120 /// now calculate the rectangle for the text
121 QStyle *s = m_parent->style();
122 const QWidget *widget = option.widget;
123 const QRect textRect = s->subElementRect(QStyle::SE_ItemViewItemText, &option, widget);
124
125 if (option.state & QStyle::State_Selected) {
126 /// selected lines are drawn with different color
127 painter->setPen(option.palette.highlightedText().color());
128 }
129
130 /// count will be empty unless only one column is shown
131 const QString count = index.column() == 0 && index.model()->columnCount() == 1 ? QString(QStringLiteral(" (%1)")).arg(index.data(ValueListModel::CountRole).toInt()) : QString();
132
133 /// squeeze the folder text if it is to big and calculate the rectangles
134 /// where the folder text and the unread count will be drawn to
135 QFontMetrics fm(painter->fontMetrics());
136 int countWidth = fm.width(count);
137 int fieldWidth = fm.width(field);
138 if (countWidth + fieldWidth > textRect.width()) {
139 /// text plus count is too wide for column, cut text and insert "..."
140 field = fm.elidedText(field, Qt::ElideRight, textRect.width() - countWidth - 8);
141 fieldWidth = textRect.width() - countWidth - 12;
142 }
143
144 /// determine rects to draw field
145 int top = textRect.top() + (textRect.height() - fm.height()) / 2;
146 QRect fieldRect = textRect;
147 QRect countRect = textRect;
148 fieldRect.setTop(top);
149 fieldRect.setHeight(fm.height());
150
151 if (m_parent->header()->visualIndex(index.column()) == 0) {
152 /// left-align text
153 fieldRect.setLeft(fieldRect.left() + 4); ///< hm, indent necessary?
154 fieldRect.setRight(fieldRect.left() + fieldWidth);
155 } else {
156 /// right-align text
157 fieldRect.setRight(fieldRect.right() - 4); ///< hm, indent necessary?
158 fieldRect.setLeft(fieldRect.right() - fieldWidth); ///< hm, indent necessary?
159 }
160
161 /// draw field name
162 painter->drawText(fieldRect, Qt::AlignLeft, field);
163
164 if (!count.isEmpty()) {
165 /// determine rects to draw count
166 countRect.setTop(top);
167 countRect.setHeight(fm.height());
168 countRect.setLeft(fieldRect.right());
169
170 /// use bold font
171 QFont font = painter->font();
172 font.setBold(true);
173 painter->setFont(font);
174 /// determine color for count number
175 const QColor countColor = (option.state & QStyle::State_Selected) ? KColorScheme(QPalette::Active, KColorScheme::Selection).foreground(KColorScheme::LinkText).color() : KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color();
176 painter->setPen(countColor);
177
178 /// draw count
179 painter->drawText(countRect, Qt::AlignLeft, count);
180 }
181
182 /// restore painter's state
183 painter->restore();
184 }
185
ValueListModel(const File * bibtexFile,const QString & fieldName,QObject * parent)186 ValueListModel::ValueListModel(const File *bibtexFile, const QString &fieldName, QObject *parent)
187 : QAbstractTableModel(parent), file(bibtexFile), fName(fieldName.toLower()), showCountColumn(true), sortBy(SortByText)
188 {
189 readConfiguration();
190 updateValues();
191 NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged);
192 }
193
rowCount(const QModelIndex & parent) const194 int ValueListModel::rowCount(const QModelIndex &parent) const
195 {
196 return parent == QModelIndex() ? values.count() : 0;
197 }
198
columnCount(const QModelIndex & parent) const199 int ValueListModel::columnCount(const QModelIndex &parent) const
200 {
201 return parent == QModelIndex() ? (showCountColumn ? 2 : 1) : 0;
202 }
203
data(const QModelIndex & index,int role) const204 QVariant ValueListModel::data(const QModelIndex &index, int role) const
205 {
206 if (index.row() >= values.count() || index.column() >= 2)
207 return QVariant();
208 if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
209 if (index.column() == 0) {
210 if (fName == Entry::ftColor) {
211 QString text = values[index.row()].text;
212 if (text.isEmpty()) return QVariant();
213 QString colorText = colorToLabel[text];
214 if (colorText.isEmpty()) return QVariant(text);
215 return QVariant(colorText);
216 } else
217 return QVariant(values[index.row()].text);
218 } else
219 return QVariant(values[index.row()].count);
220 } else if (role == SortRole) {
221 static const QRegularExpression ignoredInSorting(QStringLiteral("[{}\\\\]+"));
222
223 QString buffer = values[index.row()].sortBy.isEmpty() ? values[index.row()].text : values[index.row()].sortBy;
224 buffer = buffer.remove(ignoredInSorting).toLower();
225
226 if ((showCountColumn && index.column() == 1) || (!showCountColumn && sortBy == SortByCount)) {
227 /// Sort by string consisting of a zero-padded count and the lower-case text,
228 /// for example "0000000051keyword"
229 /// Used if (a) two columns are shown (showCountColumn is true) and column 1
230 /// (the count column) is to be sorted or (b) if only one column is shown
231 /// (showCountColumn is false) and this single column is to be sorted by count.
232 return QString(QStringLiteral("%1%2")).arg(values[index.row()].count, 10, 10, QLatin1Char('0')).arg(buffer);
233 } else {
234 /// Otherwise use lower-case text for sorting
235 return QVariant(buffer);
236 }
237 } else if (role == SearchTextRole) {
238 return QVariant(values[index.row()].text);
239 } else if (role == Qt::EditRole)
240 return QVariant::fromValue(values[index.row()].value);
241 else if (role == CountRole)
242 return QVariant(values[index.row()].count);
243 else
244 return QVariant();
245 }
246
setData(const QModelIndex & index,const QVariant & value,int role)247 bool ValueListModel::setData(const QModelIndex &index, const QVariant &value, int role)
248 {
249 Q_ASSERT_X(file != nullptr, "ValueListModel::setData", "You cannot set data if there is no BibTeX file associated with this value list.");
250
251 /// Continue only if in edit role and first column is to be changed
252 if (role == Qt::EditRole && index.column() == 0) {
253 /// Fetch the string as it was shown before the editing started
254 QString origText = data(index, Qt::DisplayRole).toString();
255 /// Special treatment for colors
256 if (fName == Entry::ftColor) {
257 /// for colors, convert color (RGB) to the associated label
258 QString color = colorToLabel.key(origText);
259 if (!color.isEmpty()) origText = color;
260 }
261
262 /// Retrieve the Value object containing the user-entered data
263 Value newValue = value.value<Value>(); /// nice variable names ... ;-)
264 if (newValue.isEmpty()) {
265 qCWarning(LOG_KBIBTEX_GUI) << "Cannot replace with empty value";
266 return false;
267 }
268
269 /// Fetch the string representing the new, user-entered value
270 const QString newText = PlainTextValue::text(newValue);
271 if (newText == origText) {
272 qCWarning(LOG_KBIBTEX_GUI) << "Skipping to replace value with itself";
273 return false;
274 }
275
276 bool success = searchAndReplaceValueInEntries(index, newValue) && searchAndReplaceValueInModel(index, newValue);
277 return success;
278 }
279 return false;
280 }
281
flags(const QModelIndex & index) const282 Qt::ItemFlags ValueListModel::flags(const QModelIndex &index) const
283 {
284 Qt::ItemFlags result = QAbstractTableModel::flags(index);
285 /// make first column editable
286 if (index.column() == 0)
287 result |= Qt::ItemIsEditable;
288 return result;
289 }
290
headerData(int section,Qt::Orientation orientation,int role) const291 QVariant ValueListModel::headerData(int section, Qt::Orientation orientation, int role) const
292 {
293 if (section >= 2 || orientation != Qt::Horizontal || role != Qt::DisplayRole)
294 return QVariant();
295 else if ((section == 0 && columnCount() == 2) || (columnCount() == 1 && sortBy == SortByText))
296 return QVariant(i18n("Value"));
297 else
298 return QVariant(i18n("Count"));
299 }
300
removeValue(const QModelIndex & index)301 void ValueListModel::removeValue(const QModelIndex &index)
302 {
303 removeValueFromEntries(index);
304 removeValueFromModel(index);
305 }
306
setShowCountColumn(bool showCountColumn)307 void ValueListModel::setShowCountColumn(bool showCountColumn)
308 {
309 beginResetModel();
310 this->showCountColumn = showCountColumn;
311 endResetModel();
312 }
313
setSortBy(SortBy sortBy)314 void ValueListModel::setSortBy(SortBy sortBy)
315 {
316 beginResetModel();
317 this->sortBy = sortBy;
318 endResetModel();
319 }
320
notificationEvent(int eventId)321 void ValueListModel::notificationEvent(int eventId)
322 {
323 if (eventId == NotificationHub::EventConfigurationChanged) {
324 beginResetModel();
325 readConfiguration();
326 endResetModel();
327 }
328 }
329
readConfiguration()330 void ValueListModel::readConfiguration()
331 {
332 /// load mapping from color value to label
333 KSharedConfigPtr config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc")));
334 KConfigGroup configGroup(config, Preferences::groupColor);
335 QStringList colorCodes = configGroup.readEntry(Preferences::keyColorCodes, Preferences::defaultColorCodes);
336 QStringList colorLabels = configGroup.readEntry(Preferences::keyColorLabels, Preferences::defaultColorLabels);
337 colorToLabel.clear();
338 for (QStringList::ConstIterator itc = colorCodes.constBegin(), itl = colorLabels.constBegin(); itc != colorCodes.constEnd() && itl != colorLabels.constEnd(); ++itc, ++itl) {
339 colorToLabel.insert(*itc, i18n((*itl).toUtf8().constData()));
340 }
341 }
342
updateValues()343 void ValueListModel::updateValues()
344 {
345 values.clear();
346 if (file == nullptr) return;
347
348 for (const auto &element : const_cast<const File &>(*file)) {
349 QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>();
350 if (!entry.isNull()) {
351 for (Entry::ConstIterator eit = entry->constBegin(); eit != entry->constEnd(); ++eit) {
352 QString key = eit.key().toLower();
353 if (key == fName) {
354 insertValue(eit.value());
355 break;
356 }
357 if (eit.value().isEmpty())
358 qCWarning(LOG_KBIBTEX_GUI) << "value for key" << key << "in entry" << entry->id() << "is empty";
359 }
360 }
361 }
362 }
363
insertValue(const Value & value)364 void ValueListModel::insertValue(const Value &value)
365 {
366 for (const QSharedPointer<ValueItem> &item : value) {
367 const QString text = PlainTextValue::text(*item);
368 if (text.isEmpty()) continue; ///< skip empty values
369
370 int index = indexOf(text);
371 if (index < 0) {
372 /// previously unknown text
373 ValueLine newValueLine;
374 newValueLine.text = text;
375 newValueLine.count = 1;
376 newValueLine.value.append(item);
377
378 /// memorize sorting criterium:
379 /// * for persons, use last name first
380 /// * in any case, use lower case
381 const QSharedPointer<Person> person = item.dynamicCast<Person>();
382 newValueLine.sortBy = person.isNull() ? text.toLower() : person->lastName().toLower() + QStringLiteral(" ") + person->firstName().toLower();
383
384 values << newValueLine;
385 } else {
386 ++values[index].count;
387 }
388 }
389 }
390
indexOf(const QString & text)391 int ValueListModel::indexOf(const QString &text)
392 {
393 QString color;
394 QString cmpText = text;
395 if (fName == Entry::ftColor && !(color = colorToLabel.key(text, QString())).isEmpty())
396 cmpText = color;
397 if (cmpText.isEmpty())
398 qCWarning(LOG_KBIBTEX_GUI) << "Should never happen";
399
400 int i = 0;
401 /// this is really slow for large data sets: O(n^2)
402 /// maybe use a hash table instead?
403 for (const ValueLine &valueLine : const_cast<const ValueLineList &>(values)) {
404 if (valueLine.text == cmpText)
405 return i;
406 ++i;
407 }
408 return -1;
409 }
410
searchAndReplaceValueInEntries(const QModelIndex & index,const Value & newValue)411 bool ValueListModel::searchAndReplaceValueInEntries(const QModelIndex &index, const Value &newValue)
412 {
413 /// Fetch the string representing the new, user-entered value
414 const QString newText = PlainTextValue::text(newValue);
415
416 if (newText.isEmpty())
417 return false;
418
419 /// Fetch the string as it was shown before the editing started
420 QString origText = data(index, Qt::DisplayRole).toString();
421 /// Special treatment for colors
422 if (fName == Entry::ftColor) {
423 /// for colors, convert color (RGB) to the associated label
424 QString color = colorToLabel.key(origText);
425 if (!color.isEmpty()) origText = color;
426 }
427
428 /// Go through all elements in the current file
429 for (const QSharedPointer<Element> &element : const_cast<const File &>(*file)) {
430 QSharedPointer<Entry> entry = element.dynamicCast<Entry>();
431 /// Process only Entry objects
432 if (!entry.isNull()) {
433 /// Go through every key-value pair in entry (author, title, ...)
434 for (Entry::Iterator eit = entry->begin(); eit != entry->end(); ++eit) {
435 /// Fetch key-value pair's key
436 const QString key = eit.key().toLower();
437 /// Process only key-value pairs that are filtered for (e.g. only keywords)
438 if (key == fName) {
439 eit.value().replace(origText, newValue.first());
440 break;
441 }
442 }
443 }
444 }
445
446 return true;
447 }
448
searchAndReplaceValueInModel(const QModelIndex & index,const Value & newValue)449 bool ValueListModel::searchAndReplaceValueInModel(const QModelIndex &index, const Value &newValue)
450 {
451 /// Fetch the string representing the new, user-entered value
452 const QString newText = PlainTextValue::text(newValue);
453 if (newText.isEmpty())
454 return false;
455
456 const int row = index.row();
457
458 /// Test if user-entered text exists already in model's data
459 /// newTextAlreadyInListIndex will be row of duplicate or
460 /// -1 if new text is unique
461 int newTextAlreadyInListIndex = -1;
462 for (int r = values.count() - 1; newTextAlreadyInListIndex < 0 && r >= 0; --r) {
463 if (row != r && values[r].text == newText)
464 newTextAlreadyInListIndex = r;
465 }
466
467 if (newTextAlreadyInListIndex < 0) {
468 /// User-entered text is unique, so simply replace
469 /// old text with new text
470 values[row].text = newText;
471 values[row].value = newValue;
472 const QSharedPointer<Person> person = newValue.first().dynamicCast<Person>();
473 values[row].sortBy = person.isNull() ? QString() : person->lastName() + QStringLiteral(" ") + person->firstName();
474 } else {
475 /// The user-entered text existed before
476
477 const int lastRow = values.count() - 1;
478 if (row != lastRow) {
479 /// Unless duplicate is last one in list,
480 /// overwrite edited row with last row's value
481 values[row].text = values[lastRow].text;
482 values[row].value = values[lastRow].value;
483 values[row].sortBy = values[lastRow].sortBy;
484 }
485
486 /// Remove last row, which is no longer used
487 beginRemoveRows(QModelIndex(), lastRow, lastRow);
488 values.remove(lastRow);
489 endRemoveRows();
490 }
491
492 /// Notify Qt about data changed
493 emit dataChanged(index, index);
494
495 return true;
496 }
497
removeValueFromEntries(const QModelIndex & index)498 void ValueListModel::removeValueFromEntries(const QModelIndex &index)
499 {
500 /// Retrieve the Value object containing the user-entered data
501 const Value toBeDeletedValue = values[index.row()].value;
502 if (toBeDeletedValue.isEmpty()) {
503 return;
504 }
505 const QString toBeDeletedText = PlainTextValue::text(toBeDeletedValue);
506 if (toBeDeletedText.isEmpty()) {
507 return;
508 }
509
510 /// Go through all elements in the current file
511 for (const QSharedPointer<Element> &element : const_cast<const File &>(*file)) {
512 QSharedPointer<Entry> entry = element.dynamicCast<Entry>();
513 /// Process only Entry objects
514 if (!entry.isNull()) {
515 /// Go through every key-value pair in entry (author, title, ...)
516 for (Entry::Iterator eit = entry->begin(); eit != entry->end(); ++eit) {
517 /// Fetch key-value pair's key
518 const QString key = eit.key().toLower();
519 /// Process only key-value pairs that are filtered for (e.g. only keywords)
520 if (key == fName) {
521 /// Fetch the key-value pair's value's textual representation
522 const QString valueFullText = PlainTextValue::text(eit.value());
523 if (valueFullText == toBeDeletedText) {
524 /// If the key-value pair's value's textual representation is the same
525 /// as the value to be delted, remove this key-value pair
526 /// This test is usually true for keys like title, year, or edition.
527 entry->remove(key); /// This would break the Iterator, but code "breakes" from loop anyways
528 } else {
529 /// The test above failed, but the delete operation may have
530 /// to be applied to a ValueItem inside the value.
531 /// Possible keys for such a case include author, editor, or keywords.
532
533 /// Process each ValueItem inside this Value
534 for (Value::Iterator vit = eit.value().begin(); vit != eit.value().end();) {
535 /// Similar procedure as for full values above:
536 /// If a ValueItem's textual representation is the same
537 /// as the shown string which has be deleted, remove the
538 /// ValueItem from this Value. If the Value becomes empty,
539 /// remove Value as well.
540 const QString valueItemText = PlainTextValue::text(* (*vit));
541 if (valueItemText == toBeDeletedText) {
542 /// Erase old ValueItem from this Value
543 vit = eit.value().erase(vit);
544 } else
545 ++vit;
546 }
547
548 if (eit.value().isEmpty()) {
549 /// This value does no longer contain any ValueItems.
550 entry->remove(key); /// This would break the Iterator, but code "breakes" from loop anyways
551 }
552 }
553 break;
554 }
555 }
556 }
557 }
558 }
559
removeValueFromModel(const QModelIndex & index)560 void ValueListModel::removeValueFromModel(const QModelIndex &index)
561 {
562 const int row = index.row();
563 const int lastRow = values.count() - 1;
564
565 if (row != lastRow) {
566 /// Unless duplicate is last one in list,
567 /// overwrite edited row with last row's value
568 values[row].text = values[lastRow].text;
569 values[row].value = values[lastRow].value;
570 values[row].sortBy = values[lastRow].sortBy;
571
572 emit dataChanged(index, index);
573 }
574
575 /// Remove last row, which is no longer used
576 beginRemoveRows(QModelIndex(), lastRow, lastRow);
577 values.remove(lastRow);
578 endRemoveRows();
579 }
580