فهرست منبع

Added autosave icons and ability to pause autosaving for document session

CPKreuz 1 سال پیش
والد
کامیت
9095b147ac

+ 6 - 2
src/PixiEditor/Data/Localization/Languages/en.json

@@ -593,11 +593,15 @@
   
   "AUTOSAVE_SAVING": "Saving...",
   "AUTOSAVE_SAVED": "Saved.",
-  "AUTOSAVE_SAVING_IN_MINUTE": "Saving in less than a minute.",
-  "AUTOSAVE_SAVING_IN": "Saving in about {0} {1}.",
+  "AUTOSAVE_SAVING_IN_MINUTE": "Saving in less than a minute",
+  "AUTOSAVE_SAVING_IN": "Saving in about {0} {1}",
   "AUTOSAVE_WAITING_FOR_SAVE": "Waiting to autosave.",
   "AUTOSAVE_PLEASE_RESAVE": "Error while saving. Save file manually to enable autosaving.",
   "AUTOSAVE_FAILED_RETRYING": "Error while saving. Retrying in {0} {1}.",
+  "AUTOSAVE_DISABLED": "Autosaving paused for document.",
+  
+  "AUTOSAVE_TOGGLE": "Toggle autosave",
+  "AUTOSAVE_TOGGLE_DESCRIPTION": "Toggle autosave for document",
   
   "AUTOSAVE_SETTINGS_PERIOD": "Autosave every",
   

+ 1 - 0
src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs

@@ -52,6 +52,7 @@ internal static class ServiceCollectionHelpers
         .AddSingleton<SearchViewModel>()
         .AddSingleton<AdditionalContentViewModel>()
         .AddSingleton(x => new ExtensionsViewModel(x.GetService<ViewModelMain>(), extensionLoader))
+        .AddSingleton<AutosaveViewModel>()
         // Controllers
         .AddSingleton<ShortcutController>()
         .AddSingleton<CommandController>()

+ 89 - 24
src/PixiEditor/ViewModels/SubViewModels/Document/AutosaveViewModel.cs → src/PixiEditor/ViewModels/SubViewModels/Document/AutosaveDocumentViewModel.cs

@@ -1,4 +1,6 @@
 using System.IO;
+using System.Windows;
+using System.Windows.Media;
 using System.Windows.Threading;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.UserPreferences;
@@ -9,36 +11,91 @@ using Timer = System.Timers.Timer;
 
 namespace PixiEditor.Models.DocumentModels.Public;
 
-internal class AutosaveViewModel : NotifyableObject
+internal class AutosaveDocumentViewModel : NotifyableObject
 {
     private readonly Timer savingTimer;
     private readonly Timer updateTextTimer;
     private readonly Timer busyTimer;
     private bool saveAfterNextFinish;
     private bool reenableAfterNextSave;
-    private string mainMenuText;
     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 DocumentViewModel Document { get; }
 
     private double AutosavePeriodMinutes { get; set; } = -1;
 
+    private LocalizedString mainMenuText;
+    
     public LocalizedString MainMenuText
     {
         get => mainMenuText; 
         set => SetProperty(ref mainMenuText, value);
     }
-    
+
+    private string mainMenuIconText;
+
+    public string MainMenuIconText
+    {
+        get => mainMenuIconText;
+        set => SetProperty(ref mainMenuIconText, value);
+    }
+
+    private Brush mainMenuBrush;
+
+    public Brush MainMenuBrush
+    {
+        get => mainMenuBrush;
+        set => SetProperty(ref mainMenuBrush, value);
+    }
+
+    private bool mainMenuPulse;
+
+    public bool MainMenuPulse
+    {
+        get => mainMenuPulse;
+        set => SetProperty(ref mainMenuPulse, value);
+    }
+
+    public bool Enabled
+    {
+        get => documentEnabled;
+        set
+        {
+            if (documentEnabled == value)
+                return;
+            
+            AutosavePeriodChanged(
+                IPreferences.Current.GetPreference(
+                    PreferencesConstants.AutosavePeriodMinutes, 
+                    PreferencesConstants.AutosavePeriodDefault),
+                value);
+            documentEnabled = value;
+        }
+    }
+
     public string LastSavedPath { get; private set; }
     
-    public AutosaveViewModel(DocumentViewModel document)
+    public AutosaveDocumentViewModel(DocumentViewModel document)
     {
         Document = document;
         tempGuid = Guid.NewGuid();
         savingTimer = new Timer();
-        updateTextTimer = new Timer(TimeSpan.FromSeconds(10));
+        updateTextTimer = new Timer(TimeSpan.FromSeconds(3));
 
         savingTimer.Elapsed += (_, _) => TryAutosave();
         savingTimer.AutoReset = false;
@@ -54,8 +111,8 @@ internal class AutosaveViewModel : NotifyableObject
 
         var preferences = IPreferences.Current;
         
-        preferences.AddCallback<double>(PreferencesConstants.AutosavePeriodMinutes, AutosavePeriodChanged);
-        AutosavePeriodChanged(preferences.GetPreference(PreferencesConstants.AutosavePeriodMinutes, PreferencesConstants.AutosavePeriodDefault));
+        preferences.AddCallback<double>(PreferencesConstants.AutosavePeriodMinutes, (v) => AutosavePeriodChanged(v, documentEnabled));
+        AutosavePeriodChanged(preferences.GetPreference(PreferencesConstants.AutosavePeriodMinutes, PreferencesConstants.AutosavePeriodDefault), documentEnabled);
     }
 
     public void HintFinishedAction()
@@ -85,7 +142,7 @@ internal class AutosaveViewModel : NotifyableObject
 
         if (timeLeft.Minutes == 0)
         {
-            UpdateMainMenuTextSave("AUTOSAVE_SAVING_IN_MINUTE");
+            UpdateMainMenuTextSave("AUTOSAVE_SAVING_IN_MINUTE", ClockIcon, InactiveBrush, false);
             return;
         }
 
@@ -95,7 +152,7 @@ internal class AutosaveViewModel : NotifyableObject
             ? new LocalizedString("MINUTE_SINGULAR")
             : new LocalizedString("MINUTE_PLURAL");
 
-        UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_SAVING_IN", adjusted.Minutes.ToString(), minute));
+        UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_SAVING_IN", adjusted.Minutes.ToString(), minute), ClockIcon, InactiveBrush, false);
     }
 
     private void TryAutosave()
@@ -103,7 +160,7 @@ internal class AutosaveViewModel : NotifyableObject
         if (Document.UpdateableChangeActive)
         {
             saveAfterNextFinish = true;
-            UpdateMainMenuTextSave("AUTOSAVE_WAITING_FOR_SAVE");
+            UpdateMainMenuTextSave("AUTOSAVE_WAITING_FOR_SAVE", SaveIcon, ActiveBrush, true);
             
             savingTimer.Stop();
             updateTextTimer.Stop();
@@ -113,7 +170,7 @@ internal class AutosaveViewModel : NotifyableObject
 
         if (Document.AllChangesSaved)
         {
-            UpdateMainMenuTextSave("AUTOSAVE_SAVED");
+            UpdateMainMenuTextSave("AUTOSAVE_SAVED", SaveIcon, SuccessBrush, false);
             updateTextTimer.Stop();
             RestartTimers();
             return;
@@ -137,7 +194,7 @@ internal class AutosaveViewModel : NotifyableObject
                 ? new LocalizedString("MINUTE_SINGULAR")
                 : new LocalizedString("MINUTE_PLURAL");
 
-            UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_FAILED_RETRYING", AutosavePeriodMinutes.ToString("0"), minute));
+            UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_FAILED_RETRYING", AutosavePeriodMinutes.ToString("0"), minute), WarnIcon, WarnBrush, true);
         
             busyTimer.Stop();
             Document.Busy = false;
@@ -157,9 +214,10 @@ internal class AutosaveViewModel : NotifyableObject
     {
         saveAfterNextFinish = false;
         
-        UpdateMainMenuTextSave("AUTOSAVE_SAVING");
+        UpdateMainMenuTextSave("AUTOSAVE_SAVING", SavingIcon, ActiveBrush, true);
 
         string filePath;
+        bool fileExists = true;
 
         if (Document.FullFilePath == null || !Document.FullFilePath.EndsWith(".pixi"))
         {
@@ -169,21 +227,22 @@ internal class AutosaveViewModel : NotifyableObject
         else
         {
             filePath = Document.FullFilePath;
+            fileExists = File.Exists(filePath);
         }
         
         busyTimer.Start();
         var result = Exporter.TrySave(Document, filePath);
 
-        if (result == SaveResult.Success)
+        if (result == SaveResult.Success && fileExists)
         {
             savingFailed = 0;
-            UpdateMainMenuTextSave("AUTOSAVE_SAVED");
+            UpdateMainMenuTextSave("AUTOSAVE_SAVED", SaveIcon, SuccessBrush, false);
             Document.MarkAsSaved();
             LastSavedPath = filePath;
         }
-        else if (result is SaveResult.InvalidPath or SaveResult.SecurityError)
+        else if (result is SaveResult.InvalidPath or SaveResult.SecurityError || !fileExists)
         {
-            UpdateMainMenuTextSave("AUTOSAVE_PLEASE_RESAVE");
+            UpdateMainMenuTextSave("AUTOSAVE_PLEASE_RESAVE", SaveIcon, ErrorBrush, true);
             busyTimer.Stop();
             Document.Busy = false;
             reenableAfterNextSave = true;
@@ -198,7 +257,7 @@ internal class AutosaveViewModel : NotifyableObject
                 ? new LocalizedString("MINUTE_SINGULAR")
                 : new LocalizedString("MINUTE_PLURAL");
             
-            UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_FAILED_RETRYING", AutosavePeriodMinutes.ToString("0"), minute));
+            UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_FAILED_RETRYING", AutosavePeriodMinutes.ToString("0"), minute), WarnIcon, WarnBrush, true);
             savingFailed++;
 
             if (savingFailed == 3)
@@ -220,15 +279,18 @@ internal class AutosaveViewModel : NotifyableObject
         updateTextTimer.Start();
     }
 
-    private void AutosavePeriodChanged(double minutes)
+    private void AutosavePeriodChanged(double minutes, bool documentEnabled)
     {
-        if (minutes == -1)
+        if ((int)minutes == -1 || !documentEnabled)
         {
             savingTimer.Enabled = false;
             updateTextTimer.Enabled = false;
             saveAfterNextFinish = false;
+
+            LocalizedString menuText = documentEnabled ? string.Empty : "AUTOSAVE_DISABLED";
+            string iconText = documentEnabled ? null : PauseIcon;
             
-            UpdateMainMenuTextSave(string.Empty);
+            UpdateMainMenuTextSave(menuText, iconText, ActiveBrush, false);
 
             AutosavePeriodMinutes = minutes;
             return;
@@ -236,7 +298,7 @@ internal class AutosaveViewModel : NotifyableObject
         
         var timerEnabled = savingTimer.Enabled;
 
-        if (AutosavePeriodMinutes == -1)
+        if ((int)AutosavePeriodMinutes == -1 || !Enabled)
         {
             timerEnabled = true;
             updateTextTimer.Start();
@@ -257,11 +319,14 @@ internal class AutosaveViewModel : NotifyableObject
         }
     }
 
-    private void UpdateMainMenuTextSave(LocalizedString text)
+    private void UpdateMainMenuTextSave(LocalizedString text, string iconText, Brush brush, bool pulse)
     {
-        Dispatcher.CurrentDispatcher.Invoke(() =>
+        Application.Current.Dispatcher.Invoke(() =>
         {
             MainMenuText = text;
+            MainMenuIconText = iconText;
+            MainMenuBrush = brush;
+            MainMenuPulse = pulse;
         });
     }
 }

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

@@ -150,7 +150,7 @@ internal partial class DocumentViewModel : NotifyableObject
     public DocumentTransformViewModel TransformViewModel { get; }
     public ReferenceLayerViewModel ReferenceLayerViewModel { get; }
     public LineToolOverlayViewModel LineToolOverlayViewModel { get; }
-    public AutosaveViewModel AutosaveViewModel { get; }
+    public AutosaveDocumentViewModel AutosaveViewModel { get; }
 
     private DocumentInternalParts Internals { get; }
 
@@ -161,7 +161,7 @@ internal partial class DocumentViewModel : NotifyableObject
         StructureHelper = new DocumentStructureModule(this);
         EventInlet = new DocumentEventsModule(this, Internals);
         Operations = new DocumentOperationsModule(this, Internals);
-        AutosaveViewModel = new AutosaveViewModel(this);
+        AutosaveViewModel = new AutosaveDocumentViewModel(this);
 
         StructureRoot = new FolderViewModel(this, Internals, Internals.Tracker.Document.StructureRoot.GuidValue);
 

+ 29 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/AutosaveViewModel.cs

@@ -0,0 +1,29 @@
+using PixiEditor.Extensions.Common.UserPreferences;
+using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.ViewModels.SubViewModels.Document;
+
+namespace PixiEditor.ViewModels.SubViewModels.Main;
+
+internal class AutosaveViewModel : SubViewModel<ViewModelMain>
+{
+    private DocumentManagerViewModel _documentManager;
+    
+    public AutosaveViewModel(ViewModelMain owner, DocumentManagerViewModel documentManager) : base(owner)
+    {
+        _documentManager = documentManager;
+    }
+    
+    [Command.Basic("PixiEditor.Autosave.ToggleAutosave", "AUTOSAVE_TOGGLE", "AUTOSAVE_TOGGLE_DESCRIPTION", CanExecute = "PixiEditor.Autosave.HasDocumentAndAutosaveEnabled")]
+    public void ToggleAutosave()
+    {
+        var autosaveViewModel = _documentManager.ActiveDocument!.AutosaveViewModel;
+
+        autosaveViewModel.Enabled = !autosaveViewModel.Enabled;
+    }
+
+    [Evaluator.CanExecute("PixiEditor.Autosave.HasDocumentAndAutosaveEnabled")]
+    public bool HasDocumentAndAutosaveEnabled() => 
+        _documentManager.DocumentNotNull() &&
+        (int)IPreferences.Current.GetPreference<double>(PreferencesConstants.AutosavePeriodMinutes) != -1;
+
+}

+ 4 - 1
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -65,6 +65,8 @@ internal class ViewModelMain : ViewModelBase
 
     public DocumentManagerViewModel DocumentManagerSubViewModel { get; set; }
 
+    public AutosaveViewModel AutosaveViewModel { get; set; }
+
     public CommandController CommandController { get; set; }
 
     public ShortcutController ShortcutController { get; set; }
@@ -80,7 +82,7 @@ internal class ViewModelMain : ViewModelBase
     public AdditionalContentViewModel AdditionalContentSubViewModel { get; set; }
 
     public ExtensionsViewModel ExtensionsSubViewModel { get; set; }
-
+    
     public IPreferences Preferences { get; set; }
     public ILocalizationProvider LocalizationProvider { get; set; }
 
@@ -126,6 +128,7 @@ internal class ViewModelMain : ViewModelBase
         WindowSubViewModel = services.GetService<WindowViewModel>();
         DocumentManagerSubViewModel = services.GetRequiredService<DocumentManagerViewModel>();
         SelectionSubViewModel = services.GetService<SelectionViewModel>();
+        AutosaveViewModel = services.GetService<AutosaveViewModel>();
 
         OnStartupCommand = new RelayCommand(OnStartup);
         CloseWindowCommand = new RelayCommand(CloseWindow);

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

@@ -468,8 +468,13 @@
                                    Foreground="White"/>
                     </Grid>
                 </Border>
-                <Label ui1:Translator.LocalizedString="{Binding DocumentManagerSubViewModel.ActiveDocument.AutosaveViewModel.MainMenuText, FallbackValue=''}"
-                       Foreground="White"/>
+                <usercontrols:AutosaveControl
+                    MinWidth="280"
+                    Height="25"
+                    Margin="5,0,0,0"
+                    DataContext="{Binding DocumentManagerSubViewModel.ActiveDocument.AutosaveViewModel}"
+                    VerticalAlignment="Top"
+                    WindowChrome.IsHitTestVisibleInChrome="True"/>
                 <StackPanel
                     DockPanel.Dock="Right"
                     VerticalAlignment="Top"

+ 98 - 0
src/PixiEditor/Views/UserControls/AutosaveControl.xaml

@@ -0,0 +1,98 @@
+<UserControl x:Class="PixiEditor.Views.UserControls.AutosaveControl"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:local="clr-namespace:PixiEditor.Views.UserControls"
+             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+             xmlns:public="clr-namespace:PixiEditor.Models.DocumentModels.Public"
+             xmlns:xaml="clr-namespace:PixiEditor.Models.Commands.XAML"
+             mc:Ignorable="d"
+             d:DesignWidth="280" d:DesignHeight="25"
+             d:DataContext="{d:DesignInstance public:AutosaveDocumentViewModel}">
+    <Border Padding="8, 0"
+            CornerRadius="5"
+            BorderThickness="0"
+            x:Name="autosaveControl">
+        <Border.Style>
+            <Style TargetType="Border">
+                <Style.Triggers>
+                    <Trigger Property="IsMouseOver" Value="False">
+                        <Setter Property="Background" Value="Transparent" />
+                    </Trigger>
+                    <Trigger Property="IsMouseOver" Value="True">
+                        <Setter Property="Background" Value="#10FFFFFF" />
+                    </Trigger>
+                </Style.Triggers>
+            </Style>
+        </Border.Style>
+        <Grid>
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition />
+                <ColumnDefinition Width="Auto" />
+            </Grid.ColumnDefinitions>
+            <StackPanel Orientation="Horizontal">
+                <TextBlock
+                    Text="{Binding MainMenuIconText, FallbackValue='', Mode=OneWay}"
+                    FontFamily="{StaticResource Feather}"
+                    FontSize="16" Margin="0,5,0,0"
+                    Foreground="{Binding MainMenuBrush, Mode=OneWay}">
+                    <TextBlock.Style>
+                        <Style TargetType="TextBlock">
+                            <Style.Triggers>
+                                <DataTrigger
+                                    Binding="{Binding MainMenuPulse, Mode=OneWay}"
+                                    Value="True">
+                                    <DataTrigger.EnterActions>
+                                        <BeginStoryboard x:Name="storyboard">
+                                            <Storyboard TargetProperty="Opacity">
+                                                <DoubleAnimation From="0.4" To="1" Duration="0:0:2"
+                                                                 AutoReverse="True"
+                                                                 RepeatBehavior="Forever">
+                                                    <DoubleAnimation.EasingFunction>
+                                                        <QuadraticEase EasingMode="EaseInOut" />
+                                                    </DoubleAnimation.EasingFunction>
+                                                </DoubleAnimation>
+                                            </Storyboard>
+                                        </BeginStoryboard>
+                                    </DataTrigger.EnterActions>
+                                    <DataTrigger.ExitActions>
+                                        <RemoveStoryboard BeginStoryboardName="storyboard" />
+                                    </DataTrigger.ExitActions>
+                                </DataTrigger>
+                            </Style.Triggers>
+                        </Style>
+                    </TextBlock.Style>
+                </TextBlock>
+                <Label
+                    ui:Translator.LocalizedString="{Binding MainMenuText, FallbackValue='', Mode=OneWay}"
+                    Foreground="White" />
+            </StackPanel>
+            <StackPanel Grid.Column="1" Orientation="Horizontal">
+                <StackPanel.Style>
+                    <Style TargetType="StackPanel">
+                        <Style.Triggers>
+                            <DataTrigger Binding="{Binding IsMouseOver, ElementName=autosaveControl}" Value="False">
+                                <Setter Property="Opacity" Value="0.0"></Setter>
+                            </DataTrigger>
+                        </Style.Triggers>
+                    </Style>
+                </StackPanel.Style>
+                <StackPanel.Resources>
+                    <Style TargetType="Button" BasedOn="{StaticResource GrayRoundButton}">
+                        <Setter Property="Margin" Value="2"></Setter>
+                        <Setter Property="Padding" Value="0"></Setter>
+                        <Setter Property="MinWidth" Value="23"></Setter>
+                        <Setter Property="Height" Value="21"></Setter>
+                    </Style>
+                </StackPanel.Resources>
+                <Button Command="{xaml:Command PixiEditor.Autosave.ToggleAutosave}">
+                    <TextBlock Text="" FontFamily="{StaticResource Feather}" />
+                </Button>
+                <Button>
+                    <TextBlock Text="" FontFamily="{StaticResource Feather}" />
+                </Button>
+            </StackPanel>
+        </Grid>
+    </Border>
+</UserControl>

+ 12 - 0
src/PixiEditor/Views/UserControls/AutosaveControl.xaml.cs

@@ -0,0 +1,12 @@
+using System.Windows.Controls;
+
+namespace PixiEditor.Views.UserControls;
+
+public partial class AutosaveControl : UserControl
+{
+    public AutosaveControl()
+    {
+        InitializeComponent();
+    }
+}
+