From 30a7b87e32de3d3f6515fba74ebd85730b31583e Mon Sep 17 00:00:00 2001 From: Steve the Killer Date: Wed, 14 Oct 2026 22:36:44 -0700 Subject: [PATCH 001/104] v1.8.1: Set context menus in Killer Sans UI like the rest of the interface instead of Segoe UI --- App.xaml | 5 ++++- CHANGELOG.md | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/App.xaml b/App.xaml index 4819c41e..8eea294a 100644 --- a/App.xaml +++ b/App.xaml @@ -921,6 +921,8 @@ + + @@ -977,7 +979,8 @@ - + + diff --git a/CHANGELOG.md b/CHANGELOG.md index b713cb97..4aad7f98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this - Centered website logos with space above and below and corrected current features, runtime requirements and documentation. - Menu separators draw their divider line instead of leaving an empty gap. - The About card's update button uses the family outline button instead of a solid dark block. +- Context menus use Killer Sans UI like the rest of the interface instead of Segoe UI. ## [1.8.0] - 2026-10-08 From a939fc584fe0bf6c9a53099c782804d7005b271f Mon Sep 17 00:00:00 2001 From: Steve the Killer Date: Wed, 14 Oct 2026 23:04:11 -0700 Subject: [PATCH 002/104] v1.8.2: Let Windows supply the busy installer message in the user's language instead of English text --- Services/InstallerExitCodes.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Services/InstallerExitCodes.cs b/Services/InstallerExitCodes.cs index 4df03411..dce7284c 100644 --- a/Services/InstallerExitCodes.cs +++ b/Services/InstallerExitCodes.cs @@ -74,7 +74,8 @@ internal static IDisposable Acquire(string product) if (!owned) { mutex.Dispose(); - throw new Win32Exception(Busy, "Another installation of this application is in progress."); + // Windows supplies this message in the user's language. + throw new Win32Exception(Busy); } return new Lease(mutex); } From 40b3e26cbabb7d4e24aa27b713414061ce2cc573 Mon Sep 17 00:00:00 2001 From: Steve the Killer Date: Wed, 14 Oct 2026 23:15:25 -0700 Subject: [PATCH 003/104] v1.8.2: Hide the device table, Services and Topology grain when Film Grain is turned off --- CHANGELOG.md | 5 +++ Controls/ScanWorkspace.xaml | 3 +- tests/SpeedTest.Tests/ReleaseUiTests.cs | 50 +++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aad7f98..358d2492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to KillerScan are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.8.2] - Unreleased + +### Fixed +- Turning off Film Grain removes the grain from the device table, Services and Topology views too, instead of leaving it behind them. + ## [1.8.1] - 2026-10-14 ### Added diff --git a/Controls/ScanWorkspace.xaml b/Controls/ScanWorkspace.xaml index 7b939bea..8dbc7e4c 100644 --- a/Controls/ScanWorkspace.xaml +++ b/Controls/ScanWorkspace.xaml @@ -320,7 +320,8 @@ Height="{DynamicResource ScanPaneBandSize}" Background="{DynamicResource ScanPaneBottomBrush}"/> - + diff --git a/tests/SpeedTest.Tests/ReleaseUiTests.cs b/tests/SpeedTest.Tests/ReleaseUiTests.cs index 4b497bba..2ee90a60 100644 --- a/tests/SpeedTest.Tests/ReleaseUiTests.cs +++ b/tests/SpeedTest.Tests/ReleaseUiTests.cs @@ -168,6 +168,13 @@ void Locale(string locale) var canvas = (Canvas)workspace.FindName("TopologyCanvas"); Require(canvas.Width > 0 && canvas.Height > 0 && canvas.Children.Count > 0, "Topology has arranged device content."); CheckTopologySettings(workspace, root, canvas, output, Locale); + foreach (string view in new[] { "devices", "services", "topology" }) + { + workspace.SetView(view); + CheckFilmGrainOff(root, view); + } + workspace.SetView("topology"); + Render(root, 1200, 780); var formatType = workspace.GetType().GetNestedType("SnapshotFormat", BindingFlags.NonPublic)!; foreach (string format in new[] { "PngTransparent", "Jpeg" }) { @@ -427,6 +434,49 @@ private static void CheckTopologySettings(KillerScan.Controls.ScanWorkspace work Console.WriteLine("PASS: " + combinations + " topology layout/size/font combinations and all menu choices."); } + /// Turns Film Grain off without saving the choice and requires every tiled grain layer in + /// the window to stop drawing, then turns it back on. + private static void CheckFilmGrainOff(FrameworkElement root, string view) + { + var grain = typeof(KillerScan.App).Assembly.GetType("KillerScan.Services.FilmGrain", true)!; + var enabled = grain.GetProperty("Enabled", Static)!; + var publish = grain.GetMethod("Publish", Static)!; + static IEnumerable Descendants(DependencyObject parent) + { + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + yield return child; + foreach (var descendant in Descendants(child)) yield return descendant; + } + } + // IsVisible is false throughout an offscreen window that is never shown, so walk the + // layer's own Visibility and Opacity up to the root instead. + bool Drawn(Border border) + { + for (DependencyObject? node = border; node != null; node = VisualTreeHelper.GetParent(node)) + if (node is UIElement element && (element.Visibility != Visibility.Visible || element.Opacity <= 0)) + return false; + return true; + } + try + { + enabled.SetValue(null, false); + publish.Invoke(null, null); + Render(root, 1200, 780); + var drawn = Descendants(root).OfType() + .Where(border => border.Background is ImageBrush { TileMode: TileMode.Tile } && Drawn(border)) + .Select(border => string.IsNullOrEmpty(border.Name) ? border.TemplatedParent?.GetType().Name ?? "Border" : border.Name) + .ToList(); + Require(drawn.Count == 0, view + ": with Film Grain off no grain layer is drawn (" + string.Join(", ", drawn) + ")."); + } + finally + { + enabled.SetValue(null, true); + publish.Invoke(null, null); + } + } + private static BitmapSource Render(FrameworkElement element, int width, int height) { element.Measure(new Size(width, height)); From 1d4b12700075343a097c6503c5a54a443b3283fa Mon Sep 17 00:00:00 2001 From: Steve the Killer Date: Wed, 14 Oct 2026 23:18:12 -0700 Subject: [PATCH 004/104] v1.8.2: Set the version to 1.8.2 and the dialog and History headings in Lacquer, keeping Killer Sans on 98SE --- App.xaml | 4 ++-- CHANGELOG.md | 5 ++++- Controls/ConfirmDialog.xaml | 2 +- Controls/HistoryWorkspace.xaml | 5 +++-- KillerScan.csproj | 6 +++--- Services/ThemeManager.cs | 5 +++++ 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/App.xaml b/App.xaml index 8eea294a..4e1072f8 100644 --- a/App.xaml +++ b/App.xaml @@ -39,8 +39,8 @@ + (commercial license) for "Killer", Lacquer (OFL) for the app name and primary headings, and + Killer Sans (OFL) for secondary headings and the footer. --> /KillerScan;component/Fonts/#Punktype /KillerScan;component/Fonts/#Lacquer /KillerScan;component/Fonts/#Killer Sans diff --git a/CHANGELOG.md b/CHANGELOG.md index 358d2492..6ba784a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this ## [1.8.2] - Unreleased +### Changed +- Dialog headings and the history detail header use Lacquer, the wordmark's app-name face, in place of Killer Sans. 98SE keeps Killer Sans. +- Context menus use Killer Sans UI like the rest of the interface instead of Segoe UI. + ### Fixed - Turning off Film Grain removes the grain from the device table, Services and Topology views too, instead of leaving it behind them. @@ -40,7 +44,6 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this - Centered website logos with space above and below and corrected current features, runtime requirements and documentation. - Menu separators draw their divider line instead of leaving an empty gap. - The About card's update button uses the family outline button instead of a solid dark block. -- Context menus use Killer Sans UI like the rest of the interface instead of Segoe UI. ## [1.8.0] - 2026-10-08 diff --git a/Controls/ConfirmDialog.xaml b/Controls/ConfirmDialog.xaml index 2849c54b..bcd6d010 100644 --- a/Controls/ConfirmDialog.xaml +++ b/Controls/ConfirmDialog.xaml @@ -86,7 +86,7 @@ is what a user reads - which is why they have to be resources. --> diff --git a/Controls/HistoryWorkspace.xaml b/Controls/HistoryWorkspace.xaml index e257aa2d..0b3acd73 100644 --- a/Controls/HistoryWorkspace.xaml +++ b/Controls/HistoryWorkspace.xaml @@ -43,8 +43,9 @@ - + + diff --git a/KillerScan.csproj b/KillerScan.csproj index 27495e48..1e226b5a 100644 --- a/KillerScan.csproj +++ b/KillerScan.csproj @@ -11,9 +11,9 @@ enable true Resources\ks-icon.ico - 1.8.1 - 1.8.1.0 - 1.8.1.0 + 1.8.2 + 1.8.2.0 + 1.8.2.0 diff --git a/Services/ThemeManager.cs b/Services/ThemeManager.cs index f0f1829b..adcdc427 100644 --- a/Services/ThemeManager.cs +++ b/Services/ThemeManager.cs @@ -33,6 +33,7 @@ internal static class ThemeManager { /// Killer Sans UI (Killer Sans scaled to Segoe UI proportions) with the Segoe fallback stack. internal static readonly FontFamily KillerSansUiStack = new(new Uri("pack://application:,,,/"), "./Fonts/#Killer Sans UI, Segoe UI, Microsoft JhengHei UI, Nirmala UI"); + internal static readonly FontFamily LacquerHeadingStack = new(new Uri("pack://application:,,,/"), "./Fonts/#Lacquer, ./Fonts/#Killer Sans UI, Segoe UI, Microsoft JhengHei UI, Nirmala UI"); internal static readonly FontFamily KillerSansFont = new(new Uri("pack://application:,,,/"), "./Fonts/#Killer Sans"); private static Theme _current = Theme.Black; // Dark, Light, and Black each remember their own accent independently. @@ -250,6 +251,10 @@ private static void LoadDict(Theme theme, Accent? accentOverride = null) Complete("UiFont", compactDialogCaption ? newDict["MenuFontFamily"] : KillerSansUiStack); // The titlebar subtitle sits on the wordmark baseline in Killer Sans, as KillerPDF's filename does. Complete("FileNameFont", compactDialogCaption ? newDict["MenuFontFamily"] : KillerSansFont); + // Primary headings (dialog headings, page and period titles): Lacquer, the app-name face of + // the wordmark, falling back to Killer Sans UI for scripts Lacquer does not cover. 98SE keeps + // Killer Sans. Secondary headings, the footer and the interface stay Killer Sans. + Complete("HeadingFont", compactDialogCaption ? KillerSansFont : LacquerHeadingStack); Complete("FileNameFontSize", compactDialogCaption ? 11.0 : 18.0); if (!newDict.Contains("FileNameTransform")) { From ae9ef0e01394eaa80623f653977cb079916166c6 Mon Sep 17 00:00:00 2001 From: Steve the Killer Date: Wed, 14 Oct 2026 23:27:50 -0700 Subject: [PATCH 005/104] v1.8.2: Set the history sidebar heading in Lacquer, keeping Killer Sans on 98SE --- CHANGELOG.md | 2 +- Shell/MainWindow.xaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ba784a8..dca06f8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this ## [1.8.2] - Unreleased ### Changed -- Dialog headings and the history detail header use Lacquer, the wordmark's app-name face, in place of Killer Sans. 98SE keeps Killer Sans. +- Dialog headings, the history sidebar heading and the history detail header use Lacquer, the wordmark's app-name face, in place of Killer Sans. 98SE keeps Killer Sans. - Context menus use Killer Sans UI like the rest of the interface instead of Segoe UI. ### Fixed diff --git a/Shell/MainWindow.xaml b/Shell/MainWindow.xaml index 73e5982c..f829ebac 100644 --- a/Shell/MainWindow.xaml +++ b/Shell/MainWindow.xaml @@ -319,7 +319,7 @@ Date: Wed, 14 Oct 2026 23:40:16 -0700 Subject: [PATCH 006/104] v1.8.2: Check for updates on startup with an Always check on startup choice, and add a Check for updates on startup checkbox to the About card --- CHANGELOG.md | 3 ++ Controls/ConfirmDialog.xaml | 4 +++ Controls/ConfirmDialog.xaml.cs | 13 ++++++++ Features/About/AboutController.cs | 54 +++++++++++++++++++++++++++++-- Features/About/IAboutHost.cs | 3 ++ Shell/About.cs | 4 +++ Shell/MainWindow.xaml | 7 ++++ Shell/MainWindow.xaml.cs | 3 ++ Strings/bn.xaml | 5 +++ Strings/cs-CZ.xaml | 5 +++ Strings/de-DE.xaml | 5 +++ Strings/en-US.xaml | 5 +++ Strings/es.xaml | 5 +++ Strings/fr-FR.xaml | 5 +++ Strings/hu-HU.xaml | 5 +++ Strings/it-IT.xaml | 5 +++ Strings/ja-JP.xaml | 5 +++ Strings/kk-KZ.xaml | 5 +++ Strings/nb-NO.xaml | 5 +++ Strings/pl-PL.xaml | 5 +++ Strings/pt-BR.xaml | 5 +++ Strings/ru-RU.xaml | 5 +++ Strings/tr-TR.xaml | 5 +++ Strings/uk-UA.xaml | 5 +++ Strings/vi-VN.xaml | 5 +++ Strings/zh-CN.xaml | 5 +++ Strings/zh-TW.xaml | 5 +++ 27 files changed, 184 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dca06f8e..6114f2a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this ## [1.8.2] - Unreleased +### Added +- KillerScan checks for updates on startup and offers a newer release in a dialog with an Always check on startup box. A Check for updates on startup checkbox on the About card turns the check back on. + ### Changed - Dialog headings, the history sidebar heading and the history detail header use Lacquer, the wordmark's app-name face, in place of Killer Sans. 98SE keeps Killer Sans. - Context menus use Killer Sans UI like the rest of the interface instead of Segoe UI. diff --git a/Controls/ConfirmDialog.xaml b/Controls/ConfirmDialog.xaml index bcd6d010..58dfee24 100644 --- a/Controls/ConfirmDialog.xaml +++ b/Controls/ConfirmDialog.xaml @@ -94,6 +94,10 @@ + + + + + +
KillerMCPKillerScanMCP

Inspect the network from your agent.

-

Install KillerMCP

KillerMCP includes all 81 KillerTools utilities plus tools from supported Killer apps in one local connection. The installer detects KillerScan and registers that shared connection with supported agent clients.

Download for Windows
Checking latest releaseWindows x64Digitally signedHosted on GitHub Releases
winget install SteveTheKiller.KillerMCPchoco install killermcp
  1. Download and run KillerMCP Setup.
  2. Let setup connect your agent.
  3. Open a new agent chat and ask. Available tools are discovered automatically.
+

Install KillerMCP

KillerMCP includes all 81 KillerTools utilities plus tools from supported Killer apps in one local connection. The installer detects KillerScan and registers that shared connection with supported agent clients.

Download for WindowsMicrosoft Store
Checking latest releaseWindows x64Digitally signedHosted on GitHub Releases
winget install SteveTheKiller.KillerMCPchoco install killermcp
  1. Download and run KillerMCP Setup.
  2. Let setup connect your agent.
  3. Open a new agent chat and ask. Available tools are discovered automatically.

Try asking:

killer is the shortest form. Your agent can infer KillerScan from a clear network request. Use killerscan when you want to make the app explicit.

killer what local network am I onkillerscan my networkkiller deep probe 192.168.50.25killer ping gateway.local five timeskiller trace the route to 1.1.1.1killer diagnose 192.168.50.25 on ports 22, 80, and 443killer watch 192.168.50.1 and 192.168.50.25run KillerSpeed test

What it can do

KillerMCP exposes all nine headless KillerScan commands.

Identify the local networkReturn the active interface, local IPv4 address, subnet, gateway, and DNS server without scanning hosts.
Scan the networkRun bounded quick discovery or full fingerprinting and port checks.
Deep probe one hostInspect one IPv4 host, including ports 1 through 1024.
Look up MAC vendorsUse KillerScan's bundled offline OUI database without contacting a lookup service.
Ping a targetMeasure latency and packet loss with up to 20 ICMP checks.
Trace a routeReturn the network path to an IPv4 address or hostname.
Diagnose a hostCheck DNS, ping, route selection, and selected TCP ports.
Watch hostsSample availability and latency for up to 16 IPv4 addresses.
Run KillerSpeedMeasure download, upload, latency, and jitter through speed.killerscan.net.
diff --git a/scan-landing/technical.html b/scan-landing/technical.html index e808001a..b3981ede 100644 --- a/scan-landing/technical.html +++ b/scan-landing/technical.html @@ -62,6 +62,7 @@ Download +Microsoft Store KillerMCP
+

Current limits

Targets must be one IPv4 address or a CIDR from /22 through /32. Scans time out after 60 seconds and return no more than 100 discovered devices. Deep probes accept one IPv4 address. Ping sends up to 20 checks, traces return up to 64 hops, diagnostics accept up to 32 ports, and host watches accept up to 16 IPv4 addresses. KillerSpeed can transfer up to 6 GiB of generated test data plus network overhead.

    Why KillerMCP runs twice

    Claude Desktop starts each local MCP server twice. One copy serves the app's own chats, and the other serves Claude sessions that use this computer remotely. Seeing two KillerMCP.exe processes under Claude in Task Manager is normal. KillerMCP is not installed twice, and nothing needs to be fixed.

    +
    - + - + From 3388b5586ef87c8d3c95f52a74916524007d1f47 Mon Sep 17 00:00:00 2001 From: Steve the Killer Date: Sat, 17 Oct 2026 12:31:39 -0700 Subject: [PATCH 037/104] v1.8.2: Outline Light Orange like Light Yellow, keep the dark Red accent a true red on buttons and lines, and give the website 4.5:1 colors and a top bar More menu instead of wrapping --- CHANGELOG.md | 6 +- Services/ThemeManager.cs | 3 +- Themes/Accents/Dark/Red.xaml | 9 +-- Themes/Accents/Light/Orange.xaml | 38 ++++++------ scan-landing/about.html | 10 +-- scan-landing/help.html | 10 +-- scan-landing/index.html | 10 +-- scan-landing/ks.css | 58 ++++++++++++------ scan-landing/ks.js | 101 ++++++++++++++++++++++++++++--- scan-landing/mcp.html | 6 +- scan-landing/technical.html | 10 +-- 11 files changed, 187 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a5afdd4..3f6eaa4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,9 +20,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this ### Fixed - The deep scan status and a few labels that were still in English are translated in every language, and the website's MCP page footer follows the language picker. -- On the Light theme's Yellow accent, buttons, links, tabs and accent icons stay yellow with a thin gray outline around yellow text and icons, instead of turning near-black. +- On the Light theme's Yellow and Orange accents, buttons, links, tabs and accent icons keep their color with a thin gray outline around accent text and icons, instead of turning near-black or brown. - Gray, menu and accent text reads at 4.5:1 contrast or better in every theme and accent, and the footer's version and copyright are slightly dimmed. -- On the website's Light theme with the Yellow accent, links, headings and labels stay yellow with a thin gray outline instead of turning olive brown. +- The Dark theme's Red accent stays a true red on buttons, lines, scroll bars and the website's accent swatches, and only small red text uses a lighter red. +- The website's text and accent colors read at 4.5:1 contrast or better in every theme, and a narrow top bar shrinks the Microsoft Store and MCP buttons to their logos and moves links into a More menu instead of wrapping. +- On the website's Light theme with the Yellow or Orange accent, links, headings and labels keep their color with a thin gray outline instead of turning olive or brown. - The exe's file properties show Steve the Killer as the copyright holder instead of leaving it blank. - Screen readers no longer read toolbar, menu, tab and title bar icons as unreadable symbols, and every part of each scroll bar now has a name. - Turning off Film Grain removes the grain from the device table, Services and Topology views too, instead of leaving it behind them. diff --git a/Services/ThemeManager.cs b/Services/ThemeManager.cs index adcdc427..32b863ea 100644 --- a/Services/ThemeManager.cs +++ b/Services/ThemeManager.cs @@ -369,7 +369,8 @@ private static void LoadDict(Theme theme, Accent? accentOverride = null) if (!newDict.Contains("WordmarkAccentShadowOpacity")) newDict["WordmarkAccentShadowOpacity"] = 0.0; if (!hasExplicitComboHighlightText) newDict["ComboHighlightTextBrush"] = newDict["PrimaryBrush"]; - newDict["PrimaryFillBrush"] = ButtonFill(newDict["PrimaryBrush"], theme == Theme.SE98); + // A palette can fill buttons with a deeper shade than its accent text (PrimaryFillBaseBrush). + newDict["PrimaryFillBrush"] = ButtonFill(newDict.Contains("PrimaryFillBaseBrush") ? newDict["PrimaryFillBaseBrush"] : newDict["PrimaryBrush"], theme == Theme.SE98); newDict["OutlineHoverBrush"] = ButtonFill(newDict["OutlineHoverBrush"], theme == Theme.SE98); Publish(newDict); } diff --git a/Themes/Accents/Dark/Red.xaml b/Themes/Accents/Dark/Red.xaml index ce707d8f..62cfabf9 100644 --- a/Themes/Accents/Dark/Red.xaml +++ b/Themes/Accents/Dark/Red.xaml @@ -5,15 +5,16 @@ - + + - + - + - + diff --git a/Themes/Accents/Light/Orange.xaml b/Themes/Accents/Light/Orange.xaml index 0e5e1903..468643fc 100644 --- a/Themes/Accents/Light/Orange.xaml +++ b/Themes/Accents/Light/Orange.xaml @@ -1,21 +1,25 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + 0.9 diff --git a/scan-landing/about.html b/scan-landing/about.html index 48361af7..d7a52d02 100644 --- a/scan-landing/about.html +++ b/scan-landing/about.html @@ -11,7 +11,7 @@ - + + @@ -578,7 +586,8 @@ left edge. In the cell rather than a column of its own: a separate column would sit at index 0 and fight the saved column order, and the marker belongs to the row rather than being something to sort by. --> - + @@ -619,7 +628,7 @@ + CellTemplate="{StaticResource DeviceTypeCell}" CellStyle="{StaticResource DeviceTypeCellStyle}"/> + + + + @@ -606,7 +626,7 @@ - + - + - +
    - + - +
    diff --git a/CHANGELOG.md b/CHANGELOG.md index d317faa1..8fac19bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this - The website's About page lists KillerBench and KillerShot under Also try, with larger app icons. ### Changed +- Rail buttons and the collapse arrow sit centered across the rail, and the arrow is larger. - Dialog headings, the history sidebar heading and the history detail header use Lacquer, the wordmark's app-name face, in place of Killer Sans. 98SE keeps Killer Sans. - The history detail header is the same size as the Scan History heading. - Context menus use Killer Sans UI like the rest of the interface instead of Segoe UI. diff --git a/Controls/GlyphInk.cs b/Controls/GlyphInk.cs new file mode 100644 index 00000000..e990d083 --- /dev/null +++ b/Controls/GlyphInk.cs @@ -0,0 +1,73 @@ +#nullable enable +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Media; + +namespace KillerFamily.Layout +{ + /// + /// Centers a glyph's ink instead of its advance box. Icon fonts draw many glyphs off-center + /// inside their box (a chevron hugs one side, a "?" sits high), so a TextBlock centered in a + /// rail button leaves the visible shape a pixel or two nearer one edge. With Center set, the + /// TextBlock's margin moves it by the distance between its box center and its ink center, on + /// both axes, so the glyph has the same space on every side. The margin is bound to the text + /// and font, so it follows a glyph swap (the collapse chevron) or a font change by itself. + /// + public static class GlyphInk + { + public static readonly DependencyProperty CenterProperty = DependencyProperty.RegisterAttached( + "Center", typeof(bool), typeof(GlyphInk), new PropertyMetadata(false, OnCenterChanged)); + + public static bool GetCenter(DependencyObject d) => (bool)d.GetValue(CenterProperty); + public static void SetCenter(DependencyObject d, bool value) => d.SetValue(CenterProperty, value); + + private static void OnCenterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not TextBlock text) return; + if (!(bool)e.NewValue) + { + BindingOperations.ClearBinding(text, FrameworkElement.MarginProperty); + return; + } + var binding = new MultiBinding { Converter = InkOffset.Instance }; + foreach (var dp in new[] { TextBlock.TextProperty, TextBlock.FontFamilyProperty, TextBlock.FontSizeProperty, + TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontStretchProperty }) + binding.Bindings.Add(new Binding { Path = new PropertyPath(dp), RelativeSource = RelativeSource.Self }); + BindingOperations.SetBinding(text, FrameworkElement.MarginProperty, binding); + } + + /// The margin that moves the glyph's ink center onto its box center. + internal static Thickness Offset(string text, FontFamily family, double size, FontStyle style, FontWeight weight, FontStretch stretch) + { + if (string.IsNullOrEmpty(text) || family == null || size <= 0) return new Thickness(0); + var formatted = new FormattedText(text, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, + new Typeface(family, style, weight, stretch), size, Brushes.Black, 1.0); + Rect ink = formatted.BuildGeometry(new Point(0, 0)).Bounds; + if (ink.IsEmpty) return new Thickness(0); + double dx = formatted.WidthIncludingTrailingWhitespace / 2 - (ink.X + ink.Width / 2); + double dy = formatted.Height / 2 - (ink.Y + ink.Height / 2); + // A centered element moves by half the difference between its two margins. + return new Thickness(Math.Max(0, 2 * dx), Math.Max(0, 2 * dy), Math.Max(0, -2 * dx), Math.Max(0, -2 * dy)); + } + + private sealed class InkOffset : IMultiValueConverter + { + internal static readonly InkOffset Instance = new(); + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Length < 6 || values[0] is not string text || values[1] is not FontFamily family || + values[2] is not double size || values[3] is not FontStyle style || + values[4] is not FontWeight weight || values[5] is not FontStretch stretch) + return new Thickness(0); + return Offset(text, family, size, style, weight, stretch); + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + => throw new NotSupportedException(); + } + } +} diff --git a/Shell/MainWindow.xaml b/Shell/MainWindow.xaml index e582304e..9212e645 100644 --- a/Shell/MainWindow.xaml +++ b/Shell/MainWindow.xaml @@ -485,7 +485,7 @@