using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Interop;
using HC_APTBS.ViewModels;
using Wpf.Ui.Controls;
namespace HC_APTBS;
///
/// Code-behind for MainWindow. Bridges selection
/// to in both directions.
///
/// Click handling: NavigationViewItem inherits from ButtonBase, so we listen
/// for the bubbling Click routed event at the NavigationView level. This
/// avoids NavigationView.SelectionChanged + Navigate(), which require items
/// to set TargetPageType (we don't — page hosting stays with the TabControl).
///
/// Programmatic selection: WPF-UI 3.0.5's NavigationView.SelectedItem is a
/// CLR property with `protected set` — there is no SelectedItemProperty DP
/// to set. Instead we drive each item's IsActive flag directly by calling
/// the public NavigationViewItem.Activate / Deactivate methods, which is
/// exactly what WPF-UI's own navigation pipeline does internally. This is
/// what makes the IsActive trigger in the BoxyNavItemTemplate fire.
///
public partial class MainWindow : FluentWindow
{
private readonly MainViewModel _viewModel;
private NavigationViewItem? _activeItem;
public MainWindow(MainViewModel viewModel)
{
InitializeComponent();
// Maximisation is handled in the OnSourceInitialized override below, AFTER
// the WM_GETMINMAXINFO hook is installed. Setting WindowState=Maximized in
// XAML applies during InitializeComponent and triggers the first maximize
// before any hook can run — that's what produces the "bigger than fullscreen
// and clipped" symptom on FluentWindow / DPI-scaled displays.
_viewModel = viewModel;
DataContext = viewModel;
// Catch every NavigationViewItem click that bubbles up.
NavView.AddHandler(
ButtonBase.ClickEvent,
new RoutedEventHandler(OnNavItemClicked));
#if DEVELOPER_TOOLS
InjectDeveloperToolsPage();
#endif
Loaded += OnLoaded;
viewModel.PropertyChanged += OnViewModelPropertyChanged;
}
#if DEVELOPER_TOOLS
///
/// Adds the Developer Tools nav item and tab at runtime. Done in code-behind
/// rather than XAML so the consumer Release build (which removes the page
/// files entirely) has zero references to a type that doesn't exist there.
/// The nav item is appended to FooterMenuItems next to Settings; the
/// hosting is appended to
/// PageTabs at index 6, matching AppPage.Developer.
///
private void InjectDeveloperToolsPage()
{
var navItem = new Wpf.Ui.Controls.NavigationViewItem
{
Content = "Developer",
Tag = AppPage.Developer,
TargetPageTag = "Developer",
Icon = new Wpf.Ui.Controls.SymbolIcon { Symbol = Wpf.Ui.Controls.SymbolRegular.WrenchScrewdriver24 },
};
NavView.FooterMenuItems.Add(navItem);
var page = new Views.Pages.DeveloperPage();
page.SetBinding(System.Windows.FrameworkElement.DataContextProperty,
new System.Windows.Data.Binding(nameof(MainViewModel.DeveloperPage)));
PageTabs.Items.Add(new System.Windows.Controls.TabItem { Content = page });
}
#endif
private void OnLoaded(object sender, RoutedEventArgs e)
{
SyncSelectionFromViewModel();
}
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.SelectedPage))
SyncSelectionFromViewModel();
}
private void OnNavItemClicked(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is DependencyObject source
&& FindAncestor(source) is { Tag: AppPage page } item
&& _viewModel.SelectedPage != page)
{
_viewModel.SelectedPage = page;
}
}
private void SyncSelectionFromViewModel()
{
var item = FindItemForPage(_viewModel.SelectedPage);
if (item is null || ReferenceEquals(_activeItem, item))
return;
// Flip IsActive on the previous and new items. NavigationViewItem
// exposes Activate/Deactivate publicly via INavigationViewItem; both
// call SetCurrentValue(IsActiveProperty, true/false) internally,
// which fires the IsActive trigger in BoxyNavItemTemplate.
_activeItem?.Deactivate(NavView);
item.Activate(NavView);
_activeItem = item;
}
private NavigationViewItem? FindItemForPage(AppPage page)
{
foreach (var menu in NavView.MenuItems)
if (menu is NavigationViewItem nvi && nvi.Tag is AppPage tag && tag == page)
return nvi;
foreach (var footer in NavView.FooterMenuItems)
if (footer is NavigationViewItem nvi && nvi.Tag is AppPage tag && tag == page)
return nvi;
return null;
}
private static T? FindAncestor(DependencyObject? start) where T : DependencyObject
{
for (var node = start; node is not null; node = System.Windows.Media.VisualTreeHelper.GetParent(node))
if (node is T match)
return match;
return null;
}
private void OnWindowClosing(object sender, CancelEventArgs e)
{
// Allow the window to close; services are disposed by the DI container.
}
// ── Maximised-bounds hook (WM_GETMINMAXINFO) ──────────────────────────────
private const int WM_GETMINMAXINFO = 0x0024;
private const int MONITOR_DEFAULTTONEAREST = 2;
///
/// Override (instead of event subscription) so the hook is installed at the
/// earliest possible point in the window lifecycle, before any subsequent
/// WM_GETMINMAXINFO. The maximize state itself is set here too, so it
/// runs only after the hook is in place.
///
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
var hwnd = new WindowInteropHelper(this).Handle;
HwndSource.FromHwnd(hwnd)?.AddHook(WindowProc);
WindowState = WindowState.Maximized;
}
private static IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == WM_GETMINMAXINFO)
{
ApplyWorkAreaMaxBounds(hwnd, lParam);
handled = true;
}
return IntPtr.Zero;
}
///
/// Constrains the window's WM_GETMINMAXINFO response to the current monitor's
/// work area. Without this hook, a maximised takes the full
/// monitor rectangle (including the taskbar) and gains an 8 px "phantom border" past
/// each edge, which is what produces the "bigger than full screen and clipped" symptom.
///
private static void ApplyWorkAreaMaxBounds(IntPtr hwnd, IntPtr lParam)
{
var mmi = Marshal.PtrToStructure(lParam);
IntPtr monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (monitor != IntPtr.Zero)
{
var info = new MONITORINFO { cbSize = Marshal.SizeOf() };
if (GetMonitorInfo(monitor, ref info))
{
var work = info.rcWork;
var monitorRect = info.rcMonitor;
mmi.ptMaxPosition.x = Math.Abs(work.left - monitorRect.left);
mmi.ptMaxPosition.y = Math.Abs(work.top - monitorRect.top);
mmi.ptMaxSize.x = Math.Abs(work.right - work.left);
mmi.ptMaxSize.y = Math.Abs(work.bottom - work.top);
}
}
Marshal.StructureToPtr(mmi, lParam, true);
}
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, int dwFlags);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
[StructLayout(LayoutKind.Sequential)]
private struct POINT { public int x; public int y; }
[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int left, top, right, bottom; }
[StructLayout(LayoutKind.Sequential)]
private struct MONITORINFO
{
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
[StructLayout(LayoutKind.Sequential)]
private struct MINMAXINFO
{
public POINT ptReserved;
public POINT ptMaxSize;
public POINT ptMaxPosition;
public POINT ptMinTrackSize;
public POINT ptMaxTrackSize;
}
}