macoswindowmouseeventnstrackingarea

How does one handle mouseEntered: and mouseExited: events when nswindow style is fullSizeContentView?


I have a macOS app with a view that occupies the entire window, with NSWindowStyleMaskFullSizeContentView set within the window's styleMask. I want to show the titlebar and window buttons when the mouse enters the window, and hide them when the mouse exits the window.

I am using a tracking area whose rect is window.contentView.bounds, with the height of the titlebar subtracted from its height.

I have implemented the mouseEntered: and mouseExited: methods to handle the show/hide actions.

The problem I have is when I move the mouse to the window's titlebar, the app receives multiple mouseExited:/mouseEntered: events in rapid succession, causing a "fluttering" effect with the titlebar.

To reproduce this problem, I first created an Objective-C project in Xcode, and used IB to set the window property to 'Full Size Content View'. Then edited ViewController.m as shown:

    //
    //  ViewController.m
    //  Repro
    //
    
    #import "ViewController.h"
    
    @implementation ViewController
    {
        NSTrackingArea  *trackingArea;
    }
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        // Do any additional setup after loading the view.
    }
    - (void)viewWillAppear
    {
        // background color
        self.view.wantsLayer = true;
        self.view.layer.backgroundColor = [NSColor systemBlueColor].CGColor;
    
        // tracking area
        [self setupTrackingArea:self.view.window.contentView.bounds];
        
        return;
    }
    - (void)setupTrackingArea:(CGRect)frame
    {
        if (trackingArea)
        {
            [self.view removeTrackingArea:trackingArea];
        }
    
        NSTrackingAreaOptions   options = NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow | NSTrackingInVisibleRect | NSTrackingMouseMoved;
    
        // subtract the titlebar height from the content view height
        frame.size.height -= [self titlebarHeight:self.view.window];
    
        trackingArea = [[NSTrackingArea alloc]initWithRect:frame
                               options:options
                                 owner:self
                              userInfo:nil];
        [self.view addTrackingArea:trackingArea];
    }
    - (CGFloat)titlebarHeight:(NSWindow *)window
    {
        CGFloat windowHeight = window.contentView.frame.size.height;
        CGFloat contentHeight = window.contentLayoutRect.size.height;
        CGFloat titlebarHeight = windowHeight - contentHeight;
        return titlebarHeight;
    }
    
    - (void)mouseEntered:(NSEvent *)event
    {
        [self showTitleBar];
        [self showWindowButtons:self.view.window];
    
        return;
    }
    - (void)mouseExited:(NSEvent *)event
    {
        [self hideTitleBar];
        [self hideWindowButtons:self.view.window];
        return;
    }
    
    - (void)showTitleBar
    {
        self.view.window.titlebarAppearsTransparent = false;
        self.view.window.title = @"Repro";
        return;
    }
    - (void)hideTitleBar
    {
        self.view.window.titlebarAppearsTransparent = true;
        self.view.window.title = @"";
        return;
    }
    
    - (void)hideWindowButtons:(NSWindow *)window
    {
        [window standardWindowButton:NSWindowZoomButton].hidden = true;
        [window standardWindowButton:NSWindowMiniaturizeButton].hidden = true;
        [window standardWindowButton:NSWindowCloseButton].hidden = true;
        return;
    }
    - (void)showWindowButtons:(NSWindow *)window
    {
        [window standardWindowButton:NSWindowZoomButton].hidden = false;
        [window standardWindowButton:NSWindowMiniaturizeButton].hidden = false;
        [window standardWindowButton:NSWindowCloseButton].hidden = false;
        return;
    }

@end

Solution

  • Thanks to suggestions by Willeke (thanks so much for your help!), there is a solution. It involved subclassing NSView (I called it MyView), moving all of the tracking area handling into that view, and removing it from the ViewController. MyView.m is:

    //
    //  MyView.m
    //  Repro
    //
    
    #import "MyView.h"
    
    @implementation MyView
    {
            NSTrackingArea  *trackingArea;
    }
    
    - (void)viewWillMoveToWindow:(NSWindow *)newWindow
    {
            if (newWindow)
            {
                    newWindow.styleMask |= NSWindowStyleMaskFullSizeContentView;
    
                    [self setupTrackingArea:newWindow];
            }
            [super viewWillMoveToWindow:newWindow];
            return;
    }
    - (void)updateTrackingAreas
    {
            [self setupTrackingArea:self.window];
    
            [super updateTrackingAreas];
    
            return;
    }
    
    - (void)setupTrackingArea:(NSWindow *)window
    {
            if (trackingArea)
                    [self removeTrackingArea:trackingArea];
    
            NSTrackingAreaOptions   options = NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways;
            trackingArea = [[NSTrackingArea alloc]initWithRect:self.bounds
                                                       options:options
                                                         owner:self
                                                      userInfo:nil];
            [self addTrackingArea:trackingArea];
            return;
    }
    - (void)mouseEntered:(NSEvent *)event
    {
            [self showTitleBar];
            [self showWindowButtons:self.window];
    
            return;
    }
    - (void)mouseExited:(NSEvent *)event
    {
            [self hideTitleBar];
            [self hideWindowButtons:self.window];
            return;
    }
    
    - (void)showTitleBar
    {
            self.window.titlebarAppearsTransparent = false;
            self.window.title = [NSProcessInfo processInfo].processName;
            return;
    }
    - (void)hideTitleBar
    {
            self.window.titlebarAppearsTransparent = true;
            self.window.title = @"";
            return;
    }
    
    - (void)hideWindowButtons:(NSWindow *)window
    {
            // hide the minimize and zoom buttons (yellow and green)
            [window standardWindowButton:NSWindowZoomButton].hidden = true;
            [window standardWindowButton:NSWindowMiniaturizeButton].hidden = true;
            [window standardWindowButton:NSWindowCloseButton].hidden = true;
            return;
    }
    - (void)showWindowButtons:(NSWindow *)window
    {
            [window standardWindowButton:NSWindowZoomButton].hidden = false;
            [window standardWindowButton:NSWindowMiniaturizeButton].hidden = false;
            [window standardWindowButton:NSWindowCloseButton].hidden = false;
            return;
    }
    @end