using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.Experimental.GraphView; using UnityEngine; using UnityEngine.UIElements; namespace AibisDream.FrameAnimation.Editor { internal sealed class FrameAnimationClipNodeView : Node { private readonly Label subtitle; private readonly Label behavior; private readonly Label flowBadge; private readonly Label issueBadge; private readonly FrameAnimationPreviewElement preview; private readonly Action selectedCallback; public AnimationNode Data { get; } public Port InputPort { get; } public Port OutputPort { get; } internal FrameAnimationClipNodeView( AnimationNode data, Action selectedCallback) { Data = data; this.selectedCallback = selectedCallback; viewDataKey = data.InternalId; AddToClassList("fa-node"); style.minWidth = 245f; style.maxWidth = 285f; InputPort = Port.Create(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(AnimationNode)); InputPort.portName = "In"; inputContainer.Add(InputPort); OutputPort = Port.Create(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(AnimationNode)); OutputPort.portName = "Next"; outputContainer.Add(OutputPort); subtitle = new Label(); behavior = new Label(); flowBadge = new Label(); issueBadge = new Label(); issueBadge.style.unityFontStyleAndWeight = FontStyle.Bold; extensionContainer.Add(subtitle); extensionContainer.Add(behavior); extensionContainer.Add(flowBadge); extensionContainer.Add(issueBadge); preview = new FrameAnimationPreviewElement(); preview.AddToClassList("fa-node__preview"); extensionContainer.Add(preview); RefreshExpandedState(); } public override void OnSelected() { base.OnSelected(); selectedCallback?.Invoke(this); } internal void Refresh( FrameAnimationGraph graph, IReadOnlyList issues, IReadOnlyList flows, IReadOnlyCollection entryFlowIds) { var clip = graph.Clips.FirstOrDefault(item => item != null && item.Id == Data.ClipId); title = string.IsNullOrWhiteSpace(Data.DisplayName) ? Data.ClipId : Data.DisplayName; subtitle.text = clip != null ? $"Clip: {Data.ClipId}" : $"Clip: "; var speed = Data.SpeedOverride.HasValue ? $"Speed {Data.SpeedOverride.Value:0.###} (Node)" : clip != null ? $"Speed {clip.Speed:0.###} (Clip)" : "Speed ?"; var end = Data.EndBehaviorOverride?.ToString() ?? "Continue / terminal fallback"; behavior.text = $"{speed} · {end}"; var used = flows.Select(flow => flow.Id).ToArray(); var entries = used.Where(entryFlowIds.Contains).ToArray(); flowBadge.text = used.Length == 0 ? "Unused" : "Flows: " + string.Join(", ", used.Take(3)) + (used.Length > 3 ? $" +{used.Length - 3}" : string.Empty) + (entries.Length > 0 ? " [ENTRY]" : string.Empty); var matching = issues.Where(issue => issue.Selection.Kind == FrameAnimationEditorSelectionKind.Node && ReferenceEquals(issue.Selection.Value, Data)).ToArray(); issueBadge.text = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error) ? "ERROR" : matching.Length > 0 ? "WARNING" : string.Empty; issueBadge.style.color = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error) ? new StyleColor(new Color(1f, 0.35f, 0.3f)) : new StyleColor(new Color(1f, 0.72f, 0.25f)); OutputPort.SetEnabled(!Data.EndBehaviorOverride.HasValue); OutputPort.tooltip = Data.EndBehaviorOverride.HasValue ? "该节点已设置终点结束行为,请先清除覆盖。" : "创建唯一的顺序后继"; } internal void SetDimmed(bool dimmed) { style.opacity = dimmed ? 0.45f : 1f; } internal void SetPreview( FrameAnimationPreviewDisplay display, FrameAnimationPreviewBackground background) { preview.SetBackground(background); preview.SetZoom(FrameAnimationPreviewZoomMode.Fit, 1f); preview.SetDisplay(display.Sprite, display.IsEmpty); preview.style.opacity = display.FlowState == FrameAnimationFlowElementState.Upcoming ? 0.55f : display.FlowState == FrameAnimationFlowElementState.Passed ? 0.82f : 1f; EnableInClassList("fa-node--preview-current", display.FlowState == FrameAnimationFlowElementState.Current); EnableInClassList("fa-node--preview-passed", display.FlowState == FrameAnimationFlowElementState.Passed); EnableInClassList("fa-node--preview-upcoming", display.FlowState == FrameAnimationFlowElementState.Upcoming); } } internal sealed class FrameAnimationEdgeView : Edge { private readonly Action selectedCallback; private Color baseColor = Color.white; public AnimationEdge Data { get; } internal FrameAnimationEdgeView(AnimationEdge data, Action selectedCallback) { Data = data; this.selectedCallback = selectedCallback; viewDataKey = data.InternalId; userData = data; } public override void OnSelected() { base.OnSelected(); selectedCallback?.Invoke(this); } internal void SetDimmed(bool dimmed) { style.opacity = dimmed ? 0.30f : 1f; } internal void Refresh(IReadOnlyList issues) { var matching = (issues ?? Array.Empty()).Where(issue => issue.Selection.Kind == FrameAnimationEditorSelectionKind.Edge && ReferenceEquals(issue.Selection.Value, Data)).ToArray(); var color = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error) ? new Color(1f, 0.3f, 0.25f) : matching.Length > 0 ? new Color(1f, 0.72f, 0.2f) : Color.white; baseColor = color; edgeControl.inputColor = color; edgeControl.outputColor = color; tooltip = matching.Length == 0 ? $"{Data.ExitName} / {Data.Condition}" : string.Join("\n", matching.Select(issue => $"[{issue.Code}] {issue.Message}")); } internal void SetPreviewState(FrameAnimationFlowElementState state) { if (state == FrameAnimationFlowElementState.Current) { edgeControl.inputColor = new Color(0.25f, 0.75f, 1f); edgeControl.outputColor = new Color(0.25f, 0.75f, 1f); } else if (state == FrameAnimationFlowElementState.Passed) { edgeControl.inputColor = new Color(0.35f, 0.68f, 0.46f); edgeControl.outputColor = new Color(0.35f, 0.68f, 0.46f); } else if (state == FrameAnimationFlowElementState.Upcoming) { edgeControl.inputColor = new Color(0.55f, 0.55f, 0.55f); edgeControl.outputColor = new Color(0.55f, 0.55f, 0.55f); } else { edgeControl.inputColor = baseColor; edgeControl.outputColor = baseColor; } } } internal sealed class FrameAnimationClipSearchProvider : ScriptableObject, ISearchWindowProvider { private FrameAnimationGraph graph; private Action selected; internal void Configure(FrameAnimationGraph value, Action callback) { graph = value; selected = callback; } public List CreateSearchTree(SearchWindowContext context) { var entries = new List { new SearchTreeGroupEntry(new GUIContent("Create Clip Node"), 0) }; foreach (var clip in graph.Clips.Where(clip => clip != null) .OrderBy(clip => clip.DisplayName, StringComparer.Ordinal) .ThenBy(clip => clip.Id, StringComparer.Ordinal)) { entries.Add(new SearchTreeEntry(new GUIContent($"{clip.DisplayName} ({clip.Id})")) { level = 1, userData = clip }); } return entries; } public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context) { if (entry.userData is FrameClip clip) { selected?.Invoke(clip); return true; } return false; } } internal sealed class FrameAnimationGraphView : GraphView { internal const string ClipDragKey = "AibisDream.FrameAnimation.ClipDrag"; private readonly Dictionary nodeViews = new Dictionary(); private readonly Dictionary edgeViews = new Dictionary(); private FrameAnimationGraph graph; private IReadOnlyList issues = Array.Empty(); private bool rebuilding; private Vector2 contextPosition; private string focusedFlowId = string.Empty; internal event Action SelectionRequested; internal event Action> MultiSelectionChanged; internal event Action> SelectionSetChanged; internal event Action CreateFlowRequested; internal event Action NodeCreated; internal event Action ExitFocusRequested; internal event Action GraphChanged; internal event Action NotificationRequested; internal event Action WorkspaceViewTransformChanged; internal FrameAnimationGraphView() { style.flexGrow = 1f; focusable = true; SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale); this.AddManipulator(new ContentDragger()); this.AddManipulator(new SelectionDragger()); this.AddManipulator(new RectangleSelector()); var grid = new GridBackground(); Insert(0, grid); grid.StretchToParentSize(); graphViewChanged = OnGraphViewChanged; viewTransformChanged += _ => WorkspaceViewTransformChanged?.Invoke(viewTransform.position, viewTransform.scale.x); RegisterCallback(_ => NotifySelectionSummary()); RegisterCallback(OnDragUpdated); RegisterCallback(OnDragPerform); RegisterCallback(OnKeyDown); } internal void Bind( FrameAnimationGraph value, IReadOnlyList validationIssues, string focusedFlowId) { graph = value; issues = validationIssues ?? Array.Empty(); var selectedIds = selection.OfType() .Select(view => view.Data.InternalId).ToArray(); var selectedEdgeIds = selection.OfType() .Select(view => view.Data.InternalId).ToArray(); rebuilding = true; DeleteElements(graphElements.ToList()); nodeViews.Clear(); edgeViews.Clear(); if (graph != null) { var topology = new FrameAnimationGraphTopology(graph); var entries = new HashSet(graph.Flows.Where(flow => flow != null) .Select(flow => flow.EntryNodeId)); foreach (var node in graph.Nodes.Where(node => node != null)) { var view = new FrameAnimationClipNodeView(node, OnNodeSelected); view.SetPosition(new Rect(FrameAnimationGraphMutationService.GetNodePosition(graph, node), new Vector2(255f, 150f))); view.Refresh(graph, issues, topology.FindFlowsUsingNode(node.InternalId), entries); nodeViews[node.InternalId] = view; AddElement(view); } foreach (var edge in graph.Edges.Where(edge => edge != null)) { if (!nodeViews.TryGetValue(edge.FromNodeId ?? string.Empty, out var from) || !nodeViews.TryGetValue(edge.ToNodeId ?? string.Empty, out var to)) { continue; } var view = new FrameAnimationEdgeView(edge, OnEdgeSelected) { output = from.OutputPort, input = to.InputPort }; view.output.Connect(view); view.input.Connect(view); view.Refresh(issues); edgeViews[edge.InternalId] = view; AddElement(view); } foreach (var id in selectedIds.Where(nodeViews.ContainsKey)) { AddToSelection(nodeViews[id]); } foreach (var id in selectedEdgeIds.Where(edgeViews.ContainsKey)) { AddToSelection(edgeViews[id]); } } rebuilding = false; SetFocusedFlow(focusedFlowId); } public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) { if (evt.target == this || evt.target == contentViewContainer) { contextPosition = contentViewContainer.WorldToLocal(evt.mousePosition); evt.menu.AppendAction("Create Clip Node", _ => OpenClipSearch(evt.mousePosition), graph != null && graph.Clips.Any(clip => clip != null) ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled); } if (selection.Count == 1 && selection[0] is FrameAnimationClipNodeView nodeView) { evt.menu.AppendAction("Create Flow From Node", _ => CreateFlowRequested?.Invoke(nodeView.Data)); } base.BuildContextualMenu(evt); } public override List GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) { var result = new List(); if (graph == null || !(startPort.node is FrameAnimationClipNodeView startNode)) { return result; } ports.ForEach(port => { if (!(port.node is FrameAnimationClipNodeView targetNode) || port.direction == startPort.direction || targetNode == startNode) { return; } var from = startPort.direction == Direction.Output ? startNode.Data : targetNode.Data; var to = startPort.direction == Direction.Output ? targetNode.Data : startNode.Data; if (FrameAnimationGraphMutationService.CanConnect(graph, from, to, out _)) { result.Add(port); } }); return result; } internal void SetFocusedFlow(string flowId) { focusedFlowId = flowId ?? string.Empty; if (graph == null || string.IsNullOrEmpty(flowId)) { foreach (var view in nodeViews.Values) view.SetDimmed(false); foreach (var view in edgeViews.Values) view.SetDimmed(false); return; } var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == flowId); var topology = new FrameAnimationGraphTopology(graph); var reachable = flow != null ? topology.GetReachable(flow.EntryNodeId) : new FrameAnimationReachability(Array.Empty(), Array.Empty()); var nodeIds = new HashSet(reachable.Nodes.Select(node => node.InternalId)); var edgeIds = new HashSet(reachable.Edges.Select(edge => edge.InternalId)); foreach (var pair in nodeViews) pair.Value.SetDimmed(!nodeIds.Contains(pair.Key)); foreach (var pair in edgeViews) pair.Value.SetDimmed(!edgeIds.Contains(pair.Key)); } internal void SelectAndFrame(FrameAnimationEditorSelection target) { GraphElement element = target.Value switch { AnimationNode node when nodeViews.TryGetValue(node.InternalId, out var nodeView) => nodeView, AnimationEdge edge when edgeViews.TryGetValue(edge.InternalId, out var edgeView) => edgeView, _ => null }; if (element == null) return; ClearSelection(); AddToSelection(element); FrameSelection(); } internal IReadOnlyList SelectedNodes => selection.OfType().Select(view => view.Data).ToArray(); internal IReadOnlyList VisibleNodes => nodeViews.Values .Where(view => view.worldBound.Overlaps(worldBound)) .Select(view => view.Data) .ToArray(); internal void ApplyPreview(FrameAnimationPreviewCoordinator coordinator) { if (coordinator == null) return; foreach (var view in nodeViews.Values) { view.SetPreview(coordinator.GetNodeDisplay(view.Data), coordinator.Background); } foreach (var view in edgeViews.Values) { view.SetPreviewState(coordinator.GetEdgeState(view.Data)); } } internal void RestoreViewTransform(Vector3 position, float scale) { var safeScale = float.IsNaN(scale) || float.IsInfinity(scale) ? 1f : Mathf.Clamp(scale, ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale); if (float.IsNaN(position.x) || float.IsInfinity(position.x) || float.IsNaN(position.y) || float.IsInfinity(position.y)) { position = Vector3.zero; } UpdateViewTransform(position, new Vector3(safeScale, safeScale, 1f)); } internal void RequestDeleteNodes(IEnumerable nodes) { var items = (nodes ?? Array.Empty()).Where(node => node != null).Distinct().ToArray(); if (graph == null || items.Length == 0) { return; } var ids = new HashSet(items.Select(node => node.InternalId)); var entryFlows = graph.Flows.Where(flow => flow != null && ids.Contains(flow.EntryNodeId)).ToArray(); var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeNodeRemoval(graph, items); if (!EditorUtility.DisplayDialog( "Delete Frame Animation Nodes", BuildRemovalMessage("Node", items.Length, impact, entryFlows), entryFlows.Length > 0 ? "Delete Nodes + Flows" : "Delete", "Cancel")) { return; } if (!FrameAnimationGraphMutationService.RemoveNodes(graph, items, entryFlows.Length > 0, out var error)) { NotificationRequested?.Invoke(error); return; } GraphChanged?.Invoke(); } internal void RequestDeleteEdges(IEnumerable edges) { var items = (edges ?? Array.Empty()).Where(edge => edge != null).Distinct().ToArray(); if (graph == null || items.Length == 0) { return; } var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeEdgeRemoval(graph, items); if (impact.Flows.Count > 0 && !EditorUtility.DisplayDialog( "Disconnect Frame Animation Nodes", BuildRemovalMessage("Edge", items.Length, impact, Array.Empty()), "Disconnect", "Cancel")) { return; } FrameAnimationGraphMutationService.RemoveEdges(graph, items); GraphChanged?.Invoke(); } internal void FrameCurrentFlow(string flowId) { if (graph == null || string.IsNullOrEmpty(flowId)) return; var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == flowId); if (flow == null) return; var ids = new HashSet(new FrameAnimationGraphTopology(graph) .GetReachable(flow.EntryNodeId).Nodes.Select(node => node.InternalId)); ClearSelection(); foreach (var pair in nodeViews.Where(pair => ids.Contains(pair.Key))) AddToSelection(pair.Value); FrameSelection(); } internal void ApplyCalculatedPositions(IReadOnlyDictionary positions) { foreach (var pair in positions) { if (nodeViews.TryGetValue(pair.Key.InternalId, out var view)) { var rect = view.GetPosition(); rect.position = pair.Value; view.SetPosition(rect); } } } private void OpenClipSearch(Vector2 panelPosition) { var provider = ScriptableObject.CreateInstance(); provider.Configure(graph, clip => CreateNode(clip, contextPosition)); SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(panelPosition)), provider); } internal void CreateNode(FrameClip clip, Vector2 position) { if (FrameAnimationGraphMutationService.CreateNode(graph, clip, position, out var node, out var error)) { NodeCreated?.Invoke(node); GraphChanged?.Invoke(); SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node)); } else { NotificationRequested?.Invoke(error); } } private GraphViewChange OnGraphViewChanged(GraphViewChange change) { if (rebuilding || graph == null) { return change; } if (change.edgesToCreate != null) { foreach (var visual in change.edgesToCreate.ToArray()) { var error = string.Empty; AnimationEdge data = null; var from = visual.output?.node as FrameAnimationClipNodeView; var to = visual.input?.node as FrameAnimationClipNodeView; if (from == null || to == null || !FrameAnimationGraphMutationService.TryConnect( graph, from.Data, to.Data, out data, out error)) { change.edgesToCreate.Remove(visual); NotificationRequested?.Invoke(string.IsNullOrEmpty(error) ? "无法创建连接。" : error); continue; } visual.userData = data; } GraphChanged?.Invoke(); } if (change.elementsToRemove != null) { ProcessRemovals(change); } if (change.movedElements != null) { var positions = change.movedElements.OfType() .ToDictionary(view => view.Data, view => view.GetPosition().position); if (positions.Count > 0) { FrameAnimationGraphMutationService.SetNodePositions(graph, positions, "Move Frame Animation Nodes"); GraphChanged?.Invoke(); } } return change; } private void ProcessRemovals(GraphViewChange change) { var nodeViewsToRemove = change.elementsToRemove.OfType().ToArray(); var removedNodeIds = new HashSet(nodeViewsToRemove.Select(view => view.Data.InternalId)); if (nodeViewsToRemove.Length > 0) { var nodes = nodeViewsToRemove.Select(view => view.Data).ToArray(); var explicitlySelectedEdges = change.elementsToRemove.OfType() .Select(edge => edge.userData as AnimationEdge) .Where(edge => edge != null).Distinct().ToArray(); var entryFlows = graph.Flows.Where(flow => flow != null && removedNodeIds.Contains(flow.EntryNodeId)).ToArray(); var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeRemoval( graph, nodes, explicitlySelectedEdges); var message = BuildRemovalMessage("Node", nodes.Length, impact, entryFlows); if (explicitlySelectedEdges.Length > 0) { message += $"\n\n同时删除选中的 Edge:{explicitlySelectedEdges.Length}"; } var confirmed = EditorUtility.DisplayDialog("Delete Frame Animation Nodes", message, entryFlows.Length > 0 ? "Delete Nodes + Flows" : "Delete", "Cancel"); var removed = false; var error = string.Empty; if (confirmed) { removed = FrameAnimationGraphMutationService.RemoveNodesAndEdges( graph, nodes, explicitlySelectedEdges, entryFlows.Length > 0, out error); } if (!removed) { change.elementsToRemove.Clear(); if (!string.IsNullOrEmpty(error)) NotificationRequested?.Invoke(error); } GraphChanged?.Invoke(); return; } var remainingEdges = change.elementsToRemove.OfType() .Where(edge => !IsIncidentTo(edge, removedNodeIds)) .Select(edge => edge.userData as AnimationEdge).Where(edge => edge != null).Distinct().ToArray(); if (remainingEdges.Length > 0) { var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeEdgeRemoval(graph, remainingEdges); if (impact.Flows.Count > 0 && !EditorUtility.DisplayDialog("Disconnect Frame Animation Nodes", BuildRemovalMessage("Edge", remainingEdges.Length, impact, Array.Empty()), "Disconnect", "Cancel")) { foreach (var edgeView in change.elementsToRemove.OfType() .Where(view => remainingEdges.Contains(view.userData as AnimationEdge)).ToArray()) { change.elementsToRemove.Remove(edgeView); } } else { FrameAnimationGraphMutationService.RemoveEdges(graph, remainingEdges); } } GraphChanged?.Invoke(); } private static bool IsIncidentTo(Edge edge, IReadOnlyCollection nodeIds) { var from = edge?.output?.node as FrameAnimationClipNodeView; var to = edge?.input?.node as FrameAnimationClipNodeView; return from != null && nodeIds.Contains(from.Data.InternalId) || to != null && nodeIds.Contains(to.Data.InternalId); } private static string BuildRemovalMessage( string kind, int count, FrameAnimationGraphImpact impact, IReadOnlyCollection entryFlows) { var message = $"将删除 {count} 个 {kind}。"; if (impact.Flows.Count > 0) { message += $"\n\n受影响 Flow:{string.Join(", ", impact.Flows.Select(flow => flow.Id))}" + $"\n将失去可达关系的 Node:{impact.LostNodeCount}"; } if (entryFlows.Count > 0) { message += $"\n\n入口 Flow 将同时删除:{string.Join(", ", entryFlows.Select(flow => flow.Id))}"; } return message; } private void OnNodeSelected(FrameAnimationClipNodeView view) { if (rebuilding) { return; } SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, view.Data)); NotifySelectionSummary(); } private void OnEdgeSelected(FrameAnimationEdgeView view) { if (rebuilding) { return; } SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Edge, view.Data)); NotifySelectionSummary(); } private void NotifySelectionSummary() { if (rebuilding) { return; } MultiSelectionChanged?.Invoke(SelectedNodes); var selected = selection.Select(item => item switch { FrameAnimationClipNodeView nodeView => new FrameAnimationEditorSelection( FrameAnimationEditorSelectionKind.Node, nodeView.Data), FrameAnimationEdgeView edgeView => new FrameAnimationEditorSelection( FrameAnimationEditorSelectionKind.Edge, edgeView.Data), _ => default }) .Where(item => item.Kind != FrameAnimationEditorSelectionKind.None) .ToArray(); SelectionSetChanged?.Invoke(selected); } private void OnKeyDown(KeyDownEvent evt) { if (!TryHandleFocusExitShortcut(evt.keyCode, evt.target is TextField)) { return; } evt.StopImmediatePropagation(); } internal bool TryHandleFocusExitShortcut(KeyCode keyCode, bool textInputActive) { if (keyCode != KeyCode.Escape || textInputActive || string.IsNullOrEmpty(focusedFlowId)) { return false; } ExitFocusRequested?.Invoke(); return true; } private void OnDragUpdated(DragUpdatedEvent evt) { if (DragAndDrop.GetGenericData(ClipDragKey) is FrameClip clip && graph?.Clips.Contains(clip) == true) { DragAndDrop.visualMode = DragAndDropVisualMode.Copy; evt.StopPropagation(); } } private void OnDragPerform(DragPerformEvent evt) { if (!(DragAndDrop.GetGenericData(ClipDragKey) is FrameClip clip) || graph?.Clips.Contains(clip) != true) { return; } DragAndDrop.AcceptDrag(); CreateNode(clip, contentViewContainer.WorldToLocal(evt.mousePosition)); DragAndDrop.SetGenericData(ClipDragKey, null); evt.StopPropagation(); } } }