528 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			528 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System.Collections.Generic;
 | |
| using System.Linq;
 | |
| using JetBrains.Annotations;
 | |
| using UnityEditor.Timeline.Actions;
 | |
| using UnityEngine;
 | |
| using UnityEngine.Playables;
 | |
| using UnityEngine.Timeline;
 | |
| 
 | |
| namespace UnityEditor.Timeline
 | |
| {
 | |
|     [MenuEntry("Edit in Animation Window", MenuPriority.TrackActionSection.editInAnimationWindow)]
 | |
|     class EditTrackInAnimationWindow : TrackAction
 | |
|     {
 | |
|         public static bool Do(TrackAsset track)
 | |
|         {
 | |
|             AnimationClip clipToEdit = null;
 | |
| 
 | |
|             AnimationTrack animationTrack = track as AnimationTrack;
 | |
|             if (animationTrack != null)
 | |
|             {
 | |
|                 if (!animationTrack.CanConvertToClipMode())
 | |
|                     return false;
 | |
| 
 | |
|                 clipToEdit = animationTrack.infiniteClip;
 | |
|             }
 | |
|             else if (track.hasCurves)
 | |
|             {
 | |
|                 clipToEdit = track.curves;
 | |
|             }
 | |
| 
 | |
|             if (clipToEdit == null)
 | |
|                 return false;
 | |
| 
 | |
|             GameObject gameObject = null;
 | |
|             if (TimelineEditor.inspectedDirector != null)
 | |
|                 gameObject = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, track);
 | |
| 
 | |
|             var timeController = TimelineAnimationUtilities.CreateTimeController(CreateTimeControlClipData(track));
 | |
|             TimelineAnimationUtilities.EditAnimationClipWithTimeController(clipToEdit, timeController, gameObject);
 | |
| 
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             if (!tracks.Any())
 | |
|                 return ActionValidity.Invalid;
 | |
| 
 | |
|             var firstTrack = tracks.First();
 | |
|             if (firstTrack is AnimationTrack)
 | |
|             {
 | |
|                 var animTrack = firstTrack as AnimationTrack;
 | |
|                 if (animTrack.CanConvertToClipMode())
 | |
|                     return ActionValidity.Valid;
 | |
|             }
 | |
|             else if (firstTrack.hasCurves)
 | |
|             {
 | |
|                 return ActionValidity.Valid;
 | |
|             }
 | |
| 
 | |
|             return ActionValidity.NotApplicable;
 | |
|         }
 | |
| 
 | |
|         public override bool Execute(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             return Do(tracks.First());
 | |
|         }
 | |
| 
 | |
|         static TimelineWindowTimeControl.ClipData CreateTimeControlClipData(TrackAsset track)
 | |
|         {
 | |
|             var data = new TimelineWindowTimeControl.ClipData();
 | |
|             data.track = track;
 | |
|             data.start = track.start;
 | |
|             data.duration = track.duration;
 | |
|             return data;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [MenuEntry("Lock selected track only", MenuPriority.TrackActionSection.lockSelected)]
 | |
|     class LockSelectedTrack : TrackAction, IMenuName
 | |
|     {
 | |
|         public static readonly string LockSelectedTrackOnlyText = L10n.Tr("Lock selected track only");
 | |
|         public static readonly string UnlockSelectedTrackOnlyText = L10n.Tr("Unlock selected track only");
 | |
| 
 | |
|         public string menuName { get; private set; }
 | |
| 
 | |
|         public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             UpdateMenuName(tracks);
 | |
|             if (tracks.Any(track => TimelineUtility.IsLockedFromGroup(track) || track is GroupTrack || !track.subTracksObjects.Any()))
 | |
|                 return ActionValidity.NotApplicable;
 | |
|             return ActionValidity.Valid;
 | |
|         }
 | |
| 
 | |
|         public override bool Execute(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             if (!tracks.Any()) return false;
 | |
| 
 | |
|             var hasUnlockedTracks = tracks.Any(x => !x.locked);
 | |
|             Lock(tracks.Where(p => !(p is GroupTrack)).ToArray(), hasUnlockedTracks);
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         void UpdateMenuName(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             menuName = tracks.All(t => t.locked) ? UnlockSelectedTrackOnlyText : LockSelectedTrackOnlyText;
 | |
|         }
 | |
| 
 | |
|         public static void Lock(TrackAsset[] tracks, bool shouldlock)
 | |
|         {
 | |
|             if (tracks.Length == 0)
 | |
|                 return;
 | |
| 
 | |
|             foreach (var track in tracks.Where(t => !TimelineUtility.IsLockedFromGroup(t)))
 | |
|             {
 | |
|                 TimelineUndo.PushUndo(track, L10n.Tr("Lock Tracks"));
 | |
|                 track.locked = shouldlock;
 | |
|             }
 | |
|             TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [MenuEntry("Lock", MenuPriority.TrackActionSection.lockTrack)]
 | |
|     [Shortcut(Shortcuts.Timeline.toggleLock)]
 | |
|     class LockTrack : TrackAction, IMenuName
 | |
|     {
 | |
|         static readonly string k_LockText = L10n.Tr("Lock");
 | |
|         static readonly string k_UnlockText = L10n.Tr("Unlock");
 | |
| 
 | |
|         public string menuName { get; private set; }
 | |
| 
 | |
|         void UpdateMenuName(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             menuName = tracks.Any(x => !x.locked) ? k_LockText : k_UnlockText;
 | |
|         }
 | |
| 
 | |
|         public override bool Execute(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             if (!tracks.Any()) return false;
 | |
| 
 | |
|             var hasUnlockedTracks = tracks.Any(x => !x.locked);
 | |
|             SetLockState(tracks, hasUnlockedTracks);
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             UpdateMenuName(tracks);
 | |
|             tracks = tracks.RemoveTimelineMarkerTrackFromList(TimelineEditor.inspectedAsset);
 | |
| 
 | |
|             if (!tracks.Any())
 | |
|                 return ActionValidity.NotApplicable;
 | |
|             if (tracks.Any(TimelineUtility.IsLockedFromGroup))
 | |
|                 return ActionValidity.Invalid;
 | |
|             return ActionValidity.Valid;
 | |
|         }
 | |
| 
 | |
|         public static void SetLockState(IEnumerable<TrackAsset> tracks, bool shouldLock)
 | |
|         {
 | |
|             if (!tracks.Any())
 | |
|                 return;
 | |
| 
 | |
|             foreach (var track in tracks)
 | |
|             {
 | |
|                 if (TimelineUtility.IsLockedFromGroup(track))
 | |
|                     continue;
 | |
| 
 | |
|                 if (track as GroupTrack == null)
 | |
|                     SetLockState(track.GetChildTracks().ToArray(), shouldLock);
 | |
| 
 | |
|                 TimelineUndo.PushUndo(track, L10n.Tr("Lock Tracks"));
 | |
|                 track.locked = shouldLock;
 | |
|             }
 | |
| 
 | |
|             // find the tracks we've locked. unselect anything locked and remove recording.
 | |
|             foreach (var track in tracks)
 | |
|             {
 | |
|                 if (TimelineUtility.IsLockedFromGroup(track) || !track.locked)
 | |
|                     continue;
 | |
| 
 | |
|                 var flattenedChildTracks = track.GetFlattenedChildTracks();
 | |
|                 foreach (var i in track.clips)
 | |
|                     SelectionManager.Remove(i);
 | |
|                 track.UnarmForRecord();
 | |
|                 foreach (var child in flattenedChildTracks)
 | |
|                 {
 | |
|                     SelectionManager.Remove(child);
 | |
|                     child.UnarmForRecord();
 | |
|                     foreach (var clip in child.GetClips())
 | |
|                         SelectionManager.Remove(clip);
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             // no need to rebuild, just repaint (including inspectors)
 | |
|             InspectorWindow.RepaintAllInspectors();
 | |
|             TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [UsedImplicitly]
 | |
|     [MenuEntry("Show Markers", MenuPriority.TrackActionSection.showHideMarkers)]
 | |
|     [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
 | |
|     class ShowHideMarkers : TrackAction, IMenuChecked
 | |
|     {
 | |
|         public bool isChecked { get; private set; }
 | |
| 
 | |
|         void UpdateCheckedStatus(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             isChecked = tracks.All(x => x.GetShowMarkers());
 | |
|         }
 | |
| 
 | |
|         public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             UpdateCheckedStatus(tracks);
 | |
|             if (tracks.Any(x => x is GroupTrack) || tracks.Any(t => t.GetMarkerCount() == 0))
 | |
|                 return ActionValidity.NotApplicable;
 | |
| 
 | |
|             if (tracks.Any(t => t.lockedInHierarchy))
 | |
|             {
 | |
|                 return ActionValidity.Invalid;
 | |
|             }
 | |
| 
 | |
|             return ActionValidity.Valid;
 | |
|         }
 | |
| 
 | |
|         public override bool Execute(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             if (!tracks.Any()) return false;
 | |
| 
 | |
|             var hasUnlockedTracks = tracks.Any(x => !x.GetShowMarkers());
 | |
|             ShowHide(tracks, hasUnlockedTracks);
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         static void ShowHide(IEnumerable<TrackAsset> tracks, bool shouldLock)
 | |
|         {
 | |
|             if (!tracks.Any())
 | |
|                 return;
 | |
| 
 | |
|             foreach (var track in tracks)
 | |
|                 track.SetShowTrackMarkers(shouldLock);
 | |
| 
 | |
|             TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [MenuEntry("Mute selected track only", MenuPriority.TrackActionSection.muteSelected), UsedImplicitly]
 | |
|     class MuteSelectedTrack : TrackAction, IMenuName
 | |
|     {
 | |
|         public static readonly string MuteSelectedText = L10n.Tr("Mute selected track only");
 | |
|         public static readonly string UnmuteSelectedText = L10n.Tr("Unmute selected track only");
 | |
| 
 | |
|         public string menuName { get; private set; }
 | |
| 
 | |
|         public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             UpdateMenuName(tracks);
 | |
|             if (tracks.Any(track => TimelineUtility.IsParentMuted(track) || track is GroupTrack || !track.subTracksObjects.Any()))
 | |
|                 return ActionValidity.NotApplicable;
 | |
|             return ActionValidity.Valid;
 | |
|         }
 | |
| 
 | |
|         public override bool Execute(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             if (!tracks.Any())
 | |
|                 return false;
 | |
| 
 | |
|             var hasUnmutedTracks = tracks.Any(x => !x.muted);
 | |
|             Mute(tracks.Where(p => !(p is GroupTrack)).ToArray(), hasUnmutedTracks);
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         void UpdateMenuName(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             menuName = tracks.All(t => t.muted) ? UnmuteSelectedText : MuteSelectedText;
 | |
|         }
 | |
| 
 | |
|         public static void Mute(TrackAsset[] tracks, bool shouldMute)
 | |
|         {
 | |
|             if (tracks.Length == 0)
 | |
|                 return;
 | |
| 
 | |
|             foreach (var track in tracks.Where(t => !TimelineUtility.IsParentMuted(t)))
 | |
|             {
 | |
|                 TimelineUndo.PushUndo(track, L10n.Tr("Mute Tracks"));
 | |
|                 track.muted = shouldMute;
 | |
|             }
 | |
| 
 | |
|             TimelineEditor.Refresh(RefreshReason.ContentsModified);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [MenuEntry("Mute", MenuPriority.TrackActionSection.mute)]
 | |
|     [Shortcut(Shortcuts.Timeline.toggleMute)]
 | |
|     class MuteTrack : TrackAction, IMenuName
 | |
|     {
 | |
|         static readonly string k_MuteText = L10n.Tr("Mute");
 | |
|         static readonly string k_UnMuteText = L10n.Tr("Unmute");
 | |
| 
 | |
|         public string menuName { get; private set; }
 | |
| 
 | |
|         void UpdateMenuName(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             menuName = tracks.Any(x => !x.muted) ? k_MuteText : k_UnMuteText;
 | |
|         }
 | |
| 
 | |
|         public override bool Execute(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             if (!tracks.Any() || tracks.Any(TimelineUtility.IsParentMuted))
 | |
|                 return false;
 | |
| 
 | |
|             var hasUnmutedTracks = tracks.Any(x => !x.muted);
 | |
|             Mute(tracks, hasUnmutedTracks);
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             UpdateMenuName(tracks);
 | |
|             if (tracks.Any(TimelineUtility.IsLockedFromGroup))
 | |
|                 return ActionValidity.Invalid;
 | |
|             return ActionValidity.Valid;
 | |
|         }
 | |
| 
 | |
|         public static void Mute(IEnumerable<TrackAsset> tracks, bool shouldMute)
 | |
|         {
 | |
|             if (!tracks.Any())
 | |
|                 return;
 | |
| 
 | |
|             foreach (var track in tracks)
 | |
|             {
 | |
|                 if (track as GroupTrack == null)
 | |
|                     Mute(track.GetChildTracks().ToArray(), shouldMute);
 | |
|                 TimelineUndo.PushUndo(track, L10n.Tr("Mute Tracks"));
 | |
|                 track.muted = shouldMute;
 | |
|             }
 | |
| 
 | |
|             TimelineEditor.Refresh(RefreshReason.ContentsModified);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     class DeleteTracks : TrackAction
 | |
|     {
 | |
|         public static void Do(TimelineAsset timeline, TrackAsset track)
 | |
|         {
 | |
|             SelectionManager.Remove(track);
 | |
|             TrackModifier.DeleteTrack(timeline, track);
 | |
|         }
 | |
| 
 | |
|         public override ActionValidity Validate(IEnumerable<TrackAsset> tracks) => ActionValidity.Valid;
 | |
| 
 | |
|         public override bool Execute(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             tracks = tracks.RemoveTimelineMarkerTrackFromList(TimelineEditor.inspectedAsset);
 | |
| 
 | |
|             // disable preview mode so deleted tracks revert to default state
 | |
|             // Case 956129: Disable preview mode _before_ deleting the tracks, since clip data is still needed
 | |
|             TimelineEditor.state.previewMode = false;
 | |
| 
 | |
|             TimelineAnimationUtilities.UnlinkAnimationWindowFromTracks(tracks);
 | |
| 
 | |
|             foreach (var track in tracks)
 | |
|                 Do(TimelineEditor.inspectedAsset, track);
 | |
| 
 | |
|             TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
 | |
| 
 | |
|             return true;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     class CopyTracksToClipboard : TrackAction
 | |
|     {
 | |
|         public static bool Do(TrackAsset[] tracks)
 | |
|         {
 | |
|             var action = new CopyTracksToClipboard();
 | |
|             return action.Execute(tracks);
 | |
|         }
 | |
| 
 | |
|         public override ActionValidity Validate(IEnumerable<TrackAsset> tracks) => ActionValidity.Valid;
 | |
| 
 | |
|         public override bool Execute(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             tracks = tracks.RemoveTimelineMarkerTrackFromList(TimelineEditor.inspectedAsset);
 | |
|             TimelineEditor.clipboard.CopyTracks(tracks);
 | |
|             return true;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     class DuplicateTracks : TrackAction
 | |
|     {
 | |
|         public override ActionValidity Validate(IEnumerable<TrackAsset> tracks) => ActionValidity.Valid;
 | |
| 
 | |
|         public override bool Execute(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             tracks = tracks.RemoveTimelineMarkerTrackFromList(TimelineEditor.inspectedAsset);
 | |
|             if (tracks.Any())
 | |
|             {
 | |
|                 SelectionManager.RemoveTimelineSelection();
 | |
|             }
 | |
| 
 | |
|             foreach (var track in TrackExtensions.FilterTracks(tracks))
 | |
|             {
 | |
|                 var newTrack = track.Duplicate(TimelineEditor.inspectedDirector, TimelineEditor.inspectedDirector);
 | |
|                 //Add all duplicated tracks to selection
 | |
|                 SelectionManager.Add(newTrack);
 | |
|                 foreach (var childTrack in newTrack.GetFlattenedChildTracks())
 | |
|                 {
 | |
|                     SelectionManager.Add(childTrack);
 | |
|                 }
 | |
| 
 | |
|                 //Duplicate bindings for tracks and subtracks
 | |
|                 if (TimelineEditor.inspectedDirector != null)
 | |
|                 {
 | |
|                     DuplicateBindings(track, newTrack, TimelineEditor.inspectedDirector);
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
 | |
| 
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         internal static void DuplicateBindings(TrackAsset track, TrackAsset newTrack, PlayableDirector director)
 | |
|         {
 | |
|             var originalTracks = track.GetFlattenedChildTracks().Append(track);
 | |
|             var newTracks = newTrack.GetFlattenedChildTracks().Append(newTrack);
 | |
|             var toBind = new List<Tuple<TrackAsset, Object>>();
 | |
| 
 | |
|             // Collect all track bindings to duplicate
 | |
|             var originalIt = originalTracks.GetEnumerator();
 | |
|             var newIt = newTracks.GetEnumerator();
 | |
|             while (originalIt.MoveNext() && newIt.MoveNext())
 | |
|             {
 | |
|                 var binding = director.GetGenericBinding(originalIt.Current);
 | |
|                 if (binding != null)
 | |
|                     toBind.Add(new Tuple<TrackAsset, Object>(newIt.Current, binding));
 | |
|             }
 | |
| 
 | |
|             //Only create Director undo if there are bindings to duplicate
 | |
|             if (toBind.Count > 0)
 | |
|                 TimelineUndo.PushUndo(TimelineEditor.inspectedDirector, L10n.Tr("Duplicate"));
 | |
| 
 | |
|             //Assign bindings for all tracks after undo.
 | |
|             foreach (var binding in toBind)
 | |
|             {
 | |
|                 TimelineEditor.inspectedDirector.SetGenericBinding(binding.Item1, binding.Item2);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [MenuEntry("Remove Invalid Markers", MenuPriority.TrackActionSection.removeInvalidMarkers), UsedImplicitly]
 | |
|     class RemoveInvalidMarkersAction : TrackAction
 | |
|     {
 | |
|         public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             if (tracks.Any(target => target != null && target.GetMarkerCount() != target.GetMarkersRaw().Count()))
 | |
|                 return ActionValidity.Valid;
 | |
| 
 | |
|             return ActionValidity.NotApplicable;
 | |
|         }
 | |
| 
 | |
|         public override bool Execute(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             bool anyRemoved = false;
 | |
|             foreach (var target in tracks)
 | |
|             {
 | |
|                 var invalids = target.GetMarkersRaw().Where(x => !(x is IMarker)).ToList();
 | |
|                 foreach (var m in invalids)
 | |
|                 {
 | |
|                     anyRemoved = true;
 | |
|                     target.DeleteMarkerRaw(m);
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (anyRemoved)
 | |
|                 TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
 | |
| 
 | |
|             return anyRemoved;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [Shortcut(Shortcuts.Timeline.collapseTrack)]
 | |
|     [UsedImplicitly]
 | |
|     class CollapseTrackAction : TrackAction
 | |
|     {
 | |
|         public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             var collapsibleTracks = tracks.Where(track => track.subTracksObjects.Any());
 | |
| 
 | |
|             if (!collapsibleTracks.Any())
 | |
|                 return ActionValidity.NotApplicable;
 | |
| 
 | |
|             if (collapsibleTracks.All(track => track.IsCollapsed()))
 | |
|                 return ActionValidity.NotApplicable;
 | |
| 
 | |
|             return ActionValidity.Valid;
 | |
|         }
 | |
| 
 | |
|         public override bool Execute(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             return KeyboardNavigation.TryCollapse(tracks.Where(track => track.subTracksObjects.Any() && !track.IsCollapsed()));
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     [Shortcut(Shortcuts.Timeline.expandTrack)]
 | |
|     [UsedImplicitly]
 | |
|     class ExpandTrackAction : TrackAction
 | |
|     {
 | |
|         public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             var collapsibleTracks = tracks.Where(track => track.subTracksObjects.Any());
 | |
| 
 | |
|             if (!collapsibleTracks.Any())
 | |
|                 return ActionValidity.NotApplicable;
 | |
| 
 | |
|             if (collapsibleTracks.All(track => !track.IsCollapsed()))
 | |
|                 return ActionValidity.NotApplicable;
 | |
| 
 | |
|             return ActionValidity.Valid;
 | |
|         }
 | |
| 
 | |
|         public override bool Execute(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             return KeyboardNavigation.TryExpand(tracks.Where(track => track.subTracksObjects.Any() && track.IsCollapsed()));
 | |
|         }
 | |
|     }
 | |
| }
 |