File indexing completed on 2025-02-23 04:34:23

0001 /**
0002  * \file frameitemdelegate.cpp
0003  * Delegate for table widget items.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 01 May 2011
0008  *
0009  * Copyright (C) 2011-2024  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  */
0026 
0027 #include "frameitemdelegate.h"
0028 #include <cmath>
0029 #include <QComboBox>
0030 #include <QLineEdit>
0031 #include <QPainter>
0032 #include <QMouseEvent>
0033 #include "frametablemodel.h"
0034 #include "genremodel.h"
0035 #include "formatconfig.h"
0036 #include "tagconfig.h"
0037 #include "tracknumbervalidator.h"
0038 #include "framenotice.h"
0039 
0040 namespace {
0041 
0042 constexpr int MAX_STAR_COUNT = 5;
0043 constexpr int STAR_SCALE_FACTOR = 20;
0044 
0045 QString ratingTypeName(const QModelIndex& index) {
0046   QString name = index.data(FrameTableModel::InternalNameRole).toString();
0047   if (name.startsWith(QLatin1String("POPM"))) {
0048     name.truncate(4);
0049     QVariantList fieldIds = index.data(FrameTableModel::FieldIdsRole).toList();
0050     if (int emailIdx = fieldIds.indexOf(Frame::ID_Email); emailIdx != -1) {
0051       QVariantList fieldValues =
0052           index.data(FrameTableModel::FieldValuesRole).toList();
0053       if (QString emailValue;
0054           emailIdx < fieldValues.size() &&
0055           !(emailValue = fieldValues.at(emailIdx).toString()).isEmpty()) {
0056         name += QLatin1Char('.');
0057         name += emailValue;
0058       }
0059     }
0060   }
0061   return name;
0062 }
0063 
0064 int starCountFromRating(int rating, const QModelIndex& index)
0065 {
0066   return rating < 1
0067       ? 0
0068       : TagConfig::instance().starCountFromRating(rating, ratingTypeName(index));
0069 }
0070 
0071 int starCountToRating(int starCount, const QModelIndex& index) {
0072   return starCount < 1
0073       ? 0
0074       : TagConfig::instance().starCountToRating(starCount, ratingTypeName(index));
0075 }
0076 
0077 
0078 /**
0079  * Validator for date/time values.
0080  */
0081 class DateTimeValidator : public QValidator {
0082 public:
0083   /**
0084    * Constructor.
0085    * @param parent parent object
0086    */
0087   explicit DateTimeValidator(QObject* parent = nullptr);
0088 
0089   /**
0090    * Destructor.
0091    */
0092   ~DateTimeValidator() override = default;
0093 
0094   /**
0095    * Validate input string.
0096    * @param input input string
0097    * @param pos current position in input string
0098    * @return validation state.
0099    */
0100   State validate(QString& input, int& pos) const override;
0101 
0102   /**
0103    * Attempt to change input string to be valid.
0104    * @param input input string
0105    */
0106   void fixup(QString& input) const override;
0107 
0108 private:
0109   Q_DISABLE_COPY(DateTimeValidator)
0110 
0111   const QRegularExpression m_re;
0112   mutable QString m_lastValidInput;
0113 };
0114 
0115 DateTimeValidator::DateTimeValidator(QObject* parent)
0116   : QValidator(parent), m_re(FrameNotice::isoDateTimeRexExp())
0117 {
0118 }
0119 
0120 QValidator::State DateTimeValidator::validate(QString& input, int& pos) const
0121 {
0122   if (auto dateTimeMatch = m_re.match(
0123         input, 0, QRegularExpression::PartialPreferCompleteMatch);
0124       dateTimeMatch.hasMatch()) {
0125     m_lastValidInput = input;
0126     return Acceptable;
0127   } else {
0128     if (const int len = dateTimeMatch.capturedLength(); len == input.size()) {
0129       return Intermediate;
0130 #if QT_VERSION >= 0x060000
0131     } else if (len > 0 && m_lastValidInput.endsWith(input.mid(len))) {
0132 #else
0133     } else if (len > 0 && m_lastValidInput.endsWith(input.midRef(len))) {
0134 #endif
0135       return Intermediate;
0136     }
0137     pos = input.size();
0138     return Invalid;
0139   }
0140 }
0141 
0142 void DateTimeValidator::fixup(QString& input) const
0143 {
0144   if (!m_lastValidInput.isEmpty()) {
0145     input = m_lastValidInput;
0146   }
0147 }
0148 
0149 
0150 /**
0151  * Helper class providing methods to paint stars for a rating.
0152  */
0153 class StarPainter {
0154 public:
0155   /**
0156    * Constructor.
0157    * @param starCount number of stars to paint
0158    * @param maxStarCount maximum number of stars
0159    */
0160   explicit StarPainter(int starCount, int maxStarCount)
0161     : m_starCount(starCount), m_maxStarCount(maxStarCount) {
0162   }
0163 
0164   /**
0165    * Get size needed for stars.
0166    */
0167   QSize sizeHint() const;
0168 
0169   /**
0170    * Paint stars to @a painter.
0171    */
0172   void paint(QPainter& painter, const QRect& rect,
0173              const QPalette& palette, bool editable) const;
0174 
0175 private:
0176   const int m_starCount;
0177   const int m_maxStarCount;
0178 
0179   static QPolygonF s_starPolygon;
0180 };
0181 
0182 QPolygonF StarPainter::s_starPolygon;
0183 
0184 QSize StarPainter::sizeHint() const {
0185   return STAR_SCALE_FACTOR * QSize(m_maxStarCount, 1);
0186 }
0187 
0188 void StarPainter::paint(QPainter& painter, const QRect& rect,
0189                         const QPalette& palette, bool editable) const
0190 {
0191   if (s_starPolygon.isEmpty()) {
0192     // First time initialization.
0193     qreal a = -0.314;
0194     for (int i = 0; i < 5; ++i) {
0195       s_starPolygon << QPointF(0.5 + 0.5 * std::cos(a), 0.5 + 0.5 * std::sin(a));
0196       a += 2.513;
0197     }
0198   }
0199 
0200   painter.save();
0201 
0202   QBrush brush(editable ? palette.highlight() : palette.windowText());
0203   QPen starPen(Qt::NoPen);
0204   QPen dotPen(brush, 0.2);
0205   dotPen.setCapStyle(Qt::RoundCap);
0206 
0207   painter.setRenderHint(QPainter::Antialiasing, true);
0208   painter.setBrush(brush);
0209 
0210   int yOffset = (rect.height() - STAR_SCALE_FACTOR) / 2;
0211   painter.translate(rect.x(), rect.y() + yOffset);
0212   painter.scale(STAR_SCALE_FACTOR, STAR_SCALE_FACTOR);
0213 
0214   for (int i = 0; i < m_maxStarCount; ++i) {
0215     if (i < m_starCount) {
0216       painter.setPen(starPen);
0217       painter.drawPolygon(s_starPolygon, Qt::WindingFill);
0218     } else if (editable) {
0219       painter.setPen(dotPen);
0220       painter.drawPoint(QPointF(0.5, 0.5));
0221     }
0222     painter.translate(1.0, 0.0);
0223   }
0224 
0225   painter.restore();
0226 }
0227 
0228 }
0229 
0230 /**
0231  * Constructor.
0232  * @param parent parent widget
0233  */
0234 StarEditor::StarEditor(QWidget* parent)
0235   : QWidget(parent), m_starCount(0), m_paintedStarCount(0),
0236     m_starCountEdited(false)
0237 {
0238   setMouseTracking(true);
0239   setAutoFillBackground(true);
0240 }
0241 
0242 /**
0243  * Get size needed by editor.
0244  * @return size needed by editor.
0245  */
0246 QSize StarEditor::sizeHint() const
0247 {
0248   return StarPainter(0, MAX_STAR_COUNT).sizeHint();
0249 }
0250 
0251 /**
0252  * Set star rating.
0253  * @param starCount number of stars
0254  */
0255 void StarEditor::setStarCount(int starCount)
0256 {
0257   m_starCount = starCount;
0258   m_paintedStarCount = starCount;
0259   m_starCountEdited = false;
0260 }
0261 
0262 void StarEditor::paintEvent(QPaintEvent*)
0263 {
0264   QPainter painter(this);
0265   StarPainter(m_paintedStarCount, MAX_STAR_COUNT)
0266       .paint(painter, rect(), palette(), true);
0267 }
0268 
0269 void StarEditor::mouseMoveEvent(QMouseEvent* event)
0270 {
0271 #if QT_VERSION >= 0x060000
0272   int starNr = starAtPosition(qRound(event->position().x()));
0273 #else
0274   int starNr = starAtPosition(event->x());
0275 #endif
0276 
0277   if (starNr != m_paintedStarCount && starNr != -1) {
0278     m_paintedStarCount = starNr;
0279     update();
0280   }
0281 }
0282 
0283 void StarEditor::mouseReleaseEvent(QMouseEvent*)
0284 {
0285   modifyStarCount(m_paintedStarCount);
0286   emit editingFinished();
0287 }
0288 
0289 void StarEditor::keyPressEvent(QKeyEvent* event)
0290 {
0291   if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
0292     modifyStarCount(m_paintedStarCount);
0293     emit editingFinished();
0294   } else if (event->key() == Qt::Key_Escape) {
0295     emit editingFinished();
0296   } else if (event->key() == Qt::Key_Left) {
0297     if (m_paintedStarCount > 0) {
0298       --m_paintedStarCount;
0299       update();
0300     }
0301   } else if (event->key() == Qt::Key_Right) {
0302     if (m_paintedStarCount < MAX_STAR_COUNT) {
0303       ++m_paintedStarCount;
0304       update();
0305     }
0306   } else {
0307     QWidget::keyPressEvent(event);
0308   }
0309 }
0310 
0311 int StarEditor::starAtPosition(int x)
0312 {
0313   int star = (x / (StarPainter(0, MAX_STAR_COUNT).sizeHint().width()
0314                    / MAX_STAR_COUNT)) + 1;
0315   if (star <= 0 || star > MAX_STAR_COUNT)
0316     return -1;
0317 
0318   return star;
0319 }
0320 
0321 void StarEditor::modifyStarCount(int starCount)
0322 {
0323   if (m_starCount != starCount) {
0324     m_starCount = starCount;
0325     m_starCountEdited = true;
0326   } else if (m_starCount == 1) {
0327     // Set zero stars when clicking again on 1 star when 1 star is already set.
0328     m_starCount = 0;
0329     m_starCountEdited = true;
0330   }
0331 }
0332 
0333 
0334 /**
0335  * Constructor.
0336  * @param genreModel genre model
0337  * @param parent parent QTableView
0338  */
0339 FrameItemDelegate::FrameItemDelegate(GenreModel* genreModel, QObject* parent)
0340   : QItemDelegate(parent),
0341     m_genreModel(genreModel),
0342     m_trackNumberValidator(new TrackNumberValidator(this)),
0343     m_dateTimeValidator(new DateTimeValidator(this))
0344 {
0345   setObjectName(QLatin1String("FrameItemDelegate"));
0346 }
0347 
0348 /**
0349  * Render delegate.
0350  * @param painter painter to be used
0351  * @param option style
0352  * @param index index of item
0353  */
0354 void FrameItemDelegate::paint(
0355     QPainter* painter, const QStyleOptionViewItem& option,
0356     const QModelIndex& index) const
0357 {
0358   if (index.row() >= 0 && index.column() == FrameTableModel::CI_Value &&
0359       index.data(FrameTableModel::FrameTypeRole).toInt() == Frame::FT_Rating) {
0360     int starCount = starCountFromRating(index.data().toInt(), index);
0361     if (option.state & QStyle::State_Selected) {
0362       painter->fillRect(option.rect, option.palette.highlight());
0363     }
0364     StarPainter(starCount, MAX_STAR_COUNT).paint(*painter, option.rect,
0365                                                  option.palette, false);
0366     return;
0367   }
0368   QItemDelegate::paint(painter, option, index);
0369 }
0370 
0371 /**
0372  * Get size needed by delegate.
0373  * @param option style
0374  * @param index  index of item
0375  * @return size needed by delegate.
0376  */
0377 QSize FrameItemDelegate::sizeHint(const QStyleOptionViewItem& option,
0378                                   const QModelIndex& index) const
0379 {
0380   if (index.row() >= 0 && index.column() == FrameTableModel::CI_Value &&
0381       index.data(FrameTableModel::FrameTypeRole).toInt() == Frame::FT_Rating) {
0382     int starCount = starCountFromRating(index.data().toInt(), index);
0383     return StarPainter(starCount, MAX_STAR_COUNT).sizeHint();
0384   }
0385   return QItemDelegate::sizeHint(option, index);
0386 }
0387 
0388 /**
0389  * Format text if enabled.
0390  * @param txt text to format and set in line edit
0391  */
0392 void FrameItemDelegate::formatTextIfEnabled(const QString& txt)
0393 {
0394   if (QLineEdit* le;
0395       TagFormatConfig::instance().formatWhileEditing() &&
0396       (le = qobject_cast<QLineEdit*>(sender())) != nullptr) {
0397     QString str(txt);
0398     TagFormatConfig::instance().formatString(str);
0399     if (str != txt) {
0400       int curPos = le->cursorPosition();
0401       le->setText(str);
0402       le->setCursorPosition(curPos + str.length() - txt.length());
0403     }
0404   }
0405 }
0406 
0407 /**
0408  * Create an editor to edit the cells contents.
0409  * @param parent parent widget
0410  * @param option style
0411  * @param index  index of item
0412  * @return combo box editor widget.
0413  */
0414 QWidget* FrameItemDelegate::createEditor(
0415   QWidget* parent, const QStyleOptionViewItem& option,
0416   const QModelIndex& index) const
0417 {
0418   int row = index.row();
0419   int col = index.column();
0420   const auto ftModel =
0421     qobject_cast<const FrameTableModel*>(index.model());
0422   if (row >= 0 && (col == FrameTableModel::CI_Value || !ftModel)) {
0423     auto type = static_cast<Frame::Type>(
0424       index.data(FrameTableModel::FrameTypeRole).toInt());
0425     bool id3v1 = ftModel && ftModel->isId3v1();
0426     if (type == Frame::FT_Genre) {
0427       auto cb = new QComboBox(parent);
0428       if (!id3v1) {
0429         cb->setEditable(true);
0430         cb->setDuplicatesEnabled(false);
0431       }
0432 
0433       cb->setModel(m_genreModel);
0434       return cb;
0435     }
0436     if (type == Frame::FT_Rating) {
0437       auto editor = new StarEditor(parent);
0438       connect(editor, &StarEditor::editingFinished,
0439               this, &FrameItemDelegate::commitAndCloseEditor);
0440       return editor;
0441     }
0442     if (ftModel && index.data().toString() == Frame::differentRepresentation()) {
0443       Frame::ExtendedType extType(
0444             type, index.data(FrameTableModel::InternalNameRole).toString());
0445       if (QSet<QString> valueSet = ftModel->getCompletionsForType(extType);
0446           !valueSet.isEmpty()) {
0447 #if QT_VERSION >= 0x050e00
0448         QStringList values(valueSet.constBegin(), valueSet.constEnd());
0449 #else
0450         QStringList values = valueSet.toList();
0451 #endif
0452         values.sort();
0453         auto cb = new QComboBox(parent);
0454         cb->setEditable(true);
0455         cb->setDuplicatesEnabled(false);
0456         cb->addItems(values);
0457         cb->setEditText(index.data().toString());
0458         return cb;
0459       }
0460     }
0461     QWidget* editor = QItemDelegate::createEditor(parent, option, index);
0462     auto lineEdit = qobject_cast<QLineEdit*>(editor);
0463     if (id3v1 &&
0464         (type == Frame::FT_Comment || type == Frame::FT_Title ||
0465          type == Frame::FT_Artist || type == Frame::FT_Album)) {
0466       if (lineEdit) {
0467         if (TagFormatConfig::instance().formatWhileEditing()) {
0468           connect(lineEdit, &QLineEdit::textEdited,
0469                   this, &FrameItemDelegate::formatTextIfEnabled);
0470         }
0471         lineEdit->setMaxLength(type == Frame::FT_Comment ? 28 : 30);
0472       }
0473     } else {
0474       if (lineEdit) {
0475         if (TagFormatConfig::instance().formatWhileEditing()) {
0476           connect(lineEdit, &QLineEdit::textEdited,
0477                   this, &FrameItemDelegate::formatTextIfEnabled);
0478         }
0479         if (TagFormatConfig::instance().enableValidation()) {
0480           if (type == Frame::FT_Track || type == Frame::FT_Disc) {
0481             lineEdit->setValidator(m_trackNumberValidator);
0482           } else if (type == Frame::FT_Date || type == Frame::FT_OriginalDate) {
0483             lineEdit->setValidator(m_dateTimeValidator);
0484           }
0485         }
0486       }
0487     }
0488     return editor;
0489   }
0490   return QItemDelegate::createEditor(parent, option, index);
0491 }
0492 
0493 /**
0494  * Set data to be edited by the editor.
0495  * @param editor editor widget
0496  * @param index  index of item
0497  */
0498 void FrameItemDelegate::setEditorData(
0499   QWidget* editor, const QModelIndex& index) const
0500 {
0501   if (index.row() >= 0 && index.column() == FrameTableModel::CI_Value &&
0502       index.data(FrameTableModel::FrameTypeRole).toInt() == Frame::FT_Rating) {
0503     if (auto starEditor = qobject_cast<StarEditor*>(editor)) {
0504       int starCount = starCountFromRating(index.data().toInt(), index);
0505       starEditor->setStarCount(starCount);
0506       return;
0507     }
0508   }
0509   if (auto cb = qobject_cast<QComboBox*>(editor)) {
0510     if (auto type = static_cast<Frame::Type>(
0511           index.data(FrameTableModel::FrameTypeRole).toInt());
0512         type == Frame::FT_Genre) {
0513       QString genreStr(index.model()->data(index).toString());
0514       cb->setCurrentIndex(m_genreModel->getRowForGenre(genreStr));
0515     }
0516   } else {
0517     QItemDelegate::setEditorData(editor, index);
0518   }
0519 }
0520 
0521 /**
0522  * Set model data supplied by editor.
0523  * @param editor editor widget
0524  * @param model  model
0525  * @param index  index of item
0526  */
0527 void FrameItemDelegate::setModelData(
0528   QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const {
0529   if (index.row() >= 0 && index.column() == FrameTableModel::CI_Value &&
0530       index.data(FrameTableModel::FrameTypeRole).toInt() == Frame::FT_Rating) {
0531     if (auto starEditor = qobject_cast<StarEditor*>(editor)) {
0532       if (starEditor->isStarCountEdited()) {
0533         model->setData(index, starCountToRating(starEditor->starCount(), index));
0534       }
0535       return;
0536     }
0537   }
0538   if (auto cb = qobject_cast<QComboBox *>(editor)) {
0539     model->setData(index, cb->currentText());
0540   } else {
0541     QItemDelegate::setModelData(editor, model, index);
0542   }
0543 }
0544 
0545 /**
0546  * Commit data and close the editor.
0547  */
0548 void FrameItemDelegate::commitAndCloseEditor()
0549 {
0550   if (auto editor = qobject_cast<StarEditor*>(sender())) {
0551     emit commitData(editor);
0552     emit closeEditor(editor);
0553   }
0554 }