Is there a way to make a custom NSWindow work with Spaces - objective-c

I'm writing an app that has a custom, transparent NSWindow created using a NSWindow subclass with the following:
- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
{
self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag];
if (self)
{
[self setOpaque:NO];
[self setBackgroundColor:[NSColor clearColor]];
}
return self;
}
- (BOOL)canBecomeKeyWindow
{
return YES;
}
- (BOOL)canBecomeMainWindow
{
return YES;
}
I have everything working perfectly, including dragging and resizing, except the window doesn't work with Spaces. I cannot move the window to another space by either holding the window while switching spaces via keyboard shortcut, or by dragging to the bottom/top/left/right of the window. Is there anyway to have a custom window behave exactly like a normal window with regards to Spaces?

After a long time I found a solution to this annoying problem.
Indeed [window setMovableByWindowBackground:YES]; conflicts with my own resizing methods, the window trembles, it looks awful!
But overriding mouse event methods like below solved the problem in my case :)
- (void)mouseMoved:(NSEvent *)event
{
//set movableByWindowBackground to YES **ONLY** when the mouse is on the title bar
NSPoint mouseLocation = [event locationInWindow];
if (NSPointInRect(mouseLocation, [titleBar frame])){
[self setMovableByWindowBackground:YES];
}else{
[self setMovableByWindowBackground:NO];
}
//This is a good place to set the appropriate cursor too
}
- (void)mouseDown:(NSEvent *)event
{
//Just in case there was no mouse movement before the click AND
//is inside the title bar frame then setMovableByWindowBackground:YES
NSPoint mouseLocation = [event locationInWindow];
if (NSPointInRect(mouseLocation, [titleBar frame])){
[self setMovableByWindowBackground:YES];
}else if (NSPointInRect(mouseLocation, bottomRightResizingCornerRect)){
[self doBottomRightResize:event];
}//... do all other resizings here. There are 6 more in OSX 10.7!
}
- (void)mouseUp:(NSEvent *)event
{
//movableByBackground must be set to YES **ONLY**
//when the mouse is inside the titlebar.
//Disable it here :)
[self setMovableByWindowBackground:NO];
}
All my resizing methods start in mouseDown:
- (void)doBottomRightResize:(NSEvent *)event {
//This is a good place to push the appropriate cursor
NSRect r = [self frame];
while ([event type] != NSLeftMouseUp) {
event = [self nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
//do a little bit of maths and adjust rect r
[self setFrame:r display:YES];
}
//This is a good place to pop the cursor :)
//Dispatch unused NSLeftMouseUp event object
if ([event type] == NSLeftMouseUp) {
[self mouseUp:event];
}
}
Now I have my Custom window and plays nice with Spaces :)

Two things here.
You need to set the window to allow dragging by background, [window setMovableByWindowBackground:YES];
And If your custom window areas you expect to be draggable are custom NSView subclasses, you must override the method - (BOOL)mouseDownCanMoveWindow to return YES in any NSView subclass that needs to be able to move the window by dragging.

Did you override isMovable?
The Apple documentation says, that it changes Spaces behavior:
If a window returns NO, that means it
can only be dragged between spaces in
F8 mode, ...
Another method that might be related:
NSWindow setCollectionBehavior

Related

Custom Window Style in Cocoa

OK, this is what I'm trying to do :
I have a custom NSPanel subclass
I want the NSPanel to be borderless (NO title - I'm drawing a titlebar myself) AND resizeable
The thing is that :
once I set the styleMask to NSResizableWindowMask, the default title bar appears as well.
once I set the styleMask to NSBorderlessWindowMask, the default title bar disappears (that's good), but the window loses its resizing ability.
This is my Code :
- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)windowStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation
{
if ((self = [super initWithContentRect:contentRect styleMask:NSTitledWindowMask backing:bufferingType defer:deferCreation])) {
[self setOpaque:NO];
[self setBackgroundColor:[NSColor clearColor]];
[self setMovableByWindowBackground:YES];
[self setLevel:NSFloatingWindowLevel];
//[self setStyleMask:[self styleMask]&~NSTitledWindowMask];
}
return self;
}
As you may see from the commented-out code, I've tried using any possible combination of bit operations with the mask so that I combine what I need.
Any ideas??
Just do them at a time like this
styleMask:NSTitledWindowMask | NSResizableWindowMask

NSTextField: end editing when user clicks outside of the text field

I have an NSTextField that I'm setting editable depending on a user action. I'd like to end editing when the user clicks anywhere outside of the text field inside the window.
Seems simple, but I could not get this to work. I implemented controlTextDidEndEditing and textDidEndEditing, but no luck, especially when I click on a user interface element that does not accept the first responder status.
Every NSEvent is pass through NSWindow's sendEvent: method.
You can create a custom NSWindow and override the sendEvent: method. If there is a mouse down event, broadcast it by the NSNotificationCenter:
- (void)sendEvent:(NSEvent *)event {
[super sendEvent:event];
if (event.type == NSLeftMouseDown) {
[[NSNotificationCenter defaultCenter] postNotificationName:kCustomWindowMouseDown object:self userInfo:#{#"event": event}];
}
}
In the ViewController which reference the NSTextField, observer this notification:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(customWindowMouseDown:)
name:kCustomWindowMouseDown
object:self.view.window];
End the editing if the mouse down event's location is outside of the text field:
- (void)customWindowMouseDown:(id)sender {
NSNotification *notification = (NSNotification *) sender;
NSEvent *event = notification.userInfo[#"event"];
NSPoint locationInWindow = event.locationInWindow;
if ([self.view.window.firstResponder isKindOfClass:NSTextView.class]) {
NSTextView *firstResponder = (NSTextView *) self.view.window.firstResponder;
//we only care about the text field referenced by current ViewController
if (firstResponder.delegate == (id <NSTextViewDelegate>) self.textField) {
NSRect rect = [self.textField convertRect:self.textField.bounds toView:nil];
//end editing if click out side
if (!NSPointInRect(locationInWindow, rect)) {
[self.view.window makeFirstResponder:nil];
}
}
}
}
You can write a subclass for the NSView and write the below method and change the class of the NSView in the NSWindow of the nib file to that subclass.
- (void)mouseDown:(NSEvent *)event
{
[text setEditable:NO];
NSLog(#"mouseDown");
}
May be a bit dirty but you could create a big transparent button on the "outside of the text field" area. Show it when editing starts and hide it when editing ends. If user taps this button you stop editing (and hide the button).
Solved that for me when I needed a fast solution.
I'd improve the answer of vignesh kumar for the cases when you can't subclass the window that contains the view.
For all sub-views/controls that handle mouseDown, including the super view itself, implement:
- (void)mouseDown:(NSEvent *)event
{
[[self window] makeFirstResponder:self];
[super mouseDown:event];
}
For some controls, like buttons, you could change to
- (void)mouseDown:(NSEvent *)event
{
[[self window] makeFirstResponder:[self superview]];
[super mouseDown:event];
}
otherwise a focus ring may appear

mouseExited isn't called when mouse leaves trackingArea while scrolling

Why mouseExited/mouseEntered isn't called when mouse exits from NStrackingArea by scrolling or doing animation?
I create code like this:
Mouse entered and exited:
-(void)mouseEntered:(NSEvent *)theEvent {
NSLog(#"Mouse entered");
}
-(void)mouseExited:(NSEvent *)theEvent
{
NSLog(#"Mouse exited");
}
Tracking area:
-(void)updateTrackingAreas
{
if(trackingArea != nil) {
[self removeTrackingArea:trackingArea];
[trackingArea release];
}
int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds]
options:opts
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
}
More details:
I have added NSViews as subviews in NSScrollView's view. Each NSView have his own tracking area and when I scroll my scrollView and leave tracking area "mouseExited" isn't called but without scrolling everything works fine. Problem is that when I scroll "updateTrackingAreas" is called and I think this makes problems.
* Same problem with just NSView without adding it as subview so that's not a problem.
As you noted in the title of the question, mouseEntered and mouseExited are only called when the mouse moves. To see why this is the case, let's first look at the process of adding NSTrackingAreas for the first time.
As a simple example, let's create a view that normally draws a white background, but if the user hovers over the view, it draws a red background. This example uses ARC.
#interface ExampleView
- (void) createTrackingArea
#property (nonatomic, retain) backgroundColor;
#property (nonatomic, retain) trackingArea;
#end
#implementation ExampleView
#synthesize backgroundColor;
#synthesize trackingArea
- (id) awakeFromNib
{
[self setBackgroundColor: [NSColor whiteColor]];
[self createTrackingArea];
}
- (void) createTrackingArea
{
int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds]
options:opts
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
}
- (void) drawRect: (NSRect) rect
{
[[self backgroundColor] set];
NSRectFill(rect);
}
- (void) mouseEntered: (NSEvent*) theEvent
{
[self setBackgroundColor: [NSColor redColor]];
}
- (void) mouseEntered: (NSEvent*) theEvent
{
[self setBackgroundColor: [NSColor whiteColor]];
}
#end
There are two problems with this code. First, when -awakeFromNib is called, if the mouse is already inside the view, -mouseEntered is not called. This means that the background will still be white, even though the mouse is over the view. This is actually mentioned in the NSView documentation for the assumeInside parameter of -addTrackingRect:owner:userData:assumeInside:
If YES, the first event will be generated when the cursor leaves aRect, regardless if the cursor is inside aRect when the tracking rectangle is added. If NO the first event will be generated when the cursor leaves aRect if the cursor is initially inside aRect, or when the cursor enters aRect if the cursor is initially outside aRect.
In both cases, if the mouse is inside the tracking area, no events will be generated until the mouse leaves the tracking area.
So to fix this, when we add the tracking area, we need to find out if the cursor is within in the tracking area. Our -createTrackingArea method thus becomes
- (void) createTrackingArea
{
int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds]
options:opts
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
NSPoint mouseLocation = [[self window] mouseLocationOutsideOfEventStream];
mouseLocation = [self convertPoint: mouseLocation
fromView: nil];
if (NSPointInRect(mouseLocation, [self bounds]))
{
[self mouseEntered: nil];
}
else
{
[self mouseExited: nil];
}
}
The second problem is scrolling. When scrolling or moving a view, we need to recalculate the NSTrackingAreas in that view. This is done by removing the tracking areas and then adding them back in. As you noted, -updateTrackingAreas is called when you scroll the view. This is the place to remove and re-add the area.
- (void) updateTrackingAreas
{
[self removeTrackingArea:trackingArea];
[self createTrackingArea];
[super updateTrackingAreas]; // Needed, according to the NSView documentation
}
And that should take care of your problem. Admittedly, needing to find the mouse location and then convert it to view coordinates every time you add a tracking area is something that gets old quickly, so I would recommend creating a category on NSView that handles this automatically. You won't always be able to call [self mouseEntered: nil] or [self mouseExited: nil], so you might want to make the category accept a couple blocks. One to run if the mouse is in the NSTrackingArea, and one to run if it is not.
#Michael offers a great answer, and solved my problem. But there is one thing,
if (CGRectContainsPoint([self bounds], mouseLocation))
{
[self mouseEntered: nil];
}
else
{
[self mouseExited: nil];
}
I found CGRectContainsPoint works in my box, not CGPointInRect,

How can I make a fullscreen overlay on the OS X desktop?

I want to make some kind of drawable surface that exists beneath the mouse cursor but above everything else rendered on the desktop. I am trying to create a "trail" behind the mouse.
How can I do this in Cocoa and Objective-C?
You need to subclass NSWindow to create a borderless window and set its window level to something like NSScreenSaverWindowLevel - 1.
- (id)initWithContentRect:(NSRect)contentRect
styleMask:(NSUInteger)aStyle
backing:(NSBackingStoreType)bufferingType
defer:(BOOL)flag
{
self=[super initWithContentRect:contentRect
styleMask:NSBorderlessWindowMask
backing:bufferingType
defer:flag];
if(self!=nil)
{
[self setHasShadow:NO];
[self setOpaque:NO];
[self setBackgroundColor:[NSColor clearColor]];
[self setLevel:NSScreenSaverWindowLevel - 1];
}
return self;
}

NSTextField events and resigning first responder

I want to implement an NSTextField where when I click it, it selects all the text. (to give the user easy way to delete all current text)
when I finish editing it, either by pressing enter/tab or moving the mouse outside of it's rect, I will move the focus out of the field, and change it's alpha values to 0.5.
My Code:
H file:
#import <Foundation/Foundation.h>
#interface MoodMsgTextField : NSTextField<NSTextFieldDelegate>
#end
M file:
-(BOOL) becomeFirstResponder
{
NSLog(#"become first responder");
BOOL result = [super becomeFirstResponder];
if(result)
{
[self setAlphaValue:1.0];
[self performSelector:#selector(selectText:) withObject:self afterDelay:0];
}
return result;
}
-(BOOL) refusesFirstResponder
{
return NO;
}
-(BOOL) resignFirstResponder
{
NSLog(#"resigning first responder");
BOOL result = [super resignFirstResponder];
NSText* fieldEditor = [self.window fieldEditor:YES forObject:self];
[fieldEditor setSelectedRange:NSMakeRange(0,0)];
[fieldEditor setNeedsDisplay:YES];
[self setAlphaValue:0.5];
return result;
}
-(void)awakeFromNib
{
self.delegate = self;
[self setAlphaValue:0.5];
[self setBordered:YES];
[self setWantsLayer:YES];
self.layer.borderWidth = 0.5;
self.layer.borderColor = [[NSColor grayColor] CGColor];
}
- (void)controlTextDidChange:(NSNotification *)aNotification
{
NSLog(#"the text is %#",self.stringValue);
}
- (void)controlTextDidEndEditing:(NSNotification *)aNotification
{
NSLog(#"end editiing : the text is %#",self.stringValue);
[self.window makeFirstResponder:nil];
}
- (void)mouseEntered:(NSEvent *)theEvent
{
[self setWantsLayer:YES];
self.layer.borderWidth = 0.5;
self.layer.borderColor = [[NSColor grayColor] CGColor];
}
- (void)mouseExited:(NSEvent *)theEvent
{
[self setWantsLayer:YES];
self.layer.borderWidth = 0;
}
So, I have a few problem:
1.
When I press inside the NSTextField (when the focus is outside) it instantly becomes and resigns first responder and I get editing end message. Why is that ?
The log I get on click is this :
2011-08-02 18:03:19.044 ooVoo[42415:707] become first responder
2011-08-02 18:03:19.045 ooVoo[42415:707] resigning first responder
2011-08-02 18:03:19.104 ooVoo[42415:707] end editing : the text is
2.
When I press the enter key it just selects all text inside and doesn't move the mouse focus. When I press the tab is does seem to move focus, however neither of the two causes the resignFirstResponder to get called. Why ?
3.
None of the mouse event function are getting called. Do I need to to do something special for that ? I thought that since they are NSResponder's ones, I will get those for free by inheriting from NSTextField. Do I need NSTrackingInfo here as well ?
4.
Last but not least, for some reason, every couple letters, one letter seems to be bold.
I have no idea why.
I'd appreciate any help.
Thanks
I am not sure why this is happening in this case but you should read about the Field Editor concept. Basically a NSTextField does not handle its own input but uses an NSTextView called the field editor to accept input.
You need to react to the Enter key yourself. Take a look at the key handling documentation. Here is an answer with an example.
To get mouse events you can use NSTrackingArea. See the docs for Mouse Tracking.
I do not have any input on this except that sometimes text drawing can look bold when really what is happening is that the text is being drawn multiple times without erasing the background.