From a583b4a748738741719f5df70086c854ce1a7302 Mon Sep 17 00:00:00 2001 From: TastelessVoid Date: Wed, 12 Nov 2025 17:25:56 +0100 Subject: [PATCH 01/18] Migrate from WinForms to Avalonia UI --- Build Tools/build.sh | 37 + MinishCapRandomizerUI.Avalonia/App.axaml | 95 ++ MinishCapRandomizerUI.Avalonia/App.axaml.cs | 24 + .../DrawConstants/Constants.cs | 40 + .../Elements/ColorPickerWrapper.cs | 314 ++++ .../Elements/DropdownWrapper.cs | 63 + .../Elements/FlagWrapper.cs | 39 + .../Elements/NumberBoxWrapper.cs | 67 + .../Elements/WrappedLogicOptionFactory.cs | 162 ++ .../Elements/WrapperBase.cs | 22 + .../Github/Release.cs | 0 .../MinishCapRandomizerUI.Avalonia.csproj | 61 + MinishCapRandomizerUI.Avalonia/Program.cs | 16 + .../Properties/ResourcesInfo.md | 19 + .../UI/About/AboutWindow.axaml | 32 + .../UI/About/AboutWindow.axaml.cs | 2 + .../UI/Config/PresetFileInfo.cs | 9 + .../UI/Config/SettingPresets.cs | 10 + .../UI/Config/UIConfiguration.cs | 19 + .../UI/InputDialog/InputDialog.axaml | 11 + .../UI/InputDialog/InputDialog.axaml.cs | 2 + .../UI/MainWindow/MainWindow.axaml | 61 + .../UI/MainWindow/MainWindow.axaml.cs | 1477 +++++++++++++++++ .../UI/MainWindow/Tabs/AdvancedTab.axaml | 46 + .../UI/MainWindow/Tabs/AdvancedTab.axaml.cs | 17 + .../UI/MainWindow/Tabs/GeneralTab.axaml | 79 + .../UI/MainWindow/Tabs/GeneralTab.axaml.cs | 17 + .../UI/MainWindow/Tabs/SeedOutputTab.axaml | 45 + .../UI/MainWindow/Tabs/SeedOutputTab.axaml.cs | 9 + .../UI/UrlDialog/UrlDialog.axaml | 10 + .../UI/UrlDialog/UrlDialog.axaml.cs | 39 + .../Wrappers/ControlsPort.cs | 17 + MinishRandomizer.sln | 51 +- 33 files changed, 2903 insertions(+), 9 deletions(-) create mode 100755 Build Tools/build.sh create mode 100644 MinishCapRandomizerUI.Avalonia/App.axaml create mode 100644 MinishCapRandomizerUI.Avalonia/App.axaml.cs create mode 100644 MinishCapRandomizerUI.Avalonia/DrawConstants/Constants.cs create mode 100644 MinishCapRandomizerUI.Avalonia/Elements/ColorPickerWrapper.cs create mode 100644 MinishCapRandomizerUI.Avalonia/Elements/DropdownWrapper.cs create mode 100644 MinishCapRandomizerUI.Avalonia/Elements/FlagWrapper.cs create mode 100644 MinishCapRandomizerUI.Avalonia/Elements/NumberBoxWrapper.cs create mode 100644 MinishCapRandomizerUI.Avalonia/Elements/WrappedLogicOptionFactory.cs create mode 100644 MinishCapRandomizerUI.Avalonia/Elements/WrapperBase.cs create mode 100644 MinishCapRandomizerUI.Avalonia/Github/Release.cs create mode 100644 MinishCapRandomizerUI.Avalonia/MinishCapRandomizerUI.Avalonia.csproj create mode 100644 MinishCapRandomizerUI.Avalonia/Program.cs create mode 100644 MinishCapRandomizerUI.Avalonia/Properties/ResourcesInfo.md create mode 100644 MinishCapRandomizerUI.Avalonia/UI/About/AboutWindow.axaml create mode 100644 MinishCapRandomizerUI.Avalonia/UI/About/AboutWindow.axaml.cs create mode 100644 MinishCapRandomizerUI.Avalonia/UI/Config/PresetFileInfo.cs create mode 100644 MinishCapRandomizerUI.Avalonia/UI/Config/SettingPresets.cs create mode 100644 MinishCapRandomizerUI.Avalonia/UI/Config/UIConfiguration.cs create mode 100644 MinishCapRandomizerUI.Avalonia/UI/InputDialog/InputDialog.axaml create mode 100644 MinishCapRandomizerUI.Avalonia/UI/InputDialog/InputDialog.axaml.cs create mode 100644 MinishCapRandomizerUI.Avalonia/UI/MainWindow/MainWindow.axaml create mode 100644 MinishCapRandomizerUI.Avalonia/UI/MainWindow/MainWindow.axaml.cs create mode 100644 MinishCapRandomizerUI.Avalonia/UI/MainWindow/Tabs/AdvancedTab.axaml create mode 100644 MinishCapRandomizerUI.Avalonia/UI/MainWindow/Tabs/AdvancedTab.axaml.cs create mode 100644 MinishCapRandomizerUI.Avalonia/UI/MainWindow/Tabs/GeneralTab.axaml create mode 100644 MinishCapRandomizerUI.Avalonia/UI/MainWindow/Tabs/GeneralTab.axaml.cs create mode 100644 MinishCapRandomizerUI.Avalonia/UI/MainWindow/Tabs/SeedOutputTab.axaml create mode 100644 MinishCapRandomizerUI.Avalonia/UI/MainWindow/Tabs/SeedOutputTab.axaml.cs create mode 100644 MinishCapRandomizerUI.Avalonia/UI/UrlDialog/UrlDialog.axaml create mode 100644 MinishCapRandomizerUI.Avalonia/UI/UrlDialog/UrlDialog.axaml.cs create mode 100644 MinishCapRandomizerUI.Avalonia/Wrappers/ControlsPort.cs diff --git a/Build Tools/build.sh b/Build Tools/build.sh new file mode 100755 index 00000000..869866d8 --- /dev/null +++ b/Build Tools/build.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail +# Simple cross-platform build script for Avalonia UI and CLI +# Usage: ./build.sh + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " + exit 1 +fi +OUTDIR="$1" +SOLUTION_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$SOLUTION_DIR" + +# Build UI (Avalonia) self-contained single-file +publish_ui(){ + local rid="$1" + local dest="$OUTDIR/UI/$rid" + dotnet publish MinishCapRandomizerUI.Avalonia/MinishCapRandomizerUI.Avalonia.csproj -c Release -r "$rid" \ + -p:PublishSingleFile=true -p:SelfContained=true -p:PublishTrimmed=true -p:TrimMode=partial \ + -p:EnableCompressionInSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o "$dest" +} + +# Build CLI +publish_cli(){ + local rid="$1" + local dest="$OUTDIR/CLI/$rid" + dotnet publish MinishCapRandomizerCLI/MinishCapRandomizerCLI.csproj -c Release -r "$rid" \ + -p:PublishSingleFile=true -p:SelfContained=true -o "$dest" +} + +for rid in linux-x64 linux-arm64 win-x64 win-arm64; do + publish_ui "$rid" + publish_cli "$rid" +done + +echo "Builds published to $OUTDIR" + diff --git a/MinishCapRandomizerUI.Avalonia/App.axaml b/MinishCapRandomizerUI.Avalonia/App.axaml new file mode 100644 index 00000000..252a5e88 --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/App.axaml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + diff --git a/MinishCapRandomizerUI.Avalonia/App.axaml.cs b/MinishCapRandomizerUI.Avalonia/App.axaml.cs new file mode 100644 index 00000000..0c6ffed8 --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/App.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace MinishCapRandomizerUI.Avalonia; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } +} + diff --git a/MinishCapRandomizerUI.Avalonia/DrawConstants/Constants.cs b/MinishCapRandomizerUI.Avalonia/DrawConstants/Constants.cs new file mode 100644 index 00000000..a1467fe2 --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/DrawConstants/Constants.cs @@ -0,0 +1,40 @@ +using Avalonia.Media; +using Avalonia; + +namespace MinishCapRandomizerUI.Avalonia.DrawConstants; + +public static class Constants +{ + public static int TopRowAboveSpacing => 15; + public static int FirstElementInRowX => 10; + public static int WidthMargin => 10; + public static int CategorySpacing => 20; + public static int CategoryLabelAlignX => 17; // kept for parity + public static int CategoryLabelAlignY => -8; + public static int CategoryWidth => 760; + + // Avalonia equivalents for WinForms styling + public static Thickness CategoryBorderThickness => new(1); + public static IBrush CategoryBorderBrush => Brushes.Gray; + public static IBrush DefaultBackgroundBrush => Brushes.White; + public static IBrush DefaultButtonBackgroundBrush => Brushes.Transparent; + + public static bool CategoryLabelsUseAutosize => true; + public static bool LabelsAndCheckboxesUseAutoEllipsis => true; // Not directly supported; kept for parity reference + + public static int DefaultStartingPaneX => 6; + public static int DefaultStartingPaneY => 15; + public static double SpecialScaling = 1; // DPI scaling factor + + public static bool UseMnemonic => false; // Parity placeholder; Avalonia handles access keys differently + + public const int TotalColorPickersPerRow = 1; + public const int TotalNumberBoxesPerRow = 2; + public const int TotalDropdownsPerRow = 2; + public const int TotalFlagsPerRow = 3; + + public const int TooltipInitialShowDelayMs = 400; + public const int TooltipRepeatDelayMs = 400; + public const int TooltipDisplayLengthMs = 30000; +} + diff --git a/MinishCapRandomizerUI.Avalonia/Elements/ColorPickerWrapper.cs b/MinishCapRandomizerUI.Avalonia/Elements/ColorPickerWrapper.cs new file mode 100644 index 00000000..3f200352 --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/Elements/ColorPickerWrapper.cs @@ -0,0 +1,314 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Layout; +using RandomizerCore.Randomizer.Logic.Options; +using MinishCapRandomizerUI.Avalonia.DrawConstants; + +namespace MinishCapRandomizerUI.Avalonia.Elements; + +public class ColorPickerWrapper : WrapperBase, ILogicOptionObserver +{ + // Parity constants with WinForms implementation + private const int NameTextWidth = 125; + private const int CheckboxWidth = 125; + private const int PreviewTextWidth = 55; + private const int ButtonWidth = 100; + private const int PictureBoxWidth = 60; + private const int Height = 23; // unified height for buttons / controls + private const string CheckboxText = "Use Random Color"; + private const string SelectColorText = "Select Color"; + private const string SelectRandomColorText = "Pick Random"; + private const string UseDefaultColorText = "Use Default"; + private const string ColorPreviewText = "Preview:"; + + // Calculated width similar to WinForms (spacing approximated with WidthMargin usage) + private static readonly int ElementWidthInternal = CheckboxWidth + NameTextWidth + PreviewTextWidth + PictureBoxWidth + 3 * ButtonWidth + 7 * Constants.WidthMargin; + + private readonly LogicColorPicker _picker; + + private TextBlock? _nameLabel; + private CheckBox? _useRandomCheckbox; + private Button? _selectColorButton; + private Button? _selectRandomColorButton; + private Button? _useDefaultColorButton; + private TextBlock? _previewLabel; + private Border? _colorPreview; + + public ColorPickerWrapper(LogicColorPicker picker) : base(ElementWidthInternal, Height, picker.SettingGroup, picker.SettingPage) + { + _picker = picker; + _picker.RegisterObserver(this); + } + + public override IList BuildControls(int initialX, int initialY) + { + if (_nameLabel != null && _useRandomCheckbox != null && _selectColorButton != null && _selectRandomColorButton != null && + _useDefaultColorButton != null && _previewLabel != null && _colorPreview != null) + { + return new List{ _nameLabel, _useRandomCheckbox, _selectColorButton, _selectRandomColorButton, _useDefaultColorButton, _previewLabel, _colorPreview }; + } + + // Name label + _nameLabel = new TextBlock + { + Text = _picker.NiceName + ":", + VerticalAlignment = VerticalAlignment.Center, + Width = NameTextWidth + }; + ToolTip.SetTip(_nameLabel, _picker.DescriptionText); + + // Random Color checkbox + _useRandomCheckbox = new CheckBox + { + Content = CheckboxText, + IsChecked = _picker.UseRandomColor, + Width = CheckboxWidth, + VerticalAlignment = VerticalAlignment.Center + }; + ToolTip.SetTip(_useRandomCheckbox, "If enabled, a random color will be selected on seed generation"); + _useRandomCheckbox.IsCheckedChanged += (_, __) => + { + _picker.UseRandomColor = _useRandomCheckbox.IsChecked == true; + UpdatePreviewColor(); + UpdateButtonEnablement(); + _picker.NotifyChildren(); + }; + + // Select Color button (opens slider dialog) + _selectColorButton = new Button + { + Content = SelectColorText, + Width = ButtonWidth + }; + ToolTip.SetTip(_selectColorButton, "Opens the custom color picker"); + _selectColorButton.Click += async (_, __) => await OpenColorDialog(); + + // Pick Random button (immediately randomizes color) + _selectRandomColorButton = new Button + { + Content = SelectRandomColorText, + Width = ButtonWidth + }; + ToolTip.SetTip(_selectRandomColorButton, "A random color is selected now and shown in the preview"); + _selectRandomColorButton.Click += (_, __) => + { + _picker.PickRandomColor(); + UpdatePreviewColor(); + _picker.NotifyChildren(); + }; + + // Use Default button + _useDefaultColorButton = new Button + { + Content = UseDefaultColorText, + Width = ButtonWidth + }; + ToolTip.SetTip(_useDefaultColorButton, "Resets the selected color back to its default"); + _useDefaultColorButton.Click += (_, __) => + { + _picker.DefinedColor = _picker.BaseColor; + UpdatePreviewColor(); + _picker.NotifyChildren(); + }; + + // Preview label + _previewLabel = new TextBlock + { + Text = ColorPreviewText, + VerticalAlignment = VerticalAlignment.Center, + Width = PreviewTextWidth + }; + + // Color preview border + _colorPreview = new Border + { + Width = PictureBoxWidth, + Height = Height - 4, + BorderBrush = Brushes.Gray, + BorderThickness = new Thickness(1) + }; + + UpdatePreviewColor(); + UpdateButtonEnablement(); + + return new List{ _nameLabel, _useRandomCheckbox, _selectColorButton, _selectRandomColorButton, _useDefaultColorButton, _previewLabel, _colorPreview }; + } + + private async Task OpenColorDialog() + { + if (_useRandomCheckbox?.IsChecked == true) return; // disabled state + + var dialog = new Window{ Width = 460, Height = 420, Title = "Pick Color" }; + var root = new StackPanel{ Margin = new Thickness(12), Spacing = 10 }; + + var current = _picker.DefinedColor; + var r = new Slider{ Minimum = 0, Maximum = 255, Value = current.R, Width = 260 }; + var g = new Slider{ Minimum = 0, Maximum = 255, Value = current.G, Width = 260 }; + var b = new Slider{ Minimum = 0, Maximum = 255, Value = current.B, Width = 260 }; + var rx = new NumericUpDown{ Minimum = 0, Maximum = 255, Value = current.R, Width = 70 }; + var gx = new NumericUpDown{ Minimum = 0, Maximum = 255, Value = current.G, Width = 70 }; + var bx = new NumericUpDown{ Minimum = 0, Maximum = 255, Value = current.B, Width = 70 }; + var hexBox = new TextBox{ Width = 120, Watermark = "#RRGGBB" }; + + var preview = new Border{ Width = 80, Height = 80, Background = new SolidColorBrush(Color.FromRgb(current.R, current.G, current.B)), BorderBrush = Brushes.Gray, BorderThickness = new Thickness(1), Margin = new Thickness(0,8,0,8) }; + + void SyncFromSliders() + { + rx.Value = (int)r.Value; gx.Value = (int)g.Value; bx.Value = (int)b.Value; + var c = Color.FromRgb((byte)r.Value, (byte)g.Value, (byte)b.Value); + preview.Background = new SolidColorBrush(c); + hexBox.Text = $"#{c.R:X2}{c.G:X2}{c.B:X2}"; + } + void SyncFromNumeric() + { + r.Value = (double)(rx.Value ?? 0); + g.Value = (double)(gx.Value ?? 0); + b.Value = (double)(bx.Value ?? 0); + var c = Color.FromRgb((byte)r.Value, (byte)g.Value, (byte)b.Value); + preview.Background = new SolidColorBrush(c); + hexBox.Text = $"#{c.R:X2}{c.G:X2}{c.B:X2}"; + } + void SyncFromHex() + { + var t = hexBox.Text?.Trim() ?? string.Empty; + if (t.StartsWith("#")) t = t[1..]; + if (t.Length == 6 && byte.TryParse(t.Substring(0,2), System.Globalization.NumberStyles.HexNumber, null, out var rr) + && byte.TryParse(t.Substring(2,2), System.Globalization.NumberStyles.HexNumber, null, out var gg) + && byte.TryParse(t.Substring(4,2), System.Globalization.NumberStyles.HexNumber, null, out var bb)) + { + r.Value = rr; g.Value = gg; b.Value = bb; + rx.Value = rr; gx.Value = gg; bx.Value = bb; + preview.Background = new SolidColorBrush(Color.FromRgb(rr, gg, bb)); + } + } + + r.PropertyChanged += (_, a) => { if (a.Property.Name == nameof(Slider.Value)) SyncFromSliders(); }; + g.PropertyChanged += (_, a) => { if (a.Property.Name == nameof(Slider.Value)) SyncFromSliders(); }; + b.PropertyChanged += (_, a) => { if (a.Property.Name == nameof(Slider.Value)) SyncFromSliders(); }; + rx.PropertyChanged += (_, a) => { if (a.Property.Name == nameof(NumericUpDown.Value)) SyncFromNumeric(); }; + gx.PropertyChanged += (_, a) => { if (a.Property.Name == nameof(NumericUpDown.Value)) SyncFromNumeric(); }; + bx.PropertyChanged += (_, a) => { if (a.Property.Name == nameof(NumericUpDown.Value)) SyncFromNumeric(); }; + hexBox.PropertyChanged += (_, a) => { if (a.Property.Name == nameof(TextBox.Text)) SyncFromHex(); }; + + var sliders = new Grid(); + sliders.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); + sliders.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star)); + sliders.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); + sliders.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + sliders.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + sliders.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + var lblR = new TextBlock{ Text = "Red", VerticalAlignment = VerticalAlignment.Center }; + var lblG = new TextBlock{ Text = "Green", VerticalAlignment = VerticalAlignment.Center }; + var lblB = new TextBlock{ Text = "Blue", VerticalAlignment = VerticalAlignment.Center }; + Grid.SetRow(lblR,0); Grid.SetColumn(lblR,0); + Grid.SetRow(r,0); Grid.SetColumn(r,1); + Grid.SetRow(rx,0); Grid.SetColumn(rx,2); + Grid.SetRow(lblG,1); Grid.SetColumn(lblG,0); + Grid.SetRow(g,1); Grid.SetColumn(g,1); + Grid.SetRow(gx,1); Grid.SetColumn(gx,2); + Grid.SetRow(lblB,2); Grid.SetColumn(lblB,0); + Grid.SetRow(b,2); Grid.SetColumn(b,1); + Grid.SetRow(bx,2); Grid.SetColumn(bx,2); + sliders.Children.Add(lblR); sliders.Children.Add(r); sliders.Children.Add(rx); + sliders.Children.Add(lblG); sliders.Children.Add(g); sliders.Children.Add(gx); + sliders.Children.Add(lblB); sliders.Children.Add(b); sliders.Children.Add(bx); + + var topRow = new StackPanel{ Orientation = Orientation.Horizontal, Spacing = 12 }; + topRow.Children.Add(preview); + topRow.Children.Add(new StackPanel{ Spacing = 8, Children = { new TextBlock{ Text = "HEX" }, hexBox } }); + + // Preset swatches + var presetColors = new []{ "#E53E3E","#DD6B20","#D69E2E","#38A169","#3182CE","#805AD5","#D53F8C","#718096","#000000","#FFFFFF" }; + var presetsPanel = new WrapPanel(); + foreach (var hex in presetColors) + { + var c = Color.Parse(hex); + var btn = new Button{ Width = 24, Height = 24, Background = new SolidColorBrush(c), BorderBrush = Brushes.Gray, BorderThickness = new Thickness(1), Tag = hex, Margin = new Thickness(3) }; + btn.Click += (_, __) => { hexBox.Text = hex; SyncFromHex(); }; + presetsPanel.Children.Add(btn); + } + + // Recent colors + _recent ??= new Queue(); + var recentsPanel = new WrapPanel(); + foreach (var rc in _recent) + { + var btn = new Button{ Width = 24, Height = 24, Background = new SolidColorBrush(Color.FromRgb(rc.R, rc.G, rc.B)), BorderBrush = Brushes.Gray, BorderThickness = new Thickness(1), Margin = new Thickness(3) }; + var cap = rc; btn.Click += (_, __) => { r.Value = cap.R; g.Value = cap.G; b.Value = cap.B; SyncFromSliders(); }; + recentsPanel.Children.Add(btn); + } + + var buttonsPanel = new StackPanel{ Orientation = Orientation.Horizontal, Spacing = 8 }; + var ok = new Button{ Content = "OK", Width = 90 }; + var cancel = new Button{ Content = "Cancel", Width = 90 }; + ok.Click += (_, __) => { + var c = (preview.Background as SolidColorBrush)?.Color ?? Color.FromRgb((byte)r.Value,(byte)g.Value,(byte)b.Value); + _picker.DefinedColor = System.Drawing.Color.FromArgb(255, c.R, c.G, c.B); + EnqueueRecent(c); + _picker.NotifyChildren(); + UpdatePreviewColor(); + dialog.Close(); + }; + cancel.Click += (_, __) => dialog.Close(); + buttonsPanel.Children.Add(ok); + buttonsPanel.Children.Add(cancel); + + root.Children.Add(topRow); + root.Children.Add(new TextBlock{ Text = "RGB" }); + root.Children.Add(sliders); + root.Children.Add(new TextBlock{ Text = "Presets" }); + root.Children.Add(presetsPanel); + if (_recent.Count > 0) + { + root.Children.Add(new TextBlock{ Text = "Recent" }); + root.Children.Add(recentsPanel); + } + root.Children.Add(buttonsPanel); + dialog.Content = root; + var top = TopLevel.GetTopLevel(_selectColorButton); + if (top is Window owner) + await dialog.ShowDialog(owner); + else + dialog.Show(); + } + + private static Queue? _recent; + private void EnqueueRecent(Color c) + { + _recent ??= new Queue(); + if (_recent.Count >= 10) _recent.Dequeue(); + _recent.Enqueue(c); + } + + private void UpdatePreviewColor() + { + if (_colorPreview == null) return; + var c = _picker.DefinedColor; + if (_useRandomCheckbox?.IsChecked == true) + { + _colorPreview.Background = new SolidColorBrush(Colors.Transparent); + } + else + { + _colorPreview.Background = new SolidColorBrush(Color.FromRgb(c.R, c.G, c.B)); + } + } + + private void UpdateButtonEnablement() + { + var disabled = _useRandomCheckbox?.IsChecked == true; + if (_selectColorButton != null) _selectColorButton.IsEnabled = !disabled; + if (_selectRandomColorButton != null) _selectRandomColorButton.IsEnabled = !disabled; + if (_useDefaultColorButton != null) _useDefaultColorButton.IsEnabled = !disabled; + } + + public void NotifyObserver() + { + // reflect random state & color changes + if (_useRandomCheckbox != null) + _useRandomCheckbox.IsChecked = _picker.UseRandomColor; + UpdateButtonEnablement(); + UpdatePreviewColor(); + } +} diff --git a/MinishCapRandomizerUI.Avalonia/Elements/DropdownWrapper.cs b/MinishCapRandomizerUI.Avalonia/Elements/DropdownWrapper.cs new file mode 100644 index 00000000..75754411 --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/Elements/DropdownWrapper.cs @@ -0,0 +1,63 @@ +using System.Linq; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using MinishCapRandomizerUI.Avalonia.DrawConstants; +using RandomizerCore.Randomizer.Logic.Options; + +namespace MinishCapRandomizerUI.Avalonia.Elements; + +public class DropdownWrapper : WrapperBase, ILogicOptionObserver +{ + private const int TextWidth = 160; // reduced further for compactness + private const int DropdownWidth = 0; // use stretch + private const int DropdownHeight = 23; + private static readonly int ElementWidthInternal = TextWidth + 160 + Constants.WidthMargin; + private const int ElementHeightInternal = DropdownHeight; + + private TextBlock? _label; + private ComboBox? _comboBox; + private readonly LogicDropdown _dropdown; + + public DropdownWrapper(LogicDropdown dropdown) : base(ElementWidthInternal, ElementHeightInternal, dropdown.SettingGroup, dropdown.SettingPage) + { + _dropdown = dropdown; + _dropdown.RegisterObserver(this); + } + + public override IList BuildControls(int initialX, int initialY) + { + if (_label != null && _comboBox != null) + return new List { _label, _comboBox }; + + _label = new TextBlock { Text = _dropdown.NiceName + ":", VerticalAlignment = VerticalAlignment.Center, Width = TextWidth, TextWrapping = TextWrapping.Wrap }; + if (!string.IsNullOrWhiteSpace(_dropdown.DescriptionText)) + ToolTip.SetTip(_label, _dropdown.DescriptionText); + + _comboBox = new ComboBox{ MinWidth = 160, HorizontalAlignment = HorizontalAlignment.Stretch }; + var items = _dropdown.SelectionOptionNames.ToList(); + _comboBox.ItemsSource = items; + var selectedName = _dropdown.OptionsToNames[_dropdown.Selection]; + _comboBox.SelectedItem = selectedName; + if (_comboBox.SelectedItem == null && items.Count > 0) + _comboBox.SelectedIndex = 0; + if (!string.IsNullOrWhiteSpace(_dropdown.DescriptionText)) + ToolTip.SetTip(_comboBox, _dropdown.DescriptionText); + _comboBox.SelectionChanged += (_, __) => + { + if (_comboBox.SelectedItem is string s) + { + _dropdown.Selection = _dropdown.NamesToOptions[s]; + _dropdown.NotifyChildren(); + } + }; + return new List{ _label, _comboBox }; + } + + public void NotifyObserver() + { + if (_comboBox == null) return; + var selectionName = _dropdown.OptionsToNames[_dropdown.Selection]; + _comboBox.SelectedItem = selectionName; + } +} diff --git a/MinishCapRandomizerUI.Avalonia/Elements/FlagWrapper.cs b/MinishCapRandomizerUI.Avalonia/Elements/FlagWrapper.cs new file mode 100644 index 00000000..d1bdb97a --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/Elements/FlagWrapper.cs @@ -0,0 +1,39 @@ +using Avalonia.Controls; +using Avalonia.Media; +using MinishCapRandomizerUI.Avalonia.DrawConstants; +using MinishCapRandomizerUI.Avalonia.Elements; +using RandomizerCore.Randomizer.Logic.Options; + +namespace MinishCapRandomizerUI.Avalonia.Elements; + +public class FlagWrapper : WrapperBase, ILogicOptionObserver +{ + private const int WidthInternal = 200; + private const int HeightInternal = 18; + private CheckBox? _checkBox; + private readonly LogicFlag _flag; + + public FlagWrapper(LogicFlag flag) : base(WidthInternal, HeightInternal, flag.SettingGroup, flag.SettingPage) + { + _flag = flag; + _flag.RegisterObserver(this); + } + + public bool IsFigurineHuntFlag => _flag.Name == "FIGURINE_HUNT" || _flag.NiceName == "Figurine Hunt"; + + public override IList BuildControls(int initialX, int initialY) + { + if (_checkBox != null) return new List{ _checkBox }; + var label = new TextBlock{ Text = _flag.NiceName, TextWrapping = TextWrapping.Wrap, MaxWidth = 220 }; + _checkBox = new CheckBox { Content = label, IsChecked = _flag.Active }; + if (!string.IsNullOrWhiteSpace(_flag.DescriptionText)) + ToolTip.SetTip(_checkBox, _flag.DescriptionText); + _checkBox.IsCheckedChanged += (_, __) => { _flag.Active = _checkBox.IsChecked == true; _flag.NotifyChildren(); }; + return new List{ _checkBox }; + } + + public void NotifyObserver() + { + if (_checkBox != null) _checkBox.IsChecked = _flag.Active; + } +} diff --git a/MinishCapRandomizerUI.Avalonia/Elements/NumberBoxWrapper.cs b/MinishCapRandomizerUI.Avalonia/Elements/NumberBoxWrapper.cs new file mode 100644 index 00000000..ea15f302 --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/Elements/NumberBoxWrapper.cs @@ -0,0 +1,67 @@ +using Avalonia.Controls; +using MinishCapRandomizerUI.Avalonia.Elements; +using MinishCapRandomizerUI.Avalonia.DrawConstants; +using RandomizerCore.Randomizer.Logic.Options; +using System; + +namespace MinishCapRandomizerUI.Avalonia.Elements; + +public class NumberBoxWrapper : WrapperBase, ILogicOptionObserver +{ + private const int TextWidth = 180; + private const int NumberBoxWidth = 90; + private TextBlock? _label; + private TextBox? _textBox; + private readonly LogicNumberBox _numberBox; + + public NumberBoxWrapper(LogicNumberBox numberBox) : base(TextWidth + NumberBoxWidth + Constants.WidthMargin, 20, numberBox.SettingGroup, numberBox.SettingPage) + { + _numberBox = numberBox; + _numberBox.RegisterObserver(this); + } + + public bool IsFigurineRelated => _numberBox.Name.StartsWith("FIGURINE", StringComparison.OrdinalIgnoreCase) + || _numberBox.NiceName.Contains("Figurine", StringComparison.OrdinalIgnoreCase); + + public override IList BuildControls(int initialX, int initialY) + { + if (_label != null && _textBox != null) return new List{ _label, _textBox }; + _label = new TextBlock{ Text = _numberBox.NiceName + ":", Width = TextWidth }; + if (!string.IsNullOrWhiteSpace(_numberBox.DescriptionText)) + ToolTip.SetTip(_label, _numberBox.DescriptionText); + _textBox = new TextBox{ Text = _numberBox.Value.ToString(), Width = NumberBoxWidth }; + if (!string.IsNullOrWhiteSpace(_numberBox.DescriptionText)) + ToolTip.SetTip(_textBox, _numberBox.DescriptionText); + _textBox.PropertyChanged += (_, e) => + { + if (e.Property == TextBox.TextProperty) + { + if (string.IsNullOrWhiteSpace(_textBox.Text)) + { + _numberBox.Value = _numberBox.DefaultValue; + _textBox.Text = _numberBox.DefaultValue.ToString(); + _numberBox.NotifyChildren(); + return; + } + if (byte.TryParse(_textBox.Text, out var val)) + { + if (val < _numberBox.MinValue) val = _numberBox.MinValue; + if (val > _numberBox.MaxValue) val = _numberBox.MaxValue; + _numberBox.Value = val; + _textBox.Text = _numberBox.Value.ToString(); + _numberBox.NotifyChildren(); + } + else + { + _textBox.Text = _numberBox.Value.ToString(); + } + } + }; + return new List{ _label, _textBox }; + } + + public void NotifyObserver() + { + if (_textBox != null) _textBox.Text = _numberBox.Value.ToString(); + } +} diff --git a/MinishCapRandomizerUI.Avalonia/Elements/WrappedLogicOptionFactory.cs b/MinishCapRandomizerUI.Avalonia/Elements/WrappedLogicOptionFactory.cs new file mode 100644 index 00000000..3a8f2b29 --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/Elements/WrappedLogicOptionFactory.cs @@ -0,0 +1,162 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; +using Avalonia.Media; +using MinishCapRandomizerUI.Avalonia.DrawConstants; +using RandomizerCore.Randomizer.Logic.Options; + +namespace MinishCapRandomizerUI.Avalonia.Elements; + +public static class WrappedLogicOptionFactory +{ + public static List BuildGenericWrappedLogicOptions(List logicOptions) + { + var wrappedOptions = new List(); + foreach (var option in logicOptions) + { + switch (option) + { + case LogicFlag flag: wrappedOptions.Add(new FlagWrapper(flag)); break; + case LogicDropdown dd: wrappedOptions.Add(new DropdownWrapper(dd)); break; + case LogicNumberBox nb: wrappedOptions.Add(new NumberBoxWrapper(nb)); break; + case LogicColorPicker cp: wrappedOptions.Add(new ColorPickerWrapper(cp)); break; + } + } + return wrappedOptions; + } + + public static List BuildGenericWrappedLogicOptions(IEnumerable logicOptions) + { + var wrappedOptions = new List(); + foreach (var option in logicOptions) + { + switch (option) + { + case LogicFlag flag: wrappedOptions.Add(new FlagWrapper(flag)); break; + case LogicDropdown dd: wrappedOptions.Add(new DropdownWrapper(dd)); break; + case LogicNumberBox nb: wrappedOptions.Add(new NumberBoxWrapper(nb)); break; + case LogicColorPicker cp: wrappedOptions.Add(new ColorPickerWrapper(cp)); break; + } + } + return wrappedOptions; + } + + public static Control BuildGroupContainer(string groupName, IEnumerable elements) + { + // Default to 2 columns; certain groups expand to 3 for compactness + int columns = 2; + if (groupName.Contains("Fusions", StringComparison.OrdinalIgnoreCase) || + groupName.Contains("Progressive", StringComparison.OrdinalIgnoreCase) || + groupName.Contains("Main Items", StringComparison.OrdinalIgnoreCase) || + groupName.Contains("Quest Status Items", StringComparison.OrdinalIgnoreCase) || + groupName.Contains("Sword Scrolls", StringComparison.OrdinalIgnoreCase) || + groupName.Contains("Joy Butterflies", StringComparison.OrdinalIgnoreCase) || + groupName.Contains("Wind Crests", StringComparison.OrdinalIgnoreCase) || + groupName.Contains("Dungeon Warps", StringComparison.OrdinalIgnoreCase) || + groupName.Contains("Speed Up", StringComparison.OrdinalIgnoreCase) || + groupName.Contains("Global", StringComparison.OrdinalIgnoreCase) || + groupName.Contains("Big Keys", StringComparison.OrdinalIgnoreCase) || + groupName.Contains("Difficulty", StringComparison.OrdinalIgnoreCase)) + { + columns = 3; + } + + bool multiColumn = columns == 3; + // Order: dropdowns first, then flags, then number boxes, then color pickers + var reordered = elements.OrderBy(e => e switch { + DropdownWrapper => 0, + FlagWrapper => 1, + NumberBoxWrapper => 2, + ColorPickerWrapper => 3, + _ => 4 + }).ToList(); + + // Figurine Hunt alignment special-case + if (groupName.Contains("Figurine", StringComparison.OrdinalIgnoreCase)) + { + var figFlag = reordered.OfType().FirstOrDefault(f => f.IsFigurineHuntFlag); + if (figFlag != null) + { + reordered.Remove(figFlag); + var figInputs = reordered.Where(w => (w as NumberBoxWrapper)?.IsFigurineRelated == true).ToList(); + foreach (var fi in figInputs) reordered.Remove(fi); + reordered.Insert(0, figFlag); + var insertAt = 1; + foreach (var fi in figInputs) reordered.Insert(insertAt++, fi); + } + columns = 2; // ensure side-by-side with inputs + multiColumn = false; + } + + var grid = new Grid{ Margin = new Thickness(2) }; + for (int i=0;i().Any(tb=>tb.Text?.StartsWith("Heart Color")==true) || + (verticalWrapper!=null && verticalWrapper.Children.OfType().Any(tb=>tb.Text?.StartsWith("Heart Color")==true)); + } + if (isHeartColorRow && col!=0){ col=0; row++; } + + Control toAdd; + if (verticalWrapper!=null) toAdd = verticalWrapper; + else + { + var container = new StackPanel{ Orientation = Orientation.Horizontal, Spacing = 4, Margin = new Thickness(0,0,6,4) }; + foreach (var c in controls) + { + if (!multiColumn && c is TextBlock tb && tb.Text?.EndsWith(":")==true){ tb.TextWrapping=TextWrapping.Wrap; tb.MaxWidth=180; } + container.Children.Add(c); + } + toAdd = container; + } + ensureRow(row); + Grid.SetRow(toAdd,row); Grid.SetColumn(toAdd,col); grid.Children.Add(toAdd); + col++; if (col>=columns){ col=0; row++; } + } + var headered = new HeaderedContentControl { + Header = groupName, + Content = grid, + Classes = { "compact-groupbox" } + }; + return headered; + } +} diff --git a/MinishCapRandomizerUI.Avalonia/Elements/WrapperBase.cs b/MinishCapRandomizerUI.Avalonia/Elements/WrapperBase.cs new file mode 100644 index 00000000..e85afe5a --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/Elements/WrapperBase.cs @@ -0,0 +1,22 @@ +using Avalonia.Controls; + +namespace MinishCapRandomizerUI.Avalonia.Elements; + +public abstract class WrapperBase +{ + public int ElementWidth { get; } + public int ElementHeight { get; } + public string SettingGrouping { get; } + public string Page { get; } + + protected WrapperBase(int elementWidth, int elementHeight, string settingGrouping, string page) + { + ElementWidth = elementWidth; + ElementHeight = elementHeight; + SettingGrouping = settingGrouping; + Page = page; + } + + public abstract IList BuildControls(int initialX, int initialY); +} + diff --git a/MinishCapRandomizerUI.Avalonia/Github/Release.cs b/MinishCapRandomizerUI.Avalonia/Github/Release.cs new file mode 100644 index 00000000..e69de29b diff --git a/MinishCapRandomizerUI.Avalonia/MinishCapRandomizerUI.Avalonia.csproj b/MinishCapRandomizerUI.Avalonia/MinishCapRandomizerUI.Avalonia.csproj new file mode 100644 index 00000000..b7091169 --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/MinishCapRandomizerUI.Avalonia.csproj @@ -0,0 +1,61 @@ + + + WinExe + net8.0 + enable + enable + Resources/icon.ico + MinishCapRandomizerUI.Avalonia + MinishCapRandomizerUI.Avalonia + true + + + + win-x64;linux-x64 + true + true + true + partial + true + true + embedded + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Patches\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + + Language Raws\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + diff --git a/MinishCapRandomizerUI.Avalonia/Program.cs b/MinishCapRandomizerUI.Avalonia/Program.cs new file mode 100644 index 00000000..83623bf0 --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/Program.cs @@ -0,0 +1,16 @@ +using Avalonia; + +namespace MinishCapRandomizerUI.Avalonia; + +internal static class Program +{ + public static void Main(string[] args) + { + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + + public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); +} diff --git a/MinishCapRandomizerUI.Avalonia/Properties/ResourcesInfo.md b/MinishCapRandomizerUI.Avalonia/Properties/ResourcesInfo.md new file mode 100644 index 00000000..f17f84ad --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/Properties/ResourcesInfo.md @@ -0,0 +1,19 @@ +# Properties Folder (Parity Placeholder) + +WinForms used `Properties/Resources.resx` and Designer for strongly-typed resources. +Avalonia embeds resources directly via `AvaloniaResource` in the csproj. This placeholder documents parity. + +Potential future work: +- Add localization `.axaml` or `.resx` conversions +- Centralize resource URIs or theme assets. +namespace MinishCapRandomizerUI.Avalonia.Github; + +// Ported from WinForms version for parity (used for GitHub release deserialization if needed) +public class Release +{ + public string? Html_Url { get; set; } + public string? Tag_Name { get; set; } + public string? Name { get; set; } + public string? Body { get; set; } +} + diff --git a/MinishCapRandomizerUI.Avalonia/UI/About/AboutWindow.axaml b/MinishCapRandomizerUI.Avalonia/UI/About/AboutWindow.axaml new file mode 100644 index 00000000..7cd7c459 --- /dev/null +++ b/MinishCapRandomizerUI.Avalonia/UI/About/AboutWindow.axaml @@ -0,0 +1,32 @@ + + +