[banshee/typeahead] Add type-ahead find to ListView



commit 85882184a97fe42b423d0db090f08a4691efe3ed
Author: Neil Loknath <neil loknath gmail com>
Date:   Fri Nov 13 19:42:07 2009 -0800

    Add type-ahead find to ListView
    
    Still need to decide how to handle starting finding, since we have many
    keybindings that conflict with the normal type-anything approach.
    
    Signed-off-by: Gabriel Burt <gabriel burt gmail com>

 .../DatabaseAlbumListModel.cs                      |    2 +
 .../DatabaseArtistListModel.cs                     |    2 +
 .../DatabaseFilterListModel.cs                     |   21 ++-
 .../DatabaseQueryFilterModel.cs                    |    9 +
 .../DatabaseTrackListModel.cs                      |   19 ++-
 .../IDatabaseTrackModelCache.cs                    |    2 +
 .../Banshee.Collection.Database/ISearchable.cs     |   39 +++
 src/Core/Banshee.Services/Makefile.am              |    1 +
 .../Banshee.Collection.Gui/BaseTrackListView.cs    |    8 +-
 .../Banshee.Collection.Gui/SearchableListView.cs   |  275 ++++++++++++++++++++
 .../Banshee.Collection.Gui/TrackFilterListView.cs  |    2 +-
 src/Core/Banshee.ThickClient/Makefile.am           |    1 +
 .../Hyena.Data.Gui/ListView/ListView_Rendering.cs  |    2 +-
 .../Hyena.Data.Gui/ListView/ListView_Windowing.cs  |    4 +
 .../Hyena.Gui/Hyena.Widgets/EntryPopup.cs          |  226 ++++++++++++++++
 src/Libraries/Hyena.Gui/Makefile.am                |    1 +
 .../Hyena/Hyena.Data.Sqlite/SqliteModelCache.cs    |   41 +++-
 17 files changed, 647 insertions(+), 8 deletions(-)
---
diff --git a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseAlbumListModel.cs b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseAlbumListModel.cs
index 76c32b9..1c847da 100644
--- a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseAlbumListModel.cs
+++ b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseAlbumListModel.cs
@@ -35,6 +35,7 @@ using Mono.Unix;
 
 using Hyena;
 using Hyena.Data.Sqlite;
+using Hyena.Query;
 
 using Banshee.Database;
 
@@ -46,6 +47,7 @@ namespace Banshee.Collection.Database
             : base (Banshee.Query.BansheeQuery.AlbumField.Name, Banshee.Query.BansheeQuery.AlbumField.Label,
                     source, trackModel, connection, DatabaseAlbumInfo.Provider, new AlbumInfo (null), uuid)
         {
+            QueryFields = new QueryFieldSet (Banshee.Query.BansheeQuery.AlbumField);
             ReloadFragmentFormat = @"
                 FROM CoreAlbums WHERE CoreAlbums.AlbumID IN
                         (SELECT CoreTracks.AlbumID FROM CoreTracks, CoreCache{0}
diff --git a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseArtistListModel.cs b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseArtistListModel.cs
index 697f28c..f105a0f 100644
--- a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseArtistListModel.cs
+++ b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseArtistListModel.cs
@@ -34,6 +34,7 @@ using Mono.Unix;
 
 using Hyena;
 using Hyena.Data.Sqlite;
+using Hyena.Query;
 
 using Banshee.Database;
 
@@ -45,6 +46,7 @@ namespace Banshee.Collection.Database
             : base (Banshee.Query.BansheeQuery.ArtistField.Name, Banshee.Query.BansheeQuery.ArtistField.Label, 
                     source, trackModel, connection, DatabaseArtistInfo.Provider, new ArtistInfo (null, null), uuid)
         {
+            QueryFields = new QueryFieldSet (Banshee.Query.BansheeQuery.ArtistField);
             ReloadFragmentFormat = @"
                 FROM CoreArtists WHERE CoreArtists.ArtistID IN
                     (SELECT CoreTracks.ArtistID FROM CoreTracks, CoreCache{0}
diff --git a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseFilterListModel.cs b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseFilterListModel.cs
index 39342ee..ee5372c 100644
--- a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseFilterListModel.cs
+++ b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseFilterListModel.cs
@@ -35,13 +35,14 @@ using System.Collections.Generic;
 using Hyena;
 using Hyena.Data;
 using Hyena.Data.Sqlite;
+using Hyena.Query;
 
 using Banshee.Collection;
 using Banshee.Database;
 
 namespace Banshee.Collection.Database
 {   
-    public abstract class DatabaseFilterListModel<T, U> : FilterListModel<U>, ICacheableDatabaseModel
+    public abstract class DatabaseFilterListModel<T, U> : FilterListModel<U>, ICacheableDatabaseModel, ISearchable
         where T : U, new () where U : ICacheableItem, new()
     {
         private readonly BansheeModelCache<T> cache;
@@ -218,6 +219,24 @@ namespace Banshee.Collection.Database
                 OnReloaded ();
             }
         }
+
+        private QueryFieldSet query_fields;
+        public QueryFieldSet QueryFields {
+            get { return query_fields; }
+            protected set { query_fields = value; }
+        }
+        
+        public int IndexOf (QueryNode query, long offset)
+        {
+            lock (cache) {
+                if (query == null) {
+                    return -1;
+                }
+                
+                int index = (int) cache.IndexOf (query.ToSql (QueryFields), offset);
+                return index >= 0 ? index + 1 : index;
+            }
+        }
         
         public abstract string FilterColumn { get; }
 
diff --git a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseQueryFilterModel.cs b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseQueryFilterModel.cs
index b9ee544..6da6b5f 100644
--- a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseQueryFilterModel.cs
+++ b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseQueryFilterModel.cs
@@ -34,6 +34,8 @@ using Hyena.Query;
 using Hyena.Data;
 using Hyena.Data.Sqlite;
 
+using Mono.Unix;
+
 using Banshee.ServiceStack;
 
 namespace Banshee.Collection.Database
@@ -41,6 +43,11 @@ namespace Banshee.Collection.Database
     public class DatabaseQueryFilterModel<T> : DatabaseFilterListModel<QueryFilterInfo<T>, QueryFilterInfo<T>>
     {
         private QueryField field;
+        private readonly QueryField query_filter_field = new QueryField (
+            "itemid", "ItemID",
+            Catalog.GetString ("Value"), "CoreCache.ItemID", false
+        );
+        
         private string select_all_fmt;
 
         public DatabaseQueryFilterModel (Banshee.Sources.DatabaseSource source, DatabaseTrackListModel trackModel, 
@@ -54,6 +61,8 @@ namespace Banshee.Collection.Database
                 FROM CoreTracks, CoreCache{0}
                     WHERE CoreCache.ModelID = {1} AND CoreCache.ItemID = {2} {3}
                     ORDER BY Value";
+
+            QueryFields = new QueryFieldSet (query_filter_field);
         }
         
         public override bool CachesValues { get { return true; } }
diff --git a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackListModel.cs b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackListModel.cs
index afcf511..83bda73 100644
--- a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackListModel.cs
+++ b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackListModel.cs
@@ -48,7 +48,7 @@ using Banshee.PlaybackController;
 namespace Banshee.Collection.Database
 {       
     public class DatabaseTrackListModel : TrackListModel, IExportableModel, 
-        ICacheableDatabaseModel, IFilterable, ISortable, ICareAboutView
+        ICacheableDatabaseModel, IFilterable, ISortable, ICareAboutView, ISearchable
     {
         private readonly BansheeDbConnection connection;
         private IDatabaseTrackModelProvider provider;
@@ -319,6 +319,23 @@ namespace Banshee.Collection.Database
             cache.Reload ();
         }
 
+        private QueryFieldSet query_fields = BansheeQuery.FieldSet;
+        public QueryFieldSet QueryFields {
+            get { return query_fields; }
+            protected set { query_fields = value; }
+        }
+        
+        public int IndexOf (QueryNode query, long offset)
+        {
+            lock (this) {
+                if (query == null) {
+                    return -1;
+                }
+                
+                return (int) cache.IndexOf (query.ToSql (QueryFields), offset);
+            }
+        }
+        
         public override int IndexOf (TrackInfo track)
         {
             lock (this) {
diff --git a/src/Core/Banshee.Services/Banshee.Collection.Database/IDatabaseTrackModelCache.cs b/src/Core/Banshee.Services/Banshee.Collection.Database/IDatabaseTrackModelCache.cs
index 618e012..abbb98a 100644
--- a/src/Core/Banshee.Services/Banshee.Collection.Database/IDatabaseTrackModelCache.cs
+++ b/src/Core/Banshee.Services/Banshee.Collection.Database/IDatabaseTrackModelCache.cs
@@ -30,6 +30,7 @@ using System;
 using System.Data;
 using Banshee.Collection;
 using Hyena.Data.Sqlite;
+using Hyena.Query;
 
 namespace Banshee.Collection.Database
 {
@@ -41,6 +42,7 @@ namespace Banshee.Collection.Database
         void RestoreSelection ();
         long Count { get; }
         void Reload ();
+        long IndexOf (string where_fragment, long offset);
         long IndexOf (Hyena.Data.ICacheableItem item);
         long IndexOf (object item_entry_id);
         TrackInfo GetSingle (string random_fragment, params object [] args);
diff --git a/src/Core/Banshee.Services/Banshee.Collection.Database/ISearchable.cs b/src/Core/Banshee.Services/Banshee.Collection.Database/ISearchable.cs
new file mode 100644
index 0000000..3333c61
--- /dev/null
+++ b/src/Core/Banshee.Services/Banshee.Collection.Database/ISearchable.cs
@@ -0,0 +1,39 @@
+//
+// ISearchable.cs
+//
+// Author:
+//   Neil Loknath <neil loknath gmail com>
+//
+// Copyright (C) 2009 Neil Loknath
+//
+// 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 Hyena.Query;
+
+namespace Banshee.Collection.Database
+{
+    public interface ISearchable
+    {
+        QueryFieldSet QueryFields { get; }
+        int IndexOf (QueryNode query, long offset);
+    }
+}
\ No newline at end of file
diff --git a/src/Core/Banshee.Services/Makefile.am b/src/Core/Banshee.Services/Makefile.am
index b58ef80..1ac8785 100644
--- a/src/Core/Banshee.Services/Makefile.am
+++ b/src/Core/Banshee.Services/Makefile.am
@@ -21,6 +21,7 @@ SOURCES =  \
 	Banshee.Collection.Database/FilterModelProvider.cs \
 	Banshee.Collection.Database/IDatabaseTrackModelCache.cs \
 	Banshee.Collection.Database/IDatabaseTrackModelProvider.cs \
+	Banshee.Collection.Database/ISearchable.cs \
 	Banshee.Collection.Database/QueryFilterInfo.cs \
 	Banshee.Collection.Database/RandomBy.cs \
 	Banshee.Collection.Database/RandomByAlbum.cs \
diff --git a/src/Core/Banshee.ThickClient/Banshee.Collection.Gui/BaseTrackListView.cs b/src/Core/Banshee.ThickClient/Banshee.Collection.Gui/BaseTrackListView.cs
index fb52f7e..d9b106d 100644
--- a/src/Core/Banshee.ThickClient/Banshee.Collection.Gui/BaseTrackListView.cs
+++ b/src/Core/Banshee.ThickClient/Banshee.Collection.Gui/BaseTrackListView.cs
@@ -44,7 +44,7 @@ using Banshee.Gui;
 
 namespace Banshee.Collection.Gui
 {
-    public class BaseTrackListView : ListView<TrackInfo>
+    public class BaseTrackListView : SearchableListView<TrackInfo>
     {
         public BaseTrackListView () : base ()
         {
@@ -67,6 +67,10 @@ namespace Banshee.Collection.Gui
             };
         }
 
+        public override bool SelectOnRowFound {
+            get { return true; }
+        }
+        
         private static TargetEntry [] source_targets = new TargetEntry [] {
             ListViewDragDropTarget.ModelSelection,
             Banshee.Gui.DragDrop.DragDropTarget.UriList
@@ -75,7 +79,7 @@ namespace Banshee.Collection.Gui
         protected override TargetEntry [] DragDropSourceEntries {
             get { return source_targets; }
         }
-        
+
         protected override bool OnKeyPressEvent (Gdk.EventKey press)
         {
             // Have o act the same as enter - activate the selection
diff --git a/src/Core/Banshee.ThickClient/Banshee.Collection.Gui/SearchableListView.cs b/src/Core/Banshee.ThickClient/Banshee.Collection.Gui/SearchableListView.cs
new file mode 100644
index 0000000..8a68695
--- /dev/null
+++ b/src/Core/Banshee.ThickClient/Banshee.Collection.Gui/SearchableListView.cs
@@ -0,0 +1,275 @@
+//
+// SearchableListView.cs
+//
+// Author:
+//   Neil Loknath <neil loknath gmail com>
+//
+// Copyright (C) 2009 Neil Loknath
+//
+// 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 Mono.Unix;
+using Gtk;
+
+using Hyena.Data;
+using Hyena.Data.Gui;
+using Hyena.Gui;
+using Hyena.Query;
+using Hyena.Widgets;
+
+using Banshee.Gui;
+using Banshee.Collection;
+using Banshee.Collection.Database;
+
+namespace Banshee.Collection.Gui
+{
+    public class SearchableListView<T> : ListView<T>
+    {
+        private EntryPopup search_popup;
+
+        private long previous_search_offset;
+        private long search_offset = 0;
+
+        private QueryFieldSet last_query_fields = null;
+
+        private QueryNode last_query = null;
+        public QueryNode LastQuery { 
+            get { return last_query; }
+        }
+        
+        public virtual bool SelectOnRowFound {
+            get { return false; }
+        }
+
+        private QueryTermNode CreateNode (QueryField field, Operator op, string target)
+        {
+            QueryTermNode node = new QueryTermNode ();
+            if (field == null || op == null || String.IsNullOrEmpty (target)) {
+                return node;
+            }
+
+            node.Field = field;
+            node.Operator = op;
+            node.Value = QueryValue.CreateFromStringValue (target, field);
+
+            return node;
+        }
+
+        private QueryNode BuildQueryTree (QueryFieldSet fields, Operator op, string target)
+        {
+            return BuildQueryTree (fields, op, target, false);    
+        }
+        
+        private QueryNode BuildQueryTree (QueryFieldSet fields, Operator op, string target, bool force)
+        {
+            if (fields == null || op == null || String.IsNullOrEmpty (target)) {
+                return null;
+            }
+            
+            QueryListNode root = new QueryListNode (Keyword.Or);
+
+            foreach (QueryField field in fields) {
+                if (force || field.IsDefault) {
+                    root.AddChild (CreateNode (field, op, target));
+                }
+            }
+
+            // force the query to build if no default fields in QueryFieldSet
+            if (!force && root.ChildCount == 0) {
+                return BuildQueryTree (fields, op, target, true);
+            }
+            
+            return root.Trim ();
+        }
+
+        private void UpdateQueryTree (QueryNode query, string target)
+        {
+            if (query == null || String.IsNullOrEmpty (target)) {
+                return;
+            }
+            
+            foreach (QueryTermNode node in query.GetTerms ()) {
+                node.Value = QueryValue.CreateFromStringValue (target, node.Field);
+            }
+        }
+        
+        private bool PerformSearch (string target)
+        {
+            if (String.IsNullOrEmpty (target)) {
+                return false;
+            }
+            
+            ISearchable model = Model as ISearchable;
+            if (model == null) {
+                return false;
+            }
+
+            if (last_query == null || !last_query_fields.Equals (model.QueryFields)) {
+                last_query_fields = model.QueryFields;
+                last_query = BuildQueryTree (last_query_fields, StringQueryValue.StartsWith, target);
+            } else {
+                UpdateQueryTree (last_query, target);
+            }
+            
+            int i = model.IndexOf (last_query, search_offset);
+            if (i >= 0) {
+                SelectRow (i);
+                return true;
+            }
+
+            return false;
+        }
+
+        private void SelectRow (int i)
+        {
+            CenterOn (i);
+            
+            Selection.FocusedIndex = i;
+            if (SelectOnRowFound) {
+                Selection.Clear (false);
+                Selection.Select (i);
+            }
+
+            InvalidateList ();
+        }
+        
+        private void PositionPopup (EntryPopup popup)
+        {
+            if (popup == null) {
+                return;
+            }
+
+            int x, y;
+            int widget_x, widget_y;
+            int widget_height, widget_width;
+
+            popup.Realize ();
+            
+            Gdk.Window widget_window = EventWindow; 
+            Gdk.Screen widget_screen = widget_window.Screen;
+            
+            Gtk.Requisition popup_req;
+            
+            widget_window.GetOrigin (out widget_x, out widget_y);
+            widget_window.GetSize (out widget_width, out widget_height);
+            
+            popup_req = popup.Requisition;
+
+            if (widget_x + widget_width > widget_screen.Width) {
+                x = widget_screen.Width - popup_req.Width;
+            } else if (widget_x + widget_width - popup_req.Width < 0) {
+                x = 0;
+            } else {
+                x = widget_x + widget_width - popup_req.Width;
+            }
+
+            if (widget_y + widget_height + popup_req.Height > widget_screen.Height) {
+                y = widget_screen.Height - popup_req.Height;
+            } else if (widget_y + widget_height < 0) { 
+                y = 0;
+            } else {
+                y = widget_y + widget_height;
+            }
+
+            popup.Move (x, y);
+        }
+
+        private bool IsCharValid (char c)
+        {
+            return Char.IsLetterOrDigit (c) || 
+                Char.IsPunctuation (c) ||
+                Char.IsSymbol (c);
+        }
+        
+        protected override bool OnKeyPressEvent (Gdk.EventKey press)
+        {
+            char input = Convert.ToChar (Gdk.Keyval.ToUnicode (press.KeyValue));
+            if (!IsCharValid (input) || Model as ISelectable == null) {
+                return base.OnKeyPressEvent (press);
+            }
+            
+            if (search_popup == null) {
+                search_popup = new EntryPopup ();
+                search_popup.Changed += (o, a) => {
+                    search_offset = 0;
+                    PerformSearch (search_popup.Text);
+                };
+                
+                search_popup.KeyPressed += OnPopupKeyPressed;
+            }
+
+            PositionPopup (search_popup);
+            search_popup.HasFocus = true;
+            search_popup.Show ();
+            search_popup.Text = String.Format ("{0}{1}", search_popup.Text, input);
+            search_popup.Entry.Position = search_popup.Text.Length;
+            return true;
+        }
+
+        private void OnPopupKeyPressed (object sender, KeyPressEventArgs args)
+        {
+            bool search_forward = false;
+            bool search_backward = false;
+
+            Gdk.EventKey press = args.Event;
+            Gdk.Key key = press.Key;
+
+            switch (key) {
+                case Gdk.Key.Up:
+                case Gdk.Key.KP_Up:
+                    search_backward = true;                    
+                    break;
+                case Gdk.Key.g:
+                case Gdk.Key.G:
+                    if ((press.State & Gdk.ModifierType.ControlMask) != 0 &&
+                        (press.State & Gdk.ModifierType.ShiftMask) != 0) {
+                        search_backward = true;
+                    } else if ((press.State & Gdk.ModifierType.ControlMask) != 0) {
+                        search_forward = true;
+                    }
+                    break;
+                case Gdk.Key.F3:
+                case Gdk.Key.KP_F3:
+                    if ((press.State & Gdk.ModifierType.ShiftMask) != 0) {
+                        search_backward = true;
+                    } else if (press.State == Gdk.ModifierType.None) {
+                        search_forward = true;
+                    }
+                    break;
+                case Gdk.Key.Down:
+                case Gdk.Key.KP_Down:
+                    search_forward = true;    
+                    break;
+            }
+
+            if (search_forward) {
+                previous_search_offset = search_offset++;
+                if (!PerformSearch (search_popup.Text)) {
+                   search_offset = previous_search_offset;
+               }
+            } else if (search_backward) {
+                search_offset = search_offset == 0 ? 0 : search_offset - 1;
+                PerformSearch (search_popup.Text);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Core/Banshee.ThickClient/Banshee.Collection.Gui/TrackFilterListView.cs b/src/Core/Banshee.ThickClient/Banshee.Collection.Gui/TrackFilterListView.cs
index 259188d..0d4c3c8 100644
--- a/src/Core/Banshee.ThickClient/Banshee.Collection.Gui/TrackFilterListView.cs
+++ b/src/Core/Banshee.ThickClient/Banshee.Collection.Gui/TrackFilterListView.cs
@@ -38,7 +38,7 @@ using Banshee.Gui;
 
 namespace Banshee.Collection.Gui
 {
-    public class TrackFilterListView<T> : ListView<T>
+    public class TrackFilterListView<T> : SearchableListView<T>
     {
         protected ColumnController column_controller;
         
diff --git a/src/Core/Banshee.ThickClient/Makefile.am b/src/Core/Banshee.ThickClient/Makefile.am
index 037370a..94549ce 100644
--- a/src/Core/Banshee.ThickClient/Makefile.am
+++ b/src/Core/Banshee.ThickClient/Makefile.am
@@ -28,6 +28,7 @@ SOURCES =  \
 	Banshee.Collection.Gui/DefaultColumnController.cs \
 	Banshee.Collection.Gui/PersistentColumnController.cs \
 	Banshee.Collection.Gui/QueryFilterView.cs \
+	Banshee.Collection.Gui/SearchableListView.cs \
 	Banshee.Collection.Gui/TerseTrackListView.cs \
 	Banshee.Collection.Gui/TrackFilterListView.cs \
 	Banshee.Collection.Gui/TrackListView.cs \
diff --git a/src/Libraries/Hyena.Gui/Hyena.Data.Gui/ListView/ListView_Rendering.cs b/src/Libraries/Hyena.Gui/Hyena.Data.Gui/ListView/ListView_Rendering.cs
index 697f2d3..8f4c265 100644
--- a/src/Libraries/Hyena.Gui/Hyena.Data.Gui/ListView/ListView_Rendering.cs
+++ b/src/Libraries/Hyena.Gui/Hyena.Data.Gui/ListView/ListView_Rendering.cs
@@ -422,7 +422,7 @@ namespace Hyena.Data.Gui
             cairo_context.Stroke ();
         }
         
-        private void InvalidateList ()
+        protected void InvalidateList ()
         {
             if (IsRealized) {
                 QueueDrawArea (list_rendering_alloc.X, list_rendering_alloc.Y, list_rendering_alloc.Width, list_rendering_alloc.Height);
diff --git a/src/Libraries/Hyena.Gui/Hyena.Data.Gui/ListView/ListView_Windowing.cs b/src/Libraries/Hyena.Gui/Hyena.Data.Gui/ListView/ListView_Windowing.cs
index c9b3c0b..27f312c 100644
--- a/src/Libraries/Hyena.Gui/Hyena.Data.Gui/ListView/ListView_Windowing.cs
+++ b/src/Libraries/Hyena.Gui/Hyena.Data.Gui/ListView/ListView_Windowing.cs
@@ -46,6 +46,10 @@ namespace Hyena.Data.Gui
         protected Rectangle ListAllocation {
             get { return list_rendering_alloc; }
         }
+
+        protected Gdk.Window EventWindow {
+            get { return event_window; }
+        }
         
         protected override void OnRealized ()
         {
diff --git a/src/Libraries/Hyena.Gui/Hyena.Widgets/EntryPopup.cs b/src/Libraries/Hyena.Gui/Hyena.Widgets/EntryPopup.cs
new file mode 100644
index 0000000..b0763a5
--- /dev/null
+++ b/src/Libraries/Hyena.Gui/Hyena.Widgets/EntryPopup.cs
@@ -0,0 +1,226 @@
+//
+// EntryPopup.cs
+//
+// Author:
+//   Neil Loknath <neil loknath gmail com>
+//
+// Copyright (C) 2009 Neil Loknath
+//
+// 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.Timers;
+using Gdk;
+using Gtk;
+
+namespace Hyena.Widgets
+{
+    public class EntryPopup : Gtk.Window
+    {
+        private Entry text_entry;
+        private uint timeout_id = 0;
+
+        public event EventHandler<EventArgs> Changed;
+        public event EventHandler<KeyPressEventArgs> KeyPressed;
+
+        public EntryPopup (string text) : this ()
+        {
+            Text = text;
+        }
+        
+        public EntryPopup () : base (Gtk.WindowType.Popup)
+        {
+            CanFocus = true;
+            Resizable = false;
+            TypeHint = Gdk.WindowTypeHint.Utility;
+            Modal = true;
+            
+            Frame frame = new Frame ();
+            frame.Shadow = ShadowType.EtchedIn;
+            Add (frame);
+            
+            HBox box = new HBox ();
+            text_entry = new Entry();
+            box.PackStart (text_entry, true, true, 0);
+            box.BorderWidth = 3;
+            
+            frame.Add (box);
+            frame.ShowAll ();
+
+            text_entry.Text = String.Empty;
+            text_entry.CanFocus = true;
+
+            //TODO figure out why this event does not get raised
+            text_entry.FocusOutEvent += (o, a) => {
+                if (hide_when_focus_lost) {
+                    HidePopup ();
+                }
+            };
+
+            text_entry.KeyReleaseEvent += delegate (object o, KeyReleaseEventArgs args) {
+                if (args.Event.Key == Gdk.Key.Escape || 
+                    args.Event.Key == Gdk.Key.Return ||
+                    args.Event.Key == Gdk.Key.Tab) {
+                    
+                    HidePopup ();
+                }
+
+                InitializeDelayedHide ();
+            };
+
+            text_entry.KeyPressEvent += (o, a) => OnKeyPressed (a);            
+            
+            text_entry.Changed += (o, a) => {
+                if (GdkWindow.IsVisible) {
+                    OnChanged (a);
+                }
+            };
+        }
+        
+        public new bool HasFocus {
+            get { return text_entry.HasFocus; }
+            set { text_entry.HasFocus = value; }
+        }
+
+        public string Text {
+            get { return text_entry.Text; }
+            set { text_entry.Text = value; }
+        }
+        
+        public Entry Entry {
+            get { return text_entry; }
+        }
+
+        private bool hide_after_timeout = true;
+        public bool HideAfterTimeout {
+            get { return hide_after_timeout; }
+            set { hide_after_timeout = value; }
+        }
+
+        private uint timeout = 5000;
+        public uint Timeout {
+            get { return timeout; }
+            set { timeout = value; }
+        }
+        
+        private bool hide_when_focus_lost = true;
+        public bool HideOnFocusOut {
+            get { return hide_when_focus_lost; }
+            set { hide_when_focus_lost = value; }
+        }
+
+        private bool reset_when_hiding = true;
+        public bool ResetOnHide {
+            get { return reset_when_hiding; }
+            set { reset_when_hiding = value; }
+        }
+        
+        public override void Dispose ()
+        {
+            text_entry.Dispose ();
+            base.Dispose ();
+        }
+
+        public new void GrabFocus ()
+        {
+            text_entry.GrabFocus ();
+        }
+            
+        private void ResetDelayedHide ()
+        {
+            if (timeout_id > 0) {
+                GLib.Source.Remove (timeout_id);
+                timeout_id = 0;
+            }
+        }
+        
+        private void InitializeDelayedHide ()
+        {
+            ResetDelayedHide ();
+            timeout_id = GLib.Timeout.Add (timeout, delegate {
+                            HidePopup ();
+                            return false;
+                        });
+        }
+        
+        private void HidePopup ()
+        {
+            ResetDelayedHide ();
+            Hide ();
+            
+            if (reset_when_hiding) {
+                text_entry.Text = String.Empty;
+            }
+        }
+
+        protected virtual void OnChanged (EventArgs args)
+        {
+            var handler = Changed;
+            if (handler != null) {
+                handler (this, EventArgs.Empty);
+            }
+        }
+
+        protected virtual void OnKeyPressed (KeyPressEventArgs args)
+        {
+            var handler = KeyPressed;
+            if (handler != null) {
+                handler (this, args);
+            }
+        }
+
+        //TODO figure out why this event does not get raised
+        protected override bool OnFocusOutEvent (Gdk.EventFocus evnt)
+        {
+            if (hide_when_focus_lost) {
+                HidePopup ();
+                return true;
+            }
+
+            return base.OnFocusOutEvent (evnt);
+        }
+        
+        protected override bool OnExposeEvent (Gdk.EventExpose evnt)
+        {
+            InitializeDelayedHide ();
+            return base.OnExposeEvent (evnt);
+        }
+
+        protected override bool OnButtonReleaseEvent (Gdk.EventButton evnt)
+        {
+            if (!text_entry.HasFocus && hide_when_focus_lost) {
+                HidePopup ();
+                return true;
+            }
+
+            return base.OnButtonReleaseEvent (evnt);
+        }
+        
+        protected override bool OnButtonPressEvent (Gdk.EventButton evnt)
+        {
+            if (!text_entry.HasFocus && hide_when_focus_lost) {
+                HidePopup ();
+                return true;
+            }
+
+            return base.OnButtonPressEvent (evnt);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Libraries/Hyena.Gui/Makefile.am b/src/Libraries/Hyena.Gui/Makefile.am
index e8b3de6..c023e8e 100644
--- a/src/Libraries/Hyena.Gui/Makefile.am
+++ b/src/Libraries/Hyena.Gui/Makefile.am
@@ -86,6 +86,7 @@ SOURCES =  \
 	Hyena.Widgets/AnimatedVBox.cs \
 	Hyena.Widgets/AnimatedWidget.cs \
 	Hyena.Widgets/ComplexMenuItem.cs \
+	Hyena.Widgets/EntryPopup.cs \
 	Hyena.Widgets/ImageButton.cs \
 	Hyena.Widgets/MenuButton.cs \
 	Hyena.Widgets/MessageBar.cs \
diff --git a/src/Libraries/Hyena/Hyena.Data.Sqlite/SqliteModelCache.cs b/src/Libraries/Hyena/Hyena.Data.Sqlite/SqliteModelCache.cs
index 9698461..4589da6 100644
--- a/src/Libraries/Hyena/Hyena.Data.Sqlite/SqliteModelCache.cs
+++ b/src/Libraries/Hyena/Hyena.Data.Sqlite/SqliteModelCache.cs
@@ -30,6 +30,9 @@
 using System;
 using System.Collections.Generic;
 using System.Data;
+using System.Text;
+
+using Hyena.Query;
 
 namespace Hyena.Data.Sqlite
 {
@@ -45,9 +48,11 @@ namespace Hyena.Data.Sqlite
         private HyenaSqliteCommand delete_selection_command;
         private HyenaSqliteCommand save_selection_command;
         private HyenaSqliteCommand get_selection_command;
+        private HyenaSqliteCommand indexof_command;
 
         private string select_str;
         private string reload_sql;
+        private string last_indexof_where_fragment;
         private long uid;
         private long selection_uid;
         private long rows;
@@ -102,7 +107,7 @@ namespace Hyena.Data.Sqlite
 
             if (model.CachesJoinTableEntries) {
                 select_str = String.Format (
-                    @"SELECT {0}, {5}.ItemID  FROM {1}
+                    @"SELECT {0}, OrderID, {5}.ItemID  FROM {1}
                         INNER JOIN {2}
                             ON {3} = {2}.{4}
                         INNER JOIN {5}
@@ -134,7 +139,7 @@ namespace Hyena.Data.Sqlite
                 );
             } else {
                 select_str = String.Format (
-                    @"SELECT {0}, {2}.ItemID FROM {1}
+                    @"SELECT {0}, OrderID, {2}.ItemID FROM {1}
                         INNER JOIN {2} 
                             ON {3} = {2}.ItemID
                         WHERE
@@ -227,6 +232,38 @@ namespace Hyena.Data.Sqlite
                 }
              }
         }
+
+        public long IndexOf (string where_fragment, long offset)
+        {
+            if (String.IsNullOrEmpty (where_fragment)) {
+                return -1;
+            }
+
+            if (!where_fragment.Equals (last_indexof_where_fragment)) {
+                last_indexof_where_fragment = where_fragment;
+                
+                if (!where_fragment.Trim ().ToLower ().StartsWith ("and ")) {
+                    where_fragment = " AND " + where_fragment;
+                }
+            
+                string sql = String.Format ("{0} {1} LIMIT ?, 1", select_str, where_fragment);
+                indexof_command = new HyenaSqliteCommand (sql);
+            }
+
+            lock (this) {
+                using (IDataReader reader = connection.Query (indexof_command, offset)) {
+                    if (reader.Read ()) {
+                        long target_id = (long) reader[reader.FieldCount - 2];
+                        if (target_id == 0) {
+                            return -1;
+                        }
+                        return target_id - FirstOrderId;
+                    }
+                }
+    
+                return -1;
+            }
+        }
         
         public long IndexOf (ICacheableItem item)
         {



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]