From a6874d115bc19ad9d7eb378f2aeca2a039196d8b Mon Sep 17 00:00:00 2001 From: Anthony Purchase Date: Thu, 15 Nov 2018 23:31:12 +0000 Subject: [PATCH] Improve/fix UI in a few ways Fixed a bug where the tames list could overwrite a user's custom search list. Improve responsiveness of UI when altering searches. Add display of the number of creatures that match a search. --- LarkatorGUI/Converters.cs | 5 +- LarkatorGUI/DebounceDispatcher.cs | 114 ++++++++++++++++++++++++++++++ LarkatorGUI/LarkatorGUI.csproj | 1 + LarkatorGUI/MainWindow.xaml | 7 +- LarkatorGUI/MainWindow.xaml.cs | 60 ++++++++++++++-- 5 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 LarkatorGUI/DebounceDispatcher.cs diff --git a/LarkatorGUI/Converters.cs b/LarkatorGUI/Converters.cs index 798d007..ce2745e 100644 --- a/LarkatorGUI/Converters.cs +++ b/LarkatorGUI/Converters.cs @@ -10,8 +10,9 @@ public class BoolToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - var v = (bool)value; - return v ? Visibility.Visible : Visibility.Collapsed; + if (value is bool asBool) return asBool ? Visibility.Visible : Visibility.Collapsed; + if (value is int asInt) return asInt != 0 ? Visibility.Visible : Visibility.Collapsed; + return Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/LarkatorGUI/DebounceDispatcher.cs b/LarkatorGUI/DebounceDispatcher.cs new file mode 100644 index 0000000..e3a1c3e --- /dev/null +++ b/LarkatorGUI/DebounceDispatcher.cs @@ -0,0 +1,114 @@ +using System; +using System.Windows.Threading; + +namespace LarkatorGUI +{ + /// + /// Provides Debounce() and Throttle() methods. + /// Use these methods to ensure that events aren't handled too frequently. + /// + /// Throttle() ensures that events are throttled by the interval specified. + /// Only the last event in the interval sequence of events fires. + /// + /// Debounce() fires an event only after the specified interval has passed + /// in which no other pending event has fired. Only the last event in the + /// sequence is fired. + /// + /// From https://weblog.west-wind.com/posts/2017/Jul/02/Debouncing-and-Throttling-Dispatcher-Events. + /// + public class DebounceDispatcher + { + private DispatcherTimer timer; + private DateTime timerStarted { get; set; } = DateTime.UtcNow.AddYears(-1); + + /// + /// Debounce an event by resetting the event timeout every time the event is + /// fired. The behavior is that the Action passed is fired only after events + /// stop firing for the given timeout period. + /// + /// Use Debounce when you want events to fire only after events stop firing + /// after the given interval timeout period. + /// + /// Wrap the logic you would normally use in your event code into + /// the Action you pass to this method to debounce the event. + /// Example: https://gist.github.com/RickStrahl/0519b678f3294e27891f4d4f0608519a + /// + /// Timeout in Milliseconds + /// Action to fire when debounced event fires + /// optional parameter + /// optional priorty for the dispatcher + /// optional dispatcher. If not passed or null CurrentDispatcher is used. + public void Debounce(int interval, Action action, + object param = null, + DispatcherPriority priority = DispatcherPriority.ApplicationIdle, + Dispatcher disp = null) + { + // kill pending timer and pending ticks + timer?.Stop(); + timer = null; + + if (disp == null) + disp = Dispatcher.CurrentDispatcher; + + // timer is recreated for each event and effectively + // resets the timeout. Action only fires after timeout has fully + // elapsed without other events firing in between + timer = new DispatcherTimer(TimeSpan.FromMilliseconds(interval), priority, (s, e) => + { + if (timer == null) + return; + + timer?.Stop(); + timer = null; + action.Invoke(param); + }, disp); + + timer.Start(); + } + + /// + /// This method throttles events by allowing only 1 event to fire for the given + /// timeout period. Only the last event fired is handled - all others are ignored. + /// Throttle will fire events every timeout ms even if additional events are pending. + /// + /// Use Throttle where you need to ensure that events fire at given intervals. + /// + /// Timeout in Milliseconds + /// Action to fire when debounced event fires + /// optional parameter + /// optional priorty for the dispatcher + /// optional dispatcher. If not passed or null CurrentDispatcher is used. + public void Throttle(int interval, Action action, + object param = null, + DispatcherPriority priority = DispatcherPriority.ApplicationIdle, + Dispatcher disp = null) + { + // kill pending timer and pending ticks + timer?.Stop(); + timer = null; + + if (disp == null) + disp = Dispatcher.CurrentDispatcher; + + var curTime = DateTime.UtcNow; + + // if timeout is not up yet - adjust timeout to fire + // with potentially new Action parameters + if (curTime.Subtract(timerStarted).TotalMilliseconds < interval) + interval -= (int)curTime.Subtract(timerStarted).TotalMilliseconds; + + timer = new DispatcherTimer(TimeSpan.FromMilliseconds(interval), priority, (s, e) => + { + if (timer == null) + return; + + timer?.Stop(); + timer = null; + action.Invoke(param); + }, disp); + + timer.Start(); + timerStarted = curTime; + } + } +} diff --git a/LarkatorGUI/LarkatorGUI.csproj b/LarkatorGUI/LarkatorGUI.csproj index 85a68fd..76be390 100644 --- a/LarkatorGUI/LarkatorGUI.csproj +++ b/LarkatorGUI/LarkatorGUI.csproj @@ -114,6 +114,7 @@ CalibrationWindow.xaml + diff --git a/LarkatorGUI/MainWindow.xaml b/LarkatorGUI/MainWindow.xaml index e0b2800..7f78c14 100644 --- a/LarkatorGUI/MainWindow.xaml +++ b/LarkatorGUI/MainWindow.xaml @@ -350,6 +350,7 @@ + @@ -380,7 +381,11 @@ - + + Matching of creatures + + + diff --git a/LarkatorGUI/MainWindow.xaml.cs b/LarkatorGUI/MainWindow.xaml.cs index 6a7916e..6f433d4 100644 --- a/LarkatorGUI/MainWindow.xaml.cs +++ b/LarkatorGUI/MainWindow.xaml.cs @@ -106,6 +106,33 @@ public string SearchText set { SetValue(SearchTextProperty, value); } } + public int ResultMatchingCount + { + get { return (int)GetValue(ResultMatchingCountProperty); } + set { SetValue(ResultMatchingCountProperty, value); } + } + + public int ResultTotalCount + { + get { return (int)GetValue(ResultTotalCountProperty); } + set { SetValue(ResultTotalCountProperty, value); } + } + + public bool ShowCounts + { + get { return (bool)GetValue(ShowCountsProperty); } + set { SetValue(ShowCountsProperty, value); } + } + + public static readonly DependencyProperty ShowCountsProperty = + DependencyProperty.Register("ShowCounts", typeof(bool), typeof(MainWindow), new PropertyMetadata(false)); + + public static readonly DependencyProperty ResultTotalCountProperty = + DependencyProperty.Register("ResultTotalCount", typeof(int), typeof(MainWindow), new PropertyMetadata(0)); + + public static readonly DependencyProperty ResultMatchingCountProperty = + DependencyProperty.Register("ResultMatchingCount", typeof(int), typeof(MainWindow), new PropertyMetadata(0)); + public static readonly DependencyProperty SearchTextProperty = DependencyProperty.Register("SearchText", typeof(string), typeof(MainWindow), new PropertyMetadata("")); @@ -147,6 +174,8 @@ public string SearchText string nameSearchArg; bool nameSearchRunning; private string lastArk; + private DebounceDispatcher refreshSearchesTimer = new DebounceDispatcher(); + private DebounceDispatcher settingsSaveTimer = new DebounceDispatcher(); public MainWindow() { @@ -601,13 +630,16 @@ private void ResultList_Sorting(object sender, DataGridSortingEventArgs e) private void MarkSearchesChanged() { - SaveSearches(); + settingsSaveTimer.Debounce(1000, o => SaveSearches()); } private void SaveSearches() { - Properties.Settings.Default.SavedSearches = JsonConvert.SerializeObject(ListSearches); - Properties.Settings.Default.Save(); + if (!ShowTames) + { + Properties.Settings.Default.SavedSearches = JsonConvert.SerializeObject(ListSearches); + Properties.Settings.Default.Save(); + } } private void LoadSavedSearches() @@ -653,12 +685,16 @@ private void UpdateSearchResults(IList searches) // Find dinos that match the given searches var found = new List(); var sourceDinos = ShowTames ? arkReader.TamedDinos : arkReader.WildDinos; + var total = 0; foreach (var search in searches) { if (String.IsNullOrWhiteSpace(search.Species)) { foreach (var speciesDinos in sourceDinos.Values) + { found.AddRange(speciesDinos); + total += speciesDinos.Count; + } } else { @@ -666,6 +702,7 @@ private void UpdateSearchResults(IList searches) { var dinoList = sourceDinos[search.Species]; found.AddRange(dinoList.Where(d => search.Matches(d))); + total += dinoList.Count; } } } @@ -673,6 +710,10 @@ private void UpdateSearchResults(IList searches) ListResults.Clear(); foreach (var result in found) ListResults.Add(result); + + ShowCounts = true; + ResultTotalCount = ShowTames ? sourceDinos.Sum(species => species.Value.Count()) : total; + ResultMatchingCount = ListResults.Count; } ((CollectionViewSource)Resources["OrderedResults"]).View.Refresh(); @@ -707,10 +748,15 @@ private async Task PerformConversion() private void UpdateCurrentSearch() { - var search = (SearchCriteria)searchesList.SelectedItem; - var searches = new List(); - if (search != null) searches.Add(search); - UpdateSearchResults(searches); + void updateSearch(object o) + { + var search = (SearchCriteria)searchesList.SelectedItem; + var searches = new List(); + if (search != null) searches.Add(search); + UpdateSearchResults(searches); + } + + refreshSearchesTimer.Debounce(250, updateSearch); } void IDropTarget.DragOver(IDropInfo dropInfo)