Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
323b917
feat: add configurable floating and taskbar stats
debugtheworldbot Aug 23, 2026
f9510da
test: document floating stats design qa
debugtheworldbot Aug 23, 2026
6e9e442
refactor: simplify floating stats display
debugtheworldbot Aug 23, 2026
d3ff500
refactor: tighten floating stats layout
debugtheworldbot Aug 23, 2026
3564b89
refactor: move floating stats options to settings
debugtheworldbot Aug 23, 2026
12a3c32
feat: add floating stats layout options
debugtheworldbot Aug 23, 2026
d222184
fix: keep floating stats above taskbar
debugtheworldbot Aug 23, 2026
3728d3f
fix: render taskbar stats on windows 11
debugtheworldbot Aug 23, 2026
6f066d6
refactor: make floating stats ultra compact
debugtheworldbot Aug 23, 2026
1e27439
refactor: remove double-row side spacing
debugtheworldbot Aug 23, 2026
cc6b437
feat: add floating stats quick actions
debugtheworldbot Aug 23, 2026
3508906
refactor: remove taskbar stats integration
debugtheworldbot Aug 23, 2026
9641b18
feat: add floating stats font size setting
debugtheworldbot Aug 23, 2026
d682c2c
feat: scale floating stats with font size
debugtheworldbot Aug 23, 2026
4047881
feat: hide floating stats in fullscreen
debugtheworldbot Aug 23, 2026
a92c023
fix: detect Bilibili fullscreen playback
debugtheworldbot Aug 23, 2026
42b49d6
style: refine floating stats material
debugtheworldbot Aug 23, 2026
9a0bee8
fix: update floating stats defaults
debugtheworldbot Aug 23, 2026
daa9435
fix: address floating stats review feedback
debugtheworldbot Aug 23, 2026
9a29bde
style: replace floating stats shadow with border
debugtheworldbot Aug 23, 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
2 changes: 2 additions & 0 deletions KeyStats.Windows/KeyStats/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<Color x:Key="SurfaceColor">#FAFAFA</Color>
<Color x:Key="WindowSurfaceColor">#B8FAFAFA</Color>
<Color x:Key="CardColor">#D9F2F2F2</Color>
<Color x:Key="FloatingStatsSurfaceColor">#A8FAFAFA</Color>
<Color x:Key="TrayPopupBorderColor">#20000000</Color>
<Color x:Key="TrayBackdropTintColor">#B8FAFAFA</Color>
<Color x:Key="DividerColor">#E5E5E5</Color>
Expand All @@ -40,6 +41,7 @@
<SolidColorBrush x:Key="SurfaceBrush" Color="{StaticResource SurfaceColor}"/>
<SolidColorBrush x:Key="WindowSurfaceBrush" Color="{StaticResource WindowSurfaceColor}"/>
<SolidColorBrush x:Key="CardBrush" Color="{StaticResource CardColor}"/>
<SolidColorBrush x:Key="FloatingStatsSurfaceBrush" Color="{StaticResource FloatingStatsSurfaceColor}"/>
<SolidColorBrush x:Key="TrayPopupBorderBrush" Color="{StaticResource TrayPopupBorderColor}"/>
<SolidColorBrush x:Key="TrayBackdropTintBrush" Color="{StaticResource TrayBackdropTintColor}"/>
<SolidColorBrush x:Key="DividerBrush" Color="{StaticResource DividerColor}"/>
Expand Down
159 changes: 159 additions & 0 deletions KeyStats.Windows/KeyStats/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Threading;
using KeyStats.Helpers;
using KeyStats.Services;
using KeyStats.ViewModels;
Expand All @@ -36,11 +37,15 @@ public partial class App : System.Windows.Application
private KeyboardHeatmapWindow? _keyboardHeatmapWindow;
private KeyHistoryWindow? _keyHistoryWindow;
private SyncSettingsWindow? _syncSettingsWindow;
private FloatingStatsWindow? _floatingStatsWindow;
private MenuItem? _floatingStatsMenuItem;
private DispatcherTimer? _floatingStatsVisibilityTimer;
private System.Threading.Mutex? _singleInstanceMutex;
private string? _appVersion;
private IPostHogAnalytics? _postHogClient;
private SyncCoordinator? _syncCoordinator;
private long _lastResumeRecoveryTicks;
private bool _isFloatingStatsHiddenForFullscreen;

protected override void OnStartup(StartupEventArgs e)
{
Expand Down Expand Up @@ -132,6 +137,11 @@ protected override void OnStartup(StartupEventArgs e)
});
RecreateTrayIntegration();

if (statsManager.Settings.FloatingStatsEnabled)
{
ShowFloatingStatsWindow();
}

Console.WriteLine("Tray icon created successfully!");
Console.WriteLine("App is running. Look for the icon in the system tray.");
}
Expand All @@ -157,6 +167,23 @@ private System.Windows.Controls.ContextMenu CreateContextMenu()
};
menu.Items.Add(openMainWindowItem);

_floatingStatsMenuItem = new System.Windows.Controls.MenuItem
{
Header = KeyStats.Properties.Strings.Tray_ShowFloatingStats,
IsCheckable = true,
IsChecked = StatsManager.Instance.Settings.FloatingStatsEnabled
};
_floatingStatsMenuItem.Click += (s, e) =>
{
var menuItem = (System.Windows.Controls.MenuItem)s!;
TrackClick("context_menu_floating_stats", new Dictionary<string, object?>
{
["enabled"] = menuItem.IsChecked
});
SetFloatingStatsVisible(menuItem.IsChecked);
};
menu.Items.Add(_floatingStatsMenuItem);

var settingsItem = new System.Windows.Controls.MenuItem { Header = KeyStats.Properties.Strings.Tray_Settings };
settingsItem.Click += (s, e) =>
{
Expand Down Expand Up @@ -271,6 +298,134 @@ public void ShowStatsPanel()
_trayIconViewModel?.ShowStatsCommand.Execute(null);
}

public void ShowFloatingStatsWindow()
{
if (!Dispatcher.CheckAccess())
{
Dispatcher.BeginInvoke(new Action(ShowFloatingStatsWindow));
return;
}

StartFloatingStatsVisibilityMonitor();
if (FullscreenWindowDetector.IsForegroundWindowFullscreen())
{
_isFloatingStatsHiddenForFullscreen = true;
_floatingStatsWindow?.Hide();
return;
}

_isFloatingStatsHiddenForFullscreen = false;

if (_floatingStatsWindow != null)
{
_floatingStatsWindow.ShowWindow();
return;
}

_floatingStatsWindow = new FloatingStatsWindow();
_floatingStatsWindow.Closed += (_, _) => _floatingStatsWindow = null;
_floatingStatsWindow.ShowWindow();
Comment on lines +325 to +327

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile independent overlay closes with enabled state

When the overlay is closed independently—for example, with Alt+F4 after interacting with this focusable borderless window—the handler only clears _floatingStatsWindow; FloatingStatsEnabled remains true and the tray item remains checked. The visibility timer also never recreates it because its non-fullscreen path returns while _isFloatingStatsHiddenForFullscreen is false, leaving the feature apparently enabled but absent until the user toggles it off and on.

Useful? React with 👍 / 👎.

}

public void SetFloatingStatsVisible(bool isVisible)
{
if (!Dispatcher.CheckAccess())
{
Dispatcher.BeginInvoke(new Action(() => SetFloatingStatsVisible(isVisible)));
return;
}

var settings = StatsManager.Instance.Settings;
if (settings.FloatingStatsEnabled != isVisible)
{
settings.FloatingStatsEnabled = isVisible;
StatsManager.Instance.SaveSettings();
}

if (_floatingStatsMenuItem != null)
{
_floatingStatsMenuItem.IsChecked = isVisible;
}

if (isVisible)
{
ShowFloatingStatsWindow();
return;
}

StopFloatingStatsVisibilityMonitor();
_isFloatingStatsHiddenForFullscreen = false;
_floatingStatsWindow?.Close();
_floatingStatsWindow = null;
}

private void StartFloatingStatsVisibilityMonitor()
{
if (_floatingStatsVisibilityTimer == null)
{
_floatingStatsVisibilityTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(500)
};
_floatingStatsVisibilityTimer.Tick += OnFloatingStatsVisibilityTimerTick;
}

_floatingStatsVisibilityTimer.Start();
}

private void StopFloatingStatsVisibilityMonitor()
{
if (_floatingStatsVisibilityTimer == null)
{
return;
}

_floatingStatsVisibilityTimer.Stop();
_floatingStatsVisibilityTimer.Tick -= OnFloatingStatsVisibilityTimerTick;
_floatingStatsVisibilityTimer = null;
}

private void OnFloatingStatsVisibilityTimerTick(object? sender, EventArgs e)
{
if (!StatsManager.Instance.Settings.FloatingStatsEnabled)
{
StopFloatingStatsVisibilityMonitor();
return;
}

var shouldHideForFullscreen = FullscreenWindowDetector.IsForegroundWindowFullscreen();
if (shouldHideForFullscreen)
Comment on lines +396 to +397

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope fullscreen hiding to the overlay's monitor

On multi-monitor systems this global boolean hides the overlay whenever the foreground window covers any monitor, without checking which monitor contains the overlay. A fullscreen video on monitor B therefore hides an overlay positioned on monitor A even though they do not overlap; conversely, focusing a normal window on A makes the detector return false and can re-show a topmost overlay over fullscreen playback that remains on B. Return the fullscreen monitor identity and compare it with the overlay's monitor before changing visibility.

Useful? React with 👍 / 👎.

{
if (_isFloatingStatsHiddenForFullscreen)
{
return;
}

_isFloatingStatsHiddenForFullscreen = true;
_floatingStatsWindow?.Hide();
return;
}

if (!_isFloatingStatsHiddenForFullscreen)
{
return;
}

_isFloatingStatsHiddenForFullscreen = false;
ShowFloatingStatsWindow();
}

public void ApplyFloatingStatsBehaviorSettings()
{
if (!Dispatcher.CheckAccess())
{
Dispatcher.BeginInvoke(new Action(ApplyFloatingStatsBehaviorSettings));
return;
}

_floatingStatsWindow?.ApplyBehaviorSettings();
}

public void ShowMainWindow()
{
_trayIconViewModel?.ShowMainWindow();
Expand Down Expand Up @@ -470,6 +625,7 @@ private static string MakeExportFileName()

protected override void OnExit(ExitEventArgs e)
{
StopFloatingStatsVisibilityMonitor();
UnregisterSystemEventHandlers();
if (_trayIconViewModel != null)
{
Expand All @@ -487,6 +643,8 @@ protected override void OnExit(ExitEventArgs e)
_trayIcon.Dispose();
_trayIcon = null;
}
_floatingStatsWindow?.Close();
_floatingStatsWindow = null;
InputMonitorService.Instance.StopMonitoring();
_syncCoordinator?.Dispose();
_syncCoordinator = null;
Expand Down Expand Up @@ -1024,6 +1182,7 @@ private void RecreateTrayIntegration()
Visible = true
};
_trayIcon.MouseClick += OnTrayIconMouseClick;

}

private void OnTrayIconMouseClick(object? sender, Forms.MouseEventArgs e)
Expand Down
81 changes: 81 additions & 0 deletions KeyStats.Windows/KeyStats/Helpers/FullscreenWindowDetector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Runtime.InteropServices;

namespace KeyStats.Helpers;

public static class FullscreenWindowDetector
{
private const int BoundsTolerance = 2;

/// <summary>
/// Returns whether the foreground app covers the bounds of its nearest monitor.
/// </summary>
public static bool IsForegroundWindowFullscreen()
{
var windowHandle = NativeInterop.GetForegroundWindow();
if (windowHandle == IntPtr.Zero ||
windowHandle == NativeInterop.GetDesktopWindow() ||
windowHandle == NativeInterop.GetShellWindow() ||
!NativeInterop.IsWindowVisible(windowHandle) ||
NativeInterop.IsIconic(windowHandle) ||
!NativeInterop.GetWindowRect(windowHandle, out var windowBounds))
{
return false;
}

var monitorHandle = NativeInterop.MonitorFromWindow(
windowHandle,
NativeInterop.MONITOR_DEFAULTTONEAREST);
if (monitorHandle == IntPtr.Zero)
{
return false;
}

var monitorInfo = new NativeInterop.MONITORINFO
{
cbSize = (uint)Marshal.SizeOf(typeof(NativeInterop.MONITORINFO))
};
if (!NativeInterop.GetMonitorInfo(monitorHandle, ref monitorInfo))
{
return false;
}

if (!CoversMonitor(windowBounds, monitorInfo.rcMonitor))
{
return false;
}

// Some video players keep WS_MAXIMIZE while Windows still reports fullscreen mode.
return !NativeInterop.IsZoomed(windowHandle) ||
IsSystemInFullscreenMode();
}

private static bool IsSystemInFullscreenMode()
{
var result = NativeInterop.SHQueryUserNotificationState(out var state);
if (result != 0)
{
return false;
}

return state == NativeInterop.UserNotificationState.Busy ||
state == NativeInterop.UserNotificationState.RunningDirect3DFullscreen ||
state == NativeInterop.UserNotificationState.PresentationMode;
}

internal static bool CoversMonitor(NativeInterop.RECT windowBounds, NativeInterop.RECT monitorBounds)
{
if (windowBounds.Right <= windowBounds.Left ||
windowBounds.Bottom <= windowBounds.Top ||
monitorBounds.Right <= monitorBounds.Left ||
monitorBounds.Bottom <= monitorBounds.Top)
{
return false;
}

return windowBounds.Left <= monitorBounds.Left + BoundsTolerance &&
windowBounds.Top <= monitorBounds.Top + BoundsTolerance &&
windowBounds.Right >= monitorBounds.Right - BoundsTolerance &&
windowBounds.Bottom >= monitorBounds.Bottom - BoundsTolerance;
}
}
37 changes: 37 additions & 0 deletions KeyStats.Windows/KeyStats/Helpers/MonitorGeometryHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Windows;
using System.Windows.Media;
using Forms = System.Windows.Forms;

namespace KeyStats.Helpers;

public static class MonitorGeometryHelper
{
/// <summary>
/// Converts a monitor work area from device pixels to WPF device-independent units.
/// </summary>
public static Rect GetWorkingAreaInDips(Forms.Screen screen, Matrix fallbackTransform)
{
var bounds = screen.Bounds;
var center = new NativeInterop.POINT
{
x = bounds.Left + bounds.Width / 2,
y = bounds.Top + bounds.Height / 2
};
var monitor = NativeInterop.MonitorFromPoint(center, NativeInterop.MONITOR_DEFAULTTONEAREST);
if (NativeInterop.TryGetMonitorScaleFactor(monitor, out var scaleFactor))
{
var workingArea = screen.WorkingArea;
return new Rect(
workingArea.Left / scaleFactor,
workingArea.Top / scaleFactor,
workingArea.Width / scaleFactor,
workingArea.Height / scaleFactor);
Comment on lines +24 to +28

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve virtual-desktop origins during DPI conversion

On mixed-DPI layouts, dividing the absolute virtual-desktop coordinates by each monitor's scale maps every monitor into a different coordinate space. For example, a 200%-scaled display beginning at physical X=1920 is reported here as starting at 960 DIPs, overlapping a 100%-scaled primary display spanning 0–1920 DIPs; restore, intersection selection, and clamping can consequently move the overlay to the wrong screen. Fresh evidence beyond the earlier review is that the replacement per-monitor helper now applies this division directly to the absolute WorkingArea origin rather than converting monitor-local dimensions while preserving a common virtual-desktop origin.

Useful? React with 👍 / 👎.

}

var fallbackTopLeft = fallbackTransform.Transform(
new Point(screen.WorkingArea.Left, screen.WorkingArea.Top));
var fallbackBottomRight = fallbackTransform.Transform(
new Point(screen.WorkingArea.Right, screen.WorkingArea.Bottom));
return new Rect(fallbackTopLeft, fallbackBottomRight);
}
}
Loading
Loading