Search code examples
objective-cmacoscocoansviewnswindow

Move NSWindow by dragging NSView that is above other views


I have a macOS application that contains an NSTableView and an NSVisualEffectView. The visual effect view is acting like a bar at the bottom of the window, it is in the table view (containing a few buttons/etc..).

Anyway if I want to move the NSWindow by dragging the visual effect view, it will only work if the table view is not below the visual effect view. The reason I want visual effect view to be above the table view is so that I get a nice blur effect when then the user is scrolling through the table view content.

However, when the visual effect view is above the table view, the mouse/drag/etc events are not registered. Instead, they get passed to the table view. How can I stop this from happening?

I tried subclassing NSVisualEffectView, but everything I have tried has failed. Here is my code:

#import <Cocoa/Cocoa.h>

@interface BottomMainBar : NSVisualEffectView {
    
}

@end

Here is the implementation code:

#import "BottomMainBar.h"

@implementation BottomMainBar

/// DRAW RECT METHOD ///

-(void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];
    
    [self setWantsLayer:YES];
    
    [self.window setMovableByWindowBackground:YES];
    
    [self setAcceptsTouchEvents:YES];
    
    [self registeredDraggedTypes];
}

/// OTHER METHODS ///

-(BOOL)mouseDownCanMoveWindow {
    return YES;
}

-(BOOL)acceptsFirstMouse:(NSEvent *)event {
    return YES;
}

-(void)mouseDown:(NSEvent *)event {}
-(void)mouseDragged:(NSEvent *)event {}
-(void)mouseUp:(NSEvent *)event {}
-(void)mouseEntered:(NSEvent *)event {}
-(void)mouseExited:(NSEvent *)event {}

@end

Nothing I have tried has worked, how can I stop the visual effect view from passing on the mouse events to the layer below it?


Solution

  • In the end I managed to find out a solution and thankfully it involves NO libraries or open source code (and obviously no private apis).

    The problem

    I have a NSVisualEffectView that spans the width of my view controller and it 38 px tall. It is positioned at the top of my view controller. It acts as a custom toolbar that contains a few buttons and labels. It is placed above a NSTableView that displays all sorts of content (images, video, text, etc...).

    I placed the visual effect view above the table view, because I wanted to have a nice blur effect when the user scrolled the table view. The problem with this, is that the mouse down events on the visual effect view, get passed to table view and NOT the overall NSWindow. This results in the user being unable to drag and move the NSWindow, when they click and drag the visual effect view (because the mouse down events are not passed to the window).

    I noticed that the top 10px of the visual effect DID pass the mouse down events to the window and not the table view. This is because the window's title bar is around 10-15px tall. However my visual effect view is 38px tall, so the bottom half of my visual effect view was unable to move the window.

    The solution

    The solution involves making two subclasses, one for the visual effect view and another for the NSWindow. The subclass for the visual effect view, simply passes the mouse down events to the nextResponder (which can be the table view or the window - depending on the size of the window title bar).

    Header code (Visual Effect View class):

    #import <Cocoa/Cocoa.h>
    
    @interface TopMainBar : NSVisualEffectView {
    
    }
    
    @end
    

    Implementation code (Visual Effect View class):

    #import "TopMainBar.h"
    
    @implementation TopMainBar
    
    /// INIT WITH FRAME ///
    
    -(id)initWithFrame:(NSRect)frameRect {
    
        if ((self = [super initWithFrame:frameRect])) {
            [self setWantsLayer:YES];
            [self.window setMovableByWindowBackground:YES];
        }
    
        return self;
    }
    
    /// MOUSE METHODS ///
    
    -(void)mouseDown:(NSEvent *)event {
        [self.window mouseDown:event];
    }
    
    @end
    

    The subclass for the window involves turning the window title bar into a toolbar, this in effect increases the size of the title bar (and as it happens increases it to around 38 px which is exactly what I needed). The ideal solution, would involve being able to increase the title bar height to any custom size, however that is not possible, so the toolbar solution is the only way.

    Because the size of the title bar is increased, all the mouse down events are not passed to the window and not the table view. This enables the user to drag the window from any part of the visual effect view.

    Header code (Window class):

    #import <Cocoa/Cocoa.h>
    
    @interface CustomWindow : NSWindowController <NSWindowDelegate> {
    
    }
    
    // UI methods.
    -(BOOL)isWindowFullScreen;
    
    @end
    

    Implementation code (Window class):

    #import "CustomWindow.h"
    
    @interface CustomWindow ()
    
    @end
    
    @implementation CustomWindow
    
    /// WINDOW DID LOAD ///
    
    -(void)windowDidLoad {
        [super windowDidLoad];
    
        // Ensure this window is the current selected one.
        [self.window makeKeyAndOrderFront:self];
    
        // Ensure the window can be moved.
        [self.window setMovableByWindowBackground:YES];
    
        // Set the window title bar options.
        self.window.titleVisibility = NSWindowTitleHidden;
        self.window.titlebarAppearsTransparent = YES;
        self.window.styleMask |= (NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskUnifiedTitleAndToolbar | NSWindowStyleMaskTitled);
        self.window.movableByWindowBackground = YES;
        self.window.toolbar.showsBaselineSeparator = NO;
        self.window.toolbar.fullScreenAccessoryView.hidden = YES;
        self.window.toolbar.visible = ![self isWindowFullScreen];
    }
    
    /// UI METHODS ///
    
    -(BOOL)isWindowFullScreen {
        return (([self.window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen);
    }
    
    /// WINDOW METHODS ///
    
    -(void)windowWillEnterFullScreen:(NSNotification *)notification {
        self.window.toolbar.visible = NO;
    }
    
    -(void)windowDidEnterFullScreen:(NSNotification *)notification {
        self.window.toolbar.visible = NO;
    }
    
    -(void)windowWillExitFullScreen:(NSNotification *)notification {
        self.window.toolbar.visible = YES;
    }
    
    -(void)windowDidExitFullScreen:(NSNotification *)notification {
        self.window.toolbar.visible = YES;
    }
    
    /// OTHER METHODS ///
    
    -(BOOL)mouseDownCanMoveWindow {
        return YES;
    }
    
    @end
    

    In the custom window class you can see that I am changing the toolbar visibility depending on the full screen state of the window. This is to stop title bar appearing and covering my custom visual effect view up, when the window goes into full screen mode.

    In order for this to work, you need to add an empty toolbar to your window, you can do this in interface builder, by dragging and dropping a NSToolbar object, to your window.

    enter image description here

    Make sure you connect the window to the window delegate, otherwise the full screen delegate method will not be called.

    Conclusion

    This solution involves increasing the size of the title bar by changing it into a toolbar. The mouse down events that are passed from the visual effect view class, are then read by the window (not any other view behind it) and thus the window can be moved.