1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 # trayicon.py -- Tray icon and notification support
22 # Jérôme Chabod (JCH) <jerome.chabod@ifrance.com> 2007-12-20
29 from gpodder
.liblogger
import log
30 from gpodder
.libgpodder
import gl
31 from gpodder
.libpodcasts
import podcastItem
37 log('Cannot find pynotify. Please install the python-notify package.')
38 log('Notification bubbles have been disabled.')
41 from gpodder
import services
42 from gpodder
import util
43 from gpodder
import draw
45 if gpodder
.interface
== gpodder
.MAEMO
:
48 class GPodderStatusIcon(gtk
.StatusIcon
):
49 """ this class display a status icon in the system tray
50 this icon serves to show or hide gPodder, notify dowload status
51 and provide a popupmenu for quick acces to some
52 gPodder functionalities
54 author: Jérôme Chabod <jerome.chabod at ifrance.com>
57 DEFAULT_TOOLTIP
= _('gPodder media aggregator')
59 # status: they are displayed as tooltip and add a small icon to the main icon
60 STATUS_DOWNLOAD_IN_PROGRESS
= (_('Downloading episodes'), gtk
.STOCK_GO_DOWN
)
61 STATUS_UPDATING_FEED_CACHE
= (_('Looking for new episodes'), gtk
.STOCK_REFRESH
)
62 STATUS_SYNCHRONIZING
= (_('Synchronizing to player'), 'multimedia-player')
63 STATUS_DELETING
= (_('Cleaning files'), gtk
.STOCK_DELETE
)
65 # actions: buttons within the notify bubble
66 ACTION_SHOW
= ('show', _('Show'))
67 ACTION_QUIT
= ('quit', _('Quit gPodder'))
68 ACTION_FORCE_EXIT
= ('force_quit', _('Quit anyway'))
69 ACTION_KEEP_DOWLOADING
= ('keep_dowloading', _('Keep dowloading'))
70 ACTION_START_DOWNLOAD
= ('download', _('Download'))
72 def __init__(self
, gp
, icon_filename
):
73 gtk
.StatusIcon
.__init
__(self
)
74 log('Creating tray icon', sender
=self
)
77 self
.__finished
_downloads
= []
78 self
.__icon
_cache
= {}
79 self
.__icon
_filename
= icon_filename
80 self
.__current
_icon
= -1
81 self
.__is
_downloading
= False
82 self
.__synchronisation
_device
= None
83 self
.__download
_start
_time
= None
84 self
.__sync
_progress
= ''
86 self
.__previous
_notification
= []
88 # try getting the icon
90 if gpodder
.interface
== gpodder
.GUI
:
91 self
.__icon
= gtk
.gdk
.pixbuf_new_from_file(self
.__icon
_filename
)
92 elif gpodder
.interface
== gpodder
.MAEMO
:
93 self
.__icon
= gtk
.gdk
.pixbuf_new_from_file_at_size(self
.__icon
_filename
, 36, 36)
94 except Exception, exc
:
95 log('Warning: Cannot load gPodder icon, will use the default icon (%s)', exc
, sender
=self
)
96 self
.__icon
= gtk
.icon_theme_get_default().load_icon(gtk
.STOCK_DIALOG_QUESTION
, 30, 30)
98 # Reset trayicon (default icon, default tooltip)
99 self
.__current
_pixbuf
= None
100 self
.__last
_ratio
= 1.0
103 menu
= self
.__create
_context
_menu
()
104 if gpodder
.interface
== gpodder
.GUI
:
105 self
.connect('activate', self
.__on
_left
_click
)
106 self
.connect('popup-menu', self
.__on
_right
_click
, menu
)
107 elif gpodder
.interface
== gpodder
.MAEMO
:
108 # On Maemo, we show the popup menu on left-click
109 self
.connect('activate', self
.__on
_right
_click
, menu
)
111 self
.set_visible(True)
113 # initialise pynotify
115 if not pynotify
.init('gPodder'):
116 log('Error: unable to initialise pynotify', sender
=self
)
118 # Register with the download status manager
119 dl_man
= services
.download_status_manager
120 dl_man
.register('progress-changed', self
.__on
_download
_progress
_changed
)
121 dl_man
.register('download-complete', self
.__on
_download
_complete
)
123 def __create_context_menu(self
):
124 # build and connect the popup menu
126 menuItem
= gtk
.ImageMenuItem(_("Check for new episodes"))
127 menuItem
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_FIND
, gtk
.ICON_SIZE_MENU
))
128 # connect the "on_itemUpdate_activate" with the parameter notify_no_new_episodes set to True
129 menuItem
.connect('activate', self
.__gpodder
.on_itemUpdate_activate
, True)
130 menu
.append(menuItem
)
132 menuItem
= gtk
.ImageMenuItem(_("Download all new episodes"))
133 menuItem
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
134 menuItem
.connect('activate', self
.__gpodder
.on_itemDownloadAllNew_activate
)
135 menu
.append(menuItem
)
137 # menus's label will adapt to the synchronisation device name
138 if gl
.config
.device_type
!= 'none':
139 sync_label
= _('Synchronize to %s') % (gl
.get_device_name(),)
140 menuItem
= gtk
.ImageMenuItem(sync_label
)
141 menuItem
.set_sensitive(gl
.config
.device_type
!= 'none')
142 menuItem
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
143 menuItem
.connect('activate', self
.__gpodder
.on_sync_to_ipod_activate
)
144 menu
.append(menuItem
)
145 menu
.append( gtk
.SeparatorMenuItem())
147 self
.menuItem_previous_msg
= gtk
.ImageMenuItem(_('Show previous message again'))
148 self
.menuItem_previous_msg
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
149 self
.menuItem_previous_msg
.connect('activate', self
.__on
_show
_previous
_message
_callback
)
150 self
.menuItem_previous_msg
.set_sensitive(False)
151 menu
.append(self
.menuItem_previous_msg
)
153 menu
.append( gtk
.SeparatorMenuItem())
155 menuItem
= gtk
.ImageMenuItem(gtk
.STOCK_PREFERENCES
)
156 menuItem
.connect('activate', self
.__gpodder
.on_itemPreferences_activate
)
157 menu
.append(menuItem
)
159 menuItem
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
160 menuItem
.connect('activate', self
.__gpodder
.on_itemAbout_activate
)
161 menu
.append(menuItem
)
162 menu
.append( gtk
.SeparatorMenuItem())
164 if gpodder
.interface
== gpodder
.MAEMO
:
165 # On Maemo, we map the left-click to the popup-menu,
166 # so add a menu item to do the left-click action
167 menuItem
= gtk
.ImageMenuItem(_('Hide gPodder'))
168 self
.item_showhide
= menuItem
170 def show_hide_gpodder_maemo(menu_item
):
171 visible
= self
.__gpodder
.gPodder
.get_property('visible')
172 (label
, image
) = menu_item
.get_children()
174 label
.set_text(_('Show gPodder'))
175 menu_item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_UP
, gtk
.ICON_SIZE_MENU
))
177 label
.set_text(_('Hide gPodder'))
178 menu_item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
179 self
.__gpodder
.gPodder
.set_property('visible', not visible
)
181 menuItem
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
182 menuItem
.connect('activate', lambda widget
: show_hide_gpodder_maemo(self
.item_showhide
))
183 menu
.append(menuItem
)
185 menuItem
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
186 menuItem
.connect('activate', self
.__on
_exit
_callback
)
187 menu
.append(menuItem
)
191 def __on_exit_callback(self
, widget
, *args
):
192 if self
.__is
_downloading
and self
.__is
_notification
_on
():
193 self
.send_notification(_("gPodder is downloading episodes\ndo you want to exit anyway?"""), "gPodder
",[self.ACTION_FORCE_EXIT, self.ACTION_KEEP_DOWLOADING])
195 self.__gpodder.close_gpodder()
197 def __on_show_previous_message_callback(self, widget, *args):
198 p = self.__previous_notification
200 self.send_notification(p[0], p[1], p[2], p[3])
202 def __on_right_click(self, widget, button=None, time=0, data=None):
203 """Open popup menu on right-click
205 if gpodder.interface == gpodder.MAEMO and data is None and button is not None:
206 # The left-click action has a different function
207 # signature, so we have to swap parameters here
211 data.popup(None, None, None, 3, time)
213 def __on_left_click(self, widget, data=None):
214 """Hide/unhide gPodder on left-click
216 if self.__gpodder.minimized:
217 self.__gpodder.uniconify_main_window()
219 if not self.__gpodder.gpodder_main_window.is_active():
220 self.__gpodder.gpodder_main_window.present()
222 self.__gpodder.iconify_main_window()
224 def __on_download_complete(self, episode):
225 """Remember finished downloads
227 self.__finished_downloads.append(episode)
229 def __on_download_progress_changed( self, count, percentage):
230 """ callback by download manager during dowloading.
231 It updates the tooltip with information on how many
232 files are dowloaded and the percentage of dowload
237 self.__is_downloading = True
238 if not self.__download_start_time:
239 self.__download_start_time = datetime.datetime.now()
241 tooltip.append(_('downloading one episode'))
243 tooltip.append(_('downloading %d episodes')%count)
245 tooltip.append(' (%d%%) :'%percentage)
248 for episode in self.__finished_downloads:
249 downloading.append(_("%s (completed
)") % episode)
250 for status in services.download_status_manager.status_list.values():
251 downloading.append(status['thread'].episode.title + " (%d%% - %s)" % (status['progress'], status['speed']))
252 tooltip.append(self.format_episode_list(downloading))
255 date_diff = datetime.datetime.now() - self.__download_start_time
256 estim = date_diff.seconds * 100 // percentage - date_diff.seconds
257 tooltip.append('\n' + _('Estimated remaining time: '))
258 tooltip.append(util.format_seconds_to_hour_min_sec(estim))
260 self.set_status(self.STATUS_DOWNLOAD_IN_PROGRESS, ''.join(tooltip))
262 self.progress_bar(float(percentage)/100.)
264 self.__is_downloading = False
265 self.__download_start_time = None
267 num = len(self.__finished_downloads)
269 title = _('one episodes downloaded:')
271 title = _('%d episodes downloaded:')%num
273 # No episodes have finished downloading, ignore
276 message = self.format_episode_list(self.__finished_downloads, title)
277 self.send_notification(message, _('gPodder downloads finished'))
279 self.__finished_downloads = []
281 def __get_status_icon(self, icon):
282 if icon in self.__icon_cache:
283 return self.__icon_cache[icon]
286 new_icon = self.__icon.copy()
287 emblem = gtk.icon_theme_get_default().load_icon(icon, int(new_icon.get_width()/1.5), 0)
288 (width, height) = (emblem.get_width(), emblem.get_height())
289 xpos = new_icon.get_width()-width
290 ypos = new_icon.get_height()-height
291 emblem.composite(new_icon, xpos, ypos, width, height, xpos, ypos, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
292 self.__icon_cache[icon] = new_icon
294 except Exception, exc:
297 log('Warning: Cannot create status icon: %s', icon, sender=self)
300 def __action_callback(self, n, action):
301 """ call back when a button is clicked in a notify bubble """
302 log("action triggered
%s", action, sender = self)
305 self.__gpodder.uniconify_main_window()
307 self.__gpodder.close_gpodder()
308 elif action=='keep_dowloading':
310 elif action=='force_quit':
311 self.__gpodder.close_gpodder()
312 elif action=='download':
313 self.__gpodder.on_itemDownloadAllNew_activate(self.__gpodder)
315 log("don
't know what to do with action %s" % action, sender = self)
318 def __is_notification_on(self):
319 # tray icon not visible or notifications disabled
320 if not self.get_visible() or not gl.config.enable_notifications:
324 def send_notification( self, message, title = "gPodder", actions = [], is_error=False):
325 if not self.__is_notification_on(): return
327 message = message.strip()
328 log('Notification
: %s', message, sender=self)
329 if gpodder.interface == gpodder.MAEMO:
330 pango_markup = '<b
>%s</b
>\n<small
>%s</small
>' % (title, message)
331 hildon.hildon_banner_show_information_with_markup(gtk.Label(''), None, pango_markup)
332 elif gpodder.interface == gpodder.GUI and have_pynotify:
333 notification = pynotify.Notification(title, message, self.__icon_filename)
334 if is_error: notification.set_urgency(pynotify.URGENCY_CRITICAL)
336 notification.attach_to_status_icon(self)
338 log('Warning: Cannot attach notification to status icon
.', sender=self)
339 for action in actions:
340 notification.add_action(action[0], action[1], self.__action_callback)
341 if not notification.show():
342 log("Error: enable to send notification %s", message)
344 gtk.main() # needed for action callback to be triggered
348 # If we showed any kind of notification, remember it for next time
349 self.__previous_notification=[message, title, actions, is_error]
350 self.menuItem_previous_msg.set_sensitive(True)
352 def set_status(self, status=None, tooltip=None):
355 tooltip = self.DEFAULT_TOOLTIP
356 if len(self.__gpodder.already_notified_new_episodes) > 0:
357 tooltip += "\n" + _("New episodes:")
358 for episode in self.__gpodder.already_notified_new_episodes:
359 tooltip += "\n" + episode.title
360 if episode.was_downloaded(): tooltip += _(" (downloaded)")
362 tooltip = 'gPodder
- %s' % tooltip
363 if self.__current_icon is not None:
364 self.__current_pixbuf = self.__icon
365 self.set_from_pixbuf(self.__current_pixbuf)
366 self.__current_icon = None
368 (status_tooltip, icon) = status
370 tooltip = 'gPodder
- %s' % status_tooltip
372 tooltip = 'gPodder
- %s' % tooltip
373 if self.__current_icon != icon:
374 self.__current_pixbuf = self.__get_status_icon(icon)
375 self.set_from_pixbuf(self.__current_pixbuf)
376 self.__current_icon = icon
377 self.set_tooltip(tooltip)
379 def format_episode_list(self, episode_list, caption=None):
381 Format a list of episodes for tooltips and notifications
382 Return a listing of episodes title separated by a line break.
383 Long title are troncated: "xxxx...xxxx"
384 If the list is too long, it is cut and the string "x others episodes" is append
387 can be either a list containing podcastItem objects
388 or a list of strings of episode's title
.
391 the formatted
list of episodes
as a string
395 MAX_TITLE_LENGTH = 100
398 if caption is not None:
399 result.append('\n%s' % caption)
400 for episode in episode_list[:min(len(episode_list),MAX_EPISODES)]:
401 if isinstance(episode, podcastItem):
402 episode_title = episode.title
404 episode_title = episode
405 if len(episode_title) < MAX_TITLE_LENGTH:
406 title = episode_title
408 middle = (MAX_TITLE_LENGTH/2)-2
409 title = '%s...%s' % (episode_title[0:middle], episode_title[-middle:])
410 result.append('\n%s' % title)
412 more_episodes = len(episode_list) - MAX_EPISODES
413 if more_episodes > 0:
414 result.append('\n(...')
415 if more_episodes == 1:
416 result.append(_('one more episode'))
418 result.append(_('%d more episodes') % more_episodes)
419 result.append('...)')
421 return ''.join(result)
423 def set_synchronisation_device(self, synchronisation_device):
424 assert not self.__synchronisation_device, "a device was already set without have been released"
426 self.__synchronisation_device = synchronisation_device
427 self.__synchronisation_device.register('progress', self.__on_synchronisation_progress)
428 self.__synchronisation_device.register('status', self.__on_synchronisation_status)
429 self.__synchronisation_device.register('done', self.__on_synchronisation_done)
431 def release_synchronisation_device(self):
432 assert self.__synchronisation_device, "request for releasing a device which was never set"
434 self.__synchronisation_device.unregister('progress', self.__on_synchronisation_progress)
435 self.__synchronisation_device.unregister('status', self.__on_synchronisation_status)
436 self.__synchronisation_device.unregister('done', self.__on_synchronisation_done)
437 self.__synchronisation_device = None
439 def __on_synchronisation_progress(self, pos, max, text=None):
441 text = _('%d of %d done') % (pos, max)
442 self.__sync_progress = text
444 def __on_synchronisation_status(self, status):
445 tooltip = _('%s\n%s') % (status, self.__sync_progress)
446 self.set_status(self.STATUS_SYNCHRONIZING, tooltip)
447 log("tooltip: %s", tooltip, sender=self)
449 def __on_synchronisation_done(self):
450 if self.__gpodder.minimized:
451 # this might propably never appends so long gPodder synchronizes in a modal windows
452 self.send_notification(_('Your device has been updated by gPodder.'), _('Operation finished'))
455 def progress_bar(self, ratio):
457 draw a progress bar on top of the tray icon
.
458 Be sure to call this method the first time with ratio
=0
459 in order to initialise background image
462 value between
0 and 1 (inclusive
) indicating the ratio
463 of the progress bar to be drawn
467 # Only update in 3-percent-steps to save some resources
468 if abs(ratio-self.__last_ratio) < 0.03 and ratio > self.__last_ratio:
471 icon = self.__current_pixbuf.copy()
472 progressbar = draw.progressbar_pixbuf(icon.get_width(), icon.get_height(), ratio)
473 progressbar.composite(icon, 0, 0, icon.get_width(), icon.get_height(), 0, 0, 1, 1, gtk.gdk.INTERP_NEAREST, 255)
475 self.set_from_pixbuf(icon)
476 self.__last_ratio = ratio