From 85bf8edbf0858035f2cc4781969eefedff9adc23 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 1 Apr 2026 13:57:02 -0400 Subject: [PATCH] LT-22452: fix DateSlice mouse wheel scrolling --- .../DetailControls/BasicTypeSlices.cs | 48 ++++++- .../Controls/DetailControls/DataTree.cs | 44 ++++++ .../DetailControlsTests/DataTreeTests.cs | 129 ++++++++++++++++++ 3 files changed, 220 insertions(+), 1 deletion(-) diff --git a/Src/Common/Controls/DetailControls/BasicTypeSlices.cs b/Src/Common/Controls/DetailControls/BasicTypeSlices.cs index f2135c7e68..26cc7504b8 100644 --- a/Src/Common/Controls/DetailControls/BasicTypeSlices.cs +++ b/Src/Common/Controls/DetailControls/BasicTypeSlices.cs @@ -10,6 +10,7 @@ // using System; +using System.Diagnostics; using System.Globalization; using System.Windows.Forms; using System.Xml; @@ -307,13 +308,58 @@ public override void PropChanged(int hvo, int tag, int ivMin, int cvIns, int cvD /// public class DateSlice : FieldSlice, IVwNotifyChange { + private static readonly TraceSwitch s_wheelTraceSwitch = new TraceSwitch("DateSliceWheel", ""); + private const int kWmMouseWheel = 0x020A; + + private sealed class WheelForwardingRichTextBox : RichTextBox + { + protected override void WndProc(ref Message m) + { + if (m.Msg == kWmMouseWheel) + { + int delta = (short)((long)m.WParam >> 16); + bool handled = TryScrollOwningDataTree(this, delta); + TraceWheel(string.Format("RichTextBoxWndProc delta={0} handled={1}", delta, handled)); + if (handled) + return; + } + + base.WndProc(ref m); + } + } + + private static void TraceWheel(string message) + { + if (s_wheelTraceSwitch.TraceInfo || s_wheelTraceSwitch.TraceVerbose) + Trace.WriteLine("DateSliceWheel: " + message); + } + + internal static bool TryScrollOwningDataTree(Control control, int delta) + { + for (Control current = control; current != null; current = current.Parent) + { + var dataTree = current as DataTree; + if (dataTree != null) + { + bool handled = DataTree.TryHandleWheelScroll(dataTree, delta); + TraceWheel(string.Format( + "TryScrollOwningDataTree delta={0} dataTreeVisible={1} handled={2}", + delta, dataTree.Visible, handled)); + return handled; + } + } + + TraceWheel(string.Format("TryScrollOwningDataTree delta={0} no-datatree", delta)); + return false; + } + /// ----------------------------------------------------------------------------------- /// /// Initializes a new instance of the class. /// /// ----------------------------------------------------------------------------------- public DateSlice(LcmCache cache, ICmObject obj, int flid) - : base(new RichTextBox(), cache, obj, flid) + : base(new WheelForwardingRichTextBox(), cache, obj, flid) { // JohnT: per comment at the end of LT-7073, we want the normal window color for this // slice. It's also nice to be able to select and copy. Setting ReadOnly is enough to prevent diff --git a/Src/Common/Controls/DetailControls/DataTree.cs b/Src/Common/Controls/DetailControls/DataTree.cs index 65adc8f8c6..0a02ea6748 100644 --- a/Src/Common/Controls/DetailControls/DataTree.cs +++ b/Src/Common/Controls/DetailControls/DataTree.cs @@ -4531,6 +4531,50 @@ private bool EquivalentKeys(object[] newKey, object[] oldKey, bool fCheckInts) } return true; } + + internal static int GetWheelScrollPixels(DataTree dataTree, int delta) + { + if (delta == 0) + return 0; + + int scrollLines = SystemInformation.MouseWheelScrollLines; + if (scrollLines == 0) + return 0; + + if (scrollLines == int.MaxValue) + return Math.Sign(delta) * dataTree.ClientRectangle.Height; + + double linesToScroll = (double)delta / SystemInformation.MouseWheelScrollDelta * scrollLines; + return (int)Math.Round(linesToScroll * dataTree.Font.Height, MidpointRounding.AwayFromZero); + } + + internal static bool TryGetWheelScrollPosition(DataTree dataTree, int delta, out int newY) + { + int currentY = -dataTree.AutoScrollPosition.Y; + int maxScroll = Math.Max(0, + dataTree.AutoScrollMinSize.Height - dataTree.ClientRectangle.Height); + int pixelDelta = GetWheelScrollPixels(dataTree, delta); + newY = Math.Max(0, Math.Min(currentY - pixelDelta, maxScroll)); + return newY != currentY; + } + + internal static bool CanRedirectWheelMessage(DataTree dataTree) + { + return dataTree.IsHandleCreated && !dataTree.IsDisposed && dataTree.Visible; + } + + internal static bool TryHandleWheelScroll(DataTree dataTree, int delta) + { + if (!CanRedirectWheelMessage(dataTree)) + return false; + + int newY; + if (!TryGetWheelScrollPosition(dataTree, delta, out newY)) + return false; + + dataTree.AutoScrollPosition = new Point(0, newY); + return true; + } } class DummyObjectSlice : Slice diff --git a/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeTests.cs b/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeTests.cs index ac83210b6d..555e744b98 100644 --- a/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeTests.cs +++ b/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeTests.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.IO; +using System.Reflection; using System.Windows.Forms; using System.Xml; using NUnit.Framework; @@ -31,6 +33,13 @@ public class DataTreeTests : MemoryOnlyBackendProviderRestoredForEachTestTestBas private DataTree m_dtree; private Form m_parent; + private sealed class ScrollTestDataTree : DataTree + { + protected override void OnPaint(PaintEventArgs e) + { + } + } + private CustomFieldForTest m_customField; #region Fixture Setup and Teardown internal static Inventory GenerateParts() @@ -77,6 +86,30 @@ public override void FixtureSetup() } #endregion + private static DataTree CreateScrollableDataTree(Form parent) + { + var dataTree = new ScrollTestDataTree(); + parent.Size = new Size(400, 200); + dataTree.Dock = DockStyle.Fill; + parent.Controls.Add(dataTree); + + for (int i = 0; i < 12; i++) + { + var slice = new Slice(new Panel { Dock = DockStyle.Fill }) + { + Visible = true, + Size = new Size(360, 50), + Location = new Point(0, i * 50) + }; + dataTree.Controls.Add(slice); + slice.Install(dataTree); + } + + parent.Show(); + Application.DoEvents(); + return dataTree; + } + #region Test setup and teardown /// ------------------------------------------------------------------------------------ /// @@ -275,6 +308,102 @@ public void GetGuidForJumpToTool_UsesRootObject_WhenNoCurrentSlice() } } + [Test] + public void GetWheelScrollPixels_UsesSystemWheelSettings() + { + m_dtree.Bounds = new Rectangle(0, 0, 200, 100); + + int delta = SystemInformation.MouseWheelScrollDelta; + int scrollLines = SystemInformation.MouseWheelScrollLines; + int expectedPixels; + if (scrollLines == 0) + { + expectedPixels = 0; + } + else if (scrollLines == int.MaxValue) + { + expectedPixels = m_dtree.ClientRectangle.Height; + } + else + { + expectedPixels = (int)Math.Round((double)scrollLines * m_dtree.Font.Height, + MidpointRounding.AwayFromZero); + } + + Assert.That(DataTree.GetWheelScrollPixels(m_dtree, delta), Is.EqualTo(expectedPixels)); + Assert.That(DataTree.GetWheelScrollPixels(m_dtree, -delta), Is.EqualTo(-expectedPixels)); + } + + [Test] + public void TryGetWheelScrollPosition_ReturnsFalse_WhenAlreadyAtTop() + { + m_dtree.Bounds = new Rectangle(0, 0, 200, 100); + m_dtree.AutoScrollMinSize = new Size(200, 1000); + m_dtree.AutoScrollPosition = new Point(0, 0); + + int newY; + bool handled = DataTree.TryGetWheelScrollPosition(m_dtree, + SystemInformation.MouseWheelScrollDelta, out newY); + + Assert.That(handled, Is.False); + Assert.That(newY, Is.EqualTo(0)); + } + + [Test] + public void CanRedirectWheelMessage_ReturnsFalse_WhenDataTreeHidden() + { + m_parent.Show(); + m_dtree.Show(); + Assert.That(m_dtree.IsHandleCreated, Is.True); + + m_dtree.Hide(); + + Assert.That(DataTree.CanRedirectWheelMessage(m_dtree), Is.False); + } + + [Test] + public void TryHandleWheelScroll_UpdatesScrollPosition_WhenScrollingIsPossible() + { + using (var parent = new Form()) + { + var dataTree = CreateScrollableDataTree(parent); + dataTree.AutoScrollPosition = new Point(0, 0); + + bool handled = DataTree.TryHandleWheelScroll(dataTree, -SystemInformation.MouseWheelScrollDelta); + + Assert.That(handled, Is.True); + Assert.That(-dataTree.AutoScrollPosition.Y, Is.GreaterThan(0)); + } + } + + [Test] + public void TryScrollOwningDataTree_ScrollsContainingDataTree_FromDateSliceHostedControl() + { + using (var parent = new Form()) + { + var dataTree = CreateScrollableDataTree(parent); + dataTree.AutoScrollPosition = new Point(0, 0); + + using (var host = new Panel()) + using (var control = new RichTextBox()) + { + host.Bounds = new Rectangle(0, 0, 150, 20); + control.Dock = DockStyle.Fill; + host.Controls.Add(control); + dataTree.Controls.Add(host); + host.Show(); + control.Show(); + Application.DoEvents(); + + bool handled = DateSlice.TryScrollOwningDataTree(control, + -SystemInformation.MouseWheelScrollDelta); + + Assert.That(handled, Is.True); + Assert.That(-dataTree.AutoScrollPosition.Y, Is.GreaterThan(0)); + } + } + } + /// [Test] public void OwnedObjects()