This commit is contained in:
2021-06-13 10:28:03 +02:00
parent eb70603c85
commit df2d24cbd3
7487 changed files with 943244 additions and 0 deletions

View File

@@ -0,0 +1,498 @@
# Changelog
All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
## [1.5.4] - 2021-03-10
### Fixed
- Fixed issue where the horizontal scrollbar could not be moved or resized.
## [1.5.3] - 2021-03-05
### Changed
- Disabled edition of Track Asset Inspector Script field as it could break Timeline Assets.
### Fixed
- Fixed issue where the timeline header track would automatically open during a drag and drop operation. ([1305436](https://issuetracker.unity3d.com/product/unity/issues/guid/1305436))
- Fixed a rare issue where some broken tracks could not be removed. ([1305388](https://issuetracker.unity3d.com/product/unity/issues/guid/1305388))
- Fixed rare issue where the time field could not be edited after opening a timeline. ([1312198](https://issuetracker.unity3d.com/product/unity/issues/guid/1312198))
- Fixed cosmetic issue where the duration marker was drawn over the scroll bar.
- Fixed issue where times without a decimal separator (. or , depending on locale) would not be interpreted correctly by the time field. (1315605)
- Fixed issue where a selection rectangle could not be made when started inside a track. ([1315840](https://issuetracker.unity3d.com/product/unity/issues/guid/1315840))
- Performing Undo/Redo will not affect Timeline window selection when the window is locked. (Selecting sub-timelines can still be undone). ([1313515](https://issuetracker.unity3d.com/product/unity/issues/guid/1313515))
- Fixed an issue where text would be clipped in the track header binding. ([1302401](https://issuetracker.unity3d.com/product/unity/issues/guid/1302401))
- Fixed issue where clicking in the Timeline window while there is no active timeline would throw an exception.
## [1.5.2] - 2021-01-08
### Added
- During recording, there are new ways to key animated properties:
- A new Inspector context menu has been added (`Key All Animated`) that sets a key to all currently animated properties.
- It is possible to make a multi-selection of tracks to set a keyframe to all currently animated properties. If no track is selected, all recording tracks are keyed.
- If properties are selected in the curve editor, only those properties are keyed.
- `TimelineEditor.GetWindow` and `TimelineEditor.GetOrCreateWindow` to get the current Timeline window or create a Timeline window.
- `TimelineEditorWindow.SetCurrentTimeline` to change which timeline asset is opened in the Timeline window.
- `TimelineEditorWindow.lock` to lock or unlock the Timeline window.
- `TrackExtensions.GetCollapsed`, `TrackExtensions.SetCollapsed`, `TrackExtensions.IsVisibleRecursive` to get and change the visibility state of a track.
- `AnimationTrackExtensions.IsRecording`, `AnimationTrackExtensions.SetRecording`, `AnimationTrackExtensions.SupportsRecording` to get or change the recording state of an Animation track.
- Added two methods in `TrackEditor` to control how an object is bound to a track: `IsBindingAssignableFrom` and `GetBindingFrom`.
- Added Japanese translation.
- The Timeline window will automatically rebuild the graph when a notifications's properties are changed.
- The Timeline window will be automatically refreshed when a marker's properties are changed.
- Added `TimelineEditor.GetInspectedTimeFromMasterTime` and `TimelineEditor.GetMasterTimeFromInspectedTime` to convert time from master to inspected timeline and vice versa when using sub-timelines.
- Added API to improve how to get/set a `TimelineClip`'s parent track:
- `TimelineClip.GetParentTrack` (replaces obsolete property getter)
- `ItemsUtils.SetParentTrack` (extension method thar replaces obsolete property setter)
- Added a new `Seconds` time display mode and renamed previous Seconds mode to Timecode.
- `TimelinePreferences.timeFormat` field,
- `UnityEditor.Timeline.TimeFormat` enum.
- Added API for the user to clip to the track area:
- API: Relevant member to `MarkerOverlayRegion`,
- API: `MarkerOverlayRegion.trackRegion`,
- API: `MarkerOverlayRegion` constructor.
- Added _Gameplay sequence_ sample.
- This sample demonstrates how Timeline can be used to create a small in-game moment, using built-in tracks.
- Added _Customization_ sample.
- This sample demonstrates how to create custom tracks, clips, markers and actions.
### Changed
- The binding field on a track header will change its background color when dragging a valid object on it.
- Timeline marker track is now selectable.
- `TimelineClip` property `parentTrack` is now obsolete.
- `TimelinePreferences.timeUnitInFrames` is now obsolete.
### Fixed
- Fixed a bug affecting the conversion between seconds and frames in the inspector.
- Fixed issue where `KeyAllAnimated` was available when right-clicking on markers and tracks that were not in record mode. (1270304)
- Fixed issue where the mouse cursor would stay stuck to a resize icon when resizing the track header. ([1076031](https://issuetracker.unity3d.com/product/unity/issues/guid/1076031/))
- Fixed case where an animation event at time 0 would not fire on a timeline loop. ([1184106](https://issuetracker.unity3d.com/product/unity/issues/guid/1184106))
- Fixed issue where Timeline objects (ie. `TrackAsset`, `ControlTrack`, `SignalAsset`, etc.) would have incorrect links to the documentation pages. *Available starting from Unity 2021.1*. ([1082941](https://issuetracker.unity3d.com/product/unity/issues/guid/1082941))
- Fixed multiple issues related to blends
- Fix display of blends when clips have ease-in/ease-out ([1178066](https://issuetracker.unity3d.com/product/unity/issues/guid/1178066))
- Fix clip disappearing when dragging it from left to right completely inside another clip.
- Fix select and drag clip discarding foreground display rule of selected clip after releasing the drag.
- Fix fully blended clips selection not available. ([1289912](https://issuetracker.unity3d.com/product/unity/issues/guid/1289912))
- Fixed issue where the clip display would flicker when moving two clips that are completely overlapped. ([1085679](https://issuetracker.unity3d.com/product/unity/issues/guid/1085679))
- The Timeline window will no longer revert to editing only the asset if the user uses the Timeline selector to pick a game object and switches focus. ([1291455](https://issuetracker.unity3d.com/product/unity/issues/guid/1291455))
- Create button on timeline panel no longer defaults to an invalid path. ([1289923](https://issuetracker.unity3d.com/product/unity/issues/guid/1289923))
- Fixed issue where Timeline's bindings field would loses names and bindings when selecting clips. ([1293941](https://issuetracker.unity3d.com/product/unity/issues/guid/1293941))
- Make Timeline's duration result displayed in the Inspector, when switching from duration mode: Based On Clips to Fixed Length, closer to the actual duration. ([1156920](https://issuetracker.unity3d.com/product/unity/issues/guid/1156920))
- Copy/Paste of clips in the Timeline Window will no longer paste clips at an invalid time in mix-mode. ([1289925](https://issuetracker.unity3d.com/product/unity/issues/guid/1289925))
## [1.4.5] - 2020-11-19
### Fixed
- Fixed issue where changing a clip's extrapolation values would clear the current clip selection. ([936046](https://issuetracker.unity3d.com/product/unity/issues/guid/936046))
- Fixed multiple issues related to the curves view:
- Fixed curve removal not functioning with `PlayableAsset`s (clips & tracks curves). ([1231002](https://issuetracker.unity3d.com/product/unity/issues/guid/1231002))
- Fixed inconsistent icon display on curves.
- Fixed incorrect ordering of properties. Properties now have a object/type/property ordering.
- Fixed unnecessary grouping of fields.
- Changed context menu from `Remove Properties` to `Remove Curves` to better reflect the change in functionality between curves for GameObjects and curves for `PlayableAssets`.
- Fixed behaviour where removing a single field in a `Position`, `Rotation` or `Scale` group would remove the entire group.
- Fixed case where pausing in Playmode and switching the active director in editor could pause the director. ([1263707](https://issuetracker.unity3d.com/product/unity/issues/guid/1263707))
- Material properties are now displayed by their shader name in the curves view when possible. ([1115961](https://issuetracker.unity3d.com/product/unity/issues/guid/1115961))
- Fixed issue where a signal could be pasted on a track that doesn't support notifications. ([1283763](https://issuetracker.unity3d.com/product/unity/issues/guid/1283763))
- Fixed issue where a clip could be paseted on an incompatible track. ([1283763](https://issuetracker.unity3d.com/product/unity/issues/guid/1283763))
- Fixed errors when leaving prefab mode when a timeline is opened. ([1280331](https://issuetracker.unity3d.com/product/unity/issues/guid/1280331))
- No preview will be shown when the PlayableDirector is disabled. ([1286198](https://issuetracker.unity3d.com/product/unity/issues/guid/1286198))
- Fixed issue where an infinite clip's `Foot Ik` property was not visible in the Inspector when selecting its track. ([1279824](https://issuetracker.unity3d.com/product/unity/issues/guid/1279824))
- Fixed issue where child particle systems were not controlled correctly when they are not subemitters. ([1212943](https://issuetracker.unity3d.com/product/unity/issues/guid/1212943))
- Fixed inconsistent recording behaviour on audio tracks and `PlayableAssets`. Default values are changed when a value is not recorded, and the key added/updated when a value is already animated. ([1283453](https://issuetracker.unity3d.com/product/unity/issues/guid/1283453))
- Fixed issue where the curves view for tracks and `PlayableAsset`s would not update when changed externally (such as from the Animation window).
- Fixed `Add Key`/`Remove Key` context menus not being properly enabled in some cases when using tracks and `PlayableAsset`s.
- Fixed simulation of subemitters when scrubbing a timeline. ([1142781](https://issuetracker.unity3d.com/product/unity/issues/guid/1142781))
- Fixed choppy playback of particles with a large fixed time step. ([1262234](https://issuetracker.unity3d.com/product/unity/issues/guid/1262234))
## [1.4.4] - 2020-10-09
### Fixed
- Disable drag and drop of Signal asset on Control Track. ([1222760](https://issuetracker.unity3d.com/product/unity/issues/guid/1222760/))
- Fixed system locale causing issues when keying float values on custom clips. ([1190877](https://issuetracker.unity3d.com/product/unity/issues/guid/1190877/))
- Fixed issue where recording to a clip would place keys on the frame. ([1274892](https://issuetracker.unity3d.com/product/unity/issues/guid/1274892/))
- Fixed keyboard clip selection from locked tracks. ([1233612](https://issuetracker.unity3d.com/product/unity/issues/guid/1233612/))
- Fixed issue where the Timeline window would stay locked even when no timeline asset is shown. ([1278598](https://issuetracker.unity3d.com/product/unity/issues/guid/1278598/))
- Fixed issue where invoking `SelectLeft` or `SelectRight` shortcuts on a group track, the group would not collapse or expand. ([1279379](https://issuetracker.unity3d.com/product/unity/issues/guid/1279379/))
- Fixed Blend Curve Editor from the clip's inspector that was not responding correctly to undo and redo commands. (978673)
- Fixed issue where the `Frame All` action would not frame keys outside of clips when the curve display is collapsed. ([1273725](https://issuetracker.unity3d.com/product/unity/issues/guid/1273725/), #295)
- Scrolling the horizontal scrollbar of the timeline to the right edge will no longer prevent the user from dragging left again. ([1127199](https://issuetracker.unity3d.com/product/unity/issues/guid/1127199/), #301)
- Splitting a clip with an ease in or out value now ensures ease duration stays on correct side of split. ([1279350](https://issuetracker.unity3d.com/product/unity/issues/guid/1279350/))
- Fixed delay when zooming in after reaching Timeline window's maximum and then zooming back. ([1214228](https://issuetracker.unity3d.com/product/unity/issues/guid/1214228/))
- Prevent creation of presets with Group Tracks. ([1281056](https://issuetracker.unity3d.com/product/unity/issues/guid/1281056))
- Fixed issue where markers placed on top of clips could not be selected. ([1284807](https://issuetracker.unity3d.com/product/unity/issues/guid/1284807), #314)
- Fixed issue where multiple markers placed on top of each other could not be selected. ([1284801](https://issuetracker.unity3d.com/product/unity/issues/guid/1284801), #314)
## [1.4.3] - 2020-08-26
### Fixed
- Fixed incorrect selection when clicking on a clip's blend. (1178052)
- Fixed issue where an exception was thrown when drawing an Audio clip's waveform when that clip wasn't in the AssetDatabase. ([1268868](https://issuetracker.unity3d.com/product/unity/issues/guid/1268868/))
- When choosing `Add Signal Emitter from Signal Asset`, closing the Object Selector window will not add an empty Signal Emitter. ([1261553](https://issuetracker.unity3d.com/product/unity/issues/guid/1261553/))
- Fixed issue where an error would appear when editing keys in the Animation window if the Timeline window is opened. (1269829)
- Fixed issue where the `Frame All` operation would continually increase the zoom value when only empty tracks are added to the timeline ([1273540](https://issuetracker.unity3d.com/product/unity/issues/guid/1273540/)).
## [1.4.2] - 2020-08-04
### Fixed
- Fixed double-click not opening the AnimationWindow on clips with animated parameters. ([1262950](https://issuetracker.unity3d.com/product/unity/issues/guid/1262950/))
- Fixed issue where the Timeline window would rebuild its Playable Graph every time an AnimationClip would be added, changed or deserialized. (1265314, [1267055](https://issuetracker.unity3d.com/product/unity/issues/guid/1267055/))
## [1.4.1] - 2020-07-15
### Fixed
- Fixed `IndexOutOfRangeException` exception being thrown when editing inspector curves. ([1259902](https://issuetracker.unity3d.com/product/unity/issues/guid/1259902/))
- Fixed `IndexOutOfRangeException` exception being thrown when the `New Signal` dialog replaces an existing signal. ([1241170](https://issuetracker.unity3d.com/product/unity/issues/guid/1241170/))
- Fixed signal state being reset on paused timelines. ([1257208](https://issuetracker.unity3d.com/product/unity/issues/guid/1257208/))
- Fixed nested custom types not updating animation values in the inspector. ([1239893](https://issuetracker.unity3d.com/product/unity/issues/guid/1239893/))
- Fixed `AnimationTrack`s SceneOffset mode incorrectly overriding root transform on tracks without root transform in editor. ([1237704](https://issuetracker.unity3d.com/product/unity/issues/guid/1237704/))
- The `DisplayName` attribute is now supported when used with `TrackAsset`s. ([1253397](https://issuetracker.unity3d.com/product/unity/issues/guid/1253397/))
- Fixed `NullReference` exception being thrown when clicking on the `Scene Preview` checkbox if the Timeline window was closed. (1261543)
## [1.4.0] - 2020-06-26
### Added
- Added `ClipCaps.AutoScale` to automatically change the speed multiplier value when the clip is trimmed in the Timeline window.
- Added a `DeleteClip` method in `TrackAsset`.
- Added dependency on Animation, Audio, Director and Particle System modules. ([1229825](https://issuetracker.unity3d.com/product/unity/issues/guid/1229825/))
- Added an option in `TimelineAsset.EditorSettings` to disable scene preview.
- Added base classes to define custom actions:
- `TimelineAction`
- `TrackAction`
- `ClipAction`
- `MarkerAction`
- Added the following attributes that can be used with action classes:
- `ApplyDefaultUndo` to automatically manage undo operations.
- `ActiveInMode` to control in which Timeline mode the action is valid.
- `MenuEntry` to add the action to the context menu.
- `TimelineShortcut` can be added to a static method to invoke the action with a shortcut.
- `Invoker` to invoke actions using Timeline's selection or context.
- `MenuOrder` contains menu priority values, to be used with `MenuEntry`.
- `TimelineModes` to specify in which mode an action is valid, to be used with `MenuEntry`.
- `ActionContext` to provide a context to invoke `TimelineAction`s.
- `ActionValidity` to specify is an action is valid for a given context.
- `UndoExtension` to manage undo operations with common Timeline types.
### Changed
- Improved performance with ControlTracks in preview mode for cases where multiple Control Tracks are assigned to the same PlayableDirector.
- Improved layout and appearance of track header buttons.
- Reduced icons' file size without any quality loss.
- A track's binding will be duplicated when pasting or duplicating a track.
- When creating a new timeline asset, the "Timeline" suffix will not be added to the file name twice.
- `ClipCaps.All` now includes the new `Autoscale` feature. To get the previous `ClipCaps.All` behaviour on clips, use
```
ClipCaps.Looping | ClipCaps.Extrapolation | ClipCaps.ClipIn | ClipCaps.SpeedMultiplier | ClipCaps.Blending
```
- Inline curve selection is now synced with the clip's selection.
- Selecting a curve view property will also select the corresponding curve view.
- Clicking and holding the `Command` or `Control` key on a curve view will deselect it if it was already selected.
- Improved Timeline window UI performance.
### Fixed
- Selecting clips from locked tracks is not allowed anymore when using the playhead's context menu.
- Inserting gaps in locked tracks is not allowed anymore.
- When adding an Activation track, the viewport is adjusted to show the new Activation clip.
- Fixed issue where trimming AnimationClips would also change the speed multiplier.
## [1.3.4] - 2020-06-09
### Fixed
- Fix a Control Track bug that caused the first frame of an animation to evaluated incorrectly when scrubbing forwards and backwards. (1253485)
- Fixed memory leak where the most recently played timeline would not get unloaded. ([1214752](https://issuetracker.unity3d.com/product/unity/issues/guid/1214752/) and 1253974)
## [1.3.3] - 2020-05-29
### Fixed
- Fixed regression where animation tracks were writing root motion when the animation clip did not contain root transform values ([1249355](https://issuetracker.unity3d.com/product/unity/issues/guid/1249355/))
## [1.3.2] - 2020-04-02
### Fixed
- Fixed issue where the clip Inspector's curve preview would close when clicking on the curve. ([1228127](https://issuetracker.unity3d.com/product/unity/issues/guid/1228127/))
- Fixed issue where the curves view was not synced between Animation and Timeline windows. ([1213937](https://issuetracker.unity3d.com/issues/animation-window-curves-are-not-updated-immediately-when-changing-them-in-timeline-window))
- Fixed issue where play range didn't loop when range ends on the final frame. ([1215926](https://issuetracker.unity3d.com/issues/timeline-play-range-doesnt-loop-when-play-range-ends-on-the-final-frame))
- Fixed issue where displaying an array in the curves view generated errors. ([1178251](https://issuetracker.unity3d.com/product/unity/issues/guid/1178251/))
## [1.3.1] - 2020-03-13
### Fixed
- Fixed issue where the curves view would flicker when editing multiple keys. ([1217326](https://issuetracker.unity3d.com/product/unity/issues/guid/1217326/))
- Fixed issue where adding a keyframe in the curves view at the end of a clip would not place the keyframe at the correct position. ([1221337](https://issuetracker.unity3d.com/product/unity/issues/guid/1221337/))
## [1.3.0] - 2020-02-26
### Added
- Inline Curve Properties can be removed.
- Tracks can be individually resized.
### Changed
- Creating a new Timeline will no longer automatically add an Animation Track and an Animator to the target GameObject.
- Ease-in and ease-out values for clips are no longer restricted to 50% of the clip's duration.
- The resize handle for inline curves has been moved to the track header area.
- Reduced the minimum width of the track header area.
- Trimming the left edge of a clip while pressing the Shift key will change the Speed Multiplier value.
### Fixed
- Fixed humanoid characters going to default pose during initial root motion recording. (1174752)
- Fixed Override Tracks not masking RootTransform when an AvatarMask without the Root Node is applied. ([1190600](https://issuetracker.unity3d.com/product/unity/issues/guid/1190600/))
- Fixed preview of Avatar Masks on base level Animation Tracks. ([1190600](https://issuetracker.unity3d.com/product/unity/issues/guid/1190600/))
## [1.2.13] - 2020-02-24
### Fixed
- Fixed Performance issue where Control Tracks would resimulate during the tail of a non-looping particle clip. ([1216702](https://issuetracker.unity3d.com/product/unity/issues/guid/1216702/))
- Fixed adjacent recording clips highlighting the wrong clip. ([1210312](https://issuetracker.unity3d.com/product/unity/issues/guid/1210312/))
- Fixed timescale drawing to only draw visible lines which avoids a hang with very large clips. ([1213189](https://issuetracker.unity3d.com/product/unity/issues/guid/1213189/))
- Fixed `SignalReceiver.ChangeSignalAtIndex` incorrectly throwing exception when multiple entries are set to null. ([1210877](https://issuetracker.unity3d.com/product/unity/issues/guid/1210877/))
- Fixed a memory leak with Animation Clips in Edit mode.
- Fixed issue where changes to a Signal Receiver component in a prefab were reverted. ([1210883](https://issuetracker.unity3d.com/product/unity/issues/guid/1210883/))
- Fixed avatar mask reassignment not causing immediate re-evaluation. ([1219326](https://issuetracker.unity3d.com/product/unity/issues/guid/1219326/))
- Fixed issues related to recursive control tracks. (1178423)
- Fixed issue where using the `HideInMenu` attribute in combination with a class inheriting from `Marker` would not hide the marker from the Timeline context menus. ([1221054](https://issuetracker.unity3d.com/product/unity/issues/guid/1221054/))
## [1.2.12] - 2020-02-21
### Fixed
- Fixed issue where the curves view would change its framing when moving a clip. ([1217353](https://issuetracker.unity3d.com/product/unity/issues/guid/1217353/))
## [1.2.11] - 2020-01-22
### Fixed
- Fixed Control Track inspector dropdown not opening. ([1208943](https://issuetracker.unity3d.com/product/unity/issues/guid/1208943/))
- Fixed issue where applying the Match content command on subtimeline clip with a newly created subtimeline with no duration makes the clip disappear. ([1203662](https://issuetracker.unity3d.com/product/unity/issues/guid/1203662/))
- Fixed issue where the opened timeline is changed to another timeline when switching focus from Unity to a different application. ([1087348](https://issuetracker.unity3d.com/product/unity/issues/guid/1087348/))
- Fixed issue where the keys in the inline curves view were incorrectly positioned ([1205835](https://issuetracker.unity3d.com/product/unity/issues/guid/1205835/))
### Changed
- ControlPlayableAsset.searchHierarchy (a.k.a. Control Children) now defaults to false.
## [1.2.10] - 2019-12-08
### Fixed
- Fixed issue where object selectors on tracks did not show bound objects. (1202853)
- Fixing inspector blend graph display for animation clips. (1201474)
- Fixed Timeline Window lock state when restarting Unity and no timeline are selected. ([1201405](https://issuetracker.unity3d.com/product/unity/issues/guid/1201405/))
## [1.2.9] - 2019-12-06
### Fixed
- Added missing high-resolution icons for Personal Skin.
## [1.2.8] - 2019-11-21
### Fixed
- Fixed issue where recording couldn't be turned on for override tracks. (1199389)
- Fixed overlay bug when panning. (1198348)
- Fixed Foot IK being applied in Editor when option is disabled. ([1197426](https://issuetracker.unity3d.com/product/unity/issues/guid/1197426/))
- Fixed issue where the Animation Track's inline curves were not properly aligned when panning the timeline. (1198364)
## [1.2.7] - 2019-11-15
### Fixed
- Fixed inline curves to display PlayableBehaviour array properties. (1178251)
- Fixed clip selection from playhead. (1187495)
- Fixed recorded clips dirtying the scene on copy/paste. (1181492)
## [1.2.6] - 2019-10-25
### Added
- Added Timeline manual.
## [1.2.5] - 2019-10-16
### Changed
- Added tooltips that were missing for Timeline selector and settings buttons. ([1152790](https://issuetracker.unity3d.com/product/unity/issues/guid/1152790/))
- Removed Undo menu entry that was added when clicking on the Inline curves button. ([1187402](https://issuetracker.unity3d.com/product/unity/issues/guid/1187402/))
### Fixed
- Fixed issue where recording couldn't be turned off when an object is deactivated. (1187174)
- Timelines listed in the Timeline selector will now be sorted alphabetically. (1190514)
- Fixed Insert Frames options from Trackhead context menu not applying to markers. (1187895)
- Fixed incorrect display when a large number of nested group tracks was added to a Timeline. (1157367)
## [1.2.4] - 2019-10-03
### Changed
- Properties in the Inline Curve editor will now be listed in the same order as the Animation window. (1184058)
- Updated the appearance of the Timeline window to conform to the [editor's UX redesign](https://blogs.unity3d.com/2019/08/29/evolving-the-unity-editor-ux/)
- Improved the appearance of clip blends.
### Fixed
- Adding a PlayableDirector with no Playable Asset will no longer trigger a repaint of the Timeline Window on each frame. ([1172707](https://issuetracker.unity3d.com/product/unity/issues/guid/1172707/))
- Fixed issue where a clip's blend selection border was not drawn correctly when there was a previous clip. (1178173)
- Fixed issue where Animation Events were fired twice when the Playable Director Wrap mode is set to Loop. ([1173281](https://issuetracker.unity3d.com/product/unity/issues/guid/1173281/))
- Fixed issue where double-clicking on a Timeline Asset would not open it in the Timeline window. ([1182159](https://issuetracker.unity3d.com/product/unity/issues/guid/1182159))
- Fixed issue where the paste shortcut would not work when copying and pasting between two different timelines. (1184967)
- Fixed audio stutter when going into playmode. ([1167289](https://issuetracker.unity3d.com/product/unity/issues/guid/1167289/))
- Fixed PreviousFrame and NextFrame controls in subtimelines with large offsets. (1175320)
- Fixed issue where exceptions were thrown when resetting a Signal Receiver component. ([1158227](https://issuetracker.unity3d.com/product/unity/issues/guid/1158227/))
- Increased font size of clip labels (1179642)
## [1.2.3] - 2019-10-03
### Fixed
- Removed unnecessary directories from the package.
## [1.2.2] - 2019-08-20
### Fixed
- Fixed issue where fields for custom clips were not responding to Add Key commands. (1174416)
- Fixed issue where a different track's bound GameObject is highlighted when clicking a track's bound GameObject box. (1141836)
- Fixed issue where a clip locks to the playhead's position when moving it. (1157280)
## [1.2.1] - 2019-08-01
### Fixed
- Fixed appearance of a selected clip's border.
- Fixed non-transform properties from AnimationClips not being correctly put into preview mode when the avatar root does not contain the animator component. ([1162334](https://issuetracker.unity3d.com/product/unity/issues/guid/1162334/))
- Fixed an issue where the context menu for inline curves keys would not open on MacOS. ([1158584](https://issuetracker.unity3d.com/product/unity/issues/guid/1158584/))
- Fixed recording state being incorrect after toggling preview mode (1146551)
- Fixed copying clips without ExposedReferences causing the scene to dirty (1144469)
## [1.2.0] - 2019-07-16
*Compatible with Unity 2019.3*
### Added
- Added ILayerable interface. Implementing this interface on a custom track will enable support for multiple layers, similar to the AnimationTracks override tracks.
- Added "Pan" autoscrolling option in the Timeline window.
- Enabled rectangle tool for inline curves.
### Changed
- Scrolling horizontally with the mouse wheel or trackpad now pans the timeline view horizontally, instead of zooming.
- Scrolling vertically with the mouse wheel or trackpad on the track headers or on the vertical scroll bar now pans the timeline view vertically, instead of zooming.
### Fixed
- Fixed an issue causing info text to overlap when displaying multiple lines (1150863).
- Fixed duration mode not reverting from "Fixed Length" to "Based On Clips" properly. (1154034)
- Fixed playrange markers being drawn over horizontal scrollbar (1156023)
- Fixed an issue where a hotkey does not autofit all when Marker is present (1158704)
- Fixed an issue where an exception was thrown when overwriting a Signal Asset through the Signal Emitter inspector. (1152202)
- Fixed Control Tracks not updating instances when source prefab change. (case 1158592)
- An exception will be thrown when calling TrackAsset.CreateMarker() with a marker that implements INotification if the track does not support notifications. (1150248)
- Fixed preview mode being reenabled when warnings change on tracks. (case 1151381)
- Fixed minimum clip duration to be frame aligned. (case 1156602)
- Fixed playhead being moved when applying undo while recording.(case 1154802)
- Fixed warnings about localEulerAnglesRaw when using RectTransform. (case 1151100)
- Fixed precision error on the duration of infinite tracks. (case 1156723)
- Fixed issue where two GatherProperties call were made when switching between two PlayableDirectors. (1159036)
- Fixed issue where inspectors for clips, tracks and markers would get incorrectly displayed when no Timeline Window is opened. (1158242, 1158283)
- Fixed issue with clip connectors that were incorrectly drawn when the timeline was panned or zoomed. (1141960)
- Fixed issue where evaluating a Playable Graph inside a Notification Receiver would cause an infinite recursion. ([1149930](https://issuetracker.unity3d.com/product/unity/issues/guid/1149930/))
- Fixed Trim and Move operations to ensure playable duration is updated upon completion. ([1151894](https://issuetracker.unity3d.com/product/unity/issues/guid/1151894/))
- Fixed options menu icon that was blurry on high-dpi screens. (1154623)
- Track binding field is now larger. (1153446)
- Fixed issue where an empty Timeline window would create new objects on each repaint. (1142894)
- Fixed an issue causing info text to overlap when displaying multiple lines (when trimming + time scaling, for example). (1150863)
- Fixed duration mode not reverting from "Fixed Length" to "Based On Clips" properly. ([1154034](https://issuetracker.unity3d.com/product/unity/issues/guid/1154034/))
- Prevented the PlayableGraph from being created twice when playing a timeline in play mode with the Timeline window opened. (1147247)
- Fixed issue where an exception was thrown when clicking on a SignalEmitter with the Timeline window in asset mode. (1146261)
- A timeline will now be played correctly when building a player with Mono and Managed Stripping Level set higher than Low. ([1133182](https://issuetracker.unity3d.com/product/unity/issues/guid/1133182/))
- The Signal Asset creation dialog will no longer throw exceptions when canceled on macOS. ([1141959](https://issuetracker.unity3d.com/product/unity/issues/guid/1141959/))
- Fixed issue where the Emit Signal property on a Signal Emitter would not get saved correctly. ([1148709](https://issuetracker.unity3d.com/product/unity/issues/guid/1148709/))
- Fixed issue where a Signal Emitter placed at the start of a timeline would be fired twice. ([1149653](https://issuetracker.unity3d.com/product/unity/issues/guid/1149653/))
- Fixed record button state not updating when offset modes are changed. ([1142747](https://issuetracker.unity3d.com/product/unity/issues/guid/1142747/))
- Cleared invalid assets from the Timeline Clipboard when going into or out of PlayMode. (1144473)
- Copying a Control Clip during play mode no longer throws exceptions. (1141581)
- Going to Play Mode while inspecting a Track Asset will no longer throw exceptions. (1141958)
- Resizing Timeline's window no longer affects the zoom value. ([1147150](https://issuetracker.unity3d.com/product/unity/issues/guid/1147150/))
- Snap relaxing now responds to Command on Mac, instead of Control. (1149144)
- Clips will no longer randomly disappear when showing or hiding inline curves. (1141661)
- The global/local time referential button will no longer be shown for a top-level timeline. (1080872)
- Playhead will not be drawn above the bottom scrollbar anymore. (1134016)
- Fixed moving a marker on an Infinite Track will keep the track in infinite mode (1141190)
- Fixed zooming in/out will keep the padding at the beginning of the timeline (1030689)
- Fixed marker UI is the same color and size on infinite track (1139370)
- Fixed Disable the possibility to add Markers to tracks of a Timeline that is ReadOnly (1134463)
- Fixed wrong context menu being shown when right-clicking a marker (1133592)
- Fixed creation of override track to work with multiselection (1133592)
## [1.1.0] - 2019-02-14
*Compatible with Unity 2019.2*
### Added
- ClipEditor, TrackEditor and MarkerEditor classes users can derive from to control visual appearance of custom timeline clips, tracks and markers using the CustomTimelineEditor attribute.
- ClipEditor.GetSubTimelines to allow user created clips that support sub-timelines in editor
- TimelineEditor.selectedClip and TimelineEditor.selectedClips to set and retrieve the currently selected timeline clips
- IPropertyCollector.AddFromName override that takes a component.
- Warning icons to SignalEmitters when they do not reference an asset
- Ability to mute/unmute a Group Track.
- Mute/Unmute only selected track command added for tracks with multiple layers.
- Animate-able Properties on Tracks and Clips can now be edited through inline curves.
- Added loop override on AnimationTrack clips (1140766)
- ReadOnly/Source Control Lock support for Timeline Scene
### Changed
- Control Track display to show a particle system icon when particle systems are being controlled
- Animate-able Properties for clips are no longer edited using by "recording"; they are edited through the inline curves just like tracks.
- AudioTrack properties can now be animated through inline curves.
- Changed Marker show/hide to be undoable. Hide will also unselect markers. (1124661)
- Changed SignalReceivers show their enabled state in the inspector. (1131163)
- Changed Track Context Menu to show "Add Signal Emitter" at the top of the list of Marker commands. (1131166)
- Moved "Add Signal Emitter" and "Add Signal Emitter From Asset" commands out of their sub-menu. (1131166)
### Fixed
- Fixed markers being drawn outside their pane. (1124381)
- Fixed non-public tracks not being recognized by the Timeline Editor. (1122803)
- Fixed keyboard shortcuts for _Frame All_ (default: A) and _Frame Selected_ (default: F) to also apply horizontally ([1126623](https://issuetracker.unity3d.com/product/unity/issues/guid/1126623/))
- Fixed recording getting disabled when selecting a different GameObject while the Timeline Window is not locked. (1123119)
- Fixed time sync between Animation and Timeline windows when clips have non-default timescale or clip-in values. ([930909](https://issuetracker.unity3d.com/product/unity/issues/guid/930909/))
- Fixed animation window link not releasing when deleting the timeline asset. (1127425)
- Fixed an exception being raised when selecting both a Track marker and a Timeline marker at the same time. ([1113006](https://issuetracker.unity3d.com/product/unity/issues/guid/1113006/))
- Fixed the header marker area will so it no longer opens its context menu if it's hidden. (1124351)
- Fixed Signal emitters to show the Signals list when created on override tracks. (1102913)
- Fixed a crash on IL2CPP platforms when the VideoPlayer component is not used. (1129572)
- Fixed Timeline Duration changes in editor not being undoable. (1109279)
- Fixed _Match Offsets_ commands causing improper animation defaults to be applied. (911678)
- Fixed Timeline Inspectors leaving _EditorGUI.showMixedValue_ in the wrong state. ([1123895](https://issuetracker.unity3d.com/product/unity/issues/guid/1123895/))
- Fixed issue where performing undo after moving items on multiple tracks would not undo some items. (1131071)
- Fixed cog icon in the Signal Receiver inspector being blurry. (1130320)
- Fixed Timeline marker track hamburger icon not being centered vertically. (1131112)
- Fixed detection of signal receivers when track is in a group. (1131811)
- Fixed exception being thrown when deleting Signal entries. (1131065)
- Fixed Markers blocking against Clips when moving both Clips and Markers in Ripple mode. (1102594)
- Fixed NullReferenceException being thrown when muting an empty marker track. (1131106)
- Fixed SignalEmitter Inspector losing the Receiver UI when it is locked and another object is selected. (1116041)
- Fixed Marker and Clip appearing to be allowed to move to another track in Ripple mode. (1131123)
- Fixed issue where the Signal Emitter inspector did not show the Signal Receiver UI when placed on the timeline marker track. (1131811)
- Fixed Replace mode not drawing clips when moved together with a Marker. (1132605)
- Fixed inline curves to retain their state when performing undo/redo or keying from the inspector. ([1125443](https://issuetracker.unity3d.com/product/unity/issues/guid/1125443))
- Fixed an issue preventing Timeline from entering preview mode when an Audio Track is present an a full assembly reload is performed. (1132243)
- Fixed an issue where the Marker context menu would show a superfluous line at the bottom. (1132662)
- Fixed an issue preventing Timeline asset to be removed from a locked Timeline Window when a new scene is loaded. (1135073)
- Fixed EaseIn/Out shortcut for clips
## [1.0.0] - 2019-01-28
*Compatible with Unity 2019.1*
### Added
- This is the first release of Timeline, as a Package
- Added API calls to access all AnimationClips used by Timeline.
- Added support in the runtime API to Animate Properties used by template-style PlayableBehaviours used as Mixers.
- Added Markers. Markers are abstract types that represent a single point in time.
- Added Signal Emitters and Signal Assets. Signal Emitters are markers that send a notification, indicated by a SignalAsset, to a GameObject indicating an event has occurred during playback of the Timeline.
- Added Signal Receiver Components. Signal Receivers are MonoBehaviour that listen for Signals from Timeline and respond by invoking UnityEvents.
- Added Signal Tracks. Signal Tracks are Timeline Tracks that are used only for Signal Emitters.
### Fixed
- Signal Receiver will no longer throw exceptions when its inspector is locked ([1114526](https://issuetracker.unity3d.com/product/unity/issues/guid/1114526/))
- Context menu operations will now be applied on all selected tracks (1089820)
- Clip edit mode clutch keys will not get stuck when holding multiple keys at the same time (1097216)
- Marker inspector will be disabled when the marker is collapsed (1102860)
- Clip inspector will no longer throw exceptions when changing values when the inspector is locked (1115984)
- Fixed appearance of muted tracks (1018643)
- Fixed multiple issues where clips and markers were selectable when located under the time ruler and the marker header track ([1117925](https://issuetracker.unity3d.com/product/unity/issues/guid/1117925/), 1102598)
- A marker aligned with the edge of a clip is now easier to select (1102591)
- Changed behaviour of the Timeline Window to apply modifications immediately during Playmode ([922846](https://issuetracker.unity3d.com/product/unity/issues/guid/922846/), 1111908)
- PlayableDirector.played event is now called after entering or exiting Playmode ([1088918](https://issuetracker.unity3d.com/product/unity/issues/guid/1088918/))
- Undoing a paste track operation in a group will no longer corrupt the timeline (1116052)
- The correct context menu will now be displayed on the marker header track (1120857)
- Fixed an issue where a circular reference warning appeared in the Control Clip inspector even if there was no circular reference (1116520)
- Fixed preview mode when animation clips with root curves are used (case 1116297, case 1116007)
- Added option to disable foot IK on animation playable assets (case 1115652)
- Fixed unevaluated animation tracks causing default pose (case 1109118)
- Fixed drawing of Group Tracks when header is off-screen (case 876340)
- Fixed drag and drop of objects inside a group being inserted outside (case 1011381, case 1014774)

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using UnityEditor.ShortcutManagement;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Timeline;
namespace DocCodeExamples
{
class ActionExamples_HideAPI
{
#region declare-sampleClipAction
[MenuEntry("Custom Actions/Sample clip Action")]
public class SampleClipAction : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clip)
{
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TimelineClip> items)
{
Debug.Log("Test Action");
return true;
}
[TimelineShortcut("SampleClipAction", KeyCode.K)]
public static void HandleShortCut(ShortcutArguments args)
{
Invoker.InvokeWithSelectedClips<SampleClipAction>();
}
}
#endregion
#region declare-sampleMarkerAction
[MenuEntry("Custom Actions/Sample marker Action")]
public class SampleMarkerAction : MarkerAction
{
public override ActionValidity Validate(IEnumerable<IMarker> markers)
{
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<IMarker> items)
{
Debug.Log("Test Action");
return true;
}
[TimelineShortcut("SampleMarkerAction", KeyCode.L)]
public static void HandleShortCut(ShortcutArguments args)
{
Invoker.InvokeWithSelectedMarkers<SampleMarkerAction>();
}
}
#endregion
#region declare-sampleTrackAction
[MenuEntry("Custom Actions/Sample track Action")]
public class SampleTrackAction : TrackAction
{
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
Debug.Log("Test Action");
return true;
}
[TimelineShortcut("SampleTrackAction", KeyCode.H)]
public static void HandleShortCut(ShortcutArguments args)
{
Invoker.InvokeWithSelectedTracks<SampleTrackAction>();
}
}
#endregion
#region declare-sampleTimelineAction
[MenuEntry("Custom Actions/Sample Timeline Action")]
public class SampleTimelineAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context)
{
return ActionValidity.Valid;
}
public override bool Execute(ActionContext context)
{
Debug.Log("Test Action");
return true;
}
[TimelineShortcut("SampleTimelineAction", KeyCode.Q)]
public static void HandleShortCut(ShortcutArguments args)
{
Invoker.InvokeWithSelected<SampleTimelineAction>();
}
}
#endregion
}
}

View File

@@ -0,0 +1,21 @@
{
"name": "DocCodeExamples",
"rootNamespace": "",
"references": [
"GUID:f06555f75b070af458a003d92f9efb00",
"GUID:02f771204943f4a40949438e873e3eff"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,20 @@
using UnityEditor;
using UnityEditor.Timeline;
using UnityEngine;
namespace DocCodeExamples
{
class MarkerEditorExamples
{
void MarkerRegionExample(MarkerOverlayRegion region)
{
#region declare-trackRegion
GUI.BeginClip(region.trackRegion, -region.trackRegion.min, Vector2.zero, false);
EditorGUI.DrawRect(region.markerRegion, Color.blue);
GUI.EndClip();
#endregion
}
}
}

View File

@@ -0,0 +1,132 @@
using System.Collections.Generic;
using UnityEditor.ShortcutManagement;
using UnityEditor.Timeline;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace DocCodeExamples
{
class TimelineAttributesExamples_HideAPI
{
#region declare-sampleTrackBindingAttr
[TrackBindingType(typeof(Light), TrackBindingFlags.AllowCreateComponent)]
public class LightTrack : TrackAsset {}
#endregion
#region declare-menuEntryAttribute
[MenuEntry("Simple Menu Action")]
class SimpleMenuAction : TimelineAction
{
public override ActionValidity Validate(ActionContext actionContext)
{
return ActionValidity.Valid;
}
public override bool Execute(ActionContext actionContext)
{
return true;
}
}
[MenuEntry("Menu Action with priority", 9999)]
class MenuActionWithPriority : TimelineAction
{
public override ActionValidity Validate(ActionContext actionContext)
{
return ActionValidity.Valid;
}
public override bool Execute(ActionContext actionContext)
{
return true;
}
}
[MenuEntry("My Menu/Menu Action inside submenu")]
class MenuActionInsideSubMenu : TimelineAction
{
public override ActionValidity Validate(ActionContext actionContext)
{
return ActionValidity.Valid;
}
public override bool Execute(ActionContext actionContext)
{
return true;
}
}
#endregion
#region declare-timelineShortcutAttr
public class ShortcutAction : TimelineAction
{
public override ActionValidity Validate(ActionContext _)
{
return ActionValidity.Valid;
}
public override bool Execute(ActionContext _)
{
Debug.Log("Action executed.");
return true;
}
[TimelineShortcut("Test Action", KeyCode.K, ShortcutModifiers.Shift | ShortcutModifiers.Alt)]
public static void HandleShortCut(ShortcutArguments args)
{
Invoker.InvokeWithSelected<ShortcutAction>();
}
}
#endregion
#region declare-applyDefaultUndoAttr
[ApplyDefaultUndo]
public class SetNameToTypeAction : TrackAction
{
public override ActionValidity Validate(IEnumerable<TrackAsset> items)
{
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TrackAsset> items)
{
foreach (TrackAsset track in items)
track.name = track.GetType().Name;
return true;
}
}
#endregion
#region declare-customStyleMarkerAttr
[CustomStyle("MyStyle")]
public class MyMarker : UnityEngine.Timeline.Marker {}
#endregion
#region declare-customTimelineEditorAttr
[CustomTimelineEditor(typeof(MyCustomClip))]
class MyCustomClipEditor : ClipEditor {}
#endregion
class MyCustomClip : PlayableAsset
{
public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
{
return Playable.Null;
}
}
}
}

View File

@@ -0,0 +1,16 @@
using UnityEditor.Timeline;
namespace DocCodeExamples
{
class TimelineEditorExamples_HideAPI
{
void RefreshReasonExample()
{
#region declare-refreshReason
TimelineEditor.Refresh(RefreshReason.ContentsModified | RefreshReason.SceneNeedsUpdate);
#endregion
}
}
}

View File

@@ -0,0 +1,17 @@
using UnityEngine;
using UnityEngine.Timeline;
namespace DocCodeExamples
{
class TrackAssetExamples_HideAPI
{
#region declare-trackAssetExample
[TrackColor(1, 0, 0)]
[TrackBindingType(typeof(Animator))]
[TrackClipType(typeof(AnimationClip))]
public class CustomAnimationTrack : TrackAsset {}
#endregion
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Action context to be used by actions.
/// </summary>
/// <seealso cref="Invoker"/>
/// <seealso cref="TimelineAction"/>
public struct ActionContext
{
IEnumerable<TrackAsset> m_Tracks;
IEnumerable<TimelineClip> m_Clips;
IEnumerable<IMarker> m_Markers;
/// <summary>
/// The Timeline asset that is currently opened in the Timeline window.
/// </summary>
public TimelineAsset timeline;
/// <summary>
/// The PlayableDirector that is used to play the current Timeline asset.
/// </summary>
public PlayableDirector director;
/// <summary>
/// Time based on the position of the cursor on the timeline (in seconds).
/// null if the time is not available (in case of a shortcut for example).
/// </summary>
public double? invocationTime;
/// <summary>
/// Tracks that will be used by the actions.
/// </summary>
public IEnumerable<TrackAsset> tracks
{
get => m_Tracks ?? Enumerable.Empty<TrackAsset>();
set => m_Tracks = value;
}
/// <summary>
/// Clips that will be used by the actions.
/// </summary>
public IEnumerable<TimelineClip> clips
{
get => m_Clips ?? Enumerable.Empty<TimelineClip>();
set => m_Clips = value;
}
/// <summary>
/// Markers that will be used by the actions.
/// </summary>
public IEnumerable<IMarker> markers
{
get => m_Markers ?? Enumerable.Empty<IMarker>();
set => m_Markers = value;
}
}
}

View File

@@ -0,0 +1,306 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Actions
{
static class ActionManager
{
static bool s_ShowActionTriggeredByShortcut = false;
public static readonly IReadOnlyList<TimelineAction> TimelineActions = InstantiateClassesOfType<TimelineAction>();
public static readonly IReadOnlyList<ClipAction> ClipActions = InstantiateClassesOfType<ClipAction>();
public static readonly IReadOnlyList<TrackAction> TrackActions = InstantiateClassesOfType<TrackAction>();
public static readonly IReadOnlyList<MarkerAction> MarkerActions = InstantiateClassesOfType<MarkerAction>();
public static readonly IReadOnlyList<TimelineAction> TimelineActionsWithShortcuts = ActionsWithShortCuts(TimelineActions);
public static readonly IReadOnlyList<ClipAction> ClipActionsWithShortcuts = ActionsWithShortCuts(ClipActions);
public static readonly IReadOnlyList<TrackAction> TrackActionsWithShortcuts = ActionsWithShortCuts(TrackActions);
public static readonly IReadOnlyList<MarkerAction> MarkerActionsWithShortcuts = ActionsWithShortCuts(MarkerActions);
public static readonly HashSet<Type> ActionsWithAutoUndo = TypesWithAttribute<ApplyDefaultUndoAttribute>();
public static TU GetCachedAction<T, TU>(this IReadOnlyList<TU> list) where T : TU
{
return list.FirstOrDefault(x => x.GetType() == typeof(T));
}
public static void GetMenuEntries(IReadOnlyList<TimelineAction> actions, Vector2? mousePos, List<MenuActionItem> menuItems)
{
var globalContext = TimelineEditor.CurrentContext(mousePos);
foreach (var action in actions)
{
try
{
BuildMenu(action, globalContext, menuItems);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
}
public static void GetMenuEntries(IReadOnlyList<TrackAction> actions, List<MenuActionItem> menuItems)
{
var tracks = SelectionManager.SelectedTracks();
if (!tracks.Any())
return;
foreach (var action in actions)
{
try
{
BuildMenu(action, tracks, menuItems);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
}
public static void GetMenuEntries(IReadOnlyList<ClipAction> actions, List<MenuActionItem> menuItems)
{
var clips = SelectionManager.SelectedClips();
bool any = clips.Any();
if (!clips.Any())
return;
foreach (var action in actions)
{
try
{
if (action is EditSubTimeline editSubTimelineAction)
editSubTimelineAction.AddMenuItem(menuItems);
else if (any)
BuildMenu(action, clips, menuItems);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
}
public static void GetMenuEntries(IReadOnlyList<MarkerAction> actions, List<MenuActionItem> menuItems)
{
var markers = SelectionManager.SelectedMarkers();
if (!markers.Any())
return;
foreach (var action in actions)
{
try
{
BuildMenu(action, markers, menuItems);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
}
static void BuildMenu(TimelineAction action, ActionContext context, List<MenuActionItem> menuItems)
{
BuildMenu(action, action.Validate(context), () => ExecuteTimelineAction(action, context), menuItems);
}
static void BuildMenu(TrackAction action, IEnumerable<TrackAsset> tracks, List<MenuActionItem> menuItems)
{
BuildMenu(action, action.Validate(tracks), () => ExecuteTrackAction(action, tracks), menuItems);
}
static void BuildMenu(ClipAction action, IEnumerable<TimelineClip> clips, List<MenuActionItem> menuItems)
{
BuildMenu(action, action.Validate(clips), () => ExecuteClipAction(action, clips), menuItems);
}
static void BuildMenu(MarkerAction action, IEnumerable<IMarker> markers, List<MenuActionItem> menuItems)
{
BuildMenu(action, action.Validate(markers), () => ExecuteMarkerAction(action, markers), menuItems);
}
static void BuildMenu(IAction action, ActionValidity validity, GenericMenu.MenuFunction executeFunction, List<MenuActionItem> menuItems)
{
var menuAttribute = action.GetType().GetCustomAttribute<MenuEntryAttribute>(false);
if (menuAttribute == null)
return;
if (validity == ActionValidity.NotApplicable)
return;
var menuActionItem = new MenuActionItem
{
state = validity,
entryName = action.GetMenuEntryName(),
priority = menuAttribute.priority,
category = menuAttribute.subMenuPath,
isActiveInMode = action.IsActionActiveInMode(TimelineWindow.instance.currentMode.mode),
shortCut = action.GetShortcut(),
callback = executeFunction,
isChecked = action.IsChecked()
};
menuItems.Add(menuActionItem);
}
internal static void BuildMenu(GenericMenu menu, List<MenuActionItem> items)
{
// sorted the outer menu by priority, then sort the innermenu by priority
var sortedItems =
items.GroupBy(x => string.IsNullOrEmpty(x.category) ? x.entryName : x.category).OrderBy(x => x.Min(y => y.priority)).SelectMany(x => x.OrderBy(z => z.priority));
int lastPriority = Int32.MinValue;
string lastCategory = string.Empty;
foreach (var s in sortedItems)
{
if (s.state == ActionValidity.NotApplicable)
continue;
var priority = s.priority;
if (lastPriority != int.MinValue && priority / MenuPriority.separatorAt > lastPriority / MenuPriority.separatorAt)
{
string path = string.Empty;
if (lastCategory == s.category)
path = s.category;
menu.AddSeparator(path);
}
lastPriority = priority;
lastCategory = s.category;
string entry = s.category + s.entryName;
if (!string.IsNullOrEmpty(s.shortCut))
entry += " " + s.shortCut;
if (s.state == ActionValidity.Valid && s.isActiveInMode)
menu.AddItem(new GUIContent(entry), s.isChecked, s.callback);
else
menu.AddDisabledItem(new GUIContent(entry), s.isChecked);
}
}
public static bool HandleShortcut(Event evt)
{
if (EditorGUI.IsEditingTextField())
return false;
return HandleShortcut(evt, TimelineActionsWithShortcuts, (x) => ExecuteTimelineAction(x, TimelineEditor.CurrentContext())) ||
HandleShortcut(evt, ClipActionsWithShortcuts, (x => ExecuteClipAction(x, SelectionManager.SelectedClips()))) ||
HandleShortcut(evt, TrackActionsWithShortcuts, (x => ExecuteTrackAction(x, SelectionManager.SelectedTracks()))) ||
HandleShortcut(evt, MarkerActionsWithShortcuts, (x => ExecuteMarkerAction(x, SelectionManager.SelectedMarkers())));
}
public static bool HandleShortcut<T>(Event evt, IReadOnlyList<T> actions, Func<T, bool> invoke) where T : class, IAction
{
for (int i = 0; i < actions.Count; i++)
{
var action = actions[i];
var attr = action.GetType().GetCustomAttributes(typeof(ShortcutAttribute), true);
foreach (ShortcutAttribute shortcut in attr)
{
if (shortcut.MatchesEvent(evt))
{
if (s_ShowActionTriggeredByShortcut)
Debug.Log(action.GetType().Name);
if (!action.IsActionActiveInMode(TimelineWindow.instance.currentMode.mode))
continue;
if (invoke(action))
return true;
}
}
}
return false;
}
public static bool ExecuteTimelineAction(TimelineAction timelineAction, ActionContext context)
{
if (timelineAction.Validate(context) == ActionValidity.Valid)
{
if (timelineAction.HasAutoUndo())
UndoExtensions.RegisterContext(context, timelineAction.GetUndoName());
return timelineAction.Execute(context);
}
return false;
}
public static bool ExecuteTrackAction(TrackAction trackAction, IEnumerable<TrackAsset> tracks)
{
if (tracks != null && tracks.Any() && trackAction.Validate(tracks) == ActionValidity.Valid)
{
if (trackAction.HasAutoUndo())
UndoExtensions.RegisterTracks(tracks, trackAction.GetUndoName());
return trackAction.Execute(tracks);
}
return false;
}
public static bool ExecuteClipAction(ClipAction clipAction, IEnumerable<TimelineClip> clips)
{
if (clips != null && clips.Any() && clipAction.Validate(clips) == ActionValidity.Valid)
{
if (clipAction.HasAutoUndo())
UndoExtensions.RegisterClips(clips, clipAction.GetUndoName());
return clipAction.Execute(clips);
}
return false;
}
public static bool ExecuteMarkerAction(MarkerAction markerAction, IEnumerable<IMarker> markers)
{
if (markers != null && markers.Any() && markerAction.Validate(markers) == ActionValidity.Valid)
{
if (markerAction.HasAutoUndo())
UndoExtensions.RegisterMarkers(markers, markerAction.GetUndoName());
return markerAction.Execute(markers);
}
return false;
}
static List<T> InstantiateClassesOfType<T>() where T : class
{
var typeCollection = TypeCache.GetTypesDerivedFrom(typeof(T));
var list = new List<T>(typeCollection.Count);
for (int i = 0; i < typeCollection.Count; i++)
{
if (typeCollection[i].IsAbstract || typeCollection[i].IsGenericType)
continue;
if (typeCollection[i].GetConstructor(Type.EmptyTypes) == null)
{
Debug.LogWarning($"{typeCollection[i].FullName} requires a default constructor to be automatically instantiated by Timeline");
continue;
}
list.Add((T)Activator.CreateInstance(typeCollection[i]));
}
return list;
}
static List<T> ActionsWithShortCuts<T>(IReadOnlyList<T> list)
{
return list.Where(x => x.GetType().GetCustomAttributes(typeof(ShortcutAttribute), true).Length > 0).ToList();
}
static HashSet<System.Type> TypesWithAttribute<T>() where T : Attribute
{
var hashSet = new HashSet<System.Type>();
var typeCollection = TypeCache.GetTypesWithAttribute(typeof(T));
for (int i = 0; i < typeCollection.Count; i++)
{
hashSet.Add(typeCollection[i]);
}
return hashSet;
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Base class for a clip action.
/// Inherit from this class to make an action that would react on selected clips after a menu click and/or a key shortcut.
/// </summary>
/// <example>
/// Simple Clip Action example (with context menu and shortcut support).
/// <code source="../../DocCodeExamples/ActionExamples.cs" region="declare-sampleClipAction" title="SampleClipAction"/>
/// </example>
/// <remarks>
/// To add an action as a menu item in the Timeline context menu, add <see cref="MenuEntryAttribute"/> on the action class.
/// To make an action to react to a shortcut, use the Shortcut Manager API with <see cref="TimelineShortcutAttribute"/>.
/// <seealso cref="UnityEditor.ShortcutManagement.ShortcutAttribute"/>
/// </remarks>
[ActiveInMode(TimelineModes.Default)]
public abstract class ClipAction : IAction
{
/// <summary>
/// Execute the action based on clips.
/// </summary>
/// <param name="clips">clips that the action will act on.</param>
/// <returns>Returns true if the action has been correctly executed, false otherwise.</returns>
public abstract bool Execute(IEnumerable<TimelineClip> clips);
/// <summary>
/// Defines the validity of an Action for a given set of clips.
/// </summary>
/// <param name="clips">The clips that the action will act on.</param>
/// <returns>The validity of the set of clips.</returns>
public abstract ActionValidity Validate(IEnumerable<TimelineClip> clips);
}
}

View File

@@ -0,0 +1,392 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
namespace UnityEditor.Timeline
{
[MenuEntry("Edit in Animation Window", MenuPriority.ClipEditActionSection.editInAnimationWindow), UsedImplicitly]
class EditClipInAnimationWindow : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
if (!GetEditableClip(clips, out _, out _))
return ActionValidity.NotApplicable;
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
TimelineClip clip;
AnimationClip clipToEdit;
if (!GetEditableClip(clips, out clip, out clipToEdit))
return false;
GameObject gameObject = null;
if (TimelineEditor.inspectedDirector != null)
gameObject = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, clip.GetParentTrack());
var timeController = TimelineAnimationUtilities.CreateTimeController(clip);
TimelineAnimationUtilities.EditAnimationClipWithTimeController(
clipToEdit, timeController, clip.animationClip != null ? gameObject : null);
return true;
}
private static bool GetEditableClip(IEnumerable<TimelineClip> clips, out TimelineClip clip, out AnimationClip animClip)
{
clip = null;
animClip = null;
if (clips.Count() != 1)
return false;
clip = clips.FirstOrDefault();
if (clip == null)
return false;
if (clip.animationClip != null)
animClip = clip.animationClip;
else if (clip.curves != null && !clip.curves.empty)
animClip = clip.curves;
return animClip != null;
}
}
[MenuEntry("Edit Sub-Timeline", MenuPriority.ClipEditActionSection.editSubTimeline), UsedImplicitly]
class EditSubTimeline : ClipAction
{
private static readonly string MultiItemPrefix = "Edit Sub-Timelines/";
private static readonly string SingleItemPrefix = "Edit ";
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
if (clips == null || clips.Count() != 1 || TimelineEditor.inspectedDirector == null)
return ActionValidity.NotApplicable;
var clip = clips.First();
var directors = TimelineUtility.GetSubTimelines(clip, TimelineEditor.inspectedDirector);
return directors.Any(x => x != null) ? ActionValidity.Valid : ActionValidity.NotApplicable;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
if (Validate(clips) != ActionValidity.Valid) return false;
var clip = clips.First();
var directors = TimelineUtility.GetSubTimelines(clip, TimelineEditor.inspectedDirector);
ExecuteInternal(directors, 0, clip);
return true;
}
static void ExecuteInternal(IList<PlayableDirector> directors, int directorIndex, TimelineClip clip)
{
SelectionManager.Clear();
TimelineWindow.instance.SetCurrentTimeline(directors[directorIndex], clip);
}
internal void AddMenuItem(List<MenuActionItem> menuItems)
{
var clips = TimelineEditor.selectedClips;
if (clips == null || clips.Length != 1)
return;
var mode = TimelineWindow.instance.currentMode.mode;
MenuEntryAttribute menuAttribute = GetType().GetCustomAttributes(typeof(MenuEntryAttribute), false).OfType<MenuEntryAttribute>().FirstOrDefault();
var menuItem = new MenuActionItem()
{
category = menuAttribute.subMenuPath ?? string.Empty,
entryName = menuAttribute.name,
isActiveInMode = this.IsActionActiveInMode(mode),
priority = menuAttribute.priority,
state = Validate(clips),
callback = null
};
var subDirectors = TimelineUtility.GetSubTimelines(clips[0], TimelineEditor.inspectedDirector);
if (subDirectors.Count == 1)
{
menuItem.entryName = SingleItemPrefix + DisplayNameHelper.GetDisplayName(subDirectors[0]);
menuItem.callback = () =>
{
Execute(clips);
};
menuItems.Add(menuItem);
}
else
{
for (int i = 0; i < subDirectors.Count; i++)
{
var index = i;
menuItem.category = MultiItemPrefix;
menuItem.entryName = DisplayNameHelper.GetDisplayName(subDirectors[i]);
menuItem.callback = () =>
{
ExecuteInternal(subDirectors, index, clips[0]);
};
menuItems.Add(menuItem);
}
}
}
}
[MenuEntry("Editing/Trim Start", MenuPriority.ClipActionSection.trimStart)]
[Shortcut(Shortcuts.Clip.trimStart), UsedImplicitly]
class TrimStart : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
return clips.All(x => TimelineEditor.inspectedSequenceTime <= x.start || TimelineEditor.inspectedSequenceTime >= x.start + x.duration) ? ActionValidity.Invalid : ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.TrimStart(clips, TimelineEditor.inspectedSequenceTime);
}
}
[MenuEntry("Editing/Trim End", MenuPriority.ClipActionSection.trimEnd), UsedImplicitly]
[Shortcut(Shortcuts.Clip.trimEnd)]
class TrimEnd : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
return clips.All(x => TimelineEditor.inspectedSequenceTime <= x.start || TimelineEditor.inspectedSequenceTime >= x.start + x.duration) ? ActionValidity.Invalid : ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.TrimEnd(clips, TimelineEditor.inspectedSequenceTime);
}
}
[Shortcut(Shortcuts.Clip.split)]
[MenuEntry("Editing/Split", MenuPriority.ClipActionSection.split), UsedImplicitly]
class Split : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
return clips.All(x => TimelineEditor.inspectedSequenceTime <= x.start || TimelineEditor.inspectedSequenceTime >= x.start + x.duration) ? ActionValidity.Invalid : ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
bool success = ClipModifier.Split(clips, TimelineEditor.inspectedSequenceTime, TimelineEditor.inspectedDirector);
if (success)
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return success;
}
}
[MenuEntry("Editing/Complete Last Loop", MenuPriority.ClipActionSection.completeLastLoop), UsedImplicitly]
class CompleteLastLoop : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.Any(TimelineHelpers.HasUsableAssetDuration);
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.CompleteLastLoop(clips);
}
}
[MenuEntry("Editing/Trim Last Loop", MenuPriority.ClipActionSection.trimLastLoop), UsedImplicitly]
class TrimLastLoop : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.Any(TimelineHelpers.HasUsableAssetDuration);
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.TrimLastLoop(clips);
}
}
[MenuEntry("Editing/Match Duration", MenuPriority.ClipActionSection.matchDuration), UsedImplicitly]
class MatchDuration : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
return clips.Count() > 1 ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.MatchDuration(clips);
}
}
[MenuEntry("Editing/Double Speed", MenuPriority.ClipActionSection.doubleSpeed), UsedImplicitly]
class DoubleSpeed : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.All(x => x.SupportsSpeedMultiplier());
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.DoubleSpeed(clips);
}
}
[MenuEntry("Editing/Half Speed", MenuPriority.ClipActionSection.halfSpeed), UsedImplicitly]
class HalfSpeed : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.All(x => x.SupportsSpeedMultiplier());
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.HalfSpeed(clips);
}
}
[MenuEntry("Editing/Reset Duration", MenuPriority.ClipActionSection.resetDuration), UsedImplicitly]
class ResetDuration : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.Any(TimelineHelpers.HasUsableAssetDuration);
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.ResetEditing(clips);
}
}
[MenuEntry("Editing/Reset Speed", MenuPriority.ClipActionSection.resetSpeed), UsedImplicitly]
class ResetSpeed : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.All(x => x.SupportsSpeedMultiplier());
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.ResetSpeed(clips);
}
}
[MenuEntry("Editing/Reset All", MenuPriority.ClipActionSection.resetAll), UsedImplicitly]
class ResetAll : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.Any(TimelineHelpers.HasUsableAssetDuration) || clips.All(x => x.SupportsSpeedMultiplier());
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
var speedResult = ClipModifier.ResetSpeed(clips);
var editResult = ClipModifier.ResetEditing(clips);
return speedResult || editResult;
}
}
[MenuEntry("Tile", MenuPriority.ClipActionSection.tile), UsedImplicitly]
class Tile : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
return clips.Count() > 1 ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.Tile(clips);
}
}
[MenuEntry("Find Source Asset", MenuPriority.ClipActionSection.findSourceAsset), UsedImplicitly]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class FindSourceAsset : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
if (clips.Count() > 1)
return ActionValidity.Invalid;
if (GetUnderlyingAsset(clips.First()) == null)
return ActionValidity.Invalid;
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
EditorGUIUtility.PingObject(GetUnderlyingAsset(clips.First()));
return true;
}
private static UnityEngine.Object GetExternalPlayableAsset(TimelineClip clip)
{
if (clip.asset == null)
return null;
if ((clip.asset.hideFlags & HideFlags.HideInHierarchy) != 0)
return null;
return clip.asset;
}
private static UnityEngine.Object GetUnderlyingAsset(TimelineClip clip)
{
var asset = clip.asset as ScriptableObject;
if (asset == null)
return null;
var fields = ObjectReferenceField.FindObjectReferences(asset.GetType());
if (fields.Length == 0)
return GetExternalPlayableAsset(clip);
// Find the first non-null field
foreach (var field in fields)
{
// skip scene refs in asset mode
if (TimelineEditor.inspectedDirector == null && field.isSceneReference)
continue;
var obj = field.Find(asset, TimelineEditor.inspectedDirector);
if (obj != null)
return obj;
}
return GetExternalPlayableAsset(clip);
}
}
class CopyClipsToClipboard : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips) => ActionValidity.Valid;
public override bool Execute(IEnumerable<TimelineClip> clips)
{
TimelineEditor.clipboard.CopyItems(clips.ToItems());
return true;
}
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using UnityEditor.ShortcutManagement;
using UnityEngine;
namespace UnityEditor.Timeline.Actions
{
/// interface indicating an Action class
interface IAction {}
/// extension methods for IActions
static class ActionExtensions
{
const string kActionPostFix = "Action";
public static string GetUndoName(this IAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
var attr = action.GetType().GetCustomAttribute<ApplyDefaultUndoAttribute>(false);
if (attr != null && !string.IsNullOrWhiteSpace(attr.UndoTitle))
return attr.UndoTitle;
return action.GetDisplayName();
}
public static string GetMenuEntryName(this IAction action)
{
var menuAction = action as IMenuName;
if (menuAction != null && !string.IsNullOrWhiteSpace(menuAction.menuName))
return menuAction.menuName;
var attr = action.GetType().GetCustomAttribute<MenuEntryAttribute>(false);
if (attr != null && !string.IsNullOrWhiteSpace(attr.name))
return attr.name;
return action.GetDisplayName();
}
public static string GetDisplayName(this IAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
var attr = action.GetType().GetCustomAttribute<DisplayNameAttribute>(false);
if (attr != null && !string.IsNullOrEmpty(attr.DisplayName))
return attr.DisplayName;
var name = action.GetType().Name;
if (name.EndsWith(kActionPostFix))
return ObjectNames.NicifyVariableName(name.Substring(0, name.Length - kActionPostFix.Length));
return ObjectNames.NicifyVariableName(name);
}
public static bool HasAutoUndo(this IAction action)
{
return action != null && ActionManager.ActionsWithAutoUndo.Contains(action.GetType());
}
public static bool IsChecked(this IAction action)
{
return (action is IMenuChecked menuAction) && menuAction.isChecked;
}
public static bool IsActionActiveInMode(this IAction action, TimelineModes mode)
{
var attr = action.GetType().GetCustomAttribute<ActiveInModeAttribute>(true);
return attr != null && (attr.modes & mode) != 0;
}
public static string GetShortcut(this IAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
var shortcutAttribute = GetShortcutAttributeForAction(action);
var shortCut = shortcutAttribute == null ? string.Empty : shortcutAttribute.GetMenuShortcut();
if (string.IsNullOrWhiteSpace(shortCut))
{
//Check if there is a static method with attribute
var customShortcutMethod = action.GetType().GetMethods().FirstOrDefault(m => m.GetCustomAttribute<TimelineShortcutAttribute>(true) != null);
if (customShortcutMethod != null)
{
var shortcutId = customShortcutMethod.GetCustomAttribute<TimelineShortcutAttribute>(true).identifier;
var shortcut = ShortcutIntegration.instance.directory.FindShortcutEntry(shortcutId);
if (shortcut != null && shortcut.combinations.Any())
shortCut = KeyCombination.SequenceToMenuString(shortcut.combinations);
}
}
return shortCut;
}
static ShortcutAttribute GetShortcutAttributeForAction(this IAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
var shortcutAttributes = action.GetType()
.GetCustomAttributes(typeof(ShortcutAttribute), true)
.Cast<ShortcutAttribute>();
foreach (var shortcutAttribute in shortcutAttributes)
{
if (shortcutAttribute is ShortcutPlatformOverrideAttribute shortcutOverride)
{
if (shortcutOverride.MatchesCurrentPlatform())
return shortcutOverride;
}
else
{
return shortcutAttribute;
}
}
return null;
}
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace UnityEditor.Timeline
{
interface IMenuChecked
{
bool isChecked { get; }
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace UnityEditor.Timeline
{
interface IMenuName
{
string menuName { get; }
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Class containing methods to invoke actions.
/// </summary>
public static class Invoker
{
/// <summary>
/// Execute a given action with a context parameter.
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <param name="context">Context for the action.</param>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool Invoke<T>(this ActionContext context) where T : TimelineAction
{
var action = ActionManager.TimelineActions.GetCachedAction<T, TimelineAction>();
return ActionManager.ExecuteTimelineAction(action, context);
}
/// <summary>
/// Execute a given action with tracks
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <param name="tracks">Tracks that the action will act on.</param>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool Invoke<T>(this IEnumerable<TrackAsset> tracks) where T : TrackAction
{
var action = ActionManager.TrackActions.GetCachedAction<T, TrackAction>();
return ActionManager.ExecuteTrackAction(action, tracks);
}
/// <summary>
/// Execute a given action with clips
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <param name="clips">Clips that the action will act on.</param>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool Invoke<T>(this IEnumerable<TimelineClip> clips) where T : ClipAction
{
var action = ActionManager.ClipActions.GetCachedAction<T, ClipAction>();
return ActionManager.ExecuteClipAction(action, clips);
}
/// <summary>
/// Execute a given action with markers
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <param name="markers">Markers that the action will act on.</param>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool Invoke<T>(this IEnumerable<IMarker> markers) where T : MarkerAction
{
var action = ActionManager.MarkerActions.GetCachedAction<T, MarkerAction>();
return ActionManager.ExecuteMarkerAction(action, markers);
}
/// <summary>
/// Execute a given timeline action with the selected clips, tracks and markers.
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool InvokeWithSelected<T>() where T : TimelineAction
{
return Invoke<T>(TimelineEditor.CurrentContext());
}
/// <summary>
/// Execute a given clip action with the selected clips.
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool InvokeWithSelectedClips<T>() where T : ClipAction
{
return Invoke<T>(SelectionManager.SelectedClips());
}
/// <summary>
/// Execute a given track action with the selected tracks.
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool InvokeWithSelectedTracks<T>() where T : TrackAction
{
return Invoke<T>(SelectionManager.SelectedTracks());
}
/// <summary>
/// Execute a given marker action with the selected markers.
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool InvokeWithSelectedMarkers<T>() where T : MarkerAction
{
return Invoke<T>(SelectionManager.SelectedMarkers());
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Base class for a marker action.
/// Inherit from this class to make an action that would react on selected markers after a menu click and/or a key shortcut.
/// </summary>
/// <example>
/// Simple track Action example (with context menu and shortcut support).
/// <code source="../../DocCodeExamples/ActionExamples.cs" region="declare-sampleMarkerAction" title="SampleMarkerAction"/>
/// </example>
/// <remarks>
/// To add an action as a menu item in the Timeline context menu, add <see cref="MenuEntryAttribute"/> on the action class.
/// To make an action to react to a shortcut, use the Shortcut Manager API with <see cref="TimelineShortcutAttribute"/>.
/// <seealso cref="UnityEditor.ShortcutManagement.ShortcutAttribute"/>
/// </remarks>
[ActiveInMode(TimelineModes.Default)]
public abstract class MarkerAction : IAction
{
/// <summary>
/// Execute the action.
/// </summary>
/// <param name="markers">Markers that will be used for the action. </param>
/// <returns>true if the action has been executed. false otherwise</returns>
public abstract bool Execute(IEnumerable<IMarker> markers);
/// <summary>
/// Defines the validity of an Action for a given set of markers.
/// </summary>
/// <param name="markers">Markers that will be used for the action. </param>
/// <returns>The validity of the set of markers.</returns>
public abstract ActionValidity Validate(IEnumerable<IMarker> markers);
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[UsedImplicitly]
class CopyMarkersToClipboard : MarkerAction
{
public override ActionValidity Validate(IEnumerable<IMarker> markers) => ActionValidity.Valid;
public override bool Execute(IEnumerable<IMarker> markers)
{
TimelineEditor.clipboard.CopyItems(markers.ToItems());
return true;
}
}
}

View File

@@ -0,0 +1,36 @@
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Indicates the validity of an action for a given data set.
/// </summary>
public enum ActionValidity
{
/// <summary>
/// Action is valid in the provided context.
/// If the action is linked to a menu item, the menu item will be visible.
/// </summary>
Valid,
/// <summary>
/// Action is not applicable in the current context.
/// If the action is linked to a menu item, the menu item will not be shown.
/// </summary>
NotApplicable,
/// <summary>
/// Action is not valid in the current context.
/// If the action is linked to a menu item, the menu item will be shown but grayed out.
/// </summary>
Invalid
}
struct MenuActionItem
{
public string category;
public string entryName;
public string shortCut;
public int priority;
public bool isActiveInMode;
public ActionValidity state;
public bool isChecked;
public GenericMenu.MenuFunction callback;
}
}

View File

@@ -0,0 +1,381 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Timeline;
using Object = UnityEngine.Object;
namespace UnityEditor.Timeline
{
static class SequencerContextMenu
{
static class Styles
{
public static readonly string addItemFromAssetTemplate = L10n.Tr("Add {0} From {1}");
public static readonly string addSingleItemFromAssetTemplate = L10n.Tr("Add From {1}");
public static readonly string addItemTemplate = L10n.Tr("Add {0}");
public static readonly string typeSelectorTemplate = L10n.Tr("Select {0}");
public static readonly string trackGroup = L10n.Tr("Track Group");
public static readonly string trackSubGroup = L10n.Tr("Track Sub-Group");
public static readonly string addTrackLayer = L10n.Tr("Add Layer");
public static readonly string layerName = L10n.Tr("Layer {0}");
}
public static void ShowNewTracksContextMenu(ICollection<TrackAsset> tracks, WindowState state)
{
var menu = new GenericMenu();
List<MenuActionItem> items = new List<MenuActionItem>(100);
BuildNewTracksContextMenu(items, tracks, state);
ActionManager.BuildMenu(menu, items);
menu.ShowAsContext();
}
public static void ShowNewTracksContextMenu(ICollection<TrackAsset> tracks, WindowState state, Rect rect)
{
var menu = new GenericMenu();
List<MenuActionItem> items = new List<MenuActionItem>(100);
BuildNewTracksContextMenu(items, tracks, state);
ActionManager.BuildMenu(menu, items);
menu.DropDown(rect);
}
public static void ShowTrackContextMenu(Vector2? mousePosition)
{
var items = new List<MenuActionItem>();
var menu = new GenericMenu();
BuildTrackContextMenu(items, mousePosition);
ActionManager.BuildMenu(menu, items);
menu.ShowAsContext();
}
public static void ShowItemContextMenu(Vector2 mousePosition)
{
var menu = new GenericMenu();
var items = new List<MenuActionItem>();
BuildItemContextMenu(items, mousePosition);
ActionManager.BuildMenu(menu, items);
menu.ShowAsContext();
}
public static void BuildItemContextMenu(List<MenuActionItem> items, Vector2 mousePosition)
{
ActionManager.GetMenuEntries(ActionManager.TimelineActions, mousePosition, items);
ActionManager.GetMenuEntries(ActionManager.ClipActions, items);
ActionManager.GetMenuEntries(ActionManager.MarkerActions, items);
var clips = TimelineEditor.selectedClips;
if (clips.Length > 0)
AddMarkerMenuCommands(items, clips.Select(c => c.GetParentTrack()).Distinct().ToList(), TimelineHelpers.GetCandidateTime(mousePosition));
}
public static void BuildNewTracksContextMenu(List<MenuActionItem> menuItems, ICollection<TrackAsset> parentTracks, WindowState state, string format = null)
{
if (parentTracks == null)
parentTracks = new TrackAsset[0];
if (string.IsNullOrEmpty(format))
format = "{0}";
// Add Group or SubGroup
var title = string.Format(format, parentTracks.Any(t => t != null) ? Styles.trackSubGroup : Styles.trackGroup);
var menuState = ActionValidity.Valid;
if (state.editSequence.isReadOnly)
menuState = ActionValidity.Invalid;
if (parentTracks.Any() && parentTracks.Any(t => t != null && t.lockedInHierarchy))
menuState = ActionValidity.Invalid;
GenericMenu.MenuFunction command = () =>
{
SelectionManager.Clear();
if (parentTracks.Count == 0)
Selection.Add(TimelineHelpers.CreateTrack<GroupTrack>(null, title));
foreach (var parentTrack in parentTracks)
Selection.Add(TimelineHelpers.CreateTrack<GroupTrack>(parentTrack, title));
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
};
menuItems.Add(
new MenuActionItem()
{
category = string.Empty,
entryName = title,
isActiveInMode = true,
priority = MenuPriority.AddItem.addGroup,
state = menuState,
isChecked = false,
callback = command
}
);
var allTypes = TypeUtility.AllTrackTypes().Where(x => x != typeof(GroupTrack) && !TypeUtility.IsHiddenInMenu(x)).ToList();
int builtInPriority = MenuPriority.AddItem.addTrack;
int customPriority = MenuPriority.AddItem.addCustomTrack;
foreach (var trackType in allTypes)
{
var trackItemType = trackType;
command = () =>
{
SelectionManager.Clear();
if (parentTracks.Count == 0)
SelectionManager.Add(TimelineHelpers.CreateTrack((Type)trackItemType, null));
foreach (var parentTrack in parentTracks)
SelectionManager.Add(TimelineHelpers.CreateTrack((Type)trackItemType, parentTrack));
};
menuItems.Add(
new MenuActionItem()
{
category = TimelineHelpers.GetTrackCategoryName(trackType),
entryName = string.Format(format, TimelineHelpers.GetTrackMenuName(trackItemType)),
isActiveInMode = true,
priority = TypeUtility.IsBuiltIn(trackType) ? builtInPriority++ : customPriority++,
state = menuState,
callback = command
}
);
}
}
public static void BuildTrackContextMenu(List<MenuActionItem> items, Vector2? mousePosition)
{
var tracks = SelectionManager.SelectedTracks().ToArray();
if (tracks.Length == 0)
return;
ActionManager.GetMenuEntries(ActionManager.TimelineActions, mousePosition, items);
ActionManager.GetMenuEntries(ActionManager.TrackActions, items);
AddLayeredTrackCommands(items, tracks);
var first = tracks.First().GetType();
var allTheSame = tracks.All(t => t.GetType() == first);
if (allTheSame)
{
if (first != typeof(GroupTrack))
{
var candidateTime = TimelineHelpers.GetCandidateTime(mousePosition, tracks);
AddClipMenuCommands(items, tracks, candidateTime);
AddMarkerMenuCommands(items, tracks, candidateTime);
}
else
{
BuildNewTracksContextMenu(items, tracks, TimelineWindow.instance.state, Styles.addItemTemplate);
}
}
}
static void AddLayeredTrackCommands(List<MenuActionItem> menuItems, ICollection<TrackAsset> tracks)
{
if (tracks.Count == 0)
return;
var layeredType = tracks.First().GetType();
// animation tracks have a special menu.
if (layeredType == typeof(AnimationTrack))
return;
// must implement ILayerable
if (!typeof(UnityEngine.Timeline.ILayerable).IsAssignableFrom(layeredType))
return;
if (tracks.Any(t => t.GetType() != layeredType))
return;
// only supported on the master track no nesting.
if (tracks.Any(t => t.isSubTrack))
return;
var enabled = tracks.All(t => t != null && !t.lockedInHierarchy) && !TimelineWindow.instance.state.editSequence.isReadOnly;
int priority = MenuPriority.AddTrackMenu.addLayerTrack;
GenericMenu.MenuFunction menuCallback = () =>
{
foreach (var track in tracks)
TimelineHelpers.CreateTrack(layeredType, track, string.Format(Styles.layerName, track.GetChildTracks().Count() + 1));
};
var entryName = Styles.addTrackLayer;
menuItems.Add(
new MenuActionItem()
{
category = string.Empty,
entryName = entryName,
isActiveInMode = true,
priority = priority++,
state = enabled ? ActionValidity.Valid : ActionValidity.Invalid,
callback = menuCallback
}
);
}
static void AddClipMenuCommands(List<MenuActionItem> menuItems, ICollection<TrackAsset> tracks, double candidateTime)
{
if (!tracks.Any())
return;
var trackAsset = tracks.First();
var trackType = trackAsset.GetType();
if (tracks.Any(t => t.GetType() != trackType))
return;
var enabled = tracks.All(t => t != null && !t.lockedInHierarchy) && !TimelineWindow.instance.state.editSequence.isReadOnly;
var assetTypes = TypeUtility.GetPlayableAssetsHandledByTrack(trackType);
var visibleAssetTypes = TypeUtility.GetVisiblePlayableAssetsHandledByTrack(trackType);
// skips the name if there is only a single type
var commandNameTemplate = assetTypes.Count() == 1 ? Styles.addSingleItemFromAssetTemplate : Styles.addItemFromAssetTemplate;
int builtInPriority = MenuPriority.AddItem.addClip;
int customPriority = MenuPriority.AddItem.addCustomClip;
foreach (var assetType in assetTypes)
{
var assetItemType = assetType;
var category = TimelineHelpers.GetItemCategoryName(assetType);
Action<Object> onObjectChanged = obj =>
{
if (obj != null)
{
foreach (var t in tracks)
{
TimelineHelpers.CreateClipOnTrack(assetItemType, obj, t, candidateTime);
}
}
};
foreach (var objectReference in TypeUtility.ObjectReferencesForType(assetType))
{
var isSceneReference = objectReference.isSceneReference;
var dataType = objectReference.type;
GenericMenu.MenuFunction menuCallback = () =>
{
ObjectSelector.get.Show(null, dataType, null, isSceneReference, null, (obj) => onObjectChanged(obj), null);
ObjectSelector.get.titleContent = EditorGUIUtility.TrTextContent(string.Format(Styles.typeSelectorTemplate, TypeUtility.GetDisplayName(dataType)));
};
menuItems.Add(
new MenuActionItem()
{
category = category,
entryName = string.Format(commandNameTemplate, TypeUtility.GetDisplayName(assetType), TypeUtility.GetDisplayName(objectReference.type)),
isActiveInMode = true,
priority = TypeUtility.IsBuiltIn(assetType) ? builtInPriority++ : customPriority++,
state = enabled ? ActionValidity.Valid : ActionValidity.Invalid,
callback = menuCallback
}
);
}
}
foreach (var assetType in visibleAssetTypes)
{
var assetItemType = assetType;
var category = TimelineHelpers.GetItemCategoryName(assetType);
var commandName = string.Format(Styles.addItemTemplate, TypeUtility.GetDisplayName(assetType));
GenericMenu.MenuFunction command = () =>
{
foreach (var t in tracks)
{
TimelineHelpers.CreateClipOnTrack(assetItemType, t, candidateTime);
}
};
menuItems.Add(
new MenuActionItem()
{
category = category,
entryName = commandName,
isActiveInMode = true,
priority = TypeUtility.IsBuiltIn(assetItemType) ? builtInPriority++ : customPriority++,
state = enabled ? ActionValidity.Valid : ActionValidity.Invalid,
callback = command
}
);
}
}
static void AddMarkerMenuCommands(List<MenuActionItem> menu, IEnumerable<Type> markerTypes, Action<Type, Object> addMarkerCommand, bool enabled)
{
int builtInPriority = MenuPriority.AddItem.addMarker;
int customPriority = MenuPriority.AddItem.addCustomMarker;
foreach (var markerType in markerTypes)
{
var markerItemType = markerType;
string category = TimelineHelpers.GetItemCategoryName(markerItemType);
menu.Add(
new MenuActionItem()
{
category = category,
entryName = string.Format(Styles.addItemTemplate, TypeUtility.GetDisplayName(markerType)),
isActiveInMode = true,
priority = TypeUtility.IsBuiltIn(markerType) ? builtInPriority++ : customPriority++,
state = enabled ? ActionValidity.Valid : ActionValidity.Invalid,
callback = () => addMarkerCommand(markerItemType, null)
}
);
foreach (var objectReference in TypeUtility.ObjectReferencesForType(markerType))
{
var isSceneReference = objectReference.isSceneReference;
GenericMenu.MenuFunction menuCallback = () =>
{
Type assetDataType = objectReference.type;
ObjectSelector.get.titleContent = EditorGUIUtility.TrTextContent(string.Format(Styles.typeSelectorTemplate, TypeUtility.GetDisplayName(assetDataType)));
ObjectSelector.get.Show(null, assetDataType, null, isSceneReference, null, obj =>
{
if (obj != null)
addMarkerCommand(markerItemType, obj);
}, null);
};
menu.Add(
new MenuActionItem
{
category = TimelineHelpers.GetItemCategoryName(markerItemType),
entryName = string.Format(Styles.addItemFromAssetTemplate, TypeUtility.GetDisplayName(markerType), TypeUtility.GetDisplayName(objectReference.type)),
isActiveInMode = true,
priority = TypeUtility.IsBuiltIn(markerType) ? builtInPriority++ : customPriority++,
state = enabled ? ActionValidity.Valid : ActionValidity.Invalid,
callback = menuCallback
}
);
}
}
}
static void AddMarkerMenuCommands(List<MenuActionItem> menuItems, ICollection<TrackAsset> tracks, double candidateTime)
{
if (tracks.Count == 0)
return;
var enabled = tracks.All(t => !t.lockedInHierarchy) && !TimelineWindow.instance.state.editSequence.isReadOnly;
var addMarkerCommand = new Action<Type, Object>((type, obj) => AddMarkersCallback(tracks, type, candidateTime, obj));
AddMarkerMenuCommands(menuItems, tracks, addMarkerCommand, enabled);
}
static void AddMarkerMenuCommands(List<MenuActionItem> menuItems, ICollection<TrackAsset> tracks, Action<Type, Object> command, bool enabled)
{
var markerTypes = TypeUtility.GetBuiltInMarkerTypes().Union(TypeUtility.GetUserMarkerTypes());
if (tracks != null)
markerTypes = markerTypes.Where(x => tracks.All(track => (track == null) || TypeUtility.DoesTrackSupportMarkerType(track, x))); // null track indicates marker track to be created
AddMarkerMenuCommands(menuItems, markerTypes, command, enabled);
}
static void AddMarkersCallback(ICollection<TrackAsset> targets, Type markerType, double time, Object obj)
{
SelectionManager.Clear();
foreach (var target in targets)
{
var marker = TimelineHelpers.CreateMarkerOnTrack(markerType, obj, target, time);
SelectionManager.Add(marker);
}
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
}
}
}

View File

@@ -0,0 +1,34 @@
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Base class for a timeline action.
/// Inherit from this class to make an action on a timeline after a menu click and/or a key shortcut.
/// </summary>
/// <remarks>
/// To add an action as a menu item in the Timeline context menu, add <see cref="MenuEntryAttribute"/> on the action class.
/// To make an action to react to a shortcut, use the Shortcut Manager API with <see cref="TimelineShortcutAttribute"/>.
/// <seealso cref="UnityEditor.ShortcutManagement.ShortcutAttribute"/>
/// <seealso cref="ActiveInModeAttribute"/>
/// </remarks>
/// <example>
/// Simple Timeline Action example (with context menu and shortcut support).
/// <code source="../../DocCodeExamples/ActionExamples.cs" region="declare-sampleTimelineAction" title="SampleTimelineAction"/>
/// </example>
[ActiveInMode(TimelineModes.Default)]
public abstract class TimelineAction : IAction
{
/// <summary>
/// Execute the action.
/// </summary>
/// <param name="context">Context for the action.</param>
/// <returns>true if the action has been executed. false otherwise</returns>
public abstract bool Execute(ActionContext context);
/// <summary>
/// Defines the validity of an Action based on the context.
/// </summary>
/// <param name="context">Context for the action.</param>
/// <returns>Visual state of the menu for the action.</returns>
public abstract ActionValidity Validate(ActionContext context);
}
}

View File

@@ -0,0 +1,952 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.ShortcutManagement;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[MenuEntry("Copy", MenuPriority.TimelineActionSection.copy)]
[Shortcut("Main Menu/Edit/Copy", EventCommandNames.Copy)]
class CopyAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context)
{
if (SelectionManager.Count() == 0)
return ActionValidity.NotApplicable;
if (context.tracks.ContainsTimelineMarkerTrack(context.timeline))
return ActionValidity.NotApplicable;
return ActionValidity.Valid;
}
public override bool Execute(ActionContext context)
{
TimelineEditor.clipboard.Clear();
var clips = context.clips;
if (clips.Any())
{
clips.Invoke<CopyClipsToClipboard>();
}
var markers = context.markers;
if (markers.Any())
{
markers.Invoke<CopyMarkersToClipboard>();
}
var tracks = context.tracks;
if (tracks.Any())
{
CopyTracksToClipboard.Do(tracks.ToArray());
}
return true;
}
}
[MenuEntry("Paste", MenuPriority.TimelineActionSection.paste)]
[Shortcut("Main Menu/Edit/Paste", EventCommandNames.Paste)]
class PasteAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context)
{
return CanPaste(context.invocationTime) ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(ActionContext context)
{
if (!CanPaste(context.invocationTime))
return false;
PasteItems(context.invocationTime);
PasteTracks();
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return true;
}
static bool CanPaste(double? invocationTime)
{
var copiedItems = TimelineEditor.clipboard.GetCopiedItems().ToList();
if (!copiedItems.Any())
return TimelineEditor.clipboard.GetTracks().Any();
return CanPasteItems(copiedItems, invocationTime);
}
static bool CanPasteItems(ICollection<ItemsPerTrack> itemsGroups, double? invocationTime)
{
var hasItemsCopiedFromMultipleTracks = itemsGroups.Count > 1;
var allItemsCopiedFromCurrentAsset = itemsGroups.All(x => x.targetTrack.timelineAsset == TimelineEditor.inspectedAsset);
var hasUsedShortcut = invocationTime == null;
var anySourceLocked = itemsGroups.Any(x => x.targetTrack != null && x.targetTrack.lockedInHierarchy);
var targetTrack = GetPickedTrack();
if (targetTrack == null)
targetTrack = SelectionManager.SelectedTracks().FirstOrDefault();
//do not paste if the user copied items from another timeline
//if the copied items comes from > 1 track (since we do not know where to paste the copied items)
//or if a keyboard shortcut was used (since the user will not see the paste result)
if (!allItemsCopiedFromCurrentAsset)
{
var isSelectedTrackInCurrentAsset = targetTrack != null && targetTrack.timelineAsset == TimelineEditor.inspectedAsset;
if (hasItemsCopiedFromMultipleTracks || (hasUsedShortcut && !isSelectedTrackInCurrentAsset))
return false;
}
// pasting to items to their source track, if items from multiple tracks are selected
// and no track is in the selection items will each be pasted to their respective track.
if (targetTrack == null || itemsGroups.All(x => x.targetTrack == targetTrack))
return !anySourceLocked;
if (hasItemsCopiedFromMultipleTracks)
{
//do not paste if the track which received the paste action does not contain a copied clip
return !anySourceLocked && itemsGroups.Select(x => x.targetTrack).Contains(targetTrack);
}
var copiedItems = itemsGroups.SelectMany(i => i.items);
return IsTrackValidForItems(targetTrack, copiedItems);
}
static void PasteItems(double? invocationTime)
{
var copiedItems = TimelineEditor.clipboard.GetCopiedItems().ToList();
var numberOfUniqueParentsInClipboard = copiedItems.Count();
if (numberOfUniqueParentsInClipboard == 0) return;
List<ITimelineItem> newItems;
//if the copied items were on a single parent, then use the mouse position to get the parent OR the original parent
if (numberOfUniqueParentsInClipboard == 1)
{
var itemsGroup = copiedItems.First();
TrackAsset target = null;
if (invocationTime.HasValue)
target = GetPickedTrack();
if (target == null)
target = FindSuitableParentForSingleTrackPasteWithoutMouse(itemsGroup);
var candidateTime = invocationTime ?? TimelineHelpers.GetCandidateTime(null, target);
newItems = TimelineHelpers.DuplicateItemsUsingCurrentEditMode(TimelineEditor.clipboard.exposedPropertyTable, TimelineEditor.inspectedDirector, itemsGroup, target, candidateTime, "Paste Items").ToList();
}
//if copied items were on multiple parents, then the destination parents are the same as the original parents
else
{
var time = invocationTime ?? TimelineHelpers.GetCandidateTime(null, copiedItems.Select(c => c.targetTrack).ToArray());
newItems = TimelineHelpers.DuplicateItemsUsingCurrentEditMode(TimelineEditor.clipboard.exposedPropertyTable, TimelineEditor.inspectedDirector, copiedItems, time, "Paste Items").ToList();
}
TimelineHelpers.FrameItems(newItems);
SelectionManager.RemoveTimelineSelection();
foreach (var item in newItems)
{
SelectionManager.Add(item);
}
}
static TrackAsset FindSuitableParentForSingleTrackPasteWithoutMouse(ItemsPerTrack itemsGroup)
{
var groupParent = itemsGroup.targetTrack; //set a main parent in the clipboard
var selectedTracks = SelectionManager.SelectedTracks();
if (selectedTracks.Contains(groupParent))
{
return groupParent;
}
//find a selected track suitable for all items
var itemsToPaste = itemsGroup.items;
var compatibleTrack = selectedTracks.FirstOrDefault(t => IsTrackValidForItems(t, itemsToPaste));
return compatibleTrack != null ? compatibleTrack : groupParent;
}
static bool IsTrackValidForItems(TrackAsset track, IEnumerable<ITimelineItem> items)
{
if (track == null || track.lockedInHierarchy) return false;
return items.All(i => i.IsCompatibleWithTrack(track));
}
static TrackAsset GetPickedTrack()
{
if (PickerUtils.pickedElements == null)
return null;
var rowGUI = PickerUtils.pickedElements.OfType<IRowGUI>().FirstOrDefault();
if (rowGUI != null)
return rowGUI.asset;
return null;
}
static void PasteTracks()
{
var trackData = TimelineEditor.clipboard.GetTracks().ToList();
if (trackData.Any())
{
SelectionManager.RemoveTimelineSelection();
}
foreach (var track in trackData)
{
var newTrack = track.item.Duplicate(TimelineEditor.clipboard.exposedPropertyTable, TimelineEditor.inspectedDirector, TimelineEditor.inspectedAsset);
if (track.binding != null)
{
BindingUtility.Bind(TimelineEditor.inspectedDirector, newTrack, track.binding);
}
SelectionManager.Add(newTrack);
foreach (var childTrack in newTrack.GetFlattenedChildTracks())
{
SelectionManager.Add(childTrack);
}
if (track.parent != null && track.parent.timelineAsset == TimelineEditor.inspectedAsset)
{
TrackExtensions.ReparentTracks(new List<TrackAsset> { newTrack }, track.parent, track.item);
}
}
}
}
[MenuEntry("Duplicate", MenuPriority.TimelineActionSection.duplicate)]
[Shortcut("Main Menu/Edit/Duplicate", EventCommandNames.Duplicate)]
class DuplicateAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context)
{
IEnumerable<TrackAsset> tracks = context.tracks.RemoveTimelineMarkerTrackFromList(context.timeline);
return context.clips.Any() || tracks.Any() || context.markers.Any() ? ActionValidity.Valid : ActionValidity.NotApplicable;
}
public bool Execute(Func<ITimelineItem, ITimelineItem, double> gapBetweenItems)
{
return Execute(TimelineEditor.CurrentContext(), gapBetweenItems);
}
public override bool Execute(ActionContext context)
{
return Execute(context, (item1, item2) => ItemsUtils.TimeGapBetweenItems(item1, item2));
}
internal bool Execute(ActionContext context, Func<ITimelineItem, ITimelineItem, double> gapBetweenItems)
{
List<ITimelineItem> items = new List<ITimelineItem>();
items.AddRange(context.clips.Select(p => p.ToItem()));
items.AddRange(context.markers.Select(p => p.ToItem()));
List<ItemsPerTrack> selectedItems = items.ToItemsPerTrack().ToList();
if (selectedItems.Any())
{
var requestedTime = CalculateDuplicateTime(selectedItems, gapBetweenItems);
var duplicatedItems = TimelineHelpers.DuplicateItemsUsingCurrentEditMode(TimelineEditor.inspectedDirector, TimelineEditor.inspectedDirector, selectedItems, requestedTime, "Duplicate Items");
TimelineHelpers.FrameItems(duplicatedItems);
SelectionManager.RemoveTimelineSelection();
foreach (var item in duplicatedItems)
SelectionManager.Add(item);
}
var tracks = context.tracks.ToArray();
if (tracks.Length > 0)
tracks.Invoke<DuplicateTracks>();
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return true;
}
static double CalculateDuplicateTime(IEnumerable<ItemsPerTrack> duplicatedItems, Func<ITimelineItem, ITimelineItem, double> gapBetweenItems)
{
//Find the end time of the rightmost item
var itemsOnTracks = duplicatedItems.SelectMany(i => i.targetTrack.GetItems()).ToList();
var time = itemsOnTracks.Max(i => i.end);
//From all the duplicated items, select the leftmost items
var firstDuplicatedItems = duplicatedItems.Select(i => i.leftMostItem);
var leftMostDuplicatedItems = firstDuplicatedItems.OrderBy(i => i.start).GroupBy(i => i.start).FirstOrDefault();
if (leftMostDuplicatedItems == null) return 0.0;
foreach (var leftMostItem in leftMostDuplicatedItems)
{
var siblings = leftMostItem.parentTrack.GetItems();
var rightMostSiblings = siblings.OrderByDescending(i => i.end).GroupBy(i => i.end).FirstOrDefault();
if (rightMostSiblings == null) continue;
foreach (var sibling in rightMostSiblings)
time = Math.Max(time, sibling.end + gapBetweenItems(leftMostItem, sibling));
}
return time;
}
}
[MenuEntry("Delete", MenuPriority.TimelineActionSection.delete)]
[Shortcut("Main Menu/Edit/Delete", EventCommandNames.Delete)]
[ShortcutPlatformOverride(RuntimePlatform.OSXEditor, KeyCode.Backspace, ShortcutModifiers.Action)]
[ActiveInMode(TimelineModes.Default)]
class DeleteAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context)
{
return CanDelete(context) ? ActionValidity.Valid : ActionValidity.Invalid;
}
static bool CanDelete(ActionContext context)
{
if (TimelineWindow.instance.state.editSequence.isReadOnly)
return false;
if (context.tracks.ContainsTimelineMarkerTrack(context.timeline))
return false;
// All() returns true when empty
return context.tracks.All(x => !x.lockedInHierarchy) &&
context.clips.All(x => x.GetParentTrack() == null || !x.GetParentTrack().lockedInHierarchy) &&
context.markers.All(x => x.parent == null || !x.parent.lockedInHierarchy);
}
public override bool Execute(ActionContext context)
{
if (!CanDelete(context))
return false;
var selectedItems = context.clips.Select(p => p.ToItem()).ToList();
selectedItems.AddRange(context.markers.Select(p => p.ToItem()));
DeleteItems(selectedItems);
if (context.tracks.Any() && SelectionManager.GetCurrentInlineEditorCurve() == null)
context.tracks.Invoke<DeleteTracks>();
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return selectedItems.Any() || context.tracks.Any();
}
internal static void DeleteItems(IEnumerable<ITimelineItem> items)
{
var tracks = items.GroupBy(c => c.parentTrack);
foreach (var track in tracks)
TimelineUndo.PushUndo(track.Key, L10n.Tr("Delete Items"));
TimelineAnimationUtilities.UnlinkAnimationWindowFromClips(items.OfType<ClipItem>().Select(i => i.clip));
EditMode.PrepareItemsDelete(ItemsUtils.ToItemsPerTrack(items));
EditModeUtils.Delete(items);
SelectionManager.RemoveAllClips();
}
}
[MenuEntry("Match Content", MenuPriority.TimelineActionSection.matchContent)]
[Shortcut(Shortcuts.Timeline.matchContent)]
class MatchContent : TimelineAction
{
public override ActionValidity Validate(ActionContext actionContext)
{
var clips = actionContext.clips;
if (!clips.Any())
return ActionValidity.NotApplicable;
return clips.Any(TimelineHelpers.HasUsableAssetDuration)
? ActionValidity.Valid
: ActionValidity.Invalid;
}
public override bool Execute(ActionContext actionContext)
{
var clips = actionContext.clips;
return clips.Any() && ClipModifier.MatchContent(clips);
}
}
[Shortcut(Shortcuts.Timeline.play)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class PlayTimelineAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
var currentState = TimelineEditor.state.playing;
TimelineEditor.state.SetPlaying(!currentState);
return true;
}
}
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectAllAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
// otherwise select all tracks.
SelectionManager.Clear();
TimelineWindow.instance.allTracks.ForEach(x => SelectionManager.Add(x.track));
return true;
}
}
[Shortcut(Shortcuts.Timeline.previousFrame)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class PreviousFrameAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
if (TimelineEditor.inspectedAsset == null)
return false;
var inspectedFrame = TimeUtility.ToFrames(TimelineEditor.inspectedSequenceTime, TimelineEditor.inspectedAsset.editorSettings.fps);
inspectedFrame = Mathf.Max(0, inspectedFrame - 1);
TimelineEditor.inspectedSequenceTime = TimeUtility.FromFrames(inspectedFrame, TimelineEditor.inspectedAsset.editorSettings.fps);
return true;
}
}
[Shortcut(Shortcuts.Timeline.nextFrame)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class NextFrameAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
if (TimelineEditor.inspectedAsset == null)
return false;
var inspectedFrame = TimeUtility.ToFrames(TimelineEditor.inspectedSequenceTime, TimelineEditor.inspectedAsset.editorSettings.fps);
inspectedFrame++;
TimelineEditor.inspectedSequenceTime = TimeUtility.FromFrames(inspectedFrame, TimelineEditor.inspectedAsset.editorSettings.fps);
return true;
}
}
[Shortcut(Shortcuts.Timeline.frameAll)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class FrameAllAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context)
{
if (context.timeline != null && !context.timeline.flattenedTracks.Any())
return ActionValidity.NotApplicable;
return ActionValidity.Valid;
}
public override bool Execute(ActionContext actionContext)
{
var inlineCurveEditor = SelectionManager.GetCurrentInlineEditorCurve();
if (FrameSelectedAction.ShouldHandleInlineCurve(inlineCurveEditor))
{
FrameSelectedAction.FrameInlineCurves(inlineCurveEditor, false);
return true;
}
if (TimelineWindow.instance.state.IsCurrentEditingASequencerTextField())
return false;
var visibleTracks = TimelineWindow.instance.treeView.visibleTracks.ToList();
if (TimelineEditor.inspectedAsset != null && TimelineEditor.inspectedAsset.markerTrack != null)
visibleTracks.Add(TimelineEditor.inspectedAsset.markerTrack);
if (visibleTracks.Count == 0)
return false;
var startTime = float.MaxValue;
var endTime = float.MinValue;
foreach (var t in visibleTracks)
{
if (t == null)
continue;
// time range based on track's curves and clips.
double trackStart, trackEnd, trackDuration;
t.GetSequenceTime(out trackStart, out trackDuration);
trackEnd = trackStart + trackDuration;
// take track's markers into account
double itemsStart, itemsEnd;
ItemsUtils.GetItemRange(t, out itemsStart, out itemsEnd);
startTime = Mathf.Min(startTime, (float)trackStart, (float)itemsStart);
endTime = Mathf.Max(endTime, (float)(trackEnd), (float)itemsEnd);
}
FrameSelectedAction.FrameRange(startTime, endTime);
return true;
}
}
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class FrameSelectedAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public static void FrameRange(float startTime, float endTime)
{
if (startTime > endTime)
{
return;
}
var halfDuration = endTime - Math.Max(0.0f, startTime);
if (halfDuration > 0.0f)
{
TimelineEditor.visibleTimeRange = new Vector2(Mathf.Max(0.0f, startTime - (halfDuration * 0.1f)), endTime + (halfDuration * 0.1f));
}
else
{
// start == end
// keep the zoom level constant, only pan the time area to center the item
var currentRange = TimelineEditor.visibleTimeRange.y - TimelineEditor.visibleTimeRange.x;
TimelineEditor.visibleTimeRange = new Vector2(startTime - currentRange / 2, startTime + currentRange / 2);
}
TimelineZoomManipulator.InvalidateWheelZoom();
TimelineEditor.Refresh(RefreshReason.SceneNeedsUpdate);
}
public override bool Execute(ActionContext actionContext)
{
var inlineCurveEditor = SelectionManager.GetCurrentInlineEditorCurve();
if (ShouldHandleInlineCurve(inlineCurveEditor))
{
FrameInlineCurves(inlineCurveEditor, true);
return true;
}
if (TimelineWindow.instance.state.IsCurrentEditingASequencerTextField())
return false;
if (SelectionManager.Count() == 0)
{
actionContext.Invoke<FrameAllAction>();
return true;
}
var startTime = float.MaxValue;
var endTime = float.MinValue;
var clips = actionContext.clips.Select(ItemToItemGui.GetGuiForClip);
var markers = actionContext.markers;
if (!clips.Any() && !markers.Any())
return false;
foreach (var c in clips)
{
startTime = Mathf.Min(startTime, (float)c.clip.start);
endTime = Mathf.Max(endTime, (float)c.clip.end);
if (c.clipCurveEditor != null)
{
c.clipCurveEditor.FrameClip();
}
}
foreach (var marker in markers)
{
startTime = Mathf.Min(startTime, (float)marker.time);
endTime = Mathf.Max(endTime, (float)marker.time);
}
FrameRange(startTime, endTime);
return true;
}
public static bool ShouldHandleInlineCurve(IClipCurveEditorOwner curveEditorOwner)
{
return curveEditorOwner?.clipCurveEditor != null &&
curveEditorOwner.inlineCurvesSelected &&
curveEditorOwner.owner != null &&
curveEditorOwner.owner.GetShowInlineCurves();
}
public static void FrameInlineCurves(IClipCurveEditorOwner curveEditorOwner, bool selectionOnly)
{
var curveEditor = curveEditorOwner.clipCurveEditor.curveEditor;
var frameBounds = selectionOnly ? curveEditor.GetSelectionBounds() : curveEditor.GetClipBounds();
var clipGUI = curveEditorOwner as TimelineClipGUI;
var areaOffset = 0.0f;
if (clipGUI != null)
{
areaOffset = (float)Math.Max(0.0, clipGUI.clip.FromLocalTimeUnbound(0.0));
var timeScale = (float)clipGUI.clip.timeScale; // Note: The getter for clip.timeScale is guaranteed to never be zero.
// Apply scaling
var newMin = frameBounds.min.x / timeScale;
var newMax = (frameBounds.max.x - frameBounds.min.x) / timeScale + newMin;
frameBounds.SetMinMax(
new Vector3(newMin, frameBounds.min.y, frameBounds.min.z),
new Vector3(newMax, frameBounds.max.y, frameBounds.max.z));
}
curveEditor.Frame(frameBounds, true, true);
var area = curveEditor.shownAreaInsideMargins;
area.x += areaOffset;
var curveStart = curveEditorOwner.clipCurveEditor.dataSource.start;
FrameRange(curveStart + frameBounds.min.x, curveStart + frameBounds.max.x);
}
}
[Shortcut(Shortcuts.Timeline.previousKey)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class PrevKeyAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
if (TimelineEditor.inspectedAsset == null)
return false;
var keyTraverser = new Utilities.KeyTraverser(TimelineEditor.inspectedAsset, 0.01f / TimelineEditor.inspectedAsset.editorSettings.fps);
var time = keyTraverser.GetPrevKey((float)TimelineEditor.inspectedSequenceTime, TimelineWindow.instance.state.dirtyStamp);
if (time != TimelineEditor.inspectedSequenceTime)
{
TimelineEditor.inspectedSequenceTime = time;
}
return true;
}
}
[Shortcut(Shortcuts.Timeline.nextKey)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class NextKeyAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
if (TimelineEditor.inspectedAsset == null)
return false;
var keyTraverser = new Utilities.KeyTraverser(TimelineEditor.inspectedAsset, 0.01f / TimelineEditor.inspectedAsset.editorSettings.fps);
var time = keyTraverser.GetNextKey((float)TimelineEditor.inspectedSequenceTime, TimelineWindow.instance.state.dirtyStamp);
if (time != TimelineEditor.inspectedSequenceTime)
{
TimelineEditor.inspectedSequenceTime = time;
}
return true;
}
}
[Shortcut(Shortcuts.Timeline.goToStart)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class GotoStartAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
TimelineEditor.inspectedSequenceTime = 0.0f;
TimelineWindow.instance.state.EnsurePlayHeadIsVisible();
return true;
}
}
[Shortcut(Shortcuts.Timeline.goToEnd)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class GotoEndAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
TimelineEditor.inspectedSequenceTime = TimelineWindow.instance.state.editSequence.duration;
TimelineWindow.instance.state.EnsurePlayHeadIsVisible();
return true;
}
}
[Shortcut(Shortcuts.Timeline.zoomIn)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class ZoomIn : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
TimelineZoomManipulator.Instance.DoZoom(1.15f);
return true;
}
}
[Shortcut(Shortcuts.Timeline.zoomOut)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class ZoomOut : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
TimelineZoomManipulator.Instance.DoZoom(0.85f);
return true;
}
}
[Shortcut(Shortcuts.Timeline.collapseGroup)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class CollapseGroup : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.CollapseGroup(actionContext.tracks);
}
}
[Shortcut(Shortcuts.Timeline.unCollapseGroup)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class UnCollapseGroup : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.UnCollapseGroup(actionContext.tracks);
}
}
[Shortcut(Shortcuts.Timeline.selectLeftItem)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectLeftClip : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
// Switches to track header if no left track exists
return KeyboardNavigation.SelectLeftItem();
}
}
[Shortcut(Shortcuts.Timeline.selectRightItem)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectRightClip : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectRightItem();
}
}
[Shortcut(Shortcuts.Timeline.selectUpItem)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectUpClip : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectUpItem();
}
}
[Shortcut(Shortcuts.Timeline.selectUpTrack)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectUpTrack : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectUpTrack();
}
}
[Shortcut(Shortcuts.Timeline.selectDownItem)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectDownClip : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectDownItem();
}
}
[Shortcut(Shortcuts.Timeline.selectDownTrack)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectDownTrack : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
if (!KeyboardNavigation.ClipAreaActive() && !KeyboardNavigation.TrackHeadActive())
return KeyboardNavigation.FocusFirstVisibleItem();
else
return KeyboardNavigation.SelectDownTrack();
}
}
[Shortcut(Shortcuts.Timeline.multiSelectLeft)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class MultiselectLeftClip : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectLeftItem(true);
}
}
[Shortcut(Shortcuts.Timeline.multiSelectRight)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class MultiselectRightClip : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectRightItem(true);
}
}
[Shortcut(Shortcuts.Timeline.multiSelectUp)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class MultiselectUpTrack : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectUpTrack(true);
}
}
[Shortcut(Shortcuts.Timeline.multiSelectDown)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class MultiselectDownTrack : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectDownTrack(true);
}
}
[Shortcut(Shortcuts.Timeline.toggleClipTrackArea)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class ToggleClipTrackArea : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
if (KeyboardNavigation.TrackHeadActive())
return KeyboardNavigation.FocusFirstVisibleItem(actionContext.tracks);
if (!KeyboardNavigation.ClipAreaActive())
return KeyboardNavigation.FocusFirstVisibleItem();
var item = KeyboardNavigation.GetVisibleSelectedItems().LastOrDefault();
if (item != null)
SelectionManager.SelectOnly(item.parentTrack);
return true;
}
}
[MenuEntry("Key All Animated", MenuPriority.TimelineActionSection.keyAllAnimated)]
[Shortcut(Shortcuts.Timeline.keyAllAnimated)]
class KeyAllAnimated : TimelineAction
{
public override ActionValidity Validate(ActionContext actionContext)
{
return CanExecute(TimelineEditor.state, actionContext)
? ActionValidity.Valid
: ActionValidity.NotApplicable;
}
public override bool Execute(ActionContext actionContext)
{
WindowState state = TimelineEditor.state;
PlayableDirector director = TimelineEditor.inspectedDirector;
if (!CanExecute(state, actionContext) || director == null)
return false;
IEnumerable<TrackAsset> keyableTracks = GetKeyableTracks(state, actionContext);
var curveSelected = SelectionManager.GetCurrentInlineEditorCurve();
if (curveSelected != null)
{
var sel = curveSelected.clipCurveEditor.GetSelectedProperties().ToList();
var go = (director.GetGenericBinding(curveSelected.owner) as Component).gameObject;
if (sel.Count > 0)
{
TimelineRecording.KeyProperties(go, state, sel);
}
else
{
var binding = director.GetGenericBinding(curveSelected.owner) as Component;
TimelineRecording.KeyAllProperties(binding, state);
}
}
else
{
foreach (var track in keyableTracks)
{
var binding = director.GetGenericBinding(track) as Component;
TimelineRecording.KeyAllProperties(binding, state);
}
}
return true;
}
static IEnumerable<TrackAsset> GetKeyableTracks(WindowState state, ActionContext context)
{
if (!context.clips.Any() && !context.tracks.Any()) //no selection -> animate all recorded tracks
return state.editSequence.asset.flattenedTracks.Where(state.IsArmedForRecord);
List<TrackAsset> parentTracks = context.tracks.ToList();
parentTracks.AddRange(context.clips.Select(clip => clip.GetParentTrack()).Distinct());
if (!parentTracks.All(state.IsArmedForRecord))
return Enumerable.Empty<TrackAsset>();
return parentTracks;
}
static bool CanExecute(WindowState state, ActionContext context)
{
if (context.markers.Any())
return false;
if (context.tracks.ContainsTimelineMarkerTrack(state.editSequence.asset))
return false;
IClipCurveEditorOwner curveSelected = SelectionManager.GetCurrentInlineEditorCurve();
// Can't have an inline curve selected and have multiple tracks also.
if (curveSelected != null)
{
return state.IsArmedForRecord(curveSelected.owner);
}
return GetKeyableTracks(state, context).Any();
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Base class for a track action.
/// Inherit from this class to make an action that would react on selected tracks after a menu click and/or a key shortcut.
/// </summary>
/// <example>
/// Simple track Action example (with context menu and shortcut support).
/// <code source="../../DocCodeExamples/ActionExamples.cs" region="declare-sampleTrackAction" title="SampleTrackAction"/>
/// </example>
/// <remarks>
/// To add an action as a menu item in the Timeline context menu, add <see cref="MenuEntryAttribute"/> on the action class.
/// To make an action to react to a shortcut, use the Shortcut Manager API with <see cref="TimelineShortcutAttribute"/>.
/// <seealso cref="UnityEditor.ShortcutManagement.ShortcutAttribute"/>
/// </remarks>
[ActiveInMode(TimelineModes.Default)]
public abstract class TrackAction : IAction
{
/// <summary>
/// Execute the action.
/// </summary>
/// <param name="tracks">Tracks that will be used for the action. </param>
/// <returns>true if the action has been executed. false otherwise</returns>
public abstract bool Execute(IEnumerable<TrackAsset> tracks);
/// <summary>
/// Defines the validity of an Action for a given set of tracks.
/// </summary>
/// <param name="tracks">tracks that the action would act on.</param>
/// <returns>The validity of the set of tracks.</returns>
public abstract ActionValidity Validate(IEnumerable<TrackAsset> tracks);
}
}

View File

@@ -0,0 +1,457 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEditor.Timeline.Actions;
using UnityEngine;
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);
SelectionManager.Add(newTrack);
foreach (var childTrack in newTrack.GetFlattenedChildTracks())
{
SelectionManager.Add(childTrack);
}
if (TimelineEditor.inspectedDirector != null)
{
var binding = TimelineEditor.inspectedDirector.GetGenericBinding(track);
if (binding != null)
{
TimelineUndo.PushUndo(TimelineEditor.inspectedDirector, L10n.Tr("Duplicate"));
TimelineEditor.inspectedDirector.SetGenericBinding(newTrack, binding);
}
}
}
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return true;
}
}
[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;
}
}
}

View File

@@ -0,0 +1,56 @@
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
namespace UnityEditor.Timeline
{
[UsedImplicitly]
[CustomTimelineEditor(typeof(ActivationTrack))]
class ActivationTrackEditor : TrackEditor
{
static readonly string ClipText = L10n.Tr("Active");
static readonly string k_ErrorParentString = L10n.Tr("The bound GameObject is a parent of the PlayableDirector.");
static readonly string k_ErrorString = L10n.Tr("The bound GameObject contains the PlayableDirector.");
public override TrackDrawOptions GetTrackOptions(TrackAsset track, Object binding)
{
var options = base.GetTrackOptions(track, binding);
options.errorText = GetErrorText(track, binding);
return options;
}
string GetErrorText(TrackAsset track, Object binding)
{
var gameObject = binding as GameObject;
var currentDirector = TimelineEditor.inspectedDirector;
if (gameObject != null && currentDirector != null)
{
var director = gameObject.GetComponent<PlayableDirector>();
if (currentDirector == director)
{
return k_ErrorString;
}
if (currentDirector.gameObject.transform.IsChildOf(gameObject.transform))
{
return k_ErrorParentString;
}
}
return base.GetErrorText(track, binding, TrackBindingErrors.PrefabBound);
}
public override void OnCreate(TrackAsset track, TrackAsset copiedFrom)
{
// Add a default clip to the newly created track
if (copiedFrom == null)
{
var clip = track.CreateClip(0);
clip.displayName = ClipText;
clip.duration = System.Math.Max(clip.duration, track.timelineAsset.duration * 0.5f);
}
}
}
}

View File

@@ -0,0 +1,43 @@
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[CustomEditor(typeof(ActivationTrack))]
class ActivationTrackInspector : TrackAssetInspector
{
static class Styles
{
public static readonly GUIContent PostPlaybackStateText = L10n.TextContent("Post-playback state");
}
SerializedProperty m_PostPlaybackProperty;
public override void OnInspectorGUI()
{
using (new EditorGUI.DisabledScope(IsTrackLocked()))
{
serializedObject.Update();
EditorGUI.BeginChangeCheck();
if (m_PostPlaybackProperty != null)
EditorGUILayout.PropertyField(m_PostPlaybackProperty, Styles.PostPlaybackStateText);
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
var activationTrack = target as ActivationTrack;
if (activationTrack != null)
activationTrack.UpdateTrackMode();
}
}
}
public override void OnEnable()
{
base.OnEnable();
m_PostPlaybackProperty = serializedObject.FindProperty("m_PostPlaybackState");
}
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine.Playables;
using UnityEngine.SceneManagement;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Analytics
{
class TimelineSceneInfo
{
public Dictionary<string, int> trackCount = new Dictionary<string, int>
{
{"ActivationTrack", 0},
{"AnimationTrack", 0},
{"AudioTrack", 0},
{"ControlTrack", 0},
{"PlayableTrack", 0},
{"UserType", 0},
{"Other", 0}
};
public Dictionary<string, int> userTrackTypesCount = new Dictionary<string, int>();
public HashSet<TimelineAsset> uniqueDirectors = new HashSet<TimelineAsset>();
public int numTracks = 0;
public int minDuration = int.MaxValue;
public int maxDuration = int.MinValue;
public int minNumTracks = int.MaxValue;
public int maxNumTracks = int.MinValue;
public int numRecorded = 0;
}
[Serializable]
struct TrackInfo
{
public string name;
public double percent;
}
[Serializable]
class TimelineEventInfo
{
public int num_timelines;
public int min_duration, max_duration;
public int min_num_tracks, max_num_tracks;
public double recorded_percent;
public List<TrackInfo> track_info = new List<TrackInfo>();
public string most_popular_user_track = string.Empty;
public TimelineEventInfo(TimelineSceneInfo sceneInfo)
{
num_timelines = sceneInfo.uniqueDirectors.Count;
min_duration = sceneInfo.minDuration;
max_duration = sceneInfo.maxDuration;
min_num_tracks = sceneInfo.minNumTracks;
max_num_tracks = sceneInfo.maxNumTracks;
recorded_percent = Math.Round(100.0 * sceneInfo.numRecorded / sceneInfo.numTracks, 1);
foreach (KeyValuePair<string, int> kv in sceneInfo.trackCount.Where(x => x.Value > 0))
{
track_info.Add(new TrackInfo()
{
name = kv.Key,
percent = Math.Round(100.0 * kv.Value / sceneInfo.numTracks, 1)
});
}
if (sceneInfo.userTrackTypesCount.Any())
{
most_popular_user_track = sceneInfo.userTrackTypesCount
.First(x => x.Value == sceneInfo.userTrackTypesCount.Values.Max()).Key;
}
}
public static bool IsUserType(Type t)
{
string nameSpace = t.Namespace;
return string.IsNullOrEmpty(nameSpace) || !nameSpace.StartsWith("UnityEngine.Timeline");
}
}
static class TimelineAnalytics
{
static TimelineSceneInfo _timelineSceneInfo = new TimelineSceneInfo();
class TimelineAnalyticsPreProcess : IPreprocessBuildWithReport
{
public int callbackOrder { get { return 0; } }
public void OnPreprocessBuild(BuildReport report)
{
_timelineSceneInfo = new TimelineSceneInfo();
}
}
class TimelineAnalyticsProcess : IProcessSceneWithReport
{
public int callbackOrder
{
get { return 0; }
}
public void OnProcessScene(Scene scene, BuildReport report)
{
var timelines = UnityEngine.Object.FindObjectsOfType<PlayableDirector>().Select(pd => pd.playableAsset).OfType<TimelineAsset>().Distinct();
foreach (var timeline in timelines)
{
if (_timelineSceneInfo.uniqueDirectors.Add(timeline))
{
_timelineSceneInfo.numTracks += timeline.flattenedTracks.Count();
_timelineSceneInfo.minDuration = Math.Min(_timelineSceneInfo.minDuration, (int)(timeline.duration * 1000));
_timelineSceneInfo.maxDuration = Math.Max(_timelineSceneInfo.maxDuration, (int)(timeline.duration * 1000));
_timelineSceneInfo.minNumTracks = Math.Min(_timelineSceneInfo.minNumTracks, timeline.flattenedTracks.Count());
_timelineSceneInfo.maxNumTracks = Math.Max(_timelineSceneInfo.maxNumTracks, timeline.flattenedTracks.Count());
foreach (var track in timeline.flattenedTracks)
{
string key = track.GetType().Name;
if (_timelineSceneInfo.trackCount.ContainsKey(key))
{
_timelineSceneInfo.trackCount[key]++;
}
else
{
if (TimelineEventInfo.IsUserType(track.GetType()))
{
_timelineSceneInfo.trackCount["UserType"]++;
if (_timelineSceneInfo.userTrackTypesCount.ContainsKey(key))
_timelineSceneInfo.userTrackTypesCount[key]++;
else
_timelineSceneInfo.userTrackTypesCount[key] = 1;
}
else
_timelineSceneInfo.trackCount["Other"]++;
}
if (track.clips.Any(x => x.recordable))
_timelineSceneInfo.numRecorded++;
else
{
var animationTrack = track as AnimationTrack;
if (animationTrack != null)
{
if (animationTrack.CanConvertToClipMode())
_timelineSceneInfo.numRecorded++;
}
}
}
}
}
}
}
class TimelineAnalyticsPostProcess : IPostprocessBuildWithReport
{
public int callbackOrder {get { return 0; }}
public void OnPostprocessBuild(BuildReport report)
{
if (_timelineSceneInfo.uniqueDirectors.Count > 0)
{
var timelineEvent = new TimelineEventInfo(_timelineSceneInfo);
EditorAnalytics.SendEventTimelineInfo(timelineEvent);
}
}
}
}
}

View File

@@ -0,0 +1,101 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
namespace UnityEditor.Timeline
{
[ApplyDefaultUndo("Match Offsets")]
[MenuEntry("Match Offsets To Previous Clip", MenuPriority.CustomClipActionSection.matchPrevious), UsedImplicitly]
class MatchOffsetsPreviousAction : ClipAction
{
public override bool Execute(IEnumerable<TimelineClip> clips)
{
if (clips == null || !clips.Any())
return false;
AnimationOffsetMenu.MatchClipsToPrevious(clips.Where(x => IsValidClip(x, TimelineEditor.inspectedDirector)).ToArray());
return true;
}
static bool IsValidClip(TimelineClip clip, PlayableDirector director)
{
return clip != null &&
clip.GetParentTrack() != null &&
(clip.asset as AnimationPlayableAsset) != null &&
clip.GetParentTrack().clips.Any(x => x.start < clip.start) &&
TimelineUtility.GetSceneGameObject(director, clip.GetParentTrack()) != null;
}
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
if (!clips.All(TimelineAnimationUtilities.IsAnimationClip))
return ActionValidity.NotApplicable;
var director = TimelineEditor.inspectedDirector;
if (TimelineEditor.inspectedDirector == null)
return ActionValidity.NotApplicable;
if (clips.Any(c => IsValidClip(c, director)))
return ActionValidity.Valid;
return ActionValidity.NotApplicable;
}
}
[ApplyDefaultUndo("Match Offsets")]
[MenuEntry("Match Offsets To Next Clip", MenuPriority.CustomClipActionSection.matchNext), UsedImplicitly]
class MatchOffsetsNextAction : ClipAction
{
public override bool Execute(IEnumerable<TimelineClip> clips)
{
AnimationOffsetMenu.MatchClipsToNext(clips.Where(x => IsValidClip(x, TimelineEditor.inspectedDirector)).ToArray());
return true;
}
static bool IsValidClip(TimelineClip clip, PlayableDirector director)
{
return clip != null &&
clip.GetParentTrack() != null &&
(clip.asset as AnimationPlayableAsset) != null &&
clip.GetParentTrack().clips.Any(x => x.start > clip.start) &&
TimelineUtility.GetSceneGameObject(director, clip.GetParentTrack()) != null;
}
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
if (!clips.All(TimelineAnimationUtilities.IsAnimationClip))
return ActionValidity.NotApplicable;
var director = TimelineEditor.inspectedDirector;
if (TimelineEditor.inspectedDirector == null)
return ActionValidity.NotApplicable;
if (clips.Any(c => IsValidClip(c, director)))
return ActionValidity.Valid;
return ActionValidity.NotApplicable;
}
}
[ApplyDefaultUndo]
[MenuEntry("Reset Offsets", MenuPriority.CustomClipActionSection.resetOffset), UsedImplicitly]
class ResetOffsets : ClipAction
{
public override bool Execute(IEnumerable<TimelineClip> clips)
{
AnimationOffsetMenu.ResetClipOffsets(clips.Where(TimelineAnimationUtilities.IsAnimationClip).ToArray());
return true;
}
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
if (!clips.All(TimelineAnimationUtilities.IsAnimationClip))
return ActionValidity.NotApplicable;
return ActionValidity.Valid;
}
}
}

View File

@@ -0,0 +1,427 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
namespace UnityEditor.Timeline
{
struct CurveBindingPair
{
public EditorCurveBinding binding;
public AnimationCurve curve;
public ObjectReferenceKeyframe[] objectCurve;
}
class CurveBindingGroup
{
public CurveBindingPair[] curveBindingPairs { get; set; }
public Vector2 timeRange { get; set; }
public Vector2 valueRange { get; set; }
public bool isFloatCurve
{
get
{
return curveBindingPairs != null && curveBindingPairs.Length > 0 &&
curveBindingPairs[0].curve != null;
}
}
public bool isObjectCurve
{
get
{
return curveBindingPairs != null && curveBindingPairs.Length > 0 &&
curveBindingPairs[0].objectCurve != null;
}
}
public int count
{
get
{
if (curveBindingPairs == null)
return 0;
return curveBindingPairs.Length;
}
}
}
class AnimationClipCurveInfo
{
bool m_CurveDirty = true;
bool m_KeysDirty = true;
public bool dirty
{
get { return m_CurveDirty; }
set
{
m_CurveDirty = value;
if (m_CurveDirty)
{
m_KeysDirty = true;
if (m_groupings != null)
m_groupings.Clear();
}
}
}
public AnimationCurve[] curves;
public EditorCurveBinding[] bindings;
public EditorCurveBinding[] objectBindings;
public List<ObjectReferenceKeyframe[]> objectCurves;
Dictionary<string, CurveBindingGroup> m_groupings;
// to tell whether the cache has changed
public int version { get; private set; }
float[] m_KeyTimes;
Dictionary<EditorCurveBinding, float[]> m_individualBindinsKey;
public float[] keyTimes
{
get
{
if (m_KeysDirty || m_KeyTimes == null)
{
RebuildKeyCache();
}
return m_KeyTimes;
}
}
public float[] GetCurveTimes(EditorCurveBinding curve)
{
return GetCurveTimes(new[] { curve });
}
public float[] GetCurveTimes(EditorCurveBinding[] curves)
{
if (m_KeysDirty || m_KeyTimes == null)
{
RebuildKeyCache();
}
var keyTimes = new List<float>();
for (int i = 0; i < curves.Length; i++)
{
var c = curves[i];
if (m_individualBindinsKey.ContainsKey(c))
{
keyTimes.AddRange(m_individualBindinsKey[c]);
}
}
return keyTimes.ToArray();
}
void RebuildKeyCache()
{
m_individualBindinsKey = new Dictionary<EditorCurveBinding, float[]>();
List<float> keys = curves.SelectMany(y => y.keys).Select(z => z.time).ToList();
for (int i = 0; i < objectCurves.Count; i++)
{
var kf = objectCurves[i];
keys.AddRange(kf.Select(x => x.time));
}
for (int b = 0; b < bindings.Count(); b++)
{
m_individualBindinsKey.Add(bindings[b], curves[b].keys.Select(k => k.time).Distinct().ToArray());
}
m_KeyTimes = keys.OrderBy(x => x).Distinct().ToArray();
m_KeysDirty = false;
}
public void Update(AnimationClip clip)
{
List<EditorCurveBinding> postfilter = new List<EditorCurveBinding>();
var clipBindings = AnimationUtility.GetCurveBindings(clip);
for (int i = 0; i < clipBindings.Length; i++)
{
var bind = clipBindings[i];
if (!bind.propertyName.Contains("LocalRotation.w"))
postfilter.Add(RotationCurveInterpolation.RemapAnimationBindingForRotationCurves(bind, clip));
}
bindings = postfilter.ToArray();
curves = new AnimationCurve[bindings.Length];
for (int i = 0; i < bindings.Length; i++)
{
curves[i] = AnimationUtility.GetEditorCurve(clip, bindings[i]);
}
objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);
objectCurves = new List<ObjectReferenceKeyframe[]>(objectBindings.Length);
for (int i = 0; i < objectBindings.Length; i++)
{
objectCurves.Add(AnimationUtility.GetObjectReferenceCurve(clip, objectBindings[i]));
}
m_CurveDirty = false;
m_KeysDirty = true;
version = version + 1;
}
public bool GetBindingForCurve(AnimationCurve curve, ref EditorCurveBinding binding)
{
for (int i = 0; i < curves.Length; i++)
{
if (curve == curves[i])
{
binding = bindings[i];
return true;
}
}
return false;
}
public AnimationCurve GetCurveForBinding(EditorCurveBinding binding)
{
for (int i = 0; i < curves.Length; i++)
{
if (binding.Equals(bindings[i]))
{
return curves[i];
}
}
return null;
}
public ObjectReferenceKeyframe[] GetObjectCurveForBinding(EditorCurveBinding binding)
{
if (objectCurves == null)
return null;
for (int i = 0; i < objectCurves.Count; i++)
{
if (binding.Equals(objectBindings[i]))
{
return objectCurves[i];
}
}
return null;
}
// given a groupID, get the list of curve bindings
public CurveBindingGroup GetGroupBinding(string groupID)
{
if (m_groupings == null)
m_groupings = new Dictionary<string, CurveBindingGroup>();
CurveBindingGroup result = null;
if (!m_groupings.TryGetValue(groupID, out result))
{
result = new CurveBindingGroup();
result.timeRange = new Vector2(float.MaxValue, float.MinValue);
result.valueRange = new Vector2(float.MaxValue, float.MinValue);
List<CurveBindingPair> found = new List<CurveBindingPair>();
for (int i = 0; i < bindings.Length; i++)
{
if (bindings[i].GetGroupID() == groupID)
{
CurveBindingPair pair = new CurveBindingPair();
pair.binding = bindings[i];
pair.curve = curves[i];
found.Add(pair);
for (int k = 0; k < curves[i].keys.Length; k++)
{
var key = curves[i].keys[k];
result.timeRange = new Vector2(Mathf.Min(key.time, result.timeRange.x), Mathf.Max(key.time, result.timeRange.y));
result.valueRange = new Vector2(Mathf.Min(key.value, result.valueRange.x), Mathf.Max(key.value, result.valueRange.y));
}
}
}
for (int i = 0; i < objectBindings.Length; i++)
{
if (objectBindings[i].GetGroupID() == groupID)
{
CurveBindingPair pair = new CurveBindingPair();
pair.binding = objectBindings[i];
pair.objectCurve = objectCurves[i];
found.Add(pair);
for (int k = 0; k < objectCurves[i].Length; k++)
{
var key = objectCurves[i][k];
result.timeRange = new Vector2(Mathf.Min(key.time, result.timeRange.x), Mathf.Max(key.time, result.timeRange.y));
}
}
}
result.curveBindingPairs = found.OrderBy(x => AnimationWindowUtility.GetComponentIndex(x.binding.propertyName)).ToArray();
m_groupings.Add(groupID, result);
}
return result;
}
}
// Cache for storing the animation clip data
class AnimationClipCurveCache
{
static AnimationClipCurveCache s_Instance;
Dictionary<AnimationClip, AnimationClipCurveInfo> m_ClipCache = new Dictionary<AnimationClip, AnimationClipCurveInfo>();
bool m_IsEnabled;
public static AnimationClipCurveCache Instance
{
get
{
if (s_Instance == null)
{
s_Instance = new AnimationClipCurveCache();
}
return s_Instance;
}
}
public void OnEnable()
{
if (!m_IsEnabled)
{
AnimationUtility.onCurveWasModified += OnCurveWasModified;
m_IsEnabled = true;
}
}
public void OnDisable()
{
if (m_IsEnabled)
{
AnimationUtility.onCurveWasModified -= OnCurveWasModified;
m_IsEnabled = false;
}
}
// callback when a curve is edited. Force the cache to update next time it's accessed
void OnCurveWasModified(AnimationClip clip, EditorCurveBinding binding, AnimationUtility.CurveModifiedType modification)
{
AnimationClipCurveInfo data;
if (m_ClipCache.TryGetValue(clip, out data))
{
data.dirty = true;
}
}
public AnimationClipCurveInfo GetCurveInfo(AnimationClip clip)
{
AnimationClipCurveInfo data;
if (clip == null)
return null;
if (!m_ClipCache.TryGetValue(clip, out data))
{
data = new AnimationClipCurveInfo();
data.dirty = true;
m_ClipCache[clip] = data;
}
if (data.dirty)
{
data.Update(clip);
}
return data;
}
public void ClearCachedProxyClips()
{
var toRemove = new List<AnimationClip>();
foreach (var entry in m_ClipCache)
{
var clip = entry.Key;
if (clip != null && (clip.hideFlags & HideFlags.HideAndDontSave) == HideFlags.HideAndDontSave)
toRemove.Add(clip);
}
foreach (var clip in toRemove)
{
m_ClipCache.Remove(clip);
Object.DestroyImmediate(clip, true);
}
}
public void Clear()
{
ClearCachedProxyClips();
m_ClipCache.Clear();
}
}
static class EditorCurveBindingExtension
{
// identifier to generate an id thats the same for all curves in the same group
public static string GetGroupID(this EditorCurveBinding binding)
{
return binding.type + AnimationWindowUtility.GetPropertyGroupName(binding.propertyName);
}
}
static class CurveBindingGroupExtensions
{
// Extentions to determine curve types
public static bool IsEnableGroup(this CurveBindingGroup curves)
{
return curves.isFloatCurve && curves.count == 1 && curves.curveBindingPairs[0].binding.propertyName == "m_Enabled";
}
public static bool IsVectorGroup(this CurveBindingGroup curves)
{
if (!curves.isFloatCurve)
return false;
if (curves.count <= 1 || curves.count > 4)
return false;
char l = curves.curveBindingPairs[0].binding.propertyName.Last();
return l == 'x' || l == 'y' || l == 'z' || l == 'w';
}
public static bool IsColorGroup(this CurveBindingGroup curves)
{
if (!curves.isFloatCurve)
return false;
if (curves.count != 3 && curves.count != 4)
return false;
char l = curves.curveBindingPairs[0].binding.propertyName.Last();
return l == 'r' || l == 'g' || l == 'b' || l == 'a';
}
public static string GetDescription(this CurveBindingGroup group, float t)
{
string result = string.Empty;
if (group.isFloatCurve)
{
if (group.count > 1)
{
result += "(" + group.curveBindingPairs[0].curve.Evaluate(t).ToString("0.##");
for (int j = 1; j < group.curveBindingPairs.Length; j++)
{
result += "," + group.curveBindingPairs[j].curve.Evaluate(t).ToString("0.##");
}
result += ")";
}
else
{
result = group.curveBindingPairs[0].curve.Evaluate(t).ToString("0.##");
}
}
else if (group.isObjectCurve)
{
Object obj = null;
if (group.curveBindingPairs[0].objectCurve.Length > 0)
obj = CurveEditUtility.Evaluate(group.curveBindingPairs[0].objectCurve, t);
result = (obj == null ? "None" : obj.name);
}
return result;
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using UnityEngine;
namespace UnityEditor.Timeline
{
static class AnimationClipExtensions
{
public static UInt64 ClipVersion(this AnimationClip clip)
{
if (clip == null)
return 0;
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
var version = (UInt32)info.version;
var count = (UInt32)info.curves.Length;
var result = (UInt64)version;
result |= ((UInt64)count) << 32;
return result;
}
public static CurveChangeType GetChangeType(this AnimationClip clip, ref UInt64 curveVersion)
{
var version = clip.ClipVersion();
var changeType = CurveChangeType.None;
if ((curveVersion >> 32) != (version >> 32))
changeType = CurveChangeType.CurveAddedOrRemoved;
else if (curveVersion != version)
changeType = CurveChangeType.CurveModified;
curveVersion = version;
return changeType;
}
}
}

View File

@@ -0,0 +1,73 @@
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class AnimationOffsetMenu
{
public static string MatchFieldsPrefix = L10n.Tr("Match Offsets Fields/");
static bool EnforcePreviewMode()
{
TimelineEditor.state.previewMode = true; // try and set the preview mode
if (!TimelineEditor.state.previewMode)
{
Debug.LogError("Match clips cannot be completed because preview mode cannot be enabed");
return false;
}
return true;
}
internal static void MatchClipsToPrevious(TimelineClip[] clips)
{
if (!EnforcePreviewMode())
return;
clips = clips.OrderBy(x => x.start).ToArray();
foreach (var clip in clips)
{
var sceneObject = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, clip.GetParentTrack());
if (sceneObject != null)
{
TimelineAnimationUtilities.MatchPrevious(clip, sceneObject.transform, TimelineEditor.inspectedDirector);
}
}
InspectorWindow.RepaintAllInspectors();
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
internal static void MatchClipsToNext(TimelineClip[] clips)
{
if (!EnforcePreviewMode())
return;
clips = clips.OrderByDescending(x => x.start).ToArray();
foreach (var clip in clips)
{
var sceneObject = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, clip.GetParentTrack());
if (sceneObject != null)
{
TimelineAnimationUtilities.MatchNext(clip, sceneObject.transform, TimelineEditor.inspectedDirector);
}
}
InspectorWindow.RepaintAllInspectors();
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
public static void ResetClipOffsets(TimelineClip[] clips)
{
foreach (var clip in clips)
{
var asset = clip.asset as AnimationPlayableAsset;
if (asset != null)
asset.ResetOffsets();
}
InspectorWindow.RepaintAllInspectors();
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
}
}

View File

@@ -0,0 +1,65 @@
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[CustomTimelineEditor(typeof(AnimationPlayableAsset)), UsedImplicitly]
class AnimationPlayableAssetEditor : ClipEditor
{
public static readonly string k_NoClipAssignedError = L10n.Tr("No animation clip assigned");
public static readonly string k_LegacyClipError = L10n.Tr("Legacy animation clips are not supported");
static readonly string k_MotionCurveError = L10n.Tr("You are using motion curves without applyRootMotion enabled on the Animator. The root transform will not be animated");
static readonly string k_RootCurveError = L10n.Tr("You are using root curves without applyRootMotion enabled on the Animator. The root transform will not be animated");
/// <inheritdoc/>
public override ClipDrawOptions GetClipOptions(TimelineClip clip)
{
var clipOptions = base.GetClipOptions(clip);
var asset = clip.asset as AnimationPlayableAsset;
if (asset != null)
clipOptions.errorText = GetErrorText(asset, clip.GetParentTrack() as AnimationTrack, clipOptions.errorText);
if (clip.recordable)
clipOptions.highlightColor = DirectorStyles.Instance.customSkin.colorAnimationRecorded;
return clipOptions;
}
/// <inheritdoc />
public override void OnCreate(TimelineClip clip, TrackAsset track, TimelineClip clonedFrom)
{
var asset = clip.asset as AnimationPlayableAsset;
if (asset != null && asset.clip != null && asset.clip.legacy)
{
asset.clip = null;
Debug.LogError("Legacy Animation Clips are not supported");
}
}
string GetErrorText(AnimationPlayableAsset animationAsset, AnimationTrack track, string defaultError)
{
if (animationAsset.clip == null)
return k_NoClipAssignedError;
if (animationAsset.clip.legacy)
return k_LegacyClipError;
if (animationAsset.clip.hasMotionCurves || animationAsset.clip.hasRootCurves)
{
if (track != null && track.trackOffset == TrackOffset.Auto)
{
var animator = track.GetBinding(TimelineEditor.inspectedDirector);
if (animator != null && !animator.applyRootMotion && !animationAsset.clip.hasGenericRootTransform)
{
if (animationAsset.clip.hasMotionCurves)
return k_MotionCurveError;
return k_RootCurveError;
}
}
}
return defaultError;
}
}
}

View File

@@ -0,0 +1,151 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[MenuEntry("Add Override Track", MenuPriority.CustomTrackActionSection.addOverrideTrack), UsedImplicitly]
class AddOverrideTrackAction : TrackAction
{
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
foreach (var animTrack in tracks.OfType<AnimationTrack>())
{
TimelineHelpers.CreateTrack(typeof(AnimationTrack), animTrack, "Override " + animTrack.GetChildTracks().Count());
}
return true;
}
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
if (tracks.Any(t => t.isSubTrack || !t.GetType().IsAssignableFrom(typeof(AnimationTrack))))
return ActionValidity.NotApplicable;
if (tracks.Any(t => t.lockedInHierarchy))
return ActionValidity.Invalid;
return ActionValidity.Valid;
}
}
[MenuEntry("Convert To Clip Track", MenuPriority.CustomTrackActionSection.convertToClipMode), UsedImplicitly]
class ConvertToClipModeAction : TrackAction
{
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
foreach (var animTrack in tracks.OfType<AnimationTrack>())
animTrack.ConvertToClipMode();
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return true;
}
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
if (tracks.Any(t => !t.GetType().IsAssignableFrom(typeof(AnimationTrack))))
return ActionValidity.NotApplicable;
if (tracks.Any(t => t.lockedInHierarchy))
return ActionValidity.Invalid;
if (tracks.OfType<AnimationTrack>().All(a => a.CanConvertToClipMode()))
return ActionValidity.Valid;
return ActionValidity.NotApplicable;
}
}
[MenuEntry("Convert To Infinite Clip", MenuPriority.CustomTrackActionSection.convertFromClipMode), UsedImplicitly]
class ConvertFromClipTrackAction : TrackAction
{
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
foreach (var animTrack in tracks.OfType<AnimationTrack>())
animTrack.ConvertFromClipMode(TimelineEditor.inspectedAsset);
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return true;
}
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
if (tracks.Any(t => !t.GetType().IsAssignableFrom(typeof(AnimationTrack))))
return ActionValidity.NotApplicable;
if (tracks.Any(t => t.lockedInHierarchy))
return ActionValidity.Invalid;
if (tracks.OfType<AnimationTrack>().All(a => a.CanConvertFromClipMode()))
return ActionValidity.Valid;
return ActionValidity.NotApplicable;
}
}
abstract class TrackOffsetBaseAction : TrackAction
{
public abstract TrackOffset trackOffset { get; }
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
if (tracks.Any(t => !t.GetType().IsAssignableFrom(typeof(AnimationTrack))))
return ActionValidity.NotApplicable;
if (tracks.Any(t => t.lockedInHierarchy))
{
return ActionValidity.Invalid;
}
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
foreach (var animTrack in tracks.OfType<AnimationTrack>())
{
animTrack.UnarmForRecord();
animTrack.trackOffset = trackOffset;
}
TimelineEditor.Refresh(RefreshReason.ContentsModified);
return true;
}
}
[MenuEntry("Track Offsets/Apply Transform Offsets", MenuPriority.CustomTrackActionSection.applyTrackOffset), UsedImplicitly]
[ApplyDefaultUndo]
class ApplyTransformOffsetAction : TrackOffsetBaseAction
{
public override TrackOffset trackOffset
{
get { return TrackOffset.ApplyTransformOffsets; }
}
}
[MenuEntry("Track Offsets/Apply Scene Offsets", MenuPriority.CustomTrackActionSection.applySceneOffset), UsedImplicitly]
[ApplyDefaultUndo]
class ApplySceneOffsetAction : TrackOffsetBaseAction
{
public override TrackOffset trackOffset
{
get { return TrackOffset.ApplySceneOffsets; }
}
}
[MenuEntry("Track Offsets/Auto (Deprecated)", MenuPriority.CustomTrackActionSection.applyAutoOffset), UsedImplicitly]
[ApplyDefaultUndo]
class ApplyAutoAction : TrackOffsetBaseAction
{
public override TrackOffset trackOffset
{
get { return TrackOffset.Auto; }
}
}
}

View File

@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class BindingSelector
{
TreeViewController m_TreeView;
public TreeViewController treeViewController
{
get { return m_TreeView; }
}
TreeViewState m_TrackGlobalTreeViewState;
TreeViewState m_TreeViewState;
BindingTreeViewDataSource m_TreeViewDataSource;
CurveDataSource m_CurveDataSource;
TimelineWindow m_Window;
CurveEditor m_CurveEditor;
ReorderableList m_DopeLines;
string[] m_StringList = {};
int[] m_Selection;
bool m_PartOfSelection;
public BindingSelector(EditorWindow window, CurveEditor curveEditor, TreeViewState trackGlobalTreeViewState)
{
m_Window = window as TimelineWindow;
m_CurveEditor = curveEditor;
m_TrackGlobalTreeViewState = trackGlobalTreeViewState;
m_DopeLines = new ReorderableList(m_StringList, typeof(string), false, false, false, false);
m_DopeLines.drawElementBackgroundCallback = null;
m_DopeLines.showDefaultBackground = false;
m_DopeLines.index = 0;
m_DopeLines.headerHeight = 0;
m_DopeLines.elementHeight = 20;
m_DopeLines.draggable = false;
}
public void OnGUI(Rect targetRect)
{
if (m_TreeView == null)
return;
m_TreeView.OnEvent();
m_TreeView.OnGUI(targetRect, GUIUtility.GetControlID(FocusType.Passive));
}
public void InitIfNeeded(Rect rect, CurveDataSource dataSource, bool isNewSelection)
{
if (Event.current.type != EventType.Layout)
return;
m_CurveDataSource = dataSource;
var clip = dataSource.animationClip;
List<EditorCurveBinding> allBindings = new List<EditorCurveBinding>();
allBindings.Add(new EditorCurveBinding { propertyName = "Summary" });
if (clip != null)
allBindings.AddRange(AnimationUtility.GetCurveBindings(clip));
m_DopeLines.list = allBindings.ToArray();
if (m_TreeViewState != null)
{
if (isNewSelection)
RefreshAll();
return;
}
m_TreeViewState = m_TrackGlobalTreeViewState != null ? m_TrackGlobalTreeViewState : new TreeViewState();
m_TreeView = new TreeViewController(m_Window, m_TreeViewState)
{
useExpansionAnimation = false,
deselectOnUnhandledMouseDown = true
};
m_TreeView.selectionChangedCallback += OnItemSelectionChanged;
m_TreeViewDataSource = new BindingTreeViewDataSource(m_TreeView, clip, m_CurveDataSource);
m_TreeView.Init(rect, m_TreeViewDataSource, new BindingTreeViewGUI(m_TreeView), null);
m_TreeViewDataSource.UpdateData();
RefreshSelection();
}
void OnItemSelectionChanged(int[] selection)
{
RefreshSelection(selection);
}
void RefreshAll()
{
RefreshTree();
RefreshSelection();
}
void RefreshSelection()
{
RefreshSelection(m_TreeViewState.selectedIDs != null ? m_TreeViewState.selectedIDs.ToArray() : null);
}
void RefreshSelection(int[] selection)
{
if (selection == null || selection.Length == 0)
{
// select all.
if (m_TreeViewDataSource.GetRows().Count > 0)
{
m_Selection = m_TreeViewDataSource.GetRows().Select(r => r.id).ToArray();
}
}
else
{
m_Selection = selection;
}
RefreshCurves();
}
public void RefreshCurves()
{
if (m_CurveDataSource == null || m_Selection == null)
return;
var bindings = new HashSet<EditorCurveBinding>(AnimationPreviewUtilities.EditorCurveBindingComparer.Instance);
foreach (int s in m_Selection)
{
var item = (CurveTreeViewNode)m_TreeView.FindItem(s);
if (item != null && item.bindings != null)
bindings.UnionWith(item.bindings);
}
var wrappers = m_CurveDataSource.GenerateWrappers(bindings);
m_CurveEditor.animationCurves = wrappers.ToArray();
}
public void RefreshTree()
{
if (m_TreeViewDataSource == null)
return;
if (m_Selection == null)
m_Selection = new int[0];
// get the names of the previous items
var selected = m_Selection.Select(x => m_TreeViewDataSource.FindItem(x)).Where(t => t != null).Select(c => c.displayName).ToArray();
// update the source
m_TreeViewDataSource.UpdateData();
// find the same items
var reselected = m_TreeViewDataSource.GetRows().Where(x => selected.Contains(x.displayName)).Select(x => x.id).ToArray();
if (!reselected.Any())
{
if (m_TreeViewDataSource.GetRows().Count > 0)
{
reselected = new[] { m_TreeViewDataSource.GetItem(0).id };
}
}
// update the selection
OnItemSelectionChanged(reselected);
}
internal virtual bool IsRenamingNodeAllowed(TreeViewItem node)
{
return false;
}
}
}

View File

@@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Timeline;
#if !UNITY_2020_2_OR_NEWER
using L10n = UnityEditor.Timeline.L10n;
#endif
namespace UnityEditor.Timeline
{
class BindingTreeViewDataSource : TreeViewDataSource
{
struct BindingGroup : IEquatable<BindingGroup>, IComparable<BindingGroup>
{
public readonly string GroupName;
public readonly string Path;
public readonly Type Type;
public BindingGroup(string path, string groupName, Type type)
{
Path = path;
GroupName = groupName;
Type = type;
}
public string groupDisplayName => string.IsNullOrEmpty(Path) ? GroupName : string.Format($"{Path} : {GroupName}");
public bool Equals(BindingGroup other) => GroupName == other.GroupName && Type == other.Type && Path == other.Path;
public int CompareTo(BindingGroup other) => GetHashCode() - other.GetHashCode();
public override bool Equals(object obj) => obj is BindingGroup other && Equals(other);
public override int GetHashCode()
{
return HashUtility.CombineHash(GroupName != null ? GroupName.GetHashCode() : 0, Type != null ? Type.GetHashCode() : 0, Path != null ? Path.GetHashCode() : 0);
}
}
static readonly string s_DefaultValue = L10n.Tr("{0} (Default Value)");
public const int RootID = int.MinValue;
public const int GroupID = -1;
private readonly AnimationClip m_Clip;
private readonly CurveDataSource m_CurveDataSource;
public BindingTreeViewDataSource(
TreeViewController treeView, AnimationClip clip, CurveDataSource curveDataSource)
: base(treeView)
{
m_Clip = clip;
showRootItem = false;
m_CurveDataSource = curveDataSource;
}
void SetupRootNodeSettings()
{
showRootItem = false;
SetExpanded(RootID, true);
SetExpanded(GroupID, true);
}
public override void FetchData()
{
if (m_Clip == null)
return;
var bindings = AnimationUtility.GetCurveBindings(m_Clip)
.Union(AnimationUtility.GetObjectReferenceCurveBindings(m_Clip))
.ToArray();
// a sorted linear list of nodes
var results = bindings.GroupBy(GetBindingGroup, p => p, CreateTuple)
.OrderBy(t => t.Item1.Path)
.ThenBy(NamePrioritySort)
// this makes component ordering match the animation window
.ThenBy(t => t.Item1.Type.ToString())
.ThenBy(t => t.Item1.GroupName).ToArray();
m_RootItem = new CurveTreeViewNode(RootID, null, "root", null)
{
children = new List<TreeViewItem>(1)
};
if (results.Any())
{
var groupingNode = new CurveTreeViewNode(GroupID, m_RootItem, m_CurveDataSource.groupingName, bindings)
{
children = new List<TreeViewItem>()
};
m_RootItem.children.Add(groupingNode);
foreach (var r in results)
{
var key = r.Item1;
var nodeBindings = r.Item2;
FillMissingTransformCurves(nodeBindings);
if (nodeBindings.Count == 1)
groupingNode.children.Add(CreateLeafNode(nodeBindings[0], groupingNode, PropertyName(nodeBindings[0], true)));
else if (nodeBindings.Count > 1)
{
var childBindings = nodeBindings.OrderBy(BindingSort).ToArray();
var parent = new CurveTreeViewNode(key.GetHashCode(), groupingNode, key.groupDisplayName, childBindings) {children = new List<TreeViewItem>()};
groupingNode.children.Add(parent);
foreach (var b in childBindings)
parent.children.Add(CreateLeafNode(b, parent, PropertyName(b, false)));
}
}
SetupRootNodeSettings();
}
m_NeedRefreshRows = true;
}
public void UpdateData()
{
m_TreeView.ReloadData();
}
string GroupName(EditorCurveBinding binding)
{
var propertyName = m_CurveDataSource.ModifyPropertyDisplayName(binding.path, binding.propertyName);
return CleanUpArrayBinding(AnimationWindowUtility.NicifyPropertyGroupName(binding.type, propertyName), true);
}
static string CleanUpArrayBinding(string propertyName, bool isGroup)
{
const string arrayIndicator = ".Array.data[";
const string arrayDisplay = ".data[";
var arrayIndex = propertyName.LastIndexOf(arrayIndicator, StringComparison.Ordinal);
if (arrayIndex == -1)
return propertyName;
if (isGroup)
propertyName = propertyName.Substring(0, arrayIndex);
return propertyName.Replace(arrayIndicator, arrayDisplay);
}
string PropertyName(EditorCurveBinding binding, bool prependPathName)
{
var propertyName = m_CurveDataSource.ModifyPropertyDisplayName(binding.path, binding.propertyName);
propertyName = CleanUpArrayBinding(AnimationWindowUtility.GetPropertyDisplayName(propertyName), false);
if (binding.isPhantom)
propertyName = string.Format(s_DefaultValue, propertyName);
if (prependPathName && !string.IsNullOrEmpty(binding.path))
propertyName = $"{binding.path} : {propertyName}";
return propertyName;
}
BindingGroup GetBindingGroup(EditorCurveBinding binding)
{
return new BindingGroup(binding.path ?? string.Empty, GroupName(binding), binding.type);
}
static CurveTreeViewNode CreateLeafNode(EditorCurveBinding binding, TreeViewItem parent, string displayName)
{
return new CurveTreeViewNode(binding.GetHashCode(), parent, displayName, new[] { binding }, AnimationWindowUtility.ForceGrouping(binding));
}
static void FillMissingTransformCurves(List<EditorCurveBinding> bindings)
{
if (!AnimationWindowUtility.IsActualTransformCurve(bindings[0]) || bindings.Count >= 3)
return;
var binding = bindings[0];
var prefixPropertyName = binding.propertyName.Split('.').First();
binding.isPhantom = true;
if (!bindings.Any(p => p.propertyName.EndsWith(".x")))
{
binding.propertyName = prefixPropertyName + ".x";
bindings.Insert(0, binding);
}
if (!bindings.Any(p => p.propertyName.EndsWith(".y")))
{
binding.propertyName = prefixPropertyName + ".y";
bindings.Insert(1, binding);
}
if (!bindings.Any(p => p.propertyName.EndsWith(".z")))
{
binding.propertyName = prefixPropertyName + ".z";
bindings.Insert(2, binding);
}
}
// make sure vectors and colors are sorted correctly in their subgroups
static int BindingSort(EditorCurveBinding b)
{
return AnimationWindowUtility.GetComponentIndex(b.propertyName);
}
static int NamePrioritySort(ValueTuple<BindingGroup, List<EditorCurveBinding>> group)
{
if (group.Item1.Type != typeof(Transform))
return 0;
switch (group.Item1.GroupName)
{
case "Position": return Int32.MinValue;
case "Rotation": return Int32.MinValue + 1;
case "Scale": return Int32.MinValue + 2;
default: return 0;
}
}
static ValueTuple<BindingGroup, List<EditorCurveBinding>> CreateTuple(BindingGroup key, IEnumerable<EditorCurveBinding> items)
{
return new ValueTuple<BindingGroup, List<EditorCurveBinding>>()
{
Item1 = key,
Item2 = items.ToList()
};
}
}
}

View File

@@ -0,0 +1,113 @@
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class BindingTreeViewGUI : TreeViewGUI
{
static readonly float s_RowRightOffset = 10;
static readonly float s_ColorIndicatorTopMargin = 3;
static readonly Color s_KeyColorForNonCurves = new Color(0.7f, 0.7f, 0.7f, 0.5f);
static readonly Color s_ChildrenCurveLabelColor = new Color(1.0f, 1.0f, 1.0f, 0.7f);
static readonly Color s_PhantomPropertyLabelColor = new Color(0.0f, 0.8f, 0.8f, 1f);
static readonly Texture2D s_DefaultScriptTexture = EditorGUIUtility.LoadIcon("cs Script Icon");
static readonly Texture2D s_TrackDefault = EditorGUIUtility.LoadIcon("UnityEngine/ScriptableObject Icon");
public BindingTreeViewGUI(TreeViewController treeView)
: base(treeView, true)
{
k_IconWidth = 13.0f;
iconOverlayGUI += OnItemIconOverlay;
}
public override void OnRowGUI(Rect rowRect, TreeViewItem node, int row, bool selected, bool focused)
{
Color originalColor = GUI.color;
bool leafNode = node.parent != null && node.parent.id != BindingTreeViewDataSource.RootID && node.parent.id != BindingTreeViewDataSource.GroupID;
GUI.color = Color.white;
if (leafNode)
{
CurveTreeViewNode curveNode = node as CurveTreeViewNode;
if (curveNode != null && curveNode.bindings.Any() && curveNode.bindings.First().isPhantom)
GUI.color = s_PhantomPropertyLabelColor;
else
GUI.color = s_ChildrenCurveLabelColor;
}
base.OnRowGUI(rowRect, node, row, selected, focused);
GUI.color = originalColor;
DoCurveColorIndicator(rowRect, node as CurveTreeViewNode);
}
protected override bool IsRenaming(int id)
{
return false;
}
public override bool BeginRename(TreeViewItem item, float delay)
{
return false;
}
static void DoCurveColorIndicator(Rect rect, CurveTreeViewNode node)
{
if (node == null)
return;
if (Event.current.type != EventType.Repaint)
return;
Color originalColor = GUI.color;
if (node.bindings.Length == 1 && !node.bindings[0].isPPtrCurve)
GUI.color = CurveUtility.GetPropertyColor(node.bindings[0].propertyName);
else
GUI.color = s_KeyColorForNonCurves;
Texture icon = CurveUtility.GetIconCurve();
rect = new Rect(rect.xMax - s_RowRightOffset - (icon.width * 0.5f) - 5, rect.yMin + s_ColorIndicatorTopMargin, icon.width, icon.height);
GUI.DrawTexture(rect, icon, ScaleMode.ScaleToFit, true, 1);
GUI.color = originalColor;
}
protected override Texture GetIconForItem(TreeViewItem item)
{
var node = item as CurveTreeViewNode;
if (node == null)
return null;
var type = node.iconType;
if (type == null)
return null;
// track type icon
if (typeof(TrackAsset).IsAssignableFrom(type))
{
var icon = TrackResourceCache.GetTrackIconForType(type);
return icon == s_TrackDefault ? s_DefaultScriptTexture : icon;
}
// custom clip icons always use the script texture
if (typeof(PlayableAsset).IsAssignableFrom(type))
return s_DefaultScriptTexture;
// this will return null for MonoBehaviours without a custom icon.
// use the scripting icon instead
return AssetPreview.GetMiniTypeThumbnail(type) ?? s_DefaultScriptTexture;
}
static void OnItemIconOverlay(TreeViewItem item, Rect rect)
{
var curveNodeItem = item as CurveTreeViewNode;
if (curveNodeItem != null && curveNodeItem.iconOverlay != null)
GUI.Label(rect, curveNodeItem.iconOverlay);
}
}
}

View File

@@ -0,0 +1,367 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Timeline;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor
{
class ClipCurveEditor
{
static readonly GUIContent s_RemoveCurveContent = new GUIContent(L10n.Tr("Remove Curve"));
static readonly GUIContent s_RemoveCurvesContent = new GUIContent(L10n.Tr("Remove Curves"));
internal readonly CurveEditor m_CurveEditor;
static readonly CurveEditorSettings s_CurveEditorSettings = new CurveEditorSettings
{
hSlider = false,
vSlider = false,
hRangeLocked = false,
vRangeLocked = false,
scaleWithWindow = true,
hRangeMin = 0.0f,
showAxisLabels = true,
allowDeleteLastKeyInCurve = true,
rectangleToolFlags = CurveEditorSettings.RectangleToolFlags.MiniRectangleTool
};
static readonly float s_GridLabelWidth = 40.0f;
readonly BindingSelector m_BindingHierarchy;
public BindingSelector bindingHierarchy
{
get { return m_BindingHierarchy; }
}
public Rect shownAreaInsideMargins
{
get { return m_CurveEditor != null ? m_CurveEditor.shownAreaInsideMargins : new Rect(1, 1, 1, 1); }
}
Vector2 m_ScrollPosition = Vector2.zero;
readonly CurveDataSource m_DataSource;
float m_LastFrameRate = 30.0f;
UInt64 m_LastClipVersion = UInt64.MaxValue;
TrackViewModelData m_ViewModel;
bool m_ShouldRestoreShownArea;
bool isNewSelection
{
get
{
if (m_ViewModel == null || m_DataSource == null)
return true;
return m_ViewModel.lastInlineCurveDataID != m_DataSource.id;
}
}
internal CurveEditor curveEditor
{
get { return m_CurveEditor; }
}
public ClipCurveEditor(CurveDataSource dataSource, TimelineWindow parentWindow, TrackAsset hostTrack)
{
m_DataSource = dataSource;
m_CurveEditor = new CurveEditor(new Rect(0, 0, 1000, 100), new CurveWrapper[0], false);
s_CurveEditorSettings.vTickStyle = new TickStyle
{
tickColor = { color = DirectorStyles.Instance.customSkin.colorInlineCurveVerticalLines },
distLabel = 20,
stubs = true
};
s_CurveEditorSettings.hTickStyle = new TickStyle
{
// hide horizontal lines by giving them a transparent color
tickColor = { color = new Color(0.0f, 0.0f, 0.0f, 0.0f) },
distLabel = 0
};
m_CurveEditor.settings = s_CurveEditorSettings;
m_ViewModel = TimelineWindowViewPrefs.GetTrackViewModelData(hostTrack);
m_ShouldRestoreShownArea = true;
m_CurveEditor.ignoreScrollWheelUntilClicked = true;
m_CurveEditor.curvesUpdated = OnCurvesUpdated;
m_BindingHierarchy = new BindingSelector(parentWindow, m_CurveEditor, m_ViewModel.inlineCurvesState);
}
public void SelectAllKeys()
{
m_CurveEditor.SelectAll();
}
public void FrameClip()
{
m_CurveEditor.InvalidateBounds();
m_CurveEditor.FrameClip(false, true);
}
public CurveDataSource dataSource
{
get { return m_DataSource; }
}
// called when curves are edited
internal void OnCurvesUpdated()
{
if (m_DataSource == null)
return;
if (m_CurveEditor == null)
return;
if (m_CurveEditor.animationCurves.Length == 0)
return;
List<CurveWrapper> curvesToUpdate = m_CurveEditor.animationCurves.Where(c => c.changed).ToList();
// nothing changed, return.
if (curvesToUpdate.Count == 0)
return;
// something changed, manage the undo properly.
m_DataSource.ApplyCurveChanges(curvesToUpdate);
m_LastClipVersion = m_DataSource.GetClipVersion();
}
public void DrawHeader(Rect headerRect)
{
m_BindingHierarchy.InitIfNeeded(headerRect, m_DataSource, isNewSelection);
try
{
GUILayout.BeginArea(headerRect);
m_ScrollPosition = GUILayout.BeginScrollView(m_ScrollPosition, GUIStyle.none, GUI.skin.verticalScrollbar);
m_BindingHierarchy.OnGUI(new Rect(0, 0, headerRect.width, headerRect.height));
if (m_BindingHierarchy.treeViewController != null)
m_BindingHierarchy.treeViewController.contextClickItemCallback = ContextClickItemCallback;
GUILayout.EndScrollView();
GUILayout.EndArea();
}
catch (Exception e)
{
Debug.LogException(e);
}
}
void ContextClickItemCallback(int obj)
{
GenerateContextMenu(obj);
}
void GenerateContextMenu(int obj = -1)
{
if (Event.current.type != EventType.ContextClick)
return;
var selectedCurves = GetSelectedProperties().ToArray();
if (selectedCurves.Length > 0)
{
var menu = new GenericMenu();
var content = selectedCurves.Length == 1 ? s_RemoveCurveContent : s_RemoveCurvesContent;
menu.AddItem(content,
false,
() => RemoveCurves(selectedCurves)
);
menu.ShowAsContext();
}
}
public IEnumerable<EditorCurveBinding> GetSelectedProperties(bool useForcedGroups = false)
{
var bindings = new HashSet<EditorCurveBinding>();
var bindingTree = m_BindingHierarchy.treeViewController.data as BindingTreeViewDataSource;
foreach (var selectedId in m_BindingHierarchy.treeViewController.GetSelection())
{
var node = bindingTree.FindItem(selectedId) as CurveTreeViewNode;
if (node == null)
continue;
var curveNodeParent = node.parent as CurveTreeViewNode;
if (useForcedGroups && node.forceGroup && curveNodeParent != null)
bindings.UnionWith(curveNodeParent.bindings);
else
bindings.UnionWith(node.bindings);
}
return bindings;
}
public void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
{
m_DataSource.RemoveCurves(bindings);
m_BindingHierarchy.RefreshTree();
TimelineWindow.instance.state.CalculateRowRects();
m_LastClipVersion = m_DataSource.GetClipVersion();
}
class CurveEditorState : ICurveEditorState
{
public TimeArea.TimeFormat timeFormat { get; set; }
public Vector2 timeRange => new Vector2(0, 1);
public bool rippleTime => false;
}
void UpdateCurveEditorIfNeeded(WindowState state)
{
if ((Event.current.type != EventType.Layout) || (m_DataSource == null) || (m_BindingHierarchy == null))
return;
// check if the curves have changed externally
var curveChange = m_DataSource.UpdateExternalChanges(ref m_LastClipVersion);
if (curveChange == CurveChangeType.None)
return;
if (curveChange == CurveChangeType.CurveAddedOrRemoved)
m_BindingHierarchy.RefreshTree();
else // curve modified
m_BindingHierarchy.RefreshCurves();
m_CurveEditor.InvalidateSelectionBounds();
m_CurveEditor.state = new CurveEditorState() {timeFormat = state.timeFormat.ToTimeAreaFormat()};
m_CurveEditor.invSnap = state.referenceSequence.frameRate;
}
public void DrawCurveEditor(Rect rect, WindowState state, Vector2 clipRange, bool loop, bool selected)
{
SetupMarginsAndRect(rect, state);
UpdateCurveEditorIfNeeded(state);
if (m_ShouldRestoreShownArea)
RestoreShownArea();
var curveVisibleTimeRange = CalculateCurveVisibleTimeRange(state.timeAreaShownRange, m_DataSource);
m_CurveEditor.SetShownHRangeInsideMargins(curveVisibleTimeRange.x, curveVisibleTimeRange.y); //align the curve with the clip.
if (m_LastFrameRate != state.referenceSequence.frameRate)
{
m_CurveEditor.hTicks.SetTickModulosForFrameRate(state.referenceSequence.frameRate);
m_LastFrameRate = state.referenceSequence.frameRate;
}
foreach (var cw in m_CurveEditor.animationCurves)
cw.renderer.SetWrap(WrapMode.Default, loop ? WrapMode.Loop : WrapMode.Default);
using (new GUIGroupScope(rect))
{
var localRect = new Rect(0.0f, 0.0f, rect.width, rect.height);
var localClipRange = new Vector2(Mathf.Floor(clipRange.x - rect.xMin), Mathf.Ceil(clipRange.y - rect.xMin));
var curveStartPosX = Mathf.Floor(state.TimeToPixel(m_DataSource.start) - rect.xMin);
EditorGUI.DrawRect(new Rect(curveStartPosX, 0.0f, 1.0f, rect.height), new Color(1.0f, 1.0f, 1.0f, 0.5f));
DrawCurveEditorBackground(localRect);
if (selected)
{
var selectionRect = new Rect(localClipRange.x, 0.0f, localClipRange.y - localClipRange.x, localRect.height);
DrawOutline(selectionRect);
}
EditorGUI.BeginChangeCheck();
{
var evt = Event.current;
if (evt.type == EventType.Layout || evt.type == EventType.Repaint || selected)
m_CurveEditor.CurveGUI();
}
if (EditorGUI.EndChangeCheck())
OnCurvesUpdated();
DrawOverlay(localRect, localClipRange, DirectorStyles.Instance.customSkin.colorInlineCurveOutOfRangeOverlay);
DrawGrid(localRect, curveStartPosX);
}
}
static Vector2 CalculateCurveVisibleTimeRange(Vector2 timeAreaShownRange, CurveDataSource curve)
{
var curveVisibleTimeRange = new Vector2
{
x = Math.Max(0.0f, timeAreaShownRange.x - curve.start),
y = timeAreaShownRange.y - curve.start
};
return curveVisibleTimeRange * curve.timeScale;
}
void SetupMarginsAndRect(Rect rect, WindowState state)
{
var startX = state.TimeToPixel(m_DataSource.start) - rect.x;
var timelineWidth = state.timeAreaRect.width;
m_CurveEditor.rect = new Rect(0.0f, 0.0f, timelineWidth, rect.height);
m_CurveEditor.leftmargin = Math.Max(startX, 0.0f);
m_CurveEditor.rightmargin = 0.0f;
m_CurveEditor.topmargin = m_CurveEditor.bottommargin = CalculateTopMargin(rect.height);
}
void RestoreShownArea()
{
if (isNewSelection)
FrameClip();
else
m_CurveEditor.shownAreaInsideMargins = m_ViewModel.inlineCurvesShownAreaInsideMargins;
m_ShouldRestoreShownArea = false;
}
static void DrawCurveEditorBackground(Rect rect)
{
if (EditorGUIUtility.isProSkin)
return;
var animEditorBackgroundRect = Rect.MinMaxRect(0.0f, rect.yMin, rect.xMax, rect.yMax);
// Curves are not legible in Personal Skin so we need to darken the background a bit.
EditorGUI.DrawRect(animEditorBackgroundRect, DirectorStyles.Instance.customSkin.colorInlineCurvesBackground);
}
static float CalculateTopMargin(float height)
{
return Mathf.Clamp(0.15f * height, 10.0f, 40.0f);
}
static void DrawOutline(Rect rect, float thickness = 2.0f)
{
// Draw top selected lines.
EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMin, rect.width, thickness), Color.white);
// Draw bottom selected lines.
EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMax - thickness, rect.width, thickness), Color.white);
// Draw Left Selected Lines
EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMin, thickness, rect.height), Color.white);
// Draw Right Selected Lines
EditorGUI.DrawRect(new Rect(rect.xMax - thickness, rect.yMin, thickness, rect.height), Color.white);
}
static void DrawOverlay(Rect rect, Vector2 clipRange, Color color)
{
var leftSide = new Rect(rect.xMin, rect.yMin, clipRange.x - rect.xMin, rect.height);
EditorGUI.DrawRect(leftSide, color);
var rightSide = new Rect(Mathf.Max(0.0f, clipRange.y), rect.yMin, rect.xMax, rect.height);
EditorGUI.DrawRect(rightSide, color);
}
void DrawGrid(Rect rect, float curveXPosition)
{
var gridXPos = Mathf.Max(curveXPosition - s_GridLabelWidth, rect.xMin);
var gridRect = new Rect(gridXPos, rect.y, s_GridLabelWidth, rect.height);
var originalRect = m_CurveEditor.rect;
m_CurveEditor.rect = new Rect(0.0f, 0.0f, rect.width, rect.height);
using (new GUIGroupScope(gridRect))
m_CurveEditor.GridGUI();
m_CurveEditor.rect = originalRect;
}
}
}

View File

@@ -0,0 +1,439 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
enum CurveChangeType
{
None,
CurveModified,
CurveAddedOrRemoved
}
abstract class CurveDataSource
{
public static CurveDataSource Create(IRowGUI trackGUI)
{
if (trackGUI.asset is AnimationTrack)
return new InfiniteClipCurveDataSource(trackGUI);
return new TrackParametersCurveDataSource(trackGUI);
}
public static CurveDataSource Create(TimelineClipGUI clipGUI)
{
if (clipGUI.clip.animationClip != null)
return new ClipAnimationCurveDataSource(clipGUI);
return new ClipParametersCurveDataSource(clipGUI);
}
int? m_ID = null;
public int id
{
get
{
if (!m_ID.HasValue)
m_ID = CreateHashCode();
return m_ID.Value;
}
}
readonly IRowGUI m_TrackGUI;
protected CurveDataSource(IRowGUI trackGUI)
{
m_TrackGUI = trackGUI;
}
public abstract AnimationClip animationClip { get; }
public abstract float start { get; }
public abstract float timeScale { get; }
public abstract string groupingName { get; }
// Applies changes from the visual curve in the curve wrapper back to the animation clips
public virtual void ApplyCurveChanges(IEnumerable<CurveWrapper> updatedCurves)
{
Undo.RegisterCompleteObjectUndo(animationClip, "Edit Clip Curve");
foreach (CurveWrapper c in updatedCurves)
{
if (c.curve.length > 0)
AnimationUtility.SetEditorCurve(animationClip, c.binding, c.curve);
else
RemoveCurves(new[] {c.binding});
c.changed = false;
}
}
/// <summary>The clip version is a value that will change when a curve gets updated.
/// it's used to detect when an animation clip has been changed externally </summary>
/// <returns>A versioning value indicating the state of the curve. If the curve is updated externally this value will change. </returns>
public virtual UInt64 GetClipVersion()
{
return animationClip.ClipVersion();
}
/// <summary>Call this method to check if the underlying clip has changed</summary>
/// <param name="curveVersion">A versioning value. This will be updated to the latest version</param>
/// <returns>A value indicating how the clip has changed</returns>
public virtual CurveChangeType UpdateExternalChanges(ref UInt64 curveVersion)
{
return animationClip.GetChangeType(ref curveVersion);
}
public virtual string ModifyPropertyDisplayName(string path, string propertyName) => propertyName;
public virtual void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
{
Undo.RegisterCompleteObjectUndo(animationClip, "Remove Curve(s)");
foreach (var binding in bindings)
{
if (binding.isPPtrCurve)
AnimationUtility.SetObjectReferenceCurve(animationClip, binding, null);
else
AnimationUtility.SetEditorCurve(animationClip, binding, null);
}
}
public Rect GetBackgroundRect(WindowState state)
{
var trackRect = m_TrackGUI.boundingRect;
return new Rect(
state.timeAreaTranslation.x + trackRect.xMin,
trackRect.y,
(float)state.editSequence.asset.duration * state.timeAreaScale.x,
trackRect.height
);
}
public List<CurveWrapper> GenerateWrappers(IEnumerable<EditorCurveBinding> bindings)
{
var wrappers = new List<CurveWrapper>(bindings.Count());
int curveWrapperId = 0;
foreach (EditorCurveBinding b in bindings)
{
// General configuration
var wrapper = new CurveWrapper
{
id = curveWrapperId++,
binding = b,
groupId = -1,
hidden = false,
readOnly = false,
getAxisUiScalarsCallback = () => new Vector2(1, 1)
};
// Specific configuration
ConfigureCurveWrapper(wrapper);
wrappers.Add(wrapper);
}
return wrappers;
}
protected virtual void ConfigureCurveWrapper(CurveWrapper wrapper)
{
wrapper.color = CurveUtility.GetPropertyColor(wrapper.binding.propertyName);
wrapper.renderer = new NormalCurveRenderer(AnimationUtility.GetEditorCurve(animationClip, wrapper.binding));
wrapper.renderer.SetCustomRange(0.0f, animationClip.length);
}
protected virtual int CreateHashCode()
{
return m_TrackGUI.asset.GetHashCode();
}
}
class ClipAnimationCurveDataSource : CurveDataSource
{
static readonly string k_GroupingName = L10n.Tr("Animated Values");
readonly TimelineClipGUI m_ClipGUI;
public ClipAnimationCurveDataSource(TimelineClipGUI clipGUI) : base(clipGUI.parent)
{
m_ClipGUI = clipGUI;
}
public override AnimationClip animationClip
{
get { return m_ClipGUI.clip.animationClip; }
}
public override float start
{
get { return (float)m_ClipGUI.clip.FromLocalTimeUnbound(0.0); }
}
public override float timeScale
{
get { return (float)m_ClipGUI.clip.timeScale; }
}
public override string groupingName
{
get { return k_GroupingName; }
}
protected override int CreateHashCode()
{
return base.CreateHashCode().CombineHash(m_ClipGUI.clip.GetHashCode());
}
public override string ModifyPropertyDisplayName(string path, string propertyName)
{
if (!AnimatedPropertyUtility.IsMaterialProperty(propertyName))
return propertyName;
var track = m_ClipGUI.clip.GetParentTrack();
if (track == null)
return propertyName;
var gameObjectBinding = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, track);
if (gameObjectBinding == null)
return propertyName;
if (!string.IsNullOrEmpty(path))
{
var transform = gameObjectBinding.transform.Find(path);
if (transform == null)
return propertyName;
gameObjectBinding = transform.gameObject;
}
return AnimatedPropertyUtility.RemapMaterialName(gameObjectBinding, propertyName);
}
}
class ClipParametersCurveDataSource : CurveDataSource
{
static readonly string k_GroupingName = L10n.Tr("Clip Properties");
readonly TimelineClipGUI m_ClipGUI;
readonly CurvesProxy m_CurvesProxy;
private int m_ClipDirtyVersion;
public ClipParametersCurveDataSource(TimelineClipGUI clipGUI) : base(clipGUI.parent)
{
m_ClipGUI = clipGUI;
m_CurvesProxy = new CurvesProxy(clipGUI.clip);
}
public override AnimationClip animationClip
{
get { return m_CurvesProxy.curves; }
}
public override UInt64 GetClipVersion()
{
return sourceAnimationClip.ClipVersion();
}
public override CurveChangeType UpdateExternalChanges(ref ulong curveVersion)
{
if (m_ClipGUI == null || m_ClipGUI.clip == null)
return CurveChangeType.None;
var changeType = sourceAnimationClip.GetChangeType(ref curveVersion);
if (changeType != CurveChangeType.None)
{
m_CurvesProxy.ApplyExternalChangesToProxy();
}
else if (m_ClipDirtyVersion != m_ClipGUI.clip.DirtyIndex)
{
m_CurvesProxy.UpdateProxyCurves();
if (changeType == CurveChangeType.None)
changeType = CurveChangeType.CurveModified;
}
m_ClipDirtyVersion = m_ClipGUI.clip.DirtyIndex;
return changeType;
}
public override float start
{
get { return (float)m_ClipGUI.clip.FromLocalTimeUnbound(0.0); }
}
public override float timeScale
{
get { return (float)m_ClipGUI.clip.timeScale; }
}
public override string groupingName
{
get { return k_GroupingName; }
}
public override void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
{
m_CurvesProxy.RemoveCurves(bindings);
}
public override void ApplyCurveChanges(IEnumerable<CurveWrapper> updatedCurves)
{
m_CurvesProxy.UpdateCurves(updatedCurves);
}
protected override void ConfigureCurveWrapper(CurveWrapper wrapper)
{
m_CurvesProxy.ConfigureCurveWrapper(wrapper);
}
protected override int CreateHashCode()
{
return base.CreateHashCode().CombineHash(m_ClipGUI.clip.GetHashCode());
}
private AnimationClip sourceAnimationClip
{
get
{
if (m_ClipGUI == null || m_ClipGUI.clip == null || m_ClipGUI.clip.curves == null)
return null;
return m_ClipGUI.clip.curves;
}
}
}
class InfiniteClipCurveDataSource : CurveDataSource
{
static readonly string k_GroupingName = L10n.Tr("Animated Values");
readonly AnimationTrack m_AnimationTrack;
public InfiniteClipCurveDataSource(IRowGUI trackGui) : base(trackGui)
{
m_AnimationTrack = trackGui.asset as AnimationTrack;
}
public override AnimationClip animationClip
{
get { return m_AnimationTrack.infiniteClip; }
}
public override float start
{
get { return 0.0f; }
}
public override float timeScale
{
get { return 1.0f; }
}
public override string groupingName
{
get { return k_GroupingName; }
}
public override string ModifyPropertyDisplayName(string path, string propertyName)
{
if (m_AnimationTrack == null || !AnimatedPropertyUtility.IsMaterialProperty(propertyName))
return propertyName;
var binding = m_AnimationTrack.GetBinding(TimelineEditor.inspectedDirector);
if (binding == null)
return propertyName;
var target = binding.transform;
if (!string.IsNullOrEmpty(path))
target = target.Find(path);
if (target == null)
return propertyName;
return AnimatedPropertyUtility.RemapMaterialName(target.gameObject, propertyName);
}
}
class TrackParametersCurveDataSource : CurveDataSource
{
static readonly string k_GroupingName = L10n.Tr("Track Properties");
readonly CurvesProxy m_CurvesProxy;
private int m_TrackDirtyVersion;
public TrackParametersCurveDataSource(IRowGUI trackGui) : base(trackGui)
{
m_CurvesProxy = new CurvesProxy(trackGui.asset);
}
public override AnimationClip animationClip
{
get { return m_CurvesProxy.curves; }
}
public override UInt64 GetClipVersion()
{
return sourceAnimationClip.ClipVersion();
}
public override CurveChangeType UpdateExternalChanges(ref ulong curveVersion)
{
if (m_CurvesProxy.targetTrack == null)
return CurveChangeType.None;
var changeType = sourceAnimationClip.GetChangeType(ref curveVersion);
if (changeType != CurveChangeType.None)
{
m_CurvesProxy.ApplyExternalChangesToProxy();
}
// track property has changed externally, update the curve proxies
else if (m_TrackDirtyVersion != m_CurvesProxy.targetTrack.DirtyIndex)
{
if (changeType == CurveChangeType.None)
changeType = CurveChangeType.CurveModified;
m_CurvesProxy.UpdateProxyCurves();
}
m_TrackDirtyVersion = m_CurvesProxy.targetTrack.DirtyIndex;
return changeType;
}
public override float start
{
get { return 0.0f; }
}
public override float timeScale
{
get { return 1.0f; }
}
public override string groupingName
{
get { return k_GroupingName; }
}
public override void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
{
m_CurvesProxy.RemoveCurves(bindings);
}
public override void ApplyCurveChanges(IEnumerable<CurveWrapper> updatedCurves)
{
m_CurvesProxy.UpdateCurves(updatedCurves);
}
protected override void ConfigureCurveWrapper(CurveWrapper wrapper)
{
m_CurvesProxy.ConfigureCurveWrapper(wrapper);
}
private AnimationClip sourceAnimationClip
{
get
{
if (m_CurvesProxy.targetTrack == null || m_CurvesProxy.targetTrack.curves == null)
return null;
return m_CurvesProxy.targetTrack.curves;
}
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
namespace UnityEditor.Timeline
{
class CurveTreeViewNode : TreeViewItem
{
public bool forceGroup { get; }
public System.Type iconType { get; }
public GUIContent iconOverlay { get; }
EditorCurveBinding[] m_Bindings;
public EditorCurveBinding[] bindings
{
get { return m_Bindings; }
}
public CurveTreeViewNode(int id, TreeViewItem parent, string displayName, EditorCurveBinding[] bindings, bool _forceGroup = false)
: base(id, parent != null ? parent.depth + 1 : -1, parent, displayName)
{
m_Bindings = bindings;
forceGroup = _forceGroup;
// capture the preview icon type. If all subbindings are the same type, use that. Otherwise use null as a default
iconType = null;
if (parent != null && parent.depth >= 0 && bindings != null && bindings.Length > 0 && bindings.All(b => b.type == bindings[0].type))
{
iconType = bindings[0].type;
// for components put the component type in a tooltip
if (iconType != null && typeof(Component).IsAssignableFrom(iconType))
iconOverlay = new GUIContent(string.Empty, ObjectNames.NicifyVariableName(iconType.Name));
}
}
}
}

View File

@@ -0,0 +1,358 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.Timeline;
using UnityObject = UnityEngine.Object;
namespace UnityEditor.Timeline
{
class CurvesProxy : ICurvesOwner
{
public AnimationClip curves
{
get { return proxyCurves != null ? proxyCurves : m_OriginalOwner.curves; }
}
public bool hasCurves
{
get { return m_IsAnimatable || m_OriginalOwner.hasCurves; }
}
public double duration
{
get { return m_OriginalOwner.duration; }
}
public string defaultCurvesName
{
get { return m_OriginalOwner.defaultCurvesName; }
}
public UnityObject asset
{
get { return m_OriginalOwner.asset; }
}
public UnityObject assetOwner
{
get { return m_OriginalOwner.assetOwner; }
}
public TrackAsset targetTrack
{
get { return m_OriginalOwner.targetTrack; }
}
readonly ICurvesOwner m_OriginalOwner;
readonly bool m_IsAnimatable;
readonly Dictionary<EditorCurveBinding, SerializedProperty> m_PropertiesMap = new Dictionary<EditorCurveBinding, SerializedProperty>();
int m_ProxyIsRebuilding = 0;
AnimationClip m_ProxyCurves;
AnimationClip proxyCurves
{
get
{
if (!m_IsAnimatable) return null;
if (m_ProxyCurves == null)
RebuildProxyCurves();
return m_ProxyCurves;
}
}
public CurvesProxy([NotNull] ICurvesOwner originalOwner)
{
m_OriginalOwner = originalOwner;
m_IsAnimatable = originalOwner.HasAnyAnimatableParameters();
RebuildProxyCurves();
}
public void CreateCurves(string curvesClipName)
{
m_OriginalOwner.CreateCurves(curvesClipName);
}
public void ConfigureCurveWrapper(CurveWrapper wrapper)
{
var color = CurveUtility.GetPropertyColor(wrapper.binding.propertyName);
wrapper.color = color;
float h, s, v;
Color.RGBToHSV(color, out h, out s, out v);
wrapper.wrapColorMultiplier = Color.HSVToRGB(h, s * 0.33f, v * 1.15f);
var curve = AnimationUtility.GetEditorCurve(proxyCurves, wrapper.binding);
wrapper.renderer = new NormalCurveRenderer(curve);
// Use curve length instead of animation clip length
wrapper.renderer.SetCustomRange(0.0f, curve.keys.Last().time);
}
public void RebuildCurves()
{
RebuildProxyCurves();
}
public void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
{
if (m_ProxyIsRebuilding > 0 || !m_OriginalOwner.hasCurves)
return;
Undo.RegisterCompleteObjectUndo(m_OriginalOwner.curves, L10n.Tr("Remove Clip Curve"));
foreach (var binding in bindings)
AnimationUtility.SetEditorCurve(m_OriginalOwner.curves, binding, null);
m_OriginalOwner.SanitizeCurvesData();
RebuildProxyCurves();
}
public void UpdateCurves(IEnumerable<CurveWrapper> updatedCurves)
{
if (m_ProxyIsRebuilding > 0)
return;
Undo.RegisterCompleteObjectUndo(m_OriginalOwner.asset, L10n.Tr("Edit Clip Curve"));
if (m_OriginalOwner.curves != null)
Undo.RegisterCompleteObjectUndo(m_OriginalOwner.curves, L10n.Tr("Edit Clip Curve"));
var requireRebuild = false;
foreach (var curve in updatedCurves)
{
requireRebuild |= curve.curve.length == 0;
UpdateCurve(curve.binding, curve.curve);
}
if (requireRebuild)
m_OriginalOwner.SanitizeCurvesData();
AnimatedParameterUtility.UpdateSerializedPlayableAsset(m_OriginalOwner.asset);
}
public void ApplyExternalChangesToProxy()
{
using (new RebuildGuard(this))
{
if (m_OriginalOwner.curves == null)
return;
var curveInfo = AnimationClipCurveCache.Instance.GetCurveInfo(m_OriginalOwner.curves);
for (int i = 0; i < curveInfo.bindings.Length; i++)
{
if (curveInfo.curves[i] != null && curveInfo.curves.Length != 0)
{
if (m_PropertiesMap.TryGetValue(curveInfo.bindings[i], out var prop) && AnimatedParameterUtility.IsParameterAnimatable(prop))
AnimationUtility.SetEditorCurve(m_ProxyCurves, curveInfo.bindings[i], curveInfo.curves[i]);
}
}
}
}
void UpdateCurve(EditorCurveBinding binding, AnimationCurve curve)
{
ApplyConstraints(binding, curve);
if (curve.length == 0)
{
HandleAllKeysDeleted(binding);
return;
}
// there is no curve in the animation clip, this is a proxy curve
if (IsConstantCurve(binding, curve))
HandleConstantCurveValueChanged(binding, curve);
else
HandleCurveUpdated(binding, curve);
}
bool IsConstantCurve(EditorCurveBinding binding, AnimationCurve curve)
{
if (curve.length != 1)
return false;
return m_OriginalOwner.curves == null || AnimationUtility.GetEditorCurve(m_OriginalOwner.curves, binding) == null;
}
void ApplyConstraints(EditorCurveBinding binding, AnimationCurve curve)
{
if (curve.length == 0)
return;
var curveUpdated = false;
var property = m_PropertiesMap[binding];
if (property.propertyType == SerializedPropertyType.Boolean)
{
TimelineAnimationUtilities.ConstrainCurveToBooleanValues(curve);
curveUpdated = true;
}
else
{
var range = AnimatedParameterUtility.GetAttributeForProperty<RangeAttribute>(property);
if (range != null)
{
TimelineAnimationUtilities.ConstrainCurveToRange(curve, range.min, range.max);
curveUpdated = true;
}
}
if (!curveUpdated)
return;
using (new RebuildGuard(this))
{
AnimationUtility.SetEditorCurve(m_ProxyCurves, binding, curve);
}
}
void HandleCurveUpdated(EditorCurveBinding binding, AnimationCurve updatedCurve)
{
if (!m_OriginalOwner.hasCurves)
m_OriginalOwner.CreateCurves(null);
AnimationUtility.SetEditorCurve(m_OriginalOwner.curves, binding, updatedCurve);
AnimationUtility.SetEditorCurve(m_ProxyCurves, binding, updatedCurve);
}
void HandleConstantCurveValueChanged(EditorCurveBinding binding, AnimationCurve updatedCurve)
{
var prop = m_PropertiesMap[binding];
if (prop == null)
return;
Undo.RegisterCompleteObjectUndo(prop.serializedObject.targetObject, L10n.Tr("Edit Clip Curve"));
prop.serializedObject.UpdateIfRequiredOrScript();
CurveEditUtility.SetFromKeyValue(prop, updatedCurve.keys[0].value);
prop.serializedObject.ApplyModifiedProperties();
AnimationUtility.SetEditorCurve(m_ProxyCurves, binding, updatedCurve);
}
void HandleAllKeysDeleted(EditorCurveBinding binding)
{
if (m_OriginalOwner.hasCurves)
{
// Remove curve from original asset
AnimationUtility.SetEditorCurve(m_OriginalOwner.curves, binding, null);
SetProxyCurve(m_PropertiesMap[binding], binding);
}
}
void RebuildProxyCurves()
{
if (!m_IsAnimatable)
return;
using (new RebuildGuard(this))
{
if (m_ProxyCurves == null)
{
m_ProxyCurves = new AnimationClip
{
legacy = true,
name = "Constant Curves",
hideFlags = HideFlags.HideAndDontSave,
frameRate = m_OriginalOwner.targetTrack.timelineAsset == null
? TimelineAsset.EditorSettings.kDefaultFps
: m_OriginalOwner.targetTrack.timelineAsset.editorSettings.fps
};
}
else
{
m_ProxyCurves.ClearCurves();
}
m_OriginalOwner.SanitizeCurvesData();
AnimatedParameterUtility.UpdateSerializedPlayableAsset(m_OriginalOwner.asset);
var parameters = m_OriginalOwner.GetAllAnimatableParameters().ToArray();
foreach (var param in parameters)
CreateProxyCurve(param, m_ProxyCurves, m_OriginalOwner.asset, param.propertyPath);
AnimationClipCurveCache.Instance.GetCurveInfo(m_ProxyCurves).dirty = true;
}
}
// updates the just the proxied values. This can be called when the asset changes, so the proxy values are properly updated
public void UpdateProxyCurves()
{
if (!m_IsAnimatable || m_ProxyCurves == null || m_ProxyCurves.empty)
return;
AnimatedParameterUtility.UpdateSerializedPlayableAsset(m_OriginalOwner.asset);
var parameters = m_OriginalOwner.GetAllAnimatableParameters().ToArray();
using (new RebuildGuard(this))
{
if (m_OriginalOwner.hasCurves)
{
var bindingInfo = AnimationClipCurveCache.Instance.GetCurveInfo(m_OriginalOwner.curves);
foreach (var param in parameters)
{
var binding = AnimatedParameterUtility.GetCurveBinding(m_OriginalOwner.asset, param.propertyPath);
if (!bindingInfo.bindings.Contains(binding, AnimationPreviewUtilities.EditorCurveBindingComparer.Instance))
SetProxyCurve(param, AnimatedParameterUtility.GetCurveBinding(m_OriginalOwner.asset, param.propertyPath));
}
}
else
{
foreach (var param in parameters)
SetProxyCurve(param, AnimatedParameterUtility.GetCurveBinding(m_OriginalOwner.asset, param.propertyPath));
}
}
AnimationClipCurveCache.Instance.GetCurveInfo(m_ProxyCurves).dirty = true;
}
void CreateProxyCurve(SerializedProperty prop, AnimationClip clip, UnityObject owner, string propertyName)
{
var binding = AnimatedParameterUtility.GetCurveBinding(owner, propertyName);
var originalCurve = m_OriginalOwner.hasCurves
? AnimationUtility.GetEditorCurve(m_OriginalOwner.curves, binding)
: null;
if (originalCurve != null)
{
AnimationUtility.SetEditorCurve(clip, binding, originalCurve);
}
else
{
SetProxyCurve(prop, binding);
}
m_PropertiesMap[binding] = prop;
}
void SetProxyCurve(SerializedProperty prop, EditorCurveBinding binding)
{
var curve = new AnimationCurve();
CurveEditUtility.AddKeyFrameToCurve(
curve, 0.0f, m_ProxyCurves.frameRate, CurveEditUtility.GetKeyValue(prop),
prop.propertyType == SerializedPropertyType.Boolean);
AnimationUtility.SetEditorCurve(m_ProxyCurves, binding, curve);
}
struct RebuildGuard : IDisposable
{
CurvesProxy m_Owner;
AnimationUtility.OnCurveWasModified m_Callback;
public RebuildGuard(CurvesProxy owner)
{
m_Callback = AnimationUtility.onCurveWasModified;
AnimationUtility.onCurveWasModified = null;
m_Owner = owner;
m_Owner.m_ProxyIsRebuilding++;
}
public void Dispose()
{
AnimationUtility.onCurveWasModified = m_Callback;
m_Owner.m_ProxyIsRebuilding--;
m_Owner = null;
}
}
}
}

View File

@@ -0,0 +1,435 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEngineInternal;
using UnityEngine.Timeline;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace UnityEditor.Timeline
{
class TimelineAnimationUtilities
{
public enum OffsetEditMode
{
None = -1,
Translation = 0,
Rotation = 1
}
public static bool ValidateOffsetAvailabitity(PlayableDirector director, Animator animator)
{
if (director == null || animator == null)
return false;
return true;
}
public static TimelineClip GetPreviousClip(TimelineClip clip)
{
TimelineClip previousClip = null;
foreach (var c in clip.GetParentTrack().clips)
{
if (c.start < clip.start && (previousClip == null || c.start >= previousClip.start))
previousClip = c;
}
return previousClip;
}
public static TimelineClip GetNextClip(TimelineClip clip)
{
return clip.GetParentTrack().clips.Where(c => c.start > clip.start).OrderBy(c => c.start).FirstOrDefault();
}
public struct RigidTransform
{
public Vector3 position;
public Quaternion rotation;
public static RigidTransform Compose(Vector3 pos, Quaternion rot)
{
RigidTransform ret;
ret.position = pos;
ret.rotation = rot;
return ret;
}
public static RigidTransform Mul(RigidTransform a, RigidTransform b)
{
RigidTransform ret;
ret.rotation = a.rotation * b.rotation;
ret.position = a.position + a.rotation * b.position;
return ret;
}
public static RigidTransform Inverse(RigidTransform a)
{
RigidTransform ret;
ret.rotation = Quaternion.Inverse(a.rotation);
ret.position = ret.rotation * (-a.position);
return ret;
}
public static RigidTransform identity
{
get { return Compose(Vector3.zero, Quaternion.identity); }
}
}
private static Matrix4x4 GetTrackMatrix(Transform transform, AnimationTrack track)
{
Matrix4x4 trackMatrix = Matrix4x4.TRS(track.position, track.rotation, Vector3.one);
// in scene off mode, the track offsets are set to the preview position which is stored in the track
if (track.trackOffset == TrackOffset.ApplySceneOffsets)
{
trackMatrix = Matrix4x4.TRS(track.sceneOffsetPosition, Quaternion.Euler(track.sceneOffsetRotation), Vector3.one);
}
// put the parent transform on to the track matrix
if (transform.parent != null)
{
trackMatrix = transform.parent.localToWorldMatrix * trackMatrix;
}
return trackMatrix;
}
// Given a world space position and rotation, updates the clip offsets to match that
public static RigidTransform UpdateClipOffsets(AnimationPlayableAsset asset, AnimationTrack track, Transform transform, Vector3 globalPosition, Quaternion globalRotation)
{
Matrix4x4 worldToLocal = transform.worldToLocalMatrix;
Matrix4x4 clipMatrix = Matrix4x4.TRS(asset.position, asset.rotation, Vector3.one);
Matrix4x4 trackMatrix = GetTrackMatrix(transform, track);
// Use the transform to find the proper goal matrix with scale taken into account
var oldPos = transform.position;
var oldRot = transform.rotation;
transform.position = globalPosition;
transform.rotation = globalRotation;
Matrix4x4 goal = transform.localToWorldMatrix;
transform.position = oldPos;
transform.rotation = oldRot;
// compute the new clip matrix.
Matrix4x4 newClip = trackMatrix.inverse * goal * worldToLocal * trackMatrix * clipMatrix;
return RigidTransform.Compose(newClip.GetColumn(3), MathUtils.QuaternionFromMatrix(newClip));
}
public static RigidTransform GetTrackOffsets(AnimationTrack track, Transform transform)
{
Vector3 position = track.position;
Quaternion rotation = track.rotation;
if (transform != null && transform.parent != null)
{
position = transform.parent.TransformPoint(position);
rotation = transform.parent.rotation * rotation;
MathUtils.QuaternionNormalize(ref rotation);
}
return RigidTransform.Compose(position, rotation);
}
public static void UpdateTrackOffset(AnimationTrack track, Transform transform, RigidTransform offsets)
{
if (transform != null && transform.parent != null)
{
offsets.position = transform.parent.InverseTransformPoint(offsets.position);
offsets.rotation = Quaternion.Inverse(transform.parent.rotation) * offsets.rotation;
MathUtils.QuaternionNormalize(ref offsets.rotation);
}
track.position = offsets.position;
track.eulerAngles = AnimationUtility.GetClosestEuler(offsets.rotation, track.eulerAngles, RotationOrder.OrderZXY);
track.UpdateClipOffsets();
}
static MatchTargetFields GetMatchFields(TimelineClip clip)
{
var track = clip.GetParentTrack() as AnimationTrack;
if (track == null)
return MatchTargetFieldConstants.None;
var asset = clip.asset as AnimationPlayableAsset;
var fields = track.matchTargetFields;
if (asset != null && !asset.useTrackMatchFields)
fields = asset.matchTargetFields;
return fields;
}
static void WriteMatchFields(AnimationPlayableAsset asset, RigidTransform result, MatchTargetFields fields)
{
Vector3 position = asset.position;
position.x = fields.HasAny(MatchTargetFields.PositionX) ? result.position.x : position.x;
position.y = fields.HasAny(MatchTargetFields.PositionY) ? result.position.y : position.y;
position.z = fields.HasAny(MatchTargetFields.PositionZ) ? result.position.z : position.z;
asset.position = position;
// check first to avoid unnecessary conversion errors
if (fields.HasAny(MatchTargetFieldConstants.Rotation))
{
Vector3 eulers = asset.eulerAngles;
Vector3 resultEulers = result.rotation.eulerAngles;
eulers.x = fields.HasAny(MatchTargetFields.RotationX) ? resultEulers.x : eulers.x;
eulers.y = fields.HasAny(MatchTargetFields.RotationY) ? resultEulers.y : eulers.y;
eulers.z = fields.HasAny(MatchTargetFields.RotationZ) ? resultEulers.z : eulers.z;
asset.eulerAngles = AnimationUtility.GetClosestEuler(Quaternion.Euler(eulers), asset.eulerAngles, RotationOrder.OrderZXY);
}
}
public static void MatchPrevious(TimelineClip currentClip, Transform matchPoint, PlayableDirector director)
{
const double timeEpsilon = 0.00001;
MatchTargetFields matchFields = GetMatchFields(currentClip);
if (matchFields == MatchTargetFieldConstants.None || matchPoint == null)
return;
double cachedTime = director.time;
// finds previous clip
TimelineClip previousClip = GetPreviousClip(currentClip);
if (previousClip == null || currentClip == previousClip)
return;
// make sure the transform is properly updated before modifying the graph
director.Evaluate();
var parentTrack = currentClip.GetParentTrack() as AnimationTrack;
var blendIn = currentClip.blendInDuration;
currentClip.blendInDuration = 0;
var blendOut = previousClip.blendOutDuration;
previousClip.blendOutDuration = 0;
//evaluate previous without current
parentTrack.RemoveClip(currentClip);
director.RebuildGraph();
double previousEndTime = currentClip.start > previousClip.end ? previousClip.end : currentClip.start;
director.time = previousEndTime - timeEpsilon;
director.Evaluate(); // add port to evaluate only track
var targetPosition = matchPoint.position;
var targetRotation = matchPoint.rotation;
// evaluate current without previous
parentTrack.AddClip(currentClip);
parentTrack.RemoveClip(previousClip);
director.RebuildGraph();
director.time = currentClip.start + timeEpsilon;
director.Evaluate();
//////////////////////////////////////////////////////////////////////
//compute offsets
var animationPlayable = currentClip.asset as AnimationPlayableAsset;
var match = UpdateClipOffsets(animationPlayable, parentTrack, matchPoint, targetPosition, targetRotation);
WriteMatchFields(animationPlayable, match, matchFields);
//////////////////////////////////////////////////////////////////////
currentClip.blendInDuration = blendIn;
previousClip.blendOutDuration = blendOut;
parentTrack.AddClip(previousClip);
director.RebuildGraph();
director.time = cachedTime;
director.Evaluate();
}
public static void MatchNext(TimelineClip currentClip, Transform matchPoint, PlayableDirector director)
{
const double timeEpsilon = 0.00001;
MatchTargetFields matchFields = GetMatchFields(currentClip);
if (matchFields == MatchTargetFieldConstants.None || matchPoint == null)
return;
double cachedTime = director.time;
// finds next clip
TimelineClip nextClip = GetNextClip(currentClip);
if (nextClip == null || currentClip == nextClip)
return;
// make sure the transform is properly updated before modifying the graph
director.Evaluate();
var parentTrack = currentClip.GetParentTrack() as AnimationTrack;
var blendOut = currentClip.blendOutDuration;
var blendIn = nextClip.blendInDuration;
currentClip.blendOutDuration = 0;
nextClip.blendInDuration = 0;
//evaluate previous without current
parentTrack.RemoveClip(currentClip);
director.RebuildGraph();
director.time = nextClip.start + timeEpsilon;
director.Evaluate(); // add port to evaluate only track
var targetPosition = matchPoint.position;
var targetRotation = matchPoint.rotation;
// evaluate current without next
parentTrack.AddClip(currentClip);
parentTrack.RemoveClip(nextClip);
director.RebuildGraph();
director.time = Math.Min(nextClip.start, currentClip.end - timeEpsilon);
director.Evaluate();
//////////////////////////////////////////////////////////////////////
//compute offsets
var animationPlayable = currentClip.asset as AnimationPlayableAsset;
var match = UpdateClipOffsets(animationPlayable, parentTrack, matchPoint, targetPosition, targetRotation);
WriteMatchFields(animationPlayable, match, matchFields);
//////////////////////////////////////////////////////////////////////
currentClip.blendOutDuration = blendOut;
nextClip.blendInDuration = blendIn;
parentTrack.AddClip(nextClip);
director.RebuildGraph();
director.time = cachedTime;
director.Evaluate();
}
public static TimelineWindowTimeControl CreateTimeController(TimelineClip clip)
{
var animationWindow = EditorWindow.GetWindow<AnimationWindow>();
var timeController = ScriptableObject.CreateInstance<TimelineWindowTimeControl>();
timeController.Init(animationWindow.state, clip);
return timeController;
}
public static TimelineWindowTimeControl CreateTimeController(TimelineWindowTimeControl.ClipData clipData)
{
var animationWindow = EditorWindow.GetWindow<AnimationWindow>();
var timeController = ScriptableObject.CreateInstance<TimelineWindowTimeControl>();
timeController.Init(animationWindow.state, clipData);
return timeController;
}
public static void EditAnimationClipWithTimeController(AnimationClip animationClip, TimelineWindowTimeControl timeController, Object sourceObject)
{
var animationWindow = EditorWindow.GetWindow<AnimationWindow>();
animationWindow.EditSequencerClip(animationClip, sourceObject, timeController);
}
public static void UnlinkAnimationWindowFromTracks(IEnumerable<TrackAsset> tracks)
{
var clips = new List<AnimationClip>();
foreach (var track in tracks)
{
var animationTrack = track as AnimationTrack;
if (animationTrack != null && animationTrack.infiniteClip != null)
clips.Add(animationTrack.infiniteClip);
GetAnimationClips(track.GetClips(), clips);
}
UnlinkAnimationWindowFromAnimationClips(clips);
}
public static void UnlinkAnimationWindowFromClips(IEnumerable<TimelineClip> timelineClips)
{
var clips = new List<AnimationClip>();
GetAnimationClips(timelineClips, clips);
UnlinkAnimationWindowFromAnimationClips(clips);
}
public static void UnlinkAnimationWindowFromAnimationClips(ICollection<AnimationClip> clips)
{
if (clips.Count == 0)
return;
UnityEngine.Object[] windows = Resources.FindObjectsOfTypeAll(typeof(AnimationWindow));
foreach (var animWindow in windows.OfType<AnimationWindow>())
{
if (animWindow != null && animWindow.state != null && animWindow.state.linkedWithSequencer && clips.Contains(animWindow.state.activeAnimationClip))
animWindow.UnlinkSequencer();
}
}
public static void UnlinkAnimationWindow()
{
UnityEngine.Object[] windows = Resources.FindObjectsOfTypeAll(typeof(AnimationWindow));
foreach (var animWindow in windows.OfType<AnimationWindow>())
{
if (animWindow != null && animWindow.state != null && animWindow.state.linkedWithSequencer)
animWindow.UnlinkSequencer();
}
}
private static void GetAnimationClips(IEnumerable<TimelineClip> timelineClips, List<AnimationClip> clips)
{
foreach (var timelineClip in timelineClips)
{
if (timelineClip.curves != null)
clips.Add(timelineClip.curves);
AnimationPlayableAsset apa = timelineClip.asset as AnimationPlayableAsset;
if (apa != null && apa.clip != null)
clips.Add(apa.clip);
}
}
public static int GetAnimationWindowCurrentFrame()
{
var animationWindow = EditorWindow.GetWindow<AnimationWindow>();
if (animationWindow)
return animationWindow.state.currentFrame;
return -1;
}
public static void SetAnimationWindowCurrentFrame(int frame)
{
var animationWindow = EditorWindow.GetWindow<AnimationWindow>();
if (animationWindow)
animationWindow.state.currentFrame = frame;
}
public static void ConstrainCurveToBooleanValues(AnimationCurve curve)
{
// Clamp the values first
var keys = curve.keys;
for (var i = 0; i < keys.Length; i++)
{
var key = keys[i];
key.value = key.value < 0.5f ? 0.0f : 1.0f;
keys[i] = key;
}
curve.keys = keys;
// Update the tangents once all the values are clamped
for (var i = 0; i < curve.length; i++)
{
AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Constant);
AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Constant);
}
}
public static void ConstrainCurveToRange(AnimationCurve curve, float minValue, float maxValue)
{
var keys = curve.keys;
for (var i = 0; i < keys.Length; i++)
{
var key = keys[i];
key.value = Mathf.Clamp(key.value, minValue, maxValue);
keys[i] = key;
}
curve.keys = keys;
}
public static bool IsAnimationClip(TimelineClip clip)
{
return clip != null && (clip.asset as AnimationPlayableAsset) != null;
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Define the activeness of an action depending on its timeline mode.
/// </summary>
/// <seealso cref="TimelineModes"/>
[AttributeUsage(AttributeTargets.Class)]
public class ActiveInModeAttribute : Attribute
{
/// <summary>
/// Modes that will be used for activeness of an action.
/// </summary>
public TimelineModes modes { get; }
/// <summary>
/// Defines in which mode the action will be active.
/// </summary>
/// <param name="timelineModes">Modes that will define activeness of the action.</param>
public ActiveInModeAttribute(TimelineModes timelineModes)
{
modes = timelineModes;
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using UnityEngine;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Use this attribute to add a menu item to a context menu.
/// Used to indicate path and priority that are auto added to the menu
/// (examples can be found on <see href="https://docs.unity3d.com/ScriptReference/MenuItem.html"/>).
/// </summary>
/// <example>
/// <code source="../../DocCodeExamples/TimelineAttributesExamples.cs" region="declare-menuEntryAttribute" title="menuEntryAttr"/>
/// </example>
/// <remarks>
/// Unlike Menu item, MenuEntryAttribute doesn't handle shortcuts in the menu name. See <see cref="TimelineShortcutAttribute"/>.
/// </remarks>
[AttributeUsage(AttributeTargets.Class)]
public class MenuEntryAttribute : Attribute
{
internal readonly int priority;
internal readonly string name;
internal readonly string subMenuPath;
/// <summary>
/// Constructor for Menu Entry Attribute to define information about the menu item for an action.
/// </summary>
/// <param name="path">Path to the menu. If there is a "/" in the path, it will create one (or multiple) submenu items.</param>
/// <param name="priority">Priority to decide where the menu will be positioned in the menu.
/// The lower the priority, the higher the menu item will be in the context menu.
/// </param>
/// <seealso cref="MenuPriority"/>
public MenuEntryAttribute(string path = default, int priority = MenuPriority.defaultPriority)
{
path = path ?? string.Empty;
path = L10n.Tr(path);
this.priority = priority;
var index = path.LastIndexOf('/');
if (index >= 0)
{
name = (index == path.Length - 1) ? string.Empty : path.Substring(index + 1);
subMenuPath = path.Substring(0, index + 1);
}
else
{
name = path;
subMenuPath = string.Empty;
}
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Linq;
using UnityEditor.ShortcutManagement;
using UnityEngine;
namespace UnityEditor.Timeline
{
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
class ShortcutAttribute : Attribute
{
readonly string m_Identifier;
readonly string m_EventCommandName;
readonly string m_MenuShortcut;
public ShortcutAttribute(string identifier)
{
m_Identifier = identifier;
m_EventCommandName = identifier;
}
public ShortcutAttribute(string identifier, string commandName)
{
m_Identifier = identifier;
m_EventCommandName = commandName;
}
public ShortcutAttribute(KeyCode key, ShortcutModifiers modifiers = ShortcutModifiers.None)
{
m_MenuShortcut = new KeyCombination(key, modifiers).ToMenuShortcutString();
}
public string GetMenuShortcut()
{
if (m_MenuShortcut != null)
return m_MenuShortcut;
//find the mapped shortcut in the shortcut manager
var shortcut = ShortcutIntegration.instance.directory.FindShortcutEntry(m_Identifier);
if (shortcut != null && shortcut.combinations.Any())
{
return KeyCombination.SequenceToMenuString(shortcut.combinations);
}
return string.Empty;
}
public bool MatchesEvent(Event evt)
{
if (evt.type != EventType.ExecuteCommand)
return false;
return evt.commandName == m_EventCommandName;
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
class ShortcutPlatformOverrideAttribute : ShortcutAttribute
{
RuntimePlatform platform { get; }
public ShortcutPlatformOverrideAttribute(RuntimePlatform platform, KeyCode key, ShortcutModifiers modifiers = ShortcutModifiers.None)
: base(key, modifiers)
{
this.platform = platform;
}
public bool MatchesCurrentPlatform()
{
return Application.platform == platform;
}
}
}

View File

@@ -0,0 +1,24 @@
using UnityEditor.ShortcutManagement;
using UnityEngine;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Use this attribute to make an action work with the shortcut system.
/// </summary>
/// <example>
/// TimelineShortcutAttribute needs to be added to a static method.
/// <code source="../../DocCodeExamples/TimelineAttributesExamples.cs" region="declare-timelineShortcutAttr" title="TimelineShortcutAttr"/>
/// </example>
public class TimelineShortcutAttribute : ShortcutManagement.ShortcutAttribute
{
/// <summary>
/// TimelineShortcutAttribute Constructor
/// </summary>
/// <param name="id">Id to register the shortcut. It will automatically be prefix by 'Timeline/' in order to be in the 'Timeline' section of the shortcut manager.</param>
/// <param name="defaultKeyCode">Optional key code for default binding.</param>
/// <param name="defaultShortcutModifiers">Optional shortcut modifiers for default binding.</param>
public TimelineShortcutAttribute(string id, KeyCode defaultKeyCode, ShortcutModifiers defaultShortcutModifiers = ShortcutModifiers.None)
: base("Timeline/" + id, typeof(TimelineWindow), defaultKeyCode, defaultShortcutModifiers) {}
}
}

View File

@@ -0,0 +1,84 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using UnityEditor;
using UnityEditor.Timeline;
#if !UNITY_2020_2_OR_NEWER
using L10n = UnityEditor.Timeline.L10n;
#endif
namespace UnityEngine.Timeline
{
[CustomPropertyDrawer(typeof(AudioClipProperties))]
class AudioClipPropertiesDrawer : PropertyDrawer
{
[UsedImplicitly] // Also used by tests
internal static class Styles
{
public const string VolumeControl = "AudioClipPropertiesDrawer.volume";
const string k_Indent = " ";
public const string valuesFormatter = "0.###";
public static string mixedPropertiesInfo = L10n.Tr("The final {3} is {0}\n") +
L10n.Tr("Calculated from:\n") +
k_Indent + L10n.Tr("Clip: {1}\n") +
k_Indent + L10n.Tr("Track: {2}");
public static string audioSourceContribution = k_Indent + L10n.Tr("AudioSource: {0}");
}
static StringBuilder s_MixInfoBuilder = new StringBuilder();
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var volumeProp = property.FindPropertyRelative("volume");
GUI.SetNextControlName(Styles.VolumeControl);
EditorGUI.Slider(position, volumeProp, 0.0f, 1.0f, AudioSourceInspector.Styles.volumeLabel);
if (TimelineEditor.inspectedDirector == null)
// Nothing more to do in asset mode
return;
var clip = SelectionManager.SelectedClips().FirstOrDefault(c => c.asset == property.serializedObject.targetObject);
if (clip == null || clip.GetParentTrack() == null)
return;
var clipVolume = volumeProp.floatValue;
var trackVolume = new SerializedObject(clip.GetParentTrack()).FindProperty("m_TrackProperties.volume").floatValue;
var binding = TimelineEditor.inspectedDirector.GetGenericBinding(clip.GetParentTrack()) as AudioSource;
if (Math.Abs(clipVolume) < float.Epsilon &&
Math.Abs(trackVolume) < float.Epsilon &&
(binding == null || Math.Abs(binding.volume) < float.Epsilon))
return;
if (Math.Abs(clipVolume - 1) < float.Epsilon &&
Math.Abs(trackVolume - 1) < float.Epsilon &&
(binding == null || Math.Abs(binding.volume - 1) < float.Epsilon))
return;
s_MixInfoBuilder.Length = 0;
var audioSourceVolume = binding == null ? 1.0f : binding.volume;
s_MixInfoBuilder.AppendFormat(
Styles.mixedPropertiesInfo,
(clipVolume * trackVolume * audioSourceVolume).ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
clipVolume.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
trackVolume.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
AudioSourceInspector.Styles.volumeLabel.text);
if (binding != null)
s_MixInfoBuilder.Append("\n")
.AppendFormat(Styles.audioSourceContribution,
audioSourceVolume.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture));
EditorGUILayout.Space();
EditorGUILayout.HelpBox(new GUIContent(s_MixInfoBuilder.ToString()));
}
}
}

View File

@@ -0,0 +1,104 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[CustomTimelineEditor(typeof(AudioPlayableAsset)), UsedImplicitly]
class AudioPlayableAssetEditor : ClipEditor
{
readonly string k_NoClipAssignedError = L10n.Tr("No audio clip assigned");
readonly Dictionary<TimelineClip, WaveformPreview> m_PersistentPreviews = new Dictionary<TimelineClip, WaveformPreview>();
ColorSpace m_ColorSpace = ColorSpace.Uninitialized;
public override ClipDrawOptions GetClipOptions(TimelineClip clip)
{
var clipOptions = base.GetClipOptions(clip);
var audioAsset = clip.asset as AudioPlayableAsset;
if (audioAsset != null && audioAsset.clip == null)
clipOptions.errorText = k_NoClipAssignedError;
return clipOptions;
}
public override void DrawBackground(TimelineClip clip, ClipBackgroundRegion region)
{
if (!TimelineWindow.instance.state.showAudioWaveform)
return;
var rect = region.position;
if (rect.width <= 0)
return;
var audioClip = clip.asset as AudioClip;
if (audioClip == null)
{
var audioPlayableAsset = clip.asset as AudioPlayableAsset;
if (audioPlayableAsset != null)
audioClip = audioPlayableAsset.clip;
}
if (audioClip == null)
return;
var quantizedRect = new Rect(Mathf.Ceil(rect.x), Mathf.Ceil(rect.y), Mathf.Ceil(rect.width), Mathf.Ceil(rect.height));
WaveformPreview preview = GetOrCreateWaveformPreview(clip, audioClip, quantizedRect, region.startTime, region.endTime);
if (Event.current.type == EventType.Repaint)
DrawWaveformPreview(preview, quantizedRect);
}
public WaveformPreview GetOrCreateWaveformPreview(TimelineClip clip, AudioClip audioClip, Rect rect, double startTime, double endTime)
{
if (QualitySettings.activeColorSpace != m_ColorSpace)
{
m_ColorSpace = QualitySettings.activeColorSpace;
m_PersistentPreviews.Clear();
}
bool previewExists = m_PersistentPreviews.TryGetValue(clip, out WaveformPreview preview);
bool audioClipHasChanged = preview != null && audioClip != preview.presentedObject;
if (!previewExists || audioClipHasChanged)
{
if (AssetDatabase.Contains(audioClip))
preview = CreateWaveformPreview(audioClip, rect);
m_PersistentPreviews[clip] = preview;
}
if (preview == null)
return null;
preview.looping = clip.SupportsLooping();
preview.SetTimeInfo(startTime, endTime - startTime);
preview.OptimizeForSize(rect.size);
return preview;
}
public static void DrawWaveformPreview(WaveformPreview preview, Rect rect)
{
if (preview != null)
{
preview.ApplyModifications();
preview.Render(rect);
}
}
static WaveformPreview CreateWaveformPreview(AudioClip audioClip, Rect quantizedRect)
{
WaveformPreview preview = WaveformPreviewFactory.Create((int)quantizedRect.width, audioClip);
Color waveColour = GammaCorrect(DirectorStyles.Instance.customSkin.colorAudioWaveform);
Color transparent = waveColour;
transparent.a = 0;
preview.backgroundColor = transparent;
preview.waveColor = waveColour;
preview.SetChannelMode(WaveformPreview.ChannelMode.MonoSum);
preview.updated += () => TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw);
return preview;
}
static Color GammaCorrect(Color color)
{
return (QualitySettings.activeColorSpace == ColorSpace.Linear) ? color.gamma : color;
}
}
}

View File

@@ -0,0 +1,27 @@
using UnityEditor;
using UnityEditor.Timeline;
using UnityEngine.Playables;
namespace UnityEngine.Timeline
{
[CustomEditor(typeof(AudioPlayableAsset))]
class AudioPlayableAssetInspector : BasicAssetInspector
{
public override void ApplyChanges()
{
// At this point, we are guaranteed that the Timeline window is focused on
// the correct asset and that a single clip is selected (see ClipInspector)
if (TimelineEditor.inspectedDirector == null)
// Do nothing if in asset mode
return;
var asset = (AudioPlayableAsset)target;
if (TimelineEditor.inspectedDirector.state == PlayState.Playing)
asset.LiveLink();
else
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
}
}

View File

@@ -0,0 +1,167 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using UnityEditor;
using UnityEditor.Timeline;
using UnityEngine.Playables;
#if !UNITY_2020_2_OR_NEWER
using L10n = UnityEditor.Timeline.L10n;
#endif
namespace UnityEngine.Timeline
{
[CustomEditor(typeof(AudioTrack))]
[CanEditMultipleObjects]
class AudioTrackInspector : TrackAssetInspector
{
[UsedImplicitly] // Also used by tests
internal static class Styles
{
public const string VolumeControl = "AudioTrackInspector.volume";
public const string StereoPanControl = "AudioTrackInspector.stereoPan";
public const string SpatialBlendControl = "AudioTrackInspector.spatialBlend";
const string k_Indent = " ";
public const string valuesFormatter = "0.###";
public const string mixInfoSectionSeparator = "\n\n";
public static string mixedPropertiesInfo = L10n.Tr("The final {3} is {0}\n") +
L10n.Tr("Calculated from:\n") +
k_Indent + L10n.Tr("Track: {1}\n") +
k_Indent + L10n.Tr("AudioSource: {2}");
}
static StringBuilder s_MixInfoBuilder = new StringBuilder();
SerializedProperty m_VolumeProperty;
SerializedProperty m_StereoPanProperty;
SerializedProperty m_SpatialBlendProperty;
PlayableDirector m_Director;
public override void OnEnable()
{
base.OnEnable();
if (((AudioTrack)target).timelineAsset == TimelineEditor.inspectedAsset)
m_Director = TimelineEditor.inspectedDirector;
m_VolumeProperty = serializedObject.FindProperty("m_TrackProperties.volume");
m_StereoPanProperty = serializedObject.FindProperty("m_TrackProperties.stereoPan");
m_SpatialBlendProperty = serializedObject.FindProperty("m_TrackProperties.spatialBlend");
}
protected override void DrawTrackProperties()
{
// Volume
GUI.SetNextControlName(Styles.VolumeControl);
EditorGUILayout.Slider(m_VolumeProperty, 0.0f, 1.0f, AudioSourceInspector.Styles.volumeLabel);
EditorGUILayout.Space();
// Stereo Pan
GUI.SetNextControlName(Styles.StereoPanControl);
EditorGUIUtility.sliderLabels.SetLabels(AudioSourceInspector.Styles.panLeftLabel, AudioSourceInspector.Styles.panRightLabel);
EditorGUILayout.Slider(m_StereoPanProperty, -1.0f, 1.0f, AudioSourceInspector.Styles.panStereoLabel);
EditorGUIUtility.sliderLabels.SetLabels(null, null);
EditorGUILayout.Space();
// Spatial Blend
using (new EditorGUI.DisabledScope(ShouldDisableSpatialBlend()))
{
GUI.SetNextControlName(Styles.SpatialBlendControl);
EditorGUIUtility.sliderLabels.SetLabels(AudioSourceInspector.Styles.spatialLeftLabel, AudioSourceInspector.Styles.spatialRightLabel);
EditorGUILayout.Slider(m_SpatialBlendProperty, 0.0f, 1.0f, AudioSourceInspector.Styles.spatialBlendLabel);
EditorGUIUtility.sliderLabels.SetLabels(null, null);
}
DrawMixInfoSection();
}
void DrawMixInfoSection()
{
if (m_Director == null || targets.Length > 1)
return;
var binding = m_Director.GetGenericBinding(target) as AudioSource;
if (binding == null)
return;
var audioSourceVolume = binding.volume;
var audioSourcePan = binding.panStereo;
var audioSourceBlend = binding.spatialBlend;
var trackVolume = m_VolumeProperty.floatValue;
var trackPan = m_StereoPanProperty.floatValue;
var trackBlend = m_SpatialBlendProperty.floatValue;
// Skip sections when result is obvious
var skipVolumeInfo = Math.Abs(audioSourceVolume) < float.Epsilon && Math.Abs(trackVolume) < float.Epsilon || // All muted
Math.Abs(audioSourceVolume - 1) < float.Epsilon && Math.Abs(trackVolume - 1) < float.Epsilon; // All max volume
var skipPanInfo = Math.Abs(audioSourcePan) < float.Epsilon && Math.Abs(trackPan) < float.Epsilon || // All centered
Math.Abs(audioSourcePan - 1) < float.Epsilon && Math.Abs(trackPan - 1) < float.Epsilon || // All right
Math.Abs(audioSourcePan - (-1.0f)) < float.Epsilon && Math.Abs(trackPan - (-1.0f)) < float.Epsilon; // All left
var skipBlendInfo = Math.Abs(audioSourceBlend) < float.Epsilon && Math.Abs(trackBlend) < float.Epsilon || // All 2D
Math.Abs(audioSourceBlend - 1) < float.Epsilon && Math.Abs(trackBlend - 1) < float.Epsilon; // All 3D
if (skipVolumeInfo && skipPanInfo && skipBlendInfo)
return;
s_MixInfoBuilder.Length = 0;
if (!skipVolumeInfo)
s_MixInfoBuilder.AppendFormat(
Styles.mixedPropertiesInfo,
(audioSourceVolume * trackVolume).ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
trackVolume.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
audioSourceVolume.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
AudioSourceInspector.Styles.volumeLabel.text);
if (!skipVolumeInfo && !skipPanInfo)
s_MixInfoBuilder.Append(Styles.mixInfoSectionSeparator);
if (!skipPanInfo)
s_MixInfoBuilder.AppendFormat(
Styles.mixedPropertiesInfo,
Mathf.Clamp(audioSourcePan + trackPan, -1.0f, 1.0f).ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
trackPan.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
audioSourcePan.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
AudioSourceInspector.Styles.panStereoLabel.text);
if ((!skipVolumeInfo || !skipPanInfo) && !skipBlendInfo)
s_MixInfoBuilder.Append(Styles.mixInfoSectionSeparator);
if (!skipBlendInfo)
s_MixInfoBuilder.AppendFormat(
Styles.mixedPropertiesInfo,
Mathf.Clamp01(audioSourceBlend + trackBlend).ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
trackBlend.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
audioSourceBlend.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
AudioSourceInspector.Styles.spatialBlendLabel.text);
EditorGUILayout.Space();
EditorGUILayout.HelpBox(new GUIContent(s_MixInfoBuilder.ToString()));
}
protected override void ApplyChanges()
{
var track = (AudioTrack)target;
if (TimelineEditor.inspectedAsset != track.timelineAsset || TimelineEditor.inspectedDirector == null)
return;
if (TimelineEditor.inspectedDirector.state == PlayState.Playing)
track.LiveLink();
else
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
bool ShouldDisableSpatialBlend()
{
return m_Director == null ||
targets.Any(selectedTrack => m_Director.GetGenericBinding(selectedTrack) == null);
}
}
}

View File

@@ -0,0 +1,69 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
namespace UnityEditor.Timeline
{
[CustomTimelineEditor(typeof(ControlPlayableAsset))]
class ControlPlayableAssetEditor : ClipEditor
{
static readonly Texture2D[] s_ParticleSystemIcon = {AssetPreview.GetMiniTypeThumbnail(typeof(ParticleSystem))};
public override ClipDrawOptions GetClipOptions(TimelineClip clip)
{
var asset = (ControlPlayableAsset)clip.asset;
var options = base.GetClipOptions(clip);
if (asset.updateParticle && TimelineEditor.inspectedDirector != null && asset.controllingParticles)
options.icons = s_ParticleSystemIcon;
return options;
}
public override void OnCreate(TimelineClip clip, TrackAsset track, TimelineClip clonedFrom)
{
var asset = (ControlPlayableAsset)clip.asset;
GameObject sourceObject = null;
// go by sourceObject first, then by prefab
if (TimelineEditor.inspectedDirector != null)
sourceObject = asset.sourceGameObject.Resolve(TimelineEditor.inspectedDirector);
if (sourceObject == null && asset.prefabGameObject != null)
sourceObject = asset.prefabGameObject;
if (sourceObject)
{
var directors = asset.GetComponent<PlayableDirector>(sourceObject);
var particleSystems = asset.GetComponent<ParticleSystem>(sourceObject);
// update the duration and loop values (used for UI purposes) here
// so they are tied to the latest gameObject bound
asset.UpdateDurationAndLoopFlag(directors, particleSystems);
clip.displayName = sourceObject.name;
}
}
public override void GetSubTimelines(TimelineClip clip, PlayableDirector director, List<PlayableDirector> subTimelines)
{
var asset = (ControlPlayableAsset)clip.asset;
// If there is a prefab, it will override the source GameObject
if (!asset.updateDirector || asset.prefabGameObject != null || director == null)
return;
var go = asset.sourceGameObject.Resolve(director);
if (go == null)
return;
foreach (var subTimeline in asset.GetComponent<PlayableDirector>(go))
{
if (subTimeline == director || subTimeline == TimelineEditor.masterDirector || subTimeline == TimelineEditor.inspectedDirector)
continue;
if (subTimeline.playableAsset is TimelineAsset)
subTimelines.Add(subTimeline);
}
}
}
}

View File

@@ -0,0 +1,657 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditorInternal;
using UnityEngine;
using Object = UnityEngine.Object;
namespace UnityEditor.Timeline
{
// Utility class for editing animation clips from serialized properties
static class CurveEditUtility
{
public static bool IsRotationKey(EditorCurveBinding binding)
{
return binding.propertyName.Contains("localEulerAnglesRaw");
}
public static void AddKey(AnimationClip clip, EditorCurveBinding sourceBinding, SerializedProperty prop, double time)
{
if (sourceBinding.isPPtrCurve)
{
AddObjectKey(clip, sourceBinding, prop, time);
}
else if (IsRotationKey(sourceBinding))
{
AddRotationKey(clip, sourceBinding, prop, time);
}
else
{
AddFloatKey(clip, sourceBinding, prop, time);
}
}
static void AddObjectKey(AnimationClip clip, EditorCurveBinding sourceBinding, SerializedProperty prop, double time)
{
if (prop.propertyType != SerializedPropertyType.ObjectReference)
return;
ObjectReferenceKeyframe[] curve = null;
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
var curveIndex = Array.IndexOf(info.objectBindings, sourceBinding);
if (curveIndex >= 0)
{
curve = info.objectCurves[curveIndex];
// where in the array does the evaluation land?
var evalIndex = EvaluateIndex(curve, (float)time);
if (KeyCompare(curve[evalIndex].time, (float)time, clip.frameRate) == 0)
{
curve[evalIndex].value = prop.objectReferenceValue;
}
// check the next key (always return the minimum value)
else if (evalIndex < curve.Length - 1 && KeyCompare(curve[evalIndex + 1].time, (float)time, clip.frameRate) == 0)
{
curve[evalIndex + 1].value = prop.objectReferenceValue;
}
// resize the array
else
{
if (time > curve[0].time)
evalIndex++;
var key = new ObjectReferenceKeyframe();
key.time = (float)time;
key.value = prop.objectReferenceValue;
ArrayUtility.Insert(ref curve, evalIndex, key);
}
}
else // curve doesn't exist, add it
{
curve = new ObjectReferenceKeyframe[1];
curve[0].time = (float)time;
curve[0].value = prop.objectReferenceValue;
}
AnimationUtility.SetObjectReferenceCurve(clip, sourceBinding, curve);
EditorUtility.SetDirty(clip);
}
static void AddRotationKey(AnimationClip clip, EditorCurveBinding sourceBind, SerializedProperty prop, double time)
{
if (prop.propertyType != SerializedPropertyType.Quaternion)
{
return;
}
var updateCurves = new List<AnimationCurve>();
var updateBindings = new List<EditorCurveBinding>();
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
for (var i = 0; i < info.bindings.Length; i++)
{
if (sourceBind.type != info.bindings[i].type)
continue;
if (info.bindings[i].propertyName.Contains("localEuler"))
{
updateBindings.Add(info.bindings[i]);
updateCurves.Add(info.curves[i]);
}
}
// use this instead of serialized properties because the editor will attempt to maintain
// correct localeulers
var eulers = ((Transform)prop.serializedObject.targetObject).localEulerAngles;
if (updateBindings.Count == 0)
{
var propName = AnimationWindowUtility.GetPropertyGroupName(sourceBind.propertyName);
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".x"));
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".y"));
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".z"));
var curveX = new AnimationCurve();
var curveY = new AnimationCurve();
var curveZ = new AnimationCurve();
AddKeyFrameToCurve(curveX, (float)time, clip.frameRate, eulers.x, false);
AddKeyFrameToCurve(curveY, (float)time, clip.frameRate, eulers.y, false);
AddKeyFrameToCurve(curveZ, (float)time, clip.frameRate, eulers.z, false);
updateCurves.Add(curveX);
updateCurves.Add(curveY);
updateCurves.Add(curveZ);
}
for (var i = 0; i < updateBindings.Count; i++)
{
var c = updateBindings[i].propertyName.Last();
var value = eulers.x;
if (c == 'y') value = eulers.y;
else if (c == 'z') value = eulers.z;
AddKeyFrameToCurve(updateCurves[i], (float)time, clip.frameRate, value, false);
}
UpdateEditorCurves(clip, updateBindings, updateCurves);
}
// Add a floating point curve key
static void AddFloatKey(AnimationClip clip, EditorCurveBinding sourceBind, SerializedProperty prop, double time)
{
var updateCurves = new List<AnimationCurve>();
var updateBindings = new List<EditorCurveBinding>();
var updated = false;
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
for (var i = 0; i < info.bindings.Length; i++)
{
var binding = info.bindings[i];
if (binding.type != sourceBind.type)
continue;
SerializedProperty valProp = null;
var curve = info.curves[i];
// perfect match on property path, editting a float
if (prop.propertyPath.Equals(binding.propertyName))
{
valProp = prop;
}
// this is a child object
else if (binding.propertyName.Contains(prop.propertyPath))
{
valProp = prop.serializedObject.FindProperty(binding.propertyName);
}
if (valProp != null)
{
var value = GetKeyValue(valProp);
if (!float.IsNaN(value)) // Nan indicates an error retrieving the property value
{
updated = true;
AddKeyFrameToCurve(curve, (float)time, clip.frameRate, value, valProp.propertyType == SerializedPropertyType.Boolean);
updateCurves.Add(curve);
updateBindings.Add(binding);
}
}
}
// Curves don't exist, add them
if (!updated)
{
var propName = AnimationWindowUtility.GetPropertyGroupName(sourceBind.propertyName);
if (!prop.hasChildren)
{
var value = GetKeyValue(prop);
if (!float.IsNaN(value))
{
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, sourceBind.propertyName));
var curve = new AnimationCurve();
AddKeyFrameToCurve(curve, (float)time, clip.frameRate, value, prop.propertyType == SerializedPropertyType.Boolean);
updateCurves.Add(curve);
}
}
else
{
// special case because subproperties on color aren't 'visible' so you can't iterate over them
if (prop.propertyType == SerializedPropertyType.Color)
{
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".r"));
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".g"));
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".b"));
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".a"));
var c = prop.colorValue;
for (var i = 0; i < 4; i++)
{
var curve = new AnimationCurve();
AddKeyFrameToCurve(curve, (float)time, clip.frameRate, c[i], prop.propertyType == SerializedPropertyType.Boolean);
updateCurves.Add(curve);
}
}
else
{
prop = prop.Copy();
foreach (SerializedProperty cp in prop)
{
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, cp.propertyPath));
var curve = new AnimationCurve();
AddKeyFrameToCurve(curve, (float)time, clip.frameRate, GetKeyValue(cp), cp.propertyType == SerializedPropertyType.Boolean);
updateCurves.Add(curve);
}
}
}
}
UpdateEditorCurves(clip, updateBindings, updateCurves);
}
public static void RemoveKey(AnimationClip clip, EditorCurveBinding sourceBinding, SerializedProperty prop, double time)
{
if (sourceBinding.isPPtrCurve)
{
RemoveObjectKey(clip, sourceBinding, time);
}
else if (IsRotationKey(sourceBinding))
{
RemoveRotationKey(clip, sourceBinding, prop, time);
}
else
{
RemoveFloatKey(clip, sourceBinding, prop, time);
}
}
public static void RemoveObjectKey(AnimationClip clip, EditorCurveBinding sourceBinding, double time)
{
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
var curveIndex = Array.IndexOf(info.objectBindings, sourceBinding);
if (curveIndex >= 0)
{
var curve = info.objectCurves[curveIndex];
var evalIndex = GetKeyframeAtTime(curve, (float)time, clip.frameRate);
if (evalIndex >= 0)
{
ArrayUtility.RemoveAt(ref curve, evalIndex);
AnimationUtility.SetObjectReferenceCurve(clip, sourceBinding, curve.Length == 0 ? null : curve);
EditorUtility.SetDirty(clip);
}
}
}
public static int GetObjectKeyCount(AnimationClip clip, EditorCurveBinding sourceBinding)
{
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
var curveIndex = Array.IndexOf(info.objectBindings, sourceBinding);
if (curveIndex >= 0)
{
var curve = info.objectCurves[curveIndex];
return curve.Length;
}
return 0;
}
static void RemoveRotationKey(AnimationClip clip, EditorCurveBinding sourceBind, SerializedProperty prop, double time)
{
if (prop.propertyType != SerializedPropertyType.Quaternion)
{
return;
}
var updateCurves = new List<AnimationCurve>();
var updateBindings = new List<EditorCurveBinding>();
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
for (var i = 0; i < info.bindings.Length; i++)
{
if (sourceBind.type != info.bindings[i].type)
continue;
if (info.bindings[i].propertyName.Contains("localEuler"))
{
updateBindings.Add(info.bindings[i]);
updateCurves.Add(info.curves[i]);
}
}
foreach (var c in updateCurves)
{
RemoveKeyFrameFromCurve(c, (float)time, clip.frameRate);
}
UpdateEditorCurves(clip, updateBindings, updateCurves);
}
// Removes the float keys from curves
static void RemoveFloatKey(AnimationClip clip, EditorCurveBinding sourceBind, SerializedProperty prop, double time)
{
var updateCurves = new List<AnimationCurve>();
var updateBindings = new List<EditorCurveBinding>();
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
for (var i = 0; i < info.bindings.Length; i++)
{
var binding = info.bindings[i];
if (binding.type != sourceBind.type)
continue;
SerializedProperty valProp = null;
var curve = info.curves[i];
// perfect match on property path, editting a float
if (prop.propertyPath.Equals(binding.propertyName))
{
valProp = prop;
}
// this is a child object
else if (binding.propertyName.Contains(prop.propertyPath))
{
valProp = prop.serializedObject.FindProperty(binding.propertyName);
}
if (valProp != null)
{
RemoveKeyFrameFromCurve(curve, (float)time, clip.frameRate);
updateCurves.Add(curve);
updateBindings.Add(binding);
}
}
// update the curve. Do this last to not mess with the curve caches we are iterating over
UpdateEditorCurves(clip, updateBindings, updateCurves);
}
static void UpdateEditorCurve(AnimationClip clip, EditorCurveBinding binding, AnimationCurve curve)
{
if (curve.keys.Length == 0)
AnimationUtility.SetEditorCurve(clip, binding, null);
else
AnimationUtility.SetEditorCurve(clip, binding, curve);
}
static void UpdateEditorCurves(AnimationClip clip, List<EditorCurveBinding> bindings, List<AnimationCurve> curves)
{
if (curves.Count == 0)
return;
for (var i = 0; i < curves.Count; i++)
{
UpdateEditorCurve(clip, bindings[i], curves[i]);
}
EditorUtility.SetDirty(clip);
}
public static void RemoveCurves(AnimationClip clip, SerializedProperty prop)
{
if (clip == null || prop == null)
return;
var toRemove = new List<EditorCurveBinding>();
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
for (var i = 0; i < info.bindings.Length; i++)
{
var binding = info.bindings[i];
// check if we match directly, or with a child object
if (prop.propertyPath.Equals(binding.propertyName) || binding.propertyName.Contains(prop.propertyPath))
{
toRemove.Add(binding);
}
}
for (int i = 0; i < toRemove.Count; i++)
{
AnimationUtility.SetEditorCurve(clip, toRemove[i], null);
}
}
// adds a stepped key frame to the given curve
public static void AddKeyFrameToCurve(AnimationCurve curve, float time, float framerate, float value, bool stepped)
{
var key = new Keyframe();
bool add = true;
var keyIndex = GetKeyframeAtTime(curve, time, framerate);
if (keyIndex != -1)
{
add = false;
key = curve[keyIndex]; // retain the tangents and mode
curve.RemoveKey(keyIndex);
}
key.value = value;
key.time = GetKeyTime(time, framerate);
keyIndex = curve.AddKey(key);
if (stepped)
{
AnimationUtility.SetKeyBroken(curve, keyIndex, stepped);
AnimationUtility.SetKeyLeftTangentMode(curve, keyIndex, AnimationUtility.TangentMode.Constant);
AnimationUtility.SetKeyRightTangentMode(curve, keyIndex, AnimationUtility.TangentMode.Constant);
key.outTangent = Mathf.Infinity;
key.inTangent = Mathf.Infinity;
}
else if (add)
{
AnimationUtility.SetKeyLeftTangentMode(curve, keyIndex, AnimationUtility.TangentMode.ClampedAuto);
AnimationUtility.SetKeyRightTangentMode(curve, keyIndex, AnimationUtility.TangentMode.ClampedAuto);
}
if (keyIndex != -1 && !stepped)
{
AnimationUtility.UpdateTangentsFromModeSurrounding(curve, keyIndex);
AnimationUtility.SetKeyBroken(curve, keyIndex, false);
}
}
// Removes a keyframe at the given time from the animation curve
public static bool RemoveKeyFrameFromCurve(AnimationCurve curve, float time, float framerate)
{
var keyIndex = GetKeyframeAtTime(curve, time, framerate);
if (keyIndex == -1)
return false;
curve.RemoveKey(keyIndex);
return true;
}
// gets the value of the key
public static float GetKeyValue(SerializedProperty prop)
{
switch (prop.propertyType)
{
case SerializedPropertyType.Integer:
return prop.intValue;
case SerializedPropertyType.Boolean:
return prop.boolValue ? 1.0f : 0.0f;
case SerializedPropertyType.Float:
return prop.floatValue;
default:
Debug.LogError("Could not convert property type " + prop.propertyType.ToString() + " to float");
break;
}
return float.NaN;
}
public static void SetFromKeyValue(SerializedProperty prop, float keyValue)
{
switch (prop.propertyType)
{
case SerializedPropertyType.Float:
{
prop.floatValue = keyValue;
return;
}
case SerializedPropertyType.Integer:
{
prop.intValue = (int)keyValue;
return;
}
case SerializedPropertyType.Boolean:
{
prop.boolValue = Math.Abs(keyValue) > 0.001f;
return;
}
}
Debug.LogError("Could not convert float to property type " + prop.propertyType.ToString());
}
// gets the index of the key, -1 if not found
public static int GetKeyframeAtTime(AnimationCurve curve, float time, float frameRate)
{
var range = 0.5f / frameRate;
var keys = curve.keys;
for (var i = 0; i < keys.Length; i++)
{
var k = keys[i];
if (k.time >= time - range && k.time < time + range)
{
return i;
}
}
return -1;
}
public static int GetKeyframeAtTime(ObjectReferenceKeyframe[] curve, float time, float frameRate)
{
if (curve == null || curve.Length == 0)
return -1;
var range = 0.5f / frameRate;
for (var i = 0; i < curve.Length; i++)
{
var t = curve[i].time;
if (t >= time - range && t < time + range)
{
return i;
}
}
return -1;
}
public static float GetKeyTime(float time, float frameRate)
{
return Mathf.Round(time * frameRate) / frameRate;
}
public static int KeyCompare(float timeA, float timeB, float frameRate)
{
if (Mathf.Abs(timeA - timeB) <= 0.5f / frameRate)
return 0;
return timeA < timeB ? -1 : 1;
}
// Evaluates an object (bool curve)
public static Object Evaluate(ObjectReferenceKeyframe[] curve, float time)
{
return curve[EvaluateIndex(curve, time)].value;
}
// returns the index from evaluation
public static int EvaluateIndex(ObjectReferenceKeyframe[] curve, float time)
{
if (curve == null || curve.Length == 0)
throw new InvalidOperationException("Can not evaluate a PPtr curve with no entries");
// clamp conditions
if (time <= curve[0].time)
return 0;
if (time >= curve.Last().time)
return curve.Length - 1;
// binary search
var max = curve.Length - 1;
var min = 0;
while (max - min > 1)
{
var imid = (min + max) / 2;
if (Mathf.Approximately(curve[imid].time, time))
return imid;
if (curve[imid].time < time)
min = imid;
else if (curve[imid].time > time)
max = imid;
}
return min;
}
// Shifts the animation clip so the time start at 0
public static void ShiftBySeconds(this AnimationClip clip, float time)
{
var floatBindings = AnimationUtility.GetCurveBindings(clip);
var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);
// update the float curves
foreach (var bind in floatBindings)
{
var curve = AnimationUtility.GetEditorCurve(clip, bind);
var keys = curve.keys;
for (var i = 0; i < keys.Length; i++)
keys[i].time += time;
curve.keys = keys;
AnimationUtility.SetEditorCurve(clip, bind, curve);
}
// update the PPtr curves
foreach (var bind in objectBindings)
{
var curve = AnimationUtility.GetObjectReferenceCurve(clip, bind);
for (var i = 0; i < curve.Length; i++)
curve[i].time += time;
AnimationUtility.SetObjectReferenceCurve(clip, bind, curve);
}
EditorUtility.SetDirty(clip);
}
public static void ScaleTime(this AnimationClip clip, float scale)
{
var floatBindings = AnimationUtility.GetCurveBindings(clip);
var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);
// update the float curves
foreach (var bind in floatBindings)
{
var curve = AnimationUtility.GetEditorCurve(clip, bind);
var keys = curve.keys;
for (var i = 0; i < keys.Length; i++)
keys[i].time *= scale;
curve.keys = keys.OrderBy(x => x.time).ToArray();
AnimationUtility.SetEditorCurve(clip, bind, curve);
}
// update the PPtr curves
foreach (var bind in objectBindings)
{
var curve = AnimationUtility.GetObjectReferenceCurve(clip, bind);
for (var i = 0; i < curve.Length; i++)
curve[i].time *= scale;
curve = curve.OrderBy(x => x.time).ToArray();
AnimationUtility.SetObjectReferenceCurve(clip, bind, curve);
}
EditorUtility.SetDirty(clip);
}
// Creates an opposing blend curve that matches the given curve to make sure the result is normalized
public static AnimationCurve CreateMatchingCurve(AnimationCurve curve)
{
Keyframe[] keys = curve.keys;
for (var i = 0; i != keys.Length; i++)
{
if (!Single.IsPositiveInfinity(keys[i].inTangent))
keys[i].inTangent = -keys[i].inTangent;
if (!Single.IsPositiveInfinity(keys[i].outTangent))
keys[i].outTangent = -keys[i].outTangent;
keys[i].value = 1.0f - keys[i].value;
}
return new AnimationCurve(keys);
}
// Sanitizes the keys on an animation to force the property to be normalized
public static Keyframe[] SanitizeCurveKeys(Keyframe[] keys, bool easeIn)
{
if (keys.Length < 2)
{
if (easeIn)
keys = new[] { new Keyframe(0, 0), new Keyframe(1, 1) };
else
keys = new[] { new Keyframe(0, 1), new Keyframe(1, 0) };
}
else if (easeIn)
{
keys[0].time = 0;
keys[keys.Length - 1].time = 1;
keys[keys.Length - 1].value = 1;
}
else
{
keys[0].time = 0;
keys[0].value = 1;
keys[keys.Length - 1].time = 1;
}
return keys;
}
}
}

View File

@@ -0,0 +1,307 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
/// <summary>
/// Description of the on-screen area where a clip is drawn
/// </summary>
public struct ClipBackgroundRegion
{
/// <summary>
/// The rectangle where the background of the clip is drawn.
/// </summary>
/// <remarks>
/// The rectangle is clipped to the screen. The rectangle does not include clip borders.
/// </remarks>
public Rect position { get; private set; }
/// <summary>
/// The start time of the region, relative to the clip.
/// </summary>
public double startTime { get; private set; }
/// <summary>
/// The end time of the region, relative to the clip.
/// </summary>
public double endTime { get; private set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="_position"></param>
/// <param name="_startTime"></param>
/// <param name="_endTime"></param>
public ClipBackgroundRegion(Rect _position, double _startTime, double _endTime)
{
position = _position;
startTime = _startTime;
endTime = _endTime;
}
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns>Returns <c>true</c> if <paramref name="obj"/> and this instance are the same type and represent the same value.</returns>
public override bool Equals(object obj)
{
if (!(obj is ClipBackgroundRegion))
return false;
return Equals((ClipBackgroundRegion)obj);
}
/// <summary>
/// Compares this object with another <c>ClipBackgroundRegion</c>.
/// </summary>
/// <param name="other">The object to compare with.</param>
/// <returns>Returns true if <c>this</c> and <paramref name="other"/> are equal.</returns>
public bool Equals(ClipBackgroundRegion other)
{
return position.Equals(other.position) &&
startTime == other.startTime &&
endTime == other.endTime;
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode()
{
return HashUtility.CombineHash(
position.GetHashCode(),
startTime.GetHashCode(),
endTime.GetHashCode()
);
}
/// <summary>
/// Compares two <c>ClipBackgroundRegion</c> objects.
/// </summary>
/// <param name="region1">The first object.</param>
/// <param name="region2">The second object.</param>
/// <returns>Returns true if they are equal.</returns>
public static bool operator==(ClipBackgroundRegion region1, ClipBackgroundRegion region2)
{
return region1.Equals(region2);
}
/// <summary>
/// Compares two <c>ClipBackgroundRegion</c> objects.
/// </summary>
/// <param name="region1">The first object.</param>
/// <param name="region2">The second object.</param>
/// <returns>Returns true if they are not equal.</returns>
public static bool operator!=(ClipBackgroundRegion region1, ClipBackgroundRegion region2)
{
return !region1.Equals(region2);
}
}
/// <summary>
/// The user-defined options for drawing a clip.
/// </summary>
public struct ClipDrawOptions
{
private IEnumerable<Texture2D> m_Icons;
/// <summary>
/// Text that indicates if the clip should display an error.
/// </summary>
/// <remarks>
/// If the error text is not empty or null, then the clip displays a warning. The error text is used as the tooltip.
/// </remarks>
public string errorText { get; set; }
/// <summary>
/// The tooltip to show for the clip.
/// </summary>
public string tooltip { get; set; }
/// <summary>
/// The color drawn under the clip. By default, the color is the same as the track color.
/// </summary>
public Color highlightColor { get; set; }
/// <summary>
/// Icons to display on the clip.
/// </summary>
public IEnumerable<Texture2D> icons
{
get { return m_Icons ?? System.Linq.Enumerable.Empty<Texture2D>(); }
set { m_Icons = value;}
}
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns>Returns <c>true</c> if <paramref name="obj"/> and this instance are the same type and represent the same value.</returns>
public override bool Equals(object obj)
{
if (!(obj is ClipDrawOptions))
return false;
return Equals((ClipDrawOptions)obj);
}
/// <summary>
/// Compares this object with another <c>ClipDrawOptions</c>.
/// </summary>
/// <param name="other">The object to compare with.</param>
/// <returns>Returns true if <c>this</c> and <paramref name="other"/> are equal.</returns>
public bool Equals(ClipDrawOptions other)
{
return errorText == other.errorText &&
tooltip == other.tooltip &&
highlightColor == other.highlightColor &&
System.Linq.Enumerable.SequenceEqual(icons, other.icons);
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode()
{
return HashUtility.CombineHash(
errorText != null ? errorText.GetHashCode() : 0,
tooltip != null ? tooltip.GetHashCode() : 0,
highlightColor.GetHashCode(),
icons != null ? icons.GetHashCode() : 0
);
}
/// <summary>
/// Compares two <c>ClipDrawOptions</c> objects.
/// </summary>
/// <param name="options1">The first object.</param>
/// <param name="options2">The second object.</param>
/// <returns>Returns true if they are equal.</returns>
public static bool operator==(ClipDrawOptions options1, ClipDrawOptions options2)
{
return options1.Equals(options2);
}
/// <summary>
/// Compares two <c>ClipDrawOptions</c> objects.
/// </summary>
/// <param name="options1">The first object.</param>
/// <param name="options2">The second object.</param>
/// <returns>Returns true if they are not equal.</returns>
public static bool operator!=(ClipDrawOptions options1, ClipDrawOptions options2)
{
return !options1.Equals(options2);
}
}
/// <summary>
/// Use this class to customize clip types in the TimelineEditor.
/// </summary>
public class ClipEditor
{
static readonly string k_NoPlayableAssetError = L10n.Tr("This clip does not contain a valid playable asset");
static readonly string k_ScriptLoadError = L10n.Tr("The associated script can not be loaded");
internal readonly bool supportsSubTimelines;
/// <summary>
/// Default constructor
/// </summary>
public ClipEditor()
{
supportsSubTimelines = TypeUtility.HasOverrideMethod(GetType(), nameof(GetSubTimelines));
}
/// <summary>
/// Implement this method to override the default options for drawing a clip.
/// </summary>
/// <param name="clip">The clip being drawn.</param>
/// <returns>The options for drawing a clip.</returns>
public virtual ClipDrawOptions GetClipOptions(TimelineClip clip)
{
return new ClipDrawOptions()
{
errorText = GetErrorText(clip),
tooltip = string.Empty,
highlightColor = GetDefaultHighlightColor(clip),
icons = System.Linq.Enumerable.Empty<Texture2D>()
};
}
/// <summary>
/// Override this method to draw a background for a clip .
/// </summary>
/// <param name="clip">The clip being drawn.</param>
/// <param name="region">The on-screen area where the clip is drawn.</param>
public virtual void DrawBackground(TimelineClip clip, ClipBackgroundRegion region)
{
}
/// <summary>
/// Called when a clip is created.
/// </summary>
/// <param name="clip">The newly created clip.</param>
/// <param name="track">The track that the clip is assigned to.</param>
/// <param name="clonedFrom">The source that the clip was copied from. This can be set to null if the clip is not a copy.</param>
/// <remarks>
/// The callback occurs before the clip is assigned to the track.
/// </remarks>
public virtual void OnCreate(TimelineClip clip, TrackAsset track, TimelineClip clonedFrom)
{
}
/// <summary>
/// Gets the error text for the specified clip.
/// </summary>
/// <param name="clip">The clip being drawn.</param>
/// <returns>Returns the error text to be displayed as the tool tip for the clip. If there is no error to be displayed, this method returns string.Empty.</returns>
public string GetErrorText(TimelineClip clip)
{
if (clip == null || clip.asset == null)
return k_NoPlayableAssetError;
var playableAsset = clip.asset as ScriptableObject;
if (playableAsset == null || MonoScript.FromScriptableObject(playableAsset) == null)
return k_ScriptLoadError;
return string.Empty;
}
/// <summary>
/// The color drawn under the clip. By default, the color is the same as the track color.
/// </summary>
/// <param name="clip">The clip being drawn.</param>
/// <returns>Returns the highlight color of the clip being drawn.</returns>
public Color GetDefaultHighlightColor(TimelineClip clip)
{
if (clip == null)
return Color.white;
return TrackResourceCache.GetTrackColor(clip.GetParentTrack());
}
/// <summary>
/// Called when a clip is changed by the Editor.
/// </summary>
/// <param name="clip">The clip that changed.</param>
public virtual void OnClipChanged(TimelineClip clip)
{
}
/// <summary>
/// Gets the sub-timelines for a specific clip. Implement this method if your clip supports playing nested timelines.
/// </summary>
/// <param name="clip">The clip with the ControlPlayableAsset.</param>
/// <param name="director">The playable director driving the Timeline Clip. This may not be the same as TimelineEditor.inspectedDirector.</param>
/// <param name="subTimelines">Specify the sub-timelines to control.</param>
public virtual void GetSubTimelines(TimelineClip clip, PlayableDirector director, List<PlayableDirector> subTimelines)
{
}
}
}

View File

@@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class CustomTimelineEditorCache
{
static class SubClassCache<TEditorClass> where TEditorClass : class, new()
{
private static Type[] s_SubClasses = null;
private static readonly TEditorClass s_DefaultInstance = new TEditorClass();
private static readonly Dictionary<System.Type, TEditorClass> s_TypeMap = new Dictionary<Type, TEditorClass>();
public static TEditorClass DefaultInstance
{
get { return s_DefaultInstance; }
}
static Type[] SubClasses
{
get
{
// order the subclass array by built-ins then user defined so built-in classes are chosen first
return s_SubClasses ??
(s_SubClasses = TypeCache.GetTypesDerivedFrom<TEditorClass>().OrderBy(t => t.Assembly == typeof(UnityEditor.Timeline.TimelineEditor).Assembly ? 1 : 0).ToArray());
}
}
public static TEditorClass GetEditorForType(Type type)
{
TEditorClass editorClass = null;
if (!s_TypeMap.TryGetValue(type, out editorClass) || editorClass == null)
{
Type editorClassType = null;
Type searchType = type;
while (searchType != null)
{
// search our way up the runtime class hierarchy so we get the best match
editorClassType = GetExactEditorClassForType(searchType);
if (editorClassType != null)
break;
searchType = searchType.BaseType;
}
if (editorClassType == null)
{
editorClass = s_DefaultInstance;
}
else
{
try
{
editorClass = (TEditorClass)Activator.CreateInstance(editorClassType);
}
catch (Exception e)
{
Debug.LogWarningFormat("Could not create a Timeline editor class of type {0}: {1}", editorClassType, e.Message);
editorClass = s_DefaultInstance;
}
}
s_TypeMap[type] = editorClass;
}
return editorClass;
}
private static Type GetExactEditorClassForType(Type type)
{
foreach (var subClass in SubClasses)
{
// first check for exact match
var attr = (CustomTimelineEditorAttribute)Attribute.GetCustomAttribute(subClass, typeof(CustomTimelineEditorAttribute), false);
if (attr != null && attr.classToEdit == type)
{
return subClass;
}
}
return null;
}
public static void Clear()
{
s_TypeMap.Clear();
s_SubClasses = null;
}
}
public static TEditorClass GetEditorForType<TEditorClass, TRuntimeClass>(Type type) where TEditorClass : class, new()
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (!typeof(TRuntimeClass).IsAssignableFrom(type))
throw new ArgumentException(type.FullName + " does not inherit from" + typeof(TRuntimeClass));
return SubClassCache<TEditorClass>.GetEditorForType(type);
}
public static void ClearCache<TEditorClass>() where TEditorClass : class, new()
{
SubClassCache<TEditorClass>.Clear();
}
public static ClipEditor GetClipEditor(TimelineClip clip)
{
if (clip == null)
throw new ArgumentNullException(nameof(clip));
var type = typeof(IPlayableAsset);
if (clip.asset != null)
type = clip.asset.GetType();
if (!typeof(IPlayableAsset).IsAssignableFrom(type))
return GetDefaultClipEditor();
return GetEditorForType<ClipEditor, IPlayableAsset>(type);
}
public static ClipEditor GetDefaultClipEditor()
{
return SubClassCache<ClipEditor>.DefaultInstance;
}
public static TrackEditor GetTrackEditor(TrackAsset track)
{
if (track == null)
throw new ArgumentNullException(nameof(track));
return GetEditorForType<TrackEditor, TrackAsset>(track.GetType());
}
public static TrackEditor GetDefaultTrackEditor()
{
return SubClassCache<TrackEditor>.DefaultInstance;
}
public static MarkerEditor GetMarkerEditor(IMarker marker)
{
if (marker == null)
throw new ArgumentNullException(nameof(marker));
return GetEditorForType<MarkerEditor, IMarker>(marker.GetType());
}
public static MarkerEditor GetDefaultMarkerEditor()
{
return SubClassCache<MarkerEditor>.DefaultInstance;
}
}
}

View File

@@ -0,0 +1,292 @@
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
/// <summary>
/// The flags that indicate the view status of a marker.
/// </summary>
[System.Flags]
public enum MarkerUIStates
{
/// <summary>
/// No extra state specified.
/// </summary>
None = 0,
/// <summary>
/// The marker is selected.
/// </summary>
Selected = 1 << 0,
/// <summary>
/// The marker is in a collapsed state.
/// </summary>
Collapsed = 1 << 1
}
/// <summary>
/// The user-defined options for drawing a marker.
/// </summary>
public struct MarkerDrawOptions
{
/// <summary>
/// The tooltip for the marker.
/// </summary>
public string tooltip { get; set; }
/// <summary>
/// Text that indicates if the marker should display an error.
/// </summary>
/// <remarks>
/// If the error text is not empty or null, then the marker displays a warning. The error text is used as the tooltip.
/// </remarks>
public string errorText { get; set; }
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns>Returns <c>true</c> if <paramref name="obj"/> and this instance are the same type and represent the same value.</returns>
public override bool Equals(object obj)
{
if (!(obj is MarkerDrawOptions))
return false;
return Equals((MarkerDrawOptions)obj);
}
/// <summary>
/// Compares this object with another <c>MarkerDrawOptions</c>.
/// </summary>
/// <param name="other">The object to compare with.</param>
/// <returns>Returns true if <c>this</c> and <paramref name="other"/> are equal.</returns>
public bool Equals(MarkerDrawOptions other)
{
return errorText == other.errorText &&
tooltip == other.tooltip;
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode()
{
return HashUtility.CombineHash(
errorText != null ? errorText.GetHashCode() : 0,
tooltip != null ? tooltip.GetHashCode() : 0
);
}
/// <summary>
/// Compares two <c>MarkerDrawOptions</c> objects.
/// </summary>
/// <param name="options1">The first object.</param>
/// <param name="options2">The second object.</param>
/// <returns>Returns true if they are equal.</returns>
public static bool operator==(MarkerDrawOptions options1, MarkerDrawOptions options2)
{
return options1.Equals(options2);
}
/// <summary>
/// Compares two <c>MarkerDrawOptions</c> objects.
/// </summary>
/// <param name="options1">The first object.</param>
/// <param name="options2">The second object.</param>
/// <returns>Returns true if they are not equal.</returns>
public static bool operator!=(MarkerDrawOptions options1, MarkerDrawOptions options2)
{
return !options1.Equals(options2);
}
}
/// <summary>
/// The description of the on-screen area where the marker is drawn.
/// </summary>
public struct MarkerOverlayRegion
{
/// <summary>
/// The area where the marker is being drawn.
/// </summary>
public Rect markerRegion { get; private set; }
/// <summary>
/// The area where the overlay is being drawn.
///
/// This region extends from the top of the time ruler to the bottom of the window, excluding any scrollbars.
/// </summary>
public Rect timelineRegion { get; private set; }
/// <summary>
/// The sub-area of the timelineRegion where the tracks are drawn.
///
/// The region extends from the bottom of the time ruler, or the timeline marker region if not hidden.
/// Use this region to clip overlays that should not be drawn over the timeline marker region or time ruler.
/// </summary>
/// <example>
/// <code source="../../DocCodeExamples/MarkerEditorExamples.cs" region="declare-trackRegion" title="trackRegion"/>
/// </example>
public Rect trackRegion => Rect.MinMaxRect(timelineRegion.xMin, timelineRegion.yMin + m_TrackOffset, timelineRegion.xMax, timelineRegion.yMax);
/// <summary>
/// The start time of the visible region of the window.
/// </summary>
public double startTime { get; private set; }
/// <summary>
/// The end time of the visible region of the window.
/// </summary>
public double endTime { get; private set; }
private float m_TrackOffset;
/// <summary>Constructor</summary>
/// <param name="_markerRegion">The area where the marker is being drawn.</param>
/// <param name="_timelineRegion">The area where the overlay is being drawn.</param>
/// <param name="_startTime">The start time of the visible region of the window.</param>
/// <param name="_endTime">The end time of the visible region of the window.</param>
public MarkerOverlayRegion(Rect _markerRegion, Rect _timelineRegion, double _startTime, double _endTime)
: this(_markerRegion, _timelineRegion, _startTime, _endTime, 19.0f)
{
}
/// <summary>Constructor</summary>
/// <param name="_markerRegion">The area where the marker is being drawn.</param>
/// <param name="_timelineRegion">The area where the overlay is being drawn.</param>
/// <param name="_startTime">The start time of the visible region of the window.</param>
/// <param name="_endTime">The end time of the visible region of the window.</param>
/// <param name="_trackOffset">The offset from the timelineRegion to the trackRegion</param>
public MarkerOverlayRegion(Rect _markerRegion, Rect _timelineRegion, double _startTime, double _endTime, float _trackOffset)
{
markerRegion = _markerRegion;
timelineRegion = _timelineRegion;
startTime = _startTime;
endTime = _endTime;
m_TrackOffset = _trackOffset;
}
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns>Returns <c>true</c> if <paramref name="obj"/> and this instance are the same type and represent the same value.</returns>
public override bool Equals(object obj)
{
if (!(obj is MarkerOverlayRegion))
return false;
return Equals((MarkerOverlayRegion)obj);
}
/// <summary>
/// Compares this object with another <c>MarkerOverlayRegion</c>.
/// </summary>
/// <param name="other">The object to compare with.</param>
/// <returns>Returns true if <c>this</c> and <paramref name="other"/> are equal.</returns>
public bool Equals(MarkerOverlayRegion other)
{
return markerRegion == other.markerRegion &&
timelineRegion == other.timelineRegion &&
startTime == other.startTime &&
endTime == other.endTime &&
m_TrackOffset == other.m_TrackOffset;
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode()
{
return HashUtility.CombineHash(
markerRegion.GetHashCode(),
timelineRegion.GetHashCode(),
startTime.GetHashCode(),
endTime.GetHashCode(),
m_TrackOffset.GetHashCode()
);
}
/// <summary>
/// Compares two <c>MarkerOverlayRegion</c> objects.
/// </summary>
/// <param name="region1">The first object.</param>
/// <param name="region2">The second object.</param>
/// <returns>Returns true if they are equal.</returns>
public static bool operator==(MarkerOverlayRegion region1, MarkerOverlayRegion region2)
{
return region1.Equals(region2);
}
/// <summary>
/// Compares two <c>MarkerOverlayRegion</c> objects.
/// </summary>
/// <param name="region1">The first object.</param>
/// <param name="region2">The second object.</param>
/// <returns>Returns true if they are not equal.</returns>
public static bool operator!=(MarkerOverlayRegion region1, MarkerOverlayRegion region2)
{
return !region1.Equals(region2);
}
}
/// <summary>
/// Use this class to customize marker types in the TimelineEditor.
/// </summary>
public class MarkerEditor
{
internal readonly bool supportsDrawOverlay;
/// <summary>
/// Default constructor
/// </summary>
public MarkerEditor()
{
supportsDrawOverlay = TypeUtility.HasOverrideMethod(GetType(), nameof(DrawOverlay));
}
/// <summary>
/// Implement this method to override the default options for drawing a marker.
/// </summary>
/// <param name="marker">The marker to draw.</param>
/// <returns></returns>
public virtual MarkerDrawOptions GetMarkerOptions(IMarker marker)
{
return new MarkerDrawOptions()
{
tooltip = string.Empty,
errorText = string.Empty,
};
}
/// <summary>
/// Called when a marker is created.
/// </summary>
/// <param name="marker">The marker that is created.</param>
/// <param name="clonedFrom">TThe source that the marker was copied from. This can be set to null if the marker is not a copy.</param>
/// <remarks>
/// The callback occurs before the marker is assigned to the track.
/// </remarks>
public virtual void OnCreate(IMarker marker, IMarker clonedFrom)
{
}
/// <summary>
/// Draws additional overlays for a marker.
/// </summary>
/// <param name="marker">The marker to draw.</param>
/// <param name="uiState">The visual state of the marker.</param>
/// <param name="region">The on-screen area where the marker is being drawn.</param>
/// <remarks>
/// Notes:
/// * It is only called during TimelineWindow's Repaint step.
/// * If there are multiple markers on top of each other, only the topmost marker receives the DrawOverlay call.
/// </remarks>
public virtual void DrawOverlay(IMarker marker, MarkerUIStates uiState, MarkerOverlayRegion region)
{
}
}
}

View File

@@ -0,0 +1,18 @@
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[CustomTimelineEditor(typeof(MarkerTrack))]
class MarkerTrackEditor : TrackEditor
{
public static readonly float DefaultMarkerTrackHeight = 24;
public override TrackDrawOptions GetTrackOptions(TrackAsset track, Object binding)
{
var options = base.GetTrackOptions(track, binding);
options.minimumHeight = DefaultMarkerTrackHeight;
return options;
}
}
}

View File

@@ -0,0 +1,417 @@
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
/// <summary>
/// The user-defined options for drawing a track."
/// </summary>
public struct TrackDrawOptions
{
/// <summary>
/// Text that indicates if the track should display an error.
/// </summary>
/// <remarks>
/// If the error text is not empty or null, then the track displays a warning. The error text is used as the tooltip.
/// </remarks>
public string errorText { get; set; }
/// <summary>
/// The highlight color of the track.
/// </summary>
public Color trackColor { get; set; }
/// <summary>
/// The minimum height of the track.
/// </summary>
public float minimumHeight { get; set; }
/// <summary>
/// The icon displayed on the track header.
/// </summary>
/// <remarks>
/// If this value is null, then the default icon for the track is used.
/// </remarks>
public Texture2D icon { get; set; }
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns>Returns <c>true</c> if <paramref name="obj"/> and this instance are the same type and represent the same value.</returns>
public override bool Equals(object obj)
{
if (!(obj is TrackDrawOptions))
return false;
return Equals((TrackDrawOptions)obj);
}
/// <summary>
/// Compares this object with another <c>TrackDrawOptions</c>.
/// </summary>
/// <param name="other">The object to compare with.</param>
/// <returns>Returns true if <c>this</c> and <paramref name="other"/> are equal.</returns>
public bool Equals(TrackDrawOptions other)
{
return errorText == other.errorText &&
trackColor == other.trackColor &&
minimumHeight == other.minimumHeight &&
icon == other.icon;
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode()
{
return HashUtility.CombineHash(
errorText != null ? errorText.GetHashCode() : 0,
trackColor.GetHashCode(),
minimumHeight.GetHashCode(),
icon != null ? icon.GetHashCode() : 0
);
}
/// <summary>
/// Compares two <c>TrackDrawOptions</c> objects.
/// </summary>
/// <param name="options1">The first object.</param>
/// <param name="options2">The second object.</param>
/// <returns>Returns true if they are equal.</returns>
public static bool operator==(TrackDrawOptions options1, TrackDrawOptions options2)
{
return options1.Equals(options2);
}
/// <summary>
/// Compares two <c>TrackDrawOptions</c> objects.
/// </summary>
/// <param name="options1">The first object.</param>
/// <param name="options2">The second object.</param>
/// <returns>Returns true if they are not equal.</returns>
public static bool operator!=(TrackDrawOptions options1, TrackDrawOptions options2)
{
return !options1.Equals(options2);
}
}
/// <summary>
/// The errors displayed for the track binding.
/// </summary>
public enum TrackBindingErrors
{
/// <summary>
/// Select no errors.
/// </summary>
None = 0,
/// <summary>
/// The bound GameObject is disabled.
/// </summary>
BoundGameObjectDisabled = 1 << 0,
/// <summary>
/// The bound GameObject does not have a valid component.
/// </summary>
NoValidComponent = 1 << 1,
/// <summary>
/// The bound Object is a disabled Behaviour.
/// </summary>
BehaviourIsDisabled = 1 << 2,
/// <summary>
/// The bound Object is not of the correct type.
/// </summary>
InvalidBinding = 1 << 3,
/// <summary>
/// The bound Object is part of a prefab, and not an instance.
/// </summary>
PrefabBound = 1 << 4,
/// <summary>
/// Select all errors.
/// </summary>
All = Int32.MaxValue
}
/// <summary>
/// Use this class to customize track types in the TimelineEditor.
/// </summary>
public class TrackEditor
{
static readonly string k_BoundGameObjectDisabled = L10n.Tr("The bound GameObject is disabled.");
static readonly string k_NoValidComponent = L10n.Tr("Could not find appropriate component on this gameObject");
static readonly string k_RequiredComponentIsDisabled = L10n.Tr("The component is disabled");
static readonly string k_InvalidBinding = L10n.Tr("The bound object is not the correct type.");
static readonly string k_PrefabBound = L10n.Tr("The bound object is a Prefab");
readonly Dictionary<TrackAsset, System.Type> m_BindingCache = new Dictionary<TrackAsset, System.Type>();
/// <summary>
/// The default height of a track.
/// </summary>
public static readonly float DefaultTrackHeight = 30.0f;
/// <summary>
/// The minimum unscaled height of a track.
/// </summary>
public static readonly float MinimumTrackHeight = 10.0f;
/// <summary>
/// The maximum height of a track.
/// </summary>
public static readonly float MaximumTrackHeight = 256.0f;
/// <summary>
/// Implement this method to override the default options for drawing a track.
/// </summary>
/// <param name="track">The track from which track options are retrieved.</param>
/// <param name="binding">The binding for the track.</param>
/// <returns>The options for drawing the track.</returns>
public virtual TrackDrawOptions GetTrackOptions(TrackAsset track, UnityEngine.Object binding)
{
return new TrackDrawOptions()
{
errorText = GetErrorText(track, binding, TrackBindingErrors.All),
minimumHeight = DefaultTrackHeight,
trackColor = GetTrackColor(track),
icon = null
};
}
/// <summary>
/// Gets the error text for the specified track.
/// </summary>
/// <param name="track">The track to retrieve options for.</param>
/// <param name="boundObject">The binding for the track.</param>
/// <param name="detectErrors">The errors to check for.</param>
/// <returns>An error to be displayed on the track, or string.Empty if there is no error.</returns>
public string GetErrorText(TrackAsset track, UnityEngine.Object boundObject, TrackBindingErrors detectErrors)
{
if (track == null || boundObject == null)
return string.Empty;
var bindingType = GetBindingType(track);
if (bindingType != null)
{
// bound to a prefab asset
if (HasFlag(detectErrors, TrackBindingErrors.PrefabBound) && PrefabUtility.IsPartOfPrefabAsset(boundObject))
{
return k_PrefabBound;
}
// If we are a component, allow for bound game objects (legacy)
if (typeof(Component).IsAssignableFrom(bindingType))
{
var gameObject = boundObject as GameObject;
var component = boundObject as Component;
if (component != null)
gameObject = component.gameObject;
// game object is bound with no component
if (HasFlag(detectErrors, TrackBindingErrors.NoValidComponent) && gameObject != null && component == null)
{
component = gameObject.GetComponent(bindingType);
if (component == null)
{
return k_NoValidComponent;
}
}
// attached gameObject is disables (ignores Activation Track)
if (HasFlag(detectErrors, TrackBindingErrors.BoundGameObjectDisabled) && gameObject != null && !gameObject.activeInHierarchy)
{
return k_BoundGameObjectDisabled;
}
// component is disabled
var behaviour = component as Behaviour;
if (HasFlag(detectErrors, TrackBindingErrors.BehaviourIsDisabled) && behaviour != null && !behaviour.enabled)
{
return k_RequiredComponentIsDisabled;
}
// mismatched binding
if (HasFlag(detectErrors, TrackBindingErrors.InvalidBinding) && component != null && !bindingType.IsAssignableFrom(component.GetType()))
{
return k_InvalidBinding;
}
}
// Mismatched binding (non-component)
else if (HasFlag(detectErrors, TrackBindingErrors.InvalidBinding) && !bindingType.IsAssignableFrom(boundObject.GetType()))
{
return k_InvalidBinding;
}
}
return string.Empty;
}
/// <summary>
/// Gets the color information of a track.
/// </summary>
/// <param name="track"></param>
/// <returns>Returns the color for the specified track.</returns>
public Color GetTrackColor(TrackAsset track)
{
return TrackResourceCache.GetTrackColor(track);
}
/// <summary>
/// Gets the binding type for a track.
/// </summary>
/// <param name="track">The track to retrieve the binding type from.</param>
/// <returns>Returns the binding type for the specified track. Returns null if the track does not have binding.</returns>
public System.Type GetBindingType(TrackAsset track)
{
if (track == null)
return null;
if (m_BindingCache.TryGetValue(track, out var result))
return result;
result = track.outputs.Select(x => x.outputTargetType).FirstOrDefault();
m_BindingCache[track] = result;
return result;
}
/// <summary>
/// Callback for when a track is created.
/// </summary>
/// <param name="track">The track that is created.</param>
/// <param name="copiedFrom">The source that the track is copied from. This can be set to null if the track is not a copy.</param>
public virtual void OnCreate(TrackAsset track, TrackAsset copiedFrom)
{
}
/// <summary>
/// Callback for when a track is changed.
/// </summary>
/// <param name="track">The track that is changed.</param>
public virtual void OnTrackChanged(TrackAsset track)
{
}
static bool HasFlag(TrackBindingErrors errors, TrackBindingErrors flag)
{
return (errors & flag) != 0;
}
/// <summary>
/// Override this method to validate if a binding for <paramref name="track"/>
/// can be determined from <paramref name="candidate"/>.
///
/// The default implementation of this method will return true if
/// - <paramref name="candidate"/> is not null or,
/// - <paramref name="candidate"/> is not part of a Prefab Asset or,
/// - <paramref name="candidate"/> is a Component that can be bound to <paramref name="track"/>
/// </summary>
/// <param name="candidate"></param>
/// <param name="track">TBD</param>
/// <returns>True if a binding can be determined from <paramref name="candidate"/>.</returns>
/// <seealso cref="UnityEngine.Timeline.TrackBindingTypeAttribute"/>
/// <seealso cref="UnityEngine.Timeline.TrackAsset"/>
public virtual bool IsBindingAssignableFrom(UnityEngine.Object candidate, TrackAsset track)
{
var action = BindingUtility.GetBindingAction(GetBindingType(track), candidate);
return action != BindingUtility.BindingAction.DoNotBind;
}
/// <summary>
/// Override this method to determine which object to bind to <paramref name="track"/>.
/// A binding object should be determined from <paramref name="candidate"/>.
///
/// By default, the `TrackBindingType` attribute from <paramref name="track"/> will be used to determine the binding.
/// </summary>
/// <param name="candidate">The source object from which a track binding should be determined.</param>
/// <param name="track">The track to bind an object to.</param>
/// <returns>The object to bind to <paramref name="track"/>.</returns>
/// <seealso cref="UnityEngine.Timeline.TrackBindingTypeAttribute"/>
/// <seealso cref="UnityEngine.Timeline.TrackAsset"/>
public virtual UnityEngine.Object GetBindingFrom(UnityEngine.Object candidate, TrackAsset track)
{
Type bindingType = GetBindingType(track);
BindingUtility.BindingAction action = BindingUtility.GetBindingAction(bindingType, candidate);
return BindingUtility.GetBinding(action, candidate, bindingType);
}
}
static class TrackEditorExtension
{
public static bool SupportsBindingAssign(this TrackEditor editor)
{
return TypeUtility.HasOverrideMethod(editor.GetType(), nameof(TrackEditor.GetBindingFrom));
}
public static void OnCreate_Safe(this TrackEditor editor, TrackAsset track, TrackAsset copiedFrom)
{
try
{
editor.OnCreate(track, copiedFrom);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
public static TrackDrawOptions GetTrackOptions_Safe(this TrackEditor editor, TrackAsset track, UnityEngine.Object binding)
{
try
{
return editor.GetTrackOptions(track, binding);
}
catch (Exception e)
{
Debug.LogException(e);
return CustomTimelineEditorCache.GetDefaultTrackEditor().GetTrackOptions(track, binding);
}
}
public static UnityEngine.Object GetBindingFrom_Safe(this TrackEditor editor, UnityEngine.Object candidate, TrackAsset track)
{
try
{
return editor.GetBindingFrom(candidate, track);
}
catch (Exception e)
{
Debug.LogException(e);
return candidate;
}
}
public static bool IsBindingAssignableFrom_Safe(this TrackEditor editor, UnityEngine.Object candidate, TrackAsset track)
{
try
{
return editor.IsBindingAssignableFrom(candidate, track);
}
catch (Exception e)
{
Debug.LogException(e);
return false;
}
}
public static void OnTrackChanged_Safe(this TrackEditor editor, TrackAsset track)
{
try
{
editor.OnTrackChanged(track);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
}
}

View File

@@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEditor.Timeline;
namespace UnityEngine.Timeline
{
[Serializable]
class DirectorNamedColor : ScriptableObject
{
[SerializeField]
public Color colorPlayhead;
[SerializeField]
public Color colorSelection;
[SerializeField]
public Color colorEndmarker;
[SerializeField]
public Color colorGroup;
[SerializeField]
public Color colorGroupTrackBackground;
[SerializeField]
public Color colorAnimation;
[SerializeField]
public Color colorAnimationRecorded;
[SerializeField]
public Color colorAudio;
[SerializeField]
public Color colorAudioWaveform;
[SerializeField]
public Color colorActivation;
[SerializeField]
public Color colorDropTarget;
[SerializeField]
public Color colorClipFont;
[SerializeField]
public Color colorInvalidClipOverlay;
[SerializeField]
public Color colorTrackBackground;
[SerializeField]
public Color colorTrackHeaderBackground;
[SerializeField]
public Color colorTrackDarken;
[SerializeField]
public Color colorTrackBackgroundRecording;
[SerializeField]
public Color colorInfiniteTrackBackgroundRecording;
[SerializeField]
public Color colorTrackBackgroundSelected;
[SerializeField]
public Color colorTrackFont;
[SerializeField]
public Color colorClipUnion;
[SerializeField]
public Color colorTopOutline3;
[SerializeField]
public Color colorDurationLine;
[SerializeField]
public Color colorRange;
[SerializeField]
public Color colorSequenceBackground;
[SerializeField]
public Color colorTooltipBackground;
[SerializeField]
public Color colorInfiniteClipLine;
[SerializeField]
public Color colorDefaultTrackDrawer;
[SerializeField]
public Color colorDuration = new Color(0.66f, 0.66f, 0.66f, 1.0f);
[SerializeField]
public Color colorRecordingClipOutline = new Color(1, 0, 0, 0.9f);
[SerializeField]
public Color colorAnimEditorBinding = new Color(54.0f / 255.0f, 54.0f / 255.0f, 54.0f / 255.0f);
[SerializeField]
public Color colorTimelineBackground = new Color(0.2f, 0.2f, 0.2f, 1.0f);
[SerializeField]
public Color colorLockTextBG = Color.red;
[SerializeField]
public Color colorInlineCurveVerticalLines = new Color(1.0f, 1.0f, 1.0f, 0.2f);
[SerializeField]
public Color colorInlineCurveOutOfRangeOverlay = new Color(0.0f, 0.0f, 0.0f, 0.5f);
[SerializeField]
public Color colorInlineCurvesBackground;
[SerializeField]
public Color markerDrawerBackgroundColor = new Color(0.4f, 0.4f, 0.4f , 1.0f);
[SerializeField]
public Color markerHeaderDrawerBackgroundColor = new Color(0.5f, 0.5f, 0.5f , 1.0f);
[SerializeField]
public Color colorControl = new Color(0.2313f, 0.6353f, 0.5843f, 1.0f);
[SerializeField]
public Color colorSubSequenceBackground = new Color(0.1294118f, 0.1764706f, 0.1764706f, 1.0f);
[SerializeField]
public Color colorTrackSubSequenceBackground = new Color(0.1607843f, 0.2156863f, 0.2156863f, 1.0f);
[SerializeField]
public Color colorTrackSubSequenceBackgroundSelected = new Color(0.0726923f, 0.252f, 0.252f, 1.0f);
[SerializeField]
public Color colorSubSequenceOverlay = new Color(0.02f, 0.025f, 0.025f, 0.30f);
[SerializeField]
public Color colorSubSequenceDurationLine = new Color(0.0f, 1.0f, 0.88f, 0.46f);
[SerializeField]
public Color clipBckg = new Color(0.5f, 0.5f, 0.5f, 1.0f);
[SerializeField]
public Color clipSelectedBckg = new Color(0.7f, 0.7f, 0.7f, 1.0f);
[SerializeField]
public Color clipBorderColor = new Color(0.4f, 0.4f, 0.4f, 1.0f);
[SerializeField]
public Color clipEaseBckgColor = new Color(0.4f, 0.4f, 0.4f, 1.0f);
[SerializeField]
public Color clipBlendIn;
[SerializeField]
public Color clipBlendInSelected;
[SerializeField]
public Color clipBlendOut;
[SerializeField]
public Color clipBlendOutSelected;
public void SetDefault()
{
colorPlayhead = DirectorStyles.Instance.timeCursor.normal.textColor;
colorSelection = DirectorStyles.Instance.selectedStyle.normal.textColor;
colorEndmarker = DirectorStyles.Instance.endmarker.normal.textColor;
colorGroup = new Color(0.094f, 0.357f, 0.384f, 0.310f);
colorGroupTrackBackground = new Color(0f, 0f, 0f, 1f);
colorAnimation = new Color(0.3f, 0.39f, 0.46f, 1.0f);
colorAnimationRecorded = new Color(colorAnimation.r * 0.75f, colorAnimation.g * 0.75f, colorAnimation.b * 0.75f, 1.0f);
colorAudio = new Color(1f, 0.635f, 0f);
colorAudioWaveform = new Color(0.129f, 0.164f, 0.254f);
colorActivation = Color.green;
colorDropTarget = new Color(0.514f, 0.627f, 0.827f);
colorClipFont = DirectorStyles.Instance.fontClip.normal.textColor;
colorTrackBackground = new Color(0.2f, 0.2f, 0.2f, 1.0f);
colorTrackBackgroundSelected = new Color(1f, 1f, 1f, 0.33f);
colorInlineCurvesBackground = new Color(0.25f, 0.25f, 0.25f, 0.6f);
colorTrackFont = DirectorStyles.Instance.trackHeaderFont.normal.textColor;
colorClipUnion = new Color(0.72f, 0.72f, 0.72f, 0.8f);
colorTopOutline3 = new Color(0.274f, 0.274f, 0.274f, 1.0f);
colorDurationLine = new Color(33.0f / 255.0f, 109.0f / 255.0f, 120.0f / 255.0f);
colorRange = new Color(0.733f, 0.733f, 0.733f, 0.70f);
colorSequenceBackground = new Color(0.16f, 0.16f, 0.16f, 1.0f);
colorTooltipBackground = new Color(29.0f / 255.0f, 32.0f / 255.0f, 33.0f / 255.0f);
colorInfiniteClipLine = new Color(72.0f / 255.0f, 78.0f / 255.0f, 82.0f / 255.0f);
colorTrackBackgroundRecording = new Color(1, 0, 0, 0.1f);
colorTrackDarken = new Color(0.0f, 0.0f, 0.0f, 0.4f);
colorTrackHeaderBackground = new Color(51.0f / 255.0f, 51.0f / 255.0f, 51.0f / 255.0f, 1.0f);
colorDefaultTrackDrawer = new Color(218.0f / 255.0f, 220.0f / 255.0f, 222.0f / 255.0f);
colorRecordingClipOutline = new Color(1, 0, 0, 0.9f);
colorInlineCurveVerticalLines = new Color(1.0f, 1.0f, 1.0f, 0.2f);
colorInlineCurveOutOfRangeOverlay = new Color(0.0f, 0.0f, 0.0f, 0.5f);
}
public void ToText(string path)
{
StringBuilder builder = new StringBuilder();
var fields = GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach (var f in fields)
{
if (f.FieldType != typeof(Color))
continue;
Color c = (Color)f.GetValue(this);
builder.Append(f.Name + "," + c);
builder.Append("\n");
}
File.WriteAllText(path, builder.ToString());
}
public void FromText(string text)
{
// parse to a map
string[] lines = text.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var map = new Dictionary<string, Color>();
foreach (var line in lines)
{
var pieces = line.Replace("RGBA(", "").Replace(")", "").Split(',');
if (pieces.Length == 5)
{
string name = pieces[0].Trim();
Color c = Color.black;
bool b = ParseFloat(pieces[1], out c.r) &&
ParseFloat(pieces[2], out c.g) &&
ParseFloat(pieces[3], out c.b) &&
ParseFloat(pieces[4], out c.a);
if (b)
{
map[name] = c;
}
}
}
var fields = typeof(DirectorNamedColor).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach (var f in fields)
{
if (f.FieldType != typeof(Color))
continue;
Color c = Color.black;
if (map.TryGetValue(f.Name, out c))
{
f.SetValue(this, c);
}
}
}
// Case 938534 - Timeline window has white background when running on .NET 4.6 depending on the set system language
// Make sure we're using an invariant culture so "0.35" is parsed as 0.35 and not 35
static bool ParseFloat(string str, out float f)
{
return float.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out f);
}
public static DirectorNamedColor CreateAndLoadFromText(string text)
{
DirectorNamedColor instance = CreateInstance<DirectorNamedColor>();
instance.FromText(text);
return instance;
}
}
}

View File

@@ -0,0 +1,370 @@
using UnityEditor.Experimental;
using UnityEditor.StyleSheets;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class DirectorStyles
{
const string k_Elipsis = "…";
const string k_ImagePath = "Packages/com.unity.timeline/Editor/StyleSheets/Images/Icons/{0}.png";
public const string resourcesPath = "Packages/com.unity.timeline/Editor/StyleSheets/res/";
//Timeline resources
public const string newTimelineDefaultNameSuffix = "Timeline";
public static readonly GUIContent referenceTrackLabel = TrTextContent("R", "This track references an external asset");
public static readonly GUIContent recordingLabel = TrTextContent("Recording...");
public static readonly GUIContent noTimelineAssetSelected = TrTextContent("To start creating a timeline, select a GameObject");
public static readonly GUIContent createTimelineOnSelection = TrTextContent("To begin a new timeline with {0}, create {1}");
public static readonly GUIContent noTimelinesInScene = TrTextContent("No timeline found in the scene");
public static readonly GUIContent createNewTimelineText = TrTextContent("Create a new Timeline and Director Component for Game Object");
public static readonly GUIContent previewContent = TrTextContent("Preview", "Enable/disable scene preview mode");
public static readonly GUIContent previewDisabledContent = L10n.TextContentWithIcon("Preview", "Scene preview is disabled for this TimelineAsset", MessageType.Info);
public static readonly GUIContent mixOff = TrIconContent("TimelineEditModeMixOFF", "Mix Mode (1)");
public static readonly GUIContent mixOn = TrIconContent("TimelineEditModeMixON", "Mix Mode (1)");
public static readonly GUIContent rippleOff = TrIconContent("TimelineEditModeRippleOFF", "Ripple Mode (2)");
public static readonly GUIContent rippleOn = TrIconContent("TimelineEditModeRippleON", "Ripple Mode (2)");
public static readonly GUIContent replaceOff = TrIconContent("TimelineEditModeReplaceOFF", "Replace Mode (3)");
public static readonly GUIContent replaceOn = TrIconContent("TimelineEditModeReplaceON", "Replace Mode (3)");
public static readonly GUIContent showMarkersOn = TrIconContent("TimelineCollapseMarkerButtonEnabled", "Show / Hide Timeline Markers");
public static readonly GUIContent showMarkersOff = TrIconContent("TimelineCollapseMarkerButtonDisabled", "Show / Hide Timeline Markers");
public static readonly GUIContent showMarkersOnTimeline = TrTextContent("Show markers");
public static readonly GUIContent timelineMarkerTrackHeader = TrTextContentWithIcon("Markers", string.Empty, "TimelineHeaderMarkerIcon");
public static readonly GUIContent signalTrackIcon = IconContent("TimelineSignal");
//Unity Default Resources
public static readonly GUIContent playContent = L10n.IconContent("Animation.Play", "Play the timeline (Space)");
public static readonly GUIContent gotoBeginingContent = L10n.IconContent("Animation.FirstKey", "Go to the beginning of the timeline (Shift+<)");
public static readonly GUIContent gotoEndContent = L10n.IconContent("Animation.LastKey", "Go to the end of the timeline (Shift+>)");
public static readonly GUIContent nextFrameContent = L10n.IconContent("Animation.NextKey", "Go to the next frame");
public static readonly GUIContent previousFrameContent = L10n.IconContent("Animation.PrevKey", "Go to the previous frame");
public static readonly GUIContent newContent = L10n.IconContent("CreateAddNew", "Add new tracks.");
public static readonly GUIContent optionsCogIcon = L10n.IconContent("_Popup", "Options");
public static readonly GUIContent animationTrackIcon = EditorGUIUtility.IconContent("AnimationClip Icon");
public static readonly GUIContent audioTrackIcon = EditorGUIUtility.IconContent("AudioSource Icon");
public static readonly GUIContent playableTrackIcon = EditorGUIUtility.IconContent("cs Script Icon");
public static readonly GUIContent timelineSelectorArrow = L10n.IconContent("icon dropdown", "Timeline Selector");
public GUIContent playrangeContent;
public static readonly float kBaseIndent = 15.0f;
public static readonly float kDurationGuiThickness = 5.0f;
// matches dark skin warning color.
public static readonly Color kClipErrorColor = new Color(0.957f, 0.737f, 0.008f, 1f);
// TODO: Make skinnable? If we do, we should probably also make the associated cursors skinnable...
public static readonly Color kMixToolColor = Color.white;
public static readonly Color kRippleToolColor = new Color(255f / 255f, 210f / 255f, 51f / 255f);
public static readonly Color kReplaceToolColor = new Color(165f / 255f, 30f / 255f, 30f / 255f);
public const string markerDefaultStyle = "MarkerItem";
public GUIStyle groupBackground;
public GUIStyle displayBackground;
public GUIStyle fontClip;
public GUIStyle fontClipLoop;
public GUIStyle trackHeaderFont;
public GUIStyle trackGroupAddButton;
public GUIStyle groupFont;
public GUIStyle timeCursor;
public GUIStyle endmarker;
public GUIStyle tinyFont;
public GUIStyle foldout;
public GUIStyle trackMuteButton;
public GUIStyle trackLockButton;
public GUIStyle trackRecordButton;
public GUIStyle playTimeRangeStart;
public GUIStyle playTimeRangeEnd;
public GUIStyle selectedStyle;
public GUIStyle trackSwatchStyle;
public GUIStyle connector;
public GUIStyle keyframe;
public GUIStyle warning;
public GUIStyle extrapolationHold;
public GUIStyle extrapolationLoop;
public GUIStyle extrapolationPingPong;
public GUIStyle extrapolationContinue;
public GUIStyle trackMarkerButton;
public GUIStyle markerMultiOverlay;
public GUIStyle bottomShadow;
public GUIStyle trackOptions;
public GUIStyle infiniteTrack;
public GUIStyle clipOut;
public GUIStyle clipIn;
public GUIStyle trackCurvesButton;
public GUIStyle trackLockOverlay;
public GUIStyle activation;
public GUIStyle playrange;
public GUIStyle timelineLockButton;
public GUIStyle trackAvatarMaskButton;
public GUIStyle markerWarning;
public GUIStyle editModeBtn;
public GUIStyle showMarkersBtn;
public GUIStyle sequenceSwitcher;
public GUIStyle inlineCurveHandle;
public GUIStyle timeReferenceButton;
public GUIStyle trackButtonSuite;
public GUIStyle previewButtonDisabled;
static internal DirectorStyles s_Instance;
DirectorNamedColor m_DarkSkinColors;
DirectorNamedColor m_LightSkinColors;
DirectorNamedColor m_DefaultSkinColors;
const string k_DarkSkinPath = resourcesPath + "Timeline_DarkSkin.txt";
const string k_LightSkinPath = resourcesPath + "Timeline_LightSkin.txt";
static readonly GUIContent s_TempContent = new GUIContent();
public static bool IsInitialized
{
get { return s_Instance != null; }
}
public static DirectorStyles Instance
{
get
{
if (s_Instance == null)
{
s_Instance = new DirectorStyles();
s_Instance.Initialize();
}
return s_Instance;
}
}
public static void ReloadStylesIfNeeded()
{
if (Instance.ShouldLoadStyles())
{
Instance.LoadStyles();
if (!Instance.ShouldLoadStyles())
Instance.Initialize();
}
}
public DirectorNamedColor customSkin
{
get { return EditorGUIUtility.isProSkin ? m_DarkSkinColors : m_LightSkinColors; }
internal set
{
if (EditorGUIUtility.isProSkin)
m_DarkSkinColors = value;
else
m_LightSkinColors = value;
}
}
DirectorNamedColor LoadColorSkin(string path)
{
var asset = EditorGUIUtility.LoadRequired(path) as TextAsset;
if (asset != null && !string.IsNullOrEmpty(asset.text))
{
return DirectorNamedColor.CreateAndLoadFromText(asset.text);
}
return m_DefaultSkinColors;
}
static DirectorNamedColor CreateDefaultSkin()
{
var nc = ScriptableObject.CreateInstance<DirectorNamedColor>();
nc.SetDefault();
return nc;
}
public void ExportSkinToFile()
{
if (customSkin == m_DarkSkinColors)
customSkin.ToText(k_DarkSkinPath);
if (customSkin == m_LightSkinColors)
customSkin.ToText(k_LightSkinPath);
}
public void ReloadSkin()
{
if (customSkin == m_DarkSkinColors)
{
m_DarkSkinColors = LoadColorSkin(k_DarkSkinPath);
}
else if (customSkin == m_LightSkinColors)
{
m_LightSkinColors = LoadColorSkin(k_LightSkinPath);
}
}
public void Initialize()
{
m_DefaultSkinColors = CreateDefaultSkin();
m_DarkSkinColors = LoadColorSkin(k_DarkSkinPath);
m_LightSkinColors = LoadColorSkin(k_LightSkinPath);
// add the built in colors (control track uses attribute)
TrackResourceCache.ClearTrackColorCache();
TrackResourceCache.SetTrackColor<AnimationTrack>(customSkin.colorAnimation);
TrackResourceCache.SetTrackColor<PlayableTrack>(Color.white);
TrackResourceCache.SetTrackColor<AudioTrack>(customSkin.colorAudio);
TrackResourceCache.SetTrackColor<ActivationTrack>(customSkin.colorActivation);
TrackResourceCache.SetTrackColor<GroupTrack>(customSkin.colorGroup);
TrackResourceCache.SetTrackColor<ControlTrack>(customSkin.colorControl);
// add default icons
TrackResourceCache.ClearTrackIconCache();
TrackResourceCache.SetTrackIcon<AnimationTrack>(animationTrackIcon);
TrackResourceCache.SetTrackIcon<AudioTrack>(audioTrackIcon);
TrackResourceCache.SetTrackIcon<PlayableTrack>(playableTrackIcon);
TrackResourceCache.SetTrackIcon<ActivationTrack>(new GUIContent(GetBackgroundImage(activation)));
TrackResourceCache.SetTrackIcon<SignalTrack>(signalTrackIcon);
}
DirectorStyles()
{
LoadStyles();
}
bool ShouldLoadStyles()
{
return endmarker == null ||
endmarker.name == GUISkin.error.name;
}
void LoadStyles()
{
endmarker = GetGUIStyle("Icon-Endmarker");
groupBackground = GetGUIStyle("groupBackground");
displayBackground = GetGUIStyle("sequenceClip");
fontClip = GetGUIStyle("Font-Clip");
trackHeaderFont = GetGUIStyle("sequenceTrackHeaderFont");
trackGroupAddButton = GetGUIStyle("sequenceTrackGroupAddButton");
groupFont = GetGUIStyle("sequenceGroupFont");
timeCursor = GetGUIStyle("Icon-TimeCursor");
tinyFont = GetGUIStyle("tinyFont");
foldout = GetGUIStyle("Icon-Foldout");
trackMuteButton = GetGUIStyle("trackMuteButton");
trackLockButton = GetGUIStyle("trackLockButton");
trackRecordButton = GetGUIStyle("trackRecordButton");
playTimeRangeStart = GetGUIStyle("Icon-PlayAreaStart");
playTimeRangeEnd = GetGUIStyle("Icon-PlayAreaEnd");
selectedStyle = GetGUIStyle("Color-Selected");
trackSwatchStyle = GetGUIStyle("Icon-TrackHeaderSwatch");
connector = GetGUIStyle("Icon-Connector");
keyframe = GetGUIStyle("Icon-Keyframe");
warning = GetGUIStyle("Icon-Warning");
extrapolationHold = GetGUIStyle("Icon-ExtrapolationHold");
extrapolationLoop = GetGUIStyle("Icon-ExtrapolationLoop");
extrapolationPingPong = GetGUIStyle("Icon-ExtrapolationPingPong");
extrapolationContinue = GetGUIStyle("Icon-ExtrapolationContinue");
bottomShadow = GetGUIStyle("Icon-Shadow");
trackOptions = GetGUIStyle("Icon-TrackOptions");
infiniteTrack = GetGUIStyle("Icon-InfiniteTrack");
clipOut = GetGUIStyle("Icon-ClipOut");
clipIn = GetGUIStyle("Icon-ClipIn");
trackCurvesButton = GetGUIStyle("trackCurvesButton");
trackLockOverlay = GetGUIStyle("trackLockOverlay");
activation = GetGUIStyle("Icon-Activation");
playrange = GetGUIStyle("Icon-Playrange");
timelineLockButton = GetGUIStyle("IN LockButton");
trackAvatarMaskButton = GetGUIStyle("trackAvatarMaskButton");
trackMarkerButton = GetGUIStyle("trackCollapseMarkerButton");
markerMultiOverlay = GetGUIStyle("MarkerMultiOverlay");
editModeBtn = GetGUIStyle("editModeBtn");
showMarkersBtn = GetGUIStyle("showMarkerBtn");
markerWarning = GetGUIStyle("markerWarningOverlay");
sequenceSwitcher = GetGUIStyle("sequenceSwitcher");
inlineCurveHandle = GetGUIStyle("RL DragHandle");
timeReferenceButton = GetGUIStyle("timeReferenceButton");
trackButtonSuite = GetGUIStyle("trackButtonSuite");
previewButtonDisabled = GetGUIStyle("previewButtonDisabled");
playrangeContent = new GUIContent(GetBackgroundImage(playrange)) { tooltip = L10n.Tr("Toggle play range markers.") };
fontClipLoop = new GUIStyle(fontClip) { fontStyle = FontStyle.Bold };
}
public static GUIStyle GetGUIStyle(string s)
{
return EditorStyles.FromUSS(s);
}
public static GUIContent TrIconContent(string iconName, string tooltip = null)
{
return L10n.IconContent(iconName == null ? null : ResolveIcon(iconName), tooltip);
}
public static GUIContent IconContent(string iconName)
{
return EditorGUIUtility.IconContent(iconName == null ? null : ResolveIcon(iconName));
}
public static GUIContent TrTextContentWithIcon(string text, string tooltip, string iconName)
{
return L10n.TextContentWithIcon(text, tooltip, iconName == null ? null : ResolveIcon(iconName));
}
public static GUIContent TrTextContent(string text, string tooltip = null)
{
return L10n.TextContent(text, tooltip);
}
public static Texture2D LoadIcon(string iconName)
{
return EditorGUIUtility.LoadIconRequired(iconName == null ? null : ResolveIcon(iconName));
}
static string ResolveIcon(string icon)
{
return string.Format(k_ImagePath, icon);
}
public static string Elipsify(string label, Rect rect, GUIStyle style)
{
var ret = label;
if (label.Length == 0)
return ret;
s_TempContent.text = label;
float neededWidth = style.CalcSize(s_TempContent).x;
return Elipsify(label, rect.width, neededWidth);
}
public static string Elipsify(string label, float destinationWidth, float neededWidth)
{
var ret = label;
if (label.Length == 0)
return ret;
if (destinationWidth < neededWidth)
{
float averageWidthOfOneChar = neededWidth / label.Length;
int floor = Mathf.Max((int)Mathf.Floor(destinationWidth / averageWidthOfOneChar), 0);
if (floor < k_Elipsis.Length)
ret = string.Empty;
else if (floor == k_Elipsis.Length)
ret = k_Elipsis;
else if (floor < label.Length)
ret = label.Substring(0, floor - k_Elipsis.Length) + k_Elipsis;
}
return ret;
}
public static Texture2D GetBackgroundImage(GUIStyle style, StyleState state = StyleState.normal)
{
var blockName = GUIStyleExtensions.StyleNameToBlockName(style.name, false);
var styleBlock = EditorResources.GetStyle(blockName, state);
return styleBlock.GetTexture(StyleCatalogKeyword.backgroundImage);
}
}
}

View File

@@ -0,0 +1,151 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class AnimatedParameterExtensions
{
public static bool HasAnyAnimatableParameters(this ICurvesOwner curvesOwner)
{
return AnimatedParameterUtility.HasAnyAnimatableParameters(curvesOwner.asset);
}
public static IEnumerable<SerializedProperty> GetAllAnimatableParameters(this ICurvesOwner curvesOwner)
{
return AnimatedParameterUtility.GetAllAnimatableParameters(curvesOwner.asset);
}
public static bool IsParameterAnimatable(this ICurvesOwner curvesOwner, string parameterName)
{
return AnimatedParameterUtility.IsParameterAnimatable(curvesOwner.asset, parameterName);
}
public static bool IsParameterAnimated(this ICurvesOwner curvesOwner, string parameterName)
{
return AnimatedParameterUtility.IsParameterAnimated(curvesOwner.asset, curvesOwner.curves, parameterName);
}
public static EditorCurveBinding GetCurveBinding(this ICurvesOwner curvesOwner, string parameterName)
{
return AnimatedParameterUtility.GetCurveBinding(curvesOwner.asset, parameterName);
}
public static string GetUniqueRecordedClipName(this ICurvesOwner curvesOwner)
{
return AnimationTrackRecorder.GetUniqueRecordedClipName(curvesOwner.assetOwner, curvesOwner.defaultCurvesName);
}
public static AnimationCurve GetAnimatedParameter(this ICurvesOwner curvesOwner, string bindingName)
{
return AnimatedParameterUtility.GetAnimatedParameter(curvesOwner.asset, curvesOwner.curves, bindingName);
}
public static bool AddAnimatedParameterValueAt(this ICurvesOwner curvesOwner, string parameterName, float value, float time)
{
if (!curvesOwner.IsParameterAnimatable(parameterName))
return false;
if (curvesOwner.curves == null)
curvesOwner.CreateCurves(curvesOwner.GetUniqueRecordedClipName());
var binding = curvesOwner.GetCurveBinding(parameterName);
var curve = AnimationUtility.GetEditorCurve(curvesOwner.curves, binding) ?? new AnimationCurve();
var serializedObject = AnimatedParameterUtility.GetSerializedPlayableAsset(curvesOwner.asset);
var property = serializedObject.FindProperty(parameterName);
bool isStepped = property.propertyType == SerializedPropertyType.Boolean ||
property.propertyType == SerializedPropertyType.Integer ||
property.propertyType == SerializedPropertyType.Enum;
TimelineUndo.PushUndo(curvesOwner.curves, "Set Key");
CurveEditUtility.AddKeyFrameToCurve(curve, time, curvesOwner.curves.frameRate, value, isStepped);
AnimationUtility.SetEditorCurve(curvesOwner.curves, binding, curve);
return true;
}
public static void SanitizeCurvesData(this ICurvesOwner curvesOwner)
{
var curves = curvesOwner.curves;
if (curves == null)
return;
// Remove any 0-length curves
foreach (var binding in AnimationUtility.GetCurveBindings(curves))
{
var curve = AnimationUtility.GetEditorCurve(curves, binding);
if (curve.length == 0)
AnimationUtility.SetEditorCurve(curves, binding, null);
}
// If no curves remain, delete the curves asset
if (curves.empty)
{
var track = curvesOwner.targetTrack;
var timeline = track != null ? track.timelineAsset : null;
TimelineUndo.PushDestroyUndo(timeline, track, curves);
}
}
public static bool AddAnimatedParameter(this ICurvesOwner curvesOwner, string parameterName)
{
var newBinding = new EditorCurveBinding();
SerializedProperty property;
if (!InternalAddParameter(curvesOwner, parameterName, ref newBinding, out property))
return false;
var duration = (float)curvesOwner.duration;
CurveEditUtility.AddKey(curvesOwner.curves, newBinding, property, 0);
CurveEditUtility.AddKey(curvesOwner.curves, newBinding, property, duration);
return true;
}
public static bool RemoveAnimatedParameter(this ICurvesOwner curvesOwner, string parameterName)
{
if (!curvesOwner.IsParameterAnimated(parameterName) || curvesOwner.curves == null)
return false;
var binding = curvesOwner.GetCurveBinding(parameterName);
AnimationUtility.SetEditorCurve(curvesOwner.curves, binding, null);
return true;
}
// Set an animated parameter. Requires the field identifier 'position.x', but will add default curves to all fields
public static bool SetAnimatedParameter(this ICurvesOwner curvesOwner, string parameterName, AnimationCurve curve)
{
// this will add a basic curve for all the related parameters
if (!curvesOwner.IsParameterAnimated(parameterName) && !curvesOwner.AddAnimatedParameter(parameterName))
return false;
var binding = curvesOwner.GetCurveBinding(parameterName);
AnimationUtility.SetEditorCurve(curvesOwner.curves, binding, curve);
return true;
}
static bool InternalAddParameter([NotNull] ICurvesOwner curvesOwner, string parameterName, ref EditorCurveBinding binding, out SerializedProperty property)
{
property = null;
if (curvesOwner.IsParameterAnimated(parameterName))
return false;
var serializedObject = AnimatedParameterUtility.GetSerializedPlayableAsset(curvesOwner.asset);
if (serializedObject == null)
return false;
property = serializedObject.FindProperty(parameterName);
if (property == null || !AnimatedParameterUtility.IsTypeAnimatable(property.propertyType))
return false;
if (curvesOwner.curves == null)
curvesOwner.CreateCurves(curvesOwner.GetUniqueRecordedClipName());
binding = curvesOwner.GetCurveBinding(parameterName);
return true;
}
}
}

View File

@@ -0,0 +1,206 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
/// <summary>
/// Extension Methods for AnimationTracks that require the Unity Editor, and may require the Timeline containing the Animation Track to be currently loaded in the Timeline Editor Window.
/// </summary>
public static class AnimationTrackExtensions
{
/// <summary>
/// Determines whether the Timeline window can enable recording mode on an AnimationTrack.
/// For a track to support recording, it needs to have a valid scene binding,
/// its offset mode should not be Auto and needs to be currently visible in the Timeline Window.
/// </summary>
/// <param name="track">The track to query.</param>
/// <returns>True if recording can start, False otherwise.</returns>
public static bool CanStartRecording(this AnimationTrack track)
{
if (track == null)
{
throw new ArgumentNullException(nameof(track));
}
if (TimelineEditor.state == null)
{
return false;
}
var director = TimelineEditor.inspectedDirector;
var animTrack = TimelineUtility.GetSceneReferenceTrack(track) as AnimationTrack;
return animTrack != null && animTrack.trackOffset != TrackOffset.Auto &&
TimelineEditor.inspectedAsset == animTrack.timelineAsset &&
director != null && TimelineUtility.GetSceneGameObject(director, animTrack) != null;
}
/// <summary>
/// Method that allows querying if a track is current enabled for animation recording.
/// </summary>
/// <param name="track">The track to query.</param>
/// <returns>True if currently recording and False otherwise.</returns>
public static bool IsRecording(this AnimationTrack track)
{
if (track == null)
{
throw new ArgumentNullException(nameof(track));
}
return TimelineEditor.state != null && TimelineEditor.state.IsArmedForRecord(track);
}
/// <summary>
/// Method that enables animation recording for an AnimationTrack.
/// </summary>
/// <param name="track">The AnimationTrack which will be put in recording mode.</param>
/// <returns>True if track was put successfully in recording mode, False otherwise. </returns>
public static bool StartRecording(this AnimationTrack track)
{
if (!CanStartRecording(track))
{
return false;
}
TimelineEditor.state.ArmForRecord(track);
return true;
}
/// <summary>
/// Disables recording mode of an AnimationTrack.
/// </summary>
/// <param name="track">The AnimationTrack which will be taken out of recording mode.</param>
public static void StopRecording(this AnimationTrack track)
{
if (!IsRecording(track) || TimelineEditor.state == null)
{
return;
}
TimelineEditor.state.UnarmForRecord(track);
}
internal static void ConvertToClipMode(this AnimationTrack track)
{
if (!track.CanConvertToClipMode())
return;
UndoExtensions.RegisterTrack(track, L10n.Tr("Convert To Clip"));
if (!track.infiniteClip.empty)
{
var animClip = track.infiniteClip;
TimelineUndo.PushUndo(animClip, L10n.Tr("Convert To Clip"));
UndoExtensions.RegisterTrack(track, L10n.Tr("Convert To Clip"));
var start = AnimationClipCurveCache.Instance.GetCurveInfo(animClip).keyTimes.FirstOrDefault();
animClip.ShiftBySeconds(-start);
track.infiniteClip = null;
var clip = track.CreateClip(animClip);
clip.start = start;
clip.preExtrapolationMode = track.infiniteClipPreExtrapolation;
clip.postExtrapolationMode = track.infiniteClipPostExtrapolation;
clip.recordable = true;
if (Mathf.Abs(animClip.length) < TimelineClip.kMinDuration)
{
clip.duration = 1;
}
var animationAsset = clip.asset as AnimationPlayableAsset;
if (animationAsset)
{
animationAsset.position = track.infiniteClipOffsetPosition;
animationAsset.eulerAngles = track.infiniteClipOffsetEulerAngles;
// going to / from infinite mode should reset this. infinite mode
animationAsset.removeStartOffset = track.infiniteClipRemoveOffset;
animationAsset.applyFootIK = track.infiniteClipApplyFootIK;
animationAsset.loop = track.infiniteClipLoop;
track.infiniteClipOffsetPosition = Vector3.zero;
track.infiniteClipOffsetEulerAngles = Vector3.zero;
}
track.CalculateExtrapolationTimes();
}
track.infiniteClip = null;
EditorUtility.SetDirty(track);
}
internal static void ConvertFromClipMode(this AnimationTrack track, TimelineAsset timeline)
{
if (!track.CanConvertFromClipMode())
return;
UndoExtensions.RegisterTrack(track, L10n.Tr("Convert From Clip"));
var clip = track.clips[0];
var delta = (float)clip.start;
track.infiniteClipTimeOffset = 0.0f;
track.infiniteClipPreExtrapolation = clip.preExtrapolationMode;
track.infiniteClipPostExtrapolation = clip.postExtrapolationMode;
var animAsset = clip.asset as AnimationPlayableAsset;
if (animAsset)
{
track.infiniteClipOffsetPosition = animAsset.position;
track.infiniteClipOffsetEulerAngles = animAsset.eulerAngles;
track.infiniteClipRemoveOffset = animAsset.removeStartOffset;
track.infiniteClipApplyFootIK = animAsset.applyFootIK;
track.infiniteClipLoop = animAsset.loop;
}
// clone it, it may not be in the same asset
var animClip = clip.animationClip;
float scale = (float)clip.timeScale;
if (!Mathf.Approximately(scale, 1.0f))
{
if (!Mathf.Approximately(scale, 0.0f))
scale = 1.0f / scale;
animClip.ScaleTime(scale);
}
TimelineUndo.PushUndo(animClip, L10n.Tr("Convert From Clip"));
animClip.ShiftBySeconds(delta);
// manually delete the clip
var asset = clip.asset;
clip.asset = null;
// Remove the clip, remove old assets
ClipModifier.Delete(timeline, clip);
TimelineUndo.PushDestroyUndo(null, track, asset);
track.infiniteClip = animClip;
EditorUtility.SetDirty(track);
}
internal static bool CanConvertToClipMode(this AnimationTrack track)
{
if (track == null || track.inClipMode)
return false;
return (track.infiniteClip != null && !track.infiniteClip.empty);
}
// Requirements to go from clip mode
// - one clip, recordable, and animation clip belongs to the same asset as the track
internal static bool CanConvertFromClipMode(this AnimationTrack track)
{
if ((track == null) ||
(!track.inClipMode) ||
(track.clips.Length != 1) ||
(track.clips[0].start < 0) ||
(!track.clips[0].recordable))
return false;
var asset = track.clips[0].asset as AnimationPlayableAsset;
if (asset == null)
return false;
return TimelineHelpers.HaveSameContainerAsset(track, asset.clip);
}
}
}

View File

@@ -0,0 +1,524 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
namespace UnityEditor.Timeline
{
/// <summary>
/// Extension Methods for Tracks that require the Unity Editor, and may require the Timeline containing the Track to be currently loaded in the Timeline Editor Window.
/// </summary>
public static class TrackExtensions
{
static readonly double kMinOverlapTime = TimeUtility.kTimeEpsilon * 1000;
/// <summary>
/// Queries whether the children of the Track are currently visible in the Timeline Editor.
/// </summary>
/// <param name="track">The track asset to query.</param>
/// <returns>True if the track is collapsed and false otherwise.</returns>
public static bool IsCollapsed(this TrackAsset track)
{
return TimelineWindowViewPrefs.IsTrackCollapsed(track);
}
/// <summary>
/// Sets whether the children of the Track are currently visible in the Timeline Editor.
/// </summary>
/// <param name="track">The track asset to collapsed state to modify.</param>
/// <param name="collapsed">`true` to collapse children, false otherwise.</param>
/// <remarks> The track collapsed state is not serialized inside the asset and is lost from one checkout of the project to another. </remarks>>
public static void SetCollapsed(this TrackAsset track, bool collapsed)
{
TimelineWindowViewPrefs.SetTrackCollapsed(track, collapsed);
}
/// <summary>
/// Queries whether any parent of the track is collapsed, rendering the track not visible to the user.
/// </summary>
/// <param name="track">The track asset to query.</param>
/// <returns>True if all parents are not collapsed, false otherwise.</returns>
public static bool IsVisibleInHierarchy(this TrackAsset track)
{
var t = track;
while ((t = t.parent as TrackAsset) != null)
{
if (t.IsCollapsed())
return false;
}
return true;
}
internal static AnimationClip GetOrCreateClip(this AnimationTrack track)
{
if (track.infiniteClip == null && !track.inClipMode)
track.CreateInfiniteClip(AnimationTrackRecorder.GetUniqueRecordedClipName(track, AnimationTrackRecorder.kRecordClipDefaultName));
return track.infiniteClip;
}
internal static TimelineClip CreateClip(this TrackAsset track, double time)
{
var attr = track.GetType().GetCustomAttributes(typeof(TrackClipTypeAttribute), true);
if (attr.Length == 0)
return null;
if (TimelineWindow.instance.state == null)
return null;
if (attr.Length == 1)
{
var clipClass = (TrackClipTypeAttribute)attr[0];
var clip = TimelineHelpers.CreateClipOnTrack(clipClass.inspectedType, track, time);
return clip;
}
return null;
}
static bool Overlaps(TimelineClip blendOut, TimelineClip blendIn)
{
if (blendIn == blendOut)
return false;
if (Math.Abs(blendIn.start - blendOut.start) < TimeUtility.kTimeEpsilon)
{
return blendIn.duration >= blendOut.duration;
}
return blendIn.start >= blendOut.start && blendIn.start < blendOut.end;
}
internal static void ComputeBlendsFromOverlaps(this TrackAsset asset)
{
ComputeBlendsFromOverlaps(asset.clips);
}
internal static void ComputeBlendsFromOverlaps(TimelineClip[] clips)
{
foreach (var clip in clips)
{
clip.blendInDuration = -1;
clip.blendOutDuration = -1;
}
Array.Sort(clips, (c1, c2) =>
Math.Abs(c1.start - c2.start) < TimeUtility.kTimeEpsilon ? c1.duration.CompareTo(c2.duration) : c1.start.CompareTo(c2.start));
for (var i = 0; i < clips.Length; i++)
{
var clip = clips[i];
if (!clip.SupportsBlending())
continue;
var blendIn = clip;
TimelineClip blendOut = null;
var blendOutCandidate = clips[Math.Max(i - 1, 0)];
if (Overlaps(blendOutCandidate, blendIn))
blendOut = blendOutCandidate;
if (blendOut != null)
{
UpdateClipIntersection(blendOut, blendIn);
}
}
}
static void UpdateClipIntersection(TimelineClip blendOutClip, TimelineClip blendInClip)
{
if (!blendOutClip.SupportsBlending() || !blendInClip.SupportsBlending())
return;
if (blendInClip.start - blendOutClip.start < blendOutClip.duration - blendInClip.duration)
return;
double duration = Math.Max(0, blendOutClip.start + blendOutClip.duration - blendInClip.start);
duration = duration <= kMinOverlapTime ? 0 : duration;
blendOutClip.blendOutDuration = duration;
blendInClip.blendInDuration = duration;
var blendInMode = blendInClip.blendInCurveMode;
var blendOutMode = blendOutClip.blendOutCurveMode;
if (blendInMode == TimelineClip.BlendCurveMode.Manual && blendOutMode == TimelineClip.BlendCurveMode.Auto)
{
blendOutClip.mixOutCurve = CurveEditUtility.CreateMatchingCurve(blendInClip.mixInCurve);
}
else if (blendInMode == TimelineClip.BlendCurveMode.Auto && blendOutMode == TimelineClip.BlendCurveMode.Manual)
{
blendInClip.mixInCurve = CurveEditUtility.CreateMatchingCurve(blendOutClip.mixOutCurve);
}
else if (blendInMode == TimelineClip.BlendCurveMode.Auto && blendOutMode == TimelineClip.BlendCurveMode.Auto)
{
blendInClip.mixInCurve = null; // resets to default curves
blendOutClip.mixOutCurve = null;
}
}
static void RecursiveSubtrackClone(TrackAsset source, TrackAsset duplicate, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, PlayableAsset assetOwner)
{
var subtracks = source.GetChildTracks();
foreach (var sub in subtracks)
{
var newSub = TimelineHelpers.Clone(duplicate, sub, sourceTable, destTable, assetOwner);
duplicate.AddChild(newSub);
RecursiveSubtrackClone(sub, newSub, sourceTable, destTable, assetOwner);
// Call the custom editor on Create
var customEditor = CustomTimelineEditorCache.GetTrackEditor(newSub);
customEditor.OnCreate_Safe(newSub, sub);
// registration has to happen AFTER recursion
TimelineCreateUtilities.SaveAssetIntoObject(newSub, assetOwner);
TimelineUndo.RegisterCreatedObjectUndo(newSub, L10n.Tr("Duplicate"));
}
}
internal static TrackAsset Duplicate(this TrackAsset track, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable,
TimelineAsset destinationTimeline = null)
{
if (track == null)
return null;
// if the destination is us, clear to avoid bad parenting (case 919421)
if (destinationTimeline == track.timelineAsset)
destinationTimeline = null;
var timelineParent = track.parent as TimelineAsset;
var trackParent = track.parent as TrackAsset;
if (timelineParent == null && trackParent == null)
{
Debug.LogWarning("Cannot duplicate track because it is not parented to known type");
return null;
}
// Determine who the final parent is. If we are pasting into another track, it's always the timeline.
// Otherwise it's the original parent
PlayableAsset finalParent = destinationTimeline != null ? destinationTimeline : track.parent;
// grab the list of tracks to generate a name from (923360) to get the list of names
// no need to do this part recursively
var finalTrackParent = finalParent as TrackAsset;
var finalTimelineAsset = finalParent as TimelineAsset;
var otherTracks = (finalTimelineAsset != null) ? finalTimelineAsset.trackObjects : finalTrackParent.subTracksObjects;
// Important to create the new objects before pushing the original undo, or redo breaks the
// sequence
var newTrack = TimelineHelpers.Clone(finalParent, track, sourceTable, destTable, finalParent);
newTrack.name = TimelineCreateUtilities.GenerateUniqueActorName(otherTracks, newTrack.name);
RecursiveSubtrackClone(track, newTrack, sourceTable, destTable, finalParent);
TimelineCreateUtilities.SaveAssetIntoObject(newTrack, finalParent);
TimelineUndo.RegisterCreatedObjectUndo(newTrack, L10n.Tr("Duplicate"));
UndoExtensions.RegisterPlayableAsset(finalParent, L10n.Tr("Duplicate"));
if (destinationTimeline != null) // other timeline
destinationTimeline.AddTrackInternal(newTrack);
else if (timelineParent != null) // this timeline, no parent
ReparentTracks(new List<TrackAsset> { newTrack }, timelineParent, timelineParent.GetRootTracks().Last(), false);
else // this timeline, with parent
trackParent.AddChild(newTrack);
// Call the custom editor. this check prevents the call when copying to the clipboard
if (destinationTimeline == null || destinationTimeline == TimelineEditor.inspectedAsset)
{
var customEditor = CustomTimelineEditorCache.GetTrackEditor(newTrack);
customEditor.OnCreate_Safe(newTrack, track);
}
return newTrack;
}
// Reparents a list of tracks to a new parent
// the new parent cannot be null (has to be track asset or sequence)
// the insertAfter can be null (will not reorder)
internal static bool ReparentTracks(List<TrackAsset> tracksToMove, PlayableAsset targetParent,
TrackAsset insertMarker = null, bool insertBefore = false)
{
var targetParentTrack = targetParent as TrackAsset;
var targetSequenceTrack = targetParent as TimelineAsset;
if (tracksToMove == null || tracksToMove.Count == 0 || (targetParentTrack == null && targetSequenceTrack == null))
return false;
// invalid parent type on a track
if (targetParentTrack != null && tracksToMove.Any(x => !TimelineCreateUtilities.ValidateParentTrack(targetParentTrack, x.GetType())))
return false;
// no valid tracks means this is simply a rearrangement
var validTracks = tracksToMove.Where(x => x.parent != targetParent).ToList();
if (insertMarker == null && !validTracks.Any())
return false;
var parents = validTracks.Select(x => x.parent).Where(x => x != null).Distinct().ToList();
// push the current state of the tracks that will change
foreach (var p in parents)
{
UndoExtensions.RegisterPlayableAsset(p, "Reparent");
}
UndoExtensions.RegisterTracks(validTracks, "Reparent");
UndoExtensions.RegisterPlayableAsset(targetParent, "Reparent");
// need to reparent tracks first, before moving them.
foreach (var t in validTracks)
{
if (t.parent != targetParent)
{
TrackAsset toMoveParent = t.parent as TrackAsset;
TimelineAsset toMoveTimeline = t.parent as TimelineAsset;
if (toMoveTimeline != null)
{
toMoveTimeline.RemoveTrack(t);
}
else if (toMoveParent != null)
{
toMoveParent.RemoveSubTrack(t);
}
if (targetParentTrack != null)
{
targetParentTrack.AddChild(t);
targetParentTrack.SetCollapsed(false);
}
else
{
targetSequenceTrack.AddTrackInternal(t);
}
}
}
if (insertMarker != null)
{
// re-ordering track. This is using internal APIs, so invalidation of the tracks must be done manually to avoid
// cache mismatches
var children = targetParentTrack != null ? targetParentTrack.subTracksObjects : targetSequenceTrack.trackObjects;
TimelineUtility.ReorderTracks(children, tracksToMove, insertMarker, insertBefore);
if (targetParentTrack != null)
targetParentTrack.Invalidate();
if (insertMarker.timelineAsset != null)
insertMarker.timelineAsset.Invalidate();
}
return true;
}
internal static IEnumerable<TrackAsset> FilterTracks(IEnumerable<TrackAsset> tracks)
{
var nTracks = tracks.Count();
// Duplicate is recursive. If should not have parent and child in the list
var hash = new HashSet<TrackAsset>(tracks);
var take = new Dictionary<TrackAsset, bool>(nTracks);
foreach (var track in tracks)
{
var parent = track.parent as TrackAsset;
var foundParent = false;
// go up the hierarchy
while (parent != null && !foundParent)
{
if (hash.Contains(parent))
{
foundParent = true;
}
parent = parent.parent as TrackAsset;
}
take[track] = !foundParent;
}
foreach (var track in tracks)
{
if (take[track])
yield return track;
}
}
internal static bool GetShowMarkers(this TrackAsset track)
{
return TimelineWindowViewPrefs.IsShowMarkers(track);
}
internal static void SetShowMarkers(this TrackAsset track, bool collapsed)
{
TimelineWindowViewPrefs.SetTrackShowMarkers(track, collapsed);
}
internal static bool GetShowInlineCurves(this TrackAsset track)
{
return TimelineWindowViewPrefs.GetShowInlineCurves(track);
}
internal static void SetShowInlineCurves(this TrackAsset track, bool inlineOn)
{
TimelineWindowViewPrefs.SetShowInlineCurves(track, inlineOn);
}
internal static bool ShouldShowInfiniteClipEditor(this AnimationTrack track)
{
return track != null && !track.inClipMode && track.infiniteClip != null;
}
// Special method to remove a track that is in a broken state. i.e. the script won't load
internal static bool RemoveBrokenTrack(PlayableAsset parent, ScriptableObject track)
{
var parentTrack = parent as TrackAsset;
var parentTimeline = parent as TimelineAsset;
if (parentTrack == null && parentTimeline == null)
throw new ArgumentException("parent is not a valid parent type", "parent");
// this object must be a Unity null, but not actually null;
object trackAsObject = track;
if (trackAsObject == null || track as TrackAsset != null) // yes, this is correct
throw new ArgumentException("track is not in a broken state");
// this belongs to a parent track
if (parentTrack != null)
{
int index = parentTrack.subTracksObjects.FindIndex(t => t.GetInstanceID() == track.GetInstanceID());
if (index >= 0)
{
UndoExtensions.RegisterTrack(parentTrack, L10n.Tr("Remove Track"));
parentTrack.subTracksObjects.RemoveAt(index);
parentTrack.Invalidate();
Undo.DestroyObjectImmediate(track);
return true;
}
}
else if (parentTimeline != null)
{
int index = parentTimeline.trackObjects.FindIndex(t => t.GetInstanceID() == track.GetInstanceID());
if (index >= 0)
{
UndoExtensions.RegisterPlayableAsset(parentTimeline, L10n.Tr("Remove Track"));
parentTimeline.trackObjects.RemoveAt(index);
parentTimeline.Invalidate();
Undo.DestroyObjectImmediate(track);
return true;
}
}
return false;
}
// Find the gap at the given time
// return true if there is a gap, false if there is an intersection
// endGap will be Infinity if the gap has no end
internal static bool GetGapAtTime(this TrackAsset track, double time, out double startGap, out double endGap)
{
startGap = 0;
endGap = Double.PositiveInfinity;
if (track == null || !track.GetClips().Any())
{
return false;
}
var discreteTime = new DiscreteTime(time);
track.SortClips();
var sortedByStartTime = track.clips;
for (int i = 0; i < sortedByStartTime.Length; i++)
{
var clip = sortedByStartTime[i];
// intersection
if (discreteTime >= new DiscreteTime(clip.start) && discreteTime < new DiscreteTime(clip.end))
{
endGap = time;
startGap = time;
return false;
}
if (clip.end < time)
{
startGap = clip.end;
}
if (clip.start > time)
{
endGap = clip.start;
break;
}
}
if (endGap - startGap < TimelineClip.kMinDuration)
{
startGap = time;
endGap = time;
return false;
}
return true;
}
internal static bool IsCompatibleWithClip(this TrackAsset track, TimelineClip clip)
{
if (track == null || clip == null || clip.asset == null)
return false;
return TypeUtility.GetPlayableAssetsHandledByTrack(track.GetType()).Contains(clip.asset.GetType());
}
// Get a flattened list of all child tracks
static void GetFlattenedChildTracks(this TrackAsset asset, List<TrackAsset> list)
{
if (asset == null || list == null)
return;
foreach (var track in asset.GetChildTracks())
{
list.Add(track);
GetFlattenedChildTracks(track, list);
}
}
internal static IEnumerable<TrackAsset> GetFlattenedChildTracks(this TrackAsset asset)
{
if (asset == null || !asset.GetChildTracks().Any())
return Enumerable.Empty<TrackAsset>();
var flattenedChildTracks = new List<TrackAsset>();
GetFlattenedChildTracks(asset, flattenedChildTracks);
return flattenedChildTracks;
}
internal static void UnarmForRecord(this TrackAsset track)
{
TimelineWindow.instance.state.UnarmForRecord(track);
}
internal static void SetShowTrackMarkers(this TrackAsset track, bool showMarkers)
{
var currentValue = track.GetShowMarkers();
if (currentValue != showMarkers)
{
TimelineUndo.PushUndo(TimelineWindow.instance.state.editSequence.viewModel, L10n.Tr("Toggle Show Markers"));
track.SetShowMarkers(showMarkers);
if (!showMarkers)
{
foreach (var marker in track.GetMarkers())
{
SelectionManager.Remove(marker);
}
}
}
}
internal static IEnumerable<TrackAsset> RemoveTimelineMarkerTrackFromList(this IEnumerable<TrackAsset> tracks, TimelineAsset asset)
{
return tracks.Where(t => t != asset.markerTrack);
}
internal static bool ContainsTimelineMarkerTrack(this IEnumerable<TrackAsset> tracks, TimelineAsset asset)
{
return tracks.Contains(asset.markerTrack);
}
}
}

View File

@@ -0,0 +1,145 @@
using System;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class ClipItem : IBlendable, ITrimmable
{
readonly TimelineClip m_Clip;
public TimelineClip clip
{
get { return m_Clip; }
}
public ClipItem(TimelineClip clip)
{
m_Clip = clip;
}
public TrackAsset parentTrack
{
get { return m_Clip.GetParentTrack(); }
set { m_Clip.SetParentTrack_Internal(value); }
}
public double start
{
get { return m_Clip.start; }
set { m_Clip.start = value; }
}
public double end
{
get { return m_Clip.end; }
}
public double duration
{
get { return m_Clip.duration; }
}
public bool IsCompatibleWithTrack(TrackAsset track)
{
return track.IsCompatibleWithClip(m_Clip);
}
public void PushUndo(string operation)
{
UndoExtensions.RegisterClip(m_Clip, operation);
}
public TimelineItemGUI gui
{
get { return ItemToItemGui.GetGuiForClip(m_Clip); }
}
public bool supportsBlending
{
get { return m_Clip.SupportsBlending(); }
}
public bool hasLeftBlend
{
get { return m_Clip.hasBlendIn; }
}
public bool hasRightBlend
{
get { return m_Clip.hasBlendOut; }
}
public double leftBlendDuration
{
get { return m_Clip.hasBlendIn ? m_Clip.blendInDuration : m_Clip.easeInDuration; }
}
public double rightBlendDuration
{
get { return m_Clip.hasBlendOut ? m_Clip.blendOutDuration : m_Clip.easeOutDuration; }
}
public void SetStart(double time, bool affectTimeScale)
{
ClipModifier.SetStart(m_Clip, time, affectTimeScale);
m_Clip.ConformEaseValues();
}
public void SetEnd(double time, bool affectTimeScale)
{
ClipModifier.SetEnd(m_Clip, time, affectTimeScale);
m_Clip.ConformEaseValues();
}
public void Delete()
{
EditorClipFactory.RemoveEditorClip(m_Clip);
ClipModifier.Delete(m_Clip.GetParentTrack().timelineAsset, m_Clip);
}
public void TrimStart(double time)
{
ClipModifier.TrimStart(m_Clip, time);
}
public void TrimEnd(double time)
{
ClipModifier.TrimEnd(m_Clip, time);
}
public ITimelineItem CloneTo(TrackAsset parent, double time)
{
return new ClipItem(TimelineHelpers.Clone(m_Clip, TimelineEditor.inspectedDirector, TimelineEditor.inspectedDirector, time, parent));
}
public override string ToString()
{
return m_Clip.ToString();
}
public bool Equals(ClipItem otherClip)
{
if (ReferenceEquals(null, otherClip)) return false;
if (ReferenceEquals(this, otherClip)) return true;
return Equals(m_Clip, otherClip.m_Clip);
}
public override int GetHashCode()
{
return (m_Clip != null ? m_Clip.GetHashCode() : 0);
}
public bool Equals(ITimelineItem other)
{
return Equals((object)other);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
var other = obj as ClipItem;
return other != null && Equals(other);
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
interface ITimelineItem : IEquatable<ITimelineItem>
{
double start { get; set; }
double end { get; }
double duration { get; }
TrackAsset parentTrack { get; set; }
bool IsCompatibleWithTrack(TrackAsset track);
void Delete();
ITimelineItem CloneTo(TrackAsset parent, double time);
void PushUndo(string operation);
TimelineItemGUI gui { get; }
}
interface ITrimmable : ITimelineItem
{
void SetStart(double time, bool affectTimeScale);
void SetEnd(double time, bool affectTimeScale);
void TrimStart(double time);
void TrimEnd(double time);
}
interface IBlendable : ITimelineItem
{
bool supportsBlending { get; }
bool hasLeftBlend { get; }
bool hasRightBlend { get; }
double leftBlendDuration { get; }
double rightBlendDuration { get; }
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UnityEditor.Timeline
{
class ItemsGroup
{
readonly ITimelineItem[] m_Items;
readonly ITimelineItem m_LeftMostItem;
readonly ITimelineItem m_RightMostItem;
public ITimelineItem[] items
{
get { return m_Items; }
}
public double start
{
get { return m_LeftMostItem.start; }
set
{
var offset = value - m_LeftMostItem.start;
foreach (var clip in m_Items)
clip.start += offset;
}
}
public double end
{
get { return m_RightMostItem.end; }
}
public ITimelineItem leftMostItem
{
get { return m_LeftMostItem; }
}
public ITimelineItem rightMostItem
{
get { return m_RightMostItem; }
}
public ItemsGroup(IEnumerable<ITimelineItem> items)
{
Debug.Assert(items != null && items.Any());
m_Items = items.ToArray();
m_LeftMostItem = null;
m_RightMostItem = null;
foreach (var item in m_Items)
{
if (m_LeftMostItem == null || item.start < m_LeftMostItem.start)
m_LeftMostItem = item;
if (m_RightMostItem == null || item.end > m_RightMostItem.end)
m_RightMostItem = item;
}
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class ItemsPerTrack
{
public virtual TrackAsset targetTrack { get; }
public IEnumerable<ITimelineItem> items
{
get { return m_ItemsGroup.items; }
}
public IEnumerable<TimelineClip> clips
{
get { return m_ItemsGroup.items.OfType<ClipItem>().Select(i => i.clip); }
}
public IEnumerable<IMarker> markers
{
get { return m_ItemsGroup.items.OfType<MarkerItem>().Select(i => i.marker); }
}
public ITimelineItem leftMostItem
{
get { return m_ItemsGroup.leftMostItem; }
}
public ITimelineItem rightMostItem
{
get { return m_ItemsGroup.rightMostItem; }
}
protected readonly ItemsGroup m_ItemsGroup;
public ItemsPerTrack(TrackAsset targetTrack, IEnumerable<ITimelineItem> items)
{
this.targetTrack = targetTrack;
m_ItemsGroup = new ItemsGroup(items);
}
}
}

View File

@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class ItemsUtils
{
static readonly Dictionary<TimelineClip, ClipItem> s_ClipCache = new Dictionary<TimelineClip, ClipItem>();
static readonly Dictionary<IMarker, MarkerItem> s_MarkerCache = new Dictionary<IMarker, MarkerItem>();
public static IEnumerable<ItemsPerTrack> ToItemsPerTrack(this IEnumerable<ITimelineItem> items)
{
var groupedItems = items.GroupBy(c => c.parentTrack);
foreach (var group in groupedItems)
{
yield return new ItemsPerTrack(group.Key, group.ToArray());
}
}
public static ITimelineItem ToItem(this TimelineClip clip)
{
if (s_ClipCache.ContainsKey(clip))
return s_ClipCache[clip];
var ret = new ClipItem(clip);
s_ClipCache.Add(clip, ret);
return ret;
}
public static ITimelineItem ToItem(this IMarker marker)
{
if (s_MarkerCache.ContainsKey(marker))
return s_MarkerCache[marker];
var ret = new MarkerItem(marker);
s_MarkerCache.Add(marker, ret);
return ret;
}
public static IEnumerable<ITimelineItem> ToItems(this IEnumerable<TimelineClip> clips)
{
return clips.Select(ToItem);
}
public static IEnumerable<ITimelineItem> ToItems(this IEnumerable<IMarker> markers)
{
return markers.Select(ToItem);
}
public static IEnumerable<ITimelineItem> GetItems(this TrackAsset track)
{
var list = track.clips.Select(clip => (ITimelineItem) new ClipItem(clip)).ToList();
list.AddRange(track.GetMarkers().Select(marker => (ITimelineItem) new MarkerItem(marker)));
list = list.OrderBy(x => x.start).ThenBy(x => x.end).ToList();
return list;
}
public static void GetItemRange(this TrackAsset track, out double start, out double end)
{
start = 0;
end = 0;
var items = track.GetItems().ToList();
if (items.Any())
{
start = items.Min(p => p.start);
end = items.Max(p => p.end);
}
}
public static IEnumerable<ITimelineItem> GetItemsExcept(this TrackAsset track, IEnumerable<ITimelineItem> items)
{
return GetItems(track).Except(items);
}
public static IEnumerable<Type> GetItemTypes(IEnumerable<ITimelineItem> items)
{
var types = new List<Type>();
if (items.OfType<ClipItem>().Any())
types.Add(typeof(ClipItem));
if (items.OfType<MarkerItem>().Any())
types.Add(typeof(MarkerItem));
return types;
}
public static IEnumerable<Type> GetItemTypes(IEnumerable<ItemsPerTrack> itemsGroups)
{
return GetItemTypes(itemsGroups.SelectMany(i => i.items)).Distinct();
}
public static void SetItemsStartTime(IEnumerable<ItemsPerTrack> newItems, double time)
{
var startTimes = newItems.Select(d => d.items.Min(x => x.start)).ToList();
var min = startTimes.Min();
startTimes = startTimes.Select(x => x - min + time).ToList();
for (int i = 0; i < newItems.Count(); ++i)
EditModeUtils.SetStart(newItems.ElementAt(i).items, startTimes[i]);
}
public static double TimeGapBetweenItems(ITimelineItem leftItem, ITimelineItem rightItem)
{
if (leftItem is MarkerItem && rightItem is MarkerItem)
{
var markerType = ((MarkerItem)leftItem).marker.GetType();
var gap = TimeReferenceUtility.PixelToTime(StyleManager.UssStyleForType(markerType).fixedWidth) - TimeReferenceUtility.PixelToTime(0);
return gap;
}
return 0.0;
}
}
}

View File

@@ -0,0 +1,98 @@
using System;
using UnityEngine.Timeline;
using Object = UnityEngine.Object;
namespace UnityEditor.Timeline
{
class MarkerItem : ITimelineItem
{
readonly IMarker m_Marker;
public IMarker marker
{
get { return m_Marker; }
}
public MarkerItem(IMarker marker)
{
m_Marker = marker;
}
public TrackAsset parentTrack
{
get { return m_Marker.parent; }
set {}
}
public double start
{
get { return m_Marker.time; }
set { m_Marker.time = value; }
}
public double end
{
get { return m_Marker.time; }
}
public double duration
{
get { return 0.0; }
}
public bool IsCompatibleWithTrack(TrackAsset track)
{
return TypeUtility.DoesTrackSupportMarkerType(track, m_Marker.GetType());
}
public void PushUndo(string operation)
{
UndoExtensions.RegisterMarker(m_Marker, operation);
}
public TimelineItemGUI gui
{
get { return ItemToItemGui.GetGuiForMarker(m_Marker); }
}
public void Delete()
{
MarkerModifier.DeleteMarker(m_Marker);
}
public ITimelineItem CloneTo(TrackAsset parent, double time)
{
var item = new MarkerItem(MarkerModifier.CloneMarkerToParent(m_Marker, parent));
item.start = time;
return item;
}
protected bool Equals(MarkerItem otherMarker)
{
return Equals(m_Marker, otherMarker.m_Marker);
}
public override int GetHashCode()
{
return (m_Marker != null ? m_Marker.GetHashCode() : 0);
}
public override string ToString()
{
return m_Marker.ToString();
}
public bool Equals(ITimelineItem other)
{
return Equals((object)other);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
var other = obj as MarkerItem;
return other != null && Equals(other);
}
}
}

View File

@@ -0,0 +1,103 @@
#if UNITY_2020_2_OR_NEWER
[assembly: UnityEditor.Localization]
#else
using UnityEngine;
using UnityEditor;
namespace UnityEditor.Timeline
{
// dummy functions
internal static class L10n
{
public static string Tr(string str)
{
return str;
}
public static string[] Tr(string[] str_list)
{
return str_list;
}
public static string Tr(string str, string groupName)
{
return str;
}
public static string TrPath(string path)
{
return path;
}
public static GUIContent TextContent(string text, string tooltip = null, Texture icon = null)
{
return EditorGUIUtility.TrTextContent(text, tooltip, icon);
}
public static GUIContent TextContent(string text, string tooltip, string iconName)
{
return EditorGUIUtility.TrTextContent(text, tooltip, iconName);
}
public static GUIContent TextContent(string text, Texture icon)
{
return EditorGUIUtility.TrTextContent(text, icon);
}
public static GUIContent TextContentWithIcon(string text, Texture icon)
{
return EditorGUIUtility.TrTextContentWithIcon(text, icon);
}
public static GUIContent TextContentWithIcon(string text, string iconName)
{
return EditorGUIUtility.TrTextContentWithIcon(text, iconName);
}
public static GUIContent TextContentWithIcon(string text, string tooltip, string iconName)
{
return EditorGUIUtility.TrTextContentWithIcon(text, tooltip, iconName);
}
public static GUIContent TextContentWithIcon(string text, string tooltip, Texture icon)
{
return EditorGUIUtility.TrTextContentWithIcon(text, tooltip, icon);
}
public static GUIContent TextContentWithIcon(string text, string tooltip, MessageType messageType)
{
return EditorGUIUtility.TrTextContentWithIcon(text, tooltip, messageType);
}
public static GUIContent TextContentWithIcon(string text, MessageType messageType)
{
return EditorGUIUtility.TrTextContentWithIcon(text, messageType);
}
public static GUIContent IconContent(string iconName, string tooltip = null)
{
return EditorGUIUtility.TrIconContent(iconName, tooltip);
}
public static GUIContent IconContent(Texture icon, string tooltip = null)
{
return EditorGUIUtility.TrIconContent(icon, tooltip);
}
public static GUIContent TempContent(string t)
{
return EditorGUIUtility.TrTempContent(t);
}
public static GUIContent[] TempContent(string[] texts)
{
return EditorGUIUtility.TrTempContent(texts);
}
public static GUIContent[] TempContent(string[] texts, string[] tooltips)
{
return EditorGUIUtility.TrTempContent(texts, tooltips);
}
}
}
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace UnityEditor.Timeline
{
class AddDeleteItemModeMix : IAddDeleteItemMode
{
public void InsertItemsAtTime(IEnumerable<ItemsPerTrack> itemsGroups, double requestedTime)
{
ItemsUtils.SetItemsStartTime(itemsGroups, requestedTime);
EditModeMixUtils.PrepareItemsForInsertion(itemsGroups);
if (!EditModeMixUtils.CanInsert(itemsGroups))
{
var validTime = itemsGroups.Select(c => c.targetTrack).Max(parent => parent.end);
ItemsUtils.SetItemsStartTime(itemsGroups, validTime);
}
}
public void RemoveItems(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace UnityEditor.Timeline
{
class AddDeleteItemModeReplace : IAddDeleteItemMode
{
public void InsertItemsAtTime(IEnumerable<ItemsPerTrack> itemsGroups, double requestedTime)
{
ItemsUtils.SetItemsStartTime(itemsGroups, requestedTime);
EditModeReplaceUtils.Insert(itemsGroups);
}
public void RemoveItems(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace UnityEditor.Timeline
{
class AddDeleteItemModeRipple : IAddDeleteItemMode
{
public void InsertItemsAtTime(IEnumerable<ItemsPerTrack> itemsGroups, double requestedTime)
{
ItemsUtils.SetItemsStartTime(itemsGroups, requestedTime);
EditModeRippleUtils.Insert(itemsGroups);
}
public void RemoveItems(IEnumerable<ItemsPerTrack> itemsGroups)
{
EditModeRippleUtils.Remove(itemsGroups);
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace UnityEditor.Timeline
{
interface IAddDeleteItemMode
{
void InsertItemsAtTime(IEnumerable<ItemsPerTrack> itemsGroups, double requestedTime);
void RemoveItems(IEnumerable<ItemsPerTrack> itemsGroups);
}
}

View File

@@ -0,0 +1,95 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.Timeline
{
class TimelineCursors
{
public enum CursorType
{
MixBoth,
MixLeft,
MixRight,
Replace,
Ripple,
Pan
}
class CursorInfo
{
public readonly string assetPath;
public readonly Vector2 hotSpot;
public readonly MouseCursor mouseCursorType;
public CursorInfo(string assetPath, Vector2 hotSpot, MouseCursor mouseCursorType)
{
this.assetPath = assetPath;
this.hotSpot = hotSpot;
this.mouseCursorType = mouseCursorType;
}
}
const string k_CursorAssetRoot = "Cursors/";
const string k_CursorAssetsNamespace = "Timeline.";
const string k_CursorAssetExtension = ".png";
const string k_MixBothCursorAssetName = k_CursorAssetsNamespace + "MixBoth" + k_CursorAssetExtension;
const string k_MixLeftCursorAssetName = k_CursorAssetsNamespace + "MixLeft" + k_CursorAssetExtension;
const string k_MixRightCursorAssetName = k_CursorAssetsNamespace + "MixRight" + k_CursorAssetExtension;
const string k_ReplaceCursorAssetName = k_CursorAssetsNamespace + "Replace" + k_CursorAssetExtension;
const string k_RippleCursorAssetName = k_CursorAssetsNamespace + "Ripple" + k_CursorAssetExtension;
static readonly string s_PlatformPath = (Application.platform == RuntimePlatform.WindowsEditor) ? "Windows/" : "macOS/";
static readonly string s_CursorAssetDirectory = k_CursorAssetRoot + s_PlatformPath;
static readonly Dictionary<CursorType, CursorInfo> s_CursorInfoLookup = new Dictionary<CursorType, CursorInfo>
{
{CursorType.MixBoth, new CursorInfo(s_CursorAssetDirectory + k_MixBothCursorAssetName, new Vector2(16, 18), MouseCursor.CustomCursor)},
{CursorType.MixLeft, new CursorInfo(s_CursorAssetDirectory + k_MixLeftCursorAssetName, new Vector2(7, 18), MouseCursor.CustomCursor)},
{CursorType.MixRight, new CursorInfo(s_CursorAssetDirectory + k_MixRightCursorAssetName, new Vector2(25, 18), MouseCursor.CustomCursor)},
{CursorType.Replace, new CursorInfo(s_CursorAssetDirectory + k_ReplaceCursorAssetName, new Vector2(16, 28), MouseCursor.CustomCursor)},
{CursorType.Ripple, new CursorInfo(s_CursorAssetDirectory + k_RippleCursorAssetName, new Vector2(26, 19), MouseCursor.CustomCursor)},
{CursorType.Pan, new CursorInfo(null, Vector2.zero, MouseCursor.Pan)}
};
static readonly Dictionary<string, Texture2D> s_CursorAssetCache = new Dictionary<string, Texture2D>();
static CursorType? s_CurrentCursor;
public static void SetCursor(CursorType cursorType)
{
if (s_CurrentCursor.HasValue && s_CurrentCursor.Value == cursorType) return;
s_CurrentCursor = cursorType;
var cursorInfo = s_CursorInfoLookup[cursorType];
Texture2D cursorAsset = null;
if (cursorInfo.mouseCursorType == MouseCursor.CustomCursor)
{
cursorAsset = LoadCursorAsset(cursorInfo.assetPath);
}
EditorGUIUtility.SetCurrentViewCursor(cursorAsset, cursorInfo.hotSpot, cursorInfo.mouseCursorType);
}
public static void ClearCursor()
{
if (!s_CurrentCursor.HasValue) return;
EditorGUIUtility.ClearCurrentViewCursor();
s_CurrentCursor = null;
}
static Texture2D LoadCursorAsset(string assetPath)
{
if (!s_CursorAssetCache.ContainsKey(assetPath))
{
s_CursorAssetCache.Add(assetPath, (Texture2D)EditorGUIUtility.Load(assetPath));
}
return s_CursorAssetCache[assetPath];
}
}
}

View File

@@ -0,0 +1,344 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class EditMode
{
public enum EditType
{
None = -1,
Mix = 0,
Ripple = 1,
Replace = 2
}
interface ISubEditMode
{
IMoveItemMode moveItemMode { get; }
IMoveItemDrawer moveItemDrawer { get; }
ITrimItemMode trimItemMode { get; }
ITrimItemDrawer trimItemDrawer { get; }
IAddDeleteItemMode addDeleteItemMode { get; }
Color color { get; }
KeyCode clutchKey { get; }
void Reset();
}
class SubEditMode<TMoveMode, TTrimMode, TAddDeleteMode>: ISubEditMode
where TMoveMode : class, IMoveItemMode, IMoveItemDrawer, new()
where TTrimMode : class, ITrimItemMode, ITrimItemDrawer, new()
where TAddDeleteMode : class, IAddDeleteItemMode, new()
{
public SubEditMode(Color guiColor, KeyCode key)
{
color = guiColor;
clutchKey = key;
Reset();
}
public void Reset()
{
m_MoveItemMode = new TMoveMode();
m_TrimItemMode = new TTrimMode();
m_AddDeleteItemMode = new TAddDeleteMode();
}
TMoveMode m_MoveItemMode;
TTrimMode m_TrimItemMode;
TAddDeleteMode m_AddDeleteItemMode;
public IMoveItemMode moveItemMode { get { return m_MoveItemMode; } }
public IMoveItemDrawer moveItemDrawer { get { return m_MoveItemMode; } }
public ITrimItemMode trimItemMode { get { return m_TrimItemMode; } }
public ITrimItemDrawer trimItemDrawer { get { return m_TrimItemMode; } }
public IAddDeleteItemMode addDeleteItemMode { get { return m_AddDeleteItemMode; } }
public Color color { get; }
public KeyCode clutchKey { get; }
}
static readonly Dictionary<EditType, ISubEditMode> k_EditModes = new Dictionary<EditType, ISubEditMode>
{
{ EditType.Mix, new SubEditMode<MoveItemModeMix, TrimItemModeMix, AddDeleteItemModeMix>(DirectorStyles.kMixToolColor, KeyCode.Alpha1) },
{ EditType.Ripple, new SubEditMode<MoveItemModeRipple, TrimItemModeRipple, AddDeleteItemModeRipple>(DirectorStyles.kRippleToolColor, KeyCode.Alpha2) },
{ EditType.Replace, new SubEditMode<MoveItemModeReplace, TrimItemModeReplace, AddDeleteItemModeReplace>(DirectorStyles.kReplaceToolColor, KeyCode.Alpha3) }
};
static EditType s_CurrentEditType = EditType.Mix;
static EditType s_OverrideEditType = EditType.None;
static ITrimmable s_CurrentTrimItem;
static TrimEdge s_CurrentTrimDirection;
static MoveItemHandler s_CurrentMoveItemHandler;
static EditModeInputHandler s_InputHandler = new EditModeInputHandler();
static ITrimItemMode trimMode
{
get { return GetSubEditMode(editType).trimItemMode; }
}
static ITrimItemDrawer trimDrawer
{
get { return GetSubEditMode(editType).trimItemDrawer; }
}
static IMoveItemMode moveMode
{
get { return GetSubEditMode(editType).moveItemMode; }
}
static IMoveItemDrawer moveDrawer
{
get { return GetSubEditMode(editType).moveItemDrawer; }
}
static IAddDeleteItemMode addDeleteMode
{
get { return GetSubEditMode(editType).addDeleteItemMode; }
}
public static EditModeInputHandler inputHandler
{
get { return s_InputHandler; }
}
static Color modeColor
{
get { return GetSubEditMode(editType).color; }
}
public static EditType editType
{
get
{
if (s_OverrideEditType != EditType.None)
return s_OverrideEditType;
var window = TimelineWindow.instance;
if (window != null)
s_CurrentEditType = window.state.editType;
return s_CurrentEditType;
}
set
{
s_CurrentEditType = value;
var window = TimelineWindow.instance;
if (window != null)
window.state.editType = value;
s_OverrideEditType = EditType.None;
}
}
static ISubEditMode GetSubEditMode(EditType type)
{
var subEditMode = k_EditModes[type];
if (subEditMode != null)
return subEditMode;
Debug.LogError("Unsupported editmode type");
return null;
}
static EditType GetSubEditType(KeyCode key)
{
foreach (var subEditMode in k_EditModes)
{
if (subEditMode.Value.clutchKey == key)
return subEditMode.Key;
}
return EditType.None;
}
public static void ClearEditMode()
{
k_EditModes[editType].Reset();
}
public static void BeginTrim(ITimelineItem item, TrimEdge trimDirection)
{
var itemToTrim = item as ITrimmable;
if (itemToTrim == null) return;
s_CurrentTrimItem = itemToTrim;
s_CurrentTrimDirection = trimDirection;
trimMode.OnBeforeTrim(itemToTrim, trimDirection);
UndoExtensions.RegisterTrack(itemToTrim.parentTrack, L10n.Tr("Trim Clip"));
}
public static void TrimStart(ITimelineItem item, double time, bool affectTimeScale)
{
var itemToTrim = item as ITrimmable;
if (itemToTrim == null) return;
trimMode.TrimStart(itemToTrim, time, affectTimeScale);
}
public static void TrimEnd(ITimelineItem item, double time, bool affectTimeScale)
{
var itemToTrim = item as ITrimmable;
if (itemToTrim == null) return;
trimMode.TrimEnd(itemToTrim, time, affectTimeScale);
}
public static void DrawTrimGUI(WindowState state, TimelineItemGUI item, TrimEdge edge)
{
trimDrawer.DrawGUI(state, item.rect, modeColor, edge);
}
public static void FinishTrim()
{
s_CurrentTrimItem = null;
TimelineCursors.ClearCursor();
ClearEditMode();
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
public static void BeginMove(MoveItemHandler moveItemHandler)
{
s_CurrentMoveItemHandler = moveItemHandler;
moveMode.BeginMove(s_CurrentMoveItemHandler.movingItems);
}
public static void UpdateMove()
{
moveMode.UpdateMove(s_CurrentMoveItemHandler.movingItems);
}
public static void OnTrackDetach(IEnumerable<ItemsPerTrack> grabbedTrackItems)
{
moveMode.OnTrackDetach(grabbedTrackItems);
}
public static void HandleTrackSwitch(IEnumerable<ItemsPerTrack> grabbedTrackItems)
{
moveMode.HandleTrackSwitch(grabbedTrackItems);
}
public static bool AllowTrackSwitch()
{
return moveMode.AllowTrackSwitch();
}
public static double AdjustStartTime(WindowState state, ItemsPerTrack itemsGroup, double time)
{
return moveMode.AdjustStartTime(state, itemsGroup, time);
}
public static bool ValidateDrag(ItemsPerTrack itemsGroup)
{
return moveMode.ValidateMove(itemsGroup);
}
public static void DrawMoveGUI(WindowState state, IEnumerable<MovingItems> movingItems)
{
moveDrawer.DrawGUI(state, movingItems, modeColor);
}
public static void FinishMove()
{
var manipulatedItemsList = s_CurrentMoveItemHandler.movingItems;
moveMode.FinishMove(manipulatedItemsList);
foreach (var itemsGroup in manipulatedItemsList)
foreach (var item in itemsGroup.items)
item.parentTrack = itemsGroup.targetTrack;
s_CurrentMoveItemHandler = null;
TimelineCursors.ClearCursor();
ClearEditMode();
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
public static void FinalizeInsertItemsAtTime(IEnumerable<ItemsPerTrack> newItems, double requestedTime)
{
addDeleteMode.InsertItemsAtTime(newItems, requestedTime);
}
public static void PrepareItemsDelete(IEnumerable<ItemsPerTrack> newItems)
{
addDeleteMode.RemoveItems(newItems);
}
public static void HandleModeClutch()
{
if (Event.current.type == EventType.KeyDown && EditorGUI.IsEditingTextField())
return;
var prevType = editType;
if (Event.current.type == EventType.KeyDown)
{
var clutchEditType = GetSubEditType(Event.current.keyCode);
if (clutchEditType != EditType.None)
{
s_OverrideEditType = clutchEditType;
Event.current.Use();
}
}
else if (Event.current.type == EventType.KeyUp)
{
var clutchEditType = GetSubEditType(Event.current.keyCode);
if (clutchEditType == s_OverrideEditType)
{
s_OverrideEditType = EditType.None;
Event.current.Use();
}
}
if (prevType != editType)
{
if (s_CurrentTrimItem != null)
{
trimMode.OnBeforeTrim(s_CurrentTrimItem, s_CurrentTrimDirection);
}
else if (s_CurrentMoveItemHandler != null)
{
if (s_CurrentMoveItemHandler.movingItems == null)
{
s_CurrentMoveItemHandler = null;
return;
}
foreach (var movingItems in s_CurrentMoveItemHandler.movingItems)
{
if (movingItems != null && movingItems.HasAnyDetachedParents())
{
foreach (var items in movingItems.items)
{
items.parentTrack = movingItems.originalTrack;
}
}
}
var movingSelection = s_CurrentMoveItemHandler.movingItems;
// Handle clutch key transition if needed
GetSubEditMode(prevType).moveItemMode.OnModeClutchExit(movingSelection);
moveMode.OnModeClutchEnter(movingSelection);
moveMode.BeginMove(movingSelection);
moveMode.HandleTrackSwitch(movingSelection);
UpdateMove();
s_CurrentMoveItemHandler.RefreshPreviewItems();
TimelineWindow.instance.state.rebuildGraph = true; // TODO Rebuild only if parent changed
}
TimelineWindow.instance.Repaint(); // TODO Refresh the toolbar without doing a full repaint?
}
}
}
}

View File

@@ -0,0 +1,199 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UnityEditor.Timeline
{
class EditModeInputHandler
{
readonly MoveInputHandler m_MoveHandler;
readonly TrimInputHandler m_TrimHandler;
public EditModeInputHandler()
{
m_MoveHandler = new MoveInputHandler();
m_TrimHandler = new TrimInputHandler();
}
public void ProcessMove(InputEvent action, double value)
{
if (TimelineWindow.instance != null && TimelineWindow.instance.state != null)
ProcessInputAction(m_MoveHandler, action, value, TimelineWindow.instance.state);
}
public void ProcessTrim(InputEvent action, double value, bool stretch)
{
if (TimelineWindow.instance != null && TimelineWindow.instance.state != null)
{
m_TrimHandler.stretch = stretch;
ProcessInputAction(m_TrimHandler, action, value, TimelineWindow.instance.state);
}
}
public void SetValueForEdge(IEnumerable<ITimelineItem> items, AttractedEdge edge, double value)
{
if (TimelineWindow.instance != null && TimelineWindow.instance.state != null)
MoveInputHandler.SetValueForEdge(items, edge, value, TimelineWindow.instance.state);
}
public void OnGUI(WindowState state, Event evt)
{
if (TimelineWindow.instance != null && TimelineWindow.instance.state != null)
{
m_MoveHandler.OnGUI(evt);
m_TrimHandler.OnGUI(state);
}
}
static void ProcessInputAction(IInputHandler handler, InputEvent action, double value, WindowState state)
{
var items = SelectionManager.SelectedItems();
switch (action)
{
case InputEvent.None:
return;
case InputEvent.DragEnter:
handler.OnEnterDrag(items, state);
break;
case InputEvent.Drag:
handler.OnDrag(value, state);
break;
case InputEvent.DragExit:
handler.OnExitDrag();
break;
case InputEvent.KeyboardInput:
handler.OnSetValue(items, value, state);
break;
default:
return;
}
}
interface IInputHandler
{
void OnEnterDrag(IEnumerable<ITimelineItem> items, WindowState state);
void OnDrag(double value, WindowState state);
void OnExitDrag();
void OnSetValue(IEnumerable<ITimelineItem> items, double value, WindowState state);
}
class TrimInputHandler : IInputHandler
{
bool isDragging { get; set; }
public bool stretch { get; set; }
IEnumerable<ITimelineItem> grabbedItems { get; set; }
public void OnEnterDrag(IEnumerable<ITimelineItem> items, WindowState state)
{
grabbedItems = items.OfType<ITrimmable>().ToArray();
foreach (var item in grabbedItems)
{
EditMode.BeginTrim(item, TrimEdge.End);
}
isDragging = true;
}
public void OnDrag(double endValue, WindowState state)
{
var trimValue = endValue;
trimValue = TimeReferenceUtility.SnapToFrameIfRequired(trimValue);
foreach (var item in grabbedItems)
{
EditMode.TrimEnd(item, trimValue, stretch);
}
state.UpdateRootPlayableDuration(state.editSequence.duration);
}
public void OnExitDrag()
{
isDragging = false;
EditMode.FinishTrim();
TimelineWindow.instance.Repaint();
}
public void OnSetValue(IEnumerable<ITimelineItem> items, double endValue, WindowState state)
{
foreach (var item in items.OfType<ITrimmable>())
{
EditMode.BeginTrim(item, TrimEdge.End);
EditMode.TrimEnd(item, endValue, stretch);
EditMode.FinishTrim();
}
state.UpdateRootPlayableDuration(state.editSequence.duration);
}
public void OnGUI(WindowState state)
{
if (!isDragging) return;
foreach (var item in grabbedItems)
{
EditMode.DrawTrimGUI(state, item.gui, TrimEdge.End);
}
}
}
class MoveInputHandler : IInputHandler
{
MoveItemHandler m_MoveItemHandler;
public void OnEnterDrag(IEnumerable<ITimelineItem> items, WindowState state)
{
if (items.Any())
{
m_MoveItemHandler = new MoveItemHandler(state);
m_MoveItemHandler.Grab(items, items.First().parentTrack);
}
}
public void OnDrag(double value, WindowState state)
{
if (m_MoveItemHandler == null) return;
var startValue = value;
startValue = TimeReferenceUtility.SnapToFrameIfRequired(startValue);
m_MoveItemHandler.OnAttractedEdge(null, ManipulateEdges.Both, AttractedEdge.None, startValue);
}
public void OnExitDrag()
{
if (m_MoveItemHandler == null) return;
m_MoveItemHandler.Drop();
m_MoveItemHandler = null;
GUIUtility.ExitGUI();
}
public void OnSetValue(IEnumerable<ITimelineItem> items, double value, WindowState state)
{
if (!items.Any()) return;
m_MoveItemHandler = new MoveItemHandler(state);
m_MoveItemHandler.Grab(items, items.First().parentTrack);
m_MoveItemHandler.OnAttractedEdge(null, ManipulateEdges.Both, AttractedEdge.None, value);
m_MoveItemHandler.Drop();
m_MoveItemHandler = null;
}
public void OnGUI(Event evt)
{
if (m_MoveItemHandler != null)
m_MoveItemHandler.OnGUI(evt);
}
public static void SetValueForEdge(IEnumerable<ITimelineItem> items, AttractedEdge edge, double value, WindowState state)
{
var handler = new MoveItemHandler(state);
foreach (var item in items)
{
handler.Grab(new[] {item}, item.parentTrack);
handler.OnAttractedEdge(null, ManipulateEdges.Both, edge, value);
handler.Drop();
}
}
}
}
}

View File

@@ -0,0 +1,49 @@
using System;
using UnityEngine;
namespace UnityEditor.Timeline
{
class HeaderSplitterManipulator : Manipulator
{
bool m_Captured;
protected override bool MouseDown(Event evt, WindowState state)
{
Rect headerSplitterRect = state.GetWindow().headerSplitterRect;
if (headerSplitterRect.Contains(evt.mousePosition))
{
m_Captured = true;
state.AddCaptured(this);
return true;
}
return false;
}
protected override bool MouseDrag(Event evt, WindowState state)
{
if (!m_Captured)
return false;
state.sequencerHeaderWidth = evt.mousePosition.x;
return true;
}
protected override bool MouseUp(Event evt, WindowState state)
{
if (!m_Captured)
return false;
state.RemoveCaptured(this);
m_Captured = false;
return true;
}
public override void Overlay(Event evt, WindowState state)
{
Rect rect = state.GetWindow().sequenceRect;
EditorGUIUtility.AddCursorRect(rect, MouseCursor.SplitResizeLeftRight);
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.Timeline
{
interface IMoveItemMode
{
void OnTrackDetach(IEnumerable<ItemsPerTrack> itemsGroups);
void HandleTrackSwitch(IEnumerable<ItemsPerTrack> itemsGroups);
bool AllowTrackSwitch();
double AdjustStartTime(WindowState state, ItemsPerTrack itemsGroup, double time);
void OnModeClutchEnter(IEnumerable<ItemsPerTrack> itemsGroups);
void OnModeClutchExit(IEnumerable<ItemsPerTrack> itemsGroups);
void BeginMove(IEnumerable<ItemsPerTrack> itemsGroups);
void UpdateMove(IEnumerable<ItemsPerTrack> itemsGroups);
void FinishMove(IEnumerable<ItemsPerTrack> itemsGroups);
bool ValidateMove(ItemsPerTrack itemsGroup);
}
interface IMoveItemDrawer
{
void DrawGUI(WindowState state, IEnumerable<MovingItems> movingItems, Color color);
}
}

View File

@@ -0,0 +1,311 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class MoveItemHandler : IAttractable, IAttractionHandler
{
bool m_Grabbing;
MovingItems m_LeftMostMovingItems;
MovingItems m_RightMostMovingItems;
HashSet<TimelineItemGUI> m_ItemGUIs;
ItemsGroup m_ItemsGroup;
public TrackAsset targetTrack { get; private set; }
public bool allowTrackSwitch { get; private set; }
int m_GrabbedModalUndoGroup = -1;
readonly WindowState m_State;
public MovingItems[] movingItems { get; private set; }
public MoveItemHandler(WindowState state)
{
m_State = state;
}
public void Grab(IEnumerable<ITimelineItem> items, TrackAsset referenceTrack)
{
Grab(items, referenceTrack, Vector2.zero);
}
public void Grab(IEnumerable<ITimelineItem> items, TrackAsset referenceTrack, Vector2 mousePosition)
{
if (items == null) return;
items = items.ToArray(); // Cache enumeration result
if (!items.Any()) return;
m_GrabbedModalUndoGroup = Undo.GetCurrentGroup();
var trackItems = items.GroupBy(c => c.parentTrack).ToArray();
var trackItemsCount = trackItems.Length;
var tracks = items.Select(c => c.parentTrack).Where(x => x != null).Distinct();
movingItems = new MovingItems[trackItemsCount];
allowTrackSwitch = trackItemsCount == 1 && !trackItems.SelectMany(x => x).Any(x => x is MarkerItem); // For now, track switch is only supported when all items are on the same track and there are no items
// one push per track handles all the clips on the track
UndoExtensions.RegisterTracks(tracks, L10n.Tr("Move Items"));
foreach (var sourceTrack in tracks)
{
// push all markers on the track because of ripple
UndoExtensions.RegisterMarkers(sourceTrack.GetMarkers(), L10n.Tr("Move Items"));
}
for (var i = 0; i < trackItemsCount; ++i)
{
var track = trackItems[i].Key;
var grabbedItems = new MovingItems(m_State, track, trackItems[i].ToArray(), referenceTrack, mousePosition, allowTrackSwitch);
movingItems[i] = grabbedItems;
}
m_LeftMostMovingItems = null;
m_RightMostMovingItems = null;
foreach (var grabbedTrackItems in movingItems)
{
if (m_LeftMostMovingItems == null || m_LeftMostMovingItems.start > grabbedTrackItems.start)
m_LeftMostMovingItems = grabbedTrackItems;
if (m_RightMostMovingItems == null || m_RightMostMovingItems.end < grabbedTrackItems.end)
m_RightMostMovingItems = grabbedTrackItems;
}
m_ItemGUIs = new HashSet<TimelineItemGUI>();
m_ItemsGroup = new ItemsGroup(items);
foreach (var item in items)
m_ItemGUIs.Add(item.gui);
targetTrack = referenceTrack;
EditMode.BeginMove(this);
m_Grabbing = true;
}
public void Drop()
{
if (IsValidDrop())
{
foreach (var grabbedItems in movingItems)
{
var track = grabbedItems.targetTrack;
UndoExtensions.RegisterTrack(track, L10n.Tr("Move Items"));
if (EditModeUtils.IsInfiniteTrack(track) && grabbedItems.clips.Any())
((AnimationTrack)track).ConvertToClipMode();
}
EditMode.FinishMove();
Done();
}
else
{
Cancel();
}
EditMode.ClearEditMode();
}
bool IsValidDrop()
{
return movingItems.All(g => g.canDrop);
}
void Cancel()
{
if (!m_Grabbing)
return;
// TODO fix undo reselection persistency
// identify the clips by their playable asset, since that reference will survive the undo
// This is a workaround, until a more persistent fix for selection of clips across Undo can be found
var assets = movingItems.SelectMany(x => x.clips).Select(x => x.asset);
Undo.RevertAllDownToGroup(m_GrabbedModalUndoGroup);
// reselect the clips from the original clip
var clipsToSelect = movingItems.Select(x => x.originalTrack).SelectMany(x => x.GetClips()).Where(x => assets.Contains(x.asset)).ToArray();
SelectionManager.RemoveTimelineSelection();
foreach (var c in clipsToSelect)
SelectionManager.Add(c);
Done();
}
void Done()
{
foreach (var movingItem in movingItems)
{
foreach (var item in movingItem.items)
{
if (item.gui != null)
item.gui.isInvalid = false;
}
}
movingItems = null;
m_LeftMostMovingItems = null;
m_RightMostMovingItems = null;
m_Grabbing = false;
m_State.Refresh();
}
public double start { get { return m_ItemsGroup.start; } }
public double end { get { return m_ItemsGroup.end; } }
public bool ShouldSnapTo(ISnappable snappable)
{
var itemGUI = snappable as TimelineItemGUI;
return itemGUI != null && !m_ItemGUIs.Contains(itemGUI);
}
public void UpdateTrackTarget(TrackAsset track)
{
if (!EditMode.AllowTrackSwitch())
return;
targetTrack = track;
var targetTracksChanged = false;
foreach (var grabbedItem in movingItems)
{
var prevTrackGUI = grabbedItem.targetTrack;
grabbedItem.SetReferenceTrack(track);
targetTracksChanged = grabbedItem.targetTrack != prevTrackGUI;
}
if (targetTracksChanged)
EditMode.HandleTrackSwitch(movingItems);
RefreshPreviewItems();
m_State.rebuildGraph |= targetTracksChanged;
}
public void OnGUI(Event evt)
{
if (!m_Grabbing)
return;
if (evt.type != EventType.Repaint)
return;
var isValid = IsValidDrop();
using (new GUIViewportScope(m_State.GetWindow().sequenceContentRect))
{
foreach (var grabbedClip in movingItems)
{
grabbedClip.RefreshBounds(m_State, evt.mousePosition);
if (!grabbedClip.HasAnyDetachedParents())
continue;
grabbedClip.Draw(isValid);
}
if (isValid)
{
EditMode.DrawMoveGUI(m_State, movingItems);
}
else
{
TimelineCursors.ClearCursor();
}
}
}
public void OnAttractedEdge(IAttractable attractable, ManipulateEdges manipulateEdges, AttractedEdge edge, double time)
{
double offset;
if (edge == AttractedEdge.Right)
{
var duration = end - start;
var startTime = time - duration;
startTime = EditMode.AdjustStartTime(m_State, m_RightMostMovingItems, startTime);
offset = startTime + duration - end;
}
else
{
if (edge == AttractedEdge.Left)
time = EditMode.AdjustStartTime(m_State, m_LeftMostMovingItems, time);
offset = time - start;
}
if (start + offset < 0.0)
offset = -start;
if (!offset.Equals(0.0))
{
foreach (var grabbedClips in movingItems)
grabbedClips.start += offset;
EditMode.UpdateMove();
RefreshPreviewItems();
}
}
public void RefreshPreviewItems()
{
foreach (var movingItemsGroup in movingItems)
{
// Check validity
var valid = ValidateItemDrag(movingItemsGroup);
foreach (var item in movingItemsGroup.items)
{
if (item.gui != null)
item.gui.isInvalid = !valid;
}
movingItemsGroup.canDrop = valid;
}
}
static bool ValidateItemDrag(ItemsPerTrack itemsGroup)
{
//TODO-marker: this is to prevent the drag operation from being canceled when moving only markers
if (itemsGroup.clips.Any())
{
if (itemsGroup.targetTrack == null)
return false;
if (itemsGroup.targetTrack.lockedInHierarchy)
return false;
if (itemsGroup.items.Any(i => !i.IsCompatibleWithTrack(itemsGroup.targetTrack)))
return false;
return EditMode.ValidateDrag(itemsGroup);
}
return true;
}
public void OnTrackDetach()
{
EditMode.OnTrackDetach(movingItems);
}
}
}

View File

@@ -0,0 +1,161 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class MoveItemModeMix : IMoveItemMode, IMoveItemDrawer
{
TimelineClip[] m_ClipsMoved;
Dictionary<TimelineClip, double> m_OriginalEaseInDuration = new Dictionary<TimelineClip, double>();
Dictionary<TimelineClip, double> m_OriginalEaseOutDuration = new Dictionary<TimelineClip, double>();
public void OnTrackDetach(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public void HandleTrackSwitch(IEnumerable<ItemsPerTrack> itemsGroups)
{
foreach (var itemsGroup in itemsGroups)
{
var targetTrack = itemsGroup.targetTrack;
if (targetTrack != null && itemsGroup.items.Any())
{
var compatible = itemsGroup.items.First().IsCompatibleWithTrack(targetTrack) &&
!EditModeUtils.IsInfiniteTrack(targetTrack);
var track = compatible ? targetTrack : null;
UndoExtensions.RegisterTrack(track, L10n.Tr("Move Items"));
EditModeUtils.SetParentTrack(itemsGroup.items, track);
}
else
{
EditModeUtils.SetParentTrack(itemsGroup.items, null);
}
}
}
public bool AllowTrackSwitch()
{
return true;
}
public double AdjustStartTime(WindowState state, ItemsPerTrack itemsGroup, double time)
{
return time;
}
public void OnModeClutchEnter(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public void OnModeClutchExit(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public void BeginMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
m_ClipsMoved = itemsGroups.SelectMany(i => i.clips).ToArray();
foreach (var clip in m_ClipsMoved)
{
m_OriginalEaseInDuration[clip] = clip.easeInDuration;
m_OriginalEaseOutDuration[clip] = clip.easeOutDuration;
}
}
public void UpdateMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
//Compute Blends before updating ease values.
foreach(var t in itemsGroups.Select(i=>i.targetTrack).Where(t => t != null))
t.ComputeBlendsFromOverlaps();
//Reset to original ease values. The trim operation will calculate the proper blend values.
foreach(var clip in m_ClipsMoved)
{
clip.easeInDuration = m_OriginalEaseInDuration[clip];
clip.easeOutDuration = m_OriginalEaseOutDuration[clip];
EditorUtility.SetDirty(clip.asset);
}
}
public void FinishMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
var allClips = itemsGroups.Select(i=>i.targetTrack)
.Where(t=>t != null).SelectMany(t => t.clips);
// update easeIn easeOut durations to apply any modifications caused by blends created or modified by clip move.
foreach (var clip in allClips)
{
clip.easeInDuration = clip.easeInDuration;
clip.easeOutDuration = clip.easeOutDuration;
}
}
public bool ValidateMove(ItemsPerTrack itemsGroup)
{
var track = itemsGroup.targetTrack;
var items = itemsGroup.items;
if (EditModeUtils.IsInfiniteTrack(track))
{
double startTime;
double stopTime;
EditModeUtils.GetInfiniteClipBoundaries(track, out startTime, out stopTime);
return items.All(item =>
!EditModeUtils.IsItemWithinRange(item, startTime, stopTime) &&
!EditModeUtils.IsRangeWithinItem(startTime, stopTime, item));
}
var siblings = ItemsUtils.GetItemsExcept(itemsGroup.targetTrack, items);
return items.All(item => EditModeMixUtils.GetPlacementValidity(item, siblings) == PlacementValidity.Valid);
}
public void DrawGUI(WindowState state, IEnumerable<MovingItems> movingItems, Color color)
{
var selectionHasAnyBlendIn = false;
var selectionHasAnyBlendOut = false;
foreach (var grabbedItems in movingItems)
{
var bounds = grabbedItems.onTrackItemsBounds;
var counter = 0;
foreach (var item in grabbedItems.items.OfType<IBlendable>())
{
if (item.hasLeftBlend)
{
EditModeGUIUtils.DrawBoundsEdge(bounds[counter], color, TrimEdge.Start);
selectionHasAnyBlendIn = true;
}
if (item.hasRightBlend)
{
EditModeGUIUtils.DrawBoundsEdge(bounds[counter], color, TrimEdge.End);
selectionHasAnyBlendOut = true;
}
counter++;
}
}
if (selectionHasAnyBlendIn && selectionHasAnyBlendOut)
{
TimelineCursors.SetCursor(TimelineCursors.CursorType.MixBoth);
}
else if (selectionHasAnyBlendIn)
{
TimelineCursors.SetCursor(TimelineCursors.CursorType.MixLeft);
}
else if (selectionHasAnyBlendOut)
{
TimelineCursors.SetCursor(TimelineCursors.CursorType.MixRight);
}
else
{
TimelineCursors.ClearCursor();
}
}
}
}

View File

@@ -0,0 +1,99 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.Timeline
{
class MoveItemModeReplace : IMoveItemMode, IMoveItemDrawer
{
public void OnTrackDetach(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public void HandleTrackSwitch(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public bool AllowTrackSwitch()
{
return true;
}
public double AdjustStartTime(WindowState state, ItemsPerTrack itemsGroup, double time)
{
return time;
}
public void OnModeClutchEnter(IEnumerable<ItemsPerTrack> itemsGroups)
{
// TODO
}
public void OnModeClutchExit(IEnumerable<ItemsPerTrack> itemsGroups)
{
// TODO
}
public void BeginMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
foreach (var itemsGroup in itemsGroups)
{
EditModeUtils.SetParentTrack(itemsGroup.items, null);
}
}
public void UpdateMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public void FinishMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
EditModeReplaceUtils.Insert(itemsGroups);
}
public bool ValidateMove(ItemsPerTrack itemsGroup)
{
return true;
}
public void DrawGUI(WindowState state, IEnumerable<MovingItems> movingItems, Color color)
{
var operationWillReplace = false;
foreach (var itemsPerTrack in movingItems)
{
var bounds = itemsPerTrack.onTrackItemsBounds;
var counter = 0;
foreach (var item in itemsPerTrack.items)
{
if (EditModeUtils.GetFirstIntersectedItem(itemsPerTrack.items, item.start) != null)
{
EditModeGUIUtils.DrawBoundsEdge(bounds[counter], color, TrimEdge.Start);
operationWillReplace = true;
}
if (EditModeUtils.GetFirstIntersectedItem(itemsPerTrack.items, item.end) != null)
{
EditModeGUIUtils.DrawBoundsEdge(bounds[counter], color, TrimEdge.End);
operationWillReplace = true;
}
counter++;
// TODO Display swallowed clips?
}
}
if (operationWillReplace)
{
TimelineCursors.SetCursor(TimelineCursors.CursorType.Replace);
}
else
{
TimelineCursors.ClearCursor();
}
}
}
}

View File

@@ -0,0 +1,271 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Object = UnityEngine.Object;
namespace UnityEditor.Timeline
{
class MoveItemModeRipple : IMoveItemMode, IMoveItemDrawer
{
const float k_SnapToEdgeDistance = 30.0f;
class PrevItemInfo
{
public ITimelineItem item;
public ITimelineItem firstSelectedItem;
public bool blending;
public PrevItemInfo(ITimelineItem item, ITimelineItem firstSelectedItem)
{
this.item = item;
this.firstSelectedItem = firstSelectedItem;
blending = item != null && item.end > firstSelectedItem.start;
}
}
readonly Dictionary<Object, List<ITimelineItem>> m_NextItems = new Dictionary<Object, List<ITimelineItem>>();
readonly Dictionary<Object, PrevItemInfo> m_PreviousItem = new Dictionary<Object, PrevItemInfo>();
double m_PreviousEnd;
bool m_TrackLocked;
bool m_Detached;
public void OnTrackDetach(IEnumerable<ItemsPerTrack> itemsGroups)
{
if (m_TrackLocked)
return;
if (m_Detached)
return;
if (itemsGroups.Any(x => x.markers.Any()))
return;
// Ripple can either remove or not clips when detaching them from their track.
// Keep it off for now. TODO: add clutch key to toggle this feature?
//EditModeRippleUtils.Remove(manipulatedClipsList);
StartDetachMode(itemsGroups);
}
public void HandleTrackSwitch(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public bool AllowTrackSwitch()
{
return !m_TrackLocked;
}
public double AdjustStartTime(WindowState state, ItemsPerTrack itemsGroup, double time)
{
var track = itemsGroup.targetTrack;
if (track == null)
return time;
double start;
double end;
if (EditModeUtils.IsInfiniteTrack(track))
{
EditModeUtils.GetInfiniteClipBoundaries(track, out start, out end);
}
else
{
var siblings = ItemsUtils.GetItemsExcept(track, itemsGroup.items);
var firstIntersectedItem = EditModeUtils.GetFirstIntersectedItem(siblings, time);
if (firstIntersectedItem == null)
return time;
start = firstIntersectedItem.start;
end = firstIntersectedItem.end;
}
var closestTime = Math.Abs(time - start) < Math.Abs(time - end) ? start : end;
var pixelTime = state.TimeToPixel(time);
var pixelClosestTime = state.TimeToPixel(closestTime);
if (Math.Abs(pixelTime - pixelClosestTime) < k_SnapToEdgeDistance)
return closestTime;
return time;
}
void StartDetachMode(IEnumerable<ItemsPerTrack> itemsGroups)
{
m_Detached = true;
foreach (var itemsGroup in itemsGroups)
EditModeUtils.SetParentTrack(itemsGroup.items, null);
}
public void OnModeClutchEnter(IEnumerable<ItemsPerTrack> itemsGroups)
{
StartDetachMode(itemsGroups);
m_TrackLocked = false;
}
public void OnModeClutchExit(IEnumerable<ItemsPerTrack> itemsGroups)
{
m_Detached = false;
m_TrackLocked = false;
}
public void BeginMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
m_NextItems.Clear();
m_PreviousItem.Clear();
var itemTypes = ItemsUtils.GetItemTypes(itemsGroups).ToList();
foreach (var itemsGroup in itemsGroups)
{
//can only ripple items of the same type as those selected
var sortedSelectedItems = itemsGroup.items.OrderBy(i => i.start).ToList();
var siblings = itemsGroup.targetTrack.GetItemsExcept(itemsGroup.items);
var sortedSiblingsToRipple = siblings.Where(i => itemTypes.Contains(i.GetType())).OrderBy(i => i.start).ToList();
var start = sortedSelectedItems.First().start;
m_NextItems.Add(itemsGroup.targetTrack, sortedSiblingsToRipple.Where(i => i.start > start).ToList());
m_PreviousItem.Add(itemsGroup.targetTrack, CalculatePrevItemInfo(sortedSelectedItems, sortedSiblingsToRipple, itemTypes));
}
m_PreviousEnd = itemsGroups.Max(m => m.items.Max(c => c.end));
}
public void UpdateMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
if (m_Detached)
return;
m_TrackLocked = true;
var overlap = 0.0;
foreach (var itemsGroup in itemsGroups)
{
var track = itemsGroup.targetTrack;
if (track == null) continue;
var prevItemInfo = m_PreviousItem[track];
if (prevItemInfo.item != null)
{
var prevItem = prevItemInfo.item;
var firstItem = prevItemInfo.firstSelectedItem;
if (prevItemInfo.blending)
prevItemInfo.blending = prevItem.end > firstItem.start;
if (prevItemInfo.blending)
{
var b = EditModeUtils.BlendDuration(firstItem, TrimEdge.End);
overlap = Math.Max(overlap, Math.Max(prevItem.start, prevItem.end - firstItem.end + firstItem.start + b) - firstItem.start);
}
else
{
overlap = Math.Max(overlap, prevItem.end - firstItem.start);
}
}
}
if (overlap > 0)
{
foreach (var itemsGroup in itemsGroups)
{
foreach (var item in itemsGroup.items)
item.start += overlap;
}
}
var newEnd = itemsGroups.Max(m => m.items.Max(c => c.end));
var offset = newEnd - m_PreviousEnd;
m_PreviousEnd = newEnd;
foreach (var itemsGroup in itemsGroups)
{
foreach (var item in m_NextItems[itemsGroup.targetTrack])
item.start += offset;
}
}
static PrevItemInfo CalculatePrevItemInfo(List<ITimelineItem> orderedSelection, List<ITimelineItem> orderedSiblings, IEnumerable<Type> itemTypes)
{
ITimelineItem previousItem = null;
ITimelineItem firstSelectedItem = null;
var gap = double.PositiveInfinity;
foreach (var type in itemTypes)
{
var firstSelectedItemOfType = orderedSelection.FirstOrDefault(i => i.GetType() == type);
if (firstSelectedItemOfType == null) continue;
var previousItemOfType = orderedSiblings.LastOrDefault(i => i.GetType() == type && i.start < firstSelectedItemOfType.start);
if (previousItemOfType == null) continue;
var currentGap = firstSelectedItemOfType.start - previousItemOfType.end;
if (currentGap < gap)
{
gap = currentGap;
firstSelectedItem = firstSelectedItemOfType;
previousItem = previousItemOfType;
}
}
return new PrevItemInfo(previousItem, firstSelectedItem);
}
public bool ValidateMove(ItemsPerTrack itemsGroup)
{
return true;
}
public void FinishMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
if (m_Detached)
EditModeRippleUtils.Insert(itemsGroups);
m_Detached = false;
m_TrackLocked = false;
}
public void DrawGUI(WindowState state, IEnumerable<MovingItems> movingItems, Color color)
{
if (m_Detached)
{
var xMin = float.MaxValue;
var xMax = float.MinValue;
foreach (var grabbedItems in movingItems)
{
xMin = Math.Min(xMin, grabbedItems.onTrackItemsBounds.Min(b => b.xMin)); // TODO Cache this?
xMax = Math.Max(xMax, grabbedItems.onTrackItemsBounds.Max(b => b.xMax));
}
foreach (var grabbedItems in movingItems)
{
var bounds = Rect.MinMaxRect(xMin, grabbedItems.onTrackItemsBounds[0].yMin,
xMax, grabbedItems.onTrackItemsBounds[0].yMax);
EditModeGUIUtils.DrawOverlayRect(bounds, new Color(1.0f, 1.0f, 1.0f, 0.5f));
EditModeGUIUtils.DrawBoundsEdge(bounds, color, TrimEdge.Start);
}
}
else
{
foreach (var grabbedItems in movingItems)
{
var bounds = Rect.MinMaxRect(grabbedItems.onTrackItemsBounds.Min(b => b.xMin), grabbedItems.onTrackItemsBounds[0].yMin,
grabbedItems.onTrackItemsBounds.Max(b => b.xMax), grabbedItems.onTrackItemsBounds[0].yMax);
EditModeGUIUtils.DrawBoundsEdge(bounds, color, TrimEdge.Start);
}
}
TimelineCursors.SetCursor(TimelineCursors.CursorType.Ripple);
}
}
}

View File

@@ -0,0 +1,137 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class MovingItems : ItemsPerTrack
{
TrackAsset m_ReferenceTrack;
readonly bool m_AllowTrackSwitch;
readonly Rect[] m_ItemsBoundsOnTrack;
readonly Vector2[] m_ItemsMouseOffsets;
static readonly Rect s_InvisibleBounds = new Rect(float.MaxValue, float.MaxValue, 0.0f, 0.0f);
public TrackAsset originalTrack { get; }
public override TrackAsset targetTrack
{
get
{
if (m_AllowTrackSwitch)
return m_ReferenceTrack;
return originalTrack;
}
}
public bool canDrop;
public double start
{
get { return m_ItemsGroup.start; }
set { m_ItemsGroup.start = value; }
}
public double end
{
get { return m_ItemsGroup.end; }
}
public Rect[] onTrackItemsBounds
{
get { return m_ItemsBoundsOnTrack; }
}
public MovingItems(WindowState state, TrackAsset parentTrack, ITimelineItem[] items, TrackAsset referenceTrack, Vector2 mousePosition, bool allowTrackSwitch)
: base(parentTrack, items)
{
originalTrack = parentTrack;
m_ReferenceTrack = referenceTrack;
m_AllowTrackSwitch = allowTrackSwitch;
m_ItemsBoundsOnTrack = new Rect[items.Length];
m_ItemsMouseOffsets = new Vector2[items.Length];
for (int i = 0; i < items.Length; ++i)
{
var itemGUi = items[i].gui;
if (itemGUi != null)
{
m_ItemsBoundsOnTrack[i] = itemGUi.rect;
m_ItemsMouseOffsets[i] = mousePosition - m_ItemsBoundsOnTrack[i].position;
}
}
canDrop = true;
}
public void SetReferenceTrack(TrackAsset track)
{
m_ReferenceTrack = track;
}
public bool HasAnyDetachedParents()
{
return m_ItemsGroup.items.Any(x => x.parentTrack == null);
}
public void RefreshBounds(WindowState state, Vector2 mousePosition)
{
for (int i = 0; i < m_ItemsGroup.items.Length; ++i)
{
var item = m_ItemsGroup.items[i];
var itemGUI = item.gui;
if (item.parentTrack != null)
{
m_ItemsBoundsOnTrack[i] = itemGUI.visible ? itemGUI.rect : s_InvisibleBounds;
}
else
{
if (targetTrack != null)
{
var trackGUI = (TimelineTrackGUI)TimelineWindow.instance.allTracks.FirstOrDefault(t => t.track == targetTrack);
if (trackGUI == null) return;
var trackRect = trackGUI.boundingRect;
m_ItemsBoundsOnTrack[i] = itemGUI.RectToTimeline(trackRect, state);
}
else
{
m_ItemsBoundsOnTrack[i].position = mousePosition - m_ItemsMouseOffsets[i];
}
}
}
}
public void Draw(bool isValid)
{
for (int i = 0; i < m_ItemsBoundsOnTrack.Length; ++i)
{
var rect = m_ItemsBoundsOnTrack[i];
DrawItemInternal(m_ItemsGroup.items[i], rect, isValid);
}
}
static void DrawItemInternal(ITimelineItem item, Rect rect, bool isValid)
{
var clipGUI = item.gui as TimelineClipGUI;
if (clipGUI != null)
{
if (isValid)
{
clipGUI.DrawGhostClip(rect);
}
else
{
clipGUI.DrawInvalidClip(rect);
}
}
}
}
}

View File

@@ -0,0 +1,140 @@
using System;
using System.Text;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class EaseClip : Manipulator
{
bool m_IsCaptured;
bool m_UndoSaved;
TimelineClipHandle m_EaseClipHandler;
ManipulateEdges m_Edges;
TimelineClip m_Clip;
StringBuilder m_OverlayText = new StringBuilder("");
double m_OriginalValue;
public static readonly string EaseInClipText = L10n.Tr("Ease In Clip");
public static readonly string EaseOutClipText = L10n.Tr("Ease Out Clip");
public static readonly string EaseInText = L10n.Tr("Ease In");
public static readonly string EaseOutText = L10n.Tr("Ease Out");
public static readonly string DurationText = L10n.Tr("Duration: ");
protected override bool MouseDown(Event evt, WindowState state)
{
if (evt.modifiers != ManipulatorsUtils.actionModifier)
return false;
return MouseDownInternal(evt, state, PickerUtils.TopmostPickedItem() as TimelineClipHandle);
}
protected bool MouseDownInternal(Event evt, WindowState state, TimelineClipHandle handle)
{
if (handle == null)
return false;
if (handle.clipGUI.clip != null && !handle.clipGUI.clip.clipCaps.HasAny(ClipCaps.Blending))
return false;
m_Edges = ManipulateEdges.Right;
if (handle.trimDirection == TrimEdge.Start)
m_Edges = ManipulateEdges.Left;
if (m_Edges == ManipulateEdges.Left && handle.clipGUI.clip.hasBlendIn || m_Edges == ManipulateEdges.Right && handle.clipGUI.clip.hasBlendOut)
return false;
m_IsCaptured = true;
m_UndoSaved = false;
m_EaseClipHandler = handle;
m_Clip = handle.clipGUI.clip;
m_OriginalValue = m_Edges == ManipulateEdges.Left ? m_Clip.easeInDuration : m_Clip.easeOutDuration;
// Change cursor only when OnGUI Process (not in test)
if (GUIUtility.guiDepth > 0)
TimelineCursors.SetCursor(m_Edges == ManipulateEdges.Left ? TimelineCursors.CursorType.MixRight : TimelineCursors.CursorType.MixLeft);
state.AddCaptured(this);
return true;
}
protected override bool MouseUp(Event evt, WindowState state)
{
if (!m_IsCaptured)
return false;
m_IsCaptured = false;
m_UndoSaved = false;
state.captured.Clear();
// Clear cursor only when OnGUI Process (not in test)
if (GUIUtility.guiDepth > 0)
TimelineCursors.ClearCursor();
return true;
}
protected override bool MouseDrag(Event evt, WindowState state)
{
if (!m_IsCaptured)
return false;
if (!m_UndoSaved)
{
var uiClip = m_EaseClipHandler.clipGUI;
string undoName = m_Edges == ManipulateEdges.Left ? EaseInClipText : EaseOutClipText;
UndoExtensions.RegisterClip(uiClip.clip, undoName);
m_UndoSaved = true;
}
double d = state.PixelDeltaToDeltaTime(evt.delta.x);
var duration = m_Clip.duration;
var easeInDurationLimit = duration - m_Clip.easeOutDuration;
var easeOutDurationLimit = duration - m_Clip.easeInDuration;
if (m_Edges == ManipulateEdges.Left)
{
m_Clip.easeInDuration = Math.Min(easeInDurationLimit, Math.Max(0, m_Clip.easeInDuration + d));
}
else if (m_Edges == ManipulateEdges.Right)
{
m_Clip.easeOutDuration = Math.Min(easeOutDurationLimit, Math.Max(0, m_Clip.easeOutDuration - d));
}
RefreshOverlayStrings(m_EaseClipHandler, state);
return true;
}
public override void Overlay(Event evt, WindowState state)
{
if (!m_IsCaptured)
return;
if (m_OverlayText.Length > 0)
{
int stringLength = m_OverlayText.Length;
var r = new Rect(evt.mousePosition.x - (stringLength / 2.0f),
m_EaseClipHandler.clipGUI.rect.yMax,
stringLength, 20);
GUI.Label(r, m_OverlayText.ToString(), TimelineWindow.styles.tinyFont);
}
}
void RefreshOverlayStrings(TimelineClipHandle handle, WindowState state)
{
m_OverlayText.Length = 0;
m_OverlayText.Append(m_Edges == ManipulateEdges.Left ? EaseInText : EaseOutText);
var easeDuration = m_Edges == ManipulateEdges.Left ? m_Clip.easeInDuration : m_Clip.easeOutDuration;
var deltaDuration = easeDuration - m_OriginalValue;
// round to frame so we don't show partial time codes due to no frame snapping
if (state.timeFormat == TimeFormat.Timecode)
{
easeDuration = TimeUtility.RoundToFrame(easeDuration, state.referenceSequence.frameRate);
deltaDuration = TimeUtility.RoundToFrame(deltaDuration, state.referenceSequence.frameRate);
}
m_OverlayText.Append(DurationText);
m_OverlayText.Append(state.timeFormat.ToTimeStringWithDelta(easeDuration, state.referenceSequence.frameRate, deltaDuration));
}
}
}

View File

@@ -0,0 +1,61 @@
using UnityEditor.ShortcutManagement;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class Jog : Manipulator
{
Vector2 m_MouseDownOrigin = Vector2.zero;
[ClutchShortcut("Timeline/Jog", typeof(TimelineWindow), KeyCode.J)]
static void JogShortcut(ShortcutArguments args)
{
if (args.stage == ShortcutStage.Begin)
{
(args.context as TimelineWindow).state.isJogging = true;
}
else if (args.stage == ShortcutStage.End)
{
(args.context as TimelineWindow).state.isJogging = false;
}
}
protected override bool MouseDown(Event evt, WindowState state)
{
if (!state.isJogging)
return false;
m_MouseDownOrigin = evt.mousePosition;
state.playbackSpeed = 0.0f;
state.Play();
return true;
}
protected override bool MouseUp(Event evt, WindowState state)
{
if (!state.isJogging)
{
return false;
}
m_MouseDownOrigin = evt.mousePosition;
state.playbackSpeed = 0.0f;
state.Play();
return false;
}
protected override bool MouseDrag(Event evt, WindowState state)
{
if (!state.isJogging)
return false;
var distance = evt.mousePosition - m_MouseDownOrigin;
state.playbackSpeed = distance.x * 0.002f;
state.Play();
return true;
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using UnityEngine;
namespace UnityEditor.Timeline
{
class MarkerHeaderTrackManipulator : Manipulator
{
protected override bool ContextClick(Event evt, WindowState state)
{
if (!IsMouseOverMarkerHeader(evt.mousePosition, state))
return false;
SelectionManager.SelectOnly(state.editSequence.asset.markerTrack);
SequencerContextMenu.ShowTrackContextMenu(evt.mousePosition);
return true;
}
protected override bool MouseDown(Event evt, WindowState state)
{
if (evt.button != 0 || !IsMouseOverMarkerHeader(evt.mousePosition, state))
return false;
SelectionManager.SelectOnly(state.editSequence.asset.markerTrack);
return true;
}
static bool IsMouseOverMarkerHeader(Vector2 mousePosition, WindowState state)
{
if (!state.showMarkerHeader)
return false;
return state.GetWindow().markerHeaderRect.Contains(mousePosition)
|| state.GetWindow().markerContentRect.Contains(mousePosition);
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Linq;
using UnityEngine;
namespace UnityEditor.Timeline
{
class RectangleSelect : RectangleTool
{
protected override bool enableAutoPan { get { return false; } }
protected override bool CanStartRectangle(Event evt)
{
if (evt.button != 0 || evt.alt)
return false;
return PickerUtils.pickedElements.All(e => e is IRowGUI);
}
protected override bool OnFinish(Event evt, WindowState state, Rect rect)
{
var selectables = state.spacePartitioner.GetItemsInArea<ISelectable>(rect).ToList();
if (!selectables.Any())
return false;
if (ItemSelection.CanClearSelection(evt))
SelectionManager.Clear();
foreach (var selectable in selectables)
{
ItemSelection.HandleItemSelection(evt, selectable);
}
return true;
}
}
}

View File

@@ -0,0 +1,175 @@
using System;
using UnityEngine;
namespace UnityEditor.Timeline
{
abstract class RectangleTool
{
readonly struct TimelinePoint
{
readonly double m_Time;
readonly float m_YPos;
readonly float m_YScrollPos;
readonly WindowState m_State;
readonly TimelineTreeViewGUI m_TreeViewGUI;
public TimelinePoint(WindowState state, Vector2 mousePosition)
{
m_State = state;
m_TreeViewGUI = state.GetWindow().treeView;
m_Time = m_State.PixelToTime(mousePosition.x);
m_YPos = mousePosition.y;
m_YScrollPos = m_TreeViewGUI.scrollPosition.y;
}
public Vector2 ToPixel()
{
return new Vector2(m_State.TimeToPixel(m_Time), m_YPos - (m_TreeViewGUI.scrollPosition.y - m_YScrollPos));
}
}
const float k_HeaderSplitterOverlap = WindowConstants.headerSplitterWidth / 2;
TimeAreaAutoPanner m_TimeAreaAutoPanner;
TimelinePoint m_StartPoint;
Vector2 m_EndPixel = Vector2.zero;
Rect m_ActiveRect;
protected abstract bool enableAutoPan { get; }
protected abstract bool CanStartRectangle(Event evt);
protected abstract bool OnFinish(Event evt, WindowState state, Rect rect);
int m_Id;
public void OnGUI(WindowState state, EventType rawType, Vector2 mousePosition)
{
if (m_Id == 0)
m_Id = GUIUtility.GetPermanentControlID();
if (state == null || state.GetWindow().treeView == null)
return;
var evt = Event.current;
if (rawType == EventType.MouseDown || evt.type == EventType.MouseDown)
{
if (state.IsCurrentEditingASequencerTextField())
return;
m_ActiveRect = TimelineWindow.instance.sequenceContentRect;
//remove the track header splitter overlap
m_ActiveRect.x += k_HeaderSplitterOverlap;
m_ActiveRect.width -= k_HeaderSplitterOverlap;
if (!m_ActiveRect.Contains(mousePosition))
return;
if (!CanStartRectangle(evt))
return;
if (enableAutoPan)
m_TimeAreaAutoPanner = new TimeAreaAutoPanner(state);
m_StartPoint = new TimelinePoint(state, mousePosition);
m_EndPixel = mousePosition;
GUIUtility.hotControl = m_Id; //HACK: Because the treeView eats all the events, steal the hotControl if necessary...
evt.Use();
return;
}
switch (evt.GetTypeForControl(m_Id))
{
case EventType.KeyDown:
{
if (GUIUtility.hotControl == m_Id)
{
if (evt.keyCode == KeyCode.Escape)
{
m_TimeAreaAutoPanner = null;
GUIUtility.hotControl = 0;
evt.Use();
}
}
return;
}
case EventType.MouseDrag:
{
if (GUIUtility.hotControl != m_Id)
return;
m_EndPixel = mousePosition;
evt.Use();
return;
}
case EventType.MouseUp:
{
if (GUIUtility.hotControl != m_Id)
return;
m_TimeAreaAutoPanner = null;
var rect = CurrentRectangle();
if (IsValidRect(rect))
OnFinish(evt, state, rect);
GUIUtility.hotControl = 0;
evt.Use();
return;
}
}
if (GUIUtility.hotControl == m_Id)
{
if (evt.type == EventType.Repaint)
{
var r = CurrentRectangle();
if (IsValidRect(r))
{
using (new GUIViewportScope(m_ActiveRect))
{
DrawRectangle(r);
}
}
}
if (m_TimeAreaAutoPanner != null)
m_TimeAreaAutoPanner.OnGUI(evt);
}
}
static void DrawRectangle(Rect rect)
{
EditorStyles.selectionRect.Draw(rect, GUIContent.none, false, false, false, false);
}
static bool IsValidRect(Rect rect)
{
return rect.width >= 1.0f && rect.height >= 1.0f;
}
Rect CurrentRectangle()
{
var startPixel = m_StartPoint.ToPixel();
return Rect.MinMaxRect(
Math.Min(startPixel.x, m_EndPixel.x),
Math.Min(startPixel.y, m_EndPixel.y),
Math.Max(startPixel.x, m_EndPixel.x),
Math.Max(startPixel.y, m_EndPixel.y));
}
}
}

View File

@@ -0,0 +1,23 @@
using UnityEngine;
namespace UnityEditor.Timeline
{
class RectangleZoom : RectangleTool
{
protected override bool enableAutoPan { get { return true; } }
protected override bool CanStartRectangle(Event evt)
{
return evt.button == 1 && evt.modifiers == (EventModifiers.Alt | EventModifiers.Shift);
}
protected override bool OnFinish(Event evt, WindowState state, Rect rect)
{
var x = state.PixelToTime(rect.xMin);
var y = state.PixelToTime(rect.xMax);
state.SetTimeAreaShownRange(x, y);
return true;
}
}
}

View File

@@ -0,0 +1,297 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class ClearSelection : Manipulator
{
protected override bool MouseDown(Event evt, WindowState state)
{
// If we hit this point this means no one used the mouse down events. We can safely clear the selection if needed
if (evt.button != 0)
return false;
var window = state.GetWindow();
if (!window.sequenceRect.Contains(evt.mousePosition))
return false;
if (ItemSelection.CanClearSelection(evt))
{
SelectionManager.Clear();
return true;
}
return false;
}
}
static class ItemSelection
{
public static bool CanClearSelection(Event evt)
{
return !evt.control && !evt.command && !evt.shift;
}
public static void RangeSelectItems(ITimelineItem lastItemToSelect)
{
var selectSorted = SelectionManager.SelectedItems().ToList();
var firstSelect = selectSorted.FirstOrDefault();
if (firstSelect == null)
{
SelectionManager.Add(lastItemToSelect);
return;
}
var allTracks = TimelineEditor.inspectedAsset.flattenedTracks;
var allItems = allTracks.SelectMany(ItemsUtils.GetItems).ToList();
TimelineHelpers.RangeSelect(allItems, selectSorted, lastItemToSelect, SelectionManager.Add, SelectionManager.Remove);
}
public static ISelectable HandleSingleSelection(Event evt)
{
var item = PickerUtils.TopmostPickedItemOfType<ISelectable>(i => i.CanSelect(evt));
if (item != null)
{
var selected = item.IsSelected();
if (!selected && CanClearSelection(evt))
SelectionManager.Clear();
if (evt.modifiers == EventModifiers.Shift)
{
if (!selected)
RangeSelectItems((item as TimelineItemGUI)?.item);
}
else
{
HandleItemSelection(evt, item);
}
}
return item;
}
public static void HandleItemSelection(Event evt, ISelectable item)
{
if (evt.modifiers == ManipulatorsUtils.actionModifier)
{
if (item.IsSelected())
item.Deselect();
else
item.Select();
}
else
{
if (!item.IsSelected())
item.Select();
}
}
}
class SelectAndMoveItem : Manipulator
{
bool m_Dragged;
SnapEngine m_SnapEngine;
TimeAreaAutoPanner m_TimeAreaAutoPanner;
Vector2 m_MouseDownPosition;
bool m_HorizontalMovementDone;
bool m_VerticalMovementDone;
MoveItemHandler m_MoveItemHandler;
bool m_CycleMarkersPending;
protected override bool MouseDown(Event evt, WindowState state)
{
if (evt.alt || evt.button != 0)
return false;
m_Dragged = false;
// Cycling markers and selection are mutually exclusive operations
if (!HandleMarkerCycle() && !HandleSingleSelection(evt))
return false;
m_MouseDownPosition = evt.mousePosition;
m_VerticalMovementDone = false;
m_HorizontalMovementDone = false;
return true;
}
protected override bool MouseUp(Event evt, WindowState state)
{
if (!m_Dragged)
{
var item = PickerUtils.TopmostPickedItem() as ISelectable;
if (item == null)
return false;
if (!item.IsSelected())
return false;
// Re-selecting an item part of a multi-selection should only keep this item selected.
if (SelectionManager.Count() > 1 && ItemSelection.CanClearSelection(evt))
{
SelectionManager.Clear();
item.Select();
return true;
}
if (m_CycleMarkersPending)
{
m_CycleMarkersPending = false;
TimelineMarkerClusterGUI.CycleMarkers();
return true;
}
return false;
}
m_TimeAreaAutoPanner = null;
DropItems();
m_SnapEngine = null;
m_MoveItemHandler = null;
state.Evaluate();
state.RemoveCaptured(this);
m_Dragged = false;
TimelineCursors.ClearCursor();
return true;
}
protected override bool DoubleClick(Event evt, WindowState state)
{
return MouseDown(evt, state) && MouseUp(evt, state);
}
protected override bool MouseDrag(Event evt, WindowState state)
{
if (state.editSequence.isReadOnly)
return false;
// case 1099285 - ctrl-click can cause no clips to be selected
var selectedItemsGUI = SelectionManager.SelectedItems();
if (!selectedItemsGUI.Any())
{
m_Dragged = false;
return false;
}
const float hDeadZone = 5.0f;
const float vDeadZone = 5.0f;
bool vDone = m_VerticalMovementDone || Math.Abs(evt.mousePosition.y - m_MouseDownPosition.y) > vDeadZone;
bool hDone = m_HorizontalMovementDone || Math.Abs(evt.mousePosition.x - m_MouseDownPosition.x) > hDeadZone;
m_CycleMarkersPending = false;
if (!m_Dragged)
{
var canStartMove = vDone || hDone;
if (canStartMove)
{
state.AddCaptured(this);
m_Dragged = true;
var referenceTrack = GetTrackDropTargetAt(state, m_MouseDownPosition);
foreach (var item in selectedItemsGUI)
item.gui.StartDrag();
m_MoveItemHandler = new MoveItemHandler(state);
m_MoveItemHandler.Grab(selectedItemsGUI, referenceTrack, m_MouseDownPosition);
m_SnapEngine = new SnapEngine(m_MoveItemHandler, m_MoveItemHandler, ManipulateEdges.Both,
state, m_MouseDownPosition);
m_TimeAreaAutoPanner = new TimeAreaAutoPanner(state);
}
}
if (!m_VerticalMovementDone)
{
m_VerticalMovementDone = vDone;
if (m_VerticalMovementDone)
m_MoveItemHandler.OnTrackDetach();
}
if (!m_HorizontalMovementDone)
{
m_HorizontalMovementDone = hDone;
}
if (m_Dragged)
{
if (m_HorizontalMovementDone)
m_SnapEngine.Snap(evt.mousePosition, evt.modifiers);
if (m_VerticalMovementDone)
{
var track = GetTrackDropTargetAt(state, evt.mousePosition);
m_MoveItemHandler.UpdateTrackTarget(track);
}
state.Evaluate();
}
return true;
}
public override void Overlay(Event evt, WindowState state)
{
if (!m_Dragged)
return;
if (m_TimeAreaAutoPanner != null)
m_TimeAreaAutoPanner.OnGUI(evt);
m_MoveItemHandler.OnGUI(evt);
if (!m_MoveItemHandler.allowTrackSwitch || m_MoveItemHandler.targetTrack != null)
{
TimeIndicator.Draw(state, m_MoveItemHandler.start, m_MoveItemHandler.end);
m_SnapEngine.OnGUI();
}
}
bool HandleMarkerCycle()
{
m_CycleMarkersPending = TimelineMarkerClusterGUI.CanCycleMarkers();
return m_CycleMarkersPending;
}
static bool HandleSingleSelection(Event evt)
{
return ItemSelection.HandleSingleSelection(evt) != null;
}
void DropItems()
{
// Order matters here: m_MoveItemHandler.movingItems is destroyed during call to Drop()
foreach (var movingItem in m_MoveItemHandler.movingItems)
{
foreach (var item in movingItem.items)
item.gui.StopDrag();
}
m_MoveItemHandler.Drop();
}
static TrackAsset GetTrackDropTargetAt(WindowState state, Vector2 point)
{
var track = state.spacePartitioner.GetItemsAtPosition<IRowGUI>(point).FirstOrDefault();
return track != null ? track.asset : null;
}
}
}

View File

@@ -0,0 +1,20 @@
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TrackZoom : Manipulator
{
// only handles 'vertical' zoom. horizontal is handled in timelineGUI
protected override bool MouseWheel(Event evt, WindowState state)
{
if (EditorGUI.actionKey)
{
state.trackScale = Mathf.Min(Mathf.Max(state.trackScale + (evt.delta.y * 0.1f), 1.0f), 100.0f);
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TrimClip : Manipulator
{
private readonly string kDurationText = L10n.Tr("Duration:");
private readonly string kSpeedText = L10n.Tr("Speed:");
class TrimClipAttractionHandler : IAttractionHandler
{
public void OnAttractedEdge(IAttractable attractable, ManipulateEdges manipulateEdges, AttractedEdge edge, double time)
{
var clipGUI = attractable as TimelineClipGUI;
if (clipGUI == null)
return;
var clipItem = ItemsUtils.ToItem(clipGUI.clip);
if (manipulateEdges == ManipulateEdges.Right)
{
bool affectTimeScale = IsAffectingTimeScale(clipGUI.clip);
EditMode.TrimEnd(clipItem, time, affectTimeScale);
}
else if (manipulateEdges == ManipulateEdges.Left)
{
bool affectTimeScale = IsAffectingTimeScale(clipGUI.clip);
EditMode.TrimStart(clipItem, time, affectTimeScale);
}
}
private bool IsAffectingTimeScale(TimelineClip clip)
{
bool autoScale = (clip.clipCaps & ClipCaps.AutoScale) == ClipCaps.AutoScale;
// TODO Do not use Event.current from here.
bool affectTimeScale = (autoScale && (Event.current.modifiers != EventModifiers.Shift))
|| (!autoScale && (Event.current.modifiers == EventModifiers.Shift));
return affectTimeScale;
}
}
bool m_IsCaptured;
TimelineClipHandle m_TrimClipHandler;
double m_OriginalDuration;
double m_OriginalTimeScale;
double m_OriginalEaseInDuration;
double m_OriginalEaseOutDuration;
bool m_UndoSaved;
SnapEngine m_SnapEngine;
readonly List<string> m_OverlayStrings = new List<string>();
static readonly double kEpsilon = 0.0000001;
protected override bool MouseDown(Event evt, WindowState state)
{
var handle = PickerUtils.TopmostPickedItem() as TimelineClipHandle;
if (handle == null)
return false;
if (handle.clipGUI.clip.GetParentTrack() != null && handle.clipGUI.clip.GetParentTrack().lockedInHierarchy)
return false;
m_TrimClipHandler = handle;
m_IsCaptured = true;
state.AddCaptured(this);
m_UndoSaved = false;
var clip = m_TrimClipHandler.clipGUI.clip;
m_OriginalDuration = clip.duration;
m_OriginalTimeScale = clip.timeScale;
m_OriginalEaseInDuration = clip.easeInDuration;
m_OriginalEaseOutDuration = clip.easeOutDuration;
RefreshOverlayStrings(m_TrimClipHandler, state);
// in ripple trim, the right edge moves and needs to snap
var edges = ManipulateEdges.Right;
if (EditMode.editType != EditMode.EditType.Ripple && m_TrimClipHandler.trimDirection == TrimEdge.Start)
edges = ManipulateEdges.Left;
m_SnapEngine = new SnapEngine(m_TrimClipHandler.clipGUI, new TrimClipAttractionHandler(), edges, state,
evt.mousePosition);
EditMode.BeginTrim(ItemsUtils.ToItem(clip), m_TrimClipHandler.trimDirection);
return true;
}
protected override bool MouseUp(Event evt, WindowState state)
{
if (!m_IsCaptured)
return false;
m_IsCaptured = false;
m_TrimClipHandler = null;
m_UndoSaved = false;
m_SnapEngine = null;
EditMode.FinishTrim();
state.captured.Clear();
return true;
}
protected override bool MouseDrag(Event evt, WindowState state)
{
if (state.editSequence.isReadOnly)
return false;
if (!m_IsCaptured)
return false;
var uiClip = m_TrimClipHandler.clipGUI;
if (!m_UndoSaved)
{
UndoExtensions.RegisterClip(uiClip.clip, L10n.Tr("Trim Clip"));
if (TimelineUtility.IsRecordableAnimationClip(uiClip.clip))
{
TimelineUndo.PushUndo(uiClip.clip.animationClip, L10n.Tr("Trim Clip"));
}
m_UndoSaved = true;
}
//Reset to original ease values. The trim operation will calculate the proper blend values.
uiClip.clip.easeInDuration = m_OriginalEaseInDuration;
uiClip.clip.easeOutDuration = m_OriginalEaseOutDuration;
if (m_SnapEngine != null)
m_SnapEngine.Snap(evt.mousePosition, evt.modifiers);
RefreshOverlayStrings(m_TrimClipHandler, state);
if (Selection.activeObject != null)
EditorUtility.SetDirty(Selection.activeObject);
// updates the duration of the graph without rebuilding
state.UpdateRootPlayableDuration(state.editSequence.duration);
return true;
}
public override void Overlay(Event evt, WindowState state)
{
if (!m_IsCaptured)
return;
EditMode.DrawTrimGUI(state, m_TrimClipHandler.clipGUI, m_TrimClipHandler.trimDirection);
bool trimStart = m_TrimClipHandler.trimDirection == TrimEdge.Start;
TimeIndicator.Draw(state, trimStart ? m_TrimClipHandler.clipGUI.start : m_TrimClipHandler.clipGUI.end);
if (m_SnapEngine != null)
m_SnapEngine.OnGUI(trimStart, !trimStart);
if (m_OverlayStrings.Count > 0)
{
const float padding = 4.0f;
var labelStyle = TimelineWindow.styles.tinyFont;
var longestLine = labelStyle.CalcSize(
new GUIContent(m_OverlayStrings.Aggregate("", (max, cur) => max.Length > cur.Length ? max : cur)));
var stringLength = longestLine.x + padding;
var lineHeight = longestLine.y + padding;
var r = new Rect(evt.mousePosition.x - (stringLength / 2.0f),
m_TrimClipHandler.clipGUI.rect.yMax,
stringLength, lineHeight);
foreach (var s in m_OverlayStrings)
{
GUI.Label(r, s, labelStyle);
r.y += lineHeight;
}
}
}
void RefreshOverlayStrings(TimelineClipHandle handle, WindowState state)
{
m_OverlayStrings.Clear();
var differenceDuration = handle.clipGUI.clip.duration - m_OriginalDuration;
m_OverlayStrings.Add($"{kDurationText} {state.timeFormat.ToTimeStringWithDelta(handle.clipGUI.clip.duration, state.referenceSequence.frameRate, differenceDuration)}");
var differenceSpeed = m_OriginalTimeScale - handle.clipGUI.clip.timeScale;
if (Math.Abs(differenceSpeed) > kEpsilon)
{
var sign = differenceSpeed > 0 ? "+" : "";
var timeScale = handle.clipGUI.clip.timeScale.ToString("f2");
var deltaSpeed = differenceSpeed.ToString("p2");
m_OverlayStrings.Add($"{kSpeedText} {timeScale} ({sign}{deltaSpeed}) ");
}
}
}
}

View File

@@ -0,0 +1,72 @@
using UnityEngine;
namespace UnityEditor.Timeline
{
class TimeAreaAutoPanner
{
readonly WindowState m_State;
readonly TimelineWindow m_Window;
readonly Rect m_ViewRect;
const float k_PixelDistanceToMaxSpeed = 100.0f;
const float k_MaxPanSpeed = 30.0f;
public TimeAreaAutoPanner(WindowState state)
{
m_State = state;
m_Window = m_State.GetWindow();
var shownRange = m_State.timeAreaShownRange;
var trackViewBounds = m_Window.sequenceRect;
m_ViewRect = Rect.MinMaxRect(m_State.TimeToPixel(shownRange.x), trackViewBounds.yMin,
m_State.TimeToPixel(shownRange.y), trackViewBounds.yMax);
}
public void OnGUI(Event evt)
{
if (evt.type != EventType.Layout)
return;
var hFactor = 0.0f;
var vFactor = 0.0f;
bool horizontalPan = GetPanFactor(evt.mousePosition.x, m_ViewRect.xMin, m_ViewRect.xMax, out hFactor);
bool verticalPan = GetPanFactor(evt.mousePosition.y, m_ViewRect.yMin, m_ViewRect.yMax, out vFactor);
if (horizontalPan)
{
var translation = m_State.timeAreaTranslation;
translation.x += hFactor * k_MaxPanSpeed;
m_State.SetTimeAreaTransform(translation, m_State.timeAreaScale);
}
if (verticalPan)
{
var translation = m_Window.treeView.scrollPosition;
translation.y -= vFactor * k_MaxPanSpeed;
m_Window.treeView.scrollPosition = translation;
}
}
static bool GetPanFactor(float v, float min, float max, out float factor)
{
factor = 0.0f;
if (v < min)
{
factor = Mathf.Clamp01((min - v) / k_PixelDistanceToMaxSpeed);
return true;
}
if (v > max)
{
factor = -Mathf.Clamp01((v - max) / k_PixelDistanceToMaxSpeed);
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,47 @@
using UnityEngine;
namespace UnityEditor.Timeline
{
static class TimeIndicator
{
static readonly Tooltip s_Tooltip = new Tooltip(DirectorStyles.Instance.displayBackground, DirectorStyles.Instance.tinyFont);
public static void Draw(WindowState state, double time)
{
var bounds = state.timeAreaRect;
bounds.xMin = Mathf.Max(bounds.xMin, state.TimeToTimeAreaPixel(time));
using (new GUIViewportScope(state.timeAreaRect))
{
s_Tooltip.text = TimeReferenceUtility.ToTimeString(time);
var tooltipBounds = s_Tooltip.bounds;
tooltipBounds.xMin = bounds.xMin - (tooltipBounds.width / 2.0f);
tooltipBounds.y = bounds.y;
s_Tooltip.bounds = tooltipBounds;
if (time >= 0)
s_Tooltip.Draw();
}
if (time >= 0)
{
Graphics.DrawLineAtTime(state, time, Color.black, true);
}
}
public static void Draw(WindowState state, double start, double end)
{
var bounds = state.timeAreaRect;
bounds.xMin = Mathf.Max(bounds.xMin, state.TimeToTimeAreaPixel(start));
bounds.xMax = Mathf.Min(bounds.xMax, state.TimeToTimeAreaPixel(end));
var color = DirectorStyles.Instance.selectedStyle.focused.textColor;
color.a = 0.12f;
EditorGUI.DrawRect(bounds, color);
Draw(state, start);
Draw(state, end);
}
}
}

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TimelineClipGroup
{
readonly TimelineClip[] m_Clips;
readonly TimelineClip m_LeftMostClip;
readonly TimelineClip m_RightMostClip;
public TimelineClip[] clips
{
get { return m_Clips; }
}
public double start
{
get { return m_LeftMostClip.start; }
set
{
var offset = value - m_LeftMostClip.start;
foreach (var clip in m_Clips)
clip.start += offset;
}
}
public double end
{
get { return m_RightMostClip.end; }
}
public TimelineClipGroup(IEnumerable<TimelineClip> clips)
{
Debug.Assert(clips != null && clips.Any());
m_Clips = clips.ToArray();
m_LeftMostClip = null;
m_RightMostClip = null;
foreach (var clip in m_Clips)
{
if (m_LeftMostClip == null || clip.start < m_LeftMostClip.start)
m_LeftMostClip = clip;
if (m_RightMostClip == null || clip.end > m_RightMostClip.end)
m_RightMostClip = clip;
}
}
}
}

View File

@@ -0,0 +1,23 @@
using UnityEngine;
namespace UnityEditor.Timeline
{
enum TrimEdge
{
Start,
End
}
interface ITrimItemMode
{
void OnBeforeTrim(ITrimmable item, TrimEdge trimDirection);
void TrimStart(ITrimmable item, double time, bool affectTimeScale);
void TrimEnd(ITrimmable item, double time, bool affectTimeScale);
}
interface ITrimItemDrawer
{
void DrawGUI(WindowState state, Rect bounds, Color color, TrimEdge edge);
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TrimItemModeMix : ITrimItemMode, ITrimItemDrawer
{
ITrimmable m_Item;
double m_Min;
double m_Max;
public void OnBeforeTrim(ITrimmable item, TrimEdge trimDirection)
{
m_Item = item;
var sortedItems = ItemsUtils.GetItemsExcept(item.parentTrack, new[] {item})
.OfType<ITrimmable>()
.OrderBy(c => c.start);
var itemStart = (DiscreteTime)item.start;
var itemEnd = (DiscreteTime)item.end;
var overlapped = sortedItems.LastOrDefault(c => (DiscreteTime)c.start == itemStart && (DiscreteTime)c.end == itemEnd);
ITrimmable nextItem;
ITrimmable prevItem;
m_Min = 0.0;
m_Max = double.PositiveInfinity;
var blendableItem = item as IBlendable;
if (blendableItem != null && blendableItem.supportsBlending)
{
if (trimDirection == TrimEdge.Start)
{
nextItem = sortedItems.FirstOrDefault(c => (DiscreteTime)c.start >= itemStart && (DiscreteTime)c.end > itemEnd);
prevItem = sortedItems.LastOrDefault(c => (DiscreteTime)c.start <= itemStart && (DiscreteTime)c.end < itemEnd);
if (prevItem != null)
m_Min = prevItem.start + EditModeUtils.BlendDuration(prevItem, TrimEdge.Start);
if (nextItem != null)
m_Max = nextItem.start;
}
else
{
nextItem = sortedItems.FirstOrDefault(c => c != overlapped && (DiscreteTime)c.start >= itemStart && (DiscreteTime)c.end >= itemEnd);
prevItem = sortedItems.LastOrDefault(c => c != overlapped && (DiscreteTime)c.start <= itemStart && (DiscreteTime)c.end <= itemEnd);
if (prevItem != null)
m_Min = prevItem.end;
if (nextItem != null)
m_Max = nextItem.end - EditModeUtils.BlendDuration(nextItem, TrimEdge.End);
}
}
else
{
nextItem = sortedItems.FirstOrDefault(c => (DiscreteTime)c.start > itemStart);
prevItem = sortedItems.LastOrDefault(c => (DiscreteTime)c.start < itemStart);
if (prevItem != null)
m_Min = prevItem.end;
if (nextItem != null)
m_Max = nextItem.start;
}
}
public void TrimStart(ITrimmable item, double time, bool affectTimeScale)
{
time = Math.Min(Math.Max(time, m_Min), m_Max);
item.SetStart(time, affectTimeScale);
}
public void TrimEnd(ITrimmable item, double time, bool affectTimeScale)
{
time = Math.Min(Math.Max(time, m_Min), m_Max);
item.SetEnd(time, affectTimeScale);
}
public void DrawGUI(WindowState state, Rect bounds, Color color, TrimEdge edge)
{
if (EditModeUtils.HasBlends(m_Item, edge))
{
EditModeGUIUtils.DrawBoundsEdge(bounds, color, edge);
var cursorType = (edge == TrimEdge.End)
? TimelineCursors.CursorType.MixRight
: TimelineCursors.CursorType.MixLeft;
TimelineCursors.SetCursor(cursorType);
}
else
{
TimelineCursors.ClearCursor();
}
}
}
}

View File

@@ -0,0 +1,139 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TrimItemModeReplace : ITrimItemMode, ITrimItemDrawer
{
ITrimmable m_Item;
ITrimmable m_ItemToBeReplaced;
double m_ClipOriginalEdgeValue;
bool m_TrimReplace;
double m_Min;
double m_Max;
public void OnBeforeTrim(ITrimmable item, TrimEdge trimDirection)
{
m_Item = item;
var sortedClips = ItemsUtils.GetItemsExcept(item.parentTrack, new[] { item })
.OfType<ITrimmable>()
.OrderBy(c => c.start);
var clipStart = (DiscreteTime)item.start;
var clipEnd = (DiscreteTime)item.end;
var overlapped = sortedClips.LastOrDefault(c => (DiscreteTime)c.start == clipStart && (DiscreteTime)c.end == clipEnd);
ITrimmable nextItem;
ITrimmable prevItem;
m_Min = 0.0;
m_Max = double.PositiveInfinity;
if (trimDirection == TrimEdge.Start)
{
nextItem = sortedClips.FirstOrDefault(c => (DiscreteTime)c.start >= clipStart && (DiscreteTime)c.end > clipEnd);
prevItem = sortedClips.LastOrDefault(c => (DiscreteTime)c.start <= clipStart && (DiscreteTime)c.end < clipEnd);
if (prevItem != null)
m_Min = prevItem.start + EditModeUtils.BlendDuration(prevItem, TrimEdge.Start) + TimelineClip.kMinDuration;
if (nextItem != null)
m_Max = nextItem.start;
m_ItemToBeReplaced = prevItem;
if (m_ItemToBeReplaced != null)
m_ClipOriginalEdgeValue = m_ItemToBeReplaced.end;
}
else
{
nextItem = sortedClips.FirstOrDefault(c => c != overlapped && (DiscreteTime)c.start >= clipStart && (DiscreteTime)c.end >= clipEnd);
prevItem = sortedClips.LastOrDefault(c => c != overlapped && (DiscreteTime)c.start <= clipStart && (DiscreteTime)c.end <= clipEnd);
if (prevItem != null)
m_Min = prevItem.end;
if (nextItem != null)
m_Max = nextItem.end - EditModeUtils.BlendDuration(nextItem, TrimEdge.End) - TimelineClip.kMinDuration;
m_ItemToBeReplaced = nextItem;
if (m_ItemToBeReplaced != null)
m_ClipOriginalEdgeValue = m_ItemToBeReplaced.start;
}
m_TrimReplace = false;
}
public void TrimStart(ITrimmable item, double time, bool affectTimeScale)
{
time = Math.Min(Math.Max(time, m_Min), m_Max);
if (m_ItemToBeReplaced != null)
{
if (!m_TrimReplace)
m_TrimReplace = item.start >= m_ItemToBeReplaced.end;
}
time = Math.Max(time, 0.0);
item.SetStart(time, affectTimeScale);
if (m_ItemToBeReplaced != null && m_TrimReplace)
{
var prevEnd = Math.Min(item.start, m_ClipOriginalEdgeValue);
m_ItemToBeReplaced.SetEnd(prevEnd, false);
}
}
public void TrimEnd(ITrimmable item, double time, bool affectTimeScale)
{
time = Math.Min(Math.Max(time, m_Min), m_Max);
if (m_ItemToBeReplaced != null)
{
if (!m_TrimReplace)
m_TrimReplace = item.end <= m_ItemToBeReplaced.start;
}
item.SetEnd(time, affectTimeScale);
if (m_ItemToBeReplaced != null && m_TrimReplace)
{
var nextStart = Math.Max(item.end, m_ClipOriginalEdgeValue);
m_ItemToBeReplaced.SetStart(nextStart, false);
}
}
public void DrawGUI(WindowState state, Rect bounds, Color color, TrimEdge edge)
{
bool shouldDraw = m_ItemToBeReplaced != null && (edge == TrimEdge.End && m_Item.end > m_ClipOriginalEdgeValue) ||
(edge == TrimEdge.Start && m_Item.start < m_ClipOriginalEdgeValue);
if (shouldDraw)
{
var cursorType = TimelineCursors.CursorType.Replace;
if (EditModeUtils.HasBlends(m_Item, edge))
{
color = DirectorStyles.kMixToolColor;
cursorType = (edge == TrimEdge.End)
? TimelineCursors.CursorType.MixRight
: TimelineCursors.CursorType.MixLeft;
}
EditModeGUIUtils.DrawBoundsEdge(bounds, color, edge);
TimelineCursors.SetCursor(cursorType);
}
else
{
TimelineCursors.ClearCursor();
}
}
}
}

View File

@@ -0,0 +1,97 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TrimItemModeRipple : ITrimItemMode, ITrimItemDrawer
{
double m_OriginalClipStart;
double m_OriginalClipEnd;
ITrimmable[] m_NextItems;
double m_BlendDuration;
double m_TrimStartShift;
public void OnBeforeTrim(ITrimmable item, TrimEdge trimDirection)
{
m_OriginalClipStart = item.start;
m_OriginalClipEnd = item.end;
m_TrimStartShift = 0.0;
var sortedClips = ItemsUtils.GetItemsExcept(item.parentTrack, new[] { item })
.OfType<ITrimmable>()
.OrderBy(c => c.start);
var clipStart = (DiscreteTime)item.start;
var clipEnd = (DiscreteTime)item.end;
m_NextItems = sortedClips.Where(c => (DiscreteTime)c.start >= clipStart && (DiscreteTime)c.end >= clipEnd).ToArray();
var overlapped = sortedClips.LastOrDefault(c => (DiscreteTime)c.start == clipStart && (DiscreteTime)c.end == clipEnd);
if (overlapped != null)
{
m_BlendDuration = overlapped.end - overlapped.start;
}
else
{
m_BlendDuration = 0.0;
var prevClip = sortedClips.LastOrDefault(c => (DiscreteTime)c.start <= clipStart && (DiscreteTime)c.end <= clipEnd);
if (prevClip != null)
m_BlendDuration += Math.Max(prevClip.end - item.start, 0.0);
var nextClip = sortedClips.FirstOrDefault(c => (DiscreteTime)c.start >= clipStart && (DiscreteTime)c.end >= clipEnd);
if (nextClip != null)
m_BlendDuration += Math.Max(item.end - nextClip.start, 0.0);
}
}
public void TrimStart(ITrimmable item, double time, bool affectTimeScale)
{
var prevEnd = item.end;
// HACK If time is negative, make sure we shift the SetStart operation to a positive space.
if (time < 0.0)
m_TrimStartShift = Math.Max(-time, m_TrimStartShift);
item.start = m_OriginalClipEnd - item.duration + m_TrimStartShift;
time += m_TrimStartShift;
if (m_BlendDuration > 0.0)
time = Math.Min(time, item.end - m_BlendDuration);
item.SetStart(time, affectTimeScale);
item.start = m_OriginalClipStart;
var offset = item.end - prevEnd;
foreach (var timelineClip in m_NextItems)
timelineClip.start += offset;
}
public void TrimEnd(ITrimmable item, double time, bool affectTimeScale)
{
var prevEnd = item.end;
if (m_BlendDuration > 0.0)
time = Math.Max(time, item.start + m_BlendDuration);
item.SetEnd(time, affectTimeScale);
var offset = item.end - prevEnd;
foreach (var timelineClip in m_NextItems)
timelineClip.start += offset;
}
public void DrawGUI(WindowState state, Rect bounds, Color color, TrimEdge edge)
{
EditModeGUIUtils.DrawBoundsEdge(bounds, color, edge);
TimelineCursors.SetCursor(TimelineCursors.CursorType.Ripple);
}
}
}

View File

@@ -0,0 +1,27 @@
using UnityEngine;
namespace UnityEditor.Timeline
{
static class EditModeGUIUtils
{
public static void DrawBoundsEdge(Rect bounds, Color color, TrimEdge edge, float width = 4.0f)
{
var r = bounds;
r.yMin += 2.0f;
r.yMax -= 2.0f;
r.width = width;
r.x = edge == TrimEdge.End ? bounds.xMax : bounds.xMin - width;
EditorGUI.DrawRect(r, color);
}
public static void DrawOverlayRect(Rect bounds, Color overlayColor)
{
var c = overlayColor;
c.a = 0.2f;
EditorGUI.DrawRect(bounds, c);
EditorGUI.DrawOutline(bounds, 1.0f, new Color(1.0f, 1.0f, 1.0f, 0.5f));
}
}
}

View File

@@ -0,0 +1,138 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class EditModeMixUtils
{
static readonly List<PlacementValidity> k_UnrecoverablePlacements = new List<PlacementValidity>
{
PlacementValidity.InvalidIsWithin,
PlacementValidity.InvalidStartsInBlend,
PlacementValidity.InvalidContainsBlend,
PlacementValidity.InvalidOverlapWithNonBlendableClip
};
public static bool CanInsert(IEnumerable<ItemsPerTrack> itemsGroups)
{
foreach (var itemsGroup in itemsGroups)
{
List<ITimelineItem> siblings = ItemsUtils.GetItemsExcept(itemsGroup.targetTrack, itemsGroup.items).ToList();
foreach (var item in itemsGroup.items)
{
var placementValidity = GetPlacementValidity(item, siblings);
if (k_UnrecoverablePlacements.Contains(placementValidity))
{
return false;
}
}
}
return true;
}
//Corrects clips durations to fit at insertion point, if needed
public static void PrepareItemsForInsertion(IEnumerable<ItemsPerTrack> itemsGroups)
{
foreach (var itemsGroup in itemsGroups)
{
var siblings = ItemsUtils.GetItemsExcept(itemsGroup.targetTrack, itemsGroup.items);
foreach (var item in itemsGroup.items.OfType<ITrimmable>())
{
var eatenItems = siblings.Where(c => EditModeUtils.IsItemWithinRange(c, item.start, item.end)).ToList();
var intersectedItem = EditModeUtils.GetFirstIntersectedItem(siblings, item.end);
if (intersectedItem != null)
eatenItems.Add(intersectedItem);
var blendableItems = eatenItems.OfType<IBlendable>();
if (blendableItems.Any())
{
var minTime = blendableItems.Min(c => c.end - c.rightBlendDuration);
if (item.end > minTime)
item.SetEnd(minTime, false);
}
}
}
}
public static PlacementValidity GetPlacementValidity(ITimelineItem item, IEnumerable<ITimelineItem> otherItems)
{
if (item.duration <= 0.0)
return PlacementValidity.Valid; //items without any duration can always be placed
var sortedItems = otherItems.Where(i => i.duration > 0.0).OrderBy(c => c.start);
var candidates = new List<ITimelineItem>();
foreach (var sortedItem in sortedItems)
{
if ((DiscreteTime)sortedItem.start >= (DiscreteTime)item.end)
{
// No need to process further
break;
}
if ((DiscreteTime)sortedItem.end <= (DiscreteTime)item.start)
{
// Skip
continue;
}
candidates.Add(sortedItem);
}
var discreteStart = (DiscreteTime)item.start;
var discreteEnd = (DiscreteTime)item.end;
// Note: Order of tests matters
for (int i = 0, n = candidates.Count; i < n; i++)
{
var candidate = candidates[i];
var blendItem = item as IBlendable;
if (blendItem != null && blendItem.supportsBlending)
{
if (EditModeUtils.Contains(candidate.start, candidate.end, item))
return PlacementValidity.InvalidIsWithin;
if (i < n - 1)
{
var nextCandidate = candidates[i + 1];
var discreteNextCandidateStart = (DiscreteTime)nextCandidate.start;
var discreteCandidateEnd = (DiscreteTime)candidate.end;
if (discreteCandidateEnd > discreteNextCandidateStart)
{
if (discreteStart >= discreteNextCandidateStart)
{
// Note: In case the placement is fully within a blend,
// InvalidStartsInBlend MUST have priority
return PlacementValidity.InvalidStartsInBlend;
}
if (discreteEnd > discreteNextCandidateStart && discreteEnd <= discreteCandidateEnd)
return PlacementValidity.InvalidEndsInBlend;
if (discreteStart < discreteNextCandidateStart && discreteEnd > discreteCandidateEnd)
return PlacementValidity.InvalidContainsBlend;
}
}
if (EditModeUtils.Contains(item.start, item.end, candidate))
return PlacementValidity.InvalidContains;
}
else
{
if (EditModeUtils.Overlaps(item, candidate.start, candidate.end)
|| EditModeUtils.Overlaps(candidate, item.start, item.end))
return PlacementValidity.InvalidOverlapWithNonBlendableClip;
}
}
return PlacementValidity.Valid;
}
}
}

Some files were not shown because too many files have changed in this diff Show More