3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
6 Minitube is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
11 Minitube is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with Minitube. If not, see <http://www.gnu.org/licenses/>.
21 #include "playlistitemdelegate.h"
22 #include "datautils.h"
23 #include "downloaditem.h"
24 #include "fontutils.h"
25 #include "iconutils.h"
26 #include "playlistmodel.h"
27 #include "playlistview.h"
28 #include "variantpromise.h"
30 #include "videodefinition.h"
32 const int PlaylistItemDelegate::thumbHeight = 90;
33 const int PlaylistItemDelegate::thumbWidth = 160;
34 const int PlaylistItemDelegate::padding = 8;
38 bool drawElidedText(QPainter *painter, const QRect &textBox, const int flags, const QString &text) {
40 painter->fontMetrics().elidedText(text, Qt::ElideRight, textBox.width(), flags);
41 painter->drawText(textBox, 0, elidedText);
42 return elidedText.length() < text.length();
46 PlaylistItemDelegate::PlaylistItemDelegate(QObject *parent, bool downloadInfo)
47 : QStyledItemDelegate(parent), downloadInfo(downloadInfo), progressBar(nullptr) {
48 listView = qobject_cast<PlaylistView *>(parent);
50 smallerBoldFont = FontUtils::smallBold();
51 smallerFont = FontUtils::small();
54 progressBar = new QProgressBar(qApp->activeWindow());
55 QPalette palette = progressBar->palette();
56 palette.setColor(QPalette::Window, Qt::transparent);
57 progressBar->setPalette(palette);
58 progressBar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
64 PlaylistItemDelegate::~PlaylistItemDelegate() {
65 if (progressBar) delete progressBar;
68 void PlaylistItemDelegate::createPlayIcon() {
70 playIcon = QPixmap(thumbWidth * maxRatio, thumbHeight * maxRatio);
71 playIcon.setDevicePixelRatio(maxRatio);
72 playIcon.fill(Qt::transparent);
74 QPixmap tempPixmap(thumbWidth * maxRatio, thumbHeight * maxRatio);
75 tempPixmap.setDevicePixelRatio(maxRatio);
76 tempPixmap.fill(Qt::transparent);
77 QPainter painter(&tempPixmap);
78 painter.setRenderHints(QPainter::Antialiasing, true);
80 const int hPadding = padding * 6;
81 const int vPadding = padding * 2;
84 polygon << QPoint(hPadding, vPadding) << QPoint(thumbWidth - hPadding, thumbHeight / 2)
85 << QPoint(hPadding, thumbHeight - vPadding);
86 painter.setBrush(Qt::white);
88 pen.setColor(Qt::white);
89 pen.setWidth(padding);
90 pen.setJoinStyle(Qt::RoundJoin);
91 pen.setCapStyle(Qt::RoundCap);
93 painter.drawPolygon(polygon);
96 QPainter painter2(&playIcon);
97 painter2.setOpacity(.75);
98 painter2.drawPixmap(0, 0, tempPixmap);
101 QSize PlaylistItemDelegate::sizeHint(const QStyleOptionViewItem & /*option*/,
102 const QModelIndex & /*index*/) const {
103 return QSize(thumbWidth, thumbHeight);
106 void PlaylistItemDelegate::paint(QPainter *painter,
107 const QStyleOptionViewItem &option,
108 const QModelIndex &index) const {
109 int itemType = index.data(ItemTypeRole).toInt();
110 if (itemType == ItemTypeVideo)
111 paintBody(painter, option, index);
113 QStyledItemDelegate::paint(painter, option, index);
116 void PlaylistItemDelegate::paintBody(QPainter *painter,
117 const QStyleOptionViewItem &option,
118 const QModelIndex &index) const {
119 const bool isSelected = option.state & QStyle::State_Selected;
121 QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter);
124 painter->translate(option.rect.topLeft());
126 QRect line(0, 0, option.rect.width(), option.rect.height());
127 if (downloadInfo) line.setWidth(line.width() / 2);
129 const bool isActive = index.data(ActiveTrackRole).toBool();
131 // get the video metadata
132 Video *video = index.data(VideoRole).value<VideoPointer>().data();
134 // draw the "current track" highlight underneath the text
135 if (isActive && !isSelected) paintActiveOverlay(painter, option, line);
138 qreal pixelRatio = painter->device()->devicePixelRatioF();
139 QByteArray thumbKey = ("t" + QString::number(pixelRatio)).toUtf8();
140 const QPixmap &thumb = video->property(thumbKey).value<QPixmap>();
141 if (!thumb.isNull()) {
142 painter->drawPixmap(0, 0, thumb);
143 if (video->getDuration() > 0) drawTime(painter, video->getFormattedDuration(), line);
145 video->loadThumb({thumbWidth, thumbHeight}, pixelRatio)
146 .then([pixelRatio, thumbKey, video](auto variant) {
148 pixmap.loadFromData(variant.toByteArray());
149 pixmap.setDevicePixelRatio(pixelRatio);
150 const int thumbWidth = PlaylistItemDelegate::thumbWidth * pixelRatio;
151 if (pixmap.width() > thumbWidth)
152 pixmap = pixmap.scaledToWidth(thumbWidth, Qt::SmoothTransformation);
153 video->setProperty(thumbKey, pixmap);
156 .onFailed([](auto msg) { qDebug() << msg; });
158 const bool thumbsOnly = line.width() <= thumbWidth + 60;
159 const bool isHovered = index.data(HoveredItemRole).toBool();
161 // play icon overlayed on the thumb
162 bool needPlayIcon = isActive;
163 if (thumbsOnly) needPlayIcon = needPlayIcon && !isHovered;
164 if (needPlayIcon) painter->drawPixmap(0, 0, playIcon);
169 painter->setPen(QPen(option.palette.highlightedText(), 0));
171 painter->setPen(QPen(option.palette.text(), 0));
174 QStringRef title(&video->getTitle());
175 QString elidedTitle = video->getTitle();
176 static const int titleFlags = Qt::AlignTop | Qt::TextWordWrap;
177 QRect textBox = line.adjusted(padding + thumbWidth, padding, -padding, 0);
178 textBox = painter->boundingRect(textBox, titleFlags, elidedTitle);
179 while (textBox.height() > 55 && elidedTitle.length() > 10) {
180 #if QT_VERSION < QT_VERSION_CHECK(5, 6, 0)
181 title = title.left(title.length() - 1);
182 #elif QT_VERSION < QT_VERSION_CHECK(5, 8, 0)
183 title.truncate(title.length() - 1);
187 elidedTitle = title.trimmed() + QStringLiteral("…");
188 textBox = painter->boundingRect(textBox, titleFlags, elidedTitle);
190 painter->drawText(textBox, titleFlags, elidedTitle);
192 painter->setFont(smallerFont);
193 painter->setOpacity(.5);
194 QFontMetrics fontMetrics = painter->fontMetrics();
195 static const int flags = Qt::AlignLeft | Qt::AlignTop;
198 const QString &published = video->getFormattedPublished();
199 QSize textSize(fontMetrics.size(Qt::TextSingleLine, published));
200 QPoint textPoint(padding + thumbWidth, padding * 2 + textBox.height());
201 textBox = QRect(textPoint, textSize);
202 painter->drawText(textBox, flags, published);
207 if (!listView || listView->isClickableAuthors()) {
208 bool authorHovered = isHovered && index.data(AuthorHoveredRole).toBool();
211 painter->setFont(smallerBoldFont);
214 painter->setPen(QPen(option.palette.brush(QPalette::Highlight), 0));
216 const QString &author = video->getChannelTitle();
217 textPoint.setX(textBox.right() + padding);
218 textSize = QSize(painter->fontMetrics().size(Qt::TextSingleLine, author));
219 textBox = QRect(textPoint, textSize);
220 authorRects.insert(index.row(), textBox);
221 if (textBox.right() > line.width() - padding) {
222 textBox.setRight(line.width());
223 elided = drawElidedText(painter, textBox, flags, author);
225 painter->drawText(textBox, flags, author);
231 if (video->getViewCount() > 0) {
232 const QString &viewCount = video->getFormattedViewCount();
233 textPoint.setX(textBox.right() + padding);
234 textSize = QSize(fontMetrics.size(Qt::TextSingleLine, viewCount));
235 if (elided || textPoint.x() + textSize.width() > line.width() - padding) {
236 textPoint.setX(thumbWidth + padding);
237 textPoint.setY(textPoint.y() + textSize.height() + padding);
239 textBox = QRect(textPoint, textSize);
240 if (textBox.bottom() <= line.height()) {
241 painter->drawText(textBox, flags, viewCount);
246 const QString &def = VideoDefinition::forCode(video->getDefinitionCode()).getName();
247 textPoint.setX(textBox.right() + padding);
248 textSize = QSize(fontMetrics.size(Qt::TextSingleLine, def));
249 textBox = QRect(textPoint, textSize);
250 painter->drawText(textBox, flags, def);
256 painter->setFont(smallerFont);
257 painter->setPen(Qt::white);
258 QStringRef title(&video->getTitle());
259 QString elidedTitle = video->getTitle();
260 static const int titleFlags = Qt::AlignTop | Qt::TextWordWrap;
261 QRect textBox(padding, padding, thumbWidth - padding * 2, thumbHeight - padding * 2);
262 textBox = painter->boundingRect(textBox, titleFlags, elidedTitle);
263 while (textBox.height() > 55 && elidedTitle.length() > 10) {
264 #if QT_VERSION < QT_VERSION_CHECK(5, 6, 0)
265 title = title.left(title.length() - 1);
266 #elif QT_VERSION < QT_VERSION_CHECK(5, 8, 0)
267 title.truncate(title.length() - 1);
271 elidedTitle = title.trimmed() + QStringLiteral("…");
272 textBox = painter->boundingRect(textBox, titleFlags, elidedTitle);
274 painter->fillRect(QRect(0, 0, thumbWidth, textBox.height() + padding * 2),
275 QColor(0, 0, 0, 128));
276 painter->drawText(textBox, titleFlags, elidedTitle);
282 if (downloadInfo) paintDownloadInfo(painter, option, index);
285 void PlaylistItemDelegate::paintActiveOverlay(QPainter *painter,
286 const QStyleOptionViewItem &option,
287 const QRect &line) const {
289 painter->setOpacity(.2);
290 painter->fillRect(line, option.palette.highlight());
294 void PlaylistItemDelegate::drawTime(QPainter *painter,
296 const QRect &line) const {
297 static const int timePadding = 4;
298 QRect textBox = painter->boundingRect(line, Qt::AlignLeft | Qt::AlignTop, time);
300 textBox.adjust(0, 0, timePadding, 0);
301 // move to bottom right corner of the thumb
302 textBox.translate(thumbWidth - textBox.width(), thumbHeight - textBox.height());
305 painter->setPen(Qt::NoPen);
306 painter->setBrush(Qt::black);
307 painter->setOpacity(.5);
308 painter->drawRect(textBox);
312 painter->setPen(Qt::white);
313 painter->setFont(smallerFont);
314 painter->drawText(textBox, Qt::AlignCenter, time);
318 void PlaylistItemDelegate::paintDownloadInfo(QPainter *painter,
319 const QStyleOptionViewItem &option,
320 const QModelIndex &index) const {
321 // get the video metadata
322 const DownloadItemPointer downloadItemPointer =
323 index.data(DownloadItemRole).value<DownloadItemPointer>();
324 const DownloadItem *downloadItem = downloadItemPointer.data();
328 const QRect line(0, 0, option.rect.width() / 2, option.rect.height());
330 painter->translate(option.rect.topLeft());
331 painter->translate(line.width(), 0);
334 DownloadItemStatus status = downloadItem->status();
336 if (status == Downloading) {
337 QString downloaded = DownloadItem::formattedFilesize(downloadItem->bytesReceived());
338 QString total = DownloadItem::formattedFilesize(downloadItem->bytesTotal());
339 QString speed = DownloadItem::formattedSpeed(downloadItem->currentSpeed());
340 QString eta = DownloadItem::formattedTime(downloadItem->remainingTime());
342 message = tr("%1 of %2 (%3) — %4").arg(downloaded, total, speed, eta);
343 } else if (status == Starting) {
344 message = tr("Preparing");
345 } else if (status == Failed) {
346 message = tr("Failed") + " — " + downloadItem->errorMessage();
347 } else if (status == Finished) {
348 message = tr("Completed");
349 } else if (status == Idle) {
350 message = tr("Stopped");
353 // progressBar->setPalette(option.palette);
354 if (status == Finished) {
355 progressBar->setValue(100);
356 progressBar->setEnabled(true);
357 } else if (status == Downloading) {
358 progressBar->setValue(downloadItem->currentPercent());
359 progressBar->setEnabled(true);
361 progressBar->setValue(0);
362 progressBar->setEnabled(false);
365 int progressBarWidth = line.width() - padding * 4 - 16;
366 progressBar->setMaximumWidth(progressBarWidth);
367 progressBar->setMinimumWidth(progressBarWidth);
369 painter->translate(padding, padding);
370 progressBar->render(painter);
373 bool downloadButtonHovered = false;
374 bool downloadButtonPressed = false;
375 const bool isHovered = index.data(HoveredItemRole).toBool();
377 downloadButtonHovered = index.data(DownloadButtonHoveredRole).toBool();
378 downloadButtonPressed = index.data(DownloadButtonPressedRole).toBool();
380 QIcon::Mode iconMode;
381 if (downloadButtonPressed)
382 iconMode = QIcon::Selected;
383 else if (downloadButtonHovered)
384 iconMode = QIcon::Active;
386 iconMode = QIcon::Normal;
388 if (status != Finished && status != Failed && status != Idle) {
389 if (downloadButtonHovered) message = tr("Stop downloading");
391 QIcon closeIcon = IconUtils::icon("window-close");
392 painter->drawPixmap(downloadButtonRect(line), closeIcon.pixmap(16, 16, iconMode));
396 else if (status == Finished) {
397 if (downloadButtonHovered)
399 message = tr("Show in %1").arg("Finder");
401 message = tr("Open parent folder");
404 QIcon searchIcon = IconUtils::icon("system-search");
405 painter->drawPixmap(downloadButtonRect(line), searchIcon.pixmap(16, 16, iconMode));
409 else if (status == Failed || status == Idle) {
410 if (downloadButtonHovered) message = tr("Restart downloading");
412 QIcon searchIcon = IconUtils::icon("view-refresh");
413 painter->drawPixmap(downloadButtonRect(line), searchIcon.pixmap(16, 16, iconMode));
417 QRect textBox = line.adjusted(padding, padding * 2 + progressBar->sizeHint().height(),
418 -2 * padding, -padding);
419 textBox = painter->boundingRect(textBox, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap,
421 painter->drawText(textBox, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, message);
426 QRect PlaylistItemDelegate::downloadButtonRect(const QRect &line) const {
427 return QRect(line.width() - padding * 2 - 16,
428 padding + progressBar->sizeHint().height() / 2 - 8, 16, 16);
431 QRect PlaylistItemDelegate::authorRect(const QModelIndex &index) const {
432 return authorRects.value(index.row());