From 3b8ecedeadf1b5dd061a7341f1e9cc7a046f759e Mon Sep 17 00:00:00 2001 From: Fotis Gimian Date: Mon, 27 Jan 2025 12:50:17 +1100 Subject: [PATCH] Switch back to TOML for the configuration file. --- README.md | 12 +- TotalMixVC.iss | 2 +- config.sample.json | 91 ------- config.sample.toml | 81 +++++++ .../IPAddressConverterTests.cs | 47 ---- .../NonNegativeDoubleConverterTests.cs | 61 ----- .../PortIntegerConverterTests.cs | 62 ----- .../PositiveDoubleConverterTests.cs | 61 ----- .../SolidColorBrushConverterTests.cs | 53 ---- src/TotalMixVC.Tests/ConfigTests.cs | 182 ++++++++++++++ src/TotalMixVC/App.xaml.cs | 228 +++++++++--------- src/TotalMixVC/Communicator/ISender.cs | 6 +- src/TotalMixVC/Communicator/Sender.cs | 7 +- src/TotalMixVC/Communicator/VolumeManager.cs | 13 +- src/TotalMixVC/Configuration/Config.cs | 46 +++- .../Converters/IPAddressConverter.cs | 46 ---- .../Converters/NonNegativeDoubleConverter.cs | 53 ---- .../Converters/PortIntegerConverter.cs | 55 ----- .../Converters/PositiveDoubleConverter.cs | 53 ---- .../Converters/SolidColorBrushConverter.cs | 54 ----- src/TotalMixVC/Configuration/Interface.cs | 38 ++- src/TotalMixVC/Configuration/Osc.cs | 29 +-- src/TotalMixVC/Configuration/Theme.cs | 80 +++--- src/TotalMixVC/Configuration/Volume.cs | 29 +-- src/TotalMixVC/Hotkeys/GlobalHotKeyManager.cs | 4 +- src/TotalMixVC/TotalMixVC.csproj | 1 + src/TotalMixVC/VolumeIndicator.xaml.cs | 42 ++-- 27 files changed, 554 insertions(+), 882 deletions(-) delete mode 100644 config.sample.json create mode 100644 config.sample.toml delete mode 100644 src/TotalMixVC.Tests/ConfigConverters/IPAddressConverterTests.cs delete mode 100644 src/TotalMixVC.Tests/ConfigConverters/NonNegativeDoubleConverterTests.cs delete mode 100644 src/TotalMixVC.Tests/ConfigConverters/PortIntegerConverterTests.cs delete mode 100644 src/TotalMixVC.Tests/ConfigConverters/PositiveDoubleConverterTests.cs delete mode 100644 src/TotalMixVC.Tests/ConfigConverters/SolidColorBrushConverterTests.cs create mode 100644 src/TotalMixVC.Tests/ConfigTests.cs delete mode 100644 src/TotalMixVC/Configuration/Converters/IPAddressConverter.cs delete mode 100644 src/TotalMixVC/Configuration/Converters/NonNegativeDoubleConverter.cs delete mode 100644 src/TotalMixVC/Configuration/Converters/PortIntegerConverter.cs delete mode 100644 src/TotalMixVC/Configuration/Converters/PositiveDoubleConverter.cs delete mode 100644 src/TotalMixVC/Configuration/Converters/SolidColorBrushConverter.cs diff --git a/README.md b/README.md index 78e673e..9a24a33 100644 --- a/README.md +++ b/README.md @@ -69,20 +69,16 @@ a configuration file. Start by browsing to `%APPDATA%` in Windows Explorer and creating a directory named "TotalMix Volume Control". Now download the -[sample configuration file](https://github.com/fgimian/totalmix-volume-control/blob/main/config.sample.json), -rename it to "config.json" and place it in the directory you created. +[sample configuration file](https://github.com/fgimian/totalmix-volume-control/blob/main/config.sample.toml), +rename it to "config.toml" and place it in the directory you created. You may open this file in any text editor and read the included instructions to configure the application. All the pre-filled values you see in the sample configuration are optional and represent the application defaults. -Despite being a JSON file, TotalMix Volume Control supports comments and trailing commas in the -configuration (as per the sample). - You may live-reload your updated configuration by right-clicking on the TotalMix Volume Control -tray icon and selecting "Reload config" which will update all settings except those related to -OSC (i.e. hostnames and ports). You will need to restart the application if you wish to change -OSC settings. +tray icon and selecting "Reload config" which will update all settings except the incoming OSC +endpoint which requires an application restart to be changed. ## Building from Source diff --git a/TotalMixVC.iss b/TotalMixVC.iss index f491045..b2bfe6e 100644 --- a/TotalMixVC.iss +++ b/TotalMixVC.iss @@ -1,5 +1,5 @@ ; The version needs to be passed explicitly via /DAppVersion=0.1.0 as GitVersion is used to -; determine the release version. Alternatively, the option below may be uncommented if required. +; determine the release version. Alternatively, the option below may be uncommented if required. ; #define AppVersion "1.0" ; The build configuration needs to be passed explicitly via /DAppBuildConfiguration=Release or diff --git a/config.sample.json b/config.sample.json deleted file mode 100644 index cc705d6..0000000 --- a/config.sample.json +++ /dev/null @@ -1,91 +0,0 @@ -// TotalMix Volume Control sample configuration file. -// -// All the pre-filled values you see in this file are optional and represent the application -// defaults. This JSON file supports comments and trailing commas. -// -// Colors may be specified as regular color names such as "blue", along with hex colors with RGB -// or ARGB components (note that the alpha channel needs to be first if you ant transparency). -{ - "osc": { - // The hostname port that TotalMix Volume Control should send to. TotalMixFX listens on - // 0.0.0.0 so the hostname may be any IP address on your network. Generally 127.0.0.1 - // (localhost) is recommended. The port should be set to match the "Port incoming" - // setting in TotalMixFX. - "outgoing_hostname": "127.0.0.1", - "outgoing_port": 7001, - - // The hostname and port that TotalMix Volume Control should receive from. This should be - // set to match the "Port outgoing" setting in TotalMixFX. - "incoming_hostname": "127.0.0.1", - "incoming_port": 9001, - }, - - "volume": { - // Whether to use dB units for volume increments and max values. See below for valid - // values when decibels are used. - "use_decibels": false, - - // The amount that the volume should be increased when using the volume keys. - // - Percentage: Up to max of 0.10 which will increase the volume by 10%. - // - Decibels: Up to max of 3.0 dB in multiples of 0.5 (e.g. 0.5, 1.0, 1.5, etc.). - "increment": 0.02, - - // The amount that the volume should be increased when using the volume keys and holding - // shift down. - // - Percentage: Up to a max of 0.05 which will increase the volume by 5%. - // - Decibels: Up to max of 1.5 dB in multiples of 0.5 (i.e.. 0.5, 1.0 or 1.5). When using - // decibels, it is generally a good idea to ensure the fine increment is a - // multiple of increment. - "fine_increment": 0.01, - - // The maximum volume to send. - // - Percentage: Up to a max of 1.0 which is 100% volume. - // - Decibels: Up to max of 6.0 dB. - "max": 1.0, - }, - - "theme": { - // The main widget and tray tooltip background corner rounding and color. - "background_rounding": 10.0, - "background_color": "#e21e2328", - - // The TotalMix Volume heading text colors. - "heading_totalmix_color": "#ffffff", - "heading_volume_color": "#e06464", - - // The main decibel volume readout text colors. - "volume_readout_color_normal": "#ffffff", - "volume_readout_color_dimmed": "#ffa500", - - // The horizontal volume bar colors. - "volume_bar_background_color": "#333333", - "volume_bar_foreground_color_normal": "#999999", - "volume_bar_foreground_color_dimmed": "#996500", - - // The tray tooltip text message color. - "tray_tooltip_message_color": "#ffffff", - }, - - "interface": { - // Scale the interface by a particular factor (e.g. 2.0 will be twice as large). - "scaling": 1.0, - - // The position offset from the top left corner to display the widget at. - "position_offset": 40.0, - - // The amount of time in seconds to display the widget upon hitting the volume keys before - // beginning the fade out animation. - "hide_delay": 2.0, - - // The duration of the fade out animation in seconds. - "fade_out_time": 1.0, - - // Whether to show the indicator any time the volume is changed via the device or any other - // external means (e.g. an RME ARC controller). - // - // Please note that in my testing RME devices send volume changes at infrequent but random - // times, even when the volume has not been changed intentionally. This is ultimately why - // this setting is off by default. - "show_remote_volume_changes": false, - }, -} diff --git a/config.sample.toml b/config.sample.toml new file mode 100644 index 0000000..e3e1594 --- /dev/null +++ b/config.sample.toml @@ -0,0 +1,81 @@ +# TotalMix Volume Control sample configuration file. +# +# All the pre-filled values you see in this file are optional and represent the application +# defaults. +# +# Colors may be specified as regular color names such as "blue", along with hex colors with RGB or +# ARGB components (note that the alpha channel needs to be first if you ant transparency). + +[osc] +# The address and port that TotalMix Volume Control should send to. TotalMixFX listens on 0.0.0.0 +# so the address may be any IP address on your network. Generally 127.0.0.1 (localhost) is +# recommended. The port should be set to match the "Port incoming" setting in TotalMixFX. +outgoing_endpoint = "127.0.0.1:7001" + +# The address and port that TotalMix Volume Control should receive from. This should be set to +# match the "Port outgoing" setting in TotalMixFX. +incoming_endpoint = "127.0.0.1:9001" + +[volume] +# Whether to use dB units for volume increments and max values. See below for valid values when +# decibels are used. +use_decibels = false + +# The amount that the volume should be increased when using the volume keys. +# - Percentage: Up to max of 0.10 which will increase the volume by 10%. +# - Decibels: Up to max of 3.0 dB in multiples of 0.5 (e.g. 0.5, 1.0, 1.5, etc.). +increment = 0.02 + +# The amount that the volume should be increased when using the volume keys and holding shift down. +# - Percentage: Up to a max of 0.05 which will increase the volume by 5%. +# - Decibels: Up to max of 1.5 dB in multiples of 0.5 (i.e.. 0.5, 1.0 or 1.5). When using decibels, +# it is generally a good idea to ensure the fine increment is a multiple of increment. +fine_increment = 0.01 + +# The maximum volume to send. +# - Percentage: Up to a max of 1.0 which is 100% volume. +# - Decibels: Up to max of 6.0 dB. +max = 1.0 + +[theme] +# The main widget and tray tooltip background corner rounding and color. +background_rounding = 10.0 +background_color = "#e21e2328" + +# The TotalMix Volume heading text colors. +heading_totalmix_color = "#ffffff" +heading_volume_color = "#e06464" + +# The main decibel volume readout text colors. +volume_readout_color_normal = "#ffffff" +volume_readout_color_dimmed = "#ffa500" + +# The horizontal volume bar colors. +volume_bar_background_color = "#333333" +volume_bar_foreground_color_normal = "#999999" +volume_bar_foreground_color_dimmed = "#996500" + +# The tray tooltip text message color. +tray_tooltip_message_color = "#ffffff" + +[interface] +# Scale the interface by a particular factor (e.g. 2.0 will be twice as large). +scaling = 1.0 + +# The position offset from the top left corner to display the widget at. +position_offset = 40.0 + +# The amount of time in seconds to display the widget upon hitting the volume keys before +# beginning the fade out animation. +hide_delay = 2.0 + +# The duration of the fade out animation in seconds. +fade_out_time = 1.0 + +# Whether to show the indicator any time the volume is changed via the device or any other +# external means (e.g. an RME ARC controller). +# +# Please note that in my testing RME devices send volume changes at infrequent but random times, +# even when the volume has not been changed intentionally. This is ultimately why this setting is +# off by default. +show_remote_volume_changes = false diff --git a/src/TotalMixVC.Tests/ConfigConverters/IPAddressConverterTests.cs b/src/TotalMixVC.Tests/ConfigConverters/IPAddressConverterTests.cs deleted file mode 100644 index a809d1a..0000000 --- a/src/TotalMixVC.Tests/ConfigConverters/IPAddressConverterTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Net; -using System.Text.Json; -using System.Text.Json.Serialization; -using TotalMixVC.Configuration.Converters; -using Xunit; - -namespace TotalMixVC.Tests.ConfigConverters; - -public class IPAddressConverterTests -{ - [Theory] - [InlineData("127.0.0.1")] - [InlineData("172.16.0.1")] - [InlineData("192.168.0.1")] - public void Read_Valid_ConvertsWithoutError(string address) - { - var json = $$"""{"Address": "{{address}}"}"""; - var model = JsonSerializer.Deserialize(json); - Assert.Equal(address, model?.Address.ToString()); - } - - [Theory] - [InlineData("abc127.0.0.1")] - [InlineData("172.16abc.0.1")] - [InlineData("example.com")] - [InlineData("osc.example.com")] - public void Read_Invalid_ThrowsException(string address) - { - var json = $$"""{"Address": "{{address}}"}"""; - Assert.Throws(() => JsonSerializer.Deserialize(json)); - } - - [Fact] - public void Write_Valid_ConvertsWithoutError() - { - var model = new Model() { Address = IPAddress.Loopback }; - var json = JsonSerializer.Serialize(model); - /*lang=json,strict*/ - Assert.Equal("""{"Address":"127.0.0.1"}""", json); - } - - internal sealed record Model - { - [JsonConverter(typeof(IPAddressConverter))] - public required IPAddress Address { get; init; } - } -} diff --git a/src/TotalMixVC.Tests/ConfigConverters/NonNegativeDoubleConverterTests.cs b/src/TotalMixVC.Tests/ConfigConverters/NonNegativeDoubleConverterTests.cs deleted file mode 100644 index cc875b8..0000000 --- a/src/TotalMixVC.Tests/ConfigConverters/NonNegativeDoubleConverterTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using TotalMixVC.Configuration.Converters; -using Xunit; - -namespace TotalMixVC.Tests.ConfigConverters; - -public class NonNegativeDoubleConverterTests -{ - [Theory] - [InlineData(0.0)] - [InlineData(0.5)] - [InlineData(1.0)] - [InlineData(10.0)] - public void Read_Valid_ConvertsWithoutError(double value) - { - var json = $$"""{"Value": {{value}}}"""; - var model = JsonSerializer.Deserialize(json); - Assert.Equal(value, model?.Value); - } - - [Theory] - [InlineData(-0.01)] - [InlineData(-0.5)] - [InlineData(-1.0)] - [InlineData(-10.5)] - public void Read_Invalid_ThrowsException(double value) - { - var json = $$"""{"Value": {{value}}}"""; - Assert.Throws(() => JsonSerializer.Deserialize(json)); - } - - [Theory] - [InlineData(0.0)] - [InlineData(0.5)] - [InlineData(1.0)] - [InlineData(10.0)] - public void Write_Valid_ConvertsWithoutError(double value) - { - var model = new Model() { Value = value }; - var json = JsonSerializer.Serialize(model); - Assert.Equal($$"""{"Value":{{value}}}""", json); - } - - [Theory] - [InlineData(-0.01)] - [InlineData(-0.5)] - [InlineData(-1.0)] - [InlineData(-10.5)] - public void Write_Invalid_ThrowsException(double value) - { - var model = new Model() { Value = value }; - Assert.Throws(() => JsonSerializer.Serialize(model)); - } - - internal sealed record Model - { - [JsonConverter(typeof(NonNegativeDoubleConverter))] - public required double Value { get; init; } - } -} diff --git a/src/TotalMixVC.Tests/ConfigConverters/PortIntegerConverterTests.cs b/src/TotalMixVC.Tests/ConfigConverters/PortIntegerConverterTests.cs deleted file mode 100644 index 9be1628..0000000 --- a/src/TotalMixVC.Tests/ConfigConverters/PortIntegerConverterTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Net; -using System.Text.Json; -using System.Text.Json.Serialization; -using TotalMixVC.Configuration.Converters; -using Xunit; - -namespace TotalMixVC.Tests.ConfigConverters; - -public class PortIntegerConverterTests -{ - [Theory] - [InlineData(IPEndPoint.MinPort)] - [InlineData(IPEndPoint.MaxPort)] - [InlineData(9000)] - [InlineData(7000)] - public void Read_Valid_ConvertsWithoutError(int port) - { - var json = $$"""{"Port": {{port}}}"""; - var model = JsonSerializer.Deserialize(json); - Assert.Equal(port, model?.Port); - } - - [Theory] - [InlineData(IPEndPoint.MinPort - 1)] - [InlineData(IPEndPoint.MaxPort + 1)] - [InlineData(100_000)] - [InlineData(-50)] - public void Read_Invalid_ThrowsException(int port) - { - var json = $$"""{"Port": {{port}}}"""; - Assert.Throws(() => JsonSerializer.Deserialize(json)); - } - - [Theory] - [InlineData(IPEndPoint.MinPort)] - [InlineData(IPEndPoint.MaxPort)] - [InlineData(9000)] - [InlineData(7000)] - public void Write_Valid_ConvertsWithoutError(int port) - { - var model = new Model() { Port = port }; - var json = JsonSerializer.Serialize(model); - Assert.Equal($$"""{"Port":{{port}}}""", json); - } - - [Theory] - [InlineData(IPEndPoint.MinPort - 1)] - [InlineData(IPEndPoint.MaxPort + 1)] - [InlineData(100_000)] - [InlineData(-50)] - public void Write_Invalid_ThrowsException(int port) - { - var model = new Model() { Port = port }; - Assert.Throws(() => JsonSerializer.Serialize(model)); - } - - internal sealed record Model - { - [JsonConverter(typeof(PortIntegerConverter))] - public required int Port { get; init; } - } -} diff --git a/src/TotalMixVC.Tests/ConfigConverters/PositiveDoubleConverterTests.cs b/src/TotalMixVC.Tests/ConfigConverters/PositiveDoubleConverterTests.cs deleted file mode 100644 index 32bd2f7..0000000 --- a/src/TotalMixVC.Tests/ConfigConverters/PositiveDoubleConverterTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using TotalMixVC.Configuration.Converters; -using Xunit; - -namespace TotalMixVC.Tests.ConfigConverters; - -public class PositiveDoubleConverterTests -{ - [Theory] - [InlineData(0.01)] - [InlineData(0.5)] - [InlineData(1.0)] - [InlineData(10.0)] - public void Read_Valid_ConvertsWithoutError(double value) - { - var json = $$"""{"Value": {{value}}}"""; - var model = JsonSerializer.Deserialize(json); - Assert.Equal(value, model?.Value); - } - - [Theory] - [InlineData(0.0)] - [InlineData(-0.5)] - [InlineData(-1.0)] - [InlineData(-10.5)] - public void Read_Invalid_ThrowsException(double value) - { - var json = $$"""{"Value": {{value}}}"""; - Assert.Throws(() => JsonSerializer.Deserialize(json)); - } - - [Theory] - [InlineData(0.01)] - [InlineData(0.5)] - [InlineData(1.0)] - [InlineData(10.0)] - public void Write_Valid_ConvertsWithoutError(double value) - { - var model = new Model() { Value = value }; - var json = JsonSerializer.Serialize(model); - Assert.Equal($$"""{"Value":{{value}}}""", json); - } - - [Theory] - [InlineData(0.0)] - [InlineData(-0.5)] - [InlineData(-1.0)] - [InlineData(-10.5)] - public void Write_Invalid_ThrowsException(double value) - { - var model = new Model() { Value = value }; - Assert.Throws(() => JsonSerializer.Serialize(model)); - } - - internal sealed record Model - { - [JsonConverter(typeof(PositiveDoubleConverter))] - public required double Value { get; init; } - } -} diff --git a/src/TotalMixVC.Tests/ConfigConverters/SolidColorBrushConverterTests.cs b/src/TotalMixVC.Tests/ConfigConverters/SolidColorBrushConverterTests.cs deleted file mode 100644 index 50a10c5..0000000 --- a/src/TotalMixVC.Tests/ConfigConverters/SolidColorBrushConverterTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Windows.Media; -using TotalMixVC.Configuration.Converters; -using Xunit; - -namespace TotalMixVC.Tests.ConfigConverters; - -public class SolidColorBrushConverterTests -{ - [Theory] - [InlineData("#fff", "#FFFFFFFF")] - [InlineData("#442266", "#FF442266")] - [InlineData("#33FFaa99", "#33FFAA99")] - [InlineData("red", "#FFFF0000")] - [InlineData("BLUE", "#FF0000FF")] - public void Read_Valid_ConvertsWithoutError(string color, string expected) - { - var json = $$"""{"Color": "{{color}}"}"""; - var model = JsonSerializer.Deserialize(json); - Assert.Equal(expected, model?.Color.ToString(CultureInfo.InvariantCulture)); - } - - [Theory] - [InlineData("invalid")] - [InlineData("ffffff")] - [InlineData("#yyxxzz")] - public void Read_Invalid_ThrowsException(string color) - { - var json = $$"""{"Color": "{{color}}"}"""; - Assert.Throws(() => JsonSerializer.Deserialize(json)); - } - - [Theory] - [InlineData("#ff00ff", "#ff00ff")] - [InlineData("#33ff00ff", "#33ff00ff")] - [InlineData("#fff", "#ffffff")] - public void Write_Valid_ConvertsWithoutError(string color, string expected) - { - var converter = new BrushConverter(); - var model = new Model() { Color = (SolidColorBrush)converter.ConvertFromString(color)! }; - var json = JsonSerializer.Serialize(model); - - Assert.Equal($$"""{"Color":"{{expected}}"}""", json); - } - - internal sealed record Model - { - [JsonConverter(typeof(SolidColorBrushConverter))] - public required SolidColorBrush Color { get; init; } - } -} diff --git a/src/TotalMixVC.Tests/ConfigTests.cs b/src/TotalMixVC.Tests/ConfigTests.cs new file mode 100644 index 0000000..81077a9 --- /dev/null +++ b/src/TotalMixVC.Tests/ConfigTests.cs @@ -0,0 +1,182 @@ +using System.Net; +using System.Windows.Media; +using TotalMixVC.Configuration; +using Xunit; + +namespace TotalMixVC.Tests; + +public sealed class ConfigTests +{ + [Fact] + public void TryFromToml_ValidConfiguration_LoadsAllProperties() + { + var isValid = Config.TryFromToml( + """ + [osc] + outgoing_endpoint = "127.0.0.1:7002" + incoming_endpoint = "127.0.0.1:9002" + + [volume] + use_decibels = true + increment = 1.0 + fine_increment = 0.5 + max = 0.0 + + [theme] + background_rounding = 5.0 + background_color = "#1e4328" + heading_totalmix_color = "#eeeeee" + heading_volume_color = "#e05454" + volume_readout_color_normal = "#eeeeee" + volume_readout_color_dimmed = "#eefa50" + volume_bar_background_color = "#222222" + volume_bar_foreground_color_normal = "#888888" + volume_bar_foreground_color_dimmed = "#886500" + tray_tooltip_message_color = "#eeeeee" + + [interface] + scaling = 1.1 + position_offset = 45.0 + hide_delay = 3.0 + fade_out_time = 0.5 + show_remote_volume_changes = true + """, + out var config, + out var diagnostics + ); + + var expectedConfig = new Config() + { + Osc = new Osc() + { + OutgoingEndPoint = new IPEndPoint(IPAddress.Loopback, 7002), + IncomingEndPoint = new IPEndPoint(IPAddress.Loopback, 9002), + }, + Volume = new Volume() + { + UseDecibels = true, + Increment = 1.0f, + FineIncrement = 0.5f, + Max = 0.0f, + }, + Theme = new Theme() + { + BackgroundRounding = 5.0, + BackgroundColor = Color.FromRgb(0x1e, 0x43, 0x28), + HeadingTotalmixColor = Color.FromRgb(0xee, 0xee, 0xee), + HeadingVolumeColor = Color.FromRgb(0xe0, 0x54, 0x54), + VolumeReadoutColorNormal = Color.FromRgb(0xee, 0xee, 0xee), + VolumeReadoutColorDimmed = Color.FromRgb(0xee, 0xfa, 0x50), + VolumeBarBackgroundColor = Color.FromRgb(0x22, 0x22, 0x22), + VolumeBarForegroundColorNormal = Color.FromRgb(0x88, 0x88, 0x88), + VolumeBarForegroundColorDimmed = Color.FromRgb(0x88, 0x65, 0x00), + TrayTooltipMessageColor = Color.FromRgb(0xee, 0xee, 0xee), + }, + Interface = new Interface() + { + Scaling = 1.1, + PositionOffset = 45.0, + HideDelay = 3.0, + FadeOutTime = 0.5, + ShowRemoteVolumeChanges = true, + }, + }; + + Assert.True(isValid); + Assert.NotNull(config); + Assert.NotNull(diagnostics); + Assert.Empty(diagnostics); + Assert.Equal(expectedConfig, config); + } + + [Fact] + public void TryFromToml_InvalidColor_SkipsLoadingProperty() + { + var isValid = Config.TryFromToml( + """ + [theme] + background_color = "#1e4328" + heading_totalmix_color = "wow" + """, + out var config, + out var diagnostics + ); + + var expectedConfig = new Config() + { + Theme = new Theme() { BackgroundColor = Color.FromRgb(0x1e, 0x43, 0x28) }, + }; + + Assert.False(isValid); + Assert.NotNull(config); + Assert.NotNull(diagnostics); + Assert.Equal(2, diagnostics.Count); + Assert.Equal(expectedConfig, config); + } + + [Fact] + public void TryFromToml_InvalidIPEndPoint_SkipsLoadingProperty() + { + var isValid = Config.TryFromToml( + """ + [osc] + outgoing_endpoint = "127.0.0.1:7002" + incoming_endpoint = "oopsies" + """, + out var config, + out var diagnostics + ); + + var expectedConfig = new Config() + { + Osc = new Osc() + { + OutgoingEndPoint = new IPEndPoint(IPAddress.Loopback, 7002), + IncomingEndPoint = new IPEndPoint(IPAddress.Loopback, 9001), + }, + }; + + Assert.False(isValid); + Assert.NotNull(config); + Assert.NotNull(diagnostics); + Assert.Equal(2, diagnostics.Count); + Assert.Equal(expectedConfig, config); + } + + [Fact] + public void TryFromToml_InvalidDoubles_ResetsPropertiesToDefaults() + { + var isValid = Config.TryFromToml( + """ + [theme] + background_rounding = -1.0 + + [interface] + scaling = 0.0 + position_offset = -1.0 + hide_delay = -10.0 + fade_out_time = -5.0 + """, + out var config, + out var diagnostics + ); + + var expectedConfig = new Config() + { + Theme = new Theme() { BackgroundRounding = 0.0 }, + Interface = new Interface() + { + Scaling = double.Epsilon, + PositionOffset = 0.0, + HideDelay = double.Epsilon, + FadeOutTime = 0.0, + }, + }; + + Assert.True(isValid); + Assert.NotNull(config); + Assert.NotNull(diagnostics); + Assert.Empty(diagnostics); + Assert.Equal(expectedConfig, config); + } +} diff --git a/src/TotalMixVC/App.xaml.cs b/src/TotalMixVC/App.xaml.cs index 8d98af3..27ff979 100644 --- a/src/TotalMixVC/App.xaml.cs +++ b/src/TotalMixVC/App.xaml.cs @@ -3,11 +3,9 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Net; using System.Net.Sockets; using System.Reflection; using System.Text; -using System.Text.Json; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -45,16 +43,9 @@ public partial class App : Application, IDisposable private static readonly string s_configPath = Path.Join( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TotalMix Volume Control", - "config.json" + "config.toml" ); - private static readonly JsonSerializerOptions s_jsonConfigSerializerOptions = new() - { - AllowTrailingCommas = true, - ReadCommentHandling = JsonCommentHandling.Skip, - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - }; - private readonly GlobalHotKeyManager _hotKeyManager = new(); // Disable non-nullable field must contain a non-null value when exiting constructor. These @@ -105,41 +96,39 @@ public void About() /// Whether the application is already running with a previous loaded configuration. /// /// Whether or not the config was loaded successfully. - [SuppressMessage( - "Design", - "CA1031:Do not catch general exception types", - Justification = "ReadAllText throws many exception types and this function can't fail." - )] public bool LoadConfig(bool running = false) { - try - { - var configText = File.ReadAllText(s_configPath); - _config = JsonSerializer.Deserialize( - configText, - s_jsonConfigSerializerOptions - )!; - return true; - } - catch (JsonException ex) + var configText = File.ReadAllText(s_configPath); + var isValid = Config.TryFromToml(configText, out var config, out var diagnostics); + + if (!isValid && diagnostics is not null) { var configDescription = running ? "existing" : "default"; - var message = new StringBuilder(); - message.Append( - CultureInfo.InvariantCulture, - $"Unable to parse the config file at {s_configPath}.\n\n{ex.Message}" - ); + if (config is null) + { + message.Append( + CultureInfo.InvariantCulture, + $"Unable to parse the config file at {s_configPath}.\n\n" + ); + } + else + { + message.Append( + CultureInfo.InvariantCulture, + $"Unable to parse one or more more properties from the config file at {s_configPath}.\n\n" + ); + } - if (ex.InnerException is not null) + foreach (var diagnostic in diagnostics) { - message.Append(CultureInfo.InvariantCulture, $" {ex.InnerException.Message}"); + message.Append(CultureInfo.InvariantCulture, $"- {diagnostic}\n"); } message.Append( CultureInfo.InvariantCulture, - $"\n\nThe application will continue with the {configDescription} configuration." + $"\nThe application will continue with the {configDescription} values for affected properties." ); if (running) @@ -161,39 +150,15 @@ public bool LoadConfig(bool running = false) icon: MessageBoxImage.Exclamation ); } - - return false; } - catch (Exception ex) - { - var configDescription = running ? "existing" : "default"; - var message = - $"Unable to load the config file at {s_configPath} " - + $"({ex.InnerException?.Message ?? ex.Message}).\n\nThe application will continue " - + $"with the {configDescription} configuration."; - if (running) - { - MessageBox.Show( - _volumeIndicator, - message, - caption: "Configuration File Error", - button: MessageBoxButton.OK, - icon: MessageBoxImage.Exclamation - ); - } - else - { - MessageBox.Show( - message, - caption: "Configuration File Error", - button: MessageBoxButton.OK, - icon: MessageBoxImage.Exclamation - ); - } - - return false; + if (config is not null) + { + _config = config; + return true; } + + return false; } /// @@ -221,23 +186,9 @@ public void ReloadConfig() return; } - try - { - ConfigureVolumeManager(); - } - catch (ArgumentOutOfRangeException ex) - { - MessageBox.Show( - _volumeIndicator, - $"Unable to configure the volume based on the config file at {s_configPath} " - + $"({ex.InnerException?.Message ?? ex.Message}).\n\nThe application will " - + "continue with the existing configuration.", - caption: "Configuration File Error", - button: MessageBoxButton.OK, - icon: MessageBoxImage.Exclamation - ); - } + _volumeManager.OutgoingEndpoint = _config.Osc.OutgoingEndPoint; + ConfigureVolumeManager(running: true); ConfigureInterface(); ConfigureTheme(); @@ -245,8 +196,8 @@ public void ReloadConfig() MessageBox.Show( _volumeIndicator, - "Configuration has been reloaded successfully. Please note that changes to OSC " - + "settings will require an application restart to take effect.", + "Configuration has been reloaded successfully. Please note that changes to the " + + "incoming OSC endpoint will require an application restart to take effect.", caption: "Configuration Reloaded", button: MessageBoxButton.OK, icon: MessageBoxImage.Information @@ -324,8 +275,8 @@ protected override void OnStartup(StartupEventArgs e) try { _volumeManager = new( - outgoingEP: new IPEndPoint(_config.Osc.OutgoingHostname, _config.Osc.OutgoingPort), - incomingEP: new IPEndPoint(_config.Osc.IncomingHostname, _config.Osc.IncomingPort) + outgoingEP: _config.Osc.OutgoingEndPoint, + incomingEP: _config.Osc.IncomingEndPoint ); } catch (SocketException) @@ -333,8 +284,8 @@ protected override void OnStartup(StartupEventArgs e) MessageBox.Show( "Unable to open a listener to receive events from the device. Please exit any " + "applications that are binding to UDP address " - + $"{_config.Osc.OutgoingHostname}:{_config.Osc.OutgoingPort} and try " - + "again.\n\nThe application will now exit.", + + $"{_config.Osc.OutgoingEndPoint} and try again.\n\nThe application will " + + "now exit.", caption: "Socket Error", button: MessageBoxButton.OK, icon: MessageBoxImage.Exclamation @@ -343,22 +294,7 @@ protected override void OnStartup(StartupEventArgs e) return; } - try - { - ConfigureVolumeManager(); - } - catch (ArgumentOutOfRangeException ex) - { - MessageBox.Show( - _volumeIndicator, - $"Unable to configure the volume based on the config file at {s_configPath} " - + $"({ex.InnerException?.Message ?? ex.Message}).\n\nThe application will " - + "continue with the default configuration.", - caption: "Configuration File Error", - button: MessageBoxButton.OK, - icon: MessageBoxImage.Exclamation - ); - } + ConfigureVolumeManager(running: false); // Start a task that will receive and record volume changes. _volumeReceiveTask = _joinableTaskFactory.RunAsync(ReceiveVolumeAsync); @@ -487,9 +423,9 @@ await _joinableTaskFactory.SwitchToMainThreadAsync( _trayToolTipStatus.Text = string.Format( CultureInfo.InvariantCulture, s_communicationErrorFormatString, - _config.Osc.OutgoingPort, - _config.Osc.IncomingPort, - _config.Osc.IncomingHostname + _config.Osc.OutgoingEndPoint.Port, + _config.Osc.IncomingEndPoint.Port, + _config.Osc.IncomingEndPoint.Address ); _trayIcon.ToolTipText = "TotalMixVC - Unable to connect to your device"; } @@ -595,23 +531,87 @@ private void RegisterHotkeys() ); } - private void ConfigureVolumeManager() + private void ConfigureVolumeManager(bool running = false) { + var exceptions = new List(); + _volumeManager.UseDecibels = _config.Volume.UseDecibels; if (_config.Volume.Increment is float volumeIncrement) { - _volumeManager.VolumeRegularIncrement = volumeIncrement; + try + { + _volumeManager.VolumeRegularIncrement = volumeIncrement; + } + catch (ArgumentOutOfRangeException ex) + { + exceptions.Add(ex); + } } if (_config.Volume.FineIncrement is float fineIncrement) { - _volumeManager.VolumeFineIncrement = fineIncrement; + try + { + _volumeManager.VolumeFineIncrement = fineIncrement; + } + catch (ArgumentOutOfRangeException ex) + { + exceptions.Add(ex); + } } if (_config.Volume.Max is float volumeMax) { - _volumeManager.VolumeMax = volumeMax; + try + { + _volumeManager.VolumeMax = volumeMax; + } + catch (ArgumentOutOfRangeException ex) + { + exceptions.Add(ex); + } + } + + if (exceptions.Count > 0) + { + var configDescription = running ? "existing" : "default"; + var message = new StringBuilder(); + + message.Append( + CultureInfo.InvariantCulture, + $"Unable to configure the volume based on the config file at {s_configPath}.\n\n" + ); + + foreach (var exception in exceptions) + { + message.Append(CultureInfo.InvariantCulture, $"- {exception.Message}\n"); + } + + message.Append( + CultureInfo.InvariantCulture, + $"\nThe application will continue with the {configDescription} values for affected properties." + ); + + if (running) + { + MessageBox.Show( + _volumeIndicator, + message.ToString(), + caption: "Configuration File Error", + button: MessageBoxButton.OK, + icon: MessageBoxImage.Exclamation + ); + } + else + { + MessageBox.Show( + message.ToString(), + caption: "Configuration File Error", + button: MessageBoxButton.OK, + icon: MessageBoxImage.Exclamation + ); + } } } @@ -639,11 +639,13 @@ private void ConfigureTheme() var trayToolTipStatus = (TextBlock) LogicalTreeHelper.FindLogicalNode(trayToolTipBorder, "TrayToolTipStatus"); - trayToolTipBorder.BorderBrush = _config.Theme.BackgroundColor; + trayToolTipBorder.BorderBrush = new SolidColorBrush(_config.Theme.BackgroundColor); trayToolTipBorder.CornerRadius = new CornerRadius(_config.Theme.BackgroundRounding); - trayToolTipPanel.Background = _config.Theme.BackgroundColor; - trayToolTipTitleTotalMix.Foreground = _config.Theme.HeadingTotalmixColor; - trayToolTipTitleVolume.Foreground = _config.Theme.HeadingVolumeColor; - trayToolTipStatus.Foreground = _config.Theme.TrayTooltipMessageColor; + trayToolTipPanel.Background = new SolidColorBrush(_config.Theme.BackgroundColor); + trayToolTipTitleTotalMix.Foreground = new SolidColorBrush( + _config.Theme.HeadingTotalmixColor + ); + trayToolTipTitleVolume.Foreground = new SolidColorBrush(_config.Theme.HeadingVolumeColor); + trayToolTipStatus.Foreground = new SolidColorBrush(_config.Theme.TrayTooltipMessageColor); } } diff --git a/src/TotalMixVC/Communicator/ISender.cs b/src/TotalMixVC/Communicator/ISender.cs index 7834317..1b9a9c9 100644 --- a/src/TotalMixVC/Communicator/ISender.cs +++ b/src/TotalMixVC/Communicator/ISender.cs @@ -1,4 +1,5 @@ -using OscCore; +using System.Net; +using OscCore; namespace TotalMixVC.Communicator; @@ -7,6 +8,9 @@ namespace TotalMixVC.Communicator; /// public interface ISender { + /// Gets or sets the outgoing OSC endpoint to send volume changes to. + public IPEndPoint LocalEP { get; set; } + /// /// Sends an OSC packet to the configured endpoint. /// diff --git a/src/TotalMixVC/Communicator/Sender.cs b/src/TotalMixVC/Communicator/Sender.cs index eb3008c..8320d3d 100644 --- a/src/TotalMixVC/Communicator/Sender.cs +++ b/src/TotalMixVC/Communicator/Sender.cs @@ -17,10 +17,11 @@ public class Sender(IPEndPoint localEP) : ISender, IDisposable { private readonly UdpClient _client = new(); - private readonly IPEndPoint _localEP = localEP; - private bool _disposed; + /// Gets or sets the outgoing OSC endpoint to send volume changes to. + public IPEndPoint LocalEP { get; set; } = localEP; + /// Disposes the current sender. public void Dispose() { @@ -38,7 +39,7 @@ public void Dispose() public Task SendAsync(OscPacket message) { var datagram = message.ToByteArray(); - return _client.SendAsync(datagram, datagram.Length, _localEP); + return _client.SendAsync(datagram, datagram.Length, LocalEP); } /// Disposes the current sender. diff --git a/src/TotalMixVC/Communicator/VolumeManager.cs b/src/TotalMixVC/Communicator/VolumeManager.cs index ce71b8c..e080f60 100644 --- a/src/TotalMixVC/Communicator/VolumeManager.cs +++ b/src/TotalMixVC/Communicator/VolumeManager.cs @@ -42,11 +42,11 @@ public class VolumeManager : IDisposable /// Initializes a new instance of the class. /// /// - /// The outgoing OSC endpoint to send volume changes to. This should be set to the + /// The outgoing OSC endpoint to send volume changes to. This should be set to the /// incoming port in TotalMix settings. /// /// - /// The incoming OSC endpoint to receive volume changes from. This should be set to the + /// The incoming OSC endpoint to receive volume changes from. This should be set to the /// outgoing port in TotalMix settings. /// [ExcludeFromCodeCoverage] @@ -69,6 +69,13 @@ public VolumeManager(ISender sender, IListener listener) _listener = listener; } + /// Gets or sets the outgoing endpoint on the sender. + public IPEndPoint OutgoingEndpoint + { + get => _sender.LocalEP; + set => _sender.LocalEP = value; + } + /// /// Gets the current device volume as a float (with a range of 0.0 to 1.0). /// @@ -265,7 +272,7 @@ public async Task ReceiveVolumeAsync( { // Ping events are sent from the device every around every 1 second, so we only // wait until a given timeout of 5 seconds before giving up and forcing a fresh - // receive request. This ensures that the receiver can detect a device which was + // receive request. This ensures that the receiver can detect a device which was // previous offline. OscPacket packet; using var receiveCancellationTokenSource = new CancellationTokenSource(); diff --git a/src/TotalMixVC/Configuration/Config.cs b/src/TotalMixVC/Configuration/Config.cs index ddc8861..0319a65 100644 --- a/src/TotalMixVC/Configuration/Config.cs +++ b/src/TotalMixVC/Configuration/Config.cs @@ -1,4 +1,9 @@ -namespace TotalMixVC.Configuration; +using System.Net; +using System.Windows.Media; +using Tomlyn; +using Tomlyn.Syntax; + +namespace TotalMixVC.Configuration; /// /// Provides all configurable settings for the application along with suitable defaults. @@ -16,4 +21,43 @@ public record Config /// Gets configuration related the behaviour of the widget user interface. public Interface Interface { get; init; } = new Interface(); + + /// + /// Parses TOML configuration text into a Config instance ensuring appropriate conversions and + /// validation are performed. + /// + /// The text to parse. + /// The output config model. + /// The diagnostics if this method returns false. + /// Whether or not the config was parsed successfully. + public static bool TryFromToml(string text, out Config? config, out DiagnosticsBag? diagnostics) + { + var isValid = Toml.TryToModel( + text, + out config, + out diagnostics, + options: new TomlModelOptions() + { + ConvertToModel = (value, type) => + value switch + { + string color when type == typeof(Color) => (Color) + ColorConverter.ConvertFromString(color)!, + string address when type == typeof(IPEndPoint) => IPEndPoint.Parse(address), + _ => null, + }, + } + ); + + if (config is not null) + { + config.Theme.BackgroundRounding = Math.Max(config.Theme.BackgroundRounding, 0.0); + config.Interface.Scaling = Math.Max(config.Interface.Scaling, double.Epsilon); + config.Interface.PositionOffset = Math.Max(config.Interface.PositionOffset, 0.0); + config.Interface.HideDelay = Math.Max(config.Interface.HideDelay, double.Epsilon); + config.Interface.FadeOutTime = Math.Max(config.Interface.FadeOutTime, 0.0); + } + + return isValid; + } } diff --git a/src/TotalMixVC/Configuration/Converters/IPAddressConverter.cs b/src/TotalMixVC/Configuration/Converters/IPAddressConverter.cs deleted file mode 100644 index 0166c1a..0000000 --- a/src/TotalMixVC/Configuration/Converters/IPAddressConverter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Net; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace TotalMixVC.Configuration.Converters; - -/// -/// Converts an IP address string into a IPAddress object. -/// -public class IPAddressConverter : JsonConverter -{ - /// Reads and converts the JSON to an IP address. - /// The reader. - /// The type to convert. - /// An object that specifies serialization options to use. - /// The converted value. - /// Thrown if value conversion fails. - public override IPAddress? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options - ) - { - try - { - return IPAddress.Parse(reader.GetString()!); - } - catch (FormatException ex) - { - throw new JsonException(message: null, innerException: ex); - } - } - - /// Writes a specified value as JSON. - /// The writer to write to. - /// The value to convert to JSON. - /// An object that specifies serialization options to use. - public override void Write( - Utf8JsonWriter writer, - IPAddress value, - JsonSerializerOptions options - ) - { - writer.WriteStringValue(value.ToString()); - } -} diff --git a/src/TotalMixVC/Configuration/Converters/NonNegativeDoubleConverter.cs b/src/TotalMixVC/Configuration/Converters/NonNegativeDoubleConverter.cs deleted file mode 100644 index def4052..0000000 --- a/src/TotalMixVC/Configuration/Converters/NonNegativeDoubleConverter.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace TotalMixVC.Configuration.Converters; - -/// -/// Validates that a specified double is non-negative. -/// -public class NonNegativeDoubleConverter : JsonConverter -{ - /// Reads and converts the JSON to a non-negative double. - /// The reader. - /// The type to convert. - /// An object that specifies serialization options to use. - /// The converted value. - /// Thrown if value range validation fails. - public override double Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options - ) - { - var value = reader.GetDouble(); - Validate(value); - return value; - } - - /// Writes a specified value as JSON. - /// The writer to write to. - /// The value to convert to JSON. - /// An object that specifies serialization options to use. - /// Thrown if value range validation fails. - public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) - { - Validate(value); - writer.WriteNumberValue(value); - } - - private static void Validate(double value) - { - if (value >= 0) - { - return; - } - - throw new JsonException( - message: null, - innerException: new InvalidOperationException( - "Specified number must be greater than or equal to 0." - ) - ); - } -} diff --git a/src/TotalMixVC/Configuration/Converters/PortIntegerConverter.cs b/src/TotalMixVC/Configuration/Converters/PortIntegerConverter.cs deleted file mode 100644 index 7a2ec1c..0000000 --- a/src/TotalMixVC/Configuration/Converters/PortIntegerConverter.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Net; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace TotalMixVC.Configuration.Converters; - -/// -/// Validates that a specified network port is within the correct range. -/// -public class PortIntegerConverter : JsonConverter -{ - /// Reads and converts the JSON to a network port. - /// The reader. - /// The type to convert. - /// An object that specifies serialization options to use. - /// The converted value. - /// Thrown if value range validation fails. - public override int Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options - ) - { - var value = reader.GetInt32(); - Validate(value); - return value; - } - - /// Writes a specified value as JSON. - /// The writer to write to. - /// The value to convert to JSON. - /// An object that specifies serialization options to use. - /// Thrown if value range validation fails. - public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) - { - Validate(value); - writer.WriteNumberValue(value); - } - - private static void Validate(int value) - { - if (value is >= IPEndPoint.MinPort and <= IPEndPoint.MaxPort) - { - return; - } - - throw new JsonException( - message: null, - innerException: new InvalidOperationException( - $"Specified port number must be in the inclusive range of {IPEndPoint.MinPort} " - + $"to {IPEndPoint.MaxPort}." - ) - ); - } -} diff --git a/src/TotalMixVC/Configuration/Converters/PositiveDoubleConverter.cs b/src/TotalMixVC/Configuration/Converters/PositiveDoubleConverter.cs deleted file mode 100644 index 138617a..0000000 --- a/src/TotalMixVC/Configuration/Converters/PositiveDoubleConverter.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace TotalMixVC.Configuration.Converters; - -/// -/// Validates that a specified double is positive. -/// -public class PositiveDoubleConverter : JsonConverter -{ - /// Reads and converts the JSON to a positive double. - /// The reader. - /// The type to convert. - /// An object that specifies serialization options to use. - /// The converted value. - /// Thrown if value range validation fails. - public override double Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options - ) - { - var value = reader.GetDouble(); - Validate(value); - return value; - } - - /// Writes a specified value as JSON. - /// The writer to write to. - /// The value to convert to JSON. - /// An object that specifies serialization options to use. - /// Thrown if value range validation fails. - public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) - { - Validate(value); - writer.WriteNumberValue(value); - } - - private static void Validate(double value) - { - if (value > 0) - { - return; - } - - throw new JsonException( - message: null, - innerException: new InvalidOperationException( - "Specified number must be greater than 0." - ) - ); - } -} diff --git a/src/TotalMixVC/Configuration/Converters/SolidColorBrushConverter.cs b/src/TotalMixVC/Configuration/Converters/SolidColorBrushConverter.cs deleted file mode 100644 index baebe32..0000000 --- a/src/TotalMixVC/Configuration/Converters/SolidColorBrushConverter.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Windows.Media; - -namespace TotalMixVC.Configuration.Converters; - -/// -/// Converts a hex color string into a SolidColorBrush object. -/// -public class SolidColorBrushConverter : JsonConverter -{ - /// Reads and converts the JSON to a solid color brush. - /// The reader. - /// The type to convert. - /// An object that specifies serialization options to use. - /// The converted value. - /// Thrown if value conversion fails. - public override SolidColorBrush? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options - ) - { - var converter = new BrushConverter(); - try - { - return (SolidColorBrush?)converter.ConvertFromString(reader.GetString()!); - } - catch (FormatException ex) - { - throw new JsonException(message: null, innerException: ex); - } - } - - /// Writes a specified value as JSON. - /// The writer to write to. - /// The value to convert to JSON. - /// An object that specifies serialization options to use. - public override void Write( - Utf8JsonWriter writer, - SolidColorBrush value, - JsonSerializerOptions options - ) - { - var hexColor = value.Color.ToString(CultureInfo.InvariantCulture); - if (hexColor.Length == 9 && hexColor.Substring(1, 2) == "FF") - { - hexColor = hexColor.Remove(1, 2); - } - - writer.WriteStringValue(hexColor.ToLowerInvariant()); - } -} diff --git a/src/TotalMixVC/Configuration/Interface.cs b/src/TotalMixVC/Configuration/Interface.cs index 7ab49c6..c0a5c48 100644 --- a/src/TotalMixVC/Configuration/Interface.cs +++ b/src/TotalMixVC/Configuration/Interface.cs @@ -1,38 +1,34 @@ -using System.Text.Json.Serialization; -using TotalMixVC.Configuration.Converters; - -namespace TotalMixVC.Configuration; +namespace TotalMixVC.Configuration; /// Provides configuration related the behaviour of the widget user interface. public record Interface { - /// Gets the UI scaling of the widget where 1.0 is a normal 100% scale. - [JsonConverter(typeof(PositiveDoubleConverter))] - public double Scaling { get; init; } = 1.0; + /// + /// Gets or sets the UI scaling of the widget where 1.0 is a normal 100% scale. + /// + public double Scaling { get; set; } = 1.0; /// - /// Gets both the horizontal and vertical offset in pixels from the top left of the screen - /// where the widget will appear. + /// Gets or sets both the horizontal and vertical offset in pixels from the top left of the + /// screen where the widget will appear. /// - [JsonConverter(typeof(NonNegativeDoubleConverter))] - public double PositionOffset { get; init; } = 40.0; + public double PositionOffset { get; set; } = 40.0; /// - /// Gets the number of seconds before the widget begins to fade away after it has appeared. + /// Gets or sets the number of seconds before the widget begins to fade away after it has + /// appeared. /// - [JsonConverter(typeof(PositiveDoubleConverter))] - public double HideDelay { get; init; } = 2.0; + public double HideDelay { get; set; } = 2.0; /// - /// Gets the number of second which the widget will take to fade out after hide delay. + /// Gets or sets the number of second which the widget will take to fade out after hide delay. /// - [JsonConverter(typeof(NonNegativeDoubleConverter))] - public double FadeOutTime { get; init; } = 1.0; + public double FadeOutTime { get; set; } = 1.0; /// - /// Gets a value indicating whether the widget should be shown when remote volume changes are - /// detected. Please note that the device seems to send volume changes at some random times - /// which is why this setting is disabled by default. + /// Gets or sets a value indicating whether the widget should be shown when remote volume + /// changes are detected. Please note that the device seems to send volume changes at some + /// random times which is why this setting is disabled by default. /// - public bool ShowRemoteVolumeChanges { get; init; } + public bool ShowRemoteVolumeChanges { get; set; } } diff --git a/src/TotalMixVC/Configuration/Osc.cs b/src/TotalMixVC/Configuration/Osc.cs index 9d1fe94..c345e67 100644 --- a/src/TotalMixVC/Configuration/Osc.cs +++ b/src/TotalMixVC/Configuration/Osc.cs @@ -1,32 +1,23 @@ using System.Net; -using System.Text.Json.Serialization; -using TotalMixVC.Configuration.Converters; +using System.Runtime.Serialization; namespace TotalMixVC.Configuration; /// Provides configuration related to OSC communication with the device. public record Osc { - /// Gets the hostname to send volume changes to. - [JsonConverter(typeof(IPAddressConverter))] - public IPAddress OutgoingHostname { get; init; } = IPAddress.Loopback; - /// - /// Gets the port to use when sending volume changes. This should match the "Port incoming" - /// setting in TotalMixFX. + /// Gets or sets the endpoint to send volume changes to. The port should match the + /// "Port incoming" setting in TotalMixFX. /// - [JsonConverter(typeof(PortIntegerConverter))] - public int OutgoingPort { get; init; } = 7001; - - /// Gets the hostname to receive volume changes from. This should match the - /// "Remote Controller Address" and should typically be "127.0.0.1". - [JsonConverter(typeof(IPAddressConverter))] - public IPAddress IncomingHostname { get; init; } = IPAddress.Loopback; + [DataMember(Name = "outgoing_endpoint")] + public IPEndPoint OutgoingEndPoint { get; set; } = new IPEndPoint(IPAddress.Loopback, 7001); /// - /// Gets the port to use when receiving volume changes. This should match the "Port outgoing" - /// setting in TotalMixFX. + /// Gets or sets the endpoint to receive volume changes from. This address should match the + /// "Remote Controller Address" and should typically be "127.0.0.1". The port should match the + /// "Port outgoing" setting in TotalMixFX. /// - [JsonConverter(typeof(PortIntegerConverter))] - public int IncomingPort { get; init; } = 9001; + [DataMember(Name = "incoming_endpoint")] + public IPEndPoint IncomingEndPoint { get; set; } = new IPEndPoint(IPAddress.Loopback, 9001); } diff --git a/src/TotalMixVC/Configuration/Theme.cs b/src/TotalMixVC/Configuration/Theme.cs index c3d106d..f59f474 100644 --- a/src/TotalMixVC/Configuration/Theme.cs +++ b/src/TotalMixVC/Configuration/Theme.cs @@ -1,71 +1,61 @@ -using System.Text.Json.Serialization; -using System.Windows.Media; -using TotalMixVC.Configuration.Converters; +using System.Windows.Media; namespace TotalMixVC.Configuration; /// Provides configuration related to the theme of the widget. public record Theme { - private static readonly BrushConverter s_converter = new(); - - /// Gets the background corner rounding of the widget and tray tooltip. - [JsonConverter(typeof(NonNegativeDoubleConverter))] - public double BackgroundRounding { get; init; } = 1.0; + /// + /// Gets or sets the background corner rounding of the widget and tray tooltip. + /// + public double BackgroundRounding { get; set; } = 1.0; - /// Gets the background color of the widget and tray tooltip. - [JsonConverter(typeof(SolidColorBrushConverter))] - public SolidColorBrush BackgroundColor { get; init; } = - (SolidColorBrush)s_converter.ConvertFromString("#e21e2328")!; + /// Gets or sets the background color of the widget and tray tooltip. + public Color BackgroundColor { get; set; } = + (Color)ColorConverter.ConvertFromString("#e21e2328")!; /// - /// Gets the color of the "TotalMix" heading text on the widget and tray tooltip. + /// Gets or sets the color of the "TotalMix" heading text on the widget and tray tooltip. /// - [JsonConverter(typeof(SolidColorBrushConverter))] - public SolidColorBrush HeadingTotalmixColor { get; init; } = - (SolidColorBrush)s_converter.ConvertFromString("#ffffff")!; + public Color HeadingTotalmixColor { get; set; } = + (Color)ColorConverter.ConvertFromString("#ffffff")!; /// - /// Gets the color of the "Volume" heading text on the widget and tray tooltip. + /// Gets or sets the color of the "Volume" heading text on the widget and tray tooltip. /// - [JsonConverter(typeof(SolidColorBrushConverter))] - public SolidColorBrush HeadingVolumeColor { get; init; } = - (SolidColorBrush)s_converter.ConvertFromString("#e06464")!; + public Color HeadingVolumeColor { get; set; } = + (Color)ColorConverter.ConvertFromString("#e06464")!; - /// Gets the color of the decibel readout text on the widget. - [JsonConverter(typeof(SolidColorBrushConverter))] - public SolidColorBrush VolumeReadoutColorNormal { get; init; } = - (SolidColorBrush)s_converter.ConvertFromString("#ffffff")!; + /// Gets or sets the color of the decibel readout text on the widget. + public Color VolumeReadoutColorNormal { get; set; } = + (Color)ColorConverter.ConvertFromString("#ffffff")!; /// - /// Gets the color of the decibel readout text on the widget when the volume is dimmed. + /// Gets or sets the color of the decibel readout text on the widget when the volume is dimmed. /// - [JsonConverter(typeof(SolidColorBrushConverter))] - public SolidColorBrush VolumeReadoutColorDimmed { get; init; } = - (SolidColorBrush)s_converter.ConvertFromString("#ffa500")!; + public Color VolumeReadoutColorDimmed { get; set; } = + (Color)ColorConverter.ConvertFromString("#ffa500")!; - /// Gets the background color of volume bar on the widget. - [JsonConverter(typeof(SolidColorBrushConverter))] - public SolidColorBrush VolumeBarBackgroundColor { get; init; } = - (SolidColorBrush)s_converter.ConvertFromString("#333333")!; + /// Gets or sets the background color of volume bar on the widget. + public Color VolumeBarBackgroundColor { get; set; } = + (Color)ColorConverter.ConvertFromString("#333333")!; - /// Gets the current reading foreground color of volume bar on the widget. - [JsonConverter(typeof(SolidColorBrushConverter))] - public SolidColorBrush VolumeBarForegroundColorNormal { get; init; } = - (SolidColorBrush)s_converter.ConvertFromString("#999999")!; + /// + /// Gets or sets the current reading foreground color of volume bar on the widget. + /// + public Color VolumeBarForegroundColorNormal { get; set; } = + (Color)ColorConverter.ConvertFromString("#999999")!; /// - /// Gets the current reading foreground color of volume bar on the widget when the volume is - /// dimmed. + /// Gets or sets the current reading foreground color of volume bar on the widget when the + /// volume is dimmed. /// - [JsonConverter(typeof(SolidColorBrushConverter))] - public SolidColorBrush VolumeBarForegroundColorDimmed { get; init; } = - (SolidColorBrush)s_converter.ConvertFromString("#996500")!; + public Color VolumeBarForegroundColorDimmed { get; set; } = + (Color)ColorConverter.ConvertFromString("#996500")!; /// - /// Gets the foreground color of message text on the tray tooltip. + /// Gets or sets the foreground color of message text on the tray tooltip. /// - [JsonConverter(typeof(SolidColorBrushConverter))] - public SolidColorBrush TrayTooltipMessageColor { get; init; } = - (SolidColorBrush)s_converter.ConvertFromString("#ffffff")!; + public Color TrayTooltipMessageColor { get; set; } = + (Color)ColorConverter.ConvertFromString("#ffffff")!; } diff --git a/src/TotalMixVC/Configuration/Volume.cs b/src/TotalMixVC/Configuration/Volume.cs index 594b701..53b7049 100644 --- a/src/TotalMixVC/Configuration/Volume.cs +++ b/src/TotalMixVC/Configuration/Volume.cs @@ -4,28 +4,29 @@ public record Volume { /// - /// Gets a value indicating whether volume units are set in dB instead of percentages. + /// Gets or sets a value indicating whether volume units are set in dB instead of percentages. /// - public bool UseDecibels { get; init; } + public bool UseDecibels { get; set; } /// - /// Gets the increment that is to be used when adjusting the volume. The volume ranges from - /// 0.0 and 1.0 and thus the max allowed increment is 0.10 for percentages or 3.0 in decibels - /// to avoid major jumps in volume. + /// Gets or sets the increment that is to be used when adjusting the volume. The volume ranges + /// from 0.0 and 1.0 and thus the max allowed increment is 0.10 for percentages or 3.0 in + /// decibels to avoid major jumps in volume. /// - public float? Increment { get; init; } + public float? Increment { get; set; } /// - /// Gets the fine increment that is to be used when adjusting the volume and holding the Shift - /// key. The volume ranges from 0.0 and 1.0 and thus the max allowed fine increment is 0.05 - /// for percentages and 1.5 in decibels to avoid major jumps in volume. When using decibels, - /// it is generally a good idea to ensure the fine increment is a multiple of increment. + /// Gets or sets the fine increment that is to be used when adjusting the volume and holding + /// the Shift key. The volume ranges from 0.0 and 1.0 and thus the max allowed fine increment + /// is 0.05 for percentages and 1.5 in decibels to avoid major jumps in volume. When using + /// decibels, it is generally a good idea to ensure the fine increment is a multiple of + /// increment. /// - public float? FineIncrement { get; init; } + public float? FineIncrement { get; set; } /// - /// Gets the maximum volume that will be sent by the application where 1.0 or 6.0 dB is the - /// loudest volume the device can receive. + /// Gets or sets the maximum volume that will be sent by the application where 1.0 or 6.0 dB is + /// the loudest volume the device can receive. /// - public float? Max { get; init; } + public float? Max { get; set; } } diff --git a/src/TotalMixVC/Hotkeys/GlobalHotKeyManager.cs b/src/TotalMixVC/Hotkeys/GlobalHotKeyManager.cs index a1351e2..9fe973b 100644 --- a/src/TotalMixVC/Hotkeys/GlobalHotKeyManager.cs +++ b/src/TotalMixVC/Hotkeys/GlobalHotKeyManager.cs @@ -30,7 +30,7 @@ public GlobalHotKeyManager() _actions = []; // Please note that the message loop pumper calls ThreadFilterMessage and then - // ThreadPreprocessMessage every time it receives a key stroke. Thus, either of these + // ThreadPreprocessMessage every time it receives a key stroke. Thus, either of these // will suffice for our purposes. ComponentDispatcher.ThreadPreprocessMessage += OnThreadPreprocessMessage; } @@ -58,7 +58,7 @@ public void Register(Hotkey hotkey, Action action) } /// - /// The event handler that executes when a keyboard is message is received. This handler + /// The event handler that executes when a keyboard is message is received. This handler /// will run the appropriate action based on the hotkey detected. /// /// Message information for the key stroke. diff --git a/src/TotalMixVC/TotalMixVC.csproj b/src/TotalMixVC/TotalMixVC.csproj index e441a9a..c3ab9ea 100644 --- a/src/TotalMixVC/TotalMixVC.csproj +++ b/src/TotalMixVC/TotalMixVC.csproj @@ -65,6 +65,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/src/TotalMixVC/VolumeIndicator.xaml.cs b/src/TotalMixVC/VolumeIndicator.xaml.cs index 6c51922..cde92fc 100644 --- a/src/TotalMixVC/VolumeIndicator.xaml.cs +++ b/src/TotalMixVC/VolumeIndicator.xaml.cs @@ -59,7 +59,7 @@ public void UpdateConfig(Config config) } /// - /// Displays the volume indicator with the current volume details. This method may be + /// Displays the volume indicator with the current volume details. This method may be /// safely called from inside an async task. /// /// The task object representing the asynchronous operation. @@ -68,7 +68,7 @@ public async Task DisplayCurrentVolumeAsync() // Switch to the UI thread. await _joinableTaskFactory.SwitchToMainThreadAsync(); - // Display the volume indicator with the current volume details. We use a dispatcher + // Display the volume indicator with the current volume details. We use a dispatcher // to ensure that this work occurs in the UI thread. var showStoryboard = (Storyboard)Resources["WindowStoryboardShow"]; showStoryboard.Begin(this); @@ -86,7 +86,7 @@ public async Task DisplayCurrentVolumeAsync() } /// - /// Updates the volume in the volume indicator given the provided volume details. This + /// Updates the volume in the volume indicator given the provided volume details. This /// method may be safely called from inside an async task. /// /// The current volume as a float. @@ -99,12 +99,16 @@ public async Task UpdateVolumeAsync(float volume, string volumeDecibels, bool is await _joinableTaskFactory.SwitchToMainThreadAsync(); // Update the color of text and the volume rectangle based on whether the volume is dimmed. - VolumeWidgetBarForeground.Fill = isDimmed - ? _config.Theme.VolumeBarForegroundColorDimmed - : _config.Theme.VolumeBarForegroundColorNormal; - VolumeWidgetReadout.Foreground = isDimmed - ? _config.Theme.VolumeReadoutColorDimmed - : _config.Theme.VolumeReadoutColorNormal; + VolumeWidgetBarForeground.Fill = new SolidColorBrush( + isDimmed + ? _config.Theme.VolumeBarForegroundColorDimmed + : _config.Theme.VolumeBarForegroundColorNormal + ); + VolumeWidgetReadout.Foreground = new SolidColorBrush( + isDimmed + ? _config.Theme.VolumeReadoutColorDimmed + : _config.Theme.VolumeReadoutColorNormal + ); // Update the volume bar with the percentage and readout text box with the decibel reading. VolumeWidgetBarForeground.Width = (int)(VolumeWidgetBarBackground.ActualWidth * volume); @@ -181,12 +185,20 @@ private void ConfigureInterface() private void ConfigureTheme() { - VolumeWidgetBorder.BorderBrush = _config.Theme.BackgroundColor; + VolumeWidgetBorder.BorderBrush = new SolidColorBrush(_config.Theme.BackgroundColor); VolumeWidgetBorder.CornerRadius = new CornerRadius(_config.Theme.BackgroundRounding); - VolumeWidgetTitleTotalMix.Foreground = _config.Theme.HeadingTotalmixColor; - VolumeWidgetTitleVolume.Foreground = _config.Theme.HeadingVolumeColor; - VolumeWidgetReadout.Foreground = _config.Theme.VolumeReadoutColorNormal; - VolumeWidgetBarBackground.Fill = _config.Theme.VolumeBarBackgroundColor; - VolumeWidgetBarForeground.Fill = _config.Theme.VolumeBarForegroundColorNormal; + VolumeWidgetTitleTotalMix.Foreground = new SolidColorBrush( + _config.Theme.HeadingTotalmixColor + ); + VolumeWidgetTitleVolume.Foreground = new SolidColorBrush(_config.Theme.HeadingVolumeColor); + VolumeWidgetReadout.Foreground = new SolidColorBrush( + _config.Theme.VolumeReadoutColorNormal + ); + VolumeWidgetBarBackground.Fill = new SolidColorBrush( + _config.Theme.VolumeBarBackgroundColor + ); + VolumeWidgetBarForeground.Fill = new SolidColorBrush( + _config.Theme.VolumeBarForegroundColorNormal + ); } }