How to deactivate window restoration? - macos

I am using SDK 10.8.
I am subclassing NSPersistentDocument (but the question might interest people sublassing NSDocument) and I want to deactivate the window restoration. So far, here is what I have done :
-(void)restoreStateWithCoder:(NSCoder *)coder
{
NSLog(#"No 1") ;
}
-(void)restoreDocumentWindowWithIdentifier:(NSString *)identifier
state:(NSCoder *)state
completionHandler:(void (^)(NSWindow *, NSError *))completionHandler
{
NSLog(#"No 2") ;
}
The problem is that when I try in my application to create a new document, no window is shown.
What is the proper way to desactivate the window restoration ?

This function of window restoration makes my app unlaunchable, for instance after a crash (or I click stop in xcode). I don't want the users of my app to face the same problem and be unable to launch anymore the app.
That should only be true if your stored state is corrupt or you've changed the keys you store it under.
In normal circumstances, state restoration exists to save user data (that they haven't manually saved) in the event of a crash—your users will want it to be there when the app crashes on them.
For debugging purposes, you can disable state restoration in your Xcode scheme:

The following works, but I have to admit that I don't know why exactly !!
-(void)restoreDocumentWindowWithIdentifier:(NSString *)identifier
state:(NSCoder *)state
completionHandler:(void (^)(NSWindow *, NSError *))completionHandler
{
DDLogWarn(#"Method 'restoreDocumentWindowWithIdentifier...' overidden") ;
[[NSDocumentController sharedDocumentController] newDocument:nil] ;
}
- (void)restoreStateWithCoder:(NSCoder *)coder
{
DDLogWarn(#"Method 'restoreStateWithCoder...' overidden") ;
[[NSDocumentController sharedDocumentController] newDocument:nil] ;
}

Related

How to determine pressed modifier keys when a Cocoa app is started?

In an app I'm developing, a database is used to store all user data, usually located somewhere in Library/Application Support. In order to enable the user to switch the database, I want to implement a functionality similar to iTunes or iPhoto, where the app asks for the library's or database's location if the option key is pressed upon starting the app.
How can I check the currently pressed (modifier) keys if no NSEvent is at hand?
I already tried:
[NSResponder flagsChanged:(NSEvent *)theEvent] – Probably not called because the option key is already down before the window and any responders are instantiated.
[[NSApplication sharedApplication] currentEvent] – Returns nil.
Put this in applicationDidFinishLaunching:
NSUInteger modifiers = ([NSEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask);
if (modifiers == NSAlternateKeyMask) {
// do your stuff here
}
Beginning with macOS 10.12, this should be:
NSUInteger modifiers = ([NSEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask);
if (modifiers == NSEventModifierFlagOption) {
// do your stuff here
}
Swift 5 variant:
func applicationDidFinishLaunch(_ aNotification: Notification?) {
if NSEvent.modifierFlags.intersection(.deviceIndependentFlagsMask) == .option {
// Your code
}
}

Drag and drop files into NSOutlineView

I'm trying to implement simple drag and drop operation into NSOutlineView Based on Apple's example - https://developer.apple.com/library/mac/samplecode/SourceView/Introduction/Intro.html
All seems to be ok, but finally when I drop some files from Finder I get error:
[<ChildNode 0x60800005a280> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key description.') was raised during a dragging session
Here is my test project: https://www.dropbox.com/s/1mgcg2dysvs292u/SimpleDrag.zip?dl=0
What I exactly need in my app: allow user to drag and drop multiple files and folder into some tree list and then display them to user. Also save all this this into some file, so it can be loaded again with all user dragged files and folders.
A final result I want to have like this:
The description property of NSObject is read-only, and is generally set by providing a getter in the implementation file:
- (NSString *)description {
return [self urlString]; // Using urlString solely for demo purposes.
}
You can't set it, either via key-value coding or by direct assignment:
self.description = [self urlString]; // Xcode error: 'Assignment to readonly property'
[self setValue:[self urlString] forKey:#"description"];
In -[ChildNode copyWithZone:] an attempt is made to do the latter of the two, and that's what causes the warning to be logged to the console.
// -------------------------------------------------------------------------------
// copyWithZone:zone
// -------------------------------------------------------------------------------
- (id)copyWithZone:(NSZone *)zone
{
id newNode = [[[self class] allocWithZone:zone] init];
// One of the keys in mutableKeys is 'description'...
// ...but it's readonly! (it's defined in the NSObject protocol)
for (NSString *key in [self mutableKeys])
{
[newNode setValue:[self valueForKey:key] forKey:key];
}
return newNode;
}
This begs the question why do you get the warning in your app, and not in the sample app? From what I can tell no ChildNode instance is ever sent a copyWithZone: message in the sample app, whereas this does happen in your app, immediately after the drop. Of course there's a second question here as well: why do Apple explicitly include the description key-path when it can't be set this way? - unfortunately I can't help you with that.
A really handy way of trying to trap errors that don't actually cause exceptions is to add an All Exceptions breakpoint. If you do this in your sample app you'll see that the app freezes at the line that's causing the problem, giving you a better chance of figuring out the issue.

(OS X) Detecting when front app goes into fullscreen mode

I'm writing a "UIElement" app that shows a status window on the side of the screen, similar to the Dock.
Now, when a program takes over the entire screen, I need to hide my status window, just like the Dock does.
What are my options to detect this and the inverse event?
I like to avoid polling via a timed event and also cannot use undocumented tricks (such as suggested here)
What doesn't work:
Registering a Carbon Event Handler for the kEventAppSystemUIModeChanged event isn't sufficient - it works to detect VLC's full screen mode, but not for modern Cocoa apps that use the new fullscreen widget at the top right corner of their windows.
Similarly, following Apple's instructions about the NSApplication presentationOptions API by observing changes to the currentSystemPresentationOptions property does not help, either - again, it only informs about VLC's fullscreen mode, but not about apps using the window' top right fullscreen widget.
Monitoring changes to the screen configuration using CGDisplayRegisterReconfigurationCallback is not working because there aren't any callbacks for these fullscreen modes.
Based on #Chuck's suggestion, I've come up with a solution that works somewhat, but may not be foolproof.
The solution is based on the assumption that 10.7's new fullscreen mode for windows moves these windows to a new Screen Space. Therefore, we subscribe to notifications for changes to the active space. In that notification handler, we check the window list to detect whether the menubar is included. If it is not, it probably means that we're in a fullscreen space.
Checking for the presence of the "Menubar" window is the best test I could come up with based on Chuck's idea. I don't like it too much, though, because it makes assumptions on the naming and presence of internally managed windows.
Here's the test code that goes inside AppDelegate.m, which also includes the test for the other app-wide fullscreen mode:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSApplication *app = [NSApplication sharedApplication];
// Observe full screen mode from apps setting SystemUIMode
// or invoking 'setPresentationOptions'
[app addObserver:self
forKeyPath:#"currentSystemPresentationOptions"
options:NSKeyValueObservingOptionNew
context:NULL];
// Observe full screen mode from apps using a separate space
// (i.e. those providing the fullscreen widget at the right
// of their window title bar).
[[[NSWorkspace sharedWorkspace] notificationCenter]
addObserverForName:NSWorkspaceActiveSpaceDidChangeNotification
object:NULL queue:NULL
usingBlock:^(NSNotification *note)
{
// The active space changed.
// Now we need to detect if this is a fullscreen space.
// Let's look at the windows...
NSArray *windows = CFBridgingRelease(CGWindowListCopyWindowInfo
(kCGWindowListOptionOnScreenOnly, kCGNullWindowID));
//NSLog(#"active space change: %#", windows);
// We detect full screen spaces by checking if there's a menubar
// in the window list.
// If not, we assume it's in fullscreen mode.
BOOL hasMenubar = NO;
for (NSDictionary *d in windows) {
if ([d[#"kCGWindowOwnerName"] isEqualToString:#"Window Server"]
&& [d[#"kCGWindowName"] isEqualToString:#"Menubar"]) {
hasMenubar = YES;
break;
}
}
NSLog(#"fullscreen: %#", hasMenubar ? #"No" : #"Yes");
}
];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqual:#"currentSystemPresentationOptions"]) {
NSLog(#"currentSystemPresentationOptions: %#", [change objectForKey:NSKeyValueChangeNewKey]); // a value of 4 indicates fullscreen mode
}
}
Since my earlier answer doesn't work for detecting full screen mode between apps, I did some experimentation. Starting with the solution that Thomas Tempelmann came up with of checking the presence of menu bar, I found a variation that I think could be more reliable.
The problem with checking for the menu bar is that in full screen mode you can move the mouse cursor to the top of the screen to make the menu bar appear, but you're still in full screen mode. I did some crawling through the CGWindow info, and discovered that when I enter full screen, there is window named "Fullscreen Backdrop" owned by the "Dock", and it's not there when not in full screen mode.
This is on Catalina (10.15.6) in an Xcode playground, so it should be tested in a real app, and on Big Sur (or whatever the current OS is, when you're reading this).
Here's the code (in Swift... easier to quickly test something)
func isFullScreen() -> Bool
{
guard let windows = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) else {
return false
}
for window in windows as NSArray
{
guard let winInfo = window as? NSDictionary else { continue }
if winInfo["kCGWindowOwnerName"] as? String == "Dock",
winInfo["kCGWindowName"] as? String == "Fullscreen Backdrop"
{
return true
}
}
return false
}
EDIT NOTE: This answer unfortunately doesn't provide a solution for detecting full screen in a different app, which is what the OP was asking. I'm leaving it because it does answer the question for detecting in in the same app going full screen - for example in a generic library that needs to know to automatically update keyEquivalents and title for an explicitly added "Enter Full Screen" menu item rather than Apple's automatically added menu item.
Although this question is quite old now, I've had to detect full screen mode in Swift recently. While it's not as simple as querying some flag in NSWindow, as we would hope for, there is an easy and reliable solution that has been available since macOS 10.7.
Whenever a window is about to enter full screen mode NSWindow sends a willEnterFullScreenNotification notification, and when it is about to exit full screen mode, it sends willExitFullScreenNotification. So you add an observer for those notifications. In the following code, I use them to set a global boolean flag.
import Cocoa
/*
Since notification closures can be run concurrently, we need to guard against
races on the Boolean flag. We could use DispatchSemaphore, but it's kind
over-kill for guarding a simple read/write to a boolean variable.
os_unfair_lock is appropriate for nanosecond-level contention. If the wait
could be milliseconds or longer, DispatchSemaphore is the thing to use.
This extension is just to make using it easier and safer to use.
*/
extension os_unfair_lock
{
mutating func withLock<R>(block: () throws -> R) rethrows -> R
{
os_unfair_lock_lock(&self)
defer { os_unfair_lock_unlock(&self) }
return try block()
}
}
fileprivate var fullScreenLock = os_unfair_lock()
public fileprivate(set) var isFullScreen: Bool = false
// Call this function in the app delegate's applicationDidFinishLaunching method
func initializeFullScreenDetection()
{
_ = NotificationCenter.default.addObserver(
forName: NSWindow.willEnterFullScreenNotification,
object: nil,
queue: nil)
{ _ in
fullScreenLock.withLock { isFullScreen = true }
}
_ = NotificationCenter.default.addObserver(
forName: NSWindow.willExitFullScreenNotification,
object: nil,
queue: nil)
{ _ in
fullScreenLock.withLock { isFullScreen = false }
}
}
Since observer closures can be run concurrently, I use os_unfair_lock to guard races on the _isFullScreen property. You could use DispatchSemaphore, though it's a bit heavy weight for just guarding a Boolean flag. Back when the question was first asked, OSSpinLock would have been the equivalent, but it's been deprecated since 10.12.
Just make sure to call initializeFullScreenDetection() in your application delegate's applicationDidFinishLaunching() method.

Cocoa: User Notifications never being delivered

I'm trying to deliver a local notification, it looks something like this:
NSUserNotification *notification = [[NSUserNotification alloc] init];
//set title, subtitle, and sound
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
But the notification doesn't appear. I know sometimes notifications are not presented if the app is frontmost but at the time of delivery it's not. In system preferences I made sure that notifications from my app are allowed, and I even overrode the userNotificationCenter:shouldPresentNotification: method to always return YES but it still doesn't present the notification.
What is most confusing is that everything was working fine until I updated to Mavericks. I suppose something has changed in the update but I can't figure out what.
Thanks for the help.
my guess is that something is nil. make sure you are either assigning a (valid and non-nil) title, or informativeText.
I imagine that having invalid values for other properties such as otherButtonTitle might prevent the notification from being displayed as well.
Are there any error messages in your console?
Use the debugger or NSLog() statements to assess the values assigned to the notification. You could also see this problem if the NSUserNotification pointer is nil (not the case from the code you posted, but worth mentioning).
Here is a minimal test for an app delegate that works on Mac OS X 10.9 (Mavericks):
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSUserNotificationCenter* unc = [NSUserNotificationCenter defaultUserNotificationCenter];
unc.delegate = self;
NSUserNotification* notice = [[NSUserNotification alloc] init];
notice.title = #"title"; // notifications need a title, or informativeText at minimum to appear on screen
//notice.informativeText = #"informativeText";
NSLog(#"notification: %#, notification center:%#", notice, unc);
[unc deliverNotification:notice];
}
// The notifications will always dispaly even if we are in the foreground
- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification
{
return YES;
}

Distinguishing a single click from a double click in Cocoa on the Mac

I have a custom NSView (it's one of many and they all live inside an NSCollectionView — I don't think that's relevant, but who knows). When I click the view, I want it to change its selection state (and redraw itself accordingly); when I double-click the view, I want it to pop up a larger preview window for the object that was just double-clicked.
My first looked like this:
- (void)mouseUp: (NSEvent *)theEvent {
if ([theEvent clickCount] == 1) [model setIsSelected: ![model isSelected]];
else if ([theEvent clickCount] == 2) if ([model hasBeenDownloaded]) [mainWindowController showPreviewWindowForPicture:model];
}
which mostly worked fine. Except, when I double-click the view, the selection state changes and the window pops up. This is not exactly what I want.
It seems like I have two options. I can either revert the selection state when responding to a double-click (undoing the errant single-click) or I can finagle some sort of NSTimer solution to build in a delay before responding to the single click. In other words, I can make sure that a second click is not forthcoming before changing the selection state.
This seemed more elegant, so it was the approach I took at first. The only real guidance I found from Google was on an unnamed site with a hyphen in its name. This approach mostly works with one big caveat.
The outstanding question is "How long should my NSTimer wait?". The unnamed site suggests using the Carbon function GetDblTime(). Aside from being unusable in 64-bit apps, the only documentation I can find for it says that it's returning clock-ticks. And I don't know how to convert those into seconds for NSTimer.
So what's the "correct" answer here? Fumble around with GetDblTime()? "Undo" the selection on a double-click? I can't figure out the Cocoa-idiomatic approach.
Delaying the changing of the selection state is (from what I've seen) the recommended way of doing this.
It's pretty simple to implement:
- (void)mouseUp:(NSEvent *)theEvent
{
if([theEvent clickCount] == 1) {
[model performSelector:#selector(toggleSelectedState) afterDelay:[NSEvent doubleClickInterval]];
}
else if([theEvent clickCount] == 2)
{
if([model hasBeenDownloaded])
{
[NSRunLoop cancelPreviousPerformRequestsWithTarget: model];
[mainWindowController showPreviewWindowForPicture:model];
}
}
}
(Notice that in 10.6, the double click interval is accessible as a class method on NSEvent)
If your single-click and double-click operations are really separate and unrelated, you need to use a timer on the first click and wait to see if a double-click is going to happen. That is true on any platform.
But that introduces an awkward delay in your single-click operation that users typically don't like. So you don't see that approach used very often.
A better approach is to have your single-click and double-click operations be related and complementary. For example, if you single-click an icon in Finder it is selected (immediately), and if you double-click an icon it is selected and opened (immediately). That is the behavior you should aim for.
In other words, the consequences of a single-click should be related to your double-click command. That way, you can deal with the effects of the single-click in your double-click handler without having to resort to using a timer.
Personally, I think you need to ask yourself why you want this non-standard behaviour.
Can you point to any other application which treats the first click in a double-click as being different from a single-click? I can't think of any...
Add two properties to your custom view.
// CustomView.h
#interface CustomView : NSView {
#protected
id m_target;
SEL m_doubleAction;
}
#property (readwrite) id target;
#property (readwrite) SEL doubleAction;
#end
Overwrite the mouseUp: method in your custom view.
// CustomView.m
#pragma mark - MouseEvents
- (void)mouseUp:(NSEvent*)event {
if (event.clickCount == 2) {
if (m_target && m_doubleAction && [m_target respondsToSelector:m_doubleAction]) {
[m_target performSelector:m_doubleAction];
}
}
}
Register your controller as the target with an doubleAction.
// CustomController.m
- (id)init {
self = [super init];
if (self) {
// Register self for double click events.
[(CustomView*)m_myView setTarget:self];
[(CustomView*)m_myView setDoubleAction:#selector(doubleClicked:)];
}
return self;
}
Implement what should be done when a double click happens.
// CustomController.m
- (void)doubleClicked:(id)sender {
// DO SOMETHING.
}
#Dave DeLong's solution in Swift 4.2 (Xcode 10, macOS 10.13), amended for use with event.location(in: view)
var singleClickPoint: CGPoint?
override func mouseDown(with event: NSEvent) {
singleClickPoint = event.location(in: self)
perform(#selector(GameScene.singleClickAction), with: nil, afterDelay: NSEvent.doubleClickInterval)
if event.clickCount == 2 {
RunLoop.cancelPreviousPerformRequests(withTarget: self)
singleClickPoint = nil
//do whatever you want on double-click
}
}
#objc func singleClickAction(){
guard let singleClickPoint = singleClickPoint else {return}
//do whatever you want on single-click
}
The reason I'm not using singleClickAction(at point: CGPoint) and calling it with: event.location(in: self) is that any point I pass in - including CGPoint.zero - ends up arriving in the singleClick Action as (0.0, 9.223372036854776e+18). I will be filing a radar for that, but for now, bypassing perform is the way to go. (Other objects seem to work just fine, but CGPoints do not.)

Resources