diff --git a/FoxTunes.Core/Behaviours/PlaylistSearchBehaviour.cs b/FoxTunes.Core/Behaviours/PlaylistSearchBehaviour.cs new file mode 100644 index 000000000..2fce466ba --- /dev/null +++ b/FoxTunes.Core/Behaviours/PlaylistSearchBehaviour.cs @@ -0,0 +1,77 @@ +using FoxTunes.Interfaces; +using System; +using System.Linq; + +namespace FoxTunes +{ + public class PlaylistSearchBehaviour : StandardBehaviour + { + public IPlaylistManager PlaylistManager { get; private set; } + + public IPlaylistBrowser PlaylistBrowser { get; private set; } + + public override void InitializeComponent(ICore core) + { + this.PlaylistManager = core.Managers.Playlist; + this.PlaylistManager.FilterChanged += this.OnFilterChanged; + this.PlaylistBrowser = core.Components.PlaylistBrowser; + base.InitializeComponent(core); + } + + protected virtual void OnFilterChanged(object sender, EventArgs e) + { + if (this.PlaylistManager.SelectedPlaylist == null || string.IsNullOrEmpty(this.PlaylistManager.Filter)) + { + return; + } + this.Dispatch(this.Search); + } + + public void Search() + { + var playlistItems = this.PlaylistBrowser.GetItems(this.PlaylistManager.SelectedPlaylist, this.PlaylistManager.Filter); + if (playlistItems != null && playlistItems.Any()) + { + this.PlaylistManager.SelectedItems = playlistItems; + } + } + + public bool IsDisposed { get; private set; } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (this.IsDisposed || !disposing) + { + return; + } + this.OnDisposing(); + this.IsDisposed = true; + } + + protected virtual void OnDisposing() + { + if (this.PlaylistManager != null) + { + this.PlaylistManager.FilterChanged -= this.OnFilterChanged; + } + } + + ~PlaylistSearchBehaviour() + { + try + { + this.Dispose(true); + } + catch + { + //Nothing can be done, never throw on GC thread. + } + } + } +} diff --git a/FoxTunes.Core/Interfaces/Managers/IPlaylistManager.cs b/FoxTunes.Core/Interfaces/Managers/IPlaylistManager.cs index 83dae2efb..2499e9ca6 100644 --- a/FoxTunes.Core/Interfaces/Managers/IPlaylistManager.cs +++ b/FoxTunes.Core/Interfaces/Managers/IPlaylistManager.cs @@ -70,6 +70,10 @@ public interface IPlaylistManager : IStandardManager, IInvocableComponent, IFile PlaylistItem[] SelectedItems { get; set; } event EventHandler SelectedItemsChanged; + + string Filter { get; set; } + + event EventHandler FilterChanged; } public enum PlaylistQueueFlags : byte diff --git a/FoxTunes.Core/Interfaces/Playlist/IPlaylistBrowser.cs b/FoxTunes.Core/Interfaces/Playlist/IPlaylistBrowser.cs index 4b093cdcc..fe2035d66 100644 --- a/FoxTunes.Core/Interfaces/Playlist/IPlaylistBrowser.cs +++ b/FoxTunes.Core/Interfaces/Playlist/IPlaylistBrowser.cs @@ -16,6 +16,8 @@ public interface IPlaylistBrowser : IStandardComponent PlaylistItem[] GetItems(Playlist playlist); + PlaylistItem[] GetItems(Playlist playlist, string filter); + PlaylistItem GetItemById(Playlist playlist, int id); PlaylistItem GetItemBySequence(Playlist playlist, int sequence); diff --git a/FoxTunes.Core/Managers/PlaylistManager.cs b/FoxTunes.Core/Managers/PlaylistManager.cs index 67e8474cd..24eb29d51 100644 --- a/FoxTunes.Core/Managers/PlaylistManager.cs +++ b/FoxTunes.Core/Managers/PlaylistManager.cs @@ -681,6 +681,36 @@ protected virtual void OnSelectedItemsChanged() public event EventHandler SelectedItemsChanged; + private string _Filter { get; set; } + + public string Filter + { + get + { + return this._Filter; + } + set + { + if (string.Equals(this._Filter, value, StringComparison.OrdinalIgnoreCase)) + { + return; + } + this._Filter = value; + this.OnFilterChanged(); + } + } + + protected virtual void OnFilterChanged() + { + if (this.FilterChanged != null) + { + this.FilterChanged(this, EventArgs.Empty); + } + this.OnPropertyChanged("Filter"); + } + + public event EventHandler FilterChanged; + public IEnumerable InvocationCategories { get diff --git a/FoxTunes.Core/Playlist/PlaylistBrowser.cs b/FoxTunes.Core/Playlist/PlaylistBrowser.cs index 6baa9f4ea..0e00d30f4 100644 --- a/FoxTunes.Core/Playlist/PlaylistBrowser.cs +++ b/FoxTunes.Core/Playlist/PlaylistBrowser.cs @@ -126,6 +126,16 @@ public PlaylistItem[] GetItems(Playlist playlist) return this.PlaylistCache.GetItems(playlist, () => this.GetItemsCore(playlist)); } + public PlaylistItem[] GetItems(Playlist playlist, string filter) + { + var playlistItems = this.GetItems(playlist); + return playlistItems.Where( + playlistItem => playlistItem.MetaDatas.Any( + metaDataItem => !string.IsNullOrEmpty(metaDataItem.Value) && metaDataItem.Value.Contains(filter, true) + ) + ).ToArray(); + } + private IEnumerable GetItemsCore(Playlist playlist) { this.State |= PlaylistBrowserState.Loading; diff --git a/FoxTunes.UI.Windows.Layout/UIComponentContainer.cs b/FoxTunes.UI.Windows.Layout/UIComponentContainer.cs index b8800354e..717ae9afe 100644 --- a/FoxTunes.UI.Windows.Layout/UIComponentContainer.cs +++ b/FoxTunes.UI.Windows.Layout/UIComponentContainer.cs @@ -367,7 +367,7 @@ public Task Wrap(string name) { return; } - var component = LayoutManager.Instance.GetComponent(name, this.Configuration.Component.Role); + var component = LayoutManager.Instance.GetComponent(name, UIComponentRole.Container); if (component == null) { return; diff --git a/FoxTunes.UI.Windows.Themes/Themes/Adamantine.xaml b/FoxTunes.UI.Windows.Themes/Themes/Adamantine.xaml index 7639a2861..5ca18e08c 100644 --- a/FoxTunes.UI.Windows.Themes/Themes/Adamantine.xaml +++ b/FoxTunes.UI.Windows.Themes/Themes/Adamantine.xaml @@ -1605,7 +1605,6 @@ - @@ -1692,12 +1691,6 @@ - - - - - - diff --git a/FoxTunes.UI.Windows.Themes/Themes/Transparent.xaml b/FoxTunes.UI.Windows.Themes/Themes/Transparent.xaml index 164c2ab9d..e34aca9e8 100644 --- a/FoxTunes.UI.Windows.Themes/Themes/Transparent.xaml +++ b/FoxTunes.UI.Windows.Themes/Themes/Transparent.xaml @@ -1605,7 +1605,6 @@ - @@ -1692,12 +1691,6 @@ - - - - - - diff --git a/FoxTunes.UI.Windows/DefaultPlaylist.xaml b/FoxTunes.UI.Windows/DefaultPlaylist.xaml index 4009def85..fb6fdf1b3 100644 --- a/FoxTunes.UI.Windows/DefaultPlaylist.xaml +++ b/FoxTunes.UI.Windows/DefaultPlaylist.xaml @@ -36,6 +36,7 @@ Windows:ListViewExtensions.DragSource="True" Windows:ListViewExtensions.DragSourceInitialized="DragSourceInitialized" Windows:ListViewExtensions.AutoSizeColumns="True" + Windows:ListViewExtensions.EnsureSelectedItemVisible="True" GridViewColumnHeader.Click="OnHeaderClick" SelectionChanged="OnSelectionChanged" Background="{DynamicResource ControlBackgroundBrush}"> diff --git a/FoxTunes.UI.Windows/Extensions/ListView_EnsureSelectedItemVisible.cs b/FoxTunes.UI.Windows/Extensions/ListView_EnsureSelectedItemVisible.cs new file mode 100644 index 000000000..e41bbc75d --- /dev/null +++ b/FoxTunes.UI.Windows/Extensions/ListView_EnsureSelectedItemVisible.cs @@ -0,0 +1,110 @@ +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; + +namespace FoxTunes +{ + public static partial class ListViewExtensions + { + private static readonly ConditionalWeakTable EnsureSelectedItemVisibleBehaviours = new ConditionalWeakTable(); + + public static readonly DependencyProperty EnsureSelectedItemVisibleProperty = DependencyProperty.RegisterAttached( + "EnsureSelectedItemVisible", + typeof(bool), + typeof(ListViewExtensions), + new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnEnsureSelectedItemVisiblePropertyChanged)) + ); + + public static bool GetEnsureSelectedItemVisible(ListView source) + { + return (bool)source.GetValue(EnsureSelectedItemVisibleProperty); + } + + public static void SetEnsureSelectedItemVisible(ListView source, bool value) + { + source.SetValue(EnsureSelectedItemVisibleProperty, value); + } + + private static void OnEnsureSelectedItemVisiblePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + var listView = sender as ListView; + if (listView == null) + { + return; + } + if (GetEnsureSelectedItemVisible(listView)) + { + var behaviour = default(EnsureSelectedItemVisibleBehaviour); + if (!EnsureSelectedItemVisibleBehaviours.TryGetValue(listView, out behaviour)) + { + EnsureSelectedItemVisibleBehaviours.Add(listView, new EnsureSelectedItemVisibleBehaviour(listView)); + } + } + else + { + var behaviour = default(EnsureSelectedItemVisibleBehaviour); + if (EnsureSelectedItemVisibleBehaviours.TryGetValue(listView, out behaviour)) + { + EnsureSelectedItemVisibleBehaviours.Remove(listView); + behaviour.Dispose(); + } + } + } + + private class EnsureSelectedItemVisibleBehaviour : UIBehaviour + { + public EnsureSelectedItemVisibleBehaviour(ListView listView) + { + this.ListView = listView; + this.ListView.SelectionChanged += this.OnSelectionChanged; + } + + public ListView ListView { get; private set; } + + protected virtual void EnsureVisible(object value) + { + if (value == null) + { + return; + } + var index = this.ListView.Items.IndexOf(value); + if (index < 0) + { + return; + } + var scrollViewer = this.ListView.FindChild(); + if (scrollViewer != null) + { + if (scrollViewer.ScrollToItemOffset(index, this.OnItemLoaded)) + { + this.ListView.UpdateLayout(); + } + } + var item = this.ListView.ItemContainerGenerator.ContainerFromItem(value) as ListViewItem; + if (item != null) + { + item.BringIntoView(); + } + } + + protected virtual void OnItemLoaded(object sender, RoutedEventArgs e) + { + this.EnsureVisible(this.ListView.SelectedItem); + } + + protected virtual void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + this.EnsureVisible(this.ListView.SelectedItem); + } + + protected override void OnDisposing() + { + if (this.ListView != null) + { + this.ListView.SelectionChanged -= this.OnSelectionChanged; + } + base.OnDisposing(); + } + } + } +} diff --git a/FoxTunes.UI.Windows/Extensions/ListView_SelectedItems.cs b/FoxTunes.UI.Windows/Extensions/ListView_SelectedItems.cs index f13876bbd..345358604 100644 --- a/FoxTunes.UI.Windows/Extensions/ListView_SelectedItems.cs +++ b/FoxTunes.UI.Windows/Extensions/ListView_SelectedItems.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.IO.IsolatedStorage; using System.Linq; using System.Runtime.CompilerServices; using System.Windows; @@ -12,6 +13,8 @@ public static partial class ListViewExtensions private static readonly ConditionalWeakTable SelectedItemsBehaviours = new ConditionalWeakTable(); + public static bool IsSuspended { get; private set; } + public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.RegisterAttached( "SelectedItems", typeof(IList), @@ -31,6 +34,10 @@ public static void SetSelectedItems(ListView source, IList value) private static void OnSelectedItemsPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { + if (IsSuspended) + { + return; + } var listView = sender as ListView; if (listView == null) { @@ -41,22 +48,30 @@ private static void OnSelectedItemsPropertyChanged(DependencyObject sender, Depe { SelectedItemsBehaviours.Add(listView, new SelectedItemsBehaviour(listView)); } - //We only need this if we need two way binding (it's sketchy and causes weird recursive - //calls to this handler anyway so let's just not). - //var items = (e.NewValue as IList); - //if (items == null) - //{ - // return; - //} - //if (Enumerable.SequenceEqual(listView.SelectedItems.Cast(), items.Cast())) - //{ - // return; - //} - //listView.SelectedItems.Clear(); - //foreach (var item in items) - //{ - // listView.SelectedItems.Add(item); - //} + IsSuspended = true; + try + { + //We only need this if we need two way binding (it's sketchy and causes weird recursive + //calls to this handler anyway so let's just not). + var items = (e.NewValue as IList); + if (items == null) + { + return; + } + if (Enumerable.SequenceEqual(listView.SelectedItems.Cast(), items.Cast())) + { + return; + } + listView.SelectedItems.Clear(); + foreach (var item in items) + { + listView.SelectedItems.Add(item); + } + } + finally + { + IsSuspended = false; + } } private class SelectedItemsBehaviour : UIBehaviour diff --git a/FoxTunes.UI.Windows/PlaylistSearch.xaml b/FoxTunes.UI.Windows/PlaylistSearch.xaml new file mode 100644 index 000000000..7bca37254 --- /dev/null +++ b/FoxTunes.UI.Windows/PlaylistSearch.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/FoxTunes.UI.Windows/PlaylistSearch.xaml.cs b/FoxTunes.UI.Windows/PlaylistSearch.xaml.cs new file mode 100644 index 000000000..1cd8c2720 --- /dev/null +++ b/FoxTunes.UI.Windows/PlaylistSearch.xaml.cs @@ -0,0 +1,14 @@ +namespace FoxTunes +{ + /// + /// Interaction logic for PlaylistSearch.xaml + /// + [UIComponent("868C483E-C23A-4960-A60F-B3AD9A4460C2", role: UIComponentRole.Playlist)] + public partial class PlaylistSearch : UIComponentBase + { + public PlaylistSearch() + { + this.InitializeComponent(); + } + } +} diff --git a/FoxTunes.UI.Windows/TabPlaylist.xaml b/FoxTunes.UI.Windows/TabPlaylist.xaml index dd62f12dc..4b7d4cbba 100644 --- a/FoxTunes.UI.Windows/TabPlaylist.xaml +++ b/FoxTunes.UI.Windows/TabPlaylist.xaml @@ -36,6 +36,7 @@ Windows:ListViewExtensions.DragSource="True" Windows:ListViewExtensions.DragSourceInitialized="DragSourceInitialized" Windows:ListViewExtensions.AutoSizeColumns="True" + Windows:ListViewExtensions.EnsureSelectedItemVisible="True" GridViewColumnHeader.Click="OnHeaderClick" SelectionChanged="OnSelectionChanged"> diff --git a/FoxTunes.UI.Windows/ViewModel/PlaylistSearch.cs b/FoxTunes.UI.Windows/ViewModel/PlaylistSearch.cs new file mode 100644 index 000000000..8661c0692 --- /dev/null +++ b/FoxTunes.UI.Windows/ViewModel/PlaylistSearch.cs @@ -0,0 +1,101 @@ +using FoxTunes.Interfaces; +using System; +using System.Windows; + +namespace FoxTunes.ViewModel +{ + public class PlaylistSearch : ViewModelBase + { + public string Filter + { + get + { + if (this.PlaylistManager == null) + { + return null; + } + return this.PlaylistManager.Filter; + } + set + { + if (this.PlaylistManager == null) + { + return; + } + this.PlaylistManager.Filter = value; + } + } + + protected virtual void OnFilterChanged() + { + if (this.FilterChanged != null) + { + this.FilterChanged(this, EventArgs.Empty); + } + this.OnPropertyChanged("Filter"); + } + + public event EventHandler FilterChanged; + + private int _SearchInterval { get; set; } + + public int SearchInterval + { + get + { + return this._SearchInterval; + } + set + { + this._SearchInterval = value; + this.OnSearchIntervalChanged(); + } + } + + protected virtual void OnSearchIntervalChanged() + { + if (this.SearchIntervalChanged != null) + { + this.SearchIntervalChanged(this, EventArgs.Empty); + } + this.OnPropertyChanged("SearchInterval"); + } + + public event EventHandler SearchIntervalChanged; + + public IPlaylistManager PlaylistManager { get; private set; } + + private IConfiguration Configuration { get; set; } + + protected override void InitializeComponent(ICore core) + { + this.PlaylistManager = core.Managers.Playlist; + this.PlaylistManager.FilterChanged += this.OnFilterChanged; + this.Configuration = core.Components.Configuration; + this.Configuration.GetElement( + SearchBehaviourConfiguration.SECTION, + SearchBehaviourConfiguration.SEARCH_INTERVAL_ELEMENT + ).ConnectValue(value => this.SearchInterval = value); + base.InitializeComponent(core); + } + + protected virtual void OnFilterChanged(object sender, EventArgs e) + { + var task = Windows.Invoke(this.OnFilterChanged); + } + + protected override void OnDisposing() + { + if (this.PlaylistManager != null) + { + this.PlaylistManager.FilterChanged -= this.OnFilterChanged; + } + base.OnDisposing(); + } + + protected override Freezable CreateInstanceCore() + { + return new LibrarySearch(); + } + } +}