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 }