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; } }