475 lines
16 KiB
C#
475 lines
16 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Diagnostics;
|
|
using System.Reflection;
|
|
using System.Windows.Media.Animation;
|
|
|
|
namespace WPFluent.Controls.Primitives;
|
|
|
|
/// <summary>
|
|
/// Manages visual states and their transitions on a control.
|
|
/// </summary>
|
|
public class SimpleVisualStateManager : VisualStateManager
|
|
{
|
|
public static bool IsAnimationsEnabled => SystemParameters.ClientAreaAnimation &&
|
|
RenderCapability.Tier > 0;
|
|
|
|
/// <summary>
|
|
/// Allows subclasses to override the GoToState logic.
|
|
/// </summary>
|
|
protected override bool GoToStateCore(FrameworkElement control, FrameworkElement stateGroupsRoot, string stateName, VisualStateGroup group, VisualState state, bool useTransitions)
|
|
{
|
|
if (state != null)
|
|
{
|
|
useTransitions &= IsAnimationsEnabled;
|
|
|
|
if (group.Transitions.Count > 0 && VisualStateGroupHelper.IsSupported)
|
|
{
|
|
return GoToStateInternal(control, stateGroupsRoot, group, state, useTransitions);
|
|
}
|
|
else
|
|
{
|
|
return base.GoToStateCore(control, stateGroupsRoot, stateName, group, state, useTransitions);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#region VisualStateGroups
|
|
|
|
internal static Collection<VisualStateGroup> GetVisualStateGroupsInternal(FrameworkElement obj)
|
|
{
|
|
if (obj == null)
|
|
{
|
|
throw new ArgumentNullException("obj");
|
|
}
|
|
|
|
// We don't want to get the default value because it will create/return an empty collection.
|
|
var source = DependencyPropertyHelper.GetValueSource(obj, VisualStateGroupsProperty).BaseValueSource;
|
|
if (source != BaseValueSource.Default)
|
|
{
|
|
return (obj.GetValue(VisualStateGroupsProperty) as Collection<VisualStateGroup>)!;
|
|
}
|
|
|
|
return null!;
|
|
}
|
|
|
|
#endregion VisualStateGroups
|
|
|
|
#region State Change
|
|
|
|
internal static bool TryGetState(IList<VisualStateGroup> groups, string stateName, out VisualStateGroup group, out VisualState state)
|
|
{
|
|
for (var groupIndex = 0; groupIndex < groups.Count; ++groupIndex)
|
|
{
|
|
var g = groups[groupIndex];
|
|
var s = g.GetState(stateName);
|
|
if (s != null)
|
|
{
|
|
group = g;
|
|
state = s;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
group = null!;
|
|
state = null!;
|
|
return false;
|
|
}
|
|
|
|
private bool GoToStateInternal(FrameworkElement control, FrameworkElement stateGroupsRoot, VisualStateGroup group, VisualState state, bool useTransitions)
|
|
{
|
|
if (stateGroupsRoot == null)
|
|
{
|
|
throw new ArgumentNullException("stateGroupsRoot");
|
|
}
|
|
|
|
if (state == null)
|
|
{
|
|
throw new ArgumentNullException("state");
|
|
}
|
|
|
|
if (group == null)
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
var lastState = group.CurrentState;
|
|
if (lastState == state)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Get the transition Storyboard. Even if there are no transitions specified, there might
|
|
// be properties that we're rolling back to their default values.
|
|
var transition = useTransitions ? GetTransition(stateGroupsRoot, group, lastState, state) : null!;
|
|
|
|
// If the transition is null, then we want to instantly snap. The dynamicTransition will
|
|
// consist of everything that is being moved back to the default state.
|
|
// If the transition.Duration and explicit storyboard duration is zero, then we want both the dynamic
|
|
// and state Storyboards to happen in the same tick, so we start them at the same time.
|
|
if (transition == null || transition.GeneratedDuration == DurationZero &&
|
|
(transition.Storyboard == null || transition.Storyboard.Duration == DurationZero))
|
|
{
|
|
// Start new state Storyboard and stop any previously running Storyboards
|
|
if (transition != null && transition.Storyboard != null)
|
|
{
|
|
group.StartNewThenStopOld(stateGroupsRoot, transition.Storyboard, state.Storyboard);
|
|
}
|
|
else
|
|
{
|
|
group.StartNewThenStopOld(stateGroupsRoot, state.Storyboard);
|
|
}
|
|
|
|
// Fire both CurrentStateChanging and CurrentStateChanged events
|
|
RaiseCurrentStateChanging(group, lastState, state, control, stateGroupsRoot);
|
|
RaiseCurrentStateChanged(group, lastState, state, control, stateGroupsRoot);
|
|
}
|
|
else
|
|
{
|
|
if (transition.Storyboard != null/* && transition.ExplicitStoryboardCompleted == true*/)
|
|
{
|
|
EventHandler transitionCompleted = null!;
|
|
transitionCompleted = new EventHandler((sender, e) =>
|
|
{
|
|
if (ShouldRunStateStoryboard(control, stateGroupsRoot, state, group))
|
|
{
|
|
group.StartNewThenStopOld(stateGroupsRoot, state.Storyboard);
|
|
}
|
|
|
|
RaiseCurrentStateChanged(group, lastState, state, control, stateGroupsRoot);
|
|
|
|
transition.Storyboard.Completed -= transitionCompleted;
|
|
//transition.ExplicitStoryboardCompleted = true;
|
|
});
|
|
|
|
// hook up explicit storyboard's Completed event handler
|
|
//transition.ExplicitStoryboardCompleted = false;
|
|
transition.Storyboard.Completed += transitionCompleted;
|
|
}
|
|
|
|
// Start transition and dynamicTransition Storyboards
|
|
// Stop any previously running Storyboards
|
|
group.StartNewThenStopOld(stateGroupsRoot, transition.Storyboard!);
|
|
|
|
RaiseCurrentStateChanging(group, lastState, state, control, stateGroupsRoot);
|
|
}
|
|
|
|
group.SetCurrentState(state);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the stateGroupsRoot or control is removed from the tree, then the new
|
|
/// storyboards will not be able to resolve target names. Thus,
|
|
/// if the stateGroupsRoot or control is not in the tree, don't start the new
|
|
/// storyboards. Also if the group has already changed state, then
|
|
/// don't start the new storyboards.
|
|
/// </summary>
|
|
private static bool ShouldRunStateStoryboard(FrameworkElement control, FrameworkElement stateGroupsRoot, VisualState state, VisualStateGroup group)
|
|
{
|
|
var controlInTree = true;
|
|
var stateGroupsRootInTree = true;
|
|
|
|
// We cannot simply check control.IsLoaded because the control may not be in the visual tree
|
|
// even though IsLoaded is true. Instead we will check that it can find a PresentationSource
|
|
// which would tell us it's in the visual tree.
|
|
if (control != null)
|
|
{
|
|
// If it's visible then it's in the visual tree, so we don't even have to look for a
|
|
// PresentationSource
|
|
if (!control.IsVisible)
|
|
{
|
|
controlInTree = PresentationSource.FromVisual(control) != null;
|
|
}
|
|
}
|
|
|
|
if (stateGroupsRoot != null)
|
|
{
|
|
if (!stateGroupsRoot.IsVisible)
|
|
{
|
|
stateGroupsRootInTree = PresentationSource.FromVisual(stateGroupsRoot) != null;
|
|
}
|
|
}
|
|
|
|
return controlInTree && stateGroupsRootInTree && state == group.CurrentState;
|
|
}
|
|
|
|
#endregion State Change
|
|
|
|
#region Transitions
|
|
|
|
/// <summary>
|
|
/// Get the most appropriate transition between two states.
|
|
/// </summary>
|
|
/// <param name="element">Element being transitioned.</param>
|
|
/// <param name="group">Group being transitioned.</param>
|
|
/// <param name="from">VisualState being transitioned from.</param>
|
|
/// <param name="to">VisualState being transitioned to.</param>
|
|
/// <returns>
|
|
/// The most appropriate transition between the desired states.
|
|
/// </returns>
|
|
internal static VisualTransition GetTransition(FrameworkElement element, VisualStateGroup group, VisualState from, VisualState to)
|
|
{
|
|
if (element == null)
|
|
{
|
|
throw new ArgumentNullException("element");
|
|
}
|
|
|
|
if (group == null)
|
|
{
|
|
throw new ArgumentNullException("group");
|
|
}
|
|
|
|
if (to == null)
|
|
{
|
|
throw new ArgumentNullException("to");
|
|
}
|
|
|
|
VisualTransition best = null!;
|
|
VisualTransition defaultTransition = null!;
|
|
var bestScore = -1;
|
|
|
|
var transitions = (IList<VisualTransition>)group.Transitions;
|
|
if (transitions != null)
|
|
{
|
|
foreach (var transition in transitions)
|
|
{
|
|
if (defaultTransition == null && IsDefault(transition))
|
|
{
|
|
defaultTransition = transition;
|
|
continue;
|
|
}
|
|
|
|
var score = -1;
|
|
|
|
var transitionFromState = group.GetState(transition.From);
|
|
var transitionToState = group.GetState(transition.To);
|
|
|
|
if (from == transitionFromState)
|
|
{
|
|
score += 1;
|
|
}
|
|
else if (transitionFromState != null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (to == transitionToState)
|
|
{
|
|
score += 2;
|
|
}
|
|
else if (transitionToState != null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (score > bestScore)
|
|
{
|
|
bestScore = score;
|
|
best = transition;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (best ?? defaultTransition)!;
|
|
}
|
|
|
|
internal static bool IsDefault(VisualTransition transition)
|
|
{
|
|
return transition.From == null && transition.To == null;
|
|
}
|
|
|
|
#endregion Transitions
|
|
|
|
#region Data
|
|
|
|
private static readonly Duration DurationZero = new Duration(TimeSpan.Zero);
|
|
|
|
#endregion Data
|
|
}
|
|
|
|
internal static class VisualStateGroupHelper
|
|
{
|
|
internal static bool IsSupported => _setCurrentState.Value != null;
|
|
|
|
internal static void SetCurrentState(this VisualStateGroup group, VisualState value)
|
|
{
|
|
if (!IsSupported)
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
_setCurrentState.Value(group, value);
|
|
Debug.Assert(group.CurrentState == value);
|
|
}
|
|
|
|
internal static VisualState GetState(this VisualStateGroup group, string stateName)
|
|
{
|
|
for (var stateIndex = 0; stateIndex < group.States.Count; ++stateIndex)
|
|
{
|
|
var state = (VisualState)group.States[stateIndex]!;
|
|
if (state.Name == stateName)
|
|
{
|
|
return state;
|
|
}
|
|
}
|
|
|
|
return null!;
|
|
}
|
|
|
|
#region CurrentStoryboards
|
|
|
|
private static readonly DependencyProperty CurrentStoryboardsProperty =
|
|
DependencyProperty.RegisterAttached(
|
|
"CurrentStoryboards",
|
|
typeof(Collection<Storyboard>),
|
|
typeof(VisualStateGroupHelper));
|
|
|
|
internal static Collection<Storyboard> GetCurrentStoryboards(VisualStateGroup group)
|
|
{
|
|
var currentStoryboards = (Collection<Storyboard>)group.GetValue(CurrentStoryboardsProperty);
|
|
if (currentStoryboards == null)
|
|
{
|
|
currentStoryboards = new Collection<Storyboard>();
|
|
group.SetValue(CurrentStoryboardsProperty, currentStoryboards);
|
|
}
|
|
return currentStoryboards;
|
|
}
|
|
|
|
#endregion CurrentStoryboards
|
|
|
|
internal static void StartNewThenStopOld(this VisualStateGroup group, FrameworkElement element, params Storyboard[] newStoryboards)
|
|
{
|
|
var currentStoryboards = GetCurrentStoryboards(group);
|
|
|
|
// Remove the old Storyboards. Remove is delayed until the next TimeManager tick, so the
|
|
// handoff to the new storyboard is unaffected.
|
|
for (var index = 0; index < currentStoryboards.Count; ++index)
|
|
{
|
|
if (currentStoryboards[index] == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
currentStoryboards[index].Remove(element);
|
|
}
|
|
currentStoryboards.Clear();
|
|
|
|
// Start the new Storyboards
|
|
for (var index = 0; index < newStoryboards.Length; ++index)
|
|
{
|
|
if (newStoryboards[index] == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
newStoryboards[index].Begin(element, HandoffBehavior.SnapshotAndReplace, true);
|
|
|
|
// Hold on to the running Storyboards
|
|
currentStoryboards.Add(newStoryboards[index]);
|
|
|
|
// Silverlight had an issue where initially, a checked CheckBox would not show the check mark
|
|
// until the second frame. They chose to do a Seek(0) at this point, which this line
|
|
// is supposed to mimic. It does not seem to be equivalent, though, and WPF ends up
|
|
// with some odd animation behavior. I haven't seen the CheckBox issue on WPF, so
|
|
// commenting this out for now.
|
|
// newStoryboards[index].SeekAlignedToLastTick(element, TimeSpan.Zero, TimeSeekOrigin.BeginTime);
|
|
}
|
|
}
|
|
|
|
private static Action<VisualStateGroup, VisualState> CreateSetCurrentStateDelegate()
|
|
{
|
|
try
|
|
{
|
|
return DelegateHelper.CreatePropertySetter<VisualStateGroup, VisualState>(
|
|
nameof(VisualStateGroup.CurrentState),
|
|
nonPublic: true);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null!;
|
|
}
|
|
}
|
|
|
|
private static readonly Lazy<Action<VisualStateGroup, VisualState>> _setCurrentState = new(CreateSetCurrentStateDelegate);
|
|
}
|
|
|
|
public static class DelegateHelper
|
|
{
|
|
private const BindingFlags DefaultLookup = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public;
|
|
|
|
public static T CreateDelegate<T>(MethodInfo method) where T : Delegate
|
|
{
|
|
return (T)Delegate.CreateDelegate(typeof(T), method);
|
|
}
|
|
|
|
public static T CreateDelegate<T>(object firstArgument, MethodInfo method) where T : Delegate
|
|
{
|
|
return (T)Delegate.CreateDelegate(typeof(T), firstArgument, method);
|
|
}
|
|
|
|
public static T CreateDelegate<T>(Type target, string method, BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public) where T : Delegate
|
|
{
|
|
if (bindingAttr != (BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public))
|
|
{
|
|
var method2 = target.GetMethod(method, bindingAttr);
|
|
if (method2 != null)
|
|
{
|
|
return CreateDelegate<T>(method2);
|
|
}
|
|
|
|
return null!;
|
|
}
|
|
|
|
return (T)Delegate.CreateDelegate(typeof(T), target, method);
|
|
}
|
|
|
|
public static T CreateDelegate<T>(object target, string method, BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public) where T : Delegate
|
|
{
|
|
if (bindingAttr != (BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public))
|
|
{
|
|
var method2 = target.GetType().GetMethod(method, bindingAttr);
|
|
if (method2 != null)
|
|
{
|
|
return CreateDelegate<T>(target, method2);
|
|
}
|
|
|
|
return null!;
|
|
}
|
|
|
|
return (T)Delegate.CreateDelegate(typeof(T), target, method);
|
|
}
|
|
|
|
public static Func<TType, TProperty> CreatePropertyGetter<TType, TProperty>(string name, BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, bool nonPublic = false)
|
|
{
|
|
var property = typeof(TType).GetProperty(name, bindingAttr);
|
|
if (property != null)
|
|
{
|
|
var getMethod = property.GetGetMethod(nonPublic);
|
|
if (getMethod != null)
|
|
{
|
|
return CreateDelegate<Func<TType, TProperty>>(getMethod);
|
|
}
|
|
}
|
|
|
|
return null!;
|
|
}
|
|
|
|
public static Action<TType, TProperty> CreatePropertySetter<TType, TProperty>(string name, BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, bool nonPublic = false)
|
|
{
|
|
var property = typeof(TType).GetProperty(name, bindingAttr);
|
|
if (property != null)
|
|
{
|
|
var setMethod = property.GetSetMethod(nonPublic);
|
|
if (setMethod != null)
|
|
{
|
|
return CreateDelegate<Action<TType, TProperty>>(setMethod);
|
|
}
|
|
}
|
|
|
|
return null!;
|
|
}
|
|
}
|