Add new browser panel and updated edit panel
authorIvan Frade <ivan.frade@gmail.com>
Sun, 6 Sep 2009 21:28:52 +0000 (00:28 +0300)
committerIvan Frade <ivan.frade@gmail.com>
Sun, 6 Sep 2009 21:28:52 +0000 (00:28 +0300)
Browser panel using a treeview to browse the music collection. Include
 interactive search.

Edit panel using a gtk.TreeModel as store

setup.py
src/browse_panel.py [new file with mode: 0755]
src/edit_panel_tm.py [new file with mode: 0644]

index bd8d92e..2365f37 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -13,8 +13,10 @@ DATA = [('share/applications/hildon', ['data/mussorgsky.desktop']),
                             'src/album_art_thread.py',
                             'src/album_art_panel.py',
                             'src/album_art_spec.py',
+                            'src/browse_panel.py',
                             'src/download_dialog.py',
                             'src/edit_panel.py',
+                            'src/edit_panel_tm.py',
                             'src/mussorgsky.py',
                             'src/mutagen_backend.py',                            
                             'src/player_backend.py',
diff --git a/src/browse_panel.py b/src/browse_panel.py
new file mode 100755 (executable)
index 0000000..57b39c0
--- /dev/null
@@ -0,0 +1,128 @@
+#!/usr/bin/env python2.5
+import hildon
+import gtk, gobject
+from edit_panel_tm import MussorgskyEditPanel
+from utils import escape_html, Set
+
+class MussorgskyBrowsePanel (hildon.StackableWindow):
+#class MussorgskyBrowsePanel (gtk.Window):
+    def __init__ (self, songs_list):
+        hildon.StackableWindow.__init__ (self)
+        #gtk.Window.__init__ (self)
+        self.set_title ("Browse collection")
+        self.set_border_width (12)
+        self.__create_view ()
+
+        # Prepare cache of artists and albums
+        self.artist_set = Set ()
+        self.albums_set = Set ()
+
+        # (uri, "Music", artist, title, album, mimetype) + "string"
+        self.full_model = gtk.ListStore (str, str, str, str, str, str, str)
+        for (uri, category, artist, title, album, mime) in songs_list:
+            text = "<b>%s</b>\n<small>%s</small>" % (escape_html (title),
+                                                     escape_html (artist) + " / " + escape_html (album))
+            self.full_model.append ((uri, category, artist, title, album, mime, text))
+            self.artist_set.insert (artist)
+            self.albums_set.insert (album)
+            
+        self.filtered_model = self.full_model.filter_new ()
+        self.treeview.set_model (self.full_model)
+        self.kpid = self.connect ("key-press-event", self.key_pressed_cb)
+
+    def __create_view (self):
+        vbox = gtk.VBox (homogeneous=False)
+        
+        self.treeview = gtk.TreeView ()
+        self.treeview.connect ("row-activated", self.row_activated_cb)
+
+        desc_column = gtk.TreeViewColumn ("Song", gtk.CellRendererText (), markup=6)
+        desc_column.set_expand (True)
+        self.treeview.append_column (desc_column)
+        self.treeview.set_enable_search (True)
+        self.treeview.set_search_column (6)
+        self.treeview.set_search_equal_func (self.entry_equals)
+
+        pannable_area = hildon.PannableArea ()
+        #pannable_area = gtk.ScrolledWindow ()
+        pannable_area.add (self.treeview)
+        
+        vbox.pack_start (pannable_area, expand=True)
+        
+        self.search_hbox = gtk.HBox ()
+        self.search_entry = gtk.Entry ()
+        self.search_hbox.pack_start (self.search_entry, expand=True)
+        
+        self.search_close = gtk.Button (stock=gtk.STOCK_CLOSE)
+        self.search_hbox.pack_start (self.search_close, expand=False)
+        self.search_close.connect ("clicked", self.close_search_cb)
+        
+        # Hide it when the window is created
+        self.search_box_visible = False
+        self.search_hbox.set_no_show_all (True)
+        self.search_hbox.hide ()
+        vbox.pack_start (self.search_hbox, expand=False)
+        self.add (vbox)
+
+    def search_type (self, widget):
+        print "Search"
+        self.filtered_model.set_visible_func (self.entry_equals, widget)
+        self.filtered_model.refilter ()
+        self.treeview.set_model (self.filtered_model)
+
+    def close_search_cb (self, widget):
+        assert not self.search_box_visible
+        self.search_hbox.hide_all ()
+        self.search_entry.set_text ("")
+        self.search_box_visible = False
+        self.kpid = self.connect ("key-press-event", self.key_pressed_cb)
+
+    def key_pressed_cb (self, widget, event):
+        if (event.type == gtk.gdk.KEY_PRESS):
+            if (event.keyval == gtk.gdk.keyval_from_name ("Alt_L")):
+                return
+            
+            if (not self.search_box_visible ):
+                self.search_hbox.set_no_show_all (False)
+                self.search_hbox.show_all ()
+                
+            print "key:", gtk.gdk.keyval_name(event.keyval)
+            self.search_entry.grab_focus ()
+            self.search_entry.connect ("changed", self.search_type)
+            self.disconnect (self.kpid)
+            
+    
+    def entry_equals (self, model, it, user_data):
+        t = user_data.get_text ()
+        return t.lower () in model.get_value (it, 6).lower()
+
+    def row_activated_cb (self, treeview, path, view_colum):
+        edit_view = MussorgskyEditPanel ()
+        edit_view.set_artist_alternatives (self.artist_set.as_list ())
+        edit_view.set_album_alternatives (self.albums_set.as_list ())
+        
+        edit_view.set_model (treeview.get_model(), treeview.get_model ().get_iter (path))
+        edit_view.show_all ()
+
+
+if __name__ == "__main__":
+
+    import random
+    def get_random_path ():
+        path = "file://"
+        for i in range (0, random.randint (1, 8)):
+            path = path + "/" + ("x"* random.randint (4, 12))
+        return path
+            
+
+    songs = [(get_random_path (),
+              "Music",
+              "Artist%d" % i,
+              "Title <%d>" % i,
+              "album <%d>" % i,
+              "audio/mp3") for i in range (0, 100)]
+
+    window = MussorgskyBrowsePanel (songs)
+    window.connect ("destroy", gtk.main_quit )
+    window.show_all ()
+    gtk.main ()
diff --git a/src/edit_panel_tm.py b/src/edit_panel_tm.py
new file mode 100644 (file)
index 0000000..201035b
--- /dev/null
@@ -0,0 +1,335 @@
+#!/usr/bin/env python2.5
+import hildon
+import gtk, gobject
+from mutagen_backend import MutagenBackend
+from player_backend import MediaPlayer
+import album_art_spec
+import os
+
+# Fields in the tuple!
+FILE_URI = 0
+ARTIST_KEY = 2
+TITLE_KEY = 3
+ALBUM_KEY = 4
+MIME_KEY = 5
+
+class MussorgskyEditPanel (hildon.StackableWindow):
+
+    def __init__ (self):
+        hildon.StackableWindow.__init__ (self)
+        self.set_border_width (12)
+        self.album_callback_id = -1
+        self.album_change_handler = -1
+        self.artist_change_handler = -1
+        self.writer = MutagenBackend ()
+        self.player = MediaPlayer ()
+        self.__create_view ()
+        self.data_loaded = False
+
+    def update_title (self):
+        self.set_title ("Edit (%d/%d)" % (self.model.get_path (self.current)[0] + 1,
+                                          len (self.model)))
+
+    def get_current_row (self):
+        if (not self.current):
+            return 6 * (None,)
+        return self.model.get (self.current, 0, 1, 2, 3, 4, 5)
+
+    def set_model (self, model, current=None):
+        self.model = model
+        if (current):
+            self.current = current
+        else:
+            self.current = self.model.get_iter_first ()
+
+        self.set_data_in_view (self.get_current_row ())
+        self.update_title ()
+
+    def set_artist_alternatives (self, alternatives):
+        self.artists_list = alternatives
+        artist_selector = hildon.TouchSelectorEntry (text=True)
+        for a in self.artists_list:
+            artist_selector.append_text (a)
+        self.artist_button.set_selector (artist_selector)
+
+    def set_album_alternatives (self, alternatives):
+        self.albums_list = alternatives
+        album_selector = hildon.TouchSelectorEntry (text=True)
+        for a in self.albums_list:
+            album_selector.append_text (a)
+        self.album_button.set_selector (album_selector)
+
+
+    def press_back_cb (self, widget):
+        if (self.player.is_playing ()):
+            self.player.stop ()
+
+        if (self.album_callback_id != -1):
+            gobject.source_remove (self.album_callback_id)
+            self.album_callback_id = -1
+
+        if self.__is_view_dirty ():
+            print "Modified data. Save!"
+            self.save_metadata ()
+            
+        self.current = self.model.iter_next (self.current)
+        if (not self.current):
+            self.destroy ()
+            
+        self.set_data_in_view (self.get_current_row ())
+
+    def press_next_cb (self, widget):
+        if (self.player.is_playing ()):
+            self.player.stop ()
+
+        if (self.album_callback_id != -1):
+            gobject.source_remove (self.album_callback_id)
+            self.album_callback_id = -1
+
+        if self.__is_view_dirty ():
+            print "Modified data. Save!"
+            self.save_metadata ()
+
+        self.current = self.model.iter_next (self.current)
+        if (not self.current):
+            print "Destroy"
+            self.destroy ()
+        else:
+            self.set_data_in_view (self.get_current_row ())
+            self.update_title ()
+            
+    def save_metadata (self):
+        # Save the data in the online model to show the appropiate data
+        # in the UI while tracker process the update.
+
+        # 0 - filename -> doesn't change
+        # 1 - "Music"  -> doesn't change
+        # 5 - mimetype -> doesn't change
+        if (type (self.model) == gtk.TreeModelFilter):
+            m = self.model.get_model ()
+        else:
+            m = self.model
+        m.set (self.current,
+               2, self.artist_button.get_value (),
+               3, self.title_entry.get_text (),
+               4, self.album_button.get_value ())
+        new_song = self.get_current_row ()
+        try:
+            self.writer.save_metadata_on_file (new_song[FILE_URI],
+                                               new_song[MIME_KEY],
+                                               self.artist_button.get_value (),
+                                               self.title_entry.get_text (),
+                                               self.album_button.get_value ())
+        except IOError, e:
+            # This error in case of tracker returning unexistent files.
+            # Uhm.... for instance after removing a memory card we are editing!
+            dialog = gtk.MessageDialog (self,
+                                        gtk.DIALOG_DESTROY_WITH_PARENT,
+                                        gtk.MESSAGE_ERROR,
+                                        gtk.BUTTONS_CLOSE,
+                                        "%s" % str(e));
+            dialog.run ()
+
+        
+
+    def __is_view_dirty (self):
+        """
+        True if the data has been modified in the widgets
+        """
+        song = self.get_current_row ()
+
+        return not (self.filename_data.get_text() == song[FILE_URI] and
+                    self.artist_button.get_value () == song[ARTIST_KEY] and
+                    self.title_entry.get_text () == song[TITLE_KEY] and
+                    self.album_button.get_value () == song[ALBUM_KEY] )
+        
+
+    def __create_view (self):
+        view_vbox = gtk.VBox (homogeneous=False, spacing = 12)
+
+        filename_row = gtk.HBox ()
+        filename_label = gtk.Label ("Filename:")
+        filename_row.pack_start (filename_label, expand=False, padding=12);
+        self.filename_data = gtk.Label ("")
+        filename_row.pack_start (self.filename_data, expand=True)
+
+        play_button = hildon.Button (hildon.BUTTON_STYLE_NORMAL, hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
+        img = gtk.image_new_from_stock (gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON)
+        play_button.set_image (img)
+        play_button.connect ("clicked", self.clicked_play)
+        filename_row.pack_start (play_button, expand=False, fill=False)
+        view_vbox.pack_start (filename_row, expand=False);
+
+        central_panel = gtk.HBox (spacing=12)
+
+        table = gtk.Table (3, 2, False)
+        table.set_col_spacings (12)
+        table.set_row_spacings (12)
+
+        central_panel.pack_start (table, fill=True)
+        view_vbox.pack_start (central_panel, expand=True, fill=True)
+
+        # Title row
+        label_title = gtk.Label ("Title:")
+        table.attach (label_title, 0, 1, 0, 1, 0)
+        self.title_entry = gtk.Entry()
+        table.attach (self.title_entry, 1, 2, 0, 1)
+
+        # Artist row
+        self.artist_button = hildon.PickerButton (hildon.BUTTON_STYLE_NORMAL,
+                                                  hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
+        self.artist_button.set_title ("Artist: ")
+        # Set data will set the selector
+        table.attach (self.artist_button, 0, 2, 1, 2)
+
+
+        # Album row
+        self.album_button = hildon.PickerButton (hildon.BUTTON_STYLE_NORMAL,
+                                                 hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
+        self.album_button.set_title ("Album: ")
+        # set_data will set the selector
+        table.attach (self.album_button, 0, 2, 2, 3) 
+        
+
+        # Album art space
+        self.album_art = gtk.Image ()
+        self.album_art.set_size_request (124, 124)
+        central_panel.pack_start (self.album_art, expand=False, fill=False)
+        
+        # Buttons row
+        button_box = gtk.HButtonBox ()
+        button_box.set_layout (gtk.BUTTONBOX_END)
+
+        back_button = hildon.Button (hildon.BUTTON_STYLE_NORMAL, hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
+        img = gtk.image_new_from_stock (gtk.STOCK_GO_BACK, gtk.ICON_SIZE_BUTTON)
+        back_button.set_image (img)
+        back_button.connect ("clicked", self.press_back_cb)
+        button_box.pack_start (back_button, expand=True, fill=True, padding=6)
+        
+        next_button = hildon.Button (hildon.BUTTON_STYLE_NORMAL, hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
+        img = gtk.image_new_from_stock (gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_BUTTON)
+        next_button.set_image (img)
+        next_button.connect ("clicked", self.press_next_cb)
+        button_box.pack_start (next_button, expand=True, fill=True, padding=6)
+        
+        view_vbox.pack_start (button_box, expand=False, fill=True, padding=6)
+        
+        self.add (view_vbox)
+
+
+    def go_to_cb (self, widget):
+        pass
+
+    def set_data_in_view (self, song):
+        """
+        Place in the screen the song information.
+        Song is a tuple like (filename, 'Music', title, artist, album, mime)
+        """
+        assert len (song) == 6
+        
+        self.filename_data.set_text (song[FILE_URI])
+        self.title_entry.set_text (song[TITLE_KEY])
+        
+
+        # Disconnect the value-change signal to avoid extra album art retrievals
+        if (self.album_button.handler_is_connected (self.album_change_handler)):
+            self.album_button.disconnect (self.album_change_handler)
+            
+        if (self.artist_button.handler_is_connected (self.artist_change_handler)):
+            self.artist_button.disconnect (self.artist_change_handler)
+
+        # Set values in the picker buttons
+        try:
+            self.artist_button.set_active (self.artists_list.index(song[ARTIST_KEY]))
+        except ValueError:
+            print "'%s' not in artist list!?" % (song[ARTIST_KEY])
+            self.artist_button.set_value ("")
+            
+        try:
+            self.album_button.set_active (self.albums_list.index (song[ALBUM_KEY]))
+        except ValueError:
+            print "'%s' is not in the album list!?" % (song[ALBUM_KEY])
+            self.album_button.set_value ("")
+
+        # Reconnect the signals!
+        self.album_change_handler = self.album_button.connect ("value-changed",
+                                                               self.album_selection_cb)
+
+        self.artist_change_handler = self.artist_button.connect ("value-changed",
+                                                                 self.artist_selection_cb)
+
+        # Set the album art given the current data
+        self.set_album_art (song[ALBUM_KEY])
+
+        if (not song[MIME_KEY] in self.writer.get_supported_mimes ()):
+            self.artist_button.set_sensitive (False)
+            self.album_button.set_sensitive (False)
+            self.title_entry.set_sensitive (False)
+        else:
+            self.artist_button.set_sensitive (True)
+            self.album_button.set_sensitive (True)
+            self.title_entry.set_sensitive (True)
+
+    def set_album_art (self, album):
+        has_album = False
+        if (album):
+            thumb = album_art_spec.getCoverArtThumbFileName (album)
+           print "%s -> %s" % (album, thumb)
+            if (os.path.exists (thumb)):
+                self.album_art.set_from_file (thumb)
+                has_album = True
+            
+        if (not has_album):
+            self.album_art.set_from_stock (gtk.STOCK_CDROM, gtk.ICON_SIZE_DIALOG)
+
+        
+    def clicked_play (self, widget):
+        if (self.player.is_playing ()):
+            self.player.stop ()
+        else:
+            song = self.songs_list [self.song_counter]
+            self.player.play ("file://" + song[FILE_URI])
+
+    def clicked_album_art (self, widget):
+        print "implement me, please"
+
+    def album_selection_cb (self, widget):
+        """
+        On album change, add the album the local list of albums and the selector
+        if it doesn't exist already. So we show the new entry in the selector next time.
+        """
+        song = self.get_current_row ()
+        if (not widget.get_value () in self.albums_list):
+            print "Inserting ", widget.get_value ()
+            widget.get_selector ().prepend_text (widget.get_value ())
+            self.albums_list.insert (0, widget.get_value ())
+        self.set_album_art (widget.get_value ())
+
+    def artist_selection_cb (self, widget):
+        """
+        On artist change, add the artist the local list of artists and the selector
+        if it doesn't exist already. So we show the new entry in the selector next time
+        """
+        song = self.get_current_row ()
+        if (not widget.get_value () in self.artists_list):
+            print "Inserting artist", widget.get_value ()
+            widget.get_selector ().prepend_text (widget.get_value ())
+            self.artists_list.insert (0, widget.get_value ())
+    
+# Testing porpuses
+if __name__ == "__main__":
+
+    TEST_DATA = [("/home/user/Music/dylan.mp3", "Music", "Bob Dylan", "Subterranean homesick blues", "Bring it all back home", "audio/mpeg"),
+                 ("/home/user/mufix/a.mp3", "Music", "", "title", "Album 2", "a/b"),
+                ("/media/mmc1/Attachments/b.m4a", "Music", "", "b", "Album 9", "audio/mpeg"),
+                 ("/home/user/mufix/3.mp2", "Music", "", "titlex", "Album 3", "audio/mpeg")]
+
+    model = gtk.ListStore (str, str, str, str, str, str)
+    for t in TEST_DATA:
+        model.append (t)
+
+    window = MussorgskyEditPanel ()
+    window.set_model (model)
+    window.connect ("destroy", gtk.main_quit)
+    window.show_all ()
+    gtk.main ()