using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Drawing;
using System.Windows.Forms;
#nullable enable
namespace LibationWinForms.ProcessQueue
{
internal partial class VirtualFlowControl : UserControl
{
///
/// Triggered when one of the 's buttons has been clicked
///
public event EventHandler? ButtonClicked;
public IList? Items { get; private set; }
private object? m_OldContext;
protected override void OnDataContextChanged(EventArgs e)
{
if (m_OldContext is INotifyCollectionChanged oldNotify)
oldNotify.CollectionChanged -= Items_CollectionChanged;
if (DataContext is INotifyCollectionChanged newNotify)
{
m_OldContext = newNotify;
newNotify.CollectionChanged += Items_CollectionChanged;
}
Items = DataContext as IList;
base.OnDataContextChanged(e);
}
private void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshDisplay();
}
public void RefreshDisplay()
{
AdjustScrollBar();
DoVirtualScroll();
}
#region Dynamic Properties
///
/// The number of virtual s in the
///
public int VirtualControlCount => Items?.Count ?? 0;
int ScrollValue => Math.Max(vScrollBar1.Value, 0);
///
/// Amount the control moves with a small scroll change
///
private int SmallScrollChange => VirtualControlHeight * SMALL_SCROLL_CHANGE_MULTIPLE;
///
/// Amount the control moves with a large scroll change. Equal to the number of whole s in the panel, less 1.
///
private int LargeScrollChange => Math.Max(DisplayHeight / VirtualControlHeight - 1, SMALL_SCROLL_CHANGE_MULTIPLE) * VirtualControlHeight;
///
/// Virtual height of all virtual controls within this
///
private int VirtualHeight => (VirtualControlCount + NUM_BLANK_SPACES_AT_BOTTOM) * VirtualControlHeight - DisplayHeight + 2 * TopMargin;
///
/// Index of the first virtual
///
private int FirstVisibleVirtualIndex => ScrollValue / VirtualControlHeight;
///
/// The display height of this
///
private int DisplayHeight => DisplayRectangle.Height;
#endregion
#region Instance variables
///
/// The total height, including margins, of the repeated
///
private readonly int VirtualControlHeight;
///
/// Margin between the top and the top of the Panel, and the bottom and the bottom of the panel
///
private readonly int TopMargin;
private readonly List BookControls = new();
#endregion
#region Global behavior settings
///
/// Total number of actual controls added to the panel. 23 is sufficient up to a 4k monitor height.
///
private const int NUM_ACTUAL_CONTROLS = 23;
///
/// Multiple of that is moved for each small scroll change
///
private const int SMALL_SCROLL_CHANGE_MULTIPLE = 1;
///
/// Amount of space at the bottom of the , in multiples of
///
private const int NUM_BLANK_SPACES_AT_BOTTOM = 2;
#endregion
public VirtualFlowControl()
{
InitializeComponent();
panel1.Resize += (_, _) => RefreshDisplay();
var control = InitControl(0);
VirtualControlHeight = this.DpiUnscale(control.Height + control.Margin.Top + control.Margin.Bottom);
TopMargin = control.Margin.Top;
BookControls.Add(control);
panel1.Controls.Add(control);
for (int i = 1; i < NUM_ACTUAL_CONTROLS; i++)
{
control = InitControl(VirtualControlHeight * i);
BookControls.Add(control);
panel1.Controls.Add(control);
}
vScrollBar1.Scroll += (_, s) => SetScrollPosition(s.NewValue);
vScrollBar1.SmallChange = SmallScrollChange;
panel1.Height += this.DpiScale(NUM_BLANK_SPACES_AT_BOTTOM * VirtualControlHeight);
}
private ProcessBookControl InitControl(int locationY)
{
var control = new ProcessBookControl();
control.Location = new Point(control.Margin.Left, locationY + control.Margin.Top);
control.Width = panel1.ClientRectangle.Width - control.Margin.Left - control.Margin.Right;
control.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top;
control.cancelBtn.Click += ControlButton_Click;
control.moveFirstBtn.Click += ControlButton_Click;
control.moveUpBtn.Click += ControlButton_Click;
control.moveDownBtn.Click += ControlButton_Click;
control.moveLastBtn.Click += ControlButton_Click;
return control;
}
///
/// Handles all button clicks from all , detects which one sent the click, and fires to notify the model of the click
///
private void ControlButton_Click(object? sender, EventArgs e)
{
Control? button = sender as Control;
Control? form = button?.Parent;
while (form is not null and not ProcessBookControl)
form = form?.Parent;
if (form is not null && button?.Name is string buttonText)
ButtonClicked?.Invoke(form, buttonText);
}
///
/// Adjusts the max width and enabled status based on the and the
///
private void AdjustScrollBar()
{
int maxFullVisible = DisplayHeight / VirtualControlHeight;
if (VirtualControlCount <= maxFullVisible)
{
vScrollBar1.Enabled = false;
vScrollBar1.Value = 0;
for (int i = VirtualControlCount; i < NUM_ACTUAL_CONTROLS; i++)
BookControls[i].Visible = false;
}
else
{
vScrollBar1.Enabled = true;
//https://stackoverflow.com/a/2882878/3335599
int newMaximum = VirtualHeight + LargeScrollChange - 1;
if (newMaximum < vScrollBar1.Maximum)
vScrollBar1.Value = Math.Max(vScrollBar1.Value - (vScrollBar1.Maximum - newMaximum), 0);
vScrollBar1.Maximum = newMaximum;
vScrollBar1.LargeChange = LargeScrollChange;
}
}
///
/// Calculated the virtual controls that are in view at the current scroll position and windows size,
/// positions to simulate scroll activity, then fires updates the controls with
/// the context corresponding to the virtual scroll position
///
private void DoVirtualScroll()
{
int firstVisible = FirstVisibleVirtualIndex;
int position = ScrollValue % VirtualControlHeight;
panel1.Location = new Point(0, -position);
int numVisible = DisplayHeight / VirtualControlHeight;
if (DisplayHeight % VirtualControlHeight != 0)
numVisible++;
numVisible = Math.Min(numVisible, VirtualControlCount);
numVisible = Math.Min(numVisible, VirtualControlCount - firstVisible);
if (Items is IList items)
{
for (int i = 0; i < numVisible; i++)
BookControls[i].DataContext = items[firstVisible + i];
}
for (int i = 0; i < BookControls.Count; i++)
{
BookControls[i].Visible = i < numVisible;
}
}
///
/// Set scroll value to an integral multiple of
///
private void SetScrollPosition(int value)
{
if (!vScrollBar1.Enabled) return;
int newPos = (int)Math.Round((double)value / SmallScrollChange) * SmallScrollChange;
if (vScrollBar1.Value != newPos)
{
//https://stackoverflow.com/a/2882878/3335599
vScrollBar1.Value = Math.Min(newPos, vScrollBar1.Maximum - vScrollBar1.LargeChange + 1);
DoVirtualScroll();
}
}
private const int WM_MOUSEWHEEL = 522;
private const int WHEEL_DELTA = 120;
protected override void WndProc(ref Message m)
{
//Capture mouse wheel movement and interpret it as a scroll event
if (m.Msg == WM_MOUSEWHEEL)
{
//https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mousewheel
int wheelDelta = -(short)(((ulong)m.WParam) >> 16 & ushort.MaxValue);
int numSmallPositionMoves = Math.Abs(wheelDelta) / WHEEL_DELTA;
int scrollDelta = Math.Sign(wheelDelta) * numSmallPositionMoves * SmallScrollChange;
int newScrollPosition;
if (scrollDelta > 0)
newScrollPosition = Math.Min(vScrollBar1.Value + scrollDelta, vScrollBar1.Maximum);
else
newScrollPosition = Math.Max(vScrollBar1.Value + scrollDelta, vScrollBar1.Minimum);
SetScrollPosition(newScrollPosition);
}
base.WndProc(ref m);
}
}
}