Browse Source

Merge pull request #98 from PixiEditor/settings-menu

Added Settings (user preferences) menu
Krzysztof Krysiński 4 years ago
parent
commit
09049f9920

+ 2 - 0
PixiEditor/App.xaml

@@ -14,6 +14,8 @@
                 <ResourceDictionary Source="Styles/DockingManagerStyle.xaml" />
                 <ResourceDictionary Source="Styles/DarkScrollBarStyle.xaml" />
                 <ResourceDictionary Source="Styles/ImageCheckBoxStyle.xaml" />
+                <ResourceDictionary Source="Styles/DarkCheckboxStyle.xaml" />
+                <ResourceDictionary Source="Styles/LabelStyles.xaml" />
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
     </Application.Resources>

+ 20 - 0
PixiEditor/Helpers/Converters/EqualityBoolToVisibilityConverter.cs

@@ -0,0 +1,20 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class EqualityBoolToVisibilityConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            return value.Equals(parameter) ? Visibility.Visible : Visibility.Collapsed;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 10 - 4
PixiEditor/Models/Dialogs/NewFileDialog.cs

@@ -1,13 +1,15 @@
-using System.Windows;
+using System;
+using System.Windows;
+using PixiEditor.Models.UserPreferences;
 using PixiEditor.Views;
 
 namespace PixiEditor.Models.Dialogs
 {
     public class NewFileDialog : CustomDialog
     {
-        private int height;
+        private int height = (int)PreferencesSettings.GetPreference("DefaultNewFileHeight", 16L);
 
-        private int width;
+        private int width = (int)PreferencesSettings.GetPreference("DefaultNewFileWidth", 16L);
 
         public int Width
         {
@@ -37,7 +39,11 @@ namespace PixiEditor.Models.Dialogs
 
         public override bool ShowDialog()
         {
-            Window popup = new NewFilePopup();
+            Window popup = new NewFilePopup()
+            {
+                FileWidth = Width,
+                FileHeight = Height
+            };
             popup.ShowDialog();
             Height = (popup as NewFilePopup).FileHeight;
             Width = (popup as NewFilePopup).FileWidth;

+ 90 - 0
PixiEditor/Models/UserPreferences/PreferencesSettings.cs

@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Newtonsoft.Json;
+
+namespace PixiEditor.Models.UserPreferences
+{
+    public static class PreferencesSettings
+    {
+        public static bool IsLoaded { get; private set; } = false;
+
+        public static string PathToUserPreferences { get; private set; } = Path.Join(
+            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+            "PixiEditor",
+            "user_preferences.json");
+
+        public static Dictionary<string, object> Preferences { get; set; } = new Dictionary<string, object>();
+
+        public static void Init()
+        {
+            Init(PathToUserPreferences);
+        }
+
+        public static void Init(string path)
+        {
+            PathToUserPreferences = path;
+            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);
+                }
+
+                IsLoaded = true;
+            }
+        }
+
+        public static void UpdatePreference<T>(string name, T value)
+        {
+            if (IsLoaded == false)
+            {
+                Init();
+            }
+
+            Preferences[name] = value;
+
+            Save();
+        }
+
+        public static void Save()
+        {
+            if (IsLoaded == false)
+            {
+                Init();
+            }
+
+            File.WriteAllText(PathToUserPreferences, JsonConvert.SerializeObject(Preferences));
+        }
+
+#nullable enable
+
+        public static T? GetPreference<T>(string name)
+        {
+            return GetPreference(name, default(T));
+        }
+
+        public static T? GetPreference<T>(string name, T? fallbackValue)
+        {
+            if (IsLoaded == false)
+            {
+                Init();
+            }
+
+            return Preferences.ContainsKey(name)
+                ? (T)Preferences[name]
+                : fallbackValue;
+        }
+    }
+}

+ 1 - 0
PixiEditor/PixiEditor.csproj

@@ -51,6 +51,7 @@
     </PackageReference>
     <PackageReference Include="Extended.Wpf.Toolkit" Version="3.8.2" />
     <PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
+    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="PixiEditor.ColorPicker" Version="1.0.1" />
     <PackageReference Include="System.Drawing.Common" Version="5.0.0" />
     <PackageReference Include="WriteableBitmapEx">

+ 1 - 1
PixiEditor/Properties/Settings.Designer.cs

@@ -12,7 +12,7 @@ namespace PixiEditor.Properties {
     
     
     [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
-    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.7.0.0")]
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")]
     internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
         
         private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));

+ 2 - 5
PixiEditor/Properties/Settings.settings

@@ -1,8 +1,5 @@
 <?xml version='1.0' encoding='utf-8'?>
-
-<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
-  <Profiles>
-    <Profile Name="(Default)" />
-  </Profiles>
+<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
+  <Profiles />
   <Settings />
 </SettingsFile>

+ 39 - 0
PixiEditor/Styles/DarkCheckboxStyle.xaml

@@ -0,0 +1,39 @@
+<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:local="clr-namespace:PixiEditor.Styles">
+    <Style TargetType="CheckBox">
+        <Setter Property="SnapsToDevicePixels" Value="true"/>
+        <Setter Property="OverridesDefaultStyle" Value="true"/>
+        <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
+        <Setter Property="Foreground" Value="White"/>
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate TargetType="CheckBox">
+                    <BulletDecorator Background="Transparent">
+                        <BulletDecorator.Bullet>
+                            <Border x:Name="Border" Width="20" Height="20" CornerRadius="2" Background="#FF1B1B1B" BorderThickness="0">
+                                <Path Width="9" Height="9" x:Name="CheckMark" SnapsToDevicePixels="False" Stroke="#FF0077C9" StrokeThickness="2" Data="M 0 4 L 3 8 8 0" />
+                            </Border>
+                        </BulletDecorator.Bullet>
+                        <ContentPresenter Margin="4,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Left" RecognizesAccessKey="True"/>
+                    </BulletDecorator>
+                    <ControlTemplate.Triggers>
+                        <Trigger Property="IsChecked" Value="false">
+                            <Setter TargetName="CheckMark" Property="Visibility" Value="Collapsed"/>
+                        </Trigger>
+                        <Trigger Property="IsChecked" Value="{x:Null}">
+                            <Setter TargetName="CheckMark" Property="Data" Value="M 0 8 L 8 0" />
+                        </Trigger>
+                        <Trigger Property="IsMouseOver" Value="true">
+                            <Setter TargetName="Border" Property="Background" Value="#FF131313" />
+                        </Trigger>
+                        <Trigger Property="IsEnabled" Value="false">
+                            <Setter TargetName="CheckMark" Property="Stroke" Value="#FF6C6C6C"/>
+                            <Setter Property="Foreground" Value="Gray"/>
+                        </Trigger>
+                    </ControlTemplate.Triggers>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+</ResourceDictionary>

+ 22 - 0
PixiEditor/Styles/LabelStyles.xaml

@@ -0,0 +1,22 @@
+<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:local="clr-namespace:PixiEditor.Styles">
+
+    <Style TargetType="Label" x:Key="BaseLabel">
+        <Setter Property="Foreground" Value="White"/>
+    </Style>
+    
+    <Style x:Key="Header1" TargetType="Label" BasedOn="{StaticResource BaseLabel}">
+        <Setter Property="FontSize" Value="36"/>
+        <Setter Property="Margin" Value="20"/>
+    </Style>
+
+    <Style x:Key="Header2" TargetType="Label" BasedOn="{StaticResource BaseLabel}">
+        <Setter Property="FontSize" Value="20"/>
+        <Setter Property="Margin" Value="20"/>
+    </Style>
+
+    <Style x:Key="Paragraph" TargetType="Label" BasedOn="{StaticResource BaseLabel}">
+        <Setter Property="Margin" Value="0 10 0 10"/>
+    </Style>
+</ResourceDictionary>

+ 31 - 0
PixiEditor/Styles/ThemeStyle.xaml

@@ -69,6 +69,37 @@
         </Setter>
     </Style>
 
+    <Style TargetType="Button" x:Key="AccentDarkRoundButton" BasedOn="{StaticResource BaseDarkButton}">
+        <Setter Property="OverridesDefaultStyle" Value="True" />
+        <Setter Property="Background" Value="{StaticResource AccentColor}" />
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate TargetType="Button">
+                    <Border CornerRadius="4" Background="{TemplateBinding Background}">
+                        <ContentPresenter Content="{TemplateBinding Content}" HorizontalAlignment="Center"
+                                          VerticalAlignment="Center" />
+                    </Border>
+                    <ControlTemplate.Triggers>
+                        <Trigger Property="IsEnabled" Value="False">
+                            <Setter Property="Background" Value="Transparent" />
+                            <Setter Property="Foreground" Value="Gray" />
+                            <Setter Property="Cursor" Value="Arrow" />
+                        </Trigger>
+                        <Trigger Property="IsMouseOver" Value="True">
+                            <Setter Property="Background" Value="#FF515151" />
+                            <Setter Property="Foreground" Value="White" />
+                            <Setter Property="Cursor" Value="Hand" />
+                        </Trigger>
+                        <Trigger Property="IsPressed" Value="True">
+                            <Setter Property="Background" Value="#505050" />
+                            <Setter Property="Foreground" Value="White" />
+                        </Trigger>
+                    </ControlTemplate.Triggers>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+
 
     <Style TargetType="Button" x:Key="ImageButtonStyle">
         <Setter Property="OverridesDefaultStyle" Value="True" />

+ 0 - 64
PixiEditor/ViewModels/FeedbackDialogViewModel.cs

@@ -1,64 +0,0 @@
-using System.Windows;
-using PixiEditor.Helpers;
-
-namespace PixiEditor.ViewModels
-{
-    internal class FeedbackDialogViewModel : ViewModelBase
-    {
-        private string _emailBody;
-
-
-        private string _mailFrom;
-
-        public FeedbackDialogViewModel()
-        {
-            CloseButtonCommand = new RelayCommand(CloseWindow);
-            SendButtonCommand = new RelayCommand(Send, CanSend);
-        }
-
-        public RelayCommand CloseButtonCommand { get; set; }
-        public RelayCommand SendButtonCommand { get; set; }
-
-        public string MailFrom
-        {
-            get => _mailFrom;
-            set
-            {
-                if (_mailFrom != value)
-                {
-                    _mailFrom = value;
-                    RaisePropertyChanged("MailFrom");
-                }
-            }
-        }
-
-        public string EmailBody
-        {
-            get => _emailBody;
-            set
-            {
-                if (_emailBody != value)
-                {
-                    _emailBody = value;
-                    RaisePropertyChanged("EmailBody");
-                }
-            }
-        }
-
-        private void CloseWindow(object parameter)
-        {
-            ((Window) parameter).DialogResult = false;
-            CloseButton(parameter);
-        }
-
-        private void Send(object parameter)
-        {
-            CloseButton(parameter);
-        }
-
-        private bool CanSend(object property)
-        {
-            return !string.IsNullOrWhiteSpace(MailFrom);
-        }
-    }
-}

+ 5 - 3
PixiEditor/ViewModels/NewFileMenuViewModel.cs

@@ -13,18 +13,20 @@ namespace PixiEditor.ViewModels
         }
 
         public RelayCommand OkCommand { get; set; }
+
         public RelayCommand CloseCommand { get; set; }
+
         public RelayCommand DragMoveCommand { get; set; }
 
         private void OkButton(object parameter)
         {
-            ((Window) parameter).DialogResult = true;
-            ((Window) parameter).Close();
+            ((Window)parameter).DialogResult = true;
+            ((Window)parameter).Close();
         }
 
         private void CloseWindow(object parameter)
         {
-            ((Window) parameter).DialogResult = false;
+            ((Window)parameter).DialogResult = false;
             CloseButton(parameter);
         }
 

+ 43 - 0
PixiEditor/ViewModels/SettingsWindowViewModel.cs

@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using PixiEditor.Helpers;
+using PixiEditor.ViewModels.SubViewModels.UserPreferences;
+
+namespace PixiEditor.ViewModels
+{
+    public class SettingsWindowViewModel : ViewModelBase
+    {
+        public RelayCommand SelectCategoryCommand { get; set; }
+
+        private string selectedCategory = "General";
+
+        public string SelectedCategory
+        {
+            get => selectedCategory;
+            set
+            {
+                selectedCategory = value;
+                RaisePropertyChanged(nameof(SelectedCategory));
+            }
+        }
+
+        public SettingsViewModel SettingsSubViewModel { get; set; }
+
+        public SettingsWindowViewModel()
+        {
+            SettingsSubViewModel = new SettingsViewModel(this);
+            SelectCategoryCommand = new RelayCommand(SelectCategory);
+        }
+
+        private void SelectCategory(object parameter)
+        {
+            if (parameter is not null && parameter is string value)
+            {
+                SelectedCategory = value;
+            }
+        }
+    }
+}

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

@@ -10,6 +10,7 @@ using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.IO;
+using PixiEditor.Models.UserPreferences;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
 {
@@ -91,7 +92,10 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             else
             {
-                OpenNewFilePopup(null);
+                if (PreferencesSettings.GetPreference("ShowNewFilePopupOnStartup", true))
+                {
+                    OpenNewFilePopup(null);
+                }
             }
         }
 

+ 47 - 0
PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs

@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using PixiEditor.Helpers;
+using PixiEditor.Views.Dialogs;
+
+namespace PixiEditor.ViewModels.SubViewModels.Main
+{
+    public class MiscViewModel : SubViewModel<ViewModelMain>
+    {
+        public RelayCommand OpenHyperlinkCommand { get; set; }
+
+        public RelayCommand OpenSettingsWindowCommand { get; set; }
+
+        public MiscViewModel(ViewModelMain owner)
+            : base(owner)
+        {
+            OpenHyperlinkCommand = new RelayCommand(OpenHyperlink);
+            OpenSettingsWindowCommand = new RelayCommand(OpenSettingsWindow);
+        }
+
+        private void OpenSettingsWindow(object parameter)
+        {
+            SettingsWindow settings = new SettingsWindow();
+            settings.Show();
+        }
+
+        private void OpenHyperlink(object parameter)
+        {
+            if (parameter == null)
+            {
+                return;
+            }
+
+            var url = (string)parameter;
+            var processInfo = new ProcessStartInfo()
+            {
+                FileName = url,
+                UseShellExecute = true
+            };
+            Process.Start(processInfo);
+        }
+    }
+}

+ 6 - 2
PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
 using System.Windows;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Processes;
+using PixiEditor.Models.UserPreferences;
 using PixiEditor.UpdateModule;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
@@ -74,7 +75,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
                     {
                         await UpdateDownloader.DownloadInstaller(UpdateChecker.LatestReleaseInfo);
                     }
-                    
+
                     UpdateReadyToInstall = true;
                     return true;
                 }
@@ -85,7 +86,10 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         private async void Owner_OnStartupEvent(object sender, EventArgs e)
         {
-            await CheckForUpdate();
+            if (PreferencesSettings.GetPreference("CheckUpdatesOnStartup", true))
+            {
+                await CheckForUpdate();
+            }
         }
 
         private void RestartApplication(object parameter)

+ 72 - 0
PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsViewModel.cs

@@ -0,0 +1,72 @@
+using System;
+using System.Configuration;
+using PixiEditor.Models.UserPreferences;
+
+namespace PixiEditor.ViewModels.SubViewModels.UserPreferences
+{
+    public class SettingsViewModel : SubViewModel<SettingsWindowViewModel>
+    {
+        private bool showNewFilePopupOnStartup = PreferencesSettings.GetPreference("ShowNewFilePopupOnStartup", true);
+
+        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 void RaiseAndUpdatePreference<T>(string name, T value)
+        {
+            RaisePropertyChanged(name);
+            PreferencesSettings.UpdatePreference(name, value);
+        }
+
+        public SettingsViewModel(SettingsWindowViewModel owner)
+            : base(owner)
+        {
+        }
+    }
+}

+ 6 - 19
PixiEditor/ViewModels/ViewModelMain.cs

@@ -14,6 +14,7 @@ using PixiEditor.Models.Events;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
+using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels.SubViewModels.Main;
 
 namespace PixiEditor.ViewModels
@@ -30,8 +31,6 @@ namespace PixiEditor.ViewModels
 
         public RelayCommand CloseWindowCommand { get; set; }
 
-        public RelayCommand OpenHyperlinkCommand { get; set; }
-
         public FileViewModel FileSubViewModel { get; set; }
 
         public UpdateViewModel UpdateSubViewModel { get; set; }
@@ -54,6 +53,8 @@ namespace PixiEditor.ViewModels
 
         public DocumentViewModel DocumentSubViewModel { get; set; }
 
+        public MiscViewModel MiscSubViewModel { get; set; }
+
         public BitmapManager BitmapManager { get; set; }
 
         public PixelChangesController ChangesController { get; set; }
@@ -62,6 +63,8 @@ namespace PixiEditor.ViewModels
 
         public ViewModelMain()
         {
+            PreferencesSettings.Init();
+
             BitmapManager = new BitmapManager();
             BitmapManager.BitmapOperations.BitmapChanged += BitmapUtility_BitmapChanged;
             BitmapManager.MouseController.StoppedRecordingChanges += MouseController_StoppedRecordingChanges;
@@ -72,7 +75,6 @@ namespace PixiEditor.ViewModels
             ChangesController = new PixelChangesController();
             OnStartupCommand = new RelayCommand(OnStartup);
             CloseWindowCommand = new RelayCommand(CloseWindow);
-            OpenHyperlinkCommand = new RelayCommand(OpenHyperlink);
 
             FileSubViewModel = new FileViewModel(this);
             UpdateSubViewModel = new UpdateViewModel(this);
@@ -84,6 +86,7 @@ namespace PixiEditor.ViewModels
             ViewportSubViewModel = new ViewportViewModel(this);
             ColorsSubViewModel = new ColorsViewModel(this);
             DocumentSubViewModel = new DocumentViewModel(this);
+            MiscSubViewModel = new MiscViewModel(this);
 
             ShortcutController = new ShortcutController
             {
@@ -154,22 +157,6 @@ namespace PixiEditor.ViewModels
             return BitmapManager.ActiveDocument != null;
         }
 
-        private void OpenHyperlink(object parameter)
-        {
-            if (parameter == null)
-            {
-                return;
-            }
-
-            var url = (string)parameter;
-            var processInfo = new ProcessStartInfo()
-            {
-                FileName = url,
-                UseShellExecute = true
-            };
-            Process.Start(processInfo);
-        }
-
         private void CloseWindow(object property)
         {
             if (!(property is CancelEventArgs))

+ 3 - 5
PixiEditor/Views/Dialogs/NewFilePopup.xaml.cs

@@ -3,7 +3,7 @@
 namespace PixiEditor.Views
 {
     /// <summary>
-    ///     Interaction logic for NewFilePopup.xaml
+    ///     Interaction logic for NewFilePopup.xaml.
     /// </summary>
     public partial class NewFilePopup : Window
     {
@@ -20,17 +20,15 @@ namespace PixiEditor.Views
             InitializeComponent();
         }
 
-
         public int FileHeight
         {
-            get => (int) GetValue(FileHeightProperty);
+            get => (int)GetValue(FileHeightProperty);
             set => SetValue(FileHeightProperty, value);
         }
 
-
         public int FileWidth
         {
-            get => (int) GetValue(FileWidthProperty);
+            get => (int)GetValue(FileWidthProperty);
             set => SetValue(FileWidthProperty, value);
         }
     }

+ 76 - 0
PixiEditor/Views/Dialogs/SettingsWindow.xaml

@@ -0,0 +1,76 @@
+<Window x:Class="PixiEditor.Views.Dialogs.SettingsWindow"
+        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:local="clr-namespace:PixiEditor.Views.Dialogs" xmlns:viewmodels="clr-namespace:PixiEditor.ViewModels" xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters" xmlns:views="clr-namespace:PixiEditor.Views"
+        mc:Ignorable="d"
+        Title="Settings" Name="window" 
+        Height="450" Width="800" WindowStyle="None" DataContext="{DynamicResource SettingsWindowViewModel}"
+        BorderBrush="Black" BorderThickness="1">
+    <Window.Resources>
+        <viewmodels:SettingsWindowViewModel x:Key="SettingsWindowViewModel"/>
+        <converters:EqualityBoolToVisibilityConverter x:Key="EqualityBoolToVisibilityConverter"/>
+    </Window.Resources>
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>
+
+    <Grid Background="{StaticResource MainColor}">
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition Width="200"/>
+            <ColumnDefinition Width="147*"/>
+        </Grid.ColumnDefinitions>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="35" />
+            <RowDefinition />
+        </Grid.RowDefinitions>
+
+        <DockPanel Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Background="{StaticResource MainColor}">
+            <Label Foreground="White" FontSize="16">Settings</Label>
+            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
+                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
+                    Command="{x:Static SystemCommands.CloseWindowCommand}" />
+        </DockPanel>
+        <StackPanel Grid.Row="1" Grid.Column="0">
+            <Button Style="{StaticResource AccentDarkRoundButton}" Margin="10 5 10 5"
+                    Command="{Binding SelectCategoryCommand}" CommandParameter="General">General</Button>
+            <Button Style="{StaticResource AccentDarkRoundButton}" Margin="10 5 10 5" 
+                    Command="{Binding SelectCategoryCommand}" CommandParameter="Updates">Updates</Button>
+        </StackPanel>
+        <Grid Grid.Row="1" Grid.Column="1" Background="{StaticResource AccentColor}">
+            <Grid Visibility="{Binding SelectedCategory, Converter={StaticResource EqualityBoolToVisibilityConverter},
+            ConverterParameter='General'}">
+                <StackPanel Orientation="Vertical">
+                    <Label Content="File" Style="{StaticResource Header1}"/>
+                    <StackPanel Orientation="Vertical" Margin="50 0 50 0">
+                        <CheckBox Content="Show New File dialog on startup" 
+                                  IsChecked="{Binding SettingsSubViewModel.ShowNewFilePopupOnStartup}"/>
+                        <Label Content="Default new file size:" Style="{StaticResource Header2}" Margin="0 20 0 20"/>
+                        <StackPanel Orientation="Horizontal" Margin="40,0,0,0">
+                            <Label Content="Width:" Style="{StaticResource BaseLabel}"/>
+                            <views:SizeInput Size="{Binding SettingsSubViewModel.DefaultNewFileWidth, Mode=TwoWay}" Width="60" Height="25"/>
+                            <Label Content="Height:" Style="{StaticResource BaseLabel}"/>
+                            <views:SizeInput Size="{Binding SettingsSubViewModel.DefaultNewFileHeight, Mode=TwoWay}" Width="60" Height="25"/>
+                        </StackPanel>
+                    </StackPanel>
+                </StackPanel>
+            </Grid>
+            <Grid Visibility="{Binding SelectedCategory, Converter={StaticResource EqualityBoolToVisibilityConverter},
+            ConverterParameter='Updates'}">
+                <StackPanel Orientation="Vertical">
+                    <Label Style="{StaticResource Header1}" Content="Auto-updates"/>
+                    <StackPanel Orientation="Vertical" Margin="50 0 50 0">
+                        <CheckBox IsChecked="{Binding SettingsSubViewModel.CheckUpdatesOnStartup}" Content="Check updates on startup"/>
+                    </StackPanel>
+                </StackPanel>
+            </Grid>
+        </Grid>
+    </Grid>
+</Window>

+ 38 - 0
PixiEditor/Views/Dialogs/SettingsWindow.xaml.cs

@@ -0,0 +1,38 @@
+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 SettingsWindow.xaml
+    /// </summary>
+    public partial class SettingsWindow : Window
+    {
+        public SettingsWindow()
+        {
+            InitializeComponent();
+        }
+
+        private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+        {
+            e.CanExecute = true;
+        }
+
+        private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+        {
+            SystemCommands.CloseWindow(this);
+        }
+
+    }
+}

+ 7 - 5
PixiEditor/Views/MainWindow.xaml

@@ -96,6 +96,8 @@
                     <Separator />
                     <MenuItem Header="_Delete Selected" Command="{Binding DocumentSubViewModel.DeletePixelsCommand}"
                               InputGestureText="Delete" />
+                    <Separator />
+                    <MenuItem Header="_Settings" Command="{Binding MiscSubViewModel.OpenSettingsWindowCommand}" />
                 </MenuItem>
                 <MenuItem Header="_Select">
                     <MenuItem Header="_Select All" Command="{Binding SelectionSubViewModel.SelectAllCommand}" InputGestureText="Ctrl+A" />
@@ -111,16 +113,16 @@
                     <MenuItem Header="Center Content" Command="{Binding DocumentSubViewModel.CenterContentCommand}" />
                 </MenuItem>
                 <MenuItem Header="_Help">
-                    <MenuItem Header="Documentation" Command="{Binding OpenHyperlinkCommand}"
+                    <MenuItem Header="Documentation" Command="{Binding MiscSubViewModel.OpenHyperlinkCommand}"
                               CommandParameter="https://github.com/PixiEditor/PixiEditor/wiki"/>
-                    <MenuItem Header="Repository" Command="{Binding OpenHyperlinkCommand}"
+                    <MenuItem Header="Repository" Command="{Binding MiscSubViewModel.OpenHyperlinkCommand}"
                               CommandParameter="https://github.com/PixiEditor/PixiEditor"/>
-                    <MenuItem Header="Shortcuts" Command="{Binding OpenHyperlinkCommand}"
+                    <MenuItem Header="Shortcuts" Command="{Binding MiscSubViewModel.OpenHyperlinkCommand}"
                               CommandParameter="https://github.com/PixiEditor/PixiEditor/wiki/Shortcuts"/>
                     <Separator/>
-                    <MenuItem Header="License" Command="{Binding OpenHyperlinkCommand}"
+                    <MenuItem Header="License" Command="{Binding MiscSubViewModel.OpenHyperlinkCommand}"
                               CommandParameter="https://github.com/PixiEditor/PixiEditor/blob/master/LICENSE"/>
-                    <MenuItem Header="Third Party Licenses" Command="{Binding OpenHyperlinkCommand}"
+                    <MenuItem Header="Third Party Licenses" Command="{Binding MiscSubViewModel.OpenHyperlinkCommand}"
                               CommandParameter="https://github.com/PixiEditor/PixiEditor/wiki/Third-party-licenses"/>
                 </MenuItem>
             </Menu>

+ 4 - 7
PixiEditor/Views/UserControls/SizePicker.xaml.cs

@@ -4,7 +4,7 @@ using System.Windows.Controls;
 namespace PixiEditor.Views
 {
     /// <summary>
-    ///     Interaction logic for SizePicker.xaml
+    ///     Interaction logic for SizePicker.xaml.
     /// </summary>
     public partial class SizePicker : UserControl
     {
@@ -25,24 +25,21 @@ namespace PixiEditor.Views
             InitializeComponent();
         }
 
-
         public bool EditingEnabled
         {
-            get => (bool) GetValue(EditingEnabledProperty);
+            get => (bool)GetValue(EditingEnabledProperty);
             set => SetValue(EditingEnabledProperty, value);
         }
 
-
         public int ChosenWidth
         {
-            get => (int) GetValue(ChosenWidthProperty);
+            get => (int)GetValue(ChosenWidthProperty);
             set => SetValue(ChosenWidthProperty, value);
         }
 
-
         public int ChosenHeight
         {
-            get => (int) GetValue(ChosenHeightProperty);
+            get => (int)GetValue(ChosenHeightProperty);
             set => SetValue(ChosenHeightProperty, value);
         }
     }

+ 67 - 0
PixiEditorTests/ModelsTests/UserPreferencesTests/PreferencesSettingsTests.cs

@@ -0,0 +1,67 @@
+using System.Collections.Generic;
+using System.IO;
+using Newtonsoft.Json;
+using PixiEditor.Models.UserPreferences;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.UserPreferencesTests
+{
+    public class PreferencesSettingsTests
+    {
+        public static string PathToPreferencesFile { get; } = Path.Join("PixiEditor", "test_preferences.json");
+
+        public PreferencesSettingsTests()
+        {
+            PreferencesSettings.Init(PathToPreferencesFile);
+        }
+
+        [Fact]
+        public void TestThatPreferencesSettingsIsLoaded()
+        {
+            Assert.True(PreferencesSettings.IsLoaded);
+        }
+
+        [Fact]
+        public void TestThatInitCreatesUserPreferencesJson()
+        {
+            Assert.True(File.Exists(PathToPreferencesFile));
+        }
+
+        [Theory]
+        [InlineData(-2)]
+        [InlineData(false)]
+        [InlineData("string")]
+        [InlineData(null)]
+        public void TestThatGetPreferenceOnNonExistingKeyReturnsFallbackValue<T>(T value)
+        {
+            T fallbackValue = value;
+            T preferenceValue = PreferencesSettings.GetPreference<T>("NonExistingPreference", fallbackValue);
+            Assert.Equal(fallbackValue, preferenceValue);
+        }
+
+        [Theory]
+        [InlineData("IntPreference", 1)]
+        [InlineData("BoolPreference", true)]
+        public void TestThatUpdatePreferenceUpdatesDictionary<T>(string name, T value)
+        {
+            PreferencesSettings.UpdatePreference(name, value);
+            Assert.Equal(value, PreferencesSettings.GetPreference<T>(name));
+        }
+
+        [Theory]
+        [InlineData("LongPreference", 1L)]
+        public void TestThatSaveUpdatesFile<T>(string name, T value)
+        {
+            PreferencesSettings.Preferences[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]);
+            }
+        }
+    }
+}

+ 3 - 3
PixiEditorTests/PixiEditorTests.csproj

@@ -19,12 +19,12 @@
 
   <ItemGroup>
     <PackageReference Include="Codecov" Version="1.12.3" />
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.0">
+    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.1">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
-    <PackageReference Include="Moq" Version="4.15.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
+    <PackageReference Include="Moq" Version="4.15.2" />
     <PackageReference Include="OpenCover" Version="4.7.922" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">