fix(ui): restore the Settings modal body and tidy the Prime schedule row

The re-templated TabControl dropped PART_ItemsPresenter, so no TabItem
containers were realized and SelectedContent stayed null -> empty body.
Keep the presenter collapsed instead. Also let the last-run label trim
with a tooltip rather than reserving a fixed MinWidth.
This commit is contained in:
mika kuns
2026-08-26 10:27:49 +02:00
parent 296251e27e
commit d71c0ceaef
2 changed files with 18 additions and 6 deletions
+1
View File
@@ -113,6 +113,7 @@ snaps `CanResize="True"` windows, which is the opt-in), and it insets itself by
- **`PathIcon` *fills* its geometry.** Line-art/stroke icons must be authored as filled geometry or rendered with a stroked `Path` (e.g. `Icon.PlanDay` via the `Path.plan-icon` style). A pure stroke path in a `PathIcon` is **invisible**. - **`PathIcon` *fills* its geometry.** Line-art/stroke icons must be authored as filled geometry or rendered with a stroked `Path` (e.g. `Icon.PlanDay` via the `Path.plan-icon` style). A pure stroke path in a `PathIcon` is **invisible**.
- **`NumericUpDown.Value` is `decimal?` and goes null while the box is empty** — i.e. every time the user clears a value to type a new one. Bound TwoWay to a non-nullable `int`/`decimal`, that null throws `InvalidCastException`. Either bind a `decimal?` property (as `AgentConfigEditorViewModel.MaxTurns` does) or add `Converter={StaticResource KeepLastNumber}`, which drops the null via `BindingOperations.DoNothing`. - **`NumericUpDown.Value` is `decimal?` and goes null while the box is empty** — i.e. every time the user clears a value to type a new one. Bound TwoWay to a non-nullable `int`/`decimal`, that null throws `InvalidCastException`. Either bind a `decimal?` property (as `AgentConfigEditorViewModel.MaxTurns` does) or add `Converter={StaticResource KeepLastNumber}`, which drops the null via `BindingOperations.DoNothing`.
- **`ScrollViewer.Padding` is excluded from the Extent (Avalonia 12).** At max scroll the content still sticks out below the viewport by the padding height, so the last line(s) are clipped and unreachable — even after `ScrollToEnd()`. Put the inset on the *content* (`Margin` on the inner `ItemsControl`/`StackPanel`) instead. Verified headlessly: 12,8,12,4 on the viewer → last item 20px below the viewport; same inset as content margin → fully visible. **`ListBox` does *not* have this trap** — its Fluent control-theme template forwards `Padding` onto `PART_ItemsPresenter`'s margin (content side), not the raw `ScrollViewer.Padding`; verified headlessly that `ListBox.Padding="10,4"` and a `Margin="10,4"` on the `ItemsPanelTemplate` panel produce an *identical* Extent and leave the last row fully visible at max scroll. Still fine to put the inset on the panel for consistency with the other scrollables above, just don't assume it's fixing a real bug there. - **`ScrollViewer.Padding` is excluded from the Extent (Avalonia 12).** At max scroll the content still sticks out below the viewport by the padding height, so the last line(s) are clipped and unreachable — even after `ScrollToEnd()`. Put the inset on the *content* (`Margin` on the inner `ItemsControl`/`StackPanel`) instead. Verified headlessly: 12,8,12,4 on the viewer → last item 20px below the viewport; same inset as content margin → fully visible. **`ListBox` does *not* have this trap** — its Fluent control-theme template forwards `Padding` onto `PART_ItemsPresenter`'s margin (content side), not the raw `ScrollViewer.Padding`; verified headlessly that `ListBox.Padding="10,4"` and a `Margin="10,4"` on the `ItemsPanelTemplate` panel produce an *identical* Extent and leave the last row fully visible at max scroll. Still fine to put the inset on the panel for consistency with the other scrollables above, just don't assume it's fixing a real bug there.
- **Re-templating a `TabControl` must keep `PART_ItemsPresenter`.** `TabControl.SelectedContent` is resolved via `ContainerFromIndex`, and containers are only realized by the `ItemsPresenter`. A template that renders just `PART_SelectedContentHost` (to hide the tab strip) leaves `SelectedContent` permanently `null` → an empty body. Keep the presenter with `IsVisible="False"`; that still realizes containers (verified headlessly, Avalonia 12.0.4). This broke the whole Settings modal body in 56c3719e.
- **Never bind bare punctuation gestures.** Window key bindings live on `MainWindow` (`Ctrl+K` search, `Ctrl+N` add-task). `OemQuestion` once held search focus and silently swallowed `#` app-wide on a German layout. - **Never bind bare punctuation gestures.** Window key bindings live on `MainWindow` (`Ctrl+K` search, `Ctrl+N` add-task). `OemQuestion` once held search focus and silently swallowed `#` app-wide on a German layout.
- **`FocusClearing`'s Escape handler is scoped to `MainWindow`** (`AddClassHandler<MainWindow>`, not `<TopLevel>`) — it clears focus from a TextBox on Escape, mirroring click-outside. Modals are separate `Window` instances that bind their own Escape → close, so it never runs there. Mission Control's ConPTY tiles are in `MissionControlWindow`, also unaffected, so **Escape always reaches the PTY**. - **`FocusClearing`'s Escape handler is scoped to `MainWindow`** (`AddClassHandler<MainWindow>`, not `<TopLevel>`) — it clears focus from a TextBox on Escape, mirroring click-outside. Modals are separate `Window` instances that bind their own Escape → close, so it never runs there. Mission Control's ConPTY tiles are in `MissionControlWindow`, also unaffected, so **Escape always reaches the PTY**.
- **Review gate:** Approve & Merge stays disabled until the diff has been opened once, and re-locks per run → [review-merge](../../docs/explore-notes/review-merge.md). - **Review gate:** Approve & Merge stays disabled until the diff has been opened once, and re-locks per run → [review-merge](../../docs/explore-notes/review-merge.md).
@@ -85,13 +85,23 @@
<Border Grid.Column="1" Background="{DynamicResource LineBrush}" Width="1" Margin="0,8"/> <Border Grid.Column="1" Background="{DynamicResource LineBrush}" Width="1" Margin="0,8"/>
<!-- TabControl stays the content host (SelectedIndex driven by the sidebar); its own <!-- TabControl stays the content host (SelectedIndex driven by the sidebar); its own
header strip is re-templated away so only PART_SelectedContentHost renders. --> header strip is re-templated away so only PART_SelectedContentHost renders.
The PART_ItemsPresenter must stay in the template even though it is collapsed:
TabControl.SelectedContent is resolved via ContainerFromIndex, and without a
presenter the TabItem containers are never realized -> SelectedContent is null
and the modal body renders empty. -->
<TabControl Grid.Column="2" Padding="20,16" SelectedIndex="{Binding SelectedIndex, Mode=OneWay}"> <TabControl Grid.Column="2" Padding="20,16" SelectedIndex="{Binding SelectedIndex, Mode=OneWay}">
<TabControl.Template> <TabControl.Template>
<ControlTemplate TargetType="TabControl"> <ControlTemplate TargetType="TabControl">
<Panel>
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}"
IsVisible="False"/>
<ContentPresenter Name="PART_SelectedContentHost" <ContentPresenter Name="PART_SelectedContentHost"
Padding="{TemplateBinding Padding}"
Content="{TemplateBinding SelectedContent}" Content="{TemplateBinding SelectedContent}"
ContentTemplate="{TemplateBinding SelectedContentTemplate}"/> ContentTemplate="{TemplateBinding SelectedContentTemplate}"/>
</Panel>
</ControlTemplate> </ControlTemplate>
</TabControl.Template> </TabControl.Template>
@@ -268,7 +278,7 @@
CornerRadius="6" Padding="10,8" Margin="0,0,0,8" CornerRadius="6" Padding="10,8" Margin="0,0,0,8"
Background="{DynamicResource DeepBrush}"> Background="{DynamicResource DeepBrush}">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" ColumnSpacing="8"> <Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto" ColumnSpacing="8">
<CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/> <CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center"> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayMo}" IsChecked="{Binding Monday, Mode=TwoWay}"/> <ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayMo}" IsChecked="{Binding Monday, Mode=TwoWay}"/>
@@ -284,7 +294,8 @@
PlaceholderText="HH:mm" MaxLength="5" PlaceholderText="HH:mm" MaxLength="5"
Width="68" VerticalAlignment="Center"/> Width="68" VerticalAlignment="Center"/>
<TextBlock Classes="meta" Grid.Column="3" Text="{Binding LastRunLabel}" VerticalAlignment="Center" <TextBlock Classes="meta" Grid.Column="3" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
MinWidth="80"/> TextTrimming="CharacterEllipsis"
ToolTip.Tip="{Binding LastRunLabel}"/>
<Button Classes="icon-btn" Grid.Column="4" Content="✕" <Button Classes="icon-btn" Grid.Column="4" Content="✕"
ToolTip.Tip="{loc:Tr settings.prime.removeScheduleTip}" ToolTip.Tip="{loc:Tr settings.prime.removeScheduleTip}"
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}" Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"