1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-06-12 23:29:32 +00:00

Restore selection and top position after change

Restoring the selection works pretty much like it used to, though
I'm capping the total size of the selection because it goes into
stupid-slow mode if you have too many elements.

Getting the item that is at the top of the ListView is astoundingly
obscure, due to the ListView's extreme generality.  I make a
simplifying assumption -- that we're using a VSP -- which allows us
to use a simple vertical offset once we've dug the appropriate
object out of the visual tree.  (The alternative is to walk through
the list of items and see what's on screen, or perform a hit test
calculation in the upper left corner.)  Yay WPF.
This commit is contained in:
Andy McFadden 2019-06-10 15:46:35 -07:00
parent 0c682e9cff
commit 9c8afab2ea
6 changed files with 194 additions and 84 deletions

View File

@ -63,6 +63,7 @@
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<Compile Include="WPFExtensions.cs" />
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>

View File

@ -0,0 +1,93 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace CommonWPF {
/// <summary>
/// Generic Visual helper.
/// </summary>
public static class VisualHelper {
/// <summary>
/// Find a child object in a WPF visual tree.
/// </summary>
/// <remarks>
/// Sample usage:
/// GridViewHeaderRowPresenter headerRow = listView.GetVisualChild&lt;GridViewHeaderRowPresenter&gt;();
///
/// From https://social.msdn.microsoft.com/Forums/vstudio/en-US/7d0626cb-67e8-4a09-a01e-8e56ee7411b2/gridviewcolumheader-radiobuttons?forum=wpf
/// </remarks>
/// <typeparam name="T"></typeparam>
/// <param name="referenceVisual"></param>
/// <returns></returns>
public static T GetVisualChild<T>(this Visual referenceVisual) where T : Visual {
Visual child = null;
for (Int32 i = 0; i < VisualTreeHelper.GetChildrenCount(referenceVisual); i++) {
child = VisualTreeHelper.GetChild(referenceVisual, i) as Visual;
if (child != null && child is T) {
break;
} else if (child != null) {
child = GetVisualChild<T>(child);
if (child != null && child is T) {
break;
}
}
}
return child as T;
}
}
/// <summary>
/// Add functions to get the element that's currently shown at the top of the ListView
/// window, and to scroll the list so that a specific item is at the top.
/// </summary>
public static class ListViewExtensions {
/// <summary>
/// Figures out which item index is at the top of the window. This only works for a
/// ListView with a VirtualizingStackPanel.
/// </summary>
/// <returns>The item index, or -1 if the list is empty.</returns>
public static int GetTopItemIndex(this ListView lv) {
if (lv.Items.Count == 0) {
return -1;
}
VirtualizingStackPanel vsp = lv.GetVisualChild<VirtualizingStackPanel>();
if (vsp == null) {
Debug.Assert(false, "ListView does not have a VirtualizingStackPanel");
return -1;
}
return (int) vsp.VerticalOffset;
}
/// <summary>
/// Scrolls the ListView so that the specified item is at the top. The standard
/// ListView.ScrollIntoView() makes the item visible but doesn't ensure a
/// specific placement.
/// </summary>
/// <remarks>
/// Equivalent to setting myListView.TopItem in WinForms.
/// </remarks>
public static void ScrollToTopItem(this ListView lv, object item) {
ScrollViewer sv = lv.GetVisualChild<ScrollViewer>();
sv.ScrollToBottom();
lv.ScrollIntoView(item);
}
}
}

View File

@ -39,6 +39,11 @@ namespace SourceGenWPF {
/// </summary>
public int Length { get { return mSelection.Length; } }
/// <summary>
/// Retrieves the number of values that are set.
/// </summary>
public int Count { get; private set; }
/// <summary>
/// Sets or gets the Nth element. True means the line is selected.
/// </summary>
@ -47,7 +52,12 @@ namespace SourceGenWPF {
return mSelection[key];
}
set {
mSelection[key] = value;
// If an entry has changed, update the count of set items.
if (mSelection[key] != value) {
Count += value ? 1 : -1;
mSelection[key] = value;
}
Debug.Assert(Count >= 0 && Count <= Length);
}
}
@ -98,11 +108,11 @@ namespace SourceGenWPF {
public void SelectionChanged(SelectionChangedEventArgs e) {
foreach (DisplayList.FormattedParts parts in e.AddedItems) {
Debug.Assert(parts.ListIndex >= 0 && parts.ListIndex < mSelection.Length);
mSelection.Set(parts.ListIndex, true);
this[parts.ListIndex] = true;
}
foreach (DisplayList.FormattedParts parts in e.RemovedItems) {
Debug.Assert(parts.ListIndex >= 0 && parts.ListIndex < mSelection.Length);
mSelection.Set(parts.ListIndex, false);
this[parts.ListIndex] = false;
}
}
@ -135,6 +145,13 @@ namespace SourceGenWPF {
return idx;
}
/// <summary>
/// Returns true if all items are selected.
/// </summary>
public bool IsAllSelected() {
return Count == Length;
}
/// <summary>
/// Confirms that the selection count matches the number of set bits. Pass
/// in {ListView}.SelectedIndices.Count.
@ -142,16 +159,20 @@ namespace SourceGenWPF {
/// <param name="expected">Expected number of selected entries.</param>
/// <returns>True if count matches.</returns>
public bool DebugValidateSelectionCount(int expected) {
int actual = 0;
if (Count != expected) {
Debug.WriteLine("SelectionCount expected " + expected + ", count=" + Count);
}
int computed = 0;
foreach (bool bit in mSelection) {
if (bit) {
actual++;
computed++;
}
}
if (actual != expected) {
Debug.WriteLine("SelectionCount expected " + expected + ", actual " + actual);
if (Count != computed) {
Debug.WriteLine("SelectionCount internal error: computed=" + computed +
", count=" + Count);
}
return (actual == expected);
return (Count == expected);
}
public void DebugDump() {
@ -161,7 +182,7 @@ namespace SourceGenWPF {
rangeSet.Add(i);
}
}
Debug.WriteLine("VirtualListViewSelection ranges:");
Debug.WriteLine("DisplayListSelection ranges:");
IEnumerator<RangeSet.Range> iter = rangeSet.RangeListIterator;
while (iter.MoveNext()) {
RangeSet.Range range = iter.Current;

View File

@ -478,14 +478,10 @@ namespace SourceGenWPF {
mReanalysisTimer.StartTask("ProjectView.ApplyChanges()");
mReanalysisTimer.StartTask("Save selection");
#if false
int topItem = codeListView.TopItem.Index;
#else
int topItem = 0;
#endif
int topOffset = CodeListGen[topItem].FileOffset;
int topItemIndex = mMainWin.GetCodeListTopIndex();
LineListGen.SavedSelection savedSel = LineListGen.SavedSelection.Generate(
CodeListGen, mMainWin.CodeDisplayList.SelectedIndices, topOffset);
CodeListGen, mMainWin.CodeDisplayList.SelectedIndices,
CodeListGen[topItemIndex].FileOffset);
//savedSel.DebugDump();
mReanalysisTimer.EndTask("Save selection");
@ -506,26 +502,15 @@ namespace SourceGenWPF {
}
mReanalysisTimer.EndTask(refreshTaskStr);
DisplayListSelection newSel = savedSel.Restore(CodeListGen, out int topIndex);
DisplayListSelection newSel = savedSel.Restore(CodeListGen, out topItemIndex);
//newSel.DebugDump();
// Refresh the various windows, and restore the selection.
mReanalysisTimer.StartTask("Invalidate controls");
#if false
InvalidateControls(newSel);
#endif
mReanalysisTimer.EndTask("Invalidate controls");
// This apparently has to be done after the EndUpdate, and inside try/catch.
// See https://stackoverflow.com/questions/626315/ for notes.
try {
Debug.WriteLine("Setting TopItem to index=" + topIndex);
#if false
codeListView.TopItem = codeListView.Items[topIndex];
#endif
} catch (NullReferenceException) {
Debug.WriteLine("Caught an NRE from TopItem");
}
// Restore the selection. The selection-changed event will cause updates to the
// references, notes, and info panels.
mReanalysisTimer.StartTask("Restore selection and top position");
mMainWin.SetSelection(newSel);
mMainWin.SetCodeListTopIndex(topItemIndex);
mReanalysisTimer.EndTask("Restore selection and top position");
mReanalysisTimer.EndTask("ProjectView.ApplyChanges()");
@ -652,54 +637,6 @@ namespace SourceGenWPF {
#region Main window UI event handlers
#if false
/// <summary>
/// Restores the ListView selection by applying a diff between the old and
/// new selection bitmaps.
///
/// The virtual list view doesn't change the selection when we rebuild the
/// list. It would be expensive to set all the bits, so we just update the
/// entries that changed.
///
/// Before returning, mCodeViewSelection is replaced with curSel.
/// </summary>
/// <param name="curSel">Selection bits for the current display list.</param>
private void RestoreSelection(DisplayListSelection curSel) {
Debug.Assert(curSel != null);
// We have to replace mCodeViewSelection immediately, because changing
// the selection will cause ItemSelectionChanged events to fire, invoking
// callbacks that expect the new selection object. Things will explode if
// the older list was shorter.
DisplayListSelection prevSel = mCodeViewSelection;
mCodeViewSelection = curSel;
// Set everything that has changed between the two sets.
int debugNumChanged = 0;
int count = Math.Min(prevSel.Length, curSel.Length);
int i;
for (i = 0; i < count; i++) {
if (prevSel[i] != curSel[i]) {
codeListView.Items[i].Selected = curSel[i];
debugNumChanged++;
}
}
// Set everything that wasn't there before. New entries default to unselected,
// so we only need to do this if the new value is "true".
for (; i < curSel.Length; i++) {
// An ItemSelectionChanged event will fire that will cause curSel[i] to
// be assigned. This is fine.
if (curSel[i]) {
codeListView.Items[i].Selected = curSel[i];
debugNumChanged++;
}
}
Debug.WriteLine("RestoreSelection: changed " + debugNumChanged +
" of " + curSel.Length + " lines");
}
#endif
public void OpenRecentProject(int projIndex) {
if (!CloseProject()) {
return;

View File

@ -189,11 +189,15 @@ limitations under the License.
<GroupBox Grid.Row="0" Header="References">
<DataGrid Name="referencesList" IsReadOnly="True"
ItemsSource="{Binding ReferencesList}"
FontFamily="{StaticResource GeneralMonoFont}"
SnapsToDevicePixels="True"
ItemsSource="{Binding ReferencesList}"
GridLinesVisibility="Vertical"
VerticalGridLinesBrush="#FF7F7F7F"
AutoGenerateColumns="False"
GridLinesVisibility="Vertical" VerticalGridLinesBrush="#FF7F7F7F">
HeadersVisibility="Column"
CanUserReorderColumns="False"
SelectionMode="Single">
<DataGrid.Columns>
<DataGridTextColumn Header="Offset" Binding="{Binding Offset}"/>
<DataGridTextColumn Header="Addr" Binding="{Binding Addr}"/>

View File

@ -209,6 +209,7 @@ namespace SourceGenWPF.ProjWin {
get { return mShowCodeListView ? Visibility.Visible : Visibility.Hidden; }
}
#region Selection management
private void CodeListView_SelectionChanged(object sender, SelectionChangedEventArgs e) {
//DateTime startWhen = DateTime.Now;
@ -295,6 +296,59 @@ namespace SourceGenWPF.ProjWin {
}
}
/// <summary>
/// Sets the code list selection.
/// </summary>
/// <param name="sel">Selection bitmap.</param>
public void SetSelection(DisplayListSelection sel) {
const int MAX_SEL_COUNT = 2000;
if (sel.IsAllSelected()) {
codeListView.SelectAll();
return;
}
Debug.Assert(codeListView.SelectedItems.Count == 0); // expected
codeListView.SelectedItems.Clear(); // just in case
if (sel.Count > MAX_SEL_COUNT) {
// Too much for WPF -- only restore the first item.
Debug.WriteLine("Not restoring selection (" + sel.Count + " items)");
codeListView.SelectedItems.Add(CodeDisplayList[sel.GetFirstSelectedIndex()]);
return;
}
//DateTime startWhen = DateTime.Now;
DisplayList.FormattedParts[] tmpArray = new DisplayList.FormattedParts[sel.Count];
int ai = 0;
foreach (int listIndex in sel) {
tmpArray[ai++] = CodeDisplayList[listIndex];
}
// Use a reflection call to provide the full set. This is much faster than
// adding the items one at a time to SelectedItems. (For one thing, it only
// invokes the SelectionChanged method once.)
listViewSetSelectedItems.Invoke(codeListView, new object[] { tmpArray });
//Debug.WriteLine("SetSelection on " + sel.Count + " items took " +
// (DateTime.Now - startWhen).TotalMilliseconds + " ms");
}
public int GetCodeListTopIndex() {
return codeListView.GetTopItemIndex();
}
public void SetCodeListTopIndex(int 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]);
}
#endregion Selection management
#region Can-execute handlers