File indexing completed on 2025-02-23 04:34:25
0001 /** 0002 * \file timeeventeditor.cpp 0003 * Editor for time events (synchronized lyrics and event timing codes). 0004 * 0005 * \b Project: Kid3 0006 * \author Urs Fleisch 0007 * \date 15 Mar 2014 0008 * 0009 * Copyright (C) 2014-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 "timeeventeditor.h" 0028 #include <QCoreApplication> 0029 #include <QVBoxLayout> 0030 #include <QLabel> 0031 #include <QPushButton> 0032 #include <QTableView> 0033 #include <QItemDelegate> 0034 #include <QTimer> 0035 #include <QAction> 0036 #include <QMenu> 0037 #include <QInputDialog> 0038 #include <QFile> 0039 #include <QTextStream> 0040 #include <QKeyEvent> 0041 #include <QApplication> 0042 #include <QClipboard> 0043 #include <QMimeData> 0044 #include <QHeaderView> 0045 #include "config.h" 0046 #include "fileconfig.h" 0047 #include "timeeventmodel.h" 0048 #include "timestampdelegate.h" 0049 #include "eventcodedelegate.h" 0050 #include "kid3application.h" 0051 #ifdef HAVE_QTMULTIMEDIA 0052 #include "audioplayer.h" 0053 #endif 0054 #include "contexthelp.h" 0055 #include "iplatformtools.h" 0056 0057 /** Table to edit time events. */ 0058 class TimeEventTableView : public QTableView { 0059 public: 0060 /** Constructor. */ 0061 explicit TimeEventTableView(QWidget* parent = nullptr) : QTableView(parent) {} 0062 /** Destructor. */ 0063 ~TimeEventTableView() override = default; 0064 0065 protected: 0066 /** 0067 * Handle key events, delete cell contents if Delete key is pressed. 0068 * @param event key event 0069 */ 0070 void keyPressEvent(QKeyEvent* event) override; 0071 0072 private: 0073 Q_DISABLE_COPY(TimeEventTableView) 0074 }; 0075 0076 void TimeEventTableView::keyPressEvent(QKeyEvent* event) 0077 { 0078 if (event->key() == Qt::Key_Delete) { 0079 QModelIndex idx = currentIndex(); 0080 if (QAbstractItemModel* mdl = model(); mdl && idx.isValid()) { 0081 #if QT_VERSION >= 0x060000 0082 mdl->setData(idx, QVariant(idx.data().metaType())); 0083 #else 0084 mdl->setData(idx, QVariant(idx.data().type())); 0085 #endif 0086 return; 0087 } 0088 } 0089 QTableView::keyPressEvent(event); 0090 } 0091 0092 0093 /** 0094 * Constructor. 0095 * 0096 * @param platformTools platform tools 0097 * @param app application context 0098 * @param parent parent widget 0099 * @param field field containing binary data 0100 * @param taggedFile tagged file 0101 * @param tagNr tag number 0102 */ 0103 TimeEventEditor::TimeEventEditor(IPlatformTools* platformTools, 0104 Kid3Application* app, 0105 QWidget* parent, const Frame::Field& field, 0106 const TaggedFile* taggedFile, 0107 Frame::TagNumber tagNr) 0108 : QWidget(parent), 0109 m_platformTools(platformTools), m_app(app), m_eventCodeDelegate(nullptr), 0110 m_model(nullptr), m_taggedFile(taggedFile), m_tagNr(tagNr), 0111 m_byteArray(field.m_value.toByteArray()), m_fileIsPlayed(false) 0112 { 0113 setObjectName(QLatin1String("TimeEventEditor")); 0114 auto vlayout = new QVBoxLayout(this); 0115 m_label = new QLabel(this); 0116 vlayout->addWidget(m_label); 0117 vlayout->setContentsMargins(0, 0, 0, 0); 0118 auto buttonLayout = new QHBoxLayout; 0119 auto addButton = new QPushButton(tr("&Add"), this); 0120 addButton->setAutoDefault(false); 0121 auto deleteButton = new QPushButton(tr("&Delete"), this); 0122 deleteButton->setAutoDefault(false); 0123 auto clipButton = new QPushButton(tr("From Clip&board"), this); 0124 clipButton->setAutoDefault(false); 0125 auto importButton = new QPushButton(tr("&Import..."), this); 0126 importButton->setAutoDefault(false); 0127 auto exportButton = new QPushButton(tr("&Export..."), this); 0128 exportButton->setAutoDefault(false); 0129 auto helpButton = new QPushButton(tr("Help"), this); 0130 helpButton->setAutoDefault(false); 0131 buttonLayout->setContentsMargins(0, 0, 0, 0); 0132 buttonLayout->addWidget(addButton); 0133 buttonLayout->addWidget(deleteButton); 0134 buttonLayout->addWidget(clipButton); 0135 buttonLayout->addWidget(importButton); 0136 buttonLayout->addWidget(exportButton); 0137 buttonLayout->addWidget(helpButton); 0138 buttonLayout->addStretch(); 0139 connect(addButton, &QAbstractButton::clicked, this, &TimeEventEditor::addItem); 0140 connect(deleteButton, &QAbstractButton::clicked, this, &TimeEventEditor::deleteRows); 0141 connect(clipButton, &QAbstractButton::clicked, this, &TimeEventEditor::clipData); 0142 connect(importButton, &QAbstractButton::clicked, this, &TimeEventEditor::importData); 0143 connect(exportButton, &QAbstractButton::clicked, this, &TimeEventEditor::exportData); 0144 connect(helpButton, &QAbstractButton::clicked, this, &TimeEventEditor::showHelp); 0145 vlayout->addLayout(buttonLayout); 0146 m_tableView = new TimeEventTableView; 0147 m_tableView->verticalHeader()->hide(); 0148 m_tableView->horizontalHeader()->setStretchLastSection(true); 0149 m_tableView->setItemDelegateForColumn(0, new TimeStampDelegate(this)); 0150 m_tableView->setContextMenuPolicy(Qt::CustomContextMenu); 0151 connect(m_tableView, &QWidget::customContextMenuRequested, 0152 this, &TimeEventEditor::customContextMenu); 0153 vlayout->addWidget(m_tableView); 0154 } 0155 0156 /** 0157 * Connect to player when editor is shown. 0158 * @param event event 0159 */ 0160 void TimeEventEditor::showEvent(QShowEvent* event) 0161 { 0162 QTimer::singleShot(0, this, &TimeEventEditor::preparePlayer); 0163 QWidget::showEvent(event); 0164 } 0165 0166 /** 0167 * Disconnect from player when editor is hidden. 0168 * @param event event 0169 */ 0170 void TimeEventEditor::hideEvent(QHideEvent* event) 0171 { 0172 if (QObject* player = m_app->getAudioPlayer()) { 0173 disconnect(player, nullptr, this, nullptr); 0174 m_fileIsPlayed = false; 0175 QWidget::hideEvent(event); 0176 } 0177 } 0178 0179 /** 0180 * Set time event model. 0181 * @param model time event model 0182 */ 0183 void TimeEventEditor::setModel(TimeEventModel* model) 0184 { 0185 m_model = model; 0186 if (m_model->getType() == TimeEventModel::EventTimingCodes) { 0187 m_label->setText(tr("Events")); 0188 if (!m_eventCodeDelegate) { 0189 m_eventCodeDelegate = new EventCodeDelegate(this); 0190 } 0191 m_tableView->setItemDelegateForColumn(1, m_eventCodeDelegate); 0192 } else { 0193 m_label->setText(tr("Lyrics")); 0194 m_tableView->setItemDelegateForColumn(1, nullptr); 0195 } 0196 m_tableView->setModel(m_model); 0197 } 0198 0199 /** 0200 * Make sure that player is visible and in the edited file. 0201 */ 0202 void TimeEventEditor::preparePlayer() 0203 { 0204 #ifdef HAVE_QTMULTIMEDIA 0205 m_app->showAudioPlayer(); 0206 if (auto player = 0207 qobject_cast<AudioPlayer*>(m_app->getAudioPlayer())) { 0208 if (QString filePath = m_taggedFile->getAbsFilename(); 0209 player->getFileName() != filePath) { 0210 player->setFiles({filePath}, -1); 0211 } 0212 m_fileIsPlayed = true; 0213 connect(player, &AudioPlayer::trackChanged, 0214 this, &TimeEventEditor::onTrackChanged, Qt::UniqueConnection); 0215 connect(player, &AudioPlayer::positionChanged, 0216 this, &TimeEventEditor::onPositionChanged, Qt::UniqueConnection); 0217 } 0218 #endif 0219 } 0220 0221 /** 0222 * Add a time event at the current player position. 0223 */ 0224 void TimeEventEditor::addItem() 0225 { 0226 #ifdef HAVE_QTMULTIMEDIA 0227 preparePlayer(); 0228 if (auto player = 0229 qobject_cast<AudioPlayer*>(m_app->getAudioPlayer())) { 0230 QTime timeStamp = QTime(0, 0).addMSecs(player->getCurrentPosition()); 0231 if (m_model) { 0232 // If the current row is empty, set the time stamp there, else insert a new 0233 // row sorted by time stamps or use the first empty row. 0234 QModelIndex index = m_tableView->currentIndex(); 0235 if (!(index.isValid() && 0236 (index = index.sibling(index.row(), TimeEventModel::CI_Time)) 0237 .data().isNull())) { 0238 int row = 0; 0239 bool insertRow = true; 0240 while (row < m_model->rowCount()) { 0241 if (QTime time = m_model->index(row, TimeEventModel::CI_Time) 0242 .data().toTime(); 0243 time.isNull()) { 0244 insertRow = false; 0245 break; 0246 } else if (time > timeStamp) { 0247 break; 0248 } 0249 ++row; 0250 } 0251 if (insertRow) { 0252 m_model->insertRow(row); 0253 } 0254 index = m_model->index(row, TimeEventModel::CI_Time); 0255 } 0256 m_model->setData(index, timeStamp); 0257 m_tableView->scrollTo(index); 0258 } 0259 } 0260 #endif 0261 } 0262 0263 /** 0264 * Load LRC data from clipboard. 0265 */ 0266 void TimeEventEditor::clipData() 0267 { 0268 if (QClipboard* cb = QApplication::clipboard(); 0269 cb && cb->mimeData()->hasText()) { 0270 QString text = cb->text(); 0271 QTextStream stream(&text, QIODevice::ReadOnly); 0272 m_model->fromLrcFile(stream); 0273 } 0274 } 0275 0276 /** 0277 * Import data in LRC format. 0278 */ 0279 void TimeEventEditor::importData() 0280 { 0281 if (!m_model) 0282 return; 0283 0284 if (QString loadFileName = m_platformTools->getOpenFileName(this, QString(), 0285 m_taggedFile->getDirname(), getLrcNameFilter(), nullptr); 0286 !loadFileName.isEmpty()) { 0287 QFile file(loadFileName); 0288 if (file.open(QIODevice::ReadOnly)) { 0289 QTextStream stream(&file); 0290 m_model->fromLrcFile(stream); 0291 file.close(); 0292 } 0293 } 0294 } 0295 0296 /** 0297 * Export data in LRC format. 0298 */ 0299 void TimeEventEditor::exportData() 0300 { 0301 if (!m_model) 0302 return; 0303 0304 QString suggestedFileName = m_taggedFile->getAbsFilename(); 0305 if (int dotPos = suggestedFileName.lastIndexOf(QLatin1Char('.')); 0306 dotPos >= 0 && dotPos >= suggestedFileName.length() - 5) { 0307 suggestedFileName.truncate(dotPos); 0308 } 0309 suggestedFileName += QLatin1String(".lrc"); 0310 if (QString saveFileName = m_platformTools->getSaveFileName( 0311 this, QString(), suggestedFileName, getLrcNameFilter(), nullptr); 0312 !saveFileName.isEmpty()) { 0313 QFile file(saveFileName); 0314 if (file.open(QIODevice::WriteOnly)) { 0315 QTextStream stream(&file); 0316 if (QString codecName = FileConfig::instance().textEncoding(); 0317 codecName != QLatin1String("System")) { 0318 #if QT_VERSION >= 0x060000 0319 if (auto encoding = QStringConverter::encodingForName(codecName.toLatin1())) { 0320 stream.setEncoding(*encoding); 0321 } 0322 #else 0323 stream.setCodec(codecName.toLatin1()); 0324 #endif 0325 } 0326 QString title, artist, album; 0327 Frame frame; 0328 if (m_taggedFile->getFrame(m_tagNr, Frame::FT_Title, frame)) { 0329 title = frame.getValue(); 0330 } 0331 if (m_taggedFile->getFrame(m_tagNr, Frame::FT_Artist, frame)) { 0332 artist = frame.getValue(); 0333 } 0334 if (m_taggedFile->getFrame(m_tagNr, Frame::FT_Album, frame)) { 0335 album = frame.getValue(); 0336 } 0337 m_model->toLrcFile(stream, title, artist, album); 0338 file.close(); 0339 } 0340 } 0341 } 0342 0343 /** 0344 * Get file name filter string for LRC files. 0345 * @return filter string. 0346 */ 0347 QString TimeEventEditor::getLrcNameFilter() const 0348 { 0349 const char* const lyricsStr = QT_TRANSLATE_NOOP("@default", "Lyrics"); 0350 const char* const allFilesStr = QT_TRANSLATE_NOOP("@default", "All Files"); 0351 return m_platformTools->fileDialogNameFilter({ 0352 qMakePair(QCoreApplication::translate("@default", lyricsStr), 0353 QString(QLatin1String("*.lrc"))), 0354 qMakePair(QCoreApplication::translate("@default", allFilesStr), 0355 QString(QLatin1Char('*'))) 0356 }); 0357 } 0358 0359 /** 0360 * Insert a new row after the current row. 0361 */ 0362 void TimeEventEditor::insertRow() 0363 { 0364 if (!m_model) 0365 return; 0366 0367 m_model->insertRow(m_tableView->currentIndex().isValid() 0368 ? m_tableView->currentIndex().row() + 1 : 0); 0369 } 0370 0371 /** 0372 * Delete the selected rows. 0373 */ 0374 void TimeEventEditor::deleteRows() 0375 { 0376 if (!m_model) 0377 return; 0378 0379 QMap<int, int> rows; 0380 if (QItemSelectionModel* selModel = m_tableView->selectionModel()) { 0381 const auto indexes = selModel->selectedIndexes(); 0382 for (const QModelIndex& index : indexes) { 0383 rows.insert(index.row(), 0); 0384 } 0385 } 0386 0387 QMapIterator it(rows); 0388 it.toBack(); 0389 while (it.hasPrevious()) { 0390 it.previous(); 0391 m_model->removeRow(it.key()); 0392 } 0393 } 0394 0395 /** 0396 * Clear the selected cells. 0397 */ 0398 void TimeEventEditor::clearCells() 0399 { 0400 if (!m_model) 0401 return; 0402 0403 #if QT_VERSION >= 0x060000 0404 QVariant emptyData(m_model->getType() == TimeEventModel::EventTimingCodes 0405 ? QMetaType(QMetaType::Int) : QMetaType(QMetaType::QString)); 0406 QVariant emptyTime((QMetaType(QMetaType::QTime))); 0407 #else 0408 QVariant emptyData(m_model->getType() == TimeEventModel::EventTimingCodes 0409 ? QVariant::Int : QVariant::String); 0410 QVariant emptyTime(QVariant::Time); 0411 #endif 0412 if (QItemSelectionModel* selModel = m_tableView->selectionModel()) { 0413 const auto indexes = selModel->selectedIndexes(); 0414 for (const QModelIndex& index : indexes) { 0415 m_model->setData(index, index.column() == TimeEventModel::CI_Time 0416 ? emptyTime : emptyData); 0417 } 0418 } 0419 } 0420 0421 /** 0422 * Add offset to time stamps. 0423 */ 0424 void TimeEventEditor::addOffset() 0425 { 0426 if (!m_model) 0427 return; 0428 0429 int offset = QInputDialog::getInt(this, tr("Offset"), tr("Milliseconds")); 0430 if (QItemSelectionModel* selModel = m_tableView->selectionModel()) { 0431 const auto indexes = selModel->selectedIndexes(); 0432 for (const QModelIndex& index : indexes) { 0433 if (index.column() == TimeEventModel::CI_Time) { 0434 m_model->setData(index, index.data().toTime().addMSecs(offset)); 0435 } 0436 } 0437 } 0438 } 0439 0440 /** 0441 * Seek to position of current time stamp. 0442 */ 0443 void TimeEventEditor::seekPosition() 0444 { 0445 #ifdef HAVE_QTMULTIMEDIA 0446 if (QModelIndex index = m_tableView->currentIndex(); 0447 index.isValid() && m_fileIsPlayed) { 0448 if (QTime timeStamp = 0449 index.sibling(index.row(), TimeEventModel::CI_Time).data().toTime(); 0450 timeStamp.isValid()) { 0451 if (auto player = 0452 qobject_cast<AudioPlayer*>(m_app->getAudioPlayer())) { 0453 player->setCurrentPosition(QTime(0, 0).msecsTo(timeStamp)); 0454 } 0455 } 0456 } 0457 #endif 0458 } 0459 0460 /** 0461 * Display custom context menu. 0462 * 0463 * @param pos position where context menu is drawn on screen 0464 */ 0465 void TimeEventEditor::customContextMenu(const QPoint& pos) 0466 { 0467 QMenu menu(this); 0468 QAction* action = menu.addAction(tr("&Insert row")); 0469 connect(action, &QAction::triggered, this, &TimeEventEditor::insertRow); 0470 if (QModelIndex index = m_tableView->indexAt(pos); index.isValid()) { 0471 action = menu.addAction(tr("&Delete rows")); 0472 connect(action, &QAction::triggered, this, &TimeEventEditor::deleteRows); 0473 action = menu.addAction(tr("C&lear")); 0474 connect(action, &QAction::triggered, this, &TimeEventEditor::clearCells); 0475 action = menu.addAction(tr("&Add offset...")); 0476 connect(action, &QAction::triggered, this, &TimeEventEditor::addOffset); 0477 action = menu.addAction(tr("&Seek to position")); 0478 connect(action, &QAction::triggered, this, &TimeEventEditor::seekPosition); 0479 } 0480 menu.setMouseTracking(true); 0481 menu.exec(m_tableView->mapToGlobal(pos)); 0482 } 0483 0484 /** 0485 * Called when the played track changed. 0486 * @param filePath path to file being played 0487 */ 0488 void TimeEventEditor::onTrackChanged(const QString& filePath) 0489 { 0490 m_fileIsPlayed = filePath == m_taggedFile->getAbsFilename(); 0491 if (m_model) { 0492 m_model->clearMarkedRow(); 0493 } 0494 } 0495 0496 /** 0497 * Called when the player position changed. 0498 * @param position time in ms 0499 */ 0500 void TimeEventEditor::onPositionChanged(qint64 position) 0501 { 0502 if (!m_fileIsPlayed || !m_model) 0503 return; 0504 0505 int oldRow = m_model->getMarkedRow(); 0506 m_model->markRowForTimeStamp(QTime(0, 0).addMSecs(position)); 0507 if (int row = m_model->getMarkedRow(); row != oldRow && row != -1) { 0508 m_tableView->scrollTo(m_model->index(row, TimeEventModel::CI_Time), 0509 QAbstractItemView::PositionAtCenter); 0510 } 0511 } 0512 0513 /** 0514 * Show help. 0515 */ 0516 void TimeEventEditor::showHelp() 0517 { 0518 ContextHelp::displayHelp(QLatin1String("synchronized-lyrics")); 0519 }