From 934a41e414df80c03bd5036d524d6db84300897b Mon Sep 17 00:00:00 2001 From: Colin Liu Date: Thu, 18 Feb 2016 19:31:15 +0800 Subject: [PATCH] Refactor ResultPanel/ResultItem with MVVM --- Wox/ResultPanel.xaml | 73 ++--- Wox/ResultPanel.xaml.cs | 177 ++++-------- Wox/ViewModel/ResultItemViewModel.cs | 127 +++++++++ Wox/ViewModel/ResultPanelViewModel.cs | 378 ++++++++++++++++++++++++++ 4 files changed, 598 insertions(+), 157 deletions(-) create mode 100644 Wox/ViewModel/ResultItemViewModel.cs create mode 100644 Wox/ViewModel/ResultPanelViewModel.cs diff --git a/Wox/ResultPanel.xaml b/Wox/ResultPanel.xaml index 6abd51faec..21c1e890f2 100644 --- a/Wox/ResultPanel.xaml +++ b/Wox/ResultPanel.xaml @@ -6,48 +6,59 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:converters="clr-namespace:Wox.Converters" mc:Ignorable="d" d:DesignWidth="100" d:DesignHeight="100"> - - - - + + + + + + + + + + - - - - - - - - - + + + + + + + + - - - - - - + + + + + - + + - - - + Grid.Row="1" x:Name="tbSubTitle" Text="{Binding SubTitle}" > + + + + + + - + - + + + diff --git a/Wox/ResultPanel.xaml.cs b/Wox/ResultPanel.xaml.cs index 4a622815de..a797bda857 100644 --- a/Wox/ResultPanel.xaml.cs +++ b/Wox/ResultPanel.xaml.cs @@ -10,123 +10,72 @@ using Wox.Core.UserSettings; using Wox.Helper; using Wox.Plugin; using Wox.Storage; +using Wox.ViewModel; namespace Wox { [Synchronization] public partial class ResultPanel : UserControl { - public event Action LeftMouseClickEvent; - public event Action RightMouseClickEvent; public event Action ItemDropEvent; - private readonly ListBoxItems _results; private readonly object _resultsUpdateLock = new object(); - protected virtual void OnRightMouseClick(Result result) - { - Action handler = RightMouseClickEvent; - if (handler != null) handler(result); - } - - protected virtual void OnLeftMouseClick(Result result) - { - Action handler = LeftMouseClickEvent; - if (handler != null) handler(result); - } - - - public int MaxResultsToShow { get { return UserSettingStorage.Instance.MaxResultsToShow * 50; } } - - internal void RemoveResultsFor(PluginMetadata metadata) - { - lock (_resultsUpdateLock) - { - _results.RemoveAll(r => r.PluginID == metadata.ID); - } - } - - internal void RemoveResultsExcept(PluginMetadata metadata) - { - lock (_resultsUpdateLock) - { - _results.RemoveAll(r => r.PluginID != metadata.ID); - } - } - public void AddResults(List newResults, string resultId) { - lock (_resultsUpdateLock) - { - // todo use async to do new result calculation - var resultsCopy = _results.ToList(); - var oldResults = resultsCopy.Where(r => r.PluginID == resultId).ToList(); - // intersection of A (old results) and B (new newResults) - var intersection = oldResults.Intersect(newResults).ToList(); - // remove result of relative complement of B in A - foreach (var result in oldResults.Except(intersection)) - { - resultsCopy.Remove(result); - } + //lock (_resultsUpdateLock) + //{ + // // todo use async to do new result calculation + // var resultsCopy = _results.ToList(); + // var oldResults = resultsCopy.Where(r => r.PluginID == resultId).ToList(); + // // intersection of A (old results) and B (new newResults) + // var intersection = oldResults.Intersect(newResults).ToList(); + // // remove result of relative complement of B in A + // foreach (var result in oldResults.Except(intersection)) + // { + // resultsCopy.Remove(result); + // } - // update scores - foreach (var result in newResults) - { - if (IsTopMostResult(result)) - { - result.Score = int.MaxValue; - } - } + // // update scores + // foreach (var result in newResults) + // { + // if (IsTopMostResult(result)) + // { + // result.Score = int.MaxValue; + // } + // } - // update index for result in intersection of A and B - foreach (var commonResult in intersection) - { - int oldIndex = resultsCopy.IndexOf(commonResult); - int oldScore = resultsCopy[oldIndex].Score; - int newScore = newResults[newResults.IndexOf(commonResult)].Score; - if (newScore != oldScore) - { - var oldResult = resultsCopy[oldIndex]; - oldResult.Score = newScore; - resultsCopy.RemoveAt(oldIndex); - int newIndex = InsertIndexOf(newScore, resultsCopy); - resultsCopy.Insert(newIndex, oldResult); + // // update index for result in intersection of A and B + // foreach (var commonResult in intersection) + // { + // int oldIndex = resultsCopy.IndexOf(commonResult); + // int oldScore = resultsCopy[oldIndex].Score; + // int newScore = newResults[newResults.IndexOf(commonResult)].Score; + // if (newScore != oldScore) + // { + // var oldResult = resultsCopy[oldIndex]; + // oldResult.Score = newScore; + // resultsCopy.RemoveAt(oldIndex); + // int newIndex = InsertIndexOf(newScore, resultsCopy); + // resultsCopy.Insert(newIndex, oldResult); - } - } + // } + // } - // insert result in relative complement of A in B - foreach (var result in newResults.Except(intersection)) - { - int newIndex = InsertIndexOf(result.Score, resultsCopy); - resultsCopy.Insert(newIndex, result); - } + // // insert result in relative complement of A in B + // foreach (var result in newResults.Except(intersection)) + // { + // int newIndex = InsertIndexOf(result.Score, resultsCopy); + // resultsCopy.Insert(newIndex, result); + // } - // update UI in one run, so it can avoid UI flickering - _results.Update(resultsCopy); + // // update UI in one run, so it can avoid UI flickering + // _results.Update(resultsCopy); - lbResults.Margin = lbResults.Items.Count > 0 ? new Thickness { Top = 8 } : new Thickness { Top = 0 }; - SelectFirst(); - } - } - - private bool IsTopMostResult(Result result) - { - return TopMostRecordStorage.Instance.IsTopMost(result); - } - - private int InsertIndexOf(int newScore, IList list) - { - int index = 0; - for (; index < list.Count; index++) - { - var result = list[index]; - if (newScore > result.Score) - { - break; - } - } - return index; + // lbResults.Margin = lbResults.Items.Count > 0 ? new Thickness { Top = 8 } : new Thickness { Top = 0 }; + // SelectFirst(); + //} } + public void SelectNext() { @@ -245,17 +194,6 @@ namespace Wox public ResultPanel() { InitializeComponent(); - _results = new ListBoxItems(); - lbResults.ItemsSource = _results; - } - - public void Clear() - { - lock (_resultsUpdateLock) - { - _results.Clear(); - lbResults.Margin = new Thickness { Top = 0 }; - } } private void lbResults_SelectionChanged(object sender, SelectionChangedEventArgs e) @@ -270,19 +208,6 @@ namespace Wox } } - private void LbResults_OnPreviewMouseDown(object sender, MouseButtonEventArgs e) - { - var item = ItemsControl.ContainerFromElement(lbResults, e.OriginalSource as DependencyObject) as ListBoxItem; - if (item != null && e.ChangedButton == MouseButton.Left) - { - OnLeftMouseClick(item.DataContext as Result); - } - if (item != null && e.ChangedButton == MouseButton.Right) - { - OnRightMouseClick(item.DataContext as Result); - } - } - public void SelectNextPage() { int index = lbResults.SelectedIndex; @@ -310,14 +235,14 @@ namespace Wox var item = ItemsControl.ContainerFromElement(lbResults, e.OriginalSource as DependencyObject) as ListBoxItem; if (item != null) { - OnItemDropEvent(item.DataContext as Result, e.Data, e); + OnItemDropEvent(item.DataContext as ResultItemViewModel, e.Data, e); } } - protected virtual void OnItemDropEvent(Result obj, IDataObject data, DragEventArgs e) + protected virtual void OnItemDropEvent(ResultItemViewModel obj, IDataObject data, DragEventArgs e) { var handler = ItemDropEvent; - if (handler != null) handler(obj, data, e); + if (handler != null) handler(obj.RawResult, data, e); } } } \ No newline at end of file diff --git a/Wox/ViewModel/ResultItemViewModel.cs b/Wox/ViewModel/ResultItemViewModel.cs new file mode 100644 index 0000000000..3af615d387 --- /dev/null +++ b/Wox/ViewModel/ResultItemViewModel.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Wox.Infrastructure.Hotkey; +using Wox.Plugin; +using Wox.Storage; + +namespace Wox.ViewModel +{ + public class ResultItemViewModel : BaseViewModel + { + #region Private Fields + + private Result _result; + + #endregion + + #region Constructor + + public ResultItemViewModel(Result result) + { + if(null!= result) + { + this._result = result; + + this.OpenResultCommand = new RelayCommand(() => { + + bool hideWindow = result.Action(new ActionContext + { + SpecialKeyState = GlobalHotkey.Instance.CheckModifiers() + }); + + if (null != this.ResultOpened) + { + this.ResultOpened(this, new ResultOpenedEventArgs(hideWindow)); + } + }); + + this.OpenResultActionPanelCommand = new RelayCommand(()=> { + + if(null!= ResultActionPanelOpened) + { + this.ResultActionPanelOpened(this, new EventArgs()); + } + + }); + } + } + + #endregion + + #region ViewModel Properties + + public string Title + { + get + { + return this._result.Title; + } + } + + public string SubTitle + { + get + { + return this._result.SubTitle; + } + } + + public string FullIcoPath + { + get + { + //TODO: Some of the properties in Result class may be moved to this class + return this._result.FullIcoPath; + } + } + + public RelayCommand OpenResultCommand + { + get; + set; + } + + public RelayCommand OpenResultActionPanelCommand + { + get; + set; + } + + #endregion + + #region Properties + + public Result RawResult + { + get + { + return this._result; + } + } + + #endregion + + public event EventHandler ResultOpened; + + public event EventHandler ResultActionPanelOpened; + + } + + public class ResultOpenedEventArgs : EventArgs + { + + public bool HideWindow + { + get; + private set; + } + + public ResultOpenedEventArgs(bool hideWindow) + { + this.HideWindow = hideWindow; + } + } +} diff --git a/Wox/ViewModel/ResultPanelViewModel.cs b/Wox/ViewModel/ResultPanelViewModel.cs new file mode 100644 index 0000000000..052fd01378 --- /dev/null +++ b/Wox/ViewModel/ResultPanelViewModel.cs @@ -0,0 +1,378 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using Wox.Core.UserSettings; +using Wox.Plugin; +using Wox.Storage; + +namespace Wox.ViewModel +{ + public class ResultPanelViewModel : BaseViewModel + { + #region Private Fields + + private ResultItemViewModel _selectedResult; + private ResultCollection _results; + private bool _isVisible; + private Thickness _margin; + + private readonly object _resultsUpdateLock = new object(); + + #endregion + + #region Constructor + + public ResultPanelViewModel() + { + this._results = new ResultCollection( + + (o, e)=> { + + if(null != ResultOpenedInPanel) + { + this.ResultOpenedInPanel(this, new ResultOpenedInPanelEventArgs(o as ResultItemViewModel, e.HideWindow)); + } + }, + + (o, e) => { + + if(null != ResultActionPanelOpenedInPanel) + { + this.ResultActionPanelOpenedInPanel(this, new ResultActionPanelOpenedInPanelEventArgs(o as ResultItemViewModel)); + } + + } + ); + } + + #endregion + + #region ViewModel Properties + + public int MaxHeight + { + get + { + return UserSettingStorage.Instance.MaxResultsToShow * 50; + } + } + + public ResultCollection Results + { + get + { + return this._results; + } + } + + public ResultItemViewModel SelectedResult + { + get + { + return this._selectedResult; + } + set + { + this._selectedResult = value; + OnPropertyChanged("SelectedResult"); + } + } + + public Thickness Margin + { + get + { + return this._margin; + } + set + { + this._margin = value; + OnPropertyChanged("Margin"); + } + } + + #endregion + + #region Private Methods + + private bool IsTopMostResult(Result result) + { + return TopMostRecordStorage.Instance.IsTopMost(result); + } + + private int InsertIndexOf(int newScore, IList list) + { + int index = 0; + for (; index < list.Count; index++) + { + var result = list[index]; + if (newScore > result.RawResult.Score) + { + break; + } + } + return index; + } + + #endregion + + #region Public Methods + + public void Clear() + { + this._results.Clear(); + } + + public void RemoveResultsExcept(PluginMetadata metadata) + { + lock (_resultsUpdateLock) + { + _results.RemoveAll(r => r.RawResult.PluginID != metadata.ID); + } + } + + public void RemoveResultsFor(PluginMetadata metadata) + { + lock (_resultsUpdateLock) + { + _results.RemoveAll(r => r.RawResult.PluginID == metadata.ID); + } + } + + public void AddResults(List newRawResults, string resultId) + { + lock (_resultsUpdateLock) + { + var newResults = new List(); + newRawResults.ForEach((re) => { newResults.Add(new ResultItemViewModel(re)); }); + // todo use async to do new result calculation + var resultsCopy = _results.ToList(); + var oldResults = resultsCopy.Where(r => r.RawResult.PluginID == resultId).ToList(); + // intersection of A (old results) and B (new newResults) + var intersection = oldResults.Intersect(newResults).ToList(); + // remove result of relative complement of B in A + foreach (var result in oldResults.Except(intersection)) + { + resultsCopy.Remove(result); + } + + // update scores + foreach (var result in newResults) + { + if (IsTopMostResult(result.RawResult)) + { + result.RawResult.Score = int.MaxValue; + } + } + + // update index for result in intersection of A and B + foreach (var commonResult in intersection) + { + int oldIndex = resultsCopy.IndexOf(commonResult); + int oldScore = resultsCopy[oldIndex].RawResult.Score; + int newScore = newResults[newResults.IndexOf(commonResult)].RawResult.Score; + if (newScore != oldScore) + { + var oldResult = resultsCopy[oldIndex]; + oldResult.RawResult.Score = newScore; + resultsCopy.RemoveAt(oldIndex); + int newIndex = InsertIndexOf(newScore, resultsCopy); + resultsCopy.Insert(newIndex, oldResult); + + } + } + + // insert result in relative complement of A in B + foreach (var result in newResults.Except(intersection)) + { + int newIndex = InsertIndexOf(result.RawResult.Score, resultsCopy); + resultsCopy.Insert(newIndex, result); + } + + // update UI in one run, so it can avoid UI flickering + _results.Update(resultsCopy); + + if(this._results.Count > 0) + { + this.Margin = new Thickness { Top = 8 }; + this.SelectedResult = this._results[0]; + } + else + { + this.Margin = new Thickness { Top = 0 }; + } + } + } + + + #endregion + + public event EventHandler ResultOpenedInPanel; + + public event EventHandler ResultActionPanelOpenedInPanel; + + public class ResultCollection : ObservableCollection + // todo implement custom moveItem,removeItem,insertItem for better performance + { + + private EventHandler _resultOpenedHandler; + private EventHandler _resultActionPanelOpenedHandler; + + public ResultCollection(EventHandler resultOpenedHandler, + EventHandler resultActionPanelOpenedHandler) + { + this._resultOpenedHandler = resultOpenedHandler; + this._resultActionPanelOpenedHandler = resultActionPanelOpenedHandler; + } + + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + base.OnCollectionChanged(e); + + if(e.Action == NotifyCollectionChangedAction.Add) + { + foreach(var item in e.NewItems) + { + var resultVM = item as ResultItemViewModel; + resultVM.ResultOpened += this._resultOpenedHandler; + resultVM.ResultActionPanelOpened += this._resultActionPanelOpenedHandler; + } + } + + if(e.Action == NotifyCollectionChangedAction.Remove) + { + foreach (var item in e.OldItems) + { + var resultVM = item as ResultItemViewModel; + resultVM.ResultOpened -= this._resultOpenedHandler; + resultVM.ResultActionPanelOpened -= this._resultActionPanelOpenedHandler; + } + } + + if(e.Action == NotifyCollectionChangedAction.Replace) + { + foreach (var item in e.NewItems) + { + var resultVM = item as ResultItemViewModel; + resultVM.ResultOpened += this._resultOpenedHandler; + resultVM.ResultActionPanelOpened += this._resultActionPanelOpenedHandler; + } + + foreach (var item in e.OldItems) + { + var resultVM = item as ResultItemViewModel; + resultVM.ResultOpened -= this._resultOpenedHandler; + resultVM.ResultActionPanelOpened -= this._resultActionPanelOpenedHandler; + } + } + } + + public void RemoveAll(Predicate predicate) + { + CheckReentrancy(); + + List itemsToRemove = Items.Where(x => predicate(x)).ToList(); + if (itemsToRemove.Count > 0) + { + + itemsToRemove.ForEach(item => { + + Items.Remove(item); + + }); + + OnPropertyChanged(new PropertyChangedEventArgs("Count")); + OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); + // fuck ms + // http://blogs.msdn.com/b/nathannesbit/archive/2009/04/20/addrange-and-observablecollection.aspx + // http://geekswithblogs.net/NewThingsILearned/archive/2008/01/16/listcollectionviewcollectionview-doesnt-support-notifycollectionchanged-with-multiple-items.aspx + // PS: don't use Reset for other data updates, it will cause UI flickering + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + + } + + public void Update(List newItems) + { + int newCount = newItems.Count; + int oldCount = Items.Count; + int location = newCount > oldCount ? oldCount : newCount; + for (int i = 0; i < location; i++) + { + ResultItemViewModel oldItem = Items[i]; + ResultItemViewModel newItem = newItems[i]; + if (!oldItem.Equals(newItem)) + { + this[i] = newItem; + } + else if (oldItem.RawResult.Score != newItem.RawResult.Score) + { + this[i].RawResult.Score = newItem.RawResult.Score; + } + } + + if (newCount > oldCount) + { + for (int i = oldCount; i < newCount; i++) + { + Add(newItems[i]); + } + } + else + { + int removeIndex = newCount; + for (int i = newCount; i < oldCount; i++) + { + RemoveAt(removeIndex); + } + } + + } + } + + } + + public class ResultOpenedInPanelEventArgs : EventArgs + { + + public bool HideWindow + { + get; + private set; + } + + public ResultItemViewModel Result + { + get; + private set; + } + + public ResultOpenedInPanelEventArgs(ResultItemViewModel result, bool hideWindow) + { + this.HideWindow = hideWindow; + this.Result = result; + } + } + + public class ResultActionPanelOpenedInPanelEventArgs : EventArgs + { + + public ResultItemViewModel Result + { + get; + private set; + } + + public ResultActionPanelOpenedInPanelEventArgs(ResultItemViewModel result) + { + this.Result = result; + } + } +}