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()
Related
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.
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
I'm creating a Mac menu bar app that I'd like to be notified when the user switches the visible desktop space (including external monitors). This is a menu bar only app (i.e no actual window).
I've seen a few similar questions, but none of the answers seemed to work for me. Most answers I've seen involve observing NSWorkspaceActiveSpaceDidChangeNotification on the NSWorkspace's notification center.
I've tried observing this in my AppDelegate in applicationDidFinishLaunching I have the following code:
NSWorkspace.sharedWorkspace().notificationCenter.addObserver(self,
selector: Selector(spaceChanged()),
name: NSWorkspaceActiveSpaceDidChangeNotification,
object: nil)
In my spaceChanged() function I'm just printing something to console for debugging purposes. This function only ever gets called on app launch. Whenever I change the desktop space though I never get notified.
Is there something I'm missing? Any help is appreciated.
Rather than Selector(spaceChanged()) you should use #selector(spaceChanged). With Selector(spaceChanged()), you're actually calling this function immediately, and using the result (which is probably just an empty tuple ()) to create a null selector. The latter syntax actually creates the proper selector referencing your spaceChanged function.
An update for Swift:
NSWorkspace.shared.notificationCenter.addObserver(
self,
selector: #selector(spaceChanged),
name: NSWorkspace.activeSpaceDidChangeNotification,
object: nil
)
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.
If you secondary-click on the Dock you can click the Turn Hiding On option to automatically hide the Dock. Alternatively, you can go to System Preferences > Dock and click the Automatically hide and show the Dock.
I want to mimic that functionality from within an app I am making (which is basically a status bar icon app) and preferably in Swift.
The code I have written so far to turn on the Dock Automatic Hiding functionality is the following:
// Update the value for key "autohide" in com.apple.dock.plist, located in ~/Library/Preferences/.
var dict = NSUserDefaults.standardUserDefaults().persistentDomainForName("com.apple.dock")
dict.updateValue(true, forKey: "autohide")
NSUserDefaults.standardUserDefaults().setPersistentDomain(dict, forName: "com.apple.dock")
// Send notification to the OS.
dispatch_async(dispatch_get_main_queue()) {
CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(), "com.apple.dock.prefchanged", nil, nil, true)
}
The first part of the code updates a value in a plist file and I have confirmed that that is working. The second part sends a notification to the OS to tell it that a value has been changed in that plist, which I have also confirmed to be working.
However, these two things are not making the Dock hide, making me believe I need to do something else. Or made my approach to the problem is wrong? How do I make the Dock start hiding?
PS: I have read something about a private, undocumented API called CoreDock, but I would like to avoid going that way, as it may cause many problems...
Almost certainly better to use AppleScript or the Scripting Bridge to do this. The following script turns Dock autohiding on:
tell application "System Events"
set autohide of dock preferences to true
end tell
You can run that using NSAppleScript.