From 3586af6224c4525c4c776d2e4172cefa7ad638a0 Mon Sep 17 00:00:00 2001 From: Brandon Chong Date: Mon, 8 Jul 2019 22:31:01 -0700 Subject: [PATCH 01/10] Arrange projects into folders in the solution --- src/AudioBand.sln | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/AudioBand.sln b/src/AudioBand.sln index 34643bd1..452fd330 100644 --- a/src/AudioBand.sln +++ b/src/AudioBand.sln @@ -21,6 +21,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AudioBand.Logging", "AudioB EndProject Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "AudioBandInstaller", "AudioBandInstaller\AudioBandInstaller.wixproj", "{4E9EEE47-23F6-45B7-9DDB-A34731D6AE4A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AudioBand Core", "AudioBand Core", "{AD1A45A7-F5E2-4657-BA09-99F8A5421FF0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Audio Sources", "Audio Sources", "{31BA7036-81E7-4D02-90CA-753832D950C5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E79F26F3-C51F-4375-BF1C-4090FAB8D16D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -135,6 +141,16 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B69832AD-8373-47AC-A52A-183238903896} = {AD1A45A7-F5E2-4657-BA09-99F8A5421FF0} + {30F2BFEA-788A-494D-88E7-F2070528EBEA} = {AD1A45A7-F5E2-4657-BA09-99F8A5421FF0} + {43B57D81-7FAE-40D0-921E-E29F7E848288} = {31BA7036-81E7-4D02-90CA-753832D950C5} + {E5E9CB59-C8CF-4042-9C13-F216153334B6} = {E79F26F3-C51F-4375-BF1C-4090FAB8D16D} + {6D881B7B-3F3F-4613-A83F-75E31EEA5252} = {31BA7036-81E7-4D02-90CA-753832D950C5} + {741DB79C-921D-4D91-85F1-CD10C746F46E} = {31BA7036-81E7-4D02-90CA-753832D950C5} + {D3F92C3E-E546-4A6B-ADA2-FACD95E229F7} = {AD1A45A7-F5E2-4657-BA09-99F8A5421FF0} + {D8E1D3E5-D0AB-43C4-8AF6-60C14C5C6843} = {AD1A45A7-F5E2-4657-BA09-99F8A5421FF0} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7C4FC539-D568-422E-8445-A0313878CB1F} EndGlobalSection From d8757ed2d079ce45383bf2bd5647526ee3234827 Mon Sep 17 00:00:00 2001 From: Brandon Chong Date: Thu, 11 Jul 2019 12:00:57 -0700 Subject: [PATCH 02/10] Update value converters with tests and better documentation Add tests for colortobrushconverter Add multiplier converter tests Add enumtoboolconverter tests Add ObjectToTypeConverter tests Add point converter tests Update converter tests folder name Add TimeSpanConverter tests --- src/AudioBand.Test/AudioBand.Test.csproj | 11 +- .../AudioBand.Test.csproj.DotSettings | 1 + .../BoolToVisibilityConverterTests.cs | 101 ++++++++++++++++ .../ColorToBrushConverterTests.cs | 45 +++++++ .../FlagToBoolConverterTests.cs | 56 +++++++++ .../ValueConverters/MultiplyConverterTests.cs | 46 +++++++ .../ObjectToTypeConverterTests.cs | 33 +++++ .../PathToImageConverterTests.cs | 114 +++++++++--------- .../ValueConverters/PointConverterTests.cs | 31 +++++ .../StringToFontFamilyConverterTests.cs | 51 ++++++++ .../TimeSpanToMsConverterTests.cs | 54 +++++++++ src/AudioBand/AudioBand.csproj | 1 + .../BoolToVisibilityConverter.cs | 42 ++++++- .../ValueConverters/ColorToBrushConverter.cs | 31 ++++- .../ValueConverters/ColorToNameConverter.cs | 13 +- .../ValueConverters/ComparisonConverter.cs | 2 +- .../DoubleToCornerRadiusConverter.cs | 11 +- .../EmptyStringToBoolConverter.cs | 3 +- .../ValueConverters/FlagToBoolConverter.cs | 15 ++- .../ValueConverters/MultiplierConverter.cs | 15 ++- .../ValueConverters/ObjectToTypeConverter.cs | 12 +- .../PathToImageSourceConverter.cs | 4 +- .../ValueConverters/PointConverter.cs | 6 +- .../ValueConverters/StringFormatConverter.cs | 3 +- .../StringToFontFamilyConverter.cs | 12 +- .../StringToVisibilityConverter.cs | 4 +- .../ValueConverters/TextAlignmentConverter.cs | 7 +- .../ValueConverters/TimeSpanToMsConverter.cs | 7 +- .../ValueConverters/ValueConverterHelper.cs | 23 ++++ 29 files changed, 654 insertions(+), 100 deletions(-) create mode 100644 src/AudioBand.Test/ValueConverters/BoolToVisibilityConverterTests.cs create mode 100644 src/AudioBand.Test/ValueConverters/ColorToBrushConverterTests.cs create mode 100644 src/AudioBand.Test/ValueConverters/FlagToBoolConverterTests.cs create mode 100644 src/AudioBand.Test/ValueConverters/MultiplyConverterTests.cs create mode 100644 src/AudioBand.Test/ValueConverters/ObjectToTypeConverterTests.cs rename src/AudioBand.Test/{ValueConveters => ValueConverters}/PathToImageConverterTests.cs (96%) create mode 100644 src/AudioBand.Test/ValueConverters/PointConverterTests.cs create mode 100644 src/AudioBand.Test/ValueConverters/StringToFontFamilyConverterTests.cs create mode 100644 src/AudioBand.Test/ValueConverters/TimeSpanToMsConverterTests.cs create mode 100644 src/AudioBand/ValueConverters/ValueConverterHelper.cs diff --git a/src/AudioBand.Test/AudioBand.Test.csproj b/src/AudioBand.Test/AudioBand.Test.csproj index b4dca433..2af20833 100644 --- a/src/AudioBand.Test/AudioBand.Test.csproj +++ b/src/AudioBand.Test/AudioBand.Test.csproj @@ -91,9 +91,18 @@ C:\Users\bdonc\.nuget\packages\unosquare.swan.lite\0.38.1\lib\net452\Unosquare.Swan.Lite.dll + - + + + + + + + + + diff --git a/src/AudioBand.Test/AudioBand.Test.csproj.DotSettings b/src/AudioBand.Test/AudioBand.Test.csproj.DotSettings index bf95c7ff..868fb131 100644 --- a/src/AudioBand.Test/AudioBand.Test.csproj.DotSettings +++ b/src/AudioBand.Test/AudioBand.Test.csproj.DotSettings @@ -1,5 +1,6 @@  True True + True True True \ No newline at end of file diff --git a/src/AudioBand.Test/ValueConverters/BoolToVisibilityConverterTests.cs b/src/AudioBand.Test/ValueConverters/BoolToVisibilityConverterTests.cs new file mode 100644 index 00000000..e2cce975 --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/BoolToVisibilityConverterTests.cs @@ -0,0 +1,101 @@ +using System.Globalization; +using System.Windows; +using AudioBand.ValueConverters; +using Xunit; +using Visibility = System.Windows.Visibility; + +namespace AudioBand.Test +{ + public class BoolToVisibilityConverterTests + { + private readonly BoolToVisibilityConverter _converter = new BoolToVisibilityConverter(); + + [Fact] + public void ConvertTrue_ReturnsVisible() + { + var result = ConvertToVisibility(true, null); + Assert.Equal(Visibility.Visible, result); + } + + [Fact] + public void ConvertFalse_ParameterNull_ReturnsCollapsed() + { + var result = ConvertToVisibility(false, null); + Assert.Equal(Visibility.Collapsed, result); + } + + [Fact] + public void ConvertFalse_ParameterTrue_ReturnsCollapsed() + { + var result = ConvertToVisibility(false, true); + Assert.Equal(Visibility.Collapsed, result); + } + + [Fact] + public void ConvertFalse_ParameterFalse_ReturnsHidden() + { + var result = ConvertToVisibility(false, false); + Assert.Equal(Visibility.Hidden, result); + } + + [Fact] + public void ConvertNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.Convert(null, typeof(Visibility), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertNonBool_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.Convert(null, typeof(Visibility), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertBackVisible_ReturnsTrue() + { + Assert.True(ConvertToBool(Visibility.Visible)); + } + + [Fact] + public void ConvertBackCollapsed_ReturnsFalse() + { + Assert.False(ConvertToBool(Visibility.Collapsed)); + } + + [Fact] + public void ConvertBackHidden_ReturnsFalse() + { + Assert.False(ConvertToBool(Visibility.Hidden)); + } + + [Fact] + public void ConvertBackNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.ConvertBack(null, typeof(bool), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertBackNonVisibility_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.ConvertBack("invalid", typeof(bool), null, CultureInfo.CurrentCulture)); + } + + private Visibility ConvertToVisibility(bool value, object parameter) + { + var result = _converter.Convert(value, typeof(Visibility), parameter, CultureInfo.CurrentCulture); + + Assert.IsType(result); + + return (Visibility)result; + } + + private bool ConvertToBool(Visibility visibility) + { + var result = _converter.ConvertBack(visibility, typeof(bool), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + + return (bool)result; + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/ColorToBrushConverterTests.cs b/src/AudioBand.Test/ValueConverters/ColorToBrushConverterTests.cs new file mode 100644 index 00000000..588f598d --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/ColorToBrushConverterTests.cs @@ -0,0 +1,45 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Media; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class ColorToBrushConverterTests + { + private readonly ColorToBrushConverter _converter = new ColorToBrushConverter(); + + [Fact] + public void ConvertColor_ReturnsSolidColorBrushWithSameColor() + { + var color = Colors.Red; + var result = _converter.Convert(color, typeof(SolidColorBrush), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(color, ((SolidColorBrush)result).Color); + } + + [Fact] + public void ConvertNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.Convert(null, typeof(SolidColorBrush), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertBackSolidColorBrush_ReturnsColorWithSameValue() + { + var brush = Brushes.Blue; + var result = _converter.ConvertBack(brush, typeof(Color), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(brush.Color, (Color)result); + } + + [Fact] + public void ConvertBackNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.ConvertBack(null, typeof(Color), null, CultureInfo.CurrentCulture)); + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/FlagToBoolConverterTests.cs b/src/AudioBand.Test/ValueConverters/FlagToBoolConverterTests.cs new file mode 100644 index 00000000..2eb7412a --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/FlagToBoolConverterTests.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class FlagToBoolConverterTests + { + private readonly FlagToBoolConverter _converter = new FlagToBoolConverter(); + + [Flags] + private enum TestEnum + { + None = 0, + Flag1 = 1, + Flag2 = 2, + } + + [Fact] + public void ConvertNoFlag_ReturnsFalse() + { + var result = _converter.Convert(TestEnum.None, typeof(bool), TestEnum.Flag2, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.False((bool)result); + } + + [Fact] + public void ConvertHasFlag_ReturnsTrue() + { + var value = TestEnum.Flag1 | TestEnum.Flag2; + var result = _converter.Convert(value, typeof(bool), TestEnum.Flag2, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.True((bool)result); + } + + [Fact] + public void ConvertNull_ReturnsFalse() + { + Assert.False((bool)_converter.Convert(null, typeof(bool), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertBack_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.ConvertBack(null, typeof(Enum), null, CultureInfo.CurrentCulture)); + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/MultiplyConverterTests.cs b/src/AudioBand.Test/ValueConverters/MultiplyConverterTests.cs new file mode 100644 index 00000000..1b52614f --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/MultiplyConverterTests.cs @@ -0,0 +1,46 @@ +using System.Globalization; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class MultiplyConverterTests + { + private readonly MultiplierConverter _converter = new MultiplierConverter(); + + [Fact] + public void ConvertSingleValue_ReturnsValue() + { + var result = _converter.Convert(new object[] {5.0}, typeof(double), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(5.0, (double)result, 4); + } + + [Fact] + public void ConvertMultipleValues_ReturnsMultipliedValue() + { + var values = new object[] {1.0, 2, 3}; + var result = _converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(6.0, (double)result, 4); + } + + [Fact] + public void ConvertNone_Returns1() + { + var result = _converter.Convert(null, typeof(double), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(1.0, (double)result); + } + + [Fact] + public void ConvertBack_NotSupported_ReturnsNull() + { + var result = _converter.ConvertBack(new object[]{"a", 1}, new[] { typeof(string), typeof(int)}, null, CultureInfo.CurrentCulture); + Assert.Null(result); + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/ObjectToTypeConverterTests.cs b/src/AudioBand.Test/ValueConverters/ObjectToTypeConverterTests.cs new file mode 100644 index 00000000..2e81ac3e --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/ObjectToTypeConverterTests.cs @@ -0,0 +1,33 @@ +using System; +using System.Globalization; +using System.Windows; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class ObjectToTypeConverterTests + { + private readonly ObjectToTypeConverter _converter = new ObjectToTypeConverter(); + + [Fact] + public void ConvertNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.Convert(null, typeof(Type), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertObject_ReturnsTypeOfObject() + { + var result = _converter.Convert("string", typeof(Type), null, CultureInfo.CurrentCulture); + + Assert.Equal(typeof(string), result); + } + + [Fact] + public void ConvertBack_NotSupported_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.ConvertBack(null, typeof(object), null, CultureInfo.CurrentCulture)); + } + } +} diff --git a/src/AudioBand.Test/ValueConveters/PathToImageConverterTests.cs b/src/AudioBand.Test/ValueConverters/PathToImageConverterTests.cs similarity index 96% rename from src/AudioBand.Test/ValueConveters/PathToImageConverterTests.cs rename to src/AudioBand.Test/ValueConverters/PathToImageConverterTests.cs index 88a43bbf..0bf133a2 100644 --- a/src/AudioBand.Test/ValueConveters/PathToImageConverterTests.cs +++ b/src/AudioBand.Test/ValueConverters/PathToImageConverterTests.cs @@ -1,57 +1,57 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Media; -using AudioBand.ValueConverters; -using Xunit; - -namespace AudioBand.Test -{ - public class PathToImageConverterTests - { - private PathToImageSourceConverter _converter = new PathToImageSourceConverter(); - private string _sampleImagePath = Path.GetFullPath("Assets/imgsource.png"); - - [Fact] - void Convert_EmptyPath_ReturnsNull() - { - Assert.Null(_converter.Convert("", typeof(ImageSource), null, CultureInfo.CurrentCulture)); - } - - [Fact] - void Convert_Null_ReturnsNull() - { - Assert.Null(_converter.Convert((object)null, typeof(ImageSource), null, CultureInfo.CurrentCulture)); - } - - [Fact] - void Convert_NoFile_ReturnsNull() - { - Assert.Null(_converter.Convert("nonexistingfile.png", typeof(ImageSource), null, CultureInfo.CurrentCulture)); - } - - [Fact] - void Convert_ValidFile_ReturnsImageSource() - { - var imgSource = _converter.Convert(_sampleImagePath, typeof(ImageSource), null, CultureInfo.CurrentCulture); - - Assert.NotNull(imgSource); - Assert.IsAssignableFrom(imgSource); - } - - [Fact] - void MultiConvert_InvalidFile_UsesFallback() - { - object fallback = new object(); - var imgSource = _converter.Convert(new[] {"invalidfile.png", fallback}, typeof(ImageSource), null, - CultureInfo.CurrentCulture); - - Assert.NotNull(imgSource); - Assert.Equal(fallback, imgSource); - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class PathToImageConverterTests + { + private PathToImageSourceConverter _converter = new PathToImageSourceConverter(); + private string _sampleImagePath = Path.GetFullPath("Assets/imgsource.png"); + + [Fact] + void Convert_EmptyPath_ReturnsNull() + { + Assert.Null(_converter.Convert("", typeof(ImageSource), null, CultureInfo.CurrentCulture)); + } + + [Fact] + void Convert_Null_ReturnsNull() + { + Assert.Null(_converter.Convert((object)null, typeof(ImageSource), null, CultureInfo.CurrentCulture)); + } + + [Fact] + void Convert_NoFile_ReturnsNull() + { + Assert.Null(_converter.Convert("nonexistingfile.png", typeof(ImageSource), null, CultureInfo.CurrentCulture)); + } + + [Fact] + void Convert_ValidFile_ReturnsImageSource() + { + var imgSource = _converter.Convert(_sampleImagePath, typeof(ImageSource), null, CultureInfo.CurrentCulture); + + Assert.NotNull(imgSource); + Assert.IsAssignableFrom(imgSource); + } + + [Fact] + void MultiConvert_InvalidFile_UsesFallback() + { + object fallback = new object(); + var imgSource = _converter.Convert(new[] {"invalidfile.png", fallback}, typeof(ImageSource), null, + CultureInfo.CurrentCulture); + + Assert.NotNull(imgSource); + Assert.Equal(fallback, imgSource); + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/PointConverterTests.cs b/src/AudioBand.Test/ValueConverters/PointConverterTests.cs new file mode 100644 index 00000000..70394cf5 --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/PointConverterTests.cs @@ -0,0 +1,31 @@ +using System.Globalization; +using System.Windows; +using Xunit; +using PointConverter = AudioBand.ValueConverters.PointConverter; + +namespace AudioBand.Test +{ + public class PointConverterTests + { + private readonly PointConverter _converter = new PointConverter(); + + [Fact] + public void ConvertXAndY_ReturnsPointWithXAndY() + { + var x = 1; + var y = 2; + var result = _converter.Convert(new object[] {x, y}, typeof(Point), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + var point = (Point) result; + Assert.Equal(x, point.X); + Assert.Equal(y, point.Y); + } + + [Fact] + public void ConvertBack_Unsupported_ReturnsNull() + { + Assert.Null(_converter.ConvertBack(null, null, null, CultureInfo.CurrentCulture)); + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/StringToFontFamilyConverterTests.cs b/src/AudioBand.Test/ValueConverters/StringToFontFamilyConverterTests.cs new file mode 100644 index 00000000..5f288534 --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/StringToFontFamilyConverterTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class StringToFontFamilyConverterTests + { + private readonly StringToFontFamilyConverter _converter = new StringToFontFamilyConverter(); + + [Fact] + public void ConvertString_ReturnsFontFamily() + { + var result = _converter.Convert("Arial", typeof(FontFamily), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal("Arial", ((FontFamily)result).Source); + } + + [Fact] + public void ConvertNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.Convert(null, typeof(FontFamily), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertBackFontFamily_ReturnsStringName() + { + var ff = new FontFamily("Arial"); + + var result = _converter.ConvertBack(ff, typeof(string), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal("Arial", (string)result); + } + + [Fact] + public void ConvertBackNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.ConvertBack(null, typeof(string), null, CultureInfo.CurrentCulture)); + + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/TimeSpanToMsConverterTests.cs b/src/AudioBand.Test/ValueConverters/TimeSpanToMsConverterTests.cs new file mode 100644 index 00000000..cd7099c1 --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/TimeSpanToMsConverterTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class TimeSpanToMsConverterTests + { + private readonly TimeSpanToMsConverter _converter = new TimeSpanToMsConverter(); + + [Fact] + public void ConvertTimeSpan_ReturnsMs() + { + var time = TimeSpan.FromMilliseconds(500); + var result = _converter.Convert(time, typeof(double), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(time.TotalMilliseconds, (double)result); + } + + [Fact] + public void ConvertTimeSpanInvalid_Returns0() + { + var result = _converter.Convert(null, typeof(double), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(0.0, (double)result); + } + + [Fact] + public void ConvertBackMs_ReturnsTimespan() + { + var ms = 1000.0; + var result = _converter.ConvertBack(ms, typeof(TimeSpan), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(TimeSpan.FromMilliseconds(ms), (TimeSpan)result); + } + + [Fact] + public void ConvertBackInvalid_ReturnsTimespan0() + { + var result = _converter.ConvertBack(null, typeof(TimeSpan), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(TimeSpan.Zero, (TimeSpan)result); + } + } +} diff --git a/src/AudioBand/AudioBand.csproj b/src/AudioBand/AudioBand.csproj index 5b038a20..c5894c46 100644 --- a/src/AudioBand/AudioBand.csproj +++ b/src/AudioBand/AudioBand.csproj @@ -107,6 +107,7 @@ + diff --git a/src/AudioBand/ValueConverters/BoolToVisibilityConverter.cs b/src/AudioBand/ValueConverters/BoolToVisibilityConverter.cs index 9f825efd..0734ea0d 100644 --- a/src/AudioBand/ValueConverters/BoolToVisibilityConverter.cs +++ b/src/AudioBand/ValueConverters/BoolToVisibilityConverter.cs @@ -8,19 +8,53 @@ namespace AudioBand.ValueConverters /// /// Converter from to . /// - [ValueConversion(typeof(bool), typeof(Visibility))] + [ValueConversion(typeof(bool), typeof(Visibility), ParameterType = typeof(bool))] public class BoolToVisibilityConverter : IValueConverter { - /// + /// + /// Converts a to a . + /// + /// The value of the bool to convert. + /// Type is . + /// A bool indicating whether the visibility should be collapsed. + /// The culture info. + /// if is true. + /// Otherwise returns except when is false in which case + /// is returned. public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + var collapse = parameter == null ? true : System.Convert.ToBoolean(parameter); - return (bool)value ? Visibility.Visible : (collapse ? Visibility.Collapsed : Visibility.Hidden); + var isVisible = (bool)value; + if (isVisible) + { + return Visibility.Visible; + } + else + { + return collapse ? Visibility.Collapsed : Visibility.Hidden; + } } - /// + /// + /// Converts a to a value. + /// + /// The . + /// The type of . + /// Parameter is ignored. + /// The current culture. + /// True if is . False otherwise. public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + var visibility = (Visibility)value; return visibility == Visibility.Visible; } diff --git a/src/AudioBand/ValueConverters/ColorToBrushConverter.cs b/src/AudioBand/ValueConverters/ColorToBrushConverter.cs index b219acb7..3c12b8b2 100644 --- a/src/AudioBand/ValueConverters/ColorToBrushConverter.cs +++ b/src/AudioBand/ValueConverters/ColorToBrushConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Data; using System.Windows.Media; @@ -8,18 +9,42 @@ namespace AudioBand.ValueConverters /// /// Converter from to . /// - [ValueConversion(typeof(Color), typeof(Brush))] + [ValueConversion(typeof(Color), typeof(SolidColorBrush))] public class ColorToBrushConverter : IValueConverter { - /// + /// + /// Converts a to a . + /// + /// The color. + /// The type of . + /// The converter parameter. + /// The current culture. + /// A representing the color. public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + return new SolidColorBrush((Color)value); } - /// + /// + /// Converts a to a . + /// + /// The brush. + /// The type of . + /// The converter parameter. + /// The current culture. + /// The color component of a . public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + return ((SolidColorBrush)value).Color; } } diff --git a/src/AudioBand/ValueConverters/ColorToNameConverter.cs b/src/AudioBand/ValueConverters/ColorToNameConverter.cs index 9e55f414..622e2171 100644 --- a/src/AudioBand/ValueConverters/ColorToNameConverter.cs +++ b/src/AudioBand/ValueConverters/ColorToNameConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Data; using System.Windows.Media; using AudioBand.Extensions; @@ -7,7 +8,7 @@ namespace AudioBand.ValueConverters { /// - /// Convert a string representation to a . + /// Convert a to a string representation. /// [ValueConversion(typeof(Color), typeof(string))] public class ColorToNameConverter : IValueConverter @@ -15,12 +16,22 @@ public class ColorToNameConverter : IValueConverter /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + return ((Color)value).GetColorName(); } /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + return ColorConverter.ConvertFromString((string)value); } } diff --git a/src/AudioBand/ValueConverters/ComparisonConverter.cs b/src/AudioBand/ValueConverters/ComparisonConverter.cs index 2b52cbf2..035ae126 100644 --- a/src/AudioBand/ValueConverters/ComparisonConverter.cs +++ b/src/AudioBand/ValueConverters/ComparisonConverter.cs @@ -18,7 +18,7 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur /// public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return null; } } } diff --git a/src/AudioBand/ValueConverters/DoubleToCornerRadiusConverter.cs b/src/AudioBand/ValueConverters/DoubleToCornerRadiusConverter.cs index fa7d7205..d2822ad0 100644 --- a/src/AudioBand/ValueConverters/DoubleToCornerRadiusConverter.cs +++ b/src/AudioBand/ValueConverters/DoubleToCornerRadiusConverter.cs @@ -11,7 +11,14 @@ namespace AudioBand.ValueConverters [ValueConversion(typeof(double), typeof(CornerRadius))] public class DoubleToCornerRadiusConverter : IValueConverter { - /// + /// + /// Uses a double as the width of a corner radius. + /// + /// The double to convert. + /// The type of . + /// The converter parameter. + /// The culture. + /// A uniform corner radius with each side equal to value / 2. public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return new CornerRadius(System.Convert.ToDouble(value) / 2); @@ -20,7 +27,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/EmptyStringToBoolConverter.cs b/src/AudioBand/ValueConverters/EmptyStringToBoolConverter.cs index 4c8b25a3..e576b395 100644 --- a/src/AudioBand/ValueConverters/EmptyStringToBoolConverter.cs +++ b/src/AudioBand/ValueConverters/EmptyStringToBoolConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Data; namespace AudioBand.ValueConverters @@ -25,7 +26,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/FlagToBoolConverter.cs b/src/AudioBand/ValueConverters/FlagToBoolConverter.cs index 9c25bf0b..3ba12f83 100644 --- a/src/AudioBand/ValueConverters/FlagToBoolConverter.cs +++ b/src/AudioBand/ValueConverters/FlagToBoolConverter.cs @@ -8,13 +8,20 @@ namespace AudioBand.ValueConverters /// /// Converts enum flag to bool. /// - [ValueConversion(typeof(Enum), typeof(bool))] + [ValueConversion(typeof(Enum), typeof(bool), ParameterType = typeof(Enum))] public class FlagToBoolConverter : IValueConverter { - /// + /// + /// Converts an enum flag to a bool value. + /// + /// The value of the flag. + /// The target type. + /// The flag to check if set. + /// The culture. + /// True if the flag specified by is set. public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null || value == DependencyProperty.UnsetValue || parameter == null) + if (value == null || parameter == null) { return false; } @@ -25,7 +32,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/MultiplierConverter.cs b/src/AudioBand/ValueConverters/MultiplierConverter.cs index d685cc34..33798d3b 100644 --- a/src/AudioBand/ValueConverters/MultiplierConverter.cs +++ b/src/AudioBand/ValueConverters/MultiplierConverter.cs @@ -6,17 +6,24 @@ namespace AudioBand.ValueConverters { /// - /// Mutliples all values together. + /// Multiplies all values together. /// [ValueConversion(typeof(double), typeof(double), ParameterType = typeof(double))] public class MultiplierConverter : IMultiValueConverter { - /// + /// + /// Converts a list of numeric values into a single value equal to multiplying them together. + /// + /// The list of values to multiply together. + /// The target type. + /// The converter parameter. + /// The culture. + /// A single value as the result of multiplying all the values together. Returns 1 if no values. public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (values == null) { - return 1; + return 1.0; } double acc = 1; @@ -31,7 +38,7 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur /// public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return null; } } } diff --git a/src/AudioBand/ValueConverters/ObjectToTypeConverter.cs b/src/AudioBand/ValueConverters/ObjectToTypeConverter.cs index bc595057..8a8cae5b 100644 --- a/src/AudioBand/ValueConverters/ObjectToTypeConverter.cs +++ b/src/AudioBand/ValueConverters/ObjectToTypeConverter.cs @@ -1,11 +1,12 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Data; namespace AudioBand.ValueConverters { /// - /// Converts and object to a type. + /// Converts an object to its type. /// [ValueConversion(typeof(object), typeof(Type))] public class ObjectToTypeConverter : IValueConverter @@ -13,13 +14,18 @@ public class ObjectToTypeConverter : IValueConverter /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return value?.GetType(); + if (value == null) + { + return DependencyProperty.UnsetValue; + } + + return value.GetType(); } /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/PathToImageSourceConverter.cs b/src/AudioBand/ValueConverters/PathToImageSourceConverter.cs index ec7b4e48..18e48cfb 100644 --- a/src/AudioBand/ValueConverters/PathToImageSourceConverter.cs +++ b/src/AudioBand/ValueConverters/PathToImageSourceConverter.cs @@ -55,7 +55,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } /// @@ -78,7 +78,7 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur /// public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return null; } } } diff --git a/src/AudioBand/ValueConverters/PointConverter.cs b/src/AudioBand/ValueConverters/PointConverter.cs index 21985fd7..d461cdc3 100644 --- a/src/AudioBand/ValueConverters/PointConverter.cs +++ b/src/AudioBand/ValueConverters/PointConverter.cs @@ -16,12 +16,12 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur { if (values == null) { - return default(Point); + return DependencyProperty.UnsetValue; } if (values.Any(v => v == DependencyProperty.UnsetValue)) { - return default(Point); + return DependencyProperty.UnsetValue; } var x = System.Convert.ToDouble(values[0]); @@ -32,7 +32,7 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur /// public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return null; } } } diff --git a/src/AudioBand/ValueConverters/StringFormatConverter.cs b/src/AudioBand/ValueConverters/StringFormatConverter.cs index 0418bf91..bc3887bf 100644 --- a/src/AudioBand/ValueConverters/StringFormatConverter.cs +++ b/src/AudioBand/ValueConverters/StringFormatConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Data; namespace AudioBand.ValueConverters @@ -24,7 +25,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/StringToFontFamilyConverter.cs b/src/AudioBand/ValueConverters/StringToFontFamilyConverter.cs index 941cf375..17c84fc9 100644 --- a/src/AudioBand/ValueConverters/StringToFontFamilyConverter.cs +++ b/src/AudioBand/ValueConverters/StringToFontFamilyConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Data; using System.Windows.Media; @@ -14,21 +15,20 @@ public class StringToFontFamilyConverter : IValueConverter /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null) + if (!ValueConverterHelper.IsValid(value)) { - throw new ArgumentNullException(nameof(value)); + return DependencyProperty.UnsetValue; } - var fontFamilyString = (string)value; - return new FontFamily(fontFamilyString); + return new FontFamily((string)value); } /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null) + if (!ValueConverterHelper.IsValid(value)) { - throw new ArgumentNullException(nameof(value)); + return DependencyProperty.UnsetValue; } var fontFamily = (FontFamily)value; diff --git a/src/AudioBand/ValueConverters/StringToVisibilityConverter.cs b/src/AudioBand/ValueConverters/StringToVisibilityConverter.cs index bb375d14..5dc600c6 100644 --- a/src/AudioBand/ValueConverters/StringToVisibilityConverter.cs +++ b/src/AudioBand/ValueConverters/StringToVisibilityConverter.cs @@ -8,7 +8,7 @@ namespace AudioBand.ValueConverters /// /// Converts a string to a value. /// - [ValueConversion(typeof(string), typeof(Visibility))] + [ValueConversion(typeof(string), typeof(Visibility), ParameterType = typeof(bool))] public class StringToVisibilityConverter : IValueConverter { /// @@ -21,7 +21,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/TextAlignmentConverter.cs b/src/AudioBand/ValueConverters/TextAlignmentConverter.cs index 5d782fff..d87d32fb 100644 --- a/src/AudioBand/ValueConverters/TextAlignmentConverter.cs +++ b/src/AudioBand/ValueConverters/TextAlignmentConverter.cs @@ -15,6 +15,11 @@ public class TextAlignmentConverter : IValueConverter /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + switch ((CustomLabel.TextAlignment)value) { case CustomLabel.TextAlignment.Center: @@ -31,7 +36,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/TimeSpanToMsConverter.cs b/src/AudioBand/ValueConverters/TimeSpanToMsConverter.cs index 25a7530e..b1a037ad 100644 --- a/src/AudioBand/ValueConverters/TimeSpanToMsConverter.cs +++ b/src/AudioBand/ValueConverters/TimeSpanToMsConverter.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using System.Windows; using System.Windows.Data; namespace AudioBand.ValueConverters @@ -14,9 +13,9 @@ public class TimeSpanToMsConverter : IValueConverter /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null || value == DependencyProperty.UnsetValue) + if (!ValueConverterHelper.IsValid(value)) { - return 0; + return 0.0; } return ((TimeSpan)value).TotalMilliseconds; @@ -25,7 +24,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null || value == DependencyProperty.UnsetValue) + if (!ValueConverterHelper.IsValid(value)) { return TimeSpan.Zero; } diff --git a/src/AudioBand/ValueConverters/ValueConverterHelper.cs b/src/AudioBand/ValueConverters/ValueConverterHelper.cs new file mode 100644 index 00000000..2ed1785b --- /dev/null +++ b/src/AudioBand/ValueConverters/ValueConverterHelper.cs @@ -0,0 +1,23 @@ +using System.Windows; + +namespace AudioBand.ValueConverters +{ + /// + /// Helpers for value conversion. + /// + internal static class ValueConverterHelper + { + /// + /// Provides a standard guard clause for the converter value. + /// + /// Expected type. + /// The value passed to the converter. + /// True if valid, false otherwise. + internal static bool IsValid(object value) + { + return value != null + && value != DependencyProperty.UnsetValue + && value.GetType() == typeof(TType); + } + } +} From 024afe45125b0bc43177075b824642651d80cf5f Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 11 Jul 2019 15:10:08 -0700 Subject: [PATCH 03/10] Create CODE_OF_CONDUCT.md Following recommended things for a github repo --- CODE_OF_CONDUCT.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..89ce0b91 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at brndnchong@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq From c0ae06a103934fb6508085c7931b6e1e50ab5ac5 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 11 Jul 2019 15:30:26 -0700 Subject: [PATCH 04/10] Create CONTRIBUTING.md (#205) --- CONTRIBUTING.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..51e0402b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Intro +Thanks for taking the time to read this! +This doc contains guidelines and resources for contributing. It's not too complete but I'll add to it. + +# Ways to contribute +## Reporting Bugs +Feel free to report bugs in the [issues tracker](https://github.com/dsafa/audio-band/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc). +There are templates when creating a new issue to help you get started. Try to search for existing bugs first, the keyword search is quite good. + +## Suggest Features +You can also suggest features and improvements in the issue tracker as well. + +## Development +Development docs are on the [github page](https://dsafa.github.io/audio-band/audioband/development/setup.html) for this repo. +The site will have instructions for setting up a build environment including instructions on how to debug a local setup as well as +an overview of the project itself. + +I try to keep it as up to date as possible but like all docs, they tend to get outdated. +If something is out of date feel free to raise an issue about it. From 5121aee229cce27be09bc63fcbc27ca860b14009 Mon Sep 17 00:00:00 2001 From: Brandon Chong Date: Sun, 14 Jul 2019 18:56:55 -0700 Subject: [PATCH 05/10] Fix audio source settings changed event not causing a save --- .../AudioSourceSettingsCollectionTests.cs | 13 ++++++++----- .../ViewModels/AudioSourceSettingKeyValue.cs | 10 +++++++++- .../AudioSourceSettingsCollectionViewModel.cs | 11 ++++++++++- .../ViewModels/AudioSourceSettingsViewModel.cs | 4 ++-- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/AudioBand.Test/ViewModels/AudioSourceSettingsCollectionTests.cs b/src/AudioBand.Test/ViewModels/AudioSourceSettingsCollectionTests.cs index 03e69864..27a50c8e 100644 --- a/src/AudioBand.Test/ViewModels/AudioSourceSettingsCollectionTests.cs +++ b/src/AudioBand.Test/ViewModels/AudioSourceSettingsCollectionTests.cs @@ -4,6 +4,7 @@ using Moq; using System.Collections.Generic; using AudioBand.Messages; +using AudioBand.Settings; using Xunit; namespace AudioBand.Test @@ -12,11 +13,13 @@ public class AudioSourceSettingsCollectionTests { private Mock _audioSourceMock; private Mock _messageBus; + private Mock _appSettings; public AudioSourceSettingsCollectionTests() { _audioSourceMock = new Mock(); _messageBus = new Mock(); + _appSettings = new Mock(); } [Fact] @@ -33,7 +36,7 @@ public void NoMatchingSettings_CreatesNoChildViewModels() }; var settings = new AudioSourceSettings { AudioSourceName = name, Settings = keyVals }; - var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object); + var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object, _appSettings.Object); Assert.Empty(vm.SettingsList); Assert.Equal(name, vm.AudioSourceName); @@ -65,7 +68,7 @@ public void MatchingSettings_ShouldCreateChildViewModelsInOrder() }; var settings = new AudioSourceSettings { Settings = settingModels }; - var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object); + var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object, _appSettings.Object); Assert.Equal(settingModels.Count, vm.SettingsList.Count); Assert.Equal(settingModels[0].Name, vm.SettingsList[0].Name); @@ -88,12 +91,12 @@ public void AudioSourceSettingUpdate_NewValueIsWrittenBackToSettings() object newSettingValue = 1; _audioSourceMock.SetupGet(s => s[It.Is(x => x == setting)]).Returns(newSettingValue); - var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object); + var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object, _appSettings.Object); _audioSourceMock.Raise(s => s.SettingChanged += null, new SettingChangedEventArgs(setting)); - vm.EndEdit(); Assert.Equal(newSettingValue, settingModel.Value); + _appSettings.Verify(m => m.Save(), Times.Once); } [Fact] @@ -121,7 +124,7 @@ public void AudioSourceSettingsUpdated_AudioSourceIsUpdatedInPriority() _audioSourceMock.InSequence(s).SetupSet(source => source[It.Is(x => x == setting1.Name)] = null); _audioSourceMock.InSequence(s).SetupSet(source => source[It.Is(x => x == setting2.Name)] = null); - var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object); + var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object, _appSettings.Object); _audioSourceMock.VerifySet(source => source[setting3.Name] = null); _audioSourceMock.VerifySet(source => source[setting1.Name] = null); diff --git a/src/AudioBand/ViewModels/AudioSourceSettingKeyValue.cs b/src/AudioBand/ViewModels/AudioSourceSettingKeyValue.cs index 4bfd94b6..95665f8b 100644 --- a/src/AudioBand/ViewModels/AudioSourceSettingKeyValue.cs +++ b/src/AudioBand/ViewModels/AudioSourceSettingKeyValue.cs @@ -96,6 +96,14 @@ public void PropagateSettingToAudioSource() } } + /// + /// When the audio sources updates the setting itself instead of from audioband. Sync the changes back to the model. + /// + public void SyncToModel() + { + MapSelf(_model, _originalSource); + } + /// protected override void OnBeginEdit() { @@ -124,7 +132,7 @@ protected override void OnEndEdit() return; } - MapSelf(_model, _originalSource); + SyncToModel(); PropagateSettingToAudioSource(); } } diff --git a/src/AudioBand/ViewModels/AudioSourceSettingsCollectionViewModel.cs b/src/AudioBand/ViewModels/AudioSourceSettingsCollectionViewModel.cs index 2f41eae2..0cc8d0f1 100644 --- a/src/AudioBand/ViewModels/AudioSourceSettingsCollectionViewModel.cs +++ b/src/AudioBand/ViewModels/AudioSourceSettingsCollectionViewModel.cs @@ -6,6 +6,7 @@ using AudioBand.AudioSource; using AudioBand.Messages; using AudioBand.Models; +using AudioBand.Settings; namespace AudioBand.ViewModels { @@ -16,6 +17,7 @@ public class AudioSourceSettingsCollectionViewModel : ViewModelBase { private readonly IInternalAudioSource _audioSource; private readonly IMessageBus _messageBus; + private readonly IAppSettings _appSettings; /// /// Initializes a new instance of the class. @@ -23,10 +25,12 @@ public class AudioSourceSettingsCollectionViewModel : ViewModelBase /// The audiosource that these settings belong to. /// The settings model. /// The message bus. - public AudioSourceSettingsCollectionViewModel(IInternalAudioSource audioSource, AudioSourceSettings settingsModel, IMessageBus messageBus) + /// The app settings. + public AudioSourceSettingsCollectionViewModel(IInternalAudioSource audioSource, AudioSourceSettings settingsModel, IMessageBus messageBus, IAppSettings appSettings) { _audioSource = audioSource; _messageBus = messageBus; + _appSettings = appSettings; SettingsList = new ObservableCollection(CreateKeyValuePairs(audioSource, settingsModel)); _audioSource.SettingChanged += AudioSourceOnSettingChanged; @@ -102,6 +106,9 @@ private List CreateKeyValuePairs(IInternalAudioSourc } // Audio sources can change settings themselves so we need to listen for them. + // Usually, settings are saved after the user edits a setting and clicks apply. + // These changes occur outside of the normal settings lifecycle, so the only time that the new values can be saved + // are when the application closes but there are some issues with detected that, so instead just save now. private void AudioSourceOnSettingChanged(object sender, SettingChangedEventArgs e) { var settingThatChanged = SettingsList.FirstOrDefault(s => s.Name == e.SettingName); @@ -114,6 +121,8 @@ private void AudioSourceOnSettingChanged(object sender, SettingChangedEventArgs try { settingThatChanged.Value = _audioSource[e.SettingName]; + settingThatChanged.SyncToModel(); + _appSettings.Save(); } catch (Exception ex) { diff --git a/src/AudioBand/ViewModels/AudioSourceSettingsViewModel.cs b/src/AudioBand/ViewModels/AudioSourceSettingsViewModel.cs index be4ae67f..2f01512b 100644 --- a/src/AudioBand/ViewModels/AudioSourceSettingsViewModel.cs +++ b/src/AudioBand/ViewModels/AudioSourceSettingsViewModel.cs @@ -48,14 +48,14 @@ public void CreateViewModelForAudioSource(IInternalAudioSource audioSource) var matchingSetting = _appSettings.AudioSourceSettings.FirstOrDefault(s => s.AudioSourceName == audioSource.Name); if (matchingSetting != null) { - var viewModel = new AudioSourceSettingsCollectionViewModel(audioSource, matchingSetting, _messageBus); + var viewModel = new AudioSourceSettingsCollectionViewModel(audioSource, matchingSetting, _messageBus, _appSettings); AudioSourcesSettings.Add(viewModel); } else { var newSettingsModel = new AudioSourceSettings { AudioSourceName = audioSource.Name }; _appSettings.AudioSourceSettings.Add(newSettingsModel); - var newViewModel = new AudioSourceSettingsCollectionViewModel(audioSource, newSettingsModel, _messageBus); + var newViewModel = new AudioSourceSettingsCollectionViewModel(audioSource, newSettingsModel, _messageBus, _appSettings); AudioSourcesSettings.Add(newViewModel); } From a91d9c95dbc79e67eaeca6cec58bf550e6471db4 Mon Sep 17 00:00:00 2001 From: Brandon Chong Date: Sun, 14 Jul 2019 19:18:05 -0700 Subject: [PATCH 06/10] Add pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4baaf616 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +## Summary + +## Checklist +- [ ] Tests passed +- [ ] Changes validated (manually or automated) +- [ ] Closes / Fixes #xxx (if applicable) + +## Additional details / comments + +## Manual test steps (if applicable) \ No newline at end of file From a5a7e010693a597b9722ecd69672db9772596ff7 Mon Sep 17 00:00:00 2001 From: Brandon Chong Date: Sun, 14 Jul 2019 21:33:23 -0700 Subject: [PATCH 07/10] Fix migration errors from older settings - Github: fixes #208 --- .../Settings/SettingsMigrationTests.cs | 132 ++++++++++++++++-- src/AudioBand/AudioBand.csproj | 2 + src/AudioBand/Settings/AppSettings.cs | 64 +++++---- .../Settings/Migrations/IdentityMigrator.cs | 18 +++ .../Settings/Migrations/Migration.cs | 25 +++- src/AudioBand/Settings/TomlHelper.cs | 27 ++++ src/AudioBandRules.ruleset | 1 + 7 files changed, 219 insertions(+), 50 deletions(-) create mode 100644 src/AudioBand/Settings/Migrations/IdentityMigrator.cs create mode 100644 src/AudioBand/Settings/TomlHelper.cs diff --git a/src/AudioBand.Test/Settings/SettingsMigrationTests.cs b/src/AudioBand.Test/Settings/SettingsMigrationTests.cs index 9c28c433..39fd35b1 100644 --- a/src/AudioBand.Test/Settings/SettingsMigrationTests.cs +++ b/src/AudioBand.Test/Settings/SettingsMigrationTests.cs @@ -9,7 +9,7 @@ using V2Settings = AudioBand.Settings.Models.V2.Settings; using Nett; using Xunit; -using AudioSourceSetting = AudioBand.Settings.Models.V1.AudioSourceSetting; +using V1AudioSourceSetting = AudioBand.Settings.Models.V1.AudioSourceSetting; namespace AudioBand.Test { @@ -137,13 +137,13 @@ public void MigrateV1ToV2_AudioSourceSettings() var setting1 = new AudioSourceSettingsCollection { Name = "test", - Settings = new List {new AudioSourceSetting {Name = "key1", Value = "val1"}} + Settings = new List {new V1AudioSourceSetting {Name = "key1", Value = "val1"}} }; var setting2 = new AudioSourceSettingsCollection { Name = "test2", - Settings = new List { new AudioSourceSetting { Name = "key2", Value = "val2" } } + Settings = new List { new V1AudioSourceSetting { Name = "key2", Value = "val2" } } }; var settings = new List {setting1,setting2}; @@ -415,17 +415,7 @@ public void MigrateV2ToV3_MigratesSuccessfully() Name = ""Spotify Client secret"" Value = ""secret"" "; - var settings = TomlSettings.Create(cfg => - { - cfg.ConfigureType(type => type.WithConversionFor(convert => convert - .ToToml(SerializationConversions.ColorToString) - .FromToml(tomlString => SerializationConversions.StringToColor(tomlString.Value)))); - cfg.ConfigureType(type => type.WithConversionFor(convert => convert - .ToToml(SerializationConversions.EnumToString) - .FromToml(str => SerializationConversions.StringToEnum(str.Value)))); - cfg.ConfigureType(type => type.WithConversionFor(c => c - .FromToml(tml => tml.Value))); - }); + var settings = TomlHelper.DefaultSettings; var v2 = Toml.ReadString(settingsFile, settings); var v3 = Migration.MigrateSettings(v2, "2", "3"); @@ -499,5 +489,119 @@ public void MigrateV2ToV3_MigratesSuccessfully() Assert.Equal(v2.CustomLabelSettings[0].Alignment, v3.Profiles[SettingsV3.DefaultProfileName].CustomLabelSettings[0].Alignment); Assert.Equal(v2.CustomLabelSettings[0].FormatString, v3.Profiles[SettingsV3.DefaultProfileName].CustomLabelSettings[0].FormatString); } + + [Fact] + public void ChainedMigrations_V1ToV3_SuccessfulMigration() + { + var v1Settings = @" +Version = ""0.1"" + +[AudioBandAppearance] +Width = 300 +Height = 30 + +[PlayPauseButtonAppearance] +XPosition = 0 +YPosition = 0 +Width = 30 +Height = 10 +IsVisible = true + +[NextSongButtonAppearance] +IsVisible = true +Width = 30 +Height = 10 +XPosition = 0 +YPosition = 0 + +[PreviousSongButtonAppearance] +IsVisible = true +Width = 30 +Height = 10 +XPosition = 0 +YPosition = 0 + +[[TextAppearances]] +IsVisible = true +Width = 100 +Height = 15 +XPosition = 150 +YPosition = 10 +FontSize = 10.0 +Color = ""White"" +Alignment = ""Center"" +ScrollSpeed = 0 +FormatString = ""{song}"" + +[ProgressBarAppearance] +ForegroundColor = ""Blue"" +BackgroundColor = ""Gray"" +IsVisible = true +XPosition = 0 +YPosition = 26 +Width = 200 +Height = 2 + +[AlbumArtAppearance] +IsVisible = true +Width = 30 +Height = 30 +XPosition = 0 +YPosition = 0 + +[AlbumArtPopupAppearance] +IsVisible = true +Width = 500 +Height = 500 +XOffset = 50 +Margin = 6 + +"; + var v1 = Toml.ReadString(v1Settings, TomlHelper.DefaultSettings); + var v3 = Migration.MigrateSettings(v1, "0.1", "3"); + var v3Profile = v3.Profiles[SettingsV3.DefaultProfileName]; + + Assert.Equal(v1.AudioBandAppearance.Width, v3Profile.AudioBandSettings.Width); + Assert.Equal(v1.AudioBandAppearance.Height, v3Profile.AudioBandSettings.Height); + + Assert.Equal(v1.PlayPauseButtonAppearance.Width, v3Profile.PlayPauseButtonSettings.Width); + Assert.Equal(v1.PlayPauseButtonAppearance.Height, v3Profile.PlayPauseButtonSettings.Height); + Assert.Equal(v1.PlayPauseButtonAppearance.XPosition, v3Profile.PlayPauseButtonSettings.XPosition); + Assert.Equal(v1.PlayPauseButtonAppearance.YPosition, v3Profile.PlayPauseButtonSettings.YPosition); + Assert.Equal(v1.PlayPauseButtonAppearance.IsVisible, v3Profile.PlayPauseButtonSettings.IsVisible); + + Assert.Equal(v1.NextSongButtonAppearance.Width, v3Profile.NextButtonSettings.Width); + Assert.Equal(v1.NextSongButtonAppearance.Height, v3Profile.NextButtonSettings.Height); + Assert.Equal(v1.NextSongButtonAppearance.XPosition, v3Profile.NextButtonSettings.XPosition); + Assert.Equal(v1.NextSongButtonAppearance.YPosition, v3Profile.NextButtonSettings.YPosition); + Assert.Equal(v1.NextSongButtonAppearance.IsVisible, v3Profile.NextButtonSettings.IsVisible); + + Assert.Equal(v1.PreviousSongButtonAppearance.Width, v3Profile.PreviousButtonSettings.Width); + Assert.Equal(v1.PreviousSongButtonAppearance.Height, v3Profile.PreviousButtonSettings.Height); + Assert.Equal(v1.PreviousSongButtonAppearance.XPosition, v3Profile.PreviousButtonSettings.XPosition); + Assert.Equal(v1.PreviousSongButtonAppearance.YPosition, v3Profile.PreviousButtonSettings.YPosition); + Assert.Equal(v1.PreviousSongButtonAppearance.IsVisible, v3Profile.PreviousButtonSettings.IsVisible); + + Assert.Single(v3Profile.CustomLabelSettings); + Assert.Equal(v1.TextAppearances[0].Color, v3Profile.CustomLabelSettings[0].Color); + Assert.Equal(v1.TextAppearances[0].Alignment, v3Profile.CustomLabelSettings[0].Alignment); + Assert.Equal(v1.TextAppearances[0].FontFamily, v3Profile.CustomLabelSettings[0].FontFamily); + Assert.Equal(v1.TextAppearances[0].FontSize, v3Profile.CustomLabelSettings[0].FontSize); + Assert.Equal(v1.TextAppearances[0].FormatString, v3Profile.CustomLabelSettings[0].FormatString); + Assert.Equal(v1.TextAppearances[0].Height, v3Profile.CustomLabelSettings[0].Height); + Assert.Equal(v1.TextAppearances[0].IsVisible, v3Profile.CustomLabelSettings[0].IsVisible); + Assert.Equal(v1.TextAppearances[0].Name, v3Profile.CustomLabelSettings[0].Name); + Assert.Equal(v1.TextAppearances[0].ScrollSpeed, v3Profile.CustomLabelSettings[0].ScrollSpeed); + Assert.Equal(v1.TextAppearances[0].Width, v3Profile.CustomLabelSettings[0].Width); + Assert.Equal(v1.TextAppearances[0].XPosition, v3Profile.CustomLabelSettings[0].XPosition); + Assert.Equal(v1.TextAppearances[0].YPosition, v3Profile.CustomLabelSettings[0].YPosition); + + Assert.Equal(v1.AlbumArtAppearance.Height, v3Profile.AlbumArtSettings.Height); + Assert.Equal(v1.AlbumArtAppearance.IsVisible, v3Profile.AlbumArtSettings.IsVisible); + Assert.Equal(v1.AlbumArtAppearance.PlaceholderPath, v3Profile.AlbumArtSettings.PlaceholderPath); + Assert.Equal(v1.AlbumArtAppearance.Width, v3Profile.AlbumArtSettings.Width); + Assert.Equal(v1.AlbumArtAppearance.XPosition, v3Profile.AlbumArtSettings.XPosition); + Assert.Equal(v1.AlbumArtAppearance.YPosition, v3Profile.AlbumArtSettings.YPosition); + } } } diff --git a/src/AudioBand/AudioBand.csproj b/src/AudioBand/AudioBand.csproj index c5894c46..b60a19ff 100644 --- a/src/AudioBand/AudioBand.csproj +++ b/src/AudioBand/AudioBand.csproj @@ -107,6 +107,8 @@ + + diff --git a/src/AudioBand/Settings/AppSettings.cs b/src/AudioBand/Settings/AppSettings.cs index 6e2b0f20..2ab6e939 100644 --- a/src/AudioBand/Settings/AppSettings.cs +++ b/src/AudioBand/Settings/AppSettings.cs @@ -32,7 +32,6 @@ public class AppSettings : IAppSettings private static readonly string SettingsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AudioBand"); private static readonly string SettingsFilePath = Path.Combine(SettingsDirectory, "audioband.settings"); private static readonly ILogger Logger = AudioBandLogManager.GetLogger(); - private readonly TomlSettings _tomlSettings; private SettingsV3 _settings; private ProfileV3 _currentProfile; @@ -41,31 +40,7 @@ public class AppSettings : IAppSettings /// public AppSettings() { - _tomlSettings = TomlSettings.Create(cfg => - { - cfg.ConfigureType(type => type.WithConversionFor(convert => convert - .ToToml(SerializationConversions.ColorToString) - .FromToml(tomlString => SerializationConversions.StringToColor(tomlString.Value)))); - cfg.ConfigureType(type => type.WithConversionFor(convert => convert - .ToToml(SerializationConversions.EnumToString) - .FromToml(str => SerializationConversions.StringToEnum(str.Value)))); - cfg.ConfigureType(type => type.WithConversionFor(c => c - .FromToml(tml => tml.Value))); - }); - - if (!Directory.Exists(SettingsDirectory)) - { - Directory.CreateDirectory(SettingsDirectory); - } - - if (!File.Exists(SettingsFilePath)) - { - CreateDefaultSettingsFile(); - } - else - { - LoadSettingsFromPath(SettingsFilePath); - } + InitSettings(); if (_settings.AudioSourceSettings == null) { @@ -243,7 +218,7 @@ public void Save() { try { - Toml.WriteFile(_settings, SettingsFilePath, _tomlSettings); + Toml.WriteFile(_settings, SettingsFilePath, TomlHelper.DefaultSettings); } catch (Exception e) { @@ -254,7 +229,7 @@ public void Save() /// public void ImportProfilesFromPath(string path) { - var profilesToImport = Toml.ReadFile(path, _tomlSettings); + var profilesToImport = Toml.ReadFile(path, TomlHelper.DefaultSettings); foreach (var keyVal in profilesToImport.Profiles) { var key = GetUniqueProfileName(keyVal.Key); @@ -266,18 +241,18 @@ public void ImportProfilesFromPath(string path) public void ExportProfilesToPath(string path) { var exportObject = new ProfileExportV3 { Profiles = _settings.Profiles }; - Toml.WriteFile(exportObject, path, _tomlSettings); + Toml.WriteFile(exportObject, path, TomlHelper.DefaultSettings); } private void LoadSettingsFromPath(string path) { - var tomlFile = Toml.ReadFile(path, _tomlSettings); + var tomlFile = Toml.ReadFile(path, TomlHelper.DefaultSettings); var version = tomlFile["Version"].Get(); // Create backup if (version != CurrentVersion) { - Toml.WriteFile(tomlFile, Path.Combine(SettingsDirectory, $"audioband.settings.{version}"), _tomlSettings); + Toml.WriteFile(tomlFile, Path.Combine(SettingsDirectory, $"audioband.settings.{version}"), TomlHelper.DefaultSettings); _settings = Migration.MigrateSettings(tomlFile.Get(SettingsTable[version]), version, CurrentVersion); Save(); } @@ -402,5 +377,32 @@ private string GetUniqueProfileName(string name) return newName; } + + private void InitSettings() + { + if (!Directory.Exists(SettingsDirectory)) + { + Directory.CreateDirectory(SettingsDirectory); + } + + if (!File.Exists(SettingsFilePath)) + { + CreateDefaultSettingsFile(); + return; + } + + try + { + LoadSettingsFromPath(SettingsFilePath); + } + catch (Exception e) + { + Logger.Error(e, "Unable to load settings"); + var backupPath = Path.Combine(SettingsDirectory, "audioband.settings.backup-" + DateTime.Now.Ticks); + File.Copy(SettingsFilePath, backupPath, true); + Logger.Info("Creating new default settings. Backup created at {backup}", backupPath); + CreateDefaultSettingsFile(); + } + } } } diff --git a/src/AudioBand/Settings/Migrations/IdentityMigrator.cs b/src/AudioBand/Settings/Migrations/IdentityMigrator.cs new file mode 100644 index 00000000..d3b7f4fb --- /dev/null +++ b/src/AudioBand/Settings/Migrations/IdentityMigrator.cs @@ -0,0 +1,18 @@ +namespace AudioBand.Settings.Migrations +{ + /// + /// Performs no migration. + /// + internal class IdentityMigrator : ISettingsMigrator + { + /// + /// Migrate settings to new version. + /// + /// Old settings to migrate. + /// The new settings. + public object MigrateSetting(object oldSetting) + { + return oldSetting; + } + } +} diff --git a/src/AudioBand/Settings/Migrations/Migration.cs b/src/AudioBand/Settings/Migrations/Migration.cs index 5e97c4fc..ce1f264e 100644 --- a/src/AudioBand/Settings/Migrations/Migration.cs +++ b/src/AudioBand/Settings/Migrations/Migration.cs @@ -11,10 +11,12 @@ namespace AudioBand.Settings.Migrations /// internal static class Migration { - private static readonly Dictionary<(string From, string To), ISettingsMigrator> SupportedMigrations = new Dictionary<(string From, string To), ISettingsMigrator>() + // Assume that migrations can be applied in order. + private static readonly List<(string version, ISettingsMigrator migrator)> MigrationsList = new List<(string, ISettingsMigrator)> { - { ("0.1", "2"), new V1ToV2() }, - { ("2", "3"), new V2ToV3() }, + ("0.1", new V1ToV2()), + ("2", new V2ToV3()), + ("3", new IdentityMigrator()), }; private static readonly ILogger Logger = AudioBandLogManager.GetLogger(typeof(Migration).FullName); @@ -35,7 +37,7 @@ public static TNew MigrateSettings(object oldSettings, string oldVersion, } var plan = FindPlan(oldVersion, newVersion); - if (!plan.Any()) + if (plan == null || !plan.Any()) { throw new ArgumentException($"No migration plan from {oldVersion} to {newVersion}"); } @@ -48,7 +50,20 @@ public static TNew MigrateSettings(object oldSettings, string oldVersion, private static List FindPlan(string from, string to) { - return SupportedMigrations.Where(x => x.Key.From == from && x.Key.To == to).Select(x => x.Value).ToList(); + var startIndex = MigrationsList.FindIndex(m => m.version == from); + var endIndex = MigrationsList.FindIndex(m => m.version == to); + if (startIndex == -1 || endIndex == -1) + { + return null; + } + + if (endIndex < startIndex) + { + Logger.Error("End index less than start index"); + return null; + } + + return MigrationsList.GetRange(startIndex, endIndex - startIndex).Select(m => m.migrator).ToList(); } } } diff --git a/src/AudioBand/Settings/TomlHelper.cs b/src/AudioBand/Settings/TomlHelper.cs new file mode 100644 index 00000000..62bcb029 --- /dev/null +++ b/src/AudioBand/Settings/TomlHelper.cs @@ -0,0 +1,27 @@ +using System.Windows.Media; +using AudioBand.Models; +using Nett; + +namespace AudioBand.Settings +{ + /// + /// Helper for TOML serialization and deserialization. + /// + internal static class TomlHelper + { + /// + /// Gets the default settings for TOML de/serialization. + /// + public static TomlSettings DefaultSettings { get; } = TomlSettings.Create(cfg => + { + cfg.ConfigureType(type => type.WithConversionFor(convert => convert + .ToToml(SerializationConversions.ColorToString) + .FromToml(tomlString => SerializationConversions.StringToColor(tomlString.Value)))); + cfg.ConfigureType(type => type.WithConversionFor(convert => convert + .ToToml(SerializationConversions.EnumToString) + .FromToml(str => SerializationConversions.StringToEnum(str.Value)))); + cfg.ConfigureType(type => type.WithConversionFor(c => c + .FromToml(tml => tml.Value))); + }); + } +} diff --git a/src/AudioBandRules.ruleset b/src/AudioBandRules.ruleset index 0a65f425..b68806b6 100644 --- a/src/AudioBandRules.ruleset +++ b/src/AudioBandRules.ruleset @@ -19,5 +19,6 @@ + \ No newline at end of file From 873930603c37c99b8d5627c103d35e3d51b5016f Mon Sep 17 00:00:00 2001 From: Brandon Chong Date: Mon, 15 Jul 2019 10:14:53 -0700 Subject: [PATCH 08/10] Fix custom labels not saving settings properly --- .../ViewModels/CustomLabelViewModelTests.cs | 29 +++++++++++++++++++ .../ViewModels/CustomLabelsViewModelTests.cs | 24 +++++++++++++++ .../ViewModels/CustomLabelViewModel.cs | 9 ++++++ .../ViewModels/CustomLabelsViewModel.cs | 10 +++++-- 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/AudioBand.Test/ViewModels/CustomLabelViewModelTests.cs b/src/AudioBand.Test/ViewModels/CustomLabelViewModelTests.cs index 396aef52..6a80d1ed 100644 --- a/src/AudioBand.Test/ViewModels/CustomLabelViewModelTests.cs +++ b/src/AudioBand.Test/ViewModels/CustomLabelViewModelTests.cs @@ -73,5 +73,34 @@ void CustomLabel_ColorChangedThenCanceled_TextSegmentsHaveCorrectColor() Assert.All(vm.TextSegments, segment => Assert.Equal(Colors.Blue, segment.Color)); } + + [Fact] + void CustomLabel_CancelEdit_ModelHasNoChanges() + { + var formatString = "test"; + var model = new CustomLabel {FormatString = formatString}; + var vm = new CustomLabelViewModel(model, _dialogMock.Object, _sessionMock.Object, _messageBusMock.Object); + + vm.FormatString = "new"; + vm.CancelEdit(); + + Assert.Equal(formatString, vm.FormatString); + Assert.Equal(formatString, model.FormatString); + } + + [Fact] + void CustomLabel_SaveEdit_ModelHasNewChanges() + { + var formatString = "test"; + var newFormatstring = "format"; + var model = new CustomLabel { FormatString = formatString }; + var vm = new CustomLabelViewModel(model, _dialogMock.Object, _sessionMock.Object, _messageBusMock.Object); + + vm.FormatString = newFormatstring; + vm.EndEdit(); + + Assert.Equal(newFormatstring, vm.FormatString); + Assert.Equal(newFormatstring, model.FormatString); + } } } diff --git a/src/AudioBand.Test/ViewModels/CustomLabelsViewModelTests.cs b/src/AudioBand.Test/ViewModels/CustomLabelsViewModelTests.cs index 2dafa58d..151bedde 100644 --- a/src/AudioBand.Test/ViewModels/CustomLabelsViewModelTests.cs +++ b/src/AudioBand.Test/ViewModels/CustomLabelsViewModelTests.cs @@ -137,5 +137,29 @@ public void ProfileChanged_NewLabelsHaveCorrectAudioSessionData() _appSettingsMock.Raise(m => m.ProfileChanged += null, null, EventArgs.Empty); Assert.True(vm.CustomLabels[0].IsPlaying); } + + [Fact] + public void RemoveLabel_PublishEdit() + { + _appSettingsMock.SetupGet(x => x.CustomLabels).Returns(new List { new CustomLabel() }); + _dialogMock.Setup(o => o.ShowConfirmationDialog(It.IsAny(), It.IsAny())).Returns(true); + _viewModel = new CustomLabelsViewModel(_appSettingsMock.Object, _dialogMock.Object, _sessionMock.Object, _messageBus.Object); + var label = _viewModel.CustomLabels[0]; + _viewModel.RemoveLabelCommand.Execute(label); + + Assert.True(_viewModel.IsEditing); + _messageBus.Verify(m => m.Publish(It.IsAny(), It.IsAny())); + } + + [Fact] + public void AddLabel_PublishEdit() + { + _appSettingsMock.SetupGet(x => x.CustomLabels).Returns(new List { new CustomLabel() }); + _viewModel = new CustomLabelsViewModel(_appSettingsMock.Object, _dialogMock.Object, _sessionMock.Object, _messageBus.Object); + _viewModel.AddLabelCommand.Execute(null); + + Assert.True(_viewModel.IsEditing); + _messageBus.Verify(m => m.Publish(It.IsAny(), It.IsAny())); + } } } diff --git a/src/AudioBand/ViewModels/CustomLabelViewModel.cs b/src/AudioBand/ViewModels/CustomLabelViewModel.cs index 7cd5526c..59fd5720 100644 --- a/src/AudioBand/ViewModels/CustomLabelViewModel.cs +++ b/src/AudioBand/ViewModels/CustomLabelViewModel.cs @@ -17,6 +17,7 @@ namespace AudioBand.ViewModels /// public class CustomLabelViewModel : LayoutViewModelBase { + private readonly CustomLabel _source; private readonly IAudioSession _audioSession; private bool _isPlaying; private IEnumerable _textSegments; @@ -31,6 +32,7 @@ public class CustomLabelViewModel : LayoutViewModelBase public CustomLabelViewModel(CustomLabel source, IDialogService dialogService, IAudioSession audioSession, IMessageBus messageBus) : base(messageBus, source) { + _source = source; _audioSession = audioSession; _audioSession.PropertyChanged += AudioSessionOnPropertyChanged; @@ -243,6 +245,13 @@ protected override void OnReset() ReParseSegments(); } + /// + protected override void OnEndEdit() + { + base.OnEndEdit(); + MapSelf(Model, _source); + } + private void AudioSessionOnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName != nameof(IAudioSession.IsPlaying)) diff --git a/src/AudioBand/ViewModels/CustomLabelsViewModel.cs b/src/AudioBand/ViewModels/CustomLabelsViewModel.cs index 206b4101..46749b76 100644 --- a/src/AudioBand/ViewModels/CustomLabelsViewModel.cs +++ b/src/AudioBand/ViewModels/CustomLabelsViewModel.cs @@ -61,6 +61,8 @@ public CustomLabelsViewModel(IAppSettings appsettings, IDialogService dialogServ /// protected override void OnBeginEdit() { + base.OnBeginEdit(); + _added.Clear(); _removed.Clear(); } @@ -68,6 +70,8 @@ protected override void OnBeginEdit() /// protected override void OnCancelEdit() { + base.OnCancelEdit(); + foreach (var label in _added) { CustomLabels.Remove(label); @@ -85,6 +89,8 @@ protected override void OnCancelEdit() /// protected override void OnEndEdit() { + base.OnEndEdit(); + _added.Clear(); _removed.Clear(); @@ -108,13 +114,13 @@ private void AddLabelCommandOnExecute() private void RemoveLabelCommandOnExecute(CustomLabelViewModel labelViewModel) { - BeginEdit(); - if (!_dialogService.ShowConfirmationDialog(ConfirmationDialogType.DeleteLabel, labelViewModel.Name)) { return; } + BeginEdit(); + CustomLabels.Remove(labelViewModel); // Only add to removed if not a new label From a77d391f505709f71f2414b77d79c16f8d5b778d Mon Sep 17 00:00:00 2001 From: Brandon Chong Date: Mon, 15 Jul 2019 11:06:45 -0700 Subject: [PATCH 09/10] Add better unhandled exception detection --- src/AudioBand/AudioBand.csproj | 9 +++++ src/AudioBand/Deskband.cs | 38 ++++++++++++++++--- src/AudioBand/GlobalSettings.Designer.cs | 38 +++++++++++++++++++ src/AudioBand/GlobalSettings.settings | 9 +++++ .../ValueConverters/PointConverter.cs | 15 ++++++-- src/AudioBand/app.config | 22 +++++++++++ 6 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 src/AudioBand/GlobalSettings.Designer.cs create mode 100644 src/AudioBand/GlobalSettings.settings diff --git a/src/AudioBand/AudioBand.csproj b/src/AudioBand/AudioBand.csproj index b60a19ff..31778fc9 100644 --- a/src/AudioBand/AudioBand.csproj +++ b/src/AudioBand/AudioBand.csproj @@ -96,6 +96,11 @@ + + True + True + GlobalSettings.settings + @@ -306,6 +311,10 @@ Designer + + SettingsSingleFileGenerator + GlobalSettings.Designer.cs + PreserveNewest diff --git a/src/AudioBand/Deskband.cs b/src/AudioBand/Deskband.cs index 60509f52..537c6b62 100644 --- a/src/AudioBand/Deskband.cs +++ b/src/AudioBand/Deskband.cs @@ -16,7 +16,6 @@ using CSDeskBand; using NLog; using SimpleInjector; -using SimpleInjector.Advanced; namespace AudioBand { @@ -39,6 +38,7 @@ public class Deskband : CSDeskBandWpf private AudioBandToolbar _audioBandToolbar; private Container _container; private Window _settingsWindow; + private ILogger _logger; /// /// Initializes a new instance of the class. @@ -55,11 +55,13 @@ public Deskband() Options.HorizontalSize = initialSize; Options.MinHorizontalSize = initialSize; AudioBandLogManager.Initialize(); - var logger = AudioBandLogManager.GetLogger("AudioBand"); - logger.Info("Starting AudioBand. Version: {version}, OS: {os}", GetType().Assembly.GetCustomAttribute().InformationalVersion, Environment.OSVersion); + _logger = AudioBandLogManager.GetLogger("AudioBand"); + _logger.Info("Starting AudioBand. Version: {version}, OS: {os}", GetType().Assembly.GetCustomAttribute().InformationalVersion, Environment.OSVersion); - AppDomain.CurrentDomain.UnhandledException += (sender, args) => AudioBandLogManager.GetLogger("AudioBand").Error((Exception)args.ExceptionObject, "Unhandled Exception"); - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + StartupCheck(); + + AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException; + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve; ConfigureDependencies(); @@ -81,7 +83,7 @@ protected override void DeskbandOnClosed() } // Problem with late binding. Fuslogvw shows its not probing the original location. - private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + private static Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args) { // name is in this format Xceed.Wpf.Toolkit, Version=3.4.0.0, Culture=neutral, PublicKeyToken=3e4669d2f30244f4 var asmName = args.Name.Split(',')[0]; @@ -94,6 +96,14 @@ private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEven return File.Exists(filename) ? Assembly.LoadFrom(filename) : null; } + private static void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs args) + { + GlobalSettings.Default.UnhandledException = true; + GlobalSettings.Default.Save(); + + AudioBandLogManager.GetLogger("AudioBand").Error((Exception)args.ExceptionObject, "Unhandled Exception"); + } + private void ConfigureDependencies() { try @@ -139,5 +149,21 @@ private void FocusCaptured(FocusChangedMessage msg) UpdateFocus(true); } } + + private void StartupCheck() + { + if (!GlobalSettings.Default.UnhandledException) + { + return; + } + + // Unhandled exception from last run. Prevent audioband from starting in case there is a crash loop. + _logger.Info("Startup prevented due to previous unhandled exception. Open audioband again to ignore."); + GlobalSettings.Default.UnhandledException = false; + GlobalSettings.Default.Save(); + + // Exception should make explorer remove the deskband from being automatically started. + throw new Exception("Startup prevented due to previous unhandled exception."); + } } } diff --git a/src/AudioBand/GlobalSettings.Designer.cs b/src/AudioBand/GlobalSettings.Designer.cs new file mode 100644 index 00000000..24cb5d28 --- /dev/null +++ b/src/AudioBand/GlobalSettings.Designer.cs @@ -0,0 +1,38 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AudioBand { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.1.0.0")] + internal sealed partial class GlobalSettings : global::System.Configuration.ApplicationSettingsBase { + + private static GlobalSettings defaultInstance = ((GlobalSettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new GlobalSettings()))); + + public static GlobalSettings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool UnhandledException { + get { + return ((bool)(this["UnhandledException"])); + } + set { + this["UnhandledException"] = value; + } + } + } +} diff --git a/src/AudioBand/GlobalSettings.settings b/src/AudioBand/GlobalSettings.settings new file mode 100644 index 00000000..be549678 --- /dev/null +++ b/src/AudioBand/GlobalSettings.settings @@ -0,0 +1,9 @@ + + + + + + False + + + \ No newline at end of file diff --git a/src/AudioBand/ValueConverters/PointConverter.cs b/src/AudioBand/ValueConverters/PointConverter.cs index d461cdc3..0af10920 100644 --- a/src/AudioBand/ValueConverters/PointConverter.cs +++ b/src/AudioBand/ValueConverters/PointConverter.cs @@ -19,14 +19,21 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur return DependencyProperty.UnsetValue; } - if (values.Any(v => v == DependencyProperty.UnsetValue)) + if (values.Any(v => v == DependencyProperty.UnsetValue || v == null)) { return DependencyProperty.UnsetValue; } - var x = System.Convert.ToDouble(values[0]); - var y = System.Convert.ToDouble(values[1]); - return new Point(x, y); + try + { + var x = System.Convert.ToDouble(values[0]); + var y = System.Convert.ToDouble(values[1]); + return new Point(x, y); + } + catch + { + return DependencyProperty.UnsetValue; + } } /// diff --git a/src/AudioBand/app.config b/src/AudioBand/app.config index 74f5bf7e..71d88d87 100644 --- a/src/AudioBand/app.config +++ b/src/AudioBand/app.config @@ -1,5 +1,13 @@  + + +
+ + +
+ + @@ -12,4 +20,18 @@ + + + + False + + + + + + + False + + + \ No newline at end of file From 02e8f6675b0d15ebd5ff61665ccb901cf237917b Mon Sep 17 00:00:00 2001 From: Brandon Chong Date: Mon, 15 Jul 2019 14:06:43 -0700 Subject: [PATCH 10/10] Update WM_DPICHANGED message handling --- src/AudioBand/Behaviors/DpiScaling.cs | 35 +++++++++++++++++++++------ src/AudioBand/NativeMethods.cs | 17 +++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/AudioBand/Behaviors/DpiScaling.cs b/src/AudioBand/Behaviors/DpiScaling.cs index 7e71484d..183ace78 100644 --- a/src/AudioBand/Behaviors/DpiScaling.cs +++ b/src/AudioBand/Behaviors/DpiScaling.cs @@ -1,9 +1,11 @@ using System; using System.Diagnostics; +using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interactivity; using System.Windows.Interop; using System.Windows.Media; +using System.Windows.Threading; namespace AudioBand.Behaviors { @@ -108,24 +110,43 @@ private IntPtr HwndSourceHook(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam switch (msg) { case WM_DPICHANGED: - UpdateDpi(HiWord(wparam)); - + var newDpi = HiWord(wparam); + if (AssociatedObject is Window window) + { + UpdateDpi(newDpi, false); + var suggestedRect = Marshal.PtrToStructure(lparam); + window.Left = suggestedRect.Left; + window.Top = suggestedRect.Top; + window.Width = suggestedRect.Right - suggestedRect.Left; + window.Height = suggestedRect.Bottom - suggestedRect.Top; + } + else + { + UpdateDpi(newDpi); + } + + handled = true; break; case WM_DPICHANGED_AFTERPARENT: + // Used for the toolbar since we don't receive WM_DPICHANGED messages there. UpdateDpi(GetParentWindowDpi(AssociatedObject)); + handled = true; break; } return IntPtr.Zero; } - private void UpdateDpi(double newDpi) + private void UpdateDpi(double newDpi, bool updateSize = true) { - var dpiScale = newDpi / CurrentDpi; - CurrentDpi = newDpi; + if (updateSize) + { + var dpiScale = newDpi / CurrentDpi; + AssociatedObject.Width *= dpiScale; + AssociatedObject.Height *= dpiScale; + } - AssociatedObject.Width *= dpiScale; - AssociatedObject.Height *= dpiScale; + CurrentDpi = newDpi; if (VisualTreeHelper.GetChildrenCount(AssociatedObject) == 0) { diff --git a/src/AudioBand/NativeMethods.cs b/src/AudioBand/NativeMethods.cs index 056e5a4f..9b29d323 100644 --- a/src/AudioBand/NativeMethods.cs +++ b/src/AudioBand/NativeMethods.cs @@ -195,6 +195,23 @@ internal struct AccentPolicy public uint GradientColor; public int AnimationId; } + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + + public RECT(int left, int top, int right, int bottom) + { + Left = left; + Top = top; + Right = right; + Bottom = bottom; + } + } } #pragma warning restore }