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 "core/application.h"
20 #include "core/logging.h"
21 #include "covers/currentartloader.h"
22 #include "osd.h"
23 #include "osdpretty.h"
24 #include "ui/iconloader.h"
25 #include "ui/systemtrayicon.h"
26 
27 #ifdef HAVE_DBUS
28 #include "dbus/notification.h"
29 #endif
30 
31 #include <QCoreApplication>
32 #include <QtDebug>
33 #include <QSettings>
34 
35 const char* OSD::kSettingsGroup = "OSD";
36 
OSD(SystemTrayIcon * tray_icon,Application * app,QObject * parent)37 OSD::OSD(SystemTrayIcon* tray_icon, Application* app, QObject* parent)
38     : QObject(parent),
39       tray_icon_(tray_icon),
40       app_(app),
41       timeout_msec_(5000),
42       behaviour_(Native),
43       show_on_volume_change_(false),
44       show_art_(true),
45       show_on_play_mode_change_(true),
46       show_on_pause_(true),
47       use_custom_text_(false),
48       custom_text1_(QString()),
49       custom_text2_(QString()),
50       preview_mode_(false),
51       force_show_next_(false),
52       ignore_next_stopped_(false),
53       pretty_popup_(new OSDPretty(OSDPretty::Mode_Popup)) {
54   connect(app_->current_art_loader(),
55           SIGNAL(ThumbnailLoaded(Song, QString, QImage)),
56           SLOT(AlbumArtLoaded(Song, QString, QImage)));
57 
58   ReloadSettings();
59   Init();
60 }
61 
~OSD()62 OSD::~OSD() { delete pretty_popup_; }
63 
ReloadSettings()64 void OSD::ReloadSettings() {
65   QSettings s;
66   s.beginGroup(kSettingsGroup);
67   behaviour_ = OSD::Behaviour(s.value("Behaviour", Native).toInt());
68   timeout_msec_ = s.value("Timeout", 5000).toInt();
69   show_on_volume_change_ = s.value("ShowOnVolumeChange", false).toBool();
70   show_art_ = s.value("ShowArt", true).toBool();
71   show_on_play_mode_change_ = s.value("ShowOnPlayModeChange", true).toBool();
72   show_on_pause_ = s.value("ShowOnPausePlayback", true).toBool();
73   use_custom_text_ = s.value(("CustomTextEnabled"), false).toBool();
74   custom_text1_ = s.value("CustomText1").toString();
75   custom_text2_ = s.value("CustomText2").toString();
76 
77   if (!SupportsNativeNotifications() && behaviour_ == Native)
78     behaviour_ = Pretty;
79   if (!SupportsTrayPopups() && behaviour_ == TrayPopup) behaviour_ = Disabled;
80 
81   ReloadPrettyOSDSettings();
82 }
83 
84 // Reload just Pretty OSD settings, not everything
ReloadPrettyOSDSettings()85 void OSD::ReloadPrettyOSDSettings() {
86   pretty_popup_->set_popup_duration(timeout_msec_);
87   pretty_popup_->ReloadSettings();
88 }
89 
ReshowCurrentSong()90 void OSD::ReshowCurrentSong() {
91   force_show_next_ = true;
92   AlbumArtLoaded(last_song_, last_image_uri_, last_image_);
93 }
94 
AlbumArtLoaded(const Song & song,const QString & uri,const QImage & image)95 void OSD::AlbumArtLoaded(const Song& song, const QString& uri,
96                          const QImage& image) {
97   // Don't change tray icon details if it's a preview
98   if (!preview_mode_ && tray_icon_) {
99     tray_icon_->SetNowPlaying(song, uri);
100   }
101 
102   last_song_ = song;
103   last_image_ = image;
104   last_image_uri_ = uri;
105 
106   QStringList message_parts;
107   QString summary;
108   if (!use_custom_text_) {
109     summary = song.PrettyTitle();
110     if (!song.artist().isEmpty())
111       summary = QString("%1 - %2").arg(song.artist(), summary);
112     if (!song.album().isEmpty()) message_parts << song.album();
113     if (song.disc() > 0) message_parts << tr("disc %1").arg(song.disc());
114     if (song.track() > 0) message_parts << tr("track %1").arg(song.track());
115   } else {
116     QRegExp variable_replacer("[%][a-z]+[%]");
117     summary = custom_text1_;
118     QString message(custom_text2_);
119 
120     // Replace the first line
121     int pos = 0;
122     variable_replacer.indexIn(custom_text1_);
123     while ((pos = variable_replacer.indexIn(custom_text1_, pos)) != -1) {
124       QStringList captured = variable_replacer.capturedTexts();
125       summary.replace(captured[0], ReplaceVariable(captured[0], song));
126       pos += variable_replacer.matchedLength();
127     }
128 
129     // Replace the second line
130     pos = 0;
131     variable_replacer.indexIn(custom_text2_);
132     while ((pos = variable_replacer.indexIn(custom_text2_, pos)) != -1) {
133       QStringList captured = variable_replacer.capturedTexts();
134       message.replace(captured[0], ReplaceVariable(captured[0], song));
135       pos += variable_replacer.matchedLength();
136     }
137     message_parts << message;
138   }
139 
140   if (show_art_) {
141     ShowMessage(summary, message_parts.join(", "), "notification-audio-play",
142                 image);
143   } else {
144     ShowMessage(summary, message_parts.join(", "), "notification-audio-play",
145                 QImage());
146   }
147 
148   // Reload the saved settings if they were changed for preview
149   if (preview_mode_) {
150     ReloadSettings();
151     preview_mode_ = false;
152   }
153 }
154 
Paused()155 void OSD::Paused() {
156   if (show_on_pause_) {
157     ShowMessage(QCoreApplication::applicationName(), tr("Paused"));
158   }
159 }
160 
Stopped()161 void OSD::Stopped() {
162   if (tray_icon_) tray_icon_->ClearNowPlaying();
163   if (ignore_next_stopped_) {
164     ignore_next_stopped_ = false;
165     return;
166   }
167 
168   ShowMessage(QCoreApplication::applicationName(), tr("Stopped"));
169 }
170 
StopAfterToggle(bool stop)171 void OSD::StopAfterToggle(bool stop) {
172   ShowMessage(
173       QCoreApplication::applicationName(),
174       tr("Stop playing after track: %1").arg(stop ? tr("On") : tr("Off")));
175 }
176 
PlaylistFinished()177 void OSD::PlaylistFinished() {
178   // We get a PlaylistFinished followed by a Stopped from the player
179   ignore_next_stopped_ = true;
180 
181   ShowMessage(QCoreApplication::applicationName(), tr("Playlist finished"));
182 }
183 
VolumeChanged(int value)184 void OSD::VolumeChanged(int value) {
185   if (!show_on_volume_change_) return;
186 
187   ShowMessage(QCoreApplication::applicationName(), tr("Volume %1%").arg(value));
188 }
189 
MagnatuneDownloadFinished(const QStringList & albums)190 void OSD::MagnatuneDownloadFinished(const QStringList& albums) {
191   QString message;
192   if (albums.count() == 1)
193     message = albums[0];
194   else
195     message = tr("%1 albums").arg(albums.count());
196 
197   ShowMessage(tr("Magnatune download finished"), message, QString(),
198               QImage(IconLoader::Load("magnatune",
199                      IconLoader::Provider).pixmap(16).toImage()));
200 }
201 
ShowMessage(const QString & summary,const QString & message,const QString & icon,const QImage & image)202 void OSD::ShowMessage(const QString& summary, const QString& message,
203                       const QString& icon, const QImage& image) {
204   if (pretty_popup_->toggle_mode()) {
205     pretty_popup_->ShowMessage(summary, message, image);
206   } else {
207     switch (behaviour_) {
208       case Native:
209         if (image.isNull()) {
210           ShowMessageNative(summary, message, icon, QImage());
211         } else {
212           ShowMessageNative(summary, message, QString(), image);
213         }
214         break;
215 
216 #ifndef Q_OS_DARWIN
217       case TrayPopup:
218         if (tray_icon_) tray_icon_->ShowPopup(summary, message, timeout_msec_);
219         break;
220 #endif
221 
222       case Disabled:
223         if (!force_show_next_) break;
224         force_show_next_ = false;
225       // fallthrough
226       case Pretty:
227         pretty_popup_->ShowMessage(summary, message, image);
228         break;
229 
230       default:
231         break;
232     }
233   }
234 }
235 
236 #if !defined(HAVE_X11) && defined(HAVE_DBUS)
CallFinished(QDBusPendingCallWatcher *)237 void OSD::CallFinished(QDBusPendingCallWatcher*) {}
238 #endif
239 
WiiremoteActived(int id)240 void OSD::WiiremoteActived(int id) {
241   ShowMessage(QString(tr("%1: Wiimotedev module"))
242                   .arg(QCoreApplication::applicationName()),
243               tr("Wii Remote %1: activated").arg(QString::number(id)));
244 }
245 
WiiremoteDeactived(int id)246 void OSD::WiiremoteDeactived(int id) {
247   ShowMessage(QString(tr("%1: Wiimotedev module"))
248                   .arg(QCoreApplication::applicationName()),
249               tr("Wii Remote %1: disactived").arg(QString::number(id)));
250 }
251 
WiiremoteConnected(int id)252 void OSD::WiiremoteConnected(int id) {
253   ShowMessage(QString(tr("%1: Wiimotedev module"))
254                   .arg(QCoreApplication::applicationName()),
255               tr("Wii Remote %1: connected").arg(QString::number(id)));
256 }
257 
WiiremoteDisconnected(int id)258 void OSD::WiiremoteDisconnected(int id) {
259   ShowMessage(QString(tr("%1: Wiimotedev module"))
260                   .arg(QCoreApplication::applicationName()),
261               tr("Wii Remote %1: disconnected").arg(QString::number(id)));
262 }
263 
WiiremoteLowBattery(int id,int live)264 void OSD::WiiremoteLowBattery(int id, int live) {
265   ShowMessage(QString(tr("%1: Wiimotedev module"))
266                   .arg(QCoreApplication::applicationName()),
267               tr("Wii Remote %1: low battery (%2%)")
268                   .arg(QString::number(id), QString::number(live)));
269 }
270 
WiiremoteCriticalBattery(int id,int live)271 void OSD::WiiremoteCriticalBattery(int id, int live) {
272   ShowMessage(QString(tr("%1: Wiimotedev module"))
273                   .arg(QCoreApplication::applicationName()),
274               tr("Wii Remote %1: critical battery (%2%) ")
275                   .arg(QString::number(id), QString::number(live)));
276 }
277 
ShuffleModeChanged(PlaylistSequence::ShuffleMode mode)278 void OSD::ShuffleModeChanged(PlaylistSequence::ShuffleMode mode) {
279   if (show_on_play_mode_change_) {
280     QString current_mode = QString();
281     switch (mode) {
282       case PlaylistSequence::Shuffle_Off:
283         current_mode = tr("Don't shuffle");
284         break;
285       case PlaylistSequence::Shuffle_All:
286         current_mode = tr("Shuffle all");
287         break;
288       case PlaylistSequence::Shuffle_InsideAlbum:
289         current_mode = tr("Shuffle tracks in this album");
290         break;
291       case PlaylistSequence::Shuffle_Albums:
292         current_mode = tr("Shuffle albums");
293         break;
294     }
295     ShowMessage(QCoreApplication::applicationName(), current_mode);
296   }
297 }
298 
RepeatModeChanged(PlaylistSequence::RepeatMode mode)299 void OSD::RepeatModeChanged(PlaylistSequence::RepeatMode mode) {
300   if (show_on_play_mode_change_) {
301     QString current_mode = QString();
302     switch (mode) {
303       case PlaylistSequence::Repeat_Off:
304         current_mode = tr("Don't repeat");
305         break;
306       case PlaylistSequence::Repeat_Track:
307         current_mode = tr("Repeat track");
308         break;
309       case PlaylistSequence::Repeat_Album:
310         current_mode = tr("Repeat album");
311         break;
312       case PlaylistSequence::Repeat_Playlist:
313         current_mode = tr("Repeat playlist");
314         break;
315       case PlaylistSequence::Repeat_OneByOne:
316         current_mode = tr("Stop after every track");
317         break;
318       case PlaylistSequence::Repeat_Intro:
319         current_mode = tr("Intro tracks");
320         break;
321     }
322     ShowMessage(QCoreApplication::applicationName(), current_mode);
323   }
324 }
325 
ReplaceVariable(const QString & variable,const Song & song)326 QString OSD::ReplaceVariable(const QString& variable, const Song& song) {
327   QString return_value;
328   if (variable == "%artist%") {
329     return song.artist();
330   } else if (variable == "%album%") {
331     return song.album();
332   } else if (variable == "%title%") {
333     return song.PrettyTitle();
334   } else if (variable == "%albumartist%") {
335     return song.effective_albumartist();
336   } else if (variable == "%year%") {
337     return song.PrettyYear();
338   } else if (variable == "%composer%") {
339     return song.composer();
340   } else if (variable == "%performer%") {
341     return song.performer();
342   } else if (variable == "%grouping%") {
343     return song.grouping();
344   } else if (variable == "%lyrics%") {
345     return song.lyrics();
346   } else if (variable == "%length%") {
347     return song.PrettyLength();
348   } else if (variable == "%disc%") {
349     return return_value.setNum(song.disc());
350   } else if (variable == "%track%") {
351     return return_value.setNum(song.track());
352   } else if (variable == "%genre%") {
353     return song.genre();
354   } else if (variable == "%playcount%") {
355     return return_value.setNum(song.playcount());
356   } else if (variable == "%skipcount%") {
357     return return_value.setNum(song.skipcount());
358   } else if (variable == "%filename%") {
359     return song.basefilename();
360   } else if (variable == "%rating%") {
361     return song.PrettyRating();
362   } else if (variable == "%score%") {
363     return QString::number(song.score());
364   } else if (variable == "%newline%") {
365     // We need different strings depending on notification type
366     switch (behaviour_) {
367       case Native:
368 #ifdef Q_OS_DARWIN
369         return "\n";
370 #endif
371 #ifdef Q_OS_LINUX
372         return "<br/>";
373 #endif
374 #ifdef Q_OS_WIN32
375         // Other OS don't support native notifications
376         qLog(Debug)
377             << "New line not supported by this notification type under Windows";
378         return "";
379 #endif
380       case TrayPopup:
381         qLog(Debug) << "New line not supported by this notification type";
382         return "";
383       case Pretty:
384       default:
385         // When notifications are disabled, we force the PrettyOSD
386         return "<br/>";
387     }
388   }
389 
390   // if the variable is not recognized, just return it
391   return variable;
392 }
393 
ShowPreview(const Behaviour type,const QString & line1,const QString & line2,const Song & song)394 void OSD::ShowPreview(const Behaviour type, const QString& line1,
395                       const QString& line2, const Song& song) {
396   behaviour_ = type;
397   custom_text1_ = line1;
398   custom_text2_ = line2;
399   if (!use_custom_text_) use_custom_text_ = true;
400 
401   // We want to reload the settings, but we can't do this here because the cover
402   // art loading is asynch
403   preview_mode_ = true;
404   AlbumArtLoaded(song, QString(), QImage());
405 }
406 
SetPrettyOSDToggleMode(bool toggle)407 void OSD::SetPrettyOSDToggleMode(bool toggle) {
408   pretty_popup_->set_toggle_mode(toggle);
409 }
410