Explorar o código

Separated out autosave control updates from autosave logic, I don't really like how it turned out tho

Equbuxu hai 1 ano
pai
achega
fb28d4372d

+ 2 - 0
src/PixiEditor.sln.DotSettings

@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Autosave/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

+ 1 - 1
src/PixiEditor/Helpers/DeadlockDetectionHelper.cs

@@ -146,7 +146,7 @@ internal class DeadlockDetectionHelper
             var list = new List<AutosaveFilePathInfo>();
             foreach (var document in viewModel.DocumentManagerSubViewModel.Documents)
             {
-                document.AutosaveViewModel.PanicAutosave();
+                document.AutosaveViewModel.PanicAutosaveFromDeadlockDetector();
                 if (document.AutosaveViewModel.LastSavedPath != null || document.FullFilePath != null)
                 {
                     list.Add(new AutosaveFilePathInfo(document.FullFilePath, document.AutosaveViewModel.LastSavedPath));

+ 0 - 1
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -39,7 +39,6 @@ internal class ActionAccumulator
         queuedActions.AddRange(actions);
         queuedActions.Add(new ChangeBoundary_Action());
         TryExecuteAccumulatedActions();
-        document.AutosaveViewModel.HintFinishedAction();
     }
 
     public void AddActions(params IAction[] actions)

+ 3 - 0
src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs

@@ -16,6 +16,7 @@ internal class ChangeExecutionController
     public VecI LastPixelPosition => lastPixelPos;
     public VecD LastPrecisePosition => lastPrecisePos;
     public bool IsChangeActive => currentSession is not null;
+    public event EventHandler? UpdateableChangeEnded;
 
     private readonly DocumentViewModel document;
     private readonly DocumentInternalParts internals;
@@ -97,6 +98,7 @@ internal class ChangeExecutionController
             throw new InvalidOperationException();
         currentSession = null;
         _queuedExecutor = null;
+        UpdateableChangeEnded?.Invoke(this, EventArgs.Empty);
     }
 
     public bool TryStopActiveExecutor()
@@ -105,6 +107,7 @@ internal class ChangeExecutionController
             return false;
         currentSession.ForceStop();
         currentSession = null;
+        UpdateableChangeEnded?.Invoke(this, EventArgs.Empty);
         return true;
     }
 

+ 8 - 0
src/PixiEditor/PixiEditor.csproj

@@ -172,6 +172,14 @@
 		<None Update="Extensions\readme.txt">
 		  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 		</None>
+		<Page Update="Views\UserControls\AutosaveControl\AutosaveControl.xaml">
+		  <Generator>MSBuild:Compile</Generator>
+		  <XamlRuntime>Wpf</XamlRuntime>
+		  <SubType>Designer</SubType>
+		</Page>
+		<Page Update="Views\UserControls\AutosaveControl.xaml">
+		  <Generator>MSBuild:Compile</Generator>
+		</Page>
 	</ItemGroup>
 
 	<ItemGroup>

+ 1 - 1
src/PixiEditor/Styles/TreeViewStyle.xaml

@@ -128,7 +128,7 @@
     <Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource TreeViewItemStyle}">
         <Style.Triggers>
             <Trigger Property="HasItems" Value="True">
-                <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
+                <Setter Property="IsExpanded" Value="{Binding IsForceExpanded, Mode=TwoWay}" />
             </Trigger>
         </Style.Triggers>
         <Setter Property="HorizontalContentAlignment" Value="Stretch" />

+ 98 - 190
src/PixiEditor/ViewModels/SubViewModels/Document/AutosaveDocumentViewModel.cs

@@ -1,115 +1,73 @@
 using System.IO;
 using System.Windows;
-using System.Windows.Media;
 using System.Windows.Threading;
-using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.UserPreferences;
 using PixiEditor.Helpers;
+using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.IO;
+using PixiEditor.Views.UserControls;
 
 namespace PixiEditor.ViewModels.SubViewModels.Document;
 
+
 internal class AutosaveDocumentViewModel : NotifyableObject
 {
     private readonly DispatcherTimer savingTimer;
-    private readonly DispatcherTimer updateTextTimer;
     private readonly DispatcherTimer busyTimer;
-    private bool saveAfterNextFinish;
     private int savingFailed;
-    private DateTime nextSave;
     private Guid tempGuid;
-    private bool documentEnabled = true;
-
-    private const string ClockIcon = "\ue84d";
-    private const string WarnIcon = "\ue81e";
-    private const string SaveIcon = "\ue8bc";
-    private const string PauseIcon = "\ue8a2";
-    private const string SavingIcon = "\ue864";
-
-    private readonly Brush errorBrush = new SolidColorBrush(Color.FromArgb(255, 214, 66, 56));
-    private readonly Brush warnBrush = new SolidColorBrush(Color.FromArgb(255, 219, 189, 53));
-    private readonly Brush successBrush = new SolidColorBrush(Color.FromArgb(255, 83, 207, 72));
-    private readonly Brush activeBrush = new SolidColorBrush(Color.FromArgb(255, 255, 255, 255));
-    private readonly Brush inactiveBrush = new SolidColorBrush(Color.FromArgb(255, 120, 120, 120));
+    private bool autosaveEnabled = true;
+    private bool waitingForUpdateableChangeEnd = false;
+    private LastAutosaveData? lastAutosaveData = null;
+    private DateTime? autosaveLaunchDateTime = null;
     
     private DocumentViewModel Document { get; }
-
-    private double AutosavePeriodMinutes { get; set; } = -1;
-
-    private bool SaveToDocumentPath => IPreferences.Current!.GetPreference(PreferencesConstants.AutosaveToDocumentPath, PreferencesConstants.AutosaveToDocumentPathDefault);
-
-    private LocalizedString mainMenuText;
-    
-    public LocalizedString MainMenuText
-    {
-        get => mainMenuText; 
-        set => SetProperty(ref mainMenuText, value);
-    }
     
+    private double AutosavePeriodMinutes { get; set; } = -1;
     
-    private bool isImportantText;
-
-    public bool IsImportantText
-    {
-        get => isImportantText;
-        set => SetProperty(ref isImportantText, value);
-    }
-
-    private string mainMenuIconText;
-
-    public string MainMenuIconText
-    {
-        get => mainMenuIconText;
-        set => SetProperty(ref mainMenuIconText, value);
-    }
-
-    private Brush mainMenuBrush;
-
-    public Brush MainMenuBrush
+    private AutosaveStateData autosaveStateData;
+    public AutosaveStateData AutosaveStateData
     {
-        get => mainMenuBrush;
-        set => SetProperty(ref mainMenuBrush, value);
-    }
-
-    private bool mainMenuPulse;
-
-    public bool MainMenuPulse
-    {
-        get => mainMenuPulse;
-        set => SetProperty(ref mainMenuPulse, value);
+        get => autosaveStateData;
+        set => SetProperty(ref autosaveStateData, value);
     }
 
     public bool Enabled
     {
-        get => documentEnabled;
+        get => autosaveEnabled;
         set
         {
-            if (documentEnabled == value)
+            if (autosaveEnabled == value)
                 return;
             
             AutosavePeriodChanged(
-                IPreferences.Current.GetPreference(
+                IPreferences.Current!.GetPreference(
                     PreferencesConstants.AutosavePeriodMinutes, 
                     PreferencesConstants.AutosavePeriodDefault),
                 value);
-            SetProperty(ref documentEnabled, value);
+            SetProperty(ref autosaveEnabled, value);
         }
     }
 
     public string LastSavedPath { get; private set; }
     
-    public AutosaveDocumentViewModel(DocumentViewModel document)
+    public static bool SaveStateEnabled => IPreferences.Current!.GetPreference(PreferencesConstants.SaveSessionStateEnabled, PreferencesConstants.SaveSessionStateDefault);
+    
+    private bool SaveToDocumentPath => IPreferences.Current!.GetPreference(PreferencesConstants.AutosaveToDocumentPath, PreferencesConstants.AutosaveToDocumentPathDefault);
+    
+    public AutosaveDocumentViewModel(DocumentViewModel document, DocumentInternalParts internals)
     {
         Document = document;
         tempGuid = Guid.NewGuid();
 
         var dispatcher = Application.Current.Dispatcher;
         
-        updateTextTimer = new DispatcherTimer(DispatcherPriority.Normal, dispatcher) { Interval = TimeSpan.FromSeconds(3.8) };
-        updateTextTimer.Tick += (_, _) => SetAutosaveText();
-
         savingTimer = new DispatcherTimer(DispatcherPriority.Normal);
-        savingTimer.Tick += (_, _) => { savingTimer.Stop(); TryAutosave(); };
+        savingTimer.Tick += (_, _) =>
+        {
+            savingTimer.Stop(); 
+            TryAutosave();
+        };
 
         busyTimer = new DispatcherTimer(DispatcherPriority.Normal, dispatcher) { Interval = TimeSpan.FromMilliseconds(80) };
         busyTimer.Tick += (_, _) =>
@@ -118,73 +76,56 @@ internal class AutosaveDocumentViewModel : NotifyableObject
             Document.Busy = true;
         };
 
+        internals.ChangeController.UpdateableChangeEnded += OnUpdateableChangeEnded;
+
         var preferences = IPreferences.Current;
         
-        preferences!.AddCallback<double>(PreferencesConstants.AutosavePeriodMinutes, (v) => AutosavePeriodChanged(v, documentEnabled));
-        AutosavePeriodChanged(preferences.GetPreference(PreferencesConstants.AutosavePeriodMinutes, PreferencesConstants.AutosavePeriodDefault), documentEnabled);
+        preferences!.AddCallback<double>(PreferencesConstants.AutosavePeriodMinutes, (v) => AutosavePeriodChanged(v, autosaveEnabled));
+        AutosavePeriodChanged(preferences.GetPreference(PreferencesConstants.AutosavePeriodMinutes, PreferencesConstants.AutosavePeriodDefault), autosaveEnabled);
     }
 
-    public static bool AutosavingEnabled =>
-        (int)IPreferences.Current!.GetPreference(PreferencesConstants.AutosavePeriodMinutes, PreferencesConstants.AutosavePeriodDefault) != -1;
-
-    public static bool SaveStateEnabled => IPreferences.Current!.GetPreference(PreferencesConstants.SaveSessionStateEnabled, PreferencesConstants.SaveSessionStateDefault);
-
-    public void HintFinishedAction()
+    private AutosaveStateData CreateAutosaveStateData()
     {
-        if (!saveAfterNextFinish)
-            return;
-
-        saveAfterNextFinish = false;
-        
-        SafeAutosave(true);
+        return new AutosaveStateData
+        {
+            LastAutosaveData = lastAutosaveData,
+            AutosaveLaunchDateTime = autosaveLaunchDateTime ?? DateTime.Now,
+            AutosaveInterval = TimeSpan.FromMinutes(AutosavePeriodMinutes),
+            AutosaveState = AutosaveState.Paused
+        };
     }
 
-    private void SetAutosaveText()
+    public static void AutosaveOnClose()
     {
-        var timeLeft = nextSave - DateTime.Now;
-
-        if (timeLeft.Minutes == 0)
-        {
-            UpdateMainMenuTextSave("AUTOSAVE_SAVING_IN_MINUTE", false, ClockIcon, inactiveBrush, false);
-            return;
-        }
-
-        var adjusted = timeLeft.Add(TimeSpan.FromSeconds(30));
         
-        var minute = adjusted.Minutes < 2
-            ? new LocalizedString("MINUTE_SINGULAR")
-            : new LocalizedString("MINUTE_PLURAL");
-
-        UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_SAVING_IN", adjusted.Minutes.ToString(), minute), false, ClockIcon, inactiveBrush, false);
     }
 
     public void TryAutosave(bool saveUserFileIfEnabled = true)
     {
-        if (Document.UpdateableChangeActive)
+        if (Document.AllChangesSaved)
         {
-            saveAfterNextFinish = true;
-            
-            savingTimer.Stop();
-            updateTextTimer.Stop();
-            
-            UpdateMainMenuTextSave("AUTOSAVE_WAITING_FOR_SAVE", false, SaveIcon, activeBrush, true);
+            RestartTimers();
             
+            lastAutosaveData = new LastAutosaveData()
+            {
+                Time = DateTime.Now,
+                BackupSaveResult = BackupAutosaveResult.NothingToSave,
+                UserFileSaveResult = UserFileAutosaveResult.NothingToSave
+            };
+            AutosaveStateData = CreateAutosaveStateData() with { AutosaveState = AutosaveState.Idle };
             return;
         }
 
-        if (Document.AllChangesSaved)
+        if (Document.UpdateableChangeActive)
         {
-            updateTextTimer.Stop();
-            RestartTimers();
-            UpdateMainMenuTextSave("AUTOSAVE_NOTHING_CHANGED", false, SaveIcon, inactiveBrush, false);
+            waitingForUpdateableChangeEnd = true;
             return;
         }
 
-        updateTextTimer.Stop();
         SafeAutosave(saveUserFileIfEnabled);
     }
 
-    public void PanicAutosave()
+    public void PanicAutosaveFromDeadlockDetector()
     {
         string filePath = Path.Join(Paths.PathToUnsavedFilesFolder, $"autosave-{tempGuid}.pixi");
         Directory.CreateDirectory(Directory.GetParent(filePath)!.FullName);
@@ -197,22 +138,24 @@ internal class AutosaveDocumentViewModel : NotifyableObject
         }
     }
 
-    private void SafeAutosave(bool saveUserFile)
+    private async void SafeAutosave(bool saveUserFile)
     {
         try
         {
-            Autosave(saveUserFile);
+            await Autosave(saveUserFile);
         }
         catch (Exception e)
         {
             savingFailed++;
             
-            var minute = AutosavePeriodMinutes <= 1
-                ? new LocalizedString("MINUTE_SINGULAR")
-                : new LocalizedString("MINUTE_PLURAL");
-
-            UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_FAILED_RETRYING", AutosavePeriodMinutes.ToString("0"), minute), true, WarnIcon, warnBrush, true);
-        
+            lastAutosaveData = new LastAutosaveData()
+            {
+                Time = DateTime.Now,
+                BackupSaveResult = BackupAutosaveResult.Error,
+                UserFileSaveResult = UserFileAutosaveResult.ExceptionWhileSaving
+            };
+            AutosaveStateData = CreateAutosaveStateData() with { AutosaveState = AutosaveState.Idle };
+            
             busyTimer.Stop();
             Document.Busy = false;
 
@@ -225,11 +168,9 @@ internal class AutosaveDocumentViewModel : NotifyableObject
         }
     }
     
-    private void Autosave(bool saveUserFile)
+    private async Task Autosave(bool saveUserFile)
     {
-        saveAfterNextFinish = false;
-        
-        UpdateMainMenuTextSave("AUTOSAVE_SAVING", true, SavingIcon, activeBrush, true);
+        AutosaveStateData = CreateAutosaveStateData() with { AutosaveState = AutosaveState.InProgress };
 
         string filePath = Path.Join(Paths.PathToUnsavedFilesFolder, $"autosave-{tempGuid}.pixi");
         Directory.CreateDirectory(Directory.GetParent(filePath)!.FullName);
@@ -237,16 +178,22 @@ internal class AutosaveDocumentViewModel : NotifyableObject
         busyTimer.Start();
         var result = Exporter.TrySave(Document, filePath);
 
+        UserFileAutosaveResult userFileSaveResult = UserFileAutosaveResult.Disabled;
+        
         if (result == SaveResult.Success)
         {
             if (saveUserFile && SaveToDocumentPath && Document.FullFilePath != null)
             {
-                CopyTemp(filePath);
+                userFileSaveResult = await CopyTempToUserFile(filePath);
             }
-            else
+            
+            lastAutosaveData = new LastAutosaveData
             {
-                UpdateMainMenuTextSave("AUTOSAVE_SAVED", true, SaveIcon, successBrush, false);
-            }
+                Time = DateTime.Now,
+                BackupSaveResult = BackupAutosaveResult.Success,
+                UserFileSaveResult = userFileSaveResult
+            };
+            AutosaveStateData = CreateAutosaveStateData() with { AutosaveState = AutosaveState.Idle, LastAutosaveData = lastAutosaveData };
             
             Document.MarkAsAutosaved();
             LastSavedPath = filePath;
@@ -256,17 +203,20 @@ internal class AutosaveDocumentViewModel : NotifyableObject
             busyTimer.Stop();
             Document.Busy = false;
             
-            var minute = AutosavePeriodMinutes <= 1
-                ? new LocalizedString("MINUTE_SINGULAR")
-                : new LocalizedString("MINUTE_PLURAL");
+            lastAutosaveData = new LastAutosaveData()
+            {
+                Time = DateTime.Now,
+                BackupSaveResult = BackupAutosaveResult.Error,
+                UserFileSaveResult = userFileSaveResult
+            };    
+            AutosaveStateData = CreateAutosaveStateData() with { AutosaveState = AutosaveState.Idle, LastAutosaveData = lastAutosaveData };
             
-            UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_FAILED_RETRYING", AutosavePeriodMinutes.ToString("0"), minute), true, WarnIcon, warnBrush, true);
             savingFailed++;
 
             if (savingFailed < 3)
             {
-                int _savingFailed = savingFailed;
-                Task.Run(() => CrashHelper.SendExceptionInfoToWebhook(new Exception($"Failed to autosave for the {_savingFailed}. time due to {result}")));
+                int savingFailedCopy = savingFailed;
+                Task.Run(() => CrashHelper.SendExceptionInfoToWebhook(new Exception($"Failed to autosave for the {savingFailedCopy}. time due to {result}")));
             }
         }
         
@@ -276,45 +226,29 @@ internal class AutosaveDocumentViewModel : NotifyableObject
         RestartTimers();
     }
 
-    private void CopyTemp(string tempPath)
+    private async Task<UserFileAutosaveResult> CopyTempToUserFile(string tempPath)
     {
-        if (File.Exists(Document.FullFilePath))
-        {
-            UpdateMainMenuTextSave("AUTOSAVE_PLEASE_RESAVE", true, SaveIcon, errorBrush, true);
-        }
+        if (!File.Exists(Document.FullFilePath))
+            return UserFileAutosaveResult.NoUserFile;
         
-        Task.Run(Copy);
+        var result = await Task.Run(Copy);
+        Document.MarkAsSaved();
+        return result;
         
-        void Copy()
+        UserFileAutosaveResult Copy()
         {
             try
             {
                 File.Copy(tempPath, Document.FullFilePath!, true);
-                Application.Current.Dispatcher.Invoke(() =>
-                {
-                    Document.MarkAsSaved();
-                    UpdateMainMenuTextSave("AUTOSAVE_SAVED", true, SaveIcon, successBrush, false);
-                });
+                return UserFileAutosaveResult.Success;
             }
             catch (Exception e) when (e is UnauthorizedAccessException or DirectoryNotFoundException)
             {
-                Application.Current.Dispatcher.Invoke(() =>
-                {
-                    UpdateMainMenuTextSave("AUTOSAVE_PLEASE_RESAVE", true, SaveIcon, errorBrush, true);
-                });
+                return UserFileAutosaveResult.NoUserFile;
             }
             catch
             {
-                var minute = AutosavePeriodMinutes <= 1
-                    ? new LocalizedString("MINUTE_SINGULAR")
-                    : new LocalizedString("MINUTE_PLURAL");
-
-                Application.Current.Dispatcher.Invoke(() =>
-                {
-                    UpdateMainMenuTextSave(
-                        new LocalizedString("AUTOSAVE_FAILED_RETRYING", AutosavePeriodMinutes.ToString("0"), minute),
-                        true, WarnIcon, warnBrush, true);
-                });
+                return UserFileAutosaveResult.ExceptionWhileSaving;
             }
         }
     }
@@ -322,35 +256,19 @@ internal class AutosaveDocumentViewModel : NotifyableObject
     private void RestartTimers()
     {
         savingTimer.Start();
-        nextSave = DateTime.Now + savingTimer.Interval;
-        updateTextTimer.Start();
     }
 
     private void AutosavePeriodChanged(double minutes, bool documentEnabled)
     {
         if ((int)minutes == -1 || !documentEnabled)
         {
-            savingTimer.IsEnabled = false;
-            updateTextTimer.IsEnabled = false;
-            saveAfterNextFinish = false;
-
-            LocalizedString menuText = documentEnabled ? string.Empty : "AUTOSAVE_DISABLED";
-            string iconText = documentEnabled ? null : PauseIcon;
-            
-            UpdateMainMenuTextSave(menuText, false, iconText, activeBrush, false);
-
             AutosavePeriodMinutes = minutes;
+            AutosaveStateData = CreateAutosaveStateData();
             return;
         }
         
-        var timerEnabled = savingTimer.IsEnabled;
+        var timerEnabled = savingTimer.IsEnabled || (int)AutosavePeriodMinutes == -1 || !Enabled;
 
-        if ((int)AutosavePeriodMinutes == -1 || !Enabled)
-        {
-            timerEnabled = true;
-            updateTextTimer.Start();
-        }
-        
         savingTimer.IsEnabled = false;
 
         var timeSpan = TimeSpan.FromMinutes(minutes);
@@ -358,12 +276,11 @@ internal class AutosaveDocumentViewModel : NotifyableObject
         AutosavePeriodMinutes = minutes;
         
         savingTimer.IsEnabled = timerEnabled;
+    }
+
+    private void OnUpdateableChangeEnded(object? sender, EventArgs args)
+    {
         
-        nextSave = DateTime.Now + timeSpan;
-        if (updateTextTimer.IsEnabled)
-        {
-            SetAutosaveText();
-        }
     }
 
     public void SetTempFileGuidAndLastSavedPath(Guid guid, string lastSavedPath)
@@ -371,13 +288,4 @@ internal class AutosaveDocumentViewModel : NotifyableObject
         tempGuid = guid;
         LastSavedPath = lastSavedPath;
     }
-
-    private void UpdateMainMenuTextSave(LocalizedString text, bool isImportantText, string iconText, Brush brush, bool pulse)
-    {
-        MainMenuText = text;
-        IsImportantText = isImportantText;
-        MainMenuIconText = iconText;
-        MainMenuBrush = brush;
-        MainMenuPulse = pulse;
-    }
 }

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -168,7 +168,7 @@ internal partial class DocumentViewModel : NotifyableObject
         StructureHelper = new DocumentStructureModule(this);
         EventInlet = new DocumentEventsModule(this, Internals);
         Operations = new DocumentOperationsModule(this, Internals);
-        AutosaveViewModel = new AutosaveDocumentViewModel(this);
+        AutosaveViewModel = new AutosaveDocumentViewModel(this, Internals);
 
         StructureRoot = new FolderViewModel(this, Internals, Internals.Tracker.Document.StructureRoot.GuidValue);
 

+ 2 - 1
src/PixiEditor/Views/MainWindow.xaml

@@ -495,10 +495,11 @@
                     </Grid>
                 </Border>
                 <usercontrols:AutosaveControl
+                    AutosaveStateData="{Binding DocumentManagerSubViewModel.ActiveDocument.AutosaveViewModel.AutosaveStateData}"
+                    AutosaveEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument.AutosaveViewModel.Enabled}"
                     MinWidth="50"
                     Height="25"
                     Margin="5,0,0,0"
-                    DataContext="{Binding DocumentManagerSubViewModel.ActiveDocument.AutosaveViewModel}"
                     VerticalAlignment="Top"
                     WindowChrome.IsHitTestVisibleInChrome="True"/>
                 <StackPanel

+ 7 - 8
src/PixiEditor/Views/UserControls/AutosaveControl.xaml

@@ -11,7 +11,7 @@
              xmlns:document="clr-namespace:PixiEditor.ViewModels.SubViewModels.Document"
              mc:Ignorable="d"
              d:DesignWidth="280" d:DesignHeight="25"
-             d:DataContext="{d:DesignInstance document:AutosaveDocumentViewModel}">
+             d:DataContext="{d:DesignInstance local:AutosaveControl}">
     <Border Padding="8, 0"
             CornerRadius="5"
             BorderThickness="0"
@@ -35,16 +35,16 @@
                 <ColumnDefinition />
             </Grid.ColumnDefinitions>
             <TextBlock
-                Text="{Binding MainMenuIconText, FallbackValue='', Mode=OneWay}"
+                Text="{Binding IconText, FallbackValue='', Mode=OneWay}"
                 FontFamily="{StaticResource Feather}"
                 FontSize="16" Margin="0,1,5,0"
                 VerticalAlignment="Center"
-                Foreground="{Binding MainMenuBrush, Mode=OneWay}">
+                Foreground="{Binding IconBrush, Mode=OneWay}">
                 <TextBlock.Style>
                     <Style TargetType="TextBlock">
                         <Style.Triggers>
                             <DataTrigger
-                                Binding="{Binding MainMenuPulse, Mode=OneWay}"
+                                Binding="{Binding PulseIcon, Mode=OneWay}"
                                 Value="True">
                                 <DataTrigger.EnterActions>
                                     <BeginStoryboard x:Name="storyboard">
@@ -87,14 +87,13 @@
                 </StackPanel.Resources>
                 <Button Command="{xaml:Command PixiEditor.Autosave.ToggleAutosave}">
                     <TextBlock
-                        Text="{Binding Enabled, Mode=OneWay, Converter={converters:BoolToValueConverter FalseValue='', TrueValue=''}}"
+                        Text="{Binding AutosaveEnabled, Mode=OneWay, Converter={converters:BoolToValueConverter FalseValue='', TrueValue=''}}"
                         FontFamily="{StaticResource Feather}" />
                 </Button>
             </StackPanel>
             <TextBlock
                 Grid.Column="2"
-                ui:Translator.LocalizedString="{Binding MainMenuText, Mode=OneWay}"
-                x:Name="autosaveText"
+                ui:Translator.LocalizedString="{Binding Text, Mode=OneWay}"
                 Foreground="White"
                 VerticalAlignment="Center">
                 <TextBlock.Style>
@@ -104,7 +103,7 @@
                                 <MultiDataTrigger.Conditions>
                                     <Condition Binding="{Binding IsMouseOver, ElementName=autosaveControl}"
                                                Value="False" />
-                                    <Condition Binding="{Binding IsImportantText}" Value="False" />
+                                    <Condition Binding="{Binding IsForceExpanded}" Value="False" />
                                 </MultiDataTrigger.Conditions>
                                 <Setter Property="Visibility" Value="Hidden" />
                             </MultiDataTrigger>

+ 258 - 2
src/PixiEditor/Views/UserControls/AutosaveControl.xaml.cs

@@ -1,11 +1,267 @@
-using System.Windows.Controls;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Threading;
+using PixiEditor.Extensions.Common.Localization;
 
 namespace PixiEditor.Views.UserControls;
 
-public partial class AutosaveControl : UserControl
+public enum AutosaveState
 {
+    Paused,
+    Idle,
+    AwaitingUpdateableChangeEnd,
+    InProgress
+}
+
+public enum UserFileAutosaveResult
+{
+    Success,
+    NoUserFile,
+    ExceptionWhileSaving,
+    Disabled,
+    NothingToSave
+}
+
+public enum BackupAutosaveResult
+{
+    Success,
+    Error,
+    NothingToSave
+}
+
+public struct LastAutosaveData
+{
+    public DateTime Time { get; set; }
+    
+    public UserFileAutosaveResult UserFileSaveResult { get; set; }
+    
+    public BackupAutosaveResult BackupSaveResult { get; set; }
+}
+
+public struct AutosaveStateData
+{
+    public LastAutosaveData? LastAutosaveData { get; set; }
+    public AutosaveState AutosaveState { get; set; }
+    public DateTime AutosaveLaunchDateTime { get; set; }
+    public TimeSpan AutosaveInterval { get; set; }
+}
+
+public partial class AutosaveControl : UserControl, INotifyPropertyChanged
+{
+    public static readonly DependencyProperty AutosaveStateDataProperty =
+        DependencyProperty.Register(nameof(AutosaveStateData), typeof(AutosaveStateData), typeof(AutosaveControl), new PropertyMetadata(OnStateChanged));
+    
+    public AutosaveStateData AutosaveStateData
+    {
+        get => (AutosaveStateData)GetValue(AutosaveStateDataProperty);
+        set => SetValue(AutosaveStateDataProperty, value);
+    }
+    
+    public static readonly DependencyProperty AutosaveEnabledProperty =
+        DependencyProperty.Register(nameof(AutosaveEnabled), typeof(AutosaveStateData), typeof(AutosaveControl));
+    
+    public bool AutosaveEnabled
+    {
+        get => (bool)GetValue(AutosaveEnabledProperty);
+        set => SetValue(AutosaveEnabledProperty, value);
+    }
+
+    public event PropertyChangedEventHandler PropertyChanged;
+    
+    private const string ClockIcon = "\ue84d";
+    private const string WarnIcon = "\ue81e";
+    private const string SaveIcon = "\ue8bc";
+    private const string PauseIcon = "\ue8a2";
+
+    private readonly Brush errorBrush = new SolidColorBrush(Color.FromArgb(255, 214, 66, 56));
+    private readonly Brush warnBrush = new SolidColorBrush(Color.FromArgb(255, 219, 189, 53));
+    private readonly Brush successBrush = new SolidColorBrush(Color.FromArgb(255, 83, 207, 72));
+    private readonly Brush activeBrush = new SolidColorBrush(Color.FromArgb(255, 255, 255, 255));
+    private readonly Brush inactiveBrush = new SolidColorBrush(Color.FromArgb(255, 120, 120, 120));
+    
+    private DispatcherTimer textUpdateTimer;
+    private const double timerIntervalSeconds = 3.8;
+
+    private string iconText;
+
+    public string IconText
+    {
+        get => iconText;
+        set => SetField(ref iconText, value);
+    }
+
+    private string text;
+
+    public LocalizedString Text
+    {
+        get => text;
+        set => SetField(ref text, value);
+    }
+
+    private bool isForceExpanded;
+
+    public bool IsForceExpanded
+    {
+        get => isForceExpanded;
+        set => SetField(ref isForceExpanded, value);
+    }
+
+    private Brush iconBrush;
+
+    public Brush IconBrush
+    {
+        get => iconBrush;
+        set => SetField(ref iconBrush, value);
+    }
+
+    private bool pulseIcon;
+
+    public bool PulseIcon
+    {
+        get => pulseIcon;
+        set => SetField(ref pulseIcon, value);
+    }
+    
     public AutosaveControl()
     {
         InitializeComponent();
+        DataContext = this;
+
+        textUpdateTimer = new DispatcherTimer(TimeSpan.FromSeconds(timerIntervalSeconds), DispatcherPriority.Normal, (_, _) => Update(), Application.Current.Dispatcher)
+        {
+            IsEnabled = true
+        };
+    }
+
+    private void Update()
+    {
+        var data = AutosaveStateData;
+        if (data.AutosaveState is AutosaveState.Paused)
+        {
+            UpdateTextSave("AUTOSAVE_DISABLED", false, PauseIcon, activeBrush, false);
+            return;
+        }
+        
+        if (data.LastAutosaveData is null)
+        {
+            SetWaitingToSave();
+            return;
+        }
+        
+        if (AutosaveStateData.AutosaveState is AutosaveState.Idle)
+        {
+            if ((DateTime.Now - data.LastAutosaveData.Value.Time).TotalSeconds < (timerIntervalSeconds - 0.1))
+            {
+                // just autosaved, show result
+                bool showingError = false;
+                if (data.LastAutosaveData.Value.UserFileSaveResult is UserFileAutosaveResult.NoUserFile)
+                {
+                    UpdateTextSave("AUTOSAVE_PLEASE_RESAVE", true, SaveIcon, errorBrush, true);
+                    showingError = true;
+                }
+                if (data.LastAutosaveData.Value.BackupSaveResult is BackupAutosaveResult.Error || 
+                    data.LastAutosaveData.Value.UserFileSaveResult is UserFileAutosaveResult.ExceptionWhileSaving)
+                {
+                    SetWaitingToSave();
+                    showingError = true;
+                }
+                if (showingError)
+                    return;
+
+                if (data.LastAutosaveData.Value.BackupSaveResult is BackupAutosaveResult.NothingToSave)
+                {
+                    UpdateTextSave("AUTOSAVE_NOTHING_CHANGED", false, SaveIcon, inactiveBrush, false);
+                    return;
+                }
+
+                if (data.LastAutosaveData.Value.BackupSaveResult is BackupAutosaveResult.Success)
+                {
+                    UpdateTextSave("AUTOSAVE_SAVED", true, SaveIcon, successBrush, false);
+                    return;
+                }
+            }
+            else
+            {
+                SetWaitingToSave();
+                return;
+            }
+        }
+        
+        if (AutosaveStateData.AutosaveState is AutosaveState.AwaitingUpdateableChangeEnd)
+        {
+            UpdateTextSave("AUTOSAVE_WAITING_FOR_SAVE", true, SaveIcon, activeBrush, true);
+            return;
+        }
+
+        if (AutosaveStateData.AutosaveState is AutosaveState.InProgress)
+        {
+            UpdateTextSave("AUTOSAVE_SAVING", true, SaveIcon, activeBrush, true);
+            return;
+        }
+    }
+
+    private void SetWaitingToSave()
+    {
+        var data = AutosaveStateData;
+        TimeSpan timeLeft = data.LastAutosaveData switch
+        {
+            null => data.AutosaveInterval - (DateTime.Now - data.AutosaveLaunchDateTime),
+            { } lastData => data.AutosaveInterval - (DateTime.Now - lastData.Time)
+        };
+        
+        bool error = data.LastAutosaveData switch
+        {
+            null => false,
+            { } lastData => lastData.BackupSaveResult != BackupAutosaveResult.Error && lastData.UserFileSaveResult != UserFileAutosaveResult.ExceptionWhileSaving
+        };
+        
+        if (timeLeft.Minutes == 0 && !error)
+        {
+            UpdateTextSave("AUTOSAVE_SAVING_IN_MINUTE", false, ClockIcon, inactiveBrush, false);
+            return;
+        }
+        
+        var adjusted = timeLeft.Add(TimeSpan.FromSeconds(30));
+        var minute = adjusted.Minutes < 2
+            ? new LocalizedString("MINUTE_SINGULAR")
+            : new LocalizedString("MINUTE_PLURAL");
+        
+        if (error)
+            UpdateTextSave(new LocalizedString("AUTOSAVE_FAILED_RETRYING", data.AutosaveInterval.TotalMinutes.ToString("0"), minute), true, WarnIcon, warnBrush, true);
+        else
+            UpdateTextSave(new LocalizedString("AUTOSAVE_SAVING_IN", adjusted.Minutes.ToString(), minute), false, ClockIcon, inactiveBrush, false);
+    }
+    
+    private void UpdateTextSave(LocalizedString text, bool isImportantText, string iconText, Brush brush, bool pulse)
+    {
+        Text = text;
+        isForceExpanded = isImportantText;
+        IconText = iconText;
+        IconBrush = brush;
+        PulseIcon = pulse;
+    }
+    
+    private static void OnStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+    {
+        var self = (AutosaveControl)d;
+        self.Update();
+        self.textUpdateTimer.Stop();
+        self.textUpdateTimer.Start();
+    }
+    
+    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
+
+    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
+    {
+        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
+        field = value;
+        OnPropertyChanged(propertyName);
+        return true;
     }
 }