From aff36248e380b212927e164fe16930daeda3229c Mon Sep 17 00:00:00 2001 From: Ivan Frade Date: Mon, 7 Sep 2009 00:28:52 +0300 Subject: [PATCH] Add new browser panel and updated edit panel Browser panel using a treeview to browse the music collection. Include interactive search. Edit panel using a gtk.TreeModel as store --- setup.py | 2 + src/browse_panel.py | 128 +++++++++++++++++++ src/edit_panel_tm.py | 335 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 465 insertions(+) create mode 100755 src/browse_panel.py create mode 100644 src/edit_panel_tm.py diff --git a/setup.py b/setup.py index bd8d92e..2365f37 100644 --- 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 index 0000000..57b39c0 --- /dev/null +++ b/src/browse_panel.py @@ -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 = "%s\n%s" % (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 index 0000000..201035b --- /dev/null +++ b/src/edit_panel_tm.py @@ -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 () -- 1.7.9.5