Hello, Jérôme!

Today, I've had some time to review your patch, I've attached my modifications. I've changed the file name to "trayicon" to fit better in the overall gPodder module naming scheme.

For a first version, it looks good already. There are still some things that need to be done:

 * Degrade gracefully when python-notify is not installed
 * "Update feeds" should be possible without the dialog
 * "Download all new episodes" should display only the episode selector
   if the gPodder main window is hidden and not the gPodder main window
 * Configuration options, as metioned in the TODO comments

Tray icon configuration options (a proposal):
  * No tray icon
  * Minimize to tray (only show icon when minimizing the window)
  * Always show tray icon (as it is currently)

Notification configuration options:
  * No notifications (error messages in message dialogs)
  * Notifications when minimized (including error messages)
  * Always use notifications (error messages in notification bubbles)

I've also attached the notification to the tray icon, which looks better in my opinion (after all, the tray icon "says" what the message contains).

Jérôme Chabod wrote:
I send you a new patch (for revison 503):
Pieter De Decker a écrit :
I have two of things to say about the notifications:
- Try to make them a little more descriptive. I'm sure that you've already thought of that and that you're working on it, but there's more. What if a certain episode is called "My super duper special podcast episode with a special guest host for December 21st 2007"? You could solve this by limiting the episode name to the first 25 (or so) characters, followed by "(...)". Example: "'My super duper special po(...)' has been downloaded successfully".
You'll probably like this version. I don't notify every file download because it's very annoying having to much popup windows, but the tooltip displays during dowload already dowloaded files, and the notify also lists them

Very good idea, I've improved the code a little to be a bit faster (''.join() instead of string concatenation).

nikosapi a écrit :
Attached is a very basic autoupdate addition to your patch. What it does, is every 20min it runs update_feed_cache. This behaviour is controlled by a configuration option in the Preferences window.
It sound good, but I was unfortunately unable to apply the patch . I just integrated manualy the thinks you modifed in GPodderStatusIcon. Can somebody, merge it with my attached patch?
Shouldn't also the 20min delay be configurable in the preference window?

Can you re-integrate Nick's patch? I have not yet had time to look into it. Maybe the patches should be seperated and one applied independent of the other.

Thomas Perl a écrit :
get_tree_icon() from gpodder.util could be helpful here. This is the function that is used to add the bullet or padlock to the icons in the episode list. You could extend this function and be able to get the gPodder icon with some "downloading" (e.g. arrow down) or checking (e.g. the "refresh" icon) in the lower right-hand corner of the icon.

I used you code as inspiration but I had my own method for more flexibility (I use a dictionary, it's quite easy to add as status)

Looks good, I've improved and simplified that a little.

Basically, the user is already notified about errors. What needs to be done is to redirect the show_notification() (and probably also show_confirmation()?) calls to the tray icon (and therefore libnotify) if the gPodder window has been hidden and only the tray icon is visible.m
I´ll check this
Have you had time looking into this already?

I've simplified some parts of your class so that it hopefully is easier to maintain.

What do you think of my modifications and the suggestions above?

Thanks,
Thomas
Index: src/gpodder/trayicon.py
===================================================================
--- src/gpodder/trayicon.py	(revision 0)
+++ src/gpodder/trayicon.py	(revision 0)
@@ -0,0 +1,279 @@
+# -*- coding: utf-8 -*-
+#
+# gPodder - A media aggregator and podcast client
+# Copyright (C) 2005-2007 Thomas Perl <thp at perli.net>
+#
+# gPodder is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# gPodder is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+
+# trayicon.py -- Tray icon and notification support
+# Jérôme Chabod (JCH) <[EMAIL PROTECTED]>  2007-12-20
+
+
+import gtk
+
+try:
+    import pynotify
+    have_pynotify = True
+except:
+    log('Cannot find pynotify. Please install the python-notify package.')
+    log('Notification bubbles have been disabled.')
+    have_pynotify = False
+
+from gpodder import services
+
+from gpodder.liblogger import log
+
+
+class GPodderStatusIcon(gtk.StatusIcon):
+    """ this class display a status icon in the system tray
+    this icon serves to show or hide gPodder, notify dowload status 
+    and provide a popupmenu for quick acces to some 
+    gPodder functionalities
+       
+    author: Jérôme Chabod (JCH) <jerome.chabod at ifrance.com>
+    12/20/2007 JCH: first release
+    12/22/2007 JCH: 
+        uses python-notify instead of dbus
+        during download, the status icon shows a small arrow
+        tooltip and notification also list successfully dowloaded files (idea from Pieter De Decker)
+        notification and icon for feed update
+       
+    """
+
+    DEFAULT_TOOLTIP = _('gPodder media aggregator')
+
+    DOWNLOAD_IN_PROGRESS = (_('Downloading episodes'), gtk.STOCK_GO_DOWN)
+    UPDATING_FEED_CACHE = (_('Looking for new episodes'), gtk.STOCK_REFRESH)
+
+
+    def __init__(self, gpodder, icon_filename):
+        gtk.StatusIcon.__init__(self)
+        log('Creating tray icon', sender=self)
+
+        self.__gpodder = gpodder
+        self.__finished_downloads = []
+        self.__icon_cache = {}
+        self.__icon_filename = icon_filename
+        self.__current_icon = -1
+
+        # Get the gPodder icon
+        try:
+            self.__icon = gtk.gdk.pixbuf_new_from_file(self.__icon_filename)
+        except Exception, exc:
+            log('Warning: Cannot load gPodder icon, will use the default icon (%s)', exc, sender=self)
+            self.__icon = gtk.icon_theme_get_default().load_icon(gtk.STOCK_DIALOG_QUESTION, 30, 30)
+
+        # Reset trayicon (default icon, default tooltip)
+        self.set_status()
+  
+        # build and connect the popup menu
+        menu = gtk.Menu()
+        menuItem = gtk.ImageMenuItem("Check for Updates")
+        menuItem.connect('activate',  self.__gpodder.on_itemUpdate_activate)
+        menu.append(menuItem)        
+        menuItem = gtk.ImageMenuItem("Download all new episodes")
+        menuItem.connect('activate',  self.__gpodder.on_itemDownloadAllNew_activate)
+        menu.append(menuItem)        
+        menu.append( gtk.SeparatorMenuItem())
+        menuItem = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES)
+        menuItem.connect('activate',  self.__gpodder.on_itemPreferences_activate)
+        menu.append(menuItem)
+        menuItem = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
+        menuItem.connect('activate',  self.__gpodder.on_itemAbout_activate)
+        menu.append(menuItem)
+        menu.append( gtk.SeparatorMenuItem())
+        menuItem = gtk.ImageMenuItem(gtk.STOCK_QUIT)
+        menuItem.connect('activate',  self.__gpodder.close_gpodder, self)
+        menu.append(menuItem)
+
+        self.connect('activate', self.__on_left_click)
+        self.connect('popup-menu', self.__on_right_click, menu)
+        self.set_visible(True)
+        
+        # initialise pynotify
+        if have_pynotify:
+            if not pynotify.init('gPodder'):
+                log('Error: unable to initialise pynotify', sender=self) 
+
+        # Register with the download status manager
+        services.download_status_manager.register('progress-changed', self.__download_progress_changed)
+        services.download_status_manager.register('progress-changed', self.__download_progress_changed)
+        services.download_status_manager.register('download-complete', self.__download_complete)
+
+
+    def __on_right_click(self, widget, button, time, data=None):
+        """Open popup menu on right-click
+        """
+        if data is not None:
+            data.show_all()
+            data.popup(None, None, None, 3, time)
+
+    def __on_left_click(self, widget, data=None):
+        """Hide/unhide gPodder on left-click
+        """
+        if self.is_iconified():
+            self.uniconify_main_window()
+        else:
+            self.iconify_main_window()  
+
+
+    def __download_complete(self, episode):
+        """Remember finished downloads
+        """
+        self.__finished_downloads.append(episode)
+
+
+    def __format_list_downloaded(self, caption=''):
+        """ format the list of already dowloaded files for displaying
+            in tooltip or notification.
+            return the formated string
+        """
+        MAX_EPISODES = 10
+
+        result = []
+        result.append('\n\n%s'%caption)
+        for episode in self.__finished_downloads[:min(len(self.__finished_downloads),MAX_EPISODES)]:
+            if len(episode) < 100:
+                title = episode
+            else:
+                title = '%s...'%episode[:97]
+            result.append('\n%s'%title)
+        
+        more_episodes = len(self.__finished_downloads) - MAX_EPISODES
+        if more_episodes > 0:
+            result.append('\n(...')
+            if more_episodes == 1:
+                result.append(_('one more episode'))
+            else:
+                result.append(_('%d more episodes')%more_episodes)
+            result.append('...)')
+
+        return ''.join(result)
+        
+    def __download_progress_changed( self, count, percentage):
+        """ callback by download manager during dowloading.
+        It updates the tooltip with information on how many 
+        files are dowloaded and the percentage of dowload
+        """
+
+        tooltip = []
+        if count > 0:
+            if count == 1:
+                tooltip.append(_('downloading one file'))
+            else:
+                tooltip.append(_('downloading %d files')%count)
+
+            tooltip.append(' (%d%%)'%percentage)
+
+            if len(self.__finished_downloads) > 0:
+                tooltip.append(self.__format_list_downloaded(_('Finished downloads:')))
+
+            self.set_status(self.DOWNLOAD_IN_PROGRESS, ''.join(tooltip))
+        else:
+            self.set_status()
+            num = len(self.__finished_downloads)
+            if num == 1:
+                title = _('Finished downloading one episode:')
+            elif num > 1:
+                title = _('Finished downloading %d episodes:')%num
+            else:
+                # No episodes have finished downloading, ignore
+                return
+
+            message = self.__format_list_downloaded(title)
+            self.send_notification(message, _('gPodder downloads finished'))
+            self.__finished_downloads = []
+    
+    def __get_status_icon(self, icon):
+        if icon in self.__icon_cache:
+            return self.__icon_cache[icon]
+        
+        try:
+            new_icon = self.__icon.copy()
+            emblem = gtk.icon_theme_get_default().load_icon(icon, int(new_icon.get_width()/1.5), 0)
+            size = emblem.get_width()
+            pos = new_icon.get_width()-size
+            emblem.composite(new_icon, pos, pos, size, size, pos, pos, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
+            self.__icon_cache[icon] = new_icon
+            return new_icon
+        except Exception, exc:
+            pass
+        
+        log('Warning: Cannot create status icon: %s', icon, sender=self)
+        return self.__icon
+
+    def __action_callback(self, n, action):
+        """ call back when a button is clicked in a notify bubble """
+        log("action triggered %s", action, sender = self)
+        if action == "show": 
+            self.uniconify_main_window()
+        elif action == "close": 
+            self.__gpodder.close_gpodder()
+        elif action == "Ignore": 
+            pass
+        else:
+            log("Warning: don't know what to do with action %s", action, sender = self)
+            
+    def send_notification( self, message, title=_('gPodder')):
+        """ Use the gnome notifier system to display a notification message 
+        """
+        message = message.strip()
+        log('Notification: %s', message, sender=self)
+
+        #TODO:  a configuration flag to make this optional
+        if have_pynotify:
+            notification = pynotify.Notification(title, message, self.__icon_filename)
+            #notification.add_action("show", "show", self.__action_callback)
+            #notification.add_action("exit", "exit gPodder", self.__action_callback)
+            #notification.add_action("ignore", "ignore", self.__action_callback)
+            notification.attach_to_status_icon(self)
+            if not notification.show():
+                log('Error: enable to send notification %s', notification, sender=self)
+            
+    def set_status(self, status=None, tooltip=None):
+        if status is None:
+            if tooltip is None:
+                tooltip = self.DEFAULT_TOOLTIP
+            else:
+                tooltip = 'gPodder - %s' % tooltip
+            if self.__current_icon is not None:
+                self.set_from_pixbuf(self.__icon)
+                self.__current_icon = None
+        else:
+            (status_tooltip, icon) = status
+            if tooltip is None:
+                tooltip = 'gPodder - %s' % status_tooltip
+            else:
+                tooltip = 'gPodder - %s' % tooltip
+            if self.__current_icon != icon:
+                self.set_from_pixbuf(self.__get_status_icon(icon))
+                self.__current_icon = icon
+
+        self.set_tooltip(tooltip)
+      
+    def is_iconified(self):
+        return self.__gpodder.minimized
+        
+    def uniconify_main_window(self):
+        if self.is_iconified():
+            self.__gpodder.gpodder_main_window.deiconify()
+            self.__gpodder.gpodder_main_window.set_skip_taskbar_hint(False) 
+        
+    def iconify_main_window(self):
+        if not self.is_iconified():
+            self.__gpodder.gpodder_main_window.set_skip_taskbar_hint(True)
+            self.__gpodder.gpodder_main_window.iconify()
+
Index: src/gpodder/download.py
===================================================================
--- src/gpodder/download.py	(revision 504)
+++ src/gpodder/download.py	(working copy)
@@ -153,6 +153,7 @@
                 self.downloader.retrieve( self.episode.url, self.tempname, reporthook = self.status_updated)
                 shutil.move( self.tempname, self.filename)
                 self.channel.addDownloadedItem( self.episode)
+                services.download_status_manager.download_completed(self.download_id)
             finally:
                 services.download_status_manager.remove_download_id( self.download_id)
                 services.download_status_manager.s_release( acquired)
Index: src/gpodder/services.py
===================================================================
--- src/gpodder/services.py	(revision 504)
+++ src/gpodder/services.py	(working copy)
@@ -82,7 +82,7 @@
         self.tree_model = gtk.ListStore( *self.COLUMN_TYPES)
         self.tree_model_lock = threading.Lock()
         
-        signal_names = ['list-changed', 'progress-changed', 'progress-detail']
+        signal_names = ['list-changed', 'progress-changed', 'progress-detail', 'download-complete']
         ObservableService.__init__(self, signal_names)
 
     def notify_progress( self):
@@ -170,6 +170,10 @@
             self.notify( 'progress-detail', self.status_list[id]['url'], kwargs['progress'], kwargs['speed'])
 
         self.notify_progress()
+        
+    def download_completed(self, id):
+        if id in self.status_list:
+            self.notify('download-complete', self.status_list[id]['episode'])
 
     def request_progress_detail( self, url):
         for status in self.status_list.values():
Index: src/gpodder/gui.py
===================================================================
--- src/gpodder/gui.py	(revision 504)
+++ src/gpodder/gui.py	(working copy)
@@ -37,6 +37,7 @@
 from gpodder import services
 from gpodder import download
 from gpodder import SimpleGladeApp
+from gpodder import trayicon
 
 from libpodcasts import podcastChannel
 from libpodcasts import channels_to_model
@@ -164,6 +165,11 @@
 
         gl = gPodderLib()
 
+        #TODO: use a configuration flag to make it optional
+        self.status_icon = trayicon.GPodderStatusIcon(self, scalable_dir)
+        self.minimized = False
+        self.gPodder.connect('window-state-event', self.window_state_event)
+        
         gl.config.connect_gtk_window( self.gPodder)
         gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
 
@@ -290,6 +296,12 @@
                 self.updateComboBox()
 
 
+    def window_state_event(self, widget, event):
+        if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:
+            self.minimized = True
+        else:
+            self.minimized = False
+
     def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
         # FIXME: Do not hardcode treeview header height
         HEADER_HEIGHT = 25
@@ -757,6 +769,7 @@
         title = _('Downloading podcast feeds')
         heading = _('Downloading feeds')
         body = _('Podcast feeds contain channel metadata and information about current episodes.')
+        self.status_icon.set_status(self.status_icon.UPDATING_FEED_CACHE)
         
         please_wait = gtk.Dialog( title, self.gPodder, gtk.DIALOG_MODAL, ( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, ))
         please_wait.set_transient_for( self.gPodder)
@@ -808,6 +821,7 @@
 
         please_wait.run()
         please_wait.destroy()
+        self.status_icon.set_status()
 
         self.updateComboBox()
         
_______________________________________________
gpodder-devel mailing list
gpodder-devel@lists.berlios.de
https://lists.berlios.de/mailman/listinfo/gpodder-devel

Reply via email to