using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Threading; using SingularityGroup.HotReload.DTO; using SingularityGroup.HotReload.Editor.Cli; using SingularityGroup.HotReload.Editor.Semver; using UnityEditor; using UnityEditor.Compilation; using UnityEngine; [assembly: InternalsVisibleTo("SingularityGroup.HotReload.EditorSamples")] namespace SingularityGroup.HotReload.Editor { class HotReloadWindow : EditorWindow { public static HotReloadWindow Current { get; private set; } List tabs; List Tabs => tabs ?? (tabs = new List { RunTab, SettingsTab, AboutTab, }); int selectedTab; internal static Vector2 scrollPos; static Timer timer; HotReloadRunTab runTab; internal HotReloadRunTab RunTab => runTab ?? (runTab = new HotReloadRunTab(this)); HotReloadSettingsTab settingsTab; internal HotReloadSettingsTab SettingsTab => settingsTab ?? (settingsTab = new HotReloadSettingsTab(this)); HotReloadAboutTab aboutTab; internal HotReloadAboutTab AboutTab => aboutTab ?? (aboutTab = new HotReloadAboutTab(this)); static ShowOnStartupEnum _showOnStartupOption; /// /// This token is cancelled when the EditorWindow is disabled. /// /// /// Use it for all tasks. /// When token is cancelled, scripts are about to be recompiled and this will cause tasks to fail for weird reasons. /// public CancellationToken cancelToken; CancellationTokenSource cancelTokenSource; static readonly PackageUpdateChecker packageUpdateChecker = new PackageUpdateChecker(); [MenuItem("Window/Hot Reload/Open &#H")] internal static void Open() { // opening the window on CI systems was keeping Unity open indefinitely if (EditorWindowHelper.IsHumanControllingUs()) { if (Current) { Current.Show(); Current.Focus(); } else { Current = GetWindow(); } } } [MenuItem("Window/Hot Reload/Recompile")] internal static void Recompile() { HotReloadRunTab.Recompile(); } void OnInterval(object o) { HotReloadRunTab.RepaintInstant(); } void OnEnable() { if (timer == null) { timer = new Timer(OnInterval, null, 20 * 1000, 20 * 1000); } Current = this; if (cancelTokenSource != null) { cancelTokenSource.Cancel(); } // Set min size initially so that full UI is visible if (!HotReloadPrefs.OpenedWindowAtLeastOnce) { this.minSize = new Vector2(Constants.RecompileButtonTextHideWidth + 1, Constants.EventsListHideHeight + 70); HotReloadPrefs.OpenedWindowAtLeastOnce = true; } cancelTokenSource = new CancellationTokenSource(); cancelToken = cancelTokenSource.Token; this.titleContent = new GUIContent(" Hot Reload", GUIHelper.GetInvertibleIcon(InvertibleIcon.Logo)); _showOnStartupOption = HotReloadPrefs.ShowOnStartup; packageUpdateChecker.StartCheckingForNewVersion(); } void Update() { foreach (var tab in Tabs) { tab.Update(); } } void OnDisable() { if (cancelTokenSource != null) { cancelTokenSource.Cancel(); cancelTokenSource = null; } if (Current == this) { Current = null; } timer.Dispose(); timer = null; } internal void SelectTab(Type tabType) { selectedTab = Tabs.FindIndex(x => x.GetType() == tabType); } public HotReloadRunTabState RunTabState { get; private set; } void OnGUI() { // TabState ensures rendering is consistent between Layout and Repaint calls // Without it errors like this happen: // ArgumentException: Getting control 2's position in a group with only 2 controls when doing repaint // See thread for more context: https://answers.unity.com/questions/17718/argumentexception-getting-control-2s-position-in-a.html if (Event.current.type == EventType.Layout) { RunTabState = HotReloadRunTabState.Current; } using(var scope = new EditorGUILayout.ScrollViewScope(scrollPos, false, false)) { scrollPos = scope.scrollPosition; // RenderDebug(); RenderTabs(); } GUILayout.FlexibleSpace(); // GUI below will be rendered on the bottom if (HotReloadWindowStyles.windowScreenHeight > 90) RenderBottomBar(); } void RenderDebug() { if (GUILayout.Button("RESET WINDOW")) { OnDisable(); RequestHelper.RequestLogin("test", "test", 1).Forget(); HotReloadPrefs.LicenseEmail = null; HotReloadPrefs.ExposeServerToLocalNetwork = true; HotReloadPrefs.LicensePassword = null; HotReloadPrefs.LoggedBurstHint = false; HotReloadPrefs.DontShowPromptForDownload = false; HotReloadPrefs.RateAppShown = false; HotReloadPrefs.ActiveDays = string.Empty; HotReloadPrefs.LaunchOnEditorStart = false; HotReloadPrefs.ShowUnsupportedChanges = true; HotReloadPrefs.RedeemLicenseEmail = null; HotReloadPrefs.RedeemLicenseInvoice = null; OnEnable(); File.Delete(EditorCodePatcher.serverDownloader.GetExecutablePath(HotReloadCli.controller)); InstallUtility.DebugClearInstallState(); InstallUtility.CheckForNewInstall(); EditorPrefs.DeleteKey(Attribution.LastLoginKey); File.Delete(RedeemLicenseHelper.registerOutcomePath); CompileMethodDetourer.Reset(); AssetDatabase.Refresh(); } } internal static void RenderLogo(int width = 243) { var isDarkMode = HotReloadWindowStyles.IsDarkMode; var tex = Resources.Load(isDarkMode ? "Logo_HotReload_DarkMode" : "Logo_HotReload_LightMode"); //Can happen during player builds where Editor Resources are unavailable if(tex == null) { return; } var targetWidth = width; var targetHeight = 44; GUILayout.Space(4f); // background padding top and bottom float padding = 5f; // reserve layout space for the texture var backgroundRect = GUILayoutUtility.GetRect(targetWidth + padding, targetHeight + padding, HotReloadWindowStyles.LogoStyle); // draw the texture into that reserved space. First the bg then the logo. if (isDarkMode) { GUI.DrawTexture(backgroundRect, EditorTextures.DarkGray17, ScaleMode.StretchToFill); } else { GUI.DrawTexture(backgroundRect, EditorTextures.LightGray238, ScaleMode.StretchToFill); } var foregroundRect = backgroundRect; foregroundRect.yMin += padding; foregroundRect.yMax -= padding; // during player build (EditorWindow still visible), Resources.Load returns null if (tex) { GUI.DrawTexture(foregroundRect, tex, ScaleMode.ScaleToFit); } } int? collapsedTab; void RenderTabs() { using(new EditorGUILayout.VerticalScope(HotReloadWindowStyles.BoxStyle)) { if (HotReloadWindowStyles.windowScreenHeight > 210 && HotReloadWindowStyles.windowScreenWidth > 375) { selectedTab = GUILayout.Toolbar( selectedTab, Tabs.Select(t => new GUIContent(t.Title.StartsWith(" ", StringComparison.Ordinal) ? t.Title : " " + t.Title, t.Icon, t.Tooltip)).ToArray(), GUILayout.Height(22f) // required, otherwise largest icon height determines toolbar height ); if (collapsedTab != null) { selectedTab = collapsedTab.Value; collapsedTab = null; } } else { if (collapsedTab == null) { collapsedTab = selectedTab; } // When window is super small, we pretty much can only show run tab SelectTab(typeof(HotReloadRunTab)); } if (HotReloadWindowStyles.windowScreenHeight > 250 && HotReloadWindowStyles.windowScreenWidth > 275) { RenderLogo(); } Tabs[selectedTab].OnGUI(); } } void RenderBottomBar() { SemVersion newVersion; var updateAvailable = packageUpdateChecker.TryGetNewVersion(out newVersion); if (HotReloadWindowStyles.windowScreenWidth > Constants.RateAppHideWidth && HotReloadWindowStyles.windowScreenHeight > Constants.RateAppHideHeight ) { RenderRateApp(); } if (updateAvailable) { RenderUpdateButton(newVersion); } using(new EditorGUILayout.HorizontalScope("ProjectBrowserBottomBarBg", GUILayout.ExpandWidth(true), GUILayout.Height(25f))) { RenderBottomBarCore(); } } static GUIStyle _renderAppBoxStyle; static GUIStyle renderAppBoxStyle => _renderAppBoxStyle ?? (_renderAppBoxStyle = new GUIStyle(GUI.skin.box) { padding = new RectOffset(10, 10, 0, 0) }); static GUILayoutOption[] _nonExpandable; public static GUILayoutOption[] NonExpandableLayout => _nonExpandable ?? (_nonExpandable = new [] {GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(true)}); internal static void RenderRateApp() { if (!ShouldShowRateApp()) { return; } using (new EditorGUILayout.VerticalScope(renderAppBoxStyle)) { using (new EditorGUILayout.HorizontalScope()) { HotReloadGUIHelper.HelpBox("Are you enjoying using Hot Reload?", MessageType.Info, 11); if (GUILayout.Button("Hide", NonExpandableLayout)) { RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.RateApp), new EditorExtraData { { "dismissed", true } }).Forget(); HotReloadPrefs.RateAppShown = true; } } using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Yes")) { var openedUrl = PackageConst.IsAssetStoreBuild && EditorUtility.DisplayDialog("Rate Hot Reload", "Thank you for using Hot Reload!\n\nPlease consider leaving a review on the Asset Store to support us.", "Open in browser", "Cancel"); if (openedUrl) { Application.OpenURL(Constants.UnityStoreRateAppURL); } HotReloadPrefs.RateAppShown = true; var data = new EditorExtraData(); if (PackageConst.IsAssetStoreBuild) { data.Add("opened_url", openedUrl); } data.Add("enjoy_app", true); RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.RateApp), data).Forget(); } if (GUILayout.Button("No")) { HotReloadPrefs.RateAppShown = true; var data = new EditorExtraData(); data.Add("enjoy_app", false); RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.RateApp), data).Forget(); } } } } internal static bool ShouldShowRateApp() { if (HotReloadPrefs.RateAppShown) { return false; } var activeDays = EditorCodePatcher.GetActiveDaysForRateApp(); if (activeDays.Count < Constants.DaysToRateApp) { return false; } return true; } void RenderUpdateButton(SemVersion newVersion) { if (GUILayout.Button($"Update To v{newVersion}", HotReloadWindowStyles.UpgradeButtonStyle)) { packageUpdateChecker.UpdatePackageAsync(newVersion).Forget(CancellationToken.None); } } internal static void RenderShowOnStartup() { var prevLabelWidth = EditorGUIUtility.labelWidth; try { EditorGUIUtility.labelWidth = 105f; using (new GUILayout.VerticalScope()) { using (new GUILayout.HorizontalScope()) { GUILayout.Label("Show On Startup"); Rect buttonRect = GUILayoutUtility.GetLastRect(); if (EditorGUILayout.DropdownButton(new GUIContent(Regex.Replace(_showOnStartupOption.ToString(), "([a-z])([A-Z])", "$1 $2")), FocusType.Passive, GUILayout.Width(110f))) { GenericMenu menu = new GenericMenu(); foreach (ShowOnStartupEnum option in Enum.GetValues(typeof(ShowOnStartupEnum))) { menu.AddItem(new GUIContent(Regex.Replace(option.ToString(), "([a-z])([A-Z])", "$1 $2")), false, () => { if (_showOnStartupOption != option) { _showOnStartupOption = option; HotReloadPrefs.ShowOnStartup = _showOnStartupOption; } }); } menu.DropDown(new Rect(buttonRect.x, buttonRect.y, 100, 0)); } } } } finally { EditorGUIUtility.labelWidth = prevLabelWidth; } } internal static readonly OpenURLButton autoRefreshTroubleshootingBtn = new OpenURLButton("Troubleshooting", Constants.TroubleshootingURL); void RenderBottomBarCore() { bool troubleshootingShown = EditorCodePatcher.Started && HotReloadWindowStyles.windowScreenWidth >= 400; bool alertsShown = EditorCodePatcher.Started && HotReloadWindowStyles.windowScreenWidth > Constants.EventFiltersShownHideWidth; using (new EditorGUILayout.VerticalScope()) { using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.FooterStyle)) { if (!troubleshootingShown) { GUILayout.FlexibleSpace(); if (alertsShown) { GUILayout.Space(-20); } } else { GUILayout.Space(21); } GUILayout.Space(0); var lastRect = GUILayoutUtility.GetLastRect(); // show events button when scrolls are hidden if (!HotReloadRunTab.CanRenderBars(RunTabState) && !RunTabState.starting) { using (new EditorGUILayout.VerticalScope()) { GUILayout.FlexibleSpace(); var icon = HotReloadState.ShowingRedDot ? InvertibleIcon.EventsNew : InvertibleIcon.Events; if (GUILayout.Button(new GUIContent("", GUIHelper.GetInvertibleIcon(icon)))) { PopupWindow.Show(new Rect(lastRect.x, lastRect.y, 0, 0), HotReloadEventPopup.I); } GUILayout.FlexibleSpace(); } GUILayout.Space(3f); } if (alertsShown) { using (new EditorGUILayout.VerticalScope()) { GUILayout.FlexibleSpace(); HotReloadTimelineHelper.RenderAlertFilters(); GUILayout.FlexibleSpace(); } } GUILayout.FlexibleSpace(); if (troubleshootingShown) { using (new EditorGUILayout.VerticalScope()) { GUILayout.FlexibleSpace(); autoRefreshTroubleshootingBtn.OnGUI(); GUILayout.FlexibleSpace(); } GUILayout.Space(21); } } } } } }