initial bring across of settings.
MODIFY
WalletWasabi.Fluent/ViewModels/SettingsPageViewModel.cs
WalletWasabi.Fluent/ViewModels/SettingsPageViewModel.cs
-using Avalonia;
-using Avalonia.Markup.Xaml.Styling;
using ReactiveUI;
using System;
+using System.Collections.Generic;
using System.Linq;
-using System.Reactive.Threading.Tasks;
-using System.Windows.Input;
-using WalletWasabi.Fluent.ViewModels.Dialogs;
+using System.Net;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Markup.Xaml.Styling;
+using Avalonia.Threading;
+using NBitcoin;
+using Splat;
using WalletWasabi.Fluent.ViewModels.NavBar;
+using WalletWasabi.Gui;
+using WalletWasabi.Gui.Helpers;
+using WalletWasabi.Gui.Models;
+using WalletWasabi.Gui.Validation;
+using WalletWasabi.Helpers;
+using WalletWasabi.Logging;
+using WalletWasabi.Models;
+using WalletWasabi.Userfacing;
namespace WalletWasabi.Fluent.ViewModels
{
public class SettingsPageViewModel : NavBarItemViewModel
{
+ private Network _network;
+ private string _torSocks5EndPoint;
+ private string _bitcoinP2pEndPoint;
+ private string _localBitcoinCoreDataDir;
+ private bool _autocopy;
+ private bool _customFee;
+ private bool _customChangeAddress;
+ private bool _useTor;
+ private bool _startLocalBitcoinCoreOnStartup;
+ private bool _stopLocalBitcoinCoreOnShutdown;
+ private bool _isModified;
+ private bool _terminateTorOnExit;
+ private string _somePrivacyLevel;
+ private string _finePrivacyLevel;
+ private string _strongPrivacyLevel;
+ private string _dustThreshold;
+ private FeeDisplayFormat _selectedFeeDisplayFormat;
+ private bool _darkModeEnabled;
+
+ private Global Global;
+
public SettingsPageViewModel(NavigationStateViewModel navigationState) : base(navigationState, NavigationTarget.HomeScreen)
{
Title = "Settings";
- BackCommand = ReactiveCommand.Create(() => ClearNavigation());
+ Global = Locator.Current.GetService<Global>();
- NextCommand = ReactiveCommand.Create(() => NavigateTo(new SettingsPageViewModel(navigationState), NavigationTarget.HomeScreen));
+ this.ValidateProperty(x => x.SomePrivacyLevel, ValidateSomePrivacyLevel);
+ this.ValidateProperty(x => x.FinePrivacyLevel, ValidateFinePrivacyLevel);
+ this.ValidateProperty(x => x.StrongPrivacyLevel, ValidateStrongPrivacyLevel);
+ this.ValidateProperty(x => x.DustThreshold, ValidateDustThreshold);
+ this.ValidateProperty(x => x.TorSocks5EndPoint, ValidateTorSocks5EndPoint);
+ this.ValidateProperty(x => x.BitcoinP2pEndPoint, ValidateBitcoinP2pEndPoint);
- OpenDialogCommand = ReactiveCommand.CreateFromTask(async () => await ConfirmSetting.Handle("Please confirm the setting:").ToTask());
+ _darkModeEnabled = true;
+ Autocopy = Global.UiConfig.Autocopy;
+ CustomFee = Global.UiConfig.IsCustomFee;
+ CustomChangeAddress = Global.UiConfig.IsCustomChangeAddress;
- OpenDialogScreenCommand = ReactiveCommand.Create(() => NavigateTo(new SettingsPageViewModel(navigationState), NavigationTarget.DialogScreen));
+ var config = new Config(Global.Config.FilePath);
+ config.LoadOrCreateDefaultFile();
- ConfirmSetting = new Interaction<string, bool>();
+ _network = config.Network;
+ _torSocks5EndPoint = config.TorSocks5EndPoint.ToString(-1);
+ UseTor = config.UseTor;
+ TerminateTorOnExit = config.TerminateTorOnExit;
+ StartLocalBitcoinCoreOnStartup = config.StartLocalBitcoinCoreOnStartup;
+ StopLocalBitcoinCoreOnShutdown = config.StopLocalBitcoinCoreOnShutdown;
- ConfirmSetting.RegisterHandler(
- async interaction =>
- {
- var x = new TestDialogViewModel(navigationState, NavigationTarget.DialogHost, interaction.Input);
- var result = await x.ShowDialogAsync(navigationState.DialogHost());
- interaction.SetOutput(result);
- });
+ _somePrivacyLevel = config.PrivacyLevelSome.ToString();
+ _finePrivacyLevel = config.PrivacyLevelFine.ToString();
+ _strongPrivacyLevel = config.PrivacyLevelStrong.ToString();
+
+ _dustThreshold = config.DustThreshold.ToString();
+
+ _bitcoinP2pEndPoint = config.GetP2PEndpoint().ToString(defaultPort: -1);
+ _localBitcoinCoreDataDir = config.LocalBitcoinCoreDataDir;
+
+ IsModified = !Global.Config.AreDeepEqual(config);
+
+ this.WhenAnyValue(
+ x => x.Network,
+ x => x.UseTor,
+ x => x.TerminateTorOnExit,
+ x => x.StartLocalBitcoinCoreOnStartup,
+ x => x.StopLocalBitcoinCoreOnShutdown)
+ .ObserveOn(RxApp.TaskpoolScheduler)
+ .Subscribe(_ => Save());
+
+ this.WhenAnyValue(x => x.Autocopy)
+ .ObserveOn(RxApp.TaskpoolScheduler)
+ .Subscribe(x => Global.UiConfig.Autocopy = x);
+
+ this.WhenAnyValue(x => x.CustomFee)
+ .ObserveOn(RxApp.TaskpoolScheduler)
+ .Subscribe(x => Global.UiConfig.IsCustomFee = x);
+
+ this.WhenAnyValue(x => x.CustomChangeAddress)
+ .ObserveOn(RxApp.TaskpoolScheduler)
+ .Subscribe(x => Global.UiConfig.IsCustomChangeAddress = x);
+
+ OpenConfigFileCommand = ReactiveCommand.CreateFromTask(OpenConfigFileAsync);
+
+ TextBoxLostFocusCommand = ReactiveCommand.Create(Save);
- ChangeThemeCommand = ReactiveCommand.Create(() =>
+ Observable
+ .Merge(OpenConfigFileCommand.ThrownExceptions)
+ .Merge(TextBoxLostFocusCommand.ThrownExceptions)
+ .ObserveOn(RxApp.TaskpoolScheduler)
+ .Subscribe(ex => Logger.LogError(ex));
+
+ SelectedFeeDisplayFormat = Enum.IsDefined(typeof(FeeDisplayFormat), Global.UiConfig.FeeDisplayFormat)
+ ? (FeeDisplayFormat)Global.UiConfig.FeeDisplayFormat
+ : FeeDisplayFormat.SatoshiPerByte;
+
+ this.WhenAnyValue(x => x.SelectedFeeDisplayFormat)
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .Subscribe(x => Global.UiConfig.FeeDisplayFormat = (int)x);
+
+ this.WhenAnyValue(x => x.DarkModeEnabled)
+ .Skip(1)
+ .Subscribe(
+ x =>
{
var currentTheme = Application.Current.Styles.Select(x => (StyleInclude)x).FirstOrDefault(x => x.Source is { } && x.Source.AbsolutePath.Contains("Themes"));
@@ -52,16 +145,252 @@ namespace WalletWasabi.Fluent.ViewModels
});
}
- public ICommand NextCommand { get; }
+ private bool TabOpened { get; set; }
+
+ private object ConfigLock { get; } = new object();
+
+ public ReactiveCommand<Unit, Unit> OpenConfigFileCommand { get; }
+
+ public ReactiveCommand<Unit, Unit> TextBoxLostFocusCommand { get; }
+
+ public Version BitcoinCoreVersion => Constants.BitcoinCoreVersion;
+
+ public IEnumerable<Network> Networks => new[]
+ {
+ Network.Main,
+ Network.TestNet,
+ Network.RegTest
+ };
+
+ public bool DarkModeEnabled
+ {
+ get => _darkModeEnabled;
+ set => this.RaiseAndSetIfChanged(ref _darkModeEnabled, value);
+ }
+
+ public Network Network
+ {
+ get => _network;
+ set => this.RaiseAndSetIfChanged(ref _network, value);
+ }
+
+ public string TorSocks5EndPoint
+ {
+ get => _torSocks5EndPoint;
+ set => this.RaiseAndSetIfChanged(ref _torSocks5EndPoint, value);
+ }
+
+ public string BitcoinP2pEndPoint
+ {
+ get => _bitcoinP2pEndPoint;
+ set => this.RaiseAndSetIfChanged(ref _bitcoinP2pEndPoint, value);
+ }
+
+ public string LocalBitcoinCoreDataDir
+ {
+ get => _localBitcoinCoreDataDir;
+ set => this.RaiseAndSetIfChanged(ref _localBitcoinCoreDataDir, value);
+ }
+
+ public bool IsModified
+ {
+ get => _isModified;
+ set => this.RaiseAndSetIfChanged(ref _isModified, value);
+ }
+
+ public bool Autocopy
+ {
+ get => _autocopy;
+ set => this.RaiseAndSetIfChanged(ref _autocopy, value);
+ }
+
+ public bool CustomFee
+ {
+ get => _customFee;
+ set => this.RaiseAndSetIfChanged(ref _customFee, value);
+ }
+
+ public bool CustomChangeAddress
+ {
+ get => _customChangeAddress;
+ set => this.RaiseAndSetIfChanged(ref _customChangeAddress, value);
+ }
+
+ public bool StartLocalBitcoinCoreOnStartup
+ {
+ get => _startLocalBitcoinCoreOnStartup;
+ set => this.RaiseAndSetIfChanged(ref _startLocalBitcoinCoreOnStartup, value);
+ }
+
+ public bool StopLocalBitcoinCoreOnShutdown
+ {
+ get => _stopLocalBitcoinCoreOnShutdown;
+ set => this.RaiseAndSetIfChanged(ref _stopLocalBitcoinCoreOnShutdown, value);
+ }
+
+ public bool UseTor
+ {
+ get => _useTor;
+ set => this.RaiseAndSetIfChanged(ref _useTor, value);
+ }
+
+ public bool TerminateTorOnExit
+ {
+ get => _terminateTorOnExit;
+ set => this.RaiseAndSetIfChanged(ref _terminateTorOnExit, value);
+ }
+
+ public string SomePrivacyLevel
+ {
+ get => _somePrivacyLevel;
+ set => this.RaiseAndSetIfChanged(ref _somePrivacyLevel, value);
+ }
+
+ public string FinePrivacyLevel
+ {
+ get => _finePrivacyLevel;
+ set => this.RaiseAndSetIfChanged(ref _finePrivacyLevel, value);
+ }
- public ICommand OpenDialogCommand { get; }
+ public string StrongPrivacyLevel
+ {
+ get => _strongPrivacyLevel;
+ set => this.RaiseAndSetIfChanged(ref _strongPrivacyLevel, value);
+ }
- public ICommand OpenDialogScreenCommand { get; }
+ public string DustThreshold
+ {
+ get => _dustThreshold;
+ set => this.RaiseAndSetIfChanged(ref _dustThreshold, value);
+ }
- public Interaction<string, bool> ConfirmSetting { get; }
+ public IEnumerable<FeeDisplayFormat> FeeDisplayFormats => Enum.GetValues(typeof(FeeDisplayFormat)).Cast<FeeDisplayFormat>();
- public ICommand ChangeThemeCommand { get; }
+ public FeeDisplayFormat SelectedFeeDisplayFormat
+ {
+ get => _selectedFeeDisplayFormat;
+ set => this.RaiseAndSetIfChanged(ref _selectedFeeDisplayFormat, value);
+ }
public override string IconName => "settings_regular";
+
+ private void Save()
+ {
+ // While the Tab is opening we are setting properties with loading and also LostFocus command called by Avalonia
+ // Those would trigger the Save function before we load the config.
+ if (!TabOpened)
+ {
+ return;
+ }
+
+ var network = Network;
+ if (network is null)
+ {
+ return;
+ }
+
+ if (Validations.Any)
+ {
+ return;
+ }
+
+ var config = new Config(Global.Config.FilePath);
+
+ Dispatcher.UIThread.PostLogException(() =>
+ {
+ lock (ConfigLock)
+ {
+ config.LoadFile();
+ if (Network == config.Network)
+ {
+ if (EndPointParser.TryParse(TorSocks5EndPoint, Constants.DefaultTorSocksPort, out EndPoint torEp))
+ {
+ config.TorSocks5EndPoint = torEp;
+ }
+ if (EndPointParser.TryParse(BitcoinP2pEndPoint, network.DefaultPort, out EndPoint p2pEp))
+ {
+ config.SetP2PEndpoint(p2pEp);
+ }
+ config.UseTor = UseTor;
+ config.TerminateTorOnExit = TerminateTorOnExit;
+ config.StartLocalBitcoinCoreOnStartup = StartLocalBitcoinCoreOnStartup;
+ config.StopLocalBitcoinCoreOnShutdown = StopLocalBitcoinCoreOnShutdown;
+ config.LocalBitcoinCoreDataDir = Guard.Correct(LocalBitcoinCoreDataDir);
+ config.DustThreshold = decimal.TryParse(DustThreshold, out var threshold) ? Money.Coins(threshold) : Config.DefaultDustThreshold;
+ config.PrivacyLevelSome = int.TryParse(SomePrivacyLevel, out int level) ? level : Config.DefaultPrivacyLevelSome;
+ config.PrivacyLevelStrong = int.TryParse(StrongPrivacyLevel, out level) ? level : Config.DefaultPrivacyLevelStrong;
+ config.PrivacyLevelFine = int.TryParse(FinePrivacyLevel, out level) ? level : Config.DefaultPrivacyLevelFine;
+ }
+ else
+ {
+ config.Network = Network;
+ BitcoinP2pEndPoint = config.GetP2PEndpoint().ToString(defaultPort: -1);
+ }
+ config.ToFile();
+ IsModified = !Global.Config.AreDeepEqual(config);
+ }
+ });
+ }
+
+ private async Task OpenConfigFileAsync()
+ {
+ await FileHelpers.OpenFileInTextEditorAsync(Global.Config.FilePath);
+ }
+
+ private void ValidateSomePrivacyLevel(IValidationErrors errors)
+ => ValidatePrivacyLevel(errors, SomePrivacyLevel, whiteSpaceOk: true);
+
+ private void ValidateFinePrivacyLevel(IValidationErrors errors)
+ => ValidatePrivacyLevel(errors, FinePrivacyLevel, whiteSpaceOk: true);
+
+ private void ValidateStrongPrivacyLevel(IValidationErrors errors)
+ => ValidatePrivacyLevel(errors, StrongPrivacyLevel, whiteSpaceOk: true);
+
+ private void ValidateDustThreshold(IValidationErrors errors)
+ => ValidateDustThreshold(errors, DustThreshold, whiteSpaceOk: true);
+
+ private void ValidateTorSocks5EndPoint(IValidationErrors errors)
+ => ValidateEndPoint(errors, TorSocks5EndPoint, Constants.DefaultTorSocksPort, whiteSpaceOk: true);
+
+ private void ValidateBitcoinP2pEndPoint(IValidationErrors errors)
+ => ValidateEndPoint(errors, BitcoinP2pEndPoint, Network.DefaultPort, whiteSpaceOk: true);
+
+ private void ValidatePrivacyLevel(IValidationErrors errors, string value, bool whiteSpaceOk)
+ {
+ if (!whiteSpaceOk || !string.IsNullOrWhiteSpace(value))
+ {
+ if (!uint.TryParse(value, out _))
+ {
+ errors.Add(ErrorSeverity.Error, "Invalid privacy level.");
+ }
+ }
+ }
+
+ private void ValidateDustThreshold(IValidationErrors errors, string dustThreshold, bool whiteSpaceOk)
+ {
+ if (!whiteSpaceOk || !string.IsNullOrWhiteSpace(dustThreshold))
+ {
+ if (!string.IsNullOrEmpty(dustThreshold) && dustThreshold.Contains(',', StringComparison.InvariantCultureIgnoreCase))
+ {
+ errors.Add(ErrorSeverity.Error, "Use decimal point instead of comma.");
+ }
+
+ if (!decimal.TryParse(dustThreshold, out var dust) || dust < 0)
+ {
+ errors.Add(ErrorSeverity.Error, "Invalid dust threshold.");
+ }
+ }
+ }
+
+ private void ValidateEndPoint(IValidationErrors errors, string endPoint, int defaultPort, bool whiteSpaceOk)
+ {
+ if (!whiteSpaceOk || !string.IsNullOrWhiteSpace(endPoint))
+ {
+ if (!EndPointParser.TryParse(endPoint, defaultPort, out _))
+ {
+ errors.Add(ErrorSeverity.Error, "Invalid endpoint.");
+ }
+ }
+ }
}
}
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/Views/SettingsPageView.axaml
WalletWasabi.Fluent/Views/SettingsPageView.axaml
xmlns:vm="using:WalletWasabi.Fluent.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:SettingsPageViewModel"
- x:CompileBindings="True"
x:Class="WalletWasabi.Fluent.Views.SettingsPageView">
<UserControl.Styles>
<Style Selector="TabItem">
<Setter Property="Padding" Value="0" />
</Style>
</UserControl.Styles>
- <c:ContentArea Title="Settings">
+ <c:ContentArea Title="Settings" Caption="Manage appearance, privacy and other settings">
<TabControl>
<TabItem Header="General">
+ <StackPanel Orientation="Vertical" Spacing="5">
+ <ToggleSwitch IsChecked="{Binding DarkModeEnabled}" Margin="0 0 10 0">
+ <TextBlock Text="Dark Mode" />
+ </ToggleSwitch>
+ <StackPanel Margin="0 10" Orientation="Horizontal" Spacing="5">
+ <ToggleSwitch IsChecked="{Binding Autocopy}" Margin="0 0 10 0" />
+ <TextBlock VerticalAlignment="Center">Auto Copy/Paste bitcoin addresses, bitcoin urls, etc...</TextBlock>
+ </StackPanel>
+ <StackPanel Margin="0 10" Orientation="Horizontal" Spacing="5">
+ <ToggleSwitch IsChecked="{Binding CustomFee}" Margin="0 0 10 0" />
+ <TextBlock VerticalAlignment="Center">Manual fee entry.</TextBlock>
+ </StackPanel>
+ <StackPanel Margin="0 10" Orientation="Horizontal" Spacing="5">
+ <ToggleSwitch IsChecked="{Binding CustomChangeAddress}" Margin="0 0 10 0" />
+ <TextBlock VerticalAlignment="Center">Custom change address.</TextBlock>
+ </StackPanel>
+ <StackPanel Margin="0 10" Orientation="Vertical" Spacing="5">
+ <TextBlock>Fee Display Format</TextBlock>
+ <DropDown Items="{Binding FeeDisplayFormats}" SelectedItem="{Binding SelectedFeeDisplayFormat}">
+ <!--<DropDown.ItemTemplate>
+ <DataTemplate>
+ <TextBlock Text="{Binding Converter={StaticResource FeeDisplayFormatStringConverter}}" />
+ </DataTemplate>
+ </DropDown.ItemTemplate>-->
+ </DropDown>
+ </StackPanel>
+
+ <StackPanel Orientation="Vertical" Spacing="5">
+ <StackPanel Margin="0 10" Spacing="5">
+ <TextBlock ToolTip.Tip="Coins under the dust threshold won't appear in the coin lists.">Dust Threshold (BTC)</TextBlock>
+ <TextBox Text="{Binding DustThreshold}" />
+ </StackPanel>
+ </StackPanel>
+ </StackPanel>
</TabItem>
<TabItem Header="Privacy">
+ <StackPanel Orientation="Vertical" Spacing="5">
+ <Grid Margin="0 10" ColumnDefinitions="30, *">
+ <DrawingPresenter Height="24" Width="24" Grid.Column="0" Margin="0 0 10 10" VerticalAlignment="Center" HorizontalAlignment="Center" Drawing="{DynamicResource PrivacySome}" />
+ <TextBox Text="{Binding SomePrivacyLevel}" Grid.Column="1" />
+ </Grid>
+ <Grid Margin="0 10" ColumnDefinitions="30, *">
+ <DrawingPresenter Height="24" Width="24" Grid.Column="0" Margin="0 0 10 10" VerticalAlignment="Center" HorizontalAlignment="Center" Drawing="{DynamicResource PrivacyFine}" />
+ <TextBox Text="{Binding FinePrivacyLevel}" Grid.Column="1" />
+ </Grid>
+ <Grid Margin="0 10" ColumnDefinitions="30, *">
+ <DrawingPresenter Height="24" Width="24" Grid.Column="0" Margin="0 0 10 10" VerticalAlignment="Center" HorizontalAlignment="Center" Drawing="{DynamicResource PrivacyStrong}" />
+ <TextBox Text="{Binding StrongPrivacyLevel}" Grid.Column="1" />
+ </Grid>
+ </StackPanel>
</TabItem>
<TabItem Header="Network">
+ <StackPanel Orientation="Vertical" Spacing="5">
+ <StackPanel Margin="0 10" Orientation="Horizontal" Spacing="5">
+ <ToggleSwitch IsChecked="{Binding UseTor}" Margin="0 0 10 0" />
+ <TextBlock VerticalAlignment="Center">Tor can be turned off for debugging.</TextBlock>
+ </StackPanel>
+ <StackPanel Margin="0 10" Spacing="5" IsVisible="{Binding UseTor}">
+ <TextBlock>Tor SOCKS5 Endpoint</TextBlock>
+ <TextBox Text="{Binding TorSocks5EndPoint}" />
+ </StackPanel>
+ <StackPanel Margin="0 10" Orientation="Horizontal" Spacing="5" IsVisible="{Binding UseTor}">
+ <ToggleSwitch IsChecked="{Binding TerminateTorOnExit}" Margin="0 0 10 0" />
+ <TextBlock VerticalAlignment="Center">Terminate Tor process on exit.</TextBlock>
+ </StackPanel>
+ </StackPanel>
</TabItem>
<TabItem Header="Node">
+ <StackPanel Orientation="Vertical" Spacing="5">
+ <StackPanel Margin="0 10" Spacing="5">
+ <TextBlock>Network</TextBlock>
+ <ComboBox Items="{Binding Networks}" SelectedItem="{Binding Network}">
+ <!--<ComboBox.ItemTemplate>
+ <DataTemplate>
+ <TextBlock Text="{Binding Converter={StaticResource NetworkStringConverter}}" />
+ </DataTemplate>
+ </ComboBox.ItemTemplate>-->
+ </ComboBox>
+ </StackPanel>
+ <Panel>
+ <StackPanel Orientation="Vertical" Spacing="5">
+ <TextBlock Classes="warningMessage">Work in progress...</TextBlock>
+ <TextBlock>Use your own Bitcoin Core or Wasabi's built-in Bitcoin Knots.</TextBlock>
+ <TextBlock>Expect high CPU, network usage and 300+ GB disk space requirement.</TextBlock>
+ <StackPanel Margin="0 10" Orientation="Horizontal" Spacing="5">
+ <ToggleSwitch IsChecked="{Binding StartLocalBitcoinCoreOnStartup}" Margin="0 0 10 0" />
+ <TextBlock VerticalAlignment="Center">Run Bitcoin Knots at Wasabi's startup.</TextBlock>
+ </StackPanel>
+ <StackPanel Margin="0 10" Spacing="5" IsVisible="{Binding StartLocalBitcoinCoreOnStartup}">
+ <TextBlock>Bitcoin Core or Bitcoin Knots Data Folder</TextBlock>
+ <TextBox Text="{Binding LocalBitcoinCoreDataDir}" />
+ </StackPanel>
+ <StackPanel Margin="0 10" Orientation="Horizontal" Spacing="5" IsVisible="{Binding StartLocalBitcoinCoreOnStartup}">
+ <ToggleSwitch IsChecked="{Binding StopLocalBitcoinCoreOnShutdown}" Margin="0 0 10 0" />
+ <TextBlock VerticalAlignment="Center">Stop Bitcoin Knots when Wasabi is shut down.</TextBlock>
+ </StackPanel>
+ </StackPanel>
+ </Panel>
+ <Panel IsVisible="{Binding !StartLocalBitcoinCoreOnStartup}">
+ <StackPanel Orientation="Vertical" Spacing="5">
+ <TextBlock>Set Wasabi to download blocks from a full node you control.</TextBlock>
+ <StackPanel Margin="0 10" Spacing="5">
+ <TextBlock>Bitcoin P2P Endpoint</TextBlock>
+ <TextBox Text="{Binding BitcoinP2pEndPoint}" />
+ </StackPanel>
+ </StackPanel>
+ </Panel>
+ </StackPanel>
</TabItem>
</TabControl>
</c:ContentArea>

initial bring across of settings.