Open and bring new window to front when pressing NSMenuItem - macos

I have a macOS menu application that is running as an agent (LSUIElement).
I need it to have a companion settings window. There is a "settings" NSMenuItem in the NSMenu, and the requirement is to open an actual window and bring it to the front when pressed.
The window is SwiftUI driven. Here's how it's working:
// main is an NSMenu
main.addItem(
withTitle: "Settings",
action: #selector(AppDelegate.openSettings),
keyEquivalent: "")
#objc func openSettings() {
let detailView = SettingsWindow(); // Swift UI view
let controller = DetailWindowController(rootView: detailView) // See below
controller.window?.title = "Settings";
controller.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
}
class DetailWindowController<RootView : View>: NSWindowController {
convenience init(rootView: RootView) {
let hostingController = NSHostingController(rootView: rootView.frame(width: 400, height: 500))
let window = NSWindow(contentViewController: hostingController)
window.setContentSize(NSSize(width: 400, height: 500))
self.init(window: window)
}
}
What actually happens
The current behaviour is that the window opens, however it's always behind whatever other windows are currently in the foreground.
I need it to be in the foreground.
NSApp.activate(ignoringOtherApps: true) in the code above is an attempt to achieve this, but that didn't work.
Any help would be amazing. Many thanks.

Figured it out in the end. I needed these 3. The activation policy should be better managed, but for what it's worth here's what works.
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
settingsWindow!.window?.orderFrontRegardless()
In context
#objc func openSettings() {
if(settingsWindow == nil) {
let detailView = ActionWindow();
settingsWindow = DetailWindowController(rootView: detailView)
settingsWindow!.window?.title = "Cloud Brains - Settings";
settingsWindow!.showWindow(nil)
}
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
settingsWindow!.window?.orderFrontRegardless()
}

Related

Programmatically creating menus with Swift 3 and Cocoa

I am trying my hand at creating a Cocoa GUI app programmatically (i.e. without a nib file) using Swift 3. I've run into trouble getting the application's menus to show.
I would expect the below code to show a File menu items on the menu bar. Instead, while the window launches and works as expected, the code to set the menu seems to have no effect:
import AppKit
final class ApplicationController: NSObject, NSApplicationDelegate {
var mainWindow: NSWindow?
func applicationDidFinishLaunching(_ aNotification: Notification) {
let mainMenu = NSMenu()
let mainMenuFileItem = NSMenuItem(title: "File", action: nil, keyEquivalent: "")
let fileMenu = NSMenu(title: "File")
fileMenu.addItem(withTitle: "New...", action: nil, keyEquivalent: "n")
mainMenuFileItem.submenu = fileMenu
mainMenu.addItem(mainMenuFileItem)
NSApp.mainMenu = mainMenu
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [.titled, .closable, .resizable, .miniaturizable],
backing: NSBackingStoreType.buffered, defer: false)
window.orderFrontRegardless()
window.title = "Hello World"
self.mainWindow = window
NSApp.activate(ignoringOtherApps: true)
}
func applicationWillTerminate(_ aNotification: Notification) {
print("terminating")
}
func applicationShouldTerminateAfterLastWindowClosed(_ app: NSApplication) -> Bool{
return true
}
}
let app = NSApplication.shared()
let controller = ApplicationController()
app.delegate = controller
app.run()
The closest I've found to a working example is this answer. However, it appears to be for an earlier version of Swift/Cocoa, and I am not able to get that example working.
What am I doing wrong?
One needs to call NSApp.setActivationPolicy(.regular) to make the application a "regular" one. Making that call before app.run() fixes the issue of no menu showing.
The "main" part of the code should thus be:
let app = NSApplication.shared()
NSApp.setActivationPolicy(.regular)
let controller = ApplicationController()
app.delegate = controller
app.run()

Open New Window in Swift

I am trying to open a new window in my Swift application but I cannot get it to open.
class AppDelegate: NSObject, NSApplicationDelegate
{
func applicationDidFinishLaunching(aNotification: NSNotification)
{
openMyWindow()
}
func openMyWindow()
{
if let storyboard = NSStoryboard(name: "Main",bundle: nil)
{
if let vc = storyboard.instantiateControllerWithIdentifier("MyList") as? MyListViewController
{
var myWindow = NSWindow(contentViewController: vc)
myWindow.makeKeyAndOrderFront(self)
let controller = NSWindowController(window: myWindow)
controller.showWindow(self)
}
}
}
}
I have set the Storyboard ID in IB.
I have traced the code and it does get into the window opening code but it doesn't do anything.
BTW I do have a default storyboard entry point set and that default window opens OK but I need to have a second window open and that is what is not working.
After the openMyWindow() method is executed, the windowController will be released and consequently the window is nil. That's why it is not there.
You have to hold the window in you class to keep it alive, then the window will be visible.
var windowController : NSWindowController?

NSStatusItem fullscreen issues

I'm making a statusbar app that displays an NSPopover when the NSStatusItem is clicked, like this:
I have added the ability to resize the popover by dragging on the edges, by subclassing the popover's view like this:
class CMView: NSView {
let tolerance : CGFloat = 10
var state = false
override func mouseDown(theEvent: NSEvent) {
let point = self.convertPoint(theEvent.locationInWindow, fromView: nil)
if (point.y <= tolerance) {
state = true
}
}
override func mouseDragged(theEvent: NSEvent) {
if (state) {
let point = self.convertPoint(theEvent.locationInWindow, fromView: nil)
self.frame = NSRect(
x: self.frame.origin.x,
y: self.frame.origin.y,
width: self.frame.size.width,
height: self.frame.size.height-point.y)
popover.contentSize = self.frame.size
}
}
override func mouseUp(theEvent: NSEvent) {
state = false
}
}
This only works if the desktop isn't in full screen. If I try to resize it in fullscreen, it simply doesn't work, and the popover arrow disappears mysteriously.
It seems like the popover isn't redrawing when invoked in a fullscreen environment. Is there any way around this problem?
Here at WWDC. Asking the same question. You have to have an app that's an UIElement app - meaning no dock icon, no main menu.

OS X storyboard: how to show a window programmatically?

I am creating an OS X status bar application.
I am trying to achieve the following:
app starts invisible, with menu bar item
click on menu bar item shows the main window
on deactivate, the window is hidden
So I am trying to programmatically show the main window when the menu item is clicked, but with no success.
My main window has "Hide on deactivate" checked. Once hidden, I cannot make it visible again using code.
Here is the code I have for now, but it doesn't work:
#IBAction func menuClick(sender: AnyObject) {
var mainWindow = NSStoryboard(name: "Main", bundle: nil)?.instantiateInitialController()
mainWindow?.makeKeyAndOrderFront(self)
}
This is how you have to do to show your Windows programmatically:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
let mainWindow = NSWindow(contentRect: NSMakeRect(0, 0, NSScreen.mainScreen()!.frame.width/2, NSScreen.mainScreen()!.frame.height/2), styleMask: NSTitledWindowMask|NSResizableWindowMask|NSMiniaturizableWindowMask|NSClosableWindowMask, backing: NSBackingStoreType.Buffered, defer: false)
func createNewWindow(){
mainWindow.title = "Main Window"
mainWindow.opaque = false
mainWindow.center()
mainWindow.hidesOnDeactivate = true
mainWindow.movableByWindowBackground = true
mainWindow.backgroundColor = NSColor(calibratedHue: 0, saturation: 0, brightness: 1, alpha: 1)
mainWindow.makeKeyAndOrderFront(nil)
}
func applicationDidFinishLaunching(aNotification: NSNotification) {
// lets get rid of the main window just closing it as soon as the app launches
NSApplication.sharedApplication().windows.first!.close()
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
#IBAction func menuClick(sender: AnyObject) {
createNewWindow()
}
}
or you can create an optional NSWindow var to store your window before you close it as follow
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var defaultWindow:NSWindow?
func applicationDidFinishLaunching(aNotification: NSNotification) {
// lets get rid of the main window just closing it as soon as the app launches
defaultWindow = NSApplication.sharedApplication().windows.first as? NSWindow
if let defaultWindow = defaultWindow {
defaultWindow.close()
}
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
#IBAction func menuClick(sender: AnyObject) {
if let defaultWindow = defaultWindow {
defaultWindow.makeKeyAndOrderFront(nil)
}
}
}
The makeKeyAndOrderFront method is a NSWindow method, but instantiateInitialController returns the window controller, not its window.
Also, if the window is hidden on deactivate, you wouldn't want to instantiate another copy. Keep a reference to the window and re-show that.
Finally, you may need to bring the app to the front too. Call [NSApp activateIgnoringOtherApps:YES] (or the Swift equivalent).

Resizing NSWindow to match view controller size in storyboard

I am working on Xcode 6.1.1 on OSX 10.10. I am trying out storyboards for Mac apps. I have a NSTabViewController using the new NSTabViewControllerTabStyleToolbar tabStyle and it is set as the default view controller for the window controller. How do I make my window resize according to the current selected view controller?
Is it possible to do entirely in Interface Builder?
Here is what my storyboard looks like:
The auto layout answer is half of it. You need to set the preferredContentSize in your ViewController for each tab to the fitting size (if you wanted the tab to size to the smallest size satisfying all constraints).
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = view.fittingSize
}
If your constraints are causing an issue below try first with a fixed size, the example below sets this in the tab item's view controller's viewWillAppear function (Swift used here, but the Objective-C version works just as well).
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = NSSize(width: 400, height: 280)
}
If that works, fiddle with your constraints to figure out what's going on
This solution for 'toolbar style' tab view controllers does animate and supports the nice crossfade effect. In the storyboard designer, add 'TabViewController' in the custom class name field of the NSTabViewController. Don't forget to assign a title to each viewController, this is used as a key value.
import Cocoa
class TabViewController: NSTabViewController {
private lazy var tabViewSizes: [String : NSSize] = [:]
override func viewDidLoad() {
// Add size of first tab to tabViewSizes
if let viewController = self.tabViewItems.first?.viewController, let title = viewController.title {
tabViewSizes[title] = viewController.view.frame.size
}
super.viewDidLoad()
}
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions, completionHandler completion: (() -> Void)?) {
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.5
self.updateWindowFrameAnimated(viewController: toViewController)
super.transition(from: fromViewController, to: toViewController, options: [.crossfade, .allowUserInteraction], completionHandler: completion)
}, completionHandler: nil)
}
func updateWindowFrameAnimated(viewController: NSViewController) {
guard let title = viewController.title, let window = view.window else {
return
}
let contentSize: NSSize
if tabViewSizes.keys.contains(title) {
contentSize = tabViewSizes[title]!
}
else {
contentSize = viewController.view.frame.size
tabViewSizes[title] = contentSize
}
let newWindowSize = window.frameRect(forContentRect: NSRect(origin: NSPoint.zero, size: contentSize)).size
var frame = window.frame
frame.origin.y += frame.height
frame.origin.y -= newWindowSize.height
frame.size = newWindowSize
window.animator().setFrame(frame, display: false)
}
}
The window containing a toolbar style tab view controller does resize without any code if you have auto layout constraints in your storyboard tab views (macOS 11.1, Xcode 12.3). I haven't tried other style tab view controllers.
If you want to resize with animation as in Finder, it is sufficient to add one override in your tab view controller. It will resize the window with system-calculated resize animation time and will hide the tab view during resize animation:
class PreferencesTabViewController: NSTabViewController {
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions = [], completionHandler completion: (() -> Void)? = nil) {
guard let window = view.window else {
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
return
}
let fromSize = window.frame.size
let toSize = window.frameRect(forContentRect: toViewController.view.frame).size
let widthDelta = toSize.width - fromSize.width
let heightDelta = toSize.height - fromSize.height
var toOrigin = window.frame.origin
toOrigin.x += widthDelta / 2
toOrigin.y -= heightDelta
let toFrame = NSRect(origin: toOrigin, size: toSize)
NSAnimationContext.runAnimationGroup { context in
context.duration = window.animationResizeTime(toFrame)
view.isHidden = true
window.animator().setFrame(toFrame, display: false)
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
} completionHandler: { [weak self] in
self?.view.isHidden = false
}
}
}
Please adjust closure syntax if you are using Swift versions older than 5.3.
Use autolayout. Set explicit size constraints on you views. Or once you have entered the UI into each tab view item's view set up the internal constraints such that they force view to be the size you want.

Resources