]> git.sur5r.net Git - minitube/blob - src/playlistitemdelegate.cpp
New upstream version 3.9.1
[minitube] / src / playlistitemdelegate.cpp
1 /* $BEGIN_LICENSE
2
3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
5
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.
10
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.
15
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/>.
18
19 $END_LICENSE */
20
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"
29 #include "video.h"
30 #include "videodefinition.h"
31
32 const int PlaylistItemDelegate::thumbHeight = 90;
33 const int PlaylistItemDelegate::thumbWidth = 160;
34 const int PlaylistItemDelegate::padding = 8;
35
36 namespace {
37
38 bool drawElidedText(QPainter *painter, const QRect &textBox, const int flags, const QString &text) {
39     QString elidedText =
40             painter->fontMetrics().elidedText(text, Qt::ElideRight, textBox.width(), flags);
41     painter->drawText(textBox, 0, elidedText);
42     return elidedText.length() < text.length();
43 }
44 } // namespace
45
46 PlaylistItemDelegate::PlaylistItemDelegate(QObject *parent, bool downloadInfo)
47     : QStyledItemDelegate(parent), downloadInfo(downloadInfo), progressBar(nullptr) {
48     listView = qobject_cast<PlaylistView *>(parent);
49
50     smallerBoldFont = FontUtils::smallBold();
51     smallerFont = FontUtils::small();
52
53     if (downloadInfo) {
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);
59         progressBar->hide();
60     } else
61         createPlayIcon();
62 }
63
64 PlaylistItemDelegate::~PlaylistItemDelegate() {
65     if (progressBar) delete progressBar;
66 }
67
68 void PlaylistItemDelegate::createPlayIcon() {
69     qreal maxRatio = 2.0;
70     playIcon = QPixmap(thumbWidth * maxRatio, thumbHeight * maxRatio);
71     playIcon.setDevicePixelRatio(maxRatio);
72     playIcon.fill(Qt::transparent);
73
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);
79
80     const int hPadding = padding * 6;
81     const int vPadding = padding * 2;
82
83     QPolygon polygon;
84     polygon << QPoint(hPadding, vPadding) << QPoint(thumbWidth - hPadding, thumbHeight / 2)
85             << QPoint(hPadding, thumbHeight - vPadding);
86     painter.setBrush(Qt::white);
87     QPen pen;
88     pen.setColor(Qt::white);
89     pen.setWidth(padding);
90     pen.setJoinStyle(Qt::RoundJoin);
91     pen.setCapStyle(Qt::RoundCap);
92     painter.setPen(pen);
93     painter.drawPolygon(polygon);
94     painter.end();
95
96     QPainter painter2(&playIcon);
97     painter2.setOpacity(.75);
98     painter2.drawPixmap(0, 0, tempPixmap);
99 }
100
101 QSize PlaylistItemDelegate::sizeHint(const QStyleOptionViewItem & /*option*/,
102                                      const QModelIndex & /*index*/) const {
103     return QSize(thumbWidth, thumbHeight);
104 }
105
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);
112     else
113         QStyledItemDelegate::paint(painter, option, index);
114 }
115
116 void PlaylistItemDelegate::paintBody(QPainter *painter,
117                                      const QStyleOptionViewItem &option,
118                                      const QModelIndex &index) const {
119     const bool isSelected = option.state & QStyle::State_Selected;
120     if (isSelected)
121         QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter);
122
123     painter->save();
124     painter->translate(option.rect.topLeft());
125
126     QRect line(0, 0, option.rect.width(), option.rect.height());
127     if (downloadInfo) line.setWidth(line.width() / 2);
128
129     const bool isActive = index.data(ActiveTrackRole).toBool();
130
131     // get the video metadata
132     Video *video = index.data(VideoRole).value<VideoPointer>().data();
133
134     // draw the "current track" highlight underneath the text
135     if (isActive && !isSelected) paintActiveOverlay(painter, option, line);
136
137     // thumb
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);
144     } else
145         video->loadThumb({thumbWidth, thumbHeight}, pixelRatio)
146                 .then([pixelRatio, thumbKey, video](auto variant) {
147                     QPixmap pixmap;
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);
154                     video->changed();
155                 })
156                 .onFailed([](auto msg) { qDebug() << msg; });
157
158     const bool thumbsOnly = line.width() <= thumbWidth + 60;
159     const bool isHovered = index.data(HoveredItemRole).toBool();
160
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);
165
166     if (!thumbsOnly) {
167         // text color
168         if (isSelected)
169             painter->setPen(QPen(option.palette.highlightedText(), 0));
170         else
171             painter->setPen(QPen(option.palette.text(), 0));
172
173         // title
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);
184 #else
185             title.chop(1);
186 #endif
187             elidedTitle = title.trimmed() + QStringLiteral("…");
188             textBox = painter->boundingRect(textBox, titleFlags, elidedTitle);
189         }
190         painter->drawText(textBox, titleFlags, elidedTitle);
191
192         painter->setFont(smallerFont);
193         painter->setOpacity(.5);
194         QFontMetrics fontMetrics = painter->fontMetrics();
195         static const int flags = Qt::AlignLeft | Qt::AlignTop;
196
197         // published date
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);
203
204         bool elided = false;
205
206         // author
207         if (!listView || listView->isClickableAuthors()) {
208             bool authorHovered = isHovered && index.data(AuthorHoveredRole).toBool();
209
210             painter->save();
211             painter->setFont(smallerBoldFont);
212             if (!isSelected) {
213                 if (authorHovered)
214                     painter->setPen(QPen(option.palette.brush(QPalette::Highlight), 0));
215             }
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);
224             } else {
225                 painter->drawText(textBox, flags, author);
226             }
227             painter->restore();
228         }
229
230         // view count
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);
238             }
239             textBox = QRect(textPoint, textSize);
240             if (textBox.bottom() <= line.height()) {
241                 painter->drawText(textBox, flags, viewCount);
242             }
243         }
244
245         if (downloadInfo) {
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);
251         }
252
253     } else {
254         // thumbs only
255         if (isHovered) {
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);
268 #else
269                 title.chop(1);
270 #endif
271                 elidedTitle = title.trimmed() + QStringLiteral("…");
272                 textBox = painter->boundingRect(textBox, titleFlags, elidedTitle);
273             }
274             painter->fillRect(QRect(0, 0, thumbWidth, textBox.height() + padding * 2),
275                               QColor(0, 0, 0, 128));
276             painter->drawText(textBox, titleFlags, elidedTitle);
277         }
278     }
279
280     painter->restore();
281
282     if (downloadInfo) paintDownloadInfo(painter, option, index);
283 }
284
285 void PlaylistItemDelegate::paintActiveOverlay(QPainter *painter,
286                                               const QStyleOptionViewItem &option,
287                                               const QRect &line) const {
288     painter->save();
289     painter->setOpacity(.2);
290     painter->fillRect(line, option.palette.highlight());
291     painter->restore();
292 }
293
294 void PlaylistItemDelegate::drawTime(QPainter *painter,
295                                     const QString &time,
296                                     const QRect &line) const {
297     static const int timePadding = 4;
298     QRect textBox = painter->boundingRect(line, Qt::AlignLeft | Qt::AlignTop, time);
299     // add padding
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());
303
304     painter->save();
305     painter->setPen(Qt::NoPen);
306     painter->setBrush(Qt::black);
307     painter->setOpacity(.5);
308     painter->drawRect(textBox);
309     painter->restore();
310
311     painter->save();
312     painter->setPen(Qt::white);
313     painter->setFont(smallerFont);
314     painter->drawText(textBox, Qt::AlignCenter, time);
315     painter->restore();
316 }
317
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();
325
326     painter->save();
327
328     const QRect line(0, 0, option.rect.width() / 2, option.rect.height());
329
330     painter->translate(option.rect.topLeft());
331     painter->translate(line.width(), 0);
332
333     QString message;
334     DownloadItemStatus status = downloadItem->status();
335
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());
341
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");
351     }
352
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);
360     } else {
361         progressBar->setValue(0);
362         progressBar->setEnabled(false);
363     }
364
365     int progressBarWidth = line.width() - padding * 4 - 16;
366     progressBar->setMaximumWidth(progressBarWidth);
367     progressBar->setMinimumWidth(progressBarWidth);
368     painter->save();
369     painter->translate(padding, padding);
370     progressBar->render(painter);
371     painter->restore();
372
373     bool downloadButtonHovered = false;
374     bool downloadButtonPressed = false;
375     const bool isHovered = index.data(HoveredItemRole).toBool();
376     if (isHovered) {
377         downloadButtonHovered = index.data(DownloadButtonHoveredRole).toBool();
378         downloadButtonPressed = index.data(DownloadButtonPressedRole).toBool();
379     }
380     QIcon::Mode iconMode;
381     if (downloadButtonPressed)
382         iconMode = QIcon::Selected;
383     else if (downloadButtonHovered)
384         iconMode = QIcon::Active;
385     else
386         iconMode = QIcon::Normal;
387
388     if (status != Finished && status != Failed && status != Idle) {
389         if (downloadButtonHovered) message = tr("Stop downloading");
390         painter->save();
391         QIcon closeIcon = IconUtils::icon("window-close");
392         painter->drawPixmap(downloadButtonRect(line), closeIcon.pixmap(16, 16, iconMode));
393         painter->restore();
394     }
395
396     else if (status == Finished) {
397         if (downloadButtonHovered)
398 #ifdef APP_MAC
399             message = tr("Show in %1").arg("Finder");
400 #else
401             message = tr("Open parent folder");
402 #endif
403         painter->save();
404         QIcon searchIcon = IconUtils::icon("system-search");
405         painter->drawPixmap(downloadButtonRect(line), searchIcon.pixmap(16, 16, iconMode));
406         painter->restore();
407     }
408
409     else if (status == Failed || status == Idle) {
410         if (downloadButtonHovered) message = tr("Restart downloading");
411         painter->save();
412         QIcon searchIcon = IconUtils::icon("view-refresh");
413         painter->drawPixmap(downloadButtonRect(line), searchIcon.pixmap(16, 16, iconMode));
414         painter->restore();
415     }
416
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,
420                                     message);
421     painter->drawText(textBox, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, message);
422
423     painter->restore();
424 }
425
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);
429 }
430
431 QRect PlaylistItemDelegate::authorRect(const QModelIndex &index) const {
432     return authorRects.value(index.row());
433 }