File indexing completed on 2024-05-19 05:05:31

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2004-2023 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 "valuelistmodel.h"
0021 
0022 #include <typeinfo>
0023 
0024 #include <QApplication>
0025 #include <QTextDocument>
0026 #include <QAbstractTextDocumentLayout>
0027 #include <QListView>
0028 #include <QLineEdit>
0029 #include <QGridLayout>
0030 #include <QStringListModel>
0031 #include <QPainter>
0032 #include <QFrame>
0033 #include <QLayout>
0034 #include <QHeaderView>
0035 #include <QComboBox>
0036 
0037 #include <KLocalizedString>
0038 #include <KColorScheme>
0039 
0040 #include <BibTeXFields>
0041 #include <Preferences>
0042 #include <Entry>
0043 #include <models/FileModel>
0044 #include "widgets/starrating.h"
0045 #include "field/fieldlineedit.h"
0046 #include "logging_gui.h"
0047 
0048 class ValueListDelegate::Private {
0049 public:
0050     QTreeView *treeView;
0051     ValueListDelegate *parent;
0052     QString fieldName;
0053 
0054     Private(QTreeView *_treeView, ValueListDelegate *_parent)
0055             : treeView(_treeView), parent(_parent)
0056     {
0057         // nothing
0058     }
0059 };
0060 
0061 ValueListDelegate::ValueListDelegate(QTreeView *parent)
0062         : QStyledItemDelegate(parent), d(new ValueListDelegate::Private(parent, this))
0063 {
0064     // nothing
0065 }
0066 
0067 ValueListDelegate::~ValueListDelegate()
0068 {
0069     delete d;
0070 }
0071 
0072 QWidget *ValueListDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &sovi, const QModelIndex &index) const
0073 {
0074     if (index.column() == 0) {
0075         const FieldDescription &fd = BibTeXFields::instance().find(d->fieldName);
0076         FieldLineEdit *fieldLineEdit = new FieldLineEdit(fd.preferredTypeFlag, fd.typeFlags, false, parent);
0077         fieldLineEdit->setAutoFillBackground(true);
0078         return fieldLineEdit;
0079     } else
0080         return QStyledItemDelegate::createEditor(parent, sovi, index);
0081 }
0082 
0083 void ValueListDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
0084 {
0085     if (index.column() == 0) {
0086         FieldLineEdit *fieldLineEdit = qobject_cast<FieldLineEdit *>(editor);
0087         if (fieldLineEdit != nullptr)
0088             fieldLineEdit->reset(index.model()->data(index, Qt::EditRole).value<Value>());
0089     }
0090 }
0091 
0092 void ValueListDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
0093 {
0094     FieldLineEdit *fieldLineEdit = qobject_cast<FieldLineEdit *>(editor);
0095     if (fieldLineEdit != nullptr) {
0096         Value v;
0097         fieldLineEdit->apply(v);
0098         if (v.count() == 1) /// field should contain exactly one value item (no zero, not two or more)
0099             model->setData(index, QVariant::fromValue(v));
0100     }
0101 }
0102 
0103 QSize ValueListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
0104 {
0105     QSize size = QStyledItemDelegate::sizeHint(option, index);
0106     size.setHeight(qMax(size.height(), option.fontMetrics.height() * 3 / 2));   // TODO calculate height better
0107     return size;
0108 }
0109 
0110 void ValueListDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
0111 {
0112     QStyledItemDelegate::initStyleOption(option, index);
0113     if (option->decorationPosition != QStyleOptionViewItem::Top) {
0114         /// remove text from style (do not draw text)
0115         option->text.clear();
0116     }
0117 
0118 }
0119 
0120 void ValueListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &_option, const QModelIndex &index) const
0121 {
0122     QStyleOptionViewItem option = _option;
0123 
0124     /// code heavily inspired by kdepimlibs-4.6.3/akonadi/collectionstatisticsdelegate.cpp
0125 
0126     /// save painter's state, restored before leaving this function
0127     painter->save();
0128 
0129     /// first, paint the basic, but without the text. We remove the text
0130     /// in initStyleOption(), which gets called by QStyledItemDelegate::paint().
0131     QStyledItemDelegate::paint(painter, option, index);
0132 
0133     /// now, we retrieve the correct style option by calling intiStyleOption from
0134     /// the superclass.
0135     QStyledItemDelegate::initStyleOption(&option, index);
0136     QString field = option.text;
0137     const bool customPainting {index.column() == 0 && d->fieldName.toLower() == Entry::ftStarRating};
0138 
0139     /// now calculate the rectangle for the text
0140     QStyle *s = d->treeView->style();
0141     const QWidget *widget = option.widget;
0142     const QRect textRect = s->subElementRect(QStyle::SE_ItemViewItemText, &option, widget);
0143 
0144     if (option.state & QStyle::State_Selected) {
0145         /// selected lines are drawn with different color
0146         painter->setPen(option.palette.highlightedText().color());
0147     }
0148 
0149     /// count will be empty unless only one column is shown
0150     const QString count = index.column() == 0 && index.model()->columnCount() == 1 ? QString(QStringLiteral(" (%1)")).arg(index.data(ValueListModel::CountRole).toInt()) : QString();
0151 
0152     /// squeeze the folder text if it is to big and calculate the rectangles
0153     /// where the folder text and the unread count will be drawn to
0154     const QFontMetrics fm(painter->fontMetrics());
0155 #if QT_VERSION >= 0x050b00
0156     const int countWidth = fm.horizontalAdvance(count);
0157     int fieldWidth = customPainting ? textRect.width() - countWidth - 8 : fm.horizontalAdvance(field);
0158 #else // QT_VERSION >= 0x050b00
0159     const int countWidth = fm.width(count);
0160     int fieldWidth = customPainting ? textRect.width() - countWidth - 8 : fm.width(field);
0161 #endif // QT_VERSION >= 0x050b00
0162     if (countWidth + fieldWidth > textRect.width()) {
0163         /// text plus count is too wide for column, cut text and insert "..."
0164         field = fm.elidedText(field, Qt::ElideRight, textRect.width() - countWidth - 8);
0165         fieldWidth = textRect.width() - countWidth - 12;
0166     }
0167 
0168     /// determine rects to draw field
0169     int top = textRect.top() + (textRect.height() - fm.height()) / 2;
0170     QRect fieldRect = textRect;
0171     QRect countRect = textRect;
0172     fieldRect.setTop(top);
0173     fieldRect.setHeight(fm.height());
0174 
0175     if (d->treeView->header()->visualIndex(index.column()) == 0) {
0176         /// left-align text
0177         fieldRect.setLeft(fieldRect.left() + 4); ///< hm, indent necessary?
0178         fieldRect.setRight(fieldRect.left() + fieldWidth);
0179     } else {
0180         /// right-align text
0181         fieldRect.setRight(fieldRect.right() - 4); ///< hm, indent necessary?
0182         fieldRect.setLeft(fieldRect.right() - fieldWidth); ///< hm, indent necessary?
0183     }
0184 
0185     if (customPainting) {
0186         static StarRatingPainter starrating;
0187         bool ok = false;
0188         const float percent = field.toFloat(&ok);
0189         if (ok && percent >= 0.0 && percent <= 100.0) {
0190             starrating.paint(painter, fieldRect, percent);
0191         }
0192     } else {
0193         // Draw field name
0194         painter->drawText(fieldRect, Qt::AlignLeft, field);
0195     }
0196 
0197     if (!count.isEmpty()) {
0198         /// determine rects to draw count
0199         countRect.setTop(top);
0200         countRect.setHeight(fm.height());
0201         countRect.setLeft(fieldRect.right());
0202 
0203         /// use bold font
0204         QFont font = painter->font();
0205         font.setBold(true);
0206         painter->setFont(font);
0207         /// determine color for count number
0208         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();
0209         painter->setPen(countColor);
0210 
0211         /// draw count
0212         painter->drawText(countRect, Qt::AlignLeft, count);
0213     }
0214 
0215     /// restore painter's state
0216     painter->restore();
0217 }
0218 
0219 void ValueListDelegate::setFieldName(const QString &fieldName) {
0220     d->fieldName = fieldName;
0221 }
0222 
0223 ValueListModel::ValueListModel(const File *bibtexFile, const QString &fieldName, QObject *parent)
0224         : QAbstractTableModel(parent), file(bibtexFile), fName(fieldName.toLower()), showCountColumn(true), sortBy(SortBy::Text)
0225 {
0226     readConfiguration();
0227     updateValues();
0228     NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged);
0229 }
0230 
0231 int ValueListModel::rowCount(const QModelIndex &parent) const
0232 {
0233     return parent == QModelIndex() ? values.count() : 0;
0234 }
0235 
0236 int ValueListModel::columnCount(const QModelIndex &parent) const
0237 {
0238     return parent == QModelIndex() ? (showCountColumn ? 2 : 1) : 0;
0239 }
0240 
0241 QVariant ValueListModel::data(const QModelIndex &index, int role) const
0242 {
0243     if (index.row() >= values.count() || index.column() >= 2)
0244         return QVariant();
0245     if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
0246         if (index.column() == 0) {
0247             if (fName == Entry::ftColor) {
0248                 QString text = values[index.row()].text;
0249                 if (text.isEmpty()) return QVariant();
0250                 QString colorText = colorToLabel[text];
0251                 if (colorText.isEmpty()) return QVariant(text);
0252                 return QVariant(colorText);
0253             } else if (fName == Entry::ftStarRating) {
0254                 bool ok = false;
0255                 const double percent {StarRatingPainter::roundToNearestHalfStarPercent(values[index.row()].text.toFloat(&ok))};
0256                 if (ok)
0257                     return QVariant(QString::number(percent, 'f', 2));
0258             }
0259 
0260             return QVariant(values[index.row()].text);
0261         } else
0262             return QVariant(values[index.row()].count);
0263     } else if (role == SortRole) {
0264         static const QRegularExpression ignoredInSorting(QStringLiteral("[{}\\\\]+"));
0265 
0266         QString buffer = values[index.row()].sortBy.isEmpty() ? values[index.row()].text : values[index.row()].sortBy;
0267         buffer = buffer.remove(ignoredInSorting).toLower();
0268 
0269         if ((showCountColumn && index.column() == 1) || (!showCountColumn && sortBy == SortBy::Count)) {
0270             /// Sort by string consisting of a zero-padded count and the lower-case text,
0271             /// for example "0000000051keyword"
0272             /// Used if (a) two columns are shown (showCountColumn is true) and column 1
0273             /// (the count column) is to be sorted or (b) if only one column is shown
0274             /// (showCountColumn is false) and this single column is to be sorted by count.
0275             return QString(QStringLiteral("%1%2")).arg(values[index.row()].count, 10, 10, QLatin1Char('0')).arg(buffer);
0276         } else {
0277             /// Otherwise use lower-case text for sorting
0278             return QVariant(buffer);
0279         }
0280     } else if (role == SearchTextRole) {
0281         return QVariant(values[index.row()].text);
0282     } else if (role == Qt::EditRole)
0283         return QVariant::fromValue(values[index.row()].value);
0284     else if (role == CountRole)
0285         return QVariant(values[index.row()].count);
0286     else
0287         return QVariant();
0288 }
0289 
0290 bool ValueListModel::setData(const QModelIndex &index, const QVariant &value, int role)
0291 {
0292     Q_ASSERT_X(file != nullptr, "ValueListModel::setData", "You cannot set data if there is no BibTeX file associated with this value list.");
0293 
0294     // Continue only if in edit role and first column is to be changed
0295     if (role == Qt::EditRole && index.column() == 0) {
0296         // Fetch the string as it was shown before the editing started
0297         QString origText = data(index, Qt::DisplayRole).toString();
0298         /// Special treatment for colors
0299         if (fName == Entry::ftColor) {
0300             /// for colors, convert color (RGB) to the associated label
0301             QString color = colorToLabel.key(origText);
0302             if (!color.isEmpty()) origText = color;
0303         }
0304 
0305         /// Retrieve the Value object containing the user-entered data
0306         Value newValue = value.value<Value>(); /// nice variable names ... ;-)
0307         if (newValue.isEmpty()) {
0308             qCWarning(LOG_KBIBTEX_GUI) << "Cannot replace with empty value";
0309             return false;
0310         }
0311 
0312         /// Fetch the string representing the new, user-entered value
0313         const QString newText = PlainTextValue::text(newValue);
0314         if (newText == origText) {
0315             qCWarning(LOG_KBIBTEX_GUI) << "Skipping to replace value with itself";
0316             return false;
0317         }
0318 
0319         bool success = searchAndReplaceValueInEntries(index, newValue) && searchAndReplaceValueInModel(index, newValue);
0320         return success;
0321     }
0322     return false;
0323 }
0324 
0325 Qt::ItemFlags ValueListModel::flags(const QModelIndex &index) const
0326 {
0327     Qt::ItemFlags result = QAbstractTableModel::flags(index);
0328     /// make first column editable
0329     if (index.column() == 0)
0330         result |= Qt::ItemIsEditable;
0331     return result;
0332 }
0333 
0334 QVariant ValueListModel::headerData(int section, Qt::Orientation orientation, int role) const
0335 {
0336     if (section >= 2 || orientation != Qt::Horizontal || role != Qt::DisplayRole)
0337         return QVariant();
0338     else if ((section == 0 && columnCount() == 2) || (columnCount() == 1 && sortBy == SortBy::Text))
0339         return QVariant(i18n("Value"));
0340     else
0341         return QVariant(i18n("Count"));
0342 }
0343 
0344 void ValueListModel::removeValue(const QModelIndex &index)
0345 {
0346     removeValueFromEntries(index);
0347     removeValueFromModel(index);
0348 }
0349 
0350 void ValueListModel::setShowCountColumn(bool showCountColumn)
0351 {
0352     beginResetModel();
0353     this->showCountColumn = showCountColumn;
0354     endResetModel();
0355 }
0356 
0357 void ValueListModel::setSortBy(SortBy sortBy)
0358 {
0359     beginResetModel();
0360     this->sortBy = sortBy;
0361     endResetModel();
0362 }
0363 
0364 void ValueListModel::notificationEvent(int eventId)
0365 {
0366     if (eventId == NotificationHub::EventConfigurationChanged) {
0367         beginResetModel();
0368         readConfiguration();
0369         endResetModel();
0370     }
0371 }
0372 
0373 void ValueListModel::readConfiguration()
0374 {
0375     // Load mapping from color value to label from Preferences
0376     colorToLabel.clear();
0377     for (QVector<QPair<QString, QString>>::ConstIterator it = Preferences::instance().colorCodes().constBegin(); it != Preferences::instance().colorCodes().constEnd(); ++it)
0378         colorToLabel.insert(it->first, it->second);
0379 }
0380 
0381 void ValueListModel::updateValues()
0382 {
0383     values.clear();
0384     if (file == nullptr) return;
0385 
0386     for (const auto &element : const_cast<const File &>(*file)) {
0387         QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>();
0388         if (!entry.isNull() && entry->contains(fName)) {
0389             const Value &v {entry->value(fName)};
0390             if (v.isEmpty())
0391                 qCWarning(LOG_KBIBTEX_GUI) << "value for key" << fName << "in entry" << entry->id() << "is empty";
0392             if (fName == Entry::ftStarRating) {
0393                 bool ok = false;
0394                 const double percent {StarRatingPainter::roundToNearestHalfStarPercent(PlainTextValue::text(v).toFloat(&ok))};
0395                 if (ok) {
0396                     const QString text {QString::number(percent, 'f', 2)};
0397                     const QString zeroPadded {QString(QStringLiteral("%1")).arg(static_cast<int>(percent * 1000.0), 6, 10, QLatin1Char('0'))};
0398                     QSharedPointer<PlainText> plainText {QSharedPointer<PlainText>(new PlainText(text))};
0399                     insertText(text, plainText, zeroPadded);
0400                 }
0401             } else
0402                 insertValue(v);
0403         }
0404     }
0405 }
0406 
0407 void ValueListModel::insertValue(const Value &value)
0408 {
0409     for (const QSharedPointer<ValueItem> &item : value) {
0410         const QString text = PlainTextValue::text(*item);
0411         if (text.isEmpty()) continue; ///< skip empty values
0412 
0413         insertText(text, item);
0414     }
0415 }
0416 
0417 void ValueListModel::insertText(const QString &text, const QSharedPointer<ValueItem> &item, const QString &sortBy)
0418 {
0419     int index = indexOf(text);
0420     if (index < 0) {
0421         // Previously unknown text
0422         ValueLine newValueLine;
0423         newValueLine.text = text;
0424         newValueLine.count = 1;
0425         newValueLine.value.append(item);
0426 
0427         // Memorize sorting criterium:
0428         // * if a sorting criterion is given via 'sortBy', use it
0429         // * for persons, use last name first
0430         // * in any other case, use lower case
0431         const QSharedPointer<Person> person = item.dynamicCast<Person>();
0432         newValueLine.sortBy = !sortBy.isEmpty() ? sortBy : (person.isNull() ? text.toLower() : person->lastName().toLower() + QStringLiteral(" ") + person->firstName().toLower());
0433 
0434         values << newValueLine;
0435     } else {
0436         ++values[index].count;
0437     }
0438 }
0439 
0440 int ValueListModel::indexOf(const QString &text)
0441 {
0442     QString color;
0443     QString cmpText = text;
0444     if (fName == Entry::ftColor && !(color = colorToLabel.key(text, QString())).isEmpty())
0445         cmpText = color;
0446     if (cmpText.isEmpty())
0447         qCWarning(LOG_KBIBTEX_GUI) << "Should never happen";
0448 
0449     int i = 0;
0450     /// TODO this is really slow for large data sets: O(n^2)
0451     /// maybe use a hash table instead?
0452     for (const ValueLine &valueLine : const_cast<const ValueLineList &>(values)) {
0453         if (valueLine.text == cmpText)
0454             return i;
0455         ++i;
0456     }
0457     return -1;
0458 }
0459 
0460 bool ValueListModel::searchAndReplaceValueInEntries(const QModelIndex &index, const Value &newValue)
0461 {
0462     /// Fetch the string representing the new, user-entered value
0463     const QString newText = PlainTextValue::text(newValue);
0464 
0465     if (newText.isEmpty())
0466         return false;
0467 
0468     /// Fetch the string as it was shown before the editing started
0469     QString origText = data(index, Qt::DisplayRole).toString();
0470     /// Special treatment for colors
0471     if (fName == Entry::ftColor) {
0472         /// for colors, convert color (RGB) to the associated label
0473         QString color = colorToLabel.key(origText);
0474         if (!color.isEmpty()) origText = color;
0475     }
0476 
0477     /// Go through all elements in the current file
0478     for (const QSharedPointer<Element> &element : const_cast<const File &>(*file)) {
0479         QSharedPointer<Entry> entry = element.dynamicCast<Entry>();
0480         /// Process only Entry objects
0481         if (!entry.isNull()) {
0482             /// Go through every key-value pair in entry (author, title, ...)
0483             for (Entry::Iterator eit = entry->begin(); eit != entry->end(); ++eit) {
0484                 /// Fetch key-value pair's key
0485                 const QString key = eit.key().toLower();
0486                 /// Process only key-value pairs that are filtered for (e.g. only keywords)
0487                 if (key == fName) {
0488                     eit.value().replace(origText, newValue.first());
0489                     break;
0490                 }
0491             }
0492         }
0493     }
0494 
0495     return true;
0496 }
0497 
0498 bool ValueListModel::searchAndReplaceValueInModel(const QModelIndex &index, const Value &newValue)
0499 {
0500     /// Fetch the string representing the new, user-entered value
0501     const QString newText = PlainTextValue::text(newValue);
0502     if (newText.isEmpty())
0503         return false;
0504 
0505     const int row = index.row();
0506 
0507     /// Test if user-entered text exists already in model's data
0508     /// newTextAlreadyInListIndex will be row of duplicate or
0509     /// -1 if new text is unique
0510     int newTextAlreadyInListIndex = -1;
0511     for (int r = values.count() - 1; newTextAlreadyInListIndex < 0 && r >= 0; --r) {
0512         if (row != r && values[r].text == newText)
0513             newTextAlreadyInListIndex = r;
0514     }
0515 
0516     if (newTextAlreadyInListIndex < 0) {
0517         /// User-entered text is unique, so simply replace
0518         /// old text with new text
0519         values[row].text = newText;
0520         values[row].value = newValue;
0521         const QSharedPointer<Person> person = newValue.first().dynamicCast<Person>();
0522         values[row].sortBy = person.isNull() ? QString() : person->lastName() + QStringLiteral(" ") + person->firstName();
0523     } else {
0524         /// The user-entered text existed before
0525 
0526         const int lastRow = values.count() - 1;
0527         if (row != lastRow) {
0528             /// Unless duplicate is last one in list,
0529             /// overwrite edited row with last row's value
0530             values[row].text = values[lastRow].text;
0531             values[row].value = values[lastRow].value;
0532             values[row].sortBy = values[lastRow].sortBy;
0533         }
0534 
0535         /// Remove last row, which is no longer used
0536         beginRemoveRows(QModelIndex(), lastRow, lastRow);
0537         values.remove(lastRow);
0538         endRemoveRows();
0539     }
0540 
0541     /// Notify Qt about data changed
0542     Q_EMIT dataChanged(index, index);
0543 
0544     return true;
0545 }
0546 
0547 void ValueListModel::removeValueFromEntries(const QModelIndex &index)
0548 {
0549     /// Retrieve the Value object containing the user-entered data
0550     const Value toBeDeletedValue = values[index.row()].value;
0551     if (toBeDeletedValue.isEmpty()) {
0552         return;
0553     }
0554     const QString toBeDeletedText = PlainTextValue::text(toBeDeletedValue);
0555     if (toBeDeletedText.isEmpty()) {
0556         return;
0557     }
0558 
0559     /// Go through all elements in the current file
0560     for (const QSharedPointer<Element> &element : const_cast<const File &>(*file)) {
0561         QSharedPointer<Entry> entry = element.dynamicCast<Entry>();
0562         /// Process only Entry objects
0563         if (!entry.isNull()) {
0564             /// Go through every key-value pair in entry (author, title, ...)
0565             for (Entry::Iterator eit = entry->begin(); eit != entry->end(); ++eit) {
0566                 /// Fetch key-value pair's key
0567                 const QString key = eit.key().toLower();
0568                 /// Process only key-value pairs that are filtered for (e.g. only keywords)
0569                 if (key == fName) {
0570                     /// Fetch the key-value pair's value's textual representation
0571                     const QString valueFullText = PlainTextValue::text(eit.value());
0572                     if (valueFullText == toBeDeletedText) {
0573                         /// If the key-value pair's value's textual representation is the same
0574                         /// as the value to be delted, remove this key-value pair
0575                         /// This test is usually true for keys like title, year, or edition.
0576                         entry->remove(key); /// This would break the Iterator, but code "breakes" from loop anyways
0577                     } else {
0578                         /// The test above failed, but the delete operation may have
0579                         /// to be applied to a ValueItem inside the value.
0580                         /// Possible keys for such a case include author, editor, or keywords.
0581 
0582                         /// Process each ValueItem inside this Value
0583                         for (Value::Iterator vit = eit.value().begin(); vit != eit.value().end();) {
0584                             /// Similar procedure as for full values above:
0585                             /// If a ValueItem's textual representation is the same
0586                             /// as the shown string which has be deleted, remove the
0587                             /// ValueItem from this Value. If the Value becomes empty,
0588                             /// remove Value as well.
0589                             const QString valueItemText = PlainTextValue::text(* (*vit));
0590                             if (valueItemText == toBeDeletedText) {
0591                                 /// Erase old ValueItem from this Value
0592                                 vit = eit.value().erase(vit);
0593                             } else
0594                                 ++vit;
0595                         }
0596 
0597                         if (eit.value().isEmpty()) {
0598                             /// This value does no longer contain any ValueItems.
0599                             entry->remove(key); /// This would break the Iterator, but code "breakes" from loop anyways
0600                         }
0601                     }
0602                     break;
0603                 }
0604             }
0605         }
0606     }
0607 }
0608 
0609 void ValueListModel::removeValueFromModel(const QModelIndex &index)
0610 {
0611     const int row = index.row();
0612     const int lastRow = values.count() - 1;
0613 
0614     if (row != lastRow) {
0615         /// Unless duplicate is last one in list,
0616         /// overwrite edited row with last row's value
0617         values[row].text = values[lastRow].text;
0618         values[row].value = values[lastRow].value;
0619         values[row].sortBy = values[lastRow].sortBy;
0620 
0621         Q_EMIT dataChanged(index, index);
0622     }
0623 
0624     /// Remove last row, which is no longer used
0625     beginRemoveRows(QModelIndex(), lastRow, lastRow);
0626     values.remove(lastRow);
0627     endRemoveRows();
0628 }