using System.Diagnostics; using System.Windows.Data; using System.Windows.Input; using WPFluent.Designer; using WPFluent.Extensions; using WPFluent.Input; using WPFluent.Interop; // ReSharper disable once CheckNamespace namespace WPFluent.Controls; /// /// Custom navigation buttons for the window. /// [TemplatePart(Name = ElementMainGrid, Type = typeof(System.Windows.Controls.Grid))] [TemplatePart(Name = ElementIcon, Type = typeof(System.Windows.Controls.Image))] [TemplatePart(Name = ElementHelpButton, Type = typeof(TitleBarButton))] [TemplatePart(Name = ElementMinimizeButton, Type = typeof(TitleBarButton))] [TemplatePart(Name = ElementMaximizeButton, Type = typeof(TitleBarButton))] [TemplatePart(Name = ElementRestoreButton, Type = typeof(TitleBarButton))] [TemplatePart(Name = ElementCloseButton, Type = typeof(TitleBarButton))] public class TitleBar : System.Windows.Controls.Control, IThemeControl { private const string ElementCloseButton = "PART_CloseButton"; private const string ElementHelpButton = "PART_HelpButton"; private const string ElementIcon = "PART_Icon"; private const string ElementMainGrid = "PART_MainGrid"; private const string ElementMaximizeButton = "PART_MaximizeButton"; private const string ElementMinimizeButton = "PART_MinimizeButton"; private const string ElementRestoreButton = "PART_RestoreButton"; private static DpiScale? dpiScale; /// /// Identifies the dependency property. /// public static readonly DependencyProperty ApplicationThemeProperty = DependencyProperty.Register( nameof(ApplicationTheme), typeof(Appearance.ApplicationTheme), typeof(TitleBar), new PropertyMetadata(Appearance.ApplicationTheme.Unknown)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty ButtonsBackgroundProperty = DependencyProperty.Register( nameof(ButtonsBackground), typeof(Brush), typeof(TitleBar), new FrameworkPropertyMetadata(SystemColors.ControlBrush, FrameworkPropertyMetadataOptions.Inherits)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty ButtonsForegroundProperty = DependencyProperty.Register( nameof(ButtonsForeground), typeof(Brush), typeof(TitleBar), new FrameworkPropertyMetadata(SystemColors.ControlTextBrush, FrameworkPropertyMetadataOptions.Inherits)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty CanMaximizeProperty = DependencyProperty.Register( nameof(CanMaximize), typeof(bool), typeof(TitleBar), new PropertyMetadata(true)); /// /// Identifies the routed event. /// public static readonly RoutedEvent CloseClickedEvent = EventManager.RegisterRoutedEvent( nameof(CloseClicked), RoutingStrategy.Bubble, typeof(TypedEventHandler), typeof(TitleBar)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty CloseWindowByDoubleClickOnIconProperty = DependencyProperty.Register( nameof(CloseWindowByDoubleClickOnIcon), typeof(bool), typeof(TitleBar), new PropertyMetadata(false)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty ForceShutdownProperty = DependencyProperty.Register( nameof(ForceShutdown), typeof(bool), typeof(TitleBar), new PropertyMetadata(false)); /// /// Property for . /// public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register( nameof(Header), typeof(object), typeof(TitleBar), new PropertyMetadata(null)); /// /// Identifies the routed event. /// public static readonly RoutedEvent HelpClickedEvent = EventManager.RegisterRoutedEvent( nameof(HelpClicked), RoutingStrategy.Bubble, typeof(TypedEventHandler), typeof(TitleBar)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty IconProperty = DependencyProperty.Register( nameof(Icon), typeof(IconElement), typeof(TitleBar), new PropertyMetadata(null)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty IsMaximizedProperty = DependencyProperty.Register( nameof(IsMaximized), typeof(bool), typeof(TitleBar), new PropertyMetadata(false)); /// /// Identifies the routed event. /// public static readonly RoutedEvent MaximizeClickedEvent = EventManager.RegisterRoutedEvent( nameof(MaximizeClicked), RoutingStrategy.Bubble, typeof(TypedEventHandler), typeof(TitleBar)); /// /// Identifies the routed event. /// public static readonly RoutedEvent MinimizeClickedEvent = EventManager.RegisterRoutedEvent( nameof(MinimizeClicked), RoutingStrategy.Bubble, typeof(TypedEventHandler), typeof(TitleBar)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty ShowCloseProperty = DependencyProperty.Register( nameof(ShowClose), typeof(bool), typeof(TitleBar), new PropertyMetadata(true)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty ShowHelpProperty = DependencyProperty.Register( nameof(ShowHelp), typeof(bool), typeof(TitleBar), new PropertyMetadata(false)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty ShowMaximizeProperty = DependencyProperty.Register( nameof(ShowMaximize), typeof(bool), typeof(TitleBar), new PropertyMetadata(true)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty ShowMinimizeProperty = DependencyProperty.Register( nameof(ShowMinimize), typeof(bool), typeof(TitleBar), new PropertyMetadata(true)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty TemplateButtonCommandProperty = DependencyProperty.Register( nameof(TemplateButtonCommand), typeof(IRelayCommand), typeof(TitleBar), new PropertyMetadata(null)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty TitleProperty = DependencyProperty.Register( nameof(Title), typeof(string), typeof(TitleBar), new PropertyMetadata(null)); /// /// Property for . /// public static readonly DependencyProperty TrailingContentProperty = DependencyProperty.Register( nameof(TrailingContent), typeof(object), typeof(TitleBar), new PropertyMetadata(null)); private readonly TitleBarButton[] _buttons = new TitleBarButton[4]; private System.Windows.Window _currentWindow = null!; /*private System.Windows.Controls.Grid _mainGrid = null!;*/ private System.Windows.Controls.ContentPresenter _icon = null!; private DependencyObject _parentWindow; private readonly TextBlock _titleBlock; /// /// Initializes a new instance of the class and sets the default event. /// public TitleBar() { SetValue(TemplateButtonCommandProperty, new RelayCommand(OnTemplateButtonClick)); dpiScale ??= VisualTreeHelper.GetDpi(this); _titleBlock = new TextBlock(); _titleBlock.VerticalAlignment = VerticalAlignment.Center; _titleBlock.SetBinding( System.Windows.Controls.TextBlock.TextProperty, new Binding(nameof(Title)) { Source = this }); _titleBlock.SetBinding( System.Windows.Controls.TextBlock.FontSizeProperty, new Binding(nameof(FontSize)) { Source = this }); Header = _titleBlock; Loaded += OnLoaded; Unloaded += OnUnloaded; } /// /// Event triggered after clicking close button. /// public event TypedEventHandler CloseClicked { add => AddHandler(CloseClickedEvent, value); remove => RemoveHandler(CloseClickedEvent, value); } /// /// Event triggered after clicking help button /// public event TypedEventHandler HelpClicked { add => AddHandler(HelpClickedEvent, value); remove => RemoveHandler(HelpClickedEvent, value); } /// /// Event triggered after clicking maximize or restore button. /// public event TypedEventHandler MaximizeClicked { add => AddHandler(MaximizeClickedEvent, value); remove => RemoveHandler(MaximizeClickedEvent, value); } /// /// Event triggered after clicking minimize button. /// public event TypedEventHandler MinimizeClicked { add => AddHandler(MinimizeClickedEvent, value); remove => RemoveHandler(MinimizeClickedEvent, value); } private void CloseWindow() { Debug.WriteLine( $"INFO | {typeof(TitleBar)}.CloseWindow:ForceShutdown - {ForceShutdown}", "WPFluent.TitleBar"); if (ForceShutdown) { UiApplication.Current.Shutdown(); return; } _currentWindow.Close(); } private T GetTemplateChild(string name) where T : DependencyObject { var element = GetTemplateChild(name); if (element is not T tElement) { throw new InvalidOperationException($"Template part '{name}' is not found or is not of type {typeof(T)}"); } return tElement; } private IntPtr HwndSourceHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { var message = (User32.WM)msg; if (message is not ( User32.WM.NCHITTEST or User32.WM.NCMOUSELEAVE or User32.WM.NCLBUTTONDOWN or User32.WM.NCLBUTTONUP )) { return IntPtr.Zero; } foreach (TitleBarButton button in _buttons) { if (!button.ReactToHwndHook(message, lParam, out IntPtr returnIntPtr)) { continue; } // Fix for when sometimes, button hover backgrounds aren't cleared correctly, causing multiple buttons to appear as if hovered. foreach (TitleBarButton anotherButton in _buttons) { if (anotherButton == button) { continue; } if (anotherButton.IsHovered && button.IsHovered) { anotherButton.RemoveHover(); } } handled = true; return returnIntPtr; } var isMouseOverHeaderContent = false; if (message == User32.WM.NCHITTEST && (TrailingContent is UIElement || Header is UIElement)) { var headerLeftUIElement = Header as UIElement; var headerRightUiElement = TrailingContent as UIElement; if (headerLeftUIElement is not null && headerLeftUIElement != _titleBlock) { isMouseOverHeaderContent = headerLeftUIElement.IsMouseOverElement(lParam) || (headerRightUiElement?.IsMouseOverElement(lParam) ?? false); } else { isMouseOverHeaderContent = headerRightUiElement?.IsMouseOverElement(lParam) ?? false; } } switch (message) { case User32.WM.NCHITTEST when CloseWindowByDoubleClickOnIcon && _icon.IsMouseOverElement(lParam): // Ideally, clicking on the icon should open the system menu, but when the system menu is opened manually, double-clicking on the icon does not close the window handled = true; return (IntPtr)User32.WM_NCHITTEST.HTSYSMENU; case User32.WM.NCHITTEST when this.IsMouseOverElement(lParam) && !isMouseOverHeaderContent: handled = true; return (IntPtr)User32.WM_NCHITTEST.HTCAPTION; default: return IntPtr.Zero; } } private void MaximizeWindow() { if (!CanMaximize) { return; } if (MaximizeActionOverride is not null) { MaximizeActionOverride(this, _currentWindow); return; } if (_currentWindow.WindowState == WindowState.Normal) { SetCurrentValue(IsMaximizedProperty, true); _currentWindow.SetCurrentValue(Window.WindowStateProperty, WindowState.Maximized); } else { SetCurrentValue(IsMaximizedProperty, false); _currentWindow.SetCurrentValue(Window.WindowStateProperty, WindowState.Normal); } } private void MinimizeWindow() { if (MinimizeActionOverride is not null) { MinimizeActionOverride(this, _currentWindow); return; } _currentWindow.SetCurrentValue(Window.WindowStateProperty, WindowState.Minimized); } private void OnParentWindowStateChanged(object sender, EventArgs e) { if (IsMaximized != (_currentWindow.WindowState == WindowState.Maximized)) { SetCurrentValue(IsMaximizedProperty, _currentWindow.WindowState == WindowState.Maximized); } } private void OnTemplateButtonClick(TitleBarButtonType buttonType) { switch (buttonType) { case TitleBarButtonType.Maximize or TitleBarButtonType.Restore: RaiseEvent(new RoutedEventArgs(MaximizeClickedEvent, this)); MaximizeWindow(); break; case TitleBarButtonType.Close: RaiseEvent(new RoutedEventArgs(CloseClickedEvent, this)); CloseWindow(); break; case TitleBarButtonType.Minimize: RaiseEvent(new RoutedEventArgs(MinimizeClickedEvent, this)); MinimizeWindow(); break; case TitleBarButtonType.Help: RaiseEvent(new RoutedEventArgs(HelpClickedEvent, this)); break; } } private void OnUnloaded(object sender, RoutedEventArgs e) { Loaded -= OnLoaded; Unloaded -= OnUnloaded; Appearance.ApplicationThemeManager.Changed -= OnThemeChanged; } /// /// Listening window hooks after rendering window content to SizeToContent support /// private void OnWindowContentRendered(object sender, EventArgs e) { if (sender is not Window window) { return; } window.ContentRendered -= OnWindowContentRendered; var handle = new WindowInteropHelper(window).Handle; var windowSource = HwndSource.FromHwnd(handle) ?? throw new InvalidOperationException("Window source is null"); windowSource.AddHook(HwndSourceHook); } /// /// Show 'SystemMenu' on mouse right button up. /// private void TitleBar_MouseRightButtonUp(object sender, MouseButtonEventArgs e) { var point = PointToScreen(e.GetPosition(this)); if (dpiScale is null) { throw new InvalidOperationException("dpiScale is not initialized."); } SystemCommands.ShowSystemMenu( _parentWindow as Window, new Point(point.X / dpiScale.Value.DpiScaleX, point.Y / dpiScale.Value.DpiScaleY)); } /// protected override void OnInitialized(EventArgs e) { base.OnInitialized(e); SetCurrentValue(ApplicationThemeProperty, Appearance.ApplicationThemeManager.GetAppTheme()); Appearance.ApplicationThemeManager.Changed += OnThemeChanged; } protected virtual void OnLoaded(object sender, RoutedEventArgs e) { if (DesignerHelper.IsInDesignMode) { return; } _currentWindow = System.Windows.Window.GetWindow(this) ?? throw new InvalidOperationException("Window is null"); _currentWindow.StateChanged += OnParentWindowStateChanged; _currentWindow.ContentRendered += OnWindowContentRendered; } /// /// This virtual method is triggered when the app's theme changes. /// protected virtual void OnThemeChanged(Appearance.ApplicationTheme currentApplicationTheme, Color systemAccent) { Debug.WriteLine( $"INFO | {typeof(TitleBar)} received theme - {currentApplicationTheme}", "WPFluent.TitleBar"); SetCurrentValue(ApplicationThemeProperty, currentApplicationTheme); } /// /// Invoked whenever application code or an internal process, such as a rebuilding layout pass, calls the /// ApplyTemplate method. /// public override void OnApplyTemplate() { base.OnApplyTemplate(); _parentWindow = VisualTreeHelper.GetParent(this); while (_parentWindow is not null and not Window) { _parentWindow = VisualTreeHelper.GetParent(_parentWindow); } MouseRightButtonUp += TitleBar_MouseRightButtonUp; /*_mainGrid = GetTemplateChild(ElementMainGrid);*/ _icon = GetTemplateChild(ElementIcon); var helpButton = GetTemplateChild(ElementHelpButton); var minimizeButton = GetTemplateChild(ElementMinimizeButton); var maximizeButton = GetTemplateChild(ElementMaximizeButton); var closeButton = GetTemplateChild(ElementCloseButton); _buttons[0] = maximizeButton; _buttons[1] = minimizeButton; _buttons[2] = closeButton; _buttons[3] = helpButton; } /// public Appearance.ApplicationTheme ApplicationTheme { get => (Appearance.ApplicationTheme)GetValue(ApplicationThemeProperty); set => SetValue(ApplicationThemeProperty, value); } /// /// Gets or sets the background of the navigation buttons when hovered. /// [Bindable(true)] [Category("Appearance")] public Brush ButtonsBackground { get => (Brush)GetValue(ButtonsBackgroundProperty); set => SetValue(ButtonsBackgroundProperty, value); } /// /// Gets or sets the foreground of the navigation buttons. /// [Bindable(true)] [Category("Appearance")] public Brush ButtonsForeground { get => (Brush)GetValue(ButtonsForegroundProperty); set => SetValue(ButtonsForegroundProperty, value); } /// /// Gets or sets a value indicating whether the maximize functionality is enabled. If disabled the /// MaximizeActionOverride action won't be called /// public bool CanMaximize { get => (bool)GetValue(CanMaximizeProperty); set => SetValue(CanMaximizeProperty, value); } /// /// Gets or sets a value indicating whether the window can be closed by double clicking on the icon /// public bool CloseWindowByDoubleClickOnIcon { get => (bool)GetValue(CloseWindowByDoubleClickOnIconProperty); set => SetValue(CloseWindowByDoubleClickOnIconProperty, value); } /// /// Gets or sets a value indicating whether the controls affect main application window. /// public bool ForceShutdown { get => (bool)GetValue(ForceShutdownProperty); set => SetValue(ForceShutdownProperty, value); } /// /// Gets or sets the content displayed in the left side of the . /// public object Header { get => GetValue(HeaderProperty); set => SetValue(HeaderProperty, value); } /// /// Gets or sets the titlebar icon. /// public IconElement Icon { get => (IconElement)GetValue(IconProperty); set => SetValue(IconProperty, value); } /// /// Gets a value indicating whether the current window is maximized. /// public bool IsMaximized { get => (bool)GetValue(IsMaximizedProperty); internal set => SetValue(IsMaximizedProperty, value); } /// /// Gets or sets the that should be executed when the Maximize button is clicked."/> /// public Action MaximizeActionOverride { get; set; } /// /// Gets or sets what should be executed when the Minimize button is clicked. /// public Action MinimizeActionOverride { get; set; } /// /// Gets or sets a value indicating whether to show the close button. /// public bool ShowClose { get => (bool)GetValue(ShowCloseProperty); set => SetValue(ShowCloseProperty, value); } /// /// Gets or sets a value indicating whether to show the help button /// public bool ShowHelp { get => (bool)GetValue(ShowHelpProperty); set => SetValue(ShowHelpProperty, value); } /// /// Gets or sets a value indicating whether to show the maximize button. /// public bool ShowMaximize { get => (bool)GetValue(ShowMaximizeProperty); set => SetValue(ShowMaximizeProperty, value); } /// /// Gets or sets a value indicating whether to show the minimize button. /// public bool ShowMinimize { get => (bool)GetValue(ShowMinimizeProperty); set => SetValue(ShowMinimizeProperty, value); } /// /// Gets the command triggered when clicking the titlebar button. /// internal IRelayCommand TemplateButtonCommand => (IRelayCommand)GetValue(TemplateButtonCommandProperty); /// /// Gets or sets title displayed on the left. /// public string Title { get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); } /// /// Gets or sets the content displayed in right side of the . /// public object TrailingContent { get => GetValue(TrailingContentProperty); set => SetValue(TrailingContentProperty, value); } }