"+[CATransaction synchronize] called within transaction" with NSSavePanel - nssavepanel

Starting with macOS Ventura (or maybe with the latest Xcode version), I'm getting log messages in my Mac app:
+[CATransaction synchronize] called within transaction
whenever the app displays an NSSavePanel (or subclass). For instance:
NSOpenPanel* panel = [NSOpenPanel openPanel];
panel.prompt = #"Import";
panel.canChooseDirectories = NO;
panel.allowsMultipleSelection = YES;
panel.message = #"Choose chromatogram files to import into the selected folder.";
panel.allowedFileTypes = #[#"com.appliedbiosystems.abif.fsa", #"com.appliedbiosystems.abif.hid"];
[panel beginSheetModalForWindow:self.parentWindow completionHandler:^(NSInteger result){
if (result == NSModalResponseOK) {
[self addSamplesFromFiles:[panel.URLs valueForKeyPath:#"#unionOfObjects.path"] toFolder:selectedFolder];
}
}];
Here, beginSheetModalForWindow triggers the warning.
If I spawn the panel with beginWithCompletionHandler (i.e., not as a sheet), the message is posted as well. In fact, it also gets posted whenever I resize the panel.
This doesn't occur with other windows or with NSAlert sheets, but this occur with all open/save panels in my app. I have checked that these panels are not used within CATransaction blocks. They are spawned from the main thread.
All panels behave normally to the user, but I'd like to know what I'm doing wrong and why this started happening.

Related

The Close button of NSColorPanel does not appear under MacOS Mojave, when called from a custom preference pane

Under Mojave, and under the System preferences custom pane of my screensaver, the Close button of [NSColorPanel sharedColorPanel] is not visible.
I am asking for your help/advice to bring it back.
I am using [NSColorPanel sharedColorPanel] to let the user select a custom color that will be used in my screensaver's rendering of sprites.
Under Mojave this panel stopped having a close button and the user cannot complete the selection. The panel is shown as modal and the user can only force quit via the System preferences (because this is a screensaver, its configuration dialog is being run as a preference pane from the System preferences).
This does not happen in versions previous to Mojave.
To make it more complex, I also have an .app version of the screensaver and the Close button is appearing Ok there.
The .app version is a standalone app and does not run via the ScreensaverEngine like the screensavers.
NSColorPanel *panel; // weak ref
panel = [NSColorPanel sharedColorPanel];
[panel setShowsAlpha:NO];
[panel setColor: color ];
[panel setDelegate:self];
[NSApp runModalForWindow:panel]; // <- does not return from that call
[panel setDelegate:nil];
NSColor *result=panel.color;
[panel close];
I tried to enable the button by
[[panel standardWindowButton:NSWindowCloseButton] setEnabled:YES];
[[panel standardWindowButton:NSWindowCloseButton] setHidden:NO];
but it did not help.
You can see the missing Close button on this screenshot
https://user-images.githubusercontent.com/4344442/51428774-5f927900-1c10-11e9-8441-ec85e71534c7.png
I can also give the public download URLs of the .saver and .app versions if this would help.

Open account preferences panel in front of actual modal window in cocoa

I have a Mac app with a preferences window. The preferences window is opened modally
-(IBAction)displayPreferencesWindow:(id)sender{
if (!pc) {
pc = [[PreferencesController alloc] initWithWindowNibName:#"PreferencesController"];
pc.delegate = self;
}
NSWindow *pcWindow = [pc window];
[NSApp runModalForWindow: pcWindow];
[NSApp endSheet: pcWindow];
[pcWindow orderOut: self];
}
In the preferences Window I have a button that opens the account preferences panel
- (IBAction)openSystemPrefs:(id)sender {
[[NSWorkspace sharedWorkspace] openFile:#"/System/Library/PreferencePanes/Accounts.prefPane"];
}
The problem is that the account preferences panel does not open in front of the actual Window. How can I achieve this?
This is kind of weird, and goes against the comments in the header, but try using this instead:
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;
From the comments there:
Open a file at some path. If you use the variant without the
withApplication: parameter, or if you pass nil for this parameter, the
default application is used. The appName parameter may be a full path
to an application, or just the application's name, with or without the
.app extension. If you pass YES for andDeactivate:, or call a variant
without this parameter, the calling app is deactivated before the new
app is launched, so that the new app may come to the foreground unless
the user switches to another application in the interim. Passing YES
for andDeactivate: is generally recommended.
So it sounds like your app should be deactivating (since you're calling a variant without the andDeactivate: parameter) but I would try explicitly using the variant with that parameter, just to be sure.
Empirically, it appears that the launched application will not activate if the launching application is presenting modal UI, at least not when using the NSWorkspace API. I was able to hack something up using AppleScript that appears to achieve the desired results:
- (IBAction)doStuff:(id)sender
{
[[NSWorkspace sharedWorkspace] openFile:#"/System/Library/PreferencePanes/Accounts.prefPane"];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSString* s = #"tell application \"System Preferences\" to activate";
NSAppleScript* as = [[[NSAppleScript alloc] initWithSource: s] autorelease];
NSDictionary* error = nil;
if ([as compileAndReturnError: &error])
{
(void)[as executeAndReturnError: &error];
}
});
}
I dispatched it to the background queue because it takes a few hundred milliseconds to compile and run the AppleScript and it's a little conspicuous if done synchronously (the button stays highlighted longer than you expect).
If you were feeling really masochistic, you could probably get the script compiling phase out of it (i.e. quicker) by conjuring up the equivalent AppleEvents and sending them directly, but this appeared to achieve the desired effect, even while presenting modal UI in the launching application.

How can I stop a NSSavePanel from closing after its completion block?

If something goes wrong saving a file, I'd like to show the error alert as a sheet over the save sheet, as overwrite prompt does. However, the save panel closes immediately upon the completion of the completion block, taking the error alert with it.
[panel beginSheetModalForWindow:window
completionHandler:^(NSInteger result) {
if (result == NSFileHandlingPanelOKButton) {
NSError *error;
// Do my saving here...
if (error)
[[NSAlert alertWithError:error] beginSheetModalForWindow:panel
modalDelegate:nil
didEndSelector:nil
contextInfo:nil];
}
}];
Can I cancel hiding the NSSavePanel from within the completion block? From a delegate? From anything?
I just checked in TextEdit and what it does in the case you're after—not confirmation of the save, but failure of the save—is the following:
The Save panel rolls up.
The app tries and fails to save. (Your block.)
The app presents its error sheet on the document window, with the Save panel long gone.
So, if you want Apple-like behavior, show your alert sheet on the document window.
Incidentally, you may be interested in presentError:modalForWindow:delegate:didPresentSelector:contextInfo:.

Modal NSAlert within block not displayed

In my document-based application, I have overridden the method openDocument: in my subclass of NSDocumentcontroller so that I can display my own openPanel. I pass the chosen URLs to the method openDocumentWithContentsOfURL:display:completionHandler:. I use this code for the call:
[self openDocumentWithContentsOfURL:[chosenFiles objectAtIndex:i] display:YES completionHandler:^(NSDocument *document, BOOL documentWasAlreadyOpen, NSError *error) {
if (document == nil)
{
NSAlert* alert = [NSAlert alertWithError:error];
[alert runModal];
}
}];
So I want to display the passed error if nil gets returned as a reference to the document. The problem is, that the program just "freezes" after I press the "Open" button in the open panel. Then I need to manually stop the program with the "stop" button in Xcode. No spinning beach ball appears, though. If I comment the line "[alert runModal]", the program does not freeze any more, but no alert is displayed, of course.
Now the strange thing: The code works sometimes. If I switch from Xcode to my browser and back and I run the program again, it sometimes works flawlessly and the error is displayed. After some time it stops working again. It is unpredictable, but most of the time it doesn't work.
All this sounds like a race-condition to me. It certainly has something to do with the block? But what do I do wrong?
Converting my comment to an answer:
runModel on the main thread.
[alert performSelectorOnMainThread:#selector(runModal) withObject:nil waitUntilDone:NO];
I think runModel needs to be called on the main thread because it's part of the AppKit framework, and it's estentially triggering UI graphics. I believe all calls to the AppKit framework or to any method that manipulates graphics needs to be on the main thread.

Cocoa: Processing thread results, and queuing multiple sheets

I have a multithreaded application that has many concurrent operations going on at once. When each thread is finished it calls one of two methods on the main thread
performSelectorOnMainThread:#selector(operationDidFinish:)
// and
performSelectorOnMainThread:#selector(operationDidFail:withMessage:)
When an operation fails, I launch a sheet that displays the error message and present the user with 2 buttons, "cancel" and "try again". Here is the code I use to launch the sheet:
// failureSheet is a NSWindowController subclass
[NSApp beginSheet:[failureSheet window]
modalForWindow:window
modalDelegate:self
didEndSelector:#selector(failureSheetDidEnd:returnCode:contextInfo:)
contextInfo:nil];
The problem is that if 2 concurrent operations fail at the same time, then the current sheet that is displayed gets overwritten with the last failure message, and then the user's "try again" action will only retry the last failed operation. Ideally, I would like to "queue" these failure sheets. If 2 operations fail at the same time then you should see 2 sheets one right after the other, allowing the user to cancel or retry them individually.
I've tried using:
[NSApp runModalSessionForWindow:[failureSheet window]]
which seems to do what I want, but doesn't work in my situation. Maybe it isn't thread safe?
For example the following code works...
- (void)displaySheet
{
[NSApp beginSheet:[failureSheet window]
modalForWindow:window
modalDelegate:self
didEndSelector:#selector(failureSheetDidEnd:returnCode:contextInfo:)
contextInfo:nil];
[NSApp runModalForWindow:[failureSheet window]];
[NSApp endSheet:[failureSheet window]];
[[failureSheet window] orderOut:nil];
}
// Calling this method from a button press works...
- (IBAction)testDisplayTwoSheets
{
[self displaySheet];
[self displaySheet];
}
However if I have 2 different threaded operations invoke displaySheet (on the main thread) when they are done, I only see one sheet and when I close it the modal session is still running and my app is essentially stuck.
Any suggestions as to what I'm doing wrong?
If you want to queue them, then just queue them. Create an NSMutableArray of result objects (you could use the operation, or the failure sheet itself, or a data object that gives you the information for the sheet; whatever is convenient). In operationDidFinish: (which always runs on the main thread, so no locking issues here), you'd do something like this:
[self.failures addObject:failure];
if ([[self window] attachedSheet] == nil)
{
// Only start showing sheets if one isn't currently being shown.
[self displayNextFailure];
}
Then you'd have:
- (void)displayNextFailure
{
if ([self.failures count] > 0)
{
MYFailure runFailure = [self.failures objectAtIndex:0];
[self.failures removeObjectAtIndex:0];
[displaySheetForFailure:failure];
}
}
And at the end of failureSheetDidEnd:returnCode:contextInfo:, just make sure to call [self displayNextFailure].
That said, this is probably a horrible UI if it can happen often (few things are worse than displaying sheet after sheet). I'd probably look for ways to modify the existing sheet to display multiple errors.
I don't think you're using the "runModalForWindow:" command properly. You wouldn't use both [NSApp beginSheet...] and "runModalForWindow:" at the same time. One is for document modal (where the application keeps running but the window with the sheet is locked) and one is for application modal (which stops everything in the entire app until the sheet is dismissed). You only want the application modal dialog so do this...
For application modal start the window with the following only:
[[failureSheet window] center];
[NSApp runModalForWindow:[failureSheet window]];
Hook the "OK" button and others to regular IBAction methods. They'll get called when a button is pressed. In the IBAction methods you need to do something like this to dismiss the window and process the action:
-(IBAction)okBtnPressed:(id)sender {
[NSApp stopModal];
NSLog(#"ok button");
[[sender window] close];
}