Browse Source

Merge pull request #152 from PixiEditor/recentlyOpened

Added recently opened files list
Krzysztof Krysiński 4 years ago
parent
commit
1e69312af5
27 changed files with 715 additions and 225 deletions
  1. 31 1
      PixiEditor/Models/DataHolders/Document/Document.IO.cs
  2. 2 2
      PixiEditor/Models/Dialogs/NewFileDialog.cs
  3. 17 0
      PixiEditor/Models/Dialogs/NoticeDialog.cs
  4. 32 0
      PixiEditor/Models/UserPreferences/IPreferences.cs
  5. 97 34
      PixiEditor/Models/UserPreferences/PreferencesSettings.cs
  6. 1 0
      PixiEditor/PixiEditor.csproj
  7. 8 8
      PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs
  8. 50 1
      PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  9. 1 1
      PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs
  10. 89 0
      PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/DiscordSettings.cs
  11. 62 0
      PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs
  12. 18 0
      PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/UpdateSettings.cs
  13. 3 3
      PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsGroup.cs
  14. 3 143
      PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsViewModel.cs
  15. 9 3
      PixiEditor/ViewModels/ViewModelMain.cs
  16. 40 0
      PixiEditor/Views/Dialogs/NoticePopup.xaml
  17. 41 0
      PixiEditor/Views/Dialogs/NoticePopup.xaml.cs
  18. 8 4
      PixiEditor/Views/Dialogs/SettingsWindow.xaml
  19. 24 10
      PixiEditor/Views/MainWindow.xaml
  20. 8 0
      PixiEditor/Views/MainWindow.xaml.cs
  21. 24 0
      PixiEditorTests/Helpers.cs
  22. 56 0
      PixiEditorTests/Mocks/PreferenceSettingsMock.cs
  23. 18 0
      PixiEditorTests/ModelsTests/DataHoldersTests/DocumentTests.cs
  24. 4 2
      PixiEditorTests/ModelsTests/ToolsTests/ZoomToolTests.cs
  25. 43 1
      PixiEditorTests/ModelsTests/UserPreferencesTests/PreferencesSettingsTests.cs
  26. 2 0
      PixiEditorTests/PixiEditorTests.csproj
  27. 24 12
      PixiEditorTests/ViewModelsTests/ViewModelMainTests.cs

+ 31 - 1
PixiEditor/Models/DataHolders/Document/Document.IO.cs

@@ -1,4 +1,6 @@
-using PixiEditor.Models.IO;
+using System.Collections.ObjectModel;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.UserPreferences;
 
 
 namespace PixiEditor.Models.DataHolders
 namespace PixiEditor.Models.DataHolders
 {
 {
@@ -14,6 +16,7 @@ namespace PixiEditor.Models.DataHolders
                 documentFilePath = value;
                 documentFilePath = value;
                 RaisePropertyChanged(nameof(DocumentFilePath));
                 RaisePropertyChanged(nameof(DocumentFilePath));
                 RaisePropertyChanged(nameof(Name));
                 RaisePropertyChanged(nameof(Name));
+                UpdateRecentlyOpened(value);
             }
             }
         }
         }
 
 
@@ -47,5 +50,32 @@ namespace PixiEditor.Models.DataHolders
             DocumentFilePath = Exporter.SaveAsEditableFile(this, path);
             DocumentFilePath = Exporter.SaveAsEditableFile(this, path);
             ChangesSaved = true;
             ChangesSaved = true;
         }
         }
+
+        private void UpdateRecentlyOpened(string newPath)
+        {
+            ObservableCollection<string> recentlyOpened = XamlAccesibleViewModel.FileSubViewModel.RecentlyOpened;
+
+            if (!recentlyOpened.Contains(newPath))
+            {
+                recentlyOpened.Insert(0, newPath);
+            }
+            else
+            {
+                int index = recentlyOpened.IndexOf(newPath);
+                recentlyOpened.Move(index, 0);
+            }
+
+            if (recentlyOpened.Count > IPreferences.Current.GetPreference("maxOpenedRecently", 10))
+            {
+                for (int i = 4; i < recentlyOpened.Count; i++)
+                {
+                    recentlyOpened.RemoveAt(i);
+                }
+            }
+
+            IPreferences.Current.UpdateLocalPreference("RecentlyOpened", recentlyOpened);
+
+            XamlAccesibleViewModel.FileSubViewModel.HasRecent = true;
+        }
     }
     }
 }
 }

+ 2 - 2
PixiEditor/Models/Dialogs/NewFileDialog.cs

@@ -7,9 +7,9 @@ namespace PixiEditor.Models.Dialogs
 {
 {
     public class NewFileDialog : CustomDialog
     public class NewFileDialog : CustomDialog
     {
     {
-        private int height = (int)PreferencesSettings.GetPreference("DefaultNewFileHeight", 16L);
+        private int height = (int)IPreferences.Current.GetPreference("DefaultNewFileHeight", 16L);
 
 
-        private int width = (int)PreferencesSettings.GetPreference("DefaultNewFileWidth", 16L);
+        private int width = (int)IPreferences.Current.GetPreference("DefaultNewFileWidth", 16L);
 
 
         public int Width
         public int Width
         {
         {

+ 17 - 0
PixiEditor/Models/Dialogs/NoticeDialog.cs

@@ -0,0 +1,17 @@
+using PixiEditor.Views.Dialogs;
+
+namespace PixiEditor.Models.Dialogs
+{
+    public static class NoticeDialog
+    {
+        public static void Show(string message)
+        {
+            NoticePopup popup = new NoticePopup
+            {
+                Body = message
+            };
+
+            popup.ShowDialog();
+        }
+    }
+}

+ 32 - 0
PixiEditor/Models/UserPreferences/IPreferences.cs

@@ -0,0 +1,32 @@
+using System;
+using PixiEditor.ViewModels;
+
+namespace PixiEditor.Models.UserPreferences
+{
+    public interface IPreferences
+    {
+        public static IPreferences Current => ViewModelMain.Current.Preferences;
+
+        public void Save();
+
+        public void AddCallback(string setting, Action<object> action);
+
+        public void Init();
+
+        public void Init(string path, string localPath);
+
+        public void UpdatePreference<T>(string name, T value);
+
+        public void UpdateLocalPreference<T>(string name, T value);
+
+#nullable enable
+
+        public T? GetPreference<T>(string name);
+
+        public T? GetPreference<T>(string name, T? fallbackValue);
+
+        public T? GetLocalPreference<T>(string name);
+
+        public T? GetLocalPreference<T>(string name, T? fallbackValue);
+    }
+}

+ 97 - 34
PixiEditor/Models/UserPreferences/PreferencesSettings.cs

@@ -2,51 +2,44 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using Newtonsoft.Json;
 using Newtonsoft.Json;
+using PixiEditor.ViewModels;
 
 
 namespace PixiEditor.Models.UserPreferences
 namespace PixiEditor.Models.UserPreferences
 {
 {
-    public static class PreferencesSettings
+    public class PreferencesSettings : IPreferences
     {
     {
-        public static bool IsLoaded { get; private set; } = false;
+        public static IPreferences Current => ViewModelMain.Current.Preferences;
 
 
-        public static string PathToUserPreferences { get; private set; } = Path.Join(
-            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
-            "PixiEditor",
-            "user_preferences.json");
+        public bool IsLoaded { get; private set; } = false;
+
+        public string PathToRoamingUserPreferences { get; private set; } = GetPathToSettings(Environment.SpecialFolder.ApplicationData, "user_preferences.json");
+
+        public string PathToLocalPreferences { get; private set; } = GetPathToSettings(Environment.SpecialFolder.LocalApplicationData, "editor_data.json");
+
+        public Dictionary<string, object> Preferences { get; set; } = new Dictionary<string, object>();
 
 
-        public static Dictionary<string, object> Preferences { get; set; } = new Dictionary<string, object>();
+        public Dictionary<string, object> LocalPreferences { get; set; } = new Dictionary<string, object>();
 
 
-        public static void Init()
+        public void Init()
         {
         {
-            Init(PathToUserPreferences);
+            Init(PathToRoamingUserPreferences, PathToLocalPreferences);
         }
         }
 
 
-        public static void Init(string path)
+        public void Init(string path, string localPath)
         {
         {
-            PathToUserPreferences = path;
+            PathToRoamingUserPreferences = path;
+            PathToLocalPreferences = localPath;
+
             if (IsLoaded == false)
             if (IsLoaded == false)
             {
             {
-                string dir = Path.GetDirectoryName(path);
-                if (!Directory.Exists(dir))
-                {
-                    Directory.CreateDirectory(dir);
-                }
-
-                if (!File.Exists(path))
-                {
-                    File.WriteAllText(path, "{\n}");
-                }
-                else
-                {
-                    string json = File.ReadAllText(path);
-                    Preferences = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
-                }
+                Preferences = InitPath(path);
+                LocalPreferences = InitPath(localPath);
 
 
                 IsLoaded = true;
                 IsLoaded = true;
             }
             }
         }
         }
 
 
-        public static void UpdatePreference<T>(string name, T value)
+        public void UpdatePreference<T>(string name, T value)
         {
         {
             if (IsLoaded == false)
             if (IsLoaded == false)
             {
             {
@@ -66,21 +59,40 @@ namespace PixiEditor.Models.UserPreferences
             Save();
             Save();
         }
         }
 
 
-        public static void Save()
+        public void UpdateLocalPreference<T>(string name, T value)
         {
         {
             if (IsLoaded == false)
             if (IsLoaded == false)
             {
             {
                 Init();
                 Init();
             }
             }
 
 
-            File.WriteAllText(PathToUserPreferences, JsonConvert.SerializeObject(Preferences));
+            LocalPreferences[name] = value;
+
+            if (Callbacks.ContainsKey(name))
+            {
+                foreach (var action in Callbacks[name])
+                {
+                    action.Invoke(value);
+                }
+            }
+
+            Save();
         }
         }
 
 
-#nullable enable
+        public void Save()
+        {
+            if (IsLoaded == false)
+            {
+                Init();
+            }
 
 
-        public static Dictionary<string, List<Action<object>>> Callbacks { get; set; } = new Dictionary<string, List<Action<object>>>();
+            File.WriteAllText(PathToRoamingUserPreferences, JsonConvert.SerializeObject(Preferences));
+            File.WriteAllText(PathToLocalPreferences, JsonConvert.SerializeObject(LocalPreferences));
+        }
+
+        public Dictionary<string, List<Action<object>>> Callbacks { get; set; } = new Dictionary<string, List<Action<object>>>();
 
 
-        public static void AddCallback(string setting, Action<object> action)
+        public void AddCallback(string setting, Action<object> action)
         {
         {
             if (Callbacks.ContainsKey(setting))
             if (Callbacks.ContainsKey(setting))
             {
             {
@@ -91,12 +103,14 @@ namespace PixiEditor.Models.UserPreferences
             Callbacks.Add(setting, new List<Action<object>>() { action });
             Callbacks.Add(setting, new List<Action<object>>() { action });
         }
         }
 
 
-        public static T? GetPreference<T>(string name)
+#nullable enable
+
+        public T? GetPreference<T>(string name)
         {
         {
             return GetPreference(name, default(T));
             return GetPreference(name, default(T));
         }
         }
 
 
-        public static T? GetPreference<T>(string name, T? fallbackValue)
+        public T? GetPreference<T>(string name, T? fallbackValue)
         {
         {
             if (IsLoaded == false)
             if (IsLoaded == false)
             {
             {
@@ -107,5 +121,54 @@ namespace PixiEditor.Models.UserPreferences
                 ? (T)Preferences[name]
                 ? (T)Preferences[name]
                 : fallbackValue;
                 : fallbackValue;
         }
         }
+
+        public T? GetLocalPreference<T>(string name)
+        {
+            return GetPreference(name, default(T));
+        }
+
+        public T? GetLocalPreference<T>(string name, T? fallbackValue)
+        {
+            if (IsLoaded == false)
+            {
+                Init();
+            }
+
+            return LocalPreferences.ContainsKey(name)
+                ? (T)LocalPreferences[name]
+                : fallbackValue;
+        }
+
+#nullable disable
+
+        private static string GetPathToSettings(Environment.SpecialFolder folder, string fileName)
+        {
+            return Path.Join(
+            Environment.GetFolderPath(folder),
+            "PixiEditor",
+            fileName);
+        }
+
+        private static Dictionary<string, object> InitPath(string path)
+        {
+            string dir = Path.GetDirectoryName(path);
+
+            if (!Directory.Exists(dir))
+            {
+                Directory.CreateDirectory(dir);
+            }
+
+            if (!File.Exists(path))
+            {
+                File.WriteAllText(path, "{\n}");
+            }
+            else
+            {
+                string json = File.ReadAllText(path);
+                return JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
+            }
+
+            return new Dictionary<string, object>();
+        }
     }
     }
 }
 }

+ 1 - 0
PixiEditor/PixiEditor.csproj

@@ -59,6 +59,7 @@
       <Version>1.0.2</Version>
       <Version>1.0.2</Version>
     </PackageReference>
     </PackageReference>
     <PackageReference Include="Extended.Wpf.Toolkit" Version="3.8.2" />
     <PackageReference Include="Extended.Wpf.Toolkit" Version="3.8.2" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
     <PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
     <PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="PixiEditor.ColorPicker" Version="2.0.0" />
     <PackageReference Include="PixiEditor.ColorPicker" Version="2.0.0" />

+ 8 - 8
PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs

@@ -30,7 +30,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             }
         }
         }
 
 
-        private bool showDocumentName = PreferencesSettings.GetPreference(nameof(ShowDocumentName), true);
+        private bool showDocumentName = IPreferences.Current.GetPreference(nameof(ShowDocumentName), true);
 
 
         public bool ShowDocumentName
         public bool ShowDocumentName
         {
         {
@@ -45,7 +45,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             }
         }
         }
 
 
-        private bool showDocumentSize = PreferencesSettings.GetPreference(nameof(ShowDocumentSize), true);
+        private bool showDocumentSize = IPreferences.Current.GetPreference(nameof(ShowDocumentSize), true);
 
 
         public bool ShowDocumentSize
         public bool ShowDocumentSize
         {
         {
@@ -60,7 +60,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             }
         }
         }
 
 
-        private bool showLayerCount = PreferencesSettings.GetPreference(nameof(ShowLayerCount), true);
+        private bool showLayerCount = IPreferences.Current.GetPreference(nameof(ShowLayerCount), true);
 
 
         public bool ShowLayerCount
         public bool ShowLayerCount
         {
         {
@@ -81,11 +81,11 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             Owner.BitmapManager.DocumentChanged += DocumentChanged;
             Owner.BitmapManager.DocumentChanged += DocumentChanged;
             this.clientId = clientId;
             this.clientId = clientId;
 
 
-            Enabled = PreferencesSettings.GetPreference<bool>("EnableRichPresence");
-            PreferencesSettings.AddCallback("EnableRichPresence", x => Enabled = (bool)x);
-            PreferencesSettings.AddCallback(nameof(ShowDocumentName), x => ShowDocumentName = (bool)x);
-            PreferencesSettings.AddCallback(nameof(ShowDocumentSize), x => ShowDocumentSize = (bool)x);
-            PreferencesSettings.AddCallback(nameof(ShowLayerCount), x => ShowLayerCount = (bool)x);
+            Enabled = IPreferences.Current.GetPreference<bool>("EnableRichPresence");
+            IPreferences.Current.AddCallback("EnableRichPresence", x => Enabled = (bool)x);
+            IPreferences.Current.AddCallback(nameof(ShowDocumentName), x => ShowDocumentName = (bool)x);
+            IPreferences.Current.AddCallback(nameof(ShowDocumentSize), x => ShowDocumentSize = (bool)x);
+            IPreferences.Current.AddCallback(nameof(ShowLayerCount), x => ShowLayerCount = (bool)x);
 
 
             AppDomain.CurrentDomain.ProcessExit += (_, _) => Enabled = false;
             AppDomain.CurrentDomain.ProcessExit += (_, _) => Enabled = false;
         }
         }

+ 50 - 1
PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -1,9 +1,12 @@
 using System;
 using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Windows;
 using System.Windows;
 using System.Windows.Media.Imaging;
 using System.Windows.Media.Imaging;
 using Microsoft.Win32;
 using Microsoft.Win32;
+using Newtonsoft.Json.Linq;
 using PixiEditor.Exceptions;
 using PixiEditor.Exceptions;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
@@ -18,6 +21,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 {
 {
     public class FileViewModel : SubViewModel<ViewModelMain>
     public class FileViewModel : SubViewModel<ViewModelMain>
     {
     {
+        private bool hasRecent;
+
         public RelayCommand OpenNewFilePopupCommand { get; set; }
         public RelayCommand OpenNewFilePopupCommand { get; set; }
 
 
         public RelayCommand SaveDocumentCommand { get; set; }
         public RelayCommand SaveDocumentCommand { get; set; }
@@ -26,6 +31,20 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
 
         public RelayCommand ExportFileCommand { get; set; } // Command that is used to save file
         public RelayCommand ExportFileCommand { get; set; } // Command that is used to save file
 
 
+        public RelayCommand OpenRecentCommand { get; set; }
+
+        public bool HasRecent
+        {
+            get => hasRecent;
+            set
+            {
+                hasRecent = value;
+                RaisePropertyChanged(nameof(HasRecent));
+            }
+        }
+
+        public ObservableCollection<string> RecentlyOpened { get; set; } = new ObservableCollection<string>();
+
         public FileViewModel(ViewModelMain owner)
         public FileViewModel(ViewModelMain owner)
             : base(owner)
             : base(owner)
         {
         {
@@ -33,7 +52,37 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             SaveDocumentCommand = new RelayCommand(SaveDocument, Owner.DocumentIsNotNull);
             SaveDocumentCommand = new RelayCommand(SaveDocument, Owner.DocumentIsNotNull);
             OpenFileCommand = new RelayCommand(Open);
             OpenFileCommand = new RelayCommand(Open);
             ExportFileCommand = new RelayCommand(ExportFile, CanSave);
             ExportFileCommand = new RelayCommand(ExportFile, CanSave);
+            OpenRecentCommand = new RelayCommand(OpenRecent);
             Owner.OnStartupEvent += Owner_OnStartupEvent;
             Owner.OnStartupEvent += Owner_OnStartupEvent;
+            RecentlyOpened = new ObservableCollection<string>(IPreferences.Current.GetLocalPreference<JArray>(nameof(RecentlyOpened), new JArray()).ToObject<string[]>());
+
+            if (RecentlyOpened.Count > 0)
+            {
+                HasRecent = true;
+            }
+        }
+
+        public void OpenRecent(object parameter)
+        {
+            string path = (string)parameter;
+
+            foreach (Document document in Owner.BitmapManager.Documents)
+            {
+                if (document.DocumentFilePath == path)
+                {
+                    Owner.BitmapManager.ActiveDocument = document;
+                    return;
+                }
+            }
+
+            if (!File.Exists(path))
+            {
+                NoticeDialog.Show("The file does no longer exist at that path");
+                RecentlyOpened.Remove(path);
+                return;
+            }
+
+            Open((string)parameter);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -98,7 +147,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             }
             else
             else
             {
             {
-                if (PreferencesSettings.GetPreference("ShowNewFilePopupOnStartup", true))
+                if (IPreferences.Current.GetPreference("ShowNewFilePopupOnStartup", true))
                 {
                 {
                     OpenNewFilePopup(null);
                     OpenNewFilePopup(null);
                 }
                 }

+ 1 - 1
PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs

@@ -86,7 +86,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
 
         private async void Owner_OnStartupEvent(object sender, EventArgs e)
         private async void Owner_OnStartupEvent(object sender, EventArgs e)
         {
         {
-            if (PreferencesSettings.GetPreference("CheckUpdatesOnStartup", true))
+            if (IPreferences.Current.GetPreference("CheckUpdatesOnStartup", true))
             {
             {
                 await CheckForUpdate();
                 await CheckForUpdate();
             }
             }

+ 89 - 0
PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/DiscordSettings.cs

@@ -0,0 +1,89 @@
+namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
+{
+    public class DiscordSettings : SettingsGroup
+    {
+        private bool enableRichPresence = GetPreference(nameof(EnableRichPresence), true);
+
+        public bool EnableRichPresence
+        {
+            get => enableRichPresence;
+            set
+            {
+                enableRichPresence = value;
+                RaiseAndUpdatePreference(nameof(EnableRichPresence), value);
+            }
+        }
+
+        private bool showDocumentName = GetPreference(nameof(ShowDocumentName), true);
+
+        public bool ShowDocumentName
+        {
+            get => showDocumentName;
+            set
+            {
+                showDocumentName = value;
+                RaiseAndUpdatePreference(nameof(ShowDocumentName), value);
+                RaisePropertyChanged(nameof(DetailPreview));
+            }
+        }
+
+        private bool showDocumentSize = GetPreference(nameof(ShowDocumentSize), true);
+
+        public bool ShowDocumentSize
+        {
+            get => showDocumentSize;
+            set
+            {
+                showDocumentSize = value;
+                RaiseAndUpdatePreference(nameof(ShowDocumentSize), value);
+                RaisePropertyChanged(nameof(StatePreview));
+            }
+        }
+
+        private bool showLayerCount = GetPreference(nameof(ShowLayerCount), true);
+
+        public bool ShowLayerCount
+        {
+            get => showLayerCount;
+            set
+            {
+                showLayerCount = value;
+                RaiseAndUpdatePreference(nameof(ShowLayerCount), value);
+                RaisePropertyChanged(nameof(StatePreview));
+            }
+        }
+
+        public string DetailPreview
+        {
+            get
+            {
+                return ShowDocumentName ? $"Editing coolPixelArt.pixi" : "Editing something (incognito)";
+            }
+        }
+
+        public string StatePreview
+        {
+            get
+            {
+                string state = string.Empty;
+
+                if (ShowDocumentSize)
+                {
+                    state = "16x16";
+                }
+
+                if (ShowDocumentSize && ShowLayerCount)
+                {
+                    state += ", ";
+                }
+
+                if (ShowLayerCount)
+                {
+                    state += "2 Layers";
+                }
+
+                return state;
+            }
+        }
+    }
+}

+ 62 - 0
PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs

@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
+{
+    public class FileSettings : SettingsGroup
+    {
+        private bool showNewFilePopupOnStartup = GetPreference("ShowNewFilePopupOnStartup", true);
+
+        public bool ShowNewFilePopupOnStartup
+        {
+            get => showNewFilePopupOnStartup;
+            set
+            {
+                showNewFilePopupOnStartup = value;
+                string name = nameof(ShowNewFilePopupOnStartup);
+                RaiseAndUpdatePreference(name, value);
+            }
+        }
+
+        private long defaultNewFileWidth = GetPreference("DefaultNewFileWidth", 16L);
+
+        public long DefaultNewFileWidth
+        {
+            get => defaultNewFileWidth;
+            set
+            {
+                defaultNewFileWidth = value;
+                string name = nameof(DefaultNewFileWidth);
+                RaiseAndUpdatePreference(name, value);
+            }
+        }
+
+        private long defaultNewFileHeight = GetPreference("DefaultNewFileHeight", 16L);
+
+        public long DefaultNewFileHeight
+        {
+            get => defaultNewFileHeight;
+            set
+            {
+                defaultNewFileHeight = value;
+                string name = nameof(DefaultNewFileHeight);
+                RaiseAndUpdatePreference(name, value);
+            }
+        }
+
+        private int maxOpenedRecently = GetPreference(nameof(MaxOpenedRecently), 10);
+
+        public int MaxOpenedRecently
+        {
+            get => maxOpenedRecently;
+            set
+            {
+                maxOpenedRecently = value;
+                RaiseAndUpdatePreference(nameof(MaxOpenedRecently), value);
+            }
+        }
+    }
+}

+ 18 - 0
PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/UpdateSettings.cs

@@ -0,0 +1,18 @@
+namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
+{
+    public class UpdateSettings : SettingsGroup
+    {
+        private bool checkUpdatesOnStartup = GetPreference("CheckUpdatesOnStartup", true);
+
+        public bool CheckUpdatesOnStartup
+        {
+            get => checkUpdatesOnStartup;
+            set
+            {
+                checkUpdatesOnStartup = value;
+                string name = nameof(CheckUpdatesOnStartup);
+                RaiseAndUpdatePreference(name, value);
+            }
+        }
+    }
+}

+ 3 - 3
PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsGroup.cs

@@ -8,14 +8,14 @@ namespace PixiEditor.ViewModels.SubViewModels.UserPreferences
     {
     {
         protected static T GetPreference<T>(string name)
         protected static T GetPreference<T>(string name)
         {
         {
-            return PreferencesSettings.GetPreference<T>(name);
+            return IPreferences.Current.GetPreference<T>(name);
         }
         }
 
 
 #nullable enable
 #nullable enable
 
 
         protected static T? GetPreference<T>(string name, T? fallbackValue)
         protected static T? GetPreference<T>(string name, T? fallbackValue)
         {
         {
-            return PreferencesSettings.GetPreference(name, fallbackValue);
+            return IPreferences.Current.GetPreference(name, fallbackValue);
         }
         }
 
 
 #nullable disable
 #nullable disable
@@ -23,7 +23,7 @@ namespace PixiEditor.ViewModels.SubViewModels.UserPreferences
         protected void RaiseAndUpdatePreference<T>(string name, T value)
         protected void RaiseAndUpdatePreference<T>(string name, T value)
         {
         {
             RaisePropertyChanged(name);
             RaisePropertyChanged(name);
-            PreferencesSettings.UpdatePreference(name, value);
+            IPreferences.Current.UpdatePreference(name, value);
         }
         }
     }
     }
 }
 }

+ 3 - 143
PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsViewModel.cs

@@ -1,158 +1,18 @@
 using System;
 using System;
 using System.Configuration;
 using System.Configuration;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.Models.UserPreferences;
+using PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings;
 
 
 namespace PixiEditor.ViewModels.SubViewModels.UserPreferences
 namespace PixiEditor.ViewModels.SubViewModels.UserPreferences
 {
 {
     public class SettingsViewModel : SubViewModel<SettingsWindowViewModel>
     public class SettingsViewModel : SubViewModel<SettingsWindowViewModel>
     {
     {
-        private bool showNewFilePopupOnStartup = PreferencesSettings.GetPreference("ShowNewFilePopupOnStartup", true);
+        public FileSettings File { get; set; } = new FileSettings();
 
 
-        public bool ShowNewFilePopupOnStartup
-        {
-            get => showNewFilePopupOnStartup;
-            set
-            {
-                showNewFilePopupOnStartup = value;
-                string name = nameof(ShowNewFilePopupOnStartup);
-                RaiseAndUpdatePreference(name, value);
-            }
-        }
-
-        private bool checkUpdatesOnStartup = PreferencesSettings.GetPreference("CheckUpdatesOnStartup", true);
-
-        public bool CheckUpdatesOnStartup
-        {
-            get => checkUpdatesOnStartup;
-            set
-            {
-                checkUpdatesOnStartup = value;
-                string name = nameof(CheckUpdatesOnStartup);
-                RaiseAndUpdatePreference(name, value);
-            }
-        }
-
-        private long defaultNewFileWidth = (int)PreferencesSettings.GetPreference("DefaultNewFileWidth", 16L);
-
-        public long DefaultNewFileWidth
-        {
-            get => defaultNewFileWidth;
-            set
-            {
-                defaultNewFileWidth = value;
-                string name = nameof(DefaultNewFileWidth);
-                RaiseAndUpdatePreference(name, value);
-            }
-        }
-
-        private long defaultNewFileHeight = (int)PreferencesSettings.GetPreference("DefaultNewFileHeight", 16L);
-
-        public long DefaultNewFileHeight
-        {
-            get => defaultNewFileHeight;
-            set
-            {
-                defaultNewFileHeight = value;
-                string name = nameof(DefaultNewFileHeight);
-                RaiseAndUpdatePreference(name, value);
-            }
-        }
-
-        public class DiscordSettings : SettingsGroup
-        {
-            private bool enableRichPresence = GetPreference(nameof(EnableRichPresence), true);
-
-            public bool EnableRichPresence
-            {
-                get => enableRichPresence;
-                set
-                {
-                    enableRichPresence = value;
-                    RaiseAndUpdatePreference(nameof(EnableRichPresence), value);
-                }
-            }
-
-            private bool showDocumentName = GetPreference(nameof(ShowDocumentName), true);
-
-            public bool ShowDocumentName
-            {
-                get => showDocumentName;
-                set
-                {
-                    showDocumentName = value;
-                    RaiseAndUpdatePreference(nameof(ShowDocumentName), value);
-                    RaisePropertyChanged(nameof(DetailPreview));
-                }
-            }
-
-            private bool showDocumentSize = GetPreference(nameof(ShowDocumentSize), true);
-
-            public bool ShowDocumentSize
-            {
-                get => showDocumentSize;
-                set
-                {
-                    showDocumentSize = value;
-                    RaiseAndUpdatePreference(nameof(ShowDocumentSize), value);
-                    RaisePropertyChanged(nameof(StatePreview));
-                }
-            }
-
-            private bool showLayerCount = GetPreference(nameof(ShowLayerCount), true);
-
-            public bool ShowLayerCount
-            {
-                get => showLayerCount;
-                set
-                {
-                    showLayerCount = value;
-                    RaiseAndUpdatePreference(nameof(ShowLayerCount), value);
-                    RaisePropertyChanged(nameof(StatePreview));
-                }
-            }
-
-            public string DetailPreview
-            {
-                get
-                {
-                    return ShowDocumentName ? $"Editing coolPixelArt.pixi" : "Editing something (incognito)";
-                }
-            }
-
-            public string StatePreview
-            {
-                get
-                {
-                    string state = string.Empty;
-
-                    if (ShowDocumentSize)
-                    {
-                        state = "16x16";
-                    }
-
-                    if (ShowDocumentSize && ShowLayerCount)
-                    {
-                        state += ", ";
-                    }
-
-                    if (ShowLayerCount)
-                    {
-                        state += "2 Layers";
-                    }
-
-                    return state;
-                }
-            }
-        }
+        public UpdateSettings Update { get; set; } = new UpdateSettings();
 
 
         public DiscordSettings Discord { get; set; } = new DiscordSettings();
         public DiscordSettings Discord { get; set; } = new DiscordSettings();
 
 
-        public void RaiseAndUpdatePreference<T>(string name, T value)
-        {
-            RaisePropertyChanged(name);
-            PreferencesSettings.UpdatePreference(name, value);
-        }
-
         public SettingsViewModel(SettingsWindowViewModel owner)
         public SettingsViewModel(SettingsWindowViewModel owner)
             : base(owner)
             : base(owner)
         {
         {

+ 9 - 3
PixiEditor/ViewModels/ViewModelMain.cs

@@ -4,6 +4,7 @@ using System.ComponentModel;
 using System.Linq;
 using System.Linq;
 using System.Windows;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Input;
+using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers.Shortcuts;
 using PixiEditor.Models.Controllers.Shortcuts;
@@ -64,9 +65,15 @@ namespace PixiEditor.ViewModels
 
 
         public ShortcutController ShortcutController { get; set; }
         public ShortcutController ShortcutController { get; set; }
 
 
-        public ViewModelMain()
+        public IPreferences Preferences { get; set; }
+
+        public ViewModelMain(IServiceProvider services)
         {
         {
-            PreferencesSettings.Init();
+            Current = this;
+
+            Preferences = services.GetRequiredService<IPreferences>();
+
+            Preferences.Init();
 
 
             BitmapManager = new BitmapManager();
             BitmapManager = new BitmapManager();
             BitmapManager.BitmapOperations.BitmapChanged += BitmapUtility_BitmapChanged;
             BitmapManager.BitmapOperations.BitmapChanged += BitmapUtility_BitmapChanged;
@@ -144,7 +151,6 @@ namespace PixiEditor.ViewModels
                 }
                 }
             };
             };
             BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor;
             BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor;
-            Current = this;
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 40 - 0
PixiEditor/Views/Dialogs/NoticePopup.xaml

@@ -0,0 +1,40 @@
+<Window x:Class="PixiEditor.Views.Dialogs.NoticePopup"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
+        mc:Ignorable="d"
+        Title="NoticePopup" Height="200" Width="500"
+        x:Name="popup">
+
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+
+    <Grid Background="{StaticResource AccentColor}" Focusable="True">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="35" />
+            <RowDefinition Height="34*" />
+            <RowDefinition Height="21*" />
+        </Grid.RowDefinitions>
+        <i:Interaction.Behaviors>
+            <behaviours:ClearFocusOnClickBehavior/>
+        </i:Interaction.Behaviors>
+        <TextBlock Grid.Row="1" Text="{Binding Body, ElementName=popup}" HorizontalAlignment="Center"
+                   VerticalAlignment="Center" FontSize="18" Foreground="White" />
+        <DockPanel Grid.Row="0" Background="{StaticResource MainColor}">
+            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
+                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
+                    Command="{Binding DataContext.CancelCommand, ElementName=popup}" />
+        </DockPanel>
+        <StackPanel Grid.Row="2" Orientation="Horizontal" VerticalAlignment="Bottom" HorizontalAlignment="Center"
+                    Margin="0,0,10,10">
+            <Button Height="30" Width="60"
+                    Click="OkButton_Close"
+                    Style="{StaticResource DarkRoundButton}" Content="Ok">
+            </Button>
+        </StackPanel>
+    </Grid>
+</Window>

+ 41 - 0
PixiEditor/Views/Dialogs/NoticePopup.xaml.cs

@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+
+namespace PixiEditor.Views.Dialogs
+{
+    /// <summary>
+    /// Interaction logic for NoticePopup.xaml.
+    /// </summary>
+    public partial class NoticePopup : Window
+    {
+        public static readonly DependencyProperty BodyProperty =
+            DependencyProperty.Register(nameof(Body), typeof(string), typeof(NoticePopup));
+
+        public string Body
+        {
+            get => (string)GetValue(BodyProperty);
+            set => SetValue(BodyProperty, value);
+        }
+
+        public NoticePopup()
+        {
+            InitializeComponent();
+        }
+
+        private void OkButton_Close(object sender, RoutedEventArgs e)
+        {
+            Close();
+        }
+    }
+}

+ 8 - 4
PixiEditor/Views/Dialogs/SettingsWindow.xaml

@@ -58,13 +58,17 @@
                     <Label Content="File" Style="{StaticResource Header1}"/>
                     <Label Content="File" Style="{StaticResource Header1}"/>
                     <StackPanel Orientation="Vertical" Margin="50 0 50 0">
                     <StackPanel Orientation="Vertical" Margin="50 0 50 0">
                         <CheckBox Content="Show New File dialog on startup" 
                         <CheckBox Content="Show New File dialog on startup" 
-                                  IsChecked="{Binding SettingsSubViewModel.ShowNewFilePopupOnStartup}"/>
+                                  IsChecked="{Binding SettingsSubViewModel.File.ShowNewFilePopupOnStartup}"/>
+                        <StackPanel Orientation="Horizontal" Margin="0,10,0,0">
+                            <Label Content="Max Saved Opened Recently:" ToolTip="How many documents are shown under File > Recent. Default: 10" Style="{StaticResource BaseLabel}"/>
+                            <views:NumberInput FontSize="16" Value="{Binding SettingsSubViewModel.File.MaxOpenedRecently}" Width="40"/>
+                        </StackPanel>
                         <Label Content="Default new file size:" Style="{StaticResource Header2}" Margin="0 20 0 20"/>
                         <Label Content="Default new file size:" Style="{StaticResource Header2}" Margin="0 20 0 20"/>
                         <StackPanel Orientation="Horizontal" Margin="40,0,0,0">
                         <StackPanel Orientation="Horizontal" Margin="40,0,0,0">
                             <Label Content="Width:" Style="{StaticResource BaseLabel}"/>
                             <Label Content="Width:" Style="{StaticResource BaseLabel}"/>
-                            <views:SizeInput FontSize="16" Size="{Binding SettingsSubViewModel.DefaultNewFileWidth, Mode=TwoWay}" Width="60" Height="25"/>
+                            <views:SizeInput FontSize="16" Size="{Binding SettingsSubViewModel.File.DefaultNewFileWidth, Mode=TwoWay}" Width="60" Height="25"/>
                             <Label Content="Height:" Style="{StaticResource BaseLabel}"/>
                             <Label Content="Height:" Style="{StaticResource BaseLabel}"/>
-                            <views:SizeInput FontSize="16" Size="{Binding SettingsSubViewModel.DefaultNewFileHeight, Mode=TwoWay}" Width="60" Height="25"/>
+                            <views:SizeInput FontSize="16" Size="{Binding SettingsSubViewModel.File.DefaultNewFileHeight, Mode=TwoWay}" Width="60" Height="25"/>
                         </StackPanel>
                         </StackPanel>
                     </StackPanel>
                     </StackPanel>
                 </StackPanel>
                 </StackPanel>
@@ -74,7 +78,7 @@
                 <StackPanel Orientation="Vertical">
                 <StackPanel Orientation="Vertical">
                     <Label Style="{StaticResource Header1}" Content="Auto-updates"/>
                     <Label Style="{StaticResource Header1}" Content="Auto-updates"/>
                     <StackPanel Orientation="Vertical" Margin="50 0 50 0">
                     <StackPanel Orientation="Vertical" Margin="50 0 50 0">
-                        <CheckBox IsChecked="{Binding SettingsSubViewModel.CheckUpdatesOnStartup}" Content="Check updates on startup"/>
+                        <CheckBox IsChecked="{Binding SettingsSubViewModel.Update.CheckUpdatesOnStartup}" Content="Check updates on startup"/>
                     </StackPanel>
                     </StackPanel>
                 </StackPanel>
                 </StackPanel>
             </Grid>
             </Grid>

+ 24 - 10
PixiEditor/Views/MainWindow.xaml

@@ -14,7 +14,7 @@
         xmlns:avalonDockTheme="clr-namespace:PixiEditor.Styles.AvalonDock"
         xmlns:avalonDockTheme="clr-namespace:PixiEditor.Styles.AvalonDock"
         mc:Ignorable="d" WindowStyle="None" Initialized="MainWindow_Initialized"
         mc:Ignorable="d" WindowStyle="None" Initialized="MainWindow_Initialized"
         Title="PixiEditor" Name="mainWindow" Height="1000" Width="1600" Background="{StaticResource MainColor}"
         Title="PixiEditor" Name="mainWindow" Height="1000" Width="1600" Background="{StaticResource MainColor}"
-        WindowStartupLocation="CenterScreen" WindowState="Maximized" DataContext="{DynamicResource ViewModelMain}">
+        WindowStartupLocation="CenterScreen" WindowState="Maximized">
     <WindowChrome.WindowChrome>
     <WindowChrome.WindowChrome>
         <WindowChrome CaptionHeight="32"
         <WindowChrome CaptionHeight="32"
                       ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
                       ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
@@ -22,7 +22,7 @@
 
 
     <Window.Resources>
     <Window.Resources>
         <ResourceDictionary>
         <ResourceDictionary>
-            <vm:ViewModelMain x:Key="ViewModelMain" />
+            <!--<vm:ViewModelMain x:Key="ViewModelMain" />-->
             <BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
             <BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
             <converters:BoolToIntConverter x:Key="BoolToIntConverter" />
             <converters:BoolToIntConverter x:Key="BoolToIntConverter" />
             <converters:NotNullToBoolConverter x:Key="NotNullToBoolConverter" />
             <converters:NotNullToBoolConverter x:Key="NotNullToBoolConverter" />
@@ -84,6 +84,14 @@
                 <MenuItem Header="_File">
                 <MenuItem Header="_File">
                     <MenuItem InputGestureText="CTRL+N" Header="_New" Command="{Binding FileSubViewModel.OpenNewFilePopupCommand}" />
                     <MenuItem InputGestureText="CTRL+N" Header="_New" Command="{Binding FileSubViewModel.OpenNewFilePopupCommand}" />
                     <MenuItem Header="_Open" InputGestureText="Ctrl+O" Command="{Binding FileSubViewModel.OpenFileCommand}" />
                     <MenuItem Header="_Open" InputGestureText="Ctrl+O" Command="{Binding FileSubViewModel.OpenFileCommand}" />
+                    <MenuItem Header="_Recent" ItemsSource="{Binding FileSubViewModel.RecentlyOpened}" x:Name="recentItemMenu" IsEnabled="{Binding FileSubViewModel.HasRecent}">
+                        <MenuItem.ItemContainerStyle>
+                            <Style TargetType="MenuItem" BasedOn="{StaticResource menuItemStyle}">
+                                <Setter Property="Command" Value="{Binding ElementName=recentItemMenu, Path=DataContext.FileSubViewModel.OpenRecentCommand}"/>
+                                <Setter Property="CommandParameter" Value="{Binding}"/>
+                            </Style>
+                        </MenuItem.ItemContainerStyle>
+                    </MenuItem>
                     <MenuItem Header="_Save" InputGestureText="Ctrl+S" Command="{Binding FileSubViewModel.SaveDocumentCommand}" />
                     <MenuItem Header="_Save" InputGestureText="Ctrl+S" Command="{Binding FileSubViewModel.SaveDocumentCommand}" />
                     <MenuItem Header="_Save As..." InputGestureText="Ctrl+Shift+S"
                     <MenuItem Header="_Save As..." InputGestureText="Ctrl+Shift+S"
                               Command="{Binding FileSubViewModel.SaveDocumentCommand}" CommandParameter="AsNew" />
                               Command="{Binding FileSubViewModel.SaveDocumentCommand}" CommandParameter="AsNew" />
@@ -273,7 +281,7 @@
                                                         <Grid.ContextMenu>
                                                         <Grid.ContextMenu>
                                                             <ContextMenu>
                                                             <ContextMenu>
                                                                 <MenuItem Header="Remove" Foreground="White"
                                                                 <MenuItem Header="Remove" Foreground="White"
-                                                                      Command="{Binding ColorsSubViewModel.RemoveSwatchCommand, Source={StaticResource ViewModelMain}}"
+                                                                      Command="{Binding ColorsSubViewModel.RemoveSwatchCommand}"
                                                                       CommandParameter="{Binding}" />
                                                                       CommandParameter="{Binding}" />
                                                             </ContextMenu>
                                                             </ContextMenu>
                                                         </Grid.ContextMenu>
                                                         </Grid.ContextMenu>
@@ -321,7 +329,7 @@
                                                     </ItemsControl.ItemsPanel>
                                                     </ItemsControl.ItemsPanel>
                                                     <ItemsControl.ItemTemplate>
                                                     <ItemsControl.ItemTemplate>
                                                         <DataTemplate>
                                                         <DataTemplate>
-                                                            <vws:LayerItem LayerIndex="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
+                                                            <vws:LayerItem Tag="{Binding DataContext, ElementName=mainWindow}" LayerIndex="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                 Path=(ItemsControl.AlternationIndex)}" SetActiveLayerCommand="{Binding Path=DataContext.LayersSubViewModel.SetActiveLayerCommand, ElementName=mainWindow}"
                                 Path=(ItemsControl.AlternationIndex)}" SetActiveLayerCommand="{Binding Path=DataContext.LayersSubViewModel.SetActiveLayerCommand, ElementName=mainWindow}"
                                                                    LayerName="{Binding Name, Mode=TwoWay}" IsActive="{Binding IsActive, Mode=TwoWay}"
                                                                    LayerName="{Binding Name, Mode=TwoWay}" IsActive="{Binding IsActive, Mode=TwoWay}"
                                                                    IsRenaming="{Binding IsRenaming, Mode=TwoWay}"
                                                                    IsRenaming="{Binding IsRenaming, Mode=TwoWay}"
@@ -331,27 +339,33 @@
                                                                 <vws:LayerItem.ContextMenu>
                                                                 <vws:LayerItem.ContextMenu>
                                                                     <ContextMenu>
                                                                     <ContextMenu>
                                                                         <MenuItem Header="Delete"
                                                                         <MenuItem Header="Delete"
-                                                                                  Command="{Binding LayersSubViewModel.DeleteLayerCommand, Source={StaticResource ViewModelMain}}"
+                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.DeleteLayerCommand,
+                                                                                    RelativeSource={RelativeSource AncestorType=ContextMenu}}"
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                 Path=(ItemsControl.AlternationIndex)}" />
                                 Path=(ItemsControl.AlternationIndex)}" />
                                                                         <MenuItem Header="Rename"
                                                                         <MenuItem Header="Rename"
-                                                                                  Command="{Binding LayersSubViewModel.RenameLayerCommand, Source={StaticResource ViewModelMain}}"
+                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.RenameLayerCommand,
+                                                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                 Path=(ItemsControl.AlternationIndex)}" />
                                 Path=(ItemsControl.AlternationIndex)}" />
                                                                         <MenuItem Header="Move to front"
                                                                         <MenuItem Header="Move to front"
-                                                                                  Command="{Binding LayersSubViewModel.MoveToFrontCommand, Source={StaticResource ViewModelMain}}"
+                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.MoveToFrontCommand, 
+                                                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                 Path=(ItemsControl.AlternationIndex)}" />
                                 Path=(ItemsControl.AlternationIndex)}" />
                                                                         <MenuItem Header="Move to back"
                                                                         <MenuItem Header="Move to back"
-                                                                                  Command="{Binding LayersSubViewModel.MoveToBackCommand, Source={StaticResource ViewModelMain}}"
+                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.MoveToBackCommand, 
+                                                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                 Path=(ItemsControl.AlternationIndex)}" />
                                 Path=(ItemsControl.AlternationIndex)}" />
                                                                         <MenuItem Header="Merge with above"
                                                                         <MenuItem Header="Merge with above"
-                                                                                  Command="{Binding LayersSubViewModel.MergeWithAboveCommand, Source={StaticResource ViewModelMain}}"
+                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.MergeWithAboveCommand, 
+                                                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                 Path=(ItemsControl.AlternationIndex)}" />
                                 Path=(ItemsControl.AlternationIndex)}" />
                                                                         <MenuItem Header="Merge with below"
                                                                         <MenuItem Header="Merge with below"
-                                                                                  Command="{Binding LayersSubViewModel.MergeWithBelowCommand, Source={StaticResource ViewModelMain}}"
+                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.MergeWithBelowCommand, 
+                                                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                 Path=(ItemsControl.AlternationIndex)}" />
                                 Path=(ItemsControl.AlternationIndex)}" />
                                                                     </ContextMenu>
                                                                     </ContextMenu>

+ 8 - 0
PixiEditor/Views/MainWindow.xaml.cs

@@ -5,9 +5,11 @@ using System.IO;
 using System.Reflection;
 using System.Reflection;
 using System.Windows;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Input;
+using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Processes;
 using PixiEditor.Models.Processes;
+using PixiEditor.Models.UserPreferences;
 using PixiEditor.UpdateModule;
 using PixiEditor.UpdateModule;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels;
 
 
@@ -23,6 +25,12 @@ namespace PixiEditor
         public MainWindow()
         public MainWindow()
         {
         {
             InitializeComponent();
             InitializeComponent();
+
+            IServiceCollection services = new ServiceCollection()
+                .AddSingleton<IPreferences>(new PreferencesSettings());
+
+            DataContext = new ViewModelMain(services.BuildServiceProvider());
+
             StateChanged += MainWindowStateChangeRaised;
             StateChanged += MainWindowStateChangeRaised;
             MaxHeight = SystemParameters.MaximizedPrimaryScreenHeight;
             MaxHeight = SystemParameters.MaximizedPrimaryScreenHeight;
             viewModel = (ViewModelMain)DataContext;
             viewModel = (ViewModelMain)DataContext;

+ 24 - 0
PixiEditorTests/Helpers.cs

@@ -0,0 +1,24 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Models.UserPreferences;
+using PixiEditor.ViewModels;
+
+namespace PixiEditorTests
+{
+    public static class Helpers
+    {
+        public static ViewModelMain MockedViewModelMain()
+        {
+            IServiceProvider provider = MockedServiceProvider();
+
+            return new ViewModelMain(provider);
+        }
+
+        public static IServiceProvider MockedServiceProvider()
+        {
+            return new ServiceCollection()
+                .AddSingleton<IPreferences>(new Mocks.PreferenceSettingsMock())
+                .BuildServiceProvider();
+        }
+    }
+}

+ 56 - 0
PixiEditorTests/Mocks/PreferenceSettingsMock.cs

@@ -0,0 +1,56 @@
+using System;
+using PixiEditor.Models.UserPreferences;
+
+namespace PixiEditorTests.Mocks
+{
+    public class PreferenceSettingsMock : IPreferences
+    {
+        public void AddCallback(string setting, Action<object> action)
+        {
+        }
+
+#nullable enable
+
+        public T? GetLocalPreference<T>(string name)
+        {
+            return default;
+        }
+
+        public T? GetLocalPreference<T>(string name, T? fallbackValue)
+        {
+            return fallbackValue;
+        }
+
+        public T? GetPreference<T>(string name)
+        {
+            return default;
+        }
+
+        public T? GetPreference<T>(string name, T? fallbackValue)
+        {
+            return fallbackValue;
+        }
+
+#nullable disable
+
+        public void Init()
+        {
+        }
+
+        public void Init(string path, string localPath)
+        {
+        }
+
+        public void Save()
+        {
+        }
+
+        public void UpdateLocalPreference<T>(string name, T value)
+        {
+        }
+
+        public void UpdatePreference<T>(string name, T value)
+        {
+        }
+    }
+}

+ 18 - 0
PixiEditorTests/ModelsTests/DataHoldersTests/DocumentTests.cs

@@ -4,6 +4,7 @@ using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
+using PixiEditor.ViewModels;
 using Xunit;
 using Xunit;
 
 
 namespace PixiEditorTests.ModelsTests.DataHoldersTests
 namespace PixiEditorTests.ModelsTests.DataHoldersTests
@@ -283,5 +284,22 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             Assert.Equal("Test", document.Layers[1].Name);
             Assert.Equal("Test", document.Layers[1].Name);
             Assert.Equal("Test2", document.Layers[0].Name);
             Assert.Equal("Test2", document.Layers[0].Name);
         }
         }
+
+        [StaFact]
+        public void TestThatDocumentGetsAddedToRecentlyOpenedList()
+        {
+            ViewModelMain viewModel = Helpers.MockedViewModelMain();
+
+            Document document = new Document(1, 1)
+            {
+                XamlAccesibleViewModel = viewModel
+            };
+
+            string testFilePath = @"C:\idk\somewhere\homework";
+
+            document.DocumentFilePath = testFilePath;
+
+            Assert.Contains(viewModel.FileSubViewModel.RecentlyOpened, x => x == testFilePath);
+        }
     }
     }
 }
 }

+ 4 - 2
PixiEditorTests/ModelsTests/ToolsTests/ZoomToolTests.cs

@@ -1,4 +1,6 @@
-using PixiEditor.Models.Tools.Tools;
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Models.Tools.Tools;
+using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels;
 using Xunit;
 using Xunit;
 
 
@@ -10,7 +12,7 @@ namespace PixiEditorTests.ModelsTests.ToolsTests
         [StaFact]
         [StaFact]
         public void TestThatZoomSetsActiveDocumentZoomPercentage()
         public void TestThatZoomSetsActiveDocumentZoomPercentage()
         {
         {
-            ViewModelMain vm = new ViewModelMain();
+            ViewModelMain vm = new ViewModelMain(new ServiceCollection().AddSingleton<IPreferences>(new Mocks.PreferenceSettingsMock()).BuildServiceProvider());
             vm.BitmapManager.ActiveDocument = new PixiEditor.Models.DataHolders.Document(10, 10);
             vm.BitmapManager.ActiveDocument = new PixiEditor.Models.DataHolders.Document(10, 10);
             ZoomTool zoomTool = new ZoomTool();
             ZoomTool zoomTool = new ZoomTool();
             double zoom = 110;
             double zoom = 110;

+ 43 - 1
PixiEditorTests/ModelsTests/UserPreferencesTests/PreferencesSettingsTests.cs

@@ -10,9 +10,13 @@ namespace PixiEditorTests.ModelsTests.UserPreferencesTests
     {
     {
         public static string PathToPreferencesFile { get; } = Path.Join("PixiEditor", "test_preferences.json");
         public static string PathToPreferencesFile { get; } = Path.Join("PixiEditor", "test_preferences.json");
 
 
+        public static string PathToLocalPreferencesFile { get; } = Path.Join("PixiEditor", "local_test_preferences.json");
+
+        public static readonly PreferencesSettings PreferencesSettings = new PreferencesSettings();
+
         public PreferencesSettingsTests()
         public PreferencesSettingsTests()
         {
         {
-            PreferencesSettings.Init(PathToPreferencesFile);
+            PreferencesSettings.Init(PathToPreferencesFile, PathToLocalPreferencesFile);
         }
         }
 
 
         [Fact]
         [Fact]
@@ -25,6 +29,7 @@ namespace PixiEditorTests.ModelsTests.UserPreferencesTests
         public void TestThatInitCreatesUserPreferencesJson()
         public void TestThatInitCreatesUserPreferencesJson()
         {
         {
             Assert.True(File.Exists(PathToPreferencesFile));
             Assert.True(File.Exists(PathToPreferencesFile));
+            Assert.True(File.Exists(PathToLocalPreferencesFile));
         }
         }
 
 
         [Theory]
         [Theory]
@@ -63,5 +68,42 @@ namespace PixiEditorTests.ModelsTests.UserPreferencesTests
                 Assert.Equal(value, dict[name]);
                 Assert.Equal(value, dict[name]);
             }
             }
         }
         }
+
+        [Theory]
+        [InlineData(-2)]
+        [InlineData(false)]
+        [InlineData("string")]
+        [InlineData(null)]
+        public void TestThatGetPreferenceOnNonExistingKeyReturnsFallbackValueLocal<T>(T value)
+        {
+            T fallbackValue = value;
+            T preferenceValue = PreferencesSettings.GetLocalPreference<T>("NonExistingPreference", fallbackValue);
+            Assert.Equal(fallbackValue, preferenceValue);
+        }
+
+        [Theory]
+        [InlineData("IntPreference", 1)]
+        [InlineData("BoolPreference", true)]
+        public void TestThatUpdatePreferenceUpdatesDictionaryLocal<T>(string name, T value)
+        {
+            PreferencesSettings.UpdateLocalPreference(name, value);
+            Assert.Equal(value, PreferencesSettings.GetLocalPreference<T>(name));
+        }
+
+        [Theory]
+        [InlineData("LongPreference", 1L)]
+        public void TestThatSaveUpdatesFileLocal<T>(string name, T value)
+        {
+            PreferencesSettings.LocalPreferences[name] = value;
+            PreferencesSettings.Save();
+            using (var fs = new FileStream(PathToPreferencesFile, FileMode.Open, FileAccess.Read, FileShare.Read))
+            {
+                using StreamReader sr = new StreamReader(fs);
+                string json = sr.ReadToEnd();
+                var dict = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
+                Assert.True(dict.ContainsKey(name));
+                Assert.Equal(value, dict[name]);
+            }
+        }
     }
     }
 }
 }

+ 2 - 0
PixiEditorTests/PixiEditorTests.csproj

@@ -23,6 +23,8 @@
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
     </PackageReference>
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
     <PackageReference Include="Moq" Version="4.16.0" />
     <PackageReference Include="Moq" Version="4.16.0" />
     <PackageReference Include="OpenCover" Version="4.7.922" />
     <PackageReference Include="OpenCover" Version="4.7.922" />

+ 24 - 12
PixiEditorTests/ViewModelsTests/ViewModelMainTests.cs

@@ -1,12 +1,15 @@
-using System.IO;
+using System;
+using System.IO;
 using System.Windows.Input;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media;
+using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
 using PixiEditor.Models.Tools.Tools;
+using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels;
 using Xunit;
 using Xunit;
 
 
@@ -15,10 +18,19 @@ namespace PixiEditorTests.ViewModelsTests
     [Collection("Application collection")]
     [Collection("Application collection")]
     public class ViewModelMainTests
     public class ViewModelMainTests
     {
     {
+        public static IServiceProvider Services;
+
+        public ViewModelMainTests()
+        {
+            Services = new ServiceCollection()
+                .AddSingleton<IPreferences>(new Mocks.PreferenceSettingsMock())
+                .BuildServiceProvider();
+        }
+
         [StaFact]
         [StaFact]
         public void TestThatConstructorSetsUpControllersCorrectly()
         public void TestThatConstructorSetsUpControllersCorrectly()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             Assert.NotNull(viewModel.ChangesController);
             Assert.NotNull(viewModel.ChangesController);
             Assert.NotNull(viewModel.ShortcutController);
             Assert.NotNull(viewModel.ShortcutController);
@@ -30,7 +42,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatSwapColorsCommandSwapsColors()
         public void TestThatSwapColorsCommandSwapsColors()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             viewModel.ColorsSubViewModel.PrimaryColor = Colors.Black;
             viewModel.ColorsSubViewModel.PrimaryColor = Colors.Black;
             viewModel.ColorsSubViewModel.SecondaryColor = Colors.White;
             viewModel.ColorsSubViewModel.SecondaryColor = Colors.White;
@@ -44,7 +56,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatNewDocumentCreatesNewDocumentWithBaseLayer()
         public void TestThatNewDocumentCreatesNewDocumentWithBaseLayer()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             viewModel.FileSubViewModel.NewDocument(5, 5);
             viewModel.FileSubViewModel.NewDocument(5, 5);
 
 
@@ -55,7 +67,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatMouseMoveCommandUpdatesCurrentCoordinates()
         public void TestThatMouseMoveCommandUpdatesCurrentCoordinates()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
             viewModel.BitmapManager.ActiveDocument = new Document(10, 10);
             viewModel.BitmapManager.ActiveDocument = new Document(10, 10);
 
 
             Assert.Equal(new Coordinates(0, 0), MousePositionConverter.CurrentCoordinates);
             Assert.Equal(new Coordinates(0, 0), MousePositionConverter.CurrentCoordinates);
@@ -71,7 +83,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatSelectToolCommandSelectsNewTool()
         public void TestThatSelectToolCommandSelectsNewTool()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             Assert.Equal(typeof(MoveTool), viewModel.BitmapManager.SelectedTool.GetType());
             Assert.Equal(typeof(MoveTool), viewModel.BitmapManager.SelectedTool.GetType());
 
 
@@ -83,7 +95,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatMouseUpCommandStopsRecordingMouseMovements()
         public void TestThatMouseUpCommandStopsRecordingMouseMovements()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             viewModel.BitmapManager.MouseController.StartRecordingMouseMovementChanges(true);
             viewModel.BitmapManager.MouseController.StartRecordingMouseMovementChanges(true);
 
 
@@ -97,7 +109,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatNewLayerCommandCreatesNewLayer()
         public void TestThatNewLayerCommandCreatesNewLayer()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
 
 
@@ -111,7 +123,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatSaveDocumentCommandSavesFile()
         public void TestThatSaveDocumentCommandSavesFile()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
             string fileName = "testFile.pixi";
             string fileName = "testFile.pixi";
 
 
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1)
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1)
@@ -129,7 +141,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatAddSwatchAddsNonDuplicateSwatch()
         public void TestThatAddSwatchAddsNonDuplicateSwatch()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
 
 
             viewModel.ColorsSubViewModel.AddSwatch(Colors.Green);
             viewModel.ColorsSubViewModel.AddSwatch(Colors.Green);
@@ -149,7 +161,7 @@ namespace PixiEditorTests.ViewModelsTests
         [InlineData(120, 150)]
         [InlineData(120, 150)]
         public void TestThatSelectAllCommandSelectsWholeDocument(int docWidth, int docHeight)
         public void TestThatSelectAllCommandSelectsWholeDocument(int docWidth, int docHeight)
         {
         {
-            ViewModelMain viewModel = new ViewModelMain
+            ViewModelMain viewModel = new ViewModelMain(Services)
             {
             {
                 BitmapManager = { ActiveDocument = new Document(docWidth, docHeight) }
                 BitmapManager = { ActiveDocument = new Document(docWidth, docHeight) }
             };
             };
@@ -165,7 +177,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatDocumentIsNotNullReturnsTrue()
         public void TestThatDocumentIsNotNullReturnsTrue()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);