Move NSWindow by dragging NSView that is above other views - objective-c

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?
Thanks for your time, Dan.

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.
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.

Related

keyDown: Not Called on NSClipView Subclass

My app is not document based, and its sole window is managed by a custom, xib-based NSWindowController subclass that I instantiate within the app delegate code:
- (void) applicationDidFinishLaunching:(NSNotification*) aNotification
{
_mainWindowController = [MainWindowController new];
// (stored in ivar just to prevent deallocation)
//[_mainWindowController showWindow:self];
// ↕︎ Not sure about the difference between these two... both seem to work.
[[_mainWindowController window] makeKeyAndOrderFront:self];
}
I have subclassed NSClipView to "center content inside a scroll view" (instead of having it pegged to the lower left corner) when it is zoomed to a size smaller than the clip view, and also implement custom functionality on mouse drag etc.
My window does have a title bar.
My window isn't borderless (I think), so I am not subclassing NSWindow.
I have overriden -acceptsFirstResponder, -canBecomeKeyView and -becomeFirstResponder in my NSClipview subclass (all return YES).
The drag events do trigger -mouseDown: etc., and if I set a breakpoint there, the first responder at that point is the same as the window hosting my clip view: [self.window firstResponder] and [self window] give the same memory address.
What am I missing?
Update
I put together a minimal project reproducing my setup.
I discovered that if my custom view is the window's main view, -keyDown: is called without problems. But if I place a scroll view and replace its clip view by my custom view (to do that, I need to change the base class from NSView to NSClipView, of course!), -keyDown: is no longer triggered.
I assume it has something to do with how NSScrollView manages events (however, as I said before, -mouseDown:, -mouseDragged: etc. seem to be unaffected).
I also discovered that I can override -keyDown: in my window controller, and that seems to work, so I have decided to do just that (still open to an answer, though). Also, since I'm trying to detect the shift key alone (not as a modifier of another key), I'd rather use:
- (void) flagsChanged:(NSEvent *) event
{
if ([event modifierFlags] & NSShiftKeyMask) {
// Shift key is DOWN
}
else{
// Shift key is UP
}
}
...instead of -keyDown: / -keyUp: (taken from this answer).

NSTextView auto-scroll

Suppose there is a window with has a NSTextView which contains enough text to trigger the scrollbars. When I resize the window, the textview is automatically scrolled so that the line which contains the cursor appears in the middle of the textview.
For example, this can also be seen in TextEdit in MacOS: paste bunch of text in it, scroll almost to the top [1], place cursor into the first visible line and resize the window. Now the view should scroll its content so that the cursor lands in the middle of the view.
My question is, how do I turn off this behavior? That is, I would like the textview to never automatically scroll the cursor to the middle when the window gets resized..?
[1] The actual scroll position at which the said behavior happens may require some trial-and-error, as I was unable to find out a pattern at which this happens. In my testing it happened when the scrollbar is at 10% - 30% position of the total height (from the top).
You can do the tweak like this below:-
Create Custom Class of NSTextView and implement one delegate method for textview resizing and one method when click on textview. Refer below:-
.h file
#interface textView : NSTextView
#end
.m file
#import "textView.h"
#implementation textView
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// Drawing code here.
}
//Below delegate method which will call when resize the textview. So just set your text view to be non editable.
- (void)viewDidEndLiveResize
{
[self setEditable:NO];
[self setSelectable:NO];
}
//Now when you click on the textview below method will called.
-(void)mouseDown:(NSEvent*) theEvent
{
[super mouseDown:theEvent];
[self setEditable:YES];
[self setSelectable:YES];
}
-(void)keyDown:(NSEvent *)theEvent
{
[super keyDown:theEvent];
[self setEditable:YES];
[self setSelectable:YES];
}
#end
Edit:-
Also, mention the custom class name in interface builder inside textview -> Custom Class

NSTextView not properly resizing with auto layout

I have a simple layout, which consists of NSView and its subview NSTextView. NSTextView is programmatically filled with some text that spawns multiple lines. I tie everything together using auto-layout (all done programmatically). However, when everything is displayed NSTextView is cut off, only one line is showing.
After searching the web, the best answer I could find was:
Using Autolayout with expanding NSTextViews
However, this only works if I manually change the text in NSTextView after everything is displayed (which is not really my use case). The views are readjusted and the whole NSTextView is displayed.
I am trying to figure out when NSViewController is done with laying out subviews so that I could call invalidateIntrinsicContentSize on the NSTextView. The equivalent of viewDidLayoutSubviews in UIViewController.
Nothing I tried worked so far. I attempted calling invalidateIntrinsicContentSize for NSTextView:
At the end of loadView
After I filled NSTextView with my text
Is there a better way to achieve this?
After further research, found the answer:
Create custom NSView subclass that contains NSTextView
In NSView subclass override layout method that calls invalidateIntrinsicContentSize
Also check out this link that explains subtleties of auto layout and intrinsic content size (among many other things):
http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html
Sample code:
#interface MyView : NSView
#property MyTextView *textView;
#end
#implementation MyView
// init & create content & set constraints
-(void) layout {
[super layout];
[self.textView invalidateIntrinsicContentSize];
}
#end
Implementation of MyTextView:
#implementation MyTextView
- (NSSize) intrinsicContentSize {
NSTextContainer* textContainer = [self textContainer];
NSLayoutManager* layoutManager = [self layoutManager];
[layoutManager ensureLayoutForTextContainer: textContainer];
return [layoutManager usedRectForTextContainer: textContainer].size;
}
- (void) didChangeText {
[super didChangeText];
[self invalidateIntrinsicContentSize];
}
#end

Detect scroll wheel movement and direction

I'm programming a game using the new Sprite-Kit framework and want to use the mouse scroll wheel to change the player's gun. First of all i want to handle when the scroll wheel moves. I tried the following method, from the Cocoa Event Handling Guide
- (void)scrollWheel:(NSEvent *)theEvent
{
NSLog(#"SCROOOOOOOLL iN MOVeMENT");
}
Result: nothing, i didn't handle when i moved the mouse wheel.
Any idea of how can i handle this event ?
UPDATE:
I saw two answers talking about class derivative from NSResponder or NSView but my class derives from SKScene, i'm programming using the Sprite-kit framework and obj-c doesn't allow multiple inheritance.
Here the class definition:
#import <SpriteKit/SpriteKit.h>
#interface OpcionesMenu : SKScene
#end
Even though SKScene derives from NSResponder, it isn't a direct recipient of AppKit events. Rather, the SKView (derived from NSView/UIView) that houses the scene handles the events. You can observe this by setting a breakpoint on your scene's mouseDown method:
-[TWTMyScene mouseDown:]
-[SKView mouseDown:] ()
-[NSWindow sendEvent:] ()
-[NSApplication sendEvent:] ()
-[NSApplication run] ()
NSApplicationMain ()
So you can subclass SKView and forward the scrollWheel event yourself. Or, if you like living dangerously, you can just paste this category at the end of any of your code and it will take care of it for you:
#implementation SKView(ScrollTouchForwarding)
- (void)scrollWheel:(NSEvent *)event {
[self.scene scrollWheel:event];
}
#end
I think that could help:
- (void)scrollWheel:(NSEvent *)theEvent
{
[super scrollWheel:theEvent];
NSLog(#"user scrolled %f horizontally and %f vertically", [theEvent deltaX], [theEvent deltaY]);
}
I know this is an old question, but I was having this exact problem with a collection view. So I have a NSScrollView that contains a NSCollectionView and some other views such as labels and buttons.
Anyway I wanted to get the collection view to be able to scroll horizontally and parent scroll view to to scroll vertically. I achieved this by storing and comparing the deltaX and deltaY values.
I made a custom scroll view class and set it to the collection view's scroll view.
Here is the header code:
#import <Cocoa/Cocoa.h>
#interface CustomCollectionScrollView : NSScrollView {
// Current scroll position.
CGFloat positionAxisX;
CGFloat positionAxisY;
}
#end
And here is the implementation code:
#import "CustomCollectionScrollView.h"
#implementation CustomCollectionScrollView
/// DRAW RECT METHOD ///
-(void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
positionAxisX = 0.0;
positionAxisY = 0.0;
}
/// SCROLL METHODS ///
-(void)scrollWheel:(NSEvent *)theEvent {
// Move the parent scroll view scroll view if the user is
// scrolling vertically otherwise move the collection scroll view.
if ((theEvent.deltaX == positionAxisX) && (theEvent.deltaY != positionAxisY)) {
[self.nextResponder scrollWheel:theEvent];
} else if ((theEvent.deltaX != positionAxisX) && (theEvent.deltaY == positionAxisY)) {
[super scrollWheel:theEvent];
}
// Update the current position values.
positionAxisX = theEvent.deltaX;
positionAxisY = theEvent.deltaY;
}
#end
I don't know much about SpriteKit, but I see that there is an SKView class that is a subclass of NSView. I would guess that you should subclass SKView and put your scrollWheel: method there.
SKScene is part of SpriteKit which is an iOS framework, not an AppKit framework. Hence there is no scroll wheel handling at all (and no NSResponder (but UIResponder) in the inheritance chain). You can only work with touch events in the sprite kit.

NSView's context NSMenu is never shown even though all the right methods are being called

I have an NSCollectionView with a bunch of NSViews in it, stacked vertically, to make it look a bit like UIKit's UITableView. Everything works as expected, except for one thing:
When right-clicking any one of the NSViews, I expect the NSMenu I set to be view's menu to be shown, but alas - nothing happens.
The crazy part is all the right methods are being called, exactly as could be expected: -rightMouseDown:, -menuForEvent: and finally -menu.
When I set up any object as the NSMenu's delegate, menuWillOpen: is not called, so it seems to me something fails over on Apple's side of things, just in between asking for the menu, and actually showing it.
Would anyone be able to shed a light on this?
Thanks in advance.
PS. For what it's worth, NSMenus I present manually (without relying on Apple's right-click handling) using popUpMenuPositioningItem:atLocation:inView: are shown.
Edit / Update / Clarification
The NSCollectionView in question is inside an NSWindow that's being shown when an NSStatusItem is clicked, like CoverSutra/TicToc/what have you. Some code from the MyWindow NSWindow subclass:
- (void)awakeFromNib {
[self setStyleMask:NSBorderlessWindowMask];
[self setExcludedFromWindowsMenu:YES];
}
- (BOOL)canBecomeMainWindow {
return YES;
}
- (BOOL)canBecomeKeyWindow {
return YES;
}
- (BOOL)isMovable {
return NO;
}
- (void)presentFromPoint:(NSPoint)point {
point.y -= self.frame.size.height;
point.x -= self.frame.size.width / 2;
[self setFrameOrigin:point];
[self makeMainWindow];
[self makeKeyAndOrderFront:self];
}
presentFromPoint: is the method I use to present it from any point I like, in my case from just below the NSStatusItem. (Not really relevant to this problem)
My application has LSUIElement in its Info.plist set to YES by the way, so it doesn't show a menu bar or a Dock icon. It lives in the status bar, and has a window that's shown when the NSStatusItem is clicked.
The view hierarchy is as follows:
MyWindow => contentView => NSScrollView => NSCollectionView
The NSCollectionView has an NSCollectionViewItem subclass connected to its itemPrototype property, and the NSCollectionViewItem subclass has an NSView subclass connected to its view property.
The NSView subclass, in turn, has an NSMenu connected to its menu property.
And last but not least: This NSMenu has one NSMenuItem sitting inside it.
Both the NSCollectionViewItem subclass and the NSView subclass do nothing interesting as of now, they're just empty subclasses.
The NSMenu connected to the NSView's menu property is what should be shown when the NSView is right-clicked, but as I hope I have made clear: It isn't actually shown.
Update
I still have no idea what caused this problem, but I've decided to 'move on' from NSCollectionView, as it wasn't really fit for what I was trying to do anyway, and I am now using TDListView which works like a charm.