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

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.

Related

NSPopover to start in a detached state

Is there a way to force the NSPopover to start in the detached state? I only see isDetached which is a read-only property for the state of the popover and an NSPopoverDelegate method detachableWindow(forPopover:) which lets me override the window that gets created. I'd like to essentially click a button and have the NSPopover start in the state in this photo.
The style of this window is exactly what a product requirement is and I can't seem to find any NSWindow style settings that would make a window do something like this (nor an NSPanel)
This detached popover functionality seems special in that it:
non-modal, but stays above main app. Able to still interact with the main app just like in Messages how you can still click around and type a new message.
Clicking another app, AppFoo, puts both the main app and the helper window behind AppFoo.
The helper window can be moved around and isn't hidden on app deactivation (another app gets selected).
Has the little, native, grey X in the top left.
If you don't mind calling private API, it's actually pretty simple:
let detach = NSSelectorFromString("detach")
if popover.responds(to: detach) {
popover.perform(detach)
}
No need to even add a delegate. I don't know when this private method was added but it's available at least since macOS 10.13. I suspect it's available since the introduction of NSPopover, though.
Here is the trick.
Use the required delegate method detachableWindowForPopover: to do the work for you, like:
- (void) showPopoverDetached
{
NSWindow* detachedWindow = [self detachableWindowForPopover:nil];
[detachedWindow.windowController showWindow:nil];
}
Seems that the Apple engineers implemented detachableWindowForPopover: on a pretty smart way, I guess it uses the content view controller class, and will always create a singleton like instance of the detached window.
Once detachableWindowForPopover: has called the presented window instance will be re-used no matter when and why it is called, called it directly (from a func like my sample above) or indirectly (e.g. when you drag out, detach, the popover from its original position)
This way they can prevent a popover from being detached 'twice' and we can also implement the detached way programmatically, nice job from them!
Here is a tiny demo of how it works in a real life (tested on macOS 10.13 - 13.0)
https://imgur.com/a/sfc7e6d

Programmatically dismiss modal dialog in MacOS

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.

NSAccessibilityNotificationName.mainWindowChanged observer to be notified when main window changes

I would like to be notified when the main window of the frontmost app changes i.e changing a tab in a browser, changing a document in SublimeText etc. and get the title of that current main window. I'm quite new to Swift so I'd appreciate any kind of info/help.
I've written a piece of code to observe the frontmostapp by using addobserver on the didActivateAppNotification object. It successfully prints out the frontmost app everytime it changes to a new one.
So I also wanna do the same with main window of the frontmost app. However, all I can do is to get the window lists of the frontmost app via CGWindowListOption as explained here, then scroll through it to get a list of the windows of frontmost app and filter it to get the mainwindow. But I don't want a polling solution, which is to get this list every second. Instead I'd like to get use of a notification observer so that I can actually ask when the mainwindow changes. -I've also tried some Applescript solutions, they all need to be polling solutions.
I couldn't find any tutorials, examples of NSAccessibilityNotificationName classes as it says they require special handling in Apple Dev Forums. How could I observe those notifications for external applications? (specifically mainWindowChanged in my example)
Cheers, happy coding!
class activeApp: NSObject {
override init() {
super.init()
NSWorkspace.shared.notificationCenter.addObserver(self,
selector: #selector(printMe(notification:)),
name: NSWorkspace.didActivateApplicationNotification,
object:nil)
}
#objc func printMe(notification: NSNotification) {
let app = notification.userInfo!["NSWorkspaceApplicationKey"] as! NSRunningApplication
print(app.localizedName!)
}
}
let runme = activeApp()
RunLoop.main.run()

Swift: Clearing .sharedApplication().shortcutItems on application quit

I'm having a very simple problem with my implemented 3D Touch dynamic quick action shortcuts.
I want the shortcuts to be cleared whenever the app is terminated (by double clicking the Home button and swiping up).
I am calling UIApplication.sharedApplication().shortcutItems.removeAll() as follows:
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
UIApplication .sharedApplication().shortcutItems?.removeAll()
self.saveContext()
}
However it has no effect, and the quick actions still show when 3D touch is used.
If I place UIApplication.sharedApplication().shortcutItems?.removeAll() inside
func applicationDidEnterBackground(application: UIApplication), this works exactly as intended...
I read something about applicationDidEnterBackground being the function used in most cases due to background processing or something...but there has to be a way to achieve what I want when the user terminates the app using the app monitor swipe up.
Thanks
Didn't tried this. But this tweak should work.
Start a background task on applicationWillTerminate and end it after some small delay. In the mean time, you can call 'UIApplication .sharedApplication().shortcutItems?.removeAll()'.
This will hopefully clear the shortcut items.
There are dynamic and static quick actions. The first kind you define through the shortcutItems property of the UIApplication instance (like in your example). The second kind you register in the plist file.
From the documentation:
Your code creates dynamic quick actions, and registers them with your app object, at runtime.
The system registers your static quick actions when your app is installed.
If a user installs an update for your app but has not yet launched the update, pressing your Home screen icon shows the dynamic quick actions for the previously-installed version.
This means that even when the app is closed the system remembers about both kinds of quick actions. While your app is in memory, such as when going into background, the system can still query the UIApplication for the dynamic actions but it must keep some other sort of persistence of quick actions when the app is closed.
I think there is just no guarantee about the point at which the system synchronizes with the dynamic quick actions. My guess is that the system does not necessarily synchronize when closing the app, yours might be an unsupported use case.

GTK: Destroy window and run an external script on button clicked

That's pretty much the question's title. I must be missing some signals interpretation here...
On PyGTK, I'm doing:
class Foo:
def __init__(self):
self.gladefile = gladefile
self.wTree = gtk.glade.XML(self.gladefile, 'some_window')
self.window = self.wTree.get_widget('some_window')
events = { 'on_code_submit_clicked' : self.submit }
self.wTree.signal_autoconnect(events)
def submit(self):
self.window.destroy()
os.system('external_script')
code = Foo()
What's happening, is that when the button is clicked, it stay pressed, then the script runs, and after the external program is closed, the window "blinks", getting destroyed and recreated again.
I also tried the "pressed" and "released" signals.
The behavior I need:
Click on the button
Destroy the current window
Run external script (that will open another program's window)
Recreate the Foo() window after closing the external app.
What I can imagine is that the event is being run during the clicked event, not after. That's why the window remain opened. The PyGTK docs don't say anything about something like gtk_signal_connect_after on the glade page, that leaves me totally lost about it.
Why not hide the window and show it after the external script?
self.window.hide()
os.system('external_script')
self.window.show()
It looks like you'll have to call the external script from a thread.
I haven't worked with threads with python, but it looks like the threading module would do the job (at least that's what I would try). Glib also has thread support, but I can't find the pygtk docs for it.
When the external script has finished, don't recreate Foo from the thread but schedule a callback function to do so using gobject.idle_add. This is because all GUI work should be done on the gtk event thread, otherwise your program may crash.
Posting my solution... it's easy as.
self.window.hide()
glib.idle_add(subprocess.call, ['external_script', 'param'])
glib.idle_add(self.window.show)