fix(notes): let Shift+Enter break the line, and clear the leftovers around the editor

Shift+Enter did nothing because AcceptsReturn was only set from a style
setter and never reached the box. Setting it on the element is half the
fix: TextBox.OnKeyDown consumes Enter regardless of modifiers and runs
before any bubbling handler, so the split chain would have died in its
place. Enter is now taken in the tunnel phase and Shift+Enter is left to
the box.

Alongside, the notes list stops borrowing furniture it has no use for:
no "0 open tasks" subtitle, no show-completed and list-settings buttons,
and no details pane beside it — the editor spans that column instead of
leaving it empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
CubeGameLP
2026-08-28 21:36:28 +02:00
co-authored by Claude Opus 5
parent 0f04b796f4
commit b90c5855f4
6 changed files with 38 additions and 9 deletions
@@ -808,7 +808,8 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
var running = Items.Count(i => i.Status == TaskStatus.Running); var running = Items.Count(i => i.Status == TaskStatus.Running);
var review = Items.Count(i => i.Status == TaskStatus.Done && i.Branch != null); var review = Items.Count(i => i.Status == TaskStatus.Done && i.Branch != null);
Subtitle = open == 1 ? "1 open task" : $"{open} open tasks"; // The notes list holds no tasks — an "0 open tasks" line under its header is just noise.
Subtitle = IsNotesList ? "" : open == 1 ? "1 open task" : $"{open} open tasks";
if (running > 0 || review > 0) if (running > 0 || review > 0)
{ {
@@ -145,7 +145,14 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
[ObservableProperty] [ObservableProperty]
private bool _isWorkerLogVisible; private bool _isWorkerLogVisible;
public bool ShowDetails => WindowWidth >= 1100; /// <summary>The notes list has nothing to show a detail pane for, so the pane goes away and
/// the editor takes the space instead of leaving an empty column beside it.</summary>
public bool ShowDetails => WindowWidth >= 1100 && Tasks?.IsNotesList != true;
/// <summary>Widens the tasks island over the splitter and the details column when that column
/// is hidden — a fixed-width ColumnDefinition keeps its space even with an invisible child.</summary>
public int TasksColumnSpan => ShowDetails ? 1 : 3;
public bool ShowLists => WindowWidth >= 780; public bool ShowLists => WindowWidth >= 780;
private readonly System.Timers.Timer _clearTimer = new(30_000) { AutoReset = false }; private readonly System.Timers.Timer _clearTimer = new(30_000) { AutoReset = false };
@@ -168,10 +175,16 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
partial void OnWindowWidthChanged(double value) partial void OnWindowWidthChanged(double value)
{ {
OnPropertyChanged(nameof(ShowDetails)); NotifyDetailsVisibility();
OnPropertyChanged(nameof(ShowLists)); OnPropertyChanged(nameof(ShowLists));
} }
private void NotifyDetailsVisibility()
{
OnPropertyChanged(nameof(ShowDetails));
OnPropertyChanged(nameof(TasksColumnSpan));
}
public void OnWorkerLogReceived(WorkerLogEntry entry) public void OnWorkerLogReceived(WorkerLogEntry entry)
{ {
var hhmm = entry.TimestampUtc.ToLocalTime().ToString("HH:mm"); var hhmm = entry.TimestampUtc.ToLocalTime().ToString("HH:mm");
@@ -341,6 +354,10 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
Lists.SelectionChanged += (_, _) => _ = RefreshStaleWorkerCheckAsync(); Lists.SelectionChanged += (_, _) => _ = RefreshStaleWorkerCheckAsync();
Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask, Tasks.SelectionSource); Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask, Tasks.SelectionSource);
Tasks.PrepRequested += () => Details.ShowPrep(); Tasks.PrepRequested += () => Details.ShowPrep();
Tasks.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(TasksIslandViewModel.IsNotesList)) NotifyDetailsVisibility();
};
Tasks.ErrorReported += FlashFooterError; Tasks.ErrorReported += FlashFooterError;
Lists.ErrorReported += FlashFooterError; Lists.ErrorReported += FlashFooterError;
Lists.SuccessReported += FlashFooterSuccess; Lists.SuccessReported += FlashFooterSuccess;
@@ -13,8 +13,6 @@
<Setter Property="BorderThickness" Value="0"/> <Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="4,3"/> <Setter Property="Padding" Value="4,3"/>
<Setter Property="MinHeight" Value="0"/> <Setter Property="MinHeight" Value="0"/>
<Setter Property="AcceptsReturn" Value="True"/>
<Setter Property="TextWrapping" Value="Wrap"/>
</Style> </Style>
<Style Selector="TextBox.note /template/ Border#PART_BorderElement"> <Style Selector="TextBox.note /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
@@ -77,8 +75,13 @@
<Ellipse Grid.Column="0" Width="4" Height="4" Margin="0,0,10,0" <Ellipse Grid.Column="0" Width="4" Height="4" Margin="0,0,10,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Fill="{DynamicResource TextFaintBrush}"/> Fill="{DynamicResource TextFaintBrush}"/>
<!-- AcceptsReturn/TextWrapping are set here, not in the style above: TextBox
reads AcceptsReturn in its own OnKeyDown, which runs before any handler a
style or the XAML could attach. Enter is intercepted in the tunnel phase
(see code-behind) so the split still wins over the newline. -->
<TextBox Grid.Column="1" Classes="note" Text="{Binding Text, Mode=TwoWay}" <TextBox Grid.Column="1" Classes="note" Text="{Binding Text, Mode=TwoWay}"
LostFocus="OnBulletLostFocus" KeyDown="OnBulletKeyDown"/> AcceptsReturn="True" TextWrapping="Wrap"
LostFocus="OnBulletLostFocus"/>
<PathIcon Grid.Column="2" Width="12" Height="12" Margin="6,0" <PathIcon Grid.Column="2" Width="12" Height="12" Margin="6,0"
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding FromAgent}" IsVisible="{Binding FromAgent}"
@@ -15,6 +15,10 @@ public partial class NotesEditorView : UserControl
{ {
InitializeComponent(); InitializeComponent();
DataContextChanged += OnDataContextChanged; DataContextChanged += OnDataContextChanged;
// Tunnel, not bubble: with AcceptsReturn on, TextBox.OnKeyDown consumes Enter (any
// modifier) before a bubbling handler ever sees it. Taking Enter in the tunnel phase
// keeps the split chain, and letting Shift+Enter through leaves the newline to the box.
AddHandler(KeyDownEvent, OnBulletKeyDown, RoutingStrategies.Tunnel);
} }
private void OnDataContextChanged(object? sender, EventArgs e) private void OnDataContextChanged(object? sender, EventArgs e)
@@ -33,7 +37,9 @@ public partial class NotesEditorView : UserControl
private void OnBulletKeyDown(object? sender, KeyEventArgs e) private void OnBulletKeyDown(object? sender, KeyEventArgs e)
{ {
if (sender is not TextBox { DataContext: NoteBulletViewModel bullet } box // Source, not sender: the tunnel handler is attached to the whole view, so this also sees
// keys from the capture box at the top — which has no bullet behind it and is skipped.
if (e.Source is not TextBox { DataContext: NoteBulletViewModel bullet } box
|| DataContext is not NotesEditorViewModel vm) return; || DataContext is not NotesEditorViewModel vm) return;
// Shift+Enter falls through to the TextBox and breaks the line — a note can be several. // Shift+Enter falls through to the TextBox and breaks the line — a note can be several.
@@ -31,6 +31,7 @@
<TextBlock Text="{Binding StatusPill}"/> <TextBlock Text="{Binding StatusPill}"/>
</Border> </Border>
<Button Classes="icon-btn" Classes.active="{Binding IsShowingCompleted}" <Button Classes="icon-btn" Classes.active="{Binding IsShowingCompleted}"
IsVisible="{Binding !IsNotesList}"
Command="{Binding ToggleShowCompletedCommand}" Command="{Binding ToggleShowCompletedCommand}"
ToolTip.Tip="{loc:Tr tasks.showCompletedTip}"> ToolTip.Tip="{loc:Tr tasks.showCompletedTip}">
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Eye}"/> <PathIcon Width="15" Height="15" Data="{StaticResource Icon.Eye}"/>
@@ -60,7 +61,8 @@
<Path Classes="icon-stroke" Data="{StaticResource Icon.PlanDay}"/> <Path Classes="icon-stroke" Data="{StaticResource Icon.PlanDay}"/>
</Viewbox> </Viewbox>
</Button> </Button>
<Button Classes="icon-btn" Command="{Binding OpenListSettingsCommand}" ToolTip.Tip="{loc:Tr tasks.listSettingsTip}"> <Button Classes="icon-btn" IsVisible="{Binding !IsNotesList}"
Command="{Binding OpenListSettingsCommand}" ToolTip.Tip="{loc:Tr tasks.listSettingsTip}">
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Settings}"/> <PathIcon Width="15" Height="15" Data="{StaticResource Icon.Settings}"/>
</Button> </Button>
</StackPanel> </StackPanel>
+1 -1
View File
@@ -144,7 +144,7 @@
ResizeDirection="Columns" ResizeDirection="Columns"
ResizeBehavior="PreviousAndNext"/> ResizeBehavior="PreviousAndNext"/>
<Border Grid.Column="2" Classes="island" Margin="3"> <Border Grid.Column="2" Grid.ColumnSpan="{Binding TasksColumnSpan}" Classes="island" Margin="3">
<islands:TasksIslandView DataContext="{Binding Tasks}"/> <islands:TasksIslandView DataContext="{Binding Tasks}"/>
</Border> </Border>