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