Contextual menu with search result for a NSSearchField - swift

I would like to have a contextual menu that shows search results as text is entered in a search field. This is an
image of the default mail app in OS X that does this. I know how to filter an array of strings according to the search request of the user, but I do not know how to display it this way. I am using Swift and for a Cocoa application. Any help is appreciated.

Building from the previous answer, here is a simple Swift 3 class which you can use to automatically handle recent searches. You can add it as a custom class in your storyboard, or directly.
It will look like this:
import Cocoa
class RecentMenuSearchField: NSSearchField {
lazy var searchesMenu: NSMenu = {
let menu = NSMenu(title: "Recents")
let recentTitleItem = menu.addItem(withTitle: "Recent Searches", action: nil, keyEquivalent: "")
recentTitleItem.tag = Int(NSSearchFieldRecentsTitleMenuItemTag)
let placeholder = menu.addItem(withTitle: "Item", action: nil, keyEquivalent: "")
placeholder.tag = Int(NSSearchFieldRecentsMenuItemTag)
menu.addItem( NSMenuItem.separator() )
let clearItem = menu.addItem(withTitle: "Clear Menu", action: nil, keyEquivalent: "")
clearItem.tag = Int(NSSearchFieldClearRecentsMenuItemTag)
let emptyItem = menu.addItem(withTitle: "No Recent Searches", action: nil, keyEquivalent: "")
emptyItem.tag = Int(NSSearchFieldNoRecentsMenuItemTag)
return menu
}()
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
initialize()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
initialize()
}
//create menu
private func initialize() {
self.searchMenuTemplate = searchesMenu
}
}

NSSearchField searchMenuTemplate (NSMenu) contains Menu Items (NSMenuItem) with specific tags used by the Search Field to populate the Menu.
The recentSearches Array here is just to pass additional String used in complement of Recents Search strings and is not required (I thought that it was to store recent search but no). NSSearchField also clear this Array when the user clears Recents Search.
You can also configure a Menu with category, more info here:
Configuring a Search Menu — Apple Developer
Example:
#IBOutlet weak var search: NSSearchField!
/// Array of string containing additional recents search (custom search)
var recentSearches = [String]()
/// Search Field Recents Menu
lazy var searchesMenu: NSMenu = {
let menu = NSMenu(title: "Recents")
let i1 = menu.addItem(withTitle: "Recents Search", action: nil, keyEquivalent: "")
i1.tag = Int(NSSearchFieldRecentsTitleMenuItemTag)
let i2 = menu.addItem(withTitle: "Item", action: nil, keyEquivalent: "")
i2.tag = Int(NSSearchFieldRecentsMenuItemTag)
let i3 = menu.addItem(withTitle: "Clear", action: nil, keyEquivalent: "")
i3.tag = Int(NSSearchFieldClearRecentsMenuItemTag)
let i4 = menu.addItem(withTitle: "No Recent Search", action: nil, keyEquivalent: "")
i4.tag = Int(NSSearchFieldNoRecentsMenuItemTag)
return menu
}()
override func viewDidLoad() {
super.viewDidLoad()
recentSearches = ["Toto","Titi","Tata"]
search.delegate = self
search.recentSearches = recentSearches
search.searchMenuTemplate = searchesMenu
}

You need to make an NSMenu Item with special tags that are placeholders so the search field knows where to put recent items, the clear action, etc.
Look at documentation for searchMenuTemplate, and the Tags : NSSearchFieldRecentsMenuItemTag etc.
Basically, make a contextual menu in IB. Drag your search field to use that menu as the searchMenuTemplate, and then populate menu items with the tags you want for clear, recent items, etc.

Related

refresh NSMenuItem on click/open of NSStatusItem

I have the following extension where I have a NSMenuItem with it's state being either on or off:
extension AppDelegate {
func createStatusBarItem() {
let sBar = NSStatusBar.system
// create status bar item in system status bar
sBarItem = sBar.statusItem(withLength: NSStatusItem.squareLength)
...
let sBarMenu = NSMenu(title: "Options")
// assign menu to status bar item
sBarItem.menu = sBarMenu
let enableDisableMenuItem = NSMenuItem(title: "Enabled", action: #selector(toggleAdvancedMouseHandlingObjc), keyEquivalent: "e")
enableDisableMenuItem.state = sHandler.isAdvancedMouseHandlingEnabled() ? NSControl.StateValue.on : NSControl.StateValue.off
sBarMenu.addItem(enableDisableMenuItem)
...
}
#objc func toggleAdvancedMouseHandlingObjc() {
if sHandler.isAdvancedMouseHandlingEnabled() {
sHandler.disableAdvancedMouseHandling()
} else {
sHandler.enableAdvancedMouseHandling()
}
}
}
As soon as I want to change the state of the sHandler object I would also like to refer this change to the NSMenuItem and enable or disable the check mark depending on the state of NSHandler.
However it looks like the menu is being built only at first launch. How do I re-trigger the menu item in order to show or not show the check mark?
Keep a reference to the created NSMenuItem in your app delegate and update its state (assuming you use the item only in a single menu).
class AppDelegate: NSApplicationDelegate {
var fooMenuItem: NSMenuItem?
}
func createStatusBarItem() {
...
let enableDisableMenuItem = NSMenuItem(title: "Enabled", action: #selector(toggleAdvancedMouseHandlingObjc), keyEquivalent: "e")
self.fooMenuItem = enableDisableMenuItem
...
}
#objc func toggleAdvancedMouseHandlingObjc() {
if sHandler.isAdvancedMouseHandlingEnabled() {
sHandler.disableAdvancedMouseHandling()
} else {
sHandler.enableAdvancedMouseHandling()
}
self.fooMenuItem.state = sHandler.isAdvancedMouseHandlingEnabled() ? NSControl.StateValue.on : NSControl.StateValue.off
}

How do I refresh menu items in a Swift app?

I have a menubar app and need to refresh one of the menu items when the user opens the menu.
I have a function that pulls the current IP's being used by the machine and stores them in a variable: addresses. The menu is called via override func awakeFromNib(). We have a timer function that I've tried using to update the addresses variable, but can't figure out how to get the menu itself to update with the new data in the variable.
I've tried using a didSet on addresses to update the variable, and I've added the function that updates addresses to a Timer function, but that doesn't update the menu, only the variable.
Here's the code to load the menu:
override func awakeFromNib() {
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateProcessTimer), userInfo: nil, repeats: true)
statusItem.menu = statusMenu
statusItem.image = icon
// get computer information
let compInfo = ComputerInfo()
let addresses = compInfo.getIPAddresses()
let compName = compInfo.getComputerName()
// present the computer info
if let computerNameMenuItem = self.statusMenu.item(withTitle: "computerName") {
computerNameMenuItem.title = compName ?? "unknown"
}
if let computerIPMenuItem = self.statusMenu.item(withTitle: "ipAddress") {
computerIPMenuItem.title = addresses
}
}
As it stands, it sets the menu items when the app loads, but that's it. I'd like to find a way to update the computerIPMenuItem.title every time the user clicks the menu.
--ADDITIONAL INFO--
The menu class is an NSObject, NSApplicationDelegate that calls a nib file. In the nib we have a menubar with NSMenuItem place-holders with the titles referenced above as computerName and ipAddress. Not sure if that helps clarify why some of the traditional override func calls aren't working.
You need to set statusMenu delegate to self:
statusMenu.delegate = self
Then you can use this handler to update the menu before it is displayed:
func menu(NSMenu, update: NSMenuItem, at: Int, shouldCancel: Bool) -> Bool {
if(update.tag == 1) { //where '1' should be the tag of the menu item you want to update
//update your menu item
update.title = "New Title";
}
}
And the main problem in your code is that you are getting the items by title where instead you should get them by tag so you can retrieve them even when title is updated
Another option will be:
override func layoutSubviews() {
super.layoutSubviews()
updateWhatYouNeed()
}

How To Detect Which NSMenuItem Was Selected

Given that I have a menuBar app with 3 items on a subMenu:
let delaySubMenu = NSMenu()
delaySubMenu.addItem(NSMenuItem(title: "5", action: #selector( setReminder(_:)), keyEquivalent: ""))
delaySubMenu.addItem(NSMenuItem(title: "10", action: #selector(setReminder(_:)), keyEquivalent: ""))
delaySubMenu.addItem(NSMenuItem(title: "15", action: #selector(setReminder(_:)), keyEquivalent: ""))
How do I detect which of my delaySubMenu items has been selected without making a unique setReminder function for each?
Thanks
The action selector will receive the sender object just like it would if you were using Interface Builder. So your setReminder(_:) selector could have the signature:
func setReminder(_ sender: Any) {
// Coerce sender to NSMenuItem and use it to make your decisions
}
or:
func setReminder(_ sender: NSMenuItem) {
// Don't do any coercion work you don't need to do…
}
You could also set the tag property of the NSMenuItem to your delay values. The tag property is an Int type so a good match for your values.
As you are creating multiple entries you could use a for in loop to traverse an array or dictionary, creating a new NSMenuItem for each entry. So we could change your original code to something like this example where I use a dictionary:
let delaySubMenu = NSMenu()
let delays = ["5 Minutes" : 5, "10 Minutes" : 10, "15 Minutes" : 15] // This is a dictionary of String:Int
for (titleKey, value) in delays {
let menuItem = NSMenuItem(title: titleKey, action: #selector(setReminder(_:)), keyEquivalent: nil)
menuItem.tag = value
delaySubMenu.addItem(menuItem)
}
func setReminder(_ sender: NSMenuItem) {
let delayValue = sender.tag // delayValue is a Int by inference from tag
// Do something with your delay value
}
disclaimer: this is just cut and paste in the browser so it may needs some tweaking to actually work.

How to invoke a func passing it as argument between controllers

I'm trying to create a dynamic menu where I send the text and the action of a button from one controller to the next controller and then I want to generate the button that runs the action, but I'm having trouble with the syntax.
So as an example in the first controller i have:
let vc = storyboard.instantiateViewController(withIdentifier:
"CompetitiveKPIChart") as! CompetitiveKPIChartViewController
vc.Menu = [["Menu1":ClickBack()],["Menu2":ClickBack()],["Menu3":ClickBack()]]
func ClickBack() {
self.dismiss(animated: true, completion: {});
}
and in the second controller:
var Menu : [Dictionary<String,()>] = []
override func viewDidLoad() {
let gesture2 = UITapGestureRecognizer(target: self,action: Menu["Menu1"])
btnBack.addGestureRecognizer(gesture2)
}
How can I call the ClickBack() from the first controller in the second controller GestureRecognizer?
The syntax of your Menu declaration is wrong - it should be () -> () for a void function, not ().
This works in Playground, your syntax does not...
var menu : [Dictionary<String,() -> ()>] = [] // Don't use capitals for variables...
func aFunc() {
print("Hello aFunc")
}
menu = [["aFunc": aFunc]]
menu.first!["aFunc"]!() // Prints "Hello aFunc"
However, I think there is a second problem with your code. Your line
UITapGestureRecognizer(target: self,action: Menu["Menu1"])
cannot compile, as Menu is an array, not a dictionary. I think you probably meant
var menu : Dictionary<String,() -> ()> = [:]
in which case your set-up code should say
vc.menu = ["Menu1":ClickBack, "Menu2":ClickBack, "Menu3":ClickBack]
However, there is a final, and probably insurmountable problem, which is that your code
override func viewDidLoad() {
let gesture2 = UITapGestureRecognizer(target: self, action: menu["Menu1"])
btnBack.addGestureRecognizer(gesture2)
}
will still give an error, since action: needs to be a Selector, which is an #objc message identifier, not a Swift function.
I'm afraid I can't see a good way to make this (rather clever) idea work. Time to refactor?

Delete Nsmenu item with specific tag

I use this function to create a NSMenuitems. They all get tagged with 2.
func addToComputerInfoMenu (title: String)
{
let addToComputerItem : NSMenuItem = NSMenuItem(title: "\(title)" , action: #selector(openWindow), keyEquivalent: "")
addToComputerItem.attributedTitle = NSAttributedString(string: "\(title)", attributes: [NSFontAttributeName: NSFont.systemFontOfSize(14), NSForegroundColorAttributeName: NSColor.blackColor()])
addToComputerItem.tag = 2
addToComputerItem.enabled = true
computerInfoMenu.addItem(addToComputerItem)
}
I would like to programmatically delete all items with "2" tag. I tried using .itemWithTag and .indexOfItemWithTag. I can't seem to iterate through the list.
let itemswithindex2 = computerInfoMenu.itemWithTag(2)
I found a way to accomplish my goal. Not sure its the best solution but it works.
for item in computerInfoMenu.itemArray
{
if (item.tag) == 2
{
computerInfoMenu.removeItem(item)
}
}