ProjectDDD/Packages/com.singularitygroup.hotreload/Editor/Window/HotReloadWindow.cs
2025-07-08 19:46:31 +09:00

388 lines
17 KiB
C#

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<HotReloadTabBase> tabs;
List<HotReloadTabBase> Tabs => tabs ?? (tabs = new List<HotReloadTabBase> {
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;
/// <summary>
/// This token is cancelled when the EditorWindow is disabled.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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<HotReloadWindow>();
}
}
}
[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<Texture>(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);
}
}
}
}
}
}