Strange controls behaviour in sheet modal window
I have a problem with controls in a sheet-modal window.
The button control’s target and action are set up to call a method in File’s Owner, which is a NSWindowController subclass that owns the sheet. The sheet displays fine, and all the outlets are correctly set. The controls all appear normal and can be clicked. The target and action in the button’s cell appear to be correctly set (checking in -awakeFromNib). But when I click the button, I just get a beep, and the action method is never called.
The stack trace of the beep is:
#0 0x00007fff323c4937 in NSBeep ()
#1 0x00007fff3239079c in -[NSControl sendAction:to:] ()
#2 0x00007fff3239069f in __26-[NSCell _sendActionFrom:]_block_invoke ()
#3 0x00007fff323905a5 in -[NSCell _sendActionFrom:] ()
#4 0x00007fff323d1858 in -[NSButtonCell _sendActionFrom:] ()
#5 0x00007fff3238ee06 in -[NSCell trackMouse:inRect:ofView:untilMouseUp:] ()
#6 0x00007fff323d159f in -[NSButtonCell trackMouse:inRect:ofView:untilMouseUp:] ()
#7 0x00007fff3238d8a4 in -[NSControl mouseDown:] ()
#8 0x00007fff32a88a01 in -[NSWindow(NSEventRouting) _handleMouseDownEvent:isDelayedEvent:] ()
#9 0x00007fff32a85658 in -[NSWindow(NSEventRouting) _reallySendEvent:isDelayedEvent:] ()
#10 0x00007fff32a84904 in -[NSWindow(NSEventRouting) sendEvent:] ()
#11 0x00007fff328e5937 in -[NSApplication(NSEvent) sendEvent:] ()
#12 0x00007fff32147861 in -[NSApplication run] ()
I’ve made literally hundreds of sheet-modal dialogs over the years and have never run into this problem.
I’m showing the sheet using -beginSheet:completionHandler: , and as mentioned I’ve verified that all outlets, targets and actions appear to be set as expected. The sheet is made key as expected and the first text field is selected as initial responder.
Any ideas? This is on 10.13.1, Objective-C.
OK, if anyone’s interested, I figured this out.toggle quoted messageShow quoted text
This is in an ARC project, and it’s a memory management issue. The window controller was being returned by a class method that just alloc+initWithWindowNibName: ‘d it and returned it. The client code then invoked the method on it that displays it as a sheet.
The probem is that left no reference to the window controller, so ARC duly released it. The window itself was shown completely normally and displayed as a sheet, with all its controls fully working. The controls targetted the now-released window controller, but controls now use a zeroing weak reference, so that ended up as nil. That caused the controls to try and send the action to First Responder, which didn’t implement the action, so it just beeped.
Keeping a strong reference to the window controller for the duration of the sheet fixes the issue.
While I fully get this, I’m not sure if there’s a better way to handle it than having a temporary ‘strong’ property for the controller within the client object. Usually for dialogs like this, I don’t need such a property, and in manual retain/release land, I can make put a self-retaining behaviour around the document modal sheet operation of the dialog so that client code is spared the need for it. Obviously with ARC I can’t do that, so the onus is on the client code to do it. This actually makes the client code much MORE memory management aware with ARC than without! A bit ironic that.
Is there a better solution?
On 5 Dec 2017, at 2:50 pm, Graham Cox <firstname.lastname@example.org> wrote:
On Dec 5, 2017, at 18:55 , Graham Cox <graham@...> wrote:
The trouble is, without some more details, it’s not clear what is the boundary between the client and the code module the client is calling into. However, you suggest the window controller is created on the client side of the boundary. If, in addition, the client is passing in a completion handler, you could try wrapping that in your own completion handler, and putting a strong reference to the window controller in your completion handler wrapper.
I haven’t thought this through in great detail, but I don’t think there a reference cycle hidden in that approach, and it should keep the window controller alive until after the action method is executed.
I have just encountered this exact same issue, and indeed, keeping a copy around of my window solved the issue. The approach that I took was a little different though and did not require me to create a property in the caller, but rather I managed to find a way to do it all locally.
So, my panel NSWindowController has a method which allows it to be presented in another NSWindow as a panel, here's what the method signature looks like (in Swift):
@objc func presentInWindow(_ window: NSWindow, callback: @escaping FileFormatSelectionResult)Where FileFormatSelectionResult is a closure defined as:
typealias FileFormatSelectionResult = (SelectedFormat, Bool) -> VoidThe closure is stored inside my NSWindowController instance until such time as the user makes a selection (Cancel or Import) which dismisses the sheet. After the sheet is dismissed, the callback closure is called.
This allowed me to write the following code in the caller (in Objective-C) which keeps the sheet NSWindowController around for the duration of the interaction:
The __block variable for the panel NSWindowController instance allows the block to capture it, and set it to nil at the end of the interaction, without the need to introduce a property or a local variable in the caller. The same could be accomplished in Swift as well.
I thought I'd post this here in case someone else runs into this issue and needs a simple approach to solving it.
Hope it helps.