Programmatically dismiss modal dialog in MacOS - swift

I have a warning dialog from a MacOS application's AppDelegate that needs to be updated with new information. When the new information is available, I want to programmatically dismiss the old dialog and present the new one. I have tried this two ways, both with problems:
Using alert.runModal()
If I use the above, a modal is presented as desired. I can then dismiss the dialog later with lockWarningModal.window.close(), it works to make the old dialog disappear, but it freezes the UI, so I can no longer interact with it. I am guessing this is because alert.runModal() is synchronous, and the main thread is still blocked. However, I don't know how to release this.
Using alert.beginSheetModal(for: NSApplication.shared.windows.last!) { (response) in }
If I use the above and dismiss the dialog with NSApplication.shared.windows.last!.endSheet(alert.window), then this solves the UI freeze problem. However, the dialog is attached to the main application window and is not brought to the front as a modal.
How can I achieve a modal dialog that is programmatically dismissible?

You can't stop a model event loop (or alert sheet) by simply closing its window. In fact, using the modern NSAlert API, you should never have to close or order out the window—the framework handles this for you.
For an alert started with runModal() use NSApplication's abortModal(), stopModal(), or stopModal(withCode:). After runModal() returns, send the alert window an orderOut(nil) to remove it.
For an alert sheet that executes a completion block afterwards, use NSWindow's endSheet(_) or endSheet(_:returnCode:). The alert will be automatically removed after your completion block executes.

Related

NSOpenPanel.runModal + NSAlert.runModal over GCD cause a hang

In my Cocoa application I have some computations done in the background. The background work is runned with DispatchQueue.global(qos: .utility).async.
This background task may report errors by showing a modal NSAlert via DispatchQueue.main.async.
Also, in my application user can run NSOpenPanel to open some files (with NSOpenPanel.runModal).
The problem is that if the user opens NSOpenPanel and at the same time the background task shows NSAlert, the application may hang.
user opens modal NSOpenPanel
background task opens modal NSAlert atop the NSOpenPanel
user clicks Close inside NSOpenPanel (it really can access NSOpenPanel despite the more modal NSAlert present).
both NSAlert and NSOpenPanel being closed and application hangs with main thread blocked inside NSOpenPanel.runModal()
application will not hang if user first close NSAlert and then NSOpenPanel.
The minimal code sample (the test IBaction is binded as the action for the button in the main window)
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
#IBAction func test(_ sender: Any) {
//run some work in background
DispatchQueue.global(qos: .utility).async
{
sleep(1) //some work
//report errors in the main thread.
DispatchQueue.main.async {
let alert = NSAlert();
alert.informativeText = "Close NSOpen panel before this alert to reproduct the hang."
alert.runModal()
}
}
//user want to open a file and opens the open file dialog
let dlg = NSOpenPanel();
dlg.runModal();
}
}
So, what's wrong with this code and why it cause the hang in particular use cases? And how can I prevent such a hang?
Additional note: I discovered, then if I replace dlg.runModal() with NSApp.RunModal(for: dlg) (which is exactly the same as per Apple documentation), this will fix hang in the usecase described above.
But it still will close NSAlert automatically right after closing NSOpenPanel. And I still can't understood why this behave as it does.
Update
I updated the code above to include full code of AppDelegate class of minimal reproducible application. To reproduce the case, just create new SwiftApp in XCode, replace AppDelegate code, add button in the main window and wire button's action with test func. I also placed full ready-to-compile project on github: https://github.com/snechaev/hangTest
The code for configuring NSOpenPanel and NSAlert along with their results handling is excluded as such a code does not affect the hang.
I think you are deadlocking the main queue because runModal() blocks the main queue in two places of your code at the same time. If your code is reentrant, that is what you get.
Possible solutions:
Please avoid the use of app modal windows and use instead window modal windows a.k.a. sheets. To know how to use NSOpenPanel as a sheet, attach it to the window it pertains to. For an example, please see this answer:
NSOpenPanel as sheet
You can set a flag that prevents the user from opening the NSOpenPanel if the alert is being shown, but that is ugly and does not solve any future problems that can cause other deadlocks because most probably your code is reentrant.
I would like to add some details in addition to #jvarela answer and make some resume about the my issue.
Looks like there is no way to solve the problem with having the NSPanel/NSAlert as modal windows with blocking the caller thread (with runModal).
The non-modal (NSPanel.begin()) or modal non-blocking (NSPanel.beginSheet, NSPanel.beginSheetModal) do not lead the hang, but still lead to to unexpected automatic closing of NSAlert if user try to close NSPanel before NSAlert. Moreover, to use such a non-blocking approach, you will be forced to refactor the whole your codebase to use callbacks/completion handlers instead of blocking operations when using NSPanel.
I do not found the reason why the NSPanel not being blocked and continue to receive user input when I show modal NSAlert atop of it. I suspect that is because of security mechanism which run NSPanel in a separate process, but I have no evidences about this. And I still interested about this.
For the my current project I decide to leave the blocking way to use of NSPanel, because I have large codebase and it will hard to change it all in moment to use of completion handlers. For particuluar case with NSPanel + NSAlert I just don't allow user to open NSPanel while this particular background work in progress. The user now should wait background work to finish or manually cancel the work to be able to run Open file functionality.

How to close System dialogs that appears on app crash?

I'm using xcuitest framework to automate mac application. I get system dialogs when the app is opened again after it crashes. I want to handle the dialog programmatically. But the dialog appears under the process `UserNotificationCenter' instead of the application under test. How can I handle the alert in such case?
You have two options:
Use InterruptionMonitor (documentation, use-case). This
approach is however kinda old and I found, that it does not work for
all dialogs and situations.
Create a method, which will wait for some regular app's button. If the app's button (or tab bar or other such XCUIElement) is visible and hittable after your app started, you can proceed with your test and if it's not, you can wait for the UserNotificationCenter dialog's button and identify&tap it by its string/position.
I'm using the second approach and its working much better, than the InterruptionMonitor. But it really depends on your app layout and use-case.
You should be able to revent it from appearing in the first place. Something like:
defaults write com.apple.CrashReporter DialogType none

Is there a property that tells if a form is deactivated by other form `ShowModal` procedure?

Is there a property that tells if a form is deactivated by other form ShowModal procedure ?
EDIT :
My program has a tray icon that brings to front the main form when it's clicked. I want to disable this when another window is shown in modal state. Because not doing so the main form (which is disable) will cover the modal form and completly block my program.
This behaviour is to be expected. When a modal form is shown, the other forms are disabled. You don't need to disable anything at all, the framework already handles it all for you. The beep is sounding because you are attempting to interact with a disabled form.
If you want to be notified when your window has been disabled, for any reason, not just because a modal form has been shown, listen to the WM_ENABLE message. To test whether or not your main form has been disabled. Do that by calling the IsWindowEnabled Win32 function.
Having said that I feel that it is likely you've not diagnosed the issue correctly. It sounds like you might be suffering from window ownership problems, which are common in Delphi 6. Or perhaps you are attempting to restore the application incorrectly from your notification icon code. Use Application.BringToFront for that.
The VCL's handling of modal dialogs seem very mixed up. When you show a system provided modal dialog, e.g. MessageBox, windows are disabled whether or not they are visible. However, the VCL only disables visible windows when ShowModal is called. What's more, you cannot use Enabled to test whether or not the window is disabled, you must use the IsWindowEnabled Win32 function.
You can test Application.ModalLevel at any point in time to find out if there's a modal form. E.g.:
if Application.ModalLevel = 0 then
MainForm.Visible := True;
Note that non-TCustomForm descendants will not set modal level, API dialogs like a file open dialog or MessageBox for instance. If there's a possibility of such a thing, you might surround code that runs those dialogs with ModalStarted and ModalFinished.
It doesn't seem necessary in your case, but if you somehow need to be notified that a form/dialog is going modal, you can attach a handler to Application.OnModalBegin and Application.OnModalEnd events. You can use an TApplicationEvents component for that.

How to close all open durandaljs modal dialogs

Currently I am working on a project that relies heavily on modal dialogs. I'm using durandal's dialog plugin. The problem I have is that within a modal, a user can click an element which displays its details in another modal.
What I would like to do is to close all open modals before I open a new modal. Can anyone give me a good idea of how I can ensure only a single dialog is open at any given time in durandaljs?
Why not use Durandal's pub/sub, or a client-side message bus such as postal.js (which is what we use)? We close all modals by sending a close message over the channel 'app' and the topic 'app/modals'. Instead of holding a reference to an observable (which could have memory implications), we just hold a reference to the message channel (which is a string). Much cleaner way to go.
Ok so the issue I was facing was that I have various Modals, where one modal could be opened from within another modal. However I wanted the modals to close when another would open. The tricky part was that I am using widgets and click events to open the modals.
Since my project is an SPA it occurred to me to simply create a ko.observable - currentModal - on my global object and each new Modal closes the previous, then replaces the old with the new in currentModal(this);
I went even further and am now using the route objects to fire the Modals open as well. Durandal is fun.

UIAutomation - How to get around modal dialog blocking using windowopendedevent handler?

I'm trying to build an automated testing frame work for one of our product. Some of the test cases involves performing automation in modal dialog. However, modal dialog blocks the UIAutomation code from executing. In another word, if I have a modal dialog opened by UIAutomation, nothing happends until I close the modal dialog.
In order to excute the UIAutomation code when the modal dialog displays, I've been trying to register an windowopenedevent handler that catches the modal dialog when it's opened, so I can execute the rest of testings within the windowopenedevent handler. However, the handler is NOT catching the event at all. I have no idea what is wrong, any suggestions?
Other workarounds for this scenario is welcome, too.
I've run into this a few times, usually with browser testing.
What I've done is this:
>> before taking the action that causes the modal dialog to show up:
>> start another thread (OR) launch a script asynchrounously
>> then take the action
>> In the other thread or script:
>> Sleep for 5 seconds or so
>> Activate the dialog
>> send keystrokes or call UI auto methods to dismiss the dialog
Your test should unblock once the dialog is dismissed.
This has always worked for me.