diff --git a/addons/autosaver_editor/AutoSaverEditorPlugin.cs b/addons/autosaver_editor/AutoSaverEditorPlugin.cs new file mode 100644 index 0000000..2cae818 --- /dev/null +++ b/addons/autosaver_editor/AutoSaverEditorPlugin.cs @@ -0,0 +1,84 @@ +#if TOOLS +using System.Text; +using AutoSaverPlugin.UI; +using AutoSaverPlugin.UI.GDComponent; +using AutoSaverPlugin.Contracts; +using AutoSaverPlugin.Shared; +using Godot; +using static AutoSaverPlugin.Shared.CommonUtils; +using System; +using System.Diagnostics; + +[Tool] +public partial class AutoSaverEditorPlugin : EditorPlugin +{ + private IAutoSaveManager _autoSaveManager; + private IConfigurationManager _configManager; + + private PanelSettingsControlNode _panelConfigNode; + private AutoSaveToggleMenuBuilder _menuTopBuilder; + private CheckButton _menuAutoSaveToggle; + + public override string _GetPluginName() => _configManager.PluginFullName; + + + public override Texture2D _GetPluginIcon() + { + return ResourceLoader.Load(_configManager.PluginIConResourcePath); + } + + + public override void _EnterTree() + { + InitializeDependencies(); + _autoSaveManager.Initialize(this); + SetupAutoSaveToggle(); + SetupSettingsPanel(); + } + + public override void _ExitTree() + { + DetachEvents(); + CleanupUI(); + } + + private void InitializeDependencies() + { + ServiceProvider.Initialize(); + + _autoSaveManager = ServiceProvider.GetService(); + _configManager = ServiceProvider.GetService(); + } + + private void SetupSettingsPanel() + { + _panelConfigNode = new PanelSettingsControlNode(); + AddControlToDock(DockSlot.LeftUr, _panelConfigNode); + } + + private void SetupAutoSaveToggle() + { + _menuTopBuilder = new AutoSaveToggleMenuBuilder(); + _menuAutoSaveToggle = _menuTopBuilder.AutoSaveToggleButton; + AddControlToContainer(CustomControlContainer.Toolbar, _menuAutoSaveToggle); + _configManager.AutoSaverStateChanged += _menuTopBuilder.UpdateToggleStateFromSettings; + } + + private void DetachEvents() + { + _menuTopBuilder.DetachAutoSaveToggleEvents(); + _configManager.AutoSaverStateChanged -= _menuTopBuilder.UpdateToggleStateFromSettings; + } + + private void CleanupUI() + { + _autoSaveManager.Deactivate(); + + RemoveControlFromDocks(_panelConfigNode); + _panelConfigNode?.QueueFree(); + + RemoveControlFromContainer(CustomControlContainer.Toolbar, _menuAutoSaveToggle); + _menuAutoSaveToggle?.Free(); + } +} +#endif //TOOLS diff --git a/addons/autosaver_editor/Contracts/IAutoSaveManager.cs b/addons/autosaver_editor/Contracts/IAutoSaveManager.cs new file mode 100644 index 0000000..fcb90dc --- /dev/null +++ b/addons/autosaver_editor/Contracts/IAutoSaveManager.cs @@ -0,0 +1,14 @@ +using System.Text; + +namespace AutoSaverPlugin.Contracts; + +internal interface IAutoSaveManager +{ + void Initialize(AutoSaverEditorPlugin plugin); + + void Activate(); + + void Reactivate(); + + void Deactivate(); +} diff --git a/addons/autosaver_editor/Contracts/IConfigurationManager.cs b/addons/autosaver_editor/Contracts/IConfigurationManager.cs new file mode 100644 index 0000000..459a7b7 --- /dev/null +++ b/addons/autosaver_editor/Contracts/IConfigurationManager.cs @@ -0,0 +1,47 @@ +using System; +using System.Text; +using AutoSaverPlugin.Shared; + +namespace AutoSaverPlugin.Contracts; + +internal interface IConfigurationManager +{ + event Action AutoSaverStateChanged; + + string PluginFullName { get; } + string PluginShortName { get; } + string PluginVersion { get; } + + string PluginIConResourcePath { get; } + VerboseLevel VerboseLevelSetting { get; } + int AutoSaverIntervalSetting { get; } + int PostponeTimeSetting { get; } + int ActivityCheckWindowSetting { get; } + bool IsOptionSaveScenesEnabled { get; } + bool IsOptionSaveScriptsEnabled { get; } + bool IsAutoSaverEnabled { get; } + + bool UseGDEditorSaveOnFocusLoss { get; } + bool UseGDEditorAutosaveIntervalSecs { get; } + bool GDEditor_save_on_focus_loss { get; } + int GDEditor_autosave_interval_secs { get; } + bool HasGDEditorAutosaveEnabled { get; } + + void SetEditorSaveOnFocusLoss(bool enabled); + + void SetEditorAutosaveIntervalSecs(int seconds); + + void LoadSettings(); + + void SaveSettings(); + + void SetAutoSaverEnabled(bool enabled, bool noEmitSignal = false); + + void SetSaverInterval(int seconds); + + void SetVerboseLevel(VerboseLevel level); + + void SetSceneEnabled(bool enabled); + + void SetScriptEnabled(bool enabled); +} diff --git a/addons/autosaver_editor/Contracts/IStatusReporter.cs b/addons/autosaver_editor/Contracts/IStatusReporter.cs new file mode 100644 index 0000000..70636f6 --- /dev/null +++ b/addons/autosaver_editor/Contracts/IStatusReporter.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Godot; + +namespace AutoSaverPlugin.Contracts; + +internal interface IStatusReporter +{ + List FetchModifiedItems(); +} + +internal interface ISceneStatusReporter : IStatusReporter +{ } + +internal interface IGDScriptStatusReporter : IStatusReporter +{ } + diff --git a/addons/autosaver_editor/Contracts/ITimerService.cs b/addons/autosaver_editor/Contracts/ITimerService.cs new file mode 100644 index 0000000..78377d4 --- /dev/null +++ b/addons/autosaver_editor/Contracts/ITimerService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AutoSaverPlugin.Contracts; + +public interface ITimerService +{ + ITimerService AttachTo(AutoSaverEditorPlugin pluginCaller); + ITimerService OnTimeout(Action onAutosaveTimerTimeout, bool oneShot = false); + ITimerService Begin(float intervalSec); + ITimerService End(); +} diff --git a/addons/autosaver_editor/LICENSE b/addons/autosaver_editor/LICENSE new file mode 100644 index 0000000..0e61dcf --- /dev/null +++ b/addons/autosaver_editor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 vrravalos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/autosaver_editor/README.pdf b/addons/autosaver_editor/README.pdf new file mode 100644 index 0000000..cafc6d8 Binary files /dev/null and b/addons/autosaver_editor/README.pdf differ diff --git a/addons/autosaver_editor/Services/AutoSaveManager.cs b/addons/autosaver_editor/Services/AutoSaveManager.cs new file mode 100644 index 0000000..2715560 --- /dev/null +++ b/addons/autosaver_editor/Services/AutoSaveManager.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AutoSaverPlugin.Contracts; +using AutoSaverPlugin.Services.GDComponent; +using AutoSaverPlugin.Shared; +using Godot; +using static AutoSaverPlugin.Shared.CommonUtils; + +namespace AutoSaverPlugin.Services; + +internal sealed class AutoSaveManager : IAutoSaveManager +{ + private readonly EditorInterface _editorInterface = EditorInterface.Singleton; + private ScriptEditor _scriptEditor => _editorInterface.GetScriptEditor(); + private readonly ISceneStatusReporter _sceneReporter; + private readonly IGDScriptStatusReporter _gdScriptReporter; + private readonly ITimerService _timerAutoSaver; + private readonly ITimerService _timerActivityUserCheck; + private readonly IConfigurationManager _configManager; + private readonly ILoggerService _logger; + private AutoSaverEditorPlugin _plugin; + private UserActivityMonitorNode _activityMonitor; + + public AutoSaveManager(ISceneStatusReporter sceneStatusReporter, IGDScriptStatusReporter scriptStatusReporter, IConfigurationManager configManager, + ILoggerService loggerService, ITimerService timerAutoSaver, ITimerService timerActivity) + { + _sceneReporter = sceneStatusReporter ?? throw new ArgumentNullException(nameof(sceneStatusReporter)); + _gdScriptReporter = scriptStatusReporter ?? throw new ArgumentNullException(nameof(scriptStatusReporter)); + _configManager = configManager ?? throw new ArgumentNullException(nameof(configManager)); + _logger = loggerService ?? throw new ArgumentNullException(nameof(loggerService)); + _timerAutoSaver = timerAutoSaver ?? throw new ArgumentNullException(nameof(timerAutoSaver)); + _timerActivityUserCheck = timerActivity ?? throw new ArgumentNullException(nameof(timerActivity)); + } + + public void Initialize(AutoSaverEditorPlugin plugin) + { + LoadingConfiguration(); + _plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); + Activate(); + } + + private void LoadingConfiguration() + { + _configManager.LoadSettings(); + } + + public void Activate() => SetupAutoSave(restart: false); + + public void Reactivate() => SetupAutoSave(restart: true); + + public void Deactivate() + { + _logger.LogDiagnostic("Stopping autosaver service.."); + _timerAutoSaver.End(); + _timerActivityUserCheck.End(); + RemoveActivityMonitor(); + PrintStatus(); + } + + private void SetupAutoSave(bool restart) + { + _logger.LogDiagnostic($"{(restart ? "Restarting" : "Initializing")} autosaver service.."); + _configManager.LoadSettings(); + SetTimers(); + ManageActivityMonitor(add: true); + PrintStatus(); + } + + private void SetTimers() + { + _logger.LogDebug("Setting timers.."); + + int intervalSec = _configManager.AutoSaverIntervalSetting; + int timeToStartCheck = Math.Clamp(intervalSec - _configManager.ActivityCheckWindowSetting, 1, intervalSec); + + _timerAutoSaver.End().AttachTo(_plugin).OnTimeout(OnAutosaveTimerTimeout); + _timerActivityUserCheck.End().AttachTo(_plugin).OnTimeout(StartMonitoringUserActivity, oneShot: true); + + if (_configManager.IsAutoSaverEnabled) + { + _timerAutoSaver.Begin(intervalSec); + _timerActivityUserCheck.Begin(timeToStartCheck); + } + } + + private void ManageActivityMonitor(bool add) + { + if (add) + { + _activityMonitor = new UserActivityMonitorNode(); + _activityMonitor.UserActivityDetected += OnUserActivityDetected; + _plugin.AddChild(_activityMonitor); + } + else + { + if (_activityMonitor != null) + { + _activityMonitor.UserActivityDetected -= OnUserActivityDetected; + + if (_activityMonitor.IsInsideTree()) + _plugin?.RemoveChild(_activityMonitor); + + if (!_activityMonitor.IsQueuedForDeletion()) + _activityMonitor.QueueFree(); + + _activityMonitor = null; + } + } + } + + private void RemoveActivityMonitor() => ManageActivityMonitor(add: false); + + private void OnUserActivityDetected(string eventName, float lastActivityTimeSec) => + _logger.LogDebug($"User activity detected: {eventName} at last activity time: {lastActivityTimeSec}ms"); + + private void StartMonitoringUserActivity() + { + _logger.LogDebug($"Starting user activity monitoring..@{GetCurrentTimestamp()}"); + _activityMonitor.StartMonitoring(); + } + + private void OnAutosaveTimerTimeout() + { + _logger.LogDiagnostic($"Running autosaver @{GetCurrentTimestamp()}"); + PerformAutoSaveIfNeeded(); + } + + private void PerformAutoSaveIfNeeded() + { + const float inTheLastMilliSecs = 500f; // 0.5 sec + if (!_activityMonitor.IsMonitoring || _activityMonitor.NoActivityTriggered(thresholdMillisec: inTheLastMilliSecs)) + { + PerformAutoSave(); + } + else + { + PostponeAutoSave(); + } + } + + private void PerformAutoSave() + { + _activityMonitor.StopMonitoring(); + + var modifiedScenes = _configManager.IsOptionSaveScenesEnabled ? GetModifiedItems(_sceneReporter.FetchModifiedItems()) : new List(); + var modifiedScripts = _configManager.IsOptionSaveScriptsEnabled ? GetModifiedItems(_gdScriptReporter.FetchModifiedItems()) : new List(); + + if (modifiedScenes.Count == 0 && modifiedScripts.Count == 0) + { + _logger.LogDiagnostic("No modified items. Skipping autosave."); + return; + } + + // save all scenes (also save all scripts) + bool savedAll = SaveScenes(modifiedScenes); + + if (!savedAll && !_configManager.HasGDEditorAutosaveEnabled) + SaveFiles(modifiedScripts); + + LogAutosaveResult(modifiedScenes.Count + modifiedScripts.Count, modifiedScenes.Concat(modifiedScripts).ToList()); + SetTimers(); // Restart timers + } + + private bool SaveScenes(List modifiedScenes) + { + List savedFiles = new(); + var openScenes = _editorInterface.GetOpenScenes(); + var editedScene = _editorInterface.GetEditedSceneRoot(); + var fnSceneRoot = Path.GetFileName(editedScene.SceneFilePath).Split('.')[0]; + bool saveAllAtOnce = false; + int numFilesSaved = 0; + + foreach (string scenePath in openScenes) + { + var fileNameScenePath = Path.GetFileName(scenePath).Split('.')[0]; + if (modifiedScenes.Contains(fileNameScenePath)) + { + numFilesSaved++; + savedFiles.Add(scenePath); + saveAllAtOnce = saveAllAtOnce || fnSceneRoot != fileNameScenePath; + } + } + + if (saveAllAtOnce) + { + _editorInterface.SaveAllScenes(); + } + else if (numFilesSaved == 1) + { + var err = _editorInterface.SaveScene(); + if (err != Error.Ok) + { + _logger.LogError($"Failed to autoSave scene: {fnSceneRoot}. Error: {err}"); + } + } + + return saveAllAtOnce; + } + + private void SaveFiles(List modifiedFiles) + { + if (modifiedFiles.Count > 0) + _editorInterface.SaveAllScenes(); + } + + private void PostponeAutoSave() + { + _logger.LogDebug($"Postponing autoSave for {_configManager.PostponeTimeSetting}sec.."); + _timerAutoSaver.End().OnTimeout(OnAutosaveTimerTimeout).Begin(_configManager.PostponeTimeSetting); + } + + private static List GetModifiedItems(List items) + { + var modifiedScripts = new List(); + + foreach (var i in items) + { + if (i.Contains("*")) + { + modifiedScripts.Add(i.Replace("(*)", "")); + } + } + + return modifiedScripts; + } + + private void LogAutosaveResult(int numFilesSaved, List savedFiles) + { + string currentTimestamp = GetCurrentTimestamp(); + if (numFilesSaved > 0) + { + _logger.LogInfo($"Autosave executed at {currentTimestamp}. {numFilesSaved} file(s) saved:"); + foreach (var file in savedFiles) + { + _logger.LogInfo($"- {file}"); + } + } + else + { + _logger.LogDiagnostic($"Autosave completed at {currentTimestamp}: No files saved."); + } + } + + private void PrintStatus() + { + string timestamp = GetCurrentTimestamp(); + string pluginSetTimestamp = $"Plugin set @{timestamp}."; + string autosaveScene = $"scenes ({(_configManager.IsOptionSaveScenesEnabled ? "ON" : "OFF")})"; + string autosaveScript = $"GDScript files ({(_configManager.IsOptionSaveScriptsEnabled ? "ON" : "OFF")})"; + string verboseLevelMessage = $"Verbose level: {_configManager.VerboseLevelSetting}."; + string editorMessage = $"[Editor] Autosave Interval: {_configManager.GDEditor_autosave_interval_secs}sec, [Editor] Save on focus loss: {_configManager.GDEditor_save_on_focus_loss}"; + + string statusAutosaving = _configManager.IsAutoSaverEnabled ? "Autosaving every {_autoSaveConfig.AutoSaverIntervalSetting} seconds: {autosaveScene} and {autosaveScript}." : "Autosaving disabled."; + + _logger.LogInfo($"{pluginSetTimestamp} {statusAutosaving}"); + _logger.LogDiagnostic($"{verboseLevelMessage} {editorMessage}"); + } +} diff --git a/addons/autosaver_editor/Services/ConfigurationManager.cs b/addons/autosaver_editor/Services/ConfigurationManager.cs new file mode 100644 index 0000000..ba50adc --- /dev/null +++ b/addons/autosaver_editor/Services/ConfigurationManager.cs @@ -0,0 +1,252 @@ +using System; +using AutoSaverPlugin.Contracts; +using AutoSaverPlugin.Shared; +using Godot; + +namespace AutoSaverPlugin.Services; + +public sealed class ConfigurationManager : IConfigurationManager +{ + // default autosaver settings + private const int AS_AUTOSAVER_INTERVAL = 60; + + private const VerboseLevel AS_VERBOSE = VerboseLevel.OFF; + private const int AS_POSTPONE_TIME = 5; + private const int AS_ACTIVITY_CHECK_WINDOW = 1; + private const bool AS_AUTOSAVE_SCENE = true; + private const bool AS_AUTOSAVE_GDSCRIPT = true; + + // default Godot editor settings + private const bool GDEDITOR_SAVE_ON_FOCUS_LOSS_DEFAULT = false; + + private const int GDEDITOR_AUTOSAVE_INTERVAL_SECS_DEFAULT = 0; + + // defines the use or not of Godot editor settings + private const bool USE_GDEDITOR_SAVE_ON_FOCUS_LOSS = false; + + private const bool USE_GDEDITOR_AUTOSAVE_INTERVAL_SECS = true; + + private readonly ILoggerService _logger; + private readonly ConfigFile _configFile = new ConfigFile(); + private readonly string _configFilePath; + private EditorSettings _gdEditorSettings => EditorInterface.Singleton.GetEditorSettings(); + + private VerboseLevel _verboseLevel = AS_VERBOSE; + + public VerboseLevel VerboseLevelSetting + { + get => _verboseLevel; + private set + { + _verboseLevel = value; + _logger?.SetOutput(value); + } + } + + public int AutoSaverIntervalSetting { get; private set; } = AS_AUTOSAVER_INTERVAL; + public int PostponeTimeSetting { get; private set; } = AS_POSTPONE_TIME; + public int ActivityCheckWindowSetting { get; private set; } = AS_ACTIVITY_CHECK_WINDOW; + public bool IsOptionSaveScenesEnabled { get; private set; } = AS_AUTOSAVE_SCENE; + public bool IsOptionSaveScriptsEnabled { get; private set; } = AS_AUTOSAVE_GDSCRIPT; + public bool IsAutoSaverEnabled { get; private set; } = true; + public bool GDEditor_save_on_focus_loss { get; private set; } = GDEDITOR_SAVE_ON_FOCUS_LOSS_DEFAULT; + public int GDEditor_autosave_interval_secs { get; private set; } = GDEDITOR_AUTOSAVE_INTERVAL_SECS_DEFAULT; + + public string PluginFullName => PluginInfo.FullName; + public string PluginShortName => PluginInfo.NameShort; + public string PluginVersion { get; } = CommonUtils.GetPluginVersion(); + + public string PluginIConResourcePath => PluginInfo.BaseResourcePath + PluginInfo.PluginIcon; + + public bool HasGDEditorAutosaveEnabled => GDEditor_autosave_interval_secs > 0; + + public bool UseGDEditorSaveOnFocusLoss { get; private set; } = USE_GDEDITOR_SAVE_ON_FOCUS_LOSS; + + public bool UseGDEditorAutosaveIntervalSecs { get; private set; } = USE_GDEDITOR_AUTOSAVE_INTERVAL_SECS; + + public event Action AutoSaverStateChanged; + + public ConfigurationManager(ILoggerService logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _configFilePath = DetermineConfigFilePath(); + } + + public void SetSaverInterval(int seconds) => AutoSaverIntervalSetting = seconds; + + public void SetVerboseLevel(VerboseLevel level) + { + VerboseLevelSetting = level; + } + + public void SetEditorSaveOnFocusLoss(bool enabled) + { + GDEditor_save_on_focus_loss = enabled; + _gdEditorSettings.SetSetting("interface/editor/save_on_focus_loss", enabled); + } + + public void SetEditorAutosaveIntervalSecs(int seconds) + { + GDEditor_autosave_interval_secs = seconds; + _gdEditorSettings.SetSetting("text_editor/behavior/files/autosave_interval_secs", seconds); + } + + public void SetSceneEnabled(bool enabled) => IsOptionSaveScenesEnabled = enabled; + + public void SetScriptEnabled(bool enabled) => IsOptionSaveScriptsEnabled = enabled; + + public void LoadSettings() + { + LoadFromGodotEditorSettings(); + + LoadFromConfigFile(); + + SyncGodotEditorSettings(); + } + + public void ResetSettings() + { + ResetGodotEditorSettings(); + + AutoSaverIntervalSetting = AS_AUTOSAVER_INTERVAL; + VerboseLevelSetting = AS_VERBOSE; + IsAutoSaverEnabled = true; + PostponeTimeSetting = AS_POSTPONE_TIME; + ActivityCheckWindowSetting = AS_ACTIVITY_CHECK_WINDOW; + } + + public void SaveSettings() + { + SaveToConfigFile(); + + if (IsAutoSaverEnabled) + { + SyncGodotEditorSettings(); + } + else + { + ResetGodotEditorSettings(); + } + } + + private void SyncGodotEditorSettings() + { + _logger.LogDiagnostic($"Syncing Godot editor settings.. UseGDEditorAutosaveIntervalSecs={UseGDEditorAutosaveIntervalSecs}:{AutoSaverIntervalSetting}, SetEditorSaveOnFocusLoss={UseGDEditorSaveOnFocusLoss}"); + + // update Godot editor settings from config file + if (UseGDEditorAutosaveIntervalSecs) + SetEditorAutosaveIntervalSecs(seconds: AutoSaverIntervalSetting); + + if (UseGDEditorSaveOnFocusLoss) + { + SetEditorSaveOnFocusLoss(enabled: IsAutoSaverEnabled); + } + } + + private void ResetGodotEditorSettings() + { + _logger.LogDiagnostic($"Resetting Godot editor settings.. UseGDEditorAutosaveIntervalSecs={UseGDEditorAutosaveIntervalSecs}, SetEditorSaveOnFocusLoss={UseGDEditorSaveOnFocusLoss}"); + + // update Godot editor settings from default values + if (UseGDEditorAutosaveIntervalSecs) + SetEditorAutosaveIntervalSecs(seconds: GDEDITOR_AUTOSAVE_INTERVAL_SECS_DEFAULT); + + if (UseGDEditorSaveOnFocusLoss) + SetEditorSaveOnFocusLoss(enabled: GDEDITOR_SAVE_ON_FOCUS_LOSS_DEFAULT); + } + + private void LoadFromGodotEditorSettings() + { + GDEditor_autosave_interval_secs = (int)_gdEditorSettings.GetSetting("text_editor/behavior/files/autosave_interval_secs"); + GDEditor_save_on_focus_loss = (bool)_gdEditorSettings.GetSetting("interface/editor/save_on_focus_loss"); + } + + public void SetAutoSaverEnabled(bool enabled, bool noEmitSignal = false) + { + IsAutoSaverEnabled = enabled; + + SaveSettings(); + + if (!noEmitSignal) + { + AutoSaverStateChanged?.Invoke(enabled); + } + } + + private string DetermineConfigFilePath() + { + var debugConfigPath = FindConfigFile(".debug.ini"); + if (!string.IsNullOrEmpty(debugConfigPath)) + { + _logger.LogDiagnostic($"Using debug config file: {debugConfigPath}"); + return debugConfigPath; + } + + var standardConfigPath = FindConfigFile(".ini"); + if (!string.IsNullOrEmpty(standardConfigPath)) + { + _logger.LogDiagnostic($"Using standard config file: {standardConfigPath}"); + return standardConfigPath; + } + + _logger.LogDiagnostic("Config file not found, using project settings."); + return null; + } + + private string FindConfigFile(string extension) + { + var fileName = PluginInfo.SettingsFileName.Replace(".ini", extension); + return CommonUtils.GetAllProjectFiles(extension).Find(f => f.Contains(fileName)); + } + + private bool LoadFromConfigFile() + { + _logger.LogDiagnostic($"Loading config file: {_configFilePath}"); + Error error = _configFile.Load(_configFilePath); + if (error != Error.Ok) + { + _logger.LogError($"Failed to load config file: {_configFilePath}, error: {error}"); + ResetSettings(); + return false; + } + + AutoSaverIntervalSetting = (int)_configFile.GetValue(PluginInfo.RootSettings, PluginInfo.KeyIntervalSec, AS_AUTOSAVER_INTERVAL); + + VerboseLevelSetting = Enum.TryParse((string)_configFile.GetValue(PluginInfo.RootSettings, PluginInfo.KeyVerbose, AS_VERBOSE.ToString()), + out VerboseLevel result) + ? result + : AS_VERBOSE; + + IsAutoSaverEnabled = (bool)_configFile.GetValue(PluginInfo.RootSettings, PluginInfo.KeyEnabled, true); + PostponeTimeSetting = (int)_configFile.GetValue(PluginInfo.RootSettings, PluginInfo.KeyPostponeTimeSec, AS_POSTPONE_TIME); + ActivityCheckWindowSetting = (int)_configFile.GetValue(PluginInfo.RootSettings, PluginInfo.KeyActivityCheckWindowSec, AS_ACTIVITY_CHECK_WINDOW); + IsOptionSaveScenesEnabled = (bool)_configFile.GetValue(PluginInfo.RootSettings, PluginInfo.KeyAutosaveScene, AS_AUTOSAVE_SCENE); + IsOptionSaveScriptsEnabled = (bool)_configFile.GetValue(PluginInfo.RootSettings, PluginInfo.KeyAutosaveGDScript, AS_AUTOSAVE_GDSCRIPT); + UseGDEditorAutosaveIntervalSecs = (bool)_configFile.GetValue(PluginInfo.RootSettings, PluginInfo.KeyUseGDEditorAutosaveInterval, USE_GDEDITOR_AUTOSAVE_INTERVAL_SECS); + UseGDEditorSaveOnFocusLoss = (bool)_configFile.GetValue(PluginInfo.RootSettings, PluginInfo.KeyUseGDEditorSaveOnFocusLoss, USE_GDEDITOR_SAVE_ON_FOCUS_LOSS); + + return true; + } + + private bool SaveToConfigFile() + { + _logger.LogDiagnostic($"Saving settings to config file: {_configFilePath}"); + _configFile.SetValue(PluginInfo.RootSettings, PluginInfo.KeyIntervalSec, AutoSaverIntervalSetting); + _configFile.SetValue(PluginInfo.RootSettings, PluginInfo.KeyVerbose, VerboseLevelSetting.ToString()); + _configFile.SetValue(PluginInfo.RootSettings, PluginInfo.KeyEnabled, IsAutoSaverEnabled); + _configFile.SetValue(PluginInfo.RootSettings, PluginInfo.KeyPostponeTimeSec, PostponeTimeSetting); + _configFile.SetValue(PluginInfo.RootSettings, PluginInfo.KeyActivityCheckWindowSec, ActivityCheckWindowSetting); + _configFile.SetValue(PluginInfo.RootSettings, PluginInfo.KeyAutosaveScene, IsOptionSaveScenesEnabled); + _configFile.SetValue(PluginInfo.RootSettings, PluginInfo.KeyAutosaveGDScript, IsOptionSaveScriptsEnabled); + _configFile.SetValue(PluginInfo.RootSettings, PluginInfo.KeyUseGDEditorAutosaveInterval, UseGDEditorAutosaveIntervalSecs); + _configFile.SetValue(PluginInfo.RootSettings, PluginInfo.KeyUseGDEditorSaveOnFocusLoss, UseGDEditorSaveOnFocusLoss); + + var error = _configFile.Save(_configFilePath); + if (error != Error.Ok) + { + _logger.LogError($"Failed to save config file: {_configFilePath}, error: {error}"); + return false; + } + return true; + } +} diff --git a/addons/autosaver_editor/Services/GDComponent/UserActivityMonitorNode.cs b/addons/autosaver_editor/Services/GDComponent/UserActivityMonitorNode.cs new file mode 100644 index 0000000..8c8459d --- /dev/null +++ b/addons/autosaver_editor/Services/GDComponent/UserActivityMonitorNode.cs @@ -0,0 +1,51 @@ +using System; +using AutoSaverPlugin.Shared; +using Godot; + +namespace AutoSaverPlugin.Services.GDComponent; + +internal partial class UserActivityMonitorNode : Node +{ + public event Action UserActivityDetected; + + private bool _isMonitoring = false; + private DateTime? _lastActivityTimeUtc; + private readonly ILoggerService _logger = ServiceProvider.GetService(); + + public bool IsMonitoring => _isMonitoring; + public bool AnyUserActivityDetected => _lastActivityTimeUtc.HasValue; + + public UserActivityMonitorNode() + { + } + + public void StartMonitoring() => _isMonitoring = true; + + public void StopMonitoring() => _isMonitoring = false; + + public override void _Input(InputEvent @event) + { + if (!_isMonitoring || !(@event is InputEventMouseMotion || @event is InputEventKey)) return; + + _lastActivityTimeUtc = DateTime.UtcNow; + + string eventType = @event is InputEventMouseMotion ? "MouseMotion" : "Key"; + _logger.LogDebug($"{nameof(UserActivityMonitorNode)}: User activity detected: {eventType}, LastDetectedActivityTimeInMillisec = {LastDetectedActivityTimeInMillisec()}"); + + UserActivityDetected?.Invoke(eventType, LastDetectedActivityTimeInMillisec()); + } + + internal float LastDetectedActivityTimeInMillisec() + { + return _lastActivityTimeUtc.HasValue + ? (float)((DateTime.UtcNow - _lastActivityTimeUtc.Value).TotalMilliseconds) + : 0; + } + + public bool NoActivityTriggered(float thresholdMillisec) + { + _logger.LogDebug($"{nameof(UserActivityMonitorNode)}: Checking for no activity in the last {thresholdMillisec}ms. Last activity was {LastDetectedActivityTimeInMillisec()}ms ago."); + + return LastDetectedActivityTimeInMillisec() > thresholdMillisec; + } +} diff --git a/addons/autosaver_editor/Services/GDScriptStatusReporter.cs b/addons/autosaver_editor/Services/GDScriptStatusReporter.cs new file mode 100644 index 0000000..9dcd458 --- /dev/null +++ b/addons/autosaver_editor/Services/GDScriptStatusReporter.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AutoSaverPlugin.Contracts; +using AutoSaverPlugin.Shared; +using Godot; + +namespace AutoSaverPlugin.Services +{ + internal sealed class GDScriptStatusReporter : IGDScriptStatusReporter + { + private readonly ILoggerService _logger; + + public GDScriptStatusReporter(ILoggerService loggerService) + { + _logger = loggerService ?? throw new ArgumentNullException(nameof(loggerService)); + } + + public List FetchModifiedItems() + { + _logger.LogDebug("Fetching modified scripts..."); + ScriptEditor editor = EditorInterface.Singleton.GetScriptEditor(); + + List listItemText = new(); + + ItemList itemList = FindItemList(editor); + + if (itemList == null) + { + return listItemText; + } + + for (int i = 0; i < itemList.ItemCount; i++) + { + var item = itemList.GetItemText(i); + _logger.LogDiagnostic($"Script file[{i}]: {item}"); + listItemText.Add(item); + } + + return listItemText; + } + + private ItemList FindItemList(Node root) + { + if (root is ItemList itemList) + { + return itemList; + } + + foreach (Node child in root.GetChildren()) + { + var result = FindItemList(child); + if (result != null) + { + return result; + } + } + + return null; + } + } +} diff --git a/addons/autosaver_editor/Services/SceneTabStatusReporter.cs b/addons/autosaver_editor/Services/SceneTabStatusReporter.cs new file mode 100644 index 0000000..f7c4582 --- /dev/null +++ b/addons/autosaver_editor/Services/SceneTabStatusReporter.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using AutoSaverPlugin.Contracts; +using AutoSaverPlugin.Shared; +using Godot; + +namespace AutoSaverPlugin.Services +{ + internal sealed class SceneTabStatusReporter : ISceneStatusReporter + { + private readonly EditorInterface _editorInterface = EditorInterface.Singleton; + private readonly ILoggerService _logger; + + public SceneTabStatusReporter(ILoggerService loggerService) + { + _logger = loggerService ?? throw new ArgumentNullException(nameof(loggerService)); + } + + public List FetchModifiedItems() + { + _logger.LogDebug("Fetching modified scenes..."); + var tabTitles = new List(); + var tabBar = FindTabBar(_editorInterface.GetBaseControl()); + + if (tabBar == null) + { + _logger.LogError("Scene tab bar not found."); + return tabTitles; + } + + for (int i = 0; i < tabBar.TabCount; i++) + { + var title = tabBar.GetTabTitle(i); + _logger.LogDiagnostic($"Scene tab[{i}]: {title}"); + tabTitles.Add(title); + } + + return tabTitles; + } + + private TabBar FindTabBar(Node root) + { + if (root is TabBar tabBar) + { + return tabBar; + } + + foreach (Node child in root.GetChildren()) + { + var result = FindTabBar(child); + if (result != null) + { + return result; + } + } + + return null; + } + } +} diff --git a/addons/autosaver_editor/Services/TimerService.cs b/addons/autosaver_editor/Services/TimerService.cs new file mode 100644 index 0000000..774e7d6 --- /dev/null +++ b/addons/autosaver_editor/Services/TimerService.cs @@ -0,0 +1,91 @@ +using System; +using AutoSaverPlugin.Contracts; +using AutoSaverPlugin.Shared; +using Godot; + +namespace AutoSaverPlugin.Services +{ + internal sealed class TimerService : ITimerService + { + private Timer _timer; + private AutoSaverEditorPlugin _plugin; + private Action _timeoutAction; + private ILoggerService _logger; + + public TimerService(ILoggerService loggerService) + { + _logger = loggerService ?? throw new ArgumentNullException(nameof(loggerService)); + } + + public ITimerService AttachTo(AutoSaverEditorPlugin plugin) + { + _plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); + return this; + } + + public ITimerService OnTimeout(Action action, bool oneShot = false) + { + _timeoutAction = action ?? throw new ArgumentNullException(nameof(action)); + SetupTimer(oneShot); + return this; + } + + public ITimerService Begin(float intervalSeconds) + { + if (_timer == null) + { + _logger.LogError("Timer is not initialized. Call OnTimeout first."); + return this; + } + + _timer.WaitTime = intervalSeconds; + + if (!_timer.IsInsideTree()) + { + _plugin.AddChild(_timer); + } + + _timer.Start(); + return this; + } + + public ITimerService End() + { + DisposeTimer(); + return this; + } + + private void SetupTimer(bool oneShot) + { + DisposeTimer(); + + _timer = new Timer + { + OneShot = oneShot + }; + + _timer.Timeout += OnTimerTimeout; + } + + private void OnTimerTimeout() + { + _timeoutAction?.Invoke(); + } + + private void DisposeTimer() + { + if (_timer != null) + { + _timer.Stop(); + if (_timer.IsInsideTree()) + { + _plugin.RemoveChild(_timer); + } + + _timer.Timeout -= OnTimerTimeout; + _timer.QueueFree(); + _timer = null; + } + } + } +} diff --git a/addons/autosaver_editor/Shared/CommonUtils.cs b/addons/autosaver_editor/Shared/CommonUtils.cs new file mode 100644 index 0000000..c97ceec --- /dev/null +++ b/addons/autosaver_editor/Shared/CommonUtils.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using Godot; + +namespace AutoSaverPlugin.Shared +{ + internal static class CommonUtils + { + internal static string GetCurrentTimestamp() => DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + + internal static string GetPluginVersion() => $"{PluginInfo.PluginVersion}"; + + internal static List GetAllProjectFiles(string extFileFilter = null) + { + List files = new List(); + EditorInterface editorInterface = EditorInterface.Singleton; + EditorFileSystem fileSystem = editorInterface.GetResourceFilesystem(); + + fileSystem.Scan(); + GetFilesRecursive(fileSystem.GetFilesystem(), files, extFileFilter); + + return files; + } + + private static void GetFilesRecursive(EditorFileSystemDirectory directory, List files, string extensionFilter = null) + { + for (int i = 0; i < directory.GetFileCount(); i++) + { + string filePath = directory.GetFilePath(i); + if (string.IsNullOrEmpty(extensionFilter) || filePath.EndsWith(extensionFilter)) + { + files.Add(filePath); + } + } + + for (int i = 0; i < directory.GetSubdirCount(); i++) + { + GetFilesRecursive(directory.GetSubdir(i), files, extensionFilter); + } + } + } +} diff --git a/addons/autosaver_editor/Shared/Logger.cs b/addons/autosaver_editor/Shared/Logger.cs new file mode 100644 index 0000000..e3f5271 --- /dev/null +++ b/addons/autosaver_editor/Shared/Logger.cs @@ -0,0 +1,84 @@ +using Godot; + +namespace AutoSaverPlugin.Shared; + +public enum VerboseLevel +{ + OFF = 0, + MIN = 1, + MAX = 2, + SECRET = 3 +} + +public enum LogType +{ + MINOR = 0, + MAJOR = 1, + WARN_ERR = 2, + DEBUG = 3 +} + +public interface ILoggerService +{ + bool IsLogInfoEnable { get; } + void SetOutput(VerboseLevel verboseLevel); + void Log(string message, LogType logLevel); + void LogDiagnostic(string message); + void LogInfo(string message); + void LogError(string message); + void LogDebug(string message); +} + +internal sealed class Logger : ILoggerService +{ + private VerboseLevel _configuredVerboseLevel = VerboseLevel.OFF; + + public VerboseLevel VerboseLevel => _configuredVerboseLevel; + + public bool IsLogInfoEnable => _configuredVerboseLevel >= VerboseLevel.MIN; + + public Logger() { } + + public void SetOutput(VerboseLevel verboseLevel) + { + _configuredVerboseLevel = verboseLevel; + } + + // +----------------------+---------------+---------------------+---------------------+ + // | LogType\VerboseLevel | MIN | MAX | SECRET | + // +----------------------+---------------+---------------------+---------------------+ + // | DEBUG | - | - | GD.Print("[DEBUG]") | + // | MINOR | - | GD.Print("[INFO]") | GD.Print("[INFO]") | + // | MAJOR | GD.Print() | GD.Print() | GD.Print() | + // | ERROR | GD.PrintErr() | GD.PrintErr() | GD.PrintErr() | + // +----------------------+---------------+---------------------+---------------------+ + + public void Log(string message, LogType logType) + { + if (_configuredVerboseLevel == VerboseLevel.OFF) return; + + string prefix = $"[{PluginInfo.NameShort}]"; + switch (logType) + { + case LogType.DEBUG: + if (_configuredVerboseLevel == VerboseLevel.SECRET) + GD.Print($"{prefix}[DEBUG] {message}"); + break; + case LogType.MINOR: + if (_configuredVerboseLevel >= VerboseLevel.MAX) + GD.Print($"{prefix} {message}"); // [INFO] + break; + case LogType.MAJOR: + GD.Print($"{prefix} {message}"); + break; + case LogType.WARN_ERR: + GD.PrintErr($"{prefix}[ERROR] {message}"); + break; + } + } + + public void LogDebug(string message) => Log(message, LogType.DEBUG); + public void LogDiagnostic(string message) => Log(message, LogType.MINOR); + public void LogInfo(string message) => Log(message, LogType.MAJOR); + public void LogError(string message) => Log(message, LogType.WARN_ERR); +} diff --git a/addons/autosaver_editor/Shared/PluginInfo.cs b/addons/autosaver_editor/Shared/PluginInfo.cs new file mode 100644 index 0000000..3c5c8d6 --- /dev/null +++ b/addons/autosaver_editor/Shared/PluginInfo.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AutoSaverPlugin.Shared +{ + internal static class PluginInfo + { + internal const string NameShort = "AutoSaver"; + internal const string FullName = "AutoSaver Toggle for Godot Editor (C#)"; + internal const string Description = "Auto Saver for Godot Editor: a peace of mind toggle to automatically save your workspace"; + internal const string Author = "Victor R. R. Avalos"; + internal const string RootSettings = "autosaver_editor"; + internal const string BaseFolderName = "autosaver_editor"; + internal const string BaseResourcePath = "res://addons/autosaver_editor/"; + internal const string SettingsFileName = "settings.ini"; + internal const string PluginVersion = "0.1.0"; + internal const string PluginIcon = "icon_autosaver.png"; + + + // settings + internal const string KeyEnabled = "enabled"; + internal const string KeyIntervalSec = "interval"; + internal const string KeyVerbose = "verbose"; + internal const string KeyPostponeTimeSec = "postpone_time"; + internal const string KeyActivityCheckWindowSec = "activity_check"; + internal const string KeyAutosaveScene = "autosave_scene"; + internal const string KeyAutosaveGDScript = "autosave_gdscript"; + + internal const string KeyUseGDEditorAutosaveInterval = "use_gd_editor_autosave_interval"; + internal const string KeyUseGDEditorSaveOnFocusLoss = "use_gd_editor_save_on_focus_loss"; + } +} diff --git a/addons/autosaver_editor/Shared/ServiceProvider.cs b/addons/autosaver_editor/Shared/ServiceProvider.cs new file mode 100644 index 0000000..257352f --- /dev/null +++ b/addons/autosaver_editor/Shared/ServiceProvider.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AutoSaverPlugin.Contracts; +using AutoSaverPlugin.Services; + +namespace AutoSaverPlugin.Shared; + +public static class ServiceProvider +{ + private static readonly Dictionary> _services = new(); + private static readonly Dictionary _singletonInstances = new(); + + public static void RegisterServiceAsTransient() where TImplementation : class, TInterface + { + _services[typeof(TInterface)] = () => CreateInstance(); + } + + public static void RegisterServiceAsSingleton() where TImplementation : class, TInterface, new() + { + _services[typeof(TInterface)] = () => GetOrCreateSingletonInstance(typeof(TInterface), () => new TImplementation()); + } + + public static void RegisterServiceAsSingleton(Func factory) + { + _services[typeof(TInterface)] = () => GetOrCreateSingletonInstance(typeof(TInterface), () => factory()); + } + + public static T GetService() where T : class + { + return (T)GetService(typeof(T)); + } + + private static object GetService(Type type) + { + if (_services.TryGetValue(type, out var serviceFactory)) + { + return serviceFactory(); + } + throw new InvalidOperationException($"Service of type {type} is not registered."); + } + + private static T CreateInstance() where T : class + { + var type = typeof(T); + var constructor = type.GetConstructors()[0]; + var parameters = constructor.GetParameters(); + var parameterInstances = parameters.Select(p => GetService(p.ParameterType)).ToArray(); + return (T)constructor.Invoke(parameterInstances); + } + + private static object GetOrCreateSingletonInstance(Type type, Func factory) + { + if (!_singletonInstances.TryGetValue(type, out var instance)) + { + instance = factory(); + _singletonInstances[type] = instance; + } + return instance; + } + + public static void Initialize() + { + RegisterServiceAsSingleton(); + RegisterServiceAsSingleton(() => new ConfigurationManager(GetService())); + RegisterServiceAsSingleton(() => new SceneTabStatusReporter(GetService())); + RegisterServiceAsSingleton(() => new GDScriptStatusReporter(GetService())); + + RegisterServiceAsTransient(); + + RegisterServiceAsSingleton(() => new AutoSaveManager( + GetService(), + GetService(), + GetService(), + GetService(), + GetService(), + GetService() + )); + } +} diff --git a/addons/autosaver_editor/UI/AutoSaveToggleMenuBuilder.cs b/addons/autosaver_editor/UI/AutoSaveToggleMenuBuilder.cs new file mode 100644 index 0000000..cd9e447 --- /dev/null +++ b/addons/autosaver_editor/UI/AutoSaveToggleMenuBuilder.cs @@ -0,0 +1,86 @@ +using System.Text; +using AutoSaverPlugin.Contracts; + +using AutoSaverPlugin.Shared; +using Godot; + +namespace AutoSaverPlugin.UI; + +/// +/// Builds and manages the toggle button for the Auto Save feature in the top menu. +/// +public class AutoSaveToggleMenuBuilder +{ + private CheckButton _autoSaveToggleButton; + private readonly IAutoSaveManager _autoSaveManager = ServiceProvider.GetService(); + private readonly IConfigurationManager _configManager = ServiceProvider.GetService(); + + /// + /// Initializes a new instance of the class. + /// + public AutoSaveToggleMenuBuilder() + { + Build(); + } + + /// + /// Gets the Auto Save toggle button with configured settings and event handlers. + /// + public CheckButton AutoSaveToggleButton => Build(); + + /// + /// Creates and initializes the Auto Save toggle button for the top menu. + /// + /// The initialized Auto Save toggle button. + private CheckButton Build() + { + if (_autoSaveToggleButton != null) + return _autoSaveToggleButton; + + _autoSaveToggleButton = new CheckButton + { + Text = "Auto Save" + }; + _autoSaveToggleButton.Toggled += HandleAutoSaveToggleChanged; + _autoSaveToggleButton.SetPressedNoSignal(_configManager.IsAutoSaverEnabled); + + return _autoSaveToggleButton; + } + + /// + /// Detaches the toggle event subscriptions to prevent memory leaks. + /// + internal void DetachAutoSaveToggleEvents() + { + _autoSaveToggleButton.Toggled -= HandleAutoSaveToggleChanged; + } + + /// + /// Handles changes to the Auto Save toggle, updating settings and service state accordingly. + /// + /// Indicates whether the feature is being enabled or disabled. + private void HandleAutoSaveToggleChanged(bool toggledOn) + { + _configManager.SetAutoSaverEnabled(enabled: toggledOn); + _configManager.SaveSettings(); + + if (toggledOn) + { + _autoSaveManager.Reactivate(); + } + else + { + _autoSaveManager.Deactivate(); + } + } + + /// + /// Updates the toggle state based on external settings invocation. + /// + /// The trace builder for logging. + /// Indicates the new enabled state. + internal void UpdateToggleStateFromSettings(bool enabled) + { + _autoSaveToggleButton.SetPressedNoSignal(enabled); + } +} diff --git a/addons/autosaver_editor/UI/GDComponent/PanelSettingsControlNode.cs b/addons/autosaver_editor/UI/GDComponent/PanelSettingsControlNode.cs new file mode 100644 index 0000000..09c263d --- /dev/null +++ b/addons/autosaver_editor/UI/GDComponent/PanelSettingsControlNode.cs @@ -0,0 +1,231 @@ +using System; +using AutoSaverPlugin.Contracts; +using AutoSaverPlugin.Shared; +using Godot; + +namespace AutoSaverPlugin.UI.GDComponent; + +[Tool] +public partial class PanelSettingsControlNode : Control +{ + private readonly IAutoSaveManager _autoSaveManager = ServiceProvider.GetService(); + private readonly IConfigurationManager _configManager = ServiceProvider.GetService(); + private readonly ILoggerService _logger = ServiceProvider.GetService(); + + private SpinBox _intervalSpinBox; + private OptionButton _verboseLevelOption; + private Button _saveButton; + private CheckButton _enableToggle; + private CheckBox _autosaveSceneCheckBox; + private CheckBox _autosaveGDScriptCheckBox; + private Label _lblStatus; + + public override void _Ready() + { + Name = _configManager.PluginFullName; + + SetupUI(); + LoadPanelSettings(); + _configManager.AutoSaverStateChanged += OnInvokeBySettings; + } + + private void SetupUI() + { + var vbox = new VBoxContainer(); + AddChild(vbox); + + SetupEnableToggle(vbox); + vbox.AddChild(new HSeparator()); + SetupIntervalSpinBox(vbox); + + if (_configManager.VerboseLevelSetting == VerboseLevel.SECRET) + { + SetupAutosaveSceneCheckBox(vbox); + SetupAutosaveGDScriptCheckBox(vbox); + SetupVerboseLevelOption(vbox); + } + + SetupSaveButton(vbox); + SetupStatusLabel(vbox); + vbox.AddChild(new HSeparator()); + SetupFooter(vbox); + } + + private void SetupAutosaveSceneCheckBox(VBoxContainer vbox) + { + var autosaveSceneHBox = new HBoxContainer(); + vbox.AddChild(autosaveSceneHBox); + autosaveSceneHBox.AddChild(new Label { Text = "Autosave modified scenes" }); + _autosaveSceneCheckBox = new CheckBox(); + autosaveSceneHBox.AddChild(_autosaveSceneCheckBox); + } + + private void SetupAutosaveGDScriptCheckBox(VBoxContainer vbox) + { + var autosaveGDScriptHBox = new HBoxContainer(); + vbox.AddChild(autosaveGDScriptHBox); + autosaveGDScriptHBox.AddChild(new Label { Text = "Autosave modified script files" }); + _autosaveGDScriptCheckBox = new CheckBox(); + autosaveGDScriptHBox.AddChild(_autosaveGDScriptCheckBox); + } + + private void SetupEnableToggle(VBoxContainer vbox) + { + var enableHBox = new HBoxContainer(); + vbox.AddChild(enableHBox); + enableHBox.AddChild(new Label { Text = "Enable AutoSaver:" }); + _enableToggle = new CheckButton(); + enableHBox.AddChild(_enableToggle); + _enableToggle.Toggled += OnPanelAutoSaveToggled; + } + + private void SetupIntervalSpinBox(VBoxContainer vbox) + { + var intervalHBox = new HBoxContainer(); + vbox.AddChild(intervalHBox); + intervalHBox.AddChild(new Label { Text = "Autosave interval (seconds):" }); + _intervalSpinBox = new SpinBox { MinValue = 5, MaxValue = 300, Value = 60, Step = 5 }; + intervalHBox.AddChild(_intervalSpinBox); + } + + private void SetupVerboseLevelOption(VBoxContainer vbox) + { + var verboseHBox = new HBoxContainer(); + vbox.AddChild(verboseHBox); + verboseHBox.AddChild(new Label { Text = "Verbose level:" }); + _verboseLevelOption = new OptionButton(); + + _verboseLevelOption.AddItem("OFF", (int)VerboseLevel.OFF); + _verboseLevelOption.AddItem("MIN", (int)VerboseLevel.MIN); + _verboseLevelOption.AddItem("MAX", (int)VerboseLevel.MAX); + _verboseLevelOption.AddItem("SECRET", (int)VerboseLevel.SECRET); + + verboseHBox.AddChild(_verboseLevelOption); + } + + private void SetupSaveButton(VBoxContainer vbox) + { + _saveButton = new Button { Text = "Update Settings" }; + vbox.AddChild(_saveButton); + _saveButton.Pressed += OnSaveButtonPressed; + } + + private void SetupStatusLabel(VBoxContainer vbox) + { + _lblStatus = new Label(); + vbox.AddChild(_lblStatus); + _lblStatus.Text = "Settings loaded @" + CommonUtils.GetCurrentTimestamp(); + } + + private void SetupFooter(VBoxContainer vbox) + { + string footerText = $"AutoSaver for Godot Editor v.{_configManager.PluginVersion} by Victor R. R. Avalos"; + vbox.AddChild(new LinkButton { Text = footerText, Uri = "https://github.com/vrravalos" }); + } + + private void OnInvokeBySettings(bool enabled) + { + _logger.LogDebug($"{{Panel: {nameof(OnInvokeBySettings)}}} AutoSaver enabled state changed.."); + _enableToggle.SetPressedNoSignal(enabled); + UpdateUIState(); + } + + private void LoadPanelSettings() + { + _logger.LogDiagnostic("Loading panel settings.."); + + _enableToggle.SetPressedNoSignal(_configManager.IsAutoSaverEnabled); + _intervalSpinBox.SetValueNoSignal(_configManager.AutoSaverIntervalSetting); + _verboseLevelOption?.Select((int)_configManager.VerboseLevelSetting); + _autosaveSceneCheckBox?.SetPressedNoSignal(_configManager.IsOptionSaveScenesEnabled); + _autosaveGDScriptCheckBox?.SetPressedNoSignal(_configManager.IsOptionSaveScriptsEnabled); + UpdateUIState(); + } + + private void OnSaveButtonPressed() + { + _logger.LogDebug("Saving settings.."); + SaveSettings(noSignal: true); + UpdateUIState(); + } + + public void OnPanelAutoSaveToggled(bool buttonPressed) + { + SaveSettings(); + UpdateUIState(); + } + + public override void _ExitTree() + { + _enableToggle.Toggled -= OnPanelAutoSaveToggled; + _configManager.AutoSaverStateChanged -= OnInvokeBySettings; + } + + private void UpdateUIState() + { + bool isEnabled = _enableToggle.ButtonPressed; + _intervalSpinBox.Editable = isEnabled; + if (_verboseLevelOption != null) + _verboseLevelOption.Disabled = !isEnabled; + + if (_autosaveSceneCheckBox != null) + _autosaveSceneCheckBox.Disabled = !isEnabled; + + if (_autosaveGDScriptCheckBox != null) + _autosaveGDScriptCheckBox.Disabled = !isEnabled; + + _saveButton.Disabled = !isEnabled; + _lblStatus.Text = "Settings updated @" + CommonUtils.GetCurrentTimestamp(); + } + + private void SaveSettings(bool noSignal = false) + { + SaveSettings(noEmitSignal: noSignal, + interval: (int?)_intervalSpinBox.Value, + verboseLevel: _verboseLevelOption?.Selected != null ? (VerboseLevel?)_verboseLevelOption.Selected : null, + sceneEnabled: _autosaveSceneCheckBox?.ButtonPressed, + scriptEnabled: _autosaveGDScriptCheckBox?.ButtonPressed, + autoSaverEnabled: _enableToggle.ButtonPressed); + } + + private void SaveSettings(bool noEmitSignal, int? interval = null, + VerboseLevel? verboseLevel = null, bool? sceneEnabled = null, bool? scriptEnabled = null, + bool? autoSaverEnabled = null) + { + if (interval.HasValue) + { + _configManager.SetSaverInterval(interval.Value); + } + + if (verboseLevel.HasValue) + { + _configManager.SetVerboseLevel(verboseLevel.Value); + } + + if (sceneEnabled.HasValue) + { + _configManager.SetSceneEnabled(enabled: sceneEnabled.Value); + } + + if (scriptEnabled.HasValue) + { + _configManager.SetScriptEnabled(enabled: scriptEnabled.Value); + } + + if (autoSaverEnabled.HasValue) + { + _configManager.SetAutoSaverEnabled(enabled: autoSaverEnabled.Value, noEmitSignal: noEmitSignal); + } + + _configManager.SaveSettings(); + + if (_configManager.IsAutoSaverEnabled) + { + _autoSaveManager.Reactivate(); + } + else + { + _autoSaveManager.Deactivate(); + } + } +} diff --git a/addons/autosaver_editor/icon_autosaver.png b/addons/autosaver_editor/icon_autosaver.png new file mode 100644 index 0000000..00124a8 Binary files /dev/null and b/addons/autosaver_editor/icon_autosaver.png differ diff --git a/addons/autosaver_editor/icon_autosaver.png.import b/addons/autosaver_editor/icon_autosaver.png.import new file mode 100644 index 0000000..c9e0a32 --- /dev/null +++ b/addons/autosaver_editor/icon_autosaver.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b1v64q61ia0mg" +path="res://.godot/imported/icon_autosaver.png-55c98d7fbebfa4f3682998528b7c512a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/autosaver_editor/icon_autosaver.png" +dest_files=["res://.godot/imported/icon_autosaver.png-55c98d7fbebfa4f3682998528b7c512a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/autosaver_editor/plugin.cfg b/addons/autosaver_editor/plugin.cfg new file mode 100644 index 0000000..8bd5b4c --- /dev/null +++ b/addons/autosaver_editor/plugin.cfg @@ -0,0 +1,6 @@ +[plugin] +name="AutoSaver Toggle for Godot Editor (C#)" +description="Auto Saver Toggle for Godot Editor: a peace of mind toggle to save your workspace automatically" +author="Victor R. R. Avalos" +version="0.1.0" +script="AutoSaverEditorPlugin.cs" diff --git a/addons/autosaver_editor/settings.ini b/addons/autosaver_editor/settings.ini new file mode 100644 index 0000000..e0569c9 --- /dev/null +++ b/addons/autosaver_editor/settings.ini @@ -0,0 +1,11 @@ +[autosaver_editor] + +interval=60 +verbose="OFF" +enabled=false +postpone_time=1 +activity_check=1 +autosave_scene=true +autosave_gdscript=true +use_gd_editor_autosave_interval=true +use_gd_editor_save_on_focus_loss=false diff --git a/project.godot b/project.godot index 8031acb..dddcd7e 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,8 @@ config_version=5 [application] config/name="导师模拟器" -config/features=PackedStringArray("4.3", "GL Compatibility") +run/main_scene="res://node_2d.tscn" +config/features=PackedStringArray("4.3", "C#", "GL Compatibility") config/icon="res://icon.svg" [autoload] @@ -34,7 +35,7 @@ version_control/autoload_on_startup=true [editor_plugins] -enabled=PackedStringArray("res://addons/dialogic/plugin.cfg") +enabled=PackedStringArray("res://addons/autosaver_editor/plugin.cfg", "res://addons/dialogic/plugin.cfg") [input] diff --git a/导师模拟器.csproj b/导师模拟器.csproj new file mode 100644 index 0000000..53cc1f1 --- /dev/null +++ b/导师模拟器.csproj @@ -0,0 +1,8 @@ + + + net6.0 + net7.0 + net8.0 + true + + \ No newline at end of file diff --git a/导师模拟器.csproj.old b/导师模拟器.csproj.old new file mode 100644 index 0000000..880eee4 --- /dev/null +++ b/导师模拟器.csproj.old @@ -0,0 +1,8 @@ + + + net6.0 + net7.0 + net8.0 + true + + \ No newline at end of file diff --git a/导师模拟器.sln b/导师模拟器.sln new file mode 100644 index 0000000..c8cefc4 --- /dev/null +++ b/导师模拟器.sln @@ -0,0 +1,19 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "导师模拟器", "导师模拟器.csproj", "{9E17CC7B-7EE8-4BE9-AE6F-CB46D9D366A7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + ExportDebug|Any CPU = ExportDebug|Any CPU + ExportRelease|Any CPU = ExportRelease|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9E17CC7B-7EE8-4BE9-AE6F-CB46D9D366A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E17CC7B-7EE8-4BE9-AE6F-CB46D9D366A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E17CC7B-7EE8-4BE9-AE6F-CB46D9D366A7}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU + {9E17CC7B-7EE8-4BE9-AE6F-CB46D9D366A7}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU + {9E17CC7B-7EE8-4BE9-AE6F-CB46D9D366A7}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU + {9E17CC7B-7EE8-4BE9-AE6F-CB46D9D366A7}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU + EndGlobalSection +EndGlobal