diff --git a/.github/actions/version-tools/action.yml b/.github/actions/version-tools/action.yml
index 7f2135ce..2f19d05e 100644
--- a/.github/actions/version-tools/action.yml
+++ b/.github/actions/version-tools/action.yml
@@ -104,8 +104,8 @@ runs:
# Determine color and status based on version
if [[ $VERSION == *"-dev"* ]]; then
- COLOR="red"
- STATUS="Unstable Development"
+ COLOR="brown"
+ STATUS="Unstable%20Development"
elif [[ $VERSION == *"-alpha"* ]]; then
COLOR="orange"
STATUS="Alpha"
@@ -113,10 +113,10 @@ runs:
COLOR="yellow"
STATUS="Beta"
elif [[ $VERSION == *"-rc"* ]]; then
- COLOR="blue"
- STATUS="Release Candidate"
+ COLOR="lightblue"
+ STATUS="Release%20Candidate"
else
- COLOR="brightgreen"
+ COLOR="lightgreen"
STATUS="Stable"
fi
@@ -131,56 +131,50 @@ runs:
echo "New version badge: $VERSION_BADGE"
echo "New status badge: $STATUS_BADGE"
+ BADGES_CHANGED=false
+
# Update README.md with new badges
if [[ -f "README.md" ]]; then
- BADGES_CHANGED=false
-
- # Get the current version from Solution.props
- echo "Current version from Solution.props: $VERSION"
-
- # Create new badge URLs
- VERSION_BADGE_URL="https://img.shields.io/badge/version-$ENCODED_VERSION-$COLOR"
- STATUS_BADGE_URL="https://img.shields.io/badge/status-$STATUS-$COLOR"
-
- # Create badge markdown
- VERSION_BADGE="[](https://github.com/architects-toolkit/SmartHopper/releases)"
- STATUS_BADGE="[](https://github.com/architects-toolkit/SmartHopper/releases)"
-
- echo "New version badge: $VERSION_BADGE"
- echo "New status badge: $STATUS_BADGE"
-
- # Check if README contains version badge
+ # Check if README contains version badge and if it needs updating
if grep -q "\[\!\[Version\]" README.md; then
echo "Found version badge in README"
- # Always update the badges with the current version from Solution.props
- sed -i "s|\[\!\[Version\](https://img\.shields\.io/badge/version[^)]*)|[|g" README.md
- echo "Updated version badge"
- BADGES_CHANGED=true
+ # Extract current badge URL to check if color matches
+ CURRENT_VERSION_BADGE=$(grep -o "\[\!\[Version\](https://img\.shields\.io/badge/version[^)]*)" README.md || echo "")
+
+ # Check if the badge needs updating (different version or color)
+ if [[ "$CURRENT_VERSION_BADGE" != *"$ENCODED_VERSION"* || "$CURRENT_VERSION_BADGE" != *"-$COLOR"* ]]; then
+ sed -i "s|\[\!\[Version\](https://img\.shields\.io/badge/version[^)]*)|[|g" README.md
+ echo "Updated version badge"
+ BADGES_CHANGED=true
+ else
+ echo "Version badge is already up to date"
+ fi
else
echo "No version badge found in README to replace"
fi
- # Check if README contains status badge
+ # Check if README contains status badge and if it needs updating
if grep -q "\[\!\[Status\]" README.md; then
echo "Found status badge in README"
- # Always update the status badge
- sed -i "s|\[\!\[Status\](https://img\.shields\.io/badge/status[^)]*)|[|g" README.md
- echo "Updated status badge"
- BADGES_CHANGED=true
+ # Extract current badge URL to check if status or color matches
+ CURRENT_STATUS_BADGE=$(grep -o "\[\!\[Status\](https://img\.shields\.io/badge/status[^)]*)" README.md || echo "")
+
+ # Check if the badge needs updating (different status or color)
+ if [[ "$CURRENT_STATUS_BADGE" != *"$STATUS"* || "$CURRENT_STATUS_BADGE" != *"-$COLOR"* ]]; then
+ sed -i "s|\[\!\[Status\](https://img\.shields\.io/badge/status[^)]*)|[|g" README.md
+ echo "Updated status badge"
+ BADGES_CHANGED=true
+ else
+ echo "Status badge is already up to date"
+ fi
else
echo "No status badge found in README to replace"
fi
- # Check if badges were changed
- if [ "$BADGES_CHANGED" = true ]; then
- echo "README.md badges were updated"
- echo "badges-changed=true" >> $GITHUB_OUTPUT
- else
- echo "No changes to README.md badges"
- echo "badges-changed=false" >> $GITHUB_OUTPUT
- fi
+ # Output whether badges were changed
+ echo "badges-changed=$BADGES_CHANGED" >> $GITHUB_OUTPUT
else
echo "README.md not found, skipping badge update"
echo "badges-changed=false" >> $GITHUB_OUTPUT
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a4cacb69..24a88814 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Modified AIChatComponent to always run when the Run parameter is true, regardless of input changes.
+- Improved version badge workflow to also update badges when color doesn't match the requirements based on version type.
- Improved ChatDialog UI with numerous enhancements:
- Modern chat-like interface featuring message bubbles and visual styling.
- Better layout with proper text wrapping to prevent horizontal scrolling.
@@ -49,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Automatically build and attach artifacts to published releases.
- Create platform-specific zip files (Rhino8-Windows, Rhino8-Mac) instead of a single zip with subfolders.
- Improved error handling in the AIStatefulAsyncComponentBase.
+- Updated settings menu to use Eto.Forms and Eto.Drawing.
### Removed
diff --git a/README.md b/README.md
index 01f55b3f..88cf4bda 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# SmartHopper - AI-Powered Grasshopper3D Plugin
-[](https://github.com/architects-toolkit/SmartHopper/releases)
-[](https://github.com/architects-toolkit/SmartHopper/releases)
+[](https://github.com/architects-toolkit/SmartHopper/releases)
+[](https://github.com/architects-toolkit/SmartHopper/releases)
[](https://www.rhino3d.com/)
[](https://mistral.ai/)
[](https://openai.com/)
diff --git a/Solution.props b/Solution.props
index c7f00988..48d0126a 100644
--- a/Solution.props
+++ b/Solution.props
@@ -1,5 +1,5 @@
- 0.1.3-dev.250330
+ 0.2.0-dev.250331
\ No newline at end of file
diff --git a/design/icons/export/mistralai_icon-old.png b/design/icons/export/mistralai_icon-old.png
new file mode 100644
index 00000000..a48b4327
Binary files /dev/null and b/design/icons/export/mistralai_icon-old.png differ
diff --git a/design/icons/export/mistralai_icon.png b/design/icons/export/mistralai_icon.png
index a48b4327..19d0fa44 100644
Binary files a/design/icons/export/mistralai_icon.png and b/design/icons/export/mistralai_icon.png differ
diff --git a/design/icons/export/openai_icon-old.png b/design/icons/export/openai_icon-old.png
new file mode 100644
index 00000000..bdf9c6d9
Binary files /dev/null and b/design/icons/export/openai_icon-old.png differ
diff --git a/design/icons/export/openai_icon.png b/design/icons/export/openai_icon.png
index bdf9c6d9..f0d9ad57 100644
Binary files a/design/icons/export/openai_icon.png and b/design/icons/export/openai_icon.png differ
diff --git a/src/SmartHopper.Config/Resources/mistralai_icon.png b/src/SmartHopper.Config/Resources/mistralai_icon.png
index a48b4327..19d0fa44 100644
Binary files a/src/SmartHopper.Config/Resources/mistralai_icon.png and b/src/SmartHopper.Config/Resources/mistralai_icon.png differ
diff --git a/src/SmartHopper.Config/Resources/openai_icon.png b/src/SmartHopper.Config/Resources/openai_icon.png
index bdf9c6d9..f0d9ad57 100644
Binary files a/src/SmartHopper.Config/Resources/openai_icon.png and b/src/SmartHopper.Config/Resources/openai_icon.png differ
diff --git a/src/SmartHopper.Menu/Dialogs/AboutDialog.cs b/src/SmartHopper.Menu/Dialogs/AboutDialog.cs
index 7f7142b1..04b4e8e5 100644
--- a/src/SmartHopper.Menu/Dialogs/AboutDialog.cs
+++ b/src/SmartHopper.Menu/Dialogs/AboutDialog.cs
@@ -52,7 +52,7 @@ public AboutDialog(string version)
);
}
- private Control CreateLogoPanel()
+ private static Control CreateLogoPanel()
{
// Create an ImageView with the SmartHopper logo from the resources
var imageView = new ImageView();
@@ -199,7 +199,7 @@ private Control CreateContentPanel(string version)
};
}
- private LinkButton CreateLinkButton(string text, string url)
+ private static LinkButton CreateLinkButton(string text, string url)
{
var link = new LinkButton
{
@@ -212,7 +212,7 @@ private LinkButton CreateLinkButton(string text, string url)
return link;
}
- private void OpenUrl(string url)
+ private static void OpenUrl(string url)
{
try
{
diff --git a/src/SmartHopper.Menu/Items/SettingsMenuItem.cs b/src/SmartHopper.Menu/Items/SettingsMenuItem.cs
index cf26d0c2..2844d226 100644
--- a/src/SmartHopper.Menu/Items/SettingsMenuItem.cs
+++ b/src/SmartHopper.Menu/Items/SettingsMenuItem.cs
@@ -12,298 +12,339 @@
using SmartHopper.Config.Models;
using System;
using System.Collections.Generic;
-using System.Drawing;
using System.Linq;
-using System.Windows.Forms;
+using Rhino;
namespace SmartHopper.Menu.Items
{
internal static class SettingsMenuItem
{
- private static readonly Dictionary> ControlFactories = new Dictionary>
+ private static readonly Dictionary> ControlFactories = new Dictionary>
{
- [typeof(string)] = descriptor => new TextBox
+ [typeof(string)] = descriptor =>
{
- UseSystemPasswordChar = descriptor.IsSecret,
- Dock = DockStyle.Fill
+ if (descriptor.IsSecret)
+ return new Eto.Forms.PasswordBox();
+ else
+ return new Eto.Forms.TextBox();
},
- [typeof(int)] = descriptor => new NumericUpDown
+ [typeof(int)] = descriptor => new Eto.Forms.NumericStepper
{
- Minimum = 1,
- Maximum = 4096,
- Value = Convert.ToInt32(descriptor.DefaultValue),
- Dock = DockStyle.Fill
+ MinValue = 1,
+ MaxValue = 4096,
+ Value = Convert.ToInt32(descriptor.DefaultValue)
}
};
- public static ToolStripMenuItem Create()
+ public static System.Windows.Forms.ToolStripMenuItem Create()
{
- var item = new ToolStripMenuItem("Settings");
+ var item = new System.Windows.Forms.ToolStripMenuItem("Settings");
item.Click += (sender, e) => ShowSettingsDialog();
return item;
}
private static void ShowSettingsDialog()
{
- using (var form = new Form())
+ // Use RhinoApp.InvokeOnUiThread to ensure UI operations run on Rhino's main UI thread
+ RhinoApp.InvokeOnUiThread(new Action(() =>
{
- form.Text = "SmartHopper Settings";
- form.Size = new Size(500, 400);
- form.StartPosition = FormStartPosition.CenterScreen;
- form.AutoScroll = true;
-
- var providers = SmartHopperSettings.DiscoverProviders().ToArray();
- var settings = SmartHopperSettings.Load();
-
- System.Diagnostics.Debug.WriteLine($"Number of providers: {providers.Length}");
-
- var outerPanel = new Panel
- {
- Dock = DockStyle.Fill,
- AutoScroll = true
- };
- form.Controls.Add(outerPanel);
-
- var panel = new TableLayoutPanel
- {
- Dock = DockStyle.Top,
- AutoSize = true,
- Padding = new Padding(10)
- };
- outerPanel.Controls.Add(panel);
-
- // Calculate total rows needed
- int totalRows = providers.Sum(p => p.GetSettingDescriptors().Count() * 2 + 1); // *2 for description rows, +1 for provider header
- totalRows += 4; // Add 4 rows: 1 for general header, 3 for default provider selection and debounce time (control + description)
- panel.RowCount = totalRows;
- panel.ColumnCount = 2;
-
- // Set column widths
- panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 40F)); // First column takes 40% of the width
- panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 60F)); // Second column takes 60% of the width
-
- var row = 0;
- var allControls = new Dictionary>();
-
- // Add general settings section
- var generalHeader = new Label
- {
- Text = "General Settings",
- Font = new Font(form.Font, FontStyle.Bold),
- Dock = DockStyle.Fill,
- Padding = new Padding(0, 15, 0, 10),
- AutoSize = true
- };
- panel.Controls.Add(generalHeader, 0, row);
- panel.SetColumnSpan(generalHeader, 2);
- row++;
-
- // Add default provider selection
- panel.Controls.Add(new Label
- {
- Text = "Default AI Provider:",
- Dock = DockStyle.Fill,
- AutoSize = true
- }, 0, row);
-
- var defaultProviderComboBox = new ComboBox
- {
- Dock = DockStyle.Fill,
- DropDownStyle = ComboBoxStyle.DropDownList
- };
-
- // Add all providers to the dropdown
- foreach (var provider in providers)
- {
- defaultProviderComboBox.Items.Add(provider.Name);
- }
-
- // Select the current default provider if set
- if (!string.IsNullOrEmpty(settings.DefaultAIProvider) &&
- defaultProviderComboBox.Items.Contains(settings.DefaultAIProvider))
- {
- defaultProviderComboBox.SelectedItem = settings.DefaultAIProvider;
- }
- else if (defaultProviderComboBox.Items.Count > 0)
- {
- defaultProviderComboBox.SelectedIndex = 0;
- }
-
- panel.Controls.Add(defaultProviderComboBox, 1, row);
- row++;
-
- // Add default provider description
- var defaultProviderDescription = new Label
- {
- Text = "The default AI provider to use when 'Default' is selected in components",
- ForeColor = SystemColors.GrayText,
- Font = new Font(form.Font.FontFamily, form.Font.Size - 1),
- Dock = DockStyle.Fill,
- AutoSize = true,
- Padding = new Padding(5, 0, 0, 5)
- };
- panel.Controls.Add(defaultProviderDescription, 0, row);
- panel.SetColumnSpan(defaultProviderDescription, 2);
- row++;
-
- // Add debounce time setting
- panel.Controls.Add(new Label
+ using (var dialog = new Eto.Forms.Dialog())
{
- Text = "Debounce Time (ms):",
- Dock = DockStyle.Fill,
- AutoSize = true
- }, 0, row);
+ dialog.Title = "SmartHopper Settings";
+ dialog.Size = new Eto.Drawing.Size(500, 400);
+ dialog.MinimumSize = new Eto.Drawing.Size(400, 300);
+ dialog.Resizable = true;
+ dialog.Padding = new Eto.Drawing.Padding(10);
+
+ // Center the dialog on screen
+ dialog.Location = new Eto.Drawing.Point(
+ (int)((Eto.Forms.Screen.PrimaryScreen.Bounds.Width - dialog.Size.Width) / 2),
+ (int)((Eto.Forms.Screen.PrimaryScreen.Bounds.Height - dialog.Size.Height) / 2)
+ );
+
+ var providers = SmartHopperSettings.DiscoverProviders().ToArray();
+ var settings = SmartHopperSettings.Load();
+
+ // Create the main layout
+ var layout = new Eto.Forms.TableLayout { Spacing = new Eto.Drawing.Size(5, 5), Padding = new Eto.Drawing.Padding(10) };
+ var scrollable = new Eto.Forms.Scrollable { Content = layout };
+
+ // Dictionary to store all controls for later retrieval
+ var allControls = new Dictionary>();
+ // Dictionary to track original values to avoid overwriting unchanged sensitive data
+ var originalValues = new Dictionary>();
+
+ // Add general settings section
+ layout.Rows.Add(new Eto.Forms.TableRow(
+ new Eto.Forms.TableCell(new Eto.Forms.Label
+ {
+ Text = "General Settings",
+ Font = new Eto.Drawing.Font(Eto.Drawing.SystemFont.Bold, 12),
+ VerticalAlignment = Eto.Forms.VerticalAlignment.Center
+ })
+ ));
+
+ // Add default provider selection
+ var defaultProviderRow = new Eto.Forms.TableLayout { Spacing = new Eto.Drawing.Size(5, 5) };
+ var defaultProviderComboBox = new Eto.Forms.DropDown();
+
+ // Add all providers to the dropdown and select current default
+ foreach (var provider in providers)
+ {
+ defaultProviderComboBox.Items.Add(new Eto.Forms.ListItem { Text = provider.Name });
+ }
+
+ if (!string.IsNullOrEmpty(settings.DefaultAIProvider))
+ {
+ for (int i = 0; i < defaultProviderComboBox.Items.Count; i++)
+ {
+ if (defaultProviderComboBox.Items[i].Text == settings.DefaultAIProvider)
+ {
+ defaultProviderComboBox.SelectedIndex = i;
+ break;
+ }
+ }
+ }
+ else if (defaultProviderComboBox.Items.Count > 0)
+ {
+ defaultProviderComboBox.SelectedIndex = 0;
+ }
- var debounceControl = new NumericUpDown
- {
- Minimum = 1000,
- Maximum = 5000,
- Value = settings.DebounceTime,
- Dock = DockStyle.Fill
- };
- panel.Controls.Add(debounceControl, 1, row);
- row++;
-
- // Add debounce description
- var debounceDescription = new Label
- {
- Text = "Time to wait before sending a new request (in milliseconds)",
- ForeColor = SystemColors.GrayText,
- Font = new Font(form.Font.FontFamily, form.Font.Size - 1),
- Dock = DockStyle.Fill,
- AutoSize = true,
- Padding = new Padding(5, 0, 0, 5)
- };
- panel.Controls.Add(debounceDescription, 0, row);
- panel.SetColumnSpan(debounceDescription, 2);
- row++;
-
- foreach (var provider in providers)
- {
- System.Diagnostics.Debug.WriteLine($"Provider: {provider.Name}");
- var descriptors = provider.GetSettingDescriptors().ToList();
- System.Diagnostics.Debug.WriteLine($"Number of descriptors: {descriptors.Count}");
+ defaultProviderRow.Rows.Add(new Eto.Forms.TableRow(
+ new Eto.Forms.TableCell(new Eto.Forms.Label { Text = "Default AI Provider:", VerticalAlignment = Eto.Forms.VerticalAlignment.Center }),
+ new Eto.Forms.TableCell(defaultProviderComboBox)
+ ));
+ layout.Rows.Add(defaultProviderRow);
- // Provider header
- var header = new Label
+ // Add default provider description
+ layout.Rows.Add(new Eto.Forms.TableRow(
+ new Eto.Forms.TableCell(new Eto.Forms.Label
+ {
+ Text = "The default AI provider to use when the 'Default' provider is selected",
+ TextColor = Eto.Drawing.Colors.Gray,
+ Font = new Eto.Drawing.Font(Eto.Drawing.SystemFont.Default, 10)
+ })
+ ));
+
+ // Add debounce time setting
+ var debounceRow = new Eto.Forms.TableLayout { Spacing = new Eto.Drawing.Size(5, 5) };
+ var debounceControl = new Eto.Forms.NumericStepper
{
- Text = provider.Name,
- Font = new Font(form.Font, FontStyle.Bold),
- Dock = DockStyle.Fill,
- Padding = new Padding(0, 15, 0, 10),
- AutoSize = true
+ MinValue = 1000,
+ MaxValue = 5000,
+ Value = settings.DebounceTime
};
- panel.Controls.Add(header, 0, row);
- panel.SetColumnSpan(header, 2);
- row++;
- var controls = new Dictionary();
- foreach (var descriptor in descriptors)
+ debounceRow.Rows.Add(new Eto.Forms.TableRow(
+ new Eto.Forms.TableCell(new Eto.Forms.Label { Text = "Debounce Time (ms):", VerticalAlignment = Eto.Forms.VerticalAlignment.Center }),
+ new Eto.Forms.TableCell(debounceControl)
+ ));
+ layout.Rows.Add(debounceRow);
+
+ // Add debounce description
+ layout.Rows.Add(new Eto.Forms.TableRow(
+ new Eto.Forms.TableCell(new Eto.Forms.Label
+ {
+ Text = "Time to wait before sending a new request (in milliseconds)",
+ TextColor = Eto.Drawing.Colors.Gray,
+ Font = new Eto.Drawing.Font(Eto.Drawing.SystemFont.Default, 10)
+ })
+ ));
+
+ // Add provider settings
+ foreach (var provider in providers)
{
- System.Diagnostics.Debug.WriteLine($"Creating control for: {descriptor.Name} ({descriptor.DisplayName})");
+ var descriptors = provider.GetSettingDescriptors().ToList();
+ var controls = new Dictionary();
+ originalValues[provider.Name] = new Dictionary();
- // Add label and control
- panel.Controls.Add(new Label
+ // Create provider header with icon
+ var headerLayout = new Eto.Forms.StackLayout
{
- Text = descriptor.DisplayName + ":",
- Dock = DockStyle.Fill,
- AutoSize = true
- }, 0, row);
-
- var control = ControlFactories[descriptor.Type](descriptor);
- panel.Controls.Add(control, 1, row);
- controls[descriptor.Name] = control;
- row++;
-
- // Add description
- if (!string.IsNullOrWhiteSpace(descriptor.Description))
+ Orientation = Eto.Forms.Orientation.Horizontal,
+ Spacing = 5,
+ VerticalContentAlignment = Eto.Forms.VerticalAlignment.Center,
+ Padding = new Eto.Drawing.Padding(0, 15, 0, 10)
+ };
+
+ // Add provider icon if available
+ if (provider.Icon != null)
{
- var descriptionLabel = new Label
+ using (var ms = new System.IO.MemoryStream())
{
- Text = descriptor.Description,
- ForeColor = SystemColors.GrayText,
- Font = new Font(form.Font.FontFamily, form.Font.Size - 1),
- Dock = DockStyle.Fill,
- AutoSize = true,
- Padding = new Padding(5, 0, 0, 5)
- };
- panel.Controls.Add(descriptionLabel, 0, row);
- panel.SetColumnSpan(descriptionLabel, 2);
- row++;
+ provider.Icon.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
+ ms.Position = 0;
+ headerLayout.Items.Add(new Eto.Forms.ImageView
+ {
+ Image = new Eto.Drawing.Bitmap(ms),
+ Size = new Eto.Drawing.Size(16, 16)
+ });
+ }
}
- // Load current value if exists
- if (settings.ProviderSettings.ContainsKey(provider.Name) &&
- settings.ProviderSettings[provider.Name].ContainsKey(descriptor.Name))
+ // Add provider name
+ headerLayout.Items.Add(new Eto.Forms.Label
{
- var value = settings.ProviderSettings[provider.Name][descriptor.Name];
- if (control is TextBox textBox)
- textBox.Text = value?.ToString() ?? "";
- else if (control is NumericUpDown numericUpDown && value != null)
- numericUpDown.Value = Convert.ToInt32(value);
- }
- else if (descriptor.DefaultValue != null)
+ Text = provider.Name,
+ Font = new Eto.Drawing.Font(Eto.Drawing.SystemFont.Bold, 12),
+ VerticalAlignment = Eto.Forms.VerticalAlignment.Center
+ });
+
+ layout.Rows.Add(new Eto.Forms.TableRow(new Eto.Forms.TableCell(headerLayout)));
+
+ // Add settings for this provider
+ foreach (var descriptor in descriptors)
{
- if (control is TextBox textBox)
- textBox.Text = descriptor.DefaultValue.ToString();
- else if (control is NumericUpDown numericUpDown)
- numericUpDown.Value = Convert.ToInt32(descriptor.DefaultValue);
+ // Create control for this setting
+ var control = ControlFactories[descriptor.Type](descriptor);
+ controls[descriptor.Name] = control;
+
+ // Add label and control
+ var settingRow = new Eto.Forms.TableLayout { Spacing = new Eto.Drawing.Size(5, 5) };
+ settingRow.Rows.Add(new Eto.Forms.TableRow(
+ new Eto.Forms.TableCell(new Eto.Forms.Label {
+ Text = descriptor.DisplayName + ":",
+ VerticalAlignment = Eto.Forms.VerticalAlignment.Center
+ }),
+ new Eto.Forms.TableCell(control)
+ ));
+ layout.Rows.Add(settingRow);
+
+ // Add description if available
+ if (!string.IsNullOrWhiteSpace(descriptor.Description))
+ {
+ layout.Rows.Add(new Eto.Forms.TableRow(
+ new Eto.Forms.TableCell(new Eto.Forms.Label
+ {
+ Text = descriptor.Description,
+ TextColor = Eto.Drawing.Colors.Gray,
+ Font = new Eto.Drawing.Font(Eto.Drawing.SystemFont.Default, 10)
+ })
+ ));
+ }
+
+ // Load current value
+ string currentValue = null;
+ if (settings.ProviderSettings.ContainsKey(provider.Name) &&
+ settings.ProviderSettings[provider.Name].ContainsKey(descriptor.Name))
+ {
+ currentValue = settings.ProviderSettings[provider.Name][descriptor.Name]?.ToString();
+ }
+ else if (descriptor.DefaultValue != null)
+ {
+ currentValue = descriptor.DefaultValue.ToString();
+ }
+
+ // Set value to control and store original for comparison
+ if (currentValue != null)
+ {
+ if (control is Eto.Forms.TextBox textBox)
+ textBox.Text = currentValue;
+ else if (control is Eto.Forms.PasswordBox passwordBox)
+ passwordBox.Text = currentValue;
+ else if (control is Eto.Forms.NumericStepper numericStepper)
+ numericStepper.Value = Convert.ToInt32(currentValue);
+
+ // Store original value for comparison
+ originalValues[provider.Name][descriptor.Name] = currentValue;
+ }
}
- }
- allControls[provider.Name] = controls;
- }
+ allControls[provider.Name] = controls;
+ }
- // Add save button at the bottom
- var buttonPanel = new Panel
- {
- Dock = DockStyle.Bottom,
- Height = 40,
- Padding = new Padding(5)
- };
+ // Add a spacer row at the end
+ layout.Rows.Add(Eto.Forms.TableLayout.AutoSized(null));
- var saveButton = new Button
- {
- Text = "Save",
- DialogResult = DialogResult.OK,
- Dock = DockStyle.Right
- };
- buttonPanel.Controls.Add(saveButton);
- form.Controls.Add(buttonPanel);
- form.AcceptButton = saveButton;
-
- if (form.ShowDialog() == DialogResult.OK)
- {
- // Save settings
- foreach (var provider in providers)
+ // Create buttons
+ var buttonLayout = new Eto.Forms.StackLayout
{
- if (!settings.ProviderSettings.ContainsKey(provider.Name))
- settings.ProviderSettings[provider.Name] = new Dictionary();
+ Orientation = Eto.Forms.Orientation.Horizontal,
+ Spacing = 5,
+ HorizontalContentAlignment = Eto.Forms.HorizontalAlignment.Right
+ };
- var controls = allControls[provider.Name];
- foreach (var descriptor in provider.GetSettingDescriptors())
- {
- var control = controls[descriptor.Name];
- object value = null;
+ var saveButton = new Eto.Forms.Button { Text = "Save" };
+ var cancelButton = new Eto.Forms.Button { Text = "Cancel" };
- if (control is TextBox textBox)
- value = textBox.Text;
- else if (control is NumericUpDown numericUpDown)
- value = (int)numericUpDown.Value;
+ buttonLayout.Items.Add(new Eto.Forms.StackLayoutItem(null, true)); // Spacer
+ buttonLayout.Items.Add(saveButton);
+ buttonLayout.Items.Add(cancelButton);
- settings.ProviderSettings[provider.Name][descriptor.Name] = value;
+ // Set up the dialog content
+ var content = new Eto.Forms.DynamicLayout();
+ content.Add(scrollable, yscale: true);
+ content.Add(buttonLayout);
+
+ dialog.Content = content;
+ dialog.DefaultButton = saveButton;
+ dialog.AbortButton = cancelButton;
+
+ // Handle button clicks
+ saveButton.Click += (sender, e) =>
+ {
+ // Create a copy of the current settings to preserve encrypted values
+ var updatedSettings = new Dictionary>();
+ foreach (var providerSetting in settings.ProviderSettings)
+ {
+ updatedSettings[providerSetting.Key] = new Dictionary(providerSetting.Value);
}
- }
+
+ // Update with new values from the UI
+ foreach (var provider in providers)
+ {
+ if (!updatedSettings.ContainsKey(provider.Name))
+ updatedSettings[provider.Name] = new Dictionary();
- // Save debounce time
- settings.DebounceTime = (int)debounceControl.Value;
-
- // Save default provider
- settings.DefaultAIProvider = defaultProviderComboBox.SelectedItem?.ToString() ?? "";
+ var controls = allControls[provider.Name];
+ foreach (var descriptor in provider.GetSettingDescriptors())
+ {
+ var control = controls[descriptor.Name];
+
+ // Get new value from control
+ object newValue = null;
+ if (control is Eto.Forms.TextBox textBox)
+ newValue = textBox.Text;
+ else if (control is Eto.Forms.PasswordBox passwordBox)
+ newValue = passwordBox.Text;
+ else if (control is Eto.Forms.NumericStepper numericStepper)
+ newValue = (int)numericStepper.Value;
+
+ // For sensitive data, only update if changed and not empty
+ if (descriptor.IsSecret && newValue is string strValue)
+ {
+ if (string.IsNullOrEmpty(strValue))
+ continue; // Keep existing value
+
+ if (originalValues[provider.Name].ContainsKey(descriptor.Name) &&
+ strValue == originalValues[provider.Name][descriptor.Name])
+ continue; // Skip unchanged values
+ }
+
+ // Update the setting
+ updatedSettings[provider.Name][descriptor.Name] = newValue;
+ }
+ }
+
+ // Update settings
+ settings.ProviderSettings = updatedSettings;
+ settings.DebounceTime = (int)debounceControl.Value;
+
+ // Save default provider
+ if (defaultProviderComboBox.SelectedIndex >= 0)
+ settings.DefaultAIProvider = defaultProviderComboBox.Items[defaultProviderComboBox.SelectedIndex].Text;
+
+ // Save settings (this will handle encryption)
+ settings.Save();
+ dialog.Close();
+ };
- settings.Save();
+ cancelButton.Click += (sender, e) => dialog.Close();
+
+ // Show the dialog
+ dialog.ShowModal();
}
- }
+ }));
}
}
}