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