Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
134 commits
Select commit Hold shift + click to select a range
5b2dac5
Add MetricFormatter for costs and durations that reach a human
erikdarlingdata Sep 14, 2026
1b38c9a
Route the cost and duration display sites through MetricFormatter
erikdarlingdata Sep 14, 2026
8910b5f
Blank the Query Store ids on grouped rows instead of showing 0
erikdarlingdata Sep 14, 2026
b5ba82e
Keep the properties panel at the width the user dragged it to
erikdarlingdata Sep 14, 2026
435e12d
Give code values the full panel width instead of a 180px column
erikdarlingdata Sep 14, 2026
ff7f1d9
Roll the per-thread stats into one collapsed breakdown per section
erikdarlingdata Sep 14, 2026
217c580
Give property values a copy menu instead of the stock text-box one
erikdarlingdata Sep 14, 2026
e156e56
Add a filter box to the properties panel
erikdarlingdata Sep 14, 2026
645716a
Keep Ctrl+C guarded and the splitter hover themeable
erikdarlingdata Sep 14, 2026
3a386b9
Stop exception text from parking in the session status strip
erikdarlingdata Sep 14, 2026
4814508
Disable Compare Plans when there is no second plan to compare
erikdarlingdata Sep 14, 2026
2027db4
Size the Query Editor tab header like its siblings
erikdarlingdata Sep 14, 2026
cee2423
Drop the duplicated connection row from embedded plan viewers
erikdarlingdata Sep 14, 2026
179f4c0
Offer the primary actions on an empty query tab
erikdarlingdata Sep 14, 2026
da1c310
Stop the Settings dialog from starting out dirty
erikdarlingdata Sep 14, 2026
fe86bed
Show the Settings window as a modal dialog
erikdarlingdata Sep 14, 2026
3ca13c8
Title the column filter popup with the column header
erikdarlingdata Sep 14, 2026
ac968a5
Make Connect a single step in the connection dialog
erikdarlingdata Sep 14, 2026
c44c421
Load a Query Store plan on double-click
erikdarlingdata Sep 14, 2026
f4399c8
Latch the Settings close walk and wait on Closed
erikdarlingdata Sep 14, 2026
91a2ec5
Extend the clipboard guard to SelectableTextBlock app-wide
erikdarlingdata Sep 14, 2026
ea10645
Report a Query Store batch's failures after the whole batch
erikdarlingdata Sep 14, 2026
3e05501
Apply the three Low findings from the integration review
erikdarlingdata Sep 14, 2026
1e3be89
Focus the column filter's value box once the popup is open
erikdarlingdata Sep 14, 2026
0e328db
Focus the existing tab when a plan file is reopened
erikdarlingdata Sep 14, 2026
6f0a721
Give the error dialog an OK button and Esc/Enter dismissal
erikdarlingdata Sep 14, 2026
7d50ea5
Merge pull request #518 from erikdarlingdata/fix/ui-review-phase-1
erikdarlingdata Sep 14, 2026
f5ca598
Make DarkTheme the app's design-token registry
erikdarlingdata Sep 14, 2026
4681611
Bound AppButton's states so hover stops shouting
erikdarlingdata Sep 14, 2026
194e997
Make scrollbars slim overlay rails that expand on hover
erikdarlingdata Sep 14, 2026
42c851d
Add AppIcons: one Fluent icon per toolbar concept
erikdarlingdata Sep 14, 2026
b71e1be
Add a Stop icon, and swap Format off the one that looked like a list
erikdarlingdata Sep 14, 2026
7820488
Put the plan tab's toolbar on the icon set
erikdarlingdata Sep 14, 2026
abb08fd
Canonicalise ErrorBrush on the red the app actually means
erikdarlingdata Sep 14, 2026
b93fe62
Alias the duplicated Slicer brushes onto their core tokens
erikdarlingdata Sep 14, 2026
2497bf9
Resolve the Phase 2 design tokens from PlanViewerControl code-behind
erikdarlingdata Sep 14, 2026
87bad14
Give the Plan Insights strip one card system instead of five tints
erikdarlingdata Sep 14, 2026
78a3370
Stop the wait stats rows clipping their trailing benefit text
erikdarlingdata Sep 14, 2026
7ff1ea1
Let the statements grid headers say what the columns are
erikdarlingdata Sep 14, 2026
51d59ca
Draw warning severities from the token set, in one place
erikdarlingdata Sep 14, 2026
edc77c3
Make the missing index CREATE statement selectable from its whitespace
erikdarlingdata Sep 14, 2026
80de6fc
Fix the hit-test and narrow-width traps a gotcha review turned up
erikdarlingdata Sep 14, 2026
dd8baee
Integration stitches: icons on the plan toolbar, status colours on to…
erikdarlingdata Sep 14, 2026
3f07dfc
Give the session toolbar one row of slots that never moves
erikdarlingdata Sep 14, 2026
f70e8bb
Make the tab strip one scrolling row instead of three stacked ones
erikdarlingdata Sep 14, 2026
962f569
Take the session control's colours from the theme instead of hex lite…
erikdarlingdata Sep 14, 2026
bdaa99c
Pin the toolbar's promises with a test, and give Format a shortcut
erikdarlingdata Sep 14, 2026
9bb0997
Pin the two pieces of this change that could fail invisibly
erikdarlingdata Sep 14, 2026
cd14836
Stop the tab strip shifting the window when it overflows
erikdarlingdata Sep 14, 2026
7cfe738
Integration stitches: session toolbar icons, plan toolbar parity
erikdarlingdata Sep 14, 2026
60ac142
Apply the phase-2 review findings
erikdarlingdata Sep 15, 2026
5213dec
Merge pull request #519 from erikdarlingdata/feature/ui-phase-2-desig…
erikdarlingdata Sep 15, 2026
082ede2
Trim the toolbars and give the overflow chevron somewhere to live
erikdarlingdata Sep 15, 2026
0f1bb96
Add the priority overflow itself
erikdarlingdata Sep 15, 2026
2f09e5d
Wire both toolbars to the overflow
erikdarlingdata Sep 15, 2026
fb4d535
Pin the overflow, and the resize trap the tests fell into first
erikdarlingdata Sep 15, 2026
a186735
Close the gaps a gotcha review turned up
erikdarlingdata Sep 15, 2026
a8446fd
Merge pull request #520 from erikdarlingdata/feature/toolbar-overflow
erikdarlingdata Sep 15, 2026
b1081ff
Reach every block in the advice pane through the logical tree
erikdarlingdata Sep 15, 2026
13345fd
Give the wait and triage renderers the seams a card view needs
erikdarlingdata Sep 15, 2026
f0830ac
Rebuild Advice for Humans as cards over the model
erikdarlingdata Sep 15, 2026
27c02ae
Pin the copied report, and the cards standing beside it
erikdarlingdata Sep 15, 2026
631be90
Resolve the advice window shell's colours from the tokens
erikdarlingdata Sep 15, 2026
762412f
Say the Query Store state in words instead of a donut
erikdarlingdata Sep 15, 2026
75e4a8e
Redraw the metric cards as small multiples behind one Total/Avg switch
erikdarlingdata Sep 15, 2026
1848b2b
Print whole counts whole, and let a tall card scroll
erikdarlingdata Sep 15, 2026
61abcc1
Pin the overview cards with headless tests
erikdarlingdata Sep 15, 2026
1d018c3
Give the bar the width the database name was taking
erikdarlingdata Sep 15, 2026
1990182
Measure a bar against its track, not just the order it sits in
erikdarlingdata Sep 15, 2026
44affa5
Fix two latent sub-tab bugs the IA wiring inventory surfaced
erikdarlingdata Sep 15, 2026
91c042b
Record the comparison report's text before touching it
erikdarlingdata Sep 15, 2026
63cfe10
Compute the comparison once, into a structure the text renders from
erikdarlingdata Sep 15, 2026
6384280
Show the comparison as a diff instead of a monospace dump
erikdarlingdata Sep 15, 2026
4733608
Close the gaps two review passes and a screenshot turned up
erikdarlingdata Sep 15, 2026
6bddbfa
Fix the five defects an Avalonia review found
erikdarlingdata Sep 15, 2026
45b3542
Cut the bar track into the card instead of drawing it in border grey
erikdarlingdata Sep 15, 2026
4ac49b6
Resolve the QS Others colour from a theme token
erikdarlingdata Sep 15, 2026
8dfc32d
Mirror the copy-button generation counter into the advice window
erikdarlingdata Sep 15, 2026
e210ae2
Merge pull request #521 from erikdarlingdata/feature/ui-phase-3-wave-a
erikdarlingdata Sep 15, 2026
f9b52b9
Add the document-tab accessor seam to QuerySessionControl
erikdarlingdata Sep 15, 2026
199747c
Route the plan-tab partial through the document seam
erikdarlingdata Sep 15, 2026
994a594
Route the Query Store partial through the document seam
erikdarlingdata Sep 15, 2026
ff26580
Route the execution partial through the document seam
erikdarlingdata Sep 15, 2026
9cf418d
Route the schema partial through the document seam
erikdarlingdata Sep 15, 2026
1979a47
Route the editor partial through the document seam
erikdarlingdata Sep 15, 2026
1be97a8
Route the empty state through the document seam
erikdarlingdata Sep 15, 2026
640569b
Route the session code-behind through the document seam
erikdarlingdata Sep 15, 2026
c89ebc9
Retemplate the sub-tab strip to render headers only
erikdarlingdata Sep 15, 2026
533fab6
Split the session's views out of its document strip
erikdarlingdata Sep 15, 2026
56cbb6a
Make the Overview survive being reloaded and cancelled
erikdarlingdata Sep 15, 2026
d8a6e7e
Write down what the seam's contracts now cost to break
erikdarlingdata Sep 15, 2026
1b17c3f
Refresh the plan-tab buttons when a captured plan lands
erikdarlingdata Sep 15, 2026
4b58ca2
Delete the manual Advice enables that overrode the button gate
erikdarlingdata Sep 15, 2026
312ace7
Let only the newest Overview load touch the status strip
erikdarlingdata Sep 15, 2026
92da3f0
Floor a restored slicer range like a hand-entered one
erikdarlingdata Sep 15, 2026
0639413
Give the sub-tab Close menu a shortcut that is true
erikdarlingdata Sep 15, 2026
e1a3558
Reach both views from the keyboard
erikdarlingdata Sep 15, 2026
2c06098
Pin the surface machine to tests instead of a paragraph
erikdarlingdata Sep 15, 2026
6a9ee71
Press the session shortcuts where the caret actually is
erikdarlingdata Sep 15, 2026
ee48414
Turn the band's measured heights into enforced ones
erikdarlingdata Sep 15, 2026
116ba4e
Pin what a view keeps and what a document lets go
erikdarlingdata Sep 15, 2026
6b91e37
Correct a premise the Overview stopped having
erikdarlingdata Sep 15, 2026
267d649
Shape the slicer drift pin for its second caller
erikdarlingdata Sep 15, 2026
f20228f
Keep the CLI entra refusal out of the test host process globals
erikdarlingdata Sep 15, 2026
8b9d23a
Pull the start back when a restored range clamps to the far end
erikdarlingdata Sep 15, 2026
b4af155
Give the plan-tab header a background so its whole rect hit-tests
erikdarlingdata Sep 15, 2026
8ceca08
Give every plan tab the same right-click menu
erikdarlingdata Sep 15, 2026
ccfa844
Merge pull request #522 from erikdarlingdata/feature/ui-phase-3-wave-b
erikdarlingdata Sep 15, 2026
4470727
Stop a 99.9% win printing as 100%
erikdarlingdata Sep 15, 2026
d6719ae
Merge pull request #523 from erikdarlingdata/fix/comparison-percent-r…
erikdarlingdata Sep 15, 2026
06ce0b1
Guard the shared Overview path, and cover a scrolled-out document
erikdarlingdata Sep 15, 2026
5e44a38
Say the segmented button once
erikdarlingdata Sep 15, 2026
012256e
Merge pull request #524 from erikdarlingdata/chore/wave-b-carry-forwards
erikdarlingdata Sep 15, 2026
caa57ce
Teach the shared duration ladder about sub-millisecond times
erikdarlingdata Sep 15, 2026
9746891
Merge pull request #525 from erikdarlingdata/feature/phase-4-polish
erikdarlingdata Sep 15, 2026
161785f
Lay the wait palette out instead of picking it
erikdarlingdata Sep 15, 2026
4b3ba41
Merge pull request #526 from erikdarlingdata/feature/wait-palette
erikdarlingdata Sep 15, 2026
0387a2e
Correct the macOS file-association comment
erikdarlingdata Sep 15, 2026
f979d08
Merge pull request #527 from erikdarlingdata/fix/macos-association-co…
erikdarlingdata Sep 15, 2026
2ead37d
Give SegmentedButton the disabled state it never had
erikdarlingdata Sep 15, 2026
659653a
Merge pull request #528 from erikdarlingdata/fix/segmented-disabled
erikdarlingdata Sep 15, 2026
e8bb98a
Move the minimap into a corner and settings out of About
erikdarlingdata Sep 15, 2026
e1f4242
Fix what the Avalonia review found in the first pass
erikdarlingdata Sep 15, 2026
00576d1
Read MCP settings through the file that writes them
erikdarlingdata Sep 15, 2026
62b5ee6
Keep Reset All away from the stored proxy password
erikdarlingdata Sep 15, 2026
5c7de17
Redirect the credential store for the test host too
erikdarlingdata Sep 15, 2026
fc0bf89
Merge pull request #529 from erikdarlingdata/feature/v7-v11-minimap-a…
erikdarlingdata Sep 15, 2026
65006ae
Let a Settings section keep an edit you navigate away from
erikdarlingdata Sep 16, 2026
a99780f
Keep a format value while its field is momentarily empty
erikdarlingdata Sep 16, 2026
7c538f7
Merge pull request #530 from erikdarlingdata/fix/settings-section-rou…
erikdarlingdata Sep 16, 2026
bca054f
Say which panel is open, and which zoom is which
erikdarlingdata Sep 16, 2026
30b44cc
Merge pull request #531 from erikdarlingdata/feature/u7-u9-toggle-sta…
erikdarlingdata Sep 16, 2026
8606b8d
Bump to 1.26.0
erikdarlingdata Sep 16, 2026
a1f7902
Merge pull request #532 from erikdarlingdata/release/v1.26.0
erikdarlingdata Sep 16, 2026
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
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ authors:
website: "https://erikdarling.com"
repository-code: "https://github.com/erikdarlingdata/PerformanceStudio"
license: MIT
version: "1.23.0"
date-released: "2026-09-02"
version: "1.26.0"
date-released: "2026-09-16"
keywords:
- sql-server
- execution-plan
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
Tests and server/ projects are outside src/ and are unaffected.
-->
<PropertyGroup>
<Version>1.25.0</Version>
<Version>1.26.0</Version>
<Authors>Erik Darling</Authors>
<Company>Darling Data LLC</Company>
<Product>Performance Studio</Product>
Expand Down
65 changes: 4 additions & 61 deletions src/PlanViewer.App/AboutWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PlanViewer.App.AboutWindow"
Title="About Performance Studio"
Width="480" Height="640"
Width="480" Height="400"
CanResize="False"
WindowStartupLocation="CenterOwner"
Icon="avares://PlanViewer.App/EDD.ico"
Expand Down Expand Up @@ -65,66 +65,9 @@
Margin="0,4,0,0"/>
</StackPanel>

<!-- MCP Settings -->
<StackPanel Grid.Row="4" Margin="0,0,0,12">
<TextBlock Text="Settings" FontWeight="SemiBold" FontSize="13"
Foreground="{DynamicResource ForegroundBrush}" Margin="0,0,0,8"/>
<StackPanel Orientation="Horizontal" Spacing="12">
<CheckBox x:Name="McpEnabledCheckBox" Content="Enable MCP Server"
FontSize="12" VerticalContentAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}"/>
<TextBlock Text="Port:" FontSize="12" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}"/>
<TextBox x:Name="McpPortInput" Width="70" Height="28"
Text="5152" FontSize="12" Padding="6,2"
VerticalContentAlignment="Center"/>
</StackPanel>
<TextBlock Text="Restart the application after changing MCP settings."
FontSize="11" Foreground="{DynamicResource ForegroundBrush}"
Margin="0,4,0,0"/>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,8,0,0">
<Button x:Name="CopyMcpCommandButton" Content="Copy MCP Command"
Click="CopyMcpCommand_Click" Padding="10,4" FontSize="12"/>
<TextBlock x:Name="McpCopyStatus" FontSize="11" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}"/>
</StackPanel>

<!-- Proxy Settings -->
<TextBlock Text="Proxy" FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource ForegroundBrush}" Margin="0,16,0,4"/>
<StackPanel Orientation="Horizontal" Spacing="12">
<RadioButton x:Name="ProxySystemRadio" GroupName="ProxyMode"
Content="Use system proxy (Windows credentials)"
FontSize="12" IsChecked="True"
Foreground="{DynamicResource ForegroundBrush}"/>
<RadioButton x:Name="ProxyManualRadio" GroupName="ProxyMode"
Content="Manual"
FontSize="12"
Foreground="{DynamicResource ForegroundBrush}"/>
</StackPanel>
<Grid x:Name="ProxyManualPanel" IsVisible="False"
ColumnDefinitions="90,*" RowDefinitions="Auto,Auto,Auto"
Margin="0,8,0,0" RowSpacing="6" ColumnSpacing="8">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Address:" FontSize="12"
VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}"/>
<TextBox Grid.Row="0" Grid.Column="1" x:Name="ProxyAddressInput"
Watermark="http://proxy.example.com:8080" FontSize="12"
Padding="6,2" Height="28"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Username:" FontSize="12"
VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}"/>
<TextBox Grid.Row="1" Grid.Column="1" x:Name="ProxyUsernameInput"
Watermark="DOMAIN\user" FontSize="12"
Padding="6,2" Height="28"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Password:" FontSize="12"
VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}"/>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="ProxyPasswordInput"
PasswordChar="•" FontSize="12"
Padding="6,2" Height="28"/>
</Grid>
</StackPanel>
<!-- MCP server and proxy configuration used to sit here. They are settings, not
information about the build, and nobody looks for a port number in an About box;
they now live under Settings > Integrations. -->

<!-- Close -->
<Button Grid.Row="5" Content="Close" Click="CloseButton_Click"
Expand Down
80 changes: 0 additions & 80 deletions src/PlanViewer.App/AboutWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using PlanViewer.App.Mcp;
using PlanViewer.App.Services;
using Velopack;

Expand All @@ -31,90 +30,11 @@ public AboutWindow()
var version = Assembly.GetExecutingAssembly().GetName().Version;
if (version != null)
VersionText.Text = $"Version {version.Major}.{version.Minor}.{version.Build}";

// Load current MCP settings
var mcp = McpSettings.Load();
McpEnabledCheckBox.IsChecked = mcp.Enabled;
McpPortInput.Text = mcp.Port.ToString();

// Save on change
McpEnabledCheckBox.IsCheckedChanged += (_, _) => SaveMcpSettings();
McpPortInput.LostFocus += (_, _) => SaveMcpSettings();

// Load proxy settings. The password is intentionally NOT round-tripped
// through the UI — TextBox.PasswordChar only masks the glyph, the cleartext
// still lives in the visual/accessibility tree. We surface "(saved — leave
// blank to keep)" via the watermark instead, and only update the credential
// when the user types a new value.
var proxy = ProxySettings.Load();
_hasStoredProxyPassword = !string.IsNullOrEmpty(proxy.Password);
ProxySystemRadio.IsChecked = proxy.Mode == ProxyMode.System;
ProxyManualRadio.IsChecked = proxy.Mode == ProxyMode.Manual;
ProxyAddressInput.Text = proxy.Address;
ProxyUsernameInput.Text = proxy.Username;
ProxyPasswordInput.Watermark = _hasStoredProxyPassword
? "(saved — leave blank to keep)"
: "";
ProxyManualPanel.IsVisible = proxy.Mode == ProxyMode.Manual;

// Both radios fire IsCheckedChanged on every selection (one going false,
// one going true). Only the now-checked one should drive the save —
// otherwise the credential write races itself.
void OnProxyRadioChanged(object? sender, RoutedEventArgs _)
{
if (sender is RadioButton rb && rb.IsChecked == true)
{
ProxyManualPanel.IsVisible = ProxyManualRadio.IsChecked == true;
SaveProxySettings();
}
}
ProxySystemRadio.IsCheckedChanged += OnProxyRadioChanged;
ProxyManualRadio.IsCheckedChanged += OnProxyRadioChanged;
ProxyAddressInput.LostFocus += (_, _) => SaveProxySettings();
ProxyUsernameInput.LostFocus += (_, _) => SaveProxySettings();
ProxyPasswordInput.LostFocus += (_, _) => SaveProxySettings();
}

private bool _hasStoredProxyPassword;

private void SaveMcpSettings()
{
Services.SettingsFile.Update(o =>
{
o["mcp_enabled"] = McpEnabledCheckBox.IsChecked == true;
o["mcp_port"] = int.TryParse(McpPortInput.Text, out var p) && p >= 1024 && p <= 65535 ? p : 5152;
});
}

private void SaveProxySettings()
{
var typedPassword = ProxyPasswordInput.Text ?? "";
var settings = new ProxySettings
{
Mode = ProxyManualRadio.IsChecked == true ? ProxyMode.Manual : ProxyMode.System,
Address = ProxyAddressInput.Text ?? "",
Username = ProxyUsernameInput.Text ?? "",
Password = typedPassword
};
// Empty textbox + an existing stored password means "keep what's there".
// Save() signals "leave the credential alone" with TouchCredential=false.
settings.TouchCredential = !(typedPassword.Length == 0 && _hasStoredProxyPassword);
settings.Save();
if (settings.TouchCredential)
_hasStoredProxyPassword = !string.IsNullOrEmpty(typedPassword);
}

private void GitHubLink_Click(object? sender, PointerPressedEventArgs e) => OpenUrl(GitHubUrl);
private void ReportIssueLink_Click(object? sender, PointerPressedEventArgs e) => OpenUrl(IssuesUrl);
private void DarlingDataLink_Click(object? sender, PointerPressedEventArgs e) => OpenUrl(DarlingDataUrl);
private async void CopyMcpCommand_Click(object? sender, RoutedEventArgs e)
{
var port = int.TryParse(McpPortInput.Text, out var p) && p >= 1024 && p <= 65535 ? p : 5152;
var command = $"claude mcp add --transport streamable-http --scope user performance-studio http://localhost:{port}/";
McpCopyStatus.Text = await ClipboardHelper.TrySetTextAsync(this, command)
? "Copied to clipboard!"
: "Clipboard busy - try again";
}

private string? _updateUrl;
private UpdateManager? _velopackMgr;
Expand Down
49 changes: 42 additions & 7 deletions src/PlanViewer.App/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,32 @@
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml"/>

<!-- Fluent defaults AllowAutoHide to true, which collapses a scrollbar to a
sliver until it is hovered. That is a bad trade on a horizontal bar under
a wide grid or a long query: the thing you need to grab is a couple of
pixels tall until after you have found it. Full size everywhere. -->
<!-- Scrollbars are slim overlay rails that expand on hover. This half turns the
overlay on; the geometry that makes it grabbable is in Application.Resources
below, and neither half is correct without the other.

Fluent reserves layout space for a scrollbar whenever AllowAutoHide is false,
so a pane built from nested scrollers surrenders a strip per scroller. The
plan view was showing three horizontal bars stacked at once. With auto-hide
on, ScrollViewer's own theme spans the content presenter under both bars
(Grid.ColumnSpan and RowSpan go to 2), so bars float over content instead of
shoving it around.

#464 is why this is not simply "auto-hide back on". That issue was the mirror
complaint: Fluent's idle thumb is scaleX(0.125) of a 16px bar, which is 2
physical pixels, and the report said the thing you need to grab is "a couple
of pixels tall until after you have found it". Turning auto-hide off fixed the
grab and bought the stacking. Redefining the geometry buys both - see the
resources below for the arithmetic.

Set explicitly rather than left to the Fluent default, because the default is
precisely what #464 overrode here and the decision should be visible. -->
<Style Selector="ScrollViewer">
<Setter Property="AllowAutoHide" Value="False"/>
<Setter Property="AllowAutoHide" Value="True"/>
</Style>

<Style Selector="ScrollBar">
<Setter Property="AllowAutoHide" Value="False"/>
<Setter Property="AllowAutoHide" Value="True"/>
</Style>

<!-- DataGrid does not scroll through a ScrollViewer, and the ScrollBar rule above
Expand All @@ -33,7 +49,7 @@
the value it pushes down is the ATTACHED ScrollViewer property read off itself,
so setting that here is what reaches the grids. -->
<Style Selector="DataGrid">
<Setter Property="ScrollViewer.AllowAutoHide" Value="False"/>
<Setter Property="ScrollViewer.AllowAutoHide" Value="True"/>
</Style>
</Application.Styles>

Expand All @@ -43,6 +59,25 @@
<ResourceInclude Source="avares://PlanViewer.App/Themes/DarkTheme.axaml"/>
<ResourceInclude Source="avares://PlanViewer.App/Themes/BarChartConfig.axaml"/>
</ResourceDictionary.MergedDictionaries>

<!-- Scrollbar geometry, overriding Fluent's own resources of these names.
Application.Resources outranks resources a theme contributes through
Application.Styles, which is the same lever DarkTheme pulls to repaint
SystemAccentColor.

Fluent's numbers are 16px with an idle scale of 0.125, so 2px of rail.
These are 14px with an idle scale of 0.5, so 7px of rail that is always
drawn and always grabbable, opening to the full 14px under the pointer.
That is the macOS overlay-scrollbar shape, and 7 logical px stays workable
at high display scaling where 2px did not.

KEEP THESE IN STEP. The idle rail is ScrollBarSize times the scale factor,
and it must not fall below 6px or #464 is back. Only the scale differs from
Fluent's own resource; the translate is theirs, is applied in the scaled
space, and merely holds the rail a hair off the edge. -->
<x:Double x:Key="ScrollBarSize">14</x:Double>
<TransformOperations x:Key="VerticalSmallScrollThumbScaleTransform">scaleX(0.5) translateX(-2px)</TransformOperations>
<TransformOperations x:Key="HorizontalSmallScrollThumbScaleTransform">scaleY(0.5) translateY(-2px)</TransformOperations>
</ResourceDictionary>
</Application.Resources>
</Application>
17 changes: 14 additions & 3 deletions src/PlanViewer.App/Controls/ColumnFilterPopup.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,15 @@ public ColumnFilterPopup()
OperatorComboBox.SelectedIndex = 0;
}

public void Initialize(string columnName, ColumnFilterState? existingFilter, bool canSearchServer)
/// <summary>
/// Prepares the popup for one column. <paramref name="columnName"/> is the internal column id
/// the filter is keyed and evaluated by; <paramref name="displayName"/> is the grid header the
/// user actually sees, and is all that is shown in the popup.
/// </summary>
public void Initialize(string columnName, string displayName, ColumnFilterState? existingFilter, bool canSearchServer)
{
_currentColumnName = columnName;
HeaderText.Text = $"Filter: {columnName}";
HeaderText.Text = $"Filter: {(string.IsNullOrEmpty(displayName) ? columnName : displayName)}";
SearchServerButton.IsVisible = canSearchServer;

if (existingFilter?.IsActive == true)
Expand All @@ -55,9 +60,15 @@ public void Initialize(string columnName, ColumnFilterState? existingFilter, boo
}

UpdateValueVisibility();
ValueTextBox.Focus();
}

/// <summary>
/// Puts the caret in the value box. Only works once the popup is open: before that this
/// control has no visual root and Focus() is a silent no-op, which is why Initialize
/// cannot do it — the caller focuses after opening the popup.
/// </summary>
internal void FocusValueBox() => ValueTextBox.Focus();

private void UpdateValueVisibility()
{
var idx = OperatorComboBox.SelectedIndex;
Expand Down
29 changes: 25 additions & 4 deletions src/PlanViewer.App/Controls/PlanViewerControl.Minimap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ private void OpenMinimapPanel()
MinimapPanel.Width = _minimapWidth;
MinimapPanel.Height = _minimapHeight;
MinimapPanel.IsVisible = true;
MinimapToggleButton.Classes.Set("on", true);
RenderMinimap();
}

Expand All @@ -43,6 +44,9 @@ private void CloseMinimapPanel()
MinimapPanel.IsVisible = false;
_minimapDragging = false;
_minimapResizing = false;
// Follows the panel, not the click: the minimap's own close button and clearing the plan
// both come through here without going near the toggle.
MinimapToggleButton.Classes.Set("on", false);
}

private void RenderMinimap()
Expand Down Expand Up @@ -461,9 +465,15 @@ private void ZoomToNode(PlanNode node)
}
}

/* Resize drags are measured against this control, NOT against MinimapPanel.

The panel is pinned to the bottom-right, so growing it moves its own top-left corner —
and the grip lives in that corner. Measured in the panel's own coordinates the grip would
therefore sit still while the pointer moved, and the drag would fight itself. This control
does not move, so deltas taken from it mean what they say. */
private void MinimapResizeGrip_PointerPressed(object? sender, PointerPressedEventArgs e)
{
var point = e.GetCurrentPoint(MinimapPanel);
var point = e.GetCurrentPoint(this);
if (!point.Properties.IsLeftButtonPressed) return;
_minimapResizing = true;
_minimapResizeStart = point.Position;
Expand All @@ -476,11 +486,22 @@ private void MinimapResizeGrip_PointerPressed(object? sender, PointerPressedEven
private void MinimapResizeGrip_PointerMoved(object? sender, PointerEventArgs e)
{
if (!_minimapResizing) return;
var current = e.GetPosition(MinimapPanel);

// Belt to PointerCaptureLost's braces: if the button is no longer down, the drag is over
// however it ended. Without this a stale flag turns an ordinary hover into a resize.
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
_minimapResizing = false;
return;
}

var current = e.GetPosition(this);
// Dragging toward the top-left — away from the pinned corner — is what grows the panel,
// so the deltas subtract.
var dx = current.X - _minimapResizeStart.X;
var dy = current.Y - _minimapResizeStart.Y;
var newW = Math.Max(MinimapMinSize, Math.Min(MinimapMaxSize, _minimapResizeStartW + dx));
var newH = Math.Max(MinimapMinSize, Math.Min(MinimapMaxSize, _minimapResizeStartH + dy));
var newW = Math.Max(MinimapMinSize, Math.Min(MinimapMaxSize, _minimapResizeStartW - dx));
var newH = Math.Max(MinimapMinSize, Math.Min(MinimapMaxSize, _minimapResizeStartH - dy));
MinimapPanel.Width = newW;
MinimapPanel.Height = newH;
_minimapWidth = newW;
Expand Down
Loading