using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using OpenNest.Data;
using OpenNest.Desktop;
namespace OpenNest.Avalonia
{
///
/// Tools > Options, as the WinForms Options form: Save writes the general options and the
/// fill strategies to the user settings both apps share and applies them; the nest server URL
/// (storage.json) and the update feed URL have their own buttons that save at once. The
/// MaterialLedger connection is not here: its API key is kept with Windows data protection,
/// so it is set in the WinForms app.
///
internal sealed class OptionsDialog : Window
{
private static readonly IBrush Muted = Brush.Parse("#6B7280");
private readonly string settingsPath;
private readonly CheckBox createNewNest = new() { Content = "Create a new nest on startup" };
private readonly NumericUpDown autoSizeFactor = new()
{
Minimum = (decimal)DesktopOptions.AutoSizePlateFactorMinimum,
Maximum = (decimal)DesktopOptions.AutoSizePlateFactorMaximum,
Increment = (decimal)DesktopOptions.AutoSizePlateFactorIncrement,
FormatString = "0.####",
Width = 150,
HorizontalAlignment = HorizontalAlignment.Left,
};
private readonly ComboBox colorScheme = new() { Width = 200 };
private readonly NumericUpDown planningEffort = new()
{
Minimum = (decimal)DesktopOptions.CuttingPlanEffortMinimum,
Maximum = (decimal)DesktopOptions.CuttingPlanEffortMaximum,
Increment = (decimal)DesktopOptions.CuttingPlanEffortIncrement,
FormatString = "0.0",
Width = 150,
HorizontalAlignment = HorizontalAlignment.Left,
};
private readonly ComboBox clonePlacement = new() { Width = 200 };
private readonly TextBox serverUrl = new() { Watermark = "http://server:port" };
private readonly TextBox updateFeedUrl = new();
private readonly CheckBox includeDevBuilds = new()
{
Content = "Include development builds (latest master; takes effect after restart)",
};
private readonly List<(CheckBox Enabled, string Name)> strategies = new();
private OptionsDialog(string settingsPath)
{
this.settingsPath = settingsPath;
Title = "Options";
Width = 720;
SizeToContent = SizeToContent.Height;
CanResize = false;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
var general = new Grid
{
ColumnDefinitions = new ColumnDefinitions("Auto,*"),
RowDefinitions = new RowDefinitions("Auto,Auto,Auto,Auto,Auto"),
ColumnSpacing = 12,
RowSpacing = 8,
};
AddRow(general, 0, "Auto-size plate factor:", autoSizeFactor,
"The amount to round the plate size up.");
AddRow(general, 1, "Color scheme:", colorScheme, null);
AddRow(general, 2, "Cut planning effort:", planningEffort,
"How long Plan Cutting searches for a route; 1 is normal.");
AddRow(general, 3, "Clone push placement:", clonePlacement,
"Which way Shift+click and Ctrl+click copies are pushed.");
Grid.SetRow(createNewNest, 4);
Grid.SetColumnSpan(createNewNest, 2);
general.Children.Add(createNewNest);
var setServer = new Button { Content = "Set / Change Server" };
setServer.Click += async (_, _) => await SetServerAsync();
var server = Group(
"Nest server URL (File / Database mode is selected under File > Storage Mode)",
UrlRow(serverUrl, setServer));
var saveFeed = new Button { Content = "Save Feed URL" };
saveFeed.Click += async (_, _) => await SaveFeedAsync();
var updates = Group(
"Auto-update feed URL (blank: nest server mirror in Database mode, else GitHub Releases)",
UrlRow(updateFeedUrl, saveFeed), includeDevBuilds);
var ledger = new TextBlock
{
Text = "MaterialLedger is set in the Windows app: its API key is saved with Windows data protection.",
Foreground = Muted,
TextWrapping = TextWrapping.Wrap,
};
var save = new Button { Content = "Save", IsDefault = true, MinWidth = 80 };
save.Click += async (_, _) => await SaveAsync();
var cancel = new Button { Content = "Cancel", IsCancel = true, MinWidth = 80 };
cancel.Click += (_, _) => Close(false);
var buttons = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Spacing = 8,
Children = { save, cancel },
};
var panel = new StackPanel
{
Margin = new Thickness(18, 18, 18, 8),
Spacing = 14,
Children =
{
general,
server,
ledger,
updates,
Group("Fill Strategies", StrategyGrid()),
},
};
// Save and Cancel stay in view when a small screen makes the options scroll.
buttons.Margin = new Thickness(18, 8, 18, 18);
DockPanel.SetDock(buttons, Dock.Bottom);
Content = new DockPanel
{
Children = { buttons, new ScrollViewer { Content = panel } },
};
Load();
}
///
/// Shows the dialog over ; true when the options were saved.
/// is the shared user-settings.json.
///
public static async Task ShowAsync(Window owner, string settingsPath) =>
await new OptionsDialog(settingsPath).ShowDialog(owner) == true;
private void Load()
{
var settings = UserSettingsFile.Read(settingsPath);
var options = DesktopOptions.Read(settings);
serverUrl.Text = DesktopSettings.ServerUrl;
updateFeedUrl.Text = UpdateFeedOption.Read(settings);
includeDevBuilds.IsChecked = options.UpdateIncludeDevBuilds;
createNewNest.IsChecked = options.CreateNewNestOnOpen;
autoSizeFactor.Value = (decimal)options.AutoSizePlateFactorForEditing;
planningEffort.Value = (decimal)options.CuttingPlanEffortForEditing;
var schemes = DesktopOptions.ColorSchemeNames;
colorScheme.ItemsSource = schemes;
colorScheme.SelectedIndex = options.ColorSchemeIndex(schemes);
clonePlacement.ItemsSource = ClonePlacement.Choices.Select(c => c.Text).ToList();
clonePlacement.SelectedIndex = ClonePlacement.IndexOf(options.ClonePlacementOrder);
var disabled = new HashSet(options.DisabledStrategies, StringComparer.OrdinalIgnoreCase);
foreach (var (enabled, name) in strategies)
enabled.IsChecked = !disabled.Contains(name);
}
private DesktopOptions FromControls() => new()
{
CreateNewNestOnOpen = createNewNest.IsChecked == true,
UpdateIncludeDevBuilds = includeDevBuilds.IsChecked == true,
// A cleared box keeps the value it had, as a WinForms numeric box does.
AutoSizePlateFactor = (double)(autoSizeFactor.Value ?? (decimal)DesktopOptions.Defaults.AutoSizePlateFactor),
CuttingPlanEffort = (double)(planningEffort.Value ?? (decimal)DesktopOptions.Defaults.CuttingPlanEffort),
ColorScheme = colorScheme.SelectedItem as string ?? ColorSchemeRegistry.DefaultSchemeName,
ClonePlacementOrder = ClonePlacement.At(clonePlacement.SelectedIndex),
DisabledStrategies = strategies.Where(s => s.Enabled.IsChecked != true).Select(s => s.Name).ToList(),
};
private async Task SaveAsync()
{
var options = FromControls();
if (!UserSettingsFile.Update(settingsPath, options.WriteTo))
{
await ConfirmDialog.ShowAsync(this, "Options",
$"Could not save the options to {settingsPath}. Nothing was changed.");
return;
}
options.Apply();
Close(true);
}
private async Task SetServerAsync()
{
var (notice, url) = NestServerUrlOption.Save(serverUrl.Text, DesktopSettings.SaveServerUrl);
if (url != null)
serverUrl.Text = DesktopSettings.ServerUrl;
await ConfirmDialog.ShowAsync(this, notice.Title, notice.Text);
}
private async Task SaveFeedAsync()
{
var (notice, url) = UpdateFeedOption.Save(updateFeedUrl.Text, next =>
{
if (!UserSettingsFile.Update(settingsPath, values => values[UpdateFeedOption.SettingName] = next))
throw new System.IO.IOException($"{settingsPath} could not be written.");
});
if (url != null)
updateFeedUrl.Text = url;
await ConfirmDialog.ShowAsync(this, notice.Title, notice.Text);
}
private Grid StrategyGrid()
{
var rows = FillStrategyOptions.Rows;
var grid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto,Auto"),
ColumnSpacing = 16,
RowSpacing = 4,
};
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
var header = 1;
foreach (var (text, column) in new[] { ("Strategy", 1), ("Phase", 2), ("Order", 3) })
{
var cell = new TextBlock { Text = text, FontWeight = FontWeight.SemiBold };
Grid.SetColumn(cell, column);
grid.Children.Add(cell);
}
for (var i = 0; i < rows.Count; i++)
{
var row = rows[i];
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
var enabled = new CheckBox { IsChecked = true, MinWidth = 0, MinHeight = 0 };
strategies.Add((enabled, row.Name));
var cells = new Control[]
{
enabled,
Cell(row.Name),
Cell(row.Phase.ToString()),
Cell(row.Order.ToString()),
};
for (var column = 0; column < cells.Length; column++)
{
Grid.SetRow(cells[column], i + header);
Grid.SetColumn(cells[column], column);
grid.Children.Add(cells[column]);
}
}
return grid;
}
private static TextBlock Cell(string text) =>
new() { Text = text, VerticalAlignment = VerticalAlignment.Center };
private static void AddRow(Grid grid, int row, string label, Control editor, string tip)
{
var text = new TextBlock { Text = label, VerticalAlignment = VerticalAlignment.Center };
Grid.SetRow(text, row);
Grid.SetRow(editor, row);
Grid.SetColumn(editor, 1);
if (tip != null)
ToolTip.SetTip(editor, tip);
grid.Children.Add(text);
grid.Children.Add(editor);
}
private static Grid UrlRow(TextBox box, Button button)
{
var row = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto"), ColumnSpacing = 8 };
Grid.SetColumn(button, 1);
button.MinWidth = 150;
row.Children.Add(box);
row.Children.Add(button);
return row;
}
private static StackPanel Group(string header, params Control[] children)
{
var group = new StackPanel { Spacing = 8 };
group.Children.Add(new TextBlock
{
Text = header,
FontWeight = FontWeight.SemiBold,
TextWrapping = TextWrapping.Wrap,
});
foreach (var child in children)
group.Children.Add(child);
return group;
}
}
}