1 /**
2 * \file trackdatamodel.cpp
3 * Model for table with track data.
4 *
5 * \b Project: Kid3
6 * \author Urs Fleisch
7 * \date 15 May 2011
8 *
9 * Copyright (C) 2011-2018 Urs Fleisch
10 *
11 * This file is part of Kid3.
12 *
13 * Kid3 is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * Kid3 is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 */
26
27 #include "trackdatamodel.h"
28 #include "frametablemodel.h"
29 #include "coretaggedfileiconprovider.h"
30
31 /**
32 * Constructor.
33 * @param colorProvider colorProvider
34 * @param parent parent widget
35 */
TrackDataModel(CoreTaggedFileIconProvider * colorProvider,QObject * parent)36 TrackDataModel::TrackDataModel(CoreTaggedFileIconProvider* colorProvider,
37 QObject* parent)
38 : QAbstractTableModel(parent),
39 m_colorProvider(colorProvider), m_maxDiff(0), m_diffCheckEnabled(false)
40 {
41 setObjectName(QLatin1String("TrackDataModel"));
42 }
43
44 /**
45 * Get item flags for index.
46 * @param index model index
47 * @return item flags
48 */
flags(const QModelIndex & index) const49 Qt::ItemFlags TrackDataModel::flags(const QModelIndex& index) const
50 {
51 Qt::ItemFlags theFlags = QAbstractTableModel::flags(index);
52 if (index.isValid()) {
53 theFlags |= Qt::ItemIsSelectable | Qt::ItemIsEnabled;
54 if (static_cast<int>(m_frameTypes.at(index.column()).getType()) <
55 FT_FirstTrackProperty) {
56 theFlags |= Qt::ItemIsEditable;
57 }
58 if (index.column() == 0) {
59 theFlags |= Qt::ItemIsUserCheckable;
60 }
61 }
62 return theFlags;
63 }
64
65 /**
66 * Get data for a given role.
67 * @param index model index
68 * @param role item data role
69 * @return data for role
70 */
data(const QModelIndex & index,int role) const71 QVariant TrackDataModel::data(const QModelIndex& index, int role) const
72 {
73 if (!index.isValid() ||
74 index.row() < 0 ||
75 index.row() >= static_cast<int>(m_trackDataVector.size()) ||
76 index.column() < 0 ||
77 index.column() >= static_cast<int>(m_frameTypes.size()))
78 return QVariant();
79
80 if (role == Qt::DisplayRole || role == Qt::EditRole) {
81 const ImportTrackData& trackData = m_trackDataVector.at(index.row());
82 Frame::ExtendedType type = m_frameTypes.at(index.column());
83 auto typeOrProperty = static_cast<int>(type.getType());
84 if (typeOrProperty < FT_FirstTrackProperty) {
85 QString value(trackData.getValue(type));
86 if (!value.isNull())
87 return value;
88 } else {
89 switch (typeOrProperty) {
90 case FT_FilePath:
91 return trackData.getAbsFilename();
92 case FT_FileName:
93 return trackData.getFilename();
94 case FT_Duration:
95 if (int duration = trackData.getFileDuration()) {
96 return TaggedFile::formatTime(duration);
97 }
98 break;
99 case FT_ImportDuration:
100 if (int duration = trackData.getImportDuration()) {
101 return TaggedFile::formatTime(duration);
102 }
103 break;
104 default:
105 ;
106 }
107 }
108 } else if (role == FrameTableModel::FrameTypeRole) {
109 return m_frameTypes.at(index.column()).getType();
110 } else if (role == Qt::BackgroundRole) {
111 if (index.column() == 0 && m_diffCheckEnabled) {
112 const ImportTrackData& trackData = m_trackDataVector.at(index.row());
113 int diff = trackData.getTimeDifference();
114 if (diff >= 0 && m_colorProvider) {
115 return m_colorProvider->colorForContext(diff > m_maxDiff
116 ? ColorContext::Error : ColorContext::None);
117 }
118 }
119 } else if (role == Qt::CheckStateRole && index.column() == 0) {
120 return m_trackDataVector.at(index.row()).isEnabled()
121 ? Qt::Checked : Qt::Unchecked;
122 }
123 return QVariant();
124 }
125
126 /**
127 * Set data for a given role.
128 * @param index model index
129 * @param value data value
130 * @param role item data role
131 * @return true if successful
132 */
setData(const QModelIndex & index,const QVariant & value,int role)133 bool TrackDataModel::setData(const QModelIndex& index,
134 const QVariant& value, int role)
135 {
136 if (!index.isValid() ||
137 index.row() < 0 ||
138 index.row() >= static_cast<int>(m_trackDataVector.size()) ||
139 index.column() < 0 ||
140 index.column() >= static_cast<int>(m_frameTypes.size()))
141 return false;
142
143 if (role == Qt::EditRole) {
144 ImportTrackData& trackData = m_trackDataVector[index.row()];
145 Frame::ExtendedType type = m_frameTypes.at(index.column());
146 if (static_cast<int>(type.getType()) >= FT_FirstTrackProperty)
147 return false;
148
149 trackData.setValue(type, value.toString());
150 return true;
151 } else if (role == Qt::CheckStateRole && index.column() == 0) {
152 bool isChecked(value.toInt() == Qt::Checked);
153 if (isChecked != m_trackDataVector.at(index.row()).isEnabled()) {
154 m_trackDataVector[index.row()].setEnabled(isChecked);
155 emit dataChanged(index, index);
156 }
157 return true;
158 }
159 return false;
160 }
161
162 /**
163 * Get data for header section.
164 * @param section column or row
165 * @param orientation horizontal or vertical
166 * @param role item data role
167 * @return header data for role
168 */
headerData(int section,Qt::Orientation orientation,int role) const169 QVariant TrackDataModel::headerData(
170 int section, Qt::Orientation orientation, int role) const
171 {
172 if (role != Qt::DisplayRole)
173 return QVariant();
174 if (orientation == Qt::Horizontal && section < m_frameTypes.size()) {
175 Frame::ExtendedType type = m_frameTypes.at(section);
176 auto typeOrProperty = static_cast<int>(type.getType());
177 if (typeOrProperty < FT_FirstTrackProperty) {
178 return typeOrProperty == Frame::FT_Track
179 ? tr("Track") // shorter header for track number
180 : Frame::getDisplayName(type.getName());
181 } else {
182 switch (typeOrProperty) {
183 case FT_FilePath:
184 return tr("Absolute path to file");
185 case FT_FileName:
186 return tr("Filename");
187 case FT_Duration:
188 return tr("Duration");
189 case FT_ImportDuration:
190 return tr("Length");
191 default:
192 ;
193 }
194 }
195 } else if (orientation == Qt::Vertical && section < m_trackDataVector.size()) {
196 int fileDuration = m_trackDataVector.at(section).getFileDuration();
197 if (fileDuration > 0) {
198 return TaggedFile::formatTime(fileDuration);
199 }
200 }
201 return section + 1;
202 }
203
204 /**
205 * Get number of rows.
206 * @param parent parent model index, invalid for table models
207 * @return number of rows,
208 * if parent is valid number of children (0 for table models)
209 */
rowCount(const QModelIndex & parent) const210 int TrackDataModel::rowCount(const QModelIndex& parent) const
211 {
212 return parent.isValid() ? 0 : m_trackDataVector.size();
213 }
214
215 /**
216 * Get number of columns.
217 * @param parent parent model index, invalid for table models
218 * @return number of columns,
219 * if parent is valid number of children (0 for table models)
220 */
columnCount(const QModelIndex & parent) const221 int TrackDataModel::columnCount(const QModelIndex& parent) const
222 {
223 return parent.isValid() ? 0 : m_frameTypes.size();
224 }
225
226 /**
227 * Insert rows.
228 * @param row rows are inserted before this row, if 0 at the begin,
229 * if rowCount() at the end
230 * @param count number of rows to insert
231 * @param parent parent model index, invalid for table models
232 * @return true if successful
233 */
insertRows(int row,int count,const QModelIndex &)234 bool TrackDataModel::insertRows(int row, int count, const QModelIndex&)
235 {
236 if (count > 0) {
237 beginInsertRows(QModelIndex(), row, row + count - 1);
238 m_trackDataVector.insert(row, count, ImportTrackData());
239 endInsertRows();
240 }
241 return true;
242 }
243
244 /**
245 * Remove rows.
246 * @param row rows are removed starting with this row
247 * @param count number of rows to remove
248 * @param parent parent model index, invalid for table models
249 * @return true if successful
250 */
removeRows(int row,int count,const QModelIndex &)251 bool TrackDataModel::removeRows(int row, int count,
252 const QModelIndex&)
253 {
254 if (count > 0) {
255 beginRemoveRows(QModelIndex(), row, row + count - 1);
256 m_trackDataVector.remove(row, count);
257 endRemoveRows();
258 }
259 return true;
260 }
261
262 /**
263 * Insert columns.
264 * @param column columns are inserted before this column, if 0 at the begin,
265 * if columnCount() at the end
266 * @param count number of columns to insert
267 * @param parent parent model index, invalid for table models
268 * @return true if successful
269 */
insertColumns(int column,int count,const QModelIndex &)270 bool TrackDataModel::insertColumns(int column, int count,
271 const QModelIndex&)
272 {
273 if (count > 0) {
274 beginInsertColumns(QModelIndex(), column, column + count - 1);
275 for (int i = 0; i < count; ++i)
276 m_frameTypes.insert(column, Frame::ExtendedType());
277 endInsertColumns();
278 }
279 return true;
280 }
281
282 /**
283 * Remove columns.
284 * @param column columns are removed starting with this column
285 * @param count number of columns to remove
286 * @param parent parent model index, invalid for table models
287 * @return true if successful
288 */
removeColumns(int column,int count,const QModelIndex &)289 bool TrackDataModel::removeColumns(int column, int count,
290 const QModelIndex&)
291 {
292 if (count > 0) {
293 beginRemoveColumns(QModelIndex(), column, column + count - 1);
294 for (int i = 0; i < count; ++i)
295 m_frameTypes.removeAt(column);
296 endRemoveColumns();
297 }
298 return true;
299 }
300
301 /**
302 * Set the check state of all tracks in the table.
303 *
304 * @param checked true to check the tracks
305 */
setAllCheckStates(bool checked)306 void TrackDataModel::setAllCheckStates(bool checked)
307 {
308 for (int row = 0; row < rowCount(); ++row) {
309 m_trackDataVector[row].setEnabled(checked);
310 }
311 }
312
313 /**
314 * Set time difference check configuration.
315 *
316 * @param enable true to enable check
317 * @param maxDiff maximum allowed time difference
318 */
setTimeDifferenceCheck(bool enable,int maxDiff)319 void TrackDataModel::setTimeDifferenceCheck(bool enable, int maxDiff) {
320 bool changed = m_diffCheckEnabled != enable || m_maxDiff != maxDiff;
321 m_diffCheckEnabled = enable;
322 m_maxDiff = maxDiff;
323 if (changed)
324 emit dataChanged(index(0,0), index(rowCount() - 1, 0));
325 }
326
327 /**
328 * Calculate accuracy of imported track data.
329 * @return accuracy in percent, -1 if unknown.
330 */
calculateAccuracy() const331 int TrackDataModel::calculateAccuracy() const
332 {
333 int numImportTracks = 0, numTracks = 0, numMismatches = 0, numMatches = 0;
334 for (auto it = m_trackDataVector.constBegin();
335 it != m_trackDataVector.constEnd();
336 ++it) {
337 const ImportTrackData& trackData = *it;
338 int diff = trackData.getTimeDifference();
339 if (diff >= 0) {
340 if (diff > 3) {
341 ++numMismatches;
342 } else {
343 ++numMatches;
344 }
345 } else {
346 // no durations available => try to match using file name and title
347 QSet<QString> titleWords = trackData.getTitleWords();
348 int numWords = titleWords.size();
349 if (numWords > 0) {
350 QSet<QString> fileWords = trackData.getFilenameWords();
351 if (fileWords.size() < numWords) {
352 numWords = fileWords.size();
353 }
354 int wordMatch = numWords > 0
355 ? 100 * (fileWords & titleWords).size() / numWords : 0;
356 if (wordMatch < 75) {
357 ++numMismatches;
358 } else {
359 ++numMatches;
360 }
361 }
362 }
363 if (trackData.getImportDuration() != 0 || !trackData.getTitle().isEmpty()) {
364 ++numImportTracks;
365 }
366 if (trackData.getFileDuration() != 0) {
367 ++numTracks;
368 }
369 }
370
371 if (numTracks > 0 && numImportTracks > 0 &&
372 (numMatches > 0 || numMismatches > 0)) {
373 return numMatches * 100 / numTracks;
374 }
375 return -1;
376 }
377
378
379 /**
380 * Get frame for index.
381 * @param index model index
382 * @return frame, 0 if no frame.
383 */
getFrameOfIndex(const QModelIndex & index) const384 const Frame* TrackDataModel::getFrameOfIndex(const QModelIndex& index) const
385 {
386 if (!index.isValid() ||
387 index.row() < 0 ||
388 index.row() >= static_cast<int>(m_trackDataVector.size()) ||
389 index.column() < 0 ||
390 index.column() >= static_cast<int>(m_frameTypes.size()))
391 return nullptr;
392
393 const ImportTrackData& trackData = m_trackDataVector.at(index.row());
394 Frame::ExtendedType type = m_frameTypes.at(index.column());
395 if (static_cast<int>(type.getType()) >= FT_FirstTrackProperty)
396 return nullptr;
397
398 auto it = trackData.findByExtendedType(type);
399 return it != trackData.cend() ? &(*it) : nullptr;
400 }
401
402 /**
403 * Set track data.
404 * @param trackDataVector track data
405 */
setTrackData(const ImportTrackDataVector & trackDataVector)406 void TrackDataModel::setTrackData(const ImportTrackDataVector& trackDataVector)
407 {
408 static const int initFrameTypes[] = {
409 FT_ImportDuration, FT_FileName, FT_FilePath,
410 Frame::FT_Track, Frame::FT_Title,
411 Frame::FT_Artist, Frame::FT_Album, Frame::FT_Date, Frame::FT_Genre,
412 Frame::FT_Comment
413 };
414
415 QList<Frame::ExtendedType> newFrameTypes;
416 for (auto initFrameType : initFrameTypes) {
417 newFrameTypes.append( // clazy:exclude=reserve-candidates
418 Frame::ExtendedType(static_cast<Frame::Type>(initFrameType), QLatin1String("")));
419 }
420
421 for (auto tit = trackDataVector.constBegin();
422 tit != trackDataVector.constEnd();
423 ++tit) {
424 for (auto fit = tit->cbegin(); fit != tit->cend(); ++fit) {
425 Frame::ExtendedType type = fit->getExtendedType();
426 if (type.getType() > Frame::FT_LastV1Frame &&
427 !newFrameTypes.contains(type)) {
428 newFrameTypes.append(type);
429 }
430 }
431 }
432
433 int oldNumTypes = m_frameTypes.size();
434 int newNumTypes = newFrameTypes.size();
435 int numColumnsChanged = qMin(oldNumTypes, newNumTypes);
436 if (newNumTypes < oldNumTypes)
437 beginRemoveColumns(QModelIndex(), newNumTypes, oldNumTypes - 1);
438 else if (newNumTypes > oldNumTypes)
439 beginInsertColumns(QModelIndex(), oldNumTypes, newNumTypes - 1);
440
441 m_frameTypes = newFrameTypes;
442
443 if (newNumTypes < oldNumTypes)
444 endRemoveColumns();
445 else if (newNumTypes > oldNumTypes)
446 endInsertColumns();
447
448 int oldNumTracks = m_trackDataVector.size();
449 int newNumTracks = trackDataVector.size();
450 int numRowsChanged = qMin(oldNumTracks, newNumTracks);
451 if (newNumTracks < oldNumTracks)
452 beginRemoveRows(QModelIndex(), newNumTracks, oldNumTracks - 1);
453 else if (newNumTracks > oldNumTracks)
454 beginInsertRows(QModelIndex(), oldNumTracks, newNumTracks - 1);
455
456 m_trackDataVector = trackDataVector;
457
458 if (newNumTracks < oldNumTracks)
459 endRemoveRows();
460 else if (newNumTracks > oldNumTracks)
461 endInsertRows();
462
463
464 if (numRowsChanged > 0)
465 emit dataChanged(
466 index(0, 0), index(numRowsChanged - 1, numColumnsChanged - 1));
467 }
468
469 /**
470 * Get track data.
471 * @return track data
472 */
getTrackData() const473 ImportTrackDataVector TrackDataModel::getTrackData() const
474 {
475 return m_trackDataVector;
476 }
477
478 /**
479 * Get the frame type for a column.
480 * @param column model column
481 * @return frame type of Frame::Type or TrackDataModel::TrackProperties,
482 * -1 if column invalid.
483 */
frameTypeForColumn(int column) const484 int TrackDataModel::frameTypeForColumn(int column) const
485 {
486 return column < m_frameTypes.size() ? m_frameTypes.at(column).getType() : -1;
487 }
488
489 /**
490 * Get column for a frame type.
491 * @param frameType frame type of Frame::Type or
492 * TrackDataModel::TrackProperties.
493 * @return model column, -1 if not found.
494 */
columnForFrameType(int frameType) const495 int TrackDataModel::columnForFrameType(int frameType) const
496 {
497 return m_frameTypes.indexOf(
498 Frame::ExtendedType(static_cast<Frame::Type>(frameType), QLatin1String("")));
499 }
500