Add autosave plugin

This commit is contained in:
wjsjwr 2024-11-19 00:32:10 +08:00
parent 57551683b6
commit a19a1fd19f
27 changed files with 1620 additions and 2 deletions

View File

@ -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<Texture2D>(_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<IAutoSaveManager>();
_configManager = ServiceProvider.GetService<IConfigurationManager>();
}
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

View File

@ -0,0 +1,14 @@
using System.Text;
namespace AutoSaverPlugin.Contracts;
internal interface IAutoSaveManager
{
void Initialize(AutoSaverEditorPlugin plugin);
void Activate();
void Reactivate();
void Deactivate();
}

View File

@ -0,0 +1,47 @@
using System;
using System.Text;
using AutoSaverPlugin.Shared;
namespace AutoSaverPlugin.Contracts;
internal interface IConfigurationManager
{
event Action<bool> 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);
}

View File

@ -0,0 +1,16 @@
using System.Collections.Generic;
using Godot;
namespace AutoSaverPlugin.Contracts;
internal interface IStatusReporter
{
List<string> FetchModifiedItems();
}
internal interface ISceneStatusReporter : IStatusReporter
{ }
internal interface IGDScriptStatusReporter : IStatusReporter
{ }

View File

@ -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();
}

View File

@ -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.

Binary file not shown.

View File

@ -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<string>();
var modifiedScripts = _configManager.IsOptionSaveScriptsEnabled ? GetModifiedItems(_gdScriptReporter.FetchModifiedItems()) : new List<string>();
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<string> modifiedScenes)
{
List<string> 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<string> 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<string> GetModifiedItems(List<string> items)
{
var modifiedScripts = new List<string>();
foreach (var i in items)
{
if (i.Contains("*"))
{
modifiedScripts.Add(i.Replace("(*)", ""));
}
}
return modifiedScripts;
}
private void LogAutosaveResult(int numFilesSaved, List<string> 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}");
}
}

View File

@ -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<bool> 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;
}
}

View File

@ -0,0 +1,51 @@
using System;
using AutoSaverPlugin.Shared;
using Godot;
namespace AutoSaverPlugin.Services.GDComponent;
internal partial class UserActivityMonitorNode : Node
{
public event Action<string, float> UserActivityDetected;
private bool _isMonitoring = false;
private DateTime? _lastActivityTimeUtc;
private readonly ILoggerService _logger = ServiceProvider.GetService<ILoggerService>();
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;
}
}

View File

@ -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<string> FetchModifiedItems()
{
_logger.LogDebug("Fetching modified scripts...");
ScriptEditor editor = EditorInterface.Singleton.GetScriptEditor();
List<string> 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;
}
}
}

View File

@ -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<string> FetchModifiedItems()
{
_logger.LogDebug("Fetching modified scenes...");
var tabTitles = new List<string>();
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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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<string> GetAllProjectFiles(string extFileFilter = null)
{
List<string> files = new List<string>();
EditorInterface editorInterface = EditorInterface.Singleton;
EditorFileSystem fileSystem = editorInterface.GetResourceFilesystem();
fileSystem.Scan();
GetFilesRecursive(fileSystem.GetFilesystem(), files, extFileFilter);
return files;
}
private static void GetFilesRecursive(EditorFileSystemDirectory directory, List<string> 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);
}
}
}
}

View File

@ -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);
}

View File

@ -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";
}
}

View File

@ -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<Type, Func<object>> _services = new();
private static readonly Dictionary<Type, object> _singletonInstances = new();
public static void RegisterServiceAsTransient<TInterface, TImplementation>() where TImplementation : class, TInterface
{
_services[typeof(TInterface)] = () => CreateInstance<TImplementation>();
}
public static void RegisterServiceAsSingleton<TInterface, TImplementation>() where TImplementation : class, TInterface, new()
{
_services[typeof(TInterface)] = () => GetOrCreateSingletonInstance(typeof(TInterface), () => new TImplementation());
}
public static void RegisterServiceAsSingleton<TInterface>(Func<TInterface> factory)
{
_services[typeof(TInterface)] = () => GetOrCreateSingletonInstance(typeof(TInterface), () => factory());
}
public static T GetService<T>() 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<T>() 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<object> factory)
{
if (!_singletonInstances.TryGetValue(type, out var instance))
{
instance = factory();
_singletonInstances[type] = instance;
}
return instance;
}
public static void Initialize()
{
RegisterServiceAsSingleton<ILoggerService, Logger>();
RegisterServiceAsSingleton<IConfigurationManager>(() => new ConfigurationManager(GetService<ILoggerService>()));
RegisterServiceAsSingleton<ISceneStatusReporter>(() => new SceneTabStatusReporter(GetService<ILoggerService>()));
RegisterServiceAsSingleton<IGDScriptStatusReporter>(() => new GDScriptStatusReporter(GetService<ILoggerService>()));
RegisterServiceAsTransient<ITimerService, TimerService>();
RegisterServiceAsSingleton<IAutoSaveManager>(() => new AutoSaveManager(
GetService<ISceneStatusReporter>(),
GetService<IGDScriptStatusReporter>(),
GetService<IConfigurationManager>(),
GetService<ILoggerService>(),
GetService<ITimerService>(),
GetService<ITimerService>()
));
}
}

View File

@ -0,0 +1,86 @@
using System.Text;
using AutoSaverPlugin.Contracts;
using AutoSaverPlugin.Shared;
using Godot;
namespace AutoSaverPlugin.UI;
/// <summary>
/// Builds and manages the toggle button for the Auto Save feature in the top menu.
/// </summary>
public class AutoSaveToggleMenuBuilder
{
private CheckButton _autoSaveToggleButton;
private readonly IAutoSaveManager _autoSaveManager = ServiceProvider.GetService<IAutoSaveManager>();
private readonly IConfigurationManager _configManager = ServiceProvider.GetService<IConfigurationManager>();
/// <summary>
/// Initializes a new instance of the <see cref="AutoSaveToggleMenuBuilder"/> class.
/// </summary>
public AutoSaveToggleMenuBuilder()
{
Build();
}
/// <summary>
/// Gets the Auto Save toggle button with configured settings and event handlers.
/// </summary>
public CheckButton AutoSaveToggleButton => Build();
/// <summary>
/// Creates and initializes the Auto Save toggle button for the top menu.
/// </summary>
/// <returns>The initialized Auto Save toggle button.</returns>
private CheckButton Build()
{
if (_autoSaveToggleButton != null)
return _autoSaveToggleButton;
_autoSaveToggleButton = new CheckButton
{
Text = "Auto Save"
};
_autoSaveToggleButton.Toggled += HandleAutoSaveToggleChanged;
_autoSaveToggleButton.SetPressedNoSignal(_configManager.IsAutoSaverEnabled);
return _autoSaveToggleButton;
}
/// <summary>
/// Detaches the toggle event subscriptions to prevent memory leaks.
/// </summary>
internal void DetachAutoSaveToggleEvents()
{
_autoSaveToggleButton.Toggled -= HandleAutoSaveToggleChanged;
}
/// <summary>
/// Handles changes to the Auto Save toggle, updating settings and service state accordingly.
/// </summary>
/// <param name="toggledOn">Indicates whether the feature is being enabled or disabled.</param>
private void HandleAutoSaveToggleChanged(bool toggledOn)
{
_configManager.SetAutoSaverEnabled(enabled: toggledOn);
_configManager.SaveSettings();
if (toggledOn)
{
_autoSaveManager.Reactivate();
}
else
{
_autoSaveManager.Deactivate();
}
}
/// <summary>
/// Updates the toggle state based on external settings invocation.
/// </summary>
/// <param name="trace">The trace builder for logging.</param>
/// <param name="enabled">Indicates the new enabled state.</param>
internal void UpdateToggleStateFromSettings(bool enabled)
{
_autoSaveToggleButton.SetPressedNoSignal(enabled);
}
}

View File

@ -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<IAutoSaveManager>();
private readonly IConfigurationManager _configManager = ServiceProvider.GetService<IConfigurationManager>();
private readonly ILoggerService _logger = ServiceProvider.GetService<ILoggerService>();
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();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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]

8
导师模拟器.csproj Normal file
View File

@ -0,0 +1,8 @@
<Project Sdk="Godot.NET.Sdk/4.3.0">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net7.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,8 @@
<Project Sdk="Godot.NET.Sdk/4.3.0-limboai-v1.2.2.gha">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net7.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
</Project>

19
导师模拟器.sln Normal file
View File

@ -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