How to programatically close NSPopover being used in Mac Menu bar app - swift

Apologies as this is a beginner question.
I am using https://github.com/davidcaddy/MenuBarPopoverExample to create a simple menu bar app.
I have added a button in the viewController and connected it.
What would the command be to simply close the NSPopover (see commented section below)?
View Controller Code:
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
static func newInstance() -> ViewController {
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
let identifier = NSStoryboard.SceneIdentifier("ViewController")
guard let viewcontroller = storyboard.instantiateController(withIdentifier: identifier) as? ViewController else {
fatalError("Unable to instantiate ViewController in Main.storyboard!")
}
return viewcontroller
}
#IBAction func closePopover(_ sender: Any) {
print("close this popover")
// what code would I put here??
// closePopover(self)?
}
}
AppDeligate code (untouched from example):
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
let popover = NSPopover()
var eventMonitor: EventMonitor?
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let button = self.statusItem.button {
button.image = NSImage(named: NSImage.Name("ExampleMenuBarIcon"))
button.action = #selector(AppDelegate.togglePopover(_:))
// Uncomment this to capture right mouse clicks, in addition to left clicks
//
// button.sendAction(on: [.leftMouseUp, .rightMouseUp])
}
self.popover.contentViewController = ViewController.newInstance()
self.popover.animates = false
self.eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in
if let strongSelf = self, strongSelf.popover.isShown {
strongSelf.closePopover(sender: event)
}
}
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
#objc func togglePopover(_ sender: NSStatusItem) {
// if sendAction(on: [.leftMouseUp, .rightMouseUp]) is uncommented in applicationDidFinishLaunching
// This can be used to check the type of the incoming mouse event
//
// let event = NSApp.currentEvent!
// if event.type == NSEvent.EventType.rightMouseUp {
// print("Right Click")
// }
if self.popover.isShown {
closePopover(sender: sender)
}
else {
showPopover(sender: sender)
}
}
func showPopover(sender: Any?) {
if let button = self.statusItem.button {
self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
self.eventMonitor?.start()
}
}
func closePopover(sender: Any?) {
self.popover.performClose(sender)
self.eventMonitor?.stop()
}
}
I have tried every command I can find online and none of them allow me to close the popover programatically from within the ViewController.
I would appreciate any assistance you can provide.

Just get the AppDelegate instance
#IBAction func closePopover(_ sender: Any) {
let appDelegate = NSApp.delegate as! AppDelegate
appDelegate.closePopover(self)
}
Note: As the sender parameter is actually not being used you can omit it in the show and close functions
func showPopover()

Related

Function called for all NSWindow

When I call function, it's called for all window opened and not just for the selected window.
If the function is called by #IBAction It's applied for the selected window. Otherwhise, it's applied for all windows.
How can i call the function just for the current selected window ?
Here is an preview:
This is the minimal reproductible code:
// AppDelegate.swift
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
#objc func openMyWindow()
{
let storyboard:NSStoryboard = NSStoryboard(name: "Main", bundle: nil)
guard let controller:NSWindowController = storyboard.instantiateController(withIdentifier: "WindowMain") as? NSWindowController else { return }
controller.showWindow(self)
}
#objc func test()
{
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "TEST"), object: nil, userInfo: nil)
}
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
let dockMenu = NSMenu()
dockMenu.addItem(withTitle: "New window", action: #selector(openMyWindow), keyEquivalent: "")
dockMenu.addItem(withTitle: "test", action: #selector(test), keyEquivalent: "")
return dockMenu
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
// ViewController.swift
import Cocoa
class ViewController: NSViewController {
#objc func Test(){
TextView.string = "It's applied for ALL views -> it's NOT ok"
}
#IBAction func button(_ sender: Any) {
TextView.string = "It's applied just for this view -> it's ok"
}
#IBOutlet var TextView: NSTextView!
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(Test), name: NSNotification.Name(rawValue: "TEST"), object: nil)
}
override var representedObject: Any? {
didSet {
}
}
}
Notification with object nil (no object) are hard to distinguish when it is not even evaluated which of the windows invoked the post.
In other words, make use of the object: parameter when you post the Notification.
Otherwise all registered observers in multiple windows will act on one and the same Notification.
So what object could be used to know who was sending?
The window object itself of course.
Your WindowController has a window it belongs to as well, just compare its address to the posted Notifications object and act when they are the same.
Or compare against the front most windows address, which usually is the window the user expects to act on commands given.
If the target of the menu item isn't set then the action message is sent to the first responder. In your view the text view is the first responder but it doesn't handle the test message and sends it to the next responder. The view controller is in the responder chain and will receive the test message.
Set the selector of the menu item to the action of the view controller and the view controller of the front window will receive it. No notifications required.
// AppDelegate.swift
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
#objc func openMyWindow() {
let storyboard:NSStoryboard = NSStoryboard(name: "Main", bundle: nil)
guard let controller:NSWindowController = storyboard.instantiateController(withIdentifier: "WindowMain") as? NSWindowController else { return }
controller.showWindow(self)
}
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
let dockMenu = NSMenu()
dockMenu.addItem(withTitle: "New window", action: #selector(openMyWindow), keyEquivalent: "")
dockMenu.addItem(withTitle: "test", action: #selector(ViewController.test), keyEquivalent: "")
return dockMenu
}
}
// ViewController.swift
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#objc func test() {
TextView.string = "It's applied for this view -> it's now ok"
}
#IBAction func button(_ sender: Any) {
TextView.string = "It's applied just for this view -> it's ok"
}
#IBOutlet var TextView: NSTextView!
}

Checking for key down on menu bar application

I am writing a MacOS Menu Bar Application which uses a popover. I have relied on a number of tutorials to get things going.
Very briefly, the code looks something like this:
class AppDelegate: NSObject, NSApplicationDelegate {
var popover=NSPopover()
var statusBarItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Popover & Content View
let contentView = ContentView()
self.popover.contentViewController = NSHostingController(rootView: contentView)
// Menu
self.statusBarItem = NSStatusBar.system.statusItem(withLength: 18)
if let statusBarButton = self.statusBarItem.button {
statusBarButton.title = "☰"
statusBarButton.action = #selector(togglePopover(_:))
}
}
#objc func togglePopover(_ sender: AnyObject?) {
let statusBarButton=self.statusBarItem.button!
func show(_ sender: AnyObject) {
self.popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
}
func hide(_ sender: AnyObject) {
popover.performClose(sender)
}
self.popover.isShown ? hide(sender as AnyObject) : show(sender as AnyObject)
}
}
How can I check whether the option key is down when the menu button is clicked?
Ask the current event whether the modifier flags contain option
func isOptionkeyPressed() -> Bool
{
return NSApp.currentEvent?.modifierFlags.contains(.option) == true
}

Can a Menu Bar Application switch between a popover or a menu?

I am writing a MacOS Menu Bar Application which uses a popover. I have relied on a number of tutorials to get things going. The application doesn’t use a storyboard or Interface Builder.
The plan is to present a menu on right-click or a popover on left-click.
Very briefly, the code looks something like this:
class AppDelegate: NSObject, NSApplicationDelegate {
var popover=NSPopover()
var statusBarItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Popover & Content View
let contentView = ContentView()
self.popover.contentViewController = NSHostingController(rootView: contentView)
// Menu
self.statusBarItem = NSStatusBar.system.statusItem(withLength: 18)
if let statusBarButton = self.statusBarItem.button {
statusBarButton.title = "☰"
statusBarButton.action = #selector(togglePopover(_:))
statusBarButton.sendAction(on: [.leftMouseUp, .rightMouseUp])
}
}
#objc func togglePopover(_ sender: AnyObject?) {
let statusBarButton=self.statusBarItem.button!
let event = NSApp.currentEvent!
event.type == NSEvent.EventType.rightMouseUp ? print("Right-Click") : print("Left-Click")
func show(_ sender: AnyObject) {
self.popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
}
func hide(_ sender: AnyObject) {
popover.performClose(sender)
}
self.popover.isShown ? hide(sender as AnyObject) : show(sender as AnyObject)
}
}
I can get the popover working, but how would I like to show a different real menu when the button is right-clicked. How do I get this going?

Can't pass value from FirstVC to SecondVC using segue

I have two ViewControllers connected via Show segue. I need to pass NSSlider's value from ViewController to SecondViewCotroller.
So, moving slider in ViewController a variable updates in SecondViewController.
How to update a value of imagesQty variable?
// FIRST VIEW CONTROLLER
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var slider: NSSlider!
#IBOutlet weak var photosLabel: NSTextField!
#IBAction func segueData(_ sender: NSSlider) {
photosLabel.stringValue = String(slider.intValue) + " photos"
self.performSegue(withIdentifier: NSStoryboardSegue.Identifier(rawValue: "SegueIdentifierForSecondVC"), sender: slider)
}
func prepare(for segue: NSStoryboardSegue, sender: NSSlider?) {
if segue.identifier!.rawValue == "SegueIdentifierForSecondVC" {
if let secondViewController =
segue.destinationController as? SecondViewController {
secondViewController.imagesQty = slider.integerValue
}
}
}
}
and
// SECOND VIEW CONTROLLER
import Cocoa
class SecondViewController: NSViewController {
var imagesQty = 30
override func viewWillAppear() {
super.viewWillAppear()
self.view.wantsLayer = true
print("viewWillAppear – Qty:\(imagesQty)")
//let arrayOfViews: [NSImageView] = [view01...view12]
let url = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Desktop/ArrayOfElements")
do {
let fileURLs = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]).reversed()
let photos = fileURLs.filter { $0.pathExtension == "jpg" }
for view in arrayOfViews {
//"imagesQty" is here
let i = Int(arc4random_uniform(UInt32(imagesQty-1)))
let image = NSImage(data: try Data(contentsOf: photos[i]))
view.image = image
view.imageScaling = .scaleNone
}
} catch {
print(error)
}
}
First of all the purpose and benefit of NSStoryboardSegue.Identifier is to create an extension to be able to avoid literals.
extension NSStoryboardSegue.Identifier {
static let secondVC = NSStoryboardSegue.Identifier("SegueIdentifierForSecondVC")
}
Then you can write
self.performSegue(withIdentifier: .secondVC, sender: slider)
and
if segue.identifier! == .secondVC { ...
This error occurs because imagesQty is declared in viewWillAppear rather than on the top level of the class.
Change it to
class SecondViewController: NSViewController {
var imagesQty = 30 // Int is inferred
// override func viewWillAppear() {
// super.viewWillAppear()
// }
}
There is another mistake: The signature of prepare(for segue is wrong. It must be
func prepare(for segue: NSStoryboardSegue, sender: Any?) {
You can‘t change the value because the var is defined in the function and not in the class.
Make your var a class property and it should work.
class SecondViewController: UIViewController {
var imagesQty: Int = 30
...
}

Cannot set delegate field of NSPopover

I'm trying to pass data back from my popover to another class which launched it. I read that the pattern to do this is using delegates, so I did this:
/*MyMainClass.swift*/
class MyMainClass: UserInfoPopoverDelegate {
var popover: NSPopover = NSPopover()
func showAskForUserInfoPopup() {
if let button = statusItem.button {
if !popover.isShown {
popover.delegate = self //error here
popover.contentViewController = UserInfoPopupController(nibName: "UserInfoPopup", bundle: nil)
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}
}
func submitAndClose(str: String){
print(str)
popover.performClose(nil)
}
}
Then I have a xib with its controller:
class UserInfoPopupController: NSViewController {
#IBOutlet weak var phoneField: NSTextField!
#IBOutlet weak var emailField: NSTextField!
weak var delegate: UserInfoPopoverDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func close(_ sender: Any) {
delegate?.submitAndClose(str: "close pressed")
}
#IBAction func submitDetails(_ sender: Any) {
delegate?.submitAndClose(str: "submit pressed")
}
}
protocol UserInfoPopoverDelegate: class {
func submitAndClose(str: String)
}
The problem happens where I left the comment in the code, and is Cannot assign value of type 'MyMainClass' to type 'NSPopoverDelegate'. If my main class is titled class MyMainClass: NSPopoverDevelegate it will complain that i dont implement all the methods of NSObjectProtocol which I dont really want to do.
This is all pretty jumbled. You created a delegate property on your UserInfoPopupController, but you are assigning a delegate to the NSPopover instead. So you need to change your code to something like this:
func showAskForUserInfoPopup() {
if let button = statusItem.button {
if !popover.isShown {
let contentViewController = UserInfoPopupController(nibName: "UserInfoPopup", bundle: nil)
contentViewController.delegate = self //This is where you should be assigning the delegate
popover.contentViewController = contentViewController
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}
}