File indexing completed on 2024-05-19 04:56:09
0001 /** 0002 * \file timeeventmodel.cpp 0003 * Time event model (synchronized lyrics or event timing codes). 0004 * 0005 * \b Project: Kid3 0006 * \author Urs Fleisch 0007 * \date 14 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 "timeeventmodel.h" 0028 #include <QTextStream> 0029 #include <QRegularExpression> 0030 #include "coretaggedfileiconprovider.h" 0031 #include "eventtimingcode.h" 0032 0033 /** 0034 * Constructor. 0035 * @param colorProvider colorProvider 0036 * @param parent parent widget 0037 */ 0038 TimeEventModel::TimeEventModel(CoreTaggedFileIconProvider* colorProvider, 0039 QObject* parent) 0040 : QAbstractTableModel(parent), m_type(SynchronizedLyrics), m_markedRow(-1), 0041 m_colorProvider(colorProvider) 0042 { 0043 setObjectName(QLatin1String("TimeEventModel")); 0044 } 0045 0046 /** 0047 * Get item flags for index. 0048 * @param index model index 0049 * @return item flags 0050 */ 0051 Qt::ItemFlags TimeEventModel::flags(const QModelIndex& index) const 0052 { 0053 Qt::ItemFlags theFlags = QAbstractTableModel::flags(index); 0054 if (index.isValid()) 0055 theFlags |= Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable; 0056 return theFlags; 0057 } 0058 0059 /** 0060 * Get data for a given role. 0061 * @param index model index 0062 * @param role item data role 0063 * @return data for role 0064 */ 0065 QVariant TimeEventModel::data(const QModelIndex& index, int role) const 0066 { 0067 if (!index.isValid() || 0068 index.row() < 0 || index.row() >= m_timeEvents.size() || 0069 index.column() < 0 || index.column() >= CI_NumColumns) 0070 return QVariant(); 0071 const TimeEvent& timeEvent = m_timeEvents.at(index.row()); 0072 if (role == Qt::DisplayRole || role == Qt::EditRole) { 0073 if (index.column() == CI_Time) 0074 return timeEvent.time; 0075 return timeEvent.data; 0076 } 0077 if (role == Qt::BackgroundRole && index.column() == CI_Data && 0078 m_colorProvider) { 0079 return m_colorProvider->colorForContext(index.row() == m_markedRow 0080 ? ColorContext::Marked : ColorContext::None); 0081 } 0082 return QVariant(); 0083 } 0084 0085 /** 0086 * Set data for a given role. 0087 * @param index model index 0088 * @param value data value 0089 * @param role item data role 0090 * @return true if successful 0091 */ 0092 bool TimeEventModel::setData(const QModelIndex& index, 0093 const QVariant& value, int role) 0094 { 0095 if (!index.isValid() || role != Qt::EditRole || 0096 index.row() < 0 || index.row() >= m_timeEvents.size() || 0097 index.column() < 0 || index.column() >= CI_NumColumns) 0098 return false; 0099 TimeEvent& timeEvent = m_timeEvents[index.row()]; // clazy:exclude=detaching-member 0100 if (index.column() == CI_Time) { 0101 timeEvent.time = value.toTime(); 0102 } else { 0103 timeEvent.data = value; 0104 } 0105 emit dataChanged(index, index); 0106 return true; 0107 } 0108 0109 /** 0110 * Get data for header section. 0111 * @param section column or row 0112 * @param orientation horizontal or vertical 0113 * @param role item data role 0114 * @return header data for role 0115 */ 0116 QVariant TimeEventModel::headerData( 0117 int section, Qt::Orientation orientation, int role) const 0118 { 0119 if (role != Qt::DisplayRole) 0120 return QVariant(); 0121 if (orientation == Qt::Horizontal && section < CI_NumColumns) { 0122 if (section == CI_Time) { 0123 return tr("Time"); 0124 } 0125 if (m_type == EventTimingCodes) { 0126 return tr("Event Code"); 0127 } 0128 return tr("Text"); 0129 } 0130 return section + 1; 0131 } 0132 0133 /** 0134 * Get number of rows. 0135 * @param parent parent model index, invalid for table models 0136 * @return number of rows, 0137 * if parent is valid number of children (0 for table models) 0138 */ 0139 int TimeEventModel::rowCount(const QModelIndex& parent) const 0140 { 0141 return parent.isValid() ? 0 : m_timeEvents.size(); 0142 } 0143 0144 /** 0145 * Get number of columns. 0146 * @param parent parent model index, invalid for table models 0147 * @return number of columns, 0148 * if parent is valid number of children (0 for table models) 0149 */ 0150 int TimeEventModel::columnCount(const QModelIndex& parent) const 0151 { 0152 return parent.isValid() ? 0 : CI_NumColumns; 0153 } 0154 0155 /** 0156 * Insert rows. 0157 * @param row rows are inserted before this row, if 0 at the begin, 0158 * if rowCount() at the end 0159 * @param count number of rows to insert 0160 * @return true if successful 0161 */ 0162 bool TimeEventModel::insertRows(int row, int count, 0163 const QModelIndex&) 0164 { 0165 if (count > 0) { 0166 beginInsertRows(QModelIndex(), row, row + count - 1); 0167 for (int i = 0; i < count; ++i) 0168 m_timeEvents.insert(row, TimeEvent(QTime(), QVariant())); 0169 endInsertRows(); 0170 } 0171 return true; 0172 } 0173 0174 /** 0175 * Remove rows. 0176 * @param row rows are removed starting with this row 0177 * @param count number of rows to remove 0178 * @return true if successful 0179 */ 0180 bool TimeEventModel::removeRows(int row, int count, 0181 const QModelIndex&) 0182 { 0183 if (count > 0) { 0184 beginRemoveRows(QModelIndex(), row, row + count - 1); 0185 for (int i = 0; i < count; ++i) 0186 m_timeEvents.removeAt(row); 0187 endRemoveRows(); 0188 } 0189 return true; 0190 } 0191 0192 /** 0193 * Set the model from a list of time events. 0194 * @param events list of time events 0195 */ 0196 void TimeEventModel::setTimeEvents(const QList<TimeEvent>& events) 0197 { 0198 beginResetModel(); 0199 m_timeEvents = events; 0200 endResetModel(); 0201 } 0202 0203 /** 0204 * Get time event list from the model. 0205 * @return list of time events. 0206 */ 0207 QList<TimeEventModel::TimeEvent> TimeEventModel::getTimeEvents() const 0208 { 0209 return m_timeEvents; 0210 } 0211 0212 /** 0213 * Set the model from a SYLT frame. 0214 * @param fields ID3v2 SYLT frame fields 0215 */ 0216 void TimeEventModel::fromSyltFrame(const Frame::FieldList& fields) 0217 { 0218 QVariantList synchedData; 0219 bool unitIsFrames = false; 0220 for (auto it = fields.constBegin(); it != fields.constEnd(); ++it) { 0221 const Frame::Field& fld = *it; 0222 if (fld.m_id == Frame::ID_TimestampFormat) { 0223 unitIsFrames = fld.m_value.toInt() == 1; 0224 #if QT_VERSION >= 0x060000 0225 } else if (fld.m_value.typeId() == QMetaType::QVariantList) { 0226 #else 0227 } else if (fld.m_value.type() == QVariant::List) { 0228 #endif 0229 synchedData = fld.m_value.toList(); 0230 } 0231 } 0232 0233 bool newLinesStartWithLineBreak = false; 0234 QList<TimeEvent> timeEvents; 0235 QListIterator it(synchedData); 0236 while (it.hasNext()) { 0237 quint32 milliseconds = it.next().toUInt(); 0238 if (!it.hasNext()) 0239 break; 0240 0241 QString str = it.next().toString(); 0242 if (timeEvents.isEmpty() && str.startsWith(QLatin1Char('\n'))) { 0243 // The first entry determines if new lines have to start with a new line 0244 // character or if all entries are supposed to be new lines. 0245 newLinesStartWithLineBreak = true; 0246 } 0247 0248 bool isNewLine = !newLinesStartWithLineBreak; 0249 if (str.startsWith(QLatin1Char('\n'))) { 0250 // New lines start with a new line character, which is removed. 0251 isNewLine = true; 0252 str.remove(0, 1); 0253 } 0254 if (isNewLine) { 0255 // If the resulting line starts with one of the special characters 0256 // (' ', '-', '_'), it is escaped with '#'. 0257 if (str.length() > 0) { 0258 if (QChar ch = str.at(0); 0259 ch == QLatin1Char(' ') || ch == QLatin1Char('-') || 0260 ch == QLatin1Char('_')) { 0261 str.prepend(QLatin1Char('#')); 0262 } 0263 } 0264 } else if (!(str.startsWith(QLatin1Char(' ')) || 0265 str.startsWith(QLatin1Char('-')))) { 0266 // Continuations of the current line do not start with a new line 0267 // character. They must start with ' ' or '-'. If the line starts with 0268 // another character, it is escaped with '_'. 0269 str.prepend(QLatin1Char('_')); 0270 } 0271 0272 QVariant timeStamp = unitIsFrames 0273 ? QVariant(milliseconds) 0274 : QVariant(QTime(0, 0).addMSecs(milliseconds)); 0275 timeEvents.append(TimeEvent(timeStamp, str)); 0276 } 0277 setTimeEvents(timeEvents); 0278 } 0279 0280 /** 0281 * Get the model as a SYLT frame. 0282 * @param fields ID3v2 SYLT frame fields to set 0283 */ 0284 void TimeEventModel::toSyltFrame(Frame::FieldList& fields) const 0285 { 0286 auto timeStampFormatIt = fields.end(); 0287 auto dataIt = fields.end(); 0288 for (auto it = fields.begin(); it != fields.end(); ++it) { 0289 if (it->m_id == Frame::ID_TimestampFormat) { 0290 timeStampFormatIt = it; 0291 #if QT_VERSION >= 0x060000 0292 } else if (it->m_value.typeId() == QMetaType::QVariantList) { 0293 #else 0294 } else if (it->m_value.type() == QVariant::List) { 0295 #endif 0296 dataIt = it; 0297 } 0298 } 0299 0300 QVariantList synchedData; 0301 bool hasMsTimeStamps = false; 0302 const auto timeEvents = m_timeEvents; 0303 for (const TimeEvent& timeEvent : timeEvents) { 0304 if (!timeEvent.time.isNull()) { 0305 QString str = timeEvent.data.toString(); 0306 // Remove escaping, restore new line characters. 0307 if (str.startsWith(QLatin1Char('_'))) { 0308 str.remove(0, 1); 0309 } else if (str.startsWith(QLatin1Char('#'))) { 0310 str.replace(0, 1, QLatin1Char('\n')); 0311 } else if (!(str.startsWith(QLatin1Char(' ')) || 0312 str.startsWith(QLatin1Char('-')))) { 0313 str.prepend(QLatin1Char('\n')); 0314 } 0315 0316 quint32 milliseconds; 0317 #if QT_VERSION >= 0x060000 0318 if (timeEvent.time.typeId() == QMetaType::QTime) { 0319 #else 0320 if (timeEvent.time.type() == QVariant::Time) { 0321 #endif 0322 hasMsTimeStamps = true; 0323 milliseconds = QTime(0, 0).msecsTo(timeEvent.time.toTime()); 0324 } else { 0325 milliseconds = timeEvent.data.toUInt(); 0326 } 0327 synchedData.append(milliseconds); 0328 synchedData.append(str); 0329 } 0330 } 0331 0332 if (hasMsTimeStamps && timeStampFormatIt != fields.end()) { 0333 timeStampFormatIt->m_value = 2; 0334 } 0335 if (dataIt != fields.end()) { 0336 dataIt->m_value = synchedData; 0337 } 0338 } 0339 0340 /** 0341 * Set the model from a ETCO frame. 0342 * @param fields ID3v2 ETCO frame fields 0343 */ 0344 void TimeEventModel::fromEtcoFrame(const Frame::FieldList& fields) 0345 { 0346 QVariantList synchedData; 0347 bool unitIsFrames = false; 0348 for (auto it = fields.constBegin(); it != fields.constEnd(); ++it) { 0349 const Frame::Field& fld = *it; 0350 if (fld.m_id == Frame::ID_TimestampFormat) { 0351 unitIsFrames = fld.m_value.toInt() == 1; 0352 #if QT_VERSION >= 0x060000 0353 } else if (fld.m_value.typeId() == QMetaType::QVariantList) { 0354 #else 0355 } else if (fld.m_value.type() == QVariant::List) { 0356 #endif 0357 synchedData = fld.m_value.toList(); 0358 } 0359 } 0360 0361 QList<TimeEvent> timeEvents; 0362 QListIterator it(synchedData); 0363 while (it.hasNext()) { 0364 quint32 milliseconds = it.next().toUInt(); 0365 if (!it.hasNext()) 0366 break; 0367 0368 int code = it.next().toInt(); 0369 QVariant timeStamp = unitIsFrames 0370 ? QVariant(milliseconds) 0371 : QVariant(QTime(0, 0).addMSecs(milliseconds)); 0372 timeEvents.append(TimeEvent(timeStamp, code)); 0373 } 0374 setTimeEvents(timeEvents); 0375 } 0376 0377 /** 0378 * Get the model as an ETCO frame. 0379 * @param fields ID3v2 ETCO frame fields to set 0380 */ 0381 void TimeEventModel::toEtcoFrame(Frame::FieldList& fields) const 0382 { 0383 auto timeStampFormatIt = fields.end(); 0384 auto dataIt = fields.end(); 0385 for (auto it = fields.begin(); it != fields.end(); ++it) { 0386 if (it->m_id == Frame::ID_TimestampFormat) { 0387 timeStampFormatIt = it; 0388 #if QT_VERSION >= 0x060000 0389 } else if (it->m_value.typeId() == QMetaType::QVariantList) { 0390 #else 0391 } else if (it->m_value.type() == QVariant::List) { 0392 #endif 0393 dataIt = it; 0394 } 0395 } 0396 0397 QVariantList synchedData; 0398 bool hasMsTimeStamps = false; 0399 const auto timeEvents = m_timeEvents; 0400 for (const TimeEvent& timeEvent : timeEvents) { 0401 if (!timeEvent.time.isNull()) { 0402 int code = timeEvent.data.toInt(); 0403 0404 quint32 milliseconds; 0405 #if QT_VERSION >= 0x060000 0406 if (timeEvent.time.typeId() == QMetaType::QTime) { 0407 #else 0408 if (timeEvent.time.type() == QVariant::Time) { 0409 #endif 0410 hasMsTimeStamps = true; 0411 milliseconds = QTime(0, 0).msecsTo(timeEvent.time.toTime()); 0412 } else { 0413 milliseconds = timeEvent.data.toUInt(); 0414 } 0415 synchedData.append(milliseconds); 0416 synchedData.append(code); 0417 } 0418 } 0419 0420 if (timeStampFormatIt != fields.end() && hasMsTimeStamps) { 0421 timeStampFormatIt->m_value = 2; 0422 } 0423 if (dataIt != fields.end()) { 0424 dataIt->m_value = synchedData; 0425 } 0426 } 0427 0428 /** 0429 * Mark row for a time stamp. 0430 * Marks the first row with time >= @a timeStamp. 0431 * @param timeStamp time 0432 * @see getMarkedRow() 0433 */ 0434 void TimeEventModel::markRowForTimeStamp(const QTime& timeStamp) 0435 { 0436 int row = 0, oldRow = m_markedRow, newRow = -1; 0437 for (auto it = m_timeEvents.constBegin(); it != m_timeEvents.constEnd(); ++it) { 0438 const TimeEvent& timeEvent = *it; 0439 if (QTime time = timeEvent.time.toTime(); 0440 time.isValid() && time >= timeStamp) { 0441 if (timeStamp.msecsTo(time) > 1000 && row > 0) { 0442 --row; 0443 } 0444 if (row == 0 && timeStamp == QTime(0, 0) && 0445 m_timeEvents.at(0).time.toTime() != timeStamp) { 0446 row = -1; 0447 } 0448 newRow = row; 0449 break; 0450 } 0451 ++row; 0452 } 0453 if (newRow != oldRow && 0454 !(newRow == -1 && oldRow == m_timeEvents.size() - 1)) { 0455 m_markedRow = newRow; 0456 if (oldRow != -1) { 0457 QModelIndex idx = index(oldRow, CI_Data); 0458 emit dataChanged(idx, idx); 0459 } 0460 if (newRow != -1) { 0461 QModelIndex idx = index(newRow, CI_Data); 0462 emit dataChanged(idx, idx); 0463 } 0464 } 0465 } 0466 0467 /** 0468 * Clear the marked row. 0469 */ 0470 void TimeEventModel::clearMarkedRow() 0471 { 0472 if (m_markedRow != -1) { 0473 QModelIndex idx = index(m_markedRow, CI_Data); 0474 m_markedRow = -1; 0475 emit dataChanged(idx, idx); 0476 } 0477 } 0478 0479 /** 0480 * Set the model from an LRC file. 0481 * @param stream LRC file stream 0482 */ 0483 void TimeEventModel::fromLrcFile(QTextStream& stream) 0484 { 0485 QRegularExpression timeStampRe(QLatin1String( 0486 R"(([[<])(\d\d):(\d\d)(?:\.(\d{1,3}))?([\]>]))")); 0487 QList<TimeEvent> timeEvents; 0488 bool isFirstLine = true; 0489 forever { 0490 QString line = stream.readLine(); 0491 if (line.isNull()) 0492 break; 0493 0494 if (line.isEmpty()) 0495 continue; 0496 0497 // If the first line does not contain a '[' character, assume that this is 0498 // not an LRC file and only import lines without time stamps. 0499 if (isFirstLine) { 0500 if (line.contains(QLatin1Char('['))) { 0501 isFirstLine = false; 0502 } else { 0503 stream.seek(0); 0504 fromTextFile(stream); 0505 return; 0506 } 0507 } 0508 0509 QList<QTime> emptyEvents; 0510 char firstChar = '\0'; 0511 auto it = timeStampRe.globalMatch(line); 0512 while (it.hasNext()) { 0513 auto match = it.next(); 0514 bool newLine = match.captured(1) == QLatin1String("["); 0515 QString millisecondsStr = match.captured(4); 0516 int milliseconds = millisecondsStr.toInt(); 0517 if (millisecondsStr.length() == 2) { 0518 milliseconds *= 10; 0519 } else if (millisecondsStr.length() == 1) { 0520 milliseconds *= 100; 0521 } 0522 QTime timeStamp(0, 0523 match.captured(2).toInt(), 0524 match.captured(3).toInt(), 0525 milliseconds); 0526 int pos = match.capturedStart(); 0527 int textBegin = pos + match.capturedLength(); 0528 int textLen = -1; 0529 pos = -1; 0530 if (it.hasNext()) { 0531 match = it.peekNext(); 0532 pos = match.capturedStart(); 0533 textLen = pos - textBegin; 0534 } 0535 QString str = line.mid(textBegin, textLen); 0536 if (m_type == EventTimingCodes) { 0537 if (EventTimeCode etc = 0538 EventTimeCode::fromString(str.toLatin1().constData()); 0539 etc.isValid()) { 0540 timeEvents.append(TimeEvent(timeStamp, etc.getCode())); 0541 } 0542 } else { 0543 if (firstChar != '\0') { 0544 str.prepend(QChar::fromLatin1(firstChar)); 0545 firstChar = '\0'; 0546 } 0547 if (newLine) { 0548 if (str.startsWith(QLatin1Char(' ')) || 0549 str.startsWith(QLatin1Char('-')) || 0550 str.startsWith(QLatin1Char('_'))) { 0551 str.prepend(QLatin1Char('#')); 0552 } 0553 } else if (!(str.startsWith(QLatin1Char(' ')) || 0554 str.startsWith(QLatin1Char('-')))) { 0555 str.prepend(QLatin1Char('_')); 0556 } 0557 if (pos != -1) { 0558 if (match.captured(1) == QLatin1String("<")) { 0559 if (str.endsWith(QLatin1Char(' ')) || 0560 str.endsWith(QLatin1Char('-'))) { 0561 firstChar = str.at(str.length() - 1).toLatin1(); 0562 str.truncate(str.length() - 1); 0563 } 0564 } 0565 if (str.isEmpty()) { 0566 // The next time stamp follows immediately with a common text. 0567 emptyEvents.append(timeStamp); 0568 continue; 0569 } 0570 } 0571 const auto times = emptyEvents; 0572 for (const QTime& time : times) { 0573 timeEvents.append(TimeEvent(time, str)); 0574 } 0575 timeEvents.append(TimeEvent(timeStamp, str)); 0576 } 0577 } 0578 } 0579 std::sort(timeEvents.begin(), timeEvents.end()); 0580 setTimeEvents(timeEvents); 0581 } 0582 0583 /** 0584 * Set the model from a text file. 0585 * @param stream text file stream 0586 */ 0587 void TimeEventModel::fromTextFile(QTextStream& stream) 0588 { 0589 QList<TimeEvent> timeEvents; 0590 forever { 0591 QString line = stream.readLine(); 0592 if (line.isNull()) 0593 break; 0594 0595 timeEvents.append(TimeEvent(QTime(), line)); 0596 } 0597 setTimeEvents(timeEvents); 0598 } 0599 0600 /** 0601 * Store the model in an LRC file. 0602 * @param stream LRC file stream 0603 * @param title optional title 0604 * @param artist optional artist 0605 * @param album optional album 0606 */ 0607 void TimeEventModel::toLrcFile(QTextStream& stream, const QString& title, 0608 const QString& artist, const QString& album) const 0609 { 0610 bool atBegin = true; 0611 if (!title.isEmpty()) { 0612 stream << QLatin1String("[ti:") << title << QLatin1String("]\r\n"); 0613 atBegin = false; 0614 } 0615 if (!artist.isEmpty()) { 0616 stream << QLatin1String("[ar:") << artist << QLatin1String("]\r\n"); 0617 atBegin = false; 0618 } 0619 if (!album.isEmpty()) { 0620 stream << QLatin1String("[al:") << album << QLatin1String("]\r\n"); 0621 atBegin = false; 0622 } 0623 const auto timeEvents = m_timeEvents; 0624 for (const TimeEvent& timeEvent : timeEvents) { 0625 if (QTime time = timeEvent.time.toTime(); time.isValid()) { 0626 char firstChar = '\0'; 0627 bool newLine = true; 0628 QString str; 0629 if (m_type == EventTimingCodes) { 0630 str = EventTimeCode(timeEvent.data.toInt()).toString(); 0631 } else { 0632 str = timeEvent.data.toString(); 0633 if (str.startsWith(QLatin1Char('_'))) { 0634 str.remove(0, 1); 0635 newLine = false; 0636 } else if (str.startsWith(QLatin1Char('#'))) { 0637 str.remove(0, 1); 0638 } else if (str.startsWith(QLatin1Char(' ')) || 0639 str.startsWith(QLatin1Char('-'))) { 0640 firstChar = str.at(0).toLatin1(); 0641 str.remove(0, 1); 0642 newLine = false; 0643 } 0644 } 0645 0646 if (newLine) { 0647 if (!atBegin) { 0648 stream << QLatin1String("\r\n"); 0649 } 0650 stream << QLatin1Char('[') << timeStampToString(time).toLatin1() 0651 << QLatin1Char(']') << str.toLatin1(); 0652 } else { 0653 if (firstChar != '\0') { 0654 stream << firstChar; 0655 } 0656 stream << QLatin1Char('<') << timeStampToString(time).toLatin1() 0657 << QLatin1Char('>') << str.toLatin1(); 0658 } 0659 atBegin = false; 0660 } 0661 } 0662 if (!atBegin) { 0663 stream << QLatin1String("\r\n"); 0664 } 0665 } 0666 0667 /** 0668 * Format a time suitable for a time stamp. 0669 * @param time time stamp 0670 * @return string of the format "mm:ss.zz" 0671 */ 0672 QString TimeEventModel::timeStampToString(const QTime& time) 0673 { 0674 int hour = time.hour(); 0675 int min = time.minute(); 0676 int sec = time.second(); 0677 int msec = time.msec(); 0678 if (hour < 0) hour = 0; 0679 if (min < 0) min = 0; 0680 if (sec < 0) sec = 0; 0681 if (msec < 0) msec = 0; 0682 QString text = QString(QLatin1String("%1:%2.%3")) 0683 .arg(min, 2, 10, QLatin1Char('0')) 0684 .arg(sec, 2, 10, QLatin1Char('0')) 0685 .arg(msec / 10, 2, 10, QLatin1Char('0')); 0686 if (hour != 0) { 0687 text.prepend(QString::number(hour) + QLatin1Char(':')); 0688 } 0689 return text; 0690 }