You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
310 lines
8.9 KiB
310 lines
8.9 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.ComponentModel; |
|
using System.Diagnostics; |
|
using System.Linq; |
|
using System.Runtime.CompilerServices; |
|
using System.Text; |
|
using System.Text.Json; |
|
using Avalonia; |
|
using Avalonia.Controls; |
|
using Avalonia.Data; |
|
using Avalonia.Media; |
|
using Avalonia.Threading; |
|
using AvaloniaEdit; |
|
using AvaloniaEdit.TextMate; |
|
using CommunityToolkit.Mvvm.ComponentModel.__Internals; |
|
using CommunityToolkit.Mvvm.Input; |
|
using FluentAvalonia.UI.Controls; |
|
using Markdown.Avalonia; |
|
using Markdown.Avalonia.SyntaxHigh.Extensions; |
|
using Refit; |
|
using StabilityMatrix.Avalonia.Controls; |
|
using TextMateSharp.Grammars; |
|
|
|
namespace StabilityMatrix.Avalonia; |
|
|
|
public static class DialogHelper |
|
{ |
|
/// <summary> |
|
/// Create a generic textbox entry content dialog. |
|
/// </summary> |
|
public static BetterContentDialog CreateTextEntryDialog( |
|
string title, |
|
string description, |
|
IReadOnlyList<TextBoxField> textFields) |
|
{ |
|
Dispatcher.UIThread.VerifyAccess(); |
|
|
|
var stackPanel = new StackPanel(); |
|
var grid = new Grid |
|
{ |
|
RowDefinitions = |
|
{ |
|
new RowDefinition(GridLength.Auto), |
|
new RowDefinition(GridLength.Star) |
|
}, |
|
Children = |
|
{ |
|
new TextBlock |
|
{ |
|
Text = description |
|
}, |
|
stackPanel |
|
} |
|
}; |
|
grid.Loaded += (_, _) => |
|
{ |
|
// Focus first textbox |
|
var firstTextBox = stackPanel.Children.OfType<TextBox>().First(); |
|
firstTextBox.Focus(); |
|
firstTextBox.CaretIndex = firstTextBox.Text?.LastIndexOf('.') ?? 0; |
|
}; |
|
|
|
// Disable primary button if any textboxes are invalid |
|
var primaryCommand = new RelayCommand(delegate { }, |
|
() => |
|
{ |
|
var invalidCount = textFields.Count(field => !field.IsValid); |
|
Debug.WriteLine($"Checking can execute: {invalidCount} invalid fields"); |
|
return invalidCount == 0; |
|
}); |
|
|
|
// Create textboxes |
|
foreach (var field in textFields) |
|
{ |
|
var label = new TextBlock |
|
{ |
|
Text = field.Label |
|
}; |
|
stackPanel.Children.Add(label); |
|
|
|
var textBox = new TextBox |
|
{ |
|
[!TextBox.TextProperty] = new Binding("TextProperty"), |
|
Watermark = field.Watermark, |
|
DataContext = field, |
|
}; |
|
stackPanel.Children.Add(textBox); |
|
|
|
// When IsValid property changes, update invalid count and primary button |
|
field.PropertyChanged += (_, args) => |
|
{ |
|
if (args.PropertyName == nameof(TextBoxField.IsValid)) |
|
{ |
|
primaryCommand.NotifyCanExecuteChanged(); |
|
} |
|
}; |
|
|
|
// Set initial value |
|
textBox.Text = field.Text; |
|
|
|
// See if initial value is valid |
|
try |
|
{ |
|
field.Validator?.Invoke(field.Text); |
|
} |
|
catch (Exception) |
|
{ |
|
field.IsValid = false; |
|
} |
|
} |
|
|
|
return new BetterContentDialog |
|
{ |
|
Title = title, |
|
Content = grid, |
|
PrimaryButtonText = "OK", |
|
CloseButtonText = "Cancel", |
|
IsPrimaryButtonEnabled = true, |
|
PrimaryButtonCommand = primaryCommand, |
|
DefaultButton = ContentDialogButton.Primary |
|
}; |
|
} |
|
|
|
/// <summary> |
|
/// Create a generic dialog for showing a markdown document |
|
/// </summary> |
|
public static BetterContentDialog CreateMarkdownDialog(string markdown, string? title = null) |
|
{ |
|
Dispatcher.UIThread.VerifyAccess(); |
|
|
|
var viewer = new MarkdownScrollViewer |
|
{ |
|
Markdown = markdown |
|
}; |
|
|
|
return new BetterContentDialog |
|
{ |
|
Title = title, |
|
Content = viewer, |
|
CloseButtonText = "Close", |
|
IsPrimaryButtonEnabled = false, |
|
}; |
|
} |
|
|
|
/// <summary> |
|
/// Create a dialog for displaying an ApiException |
|
/// </summary> |
|
public static BetterContentDialog CreateApiExceptionDialog(ApiException exception, string? title = null) |
|
{ |
|
Dispatcher.UIThread.VerifyAccess(); |
|
|
|
// Setup text editor |
|
var textEditor = new TextEditor |
|
{ |
|
IsReadOnly = true, |
|
WordWrap = true, |
|
Options = |
|
{ |
|
ShowColumnRulers = false, |
|
AllowScrollBelowDocument = false |
|
} |
|
}; |
|
var registryOptions = new RegistryOptions(ThemeName.DarkPlus); |
|
textEditor.InstallTextMate(registryOptions).SetGrammar(registryOptions.GetScopeByLanguageId("json")); |
|
|
|
var mainGrid = new StackPanel |
|
{ |
|
Spacing = 8, |
|
Margin = new Thickness(16), |
|
Children = |
|
{ |
|
new TextBlock |
|
{ |
|
Text = $"{(int) exception.StatusCode} - {exception.ReasonPhrase}", |
|
FontSize = 18, |
|
FontWeight = FontWeight.Medium, |
|
Margin = new Thickness(0,8), |
|
}, |
|
textEditor |
|
} |
|
}; |
|
|
|
var dialog = new BetterContentDialog |
|
{ |
|
Title = title, |
|
Content = mainGrid, |
|
CloseButtonText = "Close", |
|
IsPrimaryButtonEnabled = false, |
|
}; |
|
|
|
// Try to deserialize to json element |
|
if (exception.Content != null) |
|
{ |
|
try |
|
{ |
|
// Deserialize to json element then re-serialize to ensure indentation |
|
var jsonElement = JsonSerializer.Deserialize<JsonElement>(exception.Content, new JsonSerializerOptions |
|
{ |
|
AllowTrailingCommas = true, |
|
ReadCommentHandling = JsonCommentHandling.Skip |
|
}); |
|
var formatted = JsonSerializer.Serialize(jsonElement, new JsonSerializerOptions() |
|
{ |
|
WriteIndented = true |
|
}); |
|
|
|
textEditor.Document.Text = formatted; |
|
} |
|
catch (JsonException) |
|
{ |
|
// Otherwise just add the content as a code block |
|
textEditor.Document.Text = exception.Content; |
|
} |
|
} |
|
|
|
return dialog; |
|
} |
|
|
|
/// <summary> |
|
/// Create a simple title and description task dialog. |
|
/// Sets the XamlRoot to the current top level window. |
|
/// </summary> |
|
public static TaskDialog CreateTaskDialog(string title, string description) |
|
{ |
|
Dispatcher.UIThread.VerifyAccess(); |
|
|
|
var content = new StackPanel |
|
{ |
|
Children = |
|
{ |
|
new TextBlock |
|
{ |
|
Margin = new Thickness(0,0,0,8), |
|
FontSize = 16, |
|
Text = title, |
|
TextWrapping = TextWrapping.WrapWithOverflow, |
|
}, |
|
new TextBlock |
|
{ |
|
Text = description, |
|
TextWrapping = TextWrapping.WrapWithOverflow, |
|
} |
|
} |
|
}; |
|
|
|
return new TaskDialog |
|
{ |
|
Title = title, |
|
Content = content, |
|
XamlRoot = App.VisualRoot |
|
}; |
|
} |
|
} |
|
|
|
// Text fields |
|
public sealed class TextBoxField : INotifyPropertyChanged |
|
{ |
|
// Label above the textbox |
|
public string Label { get; init; } = string.Empty; |
|
// Actual text value |
|
public string Text { get; set; } = string.Empty; |
|
// Watermark text |
|
public string Watermark { get; init; } = string.Empty; |
|
|
|
/// <summary> |
|
/// Validation action on text changes. Throw exception if invalid. |
|
/// </summary> |
|
public Action<string>? Validator { get; init; } |
|
|
|
public string TextProperty |
|
{ |
|
get => Text; |
|
[DebuggerStepThrough] |
|
set |
|
{ |
|
try |
|
{ |
|
Validator?.Invoke(value); |
|
} |
|
catch (Exception e) |
|
{ |
|
IsValid = false; |
|
throw new DataValidationException(e.Message); |
|
} |
|
Text = value; |
|
IsValid = true; |
|
OnPropertyChanged(); |
|
} |
|
} |
|
|
|
// Default to true if no validator is provided |
|
private bool isValid; |
|
public bool IsValid |
|
{ |
|
get => Validator == null || isValid; |
|
set |
|
{ |
|
isValid = value; |
|
OnPropertyChanged(); |
|
} |
|
} |
|
|
|
public event PropertyChangedEventHandler? PropertyChanged; |
|
|
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) |
|
{ |
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); |
|
} |
|
}
|
|
|