Skip to content

Commit 868a420

Browse files
committed
add notice dialog and sidebar wizard controls
1 parent 6a5ac18 commit 868a420

6 files changed

Lines changed: 1307 additions & 0 deletions

File tree

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
using Avalonia;
2+
using Avalonia.Controls;
3+
using Avalonia.Controls.Metadata;
4+
using Avalonia.Controls.Primitives;
5+
using Avalonia.Interactivity;
6+
using PleasantUI.Controls;
7+
8+
namespace PleasantUI.ToolKit.Controls;
9+
10+
/// <summary>
11+
/// Severity level for the notice dialog.
12+
/// </summary>
13+
public enum NoticeSeverity
14+
{
15+
/// <summary>Informational notice (blue).</summary>
16+
Info,
17+
/// <summary>Warning notice (yellow/orange).</summary>
18+
Warning,
19+
/// <summary>Error notice (red).</summary>
20+
Error,
21+
/// <summary>Success notice (green).</summary>
22+
Success,
23+
/// <summary>Work in progress notice (yellow).</summary>
24+
WorkInProgress
25+
}
26+
27+
/// <summary>
28+
/// A modal dialog for displaying notices, warnings, or work-in-progress messages.
29+
/// Features a header with icon, message body, optional footer text, and customizable action buttons.
30+
/// </summary>
31+
[TemplatePart(PART_PrimaryButton, typeof(Button))]
32+
[TemplatePart(PART_SecondaryButton, typeof(Button))]
33+
[PseudoClasses(PC_HasSecondary, PC_HasFooter)]
34+
public class NoticeDialog : PleasantPopupElement
35+
{
36+
// ── Template part names ───────────────────────────────────────────────────
37+
38+
internal const string PART_PrimaryButton = "PART_PrimaryButton";
39+
internal const string PART_SecondaryButton = "PART_SecondaryButton";
40+
41+
// ── Pseudo-class names ────────────────────────────────────────────────────
42+
43+
private const string PC_HasSecondary = ":hasSecondary";
44+
private const string PC_HasFooter = ":hasFooter";
45+
46+
// ── Styled properties ─────────────────────────────────────────────────────
47+
48+
/// <summary>Defines the <see cref="Title"/> property.</summary>
49+
public static readonly StyledProperty<string?> TitleProperty =
50+
AvaloniaProperty.Register<NoticeDialog, string?>(nameof(Title));
51+
52+
/// <summary>Defines the <see cref="Message"/> property.</summary>
53+
public static readonly StyledProperty<string?> MessageProperty =
54+
AvaloniaProperty.Register<NoticeDialog, string?>(nameof(Message));
55+
56+
/// <summary>Defines the <see cref="FooterText"/> property.</summary>
57+
public static readonly StyledProperty<string?> FooterTextProperty =
58+
AvaloniaProperty.Register<NoticeDialog, string?>(nameof(FooterText));
59+
60+
/// <summary>Defines the <see cref="PrimaryButtonText"/> property.</summary>
61+
public static readonly StyledProperty<string?> PrimaryButtonTextProperty =
62+
AvaloniaProperty.Register<NoticeDialog, string?>(nameof(PrimaryButtonText));
63+
64+
/// <summary>Defines the <see cref="SecondaryButtonText"/> property.</summary>
65+
public static readonly StyledProperty<string?> SecondaryButtonTextProperty =
66+
AvaloniaProperty.Register<NoticeDialog, string?>(nameof(SecondaryButtonText));
67+
68+
/// <summary>Defines the <see cref="Severity"/> property.</summary>
69+
public static readonly StyledProperty<NoticeSeverity> SeverityProperty =
70+
AvaloniaProperty.Register<NoticeDialog, NoticeSeverity>(nameof(Severity), defaultValue: NoticeSeverity.Info);
71+
72+
/// <summary>Defines the <see cref="MinDialogWidth"/> property.</summary>
73+
public static readonly StyledProperty<double> MinDialogWidthProperty =
74+
AvaloniaProperty.Register<NoticeDialog, double>(nameof(MinDialogWidth), defaultValue: 400);
75+
76+
/// <summary>Defines the <see cref="MinDialogHeight"/> property.</summary>
77+
public static readonly StyledProperty<double> MinDialogHeightProperty =
78+
AvaloniaProperty.Register<NoticeDialog, double>(nameof(MinDialogHeight), defaultValue: 300);
79+
80+
// ── CLR accessors ─────────────────────────────────────────────────────────
81+
82+
/// <summary>Gets or sets the dialog title.</summary>
83+
public string? Title
84+
{
85+
get => GetValue(TitleProperty);
86+
set => SetValue(TitleProperty, value);
87+
}
88+
89+
/// <summary>Gets or sets the main message text.</summary>
90+
public string? Message
91+
{
92+
get => GetValue(MessageProperty);
93+
set => SetValue(MessageProperty, value);
94+
}
95+
96+
/// <summary>Gets or sets optional footer text (e.g., attribution or additional info).</summary>
97+
public string? FooterText
98+
{
99+
get => GetValue(FooterTextProperty);
100+
set => SetValue(FooterTextProperty, value);
101+
}
102+
103+
/// <summary>Gets or sets the text of the primary action button. Null hides the button.</summary>
104+
public string? PrimaryButtonText
105+
{
106+
get => GetValue(PrimaryButtonTextProperty);
107+
set => SetValue(PrimaryButtonTextProperty, value);
108+
}
109+
110+
/// <summary>Gets or sets the text of the secondary action button. Null hides the button.</summary>
111+
public string? SecondaryButtonText
112+
{
113+
get => GetValue(SecondaryButtonTextProperty);
114+
set => SetValue(SecondaryButtonTextProperty, value);
115+
}
116+
117+
/// <summary>Gets or sets the severity level, which affects the icon and header color.</summary>
118+
public NoticeSeverity Severity
119+
{
120+
get => GetValue(SeverityProperty);
121+
set => SetValue(SeverityProperty, value);
122+
}
123+
124+
/// <summary>Gets or sets the minimum width of the dialog.</summary>
125+
public double MinDialogWidth
126+
{
127+
get => GetValue(MinDialogWidthProperty);
128+
set => SetValue(MinDialogWidthProperty, value);
129+
}
130+
131+
/// <summary>Gets or sets the minimum height of the dialog.</summary>
132+
public double MinDialogHeight
133+
{
134+
get => GetValue(MinDialogHeightProperty);
135+
set => SetValue(MinDialogHeightProperty, value);
136+
}
137+
138+
// ── Events ────────────────────────────────────────────────────────────────
139+
140+
/// <summary>Raised when the primary button is clicked.</summary>
141+
public event EventHandler? PrimaryButtonClicked;
142+
143+
/// <summary>Raised when the secondary button is clicked.</summary>
144+
public event EventHandler? SecondaryButtonClicked;
145+
146+
/// <summary>Raised when the dialog is closed.</summary>
147+
public event EventHandler? Closed;
148+
149+
// ── Private state ─────────────────────────────────────────────────────────
150+
151+
private Button? _primaryButton;
152+
private Button? _secondaryButton;
153+
private Border? _modalBackground;
154+
private Panel? _panel;
155+
private bool _isClosing;
156+
157+
// ── Constructor ───────────────────────────────────────────────────────────
158+
159+
public NoticeDialog()
160+
{
161+
}
162+
163+
// ── Template ──────────────────────────────────────────────────────────────
164+
165+
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
166+
{
167+
base.OnApplyTemplate(e);
168+
169+
DetachHandlers();
170+
171+
_primaryButton = e.NameScope.Find<Button>(PART_PrimaryButton);
172+
_secondaryButton = e.NameScope.Find<Button>(PART_SecondaryButton);
173+
174+
AttachHandlers();
175+
UpdatePseudoClasses();
176+
}
177+
178+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
179+
{
180+
base.OnPropertyChanged(change);
181+
182+
if (change.Property == SecondaryButtonTextProperty)
183+
PseudoClasses.Set(PC_HasSecondary, change.NewValue is not null);
184+
else if (change.Property == FooterTextProperty)
185+
PseudoClasses.Set(PC_HasFooter, change.NewValue is not null);
186+
}
187+
188+
// ── Public API ────────────────────────────────────────────────────────────
189+
190+
/// <summary>Shows the dialog on the specified <see cref="TopLevel"/>.</summary>
191+
public async Task ShowAsync(TopLevel? topLevel = null)
192+
{
193+
_panel = new Panel();
194+
195+
_modalBackground = new Border
196+
{
197+
Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#3A000000")),
198+
Opacity = 0
199+
};
200+
201+
_panel.Children.Add(_modalBackground);
202+
_panel.Children.Add(this);
203+
204+
Host ??= new ModalWindowHost();
205+
Host.Content = _panel;
206+
207+
base.ShowCoreForTopLevel(topLevel);
208+
209+
PseudoClasses.Set(":open", true);
210+
211+
// Fade in modal background
212+
if (_modalBackground is not null)
213+
{
214+
var bgAnim = new Animation
215+
{
216+
Duration = TimeSpan.FromMilliseconds(200),
217+
FillMode = Avalonia.Animation.FillMode.Forward
218+
};
219+
var kf = new KeyFrame { Cue = new Cue(1.0) };
220+
kf.Setters.Add(new Setter(OpacityProperty, 1.0));
221+
bgAnim.Children.Add(kf);
222+
await bgAnim.RunAsync(_modalBackground);
223+
}
224+
}
225+
226+
/// <summary>Closes the dialog.</summary>
227+
public async Task CloseAsync()
228+
{
229+
if (_isClosing) return;
230+
_isClosing = true;
231+
232+
PseudoClasses.Set(":open", false);
233+
base.DeleteCoreForTopLevel();
234+
235+
_isClosing = false;
236+
Closed?.Invoke(this, EventArgs.Empty);
237+
}
238+
239+
// ── Private helpers ───────────────────────────────────────────────────────
240+
241+
private void AttachHandlers()
242+
{
243+
if (_primaryButton is not null) _primaryButton.Click += OnPrimaryClicked;
244+
if (_secondaryButton is not null) _secondaryButton.Click += OnSecondaryClicked;
245+
}
246+
247+
private void DetachHandlers()
248+
{
249+
if (_primaryButton is not null) _primaryButton.Click -= OnPrimaryClicked;
250+
if (_secondaryButton is not null) _secondaryButton.Click -= OnSecondaryClicked;
251+
}
252+
253+
private void OnPrimaryClicked(object? s, RoutedEventArgs e) => PrimaryButtonClicked?.Invoke(this, EventArgs.Empty);
254+
private void OnSecondaryClicked(object? s, RoutedEventArgs e) => SecondaryButtonClicked?.Invoke(this, EventArgs.Empty);
255+
256+
private void UpdatePseudoClasses()
257+
{
258+
PseudoClasses.Set(PC_HasSecondary, SecondaryButtonText is not null);
259+
PseudoClasses.Set(PC_HasFooter, FooterText is not null);
260+
}
261+
}

0 commit comments

Comments
 (0)