Hi Aaron,
Attached is the patch for HEAD.
Please let me know when you review. If you are ok with it and want to commit, I can get a ChangeLog ready for you..
Best
Ulas.
Index: src/PlayerInterface.cs =================================================================== RCS file: /cvs/gnome/banshee/src/PlayerInterface.cs,v retrieving revision 1.128 diff -u -r1.128 PlayerInterface.cs --- src/PlayerInterface.cs 5 Jan 2006 22:48:36 -0000 1.128 +++ src/PlayerInterface.cs 7 Jan 2006 15:50:30 -0000 @@ -59,6 +59,8 @@ [Widget] private Gtk.Label LabelInfo; [Widget] private HPaned SourceSplitter; [Widget] private Button HeaderCycleButton; + [Widget] private Expander CustomExpander; + [Widget] private Gtk.VBox DisclosureBox; private PlaylistModel playlistModel; @@ -75,6 +77,7 @@ private Tooltips toolTips; private Hashtable playlistMenuMap; private Viewport sourceViewLoadingVP; + private BrowserBox browser; private MultiStateToggleButton repeat_toggle_button; private MultiStateToggleButton shuffle_toggle_button; @@ -442,6 +445,14 @@ gxml["SearchLabel"].Sensitive = false; searchEntry.Sensitive = false; + + // Custom Expander and browser related stuff. + CustomExpander = ((Expander)gxml["CustomExpander"]); + DisclosureBox = ((VBox)gxml["DisclosureBox"]); + browser = new BrowserBox(); + DisclosureBox.Add(browser); + CustomExpander.Activated += OnExpandAreaClicked; + browser.BrowserSearchActivated += OnBrowserSearchActivated; // Repeat/Shuffle buttons @@ -642,6 +653,8 @@ PromptForImport(); }); } + + browser.BrowserReset(); } private bool PromptForImportTimeout() @@ -782,6 +795,17 @@ handled = true; } break; + case Gdk.Key.F5: + if((args.Event.State & Gdk.ModifierType.ControlMask) != 0) { + if (CustomExpander.Expanded) { + CustomExpander.Expanded = false; + } else { + CustomExpander.Expanded = true; + } + OnExpandAreaClicked(this, new EventArgs()); + handled = true; + } + break; case Gdk.Key.Left: if((args.Event.State & Gdk.ModifierType.ControlMask) != 0) { PlayerEngineCore.ActivePlayer.Position -= SkipDelta; @@ -1073,13 +1097,15 @@ private void OnSourceManagerActiveSourceChanged(SourceEventArgs args) { ThreadAssist.ProxyToMain(HandleSourceChanged); + browser.BrowserReset(); } private void OnSourceManagerSourceUpdated(SourceEventArgs args) { if(args.Source == SourceManager.ActiveSource) { UpdateViewName(args.Source); - } + } + browser.BrowserUpdate(); } private void UpdateViewName(Source source) @@ -1525,16 +1551,26 @@ playlistModel.Clear(); if(!searchEntry.IsQueryAvailable) { - playlistModel.ReloadSource(); + if (browser.BrowserHasFilter()) { + playlistModel.SearchSource(null,null); + } else { + playlistModel.ReloadSource(); + } + return; } - ICollection collection = SourceManager.ActiveSource.Tracks; - - foreach(TrackInfo track in collection) { + ICollection collection = null; + if (browser.BrowserHasFilter()) { + collection = playlistModel.GetBrowsedTracks(); + } else { + collection = SourceManager.ActiveSource.Tracks; + } + + foreach(TrackInfo ti in collection) { try { - if(DoesTrackMatchSearch(track)) { - playlistModel.AddTrack(track); + if(DoesTrackMatchSearch(ti)) { + playlistModel.AddTrack(ti); } } catch(Exception) { continue; @@ -1949,6 +1985,7 @@ } playlistModel.AddTrack(ti); + browser.BrowserFilter(); }); } } catch(Entagged.Audioformats.Exceptions.CannotReadException) { @@ -2051,6 +2088,14 @@ playlistView.QueueDraw(); } + private void OnBrowserSearchActivated(object o, BrowserSearchEventArgs args) + { + playlistModel.SearchSource(args.artist, args.album); + if(searchEntry.IsQueryAvailable) { + OnSimpleSearch(o, new EventArgs()); + } + } + private void OnLibraryTrackRemoveFinished(object o, EventArgs args) { } @@ -2408,6 +2453,17 @@ } log_viewer.Show(); + } + + // --- Expander --- + + private void OnExpandAreaClicked (object sender, EventArgs args) + { + if (CustomExpander.Expanded) { + DisclosureBox.Visible = true; + } else { + DisclosureBox.Visible = false; + } } // --- Help Menu --- Index: src/PlaylistModel.cs =================================================================== RCS file: /cvs/gnome/banshee/src/PlaylistModel.cs,v retrieving revision 1.31 diff -u -r1.31 PlaylistModel.cs --- src/PlaylistModel.cs 6 Jan 2006 06:12:52 -0000 1.31 +++ src/PlaylistModel.cs 7 Jan 2006 15:50:35 -0000 @@ -50,6 +50,7 @@ private TimeSpan totalDuration = new TimeSpan(0); private ArrayList trackInfoQueue; + private ArrayList browsedTracks; private bool trackInfoQueueLocked = false; private TreeIter playingIter; @@ -68,6 +69,7 @@ public PlaylistModel() : base(typeof(TrackInfo)) { trackInfoQueue = new ArrayList(); + browsedTracks = new ArrayList(); GLib.Timeout.Add(300, new GLib.TimeoutHandler(OnIdle)); SourceManager.ActiveSourceChanged += delegate(SourceEventArgs args) { ReloadSource(); @@ -158,6 +160,67 @@ SyncPlayingIter(); } + + public void SearchSource(BrowserEventInfo beiArtist, BrowserEventInfo beiAlbum) + { + ClearModel(); + if (beiArtist != null && beiAlbum!= null) { + browsedTracks.Clear(); + ICollection collection = SourceManager.ActiveSource.Tracks; + foreach(TrackInfo ti in collection) { + try { + if(DoesTrackMatchSearch(ti, beiArtist, beiAlbum)) { + AddTrack(ti); + browsedTracks.Add(ti); + } + } catch(Exception) { + continue; + } + } + + } else { + foreach(TrackInfo ti in browsedTracks) { + try { + AddTrack(ti); + } catch(Exception) { + continue; + } + } + } + } + + public ICollection GetBrowsedTracks() + { + return browsedTracks; + } + + private bool DoesTrackMatchSearch(TrackInfo ti, BrowserEventInfo Artist, BrowserEventInfo Album) + { + string artist = Artist.StrData.ToLower(); + string album = Album.StrData.ToLower(); + string ti_artist = ti.Artist.ToString().ToLower(); + string ti_album = ti.Album.ToString().ToLower(); + int artist_index = Artist.IntIndex; + int album_index = Album.IntIndex; + + if (artist_index == 0 && album_index == 0) { + return true; + } else if (artist_index == 0) { + if (album.Equals(ti_album)) { + return true; + } + } else if (album_index == 0) { + if (artist.Equals(ti_artist)) { + return true; + } + } else if (album_index != 0 && artist_index != 0) { + if (artist.Equals(ti_artist) && album.Equals(ti_album)) { + return true; + } + } + + return false; + } // --- Helper Methods --- Index: src/Makefile.am =================================================================== RCS file: /cvs/gnome/banshee/src/Makefile.am,v retrieving revision 1.84 diff -u -r1.84 Makefile.am --- src/Makefile.am 5 Jan 2006 22:48:36 -0000 1.84 +++ src/Makefile.am 7 Jan 2006 15:50:35 -0000 @@ -58,6 +58,7 @@ ExceptionDialog.cs \ VersionInformationDialog.cs \ LogCoreViewer.cs \ + SourceBrowser.cs \ ToggleStates.cs banshee_resources = \ Index: data/banshee.glade =================================================================== RCS file: /cvs/gnome/banshee/data/banshee.glade,v retrieving revision 1.5 diff -u -r1.5 banshee.glade --- data/banshee.glade 5 Jan 2006 22:48:34 -0000 1.5 +++ data/banshee.glade 7 Jan 2006 15:50:42 -0000 @@ -249,22 +249,38 @@ <property name="spacing">5</property> <child> - <widget class="GtkLabel" id="ViewNameLabel"> + <widget class="GtkExpander" id="CustomExpander"> <property name="visible">True</property> - <property name="label" translatable="yes"><b>Loading...</b></property> - <property name="use_underline">False</property> - <property name="use_markup">True</property> - <property name="justify">GTK_JUSTIFY_LEFT</property> - <property name="wrap">False</property> - <property name="selectable">False</property> - <property name="xalign">0</property> - <property name="yalign">0.5</property> - <property name="xpad">0</property> - <property name="ypad">4</property> - <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> - <property name="width_chars">-1</property> - <property name="single_line_mode">False</property> - <property name="angle">0</property> + <property name="can_focus">True</property> + <property name="expanded">False</property> + <property name="spacing">0</property> + + <child> + <placeholder/> + </child> + + <child> + <widget class="GtkLabel" id="ViewNameLabel"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>Loading...</b></property> + <property name="use_underline">False</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">4</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> </widget> <packing> <property name="padding">0</property> @@ -373,30 +389,57 @@ </child> <child> - <widget class="GtkAlignment" id="LibraryAlignment"> + <widget class="GtkVPaned" id="VPane"> <property name="visible">True</property> - <property name="xalign">0.5</property> - <property name="yalign">0.5</property> - <property name="xscale">1</property> - <property name="yscale">1</property> - <property name="top_padding">0</property> - <property name="bottom_padding">0</property> - <property name="left_padding">0</property> - <property name="right_padding">0</property> + <property name="can_focus">True</property> + <property name="position">200</property> <child> - <widget class="GtkScrolledWindow" id="LibraryContainer"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> - <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> - <property name="shadow_type">GTK_SHADOW_IN</property> - <property name="window_placement">GTK_CORNER_TOP_LEFT</property> + <widget class="GtkVBox" id="DisclosureBox"> + <property name="homogeneous">False</property> + <property name="spacing">0</property> <child> <placeholder/> </child> </widget> + <packing> + <property name="shrink">False</property> + <property name="resize">False</property> + </packing> + </child> + + <child> + <widget class="GtkAlignment" id="LibraryAlignment"> + <property name="visible">True</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xscale">1</property> + <property name="yscale">1</property> + <property name="top_padding">0</property> + <property name="bottom_padding">0</property> + <property name="left_padding">0</property> + <property name="right_padding">0</property> + + <child> + <widget class="GtkScrolledWindow" id="LibraryContainer"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="shadow_type">GTK_SHADOW_IN</property> + <property name="window_placement">GTK_CORNER_TOP_LEFT</property> + + <child> + <placeholder/> + </child> + </widget> + </child> + </widget> + <packing> + <property name="shrink">True</property> + <property name="resize">True</property> + </packing> </child> </widget> <packing> --- src/SourceBrowser.cs 2006-01-07 19:25:49.000000000 +0200 +++ src/SourceBrowser.cs 2006-01-07 19:20:22.000000000 +0200 @@ -0,0 +1,431 @@ +/* -*- Mode: csharp; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: t -*- */ +/*************************************************************************** + * SourceBrowser.cs + * + * + * Written by Aydemir Ulas Sahin (ulas arttek com tr) + ****************************************************************************/ + +/* THIS FILE IS LICENSED UNDER THE MIT LICENSE AS OUTLINED IMMEDIATELY BELOW: + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +using System; +using System.Collections; +using System.Collections.Specialized; +using GLib; +using Gtk; +using Mono.Unix; + +using Banshee.Base; +using Banshee.Sources; + +namespace Banshee +{ + public delegate void BrowserSearchEventHandler(object o, BrowserSearchEventArgs args); + + public class BrowserEventInfo + { + private string strdata; + private int index; + + public BrowserEventInfo(string str_data, int int_index) + { + this.StrData = str_data; + this.IntIndex = int_index; + } + + + public string StrData + { + get { + return this.strdata; + } + set { + this.strdata = value; + } + } + + public int IntIndex + { + get { + return this.index; + } + set { + this.index = value; + } + } + } + + + public class BrowserSearchEventArgs : EventArgs + { + public BrowserEventInfo artist; + public BrowserEventInfo album; + + public BrowserSearchEventArgs(BrowserEventInfo beiArtist, BrowserEventInfo beiAlbum) + { + this.artist = beiArtist; + this.album = beiAlbum; + } + } + + public class SourceBrowser : VBox + { + private Gtk.HBox main_box = new Gtk.HBox(); + private Gtk.ScrolledWindow artist_scroller = new Gtk.ScrolledWindow(); + private Gtk.ScrolledWindow album_scroller = new Gtk.ScrolledWindow(); + private Gtk.TreeView artist_tree = new Gtk.TreeView(); + private Gtk.TreeView album_tree = new Gtk.TreeView(); + + private ListStore artist_store; + private ListStore album_store; + + private string artist_selected = "none"; + private string album_selected = "none"; + + private int artist_selection_index = 0; + private int album_selection_index = 0; + + private int [] ResetSelectPath = {0}; + + public event BrowserSearchEventHandler BrowserClicked; + + public SourceBrowser() : base() + { + BuildUI(); + } + + private void BuildUI() + { + artist_scroller.HscrollbarPolicy = PolicyType.Automatic; + artist_scroller.VscrollbarPolicy = PolicyType.Always; + artist_scroller.ShadowType = ShadowType.EtchedIn; + artist_scroller.BorderWidth = 3; + + album_scroller.HscrollbarPolicy = PolicyType.Automatic; + album_scroller.VscrollbarPolicy = PolicyType.Always; + album_scroller.ShadowType = ShadowType.EtchedIn; + album_scroller.BorderWidth = 3; + + artist_tree.HeadersVisible = true; + artist_tree.AppendColumn(Catalog.GetString("Artist"), new CellRendererText(), "text", 0); + artist_store = CreateStore(); + artist_tree.Model = artist_store; + artist_tree.CursorChanged += OnArtistCursorChanged; + artist_scroller.Add(artist_tree); + + album_tree.HeadersVisible = true; + album_tree.AppendColumn(Catalog.GetString("Album"), new CellRendererText(), "text", 0); + album_store = CreateStore(); + album_tree.Model = album_store; + album_tree.CursorChanged += OnAlbumCursorChanged; + album_scroller.Add(album_tree); + + main_box.PackStart(artist_scroller, true, true, 0); + main_box.PackStart(album_scroller, true, true, 0); + + PackStart(main_box, true, true, 0); + Spacing = 5; + + this.ShowAll(); + } + + public void Reset() + { + ResetArtistList(); + ResetAlbumList(); + PopulateAlbumStore(false); + } + + public void Update(bool isFiltered) + { + PopulateArtistStore(true); + PopulateAlbumStore(true); + if(isFiltered) { + RunSearch(); + } + } + + public bool FilterStatus() + { + if(artist_selection_index == 0 && album_selection_index == 0) { + return false; + } else { + return true; + } + } + + private ListStore CreateStore() + { + ListStore store = new ListStore(typeof(string)); + return store; + } + + private ICollection SortTracks(ICollection collection, TrackInfoComparer.Types type) + { + ArrayList sortedlist = new ArrayList(); + sortedlist.AddRange(collection); + sortedlist.Sort(new TrackInfoComparer(type)); + return sortedlist; + } + + private void PopulateArtistStore(bool isUpdate) + { + artist_store.Clear(); + StringCollection artistlist = new StringCollection(); + + try + { + ICollection collection = SortTracks(SourceManager.ActiveSource.Tracks, + TrackInfoComparer.Types.artist); + foreach(TrackInfo track in collection) { + if (!artistlist.Contains(track.Artist)) { + artistlist.Add(track.Artist as string); + } + } + } catch(Exception) { + + } + + AddEntry("All", true, artist_store); + foreach(string entry in artistlist) { + AddEntry(entry, false, artist_store); + } + + if (isUpdate) { + UpdateArtistList(); + } else { + ResetArtistList(); + } + } + + private void PopulateAlbumStore(bool isUpdate) + { + album_store.Clear(); + StringCollection albumlist = new StringCollection(); + + try + { + ICollection collection = SortTracks(SourceManager.ActiveSource.Tracks, + TrackInfoComparer.Types.album); + if (artist_selection_index == 0) { + foreach(TrackInfo track in collection) { + if (!albumlist.Contains(track.Album)) { + albumlist.Add(track.Album as string); + } + } + } else { + foreach(TrackInfo track in collection) { + if (!albumlist.Contains(track.Album)) { + if (track.Artist.Equals(artist_selected)) { + albumlist.Add(track.Album as string); + } + } + } + } + } catch(Exception) { + + } + + AddEntry("All", true, album_store); + foreach(string entry in albumlist) { + AddEntry(entry, false, album_store); + } + + if (isUpdate) { + UpdateAlbumList(); + } else { + ResetAlbumList(); + } + } + + private void UpdateAlbumList() + { + int [] UpdateSelectPath = {album_selection_index }; + album_tree.Selection.SelectPath(new TreePath(UpdateSelectPath)); + } + + private void UpdateArtistList() + { + int [] UpdateSelectPath = {artist_selection_index }; + artist_tree.Selection.SelectPath(new TreePath(UpdateSelectPath)); + } + + private void ResetAlbumList() + { + album_tree.Selection.SelectPath(new TreePath(ResetSelectPath)); + TreePath[] treeIndex = album_tree.Selection.GetSelectedRows(); + album_selection_index = treeIndex[0].Indices[0]; + } + + private void ResetArtistList() + { + artist_tree.Selection.SelectPath(new TreePath(ResetSelectPath)); + TreePath[] treeIndex = artist_tree.Selection.GetSelectedRows(); + artist_selection_index = treeIndex[0].Indices[0]; + } + + private void AddEntry(string entry, bool prepend,ListStore store) + { + TreeIter iter = prepend ? store.Insert(0) : store.Append(); + store.SetValue(iter, 0, entry); + } + + private void OnArtistCursorChanged(object o, EventArgs args) + { + TreeIter iter; + if(!artist_tree.Selection.GetSelected(out iter)) { + return; + } + + object artist_name = artist_store.GetValue(iter, 0); + artist_selected = (artist_name as string); + + TreePath[] treeIndex = artist_tree.Selection.GetSelectedRows(); + artist_selection_index = treeIndex[0].Indices[0]; + + if(artist_name == null) { + return; + } + + PopulateAlbumStore(false); + RunSearch(); + } + + private void OnAlbumCursorChanged(object o, EventArgs args) + { + TreeIter iter; + if(!album_tree.Selection.GetSelected(out iter)) { + return; + } + + object album_name = album_store.GetValue(iter, 0); + album_selected = (album_name as string); + + TreePath[] treeIndex = album_tree.Selection.GetSelectedRows(); + album_selection_index = treeIndex[0].Indices[0]; + + if(album_name == null) { + return; + } + + RunSearch(); + } + + private void RunSearch() + { + BrowserSearchEventHandler handler = BrowserClicked; + BrowserEventInfo bei_artist = new BrowserEventInfo(artist_selected, artist_selection_index); + BrowserEventInfo bei_album = new BrowserEventInfo(album_selected, album_selection_index); + BrowserSearchEventArgs browserArgs = new BrowserSearchEventArgs(bei_artist, bei_album); + if(handler != null) + { + handler(this, browserArgs); + } + } + } + + public class TrackInfoComparer : IComparer + { + public enum Types : int { artist = 0, album = 1 }; + + private int type = (int) TrackInfoComparer.Types.artist; + + public TrackInfoComparer() {} + + public TrackInfoComparer(Types type) { this.type = (int) type; } + + public int Compare(object x, object y) + { + try { + string xstring; + string ystring; + + if(this.type == 0) { + xstring = ((TrackInfo)x).Artist; + ystring = ((TrackInfo)y).Artist; + } else { + xstring = ((TrackInfo)x).Album; + ystring = ((TrackInfo)y).Album; + } + return xstring.CompareTo(ystring); + } catch (Exception){ + return 0; + } + } + } + + public class BrowserBox : VBox + { + private SourceBrowser browser; + + public event BrowserSearchEventHandler BrowserSearchActivated; + + public BrowserBox() : base() + { + BuildUI(); + } + + private void BuildUI() + { + browser = new SourceBrowser(); + browser.HeightRequest = 150; + browser.BrowserClicked += OnBrowserClicked; + + PackStart(browser,true,true,0); + + ShowAll(); + } + + private void OnBrowserClicked(object o, BrowserSearchEventArgs args) + { + BrowserSearchEventHandler handler = BrowserSearchActivated; + if(handler != null) + handler(this, args); + } + + public void BrowserReset() + { + browser.Update(false); + browser.Reset(); + } + + public void BrowserUpdate() + { + browser.Update(false); + } + + public void BrowserFilter() + { + browser.Update(true); + } + + public bool BrowserHasFilter() + { + return browser.FilterStatus(); + } + + public void Active(bool status) + { + this.Sensitive = status; + } + } +}