1 /*
2     SPDX-FileCopyrightText: 2006 Peter Penz <peter.penz@gmx.at>
3     SPDX-FileCopyrightText: 2008 Jean-Baptiste Mardelle <jb@kdenlive.org>
4     SPDX-FileCopyrightText: 2012 Simon A. Eugster <simon.eu@gmail.com>
5 
6     Some code borrowed from Dolphin, adapted (2008) to Kdenlive
7 
8     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
9 */
10 
11 #include "statusbarmessagelabel.h"
12 #include "kdenlivesettings.h"
13 #include "core.h"
14 #include "mainwindow.h"
15 
16 #include <KNotification>
17 #include <kcolorscheme.h>
18 #include <kiconloader.h>
19 #include <klocalizedstring.h>
20 
21 #include <QDialog>
22 #include <QDialogButtonBox>
23 #include <QHBoxLayout>
24 #include <QIcon>
25 #include <QLabel>
26 #include <QMouseEvent>
27 #include <QPixmap>
28 #include <QProgressBar>
29 #include <QPropertyAnimation>
30 #include <QPushButton>
31 #include <QStyle>
32 #include <QTextEdit>
33 
FlashLabel(QWidget * parent)34 FlashLabel::FlashLabel(QWidget *parent)
35     : QWidget(parent)
36 {
37     setAutoFillBackground(true);
38 }
39 
40 FlashLabel::~FlashLabel() = default;
41 
setColor(const QColor & col)42 void FlashLabel::setColor(const QColor &col)
43 {
44     QPalette pal = palette();
45     pal.setColor(QPalette::Window, col);
46     setPalette(pal);
47     update();
48 }
49 
color() const50 QColor FlashLabel::color() const
51 {
52     return palette().window().color();
53 }
54 
StatusBarMessageLabel(QWidget * parent)55 StatusBarMessageLabel::StatusBarMessageLabel(QWidget *parent)
56     : QWidget(parent)
57     , m_minTextHeight(-1)
58     , m_keymapText()
59     , m_tooltipText()
60     , m_queueSemaphore(1)
61 {
62     setMinimumHeight(KIconLoader::SizeSmall);
63     setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
64     m_container = new FlashLabel(this);
65     auto *lay = new QHBoxLayout(this);
66     auto *lay2 = new QHBoxLayout(m_container);
67     m_pixmap = new QLabel(this);
68     m_pixmap->setAlignment(Qt::AlignCenter);
69     m_label = new QLabel(this);
70     m_label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
71     m_label->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
72     m_keyMap = new QLabel(this);
73     m_keyMap->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
74     m_keyMap->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
75     m_progress = new QProgressBar(this);
76     lay2->addWidget(m_pixmap);
77     lay2->addWidget(m_label);
78     lay2->addWidget(m_progress);
79     lay->addWidget(m_keyMap);
80 
81     auto *spacer = new QSpacerItem(1, 1, QSizePolicy::MinimumExpanding, QSizePolicy::Maximum);
82     lay->addItem(spacer);
83     lay->addWidget(m_container);
84     setLayout(lay);
85     m_progress->setVisible(false);
86     lay->setContentsMargins(BorderGap, 0, 2 * BorderGap, 0);
87     m_queueTimer.setSingleShot(true);
88     connect(&m_queueTimer, &QTimer::timeout, this, &StatusBarMessageLabel::slotMessageTimeout);
89     connect(m_label, &QLabel::linkActivated, this, &StatusBarMessageLabel::slotShowJobLog);
90 }
91 
92 StatusBarMessageLabel::~StatusBarMessageLabel() = default;
93 
mousePressEvent(QMouseEvent * event)94 void StatusBarMessageLabel::mousePressEvent(QMouseEvent *event)
95 {
96     QWidget::mousePressEvent(event);
97     if (m_pixmap->rect().contains(event->localPos().toPoint()) && m_currentMessage.type == MltError) {
98         confirmErrorMessage();
99     }
100 }
101 
setKeyMap(const QString & text)102 void StatusBarMessageLabel::setKeyMap(const QString &text)
103 {
104     m_keyMap->setText(text);
105     m_keymapText = text;
106 }
107 
setTmpKeyMap(const QString & text)108 void StatusBarMessageLabel::setTmpKeyMap(const QString &text)
109 {
110     if (text.isEmpty()) {
111         m_keyMap->setText(m_keymapText);
112     } else {
113         m_keyMap->setText(text);
114     }
115 }
116 
setProgressMessage(const QString & text,MessageType type,int progress)117 void StatusBarMessageLabel::setProgressMessage(const QString &text, MessageType type, int progress)
118 {
119     if (type == ProcessingJobMessage) {
120         m_progress->setValue(progress);
121         m_progress->setVisible(progress < 100);
122     } else if (m_currentMessage.type != ProcessingJobMessage || type == OperationCompletedMessage) {
123         m_progress->setVisible(progress < 100);
124     }
125     if (text == m_currentMessage.text) {
126         return;
127     }
128     setMessage(text, type, 0);
129 }
130 
setMessage(const QString & text,MessageType type,int timeoutMS)131 void StatusBarMessageLabel::setMessage(const QString &text, MessageType type, int timeoutMS)
132 {
133     if (type == TooltipMessage) {
134         m_tooltipText = text;
135         if (m_currentMessage.type == DefaultMessage) {
136             m_label->setText(m_tooltipText);
137         }
138         return;
139     }
140     if (type == m_currentMessage.type && text == m_currentMessage.text) {
141         return;
142     }
143     StatusBarMessageItem item(text, type, timeoutMS);
144     if (type == OperationCompletedMessage) {
145         m_progress->setVisible(false);
146     }
147     if (item.type == ErrorMessage || item.type == MltError) {
148         KNotification::event(QStringLiteral("ErrorMessage"), item.text);
149     }
150 
151     m_queueSemaphore.acquire();
152     if (!m_messageQueue.contains(item)) {
153         if (item.type == ErrorMessage || item.type == MltError || item.type == ProcessingJobMessage || item.type == OperationCompletedMessage) {
154             qCDebug(KDENLIVE_LOG) << item.text;
155 
156             // Put the new error message at first place and immediately show it
157             item.timeoutMillis = qMax(item.timeoutMillis, 3000);
158 
159             if (item.type == ProcessingJobMessage) {
160                 // This is a job progress info, discard previous ones
161                 QList<StatusBarMessageItem> cleanList;
162                 for (const StatusBarMessageItem &msg : qAsConst(m_messageQueue)) {
163                     if (msg.type != ProcessingJobMessage) {
164                         cleanList << msg;
165                     }
166                 }
167                 m_messageQueue = cleanList;
168             } else {
169                 // Important error message, delete previous queue so they don't appear afterwards out of context
170                 m_messageQueue.clear();
171             }
172 
173             m_messageQueue.push_front(item);
174 
175             // In case we are already displaying an error message, add a little delay
176             int delay = 800 * static_cast<int>(m_currentMessage.type == ErrorMessage || m_currentMessage.type == MltError);
177             m_queueTimer.start(delay);
178         } else {
179             // Message with normal priority
180             item.timeoutMillis = qMax(item.timeoutMillis, 2000);
181             m_messageQueue.push_back(item);
182             if (!m_queueTimer.isValid() || m_queueTimer.elapsed() >= m_currentMessage.timeoutMillis) {
183                 m_queueTimer.start(0);
184             }
185         }
186     }
187     m_queueSemaphore.release();
188 }
189 
slotMessageTimeout()190 bool StatusBarMessageLabel::slotMessageTimeout()
191 {
192     m_queueSemaphore.acquire();
193 
194     bool newMessage = false;
195 
196     // Get the next message from the queue, unless the current one needs to be confirmed
197     if (m_currentMessage.type == ProcessingJobMessage) {
198         // Check if we have a job completed message to cancel this one
199         StatusBarMessageItem item;
200         while (!m_messageQueue.isEmpty()) {
201             item = m_messageQueue.at(0);
202             m_messageQueue.removeFirst();
203             if (item.type == OperationCompletedMessage || item.type == ErrorMessage || item.type == MltError || item.type == ProcessingJobMessage) {
204                 m_currentMessage = item;
205                 m_label->setText(m_currentMessage.text);
206                 newMessage = true;
207                 break;
208             }
209         }
210     } else if (!m_messageQueue.isEmpty()) {
211         if (!m_currentMessage.needsConfirmation()) {
212             m_currentMessage = m_messageQueue.at(0);
213             m_label->setText(m_currentMessage.text);
214             m_messageQueue.removeFirst();
215             newMessage = true;
216         }
217     }
218 
219     // If the queue is empty, add a default (empty) message
220     if (m_messageQueue.isEmpty() && m_currentMessage.type != DefaultMessage) {
221         m_messageQueue.push_back(StatusBarMessageItem());
222     }
223 
224     // Start a new timer, unless the current message still needs to be confirmed
225     if (!m_messageQueue.isEmpty()) {
226 
227         if (!m_currentMessage.needsConfirmation()) {
228             m_queueTimer.start(m_currentMessage.timeoutMillis);
229         }
230     }
231 
232     QColor errorBgColor = KStatefulBrush(KColorScheme::Window, KColorScheme::NegativeBackground).brush(m_container->palette()).color();
233     const char *iconName = nullptr;
234     m_container->setColor(m_container->palette().window().color());
235     switch (m_currentMessage.type) {
236     case ProcessingJobMessage:
237         iconName = "chronometer";
238         m_pixmap->setCursor(Qt::ArrowCursor);
239         break;
240     case OperationCompletedMessage:
241         iconName = "dialog-ok";
242         m_pixmap->setCursor(Qt::ArrowCursor);
243         break;
244 
245     case InformationMessage: {
246         iconName = "dialog-information";
247         m_pixmap->setCursor(Qt::ArrowCursor);
248         QPropertyAnimation *anim = new QPropertyAnimation(m_container, "color", this);
249         anim->setDuration(qMin(m_currentMessage.timeoutMillis, 3000));
250         anim->setEasingCurve(QEasingCurve::InOutQuad);
251         anim->setKeyValueAt(0.2, m_container->palette().highlight().color());
252         anim->setEndValue(m_container->palette().window().color());
253         anim->start(QPropertyAnimation::DeleteWhenStopped);
254         break;
255     }
256 
257     case ErrorMessage: {
258         iconName = "dialog-warning";
259         m_pixmap->setCursor(Qt::ArrowCursor);
260         QPropertyAnimation *anim = new QPropertyAnimation(m_container, "color", this);
261         anim->setStartValue(errorBgColor);
262         anim->setKeyValueAt(0.8, errorBgColor);
263         anim->setEndValue(m_container->palette().window().color());
264         anim->setEasingCurve(QEasingCurve::OutCubic);
265         anim->setDuration(qMin(m_currentMessage.timeoutMillis, 4000));
266         anim->start(QPropertyAnimation::DeleteWhenStopped);
267         break;
268     }
269     case MltError: {
270         iconName = "dialog-close";
271         m_pixmap->setCursor(Qt::PointingHandCursor);
272         QPropertyAnimation *anim = new QPropertyAnimation(m_container, "color", this);
273         anim->setStartValue(errorBgColor);
274         anim->setEndValue(errorBgColor);
275         anim->setEasingCurve(QEasingCurve::OutCubic);
276         anim->setDuration(qMin(m_currentMessage.timeoutMillis, 3000));
277         anim->start(QPropertyAnimation::DeleteWhenStopped);
278         break;
279     }
280     case DefaultMessage:
281         m_pixmap->setCursor(Qt::ArrowCursor);
282         m_label->setText(m_tooltipText);
283     default:
284         break;
285     }
286 
287     if (iconName == nullptr) {
288         m_pixmap->setVisible(false);
289     } else {
290         m_pixmap->setPixmap(QIcon::fromTheme(iconName).pixmap(style()->pixelMetric(QStyle::PM_SmallIconSize)));
291         m_pixmap->setVisible(true);
292     }
293     m_queueSemaphore.release();
294 
295     return newMessage;
296 }
297 
confirmErrorMessage()298 void StatusBarMessageLabel::confirmErrorMessage()
299 {
300     m_currentMessage.confirmed = true;
301     m_queueTimer.start(0);
302 }
303 
resizeEvent(QResizeEvent * event)304 void StatusBarMessageLabel::resizeEvent(QResizeEvent *event)
305 {
306     QWidget::resizeEvent(event);
307 }
308 
slotShowJobLog(const QString & text)309 void StatusBarMessageLabel::slotShowJobLog(const QString &text)
310 {
311     // Special actions
312     if (text.startsWith(QLatin1Char('#'))) {
313         if (text == QLatin1String("#projectmonitor")) {
314             // Raise project monitor
315             pCore->window()->raiseMonitor(false);
316             return;
317         } else if (text == QLatin1String("#clipmonitor")) {
318             // Raise project monitor
319             pCore->window()->raiseMonitor(true);
320             return;
321         }
322     }
323     QDialog d(this);
324     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
325     QWidget *mainWidget = new QWidget(this);
326     auto *l = new QVBoxLayout;
327     QTextEdit t(&d);
328     t.insertPlainText(QUrl::fromPercentEncoding(text.toUtf8()));
329     t.setReadOnly(true);
330     l->addWidget(&t);
331     mainWidget->setLayout(l);
332     auto *mainLayout = new QVBoxLayout;
333     d.setLayout(mainLayout);
334     mainLayout->addWidget(mainWidget);
335     mainLayout->addWidget(buttonBox);
336     d.connect(buttonBox, &QDialogButtonBox::rejected, &d, &QDialog::accept);
337     d.exec();
338     confirmErrorMessage();
339 }
340