1 /*
2 * Strawberry Music Player
3 * This file was part of Clementine.
4 * Copyright 2010, David Sansome <me@davidsansome.com>
5 * Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
6 *
7 * Strawberry is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * Strawberry is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
19 *
20 */
21
22 #include "config.h"
23
24 #include <chrono>
25
26 #include <QtGlobal>
27 #include <QApplication>
28 #include <QGuiApplication>
29 #include <QWindow>
30 #include <QScreen>
31 #include <QWidget>
32 #include <QList>
33 #include <QVariant>
34 #include <QString>
35 #include <QImage>
36 #include <QPixmap>
37 #include <QBitmap>
38 #include <QLabel>
39 #include <QPainter>
40 #include <QPainterPath>
41 #include <QPalette>
42 #include <QColor>
43 #include <QBrush>
44 #include <QCursor>
45 #include <QPen>
46 #include <QRect>
47 #include <QPoint>
48 #include <QFont>
49 #include <QTimer>
50 #include <QTimeLine>
51 #include <QTransform>
52 #include <QLayout>
53 #include <QBoxLayout>
54 #include <QLinearGradient>
55 #include <QSettings>
56 #include <QFlags>
57 #include <QtEvents>
58
59 #ifdef HAVE_X11EXTRAS
60 # include <QX11Info>
61 #elif defined(HAVE_X11) && defined(HAVE_QPA_QPLATFORMNATIVEINTERFACE_H)
62 # include <qpa/qplatformnativeinterface.h>
63 #endif
64
65 #include "osdpretty.h"
66 #include "ui_osdpretty.h"
67
68 #ifdef Q_OS_WIN
69 # include <windows.h>
70 #endif
71
72 #include "core/utilities.h"
73
74 using namespace std::chrono_literals;
75
76 const char *OSDPretty::kSettingsGroup = "OSDPretty";
77
78 const int OSDPretty::kDropShadowSize = 13;
79 const int OSDPretty::kBorderRadius = 10;
80 const int OSDPretty::kMaxIconSize = 100;
81
82 const int OSDPretty::kSnapProximity = 20;
83
84 const QRgb OSDPretty::kPresetBlue = qRgb(102, 150, 227);
85 const QRgb OSDPretty::kPresetRed = qRgb(202, 22, 16);
86
87
OSDPretty(Mode mode,QWidget * parent)88 OSDPretty::OSDPretty(Mode mode, QWidget *parent)
89 : QWidget(parent),
90 ui_(new Ui_OSDPretty),
91 mode_(mode),
92 background_color_(kPresetBlue),
93 background_opacity_(0.85),
94 popup_screen_(nullptr),
95 disable_duration_(false),
96 timeout_(new QTimer(this)),
97 fading_enabled_(false),
98 fader_(new QTimeLine(300, this)),
99 toggle_mode_(false) {
100
101 Qt::WindowFlags flags = Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint;
102
103 setWindowFlags(flags);
104 setAttribute(Qt::WA_TranslucentBackground, true);
105 setAttribute(Qt::WA_X11NetWmWindowTypeNotification, true);
106 setAttribute(Qt::WA_ShowWithoutActivating, true);
107 ui_->setupUi(this);
108
109 #ifdef Q_OS_WIN
110 // Don't show the window in the taskbar. Qt::ToolTip does this too, but it adds an extra ugly shadow.
111 int ex_style = GetWindowLong((HWND) winId(), GWL_EXSTYLE);
112 ex_style |= WS_EX_NOACTIVATE;
113 SetWindowLong((HWND) winId(), GWL_EXSTYLE, ex_style);
114 #endif
115
116 // Mode settings
117 switch (mode_) {
118 case Mode_Popup:
119 setCursor(QCursor(Qt::ArrowCursor));
120 break;
121
122 case Mode_Draggable:
123 setCursor(QCursor(Qt::OpenHandCursor));
124 break;
125 }
126
127 // Timeout
128 timeout_->setSingleShot(true);
129 timeout_->setInterval(5s);
130 QObject::connect(timeout_, &QTimer::timeout, this, &OSDPretty::hide);
131
132 ui_->icon->setMaximumSize(kMaxIconSize, kMaxIconSize);
133
134 // Fader
135 QObject::connect(fader_, &QTimeLine::valueChanged, this, &OSDPretty::FaderValueChanged);
136 QObject::connect(fader_, &QTimeLine::finished, this, &OSDPretty::FaderFinished);
137
138 // Load the show edges and corners
139 QImage shadow_edge(":/pictures/osd_shadow_edge.png");
140 QImage shadow_corner(":/pictures/osd_shadow_corner.png");
141 for (int i = 0; i < 4; ++i) {
142 QTransform rotation = QTransform().rotate(90 * i);
143 shadow_edge_[i] = QPixmap::fromImage(shadow_edge.transformed(rotation));
144 shadow_corner_[i] = QPixmap::fromImage(shadow_corner.transformed(rotation));
145 }
146 background_ = QPixmap(":/pictures/osd_background.png");
147
148 // Set the margins to allow for the drop shadow
149 QBoxLayout *l = qobject_cast<QBoxLayout*>(layout());
150 QMargins margin = l->contentsMargins();
151 margin.setTop(margin.top() + kDropShadowSize);
152 margin.setBottom(margin.bottom() + kDropShadowSize);
153 margin.setLeft(margin.left() + kDropShadowSize);
154 margin.setRight(margin.right() + kDropShadowSize);
155 l->setContentsMargins(margin);
156
157 QObject::connect(qApp, &QApplication::screenAdded, this, &OSDPretty::ScreenAdded);
158 QObject::connect(qApp, &QApplication::screenRemoved, this, &OSDPretty::ScreenRemoved);
159
160 }
161
~OSDPretty()162 OSDPretty::~OSDPretty() {
163 delete ui_;
164 }
165
showEvent(QShowEvent * e)166 void OSDPretty::showEvent(QShowEvent *e) {
167
168 screens_.clear();
169 for(QScreen *screen : QGuiApplication::screens()) {
170 screens_.insert(screen->name(), screen);
171 }
172
173 // Get current screen resolution
174 QScreen *screen = current_screen();
175 if (screen) {
176 QRect resolution = screen->availableGeometry();
177 // Leave 200 px for icon
178 ui_->summary->setMaximumWidth(resolution.width() - 200);
179 ui_->message->setMaximumWidth(resolution.width() - 200);
180 // Set maximum size for the OSD, a little margin here too
181 setMaximumSize(resolution.width() - 100, resolution.height() - 100);
182 }
183
184 setWindowOpacity(fading_enabled_ ? 0.0 : 1.0);
185
186 QWidget::showEvent(e);
187
188 Load();
189 Reposition();
190
191 if (fading_enabled_) {
192 fader_->setDirection(QTimeLine::Forward);
193 fader_->start(); // Timeout will be started in FaderFinished
194 }
195 else if (mode_ == Mode_Popup) {
196 if (!disable_duration()) {
197 timeout_->start();
198 }
199 // Ensures it is above when showing the preview
200 raise();
201 }
202
203 }
204
ScreenAdded(QScreen * screen)205 void OSDPretty::ScreenAdded(QScreen *screen) {
206
207 screens_.insert(screen->name(), screen);
208
209 }
210
ScreenRemoved(QScreen * screen)211 void OSDPretty::ScreenRemoved(QScreen *screen) {
212
213 if (screens_.contains(screen->name())) screens_.remove(screen->name());
214 if (screen == popup_screen_) popup_screen_ = current_screen();
215
216 }
217
IsTransparencyAvailable()218 bool OSDPretty::IsTransparencyAvailable() {
219
220 #ifdef HAVE_X11EXTRAS
221 return QX11Info::isCompositingManagerRunning();
222 #elif defined(HAVE_X11) && defined(HAVE_QPA_QPLATFORMNATIVEINTERFACE_H)
223 if (qApp) {
224 QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface();
225 QScreen *screen = popup_screen_ == nullptr ? QGuiApplication::primaryScreen() : popup_screen_;
226 if (native && screen) {
227 return native->nativeResourceForScreen(QByteArray("compositingEnabled"), screen);
228 }
229 }
230 #endif
231
232 return true;
233
234 }
235
Load()236 void OSDPretty::Load() {
237
238 QSettings s;
239 s.beginGroup(kSettingsGroup);
240 foreground_color_ = QColor(s.value("foreground_color", 0).toInt());
241 background_color_ = QColor(s.value("background_color", kPresetBlue).toInt());
242 background_opacity_ = s.value("background_opacity", 0.85).toFloat();
243 font_.fromString(s.value("font", "Verdana,9,-1,5,50,0,0,0,0,0").toString());
244 disable_duration_ = s.value("disable_duration", false).toBool();
245 #ifdef Q_OS_WIN
246 fading_enabled_ = s.value("fading", true).toBool();
247 #else
248 fading_enabled_ = s.value("fading", false).toBool();
249 #endif
250
251 if (s.contains("popup_screen")) {
252 popup_screen_name_ = s.value("popup_screen").toString();
253 if (screens_.contains(popup_screen_name_)) {
254 popup_screen_ = screens_[popup_screen_name_];
255 }
256 else {
257 popup_screen_ = current_screen();
258 if (current_screen()) popup_screen_name_ = current_screen()->name();
259 else popup_screen_name_.clear();
260 }
261 }
262 else {
263 popup_screen_ = current_screen();
264 if (current_screen()) popup_screen_name_ = current_screen()->name();
265 }
266
267 if (s.contains("popup_pos")) {
268 popup_pos_ = s.value("popup_pos").toPoint();
269 }
270 else {
271 if (popup_screen_) {
272 QRect geometry = popup_screen_->availableGeometry();
273 popup_pos_.setX(geometry.width() - width());
274 popup_pos_.setY(0);
275 }
276 else {
277 popup_pos_.setX(0);
278 popup_pos_.setY(0);
279 }
280 }
281
282 set_font(font());
283 set_foreground_color(foreground_color());
284
285 s.endGroup();
286
287 }
288
ReloadSettings()289 void OSDPretty::ReloadSettings() {
290 Load();
291 if (isVisible()) update();
292 }
293
BoxBorder() const294 QRect OSDPretty::BoxBorder() const {
295 return rect().adjusted(kDropShadowSize, kDropShadowSize, -kDropShadowSize, -kDropShadowSize);
296 }
297
paintEvent(QPaintEvent *)298 void OSDPretty::paintEvent(QPaintEvent*) {
299
300 QPainter p(this);
301 p.setRenderHint(QPainter::Antialiasing);
302
303 QRect box(BoxBorder());
304
305 // Shadow corners
306 const int kShadowCornerSize = kDropShadowSize + kBorderRadius;
307 p.drawPixmap(0, 0, shadow_corner_[0]);
308 p.drawPixmap(width() - kShadowCornerSize, 0, shadow_corner_[1]);
309 p.drawPixmap(width() - kShadowCornerSize, height() - kShadowCornerSize, shadow_corner_[2]);
310 p.drawPixmap(0, height() - kShadowCornerSize, shadow_corner_[3]);
311
312 // Shadow edges
313 p.drawTiledPixmap(kShadowCornerSize, 0, width() - kShadowCornerSize*2, kDropShadowSize, shadow_edge_[0]);
314 p.drawTiledPixmap(width() - kDropShadowSize, kShadowCornerSize, kDropShadowSize, height() - kShadowCornerSize*2, shadow_edge_[1]);
315 p.drawTiledPixmap(kShadowCornerSize, height() - kDropShadowSize, width() - kShadowCornerSize*2, kDropShadowSize, shadow_edge_[2]);
316 p.drawTiledPixmap(0, kShadowCornerSize, kDropShadowSize, height() - kShadowCornerSize*2, shadow_edge_[3]);
317
318 // Box background
319 p.setBrush(background_color_);
320 p.setPen(QPen());
321 p.setOpacity(background_opacity_);
322 p.drawRoundedRect(box, kBorderRadius, kBorderRadius);
323
324 // Background pattern
325 QPainterPath background_path;
326 background_path.addRoundedRect(box, kBorderRadius, kBorderRadius);
327 p.setClipPath(background_path);
328 p.setOpacity(1.0);
329 p.drawPixmap(box.right() - background_.width(), box.bottom() - background_.height(), background_);
330 p.setClipping(false);
331
332 // Gradient overlay
333 QLinearGradient gradient(0, 0, 0, height());
334 gradient.setColorAt(0, QColor(255, 255, 255, 130));
335 gradient.setColorAt(1, QColor(255, 255, 255, 50));
336 p.setBrush(gradient);
337 p.drawRoundedRect(box, kBorderRadius, kBorderRadius);
338
339 // Box border
340 p.setBrush(QBrush());
341 p.setPen(QPen(background_color_.darker(150), 3));
342 p.drawRoundedRect(box, kBorderRadius, kBorderRadius);
343
344 }
345
SetMessage(const QString & summary,const QString & message,const QImage & image)346 void OSDPretty::SetMessage(const QString &summary, const QString &message, const QImage &image) {
347
348 if (!image.isNull()) {
349 QImage scaled_image = image.scaled(kMaxIconSize, kMaxIconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
350 ui_->icon->setPixmap(QPixmap::fromImage(scaled_image));
351 ui_->icon->show();
352 }
353 else {
354 ui_->icon->hide();
355 }
356
357 ui_->summary->setText(summary);
358 ui_->message->setText(message);
359
360 if (isVisible()) Reposition();
361
362 }
363
364 // Set the desired message and then show the OSD
ShowMessage(const QString & summary,const QString & message,const QImage & image)365 void OSDPretty::ShowMessage(const QString &summary, const QString &message, const QImage &image) {
366
367 SetMessage(summary, message, image);
368
369 if (isVisible() && mode_ == Mode_Popup) {
370 // The OSD is already visible, toggle or restart the timer
371 if (toggle_mode()) {
372 set_toggle_mode(false);
373 // If timeout is disabled, timer hadn't been started
374 if (!disable_duration()) {
375 timeout_->stop();
376 }
377 hide();
378 }
379 else {
380 if (!disable_duration()) {
381 timeout_->start(); // Restart the timer
382 }
383 }
384 }
385 else {
386 if (toggle_mode()) {
387 set_toggle_mode(false);
388 }
389 // The OSD is not visible, show it
390 show();
391 }
392
393 }
394
setVisible(bool visible)395 void OSDPretty::setVisible(bool visible) {
396
397 if (!visible && fading_enabled_ && fader_->direction() == QTimeLine::Forward) {
398 fader_->setDirection(QTimeLine::Backward);
399 fader_->start();
400 }
401 else {
402 QWidget::setVisible(visible);
403 }
404
405 }
406
FaderFinished()407 void OSDPretty::FaderFinished() {
408
409 if (fader_->direction() == QTimeLine::Backward) {
410 hide();
411 }
412 else if (mode_ == Mode_Popup && !disable_duration()) {
413 timeout_->start();
414 }
415
416 }
417
FaderValueChanged(const qreal value)418 void OSDPretty::FaderValueChanged(const qreal value) {
419 setWindowOpacity(value);
420 }
421
Reposition()422 void OSDPretty::Reposition() {
423
424 // Make the OSD the proper size
425 layout()->activate();
426 resize(sizeHint());
427
428 // Work out where to place the OSD. -1 for x or y means "on the right or bottom edge".
429 if (popup_screen_) {
430
431 QRect geometry = popup_screen_->availableGeometry();
432
433 int x = popup_pos_.x() < 0 ? geometry.right() - width() : geometry.left() + popup_pos_.x();
434 int y = popup_pos_.y() < 0 ? geometry.bottom() - height() : geometry.top() + popup_pos_.y();
435
436 #ifndef Q_OS_WIN
437 x = qBound(0, x, geometry.right() - width());
438 y = qBound(0, y, geometry.bottom() - height());
439 #endif
440 move(x, y);
441 }
442
443 // Create a mask for the actual area of the OSD
444 QBitmap mask(size());
445 mask.clear();
446
447 QPainter p(&mask);
448 p.setBrush(Qt::color1);
449 p.drawRoundedRect(BoxBorder().adjusted(-1, -1, 0, 0), kBorderRadius, kBorderRadius);
450 p.end();
451
452 // If there's no compositing window manager running then we have to set an XShape mask.
453 if (IsTransparencyAvailable())
454 clearMask();
455 else {
456 setMask(mask);
457 }
458
459 // On windows, enable blurbehind on the masked area
460 #ifdef Q_OS_WIN
461 Utilities::enableBlurBehindWindow(windowHandle(), QRegion(mask));
462 #endif
463
464 }
465
466 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
enterEvent(QEnterEvent *)467 void OSDPretty::enterEvent(QEnterEvent*) {
468 #else
469 void OSDPretty::enterEvent(QEvent*) {
470 #endif
471 if (mode_ == Mode_Popup) {
472 setWindowOpacity(0.25);
473 }
474
475 }
476
477 void OSDPretty::leaveEvent(QEvent*) {
478 setWindowOpacity(1.0);
479 }
480
481 void OSDPretty::mousePressEvent(QMouseEvent *e) {
482
483 if (mode_ == Mode_Popup) {
484 hide();
485 }
486 else {
487 original_window_pos_ = pos();
488 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
489 drag_start_pos_ = e->globalPosition().toPoint();
490 #else
491 drag_start_pos_ = e->globalPos();
492 #endif
493 }
494
495 }
496
497 void OSDPretty::mouseMoveEvent(QMouseEvent *e) {
498
499 if (mode_ == Mode_Draggable) {
500 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
501 QPoint delta = e->globalPosition().toPoint() - drag_start_pos_;
502 #else
503 QPoint delta = e->globalPos() - drag_start_pos_;
504 #endif
505 QPoint new_pos = original_window_pos_ + delta;
506
507 // Keep it to the bounds of the desktop
508 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
509 QScreen *screen = current_screen(e->globalPosition().toPoint());
510 #else
511 QScreen *screen = current_screen(e->globalPos());
512 #endif
513 if (!screen) return;
514
515 QRect geometry = screen->availableGeometry();
516
517 new_pos.setX(qBound(geometry.left(), new_pos.x(), geometry.right() - width()));
518 new_pos.setY(qBound(geometry.top(), new_pos.y(), geometry.bottom() - height()));
519
520 // Snap to center
521 int snap_x = geometry.center().x() - width() / 2;
522 if (new_pos.x() > snap_x - kSnapProximity && new_pos.x() < snap_x + kSnapProximity) {
523 new_pos.setX(snap_x);
524 }
525
526 move(new_pos);
527
528 popup_screen_ = screen;
529 popup_screen_name_ = screen->name();
530 }
531
532 }
533
534 void OSDPretty::mouseReleaseEvent(QMouseEvent *) {
535
536 if (current_screen() && mode_ == Mode_Draggable) {
537 popup_screen_ = current_screen();
538 popup_screen_name_ = current_screen()->name();
539 popup_pos_ = current_pos();
540 emit PositionChanged();
541 }
542
543 }
544
545 QScreen *OSDPretty::current_screen(const QPoint pos) const {
546
547 QScreen *screen(nullptr);
548 #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
549 screen = QGuiApplication::screenAt(pos);
550 #else
551 Q_UNUSED(pos)
552 if (window() && window()->windowHandle()) screen = window()->windowHandle()->screen();
553 #endif
554 if (!screen) screen = QGuiApplication::primaryScreen();
555
556 return screen;
557
558 }
559
560 QScreen *OSDPretty::current_screen() const { return current_screen(pos()); }
561
562 QPoint OSDPretty::current_pos() const {
563
564 if (current_screen()) {
565 QRect geometry = current_screen()->availableGeometry();
566
567 int x = pos().x() >= geometry.right() - width() ? -1 : pos().x() - geometry.left();
568 int y = pos().y() >= geometry.bottom() - height() ? -1 : pos().y() - geometry.top();
569
570 return QPoint(x, y);
571 }
572
573 return QPoint(0, 0);
574
575 }
576
577 void OSDPretty::set_background_color(const QRgb color) {
578 background_color_ = color;
579 if (isVisible()) update();
580 }
581
582 void OSDPretty::set_background_opacity(const qreal opacity) {
583 background_opacity_ = opacity;
584 if (isVisible()) update();
585 }
586
587 void OSDPretty::set_foreground_color(const QRgb color) {
588
589 foreground_color_ = QColor(color);
590
591 QPalette p;
592 p.setColor(QPalette::WindowText, foreground_color_);
593
594 ui_->summary->setPalette(p);
595 ui_->message->setPalette(p);
596
597 }
598
599 void OSDPretty::set_popup_duration(const int msec) {
600 timeout_->setInterval(msec);
601 }
602
603 void OSDPretty::set_font(const QFont &font) {
604
605 font_ = font;
606
607 // Update the UI
608 ui_->summary->setFont(font);
609 ui_->message->setFont(font);
610 // Now adjust OSD size so everything fits
611 ui_->verticalLayout->activate();
612 resize(sizeHint());
613 // Update the position after font change
614 Reposition();
615
616 }
617