feat(ui): Task-Zeile entdichten und Kontextmenü gruppieren (UX-Struktur A)
Gruppe 1 (Send to queue, Remove from queue, Cancel execution, Open quick session, Refine task) ist jetzt fix sichtbar und gated per IsEnabled + Grund statt komplett auszublenden. Planning/Schedule wandern in Untermenüs (Mark as existierte bereits); der Planning-Kopf zeigt einen Grund, wenn er sonst leer wäre. Refine-Button, ungesetzter Star und Dequeue-X erscheinen nur bei Hover-oder-Selektion (TaskRowViewModel.ShowRowActions); ein gesetzter Star und der Refining-Spinner bleiben immer sichtbar. Der Branch-Chip wandert aus der Zeile in eine Meta-Zeile in TaskHeaderBar. A3 headless verifiziert: Avalonia 12 zeigt ToolTips auf IsEnabled=false Controls nur mit ToolTip.ShowOnDisabled="True" (Default ist false) — dieses Attached Property existiert bereits und wird für die neuen Disabled-Reason- Tooltips genutzt.
This commit is contained in:
@@ -25,6 +25,16 @@
|
||||
TextWrapping="Wrap"
|
||||
AcceptsReturn="False"
|
||||
Padding="0"/>
|
||||
|
||||
<!-- Branch meta line: moved here from the task row's chip (A5) — the row no longer
|
||||
shows the branch anywhere else. -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" Margin="0,4,0,0"
|
||||
IsVisible="{Binding Task.HasBranch}">
|
||||
<PathIcon Width="10" Height="10"
|
||||
Data="{StaticResource Icon.GitBranch}"
|
||||
Foreground="{DynamicResource TextDimBrush}"/>
|
||||
<TextBlock Classes="meta" Text="{Binding Task.Branch}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Column 1: trash button (not running) -->
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
Classes.dragging="{Binding IsDragging}"
|
||||
Classes.drop-target="{Binding IsDropTarget}"
|
||||
Classes.done="{Binding Done}"
|
||||
ContextRequested="OnRowContextRequested">
|
||||
ContextRequested="OnRowContextRequested"
|
||||
PointerEntered="OnRowPointerEntered"
|
||||
PointerExited="OnRowPointerExited">
|
||||
<Grid ColumnDefinitions="0,18,32,*,Auto,Auto,32" Margin="6,8,10,8">
|
||||
|
||||
<!-- Chevron toggle (only for planning parent tasks) -->
|
||||
@@ -149,12 +151,18 @@
|
||||
<TextBlock Text="{Binding CreationPhaseLabel}"/>
|
||||
</Border>
|
||||
|
||||
<!-- Dequeue button (visible when row is Queued, or planning parent has queued subtasks) -->
|
||||
<!-- Dequeue button (visible when row is Queued, or planning parent has queued subtasks,
|
||||
AND the row is hovered or selected) -->
|
||||
<Button Classes="icon-btn dequeue-btn"
|
||||
IsVisible="{Binding CanRemoveFromQueue}"
|
||||
ToolTip.Tip="{loc:Tr tasks.removeFromQueueTip}"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).RemoveFromQueueCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<Button.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Path="CanRemoveFromQueue"/>
|
||||
<Binding Path="ShowRowActions"/>
|
||||
</MultiBinding>
|
||||
</Button.IsVisible>
|
||||
<PathIcon Width="10" Height="10" Data="{StaticResource Icon.X}"/>
|
||||
</Button>
|
||||
|
||||
@@ -168,16 +176,6 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Branch chip -->
|
||||
<Border Classes="chip chip-branch" IsVisible="{Binding HasBranch}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
|
||||
<PathIcon Width="10" Height="10"
|
||||
Data="{StaticResource Icon.GitBranch}"
|
||||
Foreground="{DynamicResource TextDimBrush}"/>
|
||||
<TextBlock Text="{Binding Branch}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Diff chip -->
|
||||
<Border Classes="chip chip-diff" IsVisible="{Binding HasDiff}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
|
||||
@@ -203,11 +201,16 @@
|
||||
<!-- Refine button, replaced by a spinner while the refine run is in flight -->
|
||||
<Panel Grid.Column="5">
|
||||
<Button Classes="icon-btn refine-btn"
|
||||
IsVisible="{Binding CanRefine}"
|
||||
VerticalAlignment="Top" Margin="0,2,0,0"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).RefineTaskCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
ToolTip.Tip="{loc:Tr tasks.refineTip}">
|
||||
<Button.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Path="CanRefine"/>
|
||||
<Binding Path="ShowRowActions"/>
|
||||
</MultiBinding>
|
||||
</Button.IsVisible>
|
||||
<Viewbox Width="16" Height="16">
|
||||
<Path Classes="plan-icon" Data="{StaticResource Icon.Refine}"/>
|
||||
</Viewbox>
|
||||
@@ -218,13 +221,19 @@
|
||||
ToolTip.Tip="{loc:Tr tasks.refiningTip}"/>
|
||||
</Panel>
|
||||
|
||||
<!-- Star toggle -->
|
||||
<!-- Star toggle: a set star always shows; unset, only on hover/selection -->
|
||||
<Button Grid.Column="6" Classes="icon-btn star-btn"
|
||||
Classes.on="{Binding IsStarred}"
|
||||
VerticalAlignment="Top" Margin="0,2,0,0"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).ToggleStarCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
ToolTip.Tip="{loc:Tr details.starTip}">
|
||||
<Button.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.Or}">
|
||||
<Binding Path="IsStarred"/>
|
||||
<Binding Path="ShowRowActions"/>
|
||||
</MultiBinding>
|
||||
</Button.IsVisible>
|
||||
<PathIcon Width="14" Height="14" Data="{StaticResource Icon.Star}"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
@@ -21,8 +21,8 @@ public partial class TaskRowView : UserControl
|
||||
this.GetVisualAncestors().OfType<ItemsControl>()
|
||||
.Select(ic => ic.DataContext).OfType<TasksIslandViewModel>().FirstOrDefault();
|
||||
|
||||
// The context menu is built here instead of declared in XAML so the ~18 MenuItems only
|
||||
// exist while a row's menu is actually open, not for every row rendered in the list.
|
||||
// The context menu is built here instead of declared in XAML so the MenuItems only exist
|
||||
// while a row's menu is actually open, not for every row rendered in the list.
|
||||
private void OnRowContextRequested(object? sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
if (sender is not Border border) return;
|
||||
@@ -35,19 +35,36 @@ public partial class TaskRowView : UserControl
|
||||
|
||||
var menu = new ContextMenu { DataContext = row };
|
||||
|
||||
MenuItem MakeItem(string key, EventHandler<RoutedEventArgs> click, bool isVisible = true, string? tag = null)
|
||||
// reason non-null => IsEnabled=false with that text as the tooltip (ToolTip.ShowOnDisabled
|
||||
// renders it even though the item is disabled — verified headlessly on Avalonia 12, see
|
||||
// task A3). Used for Group 1's four gated entries; submenu items keep isVisible hiding.
|
||||
MenuItem MakeItem(string key, EventHandler<RoutedEventArgs> click, bool isVisible = true, string? tag = null, string? reason = null)
|
||||
{
|
||||
var item = new MenuItem { Header = Loc.T(key), DataContext = row, IsVisible = isVisible };
|
||||
if (tag is not null) item.Tag = tag;
|
||||
if (reason is not null)
|
||||
{
|
||||
item.IsEnabled = false;
|
||||
ToolTip.SetTip(item, reason);
|
||||
ToolTip.SetShowOnDisabled(item, true);
|
||||
}
|
||||
item.Click += click;
|
||||
return item;
|
||||
}
|
||||
|
||||
menu.Items.Add(MakeItem("tasks.ctxSendToQueue", OnSendToQueueClick, row.CanSendToQueue));
|
||||
menu.Items.Add(MakeItem("tasks.ctxRemoveFromQueue", OnRemoveFromQueueClick, row.CanRemoveFromQueue));
|
||||
menu.Items.Add(MakeItem("tasks.ctxCancelExecution", OnCancelExecutionClick, row.IsRunning));
|
||||
// Group 1 — fixed length and order, always visible; an unmet precondition grays the
|
||||
// entry out instead of hiding it (Remove from queue has no dedicated reason text, only
|
||||
// the IsEnabled toggle).
|
||||
menu.Items.Add(MakeItem("tasks.ctxSendToQueue", OnSendToQueueClick, reason: row.SendToQueueDisabledReason));
|
||||
var removeFromQueueItem = MakeItem("tasks.ctxRemoveFromQueue", OnRemoveFromQueueClick);
|
||||
removeFromQueueItem.IsEnabled = row.CanRemoveFromQueue;
|
||||
menu.Items.Add(removeFromQueueItem);
|
||||
menu.Items.Add(MakeItem("tasks.ctxCancelExecution", OnCancelExecutionClick, reason: row.CancelDisabledReason));
|
||||
menu.Items.Add(MakeItem("tasks.ctxOpenConPtySession", OnOpenConPtySessionClick, reason: row.QuickSessionDisabledReason));
|
||||
menu.Items.Add(MakeItem("tasks.ctxRefineTask", OnRefineTaskClick, reason: row.RefineDisabledReason));
|
||||
menu.Items.Add(new Separator());
|
||||
|
||||
// "Mark as" — exclusive-choice hiding inside is unchanged; the header is never empty.
|
||||
var markAs = new MenuItem { Header = Loc.T("tasks.ctxMarkAs"), DataContext = row };
|
||||
markAs.Items.Add(MakeItem("tasks.ctxMarkDone", OnSetStatusClick, tag: "Done"));
|
||||
markAs.Items.Add(MakeItem("tasks.ctxMarkCancelled", OnSetStatusClick, tag: "Cancelled"));
|
||||
@@ -56,17 +73,29 @@ public partial class TaskRowView : UserControl
|
||||
markAs.Items.Add(MakeItem("tasks.ctxMarkClaudeTask", OnToggleManualClick, row.IsManual));
|
||||
menu.Items.Add(markAs);
|
||||
|
||||
menu.Items.Add(new Separator());
|
||||
menu.Items.Add(MakeItem("tasks.ctxOpenConPtySession", OnOpenConPtySessionClick));
|
||||
menu.Items.Add(MakeItem("tasks.ctxOpenPlanningSession", OnOpenPlanningSessionClick, row.CanOpenPlanningSession));
|
||||
menu.Items.Add(MakeItem("tasks.ctxResumePlanningSession", OnResumePlanningSessionClick, row.CanResumeOrDiscardPlanning));
|
||||
menu.Items.Add(MakeItem("tasks.ctxFinalizePlanningSession", OnFinalizePlanningSessionClick, row.CanFinalizePlanning));
|
||||
menu.Items.Add(MakeItem("tasks.ctxDiscardPlanningSession", OnDiscardPlanningSessionClick, row.CanResumeOrDiscardPlanning));
|
||||
menu.Items.Add(new Separator());
|
||||
menu.Items.Add(MakeItem("tasks.ctxScheduleFor", OnScheduleForClick));
|
||||
menu.Items.Add(MakeItem("tasks.ctxClearSchedule", OnClearScheduleClick, row.HasSchedule));
|
||||
menu.Items.Add(MakeItem("tasks.ctxAddToMyDay", OnAddToMyDayClick, row.CanAddToMyDay));
|
||||
menu.Items.Add(MakeItem("tasks.ctxRemoveFromMyDay", OnRemoveFromMyDayClick, row.IsMyDay));
|
||||
// "Planning" — the header itself disables + explains when it would otherwise open empty.
|
||||
var planning = new MenuItem { Header = Loc.T("tasks.ctxPlanning"), DataContext = row };
|
||||
if (row.PlanningDisabledReason is { } planningReason)
|
||||
{
|
||||
planning.IsEnabled = false;
|
||||
ToolTip.SetTip(planning, planningReason);
|
||||
ToolTip.SetShowOnDisabled(planning, true);
|
||||
}
|
||||
planning.Items.Add(MakeItem("tasks.ctxOpenPlanningSession", OnOpenPlanningSessionClick, row.CanOpenPlanningSession));
|
||||
planning.Items.Add(MakeItem("tasks.ctxResumePlanningSession", OnResumePlanningSessionClick, row.CanResumeOrDiscardPlanning));
|
||||
planning.Items.Add(MakeItem("tasks.ctxFinalizePlanningSession", OnFinalizePlanningSessionClick, row.CanFinalizePlanning));
|
||||
planning.Items.Add(MakeItem("tasks.ctxDiscardPlanningSession", OnDiscardPlanningSessionClick, row.CanResumeOrDiscardPlanning));
|
||||
menu.Items.Add(planning);
|
||||
|
||||
// "Schedule" — always has at least "Schedule for…", so the header is never empty.
|
||||
var schedule = new MenuItem { Header = Loc.T("tasks.ctxSchedule"), DataContext = row };
|
||||
schedule.Items.Add(MakeItem("tasks.ctxScheduleFor", OnScheduleForClick));
|
||||
schedule.Items.Add(MakeItem("tasks.ctxClearSchedule", OnClearScheduleClick, row.HasSchedule));
|
||||
schedule.Items.Add(new Separator());
|
||||
schedule.Items.Add(MakeItem("tasks.ctxAddToMyDay", OnAddToMyDayClick, row.CanAddToMyDay));
|
||||
schedule.Items.Add(MakeItem("tasks.ctxRemoveFromMyDay", OnRemoveFromMyDayClick, row.IsMyDay));
|
||||
menu.Items.Add(schedule);
|
||||
|
||||
menu.Items.Add(new Separator());
|
||||
menu.Items.Add(MakeItem("tasks.ctxDeleteTask", OnDeleteTaskClick));
|
||||
|
||||
@@ -92,6 +121,12 @@ public partial class TaskRowView : UserControl
|
||||
await vm.CancelRunningTaskCommand.ExecuteAsync(row);
|
||||
}
|
||||
|
||||
private async void OnRefineTaskClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||
await vm.RefineTaskCommand.ExecuteAsync(row);
|
||||
}
|
||||
|
||||
private async void OnClearScheduleClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||
@@ -194,4 +229,14 @@ public partial class TaskRowView : UserControl
|
||||
ScheduleAnchor.Flyout?.Hide();
|
||||
_pendingScheduleRow = null;
|
||||
}
|
||||
|
||||
private void OnRowPointerEntered(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskRowViewModel row) row.IsHovered = true;
|
||||
}
|
||||
|
||||
private void OnRowPointerExited(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskRowViewModel row) row.IsHovered = false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user