added rudimentary settings UI

Accessible by plugin context `config.dialog` or saying `customize settings`.

fixes #80
This commit is contained in:
alterNERDtive 2022-05-18 13:50:14 +02:00
parent d038c58595
commit 8a3b17d674
8 changed files with 279 additions and 22 deletions

View file

@ -2,19 +2,27 @@
**NOTE**: Further development is on hold and Odyssey compatibility will not be
worked on for the time being. See [the corresponding issue on
Github](https://github.com/alterNERDtive/VoiceAttack-profiles/issues/113). This
might or might not change after the Horizons/Odyssey merge when console release
is upon us. Feel free to file issues for anything that is broken on Odyssey and
it will be worked on when it is worked on.
Github](https://github.com/alterNERDtive/VoiceAttack-profiles/issues/113). Feel
free to file issues for anything that is broken on Odyssey and it will be worked
on when it is worked on.
That said, there is now a settings UI! Its not pretty, its basic, but it does
the job.
* Updated documentation for the switch to 64-bit as the standard VoiceAttack
distribution format.
* Updated included `bindED` plugin to FIXXME.
### Added
* `customize settings` command: Brings up a rudimentary settings UI. (#80)
* `open documentation` command: Opens the profiles documentatin in your
default browser.
### Fixed
* Log level settings description no longer contains literal `\n`s.
## EliteAttack 8.2.2
### Added
@ -36,7 +44,7 @@ it will be worked on when it is worked on.
AE/BE star”).
* “Not Set” case number when asking for details about an invalid case.
* Fixed RATSIGNAL parsing for locales containing `,`.
* No longer determines (and logs) nearest CMDR if announcing nearest CMDR is
* No longer determines (nor logs) nearest CMDR if announcing nearest CMDR is
turned off while also being off duty.
## SpanshAttack 7.2.1

View file

@ -1,4 +1,4 @@
# General Configuration
# General Configuration
## Settings
@ -7,7 +7,10 @@ configuration is stored in a bunch of VoiceAttack variables which in turn are
stored in your custom profile. You could even have different custom profiles
with their own distinct settings.
You change the configuration via voice commands:
The easiest way to change setings is to say `customize settings`. That will
bring up a rudminteary settings UI.
You change also change the configuration directly via voice commands:
* For toggles (booleans): `customize setting [enable;disable] <trigger phrase>`
* For everything else: `customize setting set <trigger phrase>`

View file

@ -0,0 +1,24 @@
<UserControl x:Class="alterNERDtive.SettingsDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:alterNERDtive"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<StackPanel>
<TabControl Name="tabs">
<TabItem Name="general" Header="general"></TabItem>
<TabItem Name="EliteAttack" Header="EliteAttack"></TabItem>
<TabItem Name="RatAttack" Header="RatAttack"></TabItem>
<TabItem Name="SpanshAttack" Header="SpanshAttack"></TabItem>
<TabItem Name="StreamAttack" Header="StreamAttack"></TabItem>
</TabControl>
<WrapPanel VerticalAlignment="Bottom" HorizontalAlignment="Right">
<Button Name="cancelButton" Click="cancelButton_Click" Padding="5" Margin="5">Cancel</Button>
<Button Name="okButton" Click="okButton_Click" Padding="5" Margin="5">OK</Button>
</WrapPanel>
</StackPanel>
</Grid>
</UserControl>

View file

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace alterNERDtive
{
/// <summary>
/// Interaction logic for SettingsDialog.xaml
/// </summary>
public partial class SettingsDialog : UserControl
{
private struct Setting
{
public string Profile { get; }
public dynamic Option { get; }
public dynamic Value { get; }
public dynamic UiElement { get; }
public Setting(string profile, dynamic option, dynamic value, dynamic uiElement)
=> (Profile, Option, Value, UiElement) = (profile, option, value, uiElement);
}
private List<Setting> values = new List<Setting>();
private util.Configuration config;
private util.VoiceAttackLog log;
public SettingsDialog(util.Configuration config, util.VoiceAttackLog log)
{
InitializeComponent();
this.config = config;
this.log = log;
foreach (TabItem tab in tabs.Items)
{
string profile = tab.Name;
if (profile == "general")
{
profile = "alterNERDtive-base";
}
tab.IsEnabled = BasePlugin.IsProfileActive(profile);
StackPanel panel = new StackPanel();
util.Configuration.OptDict<string, util.Configuration.Option> options = config.GetOptions(profile);
foreach (dynamic option in options.Values)
{
dynamic value = config.GetConfig(profile, option.Name);
if (option is util.Configuration.Option<bool>)
{
WrapPanel row = new WrapPanel();
CheckBox checkBox = new CheckBox();
checkBox.IsChecked = value;
checkBox.VerticalAlignment = VerticalAlignment.Center;
row.Children.Add(checkBox);
values.Add(new Setting(profile, option, value, checkBox));
Label label = new Label();
label.Content = option.Description;
row.Children.Add(label);
panel.Children.Add(row);
}
else
{
StackPanel row = new StackPanel();
Label label = new Label();
label.Content = option.Description;
row.Children.Add(label);
TextBox input = new TextBox();
input.Text = value.ToString();
row.Children.Add(input);
values.Add(new Setting(profile, option, value, input));
panel.Children.Add(row);
}
}
tab.Content = panel;
}
}
private void cancelButton_Click(object sender, RoutedEventArgs e)
{
Window.GetWindow(this).Close();
log.Log("Settings dialog cancelled.", util.LogLevel.DEBUG);
}
private void okButton_Click(object sender, RoutedEventArgs reargs)
{
foreach (Setting setting in values)
{
dynamic state = null;
try
{
if (setting.Option is util.Configuration.Option<bool>)
{
state = ((CheckBox)setting.UiElement).IsChecked ?? false;
}
else if (setting.Option is util.Configuration.Option<DateTime>)
{
state = DateTime.Parse(((TextBox)setting.UiElement).Text);
}
else if (setting.Option is util.Configuration.Option<decimal>)
{
state = decimal.Parse(((TextBox)setting.UiElement).Text);
}
else if (setting.Option is util.Configuration.Option<int>)
{
state = int.Parse(((TextBox)setting.UiElement).Text);
}
else if (setting.Option is util.Configuration.Option<short>)
{
state = short.Parse(((TextBox)setting.UiElement).Text);
}
else if (setting.Option is util.Configuration.Option<string>)
{
state = ((TextBox)setting.UiElement).Text;
}
if (state != setting.Value)
{
log.Log($@"Configuration changed via settings dialog: ""{setting.Profile}.{setting.Option.Name}"" → ""{state}""", util.LogLevel.DEBUG);
config.SetConfig(setting.Profile, setting.Option.Name, state);
}
Window.GetWindow(this).Close();
}
catch (Exception e) when (e is ArgumentNullException || e is FormatException || e is OverflowException)
{
log.Log($@"Invalid value for ""{setting.Profile}.{setting.Option.Name}"": ""{((TextBox)setting.UiElement).Text}""", util.LogLevel.ERROR);
}
}
}
}
}

View file

@ -39,27 +39,40 @@
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Net.Http.Formatting, Version=5.2.7.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll</HintPath>
</Reference>
<Reference Include="System.Numerics" />
<Reference Include="System.Xaml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="base.cs" />
<Compile Include="edts.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SettingsDialog.xaml.cs">
<DependentUpon>SettingsDialog.xaml</DependentUpon>
</Compile>
<Compile Include="util.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Page Include="SettingsDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View file

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
namespace alterNERDtive
{
@ -54,6 +55,8 @@ namespace alterNERDtive
Log.Debug($"Profiles found: {string.Join<string>(", ", ActiveProfiles)}");
}
public static bool IsProfileActive(string profileName) => ActiveProfiles.Contains(profileName);
private static void ConfigurationChanged(string option, dynamic? from, dynamic? to, Guid? guid = null)
{
try
@ -142,6 +145,24 @@ namespace alterNERDtive
| plugin contexts |
\================*/
private static void Context_Config_Dialog(dynamic vaProxy)
{
Thread dialogThread = new Thread(new ThreadStart(() =>
{
_ = new System.Windows.Window
{
Title = "alterNERDtive Profile Options",
Content = new SettingsDialog(Config, Log),
SizeToContent = System.Windows.SizeToContent.WidthAndHeight,
ResizeMode = System.Windows.ResizeMode.NoResize,
WindowStyle = System.Windows.WindowStyle.ToolWindow
}.ShowDialog();
}));
dialogThread.SetApartmentState(ApartmentState.STA);
dialogThread.IsBackground = true;
dialogThread.Start();
}
private static void Context_Config_Dump(dynamic vaProxy)
{
Config.DumpConfig();
@ -463,7 +484,7 @@ namespace alterNERDtive
| required VoiceAttack plugin shenanigans |
\========================================*/
static readonly Version VERSION = new Version("4.2.4");
static readonly Version VERSION = new Version("4.3");
public static Guid VA_Id()
=> new Guid("{F7F59CFD-1AE2-4A7E-8F62-C62372418BAC}");
@ -501,6 +522,9 @@ namespace alterNERDtive
Context_Startup(vaProxy);
break;
// config
case "config.dialog":
Context_Config_Dialog(vaProxy);
break;
case "config.dump":
Context_Config_Dump(vaProxy);
break;

View file

@ -28,7 +28,7 @@ namespace alterNERDtive.util
description: "The key used to paste in conjunction with CTRL. The physical key in your layout that would be 'V' on QWERTY.") },
{ new Option<bool>("enableAutoUpdateCheck", true, voiceTrigger: "auto update check", description: "Automatically check Github for profiles updates when the profile loads.") },
{ new Option<string>("log.logLevel", "NOTICE", voiceTrigger: "log level", validValues: new List<string>{ "ERROR", "WARN", "NOTICE", "INFO", "DEBUG" },
description: @"The level of detail for logging to the VoiceAttack log.\nValid levels are ""ERROR"", ""WARN"", ""NOTICE"", ""INFO"" and ""DEBUG"".\nDefault: ""NOTICE"".") },
description: "The level of detail for logging to the VoiceAttack log.\nValid levels are \"ERROR\", \"WARN\", \"NOTICE\", \"INFO\" and \"DEBUG\".\nDefault: \"NOTICE\".") },
}
},
{
@ -40,22 +40,22 @@ namespace alterNERDtive.util
{ new Option<bool>("announceMappingCandidates", true, voiceTrigger: "mapping candidates",
description: "Announce bodies worth mapping when you have finished scanning a system.\n(Terraformables, Water Worlds, Earth-Like Worlds and Ammonia Worlds that have not been mapped yet.)") },
{ new Option<bool>("announceOutdatedStationData", true, voiceTrigger: "outdated stations", description: "Announce stations with outdated data in the online databases.") },
{ new Option<int>("outdatedStationThreshold", 365, voiceTrigger: "outdated station threshold",
description: "The threshold for station data to count as “outdated”, in days.\nDefault: 365.") },
{ new Option<bool>("announceR2RMappingCandidates", false, voiceTrigger: "road to riches",
description: "Announce bodies worth scanning if you are looking for some starting cash on the Road to Riches.") },
{ new Option<bool>("announceRepairs", true, voiceTrigger: "repair reports", description: "Report on AFMU repairs.") },
{ new Option<bool>("announceSynthesis", true, voiceTrigger: "synthesis reports", description: "Report on synthesis.") },
{ new Option<bool>("autoHonkAllSystems", false, voiceTrigger: "auto honk all systems", description: "Automatically honk upon entering a system, each jump, without constraints.") },
{ new Option<bool>("autoHonkNewSystems", true, voiceTrigger: "auto honk new systems", description: "Automatically honk upon entering a system if it is your first visit.") },
{ new Option<bool>("autoHonkAllSystems", false, voiceTrigger: "auto honk all systems", description: "Automatically honk upon entering a system, each jump, without constraints.") },
{ new Option<int>("scannerFireGroup", 0, voiceTrigger: "scanner fire group", description: "The fire group your discovery scanner is assigned to.\nDefault: 0 (the first one).") },
{ new Option<bool>("usePrimaryFireForDiscoveryScan", false, voiceTrigger: "discovery scan on primary fire", description: "Use primary fire for honking instead of secondary.") },
{ new Option<bool>("autoRestock", true, voiceTrigger: "auto restock", description:
"Automatically restock after docking at a station.\nYou will always refuel, repair and enter the Station Services menu.") },
{ new Option<bool>("flightAssistOff", false, voiceTrigger: "flight assist off", description: "Permanent Flight Assist off mode. You should really do that, its great.") },
{ new Option<bool>("hyperspaceDethrottle", true, voiceTrigger: "hyper space dethrottle",
description: "Throttle down after a jump and when dropping from SC. Like the SC Assist module does.") },
{ new Option<bool>("limpetCheck", true, voiceTrigger: "limpet check", description: "Do a limpet check when undocking, reminding you if you forgot to buy some.") },
{ new Option<int>("outdatedStationThreshold", 365, voiceTrigger: "outdated station threshold",
description: "The threshold for station data to count as “outdated”, in days.\nDefault: 365.") },
{ new Option<int>("scannerFireGroup", 0, voiceTrigger: "scanner fire group", description: "The fire group your discovery scanner is assigned to.\nDefault: 0 (the first one).") },
{ new Option<bool>("usePrimaryFireForDiscoveryScan", false, voiceTrigger: "discovery scan on primary fire", description: "Use primary fire for honking instead of secondary.") },
}
},
{
@ -63,14 +63,14 @@ namespace alterNERDtive.util
new OptDict<string, Option>{
{ new Option<bool>("autoCloseCase", false, voiceTrigger: "auto close fuel rat case", description: "Automatically close a rat case when sending “fuel+” via voice command or ingame chat.") },
{ new Option<bool>("announceNearestCMDR", false, voiceTrigger: "nearest commander to fuel rat case", description: "Announce the nearest commander to incoming rat cases.") },
{ new Option<bool>("announcePlatform", false, voiceTrigger: "platform for fuel rat case", description: "Announce the platform for incoming rat cases.") },
{ new Option<string>("CMDRs", "", voiceTrigger: "fuel rat commanders",
description: "All your CMDRs that are ready to take rat cases.\nUse ; as separator, e.g. “Bud Spencer;Terrence Hill”.") },
{ new Option<bool>("confirmCalls", true, voiceTrigger: "fuel rat call confirmation", description: "Only make calls in #fuelrats after vocal confirmation to prevent mistakes.") },
{ new Option<bool>("onDuty", true, voiceTrigger: "fuel rat duty", description: "On duty, receiving case announcements via TTS.") },
{ new Option<bool>("announcePlatform", false, voiceTrigger: "platform for fuel rat case", description: "Announce the platform for incoming rat cases.") },
{ new Option<string>("platforms", "PC", voiceTrigger: "fuel rat platforms", validValues: new List<string>{ "PC", "Xbox", "Playstation" },
description: "The platform(s) you want to get case announcements for (PC, Xbox, Playstation).\nUse ; as separator, e.g. “PC;Xbox”.") },
{ new Option<bool>("announceSystemInfo", true, voiceTrigger: "system information for fuel rat case", description: "System information provided by Mecha.")},
{ new Option<bool>("confirmCalls", true, voiceTrigger: "fuel rat call confirmation", description: "Only make calls in #fuelrats after vocal confirmation to prevent mistakes.") },
{ new Option<bool>("onDuty", true, voiceTrigger: "fuel rat duty", description: "On duty, receiving case announcements via TTS.") },
}
},
{
@ -163,7 +163,7 @@ namespace alterNERDtive.util
public static implicit operator (string, Option)(Option<T> o) => (o.Name, o);
public static explicit operator T(Option<T> o) => o.DefaultValue;
}
private class OptDict<TKey, TValue> : Dictionary<TKey, TValue>
public class OptDict<TKey, TValue> : Dictionary<TKey, TValue>
{
public OptDict() : base() { }
public OptDict(int capacity) : base(capacity) { }
@ -192,6 +192,11 @@ namespace alterNERDtive.util
public Option GetOption(string id, string name)
{
return Defaults[id][name];
}
public OptDict<string, Option> GetOptions(string id)
{
return Defaults[id];
}
public void SetVoiceTriggers(System.Type type)
@ -353,9 +358,14 @@ namespace alterNERDtive.util
}
public void DumpConfig(string id, string name)
{
dynamic option = Defaults[id][name];
dynamic defaultValue = option.DefaultValue;
string variable = $"{id}.{option.Name}#";
dynamic defaultValue = ((dynamic)Defaults[id][name]).DefaultValue;
dynamic value = GetConfig(id, name);
Log.Notice($"{id}.{name}# = {value}{(value == defaultValue ? " (default)" : "")}");
}
public dynamic GetConfig(string id, string name)
{
dynamic defaultValue = ((dynamic)Defaults[id][name]).DefaultValue;
string variable = $"{id}.{name}#";
dynamic value;
if (defaultValue is bool)
{
@ -385,7 +395,39 @@ namespace alterNERDtive.util
{
throw new InvalidDataException($"Invalid data type for option '{id}.{name}': '{defaultValue}'");
}
Log.Notice($"{variable} = {value}{(value == defaultValue ? " (default)" : "")}");
return value;
}
public void SetConfig(string id, string name, dynamic value)
{
string variable = $"{id}.{name}#";
if (value is bool)
{
Commands.Run("alterNERDtive-base.saveVariableToProfile", wait: true, parameters: new dynamic[] { new string[] { $"{variable}" }, new bool[] { value } }); ;
}
else if (value is DateTime)
{
Commands.Run("alterNERDtive-base.saveVariableToProfile", wait: true, parameters: new dynamic[] { new string[] { $"{variable}" }, new DateTime[] { value } });
}
else if (value is decimal)
{
Commands.Run("alterNERDtive-base.saveVariableToProfile", wait: true, parameters: new dynamic[] { new string[] { $"{variable}" }, new decimal[] { value } });
}
else if (value is int)
{
Commands.Run("alterNERDtive-base.saveVariableToProfile", wait: true, parameters: new dynamic[] { new string[] { $"{variable}" }, new int[] { value } });
}
else if (value is short)
{
Commands.Run("alterNERDtive-base.saveVariableToProfile", wait: true, parameters: new dynamic[] { new string[] { $"{variable}" }, new short[] { value } });
}
else if (value is string)
{
Commands.Run("alterNERDtive-base.saveVariableToProfile", wait: true, parameters: new dynamic[] { new string[] { $"{variable}", value } });
}
else
{
throw new InvalidDataException($"Invalid data type for option '{id}.{name}': '{value}'");
}
}
public void ListConfig()