Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
publish:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true')
if: github.event_name == 'release' || github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true')
timeout-minutes: 15
permissions:
id-token: write
Expand Down
22 changes: 11 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ jobs:
prompt-file: /tmp/prompts/prompt-1.txt

- name: Save pass 1 response
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true'
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true' && steps.ai_pass1.conclusion == 'success'
run: |
cp "${{ steps.ai_pass1.outputs.response-file }}" /tmp/pass-1.txt
sleep 65
Expand All @@ -338,7 +338,7 @@ jobs:
prompt-file: /tmp/prompts/prompt-2.txt

- name: Save pass 2 response
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true'
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true' && steps.ai_pass2.conclusion == 'success'
run: |
cp "${{ steps.ai_pass2.outputs.response-file }}" /tmp/pass-2.txt
sleep 65
Expand All @@ -354,7 +354,7 @@ jobs:
prompt-file: /tmp/prompts/prompt-3.txt

- name: Save pass 3 response
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true'
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true' && steps.ai_pass3.conclusion == 'success'
run: |
cp "${{ steps.ai_pass3.outputs.response-file }}" /tmp/pass-3.txt
sleep 65
Expand All @@ -370,7 +370,7 @@ jobs:
prompt-file: /tmp/prompts/prompt-4.txt

- name: Save pass 4 response
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true'
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true' && steps.ai_pass4.conclusion == 'success'
run: |
cp "${{ steps.ai_pass4.outputs.response-file }}" /tmp/pass-4.txt
sleep 65
Expand All @@ -386,7 +386,7 @@ jobs:
prompt-file: /tmp/prompts/prompt-5.txt

- name: Save pass 5 response
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true'
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true' && steps.ai_pass5.conclusion == 'success'
run: |
cp "${{ steps.ai_pass5.outputs.response-file }}" /tmp/pass-5.txt
sleep 65
Expand All @@ -402,7 +402,7 @@ jobs:
prompt-file: /tmp/prompts/prompt-6.txt

- name: Save pass 6 response
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true'
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true' && steps.ai_pass6.conclusion == 'success'
run: |
cp "${{ steps.ai_pass6.outputs.response-file }}" /tmp/pass-6.txt
sleep 65
Expand Down Expand Up @@ -446,7 +446,7 @@ jobs:
prompt-file: /tmp/prompt-merge-a.txt

- name: Save merge-A response
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true'
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true' && steps.ai_merge_a.conclusion == 'success'
run: |
cp "${{ steps.ai_merge_a.outputs.response-file }}" /tmp/merge-a.txt
sleep 65
Expand Down Expand Up @@ -479,7 +479,7 @@ jobs:
prompt-file: /tmp/prompt-compress-a.txt

- name: Save compress-A response
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true'
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true' && steps.ai_compress_a.conclusion == 'success'
run: |
cp "${{ steps.ai_compress_a.outputs.response-file }}" /tmp/compress-a.txt
sleep 65
Expand Down Expand Up @@ -523,7 +523,7 @@ jobs:
prompt-file: /tmp/prompt-merge-b.txt

- name: Save merge-B response
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true'
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true' && steps.ai_merge_b.conclusion == 'success'
run: |
cp "${{ steps.ai_merge_b.outputs.response-file }}" /tmp/merge-b.txt
sleep 65
Expand Down Expand Up @@ -556,7 +556,7 @@ jobs:
prompt-file: /tmp/prompt-compress-b.txt

- name: Save compress-B response
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true'
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true' && steps.ai_compress_b.conclusion == 'success'
run: |
cp "${{ steps.ai_compress_b.outputs.response-file }}" /tmp/compress-b.txt
sleep 65
Expand Down Expand Up @@ -641,7 +641,7 @@ jobs:
prompt-file: /tmp/prompt-final.txt

- name: Save final merge response
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true'
if: steps.tag_check.outputs.exists == 'false' && github.event.inputs.skip_ai_notes != 'true' && steps.ai_merge.conclusion == 'success'
run: cp "${{ steps.ai_merge.outputs.response-file }}" /tmp/final-merge.txt
- name: Write release notes to file
if: steps.tag_check.outputs.exists == 'false'
Expand Down
2 changes: 1 addition & 1 deletion build/Package.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>default</LangVersion>
<PackageVersion>5.1.3-alpha2</PackageVersion>
<PackageVersion>5.1.3-alpha2-fix</PackageVersion>
<Authors>Dmitry Zhutkov (Onebeld), Andrey Savich (pieckenst)</Authors>
<Copyright>Dmitry Zhutkov (Onebeld)</Copyright>
<PackageTags>theme, design, xaml, library, ui, gui, control, csharp, styled-components, interface, dotnet, nuget, style, avalonia, controls, user-interface, styles, avaloniaui, pleasant, graphical-user-interface</PackageTags>
Expand Down
4 changes: 4 additions & 0 deletions samples/PleasantUI.Example/MainView.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ private void OnNavigationSelectionChanged(object? sender, SelectionChangedEventA
// stays visible, without going through SelectionChanged again.
MainNavigationView.SelectionChanged -= OnNavigationSelectionChanged;
MainNavigationView.SelectedItem = HomeNavItem;
// HomeNavItem must not appear highlighted while a leaf page is active.
HomeNavItem.IsSelected = false;
// Re-highlight the leaf item — SelectSingleItemCore cleared it when we redirected to HomeNavItem.
selected.IsSelected = true;
MainNavigationView.SelectionChanged += OnNavigationSelectionChanged;
}

Expand Down
92 changes: 79 additions & 13 deletions src/PleasantUI/Controls/NavigationView/NavigationView.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Input;
using Avalonia;
using Avalonia.Animation;
Expand Down Expand Up @@ -52,6 +53,10 @@ public class NavigationView : TreeView

private ILogical? _logicalSelectedContent;

// Stores the IsExpanded state of group items before the pane collapses to compact mode,
// keyed by the NavigationViewItem instance so each item's state is tracked independently.
private readonly Dictionary<NavigationViewItem, bool> _expandedStates = new();

/// <summary>
/// Defines the <see cref="Icon" /> property.
/// </summary>
Expand Down Expand Up @@ -380,6 +385,7 @@ static NavigationView()
{
SelectionModeProperty.OverrideDefaultValue<NavigationView>(SelectionMode.Single);
SelectedItemProperty.Changed.AddClassHandler<NavigationView>((x, _) => x.OnSelectedItemChanged());
IsOpenProperty.Changed.AddClassHandler<NavigationView>((x, e) => x.OnIsOpenChanged(e));
}

/// <summary>
Expand All @@ -397,18 +403,20 @@ public NavigationView()
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
Debug.WriteLine("[NavigationView] OnApplyTemplate");
base.OnApplyTemplate(e);

_stackPanelButtons = e.NameScope.Find<StackPanel>("PART_StackPanelButtons");
_headerItem = e.NameScope.Find<Button>("PART_HeaderItem");
_backButton = e.NameScope.Find<Button>("PART_BackButton");
_contentPresenter = e.NameScope.Find<ContentPresenter>("PART_SelectedContentPresenter");

_container = e.NameScope.Find<Border>("PART_Container");
_mainGrid = e.NameScope.Find<Grid>("PART_SplitViewGrid");
_dockPanel = e.NameScope.Find<DockPanel>("PART_ItemsPresenterDockPanel");
_marginPanel = e.NameScope.Find<Border>("PART_MarginPanel");

Debug.WriteLine($"[NavigationView] OnApplyTemplate parts: headerItem={_headerItem is not null} backButton={_backButton is not null} contentPresenter={_contentPresenter is not null}");

if (_headerItem != null)
{
_headerItem.Click += (_, _) => IsOpen = AlwaysOpen || !IsOpen;
Expand All @@ -423,7 +431,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
if (TopLevel.GetTopLevel(this) is PleasantWindow window)
{
titleBarHeight = window.TitleBarHeight;

Debug.WriteLine($"[NavigationView] OnApplyTemplate PleasantWindow found titleBarHeight={titleBarHeight}");
UpdateMacNavigationLayout(window);
UpdateContainerTitleHeight(window);
}
Expand All @@ -434,12 +442,13 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
Debug.WriteLine($"[NavigationView] OnLoaded itemCount={Items.Count} firstItem={(Items.Count > 0 ? (Items[0] as NavigationViewItem)?.Header : "none")}");

// Hack. For some reason it does not highlight the first item in the list after running the program
if (Items.Count > 0)
{
if (Items[0] is ISelectable selectableItem)
{
Debug.WriteLine($"[NavigationView] OnLoaded selecting first item header={(selectableItem as NavigationViewItem)?.Header}");
SelectSingleItem(selectableItem, false);
}
}
Expand All @@ -449,15 +458,18 @@ protected override void OnLoaded(RoutedEventArgs e)
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
Debug.WriteLine($"[NavigationView] OnAttachedToLogicalTree itemCount={Items.Count}");

if (Items.Count > 0 && Items[0] is ISelectable selectableItem)
{
Debug.WriteLine($"[NavigationView] OnAttachedToLogicalTree selecting first item header={(selectableItem as NavigationViewItem)?.Header}");
SelectSingleItem(selectableItem);
}
}

internal void SelectSingleItem(ISelectable item, bool runAnimation = true)
{
Debug.WriteLine($"[NavigationView] SelectSingleItem header={(item as NavigationViewItem)?.Header} runAnimation={runAnimation}");
// Always run deselection of other items, even if this item is already selected
CloseAllSubMenuPopups();
SelectSingleItemCore(item, runAnimation);
Expand Down Expand Up @@ -498,49 +510,55 @@ private void UpdateMacNavigationLayout(PleasantWindow window)

private void UpdateContainerTitleHeight(PleasantWindow window)
{
if (_container == null)
return;
if (_container == null) return;

// Determine margin based on custom title bar setting.
Thickness margin = window.EnableCustomTitleBar ? new Thickness(8, titleBarHeight + 1, 8, 8) : new Thickness(0);
Debug.WriteLine($"[NavigationView] UpdateContainerTitleHeight enableCustomTitleBar={window.EnableCustomTitleBar} margin={margin}");

_container.CornerRadius = new CornerRadius(8);

_container.Margin = margin;
}
private void OnBoundsChanged(Rect rect)
{
// Ignore zero-size bounds — this fires during initial layout before the window is rendered.
// Acting on it would corrupt the saved expanded state of all items.
if (rect.Width <= 0)
{
Debug.WriteLine($"[NavigationView] OnBoundsChanged width={rect.Width:F0} → ignored (zero width, initial layout)");
return;
}

if (DynamicDisplayMode)
{
bool isLittle = rect.Width <= LittleWidth;
bool isVeryLittle = rect.Width <= VeryLittleWidth;

if (!isLittle && !isVeryLittle)
{
Debug.WriteLine($"[NavigationView] OnBoundsChanged width={rect.Width:F0} → CompactInline IsOpen stays");
UpdatePseudoClasses(false);
DisplayMode = SplitViewDisplayMode.CompactInline;
}
else if (isLittle && !isVeryLittle)
{
Debug.WriteLine($"[NavigationView] OnBoundsChanged width={rect.Width:F0} → CompactOverlay IsOpen=false");
UpdatePseudoClasses(false);
DisplayMode = SplitViewDisplayMode.CompactOverlay;
IsOpen = false;
foreach (NavigationViewItem navigationViewItem in this.GetLogicalDescendants()
.OfType<NavigationViewItem>()) navigationViewItem.IsExpanded = false;
}
else if (isLittle && isVeryLittle)
{
Debug.WriteLine($"[NavigationView] OnBoundsChanged width={rect.Width:F0} → Overlay IsOpen=false");
UpdatePseudoClasses(true);
DisplayMode = SplitViewDisplayMode.Overlay;
IsOpen = false;
foreach (NavigationViewItem navigationViewItem in this.GetLogicalDescendants()
.OfType<NavigationViewItem>()) navigationViewItem.IsExpanded = false;
}
}
}

private void SelectSingleItemCore(object? item, bool runAnimation = true)
{
Debug.WriteLine($"[NavigationView] SelectSingleItemCore item={(item as NavigationViewItem)?.Header} tag={(item as NavigationViewItem)?.Tag}");
if (SelectedItem != item && TransitionAnimation is not null && _contentPresenter is not null && runAnimation)
{
_cancellationTokenSource?.Cancel();
Expand All @@ -563,6 +581,7 @@ private void SelectSingleItemCore(object? item, bool runAnimation = true)

private void UpdatePseudoClasses(bool isCompact)
{
Debug.WriteLine($"[NavigationView] UpdatePseudoClasses isCompact={isCompact}");
switch (isCompact)
{
case true:
Expand All @@ -577,16 +596,60 @@ private void UpdatePseudoClasses(bool isCompact)
private void UpdateTitleAndSelectedContent()
{
if (SelectedItem is not NavigationViewItem item) return;
Debug.WriteLine($"[NavigationView] UpdateTitleAndSelectedContent selectedItem header={item.Header} hasContent={item.Content is not null}");
if (item.Content is not null)
SelectedContent = item.Content;
}

private void OnSelectedItemChanged()
{
Debug.WriteLine($"[NavigationView] OnSelectedItemChanged → SelectedItem={(SelectedItem as NavigationViewItem)?.Header}");
UpdateTitleAndSelectedContent();
}

/// <summary>
/// Called when the pane IsOpen state changes.
/// Saves IsExpanded state of all group items when collapsing to compact mode,
/// and restores it when expanding back to full mode.
/// </summary>
private void OnIsOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
bool isNowOpen = (bool)(e.NewValue ?? false);
Debug.WriteLine($"[NavigationView] OnIsOpenChanged isNowOpen={isNowOpen}");

// Only group items (items that have NavigationViewItem children) participate in expand/collapse
var groupItems = this.GetLogicalDescendants()
.OfType<NavigationViewItem>()
.Where(item => item.Items.OfType<NavigationViewItem>().Any())
.ToList();

if (!isNowOpen)
{
// Pane collapsing → save IsExpanded for each group item, then collapse it
_expandedStates.Clear();
foreach (var item in groupItems)
{
_expandedStates[item] = item.IsExpanded;
Debug.WriteLine($"[NavigationView] OnIsOpenChanged saving header={item.Header} IsExpanded={item.IsExpanded}");
if (item.IsExpanded)
item.IsExpanded = false;
}
}
else
{
// Pane opening → restore saved IsExpanded for each group item
foreach (var item in groupItems)
{
if (_expandedStates.TryGetValue(item, out bool wasExpanded))
{
Debug.WriteLine($"[NavigationView] OnIsOpenChanged restoring header={item.Header} wasExpanded={wasExpanded}");
item.IsExpanded = wasExpanded;
}
}
_expandedStates.Clear();
}
}

/// <summary>
/// Closes all open submenu popups in the navigation view.
/// Called when a selection is made to ensure popups are dismissed.
Expand All @@ -596,7 +659,10 @@ private void CloseAllSubMenuPopups()
foreach (var item in this.GetLogicalDescendants().OfType<NavigationViewItem>())
{
if (item.IsSubMenuOpen)
{
Debug.WriteLine($"[NavigationView] CloseAllSubMenuPopups closing popup on header={item.Header}");
item.IsSubMenuOpen = false;
}
}
}
}
Loading
Loading