From c2ac54ff11e8bc249df58d6a11d47b5291c21ef0 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Sat, 13 Dec 2008 13:29:45 +0100 Subject: [PATCH] Update the UI more efficiently, make it much faster Remove all unnecessary full episode list reloads and reduce the number of UI updates while downloading to make the UI feel (and be) more responsive and also not need to reset the scroll position because of a full channel/episode list reload. That's good stuff! --- src/gpodder/download.py | 4 ++ src/gpodder/gui.py | 127 ++++++++++++++++++++++++++++++++++----------- src/gpodder/libpodcasts.py | 52 ++++++++----------- src/gpodder/services.py | 30 +++++++---- 4 files changed, 144 insertions(+), 69 deletions(-) diff --git a/src/gpodder/download.py b/src/gpodder/download.py index de7839dd..e25df27f 100644 --- a/src/gpodder/download.py +++ b/src/gpodder/download.py @@ -93,6 +93,10 @@ class DownloadThread(threading.Thread): threading.Thread.__init__( self) self.setDaemon( True) + if gpodder.interface == gpodder.MAEMO: + # Only update status every 3 seconds on Maemo + self.MAX_UPDATES_PER_SEC = 1./3. + self.channel = channel self.episode = episode diff --git a/src/gpodder/gui.py b/src/gpodder/gui.py index 4b8f7e89..02967f5d 100644 --- a/src/gpodder/gui.py +++ b/src/gpodder/gui.py @@ -547,6 +547,14 @@ class gPodder(GladeWidget): elif gl.config.minimize_to_tray: self.tray_icon.set_visible(False) + # a dictionary that maps episode URLs to the current + # treeAvailable row numbers to generate tree paths + self.url_path_mapping = {} + + # a dictionary that maps channel URLs to the current + # treeChannels row numbers to generate tree paths + self.channel_url_path_mapping = {} + services.download_status_manager.register( 'list-changed', self.download_status_updated) services.download_status_manager.register( 'progress-changed', self.download_progress_updated) services.cover_downloader.register('cover-available', self.cover_download_finished) @@ -565,6 +573,7 @@ class gPodder(GladeWidget): # Subscribed channels self.active_channel = None self.channels = load_channels() + self.channel_list_changed = True self.update_podcasts_tab() # load list of user applications for audio playback @@ -1112,11 +1121,22 @@ class gPodder(GladeWidget): if count == 0: if len(gl.config.cmd_all_downloads_complete) > 0: Thread(target=gl.ext_command_thread, args=(self.notification,gl.config.cmd_all_downloads_complete)).start() + + def update_selected_episode_list_icons(self): + """ + Updates the status icons in the episode list + """ + selection = self.treeAvailable.get_selection() + (model, paths) = selection.get_selected_rows() + for path in paths: + iter = model.get_iter(path) + self.active_channel.iter_set_downloading_columns(model, iter) def playback_episode(self, episode, stream=False): (success, application) = gl.playback_episode(episode, stream) if not success: self.show_message( _('The selected player application cannot be found. Please check your media player settings in the preferences dialog.'), _('Error opening player: %s') % ( saxutils.escape( application), )) + self.update_selected_episode_list_icons() self.updateComboBox(only_selected_channel=True) def treeAvailable_search_equal( self, model, column, key, iter, data = None): @@ -1241,14 +1261,20 @@ class gPodder(GladeWidget): return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) - def download_status_updated( self): + def download_status_updated(self, episode_urls, channel_urls): count = services.download_status_manager.count() if count: self.labelDownloads.set_text( _('Downloads (%d)') % count) else: self.labelDownloads.set_text( _('Downloads')) - self.updateComboBox() + model = self.treeAvailable.get_model() + for url in episode_urls: + if url in self.url_path_mapping: + path = (self.url_path_mapping[url],) + self.active_channel.iter_set_downloading_columns(model, model.get_iter(path)) + + self.updateComboBox(only_these_urls=channel_urls) def on_cbMaxDownloads_toggled(self, widget, *args): self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active()) @@ -1260,31 +1286,60 @@ class gPodder(GladeWidget): self.updateComboBox() self.updateTreeView() - def updateComboBox(self, selected_url=None, only_selected_channel=False): - (model, iter) = self.treeChannels.get_selection().get_selected() + def updateComboBox(self, selected_url=None, only_selected_channel=False, only_these_urls=None): + selection = self.treeChannels.get_selection() + (model, iter) = selection.get_selected() if only_selected_channel: + # very cheap! only update selected channel if iter and self.active_channel is not None: - update_channel_model_by_iter( self.treeChannels.get_model(), - iter, self.active_channel, self.channel_colors, - self.cover_cache, *(gl.config.podcast_list_icon_size,)*2 ) + update_channel_model_by_iter(model, iter, + self.active_channel, self.channel_colors, + self.cover_cache, + gl.config.podcast_list_icon_size, + gl.config.podcast_list_icon_size) + elif not self.channel_list_changed: + # we can keep the model, but have to update some + if only_these_urls is None: + # still cheaper than reloading the whole list + iter = model.get_iter_first() + while iter is not None: + (index,) = model.get_path(iter) + update_channel_model_by_iter(model, iter, + self.channels[index], self.channel_colors, + self.cover_cache, + gl.config.podcast_list_icon_size, + gl.config.podcast_list_icon_size) + iter = model.iter_next(iter) + else: + # ok, we got a bunch of urls to update + for url in only_these_urls: + index = self.channel_url_path_mapping[url] + path = (index,) + iter = model.get_iter(path) + update_channel_model_by_iter(model, iter, + self.channels[index], self.channel_colors, + self.cover_cache, + gl.config.podcast_list_icon_size, + gl.config.podcast_list_icon_size) else: if model and iter and selected_url is None: # Get the URL of the currently-selected podcast selected_url = model.get_value(iter, 0) - rect = self.treeChannels.get_visible_rect() - self.treeChannels.set_model( channels_to_model( self.channels, - self.channel_colors, self.cover_cache, - *(gl.config.podcast_list_icon_size,)*2 )) - util.idle_add(self.treeChannels.scroll_to_point, rect.x, rect.y) + (model, urls) = channels_to_model(self.channels, + self.channel_colors, self.cover_cache, + gl.config.podcast_list_icon_size, + gl.config.podcast_list_icon_size) + + self.channel_url_path_mapping = dict(zip(urls, range(len(urls)))) + self.treeChannels.set_model(model) try: selected_path = (0,) # Find the previously-selected URL in the new # model if we have an URL (else select first) if selected_url is not None: - model = self.treeChannels.get_model() pos = model.get_iter_first() while pos is not None: url = model.get_value(pos, 0) @@ -1296,19 +1351,20 @@ class gPodder(GladeWidget): self.treeChannels.get_selection().select_path(selected_path) except: log( 'Cannot set selection on treeChannels', sender = self) - self.on_treeChannels_cursor_changed( self.treeChannels) + self.on_treeChannels_cursor_changed( self.treeChannels) + self.channel_list_changed = False - def updateTreeView(self, retain_position=True): + def updateTreeView(self): if self.channels and self.active_channel is not None: - rect = self.treeAvailable.get_visible_rect() - self.treeAvailable.set_model(self.active_channel.tree_model) - if retain_position: - util.idle_add(self.treeAvailable.scroll_to_point, rect.x, rect.y) + (model, urls) = self.active_channel.get_tree_model() + self.treeAvailable.set_model(model) + self.url_path_mapping = dict(zip(urls, range(len(urls)))) self.treeAvailable.columns_autosize() self.play_or_download() else: - if self.treeAvailable.get_model(): - self.treeAvailable.get_model().clear() + model = self.treeAvailable.get_model() + if model is not None: + model.clear() def drag_data_received(self, widget, context, x, y, sel, ttype, time): (path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4 @@ -1393,6 +1449,7 @@ class gPodder(GladeWidget): def add_new_channel_finish( self, channel, url, error, ask_download_new, quiet, waitdlg): if channel is not None: self.channels.append( channel) + self.channel_list_changed = True save_channels( self.channels) if not quiet: # download changed channels and select the new episode in the UI afterwards @@ -1408,6 +1465,7 @@ class gPodder(GladeWidget): # data won't show up in the channel editor. # TODO: Only updated the newly added feed to save some cpu cycles self.channels = load_channels() + self.channel_list_changed = True if ask_download_new: new_episodes = channel.get_new_episodes() @@ -1499,6 +1557,7 @@ class gPodder(GladeWidget): # open the episodes selection dialog self.channels = load_channels() + self.channel_list_changed = True self.updateComboBox() if not self.feed_cache_update_cancelled: self.download_all_new(channels=channels) @@ -1546,6 +1605,7 @@ class gPodder(GladeWidget): if not force_update: self.channels = load_channels() + self.channel_list_changed = True self.updateComboBox() return @@ -1677,6 +1737,7 @@ class gPodder(GladeWidget): except Exception, e: log( 'Warning: Error in for_each_selected_episode_url for URL %s: %s', url, e, sender = self) + self.update_selected_episode_list_icons() self.updateComboBox(only_selected_channel=True) def delete_episode_list( self, episodes, confirm = True): @@ -1752,7 +1813,6 @@ class gPodder(GladeWidget): self.for_each_selected_episode_url(callback) def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False): - self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked db.update_channel_lock(self.active_channel) @@ -1763,8 +1823,8 @@ class gPodder(GladeWidget): for episode in self.active_channel.get_all_episodes(): db.mark_episode(episode.url, is_locked=self.active_channel.channel_is_locked) - self.updateComboBox() + self.updateComboBox(only_selected_channel=True) def on_item_email_subscriptions_activate(self, widget): if not self.channels: @@ -1828,13 +1888,13 @@ class gPodder(GladeWidget): else: gPodderWelcome(center_on_widget=self.gPodder, show_example_podcasts_callback=self.on_itemImportChannels_activate, setup_my_gpodder_callback=self.on_download_from_mygpo) - def download_episode_list( self, episodes): + def download_episode_list(self, episodes): services.download_status_manager.start_batch_mode() for episode in episodes: log('Downloading episode: %s', episode.title, sender = self) filename = episode.local_filename() if not episode.was_downloaded(and_exists=True) and not services.download_status_manager.is_download_in_progress( episode.url): - download.DownloadThread( episode.channel, episode, self.notification).start() + download.DownloadThread(episode.channel, episode, self.notification).start() services.download_status_manager.end_batch_mode() def new_episodes_show(self, episodes): @@ -2279,6 +2339,7 @@ class gPodder(GladeWidget): # Remove the channel self.active_channel.delete() self.channels.remove(self.active_channel) + self.channel_list_changed = True save_channels(self.channels) # Re-load the channels and select the desired new channel @@ -2397,10 +2458,14 @@ class gPodder(GladeWidget): def on_treeChannels_cursor_changed(self, widget, *args): ( model, iter ) = self.treeChannels.get_selection().get_selected() - if model is not None and iter != None: - id = model.get_path( iter)[0] + if model is not None and iter is not None: + old_active_channel = self.active_channel + (id,) = model.get_path(iter) self.active_channel = self.channels[id] + if self.active_channel == old_active_channel: + return + if gpodder.interface == gpodder.MAEMO: self.set_title(self.active_channel.title) self.itemEditChannel.show_all() @@ -2417,7 +2482,7 @@ class gPodder(GladeWidget): self.itemRemoveChannel.hide_all() self.channel_toggle_lock.hide_all() - self.updateTreeView(False) + self.updateTreeView() def on_entryAddChannel_changed(self, widget, *args): active = self.entryAddChannel.get_text() not in ('', self.ENTER_URL_TEXT) @@ -2473,10 +2538,13 @@ class gPodder(GladeWidget): self.playback_episode(episode, stream=True) elif do_epdialog: play_callback = lambda: self.playback_episode(episode) - download_callback = lambda: self.download_episode_list([episode]) + def download_callback(): + self.download_episode_list([episode]) + self.play_or_download() gPodderEpisode(episode=episode, download_callback=download_callback, play_callback=play_callback) else: self.download_episode_list(episodes) + self.play_or_download() except: log('Error in on_treeAvailable_row_activated', traceback=True, sender=self) @@ -2523,6 +2591,7 @@ class gPodder(GladeWidget): for url in cancel_urls: services.download_status_manager.cancel_by_url( url) services.download_status_manager.end_batch_mode() + self.play_or_download() def on_btnCancelDownloadStatus_clicked(self, widget, *args): self.on_treeDownloads_row_activated( widget, None) diff --git a/src/gpodder/libpodcasts.py b/src/gpodder/libpodcasts.py index 69424bd0..5a1d47a2 100644 --- a/src/gpodder/libpodcasts.py +++ b/src/gpodder/libpodcasts.py @@ -344,21 +344,6 @@ class podcastChannel(object): def get_all_episodes(self): return db.load_episodes(self, factory = lambda d: podcastItem.create_from_dict(d, self)) - # not used anymore - def update_model( self): - self.update_save_dir_size() - model = self.tree_model - - iter = model.get_iter_first() - while iter is not None: - self.iter_set_downloading_columns(model, iter) - iter = model.iter_next( iter) - - @property - def tree_model( self): - log('Returning TreeModel for %s', self.url, sender = self) - return self.items_liststore() - def iter_set_downloading_columns( self, model, iter, episode=None): global ICON_AUDIO_FILE, ICON_VIDEO_FILE global ICON_DOWNLOADING, ICON_DELETED, ICON_NEW @@ -403,7 +388,7 @@ class podcastChannel(object): model.set( iter, 4, status_icon) - def items_liststore( self): + def get_tree_model(self): """ Return a gtk.ListStore containing episodes for this channel """ @@ -411,6 +396,8 @@ class podcastChannel(object): gobject.TYPE_BOOLEAN, gtk.gdk.Pixbuf, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING ) + log('Returning TreeModel for %s', self.url, sender = self) + urls = [] for item in self.get_all_episodes(): description = item.title_and_description @@ -423,9 +410,10 @@ class podcastChannel(object): True, None, item.cute_pubdate(), description, util.remove_html_tags(item.description), item.local_filename(), item.extension())) self.iter_set_downloading_columns( new_model, new_iter, episode=item) + urls.append(item.url) self.update_save_dir_size() - return new_model + return (new_model, urls) def find_episode( self, url): return db.load_episode(url, factory=lambda x: podcastItem.create_from_dict(x, self)) @@ -734,16 +722,17 @@ class podcastItem(object): def update_channel_model_by_iter( model, iter, channel, color_dict, - cover_cache=None, max_width=0, max_height=0 ): + cover_cache=None, max_width=0, max_height=0, initialize_all=False): count_downloaded = channel.stat(state=db.STATE_DOWNLOADED) count_new = channel.stat(state=db.STATE_NORMAL, is_played=False) count_unplayed = channel.stat(state=db.STATE_DOWNLOADED, is_played=False) channel.iter = iter - model.set(iter, 0, channel.url) - model.set(iter, 1, channel.title) + if initialize_all: + model.set(iter, 0, channel.url) + model.set(iter, 1, channel.title) title_markup = saxutils.escape(channel.title) description_markup = saxutils.escape(util.get_first_line(channel.description) or _('No description available')) d = [] @@ -773,23 +762,26 @@ def update_channel_model_by_iter( model, iter, channel, color_dict, else: model.set(iter, 7, False) - # Load the cover if we have it, but don't download - # it if it's not available (to avoid blocking here) - pixbuf = services.cover_downloader.get_cover(channel, avoid_downloading=True) - new_pixbuf = None - if pixbuf is not None: - new_pixbuf = util.resize_pixbuf_keep_ratio(pixbuf, max_width, max_height, channel.url, cover_cache) - model.set(iter, 5, new_pixbuf or pixbuf) + if initialize_all: + # Load the cover if we have it, but don't download + # it if it's not available (to avoid blocking here) + pixbuf = services.cover_downloader.get_cover(channel, avoid_downloading=True) + new_pixbuf = None + if pixbuf is not None: + new_pixbuf = util.resize_pixbuf_keep_ratio(pixbuf, max_width, max_height, channel.url, cover_cache) + model.set(iter, 5, new_pixbuf or pixbuf) def channels_to_model(channels, color_dict, cover_cache=None, max_width=0, max_height=0): new_model = gtk.ListStore( str, str, str, gtk.gdk.Pixbuf, int, gtk.gdk.Pixbuf, str, bool, str ) + urls = [] for channel in channels: - update_channel_model_by_iter( new_model, new_model.append(), channel, - color_dict, cover_cache, max_width, max_height ) + update_channel_model_by_iter(new_model, new_model.append(), channel, + color_dict, cover_cache, max_width, max_height, True) + urls.append(channel.url) - return new_model + return (new_model, urls) def load_channels(): diff --git a/src/gpodder/services.py b/src/gpodder/services.py index 4e047851..7d4e5f5f 100644 --- a/src/gpodder/services.py +++ b/src/gpodder/services.py @@ -283,7 +283,7 @@ cover_downloader = CoverDownloader() class DownloadStatusManager(ObservableService): COLUMN_NAMES = { 0: 'episode', 1: 'speed', 2: 'progress', 3: 'url' } COLUMN_TYPES = ( gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_FLOAT, gobject.TYPE_STRING ) - PROGRESS_HOLDDOWN_TIMEOUT = 1 + PROGRESS_HOLDDOWN_TIMEOUT = 5 def __init__( self): self.status_list = {} @@ -303,8 +303,9 @@ class DownloadStatusManager(ObservableService): # batch add in progress? self.batch_mode_enabled = False - # we set this flag if we would notify inside batch mode - self.batch_mode_notify_flag = False + # remember which episodes and channels changed during batch mode + self.batch_mode_changed_episode_urls = set() + self.batch_mode_changed_channel_urls = set() # Used to notify all threads that they should # re-check if they can acquire the lock @@ -333,9 +334,10 @@ class DownloadStatusManager(ObservableService): This sends out a notification that the list has changed. """ self.batch_mode_enabled = False - if self.batch_mode_notify_flag: - self.notify('list-changed') - self.batch_mode_notify_flag = False + if len(self.batch_mode_changed_episode_urls) + len(self.batch_mode_changed_channel_urls) > 0: + self.notify('list-changed', self.batch_mode_changed_episode_urls, self.batch_mode_changed_channel_urls) + self.batch_mode_changed_episode_urls = set() + self.batch_mode_changed_channel_urls = set() def notify_progress(self, force=False): now = (self.count(), self.average_progress()) @@ -404,9 +406,10 @@ class DownloadStatusManager(ObservableService): self.tree_model_lock.acquire() self.status_list[id] = { 'iter': self.tree_model.append(), 'thread': thread, 'progress': 0.0, 'speed': _('Queued'), } if self.batch_mode_enabled: - self.batch_mode_notify_flag = True + self.batch_mode_changed_episode_urls.add(thread.episode.url) + self.batch_mode_changed_channel_urls.add(thread.channel.url) else: - self.notify('list-changed') + self.notify('list-changed', [thread.episode.url], [thread.channel.url]) self.tree_model_lock.release() def remove_download_id( self, id): @@ -418,15 +421,22 @@ class DownloadStatusManager(ObservableService): util.idle_add(self.remove_iter, iter) self.tree_model_lock.release() self.status_list[id]['iter'] = None + episode_url = self.status_list[id]['thread'].episode.url + channel_url = self.status_list[id]['thread'].channel.url self.status_list[id]['thread'].cancel() del self.status_list[id] if not self.has_items(): # Reset the counter now self.downloads_done_bytes = 0 + else: + episode_url = None + channel_url = None + if self.batch_mode_enabled: - self.batch_mode_notify_flag = True + self.batch_mode_changed_episode_urls.add(episode_url) + self.batch_mode_changed_channel_urls.add(channel_url) else: - self.notify('list-changed') + self.notify('list-changed', [episode_url], [channel_url]) self.notify_progress(force=True) def count( self): -- 2.11.4.GIT