20

I'm using a textured window that has a tab bar along the top of it, just below the title bar.

I've used -setContentBorderThickness:forEdge: on the window to make the gradient look right, and to make sure sheets slide out from the right position.

What's not working however, is dragging the window around. It works if I click and drag the area that's actually the title bar, but since the title bar gradient spills into a (potentially/often empty) tab bar, it's really easy to click too low and it feels really frustrating when you try to drag and realise the window is not moving.

I notice NSToolbar, while occupying roughly the same amount of space below the title bar, allows the window to be dragged around when the cursor is over it. How does one implement this?

Thanks.

Tab Bar

Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
d11wtq
  • 34,788
  • 19
  • 120
  • 195

9 Answers9

26

I tried the mouseDownCanMoveWindow solution (https://stackoverflow.com/a/4564146/901641) but it didn't work for me. I got rid of that method and instead added this to my window subclass:

- (BOOL)isMovableByWindowBackground {
    return YES;
}

which worked like a charm.

Community
  • 1
  • 1
ArtOfWarfare
  • 20,617
  • 19
  • 137
  • 193
  • 10
    In Swift: `window.movableByWindowBackground = true`. – twe4ked Jul 15 '14 at 21:58
  • 3
    @Odin - I'd imagine `window.movableByWindowBackground = YES;` works in Obj-C, but I think I went the method overriding route because I was specifically looking to make a subclass of `NSWindow` that exhibited this trait always. – ArtOfWarfare Jul 16 '14 at 00:16
  • You're probably right, I just pasted what I ended up with for anyone else that visits this page using Swift. Thanks for the answer :) – twe4ked Jul 16 '14 at 06:23
  • 1
    **Swift 3:** `window.isMovableByWindowBackground = true` – Clifton Labrum Feb 14 '17 at 23:11
16

I found this here:

-(void)mouseDown:(NSEvent *)theEvent {    
    NSRect  windowFrame = [[self window] frame];

    initialLocation = [NSEvent mouseLocation];

    initialLocation.x -= windowFrame.origin.x;
    initialLocation.y -= windowFrame.origin.y;
}

- (void)mouseDragged:(NSEvent *)theEvent {
    NSPoint currentLocation;
    NSPoint newOrigin;

    NSRect  screenFrame = [[NSScreen mainScreen] frame];
    NSRect  windowFrame = [self frame];

    currentLocation = [NSEvent mouseLocation];
    newOrigin.x = currentLocation.x - initialLocation.x;
    newOrigin.y = currentLocation.y - initialLocation.y;

    // Don't let window get dragged up under the menu bar
    if( (newOrigin.y+windowFrame.size.height) > (screenFrame.origin.y+screenFrame.size.height) ){
        newOrigin.y=screenFrame.origin.y + (screenFrame.size.height-windowFrame.size.height);
    }

    //go ahead and move the window to the new location
    [[self window] setFrameOrigin:newOrigin];
}

It works fine, though I'm not 100% sure I'm doing it correctly. There's one bug I've found so far, and that's if the drag begins inside a subview (a tab itself) and then enters the superview (the tab bar). The window jumps around. Some -hitTest: magic, or possibly even just invalidating initialLocation on mouseUp should probably fix that.

d11wtq
  • 34,788
  • 19
  • 120
  • 195
  • As suspected, invalidating the "initialLocation" variable in the mouseUp: event (just by setting the y position to a negative number), then adding a guard clause to mouseDragged: fixes the bug. – d11wtq Dec 30 '10 at 17:14
12

As of macOS 10.11, the simplest way to do this is to utilize the new -[NSWindow performWindowDragWithEvent:] method:

@interface MyView () {
    BOOL movingWindow;
}
@end

@implementation MyView

...

- (BOOL)mouseDownCanMoveWindow
{
    return NO;
}

- (void)mouseDown:(NSEvent *)event
{
    movingWindow = NO;

    CGPoint point = [self convertPoint:event.locationInWindow
                              fromView:nil];

    // The area in your view where you want the window to move:
    CGRect movableRect = CGRectMake(0, 0, 100, 100);

    if (self.window.movableByWindowBackground &&
        CGRectContainsPoint(movableRect, point)) {

        [self.window performWindowDragWithEvent:event];
        movingWindow = YES;
        return;
    }

    // Handle the -mouseDown: as usual
}

- (void)mouseDragged:(NSEvent *)event
{
    if (movingWindow) return;

    // Handle the -mouseDragged: as usual
}

@end

Here, -performWindowDragWithEvent: will handle the correct behavior of not overlapping the menu bar, and will also snap to edges on macOS 10.12 and later. Be sure to include a BOOL movingWindow instance variable with your view's private interface so you can avoid -mouseDragged: events once you determined you don't want to process them.

Here, we are also checking that -[NSWindow movableByWindowBackground] is set to YES so that this view can be used in non-movable-by-window-background windows, but that is optional.

Dimitri Bouniol
  • 735
  • 11
  • 15
  • This method works for my project. except that any mouse events in movableRect will perform moving, even if there are subviews above **the view**. – Lax Jan 30 '18 at 06:17
  • @Lax You may want to check that your view that appears above the dragging view is hit testing correctly - are both of them getting mouse down events? – Dimitri Bouniol Jan 31 '18 at 17:13
  • Thanks for the help, solved my problem but I needed it in Swift 5. Here is what I came up with – ervinbosenbacher Sep 14 '21 at 20:18
11

It works for me after TWO steps:

  1. Subclass NSView, override the mouseDownCanMoveWindow to return YES.
  2. Subclass NSWindow, override the isMovableByWindowBackground to return YES.
Nix Wang
  • 814
  • 10
  • 18
  • 3
    You can skip step two if you make your window "textured". (There's a checkbox in IB.) – Sam Soffes Feb 05 '15 at 01:10
  • 7
    You don't need to subclass NSWindow, since you can call `[self.view.window setMovableByWindowBackground:YES]` in the view controller. It appears NSWindow's `isMovableByWindowBackground` is mutable while NSView's `mouseDownCanMoveWindow` is read-only. – VinceFior Jul 11 '15 at 22:30
11

Have you tried overriding the NSView method mouseDownCanMoveWindow to return YES?

Richard
  • 3,316
  • 30
  • 41
  • I hadn't, since I didn't know such a method existed, thanks. However, it doesn't appear to have worked. The tab bar is a subview of a larger (almost entire window) view, if it makes a difference. – d11wtq Dec 30 '10 at 16:22
  • 3
    My next thought was to override `mouseDown:` (NSControl method) to pass the mouse event to `[self window]`, and ensure that the window `isMovableByWindowBackground`, but that still doesn't work for me. Sorry. – Richard Dec 30 '10 at 16:39
  • No dramas. You were on the right track. Thanks for investigating! :) – d11wtq Dec 30 '10 at 17:10
  • 1
    This is often useful but it seems like it's only called a few times when the view is loaded and then never again. Because of this you can't conditionally return based on some logic. – Keith Smiley Jan 13 '15 at 16:33
4

It's quite easy:

override mouseDownCanMoveWindow property

override var mouseDownCanMoveWindow:Bool {
    return false
}
D.A.H
  • 858
  • 2
  • 9
  • 19
4

If you got a NSTableView in your window, with selection enabled, overriding the mouseDownCanMoveWindow property won't work.

You need instead to create a NSTableView subclass and override the following mouse events (and use the performWindowDragWithEvent: mentioned in Dimitri answer):

@interface WindowDraggableTableView : NSTableView
@end

@implementation WindowDraggableTableView 
{
    BOOL _draggingWindow;
    NSEvent *_mouseDownEvent;
}

- (void)mouseDown:(NSEvent *)event
{
    if (self.window.movableByWindowBackground == NO) {
        [super mouseDown:event]; // Normal behavior.
        return;
    }

    _draggingWindow = NO;
    _mouseDownEvent = event;
}

- (void)mouseDragged:(NSEvent *)event
{
    if (self.window.movableByWindowBackground == NO) {
        [super mouseDragged:event]; // Normal behavior.
        return;
    }

    assert(_mouseDownEvent);
    _draggingWindow = YES;
    [self.window performWindowDragWithEvent:_mouseDownEvent];
}

- (void)mouseUp:(NSEvent *)event
{
    if (self.window.movableByWindowBackground == NO) {
        [super mouseUp:event]; // Normal behavior.
        return;
    }

    if (_draggingWindow == YES) {
        _draggingWindow = NO;
        return; // Event already handled by `performWindowDragWithEvent`.
    }

    // Triggers regular table selection.
    NSPoint locationInWindow = event.locationInWindow;
    NSPoint locationInTable = [self convertPoint:locationInWindow fromView:nil];
    NSInteger row = [self rowAtPoint:locationInTable];
    if (row >= 0 && [self.delegate tableView:self shouldSelectRow:row])
    {
        NSIndexSet *rowIndex = [NSIndexSet indexSetWithIndex:row];
        [self selectRowIndexes:rowIndex byExtendingSelection:NO];
    }
}

@end

Also don't forget to set the corresponding window movableByWindowBackground property as well:

self.window.movableByWindowBackground = YES;
Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
  • Are you sure you need the event code? Simply subclassing NSTableView and overriding 'mouseDownCanMoveWindow' works for me, even with selection enabled. – NickSpag Mar 26 '18 at 16:06
  • Hey @NickSpag, as far as I remember, doing as you suggested would mess up the table selection behaviour (when trying to drag the window by clicking over a given selectable table row). – Paulo Mattos Apr 07 '18 at 13:06
0

When you set property isMovableByWindowBackground in viewDidLoad, it may not work because the window property of the view is not yet set. In that case, try this:

override func viewDidAppear() {
    self.view.window?.isMovableByWindowBackground = true
}
Ely
  • 8,259
  • 1
  • 54
  • 67
0

Thank you! Dimitris' answer solved my issue but I needed it in swift 5. Here is what I came up with.

final class BlahField: NSTextView {
    
    var movingWindow = false
    
    override func mouseDown(with event: NSEvent) {
        movingWindow = false
        let point = self.convert(event.locationInWindow, from: nil)
        if (self.window!.isMovableByWindowBackground && self.frame.contains(point)) {
            self.window?.performDrag(with: event)
            movingWindow = true
            return
        }
    
    }
    
    override func mouseDragged(with event: NSEvent) {
        if (movingWindow) {
            return
        }
    }
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
ervinbosenbacher
  • 1,720
  • 13
  • 16