diff --git a/CommonWPF/WPFExtensions.cs b/CommonWPF/WPFExtensions.cs index 311a60f..4d33c9f 100644 --- a/CommonWPF/WPFExtensions.cs +++ b/CommonWPF/WPFExtensions.cs @@ -71,6 +71,9 @@ namespace CommonWPF { /// /// See https://stackoverflow.com/q/2926722/294248 for an alternative approach that /// uses hit-testing, as well as a copy of this approach. + /// + /// Looks like we get the same values from ScrollViewer.VerticalOffset. I don't know + /// if there's a reason to favor one over the other. /// /// The item index, or -1 if the list is empty. public static int GetTopItemIndex(this ListView lv) { @@ -92,7 +95,9 @@ namespace CommonWPF { /// specific placement. /// /// - /// Equivalent to setting myListView.TopItem in WinForms. + /// Equivalent to setting myListView.TopItem in WinForms. Unfortunately, the + /// ScrollIntoView call takes 60-100ms on a list with fewer than 1,000 items. And + /// sometimes it just silently fails. Prefer ScrollToIndex() to this. /// public static void ScrollToTopItem(this ListView lv, object item) { ScrollViewer sv = lv.GetVisualChild(); @@ -100,6 +105,15 @@ namespace CommonWPF { lv.ScrollIntoView(item); } + /// + /// Scrolls the ListView to the specified vertical index. The ScrollViewer should + /// be operating in "logical" units (lines) rather than "physical" units (pixels). + /// + public static void ScrollToIndex(this ListView lv, int index) { + ScrollViewer sv = lv.GetVisualChild(); + sv.ScrollToVerticalOffset(index); + } + /// /// Returns the ListViewItem that was clicked on, or null if an LVI wasn't the target /// of a click (e.g. off the bottom of the list). diff --git a/SourceGenWPF/DisplayListSelection.cs b/SourceGenWPF/DisplayListSelection.cs index faf4778..25dd4bf 100644 --- a/SourceGenWPF/DisplayListSelection.cs +++ b/SourceGenWPF/DisplayListSelection.cs @@ -106,6 +106,8 @@ namespace SourceGenWPF { /// /// Argument from SelectionChanged event. public void SelectionChanged(SelectionChangedEventArgs e) { + Debug.WriteLine("SelectionChanged event: Add=" + e.AddedItems.Count + + " Rem=" + e.RemovedItems.Count); foreach (DisplayList.FormattedParts parts in e.AddedItems) { Debug.Assert(parts.ListIndex >= 0 && parts.ListIndex < mSelection.Length); this[parts.ListIndex] = true; diff --git a/SourceGenWPF/MainController.cs b/SourceGenWPF/MainController.cs index 2f4fbb7..cbb5a00 100644 --- a/SourceGenWPF/MainController.cs +++ b/SourceGenWPF/MainController.cs @@ -695,6 +695,7 @@ namespace SourceGenWPF { mReanalysisTimer.StartTask("ProjectView.ApplyChanges()"); mReanalysisTimer.StartTask("Save selection"); + mMainWin.CodeListView_DebugValidateSelectionCount(); int topItemIndex = mMainWin.CodeListView_GetTopIndex(); LineListGen.SavedSelection savedSel = LineListGen.SavedSelection.Generate( CodeLineList, mMainWin.CodeDisplayList.SelectedIndices, topItemIndex); @@ -1493,7 +1494,6 @@ namespace SourceGenWPF { if (SelectionAnalysis.mNumItemsSelected != 1) { return false; } - Debug.WriteLine("LINE TYPE " + SelectionAnalysis.mLineType); return (SelectionAnalysis.mLineType == LineListGen.Line.Type.LongComment || SelectionAnalysis.mLineType == LineListGen.Line.Type.Note); } diff --git a/SourceGenWPF/WpfGui/MainWindow.xaml.cs b/SourceGenWPF/WpfGui/MainWindow.xaml.cs index a9ae75d..a1f3925 100644 --- a/SourceGenWPF/WpfGui/MainWindow.xaml.cs +++ b/SourceGenWPF/WpfGui/MainWindow.xaml.cs @@ -697,14 +697,21 @@ namespace SourceGenWPF.WpfGui { /// /// Selection bitmap. public void CodeListView_SetSelection(DisplayListSelection sel) { - const int MAX_SEL_COUNT = 2000; + // Time required increases non-linearly. Quick test: + // 50K: 10 seconds, 20K: 1.6 sec, 10K: 0.6 sec, 5K: 0.2 sec + const int MAX_SEL_COUNT = 5000; + + // The caller will clear the DisplayListSelection before calling here, so we + // need to clear the ListView selection to match, even if we're about to call + // SelectAll. If we don't, the SelectAll() call won't generate the necessary + // events, and our DisplayListSelection will get out of sync. + codeListView.SelectedItems.Clear(); if (sel.IsAllSelected()) { Debug.WriteLine("SetSelection: re-selecting all items"); codeListView.SelectAll(); return; } - codeListView.SelectedItems.Clear(); if (sel.Count > MAX_SEL_COUNT) { // Too much for WPF ListView -- only restore the first item. @@ -713,6 +720,9 @@ namespace SourceGenWPF.WpfGui { return; } + Debug.WriteLine("SetSelection: selecting " + sel.Count + " of " + + CodeDisplayList.Count); + //DateTime startWhen = DateTime.Now; DisplayList.FormattedParts[] tmpArray = new DisplayList.FormattedParts[sel.Count]; @@ -771,20 +781,33 @@ namespace SourceGenWPF.WpfGui { } } + /// + /// Returns the index of the line that's currently at the top of the control. + /// public int CodeListView_GetTopIndex() { int index = codeListView.GetTopItemIndex(); Debug.Assert(index >= 0); return index; } + /// + /// Scrolls the code list so that the specified index is at the top of the control. + /// + /// Line index. public void CodeListView_SetTopIndex(int index) { + //Debug.WriteLine("CodeListView_SetTopIndex(" + index + "): " + CodeDisplayList[index]); + // ScrollIntoView does the least amount of scrolling required. This extension // method scrolls to the bottom, then scrolls back up to the top item. // - // NOTE: it looks like scroll-to-bottom (which is done directly on the - // ScrollViewer) happens immediately, whiel scroll-to-item (which is done via the - // ListView) kicks in later. So don't try to check the topmost item immediately. - codeListView.ScrollToTopItem(CodeDisplayList[index]); + // It looks like scroll-to-bottom (which is done directly on the ScrollViewer) + // happens immediately, while scroll-to-item (which is done via the ListView) + // kicks in later. So you can't immediately query the top item to see where + // we were moved to. + //codeListView.ScrollToTopItem(CodeDisplayList[index]); + + // This works much better. + codeListView.ScrollToIndex(index); } ///