~ubuntu-branches/ubuntu/jaunty/totem/jaunty-updates

« back to all changes in this revision

Viewing changes to debian/patches/65_bbc-plugin.patch

  • Committer: Bazaar Package Importer
  • Author(s): Colin Watson
  • Date: 2008-10-10 21:53:06 UTC
  • Revision ID: james.westby@ubuntu.com-20081010215306-jxb12l7hubsve4q6
Tags: 2.24.2-0ubuntu2
* debian/patches/65_bbc-plugin.patch:
  - Update from Collabora, including:
    + Do network I/O, rdf parsing and apt.Cache() loading asynchronously
      or in a dedicated thread (LP: #274740).
    + Show loading progress and error messages in case of failure.
    + Fix installable codec check when the first codec in the list is
      installed but a subsequent one is not.
  - Note upstream bug in patch header.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Description: add BBC plugin; by Collabora, intended to go upstream once
2
 
# reasonably complete
 
1
# Upstream: http://bugzilla.gnome.org/show_bug.cgi?id=555823
 
2
# Description: add BBC plugin; by Collabora
3
3
#
4
4
Index: bindings/python/totem.defs
5
5
===================================================================
218
218
===================================================================
219
219
--- /dev/null
220
220
+++ src/plugins/bbc/contentview.py
221
 
@@ -0,0 +1,822 @@
 
221
@@ -0,0 +1,939 @@
222
222
+#!/usr/bin/python
223
223
+# coding=UTF-8
224
224
+#
245
245
+# Totem is covered by.
246
246
+#
247
247
+# See license_change file for details.
 
248
+#
 
249
+# TODO:
 
250
+#  - clean up code: mixed studlyCaps and foo_bar; mixed callbacks and signals
248
251
+
249
252
+import gobject
250
253
+gobject.threads_init()
 
254
+import glib
251
255
+import gio
252
256
+import pygst
253
257
+pygst.require ("0.10")
260
264
+import errno
261
265
+import random
262
266
+import time
 
267
+import thread
263
268
+
264
269
+from rdflib.Graph import ConjunctiveGraph
265
270
+from rdflib import Namespace
362
367
+
363
368
+'''
364
369
+CodecCache: keeps track of what is currently installed and what we might be
365
 
+            able to install; cache this in a dict so we don't have to do
366
 
+            expensive checks more often than necessary
 
370
+            able to install; caches things internally in a dict so we don't
 
371
+            have to do expensive checks more often than necessary; do not
 
372
+            cache results elsewhere and make sure to listen to the 'loaded'
 
373
+            signal and refilter any content once the database of installable
 
374
+            and installed codecs is loaded (methods may just return False if
 
375
+            the database hasn't been loaded yet)
367
376
+'''
368
 
+class CodecCache(object):
 
377
+class CodecCache(gobject.GObject):
369
378
+    __slots__ = [ 'codec_cache', 'installable_codecs' ]
370
379
+
 
380
+    __gsignals__ = dict(loaded=(gobject.SIGNAL_RUN_LAST, None, ()))
 
381
+
371
382
+    def __init__(self):
372
 
+        self.reset()
373
 
+
374
 
+    def reset(self):
 
383
+        gobject.GObject.__init__ (self)
375
384
+        self.codec_cache = { }
376
 
+        # FIXME: the following can take multiple seconds on machines with very
377
 
+        # low cpu power (culprit: apt.Cache()), we should probably do this
378
 
+        # async and then trigger a refiltering of the treeview when done
379
 
+        self.installable_codecs = installablecodecs.getInstallableCodecs()
380
 
+
 
385
+        self.installable_codecs = None
 
386
+
 
387
+    def reload_async(self):
 
388
+        gst.log('starting codec cache loading')
 
389
+        thread.start_new_thread(self._loading_thread, ())
 
390
+
 
391
+    def _loading_thread(self):
 
392
+        ''' idle callback to marshal result back into the main thread '''
 
393
+        def _loading_done_idle_cb(res):
 
394
+            gst.log('codec cache loaded (%d elements)' % (len(res)))
 
395
+            self.installable_codecs = res
 
396
+            self.emit('loaded')
 
397
+            return False # don't want to be called again
 
398
+
 
399
+        gst.log('in codec cache loading thread')
 
400
+        # the following can take quite a few seconds on machines with very
 
401
+        # low cpu power (culprit: apt.Cache()), so we do this in a separate
 
402
+        # thread and then trigger a refiltering of the treeview when done
 
403
+        res = installablecodecs.getInstallableCodecs()
 
404
+        gst.log('codec cache loading done, marshalling result into main thread')
 
405
+        gobject.idle_add(_loading_done_idle_cb, res)
 
406
381
407
+    def haveDecoderForCaps(self, decoder_caps):
382
408
+        caps_string = decoder_caps.to_string()
383
409
+
406
432
+        gst.debug('no element found that can handle ' + caps_string)
407
433
+        return False
408
434
+
409
 
+    def isInstalled(self, caps_needed):
410
 
+        if not caps_needed or caps_needed.is_empty():
 
435
+    ''' do not cache the result of this function '''
 
436
+    def isInstalledOrInstallable(self, caps_needed):
 
437
+        if not caps_needed or caps_needed.is_empty() or caps_needed.is_any():
 
438
+          return False
 
439
+
 
440
+        if self.installable_codecs is None:
 
441
+          gst.log('database of installable codecs not loaded yet')
411
442
+          return False
412
443
+
413
444
+        for s in caps_needed:
414
445
+          if not self.haveDecoderForCaps(gst.Caps(s)):
415
446
+            gst.debug('no decoder for %s installed' % (s.to_string()))
416
 
+            return False
 
447
+            if not s.get_name() in self.installable_codecs:
 
448
+              gst.debug('%s not installable either'  % (s.to_string()))
 
449
+              return False
417
450
+
418
451
+        return True
419
452
+
420
 
+    def isInstallable(self, caps_needed):
421
 
+        if not caps_needed or caps_needed.is_empty() or caps_needed.is_any():
422
 
+          return False
423
 
+        media_type = caps_needed[0].get_name()
424
 
+        if media_type in self.installable_codecs:
425
 
+          return True
426
 
+        return False
427
 
+
428
 
+    def isInstalledOrInstallable(self, caps_needed):
429
 
+        return self.isInstalled(caps_needed) or self.isInstallable(caps_needed)
430
 
+
431
453
+###############################################################################
432
454
+
433
455
+'''
682
704
+             gio-generated ETag for the local cache file, since those two
683
705
+             are not comparable)
684
706
+'''
685
 
+class ContentPool(object):
 
707
+# TODO:
 
708
+#  - maybe derive from list store or filtermodel directly?
 
709
+#  - aggregate codec-cache-loaded and loading-done into loading-done internally,
 
710
+#    so caller doesn't have to worry about that
 
711
+class ContentPool(gobject.GObject):
686
712
+    __slots__ = [ 'cache_dir', 'brands' ]
687
713
+
 
714
+    __gsignals__ = dict(codec_cache_loaded=(gobject.SIGNAL_RUN_LAST, None, ()),
 
715
+                        progress_message=(gobject.SIGNAL_RUN_LAST, None, (str, )),
 
716
+                        loading_error=(gobject.SIGNAL_RUN_LAST, None, (str, )),
 
717
+                        loading_done=(gobject.SIGNAL_RUN_LAST, None, ()))
 
718
+
688
719
+    CACHE_FILE_PREFIX = 'content-'
689
720
+    CACHE_FILE_SUFFIX = '.cache'
690
721
+    AVAILABLE_CONTENT_URI = 'http://open.bbc.co.uk/rad/uriplay/availablecontent'
693
724
+    def __init__(self):
694
725
+        global codec_cache
695
726
+
696
 
+        codec_cache = CodecCache() # set global singleton variable
 
727
+        gobject.GObject.__init__ (self)
 
728
+
 
729
+        # set global singleton variable
 
730
+        codec_cache = CodecCache()
 
731
+        codec_cache.connect('loaded', self._on_codec_cache_loaded)
 
732
+        codec_cache.reload_async()
 
733
+
697
734
+        self.brands = []
698
735
+        self.cache_dir = os.path.join(BaseDirectory.xdg_cache_home, 'totem',
699
736
+                                      'plugins', 'bbc')
708
745
+                      ': ' + err.strerror)
709
746
+            self.cache_dir = None
710
747
+
 
748
+    def _on_codec_cache_loaded(self, pool):
 
749
+        self.emit('codec-cache-loaded')
 
750
+
711
751
+    ''' returns True if the given filename refers to one of our cache files '''
712
752
+    def isCacheFileName(self, filename):
713
753
+      if not filename.startswith(self.CACHE_FILE_PREFIX):
772
812
+        fn = self.CACHE_FILE_PREFIX + etag +  self.CACHE_FILE_SUFFIX
773
813
+        return os.path.join(self.cache_dir, fn)
774
814
+
775
 
+    '''
776
 
+    Checks if the local cache file needs updating and does so if needed
777
 
+    FIXME: this should be done fully async one day
778
 
+    Returns the filename of the cache file
779
 
+    '''
780
 
+    def ensureCache(self):
781
 
+      etag = self.getCacheETag()
782
 
+      if etag:
783
 
+        gst.log('Cached etag: ' + etag)
784
 
+        self.deleteStaleCacheFiles(etag)
785
 
+        existing_cache_fn = self.createCacheFileName(etag)
786
 
+        existing_cache_file = gio.File(existing_cache_fn)
787
 
+        existing_cache_info = existing_cache_file.query_info('time::modified')
788
 
+        existing_cache_mtime = existing_cache_info.get_modification_time()
789
 
+        # if the cache file is not older than N minutes/hours/days, then
790
 
+        # we'll just go ahead and use it instead of downloading a new one,
791
 
+        # even if it's not perfectly up-to-date.
792
 
+        # FIXME: UI should have a way to force an update
793
 
+        secs_since_update = time.time() - existing_cache_mtime
794
 
+        if secs_since_update >= 0 and secs_since_update < self.MAX_CACHE_FILE_AGE:
795
 
+          gst.log('Cache file is fairly recent, last updated %f secs ago' % (secs_since_update))
796
 
+          return existing_cache_fn
797
 
+      else:
798
 
+        gst.log('Cached etag: None')
799
 
+
800
 
+      # CHECKME: is http always available as protocol?
801
 
+      try:
 
815
+    def parse_async(self, cache_fn):
 
816
+        self.emit('progress-message', 'Parsing available content list ...')
 
817
+        thread.start_new_thread(self._parsing_thread, (cache_fn, ))
 
818
+
 
819
+    def _parsing_thread(self, cache_fn):
 
820
+        def _parse_idle_cb(err_msg, brands):
 
821
+            self.brands = brands
 
822
+            gst.info('Parsing done: %d brands' % (len(self.brands)))
 
823
+            if err_msg:
 
824
+              self.emit('loading-error', err_msg)
 
825
+            else:
 
826
+              self.emit('loading-done')
 
827
+            return False
 
828
+
 
829
+        err_msg = None
 
830
+        brands = []
 
831
+        gst.debug('Loading ' + cache_fn)
 
832
+        store = ConjunctiveGraph()
 
833
+        try:
 
834
+          gst.debug('Reading RDF file ...')
 
835
+          store.load(cache_fn)
 
836
+          gst.debug('Parsing ' + cache_fn)
 
837
+          brands = self.parseBrands(store)
 
838
+        except:
 
839
+          gst.warning('Problem parsing RDF')
 
840
+          err_msg = 'Could not parse available content list'
 
841
+        finally:
 
842
+          gst.debug('Parsing done, marshalling result into main thread')
 
843
+          gobject.idle_add(_parse_idle_cb, err_msg, brands)
 
844
+
 
845
+    def _format_size_for_display(self, size):
 
846
+        if size < 1024:
 
847
+          return '%d bytes' % size
 
848
+        if size < 1024*1024:
 
849
+          return '%.1f kB' % (size / 1024.0)
 
850
+        else:
 
851
+          return '%.1f MB' % (size / (1024.0*1024.0))
 
852
+
 
853
+    def load_async(self):
 
854
+        def _query_done_cb(remote_file, result):
 
855
+            pdata = [''] # mutable container so subfunctions can share access
 
856
+
 
857
+            def _read_async_cb(instream, result):
 
858
+                try:
 
859
+                  partial_data = instream.read_finish(result)
 
860
+                  gst.log('Read %d bytes' % (len(partial_data)))
 
861
+                  data = pdata[0]
 
862
+                  if len(partial_data) == 0:                  
 
863
+                    instream.close()
 
864
+                    outstream = cache_file.create(gio.FILE_CREATE_NONE)
 
865
+                    outstream.write(data)
 
866
+                    outsize = outstream.query_info('*').get_size()
 
867
+                    outstream.close()
 
868
+                    gst.info('Wrote %ld bytes' % (outsize))
 
869
+                    self.parse_async(cache_fn)
 
870
+                  else:
 
871
+                    # FIXME: this probably results in many MBs of memcpy()
 
872
+                    data += partial_data
 
873
+                    pdata[0] = data
 
874
+                    instream.read_async(10240, _read_async_cb, io_priority=glib.PRIORITY_LOW-1)
 
875
+                    bytes_read = len(data)
 
876
+                    self.emit('progress-message',
 
877
+                              'Downloading available content list ... ' + '(' +
 
878
+                              self._format_size_for_display(bytes_read) + ')')
 
879
+                except IOError, e:
 
880
+                  gst.warning('Error downloading ' + self.AVAILABLE_CONTENT_URI)
 
881
+                  instream.close()
 
882
+                  try:
 
883
+                    cache_file.delete()
 
884
+                  finally:
 
885
+                    self.emit('loading-error', 'Error downloading available content list')
 
886
+
 
887
+            # _query_done_cb start:
 
888
+            gst.log('Query done')
 
889
+            try:
 
890
+              remote_info = remote_file.query_info_finish(result)
 
891
+            except Exception, e:
 
892
+              # bail out if we can't query, not much point trying to download
 
893
+              gst.warning('Could not query %s: %s' % (self.AVAILABLE_CONTENT_URI, e.message))
 
894
+              self.emit('loading-error', 'Could not connect to server')
 
895
+              return
 
896
+
 
897
+            gst.log('Got info, querying etag')
 
898
+            remote_etag = remote_info.get_etag()
 
899
+            if remote_etag:
 
900
+              remote_etag = remote_etag.strip('"')
 
901
+              gst.log('Remote etag: ' + remote_etag)
 
902
+
 
903
+            cache_fn = self.createCacheFileName(remote_etag)
 
904
+            cache_file = gio.File(cache_fn)
 
905
+
 
906
+            # if file already exists, get size to double-check against server's
 
907
+            try:
 
908
+              cache_size = cache_file.query_info('standard::size').get_size()
 
909
+            except:
 
910
+              cache_size = 0
 
911
+            finally:
 
912
+              if etag and remote_etag and etag == remote_etag:
 
913
+                remote_size = remote_info.get_size()
 
914
+                if remote_size <= 0 or cache_size == remote_size:
 
915
+                  gst.log('Cache file is up-to-date, nothing to do')
 
916
+                  self.parse_async(cache_fn)
 
917
+                  return
 
918
+
 
919
+            # delete old cache file if it exists
 
920
+            try:
 
921
+              cache_file.delete()
 
922
+            except:
 
923
+              pass
 
924
+
 
925
+            # FIXME: use gio.File.copy_async() once it's wrapped
 
926
+            remote_file.read().read_async(10240, _read_async_cb, io_priority=glib.PRIORITY_LOW-1)
 
927
+            gst.info('copying ' + self.AVAILABLE_CONTENT_URI + ' -> ' + cache_fn)
 
928
+            self.emit('progress-message', 'Downloading available content list ...')
 
929
+            return
 
930
+
 
931
+        # load_async start:
 
932
+        gst.log('starting loading')
 
933
+        etag = self.getCacheETag()
 
934
+        if etag:
 
935
+          gst.log('Cached etag: ' + etag)
 
936
+          self.deleteStaleCacheFiles(etag)
 
937
+          existing_cache_fn = self.createCacheFileName(etag)
 
938
+          existing_cache_file = gio.File(existing_cache_fn)
 
939
+          existing_cache_info = existing_cache_file.query_info('time::modified')
 
940
+          existing_cache_mtime = existing_cache_info.get_modification_time()
 
941
+          # if the cache file is not older than N minutes/hours/days, then
 
942
+          # we'll just go ahead and use it instead of downloading a new one,
 
943
+          # even if it's not perfectly up-to-date.
 
944
+          # FIXME: UI should have a way to force an update
 
945
+          secs_since_update = time.time() - existing_cache_mtime
 
946
+          if secs_since_update >= 0 and secs_since_update < self.MAX_CACHE_FILE_AGE:
 
947
+            gst.log('Cache file is fairly recent, last updated %f secs ago' % (secs_since_update))
 
948
+            self.parse_async(existing_cache_fn)
 
949
+            return
 
950
+        else:
 
951
+          gst.log('Cached etag: None')
 
952
+
 
953
+        # CHECKME: what happens if http is not available as protocol?
802
954
+        remote_file = gio.File(self.AVAILABLE_CONTENT_URI)
803
 
+        gst.log('Querying ' + self.AVAILABLE_CONTENT_URI)
804
 
+        remote_info = remote_file.query_info('*')
805
 
+        gst.log('Got info, querying etag')
806
 
+        remote_etag = remote_info.get_etag()
807
 
+        if remote_etag:
808
 
+          remote_etag = remote_etag.strip('"')
809
 
+          gst.log('Remote etag: ' + remote_etag)
810
 
+      except Exception, e:
811
 
+        gst.warning('Could not query %s: %s' % (self.AVAILABLE_CONTENT_URI, e.message))
812
 
+        remote_etag = None
813
 
+
814
 
+      cache_fn = self.createCacheFileName(remote_etag)
815
 
+      print "cache file name ", cache_fn
816
 
+
817
 
+      cache_file = gio.File(cache_fn)
818
 
+
819
 
+      # if file already exists, get size to double-check against server's
820
 
+      try:
821
 
+        cache_size = cache_file.query_info('standard::size').get_size()
822
 
+      except:
823
 
+        cache_size = 0
824
 
+
825
 
+      if etag and remote_etag and etag == remote_etag and \
826
 
+         (remote_info.get_size() <= 0 or cache_size == remote_info.get_size()):
827
 
+        gst.log('Cache file is up-to-date, nothing to do')
828
 
+        return cache_fn
829
 
+
830
 
+      try:
831
 
+        cache_file.delete()
832
 
+      except:
833
 
+        pass
834
 
+
835
 
+      # FIXME: use gio.File.copy() once we can use pygobject >= 2.15.2
836
 
+      try:
837
 
+        instream = remote_file.read()
838
 
+      except:
839
 
+        gst.error('Error reading remote file')
840
 
+        return None
841
 
+
842
 
+      try:
843
 
+        outstream = cache_file.create(gio.FILE_CREATE_NONE)
844
 
+        gst.info('copying ' + self.AVAILABLE_CONTENT_URI + ' -> ' + cache_fn)
845
 
+        while True:
846
 
+          try:
847
 
+            data = instream.read()
848
 
+            if not data or len(data) == 0:
849
 
+              break
850
 
+            gst.log('Read %d bytes' % (len(data)))
851
 
+            outstream.write(data)
852
 
+          except IOError, e:
853
 
+            if e.errno != 0:
854
 
+              raise e
855
 
+            break
856
 
+        outsize = outstream.query_info('*').get_size()
857
 
+        gst.info('Wrote %ld bytes' % (outsize))
858
 
+        instream.close()
859
 
+        outstream.close()
860
 
+      except gobject.GError, err:
861
 
+        gst.error('Error updating content cache: ' + err.message)
862
 
+        cache_file.delete()
863
 
+        return None
864
 
+      except:
865
 
+        gst.error('Error updating content cache')
866
 
+        cache_file.delete()
867
 
+        return None
868
 
+
869
 
+      return cache_fn
 
955
+        gst.log('Contacting server ' + self.AVAILABLE_CONTENT_URI)
 
956
+        self.emit('progress-message', 'Connecting to server ...')
 
957
+        remote_file.query_info_async(_query_done_cb, '*')
870
958
+
871
959
+    def parseBrands(self, graph):
872
960
+        brands = []
877
965
+          gst.log('[%3d eps] %s' % (len(brand.episodes), brand.title))
878
966
+        return brands
879
967
+
880
 
+    def load(self):
881
 
+        cache_fn = self.ensureCache()
882
 
+        if not cache_fn:
883
 
+          return False
884
 
+        gst.debug('Loading ' + cache_fn)
885
 
+        store = ConjunctiveGraph()
886
 
+        try:
887
 
+          store.load(cache_fn)
888
 
+          gst.debug('Parsing ' + cache_fn)
889
 
+          self.brands = self.parseBrands(store)
890
 
+          gst.debug('Parsing done, %d brands' % (len(self.brands)))
891
 
+        except IOError:
892
 
+          gst.warning('Problem parsing RDF')
893
 
+          self.brands = []
894
 
+          return False
895
 
+
896
 
+        return True
897
 
+
898
968
+    ''' returns array of brands which can potentially be played '''
899
969
+    def getUsableBrands(self):
900
970
+        usable_brands = []
907
977
+###############################################################################
908
978
+
909
979
+class ContentView(gtk.TreeView):
910
 
+    __slots__ = [ 'pool' ]
 
980
+    __slots__ = [ 'pool', 'content_pool_loaded', 'codec_cache_loaded' ]
911
981
+    __gsignals__ = dict(play_episode=
912
982
+                        (gobject.SIGNAL_RUN_LAST, None,
913
983
+                         (object,))) # Episode
920
990
+
921
991
+        self.set_headers_visible(False)
922
992
+
923
 
+        self.pool = ContentPool()
924
 
+        if not self.pool.load():
925
 
+          print "loading available content failed"
926
 
+          return
927
 
+
928
993
+        self.connect('row-activated', self.onRowActivated)
929
994
+
930
995
+       self.set_property('has-tooltip', True)
931
996
+       self.connect('query-tooltip', self.onQueryTooltip)
932
997
+
 
998
+        self.set_message('Loading ...')
 
999
+
 
1000
+        self.pool = ContentPool()
 
1001
+        self.pool.connect('codec-cache-loaded', self._on_codec_cache_loaded)
 
1002
+        self.pool.connect('progress-message', self._on_content_pool_message)
 
1003
+        self.pool.connect('loading-error', self._on_content_pool_error)
 
1004
+        self.pool.connect('loading-done', self._on_content_pool_loading_done)
 
1005
+        self.codec_cache_loaded = False
 
1006
+        self.content_pool_loaded = False
 
1007
+        self.pool.load_async()
 
1008
+        gst.log('started loading')
 
1009
+
 
1010
+    def _on_content_pool_message(self, content_pool, msg):
 
1011
+        self.set_message(msg)
 
1012
+
 
1013
+    def _on_content_pool_error(self, content_pool, err_msg):
 
1014
+        gst.warning('Failed to load available content: ' + err_msg)
 
1015
+        self.set_message(err_msg)
 
1016
+
 
1017
+    def _on_content_pool_loading_done(self, content_pool):
 
1018
+        gst.log('content pool loaded')
 
1019
+        self.content_pool_loaded = True
 
1020
+        if self.codec_cache_loaded:
 
1021
+          self.populate()
 
1022
+
 
1023
+    def _on_codec_cache_loaded(self, content_pool):
 
1024
+        gst.log('codec cache loaded, refilter')
 
1025
+        self.codec_cache_loaded = True
 
1026
+        #self.filter.refilter() FIXME: we don't filter at the moment
 
1027
+        if self.content_pool_loaded:
 
1028
+          self.populate()
 
1029
+
 
1030
+    def populate(self):
 
1031
+        gst.log('populating treeview')
 
1032
+        self.store.clear()
933
1033
+        brands = self.pool.getUsableBrands()
934
1034
+        gst.info('%d brands with usable episodes/encodings' % (len(brands)))
935
 
+
936
1035
+        for brand in brands:
937
 
+          self.addBrand(brand)
 
1036
+          brand_iter = self.store.append(None, [brand, None, None])
 
1037
+          for ep in brand.episodes:
 
1038
+            self.store.append(brand_iter, [brand, ep, None])
 
1039
+        self.set_model(self.filter)
938
1040
+
939
1041
+    def get_brand_tooltip(self, brand):
940
1042
+      if not brand or not brand.description:
950
1052
+                                                                 gobject.markup_escape_text(episode.description))
951
1053
+
952
1054
+    def onQueryTooltip(self, view, x, y, keyboard_tip, tip):
953
 
+      model, path, _iter = self.get_tooltip_context(x, y, keyboard_tip)
954
 
+      brand, episode = model.get(_iter, 0, 1)
 
1055
+      try:
 
1056
+        model, path, _iter = self.get_tooltip_context(x, y, keyboard_tip)
 
1057
+      except:
 
1058
+        return False # probably no content yet
 
1059
+
 
1060
+      brand, episode, msg = model.get(_iter, 0, 1, 2)
 
1061
+      if msg:
 
1062
+        return False
955
1063
+      if brand and not episode:
956
1064
+        markup = self.get_brand_tooltip(brand)
957
1065
+      elif brand and episode:
981
1089
+        markup = '<span>%s</span>' % (gobject.markup_escape_text(episode.title))
982
1090
+        renderer.set_property('markup', markup)
983
1091
+
 
1092
+    def renderMessageCell(self, column, renderer, model, _iter, msg):
 
1093
+        markup = '<i>%s</i>' % (gobject.markup_escape_text(msg))
 
1094
+        renderer.set_property('markup', markup)
 
1095
+        
984
1096
+    def renderCell(self, column, renderer, model, _iter):
985
 
+        brand, episode = model.get(_iter, 0, 1)
986
 
+        if episode == None:
 
1097
+        brand, episode, msg = model.get(_iter, 0, 1, 2)
 
1098
+        if msg:
 
1099
+          self.renderMessageCell(column, renderer, model, _iter, msg)
 
1100
+        elif not episode:
987
1101
+          self.renderBrandCell(column, renderer, model, _iter, brand)
988
1102
+        else:
989
1103
+          self.renderEpisodeCell(column, renderer, model, _iter, brand, episode)
1005
1119
+        else:
1006
1120
+          return -1
1007
1121
+
 
1122
+    def set_message(self, msg):
 
1123
+        self.msg_store.clear()
 
1124
+        self.msg_store.append(None, [None, None, msg])
 
1125
+        self.set_model(self.msg_store)
 
1126
+        gst.log('set message "' + msg + '"')
 
1127
+
1008
1128
+    def setupModel(self):
1009
 
+        self.store = gtk.TreeStore(object, object)  # Brand, Episode
 
1129
+        # columns: Brand, Episode, message string
 
1130
+        self.msg_store = gtk.TreeStore(object, object, str)
 
1131
+        self.store = gtk.TreeStore(object, object, str)
1010
1132
+        self.filter = self.store.filter_new()
1011
 
+        self.set_model(self.filter)
 
1133
+
1012
1134
+        column = gtk.TreeViewColumn()
1013
1135
+        renderer = gtk.CellRendererText()
1014
1136
+        renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
1018
1140
+        self.store.set_sort_func(self.SORT_ID_1, self.sortFunc)
1019
1141
+        self.store.set_sort_column_id(self.SORT_ID_1, gtk.SORT_ASCENDING)
1020
1142
+
1021
 
+    def addBrand(self, brand):
1022
 
+        brand_iter = self.store.append(None, [brand, None])
1023
 
+        for ep in brand.episodes:
1024
 
+          self.store.append(brand_iter, [brand, ep])
1025
 
+
1026
1143
+if __name__ == "__main__":
1027
1144
+    # ensure the caps strings in the container/video/audio map are parsable
1028
1145
+    for cs in video_map: