using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor.PackageManager.Requests; using UnityEditor.Presets; using UnityEngine; using UnityEditor.Recorder.Input; using UnityObject = UnityEngine.Object; using UnityEditor.PackageManager; using UnityEngine.UIElements; namespace UnityEditor.Recorder { /// /// Main window class of the Recorder. /// It can be accessed from an Editor script to show the Recorder Window and eventually Start and Stop the recording using current settings. /// Recorder settings are saved in Library/Recorder/recorder.pref /// public class RecorderWindow : EditorWindow { static readonly string s_WindowTitle = "Recorder"; /// /// The location in the Unity Editor menu for the Recorder Window. /// public const string MenuRoot = "Window/General/Recorder/"; /// /// The priority of the Recorder Window entries in the menu. /// public const int MenuRootIndex = 1000; private const string k_DefaultPackageVersion = "1.0.0-preview.1"; private static bool s_PackageVersionInitialized = false; private static string s_PackageVersionNumber = k_DefaultPackageVersion; private static string s_PackageShortVersionNumber = "1.0"; private static ListRequest s_Request; private static bool s_NeedToResetView = false; private static bool HasFocus() { return (focusedWindow != null && focusedWindow.GetType() == typeof(RecorderWindow)); } [MenuItem(MenuRoot + "Recorder Window", false, MenuRootIndex)] static void ToggleRecorderWindow() { if (HasFocus()) { var recorderWindow = GetWindow(typeof(RecorderWindow), false, s_WindowTitle, false); recorderWindow.Close(); return; } GetWindow(typeof(RecorderWindow), false, s_WindowTitle); } [MenuItem(MenuRoot + "Quick Recording _F10", false, MenuRootIndex + 1)] static void QuickRecording() { var recorderWindow = (RecorderWindow)GetWindow(typeof(RecorderWindow), false, s_WindowTitle, false); if (recorderWindow.m_State == State.Error || recorderWindow.m_State == State.AccumulationErrors) { Debug.LogError($"There are errors in your Recorder Window. Please address them before launching a quick recording."); return; } if (!recorderWindow.IsRecording()) { recorderWindow.StartRecording(); } else { recorderWindow.StopRecording(); } } [MenuItem(MenuRoot + "Quick Recording _F10", true, MenuRootIndex + 1)] static bool QuickRecordingValidate() { return !EditorUtility.scriptCompilationFailed; } static void Progress() { if (s_NeedToResetView) { var windows = Resources.FindObjectsOfTypeAll(); if (windows != null && windows.Length > 0) { RecorderWindow win = windows[0]; win.ClearView(); win.UnregisterCallbacks(); win.CreateView(); win.RegisterCallbacks(); } s_NeedToResetView = false; return; } if (s_Request.IsCompleted) { if (s_Request.Status == StatusCode.Success) { var result = s_Request.Result; foreach (var package in s_Request.Result) { var name = package.name; if (name.Contains("com.unity.recorder")) { var newPackageVersion = package.version; s_PackageShortVersionNumber = newPackageVersion.Substring(0, 3); s_PackageVersionInitialized = true; if (newPackageVersion != s_PackageVersionNumber && s_PackageVersionNumber != k_DefaultPackageVersion) { // Reset the view when we detect a version upgrade but not the default that gets triggered at startup s_NeedToResetView = true; } s_PackageVersionNumber = newPackageVersion; return; } } } else if (s_Request.Status >= StatusCode.Failure) Debug.Log(s_Request.Error.message); EditorApplication.update -= Progress; } } static class Styles { internal static readonly GUIContent ExitPlayModeLabel = new GUIContent("Exit Play Mode", "To exit the Play mode when the recording stops."); internal static readonly GUIContent DuplicateLabel = new GUIContent("Duplicate"); internal static readonly GUIContent DeleteLabel = new GUIContent("Delete"); internal static readonly GUIContent SaveRecorderListLabel = new GUIContent("Save Recorder List"); internal static readonly GUIContent LoadRecorderListLabel = new GUIContent("Load Recorder List"); internal static readonly GUIContent ClearRecorderListLabel = new GUIContent("Clear Recorder List"); internal static string GetLinkToManual(string shortVersion) { return $"http://docs.unity3d.com/Packages/com.unity.recorder@{shortVersion}/index.html"; } internal static readonly GUIContent WaitingForPlayModeLabel = new GUIContent("Waiting for Play Mode to start..."); } static Hashtable s_IconGUIContents = new Hashtable(); internal static GUIContent GetIconAndTooltip(string iconName, string tooltip = null) { string key = string.Format("{0}|{1}", iconName, EditorGUIUtility.pixelsPerPoint); GUIContent gc = (GUIContent)s_IconGUIContents[key]; if (gc != null) { return gc; } gc = new GUIContent(); if (tooltip != null) { gc.tooltip = L10n.Tr(tooltip); } string iconForSkinName = EditorGUIUtility.pixelsPerPoint > 1.0f ? iconName + "@2x" : iconName; iconForSkinName = EditorGUIUtility.isProSkin ? "d_" + iconForSkinName : iconForSkinName; gc.image = (Texture)EditorGUIUtility.Load(iconForSkinName); s_IconGUIContents[key] = gc; return gc; } class RecorderItemList : VisualListItem { } RecorderItemList m_RecordingListItem; VisualElement m_SettingsPanel; VisualElement m_RecordersPanel; RecorderItem m_SelectedRecorderItem; VisualElement m_ParametersControl; VisualElement m_RecorderSettingPanel; Button m_RecordButton; Button m_RecordButtonIcon; PanelSplitter m_PanelSplitter; VisualElement m_AddNewRecordPanel; VisualElement m_RecordModeOptionsPanel; VisualElement m_FrameRateOptionsPanel; RecorderControllerSettings m_ControllerSettings; RecorderController m_RecorderController; enum State { Idle, WaitingForPlayModeToStartRecording, WaitingForScenesData, Error, AccumulationErrors, Recording } State m_State = State.Idle; int m_FrameCount = 0; RecorderSettingsPrefsEditor m_RecorderSettingsPrefsEditor; /// /// Set the RecorderWindow controller settings and update the UI. This allow to set settings even if /// the window is open. /// /// The new to set. public void SetRecorderControllerSettings(RecorderControllerSettings settings) { m_ControllerSettings = settings; m_RecorderController = new RecorderController(settings); m_RecorderSettingsPrefsEditor = (RecorderSettingsPrefsEditor)Editor.CreateEditor(m_ControllerSettings); ReloadRecordings(); } internal RecorderControllerSettings GetRecorderControllerSettings() { return m_ControllerSettings; } void ClearView() { var root = rootVisualElement; root.Clear(); } void RegisterCallbacks() { Undo.undoRedoPerformed += OnUndoRedoPerformed; EditorApplication.playModeStateChanged += OnPlayModeStateChanged; EditorApplication.update += UpdateInternal; } void UnregisterCallbacks() { Undo.undoRedoPerformed -= OnUndoRedoPerformed; EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; EditorApplication.update -= UpdateInternal; } void OnUndoRedoPerformed() { ReloadRecordings(); SaveAndRepaint(); } void CreateView() { minSize = new Vector2(560.0f, 200.0f); var pathPrefix1 = $"Styles/recorder.uss"; var lightOrDark = EditorGUIUtility.isProSkin ? "recorder_darkSkin" : "recorder_lightSkin"; var pathPrefix2 = $"Styles/{lightOrDark}.uss"; var root = rootVisualElement; var sheet1 = UnityHelpers.LoadLocalPackageAsset(pathPrefix1, false); var sheet2 = UnityHelpers.LoadLocalPackageAsset(pathPrefix2, false); bool sheetNotFound = sheet1 == null || sheet2 == null; if (sheetNotFound) { s_NeedToResetView = true; return; } root.styleSheets.Add(sheet1); root.styleSheets.Add(sheet2); root.style.flexDirection = FlexDirection.Column; UIElementHelper.SetFocusable(root); var mainControls = new VisualElement { style = { flexDirection = FlexDirection.Row, minHeight = 120.0f } }; root.Add(mainControls); var controlLeftPane = new VisualElement { style = { minWidth = 180.0f, maxWidth = 450.0f, flexDirection = FlexDirection.Row, } }; UIElementHelper.SetFlex(controlLeftPane, 0.5f); var controlRightPane = new VisualElement { style = { flexDirection = FlexDirection.Column, } }; UIElementHelper.SetFlex(controlRightPane, 0.5f); mainControls.Add(controlLeftPane); mainControls.Add(controlRightPane); controlLeftPane.AddToClassList("StandardPanel"); controlRightPane.AddToClassList("StandardPanel"); m_RecordButtonIcon = new Button(OnRecordButtonClick) { name = "recorderIcon", style = { backgroundImage = UnityHelpers.LoadLocalPackageAsset("recorder_icon.png", true), }, tooltip = "Start the recording for all active recorders of the list\n\n This automatically activates the Play mode first (if not activated yet)." }; controlLeftPane.Add(m_RecordButtonIcon); var leftButtonsStack = new VisualElement { style = { flexDirection = FlexDirection.Column, } }; UIElementHelper.SetFlex(leftButtonsStack, 1.0f); m_RecordButton = new Button(OnRecordButtonClick) { name = "recordButton", tooltip = "Start/Stop the recording for all active recorders of the list\n\nStarting the recording automatically activates the Play mode first (if not activated yet)." }; UpdateRecordButtonText(); leftButtonsStack.Add(m_RecordButton); m_RecordModeOptionsPanel = new IMGUIContainer(() => { PrepareGUIState(m_RecordModeOptionsPanel.layout.width); if (m_RecorderSettingsPrefsEditor.RecordModeGUI()) OnGlobalSettingsChanged(); }); UIElementHelper.SetFlex(m_RecordModeOptionsPanel, 1.0f); leftButtonsStack.Add(m_RecordModeOptionsPanel); controlLeftPane.Add(leftButtonsStack); m_FrameRateOptionsPanel = new IMGUIContainer(() => { PrepareGUIState(m_FrameRateOptionsPanel.layout.width); if (m_RecorderSettingsPrefsEditor.FrameRateGUI()) OnGlobalSettingsChanged(); }); UIElementHelper.SetFlex(m_FrameRateOptionsPanel, 1.0f); controlRightPane.Add(m_FrameRateOptionsPanel); m_SettingsPanel = new ScrollView(); m_SettingsPanel.name = "SettingPanelScrollView"; UIElementHelper.SetFlex(m_SettingsPanel, 1.0f); UIElementHelper.ResetStylePosition(m_SettingsPanel.contentContainer.style); var recordersAndParameters = new VisualElement { style = { alignSelf = Align.Stretch, flexDirection = FlexDirection.Row, } }; UIElementHelper.SetFlex(recordersAndParameters, 1.0f); m_RecordersPanel = new VisualElement { name = "recordersPanel", style = { width = 200.0f, minWidth = 150.0f, maxWidth = 500.0f } }; m_RecordersPanel.AddToClassList("StandardPanel"); m_PanelSplitter = new PanelSplitter(m_RecordersPanel); recordersAndParameters.Add(m_RecordersPanel); recordersAndParameters.Add(m_PanelSplitter); recordersAndParameters.Add(m_SettingsPanel); m_SettingsPanel.AddToClassList("StandardPanel"); root.Add(recordersAndParameters); var addRecordButton = new Label("+ Add Recorder"); UIElementHelper.SetFlex(addRecordButton, 1.0f); var recorderListPresetButton = new VisualElement { name = "recorderListPreset", tooltip = "Manage the recorder list" }; recorderListPresetButton.RegisterCallback(evt => ShowRecorderListMenu()); recorderListPresetButton.Add(new Image { image = (Texture2D)EditorGUIUtility.Load("Builtin Skins/" + (EditorGUIUtility.isProSkin ? "DarkSkin" : "LightSkin") + "/Images/pane options.png"), style = { width = 16.0f, height = 16.0f } }); addRecordButton.AddToClassList("RecorderListHeader"); recorderListPresetButton.AddToClassList("RecorderListHeader"); addRecordButton.RegisterCallback(evt => ShowNewRecorderMenu()); m_AddNewRecordPanel = new VisualElement { name = "addRecordersButton", style = { flexDirection = FlexDirection.Row }, tooltip = "Add a new recorder item to the list" }; m_AddNewRecordPanel.Add(addRecordButton); m_AddNewRecordPanel.Add(recorderListPresetButton); m_RecordingListItem = new RecorderItemList { name = "recorderList" }; UIElementHelper.SetFlex(m_RecordingListItem, 1.0f); UIElementHelper.SetFocusable(m_RecordingListItem); m_RecordingListItem.OnItemContextMenu += OnRecordContextMenu; m_RecordingListItem.OnSelectionChanged += OnRecordSelectionChanged; m_RecordingListItem.OnItemRename += item => item.StartRenaming(); m_RecordingListItem.OnContextMenu += ShowNewRecorderMenu; m_RecordersPanel.Add(m_AddNewRecordPanel); m_RecordersPanel.Add(m_RecordingListItem); m_ParametersControl = new VisualElement { style = { minWidth = 300.0f, } }; UIElementHelper.SetFlex(m_ParametersControl, 1.0f); m_RecorderSettingPanel = new IMGUIContainer(OnRecorderSettingsGUI) { name = "recorderSettings" }; UIElementHelper.SetFlex(m_RecorderSettingPanel, 1.0f); var statusBar = new VisualElement { name = "statusBar" }; statusBar.Add(new IMGUIContainer(UpdateRecordingProgressGUI)); root.Add(statusBar); m_ParametersControl.Add(m_RecorderSettingPanel); m_SettingsPanel.Add(m_ParametersControl); m_RecordingListItem.RegisterCallback(OnRecorderListValidateCommand); m_RecordingListItem.RegisterCallback(OnRecorderListExecuteCommand); m_RecordingListItem.RegisterCallback(OnRecorderListKeyUp); SetRecorderControllerSettings(RecorderControllerSettings.GetGlobalSettings()); } void OnEnable() { // Register a callback listener to get the version of the package if (!s_PackageVersionInitialized) { s_Request = Client.List(); // List packages installed for the Project EditorApplication.update += Progress; } CreateView(); RegisterCallbacks(); } /// /// Used to Start the recording with current settings. /// If not already the case, the Editor will also switch to PlayMode. /// public void StartRecording() { if (EditorUtility.scriptCompilationFailed) { return; } if (EditorApplication.isPlaying) { // Already in play mode, so start recording now StartRecordingInternal(); } else if (m_State == State.Idle) { // Not playing yet and idle m_State = State.WaitingForPlayModeToStartRecording; GameViewSize.DisableMaxOnPlay(); EditorApplication.isPlaying = true; m_FrameCount = Time.frameCount; } } /// /// Used to get the current state of the recording. /// /// True if the Recorder is started or being started. False otherwise. public bool IsRecording() { return m_State == State.Recording || m_State == State.WaitingForPlayModeToStartRecording; } /// /// Used to Stop current recordings if any. /// Exiting PlayMode while the Recorder is recording will automatically Stop the recorder. /// public void StopRecording() { if (IsRecording()) StopRecordingInternal(); } [RuntimeInitializeOnLoadMethod] static void RuntimeInit() { var windows = Resources.FindObjectsOfTypeAll(); if (windows != null && windows.Length > 0) { RecorderWindow win = windows[0]; if (win.m_State == State.WaitingForPlayModeToStartRecording) win.RequestStartRecording(); } } void OnPlayModeStateChanged(PlayModeStateChange obj) { if (obj == PlayModeStateChange.ExitingPlayMode) { if (m_State == State.Recording) { StopRecordingInternal(); } else { m_State = State.Idle; } } } void OnGlobalSettingsChanged() { if (m_ControllerSettings == null) return; m_ControllerSettings.ApplyGlobalSettingToAllRecorders(); foreach (var recorderItem in m_RecordingListItem.items) recorderItem.UpdateState(); SaveAndRepaint(); } void SaveAndRepaint() { if (m_ControllerSettings != null) m_ControllerSettings.Save(); if (m_SelectedRecorderItem != null) UIElementHelper.SetDirty(m_RecorderSettingPanel); Repaint(); } void ReloadRecordings() { if (m_ControllerSettings == null) return; m_ControllerSettings.ApplyGlobalSettingToAllRecorders(); var recorderItems = m_ControllerSettings.RecorderSettings.Select(CreateRecorderItem).ToArray(); foreach (var recorderItem in recorderItems) recorderItem.UpdateState(); m_RecordingListItem.Reload(recorderItems); } void UpdateStateOfSelectedItem() { if (m_SelectedRecorderItem != null) m_SelectedRecorderItem.UpdateState(); } void OnRecorderListValidateCommand(ValidateCommandEvent evt) { RecorderListValidateCommand(evt, evt.commandName); } void OnRecorderListExecuteCommand(ExecuteCommandEvent evt) { RecorderListExecuteCommand(evt, evt.commandName); } void RecorderListValidateCommand(EventBase evt, string commandName) { if (m_RecordingListItem == null || m_RecordingListItem.selection == null) return; if (commandName == "Duplicate" || commandName == "SoftDelete" || commandName == "Delete") { evt.StopPropagation(); } } void RecorderListExecuteCommand(EventBase evt, string commandName) { if (m_RecordingListItem == null) return; var item = m_RecordingListItem.selection; if (item == null) return; if (commandName == "Duplicate") { DuplicateRecorder(item); evt.StopPropagation(); } else if (commandName == "SoftDelete" || commandName == "Delete") { DeleteRecorder(item); evt.StopPropagation(); } } void OnRecorderListKeyUp(KeyUpEvent evt) { if (m_RecordingListItem == null) return; if (m_RecordingListItem.items == null || m_RecordingListItem.items.Count == 0) return; if (!m_RecordingListItem.HasFocus()) return; if (evt.keyCode == KeyCode.UpArrow || evt.keyCode == KeyCode.DownArrow) { if (m_RecordingListItem.selection == null) { m_RecordingListItem.selection = m_RecordingListItem.items.First(); } else { var currentIndex = m_RecordingListItem.selectedIndex; var newIndex = ((evt.keyCode == KeyCode.UpArrow ? currentIndex - 1 : currentIndex + 1) + m_RecordingListItem.items.Count) % m_RecordingListItem.items.Count; m_RecordingListItem.selectedIndex = newIndex; } evt.StopPropagation(); } } void ApplyPreset(string presetPath) { var candidate = AssetDatabase.LoadAssetAtPath(presetPath); ApplyPreset(candidate); } /// /// Loads a previously saved Recorder List. /// /// The instance of Recorder List to load. public void ApplyPreset(RecorderControllerSettingsPreset preset) { if (preset == null) return; preset.ApplyTo(m_ControllerSettings); ReloadRecordings(); } void ShowNewRecorderMenu() { var newRecordMenu = new GenericMenu(); foreach (var info in RecordersInventory.builtInRecorderInfos) AddRecorderInfoToMenu(info, newRecordMenu); var recorderList = RecordersInventory.customRecorderInfos.ToList(); if (recorderList.Any()) { newRecordMenu.AddSeparator(string.Empty); foreach (var info in recorderList) AddRecorderInfoToMenu(info, newRecordMenu); } newRecordMenu.ShowAsContext(); } void AddRecorderInfoToMenu(RecorderInfo info, GenericMenu menu) { if (ShouldDisableRecordSettings()) menu.AddDisabledItem(new GUIContent(info.displayName)); else menu.AddItem(new GUIContent(info.displayName), false, data => OnAddNewRecorder((RecorderInfo)data), info); } RecorderItem CreateRecorderItem(RecorderSettings recorderSettings) { var info = RecordersInventory.GetRecorderInfo(recorderSettings.GetType()); var isInvalid = info == null; var recorderItem = new RecorderItem(m_ControllerSettings, recorderSettings, isInvalid ? null : info.iconName); recorderItem.OnEnableStateChanged += enabled => { if (enabled) m_RecordingListItem.selection = recorderItem; }; if (isInvalid) recorderItem.state = RecorderItem.State.Invalid; return recorderItem; } void CheckRecordersIncompatibility() { var activeRecorders = m_ControllerSettings.RecorderSettings.Where(r => r.Enabled).ToArray(); HasDuplicateOutputNames(activeRecorders); HasDifferentResolutions(activeRecorders); // Refresh the state of all items in the window foreach (var recorderItem in m_RecordingListItem.items) recorderItem.UpdateState(); if (m_ControllerSettings.InvalidContextBecauseOfAccumulation()) { ShowMessageInStatusBar("You can only use one active Recorder at a time when you capture accumulation.", MessageType.Error); m_State = State.AccumulationErrors; // Flag all the recorders that support accumulation with a warning (important to perform this after calling UpdateState on all items) foreach (var recorderItem in m_RecordingListItem.items) { if (recorderItem.settings.Enabled) { recorderItem.state = RecorderItem.State.HasErrors; } } return; } if (m_State == State.AccumulationErrors) { // There were accumulation errors but now the context has been fixed. Return to idle state. m_State = State.Idle; } var activeRecordersThatCanRun = activeRecorders.Where(r => r.Enabled && !r.HasErrors()).ToArray(); if (activeRecordersThatCanRun.Length > 0) { ShowMessageInStatusBar("Ready to start recording", MessageType.None); } } bool HasDuplicateOutputNames(RecorderSettings[] activeRecorders) { var duplicateNames = 0; m_RecorderController.ValidateRecorderNames(); foreach (var recorder in activeRecorders) { if (recorder.IsOutputNameDuplicate) duplicateNames++; } if (duplicateNames > 0) { var msg = $"There are {duplicateNames} Recorders that do not have unique output file names."; ShowMessageInStatusBar(msg, MessageType.Error); } return duplicateNames > 0; } void HasDifferentResolutions(RecorderSettings[] activeRecorders) { m_RecorderController.ValidateRecorderResolutions(); var duplicateRes = activeRecorders.OfType() .Count(x => x.IsOutputResolutionContradictory); if (duplicateRes > 0) { var msg = $"There are {duplicateRes} Recorders that use different resolutions."; ShowMessageInStatusBar(msg, MessageType.Error); } } bool ShouldDisableRecordSettings() { return IsRecording(); } void UpdateInternal() { if (!EditorApplication.isPlaying) { if (m_State == State.Recording) { StopRecordingInternal(); } } else if (m_State == State.WaitingForScenesData && UnityHelpers.AreAllSceneDataLoaded()) { StartRecordingInternal(); } var enable = !ShouldDisableRecordSettings(); m_AddNewRecordPanel.SetEnabled(enable); m_ParametersControl.SetEnabled(enable && m_SelectedRecorderItem != null && m_SelectedRecorderItem.state != RecorderItem.State.Invalid); m_RecordModeOptionsPanel.SetEnabled(enable); m_FrameRateOptionsPanel.SetEnabled(enable); if (HaveActiveRecordingsThatCanRun()) { if (IsRecording()) { SetRecordButtonsEnabled(EditorApplication.isPlaying && Time.frameCount - m_FrameCount > 5.0f); } else { SetRecordButtonsEnabled(!EditorUtility.scriptCompilationFailed && !m_ControllerSettings.InvalidContextBecauseOfAccumulation()); } } else { SetRecordButtonsEnabled(false); } UpdateRecordButtonText(); if (m_State == State.Recording) { if (!m_RecorderController.IsRecording()) StopRecordingInternal(); Repaint(); } } void SetRecordButtonsEnabled(bool enabled) { m_RecordButton.SetEnabled(enabled); m_RecordButtonIcon.SetEnabled(enabled); } void StartRecordingInternal() { if (RecorderOptions.VerboseMode) Debug.Log("Start Recording."); try { m_RecorderController.PrepareRecording(); } catch (InvalidOperationException) { StopRecordingInternal(false); m_State = State.AccumulationErrors; return; } var success = m_RecorderController.StartRecording(); RecorderAnalytics.SendStartEvent(m_RecorderController); if (success) { m_State = State.Recording; m_FrameCount = 0; } else { StopRecordingInternal(false); m_State = State.Error; } } void RequestStartRecording() { if (RecorderOptions.VerboseMode) Debug.Log("Prepare and wait all scenes to load."); m_RecorderController.PrepareRecording(); m_State = State.WaitingForScenesData; } void OnRecordButtonClick() { if (m_State == State.Error) m_State = State.Idle; switch (m_State) { case State.Idle: { StartRecording(); break; } case State.WaitingForPlayModeToStartRecording: case State.Recording: { StopRecording(); break; } } UpdateRecordButtonText(); } void UpdateRecordButtonText() { m_RecordButton.text = m_State == State.Recording ? "STOP RECORDING" : "START RECORDING"; } void StopRecordingInternal(bool success = true) { if (RecorderOptions.VerboseMode) Debug.Log("Stop Recording."); RecorderAnalytics.SendStopEvent(m_RecorderController, !success); m_RecorderController.StopRecording(); m_State = State.Idle; m_FrameCount = 0; // Settings might have changed after the session ended m_ControllerSettings.Save(); if (m_RecorderController.Settings.ExitPlayMode) EditorApplication.isPlaying = false; } static void PrepareGUIState(float contextWidth) { EditorGUIUtility.labelWidth = Mathf.Min(Mathf.Max(contextWidth * 0.45f - 40, 100), 160); } void OnRecorderSettingsGUI() { PrepareGUIState(m_RecorderSettingPanel.layout.width); if (m_SelectedRecorderItem != null) { if (m_SelectedRecorderItem.state == RecorderItem.State.Invalid) { EditorGUILayout.LabelField("This Recorder has errors and cannot record any data."); } else { var editor = m_SelectedRecorderItem.editor; if (editor == null) { EditorGUILayout.LabelField("Error while displaying the recorder inspector"); } else { EditorGUIUtility.labelWidth = Mathf.Min(Mathf.Max(m_RecorderSettingPanel.layout.width * 0.45f - 40, 110), 140); EditorGUILayout.Separator(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Recorder Type", ObjectNames.NicifyVariableName(editor.target.GetType().Name)); if (!(editor is RecorderEditor)) EditorGUILayout.LabelField(string.Empty, GUI.skin.horizontalSlider); // Open Manual if (GUILayout.Button(GetIconAndTooltip("_Help", "Open Reference for Recorder"), PresetHelper.presetButtonStyle)) { Application.OpenURL(Styles.GetLinkToManual(s_PackageShortVersionNumber)); } if (GUILayout.Button(PresetHelper.presetIcon, PresetHelper.presetButtonStyle)) { var settings = editor.target as RecorderSettings; if (settings != null) { PresetHelper.ShowPresetSelectorWrapper(settings, null, Repaint, () => { EditorUtility.SetDirty(settings); }); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Separator(); EditorGUI.BeginChangeCheck(); EditorGUILayout.Separator(); var prevValue = RecorderEditor.FromRecorderWindow; RecorderEditor.FromRecorderWindow = true; editor.OnInspectorGUI(); RecorderEditor.FromRecorderWindow = prevValue; if (EditorGUI.EndChangeCheck() || EditorUtility.IsDirty(m_SelectedRecorderItem.settings)) { m_ControllerSettings.Save(); m_SelectedRecorderItem.UpdateState(); UIElementHelper.SetDirty(m_RecorderSettingPanel); } } } } else { EditorGUILayout.LabelField("No recorder selected"); } } void ShowRecorderListMenu() { var menu = new GenericMenu(); menu.AddItem(Styles.SaveRecorderListLabel, false, () => { var path = EditorUtility.SaveFilePanelInProject("Save Preset", "RecorderSettingPreset.asset", "asset", ""); if (path.Length != 0) RecorderControllerSettingsPreset.SaveAtPath(m_ControllerSettings, path); }); var presets = AssetDatabase.FindAssets("t:" + typeof(RecorderControllerSettingsPreset).Name); if (presets.Length > 0) { foreach (var preset in presets) { var path = AssetDatabase.GUIDToAssetPath(preset); var fileName = Path.GetFileNameWithoutExtension(path); menu.AddItem(new GUIContent("Load Recorder List/" + fileName), false, data => { ApplyPreset((string)data); }, path); } } else { menu.AddDisabledItem(Styles.LoadRecorderListLabel); } var items = m_RecordingListItem.items.ToArray(); if (items.Length > 0) { menu.AddItem(Styles.ClearRecorderListLabel, false, () => { Undo.IncrementCurrentGroup(); Undo.SetCurrentGroupName("Clear Recorder List"); var undoGroupIndex = Undo.GetCurrentGroup(); foreach (var item in items) { if (item.editor != null) DestroyImmediate(item.editor); DeleteRecorder(item); } Undo.CollapseUndoOperations(undoGroupIndex); ReloadRecordings(); }); } else { menu.AddDisabledItem(Styles.ClearRecorderListLabel); } menu.ShowAsContext(); } void OnDestroy() { if (IsRecording()) StopRecording(); if (m_ControllerSettings != null) { m_ControllerSettings.Save(); DestroyImmediate(m_ControllerSettings); } if (m_RecorderSettingsPrefsEditor != null) DestroyImmediate(m_RecorderSettingsPrefsEditor); UnregisterCallbacks(); } void AddLastAndSelect(RecorderSettings recorder, string desiredName, bool enabled) { recorder.name = GetUniqueRecorderName(desiredName); recorder.Enabled = enabled; m_ControllerSettings.AddRecorderSettings(recorder); var item = CreateRecorderItem(recorder); m_RecordingListItem.Add(item); m_RecordingListItem.selection = item; m_RecordingListItem.Focus(); } void DuplicateRecorder(RecorderItem item) { var candidate = item.settings; var copy = Instantiate(candidate); copy.OnAfterDuplicate(); AddLastAndSelect(copy, candidate.name, candidate.Enabled); } void DeleteRecorder(RecorderItem item) { var s = item.settings; m_ControllerSettings.RemoveRecorder(s); UnityHelpers.Destroy(item.editor, true); m_RecordingListItem.Remove(item); } void OnAddNewRecorder(RecorderInfo info) { var recorder = RecordersInventory.CreateDefaultRecorderSettings(info.settingsType); AddLastAndSelect(recorder, ObjectNames.NicifyVariableName(info.displayName), true); UIElementHelper.SetDirty(m_RecorderSettingPanel); m_State = State.Idle; } string GetUniqueRecorderName(string desiredName) { return ObjectNames.GetUniqueName(m_ControllerSettings.RecorderSettings.Select(r => r.name).ToArray(), desiredName); } void OnRecordContextMenu(RecorderItem recorder) { var contextMenu = new GenericMenu(); if (ShouldDisableRecordSettings()) { contextMenu.AddDisabledItem(Styles.DuplicateLabel); contextMenu.AddDisabledItem(Styles.DeleteLabel); } else { contextMenu.AddItem(Styles.DuplicateLabel, false, data => { DuplicateRecorder((RecorderItem)data); }, recorder); contextMenu.AddItem(Styles.DeleteLabel, false, data => { DeleteRecorder((RecorderItem)data); }, recorder); } contextMenu.ShowAsContext(); } void OnRecordSelectionChanged() { var previousSelection = m_SelectedRecorderItem; if (previousSelection != null) { // Remove the action trigger var recorderEditor = (RecorderEditor)(previousSelection.editor); recorderEditor.OnRecorderDataHasChaged -= RecorderDataHasChanged; } m_SelectedRecorderItem = m_RecordingListItem.selection; foreach (var r in m_RecordingListItem.items) { r.SetItemSelected(m_SelectedRecorderItem == r); } if (m_SelectedRecorderItem != null) { UIElementHelper.SetDirty(m_RecorderSettingPanel); // Listen to the action trigger var recorderEditor = (RecorderEditor)(m_SelectedRecorderItem.editor); recorderEditor.OnRecorderDataHasChaged += RecorderDataHasChanged; } Repaint(); } void RecorderDataHasChanged() { // Save the controller settings when the data has changed if (m_ControllerSettings != null) m_ControllerSettings.Save(); } bool HaveActiveRecordingsThatCanRun() { return m_ControllerSettings.RecorderSettings.Any(r => r.Enabled && !r.HasErrors()); } static void ShowMessageInStatusBar(string msg, MessageType messageType) { var r = EditorGUILayout.GetControlRect(); if (messageType != MessageType.None) { var iconR = r; iconR.width = iconR.height; var icon = messageType == MessageType.Error ? StatusBarHelper.errorIcon : (messageType == MessageType.Warning ? StatusBarHelper.warningIcon : StatusBarHelper.infoIcon); GUI.DrawTexture(iconR, icon); r.xMin = iconR.xMax + 5.0f; } var style = messageType == MessageType.Error ? StatusBarHelper.errorStyle : (messageType == MessageType.Warning ? StatusBarHelper.warningStyle : StatusBarHelper.infoStyle); GUI.Label(r, msg, style); } void UpdateRecordingProgressGUI() { CheckRecordersIncompatibility(); if (m_State == State.Error) { if (!HaveActiveRecordingsThatCanRun()) { ShowMessageInStatusBar("Unable to start recording because no recorders are active and have no errors.", MessageType.Warning); } else { var activeRecorders = m_ControllerSettings.RecorderSettings.Where(r => r.Enabled).ToArray(); var thereAreWarnings = activeRecorders.Any(r => r.HasWarnings()); var thereAreErrors = activeRecorders.Any(r => r.HasErrors()); if (thereAreWarnings || thereAreErrors) { ShowMessageInStatusBar("Please check the recorders for warnings and errors.", MessageType.Warning); } } return; } if (m_State == State.Idle) { if (!HaveActiveRecordingsThatCanRun()) { ShowMessageInStatusBar("No active recorder with no errors.", MessageType.Info); } return; } if (m_State == State.WaitingForPlayModeToStartRecording) { EditorGUILayout.LabelField(Styles.WaitingForPlayModeLabel); return; } var recordingSessions = m_RecorderController.GetRecordingSessions(); var session = recordingSessions.FirstOrDefault(); // Follow the progress of the first recorder, since they all share the same timing. if (session == null) return; var progressBarRect = EditorGUILayout.GetControlRect(); var settings = session.settings; switch (settings.RecordMode) { case RecordMode.Manual: { var label = string.Format("{0} Frame(s) processed", session.frameIndex); EditorGUI.ProgressBar(progressBarRect, 0, label); break; } case RecordMode.SingleFrame: case RecordMode.FrameInterval: { var expectedFrames = settings.EndFrame - settings.StartFrame; var waitingText = settings.StartFrame == 0 ? "Waiting for the first frame..." // not skipping frames, just waiting for the first frame to be recorded : $"Skipping first {settings.StartFrame} frame(s)..."; // if we start at frame 30, we are skipping frames [0;29] so total 30 frames skipped var label = session.recorder.RecordedFramesCount == 0 ? waitingText : $"{session.recorder.RecordedFramesCount} frame(s) processed"; var progress = (float)session.recorder.RecordedFramesCount / expectedFrames; EditorGUI.ProgressBar(progressBarRect, progress, label); break; } case RecordMode.TimeInterval: { var expectedFrames = (settings.EndTime - settings.StartTime) * settings.FrameRate; var waitingText = settings.StartTime == 0 ? "Waiting for the first frame..." // not skipping frames, just waiting for the first frame to be recorded : $"Skipping first {settings.StartTime} second(s)..."; // really skipping some time var label = session.recorder.RecordedFramesCount == 0 ? waitingText : $"{session.recorder.RecordedFramesCount} frame(s) processed"; var progress = (float)session.recorder.RecordedFramesCount / expectedFrames; EditorGUI.ProgressBar(progressBarRect, progress, label); break; } } } } }