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 }