diff --git a/src/tool/App.config b/src/tool/App.config
new file mode 100644
index 00000000..e6206dc0
--- /dev/null
+++ b/src/tool/App.config
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+ False
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/tool/Model/Configuration.cs b/src/tool/Model/Configuration.cs
index b8b74d2a..794addb4 100644
--- a/src/tool/Model/Configuration.cs
+++ b/src/tool/Model/Configuration.cs
@@ -726,6 +726,17 @@ public byte[] WriteToBuffer()
}
}
+ ///
+ /// Creates a new Configuration, copies the contents to it, and returns the new config object
+ ///
+ /// A completely new config object with the same values as this configuration
+ public Configuration Clone()
+ {
+ Configuration newConfig = new Configuration();
+ newConfig.CopyFrom(this);
+ return newConfig;
+ }
+
public void CopyFrom(Configuration cfg)
{
Target = cfg.Target;
@@ -796,9 +807,14 @@ public void WriteToFile(string filepath)
{
var serializer = new XmlSerializer(typeof(Configuration));
var settings = new XmlWriterSettings { Encoding = Encoding.UTF8, Indent = true };
- using (var xmlWriter = XmlWriter.Create(new StreamWriter(filepath), settings))
+ // This line used to create a new xmlWriter directly, which seems like it should work,
+ // but it doesn't. (saving the same file repeatedly very fast causes exceptions)
+ // So we create a streamWriter and pass it to the xmlWriter so we can close it explicitly
+ using (var streamWriter = new StreamWriter(filepath))
{
+ var xmlWriter = XmlWriter.Create(streamWriter, settings);
serializer.Serialize(xmlWriter, this);
+ xmlWriter.Close();
}
}
diff --git a/src/tool/Properties/Settings.Designer.cs b/src/tool/Properties/Settings.Designer.cs
index 5c5cc2be..7eb3ab3a 100644
--- a/src/tool/Properties/Settings.Designer.cs
+++ b/src/tool/Properties/Settings.Designer.cs
@@ -12,7 +12,7 @@ namespace BBSFW.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", "17.13.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
@@ -34,5 +34,17 @@ public bool UseFreedomUnits {
this["UseFreedomUnits"] = value;
}
}
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("")]
+ public string LastLoadedFile {
+ get {
+ return ((string)(this["LastLoadedFile"]));
+ }
+ set {
+ this["LastLoadedFile"] = value;
+ }
+ }
}
}
diff --git a/src/tool/Properties/Settings.settings b/src/tool/Properties/Settings.settings
index 6e0000b8..81e46b22 100644
--- a/src/tool/Properties/Settings.settings
+++ b/src/tool/Properties/Settings.settings
@@ -1,9 +1,12 @@
-
+
False
+
+
+
\ No newline at end of file
diff --git a/src/tool/View/MainWindow.xaml b/src/tool/View/MainWindow.xaml
index 8d9d0e5a..5c66de0d 100644
--- a/src/tool/View/MainWindow.xaml
+++ b/src/tool/View/MainWindow.xaml
@@ -7,8 +7,9 @@
xmlns:vm="clr-namespace:BBSFW.ViewModel"
xmlns:vw="clr-namespace:BBSFW.View"
mc:Ignorable="d"
- Title="BBS-FW Tool" Height="640" Width="860"
- Background="#FFE8E8E8">
+ Title="{Binding ApplicationTitle}" Height="640" Width="860"
+ Background="#FFE8E8E8"
+ Closing="Window_Closing">
@@ -20,30 +21,32 @@
-
+
@@ -64,6 +67,19 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tool/View/MainWindow.xaml.cs b/src/tool/View/MainWindow.xaml.cs
index 0c1c0831..68ce3bc5 100644
--- a/src/tool/View/MainWindow.xaml.cs
+++ b/src/tool/View/MainWindow.xaml.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -24,5 +25,19 @@ public MainWindow()
{
InitializeComponent();
}
+
+ ///
+ /// Application closing callback.
+ ///
+ ///
+ ///
+ private void Window_Closing(object sender, CancelEventArgs e)
+ {
+ // Get the view model
+ if (DataContext is ViewModel.MainViewModel viewModel)
+ {
+ e.Cancel = viewModel.OnExitCancellable();
+ }
+ }
}
}
diff --git a/src/tool/ViewModel/MainViewModel.cs b/src/tool/ViewModel/MainViewModel.cs
index e872a698..4734f977 100644
--- a/src/tool/ViewModel/MainViewModel.cs
+++ b/src/tool/ViewModel/MainViewModel.cs
@@ -2,6 +2,8 @@
using BBSFW.ViewModel.Base;
using Microsoft.Win32;
using System;
+using System.Collections.Generic;
+using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
@@ -11,6 +13,102 @@ namespace BBSFW.ViewModel
public class MainViewModel : ObservableObject
{
+ private const string APP_TITLE = "BBS-FW Tool";
+ // Stores the name of the open config file. It's explicitly not a file handle, just a filename.
+ private string _configFileName;
+
+ // Has the configuration been modified by a reload or UI changes?
+ private bool _configModified;
+ public bool ConfigModified
+ {
+ get { return _configModified; }
+ set
+ {
+ if (value != _configModified)
+ {
+ _configModified = value;
+ OnPropertyChanged(nameof(ConfigModified));
+ }
+ }
+ }
+
+ // Stores a set of the changed UI properties so we can mark the config unmodified if it's reverted
+ private HashSet ChangedProperties = new HashSet();
+
+ // Store a set of properties to ignore because they're triggered by a related metric update
+ private HashSet ImperialProperties = new HashSet {
+ "PretensionSpeedCutoffMph", "MaxSpeedMph" };
+
+ public string ConfigFileName
+ {
+ get { return _configFileName; }
+ set
+ {
+ if (_configFileName != value)
+ {
+ _configFileName = value;
+ OnPropertyChanged(nameof(ConfigFileName));
+ OnPropertyChanged(nameof(ConfigFilenameExists));
+ }
+ }
+ }
+
+ ///
+ /// Not sure if there's a better way of doing this, But it's used to hide certain menus when there is no file loaded.
+ /// This check is trivial but allows us to do IsEnabled="{Binding ConfigFilenameExists}" in MainViewModel.xaml to show/hide menus.
+ ///
+ public bool ConfigFilenameExists
+ {
+ get
+ {
+ return !String.IsNullOrEmpty(ConfigFileName);
+ }
+ }
+
+ private string _applicationTitle = "BBS - FW Tool";
+
+ public string ApplicationTitle {
+ get { return _applicationTitle; }
+ set {
+ if (_applicationTitle != value)
+ {
+ _applicationTitle = value;
+ OnPropertyChanged(nameof(ApplicationTitle));
+ }
+ }
+ }
+
+ private string GetAppTitle()
+ {
+ string modified = ConfigModified ? " (modified)" : String.Empty;
+ if (ConfigFilenameExists)
+ {
+ string fileName;
+ try
+ {
+ // this would likely only fail if the config file was deleted or moved.
+ fileName = System.IO.Path.GetFileName(ConfigFileName);
+ } catch
+ {
+ // and if that's the case it's still valid to use that filename, we'll just make it explicit what the path is also.
+ fileName = ConfigFileName;
+ }
+
+ return $"{APP_TITLE} - {fileName}{modified}";
+ }
+ else
+ {
+ return $"{APP_TITLE}{modified}";
+ }
+ }
+
+ ///
+ /// Original state of the configuration, so we can check for modifications
+ ///
+ private Configuration OriginalConfigurationState
+ {
+ get; set;
+ }
public ConfigurationViewModel ConfigVm { get; private set; }
@@ -31,6 +129,24 @@ public ICommand OpenConfigCommand
get { return new DelegateCommand(OnOpenConfig); }
}
+ public ICommand OpenConfigDirectCommand
+ {
+ get { return new DelegateCommand(OnOpenConfigDirect); }
+ }
+
+ /**
+ * This doesnt actually close the config, it just clears the saved file name so you can't accidentally overwrite
+ */
+ public ICommand CloseConfigCommand
+ {
+ get { return new DelegateCommand(OnCloseConfig); }
+ }
+
+ public ICommand SaveAsConfigCommand
+ {
+ get { return new DelegateCommand(OnSaveAsConfig); }
+ }
+
public ICommand SaveConfigCommand
{
get { return new DelegateCommand(OnSaveConfig); }
@@ -66,22 +182,85 @@ public ICommand ShowAboutCommand
get { return new DelegateCommand(OnShowAbout); }
}
+ public ICommand UseMetricUnitsCommand
+ {
+ get { return new DelegateCommand(OnUseMetric); }
+ }
+ public ICommand UseImperialUnitsCommand
+ {
+ get { return new DelegateCommand(OnUseImperial); }
+ }
public MainViewModel()
{
ConfigVm = new ConfigurationViewModel();
+ this.PropertyChanged += (sender, args) =>
+ {
+ // Updates the title when the config is modified (to mark it so), and the filename when it's set.
+ if (args.PropertyName == nameof(ConfigModified) ||
+ args.PropertyName == nameof(ConfigFileName))
+ {
+ ApplicationTitle = GetAppTitle();
+ }
+ };
+
ConnectionVm = new ConnectionViewModel();
SystemVm = new SystemViewModel(ConfigVm);
AssistLevelsVm = new AssistLevelsViewModel(ConfigVm);
CalibrationVm = new CalibrationViewModel(ConnectionVm);
EventLogVm = new EventLogViewModel();
-
-
+ if (!String.IsNullOrEmpty(Properties.Settings.Default.LastLoadedFile))
+ {
+ ConfigFileName = Properties.Settings.Default.LastLoadedFile;
+ OpenConfigDirectCommand.Execute(ConfigFileName);
+ }
+ // start monitoring after load so we don't trigger all the events
+ StartConfigMonitoring();
ConnectionVm.EventLogReceived += EventLogVm.AddEvent;
}
+ ///
+ /// Updates the OriginalConfigurationState to match the current config, for when loading/reseting etc.
+ ///
+ private void ResetOriginalConfigurationState()
+ {
+ OriginalConfigurationState = ConfigVm.GetConfig().Clone();
+ }
+
+ ///
+ /// Start monitoring the ConfigurationViewModel for changes so we can mark it as modified when the user changes something in the UI.
+ ///
+ private void ConfigMonitoring(object? sender, PropertyChangedEventArgs args)
+ {
+ var propertyName = args.PropertyName;
+ if (propertyName != null && sender != null && !ImperialProperties.Contains(propertyName))
+ {
+ var property = sender.GetType().GetProperty(propertyName);
+ if (property != null)
+ {
+ var value = property.GetValue(sender) ?? "NULL";
+ string originalValue = OriginalConfigurationState.GetType().GetField(propertyName)?.GetValue(OriginalConfigurationState).ToString() ?? "NULL";
+ string newValue = value?.ToString();
+
+ if (newValue != originalValue)
+ {
+ ConfigModified = true;
+ ChangedProperties.Add(propertyName);
+ }
+ else
+ {
+ ChangedProperties.Remove(propertyName);
+ // no values left means we have the original config again
+ if (ChangedProperties.Count == 0)
+ {
+ ConfigModified = false;
+ }
+ }
+ }
+ }
+ }
private void OnSaveLog()
{
@@ -98,13 +277,31 @@ private void OnSaveLog()
{
EventLogVm.ExportLog(dialog.FileName);
}
- catch(Exception e)
+ catch (Exception e)
{
MessageBox.Show(e.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
+ private void OnCloseConfig()
+ {
+ ConfigFileName = null;
+ ConfigModified = false;
+ }
+
+ private void StopConfigMonitoring()
+ {
+ ChangedProperties.Clear();
+ ConfigVm.PropertyChanged -= ConfigMonitoring;
+ }
+ private void StartConfigMonitoring()
+ {
+ ChangedProperties.Clear();
+ ResetOriginalConfigurationState();
+ ConfigVm.PropertyChanged += ConfigMonitoring;
+ }
+
private void OnOpenConfig()
{
var dialog = new OpenFileDialog();
@@ -116,12 +313,34 @@ private void OnOpenConfig()
{
try
{
+ StopConfigMonitoring();
ConfigVm.ReadConfiguration(dialog.FileName);
+ ConfigFileName = dialog.FileName;
+ ConfigModified = false;
}
catch (Exception e)
{
MessageBox.Show(e.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
+ finally
+ {
+ StartConfigMonitoring();
+ }
+ }
+ }
+ private void OnOpenConfigDirect()
+ {
+ try
+ {
+ StopConfigMonitoring();
+ ConfigVm.ReadConfiguration(_configFileName);
+ ConfigFileName = _configFileName;
+ ConfigModified = false;
+ StartConfigMonitoring();
+ }
+ catch (Exception e)
+ {
+ MessageBox.Show(e.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
@@ -132,11 +351,44 @@ private void OnSaveConfig()
return;
}
+ if (!ConfigFilenameExists)
+ {
+ OnSaveAsConfig();
+ return;
+ }
+
+ try
+ {
+ ConfigVm.WriteConfiguration(ConfigFileName);
+ ConfigModified = false;
+ }
+ catch (Exception e)
+ {
+ MessageBox.Show(e.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void OnSaveAsConfig()
+ {
+ if (!ValidateConfig())
+ {
+ return;
+ }
+
var dialog = new SaveFileDialog();
dialog.Filter = "XML File|*.xml";
dialog.Title = "Save Configuration";
- dialog.FileName = "bbsfw.xml";
+ if (String.IsNullOrEmpty(ConfigFileName))
+ {
+ dialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
+ dialog.FileName = "bbsfw.xml";
+ }
+ else
+ {
+ dialog.InitialDirectory = System.IO.Path.GetDirectoryName(ConfigFileName);
+ dialog.FileName = System.IO.Path.GetFileName(ConfigFileName);
+ }
var result = dialog.ShowDialog();
if (result.HasValue && result.Value)
@@ -144,6 +396,9 @@ private void OnSaveConfig()
try
{
ConfigVm.WriteConfiguration(dialog.FileName);
+ // Updating the config file name will also update the application title.
+ ConfigFileName = dialog.FileName;
+ ConfigModified = false;
}
catch (Exception e)
{
@@ -167,12 +422,15 @@ private async void OnReadFlash()
var res = await ConnectionVm.GetConnection().ReadConfiguration(TimeSpan.FromSeconds(5));
if (!res.Timeout && res.Result != null)
{
+ StopConfigMonitoring();
ConfigVm.UpdateFrom(res.Result);
+ StartConfigMonitoring();
}
else
{
MessageBox.Show("Failed to read configuration from flash, timeout occured.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
+ ConfigModified = true;
}
private async void OnWriteFlash()
@@ -235,6 +493,15 @@ private async void OnResetFlash()
}
}
+ private void OnUseMetric()
+ {
+ ConfigVm.UseMetricUnits = true;
+ }
+
+ private void OnUseImperial()
+ {
+ ConfigVm.UseImperialUnits = true;
+ }
private void OnShowAbout()
{
@@ -242,9 +509,42 @@ private void OnShowAbout()
MessageBox.Show($"Version: {version.Major}.{version.Minor}.{version.Build}\nAuthor: Daniel Nilsson", "BBS-FW Tool", MessageBoxButton.OK, MessageBoxImage.Information);
}
+ ///
+ /// Displays the save/save as dialog as necessary. This should probably be in a service class?
+ ///
+ /// True if this exit was cancelled
+ public bool OnExitCancellable()
+ {
+ if (ConfigModified)
+ {
+ var result = MessageBox.Show($"You have unsaved changes, would you like to save them?",
+ "BBS-FW Tool", MessageBoxButton.YesNoCancel, MessageBoxImage.Information);
+
+ if (result == MessageBoxResult.Yes)
+ {
+ if (ConfigFilenameExists)
+ {
+ OnSaveConfig();
+ }
+ else
+ {
+ OnSaveAsConfig();
+ }
+ }
+ return (result == MessageBoxResult.Cancel);
+ }
+ else
+ {
+ Properties.Settings.Default.LastLoadedFile = ConfigFileName ?? "";
+ Properties.Settings.Default.Save();
+ return false;
+ }
+ }
+
private void OnExit()
{
- Application.Current.Shutdown();
+
+ Application.Current.MainWindow.Close();
}
private bool VerifyConfigVersionForRead()