feat(ui): add subtask tree view with expand/collapse in task list

Tasks with subtasks show a chevron for inline expand/collapse.
Subtask checkboxes toggle completion state directly. Also sets
Windows AppUserModelID for proper taskbar identity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-16 12:16:22 +02:00
parent 4ca48044db
commit 32bb52875f
5 changed files with 234 additions and 67 deletions

View File

@@ -31,72 +31,128 @@
KeyDown="OnTaskListKeyDown">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:TaskItemViewModel">
<Grid ColumnDefinitions="Auto,*" Margin="4,4"
<Grid RowDefinitions="Auto,Auto"
Background="Transparent"
Opacity="{Binding RowOpacity}"
DoubleTapped="OnTaskItemDoubleTapped"
PointerPressed="OnTaskItemPointerPressed">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Edit"
Command="{Binding #Root.((vm:TaskListViewModel)DataContext).EditTaskCommand}"/>
<MenuItem Header="Delete"
Command="{Binding #Root.((vm:TaskListViewModel)DataContext).DeleteTaskCommand}"/>
<Separator/>
<MenuItem Header="Run Now"
Command="{Binding RunNowCommand}"/>
</MenuFlyout>
</Grid.ContextFlyout>
Opacity="{Binding RowOpacity}">
<!-- Row 0: Task row -->
<Grid Grid.Row="0" ColumnDefinitions="20,Auto,*" Margin="4,4"
DoubleTapped="OnTaskItemDoubleTapped"
PointerPressed="OnTaskItemPointerPressed">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Edit"
Command="{Binding #Root.((vm:TaskListViewModel)DataContext).EditTaskCommand}"/>
<MenuItem Header="Delete"
Command="{Binding #Root.((vm:TaskListViewModel)DataContext).DeleteTaskCommand}"/>
<Separator/>
<MenuItem Header="Run Now"
Command="{Binding RunNowCommand}"/>
</MenuFlyout>
</Grid.ContextFlyout>
<!-- Circular checkbox -->
<Border Grid.Column="0" Width="22" Height="22"
CornerRadius="11"
BorderThickness="2"
BorderBrush="{Binding StatusText, Converter={x:Static conv:CheckboxBorderConverter.Instance}}"
Background="Transparent"
VerticalAlignment="Center" Margin="0,0,10,0"
Cursor="Hand"
PointerPressed="OnCheckboxPressed">
<Panel>
<!-- Checkmark for done -->
<Canvas Width="12" Height="12"
IsVisible="{Binding IsDone}"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Path Stroke="{StaticResource StatusGreenBrush}" StrokeThickness="2"
Data="M 1,6 L 4.5,9.5 L 11,3"/>
</Canvas>
<!-- Running dot -->
<Ellipse Width="8" Height="8"
Fill="{StaticResource StatusOrangeBrush}"
IsVisible="{Binding IsRunning}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<!-- Starting dot -->
<Ellipse Width="8" Height="8" Fill="#FFD700"
IsVisible="{Binding IsStarting}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Panel>
</Border>
<!-- Expand/collapse chevron -->
<Button Grid.Column="0"
Command="{Binding ToggleExpandedCommand}"
IsVisible="{Binding HasSubtasks}"
Background="Transparent"
BorderThickness="0"
Padding="0"
Width="16" Height="16"
VerticalAlignment="Center"
Cursor="Hand">
<Panel>
<Canvas Width="10" Height="10"
IsVisible="{Binding !IsExpanded}">
<Path Stroke="{StaticResource TextDimBrush}" StrokeThickness="1.5"
Data="M 2,0 L 8,5 L 2,10"/>
</Canvas>
<Canvas Width="10" Height="10"
IsVisible="{Binding IsExpanded}">
<Path Stroke="{StaticResource TextDimBrush}" StrokeThickness="1.5"
Data="M 0,2 L 5,8 L 10,2"/>
</Canvas>
</Panel>
</Button>
<!-- Task content -->
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="{Binding Title}" FontWeight="Medium"
Foreground="{Binding TitleForeground}"
TextDecorations="{Binding TitleDecorations}"
TextTrimming="CharacterEllipsis"/>
<TextBlock FontSize="11"
Foreground="{StaticResource TextDimBrush}"
IsVisible="{Binding TagsText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} · {1}">
<Binding Path="TagsText"/>
<Binding Path="StatusText"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock Text="{Binding StatusText}" FontSize="11"
Foreground="{StaticResource TextDimBrush}"
IsVisible="{Binding TagsText, Converter={x:Static StringConverters.IsNullOrEmpty}}"/>
</StackPanel>
<!-- Circular checkbox -->
<Border Grid.Column="1" Width="22" Height="22"
CornerRadius="11"
BorderThickness="2"
BorderBrush="{Binding StatusText, Converter={x:Static conv:CheckboxBorderConverter.Instance}}"
Background="Transparent"
VerticalAlignment="Center" Margin="0,0,10,0"
Cursor="Hand"
PointerPressed="OnCheckboxPressed">
<Panel>
<Canvas Width="12" Height="12"
IsVisible="{Binding IsDone}"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Path Stroke="{StaticResource StatusGreenBrush}" StrokeThickness="2"
Data="M 1,6 L 4.5,9.5 L 11,3"/>
</Canvas>
<Ellipse Width="8" Height="8"
Fill="{StaticResource StatusOrangeBrush}"
IsVisible="{Binding IsRunning}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Ellipse Width="8" Height="8" Fill="#FFD700"
IsVisible="{Binding IsStarting}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Panel>
</Border>
<!-- Task content -->
<StackPanel Grid.Column="2" VerticalAlignment="Center">
<TextBlock Text="{Binding Title}" FontWeight="Medium"
Foreground="{Binding TitleForeground}"
TextDecorations="{Binding TitleDecorations}"
TextTrimming="CharacterEllipsis"/>
<TextBlock FontSize="11"
Foreground="{StaticResource TextDimBrush}"
IsVisible="{Binding TagsText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} · {1}">
<Binding Path="TagsText"/>
<Binding Path="StatusText"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock Text="{Binding StatusText}" FontSize="11"
Foreground="{StaticResource TextDimBrush}"
IsVisible="{Binding TagsText, Converter={x:Static StringConverters.IsNullOrEmpty}}"/>
</StackPanel>
</Grid>
<!-- Row 1: Subtask list (visible when expanded) -->
<ItemsControl Grid.Row="1"
ItemsSource="{Binding Subtasks}"
IsVisible="{Binding IsExpanded}"
Margin="40,0,0,4">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:SubtaskItemViewModel">
<Grid ColumnDefinitions="Auto,*" Margin="0,2"
PointerPressed="OnSubtaskPointerPressed">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Edit Task"
Command="{Binding #Root.((vm:TaskListViewModel)DataContext).EditTaskCommand}"/>
</MenuFlyout>
</Grid.ContextFlyout>
<CheckBox Grid.Column="0"
IsChecked="{Binding Completed, Mode=OneWay}"
VerticalAlignment="Center"
Margin="0,0,6,0"
MinWidth="0"
Click="OnSubtaskCheckboxClick"/>
<TextBlock Grid.Column="1"
Text="{Binding Title}"
FontSize="12"
Foreground="{StaticResource TextDimBrush}"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>

View File

@@ -1,3 +1,4 @@
using System.Collections.ObjectModel;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
@@ -97,6 +98,29 @@ public partial class TaskListView : UserControl
}
}
private void OnSubtaskPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(null).Properties.IsRightButtonPressed
&& sender is Control { DataContext: SubtaskItemViewModel subtask }
&& DataContext is TaskListViewModel vm)
{
var parent = vm.Tasks.FirstOrDefault(t => t.Subtasks.Contains(subtask));
if (parent is not null)
vm.SelectedTask = parent;
}
}
private async void OnSubtaskCheckboxClick(object? sender, RoutedEventArgs e)
{
if (sender is CheckBox { DataContext: SubtaskItemViewModel subtask }
&& DataContext is TaskListViewModel vm)
{
var parent = vm.Tasks.FirstOrDefault(t => t.Subtasks.Contains(subtask));
if (parent is not null)
await parent.ToggleSubtaskDoneCommand.ExecuteAsync(subtask.Id);
}
}
public void FocusInlineAdd()
{
this.FindControl<TextBox>("InlineAddBox")?.Focus();