1 /* This file is part of Clementine.
2    Copyright 2010, David Sansome <me@davidsansome.com>
3 
4    Clementine is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8 
9    Clementine is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13 
14    You should have received a copy of the GNU General Public License
15    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
16 */
17 
18 #include "config.h"
19 #include "osdpretty.h"
20 #include "ui_osdpretty.h"
21 
22 #include <QApplication>
23 #include <QBitmap>
24 #include <QColor>
25 #include <QDesktopWidget>
26 #include <QLayout>
27 #include <QMouseEvent>
28 #include <QPainter>
29 #include <QPainterPath>
30 #include <QSettings>
31 #include <QTimer>
32 #include <QTimeLine>
33 
34 #include <QtDebug>
35 
36 #ifdef HAVE_X11
37 #include <QX11Info>
38 #endif
39 #ifdef Q_OS_WIN32
40 # include <QtWin>
41 #endif
42 
43 #ifdef Q_OS_WIN32
44 #include <windows.h>
45 #endif
46 
47 const char* OSDPretty::kSettingsGroup = "OSDPretty";
48 
49 const int OSDPretty::kDropShadowSize = 13;
50 const int OSDPretty::kBorderRadius = 10;
51 const int OSDPretty::kMaxIconSize = 100;
52 
53 const int OSDPretty::kSnapProximity = 20;
54 
55 const QRgb OSDPretty::kPresetBlue = qRgb(102, 150, 227);
56 const QRgb OSDPretty::kPresetOrange = qRgb(254, 156, 67);
57 
OSDPretty(Mode mode,QWidget * parent)58 OSDPretty::OSDPretty(Mode mode, QWidget* parent)
59     : QWidget(parent),
60       ui_(new Ui_OSDPretty),
61       mode_(mode),
62       background_color_(kPresetBlue),
63       background_opacity_(0.85),
64       popup_display_(0),
65       font_(QFont()),
66       disable_duration_(false),
67       timeout_(new QTimer(this)),
68       fading_enabled_(false),
69       fader_(new QTimeLine(300, this)),
70       toggle_mode_(false) {
71   Qt::WindowFlags flags = Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint |
72                           Qt::X11BypassWindowManagerHint;
73 
74   setWindowFlags(flags);
75   setAttribute(Qt::WA_TranslucentBackground, true);
76   setAttribute(Qt::WA_X11NetWmWindowTypeNotification, true);
77   setAttribute(Qt::WA_ShowWithoutActivating, true);
78   ui_->setupUi(this);
79 
80 #ifdef Q_OS_WIN32
81   // Don't show the window in the taskbar.  Qt::ToolTip does this too, but it
82   // adds an extra ugly shadow.
83   int ex_style = GetWindowLong((HWND)winId(), GWL_EXSTYLE);
84   ex_style |= WS_EX_NOACTIVATE;
85   SetWindowLong((HWND)winId(), GWL_EXSTYLE, ex_style);
86 #endif
87 
88   // Mode settings
89   switch (mode_) {
90     case Mode_Popup:
91       setCursor(QCursor(Qt::ArrowCursor));
92       break;
93 
94     case Mode_Draggable:
95       setCursor(QCursor(Qt::OpenHandCursor));
96       break;
97   }
98 
99   // Timeout
100   timeout_->setSingleShot(true);
101   timeout_->setInterval(5000);
102   connect(timeout_, SIGNAL(timeout()), SLOT(hide()));
103 
104   ui_->icon->setMaximumSize(kMaxIconSize, kMaxIconSize);
105 
106   // Fader
107   connect(fader_, SIGNAL(valueChanged(qreal)), SLOT(FaderValueChanged(qreal)));
108   connect(fader_, SIGNAL(finished()), SLOT(FaderFinished()));
109 
110 #ifdef Q_OS_WIN32
111   set_fading_enabled(true);
112 #endif
113 
114   // Load the show edges and corners
115   QImage shadow_edge(":osd_shadow_edge.png");
116   QImage shadow_corner(":osd_shadow_corner.png");
117   for (int i = 0; i < 4; ++i) {
118     QTransform rotation = QTransform().rotate(90 * i);
119     shadow_edge_[i] = QPixmap::fromImage(shadow_edge.transformed(rotation));
120     shadow_corner_[i] = QPixmap::fromImage(shadow_corner.transformed(rotation));
121   }
122   background_ = QPixmap(":osd_background.png");
123 
124   // Set the margins to allow for the drop shadow
125   QBoxLayout* l = static_cast<QBoxLayout*>(layout());
126   int margin = l->margin() + kDropShadowSize;
127   l->setMargin(margin);
128 
129   // Get current screen resolution
130   QRect screenResolution = QApplication::desktop()->screenGeometry();
131   // Leave 200 px for icon
132   ui_->summary->setMaximumWidth(screenResolution.width() - 200);
133   ui_->message->setMaximumWidth(screenResolution.width() - 200);
134   // Set maximum size for the OSD, a little margin here too
135   setMaximumSize(screenResolution.width() - 100,
136                  screenResolution.height() - 100);
137 
138   // Don't load settings here, they will be reloaded anyway on creation
139 }
140 
~OSDPretty()141 OSDPretty::~OSDPretty() { delete ui_; }
142 
IsTransparencyAvailable()143 bool OSDPretty::IsTransparencyAvailable() {
144 #if defined(HAVE_X11) && (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
145   return QX11Info::isCompositingManagerRunning();
146 #endif
147   return true;
148 }
149 
Load()150 void OSDPretty::Load() {
151   QSettings s;
152   s.beginGroup(kSettingsGroup);
153 
154   foreground_color_ = QColor(s.value("foreground_color", 0).toInt());
155   background_color_ = QColor(s.value("background_color", kPresetBlue).toInt());
156   background_opacity_ = s.value("background_opacity", 0.85).toDouble();
157   popup_display_ = s.value("popup_display", -1).toInt();
158   popup_pos_ = s.value("popup_pos", QPoint(0, 0)).toPoint();
159   font_.fromString(s.value("font", "Verdana,9,-1,5,50,0,0,0,0,0").toString());
160   disable_duration_ = s.value("disable_duration", false).toBool();
161 
162   set_font(font());
163   set_foreground_color(foreground_color());
164 }
165 
ReloadSettings()166 void OSDPretty::ReloadSettings() {
167   Load();
168   if (isVisible()) update();
169 }
170 
BoxBorder() const171 QRect OSDPretty::BoxBorder() const {
172   return rect().adjusted(kDropShadowSize, kDropShadowSize, -kDropShadowSize,
173                          -kDropShadowSize);
174 }
175 
paintEvent(QPaintEvent *)176 void OSDPretty::paintEvent(QPaintEvent*) {
177   QPainter p(this);
178   p.setRenderHint(QPainter::Antialiasing);
179   p.setRenderHint(QPainter::HighQualityAntialiasing);
180 
181   QRect box(BoxBorder());
182 
183   // Shadow corners
184   const int kShadowCornerSize = kDropShadowSize + kBorderRadius;
185   p.drawPixmap(0, 0, shadow_corner_[0]);
186   p.drawPixmap(width() - kShadowCornerSize, 0, shadow_corner_[1]);
187   p.drawPixmap(width() - kShadowCornerSize, height() - kShadowCornerSize,
188                shadow_corner_[2]);
189   p.drawPixmap(0, height() - kShadowCornerSize, shadow_corner_[3]);
190 
191   // Shadow edges
192   p.drawTiledPixmap(kShadowCornerSize, 0, width() - kShadowCornerSize * 2,
193                     kDropShadowSize, shadow_edge_[0]);
194   p.drawTiledPixmap(width() - kDropShadowSize, kShadowCornerSize,
195                     kDropShadowSize, height() - kShadowCornerSize * 2,
196                     shadow_edge_[1]);
197   p.drawTiledPixmap(kShadowCornerSize, height() - kDropShadowSize,
198                     width() - kShadowCornerSize * 2, kDropShadowSize,
199                     shadow_edge_[2]);
200   p.drawTiledPixmap(0, kShadowCornerSize, kDropShadowSize,
201                     height() - kShadowCornerSize * 2, shadow_edge_[3]);
202 
203   // Box background
204   p.setBrush(background_color_);
205   p.setPen(QPen());
206   p.setOpacity(background_opacity_);
207   p.drawRoundedRect(box, kBorderRadius, kBorderRadius);
208 
209   // Background pattern
210   QPainterPath background_path;
211   background_path.addRoundedRect(box, kBorderRadius, kBorderRadius);
212   p.setClipPath(background_path);
213   p.setOpacity(1.0);
214   p.drawPixmap(box.right() - background_.width(),
215                box.bottom() - background_.height(), background_);
216   p.setClipping(false);
217 
218   // Gradient overlay
219   QLinearGradient gradient(0, 0, 0, height());
220   gradient.setColorAt(0, QColor(255, 255, 255, 130));
221   gradient.setColorAt(1, QColor(255, 255, 255, 50));
222   p.setBrush(gradient);
223   p.drawRoundedRect(box, kBorderRadius, kBorderRadius);
224 
225   // Box border
226   p.setBrush(QBrush());
227   p.setPen(QPen(background_color_.darker(150), 2));
228   p.drawRoundedRect(box, kBorderRadius, kBorderRadius);
229 }
230 
SetMessage(const QString & summary,const QString & message,const QImage & image)231 void OSDPretty::SetMessage(const QString& summary, const QString& message,
232                            const QImage& image) {
233 
234   if (!image.isNull()) {
235     QImage scaled_image =
236         image.scaled(kMaxIconSize, kMaxIconSize, Qt::KeepAspectRatio,
237                      Qt::SmoothTransformation);
238     ui_->icon->setPixmap(QPixmap::fromImage(scaled_image));
239     ui_->icon->show();
240   } else {
241     ui_->icon->hide();
242   }
243 
244   ui_->summary->setText(summary);
245   ui_->message->setText(message);
246 
247   if (isVisible()) Reposition();
248 }
249 
250 // Set the desired message and then show the OSD
ShowMessage(const QString & summary,const QString & message,const QImage & image)251 void OSDPretty::ShowMessage(const QString& summary, const QString& message,
252                             const QImage& image) {
253   SetMessage(summary, message, image);
254 
255   if (isVisible() && mode_ == Mode_Popup) {
256     // The OSD is already visible, toggle or restart the timer
257     if (toggle_mode()) {
258       set_toggle_mode(false);
259       // If timeout is disabled, timer hadn't been started
260       if (!disable_duration()) timeout_->stop();
261       hide();
262     } else {
263       if (!disable_duration()) timeout_->start();  // Restart the timer
264     }
265   } else {
266     if (toggle_mode()) set_toggle_mode(false);
267     // The OSD is not visible, show it
268     show();
269   }
270 }
271 
showEvent(QShowEvent * e)272 void OSDPretty::showEvent(QShowEvent* e) {
273   setWindowOpacity(fading_enabled_ ? 0.0 : 1.0);
274 
275   QWidget::showEvent(e);
276 
277   Reposition();
278 
279   if (fading_enabled_) {
280     fader_->setDirection(QTimeLine::Forward);
281     fader_->start();  // Timeout will be started in FaderFinished
282   } else if (mode_ == Mode_Popup) {
283     if (!disable_duration()) timeout_->start();
284     // Ensures it is above when showing the preview
285     raise();
286   }
287 }
288 
setVisible(bool visible)289 void OSDPretty::setVisible(bool visible) {
290   if (!visible && fading_enabled_ &&
291       fader_->direction() == QTimeLine::Forward) {
292     fader_->setDirection(QTimeLine::Backward);
293     fader_->start();
294   } else {
295     QWidget::setVisible(visible);
296   }
297 }
298 
FaderFinished()299 void OSDPretty::FaderFinished() {
300   if (fader_->direction() == QTimeLine::Backward)
301     hide();
302   else if (mode_ == Mode_Popup && !disable_duration())
303     timeout_->start();
304 }
305 
FaderValueChanged(qreal value)306 void OSDPretty::FaderValueChanged(qreal value) { setWindowOpacity(value); }
307 
Reposition()308 void OSDPretty::Reposition() {
309   QDesktopWidget* desktop = QApplication::desktop();
310 
311   // Make the OSD the proper size
312   layout()->activate();
313   resize(sizeHint());
314 
315   // Work out where to place the OSD.  -1 for x or y means "on the right or
316   // bottom edge".
317   QRect geometry(desktop->availableGeometry(popup_display_));
318 
319   int x = popup_pos_.x() < 0 ? geometry.right() - width()
320                              : geometry.left() + popup_pos_.x();
321   int y = popup_pos_.y() < 0 ? geometry.bottom() - height()
322                              : geometry.top() + popup_pos_.y();
323 
324 #ifndef Q_OS_WIN32
325   // windows needs negative coordinates for monitors
326   // to the left or above the primary
327   x = qBound(0, x, geometry.right() - width());
328   y = qBound(0, y, geometry.bottom() - height());
329 #endif
330 
331   move(x, y);
332 
333   // Create a mask for the actual area of the OSD
334   QBitmap mask(size());
335   mask.clear();
336 
337   QPainter p(&mask);
338   p.setBrush(Qt::color1);
339   p.drawRoundedRect(BoxBorder().adjusted(-1, -1, 0, 0), kBorderRadius,
340                     kBorderRadius);
341   p.end();
342 
343   // If there's no compositing window manager running then we have to set an
344   // XShape mask.
345   if (IsTransparencyAvailable())
346     clearMask();
347   else {
348     setMask(mask);
349   }
350 
351 #ifdef Q_OS_WIN32
352   // On windows, enable blurbehind on the masked area
353   QtWin::enableBlurBehindWindow(this, QRegion(mask));
354 #endif
355 }
356 
enterEvent(QEvent *)357 void OSDPretty::enterEvent(QEvent*) {
358   if (mode_ == Mode_Popup) setWindowOpacity(0.25);
359 }
360 
leaveEvent(QEvent *)361 void OSDPretty::leaveEvent(QEvent*) { setWindowOpacity(1.0); }
362 
mousePressEvent(QMouseEvent * e)363 void OSDPretty::mousePressEvent(QMouseEvent* e) {
364   if (mode_ == Mode_Popup)
365     hide();
366   else {
367     original_window_pos_ = pos();
368     drag_start_pos_ = e->globalPos();
369   }
370 }
371 
mouseMoveEvent(QMouseEvent * e)372 void OSDPretty::mouseMoveEvent(QMouseEvent* e) {
373   if (mode_ == Mode_Draggable) {
374     QPoint delta = e->globalPos() - drag_start_pos_;
375     QPoint new_pos = original_window_pos_ + delta;
376 
377     // Keep it to the bounds of the desktop
378     QDesktopWidget* desktop = QApplication::desktop();
379     QRect geometry(desktop->availableGeometry(e->globalPos()));
380 
381     new_pos.setX(
382         qBound(geometry.left(), new_pos.x(), geometry.right() - width()));
383     new_pos.setY(
384         qBound(geometry.top(), new_pos.y(), geometry.bottom() - height()));
385 
386     // Snap to center
387     int snap_x = geometry.center().x() - width() / 2;
388     if (new_pos.x() > snap_x - kSnapProximity &&
389         new_pos.x() < snap_x + kSnapProximity) {
390       new_pos.setX(snap_x);
391     }
392 
393     move(new_pos);
394 
395     popup_display_ = current_display();
396     popup_pos_ = current_pos();
397   }
398 }
399 
current_pos() const400 QPoint OSDPretty::current_pos() const {
401   QDesktopWidget* desktop = QApplication::desktop();
402   QRect geometry(desktop->availableGeometry(current_display()));
403 
404   int x = pos().x() >= geometry.right() - width() ? -1
405                                                   : pos().x() - geometry.left();
406   int y = pos().y() >= geometry.bottom() - height() ? -1 : pos().y() -
407                                                                geometry.top();
408 
409   return QPoint(x, y);
410 }
411 
current_display() const412 int OSDPretty::current_display() const {
413   QDesktopWidget* desktop = QApplication::desktop();
414   return desktop->screenNumber(pos());
415 }
416 
set_background_color(QRgb color)417 void OSDPretty::set_background_color(QRgb color) {
418   background_color_ = color;
419   if (isVisible()) update();
420 }
421 
set_background_opacity(qreal opacity)422 void OSDPretty::set_background_opacity(qreal opacity) {
423   background_opacity_ = opacity;
424   if (isVisible()) update();
425 }
426 
set_foreground_color(QRgb color)427 void OSDPretty::set_foreground_color(QRgb color) {
428   foreground_color_ = QColor(color);
429 
430   QPalette p;
431   p.setColor(QPalette::WindowText, foreground_color_);
432 
433   ui_->summary->setPalette(p);
434   ui_->message->setPalette(p);
435 }
436 
set_popup_duration(int msec)437 void OSDPretty::set_popup_duration(int msec) { timeout_->setInterval(msec); }
438 
mouseReleaseEvent(QMouseEvent *)439 void OSDPretty::mouseReleaseEvent(QMouseEvent*) {
440   if (mode_ == Mode_Draggable) {
441     popup_display_ = current_display();
442     popup_pos_ = current_pos();
443   }
444 }
445 
set_font(QFont font)446 void OSDPretty::set_font(QFont font) {
447   font_ = font;
448 
449   // Update the UI
450   ui_->summary->setFont(font);
451   ui_->message->setFont(font);
452   // Now adjust OSD size so everything fits
453   ui_->verticalLayout->activate();
454   resize(sizeHint());
455   // Update the position after font change
456   Reposition();
457 }
458