Initial Commit
This commit is contained in:
611
Viewport2D.cs
Normal file
611
Viewport2D.cs
Normal file
@@ -0,0 +1,611 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary;
|
||||
using Microsoft.Research.DynamicDataDisplay.ViewportRestrictions;
|
||||
using Microsoft.Research.DynamicDataDisplay.Common;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Viewport2D provides virtual coordinates.
|
||||
/// </summary>
|
||||
public partial class Viewport2D : DependencyObject
|
||||
{
|
||||
private readonly Plotter2D plotter;
|
||||
internal Plotter2D Plotter2D
|
||||
{
|
||||
get { return plotter; }
|
||||
}
|
||||
|
||||
private readonly FrameworkElement hostElement;
|
||||
internal FrameworkElement HostElement
|
||||
{
|
||||
get { return hostElement; }
|
||||
}
|
||||
|
||||
protected internal Viewport2D(FrameworkElement host, Plotter2D plotter)
|
||||
{
|
||||
hostElement = host;
|
||||
host.ClipToBounds = true;
|
||||
host.SizeChanged += OnHostElementSizeChanged;
|
||||
|
||||
this.plotter = plotter;
|
||||
plotter.Children.CollectionChanged += OnPlotterChildrenChanged;
|
||||
|
||||
restrictions = new RestrictionCollection(this);
|
||||
restrictions.Add(new MinimalSizeRestriction());
|
||||
restrictions.CollectionChanged += restrictions_CollectionChanged;
|
||||
|
||||
fitToViewRestrictions = new RestrictionCollection(this);
|
||||
fitToViewRestrictions.CollectionChanged += fitToViewRestrictions_CollectionChanged;
|
||||
|
||||
readonlyContentBoundsHosts = new ReadOnlyObservableCollection<DependencyObject>(contentBoundsHosts);
|
||||
|
||||
UpdateVisible();
|
||||
UpdateTransform();
|
||||
}
|
||||
|
||||
private void OnHostElementSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
SetValue(OutputPropertyKey, new Rect(e.NewSize));
|
||||
CoerceValue(VisibleProperty);
|
||||
}
|
||||
|
||||
private void fitToViewRestrictions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (IsFittedToView)
|
||||
{
|
||||
CoerceValue(VisibleProperty);
|
||||
}
|
||||
}
|
||||
|
||||
private void restrictions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
CoerceValue(VisibleProperty);
|
||||
}
|
||||
|
||||
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
Viewport2D viewport = (Viewport2D)d;
|
||||
viewport.UpdateTransform();
|
||||
viewport.RaisePropertyChangedEvent(e);
|
||||
}
|
||||
|
||||
public BindingExpressionBase SetBinding(DependencyProperty property, BindingBase binding)
|
||||
{
|
||||
return BindingOperations.SetBinding(this, property, binding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces viewport to go to fit to view mode - clears locally set value of <see cref="Visible"/> property
|
||||
/// and sets it during the coercion process to a value of united content bounds of all charts inside of <see cref="Plotter"/>.
|
||||
/// </summary>
|
||||
public void FitToView()
|
||||
{
|
||||
if (!IsFittedToView)
|
||||
{
|
||||
ClearValue(VisibleProperty);
|
||||
CoerceValue(VisibleProperty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Viewport is fitted to view.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if Viewport is fitted to view; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
public bool IsFittedToView
|
||||
{
|
||||
get { return ReadLocalValue(VisibleProperty) == DependencyProperty.UnsetValue; }
|
||||
}
|
||||
|
||||
// DispatcherOperation updateVisibleOperation;
|
||||
internal void UpdateVisible()
|
||||
{
|
||||
//if (updateVisibleOperation == null)
|
||||
//{
|
||||
// updateVisibleOperation = Dispatcher.BeginInvoke(() => UpdateVisible(), DispatcherPriority.Normal);
|
||||
// return;
|
||||
//}
|
||||
|
||||
//updateVisibleOperation = Dispatcher.BeginInvoke(() =>
|
||||
//{
|
||||
// updateVisibleOperation = null;
|
||||
|
||||
if (IsFittedToView)
|
||||
{
|
||||
CoerceValue(VisibleProperty);
|
||||
}
|
||||
//}, DispatcherPriority.Normal);
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Plotter2D Plotter
|
||||
{
|
||||
get { return plotter; }
|
||||
}
|
||||
|
||||
private readonly RestrictionCollection restrictions;
|
||||
/// <summary>
|
||||
/// Gets the collection of <see cref="ViewportRestriction"/>s that are applied each time <see cref="Visible"/> is updated.
|
||||
/// </summary>
|
||||
/// <value>The restrictions.</value>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
|
||||
public RestrictionCollection Restrictions
|
||||
{
|
||||
get { return restrictions; }
|
||||
}
|
||||
|
||||
|
||||
private readonly RestrictionCollection fitToViewRestrictions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of <see cref="ViewportRestriction"/>s that are applied only when Viewport is fitted to view.
|
||||
/// </summary>
|
||||
/// <value>The fit to view restrictions.</value>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
|
||||
public RestrictionCollection FitToViewRestrictions
|
||||
{
|
||||
get { return fitToViewRestrictions; }
|
||||
}
|
||||
|
||||
#region Output property
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rectangle in screen coordinates that is output.
|
||||
/// </summary>
|
||||
/// <value>The output.</value>
|
||||
public Rect Output
|
||||
{
|
||||
get { return (Rect)GetValue(OutputProperty); }
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")]
|
||||
private static readonly DependencyPropertyKey OutputPropertyKey = DependencyProperty.RegisterReadOnly(
|
||||
"Output",
|
||||
typeof(Rect),
|
||||
typeof(Viewport2D),
|
||||
new FrameworkPropertyMetadata(new Rect(0, 0, 1, 1), OnPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="Output"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty OutputProperty = OutputPropertyKey.DependencyProperty;
|
||||
|
||||
#endregion
|
||||
|
||||
#region UnitedContentBounds property
|
||||
|
||||
/// <summary>
|
||||
/// Gets the united content bounds of all the charts.
|
||||
/// </summary>
|
||||
/// <value>The content bounds.</value>
|
||||
public DataRect UnitedContentBounds
|
||||
{
|
||||
get { return (DataRect)GetValue(UnitedContentBoundsProperty); }
|
||||
internal set { SetValue(UnitedContentBoundsProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="UnitedContentBounds"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty UnitedContentBoundsProperty = DependencyProperty.Register(
|
||||
"UnitedContentBounds",
|
||||
typeof(DataRect),
|
||||
typeof(Viewport2D),
|
||||
new FrameworkPropertyMetadata(DataRect.Empty, OnUnitedContentBoundsChanged));
|
||||
|
||||
private static void OnUnitedContentBoundsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
Viewport2D owner = (Viewport2D)d;
|
||||
owner.ContentBoundsChanged.Raise(owner);
|
||||
}
|
||||
|
||||
public event EventHandler ContentBoundsChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Visible property
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the visible rectangle.
|
||||
/// </summary>
|
||||
/// <value>The visible.</value>
|
||||
public DataRect Visible
|
||||
{
|
||||
get { return (DataRect)GetValue(VisibleProperty); }
|
||||
set { SetValue(VisibleProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="Visible"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty VisibleProperty =
|
||||
DependencyProperty.Register("Visible", typeof(DataRect), typeof(Viewport2D),
|
||||
new FrameworkPropertyMetadata(
|
||||
new DataRect(0, 0, 1, 1),
|
||||
OnPropertyChanged,
|
||||
OnCoerceVisible),
|
||||
ValidateVisibleCallback);
|
||||
|
||||
private static bool ValidateVisibleCallback(object value)
|
||||
{
|
||||
DataRect rect = (DataRect)value;
|
||||
|
||||
return !rect.IsNaN();
|
||||
}
|
||||
|
||||
private void UpdateContentBoundsHosts()
|
||||
{
|
||||
contentBoundsHosts.Clear();
|
||||
foreach (var item in plotter.Children)
|
||||
{
|
||||
DependencyObject dependencyObject = item as DependencyObject;
|
||||
if (dependencyObject != null)
|
||||
{
|
||||
bool hasNonEmptyBounds = !Viewport2D.GetContentBounds(dependencyObject).IsEmpty;
|
||||
if (hasNonEmptyBounds && Viewport2D.GetIsContentBoundsHost(dependencyObject))
|
||||
{
|
||||
contentBoundsHosts.Add(dependencyObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UpdateVisible();
|
||||
}
|
||||
|
||||
private readonly ObservableCollection<DependencyObject> contentBoundsHosts = new ObservableCollection<DependencyObject>();
|
||||
private readonly ReadOnlyObservableCollection<DependencyObject> readonlyContentBoundsHosts;
|
||||
/// <summary>
|
||||
/// Gets the collection of all charts that can has its own content bounds.
|
||||
/// </summary>
|
||||
/// <value>The content bounds hosts.</value>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
public ReadOnlyObservableCollection<DependencyObject> ContentBoundsHosts
|
||||
{
|
||||
get { return readonlyContentBoundsHosts; }
|
||||
}
|
||||
|
||||
private bool useApproximateContentBoundsComparison = true;
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use approximate content bounds comparison.
|
||||
/// This this property to true can increase performance, as Visible will change less often.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if approximate content bounds comparison is used; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool UseApproximateContentBoundsComparison
|
||||
{
|
||||
get { return useApproximateContentBoundsComparison; }
|
||||
set { useApproximateContentBoundsComparison = value; }
|
||||
}
|
||||
|
||||
private double maxContentBoundsComparisonMistake = 0.02;
|
||||
public double MaxContentBoundsComparisonMistake
|
||||
{
|
||||
get { return maxContentBoundsComparisonMistake; }
|
||||
set { maxContentBoundsComparisonMistake = value; }
|
||||
}
|
||||
|
||||
private DataRect prevContentBounds = DataRect.Empty;
|
||||
protected virtual DataRect CoerceVisible(DataRect newVisible)
|
||||
{
|
||||
if (Plotter == null)
|
||||
{
|
||||
return newVisible;
|
||||
}
|
||||
|
||||
bool isDefaultValue = newVisible == (DataRect)VisibleProperty.DefaultMetadata.DefaultValue;
|
||||
if (isDefaultValue)
|
||||
{
|
||||
newVisible = DataRect.Empty;
|
||||
}
|
||||
|
||||
if (isDefaultValue && IsFittedToView)
|
||||
{
|
||||
// determining content bounds
|
||||
DataRect bounds = DataRect.Empty;
|
||||
|
||||
foreach (var item in contentBoundsHosts)
|
||||
{
|
||||
IPlotterElement plotterElement = item as IPlotterElement;
|
||||
if (plotterElement == null)
|
||||
continue;
|
||||
if (plotterElement.Plotter == null)
|
||||
continue;
|
||||
|
||||
var plotter = (Plotter2D)plotterElement.Plotter;
|
||||
var visual = plotter.VisualBindings[plotterElement];
|
||||
if (visual.Visibility == Visibility.Visible)
|
||||
{
|
||||
DataRect contentBounds = Viewport2D.GetContentBounds(item);
|
||||
if (contentBounds.Width.IsNaN() || contentBounds.Height.IsNaN())
|
||||
continue;
|
||||
|
||||
bounds.UnionFinite(contentBounds);
|
||||
}
|
||||
}
|
||||
|
||||
if (useApproximateContentBoundsComparison)
|
||||
{
|
||||
var intersection = prevContentBounds;
|
||||
intersection.Intersect(bounds);
|
||||
|
||||
double currSquare = bounds.GetSquare();
|
||||
double prevSquare = prevContentBounds.GetSquare();
|
||||
double intersectionSquare = intersection.GetSquare();
|
||||
|
||||
double squareTopLimit = 1 + maxContentBoundsComparisonMistake;
|
||||
double squareBottomLimit = 1 - maxContentBoundsComparisonMistake;
|
||||
|
||||
if (intersectionSquare != 0)
|
||||
{
|
||||
double currRatio = currSquare / intersectionSquare;
|
||||
double prevRatio = prevSquare / intersectionSquare;
|
||||
|
||||
if (squareBottomLimit < currRatio &&
|
||||
currRatio < squareTopLimit &&
|
||||
squareBottomLimit < prevRatio &&
|
||||
prevRatio < squareTopLimit)
|
||||
{
|
||||
bounds = prevContentBounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevContentBounds = bounds;
|
||||
UnitedContentBounds = bounds;
|
||||
|
||||
// applying fit-to-view restrictions
|
||||
bounds = fitToViewRestrictions.Apply(Visible, bounds, this);
|
||||
|
||||
// enlarging
|
||||
if (!bounds.IsEmpty)
|
||||
{
|
||||
bounds = CoordinateUtilities.RectZoom(bounds, bounds.GetCenter(), clipToBoundsEnlargeFactor);
|
||||
}
|
||||
else
|
||||
{
|
||||
bounds = (DataRect)VisibleProperty.DefaultMetadata.DefaultValue;
|
||||
}
|
||||
newVisible.Union(bounds);
|
||||
}
|
||||
|
||||
if (newVisible.IsEmpty)
|
||||
{
|
||||
newVisible = (DataRect)VisibleProperty.DefaultMetadata.DefaultValue;
|
||||
}
|
||||
else if (newVisible.Width == 0 || newVisible.Height == 0)
|
||||
{
|
||||
DataRect defRect = (DataRect)VisibleProperty.DefaultMetadata.DefaultValue;
|
||||
Size size = newVisible.Size;
|
||||
Point loc = newVisible.Location;
|
||||
|
||||
if (newVisible.Width == 0)
|
||||
{
|
||||
size.Width = defRect.Width;
|
||||
loc.X -= size.Width / 2;
|
||||
}
|
||||
if (newVisible.Height == 0)
|
||||
{
|
||||
size.Height = defRect.Height;
|
||||
loc.Y -= size.Height / 2;
|
||||
}
|
||||
|
||||
newVisible = new DataRect(loc, size);
|
||||
}
|
||||
|
||||
// apply domain restriction
|
||||
newVisible = domainRestriction.Apply(Visible, newVisible, this);
|
||||
|
||||
// apply other restrictions
|
||||
newVisible = restrictions.Apply(Visible, newVisible, this);
|
||||
|
||||
// applying transform's data domain restriction
|
||||
if (!transform.DataTransform.DataDomain.IsEmpty)
|
||||
{
|
||||
var newDataRect = newVisible.ViewportToData(transform);
|
||||
newDataRect = DataRect.Intersect(newDataRect, transform.DataTransform.DataDomain);
|
||||
newVisible = newDataRect.DataToViewport(transform);
|
||||
}
|
||||
|
||||
if (newVisible.IsEmpty) newVisible = new Rect(0, 0, 1, 1);
|
||||
|
||||
return newVisible;
|
||||
}
|
||||
|
||||
private static object OnCoerceVisible(DependencyObject d, object newValue)
|
||||
{
|
||||
Viewport2D viewport = (Viewport2D)d;
|
||||
|
||||
DataRect newRect = viewport.CoerceVisible((DataRect)newValue);
|
||||
|
||||
if (newRect.Width == 0 || newRect.Height == 0)
|
||||
{
|
||||
// doesn't apply rects with zero square
|
||||
return DependencyProperty.UnsetValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return newRect;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Domain
|
||||
|
||||
private readonly DomainRestriction domainRestriction = new DomainRestriction { Domain = Rect.Empty };
|
||||
/// <summary>
|
||||
/// Gets or sets the domain - rectangle in viewport coordinates that limits maximal size of <see cref="Visible"/> rectangle.
|
||||
/// </summary>
|
||||
/// <value>The domain.</value>
|
||||
public DataRect Domain
|
||||
{
|
||||
get { return domainRestriction.Domain; }
|
||||
set
|
||||
{
|
||||
if (domainRestriction.Domain != value)
|
||||
{
|
||||
domainRestriction.Domain = value;
|
||||
DomainChanged.Raise(this);
|
||||
CoerceValue(VisibleProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when <see cref="Domain"/> property changes.
|
||||
/// </summary>
|
||||
public event EventHandler DomainChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
private double clipToBoundsEnlargeFactor = 1.10;
|
||||
/// <summary>
|
||||
/// Gets or sets the viewport enlarge factor.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Default value is 1.10.
|
||||
/// </remarks>
|
||||
/// <value>The clip to bounds factor.</value>
|
||||
public double ClipToBoundsEnlargeFactor
|
||||
{
|
||||
get { return clipToBoundsEnlargeFactor; }
|
||||
set
|
||||
{
|
||||
if (clipToBoundsEnlargeFactor != value)
|
||||
{
|
||||
clipToBoundsEnlargeFactor = value;
|
||||
UpdateVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTransform()
|
||||
{
|
||||
transform = transform.WithRects(Visible, Output);
|
||||
}
|
||||
|
||||
private CoordinateTransform transform = CoordinateTransform.CreateDefault();
|
||||
/// <summary>
|
||||
/// Gets or sets the coordinate transform of Viewport.
|
||||
/// </summary>
|
||||
/// <value>The transform.</value>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[NotNull]
|
||||
public virtual CoordinateTransform Transform
|
||||
{
|
||||
get { return transform; }
|
||||
set
|
||||
{
|
||||
value.VerifyNotNull();
|
||||
|
||||
if (value != transform)
|
||||
{
|
||||
var oldTransform = transform;
|
||||
|
||||
transform = value;
|
||||
|
||||
RaisePropertyChangedEvent("Transform", oldTransform, transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when viewport property changes.
|
||||
/// </summary>
|
||||
public event EventHandler<ExtendedPropertyChangedEventArgs> PropertyChanged;
|
||||
|
||||
private void RaisePropertyChangedEvent(string propertyName, object oldValue, object newValue)
|
||||
{
|
||||
if (PropertyChanged != null)
|
||||
{
|
||||
RaisePropertyChanged(new ExtendedPropertyChangedEventArgs { PropertyName = propertyName, OldValue = oldValue, NewValue = newValue });
|
||||
}
|
||||
}
|
||||
|
||||
private void RaisePropertyChangedEvent(string propertyName)
|
||||
{
|
||||
if (PropertyChanged != null)
|
||||
{
|
||||
RaisePropertyChanged(new ExtendedPropertyChangedEventArgs { PropertyName = propertyName });
|
||||
}
|
||||
}
|
||||
|
||||
private void RaisePropertyChangedEvent(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (PropertyChanged != null)
|
||||
{
|
||||
RaisePropertyChanged(ExtendedPropertyChangedEventArgs.FromDependencyPropertyChanged(e));
|
||||
}
|
||||
}
|
||||
|
||||
//private DispatcherOperation pendingRaisePropertyChangedOperation;
|
||||
//private bool inRaisePropertyChanged = false;
|
||||
protected virtual void RaisePropertyChanged(ExtendedPropertyChangedEventArgs args)
|
||||
{
|
||||
//if (inRaisePropertyChanged)
|
||||
//{
|
||||
// if (pendingRaisePropertyChangedOperation != null)
|
||||
// pendingRaisePropertyChangedOperation.Abort();
|
||||
// pendingRaisePropertyChangedOperation = Dispatcher.BeginInvoke(() => RaisePropertyChanged(args), DispatcherPriority.Normal);
|
||||
// return;
|
||||
//}
|
||||
|
||||
//pendingRaisePropertyChangedOperation = null;
|
||||
//inRaisePropertyChanged = true;
|
||||
|
||||
PropertyChanged.Raise(this, args);
|
||||
|
||||
//inRaisePropertyChanged = false;
|
||||
}
|
||||
|
||||
private void OnPlotterChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateContentBoundsHosts();
|
||||
}
|
||||
|
||||
#region Panning state
|
||||
|
||||
private Viewport2DPanningState panningState = Viewport2DPanningState.NotPanning;
|
||||
public Viewport2DPanningState PanningState
|
||||
{
|
||||
get { return panningState; }
|
||||
set
|
||||
{
|
||||
var prevState = panningState;
|
||||
|
||||
panningState = value;
|
||||
|
||||
OnPanningStateChanged(prevState, panningState);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPanningStateChanged(Viewport2DPanningState prevState, Viewport2DPanningState currState)
|
||||
{
|
||||
PanningStateChanged.Raise(this, prevState, currState);
|
||||
if (currState == Viewport2DPanningState.Panning)
|
||||
BeginPanning.Raise(this);
|
||||
else if (currState == Viewport2DPanningState.NotPanning)
|
||||
EndPanning.Raise(this);
|
||||
}
|
||||
|
||||
internal event EventHandler<ValueChangedEventArgs<Viewport2DPanningState>> PanningStateChanged;
|
||||
|
||||
public event EventHandler BeginPanning;
|
||||
public event EventHandler EndPanning;
|
||||
|
||||
#endregion // end of Panning state
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user