From caf061b3cab7774fedc4600d8d93ef72c6ae4fa2 Mon Sep 17 00:00:00 2001 From: SamFinni Date: Sun, 15 Mar 2026 19:02:12 -0700 Subject: [PATCH] Preserve Skill Planner grid scroll position across plan updates The lvSkills ListView was resetting its scroll position to the top on every plan/character change because TopItem (LVM_SCROLL) is silently ignored while WM_SETREDRAW is FALSE inside BeginUpdate/EndUpdate. focusedItem.Focused also triggers EnsureVisible, compounding the issue. Fix: capture the top-visible item's tag hash before the rebuild and restore it via a new RestoreScrollPosition() helper called after EndUpdate() in both UpdateSkillList() and UpdateListColumns(), mirroring the existing StoreSelection/RestoreSelection pattern. --- src/EVEMon/SkillPlanner/PlanEditorControl.cs | 36 +++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/EVEMon/SkillPlanner/PlanEditorControl.cs b/src/EVEMon/SkillPlanner/PlanEditorControl.cs index 1c617a12b..1057e0f24 100644 --- a/src/EVEMon/SkillPlanner/PlanEditorControl.cs +++ b/src/EVEMon/SkillPlanner/PlanEditorControl.cs @@ -402,9 +402,10 @@ private void UpdateSkillList() // Disable autorefresh timer, it will be enabled if a training entry is found tmrAutoRefresh.Stop(); - // Stores selection and focus, to restore them after the update + // Stores selection, focus, and scroll position, to restore them after the update Dictionary selection = StoreSelection(); int focusedHashCode = lvSkills.FocusedItem?.Tag.GetHashCode() ?? 0; + int topItemHash = lvSkills.TopItem?.Tag?.GetHashCode() ?? 0; lvSkills.BeginUpdate(); try @@ -459,6 +460,11 @@ private void UpdateSkillList() { lvSkills.EndUpdate(); } + + // Restore scroll position AFTER EndUpdate — TopItem uses LVM_SCROLL which is + // ignored while WM_SETREDRAW is FALSE (i.e. inside BeginUpdate/EndUpdate), so + // it must be set here once the control is fully ready to accept scroll commands. + RestoreScrollPosition(topItemHash); } /// @@ -783,6 +789,12 @@ private static string GetColumnTextForItem(PlanEntry entry, PlanColumn column, s private void UpdateListColumns() { m_isUpdatingColumns = true; + + // Capture scroll position before BeginUpdate/Items.Clear can affect it. + // Must be outside the BeginUpdate block because TopItem restoration (below) + // also needs to happen outside BeginUpdate for the same reason. + int topItemHash = lvSkills.TopItem?.Tag?.GetHashCode() ?? 0; + lvSkills.BeginUpdate(); try @@ -819,6 +831,10 @@ private void UpdateListColumns() lvSkills.EndUpdate(); m_isUpdatingColumns = false; } + + // Re-apply scroll position after this method's own EndUpdate for the same + // reason as in UpdateSkillList: LVM_SCROLL is suppressed inside BeginUpdate. + RestoreScrollPosition(topItemHash); } /// @@ -838,6 +854,24 @@ private Dictionary StoreSelection() return c; } + /// + /// Restores the scroll position to the item whose tag hash matches . + /// Must be called after — the underlying LVM_SCROLL message is + /// suppressed while WM_SETREDRAW is FALSE (i.e. inside a BeginUpdate/EndUpdate block). + /// + /// Hash code of the item to restore as the top visible row, or 0 to skip. + private void RestoreScrollPosition(int topItemHash) + { + if (topItemHash == 0) + return; + + ListViewItem topItem = lvSkills.Items.Cast() + .FirstOrDefault(x => x.Tag?.GetHashCode() == topItemHash); + + if (topItem != null) + lvSkills.TopItem = topItem; + } + /// /// Restores the selection from a dictionary where keys are tags' hash codes. ///